From 5e95741364f07f85fac66a63b9a3422df22d0b17 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Thu, 22 Oct 2026 09:36:34 +0800 Subject: [PATCH] fix(01-design-tokens-rams-foundation): WR-02 repair familiesFromShorthand + flag literal font: shorthands --- theme/scripts/audit.mjs | 46 +++++++++++++++++++++++++++++------- theme/scripts/audit.test.mjs | 30 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/theme/scripts/audit.mjs b/theme/scripts/audit.mjs index 823e46b..02c586a 100644 --- a/theme/scripts/audit.mjs +++ b/theme/scripts/audit.mjs @@ -439,8 +439,19 @@ function checkDeclaration(prop, value, tokens) { violations.push(...literalHexViolations(value, tokens)); if (prop.startsWith('--')) return violations; // custom property: color literals above are the checkable surface violations.push(...shadowViolations(prop, value)); - if (prop === 'font-family' || prop === 'font') { - if (!/var\(/i.test(value)) violations.push(...fontViolations(prop === 'font' ? familiesFromShorthand(value) : value, tokens)); + if (prop === 'font') { + /* WR-02: a literal font: shorthand embeds unvalidated weight/ + size/line-height numerics — flag it outright (package convention: + font: var(--type-*)), naming the extracted families. */ + if (!/var\(/i.test(value)) { + const families = familiesFromShorthand(value); + violations.push( + 'literal font: shorthand — use font: var(--type-*) (Rams typography tokens)' + + (families ? `; families: ${families}` : ''), + ); + } + } else if (prop === 'font-family' && !/var\(/i.test(value)) { + violations.push(...fontViolations(value, tokens)); } if (RADIUS_PROPS.test(prop) && !radiusAllowed(value)) { violations.push('non-zero border-radius (Rams Shapes: absolute zero roundedness)'); @@ -448,13 +459,32 @@ function checkDeclaration(prop, value, tokens) { return violations; } -/* font: shorthand — families are the comma groups without sizes/weights. */ +/* font: shorthand — families are the comma groups after the leading + style/variant/weight keyword run and the size[/line-height] + component: `700 14px/18px Arial, sans-serif` → `Arial, sans-serif`. + (WR-02: the old single-strip regex removed only the first token, + leaving "14px/18px Arial", which the numeric filter then dropped — + so no font: shorthand could ever be flagged.) */ function familiesFromShorthand(value) { - return value - .split(',') - .map((g) => g.trim().replace(/^[0-9./]+\S*\s+/g, '').trim()) - .filter((g) => !/^[0-9.]/.test(g) && g.length > 0) - .join(','); + const families = []; + for (const group of splitTopLevel(value, ',')) { + let s = group.trim(); + if (!s) continue; + /* strip the leading style/variant/weight run (any order per the + CSS grammar, incl. bare weights and an oblique angle) */ + let m; + while ((m = /^(?:normal|italic|oblique(?:\s+[0-9.]+(?:deg|grad|rad|turn))?|small-caps|bold|bolder|lighter|[1-9][0-9]{0,2})\s+/i.exec(s))) { + s = s.slice(m[0].length); + } + /* strip the mandatory size (optionally /line-height) once */ + s = s.replace( + /^(?:calc\([^()]*\)|[0-9]*\.?[0-9]+(?:[a-z%]+)?)\s*\/\s*(?:calc\([^()]*\)|[0-9]*\.?[0-9]+(?:[a-z%]+)?)\s+/i, + '', + ).replace(/^(?:calc\([^()]*\)|[0-9]*\.?[0-9]+(?:[a-z%]+)?)\s+/i, ''); + const family = stripFamily(s); + if (family) families.push(family); + } + return families.join(','); } /* ————— audit driver ————— */ diff --git a/theme/scripts/audit.test.mjs b/theme/scripts/audit.test.mjs index 663b35d..8989910 100644 --- a/theme/scripts/audit.test.mjs +++ b/theme/scripts/audit.test.mjs @@ -33,6 +33,10 @@ * bare `rounded` → var(--radius) = 0px (controlled * build); non-zero bare key → exit 2; the inert * v3-style DEFAULT sub-key must not reappear + * T10 literal font: shorthands flagged regardless of + * family (convention: font: var(--type-*)); var() + * shorthands pass; familiesFromShorthand extracts + * the family for the message → flagged (exit 1) */ import { spawnSync } from 'node:child_process'; import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs'; @@ -164,6 +168,17 @@ writeFileSync(styleAttrPage, ` `); +/* T10 — literal font: shorthands (WR-02: the familiesFromShorthand + * strip bug left "14px/18px Arial" behind, which the numeric filter + * then dropped — no font: shorthand could ever be flagged). */ +const fontShorthand = join(FIX, 'font-shorthand.css'); +writeFileSync(fontShorthand, `/* literal font: shorthand — flagged regardless of family (package + convention: font: var(--type-*)); var() shorthands pass */ +.a-font-shorthand { font: 700 14px/18px Arial; } +.b-font-shorthand { font: italic 700 12px/16px "Space Grotesk", sans-serif; } +.c-font-shorthand-var { font: var(--type-body-md); } +`); + /* ————— tests ————— */ // T1 — clean stylesheet exits 0 @@ -289,6 +304,21 @@ writeFileSync(styleAttrPage, ` 'bare rounded still compiles to the v3 .25rem default'); } +// T10 — literal font: shorthands are flagged; var() shorthands pass +{ + const r = runAudit([fontShorthand]); + check('T10 literal font: shorthand → exit 1', r.status === 1, + `status=${r.status} error=${r.error} out=${r.out.slice(0, 300)}`); + check('T10 off-token shorthand family extracted (Arial)', + /font: 700 14px\/18px Arial/.test(r.out) && /Arial/.test(r.out), + `out=${r.out.slice(0, 300)}`); + check('T10 sanctioned-family shorthand still flagged (convention: font: var(--type-*))', + /font: italic 700 12px\/16px "Space Grotesk", sans-serif/.test(r.out), + `out=${r.out.slice(0, 300)}`); + check('T10 var() shorthand passes', !/var\(--type-body-md\)/.test(r.out), + `out=${r.out.slice(0, 300)}`); +} + /* ————— report ————— */ console.log(`audit.test: ${passes.length} passed, ${failures.length} failed`); passes.forEach((p) => console.log(` PASS ${p}`));