htmlnano 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/README.md +12 -893
  3. package/docs/README.md +33 -0
  4. package/docs/babel.config.js +3 -0
  5. package/docs/docs/010-introduction.md +22 -0
  6. package/docs/docs/020-usage.md +77 -0
  7. package/docs/docs/030-config.md +21 -0
  8. package/docs/docs/040-presets.md +75 -0
  9. package/docs/docs/050-modules.md +838 -0
  10. package/docs/docs/060-contribute.md +16 -0
  11. package/docs/docusaurus.config.js +60 -0
  12. package/docs/netlify.toml +4 -0
  13. package/docs/package-lock.json +27251 -0
  14. package/docs/package.json +39 -0
  15. package/docs/sidebars.js +26 -0
  16. package/docs/versioned_docs/version-1.1.1/010-introduction.md +22 -0
  17. package/docs/versioned_docs/version-1.1.1/020-usage.md +77 -0
  18. package/docs/versioned_docs/version-1.1.1/030-config.md +21 -0
  19. package/docs/versioned_docs/version-1.1.1/040-presets.md +75 -0
  20. package/docs/versioned_docs/version-1.1.1/050-modules.md +786 -0
  21. package/docs/versioned_docs/version-1.1.1/060-contribute.md +16 -0
  22. package/docs/versioned_docs/version-2.0.0/010-introduction.md +22 -0
  23. package/docs/versioned_docs/version-2.0.0/020-usage.md +77 -0
  24. package/docs/versioned_docs/version-2.0.0/030-config.md +21 -0
  25. package/docs/versioned_docs/version-2.0.0/040-presets.md +75 -0
  26. package/docs/versioned_docs/version-2.0.0/050-modules.md +838 -0
  27. package/docs/versioned_docs/version-2.0.0/060-contribute.md +16 -0
  28. package/docs/versioned_sidebars/version-1.1.1-sidebars.json +8 -0
  29. package/docs/versioned_sidebars/version-2.0.0-sidebars.json +8 -0
  30. package/docs/versions.json +4 -0
  31. package/lib/helpers.js +19 -1
  32. package/lib/htmlnano.js +67 -6
  33. package/lib/modules/collapseAttributeWhitespace.js +63 -7
  34. package/lib/modules/collapseWhitespace.js +42 -17
  35. package/lib/modules/minifyCss.js +8 -8
  36. package/lib/modules/minifyJs.js +9 -10
  37. package/lib/modules/minifySvg.js +5 -2
  38. package/lib/modules/minifyUrls.js +71 -29
  39. package/lib/modules/normalizeAttributeValues.js +48 -0
  40. package/lib/modules/removeComments.js +25 -1
  41. package/lib/modules/removeEmptyAttributes.js +53 -8
  42. package/lib/modules/removeRedundantAttributes.js +69 -14
  43. package/lib/modules/removeUnusedCss.js +10 -10
  44. package/lib/presets/max.js +2 -0
  45. package/lib/presets/safe.js +13 -13
  46. package/package.json +58 -21
  47. package/test.js +25 -16
@@ -0,0 +1,16 @@
1
+ # Contribute
2
+
3
+ Since the minifier is modular, it's very easy to add new modules:
4
+
5
+ 1. Create a ES6-file inside `lib/modules/` with a function that does some minification. For example you can check [`lib/modules/example.es6`](https://github.com/posthtml/htmlnano/blob/master/lib/modules/example.es6).
6
+
7
+ 2. Add the module's name into one of those [presets](https://github.com/posthtml/htmlnano/tree/master/lib/presets). You can choose either `ampSafe`, `max`, or `safe`.
8
+
9
+ 3. Create a JS-file inside `test/modules/` with some unit-tests.
10
+
11
+ 4. Describe your module in the section "[Modules](https://github.com/posthtml/htmlnano/blob/master/README.md#modules)".
12
+
13
+ 5. Send me a pull request.
14
+
15
+ Other types of contribution (bug fixes, documentation improves, etc) are also welcome!
16
+ Would like to contribute, but don't have any ideas what to do? Check out [our issues](https://github.com/posthtml/htmlnano/labels/help%20wanted).
@@ -0,0 +1,8 @@
1
+ {
2
+ "version-1.1.1/tutorialSidebar": [
3
+ {
4
+ "type": "autogenerated",
5
+ "dirName": "."
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "version-2.0.0/tutorialSidebar": [
3
+ {
4
+ "type": "autogenerated",
5
+ "dirName": "."
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,4 @@
1
+ [
2
+ "2.0.0",
3
+ "1.1.1"
4
+ ]
package/lib/helpers.js CHANGED
@@ -3,11 +3,13 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.extractCssFromStyleNode = extractCssFromStyleNode;
6
7
  exports.isAmpBoilerplate = isAmpBoilerplate;
7
8
  exports.isComment = isComment;
8
9
  exports.isConditionalComment = isConditionalComment;
10
+ exports.isEventHandler = isEventHandler;
9
11
  exports.isStyleNode = isStyleNode;
10
- exports.extractCssFromStyleNode = extractCssFromStyleNode;
12
+ exports.optionalRequire = optionalRequire;
11
13
  const ampBoilerplateAttributes = ['amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate'];
12
14
 
13
15
  function isAmpBoilerplate(node) {
@@ -38,4 +40,20 @@ function isStyleNode(node) {
38
40
 
39
41
  function extractCssFromStyleNode(node) {
40
42
  return Array.isArray(node.content) ? node.content.join(' ') : node.content;
43
+ }
44
+
45
+ function isEventHandler(attributeName) {
46
+ return attributeName && attributeName.slice && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
47
+ }
48
+
49
+ function optionalRequire(moduleName) {
50
+ try {
51
+ return require(moduleName);
52
+ } catch (e) {
53
+ if (e.code === 'MODULE_NOT_FOUND') {
54
+ return null;
55
+ }
56
+
57
+ throw e;
58
+ }
41
59
  }
package/lib/htmlnano.js CHANGED
@@ -4,18 +4,66 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
+ exports.loadConfig = loadConfig;
7
8
 
8
9
  var _posthtml = _interopRequireDefault(require("posthtml"));
9
10
 
11
+ var _cosmiconfig = require("cosmiconfig");
12
+
10
13
  var _safe = _interopRequireDefault(require("./presets/safe"));
11
14
 
12
15
  var _ampSafe = _interopRequireDefault(require("./presets/ampSafe"));
13
16
 
14
17
  var _max = _interopRequireDefault(require("./presets/max"));
15
18
 
19
+ var _package = _interopRequireDefault(require("../package.json"));
20
+
16
21
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
22
 
18
- function htmlnano(options = {}, preset = _safe.default) {
23
+ const presets = {
24
+ safe: _safe.default,
25
+ ampSafe: _ampSafe.default,
26
+ max: _max.default
27
+ };
28
+
29
+ function loadConfig(options, preset, configPath) {
30
+ var _options;
31
+
32
+ if (!((_options = options) !== null && _options !== void 0 && _options.skipConfigLoading)) {
33
+ const explorer = (0, _cosmiconfig.cosmiconfigSync)(_package.default.name);
34
+ const rc = configPath ? explorer.load(configPath) : explorer.search();
35
+
36
+ if (rc) {
37
+ const {
38
+ preset: presetName
39
+ } = rc.config;
40
+
41
+ if (presetName) {
42
+ if (!preset && presets[presetName]) {
43
+ preset = presets[presetName];
44
+ }
45
+
46
+ delete rc.config.preset;
47
+ }
48
+
49
+ if (!options) {
50
+ options = rc.config;
51
+ }
52
+ }
53
+ }
54
+
55
+ return [options || {}, preset || _safe.default];
56
+ }
57
+
58
+ const optionalDependencies = {
59
+ minifyCss: ['cssnano', 'postcss'],
60
+ minifyJs: ['terser'],
61
+ minifyUrl: ['relateurl', 'srcset', 'terser'],
62
+ minifySvg: ['svgo']
63
+ };
64
+
65
+ function htmlnano(optionsRun, presetRun) {
66
+ let [options, preset] = loadConfig(optionsRun, presetRun);
19
67
  return function minifier(tree) {
20
68
  options = { ...preset,
21
69
  ...options
@@ -32,6 +80,18 @@ function htmlnano(options = {}, preset = _safe.default) {
32
80
  throw new Error('Module "' + moduleName + '" is not defined');
33
81
  }
34
82
 
83
+ (optionalDependencies[moduleName] || []).forEach(dependency => {
84
+ try {
85
+ require(dependency);
86
+ } catch (e) {
87
+ if (e.code === 'MODULE_NOT_FOUND') {
88
+ console.warn(`You have to install "${dependency}" in order to use htmlnano's "${moduleName}" module`);
89
+ } else {
90
+ throw e;
91
+ }
92
+ }
93
+ });
94
+
35
95
  let module = require('./modules/' + moduleName);
36
96
 
37
97
  promise = promise.then(tree => module.default(tree, options, moduleOptions));
@@ -41,14 +101,15 @@ function htmlnano(options = {}, preset = _safe.default) {
41
101
  };
42
102
  }
43
103
 
104
+ htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
105
+ const [options] = loadConfig(optionsRun, presetRun);
106
+ return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
107
+ };
108
+
44
109
  htmlnano.process = function (html, options, preset, postHtmlOptions) {
45
110
  return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
46
111
  };
47
112
 
48
- htmlnano.presets = {
49
- safe: _safe.default,
50
- ampSafe: _ampSafe.default,
51
- max: _max.default
52
- };
113
+ htmlnano.presets = presets;
53
114
  var _default = htmlnano;
54
115
  exports.default = _default;
@@ -3,12 +3,60 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = collapseAttributeWhitespace;
7
6
  exports.attributesWithLists = void 0;
8
- const attributesWithLists = new Set(['class', 'rel', 'ping']);
9
- /** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
7
+ exports.default = collapseAttributeWhitespace;
8
+
9
+ var _helpers = require("../helpers");
10
+
11
+ const attributesWithLists = new Set(['class', 'dropzone', 'rel', // a, area, link
12
+ 'ping', // a, area
13
+ 'sandbox', // iframe
14
+ 'sizes', // link
15
+ 'headers' // td, th
16
+ ]);
17
+ /** @type Record<string, string[] | null> */
10
18
 
11
19
  exports.attributesWithLists = attributesWithLists;
20
+ const attributesWithSingleValue = {
21
+ accept: ['input'],
22
+ action: ['form'],
23
+ accesskey: null,
24
+ 'accept-charset': ['form'],
25
+ cite: ['blockquote', 'del', 'ins', 'q'],
26
+ cols: ['textarea'],
27
+ colspan: ['td', 'th'],
28
+ data: ['object'],
29
+ dropzone: null,
30
+ formaction: ['button', 'input'],
31
+ height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
32
+ high: ['meter'],
33
+ href: ['a', 'area', 'base', 'link'],
34
+ itemid: null,
35
+ low: ['meter'],
36
+ manifest: ['html'],
37
+ max: ['meter', 'progress'],
38
+ maxlength: ['input', 'textarea'],
39
+ media: ['source'],
40
+ min: ['meter'],
41
+ minlength: ['input', 'textarea'],
42
+ optimum: ['meter'],
43
+ ping: ['a', 'area'],
44
+ poster: ['video'],
45
+ profile: ['head'],
46
+ rows: ['textarea'],
47
+ rowspan: ['td', 'th'],
48
+ size: ['input', 'select'],
49
+ span: ['col', 'colgroup'],
50
+ src: ['audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video'],
51
+ start: ['ol'],
52
+ step: ['input'],
53
+ style: null,
54
+ tabindex: null,
55
+ usemap: ['img', 'object'],
56
+ value: ['li', 'meter', 'progress'],
57
+ width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
58
+ };
59
+ /** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
12
60
 
13
61
  function collapseAttributeWhitespace(tree) {
14
62
  tree.walk(node => {
@@ -19,14 +67,22 @@ function collapseAttributeWhitespace(tree) {
19
67
  Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
20
68
  const attrNameLower = attrName.toLowerCase();
21
69
 
22
- if (!attributesWithLists.has(attrNameLower)) {
23
- return;
70
+ if (attributesWithLists.has(attrNameLower)) {
71
+ const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
72
+ node.attrs[attrName] = newAttrValue;
73
+ return node;
24
74
  }
25
75
 
26
- const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
27
- node.attrs[attrName] = newAttrValue;
76
+ if ((0, _helpers.isEventHandler)(attrNameLower) || Object.hasOwnProperty.call(attributesWithSingleValue, attrNameLower) && (attributesWithSingleValue[attrNameLower] === null || attributesWithSingleValue[attrNameLower].includes(node.tag))) {
77
+ node.attrs[attrName] = minifySingleAttributeValue(attrValue);
78
+ return node;
79
+ }
28
80
  });
29
81
  return node;
30
82
  });
31
83
  return tree;
84
+ }
85
+
86
+ function minifySingleAttributeValue(value) {
87
+ return typeof value === 'string' ? String(value).trim() : value;
32
88
  }
@@ -13,34 +13,40 @@ const noTrimWhitespacesArroundElements = new Set([// non-empty tags that will ma
13
13
  'comment', 'img', 'input', 'wbr']);
14
14
  const noTrimWhitespacesInsideElements = new Set([// non-empty tags that will maintain whitespace within them
15
15
  'a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var']);
16
- const whitespacePattern = /\s+/g;
16
+ const startsWithWhitespacePattern = /^\s/;
17
+ const endsWithWhitespacePattern = /\s$/;
18
+ const multipleWhitespacePattern = /\s+/g;
17
19
  const NONE = '';
18
20
  const SINGLE_SPACE = ' ';
19
21
  const validOptions = ['all', 'aggressive', 'conservative'];
20
22
  /** Collapses redundant whitespaces */
21
23
 
22
- function collapseWhitespace(tree, options, collapseType, tag) {
24
+ function collapseWhitespace(tree, options, collapseType, parent) {
23
25
  collapseType = validOptions.includes(collapseType) ? collapseType : 'conservative';
24
26
  tree.forEach((node, index) => {
27
+ const prevNode = tree[index - 1];
28
+ const nextNode = tree[index + 1];
29
+
25
30
  if (typeof node === 'string') {
26
- const prevNode = tree[index - 1];
27
- const nextNode = tree[index + 1];
28
- const prevNodeTag = prevNode && prevNode.tag;
29
- const nextNodeTag = nextNode && nextNode.tag;
30
- const isTopLevel = !tag || tag === 'html' || tag === 'head';
31
+ const parentNodeTag = parent && parent.node && parent.node.tag;
32
+ const isTopLevel = !parentNodeTag || parentNodeTag === 'html' || parentNodeTag === 'head';
31
33
  const shouldTrim = collapseType === 'all' || isTopLevel ||
32
34
  /*
33
35
  * When collapseType is set to 'aggressive', and the tag is not inside 'noTrimWhitespacesInsideElements'.
34
36
  * the first & last space inside the tag will be trimmed
35
37
  */
36
- collapseType === 'aggressive' && !noTrimWhitespacesInsideElements.has(tag);
37
- node = collapseRedundantWhitespaces(node, collapseType, shouldTrim, tag, prevNodeTag, nextNodeTag);
38
+ collapseType === 'aggressive';
39
+ node = collapseRedundantWhitespaces(node, collapseType, shouldTrim, parent, prevNode, nextNode);
38
40
  }
39
41
 
40
42
  const isAllowCollapseWhitespace = !noWhitespaceCollapseElements.has(node.tag);
41
43
 
42
44
  if (node.content && node.content.length && isAllowCollapseWhitespace) {
43
- node.content = collapseWhitespace(node.content, options, collapseType, node.tag);
45
+ node.content = collapseWhitespace(node.content, options, collapseType, {
46
+ node,
47
+ prevNode,
48
+ nextNode
49
+ });
44
50
  }
45
51
 
46
52
  tree[index] = node;
@@ -48,23 +54,42 @@ function collapseWhitespace(tree, options, collapseType, tag) {
48
54
  return tree;
49
55
  }
50
56
 
51
- function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false, currentTag, prevNodeTag, nextNodeTag) {
57
+ function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false, parent, prevNode, nextNode) {
52
58
  if (!text || text.length === 0) {
53
59
  return NONE;
54
60
  }
55
61
 
56
62
  if (!(0, _helpers.isComment)(text)) {
57
- text = text.replace(whitespacePattern, SINGLE_SPACE);
63
+ text = text.replace(multipleWhitespacePattern, SINGLE_SPACE);
58
64
  }
59
65
 
60
66
  if (shouldTrim) {
61
67
  if (collapseType === 'aggressive') {
62
- if (!noTrimWhitespacesArroundElements.has(prevNodeTag)) {
63
- text = text.trimStart();
64
- }
68
+ if (!noTrimWhitespacesInsideElements.has(parent && parent.node && parent.node.tag)) {
69
+ if (!noTrimWhitespacesArroundElements.has(prevNode && prevNode.tag)) {
70
+ text = text.trimStart();
71
+ } else {
72
+ // previous node is a "no trim whitespaces arround element"
73
+ if ( // but previous node ends with a whitespace
74
+ prevNode && prevNode.content && prevNode.content.length && endsWithWhitespacePattern.test(prevNode.content[prevNode.content.length - 1]) && (!nextNode // either the current node is the last child of the parent
75
+ || // or the next node starts with a white space
76
+ nextNode && nextNode.content && nextNode.content.length && !startsWithWhitespacePattern.test(nextNode.content[0]))) {
77
+ text = text.trimStart();
78
+ }
79
+ }
65
80
 
66
- if (!noTrimWhitespacesArroundElements.has(nextNodeTag)) {
67
- text = text.trimEnd();
81
+ if (!noTrimWhitespacesArroundElements.has(nextNode && nextNode.tag)) {
82
+ text = text.trimEnd();
83
+ }
84
+ } else {
85
+ // now it is a textNode inside a "no trim whitespaces inside elements" node
86
+ if (!prevNode // it the textnode is the first child of the node
87
+ && startsWithWhitespacePattern.test(text[0]) // it starts with white space
88
+ && typeof parent.prevNode === 'string' // the prev of the node is a textNode as well
89
+ && endsWithWhitespacePattern.test(parent.prevNode[parent.prevNode.length - 1]) // that prev is ends with a white
90
+ ) {
91
+ text = text.trimStart();
92
+ }
68
93
  }
69
94
  } else {
70
95
  // collapseType is 'all', trim spaces
@@ -7,12 +7,8 @@ exports.default = minifyCss;
7
7
 
8
8
  var _helpers = require("../helpers");
9
9
 
10
- var _postcss = _interopRequireDefault(require("postcss"));
11
-
12
- var _cssnano = _interopRequireDefault(require("cssnano"));
13
-
14
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
-
10
+ const cssnano = (0, _helpers.optionalRequire)('cssnano');
11
+ const postcss = (0, _helpers.optionalRequire)('postcss');
16
12
  const postcssOptions = {
17
13
  // Prevent the following warning from being shown:
18
14
  // > Without `from` option PostCSS could generate wrong source map and will not find Browserslist config.
@@ -22,6 +18,10 @@ const postcssOptions = {
22
18
  /** Minify CSS with cssnano */
23
19
 
24
20
  function minifyCss(tree, options, cssnanoOptions) {
21
+ if (!cssnano || !postcss) {
22
+ return tree;
23
+ }
24
+
25
25
  let promises = [];
26
26
  tree.walk(node => {
27
27
  if ((0, _helpers.isStyleNode)(node)) {
@@ -46,7 +46,7 @@ function processStyleNode(styleNode, cssnanoOptions) {
46
46
  css = strippedCss;
47
47
  }
48
48
 
49
- return (0, _postcss.default)([(0, _cssnano.default)(cssnanoOptions)]).process(css, postcssOptions).then(result => {
49
+ return postcss([cssnano(cssnanoOptions)]).process(css, postcssOptions).then(result => {
50
50
  if (isCdataWrapped) {
51
51
  return styleNode.content = ['<![CDATA[' + result + ']]>'];
52
52
  }
@@ -61,7 +61,7 @@ function processStyleAttr(node, cssnanoOptions) {
61
61
  const wrapperStart = 'a{';
62
62
  const wrapperEnd = '}';
63
63
  const wrappedStyle = wrapperStart + (node.attrs.style || '') + wrapperEnd;
64
- return (0, _postcss.default)([(0, _cssnano.default)(cssnanoOptions)]).process(wrappedStyle, postcssOptions).then(result => {
64
+ return postcss([cssnano(cssnanoOptions)]).process(wrappedStyle, postcssOptions).then(result => {
65
65
  const minifiedCss = result.css; // Remove wrapperStart at the start and wrapperEnd at the end of minifiedCss
66
66
 
67
67
  node.attrs.style = minifiedCss.substring(wrapperStart.length, minifiedCss.length - wrapperEnd.length);
@@ -5,14 +5,15 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = minifyJs;
7
7
 
8
- var _terser = _interopRequireDefault(require("terser"));
8
+ var _helpers = require("../helpers");
9
9
 
10
10
  var _removeRedundantAttributes = require("./removeRedundantAttributes");
11
11
 
12
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
-
12
+ const terser = (0, _helpers.optionalRequire)('terser');
14
13
  /** Minify JS with Terser */
14
+
15
15
  function minifyJs(tree, options, terserOptions) {
16
+ if (!terser) return tree;
16
17
  let promises = [];
17
18
  tree.walk(node => {
18
19
  if (node.tag && node.tag === 'script') {
@@ -60,7 +61,7 @@ function processScriptNode(scriptNode, terserOptions) {
60
61
  js = strippedJs;
61
62
  }
62
63
 
63
- return _terser.default.minify(js, terserOptions).then(result => {
64
+ return terser.minify(js, terserOptions).then(result => {
64
65
  if (result.error) {
65
66
  throw new Error(result.error);
66
67
  }
@@ -80,12 +81,12 @@ function processScriptNode(scriptNode, terserOptions) {
80
81
  }
81
82
 
82
83
  function processNodeWithOnAttrs(node, terserOptions) {
83
- const jsWrapperStart = 'function _(){';
84
- const jsWrapperEnd = '}';
84
+ const jsWrapperStart = 'function a(){';
85
+ const jsWrapperEnd = '}a();';
85
86
  const promises = [];
86
87
 
87
88
  for (const attrName of Object.keys(node.attrs || {})) {
88
- if (!attrName.startsWith('on')) {
89
+ if (!(0, _helpers.isEventHandler)(attrName)) {
89
90
  continue;
90
91
  } // For example onclick="return false" is valid,
91
92
  // but "return false;" is invalid (error: 'return' outside of function)
@@ -94,14 +95,12 @@ function processNodeWithOnAttrs(node, terserOptions) {
94
95
 
95
96
 
96
97
  let wrappedJs = jsWrapperStart + node.attrs[attrName] + jsWrapperEnd;
97
-
98
- let promise = _terser.default.minify(wrappedJs, terserOptions).then(({
98
+ let promise = terser.minify(wrappedJs, terserOptions).then(({
99
99
  code
100
100
  }) => {
101
101
  let minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
102
102
  node.attrs[attrName] = minifiedJs;
103
103
  });
104
-
105
104
  promises.push(promise);
106
105
  }
107
106
 
@@ -5,10 +5,13 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = minifySvg;
7
7
 
8
- var _svgo = require("svgo");
8
+ var _helpers = require("../helpers");
9
9
 
10
+ const svgo = (0, _helpers.optionalRequire)('svgo');
10
11
  /** Minify SVG with SVGO */
12
+
11
13
  function minifySvg(tree, options, svgoOptions = {}) {
14
+ if (!svgo) return tree;
12
15
  tree.match({
13
16
  tag: 'svg'
14
17
  }, node => {
@@ -16,7 +19,7 @@ function minifySvg(tree, options, svgoOptions = {}) {
16
19
  closingSingleTag: 'slash',
17
20
  quoteAllAttributes: true
18
21
  });
19
- const result = (0, _svgo.optimize)(svgStr, svgoOptions);
22
+ const result = svgo.optimize(svgStr, svgoOptions);
20
23
  node.tag = false;
21
24
  node.attrs = {};
22
25
  node.content = result.data;
@@ -5,27 +5,28 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = minifyUrls;
7
7
 
8
- var _relateurl = _interopRequireDefault(require("relateurl"));
8
+ var _helpers = require("../helpers");
9
9
 
10
- var _srcset = _interopRequireDefault(require("srcset"));
10
+ const RelateUrl = (0, _helpers.optionalRequire)('relateurl');
11
+ const srcset = (0, _helpers.optionalRequire)('srcset');
12
+ const terser = (0, _helpers.optionalRequire)('terser'); // Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
11
13
 
12
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
-
14
- // Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
15
- const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'img', 'object', 'q', 'blockquote', 'ins', 'form', 'input', 'head', 'script']);
14
+ const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'object', 'blockquote', 'q', 'del', 'ins', 'form', 'input', 'head', 'audio', 'embed', 'iframe', 'img', 'script', 'track', 'video']);
16
15
  const tagsHasHrefAttributes = new Set(['a', 'area', 'link', 'base']);
17
16
  const attributesOfImgTagHasUriValues = new Set(['src', 'longdesc', 'usemap']);
18
17
  const attributesOfObjectTagHasUriValues = new Set(['classid', 'codebase', 'data', 'usemap']);
18
+ const tagsHasCiteAttributes = new Set(['blockquote', 'q', 'ins', 'del']);
19
+ const tagsHasSrcAttributes = new Set(['audio', 'embed', 'iframe', 'img', 'input', 'script', 'track', 'video',
20
+ /**
21
+ * https://html.spec.whatwg.org/#attr-source-src
22
+ *
23
+ * Although most of browsers recommend not to use "src" in <source>,
24
+ * but technically it does comply with HTML Standard.
25
+ */
26
+ 'source']);
19
27
 
20
28
  const isUriTypeAttribute = (tag, attr) => {
21
- return tagsHasHrefAttributes.has(tag) && attr === 'href' || tag === 'img' && attributesOfImgTagHasUriValues.has(attr) || tag === 'object' && attributesOfObjectTagHasUriValues.has(attr) || tag === 'q' && attr === 'cite' || tag === 'blockquote' && attr === 'cite' || (tag === 'ins' || tag === 'del') && attr === 'cite' || tag === 'form' && attr === 'action' || tag === 'input' && (attr === 'src' || attr === 'usemap') || tag === 'head' && attr === 'profile' || tag === 'script' && (attr === 'src' || attr === 'for') ||
22
- /**
23
- * https://html.spec.whatwg.org/#attr-source-src
24
- *
25
- * Although most of browsers recommend not to use "src" in <source>,
26
- * but technically it does comply with HTML Standard.
27
- */
28
- tag === 'source' && attr === 'src';
29
+ return tagsHasHrefAttributes.has(tag) && attr === 'href' || tag === 'img' && attributesOfImgTagHasUriValues.has(attr) || tag === 'object' && attributesOfObjectTagHasUriValues.has(attr) || tagsHasCiteAttributes.has(tag) && attr === 'cite' || tag === 'form' && attr === 'action' || tag === 'input' && attr === 'usemap' || tag === 'head' && attr === 'profile' || tag === 'script' && attr === 'for' || tagsHasSrcAttributes.has(tag) && attr === 'src';
29
30
  };
30
31
 
31
32
  const isSrcsetAttribute = (tag, attr) => {
@@ -55,11 +56,13 @@ const isLinkRelCanonical = ({
55
56
  return false;
56
57
  };
57
58
 
59
+ const JAVASCRIPT_URL_PROTOCOL = 'javascript:';
58
60
  let relateUrlInstance;
59
61
  let STORED_URL_BASE;
60
62
  /** Convert absolute url into relative url */
61
63
 
62
64
  function minifyUrls(tree, options, moduleOptions) {
65
+ let promises = [];
63
66
  const urlBase = processModuleOptions(moduleOptions); // Invalid configuration, return tree directly
64
67
 
65
68
  if (!urlBase) return tree;
@@ -71,7 +74,10 @@ function minifyUrls(tree, options, moduleOptions) {
71
74
  */
72
75
 
73
76
  if (!relateUrlInstance || STORED_URL_BASE !== urlBase) {
74
- relateUrlInstance = new _relateurl.default(urlBase);
77
+ if (RelateUrl) {
78
+ relateUrlInstance = new RelateUrl(urlBase);
79
+ }
80
+
75
81
  STORED_URL_BASE = urlBase;
76
82
  }
77
83
 
@@ -87,23 +93,34 @@ function minifyUrls(tree, options, moduleOptions) {
87
93
  const attrNameLower = attrName.toLowerCase();
88
94
 
89
95
  if (isUriTypeAttribute(node.tag, attrNameLower)) {
90
- // FIXME!
91
- // relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
92
- // the WHATWG URL API is very strict while attrValue might not be a valid URL
93
- // new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
94
- node.attrs[attrName] = relateUrlInstance.relate(attrValue);
96
+ if (isJavaScriptUrl(attrValue)) {
97
+ promises.push(minifyJavaScriptUrl(node, attrName));
98
+ } else {
99
+ if (relateUrlInstance) {
100
+ // FIXME!
101
+ // relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
102
+ // the WHATWG URL API is very strict while attrValue might not be a valid URL
103
+ // new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
104
+ node.attrs[attrName] = relateUrlInstance.relate(attrValue);
105
+ }
106
+ }
107
+
95
108
  continue;
96
109
  }
97
110
 
98
111
  if (isSrcsetAttribute(node.tag, attrNameLower)) {
99
- try {
100
- const parsedSrcset = _srcset.default.parse(attrValue);
101
-
102
- node.attrs[attrName] = _srcset.default.stringify(parsedSrcset.map(srcset => {
103
- srcset.url = relateUrlInstance.relate(srcset.url);
104
- return srcset;
105
- }));
106
- } catch (e) {// srcset will throw an Error for invalid srcset.
112
+ if (srcset) {
113
+ try {
114
+ const parsedSrcset = srcset.parse(attrValue);
115
+ node.attrs[attrName] = srcset.stringify(parsedSrcset.map(srcset => {
116
+ if (relateUrlInstance) {
117
+ srcset.url = relateUrlInstance.relate(srcset.url);
118
+ }
119
+
120
+ return srcset;
121
+ }));
122
+ } catch (e) {// srcset will throw an Error for invalid srcset.
123
+ }
107
124
  }
108
125
 
109
126
  continue;
@@ -112,5 +129,30 @@ function minifyUrls(tree, options, moduleOptions) {
112
129
 
113
130
  return node;
114
131
  });
115
- return tree;
132
+ if (promises.length > 0) return Promise.all(promises).then(() => tree);
133
+ return Promise.resolve(tree);
134
+ }
135
+
136
+ function isJavaScriptUrl(url) {
137
+ return typeof url === 'string' && url.toLowerCase().startsWith(JAVASCRIPT_URL_PROTOCOL);
138
+ }
139
+
140
+ function minifyJavaScriptUrl(node, attrName) {
141
+ if (!terser) return Promise.resolve();
142
+ const jsWrapperStart = 'function a(){';
143
+ const jsWrapperEnd = '}a();';
144
+ let result = node.attrs[attrName];
145
+
146
+ if (result) {
147
+ result = result.slice(JAVASCRIPT_URL_PROTOCOL.length);
148
+ return terser.minify(result, {}) // Default Option is good enough
149
+ .then(({
150
+ code
151
+ }) => {
152
+ const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
153
+ node.attrs[attrName] = minifiedJs;
154
+ });
155
+ }
156
+
157
+ return Promise.resolve();
116
158
  }