UNPKG

svgo

Version:

SVGO is a Node.js library and command-line application for optimizing vector images.

146 lines (127 loc) 4.25 kB
import { attrsGroups } from './_collections.js'; /** * @typedef ConvertStyleToAttrsParams * @property {boolean=} keepImportant */ export const name = 'convertStyleToAttrs'; export const description = 'converts style to attributes'; /** * @param {...string} args * @returns {string} */ const g = (...args) => { return '(?:' + args.join('|') + ')'; }; const stylingProps = attrsGroups.presentation; const rEscape = '\\\\(?:[0-9a-f]{1,6}\\s?|\\r\\n|.)'; // Like \" or \2051. Code points consume one space. /** Pattern to match attribute name like: 'fill' */ const rAttr = '\\s*(' + g('[^:;\\\\]', rEscape) + '*?)\\s*'; /** Pattern to match string in single quotes like: 'foo' */ const rSingleQuotes = "'(?:[^'\\n\\r\\\\]|" + rEscape + ")*?(?:'|$)"; /** Pattern to match string in double quotes like: "foo" */ const rQuotes = '"(?:[^"\\n\\r\\\\]|' + rEscape + ')*?(?:"|$)'; const rQuotedString = new RegExp('^' + g(rSingleQuotes, rQuotes) + '$'); // Parentheses, E.g.: url(data:image/png;base64,iVBO...). // ':' and ';' inside of it should be treated as is. (Just like in strings.) const rParenthesis = '\\(' + g('[^\'"()\\\\]+', rEscape, rSingleQuotes, rQuotes) + '*?' + '\\)'; // The value. It can have strings and parentheses (see above). Fallbacks to anything in case of unexpected input. const rValue = '\\s*(' + g( '[^!\'"();\\\\]+?', rEscape, rSingleQuotes, rQuotes, rParenthesis, '[^;]*?', ) + '*?' + ')'; // End of declaration. Spaces outside of capturing groups help to do natural trimming. const rDeclEnd = '\\s*(?:;\\s*|$)'; // Important rule const rImportant = '(\\s*!important(?![-(\\w]))?'; // Final RegExp to parse CSS declarations. const regDeclarationBlock = new RegExp( rAttr + ':' + rValue + rImportant + rDeclEnd, 'ig', ); // Comments expression. Honors escape sequences and strings. const regStripComments = new RegExp( g(rEscape, rSingleQuotes, rQuotes, '/\\*[^]*?\\*/'), 'ig', ); /** * Convert style in attributes. Cleanups comments and illegal declarations (without colon) as a side effect. * * @example * <g style="fill:#000; color: #fff;"> * ⬇ * <g fill="#000" color="#fff"> * * @example * <g style="fill:#000; color: #fff; -webkit-blah: blah"> * ⬇ * <g fill="#000" color="#fff" style="-webkit-blah: blah"> * * @author Kir Belevich * * @type {import('../lib/types.js').Plugin<ConvertStyleToAttrsParams>} */ export const fn = (_root, params) => { const { keepImportant = false } = params; return { element: { enter: (node) => { if (node.attributes.style != null) { // ['opacity: 1', 'color: #000'] let styles = []; /** @type {Record<string, string>} */ const newAttributes = {}; // Strip CSS comments preserving escape sequences and strings. const styleValue = node.attributes.style.replace( regStripComments, (match) => { return match[0] == '/' ? '' : match[0] == '\\' && /[-g-z]/i.test(match[1]) ? match[1] : match; }, ); regDeclarationBlock.lastIndex = 0; for (var rule; (rule = regDeclarationBlock.exec(styleValue)); ) { if (!keepImportant || !rule[3]) { styles.push([rule[1], rule[2]]); } } if (styles.length) { styles = styles.filter(function (style) { if (style[0]) { const prop = style[0].toLowerCase(); let val = style[1]; if (rQuotedString.test(val)) { val = val.slice(1, -1); } if (stylingProps.has(prop)) { newAttributes[prop] = val; return false; } } return true; }); Object.assign(node.attributes, newAttributes); if (styles.length) { node.attributes.style = styles .map((declaration) => declaration.join(':')) .join(';'); } else { delete node.attributes.style; } } } }, }, }; };