mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 01:26:29 +03:00
Derive static-cascade color extraction from the shared named-color table
The static-html engine never emitted side-tab for `border-left: 4px solid purple` (or any named color outside a hardcoded 9-name list) in .html files: extractStaticColor's regex dropped the color token from border shorthands, the side defaulted to neutral black, and checkBorders skipped it. The same declaration in a .css file was flagged by the regex engine, so the two engines disagreed while both exited cleanly (issue #359). Build the extraction alternation from the same CSS_NAMED_COLORS table parseAnyColor resolves against (longest-first, whole-token), so the set of names the extractor recognizes and the set the parser can resolve cannot drift apart again. STATIC_NAMED_COLORS shrinks to the one keyword parseAnyColor deliberately refuses (`transparent` as zero-alpha), since parseAnyColor already covers every real named color in the table. New two-column fixture (named-color-borders.html) covers the issue reproducers: purple shorthand + radius, rebeccapurple (substring-safe matching), crimson top stripe, bare 3px teal, var() resolving to a named color, and an inline style attribute — with neutral named colors (dimgray, gainsboro, black), thin, and uniform borders as pass cases. Prepared with AI assistance (Claude Code), directed by @pbakaus. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Code
parent
af78b1e512
commit
7622cc8440
@@ -366,6 +366,36 @@ describe('detectHtml — static HTML/CSS fixtures', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('named-color-borders: named-color side-tabs are flagged, neutral names pass', async () => {
|
||||
// Regression for issue #359: the static cascade's shorthand color
|
||||
// extraction recognized only 9 named colors, so `border-left: 4px solid
|
||||
// purple` (or any of the other named colors parseAnyColor understands)
|
||||
// lost its color during expansion, defaulted to neutral black, and never
|
||||
// fired side-tab — while the same declaration in a .css file was flagged
|
||||
// by the regex engine. The extraction list is now derived from the same
|
||||
// CSS_NAMED_COLORS table the parser uses, so the two can't drift apart.
|
||||
const f = await detectHtml(path.join(FIXTURES, 'named-color-borders.html'));
|
||||
const sideTabs = f.filter(r => r.antipattern === 'side-tab');
|
||||
// Six FLAG cases: purple + radius (the issue reproducer), rebeccapurple
|
||||
// (contains "purple" as a substring — whole-token matching), crimson
|
||||
// top stripe, bare 3px teal, a var() resolving to a named color, and an
|
||||
// inline style attribute. Each must produce exactly one side-tab.
|
||||
assert.equal(
|
||||
sideTabs.length, 6,
|
||||
`expected 6 side-tab findings from the FLAG column, got ${sideTabs.length}: ${sideTabs.map(r => r.snippet).join('; ')}`
|
||||
);
|
||||
const topFindings = sideTabs.filter(r => /border-top/.test(r.snippet || ''));
|
||||
assert.equal(topFindings.length, 1, `expected 1 border-top finding, got ${topFindings.length}`);
|
||||
// PASS column (neutral named colors, thin, and uniform borders) must
|
||||
// contribute nothing — dimgray/gainsboro/black have to parse AND read
|
||||
// as neutral rather than being dropped as unknown colors.
|
||||
const borderAccent = f.filter(r => r.antipattern === 'border-accent-on-rounded');
|
||||
assert.equal(
|
||||
borderAccent.length, 0,
|
||||
`expected 0 border-accent-on-rounded, got ${borderAccent.length}: ${borderAccent.map(r => r.snippet).join('; ')}`
|
||||
);
|
||||
});
|
||||
|
||||
it('modern-color-borders: regex fallback skips neutral 1px oklch dividers', () => {
|
||||
const css = `
|
||||
.flag-side-tab {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Side-Tab with CSS Named Colors (purple/rebeccapurple/crimson/teal)</title>
|
||||
<style>
|
||||
/* Two-column fixture: left col = should-flag, right col = should-pass.
|
||||
Regression for issue #359: `border-left: 4px solid purple` in a .html
|
||||
file never fired side-tab because the static cascade's shorthand color
|
||||
extraction only recognized 9 named colors — every other spec name
|
||||
(purple, rebeccapurple, crimson, teal, ...) was dropped from the
|
||||
shorthand, leaving the side at the default black, which reads as
|
||||
neutral and silently skips the check. The same declaration in a .css
|
||||
file was flagged by the regex engine, so the two engines disagreed. */
|
||||
|
||||
:root {
|
||||
--accent: orange; /* named color behind a var() — must resolve and flag */
|
||||
}
|
||||
|
||||
body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; background: #fafafa; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1120px; margin: 0 auto; }
|
||||
.col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }
|
||||
.case { padding: 12px 16px; margin-bottom: 16px; }
|
||||
.case h3 { font-size: 14px; margin: 0 0 4px; }
|
||||
.case p { font-size: 13px; margin: 0; color: #64748b; }
|
||||
|
||||
/* ── FLAG cases: colored side-tab borders using CSS named colors ── */
|
||||
|
||||
/* 1: the exact reproducer from issue #359 — named purple in a <style>
|
||||
rule with background + padding + radius */
|
||||
#flag-named-purple {
|
||||
width: 400px;
|
||||
background: #f6f6f6;
|
||||
padding: 16px;
|
||||
border-left: 4px solid purple;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 2: rebeccapurple — a longer name that contains another color name
|
||||
("purple") as a substring; must match whole-token */
|
||||
#flag-named-rebecca {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid rebeccapurple;
|
||||
}
|
||||
|
||||
/* 3: horizontal stripe variant — named crimson riding the top edge */
|
||||
#flag-named-crimson-top {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-top: 4px solid crimson;
|
||||
}
|
||||
|
||||
/* 4: named teal, 3px, no radius — the bare w >= 3 arm */
|
||||
#flag-named-teal {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-left: 3px solid teal;
|
||||
}
|
||||
|
||||
/* 5: named color behind a var() in the shorthand */
|
||||
#flag-named-var {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid var(--accent);
|
||||
}
|
||||
|
||||
/* ── PASS cases: neutral named colors and non-side-tab shapes ── */
|
||||
|
||||
/* 1: dimgray — chromatic-looking name, neutral value; must NOT fire */
|
||||
#pass-named-dimgray {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid dimgray;
|
||||
}
|
||||
|
||||
/* 2: gainsboro — light neutral named color */
|
||||
#pass-named-gainsboro {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid gainsboro;
|
||||
}
|
||||
|
||||
/* 3: named black side border — neutral, not a colored stripe */
|
||||
#pass-named-black {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid black;
|
||||
}
|
||||
|
||||
/* 4: 1px named purple — too thin to qualify */
|
||||
#pass-named-thin {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border-left: 1px solid purple;
|
||||
}
|
||||
|
||||
/* 5: uniform named purple border on all four sides — not a side-tab */
|
||||
#pass-named-allsides {
|
||||
width: 400px;
|
||||
background: #ffffff;
|
||||
border: 3px solid purple;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="grid">
|
||||
<div class="col" data-col="flag">
|
||||
<h2>Should flag</h2>
|
||||
<div class="case" id="flag-named-purple"><h3>named purple</h3><p>border-left 4px solid purple + radius</p></div>
|
||||
<div class="case" id="flag-named-rebecca"><h3>rebeccapurple</h3><p>border-left 4px + radius</p></div>
|
||||
<div class="case" id="flag-named-crimson-top"><h3>crimson top stripe</h3><p>border-top 4px, horizontal variant</p></div>
|
||||
<div class="case" id="flag-named-teal"><h3>named teal</h3><p>border-left 3px, no radius</p></div>
|
||||
<div class="case" id="flag-named-var"><h3>var() to named</h3><p>border-left 4px solid var(--accent)</p></div>
|
||||
<div class="case" style="width: 400px; background: #ffffff; border-left: 4px solid purple">
|
||||
<h3>inline named purple</h3><p>style attribute, issue #359 case (a)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col" data-col="pass">
|
||||
<h2>Should pass</h2>
|
||||
<div class="case" id="pass-named-dimgray"><h3>dimgray</h3><p>neutral named color</p></div>
|
||||
<div class="case" id="pass-named-gainsboro"><h3>gainsboro</h3><p>light neutral named color</p></div>
|
||||
<div class="case" id="pass-named-black"><h3>named black</h3><p>neutral side border</p></div>
|
||||
<div class="case" id="pass-named-thin"><h3>1px purple</h3><p>too thin to qualify</p></div>
|
||||
<div class="case" id="pass-named-allsides"><h3>uniform purple</h3><p>all four sides, not a stripe</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/detect-antipatterns-browser.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user