Sync generated provider output

This commit is contained in:
github-actions[bot]
2026-08-31 23:21:27 +00:00
parent 2cfd60765a
commit 61a2d230fd
64 changed files with 832 additions and 480 deletions
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,
@@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -1591,7 +1605,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -5190,8 +5204,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs';
import {
checkSourceDesignSystem,
collectStaticDesignSystemFindings,
@@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) {
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {
const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
if (!hasText) continue;
const ff = window.getComputedStyle(el).fontFamily || '';
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(window.getComputedStyle(el).fontFamily);
if (!primary) continue;
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -9,6 +9,7 @@ import {
WCAG_LARGE_BOLD_TEXT_PX,
WCAG_LARGE_TEXT_PX,
isBrandFontOnOwnDomain,
primaryFontFace,
} from '../shared/constants.mjs';
import {
CSS_NAMED_COLORS,
@@ -331,7 +332,7 @@ function checkIconTile(opts) {
function resolveSerif(fontFamily) {
if (!fontFamily) return { primary: null, isSerif: false };
const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
const primary = primaryFontFace(fontFamily, GENERIC_FONTS);
if (!primary) return { primary: null, isSerif: false };
if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
if (tokens.includes('serif')) return { primary, isSerif: true };
@@ -3930,8 +3931,7 @@ function checkTypography() {
const style = getComputedStyle(el);
const ff = style.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (!primary) continue;
fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1);
totalTextElements++;
@@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) {
if (rule.type !== 1) continue;
const ff = rule.style?.fontFamily;
if (!ff) continue;
const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
const primary = stack.find(f => f && !GENERIC_FONTS.has(f));
const primary = primaryFontFace(ff);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
@@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) {
const ffRe = /font-family\s*:\s*([^;}]+)/gi;
let fm;
while ((fm = ffRe.exec(html)) !== null) {
for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
if (f && !GENERIC_FONTS.has(f)) {
fonts.add(f);
if (OVERUSED_FONTS.has(f)) overusedFound.add(f);
}
const primary = primaryFontFace(fm[1]);
if (primary) {
fonts.add(primary);
if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);
}
}
@@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) {
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([
// Overused-font primary selection skips only CSS generics so a system stack
// keeps the system face as primary; GENERIC_FONTS still includes platform
// faces for design-system/serif resolution.
const CSS_GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
'inherit', 'initial', 'unset', 'revert',
]);
const GENERIC_FONTS = new Set([
...CSS_GENERIC_FONTS,
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
]);
function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) {
return String(fontFamily || '')
.split(',')
.map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())
.find(f => f && !skip.has(f)) || null;
}
// WCAG large text thresholds are defined in points: 18pt normal text and
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
@@ -104,6 +118,7 @@ export {
BRAND_FONT_DOMAINS,
isBrandFontOnOwnDomain,
GENERIC_FONTS,
primaryFontFace,
WCAG_LARGE_TEXT_PX,
WCAG_LARGE_BOLD_TEXT_PX,
EM_DASH_FLOOR,