diff --git a/lib/svgo.test.js b/lib/svgo.test.js index e23d29259..21be404c4 100644 --- a/lib/svgo.test.js +++ b/lib/svgo.test.js @@ -1,5 +1,9 @@ 'use strict'; +/** + * @typedef {import('../lib/types').Plugin} Plugin + */ + const { optimize } = require('./svgo.js'); test('allow to setup default preset', () => { @@ -324,3 +328,28 @@ test('provides legacy error message', () => { 4 | `); }); + +test('multipass option should trigger plugins multiple times', () => { + const svg = ``; + const list = []; + /** + * @type {Plugin} + */ + const testPlugin = { + type: 'visitor', + name: 'testPlugin', + fn: (_root, _params, info) => { + list.push(info.multipassCount); + return { + element: { + enter: (node) => { + node.attributes.id = node.attributes.id.slice(1); + }, + }, + }; + }, + }; + const { data } = optimize(svg, { multipass: true, plugins: [testPlugin] }); + expect(list).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(data).toEqual(``); +}); diff --git a/lib/types.ts b/lib/types.ts index 396bd790b..f260a283f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -71,7 +71,16 @@ export type Visitor = { root?: VisitorRoot; }; -export type Plugin = (root: XastRoot, params: Params) => null | Visitor; +export type PluginInfo = { + path?: string; + multipassCount: number; +}; + +export type Plugin = ( + root: XastRoot, + params: Params, + info: PluginInfo +) => null | Visitor; export type Specificity = [number, number, number, number]; diff --git a/plugins/prefixIds.js b/plugins/prefixIds.js index b0c884f68..4de5e837f 100644 --- a/plugins/prefixIds.js +++ b/plugins/prefixIds.js @@ -1,300 +1,240 @@ 'use strict'; -exports.name = 'prefixIds'; +const csstree = require('css-tree'); +const { referencesProps } = require('./_collections.js'); -exports.type = 'perItem'; +/** + * @typedef {import('../lib/types').XastElement} XastElement + * @typedef {import('../lib/types').PluginInfo} PluginInfo + */ +exports.type = 'visitor'; +exports.name = 'prefixIds'; exports.active = false; - -exports.params = { - delim: '__', - prefixIds: true, - prefixClassNames: true, -}; - exports.description = 'prefix IDs'; -var csstree = require('css-tree'), - collections = require('./_collections.js'), - referencesProps = collections.referencesProps, - rxId = /^#(.*)$/, // regular expression for matching an ID + extracing its name - addPrefix = null; - -const unquote = (string) => { - const first = string.charAt(0); - if (first === "'" || first === '"') { - if (first === string.charAt(string.length - 1)) { - return string.slice(1, -1); - } +/** + * extract basename from path + * @type {(path: string) => string} + */ +const getBasename = (path) => { + // extract everything after latest slash or backslash + const matched = path.match(/[/\\]?([^/\\]+)$/); + if (matched) { + return matched[1]; } - return string; + return ''; }; -// Escapes a string for being used as ID -var escapeIdentifierName = function (str) { +/** + * escapes a string for being used as ID + * @type {(string: string) => string} + */ +const escapeIdentifierName = (str) => { return str.replace(/[. ]/g, '_'); }; -// Matches an #ID value, captures the ID name -var matchId = function (urlVal) { - var idUrlMatches = urlVal.match(rxId); - if (idUrlMatches === null) { - return false; - } - return idUrlMatches[1]; -}; - -// Matches an url(...) value, captures the URL -var matchUrl = function (val) { - var urlMatches = /url\((.*?)\)/gi.exec(val); - if (urlMatches === null) { - return false; - } - return urlMatches[1]; -}; - -// prefixes an #ID -var prefixId = function (val) { - var idName = matchId(val); - if (!idName) { - return false; - } - return '#' + addPrefix(idName); -}; - -// prefixes a class attribute value -const addPrefixToClassAttr = (element, name) => { - if ( - element.attributes[name] == null || - element.attributes[name].length === 0 - ) { - return; - } - - element.attributes[name] = element.attributes[name] - .split(/\s+/) - .map(addPrefix) - .join(' '); -}; - -// prefixes an ID attribute value -const addPrefixToIdAttr = (element, name) => { - if ( - element.attributes[name] == null || - element.attributes[name].length === 0 - ) { - return; - } - - element.attributes[name] = addPrefix(element.attributes[name]); -}; - -// prefixes a href attribute value -const addPrefixToHrefAttr = (element, name) => { - if ( - element.attributes[name] == null || - element.attributes[name].length === 0 - ) { - return; - } - - const idPrefixed = prefixId(element.attributes[name]); - if (!idPrefixed) { - return; - } - element.attributes[name] = idPrefixed; -}; - -// prefixes an URL attribute value -const addPrefixToUrlAttr = (element, name) => { +/** + * @type {(string: string) => string} + */ +const unquote = (string) => { if ( - element.attributes[name] == null || - element.attributes[name].length === 0 + (string.startsWith('"') && string.endsWith('"')) || + (string.startsWith("'") && string.endsWith("'")) ) { - return; - } - - // url(...) in value - const urlVal = matchUrl(element.attributes[name]); - if (!urlVal) { - return; + return string.slice(1, -1); } - - const idPrefixed = prefixId(urlVal); - if (!idPrefixed) { - return; - } - - element.attributes[name] = 'url(' + idPrefixed + ')'; + return string; }; -// prefixes begin/end attribute value -const addPrefixToBeginEndAttr = (element, name) => { - if ( - element.attributes[name] == null || - element.attributes[name].length === 0 - ) { - return; +/** + * prefix an ID + * @type {(prefix: string, name: string) => string} + */ +const prefixId = (prefix, value) => { + if (value.startsWith(prefix)) { + return value; } - - const parts = element.attributes[name].split('; ').map((val) => { - val = val.trim(); - - if (val.endsWith('.end') || val.endsWith('.start')) { - const [id, postfix] = val.split('.'); - - let idPrefixed = prefixId(`#${id}`); - - if (!idPrefixed) { - return val; - } - - idPrefixed = idPrefixed.slice(1); - return `${idPrefixed}.${postfix}`; - } else { - return val; - } - }); - - element.attributes[name] = parts.join('; '); + return prefix + value; }; -const getBasename = (path) => { - // extract everything after latest slash or backslash - const matched = path.match(/[/\\]([^/\\]+)$/); - if (matched) { - return matched[1]; +/** + * prefix an #ID + * @type {(prefix: string, name: string) => string | null} + */ +const prefixReference = (prefix, value) => { + if (value.startsWith('#')) { + return '#' + prefixId(prefix, value.slice(1)); } - return ''; + return null; }; /** * Prefixes identifiers * - * @param {Object} node node - * @param {Object} opts plugin params - * @param {Object} extra plugin extra information - * * @author strarsis + * + * @type {import('../lib/types').Plugin<{ + * prefix?: boolean | string | ((node: XastElement, info: PluginInfo) => string), + * delim?: string, + * prefixIds?: boolean, + * prefixClassNames?: boolean, + * }>} */ -exports.fn = function (node, opts, extra) { - // skip subsequent passes when multipass is used - if (extra.multipassCount && extra.multipassCount > 0) { - return; - } - - // prefix, from file name or option - var prefix = 'prefix'; - if (opts.prefix) { - if (typeof opts.prefix === 'function') { - prefix = opts.prefix(node, extra); - } else { - prefix = opts.prefix; - } - } else if (opts.prefix === false) { - prefix = false; - } else if (extra && extra.path && extra.path.length > 0) { - var filename = getBasename(extra.path); - prefix = filename; - } - - // prefixes a normal value - addPrefix = function (name) { - if (prefix === false) { - return escapeIdentifierName(name); - } - return escapeIdentifierName(prefix + opts.delim + name); - }; - - //