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);
- };
-
- // property values
-
- if (node.type === 'element' && node.name === 'style') {
- if (node.children.length === 0) {
- // skip empty s
- return;
- }
-
- var cssStr = '';
- if (node.children[0].type === 'text' || node.children[0].type === 'cdata') {
- cssStr = node.children[0].value;
- }
-
- var cssAst = {};
- try {
- cssAst = csstree.parse(cssStr, {
- parseValue: true,
- parseCustomProperty: false,
- });
- } catch (parseError) {
- console.warn(
- 'Warning: Parse error of styles of element, skipped. Error details: ' +
- parseError
- );
- return;
- }
-
- var idPrefixed = '';
- csstree.walk(cssAst, function (node) {
- // #ID, .class
- if (
- ((opts.prefixIds && node.type === 'IdSelector') ||
- (opts.prefixClassNames && node.type === 'ClassSelector')) &&
- node.name
- ) {
- node.name = addPrefix(node.name);
- return;
- }
+exports.fn = (_root, params, info) => {
+ const { delim = '__', prefixIds = true, prefixClassNames = true } = params;
+
+ return {
+ element: {
+ enter: (node) => {
+ /**
+ * prefix, from file name or option
+ * @type {string}
+ */
+ let prefix = 'prefix' + delim;
+ if (typeof params.prefix === 'function') {
+ prefix = params.prefix(node, info) + delim;
+ } else if (typeof params.prefix === 'string') {
+ prefix = params.prefix + delim;
+ } else if (params.prefix === false) {
+ prefix = '';
+ } else if (info.path != null && info.path.length > 0) {
+ prefix = escapeIdentifierName(getBasename(info.path)) + delim;
+ }
- // url(...) in value
- if (
- node.type === 'Url' &&
- node.value.value &&
- node.value.value.length > 0
- ) {
- idPrefixed = prefixId(unquote(node.value.value));
- if (!idPrefixed) {
+ // prefix id/class selectors and url() references in styles
+ if (node.name === 'style') {
+ // skip empty elements
+ if (node.children.length === 0) {
+ return;
+ }
+
+ // parse styles
+ let cssText = '';
+ if (
+ node.children[0].type === 'text' ||
+ node.children[0].type === 'cdata'
+ ) {
+ cssText = node.children[0].value;
+ }
+ /**
+ * @type {null | csstree.CssNode}
+ */
+ let cssAst = null;
+ try {
+ cssAst = csstree.parse(cssText, {
+ parseValue: true,
+ parseCustomProperty: false,
+ });
+ } catch {
+ return;
+ }
+
+ csstree.walk(cssAst, (node) => {
+ // #ID, .class selectors
+ if (
+ (prefixIds && node.type === 'IdSelector') ||
+ (prefixClassNames && node.type === 'ClassSelector')
+ ) {
+ node.name = prefixId(prefix, node.name);
+ return;
+ }
+ // url(...) references
+ if (
+ node.type === 'Url' &&
+ node.value.value &&
+ node.value.value.length > 0
+ ) {
+ const prefixed = prefixReference(
+ prefix,
+ unquote(node.value.value)
+ );
+ if (prefixed != null) {
+ node.value.value = prefixed;
+ }
+ }
+ });
+
+ // update styles
+ if (
+ node.children[0].type === 'text' ||
+ node.children[0].type === 'cdata'
+ ) {
+ node.children[0].value = csstree.generate(cssAst);
+ }
return;
}
- node.value.value = idPrefixed;
- }
- });
-
- // update