提交学习笔记专用
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

  1. 'use strict';
  2. const valueParser = require('postcss-value-parser');
  3. const { optimize } = require('svgo');
  4. const { encode, decode } = require('./lib/url');
  5. const PLUGIN = 'postcss-svgo';
  6. const dataURI = /data:image\/svg\+xml(;((charset=)?utf-8|base64))?,/i;
  7. const dataURIBase64 = /data:image\/svg\+xml;base64,/i;
  8. // the following regex will globally match:
  9. // \b([\w-]+) --> a word (a sequence of one or more [alphanumeric|underscore|dash] characters; followed by
  10. // \s*=\s* --> an equal sign character (=) between optional whitespaces; followed by
  11. // \\"([\S\s]+?)\\" --> any characters (including whitespaces and newlines) between literal escaped quotes (\")
  12. const escapedQuotes = /\b([\w-]+)\s*=\s*\\"([\S\s]+?)\\"/g;
  13. /**
  14. * @param {string} input the SVG string
  15. * @param {Options} opts
  16. * @return {{result: string, isUriEncoded: boolean}} the minification result
  17. */
  18. function minifySVG(input, opts) {
  19. let svg = input;
  20. let decodedUri, isUriEncoded;
  21. try {
  22. decodedUri = decode(input);
  23. isUriEncoded = decodedUri !== input;
  24. } catch (e) {
  25. // Swallow exception if we cannot decode the value
  26. isUriEncoded = false;
  27. }
  28. if (isUriEncoded) {
  29. svg = /** @type {string} */ (decodedUri);
  30. }
  31. if (opts.encode !== undefined) {
  32. isUriEncoded = opts.encode;
  33. }
  34. // normalize all escaped quote characters from svg attributes
  35. // from <svg attr=\"value\"... /> to <svg attr="value"... />
  36. // see: https://github.com/cssnano/cssnano/issues/1194
  37. svg = svg.replace(escapedQuotes, '$1="$2"');
  38. const result = optimize(svg, opts);
  39. if (result.error) {
  40. throw new Error(result.error);
  41. }
  42. return {
  43. result: /** @type {import('svgo').OptimizedSvg}*/ (result).data,
  44. isUriEncoded,
  45. };
  46. }
  47. /**
  48. * @param {import('postcss').Declaration} decl
  49. * @param {Options} opts
  50. * @param {import('postcss').Result} postcssResult
  51. * @return {void}
  52. */
  53. function minify(decl, opts, postcssResult) {
  54. const parsed = valueParser(decl.value);
  55. const minified = parsed.walk((node) => {
  56. if (
  57. node.type !== 'function' ||
  58. node.value.toLowerCase() !== 'url' ||
  59. !node.nodes.length
  60. ) {
  61. return;
  62. }
  63. let { value, quote } = /** @type {valueParser.StringNode} */ (
  64. node.nodes[0]
  65. );
  66. let optimizedValue;
  67. try {
  68. if (dataURIBase64.test(value)) {
  69. const url = new URL(value);
  70. const base64String = `${url.protocol}${url.pathname}`.replace(
  71. dataURI,
  72. ''
  73. );
  74. const svg = Buffer.from(base64String, 'base64').toString('utf8');
  75. const { result } = minifySVG(svg, opts);
  76. const data = Buffer.from(result).toString('base64');
  77. optimizedValue = 'data:image/svg+xml;base64,' + data + url.hash;
  78. } else if (dataURI.test(value)) {
  79. const svg = value.replace(dataURI, '');
  80. const { result, isUriEncoded } = minifySVG(svg, opts);
  81. let data = isUriEncoded ? encode(result) : result;
  82. // Should always encode # otherwise we yield a broken SVG
  83. // in Firefox (works in Chrome however). See this issue:
  84. // https://github.com/cssnano/cssnano/issues/245
  85. data = data.replace(/#/g, '%23');
  86. optimizedValue = 'data:image/svg+xml;charset=utf-8,' + data;
  87. quote = isUriEncoded ? '"' : "'";
  88. } else {
  89. return;
  90. }
  91. } catch (error) {
  92. decl.warn(postcssResult, `${error}`);
  93. return;
  94. }
  95. node.nodes[0] = Object.assign({}, node.nodes[0], {
  96. value: optimizedValue,
  97. quote: quote,
  98. type: 'string',
  99. before: '',
  100. after: '',
  101. });
  102. return false;
  103. });
  104. decl.value = minified.toString();
  105. }
  106. /** @typedef {{encode?: boolean, plugins?: object[]} & import('svgo').OptimizeOptions} Options */
  107. /**
  108. * @type {import('postcss').PluginCreator<Options>}
  109. * @param {Options} opts
  110. * @return {import('postcss').Plugin}
  111. */
  112. function pluginCreator(opts = {}) {
  113. return {
  114. postcssPlugin: PLUGIN,
  115. OnceExit(css, { result }) {
  116. css.walkDecls((decl) => {
  117. if (!dataURI.test(decl.value)) {
  118. return;
  119. }
  120. minify(decl, opts, result);
  121. });
  122. },
  123. };
  124. }
  125. pluginCreator.postcss = true;
  126. module.exports = pluginCreator;