diff --git a/lib/design-lint/css.ts b/lib/design-lint/css.ts index d539cc13b..f2e39c08c 100644 --- a/lib/design-lint/css.ts +++ b/lib/design-lint/css.ts @@ -86,15 +86,16 @@ export function selectorNames(selectors: string[]): { tags: Set; 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); } diff --git a/lib/design-lint/html.ts b/lib/design-lint/html.ts index 8c4c23496..cda3d3d2a 100644 --- a/lib/design-lint/html.ts +++ b/lib/design-lint/html.ts @@ -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); diff --git a/lib/design-lint/render-probe-space.ts b/lib/design-lint/render-probe-space.ts index ba60a6185..47fe5b989 100644 --- a/lib/design-lint/render-probe-space.ts +++ b/lib/design-lint/render-probe-space.ts @@ -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' });