htmlnano 2.0.2 → 2.0.4

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 +35 -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 +7763 -11645
  8. package/docs/package.json +3 -3
  9. package/docs/versioned_docs/version-1.1.1/010-introduction.md +4 -4
  10. package/docs/versioned_docs/version-1.1.1/030-config.md +1 -1
  11. package/docs/versioned_docs/version-2.0.0/010-introduction.md +4 -4
  12. package/docs/versioned_docs/version-2.0.0/030-config.md +2 -2
  13. package/index.d.ts +93 -0
  14. package/lib/helpers.js +6 -12
  15. package/lib/htmlnano.js +18 -40
  16. package/lib/modules/collapseAttributeWhitespace.js +11 -12
  17. package/lib/modules/collapseBooleanAttributes.js +33 -9
  18. package/lib/modules/collapseWhitespace.js +17 -19
  19. package/lib/modules/custom.js +0 -3
  20. package/lib/modules/deduplicateAttributeValues.js +3 -5
  21. package/lib/modules/mergeScripts.js +3 -12
  22. package/lib/modules/mergeStyles.js +5 -9
  23. package/lib/modules/minifyConditionalComments.js +4 -15
  24. package/lib/modules/minifyCss.js +9 -16
  25. package/lib/modules/minifyJs.js +25 -29
  26. package/lib/modules/minifyJson.js +6 -3
  27. package/lib/modules/minifySvg.js +17 -9
  28. package/lib/modules/minifyUrls.js +18 -34
  29. package/lib/modules/normalizeAttributeValues.js +80 -2
  30. package/lib/modules/removeAttributeQuotes.js +0 -2
  31. package/lib/modules/removeComments.js +10 -28
  32. package/lib/modules/removeEmptyAttributes.js +6 -6
  33. package/lib/modules/removeOptionalTags.js +9 -46
  34. package/lib/modules/removeRedundantAttributes.js +20 -68
  35. package/lib/modules/removeUnusedCss.js +7 -18
  36. package/lib/modules/sortAttributes.js +10 -25
  37. package/lib/modules/sortAttributesWithLists.js +7 -29
  38. package/lib/presets/ampSafe.js +2 -5
  39. package/lib/presets/max.js +2 -5
  40. package/lib/presets/safe.js +32 -15
  41. package/package.json +15 -21
package/docs/package.json CHANGED
@@ -13,10 +13,10 @@
13
13
  "write-heading-ids": "docusaurus write-heading-ids"
14
14
  },
15
15
  "dependencies": {
16
- "@docusaurus/core": "2.0.0-beta.4",
17
- "@docusaurus/preset-classic": "2.0.0-beta.4",
16
+ "@docusaurus/core": "2.2.0",
17
+ "@docusaurus/preset-classic": "2.2.0",
18
18
  "@mdx-js/react": "^1.6.21",
19
- "@svgr/webpack": "^5.5.0",
19
+ "@svgr/webpack": "^6.5.1",
20
20
  "clsx": "^1.1.1",
21
21
  "file-loader": "^6.2.0",
22
22
  "prism-react-renderer": "^1.2.1",
@@ -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,49 +11,43 @@ 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
- return require(moduleName);
45
+ const module = require(moduleName);
46
+ return module.default || module;
52
47
  } catch (e) {
53
48
  if (e.code === 'MODULE_NOT_FOUND') {
54
49
  return null;
55
50
  }
56
-
57
51
  throw e;
58
52
  }
59
53
  }
package/lib/htmlnano.js CHANGED
@@ -5,84 +5,68 @@ 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
- var _options;
31
-
32
- if (!((_options = options) !== null && _options !== void 0 && _options.skipConfigLoading)) {
21
+ let {
22
+ skipConfigLoading = false,
23
+ ...rest
24
+ } = options || {};
25
+ if (!skipConfigLoading) {
33
26
  const explorer = (0, _cosmiconfig.cosmiconfigSync)(_package.default.name);
34
27
  const rc = configPath ? explorer.load(configPath) : explorer.search();
35
-
36
28
  if (rc) {
37
29
  const {
38
30
  preset: presetName
39
31
  } = rc.config;
40
-
41
32
  if (presetName) {
42
33
  if (!preset && presets[presetName]) {
43
34
  preset = presets[presetName];
44
35
  }
45
-
46
36
  delete rc.config.preset;
47
37
  }
48
-
49
38
  if (!options) {
50
- options = rc.config;
39
+ rest = rc.config;
51
40
  }
52
41
  }
53
42
  }
54
-
55
- return [options || {}, preset || _safe.default];
43
+ return [rest || {}, preset || _safe.default];
56
44
  }
57
-
58
45
  const optionalDependencies = {
59
46
  minifyCss: ['cssnano', 'postcss'],
60
47
  minifyJs: ['terser'],
61
48
  minifyUrl: ['relateurl', 'srcset', 'terser'],
62
49
  minifySvg: ['svgo']
63
50
  };
64
-
65
51
  function htmlnano(optionsRun, presetRun) {
66
52
  let [options, preset] = loadConfig(optionsRun, presetRun);
67
53
  return function minifier(tree) {
68
54
  const nodeHandlers = [];
69
55
  const attrsHandlers = [];
70
56
  const contentsHandlers = [];
71
- options = { ...preset,
57
+ options = {
58
+ ...preset,
72
59
  ...options
73
60
  };
74
61
  let promise = Promise.resolve(tree);
75
-
76
62
  for (const [moduleName, moduleOptions] of Object.entries(options)) {
77
63
  if (!moduleOptions) {
78
64
  // The module is disabled
79
65
  continue;
80
66
  }
81
-
82
67
  if (_safe.default[moduleName] === undefined) {
83
68
  throw new Error('Module "' + moduleName + '" is not defined');
84
69
  }
85
-
86
70
  (optionalDependencies[moduleName] || []).forEach(dependency => {
87
71
  try {
88
72
  require(dependency);
@@ -94,30 +78,23 @@ function htmlnano(optionsRun, presetRun) {
94
78
  }
95
79
  }
96
80
  });
97
-
98
81
  const module = require('./modules/' + moduleName);
99
-
100
82
  if (typeof module.onAttrs === 'function') {
101
83
  attrsHandlers.push(module.onAttrs(options, moduleOptions));
102
84
  }
103
-
104
85
  if (typeof module.onContent === 'function') {
105
86
  contentsHandlers.push(module.onContent(options, moduleOptions));
106
87
  }
107
-
108
88
  if (typeof module.onNode === 'function') {
109
89
  nodeHandlers.push(module.onNode(options, moduleOptions));
110
90
  }
111
-
112
91
  if (typeof module.default === 'function') {
113
92
  promise = promise.then(tree => module.default(tree, options, moduleOptions));
114
93
  }
115
94
  }
116
-
117
95
  if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
118
96
  return promise;
119
97
  }
120
-
121
98
  return promise.then(tree => {
122
99
  tree.walk(node => {
123
100
  if (node) {
@@ -127,17 +104,13 @@ function htmlnano(optionsRun, presetRun) {
127
104
  Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
128
105
  newAttrsObj[attrName.toLowerCase()] = attrValue;
129
106
  });
130
-
131
107
  for (const handler of attrsHandlers) {
132
108
  newAttrsObj = handler(newAttrsObj, node);
133
109
  }
134
-
135
110
  node.attrs = newAttrsObj;
136
111
  }
137
-
138
112
  if (node.content) {
139
113
  node.content = typeof node.content === 'string' ? [node.content] : node.content;
140
-
141
114
  if (Array.isArray(node.content) && node.content.length > 0) {
142
115
  for (const handler of contentsHandlers) {
143
116
  const result = handler(node.content, node);
@@ -145,28 +118,33 @@ function htmlnano(optionsRun, presetRun) {
145
118
  }
146
119
  }
147
120
  }
148
-
149
121
  for (const handler of nodeHandlers) {
150
122
  node = handler(node);
151
123
  }
152
124
  }
153
-
154
125
  return node;
155
126
  });
156
127
  return tree;
157
128
  });
158
129
  };
159
130
  }
160
-
161
131
  htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
162
132
  const [options] = loadConfig(optionsRun, presetRun);
163
133
  return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
164
134
  };
165
-
166
135
  htmlnano.process = function (html, options, preset, postHtmlOptions) {
167
136
  return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
168
137
  };
169
138
 
139
+ // https://github.com/webpack-contrib/html-minimizer-webpack-plugin/blob/faca00f2219514bc671c5942685721f0b5dbaa70/src/utils.js#L74
140
+ htmlnano.htmlMinimizerWebpackPluginMinify = function htmlNano(input, minimizerOptions = {}) {
141
+ const [[, code]] = Object.entries(input);
142
+ return htmlnano.process(code, minimizerOptions, presets.safe).then(result => {
143
+ return {
144
+ code: result.html
145
+ };
146
+ });
147
+ };
170
148
  htmlnano.presets = presets;
171
149
  var _default = htmlnano;
172
150
  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
  }