@@ -15,6 +15,7 @@ const {
1515} = require ( '../lib/xast.js' ) ;
1616const { collectStylesheet, computeStyle } = require ( '../lib/style.js' ) ;
1717const { parsePathData } = require ( '../lib/path.js' ) ;
18+ const { hasScripts } = require ( '../lib/svgo/tools.js' ) ;
1819
1920const nonRendering = elemsGroups . nonRendering ;
2021
@@ -74,6 +75,21 @@ exports.fn = (root, params) => {
7475 */
7576 const removedDefIds = new Set ( ) ;
7677
78+ /**
79+ * @type {Map<XastElement, XastParent> }
80+ */
81+ const allDefs = new Map ( ) ;
82+
83+ /**
84+ * @type {Map<string, Array<{ node: XastElement, parentNode: XastParent }>> }
85+ */
86+ const referencesById = new Map ( ) ;
87+
88+ /**
89+ * If styles are present, we can't be sure if a definition is unused or not
90+ */
91+ let deoptimized = false ;
92+
7793 /**
7894 * @param {XastChild } node
7995 * @param {XastParent } parentNode
@@ -123,6 +139,33 @@ exports.fn = (root, params) => {
123139 return {
124140 element : {
125141 enter : ( node , parentNode ) => {
142+ if (
143+ ( node . name === 'style' && node . children . length !== 0 ) ||
144+ hasScripts ( node )
145+ ) {
146+ deoptimized = true ;
147+ return ;
148+ }
149+
150+ if ( node . name === 'defs' ) {
151+ allDefs . set ( node , parentNode ) ;
152+ }
153+
154+ if ( node . name === 'use' ) {
155+ for ( const attr of Object . keys ( node . attributes ) ) {
156+ if ( attr !== 'href' && ! attr . endsWith ( ':href' ) ) continue ;
157+ const value = node . attributes [ attr ] ;
158+ const id = value . slice ( 1 ) ;
159+
160+ let refs = referencesById . get ( id ) ;
161+ if ( ! refs ) {
162+ refs = [ ] ;
163+ referencesById . set ( id , refs ) ;
164+ }
165+ refs . push ( { node, parentNode } ) ;
166+ }
167+ }
168+
126169 // Removes hidden elements
127170 // https://www.w3schools.com/cssref/pr_class_visibility.asp
128171 const computedStyle = computeStyle ( stylesheet , node ) ;
@@ -350,46 +393,41 @@ exports.fn = (root, params) => {
350393 removeElement ( node , parentNode ) ;
351394 }
352395 } ,
353-
354- exit : ( node , parentNode ) => {
355- if ( node . name === 'defs' && node . children . length === 0 ) {
356- removeElement ( node , parentNode ) ;
357- return ;
358- }
359-
360- if ( node . name === 'use' ) {
361- const referencesRemovedDef = Object . entries ( node . attributes ) . some (
362- ( [ attrKey , attrValue ] ) =>
363- ( attrKey === 'href' || attrKey . endsWith ( ':href' ) ) &&
364- removedDefIds . has (
365- attrValue . slice ( attrValue . indexOf ( '#' ) + 1 ) . trim ( )
366- )
367- ) ;
368-
369- if ( referencesRemovedDef ) {
370- detachNodeFromParent ( node , parentNode ) ;
396+ } ,
397+ root : {
398+ exit : ( ) => {
399+ for ( const id of removedDefIds ) {
400+ const refs = referencesById . get ( id ) ;
401+ if ( refs ) {
402+ for ( const { node, parentNode } of refs ) {
403+ detachNodeFromParent ( node , parentNode ) ;
404+ }
371405 }
372-
373- return ;
374406 }
375407
376- if ( node . name === 'svg' && parentNode . type === 'root' ) {
408+ if ( ! deoptimized ) {
377409 for ( const [
378410 nonRenderedNode ,
379411 nonRenderedParent ,
380412 ] of nonRenderedNodes . entries ( ) ) {
413+ const id = nonRenderedNode . attributes . id ;
381414 const selector = referencesProps
382- . map (
383- ( attr ) => `[${ attr } ="url(#${ nonRenderedNode . attributes . id } )"]`
384- )
415+ . map ( ( attr ) => `[${ attr } ="url(#${ id } )"]` )
416+ . concat ( `[href="#${ id } "]` , `[xlink\\:href="#${ id } "]` )
385417 . join ( ',' ) ;
386418
387419 const element = querySelector ( root , selector ) ;
388420 if ( element == null ) {
389- detachNodeFromParent ( node , nonRenderedParent ) ;
421+ detachNodeFromParent ( nonRenderedNode , nonRenderedParent ) ;
390422 }
391423 }
392424 }
425+
426+ for ( const [ node , parentNode ] of allDefs . entries ( ) ) {
427+ if ( node . children . length === 0 ) {
428+ detachNodeFromParent ( node , parentNode ) ;
429+ }
430+ }
393431 } ,
394432 } ,
395433 } ;
0 commit comments