1
0
mirror of https://github.com/svg/svgo.git synced 2025-04-19 10:22:15 +03:00
svgo/plugins/prefixIds.js
Bogdan Chadkin 07f8d606e0
Implement preset-default plugin (#1513)
I saw complaints about `extendDefaultPlugins` api

- it cannot be used when svgo is installed globally
- it requires svgo to be installed when using svgo-loader or svgo-jsx
- it prevents using serializable config formats like json

In this diff I introduced the new plugin which is a bundle of all
default plugins.

```js
module.exports = {
  plugins: [
    'preset_default',
    // or
    {
      name: 'preset_default',
      floatPrecision: 4,
      overrides: {
        convertPathData: {
          applyTransforms: false
        }
      }
    }
  ]
}
```
2021-08-13 19:07:08 +03:00

301 lines
6.4 KiB
JavaScript

'use strict';
exports.name = 'prefixIds';
exports.type = 'perItem';
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);
}
}
return string;
};
// Escapes a string for being used as ID
var escapeIdentifierName = function (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) => {
if (
element.attributes[name] == null ||
element.attributes[name].length === 0
) {
return;
}
// url(...) in value
const urlVal = matchUrl(element.attributes[name]);
if (!urlVal) {
return;
}
const idPrefixed = prefixId(urlVal);
if (!idPrefixed) {
return;
}
element.attributes[name] = 'url(' + idPrefixed + ')';
};
// prefixes begin/end attribute value
const addPrefixToBeginEndAttr = (element, name) => {
if (
element.attributes[name] == null ||
element.attributes[name].length === 0
) {
return;
}
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('; ');
};
const getBasename = (path) => {
// extract everything after latest slash or backslash
const matched = path.match(/[/\\]([^/\\]+)$/);
if (matched) {
return matched[1];
}
return '';
};
/**
* Prefixes identifiers
*
* @param {Object} node node
* @param {Object} opts plugin params
* @param {Object} extra plugin extra information
*
* @author strarsis <strarsis@gmail.com>
*/
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);
};
// <style/> property values
if (node.type === 'element' && node.name === 'style') {
if (node.children.length === 0) {
// skip empty <style/>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 <style/> 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;
}
// url(...) in value
if (
node.type === 'Url' &&
node.value.value &&
node.value.value.length > 0
) {
idPrefixed = prefixId(unquote(node.value.value));
if (!idPrefixed) {
return;
}
node.value.value = idPrefixed;
}
});
// update <style>s
node.children[0].value = csstree.generate(cssAst);
return;
}
// element attributes
if (node.type !== 'element') {
return;
}
// Nodes
if (opts.prefixIds) {
// ID
addPrefixToIdAttr(node, 'id');
}
if (opts.prefixClassNames) {
// Class
addPrefixToClassAttr(node, 'class');
}
// References
// href
addPrefixToHrefAttr(node, 'href');
// (xlink:)href (deprecated, must be still supported)
addPrefixToHrefAttr(node, 'xlink:href');
// (referenceable) properties
for (var referencesProp of referencesProps) {
addPrefixToUrlAttr(node, referencesProp);
}
addPrefixToBeginEndAttr(node, 'begin');
addPrefixToBeginEndAttr(node, 'end');
};