fix(lint): mention src: local() for system fonts in font_family_without_font_face (#1859)

Two independent post-release feedback reports of this rule hard-erroring
on OS system fonts (Hiragino Sans, Microsoft YaHei) that have no
downloadable file. Both asked for the same thing, in slightly different
words: a documented way to satisfy the check for a font that's genuinely
OS-bundled, not missing.

That way already exists and already works — extractFontFaceFamilies only
looks at the font-family declaration inside @font-face, never the src
value, so `@font-face { font-family: 'X'; src: local('X'); }` already
passes. One report found this themselves; the other didn't. The gap was
discoverability: the fixHint only described bundling a real font file, so
nobody would think to try `local()` unless they already knew about it.

Considered and rejected a broader fix: adding CJK system-font names to the
shared FONT_ALIAS_MAP (the mechanism that already exempts Latin system
fonts like Segoe UI/Verdana by aliasing them to a bundled fallback font).
That map has no CJK-equivalent bundled font to alias to (only Japanese has
one, noto-sans-jp) — aliasing "Microsoft YaHei" (Simplified Chinese) to a
Japanese font would silently swap in the wrong glyph shapes for shared Han
characters, and would specifically break distributed/Lambda rendering
(where system-font capture is disabled, per system_font_will_alias's own
comment) by removing the warning that currently prompts a real fix. The
local() message fix has none of that risk: it changes no detection logic,
only points at an already-correct existing escape hatch.

Tests: local() font-face no longer flags (proves the advice is accurate,
not just documented); fixHint contains "local(". 308 lint tests pass.
This commit is contained in:
Miguel Ángel
2026-07-02 17:45:01 -07:00
committed by GitHub
parent a59ff0d91b
commit b7eb0dfb5a
2 changed files with 28 additions and 1 deletions
+24
View File
@@ -146,6 +146,30 @@ describe("font rules", () => {
expect(findings).toHaveLength(0);
});
it("does not flag a system font declared via @font-face src: local()", async () => {
// Regression: two independent reports of this rule hard-erroring on OS
// system fonts (Hiragino Sans, Microsoft YaHei) that have no downloadable
// file. src: local(...) already satisfies the check (extractFontFaceFamilies
// only looks at the font-family declaration, not the src value) — the gap
// was that the fixHint didn't mention this as an option.
const html = `<div data-composition-id="test" data-width="1920" data-height="1080">
<style>
@font-face { font-family: 'Microsoft YaHei'; src: local('Microsoft YaHei'); }
body { font-family: 'Microsoft YaHei', sans-serif; }
</style>
</div>`;
const findings = await findByCode(html, "font_family_without_font_face");
expect(findings).toHaveLength(0);
});
it("fixHint mentions the local() pattern for system fonts", async () => {
const html = `<div data-composition-id="test" data-width="1920" data-height="1080">
<style>body { font-family: 'GT Walsheim', sans-serif; }</style>
</div>`;
const findings = await findByCode(html, "font_family_without_font_face");
expect(findings[0]!.fixHint).toContain("local(");
});
it("does not flag generic font families", async () => {
const html = `<div data-composition-id="test" data-width="1920" data-height="1080">
<style>body { font-family: monospace; }</style>
+4 -1
View File
@@ -232,7 +232,10 @@ export const fontRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
"Text will fall back to a generic font, producing incorrect typography in the video.",
fixHint:
"Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } " +
"for each font family, pointing to the captured .woff2 files.",
"for each font family, pointing to the captured .woff2 files. For an OS-bundled " +
"system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, " +
"use src: local('Exact Font Name') instead — the declaration alone satisfies this " +
"check without needing a font file.",
});
return findings;
},