1
0
mirror of https://github.com/svg/svgo.git synced 2025-04-19 10:22:15 +03:00
svgo/plugins/minifyStyles.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

154 lines
3.6 KiB
JavaScript

'use strict';
const csso = require('csso');
const { traverse } = require('../lib/xast.js');
exports.name = 'minifyStyles';
exports.type = 'full';
exports.active = true;
exports.description =
'minifies styles and removes unused styles based on usage data';
exports.params = {
// ... CSSO options goes here
// additional
usage: {
force: false, // force to use usage data even if it unsafe (document contains <script> or on* attributes)
ids: true,
classes: true,
tags: true,
},
};
/**
* Minifies styles (<style> element + style attribute) using CSSO
*
* @author strarsis <strarsis@gmail.com>
*/
exports.fn = function (ast, options) {
options = options || {};
var minifyOptionsForStylesheet = cloneObject(options);
var minifyOptionsForAttribute = cloneObject(options);
var elems = findStyleElems(ast);
minifyOptionsForStylesheet.usage = collectUsageData(ast, options);
minifyOptionsForAttribute.usage = null;
elems.forEach(function (elem) {
if (elem.isElem('style')) {
if (
elem.children[0].type === 'text' ||
elem.children[0].type === 'cdata'
) {
const styleCss = elem.children[0].value;
const minified = csso.minify(styleCss, minifyOptionsForStylesheet).css;
// preserve cdata if necessary
// TODO split cdata -> text optimisation into separate plugin
if (styleCss.indexOf('>') >= 0 || styleCss.indexOf('<') >= 0) {
elem.children[0].type = 'cdata';
elem.children[0].value = minified;
} else {
elem.children[0].type = 'text';
elem.children[0].value = minified;
}
}
} else {
// style attribute
var elemStyle = elem.attributes.style;
elem.attributes.style = csso.minifyBlock(
elemStyle,
minifyOptionsForAttribute
).css;
}
});
return ast;
};
function cloneObject(obj) {
return { ...obj };
}
function findStyleElems(ast) {
const nodesWithStyles = [];
traverse(ast, (node) => {
if (node.type === 'element') {
if (node.name === 'style' && node.children.length !== 0) {
nodesWithStyles.push(node);
} else if (node.attributes.style != null) {
nodesWithStyles.push(node);
}
}
});
return nodesWithStyles;
}
function shouldFilter(options, name) {
if ('usage' in options === false) {
return true;
}
if (options.usage && name in options.usage === false) {
return true;
}
return Boolean(options.usage && options.usage[name]);
}
function collectUsageData(ast, options) {
let safe = true;
const usageData = {};
let hasData = false;
const rawData = {
ids: Object.create(null),
classes: Object.create(null),
tags: Object.create(null),
};
traverse(ast, (node) => {
if (node.type === 'element') {
if (node.name === 'script') {
safe = false;
}
rawData.tags[node.name] = true;
if (node.attributes.id != null) {
rawData.ids[node.attributes.id] = true;
}
if (node.attributes.class != null) {
node.attributes.class
.replace(/^\s+|\s+$/g, '')
.split(/\s+/)
.forEach((className) => {
rawData.classes[className] = true;
});
}
if (Object.keys(node.attributes).some((name) => /^on/i.test(name))) {
safe = false;
}
}
});
if (!safe && options.usage && options.usage.force) {
safe = true;
}
for (const [key, data] of Object.entries(rawData)) {
if (shouldFilter(options, key)) {
usageData[key] = Object.keys(data);
hasData = true;
}
}
return safe && hasData ? usageData : null;
}