htmlnano 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/htmlnano.js CHANGED
@@ -65,6 +65,9 @@ const optionalDependencies = {
65
65
  function htmlnano(optionsRun, presetRun) {
66
66
  let [options, preset] = loadConfig(optionsRun, presetRun);
67
67
  return function minifier(tree) {
68
+ const nodeHandlers = [];
69
+ const attrsHandlers = [];
70
+ const contentsHandlers = [];
68
71
  options = { ...preset,
69
72
  ...options
70
73
  };
@@ -92,12 +95,59 @@ function htmlnano(optionsRun, presetRun) {
92
95
  }
93
96
  });
94
97
 
95
- let module = require('./modules/' + moduleName);
98
+ const module = require('./modules/' + moduleName);
96
99
 
97
- promise = promise.then(tree => module.default(tree, options, moduleOptions));
100
+ if (module.onAttrs) {
101
+ attrsHandlers.push(module.onAttrs(options, moduleOptions));
102
+ }
103
+
104
+ if (module.onContent) {
105
+ contentsHandlers.push(module.onContent(options, moduleOptions));
106
+ }
107
+
108
+ if (module.onNode) {
109
+ nodeHandlers.push(module.onNode(options, moduleOptions));
110
+ }
111
+
112
+ if (module.default) {
113
+ promise = promise.then(tree => module.default(tree, options, moduleOptions));
114
+ }
98
115
  }
99
116
 
100
- return promise;
117
+ if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
118
+ return promise;
119
+ }
120
+
121
+ return promise.then(tree => {
122
+ tree.walk(node => {
123
+ if (node.attrs) {
124
+ // Convert all attrs' key to lower case
125
+ let newAttrsObj = {};
126
+ Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
127
+ newAttrsObj[attrName.toLowerCase()] = attrValue;
128
+ });
129
+
130
+ for (const handler of attrsHandlers) {
131
+ newAttrsObj = handler(newAttrsObj, node);
132
+ }
133
+
134
+ node.attrs = newAttrsObj;
135
+ }
136
+
137
+ if (node.content) {
138
+ for (const handler of contentsHandlers) {
139
+ node.content = handler(node.content, node);
140
+ }
141
+ }
142
+
143
+ for (const handler of nodeHandlers) {
144
+ node = handler(node, node);
145
+ }
146
+
147
+ return node;
148
+ });
149
+ return tree;
150
+ });
101
151
  };
102
152
  }
103
153
 
@@ -4,14 +4,21 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.attributesWithLists = void 0;
7
- exports.default = collapseAttributeWhitespace;
7
+ exports.onAttrs = onAttrs;
8
8
 
9
9
  var _helpers = require("../helpers");
10
10
 
11
11
  const attributesWithLists = new Set(['class', 'dropzone', 'rel', // a, area, link
12
12
  'ping', // a, area
13
13
  'sandbox', // iframe
14
- 'sizes', // link
14
+
15
+ /**
16
+ * https://github.com/posthtml/htmlnano/issues/180
17
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes
18
+ *
19
+ * "sizes" of <img> should not be modified, while "sizes" of <link> will only have one entry in most cases.
20
+ */
21
+ // 'sizes', // link
15
22
  'headers' // td, th
16
23
  ]);
17
24
  /** @type Record<string, string[] | null> */
@@ -58,29 +65,22 @@ const attributesWithSingleValue = {
58
65
  };
59
66
  /** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
60
67
 
61
- function collapseAttributeWhitespace(tree) {
62
- tree.walk(node => {
63
- if (!node.attrs) {
64
- return node;
65
- }
66
-
67
- Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
68
- const attrNameLower = attrName.toLowerCase();
69
-
70
- if (attributesWithLists.has(attrNameLower)) {
68
+ function onAttrs() {
69
+ return (attrs, node) => {
70
+ const newAttrs = attrs;
71
+ Object.entries(attrs).forEach(([attrName, attrValue]) => {
72
+ if (attributesWithLists.has(attrName)) {
71
73
  const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
72
- node.attrs[attrName] = newAttrValue;
73
- return node;
74
+ newAttrs[attrName] = newAttrValue;
75
+ return;
74
76
  }
75
77
 
76
- if ((0, _helpers.isEventHandler)(attrNameLower) || Object.hasOwnProperty.call(attributesWithSingleValue, attrNameLower) && (attributesWithSingleValue[attrNameLower] === null || attributesWithSingleValue[attrNameLower].includes(node.tag))) {
77
- node.attrs[attrName] = minifySingleAttributeValue(attrValue);
78
- return node;
78
+ if ((0, _helpers.isEventHandler)(attrName) || Object.hasOwnProperty.call(attributesWithSingleValue, attrName) && (attributesWithSingleValue[attrName] === null || attributesWithSingleValue[attrName].includes(node.tag))) {
79
+ newAttrs[attrName] = minifySingleAttributeValue(attrValue);
79
80
  }
80
81
  });
81
- return node;
82
- });
83
- return tree;
82
+ return newAttrs;
83
+ };
84
84
  }
85
85
 
86
86
  function minifySingleAttributeValue(value) {
@@ -3,42 +3,36 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = collapseBooleanAttributes;
6
+ exports.onAttrs = onAttrs;
7
7
  // Source: https://github.com/kangax/html-minifier/issues/63
8
8
  const htmlBooleanAttributes = new Set(['allowfullscreen', 'allowpaymentrequest', 'allowtransparency', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
9
9
  const amphtmlBooleanAttributes = new Set(['⚡', 'amp', '⚡4ads', 'amp4ads', '⚡4email', 'amp4email', 'amp-custom', 'amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate', 'allow-blocked-ranges', 'amp-access-hide', 'amp-access-template', 'amp-keyframes', 'animate', 'arrows', 'data-block-on-consent', 'data-enable-refresh', 'data-multi-size', 'date-template', 'disable-double-tap', 'disable-session-states', 'disableremoteplayback', 'dots', 'expand-single-section', 'expanded', 'fallback', 'first', 'fullscreen', 'inline', 'lightbox', 'noaudio', 'noautoplay', 'noloading', 'once', 'open-after-clear', 'open-after-select', 'open-button', 'placeholder', 'preload', 'reset-on-refresh', 'reset-on-resize', 'resizable', 'rotate-to-fullscreen', 'second', 'standalone', 'stereo', 'submit-error', 'submit-success', 'submitting', 'subscriptions-actions', 'subscriptions-dialog']);
10
10
 
11
- function collapseBooleanAttributes(tree, options, moduleOptions) {
12
- tree.walk(node => {
13
- if (!node.attrs) {
14
- return node;
15
- }
16
-
17
- if (!node.tag) {
18
- return node;
19
- }
11
+ function onAttrs(options, moduleOptions) {
12
+ return (attrs, node) => {
13
+ if (!node.tag) return attrs;
14
+ const newAttrs = attrs;
20
15
 
21
- for (const attrName of Object.keys(node.attrs)) {
16
+ for (const attrName of Object.keys(attrs)) {
22
17
  if (attrName === 'visible' && node.tag.startsWith('a-')) {
23
18
  continue;
24
19
  }
25
20
 
26
21
  if (htmlBooleanAttributes.has(attrName)) {
27
- node.attrs[attrName] = true;
22
+ newAttrs[attrName] = true;
28
23
  }
29
24
 
30
- if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && node.attrs[attrName] === '') {
31
- node.attrs[attrName] = true;
25
+ if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && attrs[attrName] === '') {
26
+ newAttrs[attrName] = true;
32
27
  } // collapse crossorigin attributes
33
28
  // Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
34
29
 
35
30
 
36
- if (attrName.toLowerCase() === 'crossorigin' && (node.attrs[attrName] === 'anonymous' || node.attrs[attrName] === '')) {
37
- node.attrs[attrName] = true;
31
+ if (attrName.toLowerCase() === 'crossorigin' && (attrs[attrName] === 'anonymous' || attrs[attrName] === '')) {
32
+ newAttrs[attrName] = true;
38
33
  }
39
34
  }
40
35
 
41
- return node;
42
- });
43
- return tree;
36
+ return newAttrs;
37
+ };
44
38
  }
@@ -3,25 +3,20 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = collapseAttributeWhitespace;
6
+ exports.onAttrs = onAttrs;
7
7
 
8
8
  var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace");
9
9
 
10
10
  /** Deduplicate values inside list-like attributes (e.g. class, rel) */
11
- function collapseAttributeWhitespace(tree) {
12
- tree.walk(node => {
13
- if (!node.attrs) {
14
- return node;
15
- }
16
-
17
- Object.keys(node.attrs).forEach(attrName => {
18
- const attrNameLower = attrName.toLowerCase();
19
-
20
- if (!_collapseAttributeWhitespace.attributesWithLists.has(attrNameLower)) {
11
+ function onAttrs() {
12
+ return attrs => {
13
+ const newAttrs = attrs;
14
+ Object.keys(attrs).forEach(attrName => {
15
+ if (!_collapseAttributeWhitespace.attributesWithLists.has(attrName)) {
21
16
  return;
22
17
  }
23
18
 
24
- const attrValues = node.attrs[attrName].split(/\s/);
19
+ const attrValues = attrs[attrName].split(/\s/);
25
20
  const uniqeAttrValues = new Set();
26
21
  const deduplicatedAttrValues = [];
27
22
  attrValues.forEach(attrValue => {
@@ -38,9 +33,8 @@ function collapseAttributeWhitespace(tree) {
38
33
  deduplicatedAttrValues.push(attrValue);
39
34
  uniqeAttrValues.add(attrValue);
40
35
  });
41
- node.attrs[attrName] = deduplicatedAttrValues.join(' ');
36
+ newAttrs[attrName] = deduplicatedAttrValues.join(' ');
42
37
  });
43
- return node;
44
- });
45
- return tree;
38
+ return newAttrs;
39
+ };
46
40
  }
@@ -3,31 +3,20 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = minifyJson;
6
+ exports.onContent = onContent;
7
+ const rNodeAttrsTypeJson = /(\/|\+)json/;
7
8
 
8
- /* Minify JSON inside <script> tags */
9
- function minifyJson(tree) {
10
- // Match all <script> tags which have JSON mime type
11
- tree.match({
12
- tag: 'script',
13
- attrs: {
14
- type: /(\/|\+)json/
15
- }
16
- }, node => {
17
- let content = (node.content || []).join('');
18
-
19
- if (!content) {
20
- return node;
21
- }
9
+ function onContent() {
10
+ return (content, node) => {
11
+ let newContent = content;
22
12
 
23
- try {
24
- content = JSON.stringify(JSON.parse(content));
25
- } catch (error) {
26
- return node;
13
+ if (node.attrs && node.attrs.type && rNodeAttrsTypeJson.test(node.attrs.type)) {
14
+ try {
15
+ newContent = JSON.stringify(JSON.parse((content || []).join('')));
16
+ } catch (error) {// Invalid JSON
17
+ }
27
18
  }
28
19
 
29
- node.content = [content];
30
- return node;
31
- });
32
- return tree;
20
+ return newContent;
21
+ };
33
22
  }
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = normalizeAttributeValues;
6
+ exports.onAttrs = onAttrs;
7
7
  const caseInsensitiveAttributes = {
8
8
  autocomplete: ['form'],
9
9
  charset: ['meta', 'script'],
@@ -29,20 +29,14 @@ const caseInsensitiveAttributes = {
29
29
  wrap: ['textarea']
30
30
  };
31
31
 
32
- function normalizeAttributeValues(tree) {
33
- tree.walk(node => {
34
- if (!node.attrs) {
35
- return node;
36
- }
37
-
38
- Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
39
- const attrNameLower = attrName.toLowerCase();
40
-
41
- if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrNameLower) && (caseInsensitiveAttributes[attrNameLower] === null || caseInsensitiveAttributes[attrNameLower].includes(node.tag))) {
42
- node.attrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
32
+ function onAttrs() {
33
+ return (attrs, node) => {
34
+ const newAttrs = attrs;
35
+ Object.entries(attrs).forEach(([attrName, attrValue]) => {
36
+ if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName) && (caseInsensitiveAttributes[attrName] === null || caseInsensitiveAttributes[attrName].includes(node.tag))) {
37
+ newAttrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
43
38
  }
44
39
  });
45
- return node;
46
- });
47
- return tree;
40
+ return newAttrs;
41
+ };
48
42
  }
@@ -3,28 +3,36 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = removeComments;
6
+ exports.onContent = onContent;
7
+ exports.onNode = onNode;
7
8
 
8
9
  var _helpers = require("../helpers");
9
10
 
10
11
  const MATCH_EXCERPT_REGEXP = /<!-- ?more ?-->/i;
11
12
  /** Removes HTML comments */
12
13
 
13
- function removeComments(tree, options, removeType) {
14
+ function onNode(options, removeType) {
14
15
  if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
15
16
  removeType = 'safe';
16
17
  }
17
18
 
18
- tree.walk(node => {
19
- if (node.contents && node.contents.length) {
20
- node.contents = node.contents.filter(content => !isCommentToRemove(content, removeType));
21
- } else if (isCommentToRemove(node, removeType)) {
22
- node = '';
19
+ return node => {
20
+ if (isCommentToRemove(node, removeType)) {
21
+ return '';
23
22
  }
24
23
 
25
24
  return node;
26
- });
27
- return tree;
25
+ };
26
+ }
27
+
28
+ function onContent(options, removeType) {
29
+ if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
30
+ removeType = 'safe';
31
+ }
32
+
33
+ return contents => {
34
+ return contents.filter(content => !isCommentToRemove(content, removeType));
35
+ };
28
36
  }
29
37
 
30
38
  function isCommentToRemove(text, removeType) {
@@ -3,7 +3,10 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = removeEmptyAttributes;
6
+ exports.onAttrs = onAttrs;
7
+
8
+ var _helpers = require("../helpers");
9
+
7
10
  const safeToRemoveAttrs = {
8
11
  id: null,
9
12
  class: null,
@@ -52,25 +55,18 @@ const safeToRemoveAttrs = {
52
55
  value: ['button', 'input', 'li'],
53
56
  width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
54
57
  };
55
- /** Removes empty attributes */
56
-
57
- function removeEmptyAttributes(tree) {
58
- tree.walk(node => {
59
- if (!node.attrs) {
60
- return node;
61
- }
62
-
63
- Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
64
- const attrNameLower = attrName.toLowerCase();
65
58
 
66
- if (attrNameLower.slice(0, 2).toLowerCase() === 'on' && attrName.length >= 5 // Event Handler
67
- || Object.hasOwnProperty.call(safeToRemoveAttrs, attrNameLower) && (safeToRemoveAttrs[attrNameLower] === null || safeToRemoveAttrs[attrNameLower].includes(node.tag))) {
59
+ function onAttrs() {
60
+ return (attrs, node) => {
61
+ const newAttrs = { ...attrs
62
+ };
63
+ Object.entries(attrs).forEach(([attrName, attrValue]) => {
64
+ if ((0, _helpers.isEventHandler)(attrName) || Object.hasOwnProperty.call(safeToRemoveAttrs, attrName) && (safeToRemoveAttrs[attrName] === null || safeToRemoveAttrs[attrName].includes(node.tag))) {
68
65
  if (attrValue === '' || (attrValue || '').match(/^\s+$/)) {
69
- delete node.attrs[attrName];
66
+ delete newAttrs[attrName];
70
67
  }
71
68
  }
72
69
  });
73
- return node;
74
- });
75
- return tree;
70
+ return newAttrs;
71
+ };
76
72
  }
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = removeRedundantAttributes;
6
+ exports.onAttrs = onAttrs;
7
7
  exports.redundantScriptTypes = void 0;
8
8
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#JavaScript_types
9
9
  const redundantScriptTypes = new Set(['application/javascript', 'application/ecmascript', 'application/x-ecmascript', 'application/x-javascript', 'text/javascript', 'text/ecmascript', 'text/javascript1.0', 'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4', 'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript']);
@@ -20,8 +20,8 @@ const redundantAttributes = {
20
20
  },
21
21
  'script': {
22
22
  'language': 'javascript',
23
- 'type': node => {
24
- for (const [attrName, attrValue] of Object.entries(node.attrs)) {
23
+ 'type': attrs => {
24
+ for (const [attrName, attrValue] of Object.entries(attrs)) {
25
25
  if (attrName.toLowerCase() !== 'type') {
26
26
  continue;
27
27
  }
@@ -32,10 +32,10 @@ const redundantAttributes = {
32
32
  return false;
33
33
  },
34
34
  // Remove attribute if the function returns false
35
- 'charset': node => {
35
+ 'charset': attrs => {
36
36
  // The charset attribute only really makes sense on “external” SCRIPT elements:
37
37
  // http://perfectionkills.com/optimizing-html/#8_script_charset
38
- return node.attrs && !node.attrs.src;
38
+ return !attrs.src;
39
39
  }
40
40
  },
41
41
  'style': {
@@ -44,13 +44,13 @@ const redundantAttributes = {
44
44
  },
45
45
  'link': {
46
46
  'media': 'all',
47
- 'type': node => {
47
+ 'type': attrs => {
48
48
  // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
49
49
  let isRelStyleSheet = false;
50
50
  let isTypeTextCSS = false;
51
51
 
52
- if (node.attrs) {
53
- for (const [attrName, attrValue] of Object.entries(node.attrs)) {
52
+ if (attrs) {
53
+ for (const [attrName, attrValue] of Object.entries(attrs)) {
54
54
  if (attrName.toLowerCase() === 'rel' && attrValue === 'stylesheet') {
55
55
  isRelStyleSheet = true;
56
56
  }
@@ -115,13 +115,10 @@ const tagsHaveRedundantAttributes = new Set(Object.keys(redundantAttributes));
115
115
  const tagsHaveMissingValueDefaultAttributes = new Set(Object.keys(canBeReplacedWithEmptyStringAttributes));
116
116
  /** Removes redundant attributes */
117
117
 
118
- function removeRedundantAttributes(tree) {
119
- tree.walk(node => {
120
- if (!node.tag) {
121
- return node;
122
- }
123
-
124
- node.attrs = node.attrs || {};
118
+ function onAttrs() {
119
+ return (attrs, node) => {
120
+ if (!node.tag) return attrs;
121
+ const newAttrs = attrs;
125
122
 
126
123
  if (tagsHaveRedundantAttributes.has(node.tag)) {
127
124
  const tagRedundantAttributes = redundantAttributes[node.tag];
@@ -131,13 +128,13 @@ function removeRedundantAttributes(tree) {
131
128
  let isRemove = false;
132
129
 
133
130
  if (typeof tagRedundantAttributeValue === 'function') {
134
- isRemove = tagRedundantAttributeValue(node);
135
- } else if (node.attrs[redundantAttributeName] === tagRedundantAttributeValue) {
131
+ isRemove = tagRedundantAttributeValue(attrs);
132
+ } else if (attrs[redundantAttributeName] === tagRedundantAttributeValue) {
136
133
  isRemove = true;
137
134
  }
138
135
 
139
136
  if (isRemove) {
140
- delete node.attrs[redundantAttributeName];
137
+ delete newAttrs[redundantAttributeName];
141
138
  }
142
139
  }
143
140
  }
@@ -149,17 +146,16 @@ function removeRedundantAttributes(tree) {
149
146
  let tagMissingValueDefaultAttribute = tagMissingValueDefaultAttributes[canBeReplacedWithEmptyStringAttributeName];
150
147
  let isReplace = false;
151
148
 
152
- if (node.attrs[canBeReplacedWithEmptyStringAttributeName] === tagMissingValueDefaultAttribute) {
149
+ if (attrs[canBeReplacedWithEmptyStringAttributeName] === tagMissingValueDefaultAttribute) {
153
150
  isReplace = true;
154
151
  }
155
152
 
156
153
  if (isReplace) {
157
- node.attrs[canBeReplacedWithEmptyStringAttributeName] = '';
154
+ newAttrs[canBeReplacedWithEmptyStringAttributeName] = '';
158
155
  }
159
156
  }
160
157
  }
161
158
 
162
- return node;
163
- });
164
- return tree;
159
+ return newAttrs;
160
+ };
165
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlnano",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Modular HTML minifier, built on top of the PostHTML",
5
5
  "main": "index.js",
6
6
  "author": "Kirill Maltsev <maltsevkirill@gmail.com>",
@@ -46,11 +46,11 @@
46
46
  "devDependencies": {
47
47
  "@babel/cli": "^7.15.7",
48
48
  "@babel/core": "^7.15.5",
49
+ "@babel/eslint-parser": "^7.17.0",
49
50
  "@babel/preset-env": "^7.15.6",
50
51
  "@babel/register": "^7.15.3",
51
- "babel-eslint": "^10.1.0",
52
52
  "cssnano": "^5.0.11",
53
- "eslint": "^7.32.0",
53
+ "eslint": "^8.12.0",
54
54
  "expect": "^27.2.0",
55
55
  "mocha": "^9.1.0",
56
56
  "postcss": "^8.3.11",