diff --git a/internal/check-one-palette.mjs b/internal/check-one-palette.mjs index f0edc87be..3c7366901 100644 --- a/internal/check-one-palette.mjs +++ b/internal/check-one-palette.mjs @@ -63,17 +63,61 @@ if (!existsSync(BUILT)) { process.exit(0) } +/** + * The hexes of the PALETTE export ONLY: from `export const PALETTE` to its matching closing brace, and + * only inside string literals. It used to match every hex from the export to the END OF THE FILE, + * comments included, so a later note such as "REJECTED #AF7AC5, scored 2.95, under the 3.0 floor" + * (affinity-block.js) obliged every consumer to ship a colour rejected on WCAG grounds. + * Returns null when the module has no PALETTE export. + */ +export function paletteHexes(src) { + const at = src.indexOf('export const PALETTE') + if (at < 0) return null + const open = src.indexOf('{', at), openArr = src.indexOf('[', at) + const start = [open, openArr].filter((i) => i >= 0).sort((a, b) => a - b)[0] + if (start === undefined) return [] + const close = { '{': '}', '[': ']' }[src[start]] + let depth = 0, i = start, quote = null + const strings = [] + let cur = '' + for (; i < src.length; i++) { + const c = src[i], n = src[i + 1] + if (quote) { + if (c === '\\') { cur += c + n; i++; continue } + if (c === quote) { strings.push(cur); cur = ''; quote = null; continue } + cur += c; continue + } + if (c === '/' && n === '/') { i = src.indexOf('\n', i); if (i < 0) break; continue } + if (c === '/' && n === '*') { i = src.indexOf('*/', i + 2) + 1; if (i <= 0) break; continue } + if (c === "'" || c === '"' || c === '`') { quote = c; continue } + if (c === src[start]) depth++ + else if (c === close && --depth === 0) break + } + return [...new Set(strings.flatMap((t) => [...t.matchAll(/#[0-9a-fA-F]{6}\b/g)].map((m) => m[0].toLowerCase())))] +} + +if (process.argv.includes('--self-test')) { + const src = [ + "export const PALETTE = {", + " a: '#112233', // not #445566 (a comment inside the export)", + " /* nor #778899 */ b: \"#aabbcc\",", + "}", + "// REJECTED #af7ac5, scored 2.95, under the 3.0 floor", + "export const OTHER = { c: '#ddeeff' }", + ].join('\n') + const got = paletteHexes(src).join(',') + if (got !== '#112233,#aabbcc') { console.error(`ONE-PALETTE: SELF-TEST FAIL — read ${got}, want #112233,#aabbcc`); process.exit(1) } + console.log('ONE-PALETTE: SELF-TEST PASS — only the PALETTE export\'s string values are read; comments and later code are not.') + process.exit(0) +} + // ── the expected palettes, read from the strategy modules the MAP uses ──────────────────────── const strategies = {} for (const f of readdirSync(COLORS)) { if (!f.endsWith('.js') || f.startsWith('_') || f.startsWith('.')) continue const src = readFileSync(join(COLORS, f), 'utf8') - // Only the PALETTE export — a module may also contain incidental hexes (a default, a comment) - // that no consumer is obliged to ship. - const at = src.indexOf('export const PALETTE') - if (at < 0) continue - const hexes = [...new Set([...src.slice(at).matchAll(/#[0-9a-fA-F]{6}/g)].map((m) => m[0].toLowerCase()))] - if (hexes.length) strategies[f.replace(/\.js$/, '')] = hexes + const hexes = paletteHexes(src) + if (hexes && hexes.length) strategies[f.replace(/\.js$/, '')] = hexes } // ── the consumers that must agree ───────────────────────────────────────────────────────────── diff --git a/package.json b/package.json index ab16932c8..9c0621745 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "check:no-orphan-gates": "node internal/check-no-orphan-gates.mjs", "check:no-public-binds": "node internal/check-no-public-binds.mjs", "check:one-palette": "node internal/check-one-palette.mjs", + "check:one-palette:self-test": "node internal/check-one-palette.mjs --self-test", "check:packet-browser": "node internal/check-packet-browser.mjs", "check:packet-fixture": "node internal/generate-packet-fixture.mjs --check", "check:packet-render": "node internal/check-packet-render.mjs", @@ -199,7 +200,7 @@ "dev": "bun x vite", "fix:sri": "node internal/fix-sri.mjs", "gallery": "node internal/generate-gallery.mjs", - "gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd", + "gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test", "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts", "library-index": "node internal/generate-library-index.mjs", "lists": "node library/scripts/people-groups/build-lists.mjs",