fix(lint): match @font-face when a comment inside the block holds a brace (#1538)

font_family_without_font_face (and system_font_will_alias) collect
@font-face blocks with `@font-face\s*\{[^}]*\}`, which stops at the first
`}`. A CSS comment inside the block that contains a brace —
`@font-face { /* 400 } regular */ font-family: 'X'; ... }` — truncates the
match before the font-family, so the family is never recorded as declared
and a later `font-family: 'X'` usage is wrongly flagged as used without an
@font-face. Strip CSS comments before scanning so a brace inside one cannot
split a block.

Refs #1534
This commit is contained in:
Leonel Rivas
2026-06-17 14:21:45 -04:00
committed by GitHub
parent c040e4973a
commit f72ea25b3b
2 changed files with 25 additions and 2 deletions
@@ -215,5 +215,16 @@ describe("font rules", () => {
const findings = await findByCode(html, "font_family_without_font_face");
expect(findings).toHaveLength(0);
});
it("matches @font-face even when a CSS comment inside the block contains a brace (#1534)", async () => {
const html = `<div data-composition-id="test" data-width="1920" data-height="1080">
<style>
@font-face { /* weight 400 } regular */ font-family: 'Noto Sans SC'; src: url('../fonts/noto-400.woff2'); }
.title { font-family: 'Noto Sans SC'; }
</style>
</div>`;
const findings = await findByCode(html, "font_family_without_font_face");
expect(findings).toHaveLength(0);
});
});
});
+14 -2
View File
@@ -22,13 +22,25 @@ const GENERIC_FAMILIES = new Set([
"revert",
]);
// A CSS comment can contain a `}` (e.g. `@font-face { /* 400 } regular */
// font-family: 'X'; ... }`), which truncates the naive `@font-face\s*\{[^}]*\}`
// block match at the comment's brace — so the rule never sees the real
// `font-family` and reports a false-positive font_family_without_font_face.
// Large/"framework" stylesheets hit this far more often than minimal ones,
// which is why a simple <style> passes while a complex one fails. Strip
// comments before scanning so a brace inside one cannot split a block. See #1534.
function stripCssComments(css: string): string {
return css.replace(/\/\*[\s\S]*?\*\//g, " ");
}
function extractFontFaceFamilies(styles: Array<{ content: string }>): Set<string> {
const families = new Set<string>();
const fontFaceRe = /@font-face\s*\{[^}]*\}/gi;
const familyRe = /font-family\s*:\s*(['"]?)([^;'"]+)\1/i;
for (const style of styles) {
const content = stripCssComments(style.content);
let match: RegExpExecArray | null;
while ((match = fontFaceRe.exec(style.content)) !== null) {
while ((match = fontFaceRe.exec(content)) !== null) {
const familyMatch = match[0].match(familyRe);
if (familyMatch?.[2]) {
families.add(familyMatch[2].trim().toLowerCase());
@@ -43,7 +55,7 @@ function extractUsedFontFamilies(styles: Array<{ content: string }>): string[] {
const seen = new Set<string>();
const propRe = /font-family\s*:\s*([^;}{]+)/gi;
for (const style of styles) {
const withoutFontFace = style.content.replace(/@font-face\s*\{[^}]*\}/gi, "");
const withoutFontFace = stripCssComments(style.content).replace(/@font-face\s*\{[^}]*\}/gi, "");
let match: RegExpExecArray | null;
while ((match = propRe.exec(withoutFontFace)) !== null) {
const stack = match[1]!;