htmlnano 0.2.9 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/README.md +15 -886
  3. package/docs/README.md +33 -0
  4. package/docs/babel.config.js +3 -0
  5. package/docs/docs/010-introduction.md +22 -0
  6. package/docs/docs/020-usage.md +77 -0
  7. package/docs/docs/030-config.md +21 -0
  8. package/docs/docs/040-presets.md +75 -0
  9. package/docs/docs/050-modules.md +786 -0
  10. package/docs/docs/060-contribute.md +16 -0
  11. package/docs/docusaurus.config.js +60 -0
  12. package/docs/netlify.toml +4 -0
  13. package/docs/package-lock.json +11621 -0
  14. package/docs/package.json +39 -0
  15. package/docs/sidebars.js +26 -0
  16. package/docs/versioned_docs/version-1.1.1/010-introduction.md +22 -0
  17. package/docs/versioned_docs/version-1.1.1/020-usage.md +77 -0
  18. package/docs/versioned_docs/version-1.1.1/030-config.md +21 -0
  19. package/docs/versioned_docs/version-1.1.1/040-presets.md +75 -0
  20. package/docs/versioned_docs/version-1.1.1/050-modules.md +786 -0
  21. package/docs/versioned_docs/version-1.1.1/060-contribute.md +16 -0
  22. package/docs/versioned_sidebars/version-1.1.1-sidebars.json +8 -0
  23. package/docs/versions.json +3 -0
  24. package/lib/helpers.js +5 -0
  25. package/lib/htmlnano.js +43 -6
  26. package/lib/modules/collapseAttributeWhitespace.js +62 -6
  27. package/lib/modules/collapseWhitespace.js +42 -17
  28. package/lib/modules/minifyCss.js +4 -2
  29. package/lib/modules/minifyJs.js +5 -3
  30. package/lib/modules/minifySvg.js +6 -12
  31. package/lib/modules/minifyUrls.js +50 -15
  32. package/lib/modules/normalizeAttributeValues.js +48 -0
  33. package/lib/modules/removeComments.js +25 -1
  34. package/lib/modules/removeEmptyAttributes.js +52 -8
  35. package/lib/modules/removeRedundantAttributes.js +69 -14
  36. package/lib/presets/safe.js +9 -4
  37. package/package.json +18 -16
  38. package/test.js +25 -16
  39. package/uncss-fork.patch +13 -0
@@ -4,8 +4,54 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = removeEmptyAttributes;
7
- // Source: https://www.w3.org/TR/html4/sgml/dtd.html#events (Generic Attributes)
8
- const safeToRemoveAttrs = new Set(['id', 'class', 'style', 'title', 'lang', 'dir', 'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmousemove', 'onmouseout', 'onkeypress', 'onkeydown', 'onkeyup']);
7
+ const safeToRemoveAttrs = {
8
+ id: null,
9
+ class: null,
10
+ style: null,
11
+ title: null,
12
+ lang: null,
13
+ dir: null,
14
+ abbr: ['th'],
15
+ accept: ['input'],
16
+ 'accept-charset': ['form'],
17
+ charset: ['meta', 'script'],
18
+ action: ['form'],
19
+ cols: ['textarea'],
20
+ colspan: ['td', 'th'],
21
+ coords: ['area'],
22
+ dirname: ['input', 'textarea'],
23
+ dropzone: null,
24
+ headers: ['td', 'th'],
25
+ form: ['button', 'fieldset', 'input', 'keygen', 'object', 'output', 'select', 'textarea'],
26
+ formaction: ['button', 'input'],
27
+ height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
28
+ high: 'meter',
29
+ href: 'link',
30
+ list: 'input',
31
+ low: 'meter',
32
+ manifest: 'html',
33
+ max: ['meter', 'progress'],
34
+ maxLength: ['input', 'textarea'],
35
+ menu: 'button',
36
+ min: 'meter',
37
+ minLength: ['input', 'textarea'],
38
+ name: ['button', 'fieldset', 'input', 'keygen', 'output', 'select', 'textarea', 'form', 'map', 'meta', 'param', 'slot'],
39
+ pattern: ['input'],
40
+ ping: ['a', 'area'],
41
+ placeholder: ['input', 'textarea'],
42
+ poster: ['video'],
43
+ rel: ['a', 'area', 'link'],
44
+ rows: 'textarea',
45
+ rowspan: ['td', 'th'],
46
+ size: ['input', 'select'],
47
+ span: ['col', 'colgroup'],
48
+ src: ['audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video'],
49
+ start: 'ol',
50
+ tabindex: null,
51
+ type: ['a', 'link', 'button', 'embed', 'object', 'script', 'source', 'style', 'input', 'menu', 'menuitem', 'ol'],
52
+ value: ['button', 'input', 'li'],
53
+ width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
54
+ };
9
55
  /** Removes empty attributes */
10
56
 
11
57
  function removeEmptyAttributes(tree) {
@@ -17,12 +63,10 @@ function removeEmptyAttributes(tree) {
17
63
  Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
18
64
  const attrNameLower = attrName.toLowerCase();
19
65
 
20
- if (!safeToRemoveAttrs.has(attrNameLower)) {
21
- return;
22
- }
23
-
24
- if (attrValue === '' || (attrValue || '').match(/^\s+$/)) {
25
- delete node.attrs[attrName];
66
+ if (attrNameLower.slice(0, 2).toLowerCase() === 'on' && attrName.length >= 5 || Object.hasOwnProperty.call(safeToRemoveAttrs, attrNameLower) && (safeToRemoveAttrs[attrNameLower] === null || safeToRemoveAttrs[attrNameLower].includes(node.tag))) {
67
+ if (attrValue === '' || (attrValue || '').match(/^\s+$/)) {
68
+ delete node.attrs[attrName];
69
+ }
26
70
  }
27
71
  });
28
72
  return node;
@@ -72,8 +72,47 @@ const redundantAttributes = {
72
72
  'iframe': {
73
73
  'loading': 'eager'
74
74
  }
75
+ }; // See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#missing-value-default
76
+
77
+ const canBeReplacedWithEmptyStringAttributes = {
78
+ audio: {
79
+ // https://html.spec.whatwg.org/#attr-media-preload
80
+ preload: 'auto'
81
+ },
82
+ video: {
83
+ preload: 'auto'
84
+ },
85
+ form: {
86
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
87
+ autocomplete: 'on'
88
+ },
89
+ img: {
90
+ // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-decoding
91
+ decoding: 'auto'
92
+ },
93
+ track: {
94
+ // https://html.spec.whatwg.org/multipage/media.html#htmltrackelement
95
+ kind: 'subtitles'
96
+ },
97
+ textarea: {
98
+ // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-wrap
99
+ wrap: 'soft'
100
+ },
101
+ area: {
102
+ // https://html.spec.whatwg.org/multipage/image-maps.html#attr-area-shape
103
+ shape: 'rect'
104
+ },
105
+ button: {
106
+ // https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type
107
+ type: 'submit'
108
+ },
109
+ input: {
110
+ // https://html.spec.whatwg.org/multipage/input.html#states-of-the-type-attribute
111
+ type: 'text'
112
+ }
75
113
  };
76
114
  const tagsHaveRedundantAttributes = new Set(Object.keys(redundantAttributes));
115
+ const tagsHaveMissingValueDefaultAttributes = new Set(Object.keys(canBeReplacedWithEmptyStringAttributes));
77
116
  /** Removes redundant attributes */
78
117
 
79
118
  function removeRedundantAttributes(tree) {
@@ -82,25 +121,41 @@ function removeRedundantAttributes(tree) {
82
121
  return node;
83
122
  }
84
123
 
85
- if (!tagsHaveRedundantAttributes.has(node.tag)) {
86
- return node;
87
- }
88
-
89
- const tagRedundantAttributes = redundantAttributes[node.tag];
90
124
  node.attrs = node.attrs || {};
91
125
 
92
- for (const redundantAttributeName of Object.keys(tagRedundantAttributes)) {
93
- let tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName];
94
- let isRemove = false;
126
+ if (tagsHaveRedundantAttributes.has(node.tag)) {
127
+ const tagRedundantAttributes = redundantAttributes[node.tag];
128
+
129
+ for (const redundantAttributeName of Object.keys(tagRedundantAttributes)) {
130
+ let tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName];
131
+ let isRemove = false;
132
+
133
+ if (typeof tagRedundantAttributeValue === 'function') {
134
+ isRemove = tagRedundantAttributeValue(node);
135
+ } else if (node.attrs[redundantAttributeName] === tagRedundantAttributeValue) {
136
+ isRemove = true;
137
+ }
95
138
 
96
- if (typeof tagRedundantAttributeValue === 'function') {
97
- isRemove = tagRedundantAttributeValue(node);
98
- } else if (node.attrs[redundantAttributeName] === tagRedundantAttributeValue) {
99
- isRemove = true;
139
+ if (isRemove) {
140
+ delete node.attrs[redundantAttributeName];
141
+ }
100
142
  }
143
+ }
101
144
 
102
- if (isRemove) {
103
- delete node.attrs[redundantAttributeName];
145
+ if (tagsHaveMissingValueDefaultAttributes.has(node.tag)) {
146
+ const tagMissingValueDefaultAttributes = canBeReplacedWithEmptyStringAttributes[node.tag];
147
+
148
+ for (const canBeReplacedWithEmptyStringAttributeName of Object.keys(tagMissingValueDefaultAttributes)) {
149
+ let tagMissingValueDefaultAttribute = tagMissingValueDefaultAttributes[canBeReplacedWithEmptyStringAttributeName];
150
+ let isReplace = false;
151
+
152
+ if (node.attrs[canBeReplacedWithEmptyStringAttributeName] === tagMissingValueDefaultAttribute) {
153
+ isReplace = true;
154
+ }
155
+
156
+ if (isReplace) {
157
+ node.attrs[canBeReplacedWithEmptyStringAttributeName] = '';
158
+ }
104
159
  }
105
160
  }
106
161
 
@@ -27,14 +27,19 @@ var _default = {
27
27
  minifyJson: {},
28
28
  minifySvg: {
29
29
  plugins: [{
30
- collapseGroups: false
31
- }, {
32
- convertShapeToPath: false
30
+ name: 'preset-default',
31
+ params: {
32
+ overrides: {
33
+ collapseGroups: false,
34
+ convertShapeToPath: false
35
+ }
36
+ }
33
37
  }]
34
38
  },
35
39
  minifyConditionalComments: false,
36
- removeEmptyAttributes: true,
37
40
  removeRedundantAttributes: false,
41
+ normalizeAttributeValues: true,
42
+ removeEmptyAttributes: true,
38
43
  removeComments: 'safe',
39
44
  removeAttributeQuotes: false,
40
45
  sortAttributesWithLists: 'alphabetical',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlnano",
3
- "version": "0.2.9",
3
+ "version": "1.1.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>",
@@ -10,7 +10,7 @@
10
10
  "lint": "eslint *.js lib/*.es6 lib/modules/*.es6 lib/presets/*.es6 test/",
11
11
  "pretest": "npm run lint && npm run compile",
12
12
  "test": ":",
13
- "posttest": "mocha --require @babel/register --recursive --check-leaks --globals addresses",
13
+ "posttest": "mocha --timeout 5000 --require @babel/register --recursive --check-leaks --globals addresses",
14
14
  "prepare": "npm run compile",
15
15
  "release:patch": "release-it patch -n"
16
16
  },
@@ -39,26 +39,28 @@
39
39
  ]
40
40
  },
41
41
  "dependencies": {
42
- "cssnano": "^4.1.11",
43
- "posthtml": "^0.15.1",
44
- "purgecss": "^2.3.0",
42
+ "cosmiconfig": "^7.0.1",
43
+ "cssnano": "^5.0.8",
44
+ "postcss": "^8.3.6",
45
+ "posthtml": "^0.16.5",
46
+ "purgecss": "^4.0.0",
45
47
  "relateurl": "^0.2.7",
46
- "srcset": "^3.0.0",
47
- "svgo": "^1.3.2",
48
- "terser": "^5.6.1",
48
+ "srcset": "^4.0.0",
49
+ "svgo": "^2.6.1",
50
+ "terser": "^5.8.0",
49
51
  "timsort": "^0.3.0",
50
52
  "uncss": "^0.17.3"
51
53
  },
52
54
  "devDependencies": {
53
- "@babel/cli": "^7.13.14",
54
- "@babel/core": "^7.13.15",
55
- "@babel/preset-env": "^7.13.15",
56
- "@babel/register": "^7.13.14",
55
+ "@babel/cli": "^7.15.7",
56
+ "@babel/core": "^7.15.5",
57
+ "@babel/preset-env": "^7.15.6",
58
+ "@babel/register": "^7.15.3",
57
59
  "babel-eslint": "^10.1.0",
58
- "eslint": "^7.23.0",
59
- "expect": "^26.6.2",
60
- "mocha": "^8.3.2",
61
- "release-it": "^14.5.1",
60
+ "eslint": "^7.32.0",
61
+ "expect": "^27.2.0",
62
+ "mocha": "^9.1.0",
63
+ "release-it": "^14.11.5",
62
64
  "rimraf": "^3.0.2"
63
65
  },
64
66
  "repository": {
package/test.js CHANGED
@@ -1,8 +1,10 @@
1
1
  const htmlnano = require('.');
2
- const options = {
2
+ // const posthtml = require('posthtml');
3
+ const safePreset = require('./lib/presets/safe');
4
+ // const options = {
3
5
  // minifySvg: false,
4
6
  // minifyJs: false,
5
- };
7
+ // };
6
8
  // // posthtml, posthtml-render, and posthtml-parse options
7
9
  // const postHtmlOptions = {
8
10
  // sync: true, // https://github.com/posthtml/posthtml#usage
@@ -10,26 +12,33 @@ const options = {
10
12
  // quoteAllAttributes: false, // https://github.com/posthtml/posthtml-render#options
11
13
  // };
12
14
 
15
+ // const html = `
16
+ // <!doctype html>
17
+ // <html lang="en">
18
+ // <head>
19
+ // <meta charset="utf-8">
20
+ // <title></title>
21
+ // <script class="fob">alert(1)</script>
22
+ // <script>alert(2)</script>
23
+ // </head>
24
+ // <body>
25
+ // <script>alert(3)</script>
26
+ // <script>alert(4)</script>
27
+ // </body>
28
+ // </html>
29
+ // `;
30
+
31
+ const options = {
32
+ minifySvg: safePreset.minifySvg,
33
+ };
13
34
  const html = `
14
- <!doctype html>
15
- <html lang="en">
16
- <head>
17
- <meta charset="utf-8">
18
- <title></title>
19
- <script class="fob">alert(1)</script>
20
- <script>alert(2)</script>
21
- </head>
22
- <body>
23
- <script>alert(3)</script>
24
- <script>alert(4)</script>
25
- </body>
26
- </html>
35
+ <input type="text" class="form-control" name="testInput" autofocus="" autocomplete="off" id="testId"><a id="testId" href="#" class="testClass"></a><img width="20" src="../images/image.png" height="40" alt="image" class="cls" id="id2">
27
36
  `;
28
37
 
29
38
  htmlnano
30
39
  // "preset" arg might be skipped (see "Presets" section below for more info)
31
40
  // "postHtmlOptions" arg might be skipped
32
- .process(html, options)
41
+ .process(html)
33
42
  .then(function (result) {
34
43
  // result.html is minified
35
44
  console.log(result.html);
@@ -0,0 +1,13 @@
1
+ diff --git a/package.json b/package.json
2
+ index a127c0f..66455bb 100644
3
+ --- a/package.json
4
+ +++ b/package.json
5
+ @@ -49,7 +49,7 @@
6
+ "svgo": "^2.4.0",
7
+ "terser": "^5.7.0",
8
+ "timsort": "^0.3.0",
9
+ - "uncss": "^0.17.3"
10
+ + "@novaatwarren/uncss": "^0.17.4"
11
+ },
12
+ "devDependencies": {
13
+ "@babel/cli": "^7.14.3",