htmlnano 0.2.9 → 1.1.1

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 (39) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/README.md +15 -886
  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 +786 -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 +11621 -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_sidebars/version-1.1.1-sidebars.json +8 -0
  23. package/docs/versions.json +3 -0
  24. package/lib/helpers.js +5 -0
  25. package/lib/htmlnano.js +43 -6
  26. package/lib/modules/collapseAttributeWhitespace.js +62 -6
  27. package/lib/modules/collapseWhitespace.js +42 -17
  28. package/lib/modules/minifyCss.js +4 -2
  29. package/lib/modules/minifyJs.js +5 -3
  30. package/lib/modules/minifySvg.js +6 -12
  31. package/lib/modules/minifyUrls.js +50 -15
  32. package/lib/modules/normalizeAttributeValues.js +48 -0
  33. package/lib/modules/removeComments.js +25 -1
  34. package/lib/modules/removeEmptyAttributes.js +52 -8
  35. package/lib/modules/removeRedundantAttributes.js +69 -14
  36. package/lib/presets/safe.js +9 -4
  37. package/package.json +18 -16
  38. package/test.js +25 -16
  39. package/uncss-fork.patch +13 -0
@@ -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,3 @@
1
+ [
2
+ "1.1.1"
3
+ ]
package/lib/helpers.js CHANGED
@@ -8,6 +8,7 @@ exports.isComment = isComment;
8
8
  exports.isConditionalComment = isConditionalComment;
9
9
  exports.isStyleNode = isStyleNode;
10
10
  exports.extractCssFromStyleNode = extractCssFromStyleNode;
11
+ exports.isEventHandler = isEventHandler;
11
12
  const ampBoilerplateAttributes = ['amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate'];
12
13
 
13
14
  function isAmpBoilerplate(node) {
@@ -38,4 +39,8 @@ function isStyleNode(node) {
38
39
 
39
40
  function extractCssFromStyleNode(node) {
40
41
  return Array.isArray(node.content) ? node.content.join(' ') : node.content;
42
+ }
43
+
44
+ function isEventHandler(attributeName) {
45
+ return attributeName && attributeName.slice && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
41
46
  }
package/lib/htmlnano.js CHANGED
@@ -3,19 +3,60 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.loadConfig = loadConfig;
6
7
  exports.default = void 0;
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
+ function htmlnano(optionsRun, presetRun) {
59
+ let [options, preset] = loadConfig(optionsRun, presetRun);
19
60
  return function minifier(tree) {
20
61
  options = { ...preset,
21
62
  ...options
@@ -45,10 +86,6 @@ htmlnano.process = function (html, options, preset, postHtmlOptions) {
45
86
  return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
46
87
  };
47
88
 
48
- htmlnano.presets = {
49
- safe: _safe.default,
50
- ampSafe: _ampSafe.default,
51
- max: _max.default
52
- };
89
+ htmlnano.presets = presets;
53
90
  var _default = htmlnano;
54
91
  exports.default = _default;
@@ -5,10 +5,58 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = collapseAttributeWhitespace;
7
7
  exports.attributesWithLists = void 0;
8
- const attributesWithLists = new Set(['class', 'rel', 'ping']);
9
- /** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
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,6 +7,8 @@ exports.default = minifyCss;
7
7
 
8
8
  var _helpers = require("../helpers");
9
9
 
10
+ var _postcss = _interopRequireDefault(require("postcss"));
11
+
10
12
  var _cssnano = _interopRequireDefault(require("cssnano"));
11
13
 
12
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -44,7 +46,7 @@ function processStyleNode(styleNode, cssnanoOptions) {
44
46
  css = strippedCss;
45
47
  }
46
48
 
47
- return _cssnano.default.process(css, postcssOptions, cssnanoOptions).then(result => {
49
+ return (0, _postcss.default)([(0, _cssnano.default)(cssnanoOptions)]).process(css, postcssOptions).then(result => {
48
50
  if (isCdataWrapped) {
49
51
  return styleNode.content = ['<![CDATA[' + result + ']]>'];
50
52
  }
@@ -59,7 +61,7 @@ function processStyleAttr(node, cssnanoOptions) {
59
61
  const wrapperStart = 'a{';
60
62
  const wrapperEnd = '}';
61
63
  const wrappedStyle = wrapperStart + (node.attrs.style || '') + wrapperEnd;
62
- return _cssnano.default.process(wrappedStyle, postcssOptions, cssnanoOptions).then(result => {
64
+ return (0, _postcss.default)([(0, _cssnano.default)(cssnanoOptions)]).process(wrappedStyle, postcssOptions).then(result => {
63
65
  const minifiedCss = result.css; // Remove wrapperStart at the start and wrapperEnd at the end of minifiedCss
64
66
 
65
67
  node.attrs.style = minifiedCss.substring(wrapperStart.length, minifiedCss.length - wrapperEnd.length);
@@ -7,6 +7,8 @@ exports.default = minifyJs;
7
7
 
8
8
  var _terser = _interopRequireDefault(require("terser"));
9
9
 
10
+ var _helpers = require("../helpers");
11
+
10
12
  var _removeRedundantAttributes = require("./removeRedundantAttributes");
11
13
 
12
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -80,12 +82,12 @@ function processScriptNode(scriptNode, terserOptions) {
80
82
  }
81
83
 
82
84
  function processNodeWithOnAttrs(node, terserOptions) {
83
- const jsWrapperStart = 'function _(){';
84
- const jsWrapperEnd = '}';
85
+ const jsWrapperStart = 'function a(){';
86
+ const jsWrapperEnd = '}a();';
85
87
  const promises = [];
86
88
 
87
89
  for (const attrName of Object.keys(node.attrs || {})) {
88
- if (!attrName.startsWith('on')) {
90
+ if (!(0, _helpers.isEventHandler)(attrName)) {
89
91
  continue;
90
92
  } // For example onclick="return false" is valid,
91
93
  // but "return false;" is invalid (error: 'return' outside of function)
@@ -5,14 +5,10 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = minifySvg;
7
7
 
8
- var _svgo = _interopRequireDefault(require("svgo"));
9
-
10
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
8
+ var _svgo = require("svgo");
11
9
 
12
10
  /** Minify SVG with SVGO */
13
11
  function minifySvg(tree, options, svgoOptions = {}) {
14
- let promises = [];
15
- const svgo = new _svgo.default(svgoOptions);
16
12
  tree.match({
17
13
  tag: 'svg'
18
14
  }, node => {
@@ -20,13 +16,11 @@ function minifySvg(tree, options, svgoOptions = {}) {
20
16
  closingSingleTag: 'slash',
21
17
  quoteAllAttributes: true
22
18
  });
23
- let promise = svgo.optimize(svgStr).then(result => {
24
- node.tag = false;
25
- node.attrs = {};
26
- node.content = result.data;
27
- });
28
- promises.push(promise);
19
+ const result = (0, _svgo.optimize)(svgStr, svgoOptions);
20
+ node.tag = false;
21
+ node.attrs = {};
22
+ node.content = result.data;
29
23
  return node;
30
24
  });
31
- return Promise.all(promises).then(() => tree);
25
+ return tree;
32
26
  }
@@ -9,23 +9,27 @@ var _relateurl = _interopRequireDefault(require("relateurl"));
9
9
 
10
10
  var _srcset = _interopRequireDefault(require("srcset"));
11
11
 
12
+ var _terser = _interopRequireDefault(require("terser"));
13
+
12
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
15
 
14
16
  // 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']);
17
+ const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'object', 'blockquote', 'q', 'del', 'ins', 'form', 'input', 'head', 'audio', 'embed', 'iframe', 'img', 'script', 'track', 'video']);
16
18
  const tagsHasHrefAttributes = new Set(['a', 'area', 'link', 'base']);
17
19
  const attributesOfImgTagHasUriValues = new Set(['src', 'longdesc', 'usemap']);
18
20
  const attributesOfObjectTagHasUriValues = new Set(['classid', 'codebase', 'data', 'usemap']);
21
+ const tagsHasCiteAttributes = new Set(['blockquote', 'q', 'ins', 'del']);
22
+ const tagsHasSrcAttributes = new Set(['audio', 'embed', 'iframe', 'img', 'input', 'script', 'track', 'video',
23
+ /**
24
+ * https://html.spec.whatwg.org/#attr-source-src
25
+ *
26
+ * Although most of browsers recommend not to use "src" in <source>,
27
+ * but technically it does comply with HTML Standard.
28
+ */
29
+ 'source']);
19
30
 
20
31
  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';
32
+ 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
33
  };
30
34
 
31
35
  const isSrcsetAttribute = (tag, attr) => {
@@ -55,11 +59,13 @@ const isLinkRelCanonical = ({
55
59
  return false;
56
60
  };
57
61
 
62
+ const JAVASCRIPT_URL_PROTOCOL = 'javascript:';
58
63
  let relateUrlInstance;
59
64
  let STORED_URL_BASE;
60
65
  /** Convert absolute url into relative url */
61
66
 
62
67
  function minifyUrls(tree, options, moduleOptions) {
68
+ let promises = [];
63
69
  const urlBase = processModuleOptions(moduleOptions); // Invalid configuration, return tree directly
64
70
 
65
71
  if (!urlBase) return tree;
@@ -87,11 +93,16 @@ 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
+ // FIXME!
100
+ // relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
101
+ // the WHATWG URL API is very strict while attrValue might not be a valid URL
102
+ // new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
103
+ node.attrs[attrName] = relateUrlInstance.relate(attrValue);
104
+ }
105
+
95
106
  continue;
96
107
  }
97
108
 
@@ -112,5 +123,29 @@ function minifyUrls(tree, options, moduleOptions) {
112
123
 
113
124
  return node;
114
125
  });
115
- return tree;
126
+ if (promises.length > 0) return Promise.all(promises).then(() => tree);
127
+ return Promise.resolve(tree);
128
+ }
129
+
130
+ function isJavaScriptUrl(url) {
131
+ return typeof url === 'string' && url.toLowerCase().startsWith(JAVASCRIPT_URL_PROTOCOL);
132
+ }
133
+
134
+ function minifyJavaScriptUrl(node, attrName) {
135
+ const jsWrapperStart = 'function a(){';
136
+ const jsWrapperEnd = '}a();';
137
+ let result = node.attrs[attrName];
138
+
139
+ if (result) {
140
+ result = result.slice(JAVASCRIPT_URL_PROTOCOL.length);
141
+ return _terser.default.minify(result, {}) // Default Option is good enough
142
+ .then(({
143
+ code
144
+ }) => {
145
+ const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
146
+ node.attrs[attrName] = minifiedJs;
147
+ });
148
+ }
149
+
150
+ return Promise.resolve();
116
151
  }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = normalizeAttributeValues;
7
+ const caseInsensitiveAttributes = {
8
+ autocomplete: ['form'],
9
+ charset: ['meta', 'script'],
10
+ contenteditable: null,
11
+ crossorigin: ['audio', 'img', 'link', 'script', 'video'],
12
+ dir: null,
13
+ draggable: null,
14
+ dropzone: null,
15
+ formmethod: ['button', 'input'],
16
+ inputmode: ['input', 'textarea'],
17
+ kind: ['track'],
18
+ method: ['form'],
19
+ preload: ['audio', 'video'],
20
+ referrerpolicy: ['a', 'area', 'iframe', 'img', 'link'],
21
+ sandbox: ['iframe'],
22
+ spellcheck: null,
23
+ scope: ['th'],
24
+ shape: ['area'],
25
+ sizes: ['link'],
26
+ step: ['input'],
27
+ translate: null,
28
+ type: ['a', 'link', 'button', 'embed', 'object', 'script', 'source', 'style', 'input', 'menu', 'menuitem'],
29
+ wrap: ['textarea']
30
+ };
31
+
32
+ function normalizeAttributeValues(tree) {
33
+ tree.walk(node => {
34
+ if (!node.attrs) {
35
+ return node;
36
+ }
37
+
38
+ Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
39
+ const attrNameLower = attrName.toLowerCase();
40
+
41
+ if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrNameLower) && (caseInsensitiveAttributes[attrNameLower] === null || caseInsensitiveAttributes[attrNameLower].includes(node.tag))) {
42
+ node.attrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
43
+ }
44
+ });
45
+ return node;
46
+ });
47
+ return tree;
48
+ }
@@ -11,7 +11,7 @@ const MATCH_EXCERPT_REGEXP = /<!-- ?more ?-->/i;
11
11
  /** Removes HTML comments */
12
12
 
13
13
  function removeComments(tree, options, removeType) {
14
- if (removeType !== 'all' && removeType !== 'safe') {
14
+ if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
15
15
  removeType = 'safe';
16
16
  }
17
17
 
@@ -68,5 +68,29 @@ function isCommentToRemove(text, removeType) {
68
68
  }
69
69
  }
70
70
 
71
+ if (isMatcher(removeType)) {
72
+ return isMatch(text, removeType);
73
+ }
74
+
71
75
  return true;
76
+ }
77
+
78
+ function isMatch(input, matcher) {
79
+ if (matcher instanceof RegExp) {
80
+ return matcher.test(input);
81
+ }
82
+
83
+ if (typeof matcher === 'function') {
84
+ return Boolean(matcher(input));
85
+ }
86
+
87
+ return false;
88
+ }
89
+
90
+ function isMatcher(matcher) {
91
+ if (matcher instanceof RegExp || typeof matcher === 'function') {
92
+ return true;
93
+ }
94
+
95
+ return false;
72
96
  }