fix: skip transparent svg use references

This commit is contained in:
Marco Beretta 2026-06-25 01:19:47 +02:00
parent d168091f1f
commit 1d4da287ea
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
2 changed files with 45 additions and 6 deletions

View file

@ -209,6 +209,24 @@ describe('isMonochromeSvg', () => {
expect(isMonochromeSvg(svg)).toBe(true);
});
it('ignores a referenced color rendered only through an opacity-zero use', () => {
const svg =
'<svg viewBox="0 0 24 24"><defs><path id="red" fill="#f00" d="M6 6h12v12H6z" /></defs><path fill="#333" d="M0 0h4v4H0z" /><use href="#red" opacity="0" /></svg>';
expect(isMonochromeSvg(svg)).toBe(true);
});
it('ignores a nested referenced color hidden by an opacity-zero use', () => {
const svg =
'<svg viewBox="0 0 24 24"><defs><symbol id="s"><use href="#red" opacity="0" /></symbol><path id="red" fill="#f00" d="M6 6h12v12H6z" /></defs><path fill="#333" d="M0 0h4v4H0z" /><use href="#s" /></svg>';
expect(isMonochromeSvg(svg)).toBe(true);
});
it('ignores a default-black use hidden with opacity zero', () => {
const svg =
'<svg viewBox="0 0 24 24"><defs><path id="g" d="M6 6h12v12H6z" /></defs><path fill="#fff" d="M0 0h4v4H0z" /><use href="#g" opacity="0" /></svg>';
expect(isMonochromeSvg(svg)).toBe(true);
});
it('tints a referenced glyph whose own fill overrides the use fill', () => {
const svg =
'<svg viewBox="0 0 24 24"><defs><path id="p" fill="#333" d="M4 4h16v16H4z" /></defs><use href="#p" fill="#000" /></svg>';

View file

@ -342,8 +342,8 @@ function currentColorTones(
* instantiating `<use>` elements, so its template paint counts even though it
* lives in a deferred container, and `currentColor` can resolve against the
* instance's inherited `color`. Nested `<use>` inside a referenced template are
* followed too (a symbol may reference another template). Hidden uses render
* nothing and are skipped; a `seen` set guards against reference cycles.
* followed too (a symbol may reference another template). Hidden or opacity-zero
* uses render nothing and are skipped; a `seen` set guards against reference cycles.
*/
function referenceMap(root: Element, rules: StyleRule[]): Map<Element, Element[]> {
const map = new Map<Element, Element[]>();
@ -365,7 +365,7 @@ function referenceMap(root: Element, rules: StyleRule[]): Map<Element, Element[]
link(el, use);
}
for (const nested of Array.from(target.querySelectorAll('use'))) {
if (isHidden(nested, root, rules)) {
if (instanceInvisible(nested, root, rules)) {
continue;
}
const nestedTarget = referencedTarget(nested, root);
@ -375,7 +375,7 @@ function referenceMap(root: Element, rules: StyleRule[]): Map<Element, Element[]
}
};
for (const use of Array.from(root.querySelectorAll('use'))) {
if (isHidden(use, root, rules) || isInside(use, root, DEFERRED_CONTAINERS)) {
if (instanceInvisible(use, root, rules) || isInside(use, root, DEFERRED_CONTAINERS)) {
continue;
}
const target = referencedTarget(use, root);
@ -593,6 +593,24 @@ function isHidden(el: Element, root: Element, rules: StyleRule[]): boolean {
return false;
}
function hasZeroOpacity(el: Element, root: Element, rules: StyleRule[]): boolean {
let current: Element | null = el;
while (current != null) {
if (styleNumber(current, rules, 'opacity') === 0) {
return true;
}
if (current === root) {
break;
}
current = current.parentElement;
}
return false;
}
function instanceInvisible(el: Element, root: Element, rules: StyleRule[]): boolean {
return isHidden(el, root, rules) || hasZeroOpacity(el, root, rules);
}
/**
* True when a paint is fully transparent through opacity: the element or any
* ancestor group has `opacity:0` (group opacity is not inherited and composites
@ -776,7 +794,10 @@ function targetInheritsPaint(
}
}
for (const nested of Array.from(target.querySelectorAll('use'))) {
if (isHidden(nested, root, rules) || resolvePaint(nested, target, rules, property) != null) {
if (
instanceInvisible(nested, root, rules) ||
resolvePaint(nested, target, rules, property) != null
) {
continue;
}
const nestedTarget = referencedTarget(nested, root);
@ -811,7 +832,7 @@ function instanceContributesPaint(
function hasDefaultBlackUse(root: Element, rules: StyleRule[]): boolean {
for (const use of Array.from(root.querySelectorAll('use'))) {
if (
isHidden(use, root, rules) ||
instanceInvisible(use, root, rules) ||
isInside(use, root, DEFERRED_CONTAINERS) ||
fillIsResolved(use, root, rules)
) {