diff --git a/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.agents/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.agents/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.agents/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.agents/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.agents/skills/impeccable/scripts/detector/rules/checks.mjs b/.agents/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.agents/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.agents/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.agents/skills/impeccable/scripts/detector/shared/fonts.mjs b/.agents/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.agents/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.claude/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.claude/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.claude/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.claude/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.claude/skills/impeccable/scripts/detector/rules/checks.mjs b/.claude/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.claude/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.claude/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.claude/skills/impeccable/scripts/detector/shared/fonts.mjs b/.claude/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.claude/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.cursor/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.cursor/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.cursor/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.cursor/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs b/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.cursor/skills/impeccable/scripts/detector/shared/fonts.mjs b/.cursor/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.cursor/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.gemini/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.gemini/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.gemini/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.gemini/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs b/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.gemini/skills/impeccable/scripts/detector/shared/fonts.mjs b/.gemini/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.gemini/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.github/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.github/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.github/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.github/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.github/skills/impeccable/scripts/detector/rules/checks.mjs b/.github/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.github/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.github/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.github/skills/impeccable/scripts/detector/shared/fonts.mjs b/.github/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.github/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.kiro/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.kiro/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.kiro/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.kiro/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs b/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.kiro/skills/impeccable/scripts/detector/shared/fonts.mjs b/.kiro/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.kiro/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.opencode/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.opencode/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.opencode/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.opencode/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs b/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.opencode/skills/impeccable/scripts/detector/shared/fonts.mjs b/.opencode/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.opencode/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.pi/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.pi/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.pi/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.pi/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.pi/skills/impeccable/scripts/detector/rules/checks.mjs b/.pi/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.pi/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.pi/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.pi/skills/impeccable/scripts/detector/shared/fonts.mjs b/.pi/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.pi/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.qoder/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.qoder/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.qoder/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.qoder/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs b/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.qoder/skills/impeccable/scripts/detector/shared/fonts.mjs b/.qoder/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.qoder/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.rovodev/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.rovodev/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.rovodev/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs b/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.rovodev/skills/impeccable/scripts/detector/shared/fonts.mjs b/.rovodev/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.trae-cn/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.trae-cn/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs b/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.trae-cn/skills/impeccable/scripts/detector/shared/fonts.mjs b/.trae-cn/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.trae/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/.trae/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/.trae/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/.trae/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/.trae/skills/impeccable/scripts/detector/rules/checks.mjs b/.trae/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/.trae/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.trae/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/.trae/skills/impeccable/scripts/detector/shared/fonts.mjs b/.trae/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/.trae/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies }; diff --git a/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 3bfc2de47..6eeea8e4c 100644 --- a/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -628,6 +628,36 @@ function colorToHex(c) { return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join(''); } +// --- cli/engine/shared/fonts.mjs --- +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + // --- cli/engine/rules/checks.mjs --- const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2682,14 +2712,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/plugin/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs b/plugin/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs index abbdd7b1c..1affdda43 100644 --- a/plugin/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +++ b/plugin/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs @@ -1,5 +1,6 @@ -import { GENERIC_FONTS } from '../../shared/constants.mjs'; +import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; import { isNeutralColor } from '../../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../../shared/fonts.mjs'; import { checkSourceDesignSystem } from '../../design-system.mjs'; import { isFullPage } from '../../shared/page.mjs'; import { applyInlineIgnores } from '../../shared/inline-ignores.mjs'; @@ -38,6 +39,10 @@ function shouldRunPageAnalyzers(content, filePath) { return !ext || PAGE_ANALYZER_EXTS.has(ext); } +function firstOverusedGoogleFont(text) { + return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || ''; +} + function isNeutralBorderColor(str) { const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i); if (!m) return false; @@ -88,9 +93,12 @@ const REGEX_MATCHERS = [ { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi, test: () => true, fmt: (m) => m[0] }, - { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi, - test: () => true, - fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` }, + { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi, + test: (m) => { + m.overusedGoogleFont = firstOverusedGoogleFont(m[0]); + return Boolean(m.overusedGoogleFont); + }, + fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` }, // --- Gradient text --- { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi, test: (m, line) => /gradient/i.test(line), @@ -170,10 +178,7 @@ const REGEX_ANALYZERS = [ if (f && !GENERIC_FONTS.has(f)) fonts.add(f); } } - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - while ((m = gfRe.exec(content)) !== null) { - for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f); - } + for (const f of extractGoogleFontFamilies(content)) fonts.add(f); if (fonts.size !== 1 || content.split('\n').length < 20) return []; const name = [...fonts][0]; const lines = content.split('\n'); diff --git a/plugin/skills/impeccable/scripts/detector/rules/checks.mjs b/plugin/skills/impeccable/scripts/detector/rules/checks.mjs index 13bcdc224..acac765c7 100644 --- a/plugin/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/plugin/skills/impeccable/scripts/detector/rules/checks.mjs @@ -18,6 +18,7 @@ import { parseRgb, relativeLuminance, } from '../shared/color.mjs'; +import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; @@ -2072,14 +2073,9 @@ function checkPageTypography(doc, win) { // Check Google Fonts links in HTML const html = doc.documentElement?.outerHTML || ''; - const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi; - let m; - while ((m = gfRe.exec(html)) !== null) { - const families = m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase()); - for (const f of families) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + for (const f of extractGoogleFontFamilies(html)) { + fonts.add(f); + if (OVERUSED_FONTS.has(f)) overusedFound.add(f); } // Also parse raw HTML/style content for font-family (jsdom may not expose all via CSSOM) diff --git a/plugin/skills/impeccable/scripts/detector/shared/fonts.mjs b/plugin/skills/impeccable/scripts/detector/shared/fonts.mjs new file mode 100644 index 000000000..4c4d7fc45 --- /dev/null +++ b/plugin/skills/impeccable/scripts/detector/shared/fonts.mjs @@ -0,0 +1,30 @@ +const GOOGLE_FONTS_URL_RE = /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi; + +function normalizeGoogleFontFamilyParam(value) { + return String(value || '') + .split('|') + .map(part => part.split(':')[0].trim().toLowerCase()) + .filter(Boolean); +} + +function extractGoogleFontFamilies(text) { + const families = []; + if (!text) return families; + + GOOGLE_FONTS_URL_RE.lastIndex = 0; + let urlMatch; + while ((urlMatch = GOOGLE_FONTS_URL_RE.exec(text)) !== null) { + const url = urlMatch[0]; + const queryStart = url.indexOf('?'); + if (queryStart === -1) continue; + + const params = new URLSearchParams(url.slice(queryStart + 1).replace(/&/g, '&')); + for (const value of params.getAll('family')) { + families.push(...normalizeGoogleFontFamilyParam(value)); + } + } + + return families; +} + +export { extractGoogleFontFamilies };