htmlnano 2.0.1 → 2.0.3

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 (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -2
  3. package/docs/docs/010-introduction.md +4 -4
  4. package/docs/docs/020-usage.md +63 -23
  5. package/docs/docs/030-config.md +1 -1
  6. package/docs/docs/050-modules.md +500 -483
  7. package/docs/package-lock.json +289 -95
  8. package/docs/versioned_docs/version-1.1.1/010-introduction.md +4 -4
  9. package/docs/versioned_docs/version-1.1.1/030-config.md +1 -1
  10. package/docs/versioned_docs/version-2.0.0/010-introduction.md +4 -4
  11. package/docs/versioned_docs/version-2.0.0/030-config.md +2 -2
  12. package/index.d.ts +93 -0
  13. package/lib/helpers.js +4 -11
  14. package/lib/htmlnano.js +37 -55
  15. package/lib/modules/collapseAttributeWhitespace.js +11 -12
  16. package/lib/modules/collapseBooleanAttributes.js +33 -9
  17. package/lib/modules/collapseWhitespace.js +17 -19
  18. package/lib/modules/custom.js +0 -3
  19. package/lib/modules/deduplicateAttributeValues.js +3 -5
  20. package/lib/modules/mergeScripts.js +0 -11
  21. package/lib/modules/mergeStyles.js +2 -8
  22. package/lib/modules/minifyConditionalComments.js +4 -15
  23. package/lib/modules/minifyCss.js +5 -16
  24. package/lib/modules/minifyJs.js +8 -28
  25. package/lib/modules/minifyJson.js +5 -7
  26. package/lib/modules/minifySvg.js +13 -4
  27. package/lib/modules/minifyUrls.js +18 -34
  28. package/lib/modules/normalizeAttributeValues.js +85 -2
  29. package/lib/modules/removeAttributeQuotes.js +0 -2
  30. package/lib/modules/removeComments.js +10 -28
  31. package/lib/modules/removeEmptyAttributes.js +6 -6
  32. package/lib/modules/removeOptionalTags.js +9 -46
  33. package/lib/modules/removeRedundantAttributes.js +20 -68
  34. package/lib/modules/removeUnusedCss.js +7 -18
  35. package/lib/modules/sortAttributes.js +10 -25
  36. package/lib/modules/sortAttributesWithLists.js +7 -29
  37. package/lib/presets/ampSafe.js +2 -5
  38. package/lib/presets/max.js +2 -5
  39. package/lib/presets/safe.js +32 -15
  40. package/package.json +9 -15
  41. package/test.js +0 -48
@@ -5,18 +5,18 @@ slug: /
5
5
 
6
6
  # Introduction
7
7
 
8
- Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml).
8
+ Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml).
9
9
  Inspired by [cssnano](http://cssnano.co/).
10
10
 
11
11
 
12
12
  ## [Benchmark](https://github.com/maltsev/html-minifiers-benchmark/blob/master/README.md)
13
13
  [html-minifier-terser@5.1.1]: https://www.npmjs.com/package/html-minifier-terser
14
- [htmlnano@1.0.0]: https://www.npmjs.com/package/htmlnano
14
+ [htmlnano@1.1.1]: https://www.npmjs.com/package/htmlnano
15
15
 
16
- | Website | Source (KB) | [html-minifier-terser@5.1.1] | [htmlnano@1.0.0] |
16
+ | Website | Source (KB) | [html-minifier-terser@5.1.1] | [htmlnano@1.1.1] |
17
17
  |---------|------------:|----------------:|-----------:|
18
18
  | [stackoverflow.blog](https://stackoverflow.blog/) | 95 | 87 | 82 |
19
19
  | [github.com](https://github.com/) | 210 | 183 | 171 |
20
20
  | [en.wikipedia.org](https://en.wikipedia.org/wiki/Main_Page) | 78 | 72 | 72 |
21
21
  | [npmjs.com](https://www.npmjs.com/features) | 41 | 38 | 36 |
22
- | **Avg. minify rate** | 0% | **9%** | **13%** |
22
+ | **Avg. minify rate** | 0% | **9%** | **13%** |
@@ -6,7 +6,7 @@ There are two main ways to configure htmlnano:
6
6
  This is the way described above in the examples.
7
7
 
8
8
  ## Using configuration file
9
- Alternatively, you might create a configuration file (e.g., `htmlanorc.json` or `htmlnanorc.js`) or save options to `package.json` with `htmlnano` key.
9
+ Alternatively, you might create a configuration file (e.g., `htmlnanorc.json` or `htmlnanorc.js`) or save options to `package.json` with `htmlnano` key.
10
10
  `htmlnano` uses `cosmiconfig`, so refer to [its documentation](https://github.com/davidtheclark/cosmiconfig/blob/main/README.md) for a more detailed description.
11
11
 
12
12
  If you want to specify a preset that way, use `preset` key:
@@ -5,18 +5,18 @@ slug: /
5
5
 
6
6
  # Introduction
7
7
 
8
- Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml).
8
+ Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml).
9
9
  Inspired by [cssnano](http://cssnano.co/).
10
10
 
11
11
 
12
12
  ## [Benchmark](https://github.com/maltsev/html-minifiers-benchmark/blob/master/README.md)
13
13
  [html-minifier-terser@5.1.1]: https://www.npmjs.com/package/html-minifier-terser
14
- [htmlnano@1.0.0]: https://www.npmjs.com/package/htmlnano
14
+ [htmlnano@2.0.0]: https://www.npmjs.com/package/htmlnano
15
15
 
16
- | Website | Source (KB) | [html-minifier-terser@5.1.1] | [htmlnano@1.0.0] |
16
+ | Website | Source (KB) | [html-minifier-terser@5.1.1] | [htmlnano@2.0.0] |
17
17
  |---------|------------:|----------------:|-----------:|
18
18
  | [stackoverflow.blog](https://stackoverflow.blog/) | 95 | 87 | 82 |
19
19
  | [github.com](https://github.com/) | 210 | 183 | 171 |
20
20
  | [en.wikipedia.org](https://en.wikipedia.org/wiki/Main_Page) | 78 | 72 | 72 |
21
21
  | [npmjs.com](https://www.npmjs.com/features) | 41 | 38 | 36 |
22
- | **Avg. minify rate** | 0% | **9%** | **13%** |
22
+ | **Avg. minify rate** | 0% | **9%** | **13%** |
@@ -6,7 +6,7 @@ There are two main ways to configure htmlnano:
6
6
  This is the way described above in the examples.
7
7
 
8
8
  ## Using configuration file
9
- Alternatively, you might create a configuration file (e.g., `htmlanorc.json` or `htmlnanorc.js`) or save options to `package.json` with `htmlnano` key.
9
+ Alternatively, you might create a configuration file (e.g., `htmlnanorc.json` or `htmlnanorc.js`) or save options to `package.json` with `htmlnano` key.
10
10
  `htmlnano` uses `cosmiconfig`, so refer to [its documentation](https://github.com/davidtheclark/cosmiconfig/blob/main/README.md) for a more detailed description.
11
11
 
12
12
  If you want to specify a preset that way, use `preset` key:
@@ -18,4 +18,4 @@ If you want to specify a preset that way, use `preset` key:
18
18
  ```
19
19
 
20
20
  Configuration files have lower precedence than passing options to `htmlnano` directly.
21
- So if you use both ways, then the configuration file would be ignored.
21
+ So if you use both ways, then the configuration file would be ignored.
package/index.d.ts ADDED
@@ -0,0 +1,93 @@
1
+ import type PostHTML from "posthtml";
2
+ import type terser from "terser";
3
+ import type { Options as CssNanoOptions } from "cssnano";
4
+ import type { OptimizeOptions as SvgoOptimizeOptions } from "svgo";
5
+
6
+ export interface HtmlnanoOptions {
7
+ skipConfigLoading?: boolean;
8
+ collapseAttributeWhitespace?: boolean;
9
+ collapseBooleanAttributes?: {
10
+ amphtml?: boolean;
11
+ };
12
+ collapseWhitespace?: "conservative" | "all" | "aggressive";
13
+ custom?: (tree: PostHTML.Node, options?: any) => PostHTML.Node;
14
+ deduplicateAttributeValues?: boolean;
15
+ minifyUrls?: URL | string | false;
16
+ mergeStyles?: boolean;
17
+ mergeScripts?: boolean;
18
+ minifyCss?: CssNanoOptions | boolean;
19
+ minifyConditionalComments?: boolean;
20
+ minifyJs?: terser.FormatOptions | boolean;
21
+ minifyJson?: boolean;
22
+ minifySvg?: SvgoOptimizeOptions | boolean;
23
+ normalizeAttributeValues?: boolean;
24
+ removeAttributeQuotes?: boolean;
25
+ removeComments?: boolean | "safe" | "all" | RegExp | (() => boolean);
26
+ removeEmptyAttributes?: boolean;
27
+ removeRedundantAttributes?: boolean;
28
+ removeOptionalTags?: boolean;
29
+ removeUnusedCss?: boolean;
30
+ sortAttributes?: boolean | "alphabetical" | "frequency";
31
+ sortAttributesWithLists?: boolean | "alphabetical" | "frequency";
32
+ }
33
+
34
+ export interface HtmlnanoPreset extends Omit<HtmlnanoOptions, "skipConfigLoading"> {}
35
+
36
+ export interface Presets {
37
+ safe: HtmlnanoPreset;
38
+ ampSafe: HtmlnanoPreset;
39
+ max: HtmlnanoPreset;
40
+ }
41
+
42
+ type Preset = Presets[keyof Presets];
43
+
44
+ export function loadConfig(
45
+ options?: HtmlnanoOptions,
46
+ preset?: Preset,
47
+ configPath?: string
48
+ ): [HtmlnanoOptions | {}, Preset];
49
+
50
+ declare function htmlnano<TMessage>(
51
+ options?: HtmlnanoOptions,
52
+ preset?: Preset
53
+ ): (tree: PostHTML.Node) => Promise<PostHTML.Result<TMessage>>;
54
+
55
+ interface PostHtmlOptions {
56
+ directives?: Array<{
57
+ name: string | RegExp;
58
+ start: string;
59
+ end: string;
60
+ }>;
61
+ sourceLocations?: boolean;
62
+ recognizeNoValueAttribute?: boolean;
63
+ xmlMode?: boolean;
64
+ decodeEntities?: boolean;
65
+ lowerCaseTags?: boolean;
66
+ lowerCaseAttributeNames?: boolean;
67
+ recognizeCDATA?: boolean;
68
+ recognizeSelfClosing?: boolean;
69
+ Tokenizer?: any;
70
+ }
71
+
72
+ declare namespace htmlnano {
73
+ export function process<TMessage>(
74
+ html: string,
75
+ options?: HtmlnanoOptions,
76
+ preset?: Preset,
77
+ postHtmlOptions?: PostHtmlOptions
78
+ ): Promise<PostHTML.Result<TMessage>>;
79
+
80
+ export function getRequiredOptionalDependencies(
81
+ optionsRun?: HtmlnanoOptions,
82
+ presetRun?: Preset
83
+ ): string[];
84
+
85
+ export function htmlMinimizerWebpackPluginMinify(
86
+ input: { [file: string]: string },
87
+ minimizerOptions?: HtmlnanoOptions
88
+ ): any;
89
+
90
+ export const presets: Presets;
91
+ }
92
+
93
+ export default htmlnano;
package/lib/helpers.js CHANGED
@@ -11,41 +11,35 @@ exports.isEventHandler = isEventHandler;
11
11
  exports.isStyleNode = isStyleNode;
12
12
  exports.optionalRequire = optionalRequire;
13
13
  const ampBoilerplateAttributes = ['amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate'];
14
-
15
14
  function isAmpBoilerplate(node) {
16
15
  if (!node.attrs) {
17
16
  return false;
18
17
  }
19
-
20
18
  for (const attr of ampBoilerplateAttributes) {
21
19
  if (attr in node.attrs) {
22
20
  return true;
23
21
  }
24
22
  }
25
-
26
23
  return false;
27
24
  }
28
-
29
25
  function isComment(content) {
30
- return (content || '').trim().startsWith('<!--');
26
+ if (typeof content === 'string') {
27
+ return content.trim().startsWith('<!--');
28
+ }
29
+ return false;
31
30
  }
32
-
33
31
  function isConditionalComment(content) {
34
32
  return (content || '').trim().startsWith('<!--[if');
35
33
  }
36
-
37
34
  function isStyleNode(node) {
38
35
  return node.tag === 'style' && !isAmpBoilerplate(node) && 'content' in node && node.content.length > 0;
39
36
  }
40
-
41
37
  function extractCssFromStyleNode(node) {
42
38
  return Array.isArray(node.content) ? node.content.join(' ') : node.content;
43
39
  }
44
-
45
40
  function isEventHandler(attributeName) {
46
41
  return attributeName && attributeName.slice && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
47
42
  }
48
-
49
43
  function optionalRequire(moduleName) {
50
44
  try {
51
45
  return require(moduleName);
@@ -53,7 +47,6 @@ function optionalRequire(moduleName) {
53
47
  if (e.code === 'MODULE_NOT_FOUND') {
54
48
  return null;
55
49
  }
56
-
57
50
  throw e;
58
51
  }
59
52
  }
package/lib/htmlnano.js CHANGED
@@ -5,84 +5,65 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = void 0;
7
7
  exports.loadConfig = loadConfig;
8
-
9
8
  var _posthtml = _interopRequireDefault(require("posthtml"));
10
-
11
9
  var _cosmiconfig = require("cosmiconfig");
12
-
13
10
  var _safe = _interopRequireDefault(require("./presets/safe"));
14
-
15
11
  var _ampSafe = _interopRequireDefault(require("./presets/ampSafe"));
16
-
17
12
  var _max = _interopRequireDefault(require("./presets/max"));
18
-
19
13
  var _package = _interopRequireDefault(require("../package.json"));
20
-
21
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
22
-
23
15
  const presets = {
24
16
  safe: _safe.default,
25
17
  ampSafe: _ampSafe.default,
26
18
  max: _max.default
27
19
  };
28
-
29
20
  function loadConfig(options, preset, configPath) {
30
21
  var _options;
31
-
32
22
  if (!((_options = options) !== null && _options !== void 0 && _options.skipConfigLoading)) {
33
23
  const explorer = (0, _cosmiconfig.cosmiconfigSync)(_package.default.name);
34
24
  const rc = configPath ? explorer.load(configPath) : explorer.search();
35
-
36
25
  if (rc) {
37
26
  const {
38
27
  preset: presetName
39
28
  } = rc.config;
40
-
41
29
  if (presetName) {
42
30
  if (!preset && presets[presetName]) {
43
31
  preset = presets[presetName];
44
32
  }
45
-
46
33
  delete rc.config.preset;
47
34
  }
48
-
49
35
  if (!options) {
50
36
  options = rc.config;
51
37
  }
52
38
  }
53
39
  }
54
-
55
40
  return [options || {}, preset || _safe.default];
56
41
  }
57
-
58
42
  const optionalDependencies = {
59
43
  minifyCss: ['cssnano', 'postcss'],
60
44
  minifyJs: ['terser'],
61
45
  minifyUrl: ['relateurl', 'srcset', 'terser'],
62
46
  minifySvg: ['svgo']
63
47
  };
64
-
65
48
  function htmlnano(optionsRun, presetRun) {
66
49
  let [options, preset] = loadConfig(optionsRun, presetRun);
67
50
  return function minifier(tree) {
68
51
  const nodeHandlers = [];
69
52
  const attrsHandlers = [];
70
53
  const contentsHandlers = [];
71
- options = { ...preset,
54
+ options = {
55
+ ...preset,
72
56
  ...options
73
57
  };
74
58
  let promise = Promise.resolve(tree);
75
-
76
59
  for (const [moduleName, moduleOptions] of Object.entries(options)) {
77
60
  if (!moduleOptions) {
78
61
  // The module is disabled
79
62
  continue;
80
63
  }
81
-
82
64
  if (_safe.default[moduleName] === undefined) {
83
65
  throw new Error('Module "' + moduleName + '" is not defined');
84
66
  }
85
-
86
67
  (optionalDependencies[moduleName] || []).forEach(dependency => {
87
68
  try {
88
69
  require(dependency);
@@ -94,72 +75,73 @@ function htmlnano(optionsRun, presetRun) {
94
75
  }
95
76
  }
96
77
  });
97
-
98
78
  const module = require('./modules/' + moduleName);
99
-
100
- if (module.onAttrs) {
79
+ if (typeof module.onAttrs === 'function') {
101
80
  attrsHandlers.push(module.onAttrs(options, moduleOptions));
102
81
  }
103
-
104
- if (module.onContent) {
82
+ if (typeof module.onContent === 'function') {
105
83
  contentsHandlers.push(module.onContent(options, moduleOptions));
106
84
  }
107
-
108
- if (module.onNode) {
85
+ if (typeof module.onNode === 'function') {
109
86
  nodeHandlers.push(module.onNode(options, moduleOptions));
110
87
  }
111
-
112
- if (module.default) {
88
+ if (typeof module.default === 'function') {
113
89
  promise = promise.then(tree => module.default(tree, options, moduleOptions));
114
90
  }
115
91
  }
116
-
117
92
  if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
118
93
  return promise;
119
94
  }
120
-
121
95
  return promise.then(tree => {
122
96
  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);
97
+ if (node) {
98
+ if (node.attrs && typeof node.attrs === 'object') {
99
+ // Convert all attrs' key to lower case
100
+ let newAttrsObj = {};
101
+ Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
102
+ newAttrsObj[attrName.toLowerCase()] = attrValue;
103
+ });
104
+ for (const handler of attrsHandlers) {
105
+ newAttrsObj = handler(newAttrsObj, node);
106
+ }
107
+ node.attrs = newAttrsObj;
132
108
  }
133
-
134
- node.attrs = newAttrsObj;
135
- }
136
-
137
- if (node.content) {
138
- for (const handler of contentsHandlers) {
139
- node.content = handler(node.content, node);
109
+ if (node.content) {
110
+ node.content = typeof node.content === 'string' ? [node.content] : node.content;
111
+ if (Array.isArray(node.content) && node.content.length > 0) {
112
+ for (const handler of contentsHandlers) {
113
+ const result = handler(node.content, node);
114
+ node.content = typeof result === 'string' ? [result] : result;
115
+ }
116
+ }
117
+ }
118
+ for (const handler of nodeHandlers) {
119
+ node = handler(node);
140
120
  }
141
121
  }
142
-
143
- for (const handler of nodeHandlers) {
144
- node = handler(node, node);
145
- }
146
-
147
122
  return node;
148
123
  });
149
124
  return tree;
150
125
  });
151
126
  };
152
127
  }
153
-
154
128
  htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
155
129
  const [options] = loadConfig(optionsRun, presetRun);
156
130
  return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
157
131
  };
158
-
159
132
  htmlnano.process = function (html, options, preset, postHtmlOptions) {
160
133
  return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
161
134
  };
162
135
 
136
+ // https://github.com/webpack-contrib/html-minimizer-webpack-plugin/blob/faca00f2219514bc671c5942685721f0b5dbaa70/src/utils.js#L74
137
+ htmlnano.htmlMinimizerWebpackPluginMinify = function htmlNano(input, minimizerOptions = {}) {
138
+ const [[, code]] = Object.entries(input);
139
+ return htmlnano.process(code, minimizerOptions, presets.safe).then(result => {
140
+ return {
141
+ code: result.html
142
+ };
143
+ });
144
+ };
163
145
  htmlnano.presets = presets;
164
146
  var _default = htmlnano;
165
147
  exports.default = _default;
@@ -5,13 +5,13 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.attributesWithLists = void 0;
7
7
  exports.onAttrs = onAttrs;
8
-
9
8
  var _helpers = require("../helpers");
10
-
11
- const attributesWithLists = new Set(['class', 'dropzone', 'rel', // a, area, link
12
- 'ping', // a, area
13
- 'sandbox', // iframe
14
-
9
+ const attributesWithLists = new Set(['class', 'dropzone', 'rel',
10
+ // a, area, link
11
+ 'ping',
12
+ // a, area
13
+ 'sandbox',
14
+ // iframe
15
15
  /**
16
16
  * https://github.com/posthtml/htmlnano/issues/180
17
17
  * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes
@@ -21,8 +21,8 @@ const attributesWithLists = new Set(['class', 'dropzone', 'rel', // a, area, lin
21
21
  // 'sizes', // link
22
22
  'headers' // td, th
23
23
  ]);
24
- /** @type Record<string, string[] | null> */
25
24
 
25
+ /** @type Record<string, string[] | null> */
26
26
  exports.attributesWithLists = attributesWithLists;
27
27
  const attributesWithSingleValue = {
28
28
  accept: ['input'],
@@ -63,26 +63,25 @@ const attributesWithSingleValue = {
63
63
  value: ['li', 'meter', 'progress'],
64
64
  width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
65
65
  };
66
- /** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
67
66
 
67
+ /** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
68
68
  function onAttrs() {
69
69
  return (attrs, node) => {
70
70
  const newAttrs = attrs;
71
71
  Object.entries(attrs).forEach(([attrName, attrValue]) => {
72
+ if (typeof attrValue !== 'string') return;
72
73
  if (attributesWithLists.has(attrName)) {
73
74
  const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
74
75
  newAttrs[attrName] = newAttrValue;
75
76
  return;
76
77
  }
77
-
78
- if ((0, _helpers.isEventHandler)(attrName) || Object.hasOwnProperty.call(attributesWithSingleValue, attrName) && (attributesWithSingleValue[attrName] === null || attributesWithSingleValue[attrName].includes(node.tag))) {
78
+ if ((0, _helpers.isEventHandler)(attrName) || Object.prototype.hasOwnProperty.call(attributesWithSingleValue, attrName) && (attributesWithSingleValue[attrName] === null || attributesWithSingleValue[attrName].includes(node.tag))) {
79
79
  newAttrs[attrName] = minifySingleAttributeValue(attrValue);
80
80
  }
81
81
  });
82
82
  return newAttrs;
83
83
  };
84
84
  }
85
-
86
85
  function minifySingleAttributeValue(value) {
87
- return typeof value === 'string' ? String(value).trim() : value;
86
+ return typeof value === 'string' ? value.trim() : value;
88
87
  }
@@ -5,34 +5,58 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.onAttrs = onAttrs;
7
7
  // Source: https://github.com/kangax/html-minifier/issues/63
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']);
8
+ // https://html.spec.whatwg.org/#boolean-attribute
9
+ // https://html.spec.whatwg.org/#attributes-1
10
+ 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', 'nomodule', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'playsinline', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
9
11
  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
-
12
+ const missingValueDefaultEmptyStringAttributes = {
13
+ // https://html.spec.whatwg.org/#attr-media-preload
14
+ audio: {
15
+ preload: 'auto'
16
+ },
17
+ video: {
18
+ preload: 'auto'
19
+ }
20
+ };
21
+ const tagsHasMissingValueDefaultEmptyStringAttributes = new Set(Object.keys(missingValueDefaultEmptyStringAttributes));
11
22
  function onAttrs(options, moduleOptions) {
12
23
  return (attrs, node) => {
13
24
  if (!node.tag) return attrs;
14
25
  const newAttrs = attrs;
15
-
26
+ if (tagsHasMissingValueDefaultEmptyStringAttributes.has(node.tag)) {
27
+ const tagAttributesCanBeReplacedWithEmptyString = missingValueDefaultEmptyStringAttributes[node.tag];
28
+ for (const attributesCanBeReplacedWithEmptyString of Object.keys(tagAttributesCanBeReplacedWithEmptyString)) {
29
+ if (Object.prototype.hasOwnProperty.call(attrs, attributesCanBeReplacedWithEmptyString) && attrs[attributesCanBeReplacedWithEmptyString] === tagAttributesCanBeReplacedWithEmptyString[attributesCanBeReplacedWithEmptyString]) {
30
+ attrs[attributesCanBeReplacedWithEmptyString] = true;
31
+ }
32
+ }
33
+ }
16
34
  for (const attrName of Object.keys(attrs)) {
17
35
  if (attrName === 'visible' && node.tag.startsWith('a-')) {
18
36
  continue;
19
37
  }
20
-
21
38
  if (htmlBooleanAttributes.has(attrName)) {
22
39
  newAttrs[attrName] = true;
23
40
  }
24
41
 
42
+ // Fast path optimization.
43
+ // The rest of tranformations are only for string type attrValue.
44
+ if (typeof newAttrs[attrName] !== 'string') continue;
25
45
  if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && attrs[attrName] === '') {
26
46
  newAttrs[attrName] = true;
27
- } // collapse crossorigin attributes
28
- // Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
29
-
47
+ }
48
+ // https://html.spec.whatwg.org/#a-quick-introduction-to-html
49
+ // The value, along with the "=" character, can be omitted altogether if the value is the empty string.
50
+ if (attrs[attrName] === '') {
51
+ newAttrs[attrName] = true;
52
+ }
30
53
 
31
- if (attrName.toLowerCase() === 'crossorigin' && (attrs[attrName] === 'anonymous' || attrs[attrName] === '')) {
54
+ // collapse crossorigin attributes
55
+ // Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
56
+ if (attrName.toLowerCase() === 'crossorigin' && attrs[attrName] === 'anonymous') {
32
57
  newAttrs[attrName] = true;
33
58
  }
34
59
  }
35
-
36
60
  return newAttrs;
37
61
  };
38
62
  }
@@ -4,14 +4,15 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = collapseWhitespace;
7
-
8
7
  var _helpers = require("../helpers");
9
-
10
8
  const noWhitespaceCollapseElements = new Set(['script', 'style', 'pre', 'textarea']);
11
- const noTrimWhitespacesArroundElements = new Set([// non-empty tags that will maintain whitespace around them
12
- 'a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'ins', 'kbd', 'label', 'mark', 'math', 'nobr', 'object', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', // self-closing tags that will maintain whitespace around them
9
+ const noTrimWhitespacesArroundElements = new Set([
10
+ // non-empty tags that will maintain whitespace around them
11
+ 'a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'ins', 'kbd', 'label', 'mark', 'math', 'nobr', 'object', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var',
12
+ // self-closing tags that will maintain whitespace around them
13
13
  'comment', 'img', 'input', 'wbr']);
14
- const noTrimWhitespacesInsideElements = new Set([// non-empty tags that will maintain whitespace within them
14
+ const noTrimWhitespacesInsideElements = new Set([
15
+ // non-empty tags that will maintain whitespace within them
15
16
  '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
17
  const startsWithWhitespacePattern = /^\s/;
17
18
  const endsWithWhitespacePattern = /\s$/;
@@ -19,14 +20,13 @@ const multipleWhitespacePattern = /\s+/g;
19
20
  const NONE = '';
20
21
  const SINGLE_SPACE = ' ';
21
22
  const validOptions = ['all', 'aggressive', 'conservative'];
22
- /** Collapses redundant whitespaces */
23
23
 
24
+ /** Collapses redundant whitespaces */
24
25
  function collapseWhitespace(tree, options, collapseType, parent) {
25
26
  collapseType = validOptions.includes(collapseType) ? collapseType : 'conservative';
26
27
  tree.forEach((node, index) => {
27
28
  const prevNode = tree[index - 1];
28
29
  const nextNode = tree[index + 1];
29
-
30
30
  if (typeof node === 'string') {
31
31
  const parentNodeTag = parent && parent.node && parent.node.tag;
32
32
  const isTopLevel = !parentNodeTag || parentNodeTag === 'html' || parentNodeTag === 'head';
@@ -38,9 +38,7 @@ function collapseWhitespace(tree, options, collapseType, parent) {
38
38
  collapseType === 'aggressive';
39
39
  node = collapseRedundantWhitespaces(node, collapseType, shouldTrim, parent, prevNode, nextNode);
40
40
  }
41
-
42
41
  const isAllowCollapseWhitespace = !noWhitespaceCollapseElements.has(node.tag);
43
-
44
42
  if (node.content && node.content.length && isAllowCollapseWhitespace) {
45
43
  node.content = collapseWhitespace(node.content, options, collapseType, {
46
44
  node,
@@ -48,37 +46,38 @@ function collapseWhitespace(tree, options, collapseType, parent) {
48
46
  nextNode
49
47
  });
50
48
  }
51
-
52
49
  tree[index] = node;
53
50
  });
54
51
  return tree;
55
52
  }
56
-
57
53
  function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false, parent, prevNode, nextNode) {
58
54
  if (!text || text.length === 0) {
59
55
  return NONE;
60
56
  }
61
-
62
57
  if (!(0, _helpers.isComment)(text)) {
63
58
  text = text.replace(multipleWhitespacePattern, SINGLE_SPACE);
64
59
  }
65
-
66
60
  if (shouldTrim) {
67
61
  if (collapseType === 'aggressive') {
68
62
  if (!noTrimWhitespacesInsideElements.has(parent && parent.node && parent.node.tag)) {
69
- if (!noTrimWhitespacesArroundElements.has(prevNode && prevNode.tag)) {
63
+ if (
64
+ // It is the first child node of the parent
65
+ !prevNode
66
+ // It is not the first child node, and prevNode not a text node, and prevNode is safe to trim around
67
+ || prevNode && prevNode.tag && !noTrimWhitespacesArroundElements.has(prevNode.tag)) {
70
68
  text = text.trimStart();
71
69
  } else {
72
70
  // previous node is a "no trim whitespaces arround element"
73
- if ( // but previous node ends with a whitespace
71
+ if (
72
+ // but previous node ends with a whitespace
74
73
  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
74
+ ||
75
+ // or the next node starts with a white space
76
76
  nextNode && nextNode.content && nextNode.content.length && !startsWithWhitespacePattern.test(nextNode.content[0]))) {
77
77
  text = text.trimStart();
78
78
  }
79
79
  }
80
-
81
- if (!noTrimWhitespacesArroundElements.has(nextNode && nextNode.tag)) {
80
+ if (!nextNode || nextNode && nextNode.tag && !noTrimWhitespacesArroundElements.has(nextNode.tag)) {
82
81
  text = text.trimEnd();
83
82
  }
84
83
  } else {
@@ -96,6 +95,5 @@ function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false, pa
96
95
  text = text.trim();
97
96
  }
98
97
  }
99
-
100
98
  return text;
101
99
  }