htmlnano 2.0.2 → 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 +6 -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 +11 -36
  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 +2 -3
  26. package/lib/modules/minifySvg.js +13 -5
  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,30 +75,23 @@ function htmlnano(optionsRun, presetRun) {
94
75
  }
95
76
  }
96
77
  });
97
-
98
78
  const module = require('./modules/' + moduleName);
99
-
100
79
  if (typeof module.onAttrs === 'function') {
101
80
  attrsHandlers.push(module.onAttrs(options, moduleOptions));
102
81
  }
103
-
104
82
  if (typeof module.onContent === 'function') {
105
83
  contentsHandlers.push(module.onContent(options, moduleOptions));
106
84
  }
107
-
108
85
  if (typeof module.onNode === 'function') {
109
86
  nodeHandlers.push(module.onNode(options, moduleOptions));
110
87
  }
111
-
112
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
97
  if (node) {
@@ -127,17 +101,13 @@ function htmlnano(optionsRun, presetRun) {
127
101
  Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
128
102
  newAttrsObj[attrName.toLowerCase()] = attrValue;
129
103
  });
130
-
131
104
  for (const handler of attrsHandlers) {
132
105
  newAttrsObj = handler(newAttrsObj, node);
133
106
  }
134
-
135
107
  node.attrs = newAttrsObj;
136
108
  }
137
-
138
109
  if (node.content) {
139
110
  node.content = typeof node.content === 'string' ? [node.content] : node.content;
140
-
141
111
  if (Array.isArray(node.content) && node.content.length > 0) {
142
112
  for (const handler of contentsHandlers) {
143
113
  const result = handler(node.content, node);
@@ -145,28 +115,33 @@ function htmlnano(optionsRun, presetRun) {
145
115
  }
146
116
  }
147
117
  }
148
-
149
118
  for (const handler of nodeHandlers) {
150
119
  node = handler(node);
151
120
  }
152
121
  }
153
-
154
122
  return node;
155
123
  });
156
124
  return tree;
157
125
  });
158
126
  };
159
127
  }
160
-
161
128
  htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
162
129
  const [options] = loadConfig(optionsRun, presetRun);
163
130
  return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
164
131
  };
165
-
166
132
  htmlnano.process = function (html, options, preset, postHtmlOptions) {
167
133
  return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
168
134
  };
169
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
+ };
170
145
  htmlnano.presets = presets;
171
146
  var _default = htmlnano;
172
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
  }
@@ -4,17 +4,14 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = custom;
7
-
8
7
  /** Meta-module that runs custom modules */
9
8
  function custom(tree, options, customModules) {
10
9
  if (!customModules) {
11
10
  return tree;
12
11
  }
13
-
14
12
  if (!Array.isArray(customModules)) {
15
13
  customModules = [customModules];
16
14
  }
17
-
18
15
  customModules.forEach(customModule => {
19
16
  tree = customModule(tree, options);
20
17
  });