htmlnano 1.1.0 → 2.0.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 (38) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +8 -13
  3. package/docs/docs/050-modules.md +54 -2
  4. package/docs/package-lock.json +14644 -824
  5. package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/010-introduction.md +0 -0
  6. package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/020-usage.md +0 -0
  7. package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/030-config.md +0 -0
  8. package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/040-presets.md +0 -0
  9. package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/050-modules.md +0 -0
  10. package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/060-contribute.md +0 -0
  11. package/docs/versioned_docs/version-2.0.0/010-introduction.md +22 -0
  12. package/docs/versioned_docs/version-2.0.0/020-usage.md +77 -0
  13. package/docs/versioned_docs/version-2.0.0/030-config.md +21 -0
  14. package/docs/versioned_docs/version-2.0.0/040-presets.md +75 -0
  15. package/docs/versioned_docs/version-2.0.0/050-modules.md +838 -0
  16. package/docs/versioned_docs/version-2.0.0/060-contribute.md +16 -0
  17. package/docs/versioned_sidebars/{version-1.1.0-sidebars.json → version-1.1.1-sidebars.json} +1 -1
  18. package/docs/versioned_sidebars/version-2.0.0-sidebars.json +8 -0
  19. package/docs/versions.json +2 -1
  20. package/lib/helpers.js +15 -2
  21. package/lib/htmlnano.js +78 -4
  22. package/lib/modules/collapseAttributeWhitespace.js +21 -21
  23. package/lib/modules/collapseBooleanAttributes.js +13 -19
  24. package/lib/modules/deduplicateAttributeValues.js +10 -16
  25. package/lib/modules/minifyCss.js +8 -8
  26. package/lib/modules/minifyJs.js +6 -9
  27. package/lib/modules/minifyJson.js +12 -23
  28. package/lib/modules/minifySvg.js +5 -2
  29. package/lib/modules/minifyUrls.js +29 -22
  30. package/lib/modules/normalizeAttributeValues.js +9 -15
  31. package/lib/modules/removeComments.js +17 -9
  32. package/lib/modules/removeEmptyAttributes.js +13 -16
  33. package/lib/modules/removeRedundantAttributes.js +19 -23
  34. package/lib/modules/removeUnusedCss.js +10 -10
  35. package/lib/presets/max.js +2 -0
  36. package/lib/presets/safe.js +2 -2
  37. package/package.json +49 -13
  38. package/uncss-fork.patch +0 -13
@@ -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).
@@ -1,5 +1,5 @@
1
1
  {
2
- "version-1.1.0/tutorialSidebar": [
2
+ "version-1.1.1/tutorialSidebar": [
3
3
  {
4
4
  "type": "autogenerated",
5
5
  "dirName": "."
@@ -0,0 +1,8 @@
1
+ {
2
+ "version-2.0.0/tutorialSidebar": [
3
+ {
4
+ "type": "autogenerated",
5
+ "dirName": "."
6
+ }
7
+ ]
8
+ }
@@ -1,3 +1,4 @@
1
1
  [
2
- "1.1.0"
2
+ "2.0.0",
3
+ "1.1.1"
3
4
  ]
package/lib/helpers.js CHANGED
@@ -3,12 +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;
9
- exports.isStyleNode = isStyleNode;
10
- exports.extractCssFromStyleNode = extractCssFromStyleNode;
11
10
  exports.isEventHandler = isEventHandler;
11
+ exports.isStyleNode = isStyleNode;
12
+ exports.optionalRequire = optionalRequire;
12
13
  const ampBoilerplateAttributes = ['amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate'];
13
14
 
14
15
  function isAmpBoilerplate(node) {
@@ -43,4 +44,16 @@ function extractCssFromStyleNode(node) {
43
44
 
44
45
  function isEventHandler(attributeName) {
45
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
+ }
46
59
  }
package/lib/htmlnano.js CHANGED
@@ -3,8 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.loadConfig = loadConfig;
7
6
  exports.default = void 0;
7
+ exports.loadConfig = loadConfig;
8
8
 
9
9
  var _posthtml = _interopRequireDefault(require("posthtml"));
10
10
 
@@ -55,9 +55,19 @@ function loadConfig(options, preset, configPath) {
55
55
  return [options || {}, preset || _safe.default];
56
56
  }
57
57
 
58
+ const optionalDependencies = {
59
+ minifyCss: ['cssnano', 'postcss'],
60
+ minifyJs: ['terser'],
61
+ minifyUrl: ['relateurl', 'srcset', 'terser'],
62
+ minifySvg: ['svgo']
63
+ };
64
+
58
65
  function htmlnano(optionsRun, presetRun) {
59
66
  let [options, preset] = loadConfig(optionsRun, presetRun);
60
67
  return function minifier(tree) {
68
+ const nodeHandlers = [];
69
+ const attrsHandlers = [];
70
+ const contentsHandlers = [];
61
71
  options = { ...preset,
62
72
  ...options
63
73
  };
@@ -73,15 +83,79 @@ function htmlnano(optionsRun, presetRun) {
73
83
  throw new Error('Module "' + moduleName + '" is not defined');
74
84
  }
75
85
 
76
- let module = require('./modules/' + moduleName);
86
+ (optionalDependencies[moduleName] || []).forEach(dependency => {
87
+ try {
88
+ require(dependency);
89
+ } catch (e) {
90
+ if (e.code === 'MODULE_NOT_FOUND') {
91
+ console.warn(`You have to install "${dependency}" in order to use htmlnano's "${moduleName}" module`);
92
+ } else {
93
+ throw e;
94
+ }
95
+ }
96
+ });
97
+
98
+ const module = require('./modules/' + moduleName);
77
99
 
78
- promise = promise.then(tree => module.default(tree, options, moduleOptions));
100
+ if (module.onAttrs) {
101
+ attrsHandlers.push(module.onAttrs(options, moduleOptions));
102
+ }
103
+
104
+ if (module.onContent) {
105
+ contentsHandlers.push(module.onContent(options, moduleOptions));
106
+ }
107
+
108
+ if (module.onNode) {
109
+ nodeHandlers.push(module.onNode(options, moduleOptions));
110
+ }
111
+
112
+ if (module.default) {
113
+ promise = promise.then(tree => module.default(tree, options, moduleOptions));
114
+ }
79
115
  }
80
116
 
81
- return promise;
117
+ if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
118
+ return promise;
119
+ }
120
+
121
+ return promise.then(tree => {
122
+ tree.walk(node => {
123
+ if (node.attrs) {
124
+ // Convert all attrs' key to lower case
125
+ let newAttrsObj = {};
126
+ Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
127
+ newAttrsObj[attrName.toLowerCase()] = attrValue;
128
+ });
129
+
130
+ for (const handler of attrsHandlers) {
131
+ newAttrsObj = handler(newAttrsObj, node);
132
+ }
133
+
134
+ node.attrs = newAttrsObj;
135
+ }
136
+
137
+ if (node.content) {
138
+ for (const handler of contentsHandlers) {
139
+ node.content = handler(node.content, node);
140
+ }
141
+ }
142
+
143
+ for (const handler of nodeHandlers) {
144
+ node = handler(node, node);
145
+ }
146
+
147
+ return node;
148
+ });
149
+ return tree;
150
+ });
82
151
  };
83
152
  }
84
153
 
154
+ htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
155
+ const [options] = loadConfig(optionsRun, presetRun);
156
+ return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
157
+ };
158
+
85
159
  htmlnano.process = function (html, options, preset, postHtmlOptions) {
86
160
  return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
87
161
  };
@@ -3,15 +3,22 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = collapseAttributeWhitespace;
7
6
  exports.attributesWithLists = void 0;
7
+ exports.onAttrs = onAttrs;
8
8
 
9
- var _helpers = require("../helpers.es6");
9
+ var _helpers = require("../helpers");
10
10
 
11
11
  const attributesWithLists = new Set(['class', 'dropzone', 'rel', // a, area, link
12
12
  'ping', // a, area
13
13
  'sandbox', // iframe
14
- 'sizes', // link
14
+
15
+ /**
16
+ * https://github.com/posthtml/htmlnano/issues/180
17
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes
18
+ *
19
+ * "sizes" of <img> should not be modified, while "sizes" of <link> will only have one entry in most cases.
20
+ */
21
+ // 'sizes', // link
15
22
  'headers' // td, th
16
23
  ]);
17
24
  /** @type Record<string, string[] | null> */
@@ -58,29 +65,22 @@ const attributesWithSingleValue = {
58
65
  };
59
66
  /** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
60
67
 
61
- function collapseAttributeWhitespace(tree) {
62
- tree.walk(node => {
63
- if (!node.attrs) {
64
- return node;
65
- }
66
-
67
- Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
68
- const attrNameLower = attrName.toLowerCase();
69
-
70
- if (attributesWithLists.has(attrNameLower)) {
68
+ function onAttrs() {
69
+ return (attrs, node) => {
70
+ const newAttrs = attrs;
71
+ Object.entries(attrs).forEach(([attrName, attrValue]) => {
72
+ if (attributesWithLists.has(attrName)) {
71
73
  const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
72
- node.attrs[attrName] = newAttrValue;
73
- return node;
74
+ newAttrs[attrName] = newAttrValue;
75
+ return;
74
76
  }
75
77
 
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;
78
+ if ((0, _helpers.isEventHandler)(attrName) || Object.hasOwnProperty.call(attributesWithSingleValue, attrName) && (attributesWithSingleValue[attrName] === null || attributesWithSingleValue[attrName].includes(node.tag))) {
79
+ newAttrs[attrName] = minifySingleAttributeValue(attrValue);
79
80
  }
80
81
  });
81
- return node;
82
- });
83
- return tree;
82
+ return newAttrs;
83
+ };
84
84
  }
85
85
 
86
86
  function minifySingleAttributeValue(value) {
@@ -3,42 +3,36 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = collapseBooleanAttributes;
6
+ exports.onAttrs = onAttrs;
7
7
  // Source: https://github.com/kangax/html-minifier/issues/63
8
8
  const htmlBooleanAttributes = new Set(['allowfullscreen', 'allowpaymentrequest', 'allowtransparency', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
9
9
  const amphtmlBooleanAttributes = new Set(['⚡', 'amp', '⚡4ads', 'amp4ads', '⚡4email', 'amp4email', 'amp-custom', 'amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate', 'allow-blocked-ranges', 'amp-access-hide', 'amp-access-template', 'amp-keyframes', 'animate', 'arrows', 'data-block-on-consent', 'data-enable-refresh', 'data-multi-size', 'date-template', 'disable-double-tap', 'disable-session-states', 'disableremoteplayback', 'dots', 'expand-single-section', 'expanded', 'fallback', 'first', 'fullscreen', 'inline', 'lightbox', 'noaudio', 'noautoplay', 'noloading', 'once', 'open-after-clear', 'open-after-select', 'open-button', 'placeholder', 'preload', 'reset-on-refresh', 'reset-on-resize', 'resizable', 'rotate-to-fullscreen', 'second', 'standalone', 'stereo', 'submit-error', 'submit-success', 'submitting', 'subscriptions-actions', 'subscriptions-dialog']);
10
10
 
11
- function collapseBooleanAttributes(tree, options, moduleOptions) {
12
- tree.walk(node => {
13
- if (!node.attrs) {
14
- return node;
15
- }
16
-
17
- if (!node.tag) {
18
- return node;
19
- }
11
+ function onAttrs(options, moduleOptions) {
12
+ return (attrs, node) => {
13
+ if (!node.tag) return attrs;
14
+ const newAttrs = attrs;
20
15
 
21
- for (const attrName of Object.keys(node.attrs)) {
16
+ for (const attrName of Object.keys(attrs)) {
22
17
  if (attrName === 'visible' && node.tag.startsWith('a-')) {
23
18
  continue;
24
19
  }
25
20
 
26
21
  if (htmlBooleanAttributes.has(attrName)) {
27
- node.attrs[attrName] = true;
22
+ newAttrs[attrName] = true;
28
23
  }
29
24
 
30
- if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && node.attrs[attrName] === '') {
31
- node.attrs[attrName] = true;
25
+ if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && attrs[attrName] === '') {
26
+ newAttrs[attrName] = true;
32
27
  } // collapse crossorigin attributes
33
28
  // Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
34
29
 
35
30
 
36
- if (attrName.toLowerCase() === 'crossorigin' && (node.attrs[attrName] === 'anonymous' || node.attrs[attrName] === '')) {
37
- node.attrs[attrName] = true;
31
+ if (attrName.toLowerCase() === 'crossorigin' && (attrs[attrName] === 'anonymous' || attrs[attrName] === '')) {
32
+ newAttrs[attrName] = true;
38
33
  }
39
34
  }
40
35
 
41
- return node;
42
- });
43
- return tree;
36
+ return newAttrs;
37
+ };
44
38
  }
@@ -3,25 +3,20 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = collapseAttributeWhitespace;
6
+ exports.onAttrs = onAttrs;
7
7
 
8
8
  var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace");
9
9
 
10
10
  /** Deduplicate values inside list-like attributes (e.g. class, rel) */
11
- function collapseAttributeWhitespace(tree) {
12
- tree.walk(node => {
13
- if (!node.attrs) {
14
- return node;
15
- }
16
-
17
- Object.keys(node.attrs).forEach(attrName => {
18
- const attrNameLower = attrName.toLowerCase();
19
-
20
- if (!_collapseAttributeWhitespace.attributesWithLists.has(attrNameLower)) {
11
+ function onAttrs() {
12
+ return attrs => {
13
+ const newAttrs = attrs;
14
+ Object.keys(attrs).forEach(attrName => {
15
+ if (!_collapseAttributeWhitespace.attributesWithLists.has(attrName)) {
21
16
  return;
22
17
  }
23
18
 
24
- const attrValues = node.attrs[attrName].split(/\s/);
19
+ const attrValues = attrs[attrName].split(/\s/);
25
20
  const uniqeAttrValues = new Set();
26
21
  const deduplicatedAttrValues = [];
27
22
  attrValues.forEach(attrValue => {
@@ -38,9 +33,8 @@ function collapseAttributeWhitespace(tree) {
38
33
  deduplicatedAttrValues.push(attrValue);
39
34
  uniqeAttrValues.add(attrValue);
40
35
  });
41
- node.attrs[attrName] = deduplicatedAttrValues.join(' ');
36
+ newAttrs[attrName] = deduplicatedAttrValues.join(' ');
42
37
  });
43
- return node;
44
- });
45
- return tree;
38
+ return newAttrs;
39
+ };
46
40
  }
@@ -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,16 +5,15 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = minifyJs;
7
7
 
8
- var _terser = _interopRequireDefault(require("terser"));
9
-
10
- var _helpers = require("../helpers.es6");
8
+ var _helpers = require("../helpers");
11
9
 
12
10
  var _removeRedundantAttributes = require("./removeRedundantAttributes");
13
11
 
14
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
-
12
+ const terser = (0, _helpers.optionalRequire)('terser');
16
13
  /** Minify JS with Terser */
14
+
17
15
  function minifyJs(tree, options, terserOptions) {
16
+ if (!terser) return tree;
18
17
  let promises = [];
19
18
  tree.walk(node => {
20
19
  if (node.tag && node.tag === 'script') {
@@ -62,7 +61,7 @@ function processScriptNode(scriptNode, terserOptions) {
62
61
  js = strippedJs;
63
62
  }
64
63
 
65
- return _terser.default.minify(js, terserOptions).then(result => {
64
+ return terser.minify(js, terserOptions).then(result => {
66
65
  if (result.error) {
67
66
  throw new Error(result.error);
68
67
  }
@@ -96,14 +95,12 @@ function processNodeWithOnAttrs(node, terserOptions) {
96
95
 
97
96
 
98
97
  let wrappedJs = jsWrapperStart + node.attrs[attrName] + jsWrapperEnd;
99
-
100
- let promise = _terser.default.minify(wrappedJs, terserOptions).then(({
98
+ let promise = terser.minify(wrappedJs, terserOptions).then(({
101
99
  code
102
100
  }) => {
103
101
  let minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
104
102
  node.attrs[attrName] = minifiedJs;
105
103
  });
106
-
107
104
  promises.push(promise);
108
105
  }
109
106
 
@@ -3,31 +3,20 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = minifyJson;
6
+ exports.onContent = onContent;
7
+ const rNodeAttrsTypeJson = /(\/|\+)json/;
7
8
 
8
- /* Minify JSON inside <script> tags */
9
- function minifyJson(tree) {
10
- // Match all <script> tags which have JSON mime type
11
- tree.match({
12
- tag: 'script',
13
- attrs: {
14
- type: /(\/|\+)json/
15
- }
16
- }, node => {
17
- let content = (node.content || []).join('');
18
-
19
- if (!content) {
20
- return node;
21
- }
9
+ function onContent() {
10
+ return (content, node) => {
11
+ let newContent = content;
22
12
 
23
- try {
24
- content = JSON.stringify(JSON.parse(content));
25
- } catch (error) {
26
- return node;
13
+ if (node.attrs && node.attrs.type && rNodeAttrsTypeJson.test(node.attrs.type)) {
14
+ try {
15
+ newContent = JSON.stringify(JSON.parse((content || []).join('')));
16
+ } catch (error) {// Invalid JSON
17
+ }
27
18
  }
28
19
 
29
- node.content = [content];
30
- return node;
31
- });
32
- return tree;
20
+ return newContent;
21
+ };
33
22
  }
@@ -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,15 +5,12 @@ 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
- var _terser = _interopRequireDefault(require("terser"));
13
-
14
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
-
16
- // Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
17
14
  const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'object', 'blockquote', 'q', 'del', 'ins', 'form', 'input', 'head', 'audio', 'embed', 'iframe', 'img', 'script', 'track', 'video']);
18
15
  const tagsHasHrefAttributes = new Set(['a', 'area', 'link', 'base']);
19
16
  const attributesOfImgTagHasUriValues = new Set(['src', 'longdesc', 'usemap']);
@@ -77,7 +74,10 @@ function minifyUrls(tree, options, moduleOptions) {
77
74
  */
78
75
 
79
76
  if (!relateUrlInstance || STORED_URL_BASE !== urlBase) {
80
- relateUrlInstance = new _relateurl.default(urlBase);
77
+ if (RelateUrl) {
78
+ relateUrlInstance = new RelateUrl(urlBase);
79
+ }
80
+
81
81
  STORED_URL_BASE = urlBase;
82
82
  }
83
83
 
@@ -96,25 +96,31 @@ function minifyUrls(tree, options, moduleOptions) {
96
96
  if (isJavaScriptUrl(attrValue)) {
97
97
  promises.push(minifyJavaScriptUrl(node, attrName));
98
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);
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
+ }
104
106
  }
105
107
 
106
108
  continue;
107
109
  }
108
110
 
109
111
  if (isSrcsetAttribute(node.tag, attrNameLower)) {
110
- try {
111
- const parsedSrcset = _srcset.default.parse(attrValue);
112
-
113
- node.attrs[attrName] = _srcset.default.stringify(parsedSrcset.map(srcset => {
114
- srcset.url = relateUrlInstance.relate(srcset.url);
115
- return srcset;
116
- }));
117
- } 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
+ }
118
124
  }
119
125
 
120
126
  continue;
@@ -132,13 +138,14 @@ function isJavaScriptUrl(url) {
132
138
  }
133
139
 
134
140
  function minifyJavaScriptUrl(node, attrName) {
141
+ if (!terser) return Promise.resolve();
135
142
  const jsWrapperStart = 'function a(){';
136
143
  const jsWrapperEnd = '}a();';
137
144
  let result = node.attrs[attrName];
138
145
 
139
146
  if (result) {
140
147
  result = result.slice(JAVASCRIPT_URL_PROTOCOL.length);
141
- return _terser.default.minify(result, {}) // Default Option is good enough
148
+ return terser.minify(result, {}) // Default Option is good enough
142
149
  .then(({
143
150
  code
144
151
  }) => {
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = normalizeAttributeValues;
6
+ exports.onAttrs = onAttrs;
7
7
  const caseInsensitiveAttributes = {
8
8
  autocomplete: ['form'],
9
9
  charset: ['meta', 'script'],
@@ -29,20 +29,14 @@ const caseInsensitiveAttributes = {
29
29
  wrap: ['textarea']
30
30
  };
31
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();
32
+ function onAttrs() {
33
+ return (attrs, node) => {
34
+ const newAttrs = attrs;
35
+ Object.entries(attrs).forEach(([attrName, attrValue]) => {
36
+ if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName) && (caseInsensitiveAttributes[attrName] === null || caseInsensitiveAttributes[attrName].includes(node.tag))) {
37
+ newAttrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
43
38
  }
44
39
  });
45
- return node;
46
- });
47
- return tree;
40
+ return newAttrs;
41
+ };
48
42
  }