design-lint: eleventh review cycle fixes

A nested rule under a parent longer than the selector cap falls back to the
parent's first part cut to 256 characters, the :is() wrapper is built only on
the expanding path, and every stored selector is cut at MAX_SELECTOR; the
limited stripTags counts visibility with the whitespace class its callers
collapse on and never cuts a surrogate pair; glWords slides its window by
innerHeight.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 12:57:05 +00:00
1 parent 6d01b1f3cd
commit 7c9dab30ef
3 files changed
+8 -6

No files matched your search

+6 -4
View File
@@ -86,15 +86,16 @@ export function selectorNames(selectors: string[]): { tags: Set<string>; classes
return { tags, classes };
}
/** A selector longer than this is a nesting blow-up (comma lists nested in comma lists), not a stylesheet selector; past it the nested selector stands without its parent, so a sheet costs its length and not parts^depth. */
/** A selector longer than this is a nesting blow-up (comma lists nested in comma lists, a parent repeated per '&'), not a stylesheet selector; a nested selector that would pass it stands without its parent, and every stored selector is cut there, so a sheet costs its length and not parts^depth. */
const MAX_SELECTOR = 8192;
/** Expand a nested selector against its parent: `&` is replaced, a bare selector is descendant-joined. */
function nestSelector(sel: string, parent: string): string {
const p = parent.includes(',') ? `:is(${parent})` : parent;
const parts = splitTop(sel).map((part) => part.trim());
const substitutions = parts.reduce((n, part) => n + Math.max(1, part.split('&').length - 1), 0);
if (sel.length + p.length * substitutions > MAX_SELECTOR) return parts.map((part) => part.replace(/&/g, '').trim()).filter(Boolean).join(', ') || p;
const wrapped = parent.includes(',') ? parent.length + 5 : parent.length;
if (sel.length + wrapped * substitutions > MAX_SELECTOR) return parts.map((part) => part.replace(/&/g, '').trim()).filter(Boolean).join(', ') || splitTop(parent.slice(0, 256))[0].trim();
const p = parent.includes(',') ? `:is(${parent})` : parent;
return parts.map((part) => part.includes('&') ? part.replace(/&/g, p) : `${p} ${part}`).join(', ');
}
@@ -167,7 +168,8 @@ export function parseCss(source: string, file: string, lineOffset = 0): CssRule[
if (depthLeft > 0) walk(blockStart, blockEnd, media, keyframes, null, depthLeft - 1);
}
} else {
const sel = parent && !keyframes ? nestSelector(selector, parent) : selector;
const nested = parent && !keyframes ? nestSelector(selector, parent) : selector;
const sel = nested.length > MAX_SELECTOR ? nested.slice(0, MAX_SELECTOR) : nested;
rules.push(makeRule(sel, blockStart, blockEnd, media, keyframes));
if (!keyframes && depthLeft > 0 && src.slice(blockStart, blockEnd).includes('{')) walk(blockStart, blockEnd, media, keyframes, sel, depthLeft - 1);
}
+1 -1
View File
@@ -115,7 +115,7 @@ function blankRanges(s: string, ranges: Array<[number, number]>, cut = false): s
export function stripTags(s: string, keep = false, limit = Infinity): string {
let out = '', i = 0, seen = 0;
for (;;) {
if (limit < Infinity) for (let k = i; k < s.length && s[k] !== '<'; k++) if (s[k] > ' ' && ++seen > limit) return out + s.slice(i, k + 1);
if (limit < Infinity) for (let k = i; k < s.length && s[k] !== '<'; k++) if (!/\s/.test(s[k]) && ++seen > limit) return out + s.slice(i, k + (s.charCodeAt(k) >= 0xd800 && s.charCodeAt(k) < 0xdc00 ? 2 : 1));
const lt = s.indexOf('<', i);
if (lt < 0) break;
const gt = s.indexOf('>', lt + 1);
+1 -1
View File
@@ -155,7 +155,7 @@ export function glWords(P: Probes, ctx: HeroCtx): ProbeFinding[] {
const H = document.documentElement.scrollHeight;
let maxWords = 0, at = 0;
for (let y = 0; y < H; y += 50) {
const w = Math.round(words.reduce((a, x) => { const h = Math.max(1, x.bottom - x.top); const ov = Math.max(0, Math.min(x.bottom, y + 900) - Math.max(x.top, y)); return a + x.c * Math.min(1, ov / h); }, 0));
const w = Math.round(words.reduce((a, x) => { const h = Math.max(1, x.bottom - x.top); const ov = Math.max(0, Math.min(x.bottom, y + innerHeight) - Math.max(x.top, y)); return a + x.c * Math.min(1, ov / h); }, 0));
if (w > maxWords) { maxWords = w; at = y; }
}
if (maxWords > 95) out.push({ rule: 'screen-word-budget', grade: 'fail', evidence: maxWords + ' words on one ' + innerHeight + 'px screen (from y=' + at + ')', fix: 'one idea per screen: cut to about 80 words and make the section an object' });