UNPKG

svgo

Version:

Nodejs-based tool for optimizing SVG vector graphics files

269 lines (240 loc) 7.79 kB
'use strict'; /** * @typedef {import('../lib/types.js').PluginInfo} PluginInfo * @typedef {import('../lib/types').XastElement} XastElement */ const csstree = require('css-tree'); const { referencesProps } = require('./_collections.js'); exports.name = 'prefixIds'; exports.description = 'prefix IDs'; /** * extract basename from path * @type {(path: string) => string} */ const getBasename = (path) => { // extract everything after latest slash or backslash const matched = /[/\\]?([^/\\]+)$/.exec(path); if (matched) { return matched[1]; } return ''; }; /** * escapes a string for being used as ID * @type {(string: string) => string} */ const escapeIdentifierName = (str) => { return str.replace(/[. ]/g, '_'); }; /** * @type {(string: string) => string} */ const unquote = (string) => { if ( (string.startsWith('"') && string.endsWith('"')) || (string.startsWith("'") && string.endsWith("'")) ) { return string.slice(1, -1); } return string; }; /** * Prefix the given string, unless it already starts with the generated prefix. * * @param {(id: string) => string} prefixGenerator Function to generate a prefix. * @param {string} body An arbitrary string. * @returns {string} The given string with a prefix prepended to it. */ const prefixId = (prefixGenerator, body) => { const prefix = prefixGenerator(body); if (body.startsWith(prefix)) { return body; } return prefix + body; }; /** * Insert the prefix in a reference string. A reference string is already * prefixed with #, so the prefix is inserted after the first character. * * @param {(id: string) => string} prefixGenerator Function to generate a prefix. * @param {string} reference An arbitrary string, should start with "#". * @returns {?string} The given string with a prefix inserted, or null if the string did not start with "#". */ const prefixReference = (prefixGenerator, reference) => { if (reference.startsWith('#')) { return '#' + prefixId(prefixGenerator, reference.slice(1)); } return null; }; /** * Generates a prefix for the given string. * * @param {string} body An arbitrary string. * @param {XastElement} node XML node that the identifier belongs to. * @param {PluginInfo} info * @param {((node: XastElement, info: PluginInfo) => string)|string|boolean|undefined} prefixGenerator Some way of obtaining a prefix. * @param {string} delim Content to insert between the prefix and original value. * @param {Map<string, string>} history Map of previously generated prefixes to IDs. * @returns {string} A generated prefix. */ const generatePrefix = (body, node, info, prefixGenerator, delim, history) => { if (typeof prefixGenerator === 'function') { let prefix = history.get(body); if (prefix != null) { return prefix; } prefix = prefixGenerator(node, info) + delim; history.set(body, prefix); return prefix; } if (typeof prefixGenerator === 'string') { return prefixGenerator + delim; } if (prefixGenerator === false) { return ''; } if (info.path != null && info.path.length > 0) { return escapeIdentifierName(getBasename(info.path)) + delim; } return 'prefix' + delim; }; /** * Prefixes identifiers * * @author strarsis <strarsis@gmail.com> * @type {import('./plugins-types').Plugin<'prefixIds'>} */ exports.fn = (_root, params, info) => { const { delim = '__', prefix, prefixIds = true, prefixClassNames = true, } = params; /** @type {Map<string, string>} */ const prefixMap = new Map(); return { element: { enter: (node) => { /** * @param {string} id A node identifier or class. * @returns {string} Given string with a prefix inserted, or null if the string did not start with "#". */ const prefixGenerator = (id) => generatePrefix(id, node, info, prefix, delim, prefixMap); // prefix id/class selectors and url() references in styles if (node.name === 'style') { // skip empty <style/> elements if (node.children.length === 0) { return; } for (const child of node.children) { if (child.type !== 'text' && child.type !== 'cdata') { continue; } const cssText = child.value; /** @type {?csstree.CssNode} */ let cssAst = null; try { cssAst = csstree.parse(cssText, { parseValue: true, parseCustomProperty: false, }); } catch { return; } csstree.walk(cssAst, (node) => { if ( (prefixIds && node.type === 'IdSelector') || (prefixClassNames && node.type === 'ClassSelector') ) { node.name = prefixId(prefixGenerator, node.name); return; } if (node.type === 'Url' && node.value.length > 0) { const prefixed = prefixReference( prefixGenerator, unquote(node.value), ); if (prefixed != null) { node.value = prefixed; } } }); child.value = csstree.generate(cssAst); return; } } // prefix an ID attribute value if ( prefixIds && node.attributes.id != null && node.attributes.id.length !== 0 ) { node.attributes.id = prefixId(prefixGenerator, node.attributes.id); } // prefix a class attribute value if ( prefixClassNames && node.attributes.class != null && node.attributes.class.length !== 0 ) { node.attributes.class = node.attributes.class .split(/\s+/) .map((name) => prefixId(prefixGenerator, name)) .join(' '); } // prefix a href attribute value // xlink:href is deprecated, must be still supported for (const name of ['href', 'xlink:href']) { if ( node.attributes[name] != null && node.attributes[name].length !== 0 ) { const prefixed = prefixReference( prefixGenerator, node.attributes[name], ); if (prefixed != null) { node.attributes[name] = prefixed; } } } // prefix a URL attribute value for (const name of referencesProps) { if ( node.attributes[name] != null && node.attributes[name].length !== 0 ) { node.attributes[name] = node.attributes[name].replace( /\burl\((["'])?(#.+?)\1\)/gi, (match, _, url) => { const prefixed = prefixReference(prefixGenerator, url); if (prefixed == null) { return match; } return `url(${prefixed})`; }, ); } } // prefix begin/end attribute value for (const name of ['begin', 'end']) { if ( node.attributes[name] != null && node.attributes[name].length !== 0 ) { const parts = node.attributes[name].split(/\s*;\s+/).map((val) => { if (val.endsWith('.end') || val.endsWith('.start')) { const [id, postfix] = val.split('.'); return `${prefixId(prefixGenerator, id)}.${postfix}`; } return val; }); node.attributes[name] = parts.join('; '); } } }, }, }; };