diff --git a/src/clone-node.ts b/src/clone-node.ts index 5dfcd117..93ac20ee 100644 --- a/src/clone-node.ts +++ b/src/clone-node.ts @@ -80,7 +80,23 @@ async function cloneChildren( clonedNode: T, options: Options, ): Promise { - if (isSVGElement(clonedNode)) { + if (isSVGElement(nativeNode)) { + // SVG was deep-cloned for performance, but CSS custom properties (var()) + // defined in external stylesheets won't be available in the exported image. + // We resolve them by copying getComputedStyle() values — which resolves + // var() to their actual computed values — onto each cloned descendant. + const nativeDescendants = Array.from( + nativeNode.querySelectorAll('*'), + ) + const clonedDescendants = Array.from( + clonedNode.querySelectorAll('*'), + ) + nativeDescendants.forEach((native, i) => { + const cloned = clonedDescendants[i] + if (cloned) { + cloneCSSStyle(native, cloned, options) + } + }) return clonedNode } diff --git a/test/resources/svg-css-var/node.html b/test/resources/svg-css-var/node.html new file mode 100644 index 00000000..b6022382 --- /dev/null +++ b/test/resources/svg-css-var/node.html @@ -0,0 +1,9 @@ + + + diff --git a/test/resources/svg-css-var/style.css b/test/resources/svg-css-var/style.css new file mode 100644 index 00000000..c829c7ca --- /dev/null +++ b/test/resources/svg-css-var/style.css @@ -0,0 +1,12 @@ +#dom-node { + width: 100px; + overflow: hidden; +} + +:root { + --rect-fill: rgb(0, 128, 0); +} + +.var-rect { + fill: var(--rect-fill); +} diff --git a/test/spec/svg.spec.ts b/test/spec/svg.spec.ts index f4595a11..28590b9f 100644 --- a/test/spec/svg.spec.ts +++ b/test/spec/svg.spec.ts @@ -57,4 +57,29 @@ describe('work with svg element', () => { .then(done) .catch(done) }) + + it('should resolve CSS custom properties (var()) in SVG descendants', (done) => { + // Regression test for: SVG deep-clone skips cloneCSSStyle for descendants, + // leaving CSS var() unresolved in exported image. + // Fix in clone-node.ts: walk native/cloned descendant pairs, call cloneCSSStyle. + // The CSS defines: :root { --rect-fill: rgb(0, 128, 0); } and .var-rect { fill: var(--rect-fill); } + // Without the fix: the cloned rect has no inline style (deep-clone skips descendants). + // With the fix: the cloned rect has fill: rgb(0, 128, 0) as an inline style. + bootstrap('svg-css-var/node.html', 'svg-css-var/style.css') + .then(toSvg) + .then(getSvgDocument) + .then((doc) => { + const rect = doc.querySelector('.var-rect') as SVGRectElement | null + expect(rect).not.toBeNull() + // After the fix, cloneCSSStyle copies computed styles onto the cloned rect. + // The inline style must contain a resolved fill color (not a var() reference). + const inlineStyle = rect?.getAttribute('style') ?? '' + expect(inlineStyle).not.toContain('var(') + // Positive assertion: the fill property must be present and resolved to rgb(0, 128, 0). + // This fails without the fix (style would be empty since descendants are skipped). + expect(inlineStyle).toMatch(/fill\s*:\s*rgb\(\s*0\s*,\s*128\s*,\s*0\s*\)/) + }) + .then(done) + .catch(done) + }) })