summaryrefslogtreecommitdiff
path: root/node_modules/svgo/plugins/cleanupIds.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/svgo/plugins/cleanupIds.js')
-rw-r--r--node_modules/svgo/plugins/cleanupIds.js287
1 files changed, 287 insertions, 0 deletions
diff --git a/node_modules/svgo/plugins/cleanupIds.js b/node_modules/svgo/plugins/cleanupIds.js
new file mode 100644
index 00000000..1d2532b2
--- /dev/null
+++ b/node_modules/svgo/plugins/cleanupIds.js
@@ -0,0 +1,287 @@
+'use strict';
+
+/**
+ * @typedef {import('../lib/types').XastElement} XastElement
+ */
+
+const { visitSkip } = require('../lib/xast.js');
+const { referencesProps } = require('./_collections.js');
+
+exports.name = 'cleanupIds';
+exports.description = 'removes unused IDs and minifies used';
+
+const regReferencesUrl = /\burl\((["'])?#(.+?)\1\)/;
+const regReferencesHref = /^#(.+?)$/;
+const regReferencesBegin = /(\D+)\./;
+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>) => string}
+ */
+const getIdString = (arr) => {
+ return 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('./plugins-types').Plugin<'cleanupIds'>}
+ */
+exports.fn = (_root, params) => {
+ const {
+ remove = true,
+ minify = true,
+ 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);
+ } 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;
+ }
+ }
+ }
+ },
+ },
+ };
+};