diff --git a/client/src/utils/__tests__/svg.test.ts b/client/src/utils/__tests__/svg.test.ts index 6ac459b78e..e150f1b80a 100644 --- a/client/src/utils/__tests__/svg.test.ts +++ b/client/src/utils/__tests__/svg.test.ts @@ -360,6 +360,18 @@ describe('isMonochromeSvg', () => { ''; expect(isMonochromeSvg(svg)).toBe(true); }); + + it('preserves a glyph instantiated twice with different colors through nested use', () => { + const svg = + ''; + expect(isMonochromeSvg(svg)).toBe(false); + }); + + it('terminates on a self-referential use cycle and still finds its color', () => { + const svg = + ''; + expect(isMonochromeSvg(svg)).toBe(false); + }); }); describe('paint set on non-rendering containers', () => { @@ -525,6 +537,18 @@ describe('isMonochromeSvg', () => { ''; expect(isMonochromeSvg(svg)).toBe(true); }); + + it('resolves a mixed-case filter id referenced by attribute', () => { + const svg = + ''; + expect(isMonochromeSvg(svg)).toBe(false); + }); + + it('resolves a mixed-case filter id referenced by a CSS rule', () => { + const svg = + ''; + expect(isMonochromeSvg(svg)).toBe(false); + }); }); describe('opaque background (not tintable as a mask)', () => { diff --git a/client/src/utils/svg.ts b/client/src/utils/svg.ts index 29f6b8f980..a84c6522e2 100644 --- a/client/src/utils/svg.ts +++ b/client/src/utils/svg.ts @@ -391,7 +391,9 @@ function currentColorTones( * the innermost reference outward; nested `` inside a referenced template are * followed (a symbol may reference another template) with the outer chain preserved, * so an outer `` reaches a nested `currentColor` shape. Hidden or - * opacity-zero uses render nothing and are skipped; a `seen` set guards cycles. + * opacity-zero uses render nothing and are skipped; a per-path guard prevents + * reference cycles without dropping a target's other instantiation chains (the same + * template may be instantiated more than once with different colors). */ function referenceMap(root: Element, rules: StyleRule[]): Map { const map = new Map(); @@ -403,11 +405,11 @@ function referenceMap(root: Element, rules: StyleRule[]): Map) => { - if (seen.has(target)) { + const linkTarget = (target: Element, chain: Element[], path: Set) => { + if (path.has(target)) { return; } - seen.add(target); + path.add(target); link(target, chain); for (const el of Array.from(target.querySelectorAll('*'))) { link(el, chain); @@ -418,9 +420,10 @@ function referenceMap(root: Element, rules: StyleRule[]): Map 0) { @@ -671,14 +689,16 @@ function cssDecl(el: Element, rules: StyleRule[], property: string): string | nu function styleValue(el: Element, rules: StyleRule[], property: string): string | null { const inline = inlineStyleValue(el, property); if (inline != null && inline.trim() !== '') { - return inline.trim().toLowerCase(); + return lowerPreservingRefs(inline.trim()); } const css = cssDecl(el, rules, property); if (css != null && css !== '') { return css; } const attribute = el.getAttribute(property); - return attribute != null && attribute.trim() !== '' ? attribute.trim().toLowerCase() : null; + return attribute != null && attribute.trim() !== '' + ? lowerPreservingRefs(attribute.trim()) + : null; } /** Parses a property to a number, or null when absent/unparseable. */