fix: match root for CSS currentColor and skip unreferenced defs tones

This commit is contained in:
Marco Beretta 2026-06-23 17:28:33 +02:00
parent ef93643a16
commit 166ca58919
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
2 changed files with 41 additions and 1 deletions

View file

@ -178,6 +178,12 @@ describe('isMonochromeSvg', () => {
'<svg viewBox="0 0 24 24"><defs><path id="glyph" d="M6 6h12v12H6z" /></defs><rect x="0" y="0" width="8" height="8" fill="#fff" /><use href="#glyph" /></svg>';
expect(isMonochromeSvg(svg)).toBe(false);
});
it('ignores an unreferenced colored template in defs', () => {
const svg =
'<svg viewBox="0 0 24 24"><defs><symbol id="unused"><path fill="#f00" d="M0 0h4v4H0z" /></symbol></defs><path fill="#333" d="M6 6h12v12H6z" /></svg>';
expect(isMonochromeSvg(svg)).toBe(true);
});
});
describe('currentColor resolved against a fixed color', () => {
@ -240,6 +246,12 @@ describe('isMonochromeSvg', () => {
'<svg viewBox="0 0 24 24"><style>svg{fill:#f00}</style><path d="M4 4h16v16H4z" /></svg>';
expect(isMonochromeSvg(svg)).toBe(false);
});
it('preserves a root CSS currentColor fill fixed by the root color', () => {
const svg =
'<svg viewBox="0 0 24 24" color="#e00"><style>svg{fill:currentColor}</style><path d="M4 4h16v16H4z" /></svg>';
expect(isMonochromeSvg(svg)).toBe(false);
});
});
describe('default fills alongside <style> rules', () => {

View file

@ -56,6 +56,9 @@ const FUNCTIONAL_CONTAINERS = new Set(['clippath', 'mask', 'marker', 'pattern'])
*/
const DEFERRED_CONTAINERS = new Set(['clippath', 'mask', 'marker', 'pattern', 'defs', 'symbol']);
/** Containers whose content renders only when referenced (e.g. through `<use>`). */
const TEMPLATE_CONTAINERS = new Set(['defs', 'symbol']);
/** Paint properties whose corresponding opacity makes them invisible at zero. */
const PAINT_OPACITY = new Map([
['fill', 'fill-opacity'],
@ -290,6 +293,9 @@ function resolveCssCurrentColor(root: Element, selector: string, rules: StyleRul
let matched: Element[];
try {
matched = Array.from(root.querySelectorAll(selector));
if (root.matches(selector)) {
matched = [root, ...matched];
}
} catch {
return [CURRENT_COLOR];
}
@ -373,13 +379,35 @@ function resolveCurrentColor(el: Element, root: Element, rules: StyleRule[]): st
return CURRENT_COLOR;
}
/** Elements rendered through a visible `<use>` (target subtrees), so their
* template paint counts even though they live in a deferred container. */
function referencedElements(root: Element, rules: StyleRule[]): Set<Element> {
const referenced = new Set<Element>();
for (const use of Array.from(root.querySelectorAll('use'))) {
if (isHidden(use, root, rules) || isInside(use, root, DEFERRED_CONTAINERS)) {
continue;
}
const target = referencedTarget(use, root);
if (target == null) {
continue;
}
referenced.add(target);
for (const el of Array.from(target.querySelectorAll('*'))) {
referenced.add(el);
}
}
return referenced;
}
function collectColors(root: Element, rules: StyleRule[]): string[] {
const colors: string[] = [];
const referenced = referencedElements(root, rules);
for (const el of [root, ...Array.from(root.querySelectorAll('*'))]) {
if (
el.nodeName.toLowerCase() === 'style' ||
isInside(el, root, FUNCTIONAL_CONTAINERS) ||
isHidden(el, root, rules)
isHidden(el, root, rules) ||
(isInside(el, root, TEMPLATE_CONTAINERS) && !referenced.has(el))
) {
continue;
}