| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 | 
							- 'use strict';
 
- /**
 
-  * @typedef {import('../lib/types').XastElement} XastElement
 
-  */
 
- const { visitSkip } = require('../lib/xast.js');
 
- const { referencesProps } = require('./_collections.js');
 
- exports.type = 'visitor';
 
- exports.name = 'cleanupIDs';
 
- exports.active = true;
 
- exports.description = 'removes unused IDs and minifies used';
 
- const regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/;
 
- const regReferencesHref = /^#(.+?)$/;
 
- const regReferencesBegin = /(\w+)\./;
 
- const generateIDchars = [
 
-   'a',
 
-   'b',
 
-   'c',
 
-   'd',
 
-   'e',
 
-   'f',
 
-   'g',
 
-   'h',
 
-   'i',
 
-   'j',
 
-   'k',
 
-   'l',
 
-   'm',
 
-   'n',
 
-   'o',
 
-   'p',
 
-   'q',
 
-   'r',
 
-   's',
 
-   't',
 
-   'u',
 
-   'v',
 
-   'w',
 
-   'x',
 
-   'y',
 
-   'z',
 
-   'A',
 
-   'B',
 
-   'C',
 
-   'D',
 
-   'E',
 
-   'F',
 
-   'G',
 
-   'H',
 
-   'I',
 
-   'J',
 
-   'K',
 
-   'L',
 
-   'M',
 
-   'N',
 
-   'O',
 
-   'P',
 
-   'Q',
 
-   'R',
 
-   'S',
 
-   'T',
 
-   'U',
 
-   'V',
 
-   'W',
 
-   'X',
 
-   'Y',
 
-   'Z',
 
- ];
 
- const maxIDindex = generateIDchars.length - 1;
 
- /**
 
-  * Check if an ID starts with any one of a list of strings.
 
-  *
 
-  * @type {(string: string, prefixes: Array<string>) => boolean}
 
-  */
 
- const hasStringPrefix = (string, prefixes) => {
 
-   for (const prefix of prefixes) {
 
-     if (string.startsWith(prefix)) {
 
-       return true;
 
-     }
 
-   }
 
-   return false;
 
- };
 
- /**
 
-  * Generate unique minimal ID.
 
-  *
 
-  * @type {(currentID: null | Array<number>) => Array<number>}
 
-  */
 
- const generateID = (currentID) => {
 
-   if (currentID == null) {
 
-     return [0];
 
-   }
 
-   currentID[currentID.length - 1] += 1;
 
-   for (let i = currentID.length - 1; i > 0; i--) {
 
-     if (currentID[i] > maxIDindex) {
 
-       currentID[i] = 0;
 
-       if (currentID[i - 1] !== undefined) {
 
-         currentID[i - 1]++;
 
-       }
 
-     }
 
-   }
 
-   if (currentID[0] > maxIDindex) {
 
-     currentID[0] = 0;
 
-     currentID.unshift(0);
 
-   }
 
-   return currentID;
 
- };
 
- /**
 
-  * Get string from generated ID array.
 
-  *
 
-  * @type {(arr: Array<number>, prefix: string) => string}
 
-  */
 
- const getIDstring = (arr, prefix) => {
 
-   return prefix + arr.map((i) => generateIDchars[i]).join('');
 
- };
 
- /**
 
-  * Remove unused and minify used IDs
 
-  * (only if there are no any <style> or <script>).
 
-  *
 
-  * @author Kir Belevich
 
-  *
 
-  * @type {import('../lib/types').Plugin<{
 
-  *   remove?: boolean,
 
-  *   minify?: boolean,
 
-  *   prefix?: string,
 
-  *   preserve?: Array<string>,
 
-  *   preservePrefixes?: Array<string>,
 
-  *   force?: boolean,
 
-  * }>}
 
-  */
 
- exports.fn = (_root, params) => {
 
-   const {
 
-     remove = true,
 
-     minify = true,
 
-     prefix = '',
 
-     preserve = [],
 
-     preservePrefixes = [],
 
-     force = false,
 
-   } = params;
 
-   const preserveIDs = new Set(
 
-     Array.isArray(preserve) ? preserve : preserve ? [preserve] : []
 
-   );
 
-   const preserveIDPrefixes = Array.isArray(preservePrefixes)
 
-     ? preservePrefixes
 
-     : preservePrefixes
 
-     ? [preservePrefixes]
 
-     : [];
 
-   /**
 
-    * @type {Map<string, XastElement>}
 
-    */
 
-   const nodeById = new Map();
 
-   /**
 
-    * @type {Map<string, Array<{element: XastElement, name: string, value: string }>>}
 
-    */
 
-   const referencesById = new Map();
 
-   let deoptimized = false;
 
-   return {
 
-     element: {
 
-       enter: (node) => {
 
-         if (force == false) {
 
-           // deoptimize if style or script elements are present
 
-           if (
 
-             (node.name === 'style' || node.name === 'script') &&
 
-             node.children.length !== 0
 
-           ) {
 
-             deoptimized = true;
 
-             return;
 
-           }
 
-           // avoid removing IDs if the whole SVG consists only of defs
 
-           if (node.name === 'svg') {
 
-             let hasDefsOnly = true;
 
-             for (const child of node.children) {
 
-               if (child.type !== 'element' || child.name !== 'defs') {
 
-                 hasDefsOnly = false;
 
-                 break;
 
-               }
 
-             }
 
-             if (hasDefsOnly) {
 
-               return visitSkip;
 
-             }
 
-           }
 
-         }
 
-         for (const [name, value] of Object.entries(node.attributes)) {
 
-           if (name === 'id') {
 
-             // collect all ids
 
-             const id = value;
 
-             if (nodeById.has(id)) {
 
-               delete node.attributes.id; // remove repeated id
 
-             } else {
 
-               nodeById.set(id, node);
 
-             }
 
-           } else {
 
-             // collect all references
 
-             /**
 
-              * @type {null | string}
 
-              */
 
-             let id = null;
 
-             if (referencesProps.includes(name)) {
 
-               const match = value.match(regReferencesUrl);
 
-               if (match != null) {
 
-                 id = match[2]; // url() reference
 
-               }
 
-             }
 
-             if (name === 'href' || name.endsWith(':href')) {
 
-               const match = value.match(regReferencesHref);
 
-               if (match != null) {
 
-                 id = match[1]; // href reference
 
-               }
 
-             }
 
-             if (name === 'begin') {
 
-               const match = value.match(regReferencesBegin);
 
-               if (match != null) {
 
-                 id = match[1]; // href reference
 
-               }
 
-             }
 
-             if (id != null) {
 
-               let refs = referencesById.get(id);
 
-               if (refs == null) {
 
-                 refs = [];
 
-                 referencesById.set(id, refs);
 
-               }
 
-               refs.push({ element: node, name, value });
 
-             }
 
-           }
 
-         }
 
-       },
 
-     },
 
-     root: {
 
-       exit: () => {
 
-         if (deoptimized) {
 
-           return;
 
-         }
 
-         /**
 
-          * @type {(id: string) => boolean}
 
-          **/
 
-         const isIdPreserved = (id) =>
 
-           preserveIDs.has(id) || hasStringPrefix(id, preserveIDPrefixes);
 
-         /**
 
-          * @type {null | Array<number>}
 
-          */
 
-         let currentID = null;
 
-         for (const [id, refs] of referencesById) {
 
-           const node = nodeById.get(id);
 
-           if (node != null) {
 
-             // replace referenced IDs with the minified ones
 
-             if (minify && isIdPreserved(id) === false) {
 
-               /**
 
-                * @type {null | string}
 
-                */
 
-               let currentIDString = null;
 
-               do {
 
-                 currentID = generateID(currentID);
 
-                 currentIDString = getIDstring(currentID, prefix);
 
-               } while (isIdPreserved(currentIDString));
 
-               node.attributes.id = currentIDString;
 
-               for (const { element, name, value } of refs) {
 
-                 if (value.includes('#')) {
 
-                   // replace id in href and url()
 
-                   element.attributes[name] = value.replace(
 
-                     `#${id}`,
 
-                     `#${currentIDString}`
 
-                   );
 
-                 } else {
 
-                   // replace id in begin attribute
 
-                   element.attributes[name] = value.replace(
 
-                     `${id}.`,
 
-                     `${currentIDString}.`
 
-                   );
 
-                 }
 
-               }
 
-             }
 
-             // keep referenced node
 
-             nodeById.delete(id);
 
-           }
 
-         }
 
-         // remove non-referenced IDs attributes from elements
 
-         if (remove) {
 
-           for (const [id, node] of nodeById) {
 
-             if (isIdPreserved(id) === false) {
 
-               delete node.attributes.id;
 
-             }
 
-           }
 
-         }
 
-       },
 
-     },
 
-   };
 
- };
 
 
  |