Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/detector/engines/static-html/detect-html.mjs
1import fs from 'node:fs';2import path from 'node:path';34import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';5import {6checkSourceDesignSystem,7collectStaticDesignSystemFindings,8mergeDesignSystemFindings,9} from '../../design-system.mjs';10import { isFullPage } from '../../shared/page.mjs';11import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';12import { finding } from '../../findings.mjs';13import { profileFindings, profileStep, profileStepAsync } from '../../profile/profiler.mjs';14import {15checkElementBorders,16checkElementClippedOverflow,17checkElementColors,18checkElementGlow,19checkElementGptBorderShadow,20checkElementHeroEyebrow,21checkElementIconTile,22checkElementItalicSerif,23checkElementMotion,24checkElementOversizedH1,25checkElementQuality,26checkCreamPalette,27checkHtmlPatterns,28checkPageLayout,29checkPageQualityFromDoc,30checkRepeatedSectionKickersFromDoc,31resolveBackground,32resolveBorderRadiusPx,33} from '../../rules/checks.mjs';34import { filterByProviders } from '../../registry/antipatterns.mjs';35import { detectText, runTextContentAnalyzers } from '../regex/detect-text.mjs';36import {37StaticDocument,38buildStaticStyleMap,39buildStaticWindow,40collectStaticCssText,41} from './css-cascade.mjs';4243function checkStaticPageTypography(document, window) {44const findings = [];45const fonts = new Set();46const overusedFound = new Set();47for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) {48const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0);49if (!hasText) continue;50const ff = window.getComputedStyle(el).fontFamily || '';51const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());52const primary = stack.find(f => f && !GENERIC_FONTS.has(f));53if (!primary) continue;54fonts.add(primary);55if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary);56}57for (const font of overusedFound) {58findings.push({ id: 'overused-font', snippet: `Primary font: ${font}` });59}60if (fonts.size === 1 && document.querySelectorAll('*').length >= 20) {61findings.push({ id: 'single-font', snippet: `only font used is ${[...fonts][0]}` });62}63const sizes = new Set();64for (const el of document.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, a, li, td, th, label, button, div')) {65const fontSize = parseFloat(window.getComputedStyle(el).fontSize);66if (fontSize >= 8 && fontSize < 200) sizes.add(Math.round(fontSize * 10) / 10);67}68if (sizes.size >= 3) {69const sorted = [...sizes].sort((a, b) => a - b);70const ratio = sorted[sorted.length - 1] / sorted[0];71if (ratio < 2.0) {72findings.push({ id: 'flat-type-hierarchy', snippet: `Sizes: ${sorted.map(s => s + 'px').join(', ')} (ratio ${ratio.toFixed(1)}:1)` });73}74}75return findings;76}7778function checkElementBrokenImage(el) {79const src = (el.getAttribute && el.getAttribute('src')) ?? el.attribs?.src;80// Missing src attribute entirely81if (src === undefined || src === null) {82return [{ id: 'broken-image', snippet: '<img> with no src attribute' }];83}84const trimmed = String(src).trim();85// Empty or placeholder-only src values86if (trimmed === '' || trimmed === '#') {87return [{ id: 'broken-image', snippet: `<img src="${src}">` }];88}89return [];90}9192const STATIC_ELEMENT_RULES = [93{ id: 'border-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementBorders(tag, style, null, resolveBorderRadiusPx(el, style, parseFloat(style.width) || 0, window)) },94{ id: 'color-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementColors(el, style, tag, window, customPropMap, false) },95{ id: 'dark-glow', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementGlow(tag, style, resolveBackground(el.parentElement || el, window, customPropMap)) },96{ id: 'motion-rules', selector: '*', run: (el, tag, style) => checkElementMotion(tag, style) },97{ id: 'icon-tile-stack', selector: 'h1,h2,h3,h4,h5,h6', run: (el, tag, _style, window) => checkElementIconTile(el, tag, window) },98{ id: 'italic-serif-display', selector: 'h1,h2', run: (el, tag, style) => checkElementItalicSerif(el, style, tag) },99{ id: 'hero-eyebrow-chip', selector: 'h1', run: (el, tag, style, window, customPropMap) => checkElementHeroEyebrow(el, style, tag, window, customPropMap) },100{ id: 'broken-image', selector: 'img', run: (el) => checkElementBrokenImage(el) },101{ id: 'quality-rules', selector: '*', run: (el, tag, style, window) => checkElementQuality(el, style, tag, window) },102{ id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) },103{ id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) },104{ id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) },105];106107async function detectHtml(filePath, options = {}) {108const profile = options?.profile;109const html = profileStep(profile, {110engine: 'static-html',111phase: 'setup',112ruleId: 'read-html',113target: filePath,114}, () => fs.readFileSync(filePath, 'utf-8'));115116let modules;117try {118modules = await profileStepAsync(profile, {119engine: 'static-html',120phase: 'setup',121ruleId: 'import-static-parser',122target: filePath,123}, async () => {124const [htmlparser2, cssSelect, csstree, domutils] = await Promise.all([125import('htmlparser2'),126import('css-select'),127import('css-tree'),128import('domutils'),129]);130return {131parseDocument: htmlparser2.parseDocument,132selectAll: cssSelect.selectAll,133selectOne: cssSelect.selectOne,134is: cssSelect.is,135csstree,136domutils,137};138});139} catch {140return detectText(html, filePath, options);141}142143const resolvedPath = path.resolve(filePath);144const fileDir = path.dirname(resolvedPath);145const root = profileStep(profile, {146engine: 'static-html',147phase: 'parse-html',148ruleId: 'parse-document',149target: filePath,150}, () => modules.parseDocument(html, { lowerCaseAttributeNames: false, lowerCaseTags: true }));151152const cssText = collectStaticCssText(root, fileDir, profile, filePath, modules);153const document = new StaticDocument(root, modules);154buildStaticStyleMap(root, document, cssText, modules, profile, filePath);155const window = buildStaticWindow(document);156157const customPropMap = null;158159const findings = [];160const runElementCheck = (ruleId, callback) => profile161? profileFindings(profile, { engine: 'static-html', phase: 'element', ruleId, target: filePath }, callback)162: callback();163164const visitedByRule = new Map();165for (const rule of STATIC_ELEMENT_RULES) {166const elements = document.querySelectorAll(rule.selector);167visitedByRule.set(rule.id, elements.length);168for (const el of elements) {169const tag = el.tagName.toLowerCase();170const style = window.getComputedStyle(el);171for (const f of runElementCheck(rule.id, () => rule.run(el, tag, style, window, customPropMap))) {172findings.push(finding(f.id, filePath, f.snippet));173}174}175}176177if (options?.designSystem) {178const sourceDesignFindings = profileFindings(profile, {179engine: 'static-html',180phase: 'source',181ruleId: 'design-system',182target: filePath,183}, () => checkSourceDesignSystem(html, filePath, { designSystem: options.designSystem }));184const staticDesignFindings = profileFindings(profile, {185engine: 'static-html',186phase: 'page',187ruleId: 'design-system',188target: filePath,189}, () => collectStaticDesignSystemFindings(document, window, filePath, options.designSystem));190findings.push(...mergeDesignSystemFindings(staticDesignFindings, sourceDesignFindings));191}192193if (isFullPage(html)) {194const runPageCheck = (ruleId, callback) => profile195? profileFindings(profile, { engine: 'static-html', phase: 'page', ruleId, target: filePath }, callback)196: callback();197for (const f of runPageCheck('typography-rules', () => checkStaticPageTypography(document, window))) {198findings.push(finding(f.id, filePath, f.snippet));199}200for (const f of runPageCheck('repeated-section-kickers', () => checkRepeatedSectionKickersFromDoc(document, window))) {201findings.push(finding(f.id, filePath, f.snippet));202}203for (const f of runPageCheck('layout-rules', () => checkPageLayout(document, window))) {204findings.push(finding(f.id, filePath, f.snippet));205}206for (const f of runPageCheck('cream-palette', () => checkCreamPalette(document, window))) {207findings.push(finding(f.id, filePath, f.snippet));208}209for (const f of runPageCheck('skipped-heading', () => checkPageQualityFromDoc(document))) {210findings.push(finding(f.id, filePath, f.snippet));211}212for (const f of runPageCheck('html-patterns', () => checkHtmlPatterns(html).filter(item =>213item.id !== 'bounce-easing' && item.id !== 'layout-transition'214))) {215findings.push(finding(f.id, filePath, f.snippet));216}217// Text-content analyzers (em-dash overuse, marketing buzzwords,218// numbered section markers, aphoristic cadence) live in the regex219// engine. Call them from here so .html files get the same coverage220// as .css/.tsx files. These are scoped to text content only and221// don't overlap with static-html's element/page rules.222for (const f of runPageCheck('text-content', () => runTextContentAnalyzers(html, filePath, options))) {223findings.push(finding(f.antipattern, filePath, f.snippet));224}225}226227const byProvider = filterByProviders(findings, options.providers);228// Static-HTML findings carry no line number, so only whole-file229// `impeccable-disable` directives apply here — exactly the standalone-document230// waiver this primitive targets. Bypassed by `--no-config` / `--no-inline-ignores`.231return options?.inlineIgnores === false ? byProvider : applyInlineIgnores(byProvider, html);232}233234export { checkStaticPageTypography, STATIC_ELEMENT_RULES, detectHtml };235