From cf19c53d320681ccfbc11bfc66b47ff7341492b9 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 2 Sep 2026 11:39:44 -0700 Subject: [PATCH] Restore live linked keyframes AI assistance disclosure: Codex helped implement and verify this fix under maintainer direction. --- cli/engine/browser/injected/index.mjs | 117 ++++++++++++++++-- cli/engine/detect-antipatterns-browser.js | 117 ++++++++++++++++-- tests/detect-antipatterns-browser.test.mjs | 6 + .../antipatterns/linked-url-patterns.css | 33 +++++ .../antipatterns/linked-url-patterns.html | 3 + 5 files changed, 256 insertions(+), 20 deletions(-) diff --git a/cli/engine/browser/injected/index.mjs b/cli/engine/browser/injected/index.mjs index 66515ef3a..d242a31c1 100644 --- a/cli/engine/browser/injected/index.mjs +++ b/cli/engine/browser/injected/index.mjs @@ -1445,6 +1445,81 @@ if (IS_BROWSER) { return true; } + function splitCssCommaList(value) { + const parts = []; + let current = ''; + let quote = ''; + let escaped = false; + for (const char of String(value || '')) { + if (escaped) { + current += char; + escaped = false; + continue; + } + if (char === '\\') { + current += char; + escaped = true; + continue; + } + if (quote) { + current += char; + if (char === quote) quote = ''; + continue; + } + if (char === '"' || char === "'") { + quote = char; + current += char; + continue; + } + if (char === ',') { + parts.push(current); + current = ''; + continue; + } + current += char; + } + parts.push(current); + return parts; + } + + function normalizeAnimationName(value) { + const name = String(value || '').trim(); + if (name.length >= 2 && name[0] === name[name.length - 1] && (name[0] === '"' || name[0] === "'")) { + return name.slice(1, -1); + } + return name; + } + + function animationNamesDeclaredByRule(rule) { + const style = rule?.style; + if (!style) return []; + let value = ''; + try { + value = style.animationName + || style.getPropertyValue?.('animation-name') + || style.webkitAnimationName + || style.getPropertyValue?.('-webkit-animation-name') + || ''; + } catch { + return []; + } + return splitCssCommaList(value) + .map(normalizeAnimationName) + .filter(name => name && name.toLowerCase() !== 'none'); + } + + function keyframesRuleName(rule, cssText) { + const constructorName = rule?.constructor?.name || ''; + const type = Number(rule?.type); + const isKeyframes = constructorName === 'CSSKeyframesRule' + || constructorName === 'WebKitCSSKeyframesRule' + || type === 7 + || /^\s*@(?:-webkit-)?keyframes\b/i.test(cssText); + if (!isKeyframes) return ''; + const match = String(cssText || '').match(/^\s*@(?:-webkit-)?keyframes\s+([^\s{]+)/i); + return normalizeAnimationName(rule?.name || match?.[1] || ''); + } + // Read CSS that is absent from document.outerHTML. Inline