Files
rspade_system/node_modules/svgo/plugins/cleanupIds.js
root d523f0f600 Fix code quality violations and exclude Manifest from checks
Document application modes (development/debug/production)
Add global file drop handler, order column normalization, SPA hash fix
Serve CDN assets via /_vendor/ URLs instead of merging into bundles
Add production minification with license preservation
Improve JSON formatting for debugging and production optimization
Add CDN asset caching with CSS URL inlining for production builds
Add three-mode system (development, debug, production)
Update Manifest CLAUDE.md to reflect helper class architecture
Refactor Manifest.php into helper classes for better organization
Pre-manifest-refactor checkpoint: Add app_mode documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 10:38:22 +00:00

267 lines
6.2 KiB
JavaScript

import { visitSkip } from '../lib/util/visit.js';
import { findReferences, hasScripts } from '../lib/svgo/tools.js';
/**
* @typedef CleanupIdsParams
* @property {boolean=} remove
* @property {boolean=} minify
* @property {string[]=} preserve
* @property {string[]=} preservePrefixes
* @property {boolean=} force
*/
export const name = 'cleanupIds';
export const description = 'removes unused IDs and minifies used';
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.
*
* @param {string} string
* @param {ReadonlyArray<string>} prefixes
* @returns {boolean}
*/
const hasStringPrefix = (string, prefixes) => {
for (const prefix of prefixes) {
if (string.startsWith(prefix)) {
return true;
}
}
return false;
};
/**
* Generate unique minimal ID.
*
* @param {?number[]} currentId
* @returns {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.
*
* @param {ReadonlyArray<number>} arr
* @returns {string}
*/
const getIdString = (arr) => {
return arr.map((i) => generateIdChars[i]).join('');
};
/**
* Remove unused and minify used IDs (only if there are no `<style>` or
* `<script>` nodes).
*
* @author Kir Belevich
*
* @type {import('../lib/types.js').Plugin<CleanupIdsParams>}
*/
export const 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, import('../lib/types.js').XastElement>} */
const nodeById = new Map();
/** @type {Map<string, {element: import('../lib/types.js').XastElement, name: string }[]>} */
const referencesById = new Map();
let deoptimized = false;
return {
element: {
enter: (node) => {
if (!force) {
// deoptimize if style or scripts are present
if (
(node.name === 'style' && node.children.length !== 0) ||
hasScripts(node)
) {
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 {
const ids = findReferences(name, value);
for (const id of ids) {
let refs = referencesById.get(id);
if (refs == null) {
refs = [];
referencesById.set(id, refs);
}
refs.push({ element: node, name });
}
}
}
},
},
root: {
exit: () => {
if (deoptimized) {
return;
}
/**
* @param {string} id
* @returns {boolean}
*/
const isIdPreserved = (id) =>
preserveIds.has(id) || hasStringPrefix(id, preserveIdPrefixes);
/** @type {?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 {?string} */
let currentIdString;
do {
currentId = generateId(currentId);
currentIdString = getIdString(currentId);
} while (
isIdPreserved(currentIdString) ||
(referencesById.has(currentIdString) &&
nodeById.get(currentIdString) == null)
);
node.attributes.id = currentIdString;
for (const { element, name } of refs) {
const value = element.attributes[name];
if (value.includes('#')) {
// replace id in href and url()
element.attributes[name] = value
.replace(`#${encodeURI(id)}`, `#${currentIdString}`)
.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;
}
}
}
},
},
};
};