You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
143 lines
4.1 KiB
143 lines
4.1 KiB
'use strict'; |
|
const valueParser = require('postcss-value-parser'); |
|
const { optimize } = require('svgo'); |
|
const { encode, decode } = require('./lib/url'); |
|
|
|
const PLUGIN = 'postcss-svgo'; |
|
const dataURI = /data:image\/svg\+xml(;((charset=)?utf-8|base64))?,/i; |
|
const dataURIBase64 = /data:image\/svg\+xml;base64,/i; |
|
|
|
// the following regex will globally match: |
|
// \b([\w-]+) --> a word (a sequence of one or more [alphanumeric|underscore|dash] characters; followed by |
|
// \s*=\s* --> an equal sign character (=) between optional whitespaces; followed by |
|
// \\"([\S\s]+?)\\" --> any characters (including whitespaces and newlines) between literal escaped quotes (\") |
|
const escapedQuotes = /\b([\w-]+)\s*=\s*\\"([\S\s]+?)\\"/g; |
|
|
|
/** |
|
* @param {string} input the SVG string |
|
* @param {Options} opts |
|
* @return {{result: string, isUriEncoded: boolean}} the minification result |
|
*/ |
|
function minifySVG(input, opts) { |
|
let svg = input; |
|
let decodedUri, isUriEncoded; |
|
try { |
|
decodedUri = decode(input); |
|
isUriEncoded = decodedUri !== input; |
|
} catch (e) { |
|
// Swallow exception if we cannot decode the value |
|
isUriEncoded = false; |
|
} |
|
|
|
if (isUriEncoded) { |
|
svg = /** @type {string} */ (decodedUri); |
|
} |
|
|
|
if (opts.encode !== undefined) { |
|
isUriEncoded = opts.encode; |
|
} |
|
|
|
// normalize all escaped quote characters from svg attributes |
|
// from <svg attr=\"value\"... /> to <svg attr="value"... /> |
|
// see: https://github.com/cssnano/cssnano/issues/1194 |
|
svg = svg.replace(escapedQuotes, '$1="$2"'); |
|
|
|
const result = optimize(svg, opts); |
|
if (result.error) { |
|
throw new Error(result.error); |
|
} |
|
|
|
return { |
|
result: /** @type {import('svgo').OptimizedSvg}*/ (result).data, |
|
isUriEncoded, |
|
}; |
|
} |
|
|
|
/** |
|
* @param {import('postcss').Declaration} decl |
|
* @param {Options} opts |
|
* @param {import('postcss').Result} postcssResult |
|
* @return {void} |
|
*/ |
|
function minify(decl, opts, postcssResult) { |
|
const parsed = valueParser(decl.value); |
|
|
|
const minified = parsed.walk((node) => { |
|
if ( |
|
node.type !== 'function' || |
|
node.value.toLowerCase() !== 'url' || |
|
!node.nodes.length |
|
) { |
|
return; |
|
} |
|
let { value, quote } = /** @type {valueParser.StringNode} */ ( |
|
node.nodes[0] |
|
); |
|
|
|
let optimizedValue; |
|
|
|
try { |
|
if (dataURIBase64.test(value)) { |
|
const url = new URL(value); |
|
const base64String = `${url.protocol}${url.pathname}`.replace( |
|
dataURI, |
|
'' |
|
); |
|
const svg = Buffer.from(base64String, 'base64').toString('utf8'); |
|
const { result } = minifySVG(svg, opts); |
|
const data = Buffer.from(result).toString('base64'); |
|
optimizedValue = 'data:image/svg+xml;base64,' + data + url.hash; |
|
} else if (dataURI.test(value)) { |
|
const svg = value.replace(dataURI, ''); |
|
const { result, isUriEncoded } = minifySVG(svg, opts); |
|
let data = isUriEncoded ? encode(result) : result; |
|
// Should always encode # otherwise we yield a broken SVG |
|
// in Firefox (works in Chrome however). See this issue: |
|
// https://github.com/cssnano/cssnano/issues/245 |
|
data = data.replace(/#/g, '%23'); |
|
optimizedValue = 'data:image/svg+xml;charset=utf-8,' + data; |
|
quote = isUriEncoded ? '"' : "'"; |
|
} else { |
|
return; |
|
} |
|
} catch (error) { |
|
decl.warn(postcssResult, `${error}`); |
|
return; |
|
} |
|
node.nodes[0] = Object.assign({}, node.nodes[0], { |
|
value: optimizedValue, |
|
quote: quote, |
|
type: 'string', |
|
before: '', |
|
after: '', |
|
}); |
|
|
|
return false; |
|
}); |
|
|
|
decl.value = minified.toString(); |
|
} |
|
/** @typedef {{encode?: boolean, plugins?: object[]} & import('svgo').OptimizeOptions} Options */ |
|
/** |
|
* @type {import('postcss').PluginCreator<Options>} |
|
* @param {Options} opts |
|
* @return {import('postcss').Plugin} |
|
*/ |
|
function pluginCreator(opts = {}) { |
|
return { |
|
postcssPlugin: PLUGIN, |
|
|
|
OnceExit(css, { result }) { |
|
css.walkDecls((decl) => { |
|
if (!dataURI.test(decl.value)) { |
|
return; |
|
} |
|
|
|
minify(decl, opts, result); |
|
}); |
|
}, |
|
}; |
|
} |
|
|
|
pluginCreator.postcss = true; |
|
module.exports = pluginCreator;
|
|
|