@@ -23,12 +23,17 @@ export async function exportToSVG(
2323 svg : SVGSVGElement ,
2424 options : Omit < SVGExportOptions , 'type' > = { } ,
2525) {
26- const { embedResources = true } = options ;
26+ const { embedResources = true , removeIds = false } = options ;
2727 const clonedSVG = svg . cloneNode ( true ) as SVGSVGElement ;
2828 const { width, height } = getViewBox ( svg ) ;
2929 setAttributes ( clonedSVG , { width, height } ) ;
3030
31- await embedIcons ( clonedSVG ) ;
31+ if ( removeIds ) {
32+ inlineUseElements ( clonedSVG ) ;
33+ inlineDefsReferences ( clonedSVG ) ;
34+ } else {
35+ await embedIcons ( clonedSVG ) ;
36+ }
3237 await embedFonts ( clonedSVG , embedResources ) ;
3338
3439 cleanSVG ( clonedSVG ) ;
@@ -62,6 +67,253 @@ function getDefs(svg: SVGSVGElement) {
6267 return _defs ;
6368}
6469
70+ function inlineUseElements ( svg : SVGSVGElement ) {
71+ const uses = Array . from ( svg . querySelectorAll < SVGUseElement > ( 'use' ) ) ;
72+ if ( ! uses . length ) return ;
73+
74+ uses . forEach ( ( use ) => {
75+ const href = getUseHref ( use ) ;
76+ if ( ! href || ! href . startsWith ( '#' ) ) return ;
77+ const target = resolveUseTarget ( svg , href ) ;
78+ if ( ! target || target === use ) return ;
79+
80+ const replacement = createInlineElement ( use , target ) ;
81+ if ( ! replacement ) return ;
82+ use . replaceWith ( replacement ) ;
83+ } ) ;
84+ }
85+
86+ function getUseHref ( use : SVGUseElement ) {
87+ return use . getAttribute ( 'href' ) ?? use . getAttribute ( 'xlink:href' ) ;
88+ }
89+
90+ function resolveUseTarget ( svg : SVGSVGElement , href : string ) {
91+ const localTarget = svg . querySelector ( href ) ;
92+ if ( localTarget ) return localTarget as SVGElement ;
93+ const docTarget = document . querySelector ( href ) ;
94+ return docTarget as SVGElement | null ;
95+ }
96+
97+ function createInlineElement ( use : SVGUseElement , target : SVGElement ) {
98+ const tag = target . tagName . toLowerCase ( ) ;
99+ if ( tag === 'symbol' ) {
100+ return materializeSymbol ( use , target as SVGSymbolElement ) ;
101+ }
102+ if ( tag === 'svg' ) {
103+ return materializeSVG ( use , target as SVGSVGElement ) ;
104+ }
105+ return materializeElement ( use , target ) ;
106+ }
107+
108+ function materializeSymbol ( use : SVGUseElement , symbol : SVGSymbolElement ) {
109+ const symbolClone = symbol . cloneNode ( true ) as SVGSymbolElement ;
110+ const svg = createElement < SVGSVGElement > ( 'svg' ) ;
111+
112+ applyAttributes ( svg , symbolClone , new Set ( [ 'id' ] ) ) ;
113+ applyAttributes ( svg , use , new Set ( [ 'href' , 'xlink:href' ] ) ) ;
114+
115+ while ( symbolClone . firstChild ) {
116+ svg . appendChild ( symbolClone . firstChild ) ;
117+ }
118+
119+ return svg ;
120+ }
121+
122+ function materializeSVG ( use : SVGUseElement , source : SVGSVGElement ) {
123+ const clone = source . cloneNode ( true ) as SVGSVGElement ;
124+ clone . removeAttribute ( 'id' ) ;
125+ applyAttributes ( clone , use , new Set ( [ 'href' , 'xlink:href' ] ) ) ;
126+ return clone ;
127+ }
128+
129+ function materializeElement ( use : SVGUseElement , source : SVGElement ) {
130+ const clone = source . cloneNode ( true ) as SVGElement ;
131+ clone . removeAttribute ( 'id' ) ;
132+
133+ const wrapper = createElement < SVGGElement > ( 'g' ) ;
134+ applyAttributes (
135+ wrapper ,
136+ use ,
137+ new Set ( [ 'href' , 'xlink:href' , 'x' , 'y' , 'width' , 'height' , 'transform' ] ) ,
138+ ) ;
139+
140+ const transform = buildUseTransform ( use ) ;
141+ if ( transform ) {
142+ wrapper . setAttribute ( 'transform' , transform ) ;
143+ }
144+
145+ wrapper . appendChild ( clone ) ;
146+ return wrapper ;
147+ }
148+
149+ function buildUseTransform ( use : SVGUseElement ) {
150+ const x = use . getAttribute ( 'x' ) ;
151+ const y = use . getAttribute ( 'y' ) ;
152+ const translate = x || y ? `translate(${ x ?? 0 } ${ y ?? 0 } )` : '' ;
153+ const transform = use . getAttribute ( 'transform' ) ?? '' ;
154+ if ( translate && transform ) return `${ translate } ${ transform } ` ;
155+ return translate || transform || null ;
156+ }
157+
158+ function applyAttributes (
159+ target : SVGElement ,
160+ source : SVGElement ,
161+ exclude : Set < string > = new Set ( ) ,
162+ ) {
163+ Array . from ( source . attributes ) . forEach ( ( attr ) => {
164+ if ( exclude . has ( attr . name ) ) return ;
165+ if ( attr . name === 'style' ) {
166+ mergeStyleAttribute ( target , attr . value ) ;
167+ return ;
168+ }
169+ if ( attr . name === 'class' ) {
170+ mergeClassAttribute ( target , attr . value ) ;
171+ return ;
172+ }
173+ target . setAttribute ( attr . name , attr . value ) ;
174+ } ) ;
175+ }
176+
177+ function mergeStyleAttribute ( target : SVGElement , value : string ) {
178+ const current = target . getAttribute ( 'style' ) ;
179+ if ( ! current ) {
180+ target . setAttribute ( 'style' , value ) ;
181+ return ;
182+ }
183+ const separator = current . trim ( ) . endsWith ( ';' ) ? '' : ';' ;
184+ target . setAttribute ( 'style' , `${ current } ${ separator } ${ value } ` ) ;
185+ }
186+
187+ function mergeClassAttribute ( target : SVGElement , value : string ) {
188+ const current = target . getAttribute ( 'class' ) ;
189+ if ( ! current ) {
190+ target . setAttribute ( 'class' , value ) ;
191+ return ;
192+ }
193+ target . setAttribute ( 'class' , `${ current } ${ value } ` . trim ( ) ) ;
194+ }
195+
196+ const urlRefRegex = / u r l \( \s * [ ' " ] ? # ( [ ^ ' " ) \s ] + ) [ ' " ] ? \s * \) / g;
197+ function inlineDefsReferences ( svg : SVGSVGElement ) {
198+ const referencedIds = collectReferencedIds ( svg ) ;
199+ if ( referencedIds . size === 0 ) {
200+ removeDefs ( svg ) ;
201+ return ;
202+ }
203+
204+ const defsDataUrl = createDefsDataUrl ( svg , referencedIds ) ;
205+ if ( ! defsDataUrl ) return ;
206+
207+ traverse ( svg , ( node ) => {
208+ if ( node . tagName . toLowerCase ( ) === 'defs' ) return false ;
209+ const attrs = Array . from ( node . attributes ) ;
210+ attrs . forEach ( ( attr ) => {
211+ const value = attr . value ;
212+ if ( ! value . includes ( 'url(' ) ) return ;
213+ const updated = value . replace ( urlRefRegex , ( _match , id ) => {
214+ const encodedId = encodeURIComponent ( id ) ;
215+ return `url("${ defsDataUrl } #${ encodedId } ")` ;
216+ } ) ;
217+ if ( updated !== value ) node . setAttribute ( attr . name , updated ) ;
218+ } ) ;
219+ } ) ;
220+
221+ removeDefs ( svg ) ;
222+ }
223+
224+ function collectReferencedIds ( svg : SVGSVGElement ) {
225+ const ids = new Set < string > ( ) ;
226+ traverse ( svg , ( node ) => {
227+ if ( node . tagName . toLowerCase ( ) === 'defs' ) return false ;
228+ collectIdsFromAttributes ( node , ( id ) => ids . add ( id ) ) ;
229+ } ) ;
230+ return ids ;
231+ }
232+
233+ function collectIdsFromAttributes (
234+ node : SVGElement ,
235+ addId : ( id : string ) => void ,
236+ ) {
237+ for ( const attr of Array . from ( node . attributes ) ) {
238+ const value = attr . value ;
239+ if ( value . includes ( 'url(' ) ) {
240+ for ( const match of value . matchAll ( urlRefRegex ) ) {
241+ if ( match [ 1 ] ) addId ( match [ 1 ] ) ;
242+ }
243+ }
244+ if (
245+ ( attr . name === 'href' || attr . name === 'xlink:href' ) &&
246+ value [ 0 ] === '#'
247+ ) {
248+ addId ( value . slice ( 1 ) ) ;
249+ }
250+ }
251+ }
252+
253+ function createDefsDataUrl ( svg : SVGSVGElement , ids : Set < string > ) {
254+ if ( ids . size === 0 ) return null ;
255+
256+ const collected = collectDefElements ( svg , ids ) ;
257+ if ( collected . size === 0 ) return null ;
258+
259+ const defsSvg = createElement < SVGSVGElement > ( 'svg' , {
260+ xmlns : 'http://www.w3.org/2000/svg' ,
261+ 'xmlns:xlink' : 'http://www.w3.org/1999/xlink' ,
262+ } ) ;
263+ const defs = createElement < SVGDefsElement > ( 'defs' ) ;
264+
265+ collected . forEach ( ( node ) => {
266+ defs . appendChild ( node . cloneNode ( true ) ) ;
267+ } ) ;
268+
269+ if ( ! defs . children . length ) return null ;
270+ defsSvg . appendChild ( defs ) ;
271+
272+ const serialized = new XMLSerializer ( ) . serializeToString ( defsSvg ) ;
273+ return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent ( serialized ) ;
274+ }
275+
276+ function collectDefElements ( svg : SVGSVGElement , ids : Set < string > ) {
277+ const collected = new Map < string , SVGElement > ( ) ;
278+ const queue = Array . from ( ids ) ;
279+ const queued = new Set ( queue ) ;
280+ const visited = new Set < string > ( ) ;
281+ const enqueue = ( id : string ) => {
282+ if ( visited . has ( id ) || queued . has ( id ) ) return ;
283+ queue . push ( id ) ;
284+ queued . add ( id ) ;
285+ } ;
286+
287+ while ( queue . length ) {
288+ const id = queue . shift ( ) ! ;
289+ if ( visited . has ( id ) ) continue ;
290+ visited . add ( id ) ;
291+
292+ const selector = `#${ escapeCssId ( id ) } ` ;
293+ const target = svg . querySelector ( selector ) ;
294+ if ( ! target ) continue ;
295+ collected . set ( id , target as SVGElement ) ;
296+
297+ traverse ( target as SVGElement , ( node ) => {
298+ collectIdsFromAttributes ( node , enqueue ) ;
299+ } ) ;
300+ }
301+
302+ return collected ;
303+ }
304+
305+ function escapeCssId ( id : string ) {
306+ if ( globalThis . CSS && typeof globalThis . CSS . escape === 'function' ) {
307+ return globalThis . CSS . escape ( id ) ;
308+ }
309+ return id . replace ( / ( [ ! " # $ % & ' ( ) * + , . / : ; < = > ? @ [ \] ^ ` { | } ~ ] ) / g, '\\$1' ) ;
310+ }
311+
312+ function removeDefs ( svg : SVGSVGElement ) {
313+ const defsList = Array . from ( svg . querySelectorAll ( 'defs' ) ) ;
314+ defsList . forEach ( ( defs ) => defs . remove ( ) ) ;
315+ }
316+
65317function cleanSVG ( svg : SVGSVGElement ) {
66318 removeBtnGroup ( svg ) ;
67319 removeTransientContainer ( svg ) ;
0 commit comments