| '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 { |
| // 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); |
| |
| return { |
| result: /** @type {import('svgo').Output}*/ (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} & import('svgo').Config} 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; |