@@ -104,9 +104,14 @@ function exportSVG(options: ToSVGOptions): Promise<SVGElement> {
104104 return convertingImages . then ( ( ) => {
105105 // workaround to include only graph-explorer-related stylesheets
106106 const exportedCssText = extractCSSFromDocument ( svgClone ) ;
107+ // design tokens have to be re-declared on the detached export root,
108+ // otherwise the `var(--…)` references in the rules above resolve to nothing
109+ const exportedVariables = extractCSSVariables (
110+ options . paper . closest ( ".graph-explorer" )
111+ ) ;
107112
108113 const defs = document . createElementNS ( "http://www.w3.org/2000/svg" , "defs" ) ;
109- defs . innerHTML = `<style>${ exportedCssText } </style>` ;
114+ defs . innerHTML = `<style>${ exportedVariables } \n ${ exportedCssText } </style>` ;
110115 svgClone . insertBefore ( defs , svgClone . firstChild ) ;
111116
112117 if ( options . elementsToRemoveSelector ) {
@@ -176,8 +181,7 @@ function _clearAttributes(svg: SVGElement) {
176181 }
177182}
178183
179- function extractCSSFromDocument ( targetSubtree : Element ) : string {
180- const exportedRules = new Set < CSSStyleRule > ( ) ;
184+ function forEachStyleRule ( callback : ( rule : CSSStyleRule ) => void ) {
181185 for ( let i = 0 ; i < document . styleSheets . length ; i ++ ) {
182186 let rules : CSSRuleList ;
183187 try {
@@ -187,18 +191,68 @@ function extractCSSFromDocument(targetSubtree: Element): string {
187191 continue ;
188192 }
189193 } catch ( _e ) {
194+ // cross-origin stylesheets cannot be inspected
190195 continue ;
191196 }
192197
193198 for ( let j = 0 ; j < rules . length ; j ++ ) {
194199 const rule = rules [ j ] ;
195200 if ( rule instanceof CSSStyleRule ) {
196- if ( targetSubtree . querySelector ( rule . selectorText ) ) {
197- exportedRules . add ( rule ) ;
198- }
201+ callback ( rule ) ;
199202 }
200203 }
201204 }
205+ }
206+
207+ /**
208+ * The exported SVG is detached from the workspace, so CSS custom properties
209+ * (the design tokens) declared on the workspace root are not in scope and every
210+ * `var(--…)` reference in the exported rules would resolve to nothing, dropping
211+ * colours, radii and shadows. Re-declare the resolved values on the exported
212+ * root so the export matches what is on the canvas.
213+ */
214+ function extractCSSVariables ( source : Element | null | undefined ) : string {
215+ if ( ! source ) {
216+ return "" ;
217+ }
218+ const computed = getComputedStyle ( source ) ;
219+ const declarations = new Map < string , string > ( ) ;
220+
221+ forEachStyleRule ( ( rule ) => {
222+ for ( let i = 0 ; i < rule . style . length ; i ++ ) {
223+ const property = rule . style . item ( i ) ;
224+ if ( ! property . startsWith ( "--" ) || declarations . has ( property ) ) {
225+ continue ;
226+ }
227+ const value = computed . getPropertyValue ( property ) . trim ( ) ;
228+ if ( value ) {
229+ declarations . set ( property , value ) ;
230+ }
231+ }
232+ } ) ;
233+
234+ if ( declarations . size === 0 ) {
235+ return "" ;
236+ }
237+ const variables = Array . from (
238+ declarations ,
239+ ( [ name , value ] ) => `${ name } : ${ value } ;`
240+ ) . join ( " " ) ;
241+ return `svg { ${ variables } }` ;
242+ }
243+
244+ function extractCSSFromDocument ( targetSubtree : Element ) : string {
245+ const exportedRules = new Set < CSSStyleRule > ( ) ;
246+ forEachStyleRule ( ( rule ) => {
247+ try {
248+ if ( targetSubtree . querySelector ( rule . selectorText ) ) {
249+ exportedRules . add ( rule ) ;
250+ }
251+ } catch ( _e ) {
252+ // selectors that are not valid for querySelector (e.g. pseudo-elements)
253+ // must not abort the whole export
254+ }
255+ } ) ;
202256
203257 const exportedCssTexts : string [ ] = [ ] ;
204258 exportedRules . forEach ( ( rule ) => exportedCssTexts . push ( rule . cssText ) ) ;
0 commit comments