| 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;            }          }        }      },    },  };};
 |