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