htmlnano 2.1.5 → 3.1.0

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 (116) hide show
  1. package/README.md +24 -17
  2. package/dist/_modules/collapseAttributeWhitespace.d.mts +42 -5
  3. package/dist/_modules/collapseAttributeWhitespace.d.ts +42 -5
  4. package/dist/_modules/collapseAttributeWhitespace.js +69 -16
  5. package/dist/_modules/collapseAttributeWhitespace.mjs +67 -17
  6. package/dist/_modules/collapseBooleanAttributes.d.mts +37 -3
  7. package/dist/_modules/collapseBooleanAttributes.d.ts +37 -3
  8. package/dist/_modules/collapseBooleanAttributes.js +18 -11
  9. package/dist/_modules/collapseBooleanAttributes.mjs +18 -11
  10. package/dist/_modules/collapseWhitespace.d.mts +37 -3
  11. package/dist/_modules/collapseWhitespace.d.ts +37 -3
  12. package/dist/_modules/collapseWhitespace.js +25 -2
  13. package/dist/_modules/collapseWhitespace.mjs +25 -2
  14. package/dist/_modules/custom.d.mts +37 -3
  15. package/dist/_modules/custom.d.ts +37 -3
  16. package/dist/_modules/deduplicateAttributeValues.d.mts +37 -3
  17. package/dist/_modules/deduplicateAttributeValues.d.ts +37 -3
  18. package/dist/_modules/deduplicateAttributeValues.js +20 -5
  19. package/dist/_modules/deduplicateAttributeValues.mjs +21 -6
  20. package/dist/_modules/example.d.mts +37 -3
  21. package/dist/_modules/example.d.ts +37 -3
  22. package/dist/_modules/mergeScripts.d.mts +37 -3
  23. package/dist/_modules/mergeScripts.d.ts +37 -3
  24. package/dist/_modules/mergeScripts.js +99 -24
  25. package/dist/_modules/mergeScripts.mjs +99 -24
  26. package/dist/_modules/mergeStyles.d.mts +37 -3
  27. package/dist/_modules/mergeStyles.d.ts +37 -3
  28. package/dist/_modules/mergeStyles.js +57 -6
  29. package/dist/_modules/mergeStyles.mjs +57 -6
  30. package/dist/_modules/minifyAttributes.d.mts +95 -0
  31. package/dist/_modules/minifyAttributes.d.ts +95 -0
  32. package/dist/_modules/minifyAttributes.js +159 -0
  33. package/dist/_modules/minifyAttributes.mjs +157 -0
  34. package/dist/_modules/minifyConditionalComments.d.mts +37 -3
  35. package/dist/_modules/minifyConditionalComments.d.ts +37 -3
  36. package/dist/_modules/minifyConditionalComments.js +37 -19
  37. package/dist/_modules/minifyConditionalComments.mjs +37 -19
  38. package/dist/_modules/minifyCss.d.mts +37 -3
  39. package/dist/_modules/minifyCss.d.ts +37 -3
  40. package/dist/_modules/minifyCss.js +13 -27
  41. package/dist/_modules/minifyCss.mjs +14 -28
  42. package/dist/_modules/minifyHtmlTemplate.d.mts +91 -0
  43. package/dist/_modules/minifyHtmlTemplate.d.ts +91 -0
  44. package/dist/_modules/minifyHtmlTemplate.js +231 -0
  45. package/dist/_modules/minifyHtmlTemplate.mjs +228 -0
  46. package/dist/_modules/minifyJs.d.mts +37 -3
  47. package/dist/_modules/minifyJs.d.ts +37 -3
  48. package/dist/_modules/minifyJs.js +94 -5
  49. package/dist/_modules/minifyJs.mjs +95 -6
  50. package/dist/_modules/minifyJson.d.mts +37 -3
  51. package/dist/_modules/minifyJson.d.ts +37 -3
  52. package/dist/_modules/minifyJson.js +9 -12
  53. package/dist/_modules/minifyJson.mjs +9 -12
  54. package/dist/_modules/minifySvg.d.mts +37 -3
  55. package/dist/_modules/minifySvg.d.ts +37 -3
  56. package/dist/_modules/minifySvg.js +36 -5
  57. package/dist/_modules/minifySvg.mjs +36 -5
  58. package/dist/_modules/minifyUrls.d.mts +38 -4
  59. package/dist/_modules/minifyUrls.d.ts +38 -4
  60. package/dist/_modules/minifyUrls.js +53 -28
  61. package/dist/_modules/minifyUrls.mjs +53 -28
  62. package/dist/_modules/normalizeAttributeValues.d.mts +37 -3
  63. package/dist/_modules/normalizeAttributeValues.d.ts +37 -3
  64. package/dist/_modules/normalizeAttributeValues.js +10 -8
  65. package/dist/_modules/normalizeAttributeValues.mjs +10 -8
  66. package/dist/_modules/removeAttributeQuotes.d.mts +41 -4
  67. package/dist/_modules/removeAttributeQuotes.d.ts +41 -4
  68. package/dist/_modules/removeAttributeQuotes.js +9 -5
  69. package/dist/_modules/removeAttributeQuotes.mjs +9 -5
  70. package/dist/_modules/removeComments.d.mts +38 -4
  71. package/dist/_modules/removeComments.d.ts +38 -4
  72. package/dist/_modules/removeComments.js +44 -12
  73. package/dist/_modules/removeComments.mjs +44 -12
  74. package/dist/_modules/removeEmptyAttributes.d.mts +37 -3
  75. package/dist/_modules/removeEmptyAttributes.d.ts +37 -3
  76. package/dist/_modules/removeEmptyAttributes.js +37 -16
  77. package/dist/_modules/removeEmptyAttributes.mjs +37 -16
  78. package/dist/_modules/removeEmptyElements.d.mts +95 -0
  79. package/dist/_modules/removeEmptyElements.d.ts +95 -0
  80. package/dist/_modules/removeEmptyElements.js +90 -0
  81. package/dist/_modules/removeEmptyElements.mjs +88 -0
  82. package/dist/_modules/removeOptionalTags.d.mts +37 -3
  83. package/dist/_modules/removeOptionalTags.d.ts +37 -3
  84. package/dist/_modules/removeOptionalTags.js +39 -28
  85. package/dist/_modules/removeOptionalTags.mjs +39 -28
  86. package/dist/_modules/removeRedundantAttributes.d.mts +37 -3
  87. package/dist/_modules/removeRedundantAttributes.d.ts +37 -3
  88. package/dist/_modules/removeRedundantAttributes.js +43 -28
  89. package/dist/_modules/removeRedundantAttributes.mjs +43 -28
  90. package/dist/_modules/removeUnusedCss.d.mts +38 -3
  91. package/dist/_modules/removeUnusedCss.d.ts +38 -3
  92. package/dist/_modules/removeUnusedCss.js +38 -13
  93. package/dist/_modules/removeUnusedCss.mjs +39 -14
  94. package/dist/_modules/sortAttributes.d.mts +37 -3
  95. package/dist/_modules/sortAttributes.d.ts +37 -3
  96. package/dist/_modules/sortAttributes.js +23 -5
  97. package/dist/_modules/sortAttributes.mjs +23 -5
  98. package/dist/_modules/sortAttributesWithLists.d.mts +37 -3
  99. package/dist/_modules/sortAttributesWithLists.d.ts +37 -3
  100. package/dist/_modules/sortAttributesWithLists.js +30 -8
  101. package/dist/_modules/sortAttributesWithLists.mjs +31 -9
  102. package/dist/bin.js +34 -0
  103. package/dist/helpers.d.ts +8 -1
  104. package/dist/helpers.js +48 -0
  105. package/dist/helpers.mjs +45 -1
  106. package/dist/index.d.ts +43 -8
  107. package/dist/index.js +20 -5
  108. package/dist/index.mjs +20 -6
  109. package/dist/presets/ampSafe.d.ts +38 -4
  110. package/dist/presets/max.d.ts +38 -4
  111. package/dist/presets/max.js +4 -0
  112. package/dist/presets/max.mjs +4 -0
  113. package/dist/presets/safe.d.ts +38 -4
  114. package/dist/presets/safe.js +6 -0
  115. package/dist/presets/safe.mjs +6 -0
  116. package/package.json +27 -16
@@ -1,4 +1,4 @@
1
- import { attributesWithLists } from './collapseAttributeWhitespace.mjs';
1
+ import { isListAttribute } from './collapseAttributeWhitespace.mjs';
2
2
 
3
3
  // class, rel, ping
4
4
  const validOptions = new Set([
@@ -13,6 +13,9 @@ const processModuleOptions = (options)=>{
13
13
  class AttributeTokenChain {
14
14
  addFromNodeAttrsArray(attrValuesArray) {
15
15
  attrValuesArray.forEach((attrValue)=>{
16
+ if (!attrValue) {
17
+ return;
18
+ }
16
19
  if (this.freqData.has(attrValue)) {
17
20
  this.freqData.set(attrValue, this.freqData.get(attrValue) + 1);
18
21
  } else {
@@ -24,16 +27,28 @@ class AttributeTokenChain {
24
27
  const _sortOrder = [
25
28
  ...this.freqData.entries()
26
29
  ];
27
- _sortOrder.sort((a, b)=>b[1] - a[1]);
30
+ _sortOrder.sort((a, b)=>b[1] - a[1] || a[0].localeCompare(b[0]));
28
31
  this.sortOrder = _sortOrder.map((i)=>i[0]);
29
32
  }
30
33
  sortFromNodeAttrsArray(attrValuesArray) {
31
34
  const resultArray = [];
35
+ const tokenCounts = new Map();
36
+ attrValuesArray.forEach((attrValue)=>{
37
+ var _tokenCounts_get;
38
+ if (!attrValue) {
39
+ return;
40
+ }
41
+ tokenCounts.set(attrValue, ((_tokenCounts_get = tokenCounts.get(attrValue)) != null ? _tokenCounts_get : 0) + 1);
42
+ });
32
43
  if (!this.sortOrder) {
33
44
  this.createSortOrder();
34
45
  }
35
46
  this.sortOrder.forEach((k)=>{
36
- if (attrValuesArray.includes(k)) {
47
+ const count = tokenCounts.get(k);
48
+ if (!count) {
49
+ return;
50
+ }
51
+ for(let i = 0; i < count; i += 1){
37
52
  resultArray.push(k);
38
53
  }
39
54
  });
@@ -57,17 +72,22 @@ class AttributeTokenChain {
57
72
  return tree;
58
73
  }
59
74
  };
75
+ const splitListAttributeValues = (attrValue)=>attrValue.split(/\s+/).filter(Boolean);
60
76
  function sortAttributesWithListsInAlphabeticalOrder(tree) {
61
77
  tree.walk((node)=>{
62
78
  if (!node.attrs) {
63
79
  return node;
64
80
  }
81
+ const tagName = node.tag ? node.tag.toLowerCase() : undefined;
65
82
  Object.keys(node.attrs).forEach((attrName)=>{
66
83
  const attrNameLower = attrName.toLowerCase();
67
- if (!attributesWithLists.has(attrNameLower)) {
84
+ if (!isListAttribute(attrNameLower, tagName)) {
85
+ return;
86
+ }
87
+ const attrValues = splitListAttributeValues(node.attrs[attrName]);
88
+ if (attrValues.length < 2) {
68
89
  return;
69
90
  }
70
- const attrValues = node.attrs[attrName].split(/\s/);
71
91
  node.attrs[attrName] = attrValues.sort((a, b)=>{
72
92
  // @ts-expect-error -- deliberately use minus operator to sort things
73
93
  return typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b;
@@ -84,13 +104,14 @@ function sortAttributesWithListsByFrequency(tree) {
84
104
  if (!node.attrs) {
85
105
  return node;
86
106
  }
107
+ const tagName = node.tag ? node.tag.toLowerCase() : undefined;
87
108
  Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
88
109
  const attrNameLower = attrName.toLowerCase();
89
- if (!attributesWithLists.has(attrNameLower)) {
110
+ if (!isListAttribute(attrNameLower, tagName)) {
90
111
  return;
91
112
  }
92
113
  tokenChainObj[attrNameLower] = tokenChainObj[attrNameLower] || new AttributeTokenChain();
93
- tokenChainObj[attrNameLower].addFromNodeAttrsArray(attrValues.split(/\s/));
114
+ tokenChainObj[attrNameLower].addFromNodeAttrsArray(splitListAttributeValues(attrValues));
94
115
  });
95
116
  return node;
96
117
  });
@@ -99,13 +120,14 @@ function sortAttributesWithListsByFrequency(tree) {
99
120
  if (!node.attrs) {
100
121
  return node;
101
122
  }
123
+ const tagName = node.tag ? node.tag.toLowerCase() : undefined;
102
124
  Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
103
125
  const attrNameLower = attrName.toLowerCase();
104
- if (!attributesWithLists.has(attrNameLower)) {
126
+ if (!isListAttribute(attrNameLower, tagName)) {
105
127
  return;
106
128
  }
107
129
  if (tokenChainObj[attrNameLower]) {
108
- node.attrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(attrValues.split(/\s/)).join(' ');
130
+ node.attrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(splitListAttributeValues(attrValues)).join(' ');
109
131
  }
110
132
  });
111
133
  return node;
package/dist/bin.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ var commander = require('commander');
3
+ var fs = require('fs');
4
+ var process = require('process');
5
+ var index_js = require('./index.js');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
10
+ var process__default = /*#__PURE__*/_interopDefault(process);
11
+
12
+ commander.program.name('htmlnano').description('Minify HTML with htmlnano').argument('[input]', 'input file', '-').option('-o, --output <file>', 'output file', '-').option('-p, --preset <preset>', 'preset to use', 'safe').option('-c, --config <file>', 'path to config file').action(async (input, options)=>{
13
+ const { preset, output } = options;
14
+ if (!preset || !(preset in index_js.presets)) {
15
+ const available = Object.keys(index_js.presets).join(', ');
16
+ process__default.default.stderr.write(`Unknown preset: ${preset}. Available presets: ${available}\n`);
17
+ process__default.default.exitCode = 1;
18
+ return;
19
+ }
20
+ const html = fs__default.default.readFileSync(input && input !== '-' ? input : 0, 'utf8');
21
+ const key = preset;
22
+ const chosenPreset = index_js.presets[key];
23
+ const htmlnanoOptions = {};
24
+ if (options.config) {
25
+ htmlnanoOptions.configPath = options.config;
26
+ }
27
+ const result = await index_js.process(html, htmlnanoOptions, chosenPreset);
28
+ if (output && output !== '-') {
29
+ fs__default.default.writeFileSync(output, result.html);
30
+ } else {
31
+ process__default.default.stdout.write(result.html);
32
+ }
33
+ });
34
+ commander.program.parse();
package/dist/helpers.d.ts CHANGED
@@ -7,10 +7,17 @@ declare function isComment(content: PostHTMLNodeLike | null): boolean;
7
7
  declare function isConditionalComment(content: string): boolean;
8
8
  declare function isStyleNode(node: PostHTML.Node): boolean | undefined;
9
9
  declare function extractCssFromStyleNode(node: PostHTML.Node): string | undefined;
10
+ declare function stripCssCdata(css: string): {
11
+ strippedCss: string;
12
+ isCdataWrapped: boolean;
13
+ };
14
+ declare function wrapCssCdata(css: string, isCdataWrapped: boolean): string;
15
+ declare function isCssStyleType(node: PostHTML.Node): boolean;
16
+ declare function normalizeMimeType(value: string): string;
10
17
  declare function isEventHandler(attributeName: string): boolean | "";
11
18
  declare function extractTextContentFromNode(node: PostHTML.Node): string;
12
19
  declare function optionalImport<Module = unknown, Default = Module>(moduleName: string): Promise<(Module & {
13
20
  default?: Default;
14
21
  }) | NonNullable<Default> | null>;
15
22
 
16
- export { extractCssFromStyleNode, extractTextContentFromNode, isAmpBoilerplate, isComment, isConditionalComment, isEventHandler, isStyleNode, optionalImport };
23
+ export { extractCssFromStyleNode, extractTextContentFromNode, isAmpBoilerplate, isComment, isConditionalComment, isCssStyleType, isEventHandler, isStyleNode, normalizeMimeType, optionalImport, stripCssCdata, wrapCssCdata };
package/dist/helpers.js CHANGED
@@ -5,6 +5,8 @@ const ampBoilerplateAttributes = [
5
5
  'amp4ads-boilerplate',
6
6
  'amp4email-boilerplate'
7
7
  ];
8
+ const cssCdataStart = '<![CDATA[';
9
+ const cssCdataEnd = ']]>';
8
10
  function isAmpBoilerplate(node) {
9
11
  if (!node.attrs) {
10
12
  return false;
@@ -32,6 +34,48 @@ function isStyleNode(node) {
32
34
  function extractCssFromStyleNode(node) {
33
35
  return Array.isArray(node.content) ? node.content.join(' ') : node.content;
34
36
  }
37
+ function stripCssCdata(css) {
38
+ const trimmed = css.trim();
39
+ if (!trimmed.startsWith(cssCdataStart) || !trimmed.endsWith(cssCdataEnd)) {
40
+ return {
41
+ strippedCss: css,
42
+ isCdataWrapped: false
43
+ };
44
+ }
45
+ const strippedCss = trimmed.slice(cssCdataStart.length, trimmed.length - cssCdataEnd.length);
46
+ return {
47
+ strippedCss,
48
+ isCdataWrapped: true
49
+ };
50
+ }
51
+ function wrapCssCdata(css, isCdataWrapped) {
52
+ if (!isCdataWrapped) {
53
+ return css;
54
+ }
55
+ return `${cssCdataStart}${css}${cssCdataEnd}`;
56
+ }
57
+ function isCssStyleType(node) {
58
+ if (!node.attrs || !('type' in node.attrs)) {
59
+ return true;
60
+ }
61
+ const rawType = node.attrs.type;
62
+ if (rawType === '') {
63
+ return true;
64
+ }
65
+ if (typeof rawType !== 'string') {
66
+ return false;
67
+ }
68
+ const normalizedType = rawType.trim().toLowerCase();
69
+ return /^text\/css(?:$|\s*;)/.test(normalizedType);
70
+ }
71
+ function normalizeMimeType(value) {
72
+ const trimmed = value.trim();
73
+ if (!trimmed) {
74
+ return '';
75
+ }
76
+ const [mimeType] = trimmed.split(';');
77
+ return mimeType.trim().toLowerCase();
78
+ }
35
79
  function isEventHandler(attributeName) {
36
80
  return attributeName && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
37
81
  }
@@ -67,6 +111,10 @@ exports.extractTextContentFromNode = extractTextContentFromNode;
67
111
  exports.isAmpBoilerplate = isAmpBoilerplate;
68
112
  exports.isComment = isComment;
69
113
  exports.isConditionalComment = isConditionalComment;
114
+ exports.isCssStyleType = isCssStyleType;
70
115
  exports.isEventHandler = isEventHandler;
71
116
  exports.isStyleNode = isStyleNode;
117
+ exports.normalizeMimeType = normalizeMimeType;
72
118
  exports.optionalImport = optionalImport;
119
+ exports.stripCssCdata = stripCssCdata;
120
+ exports.wrapCssCdata = wrapCssCdata;
package/dist/helpers.mjs CHANGED
@@ -3,6 +3,8 @@ const ampBoilerplateAttributes = [
3
3
  'amp4ads-boilerplate',
4
4
  'amp4email-boilerplate'
5
5
  ];
6
+ const cssCdataStart = '<![CDATA[';
7
+ const cssCdataEnd = ']]>';
6
8
  function isAmpBoilerplate(node) {
7
9
  if (!node.attrs) {
8
10
  return false;
@@ -30,6 +32,48 @@ function isStyleNode(node) {
30
32
  function extractCssFromStyleNode(node) {
31
33
  return Array.isArray(node.content) ? node.content.join(' ') : node.content;
32
34
  }
35
+ function stripCssCdata(css) {
36
+ const trimmed = css.trim();
37
+ if (!trimmed.startsWith(cssCdataStart) || !trimmed.endsWith(cssCdataEnd)) {
38
+ return {
39
+ strippedCss: css,
40
+ isCdataWrapped: false
41
+ };
42
+ }
43
+ const strippedCss = trimmed.slice(cssCdataStart.length, trimmed.length - cssCdataEnd.length);
44
+ return {
45
+ strippedCss,
46
+ isCdataWrapped: true
47
+ };
48
+ }
49
+ function wrapCssCdata(css, isCdataWrapped) {
50
+ if (!isCdataWrapped) {
51
+ return css;
52
+ }
53
+ return `${cssCdataStart}${css}${cssCdataEnd}`;
54
+ }
55
+ function isCssStyleType(node) {
56
+ if (!node.attrs || !('type' in node.attrs)) {
57
+ return true;
58
+ }
59
+ const rawType = node.attrs.type;
60
+ if (rawType === '') {
61
+ return true;
62
+ }
63
+ if (typeof rawType !== 'string') {
64
+ return false;
65
+ }
66
+ const normalizedType = rawType.trim().toLowerCase();
67
+ return /^text\/css(?:$|\s*;)/.test(normalizedType);
68
+ }
69
+ function normalizeMimeType(value) {
70
+ const trimmed = value.trim();
71
+ if (!trimmed) {
72
+ return '';
73
+ }
74
+ const [mimeType] = trimmed.split(';');
75
+ return mimeType.trim().toLowerCase();
76
+ }
33
77
  function isEventHandler(attributeName) {
34
78
  return attributeName && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
35
79
  }
@@ -60,4 +104,4 @@ async function optionalImport(moduleName) {
60
104
  }
61
105
  }
62
106
 
63
- export { extractCssFromStyleNode, extractTextContentFromNode, isAmpBoilerplate, isComment, isConditionalComment, isEventHandler, isStyleNode, optionalImport };
107
+ export { extractCssFromStyleNode, extractTextContentFromNode, isAmpBoilerplate, isComment, isConditionalComment, isCssStyleType, isEventHandler, isStyleNode, normalizeMimeType, optionalImport, stripCssCdata, wrapCssCdata };
package/dist/index.d.ts CHANGED
@@ -2,18 +2,27 @@ import PostHTML from 'posthtml';
2
2
  import { MinifyOptions } from 'terser';
3
3
  import { Options } from 'cssnano';
4
4
  import { Config } from 'svgo';
5
+ import { UserDefinedOptions } from 'purgecss';
5
6
 
6
7
  type PostHTMLTreeLike = [PostHTML.Node] & PostHTML.NodeAPI & {
7
8
  options?: {
8
9
  quoteAllAttributes?: boolean | undefined;
10
+ quoteStyle?: 0 | 1 | 2 | undefined;
11
+ replaceQuote?: boolean | undefined;
9
12
  } | undefined;
10
13
  render(): string;
11
14
  render(node: PostHTML.Node | PostHTMLTreeLike, renderOptions?: any): string;
12
15
  };
13
16
  type MaybeArray<T> = T | Array<T>;
14
17
  type PostHTMLNodeLike = PostHTML.Node | string;
18
+ type HtmlnanoTemplateRule = {
19
+ tag: string;
20
+ attrs?: Record<string, string | boolean | void>;
21
+ };
22
+ type MinifyHtmlTemplateOptions = boolean | HtmlnanoTemplateRule[];
15
23
  interface HtmlnanoOptions {
16
24
  skipConfigLoading?: boolean;
25
+ configPath?: string;
17
26
  skipInternalWarnings?: boolean;
18
27
  collapseAttributeWhitespace?: boolean;
19
28
  collapseBooleanAttributes?: {
@@ -26,25 +35,50 @@ interface HtmlnanoOptions {
26
35
  mergeStyles?: boolean;
27
36
  mergeScripts?: boolean;
28
37
  minifyCss?: Options | boolean;
38
+ minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
29
39
  minifyConditionalComments?: boolean;
30
40
  minifyJs?: MinifyOptions | boolean;
31
41
  minifyJson?: boolean;
42
+ minifyAttributes?: boolean | {
43
+ metaContent?: boolean;
44
+ redundantWhitespaces?: 'safe' | 'agressive' | false;
45
+ };
32
46
  minifySvg?: Config | boolean;
33
47
  normalizeAttributeValues?: boolean;
34
- removeAttributeQuotes?: boolean;
35
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
48
+ removeAttributeQuotes?: boolean | {
49
+ force?: boolean;
50
+ };
51
+ removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
36
52
  removeEmptyAttributes?: boolean;
53
+ removeEmptyElements?: boolean | {
54
+ removeWithAttributes?: boolean;
55
+ };
37
56
  removeRedundantAttributes?: boolean;
38
57
  removeOptionalTags?: boolean;
39
- removeUnusedCss?: boolean;
58
+ removeUnusedCss?: boolean | ({
59
+ tool: 'purgeCSS';
60
+ } & Omit<UserDefinedOptions, 'content' | 'css' | 'extractors'>) | {
61
+ banner?: boolean;
62
+ csspath?: string;
63
+ htmlroot?: string;
64
+ ignore?: (string | RegExp)[];
65
+ inject?: string;
66
+ jsdom?: object;
67
+ media?: string[];
68
+ report?: boolean;
69
+ strictSSL?: boolean;
70
+ timeout?: number;
71
+ uncssrc?: string;
72
+ userAgent?: string;
73
+ };
40
74
  sortAttributes?: boolean | 'alphabetical' | 'frequency';
41
75
  sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
42
76
  }
43
- interface HtmlnanoPreset extends Omit<HtmlnanoOptions, 'skipConfigLoading'> {
77
+ interface HtmlnanoPreset extends Omit<HtmlnanoOptions, 'skipConfigLoading' | 'configPath'> {
44
78
  }
45
79
  type HtmlnanoPredefinedPreset = 'safe' | 'ampSafe' | 'max';
46
80
  type HtmlnanoPredefinedPresets = Record<HtmlnanoPredefinedPreset, HtmlnanoPreset>;
47
- type HtmlnanoOptionsConfigFile = Omit<HtmlnanoOptions, 'skipConfigLoading'> & {
81
+ type HtmlnanoOptionsConfigFile = Omit<HtmlnanoOptions, 'skipConfigLoading' | 'configPath'> & {
48
82
  preset?: HtmlnanoPredefinedPreset;
49
83
  };
50
84
  type HtmlnanoModuleAttrsHandler = (attrs: Record<string, string | boolean | void>, node: PostHTML.Node) => Record<string, string | boolean | void>;
@@ -58,7 +92,8 @@ type HtmlnanoModule<Options = any> = {
58
92
  default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
59
93
  };
60
94
 
61
- declare function loadConfig(options?: HtmlnanoOptions, preset?: HtmlnanoPreset, configPath?: string): [Partial<HtmlnanoOptions>, HtmlnanoPreset];
95
+ declare const presets: HtmlnanoPredefinedPresets;
96
+ declare function loadConfig(options?: HtmlnanoOptions, preset?: HtmlnanoPreset): [Partial<HtmlnanoOptions>, HtmlnanoPreset];
62
97
  declare const htmlnano: ((optionsRun?: HtmlnanoOptions, presetRun?: HtmlnanoPreset) => PostHTML.Plugin<never>) & {
63
98
  presets: HtmlnanoPredefinedPresets;
64
99
  getRequiredOptionalDependencies: typeof getRequiredOptionalDependencies;
@@ -74,5 +109,5 @@ declare function htmlMinimizerWebpackPluginMinify(input: {
74
109
  code: string;
75
110
  }>;
76
111
 
77
- export { htmlnano as default, getRequiredOptionalDependencies, htmlMinimizerWebpackPluginMinify, loadConfig, process };
78
- export type { HtmlnanoModule, HtmlnanoModuleAttrsHandler, HtmlnanoModuleContentHandler, HtmlnanoModuleNodeHandler, HtmlnanoOptions, HtmlnanoOptionsConfigFile, HtmlnanoPredefinedPreset, HtmlnanoPredefinedPresets, HtmlnanoPreset, PostHTMLNodeLike, PostHTMLTreeLike };
112
+ export { htmlnano as default, getRequiredOptionalDependencies, htmlMinimizerWebpackPluginMinify, loadConfig, presets, process };
113
+ export type { HtmlnanoModule, HtmlnanoModuleAttrsHandler, HtmlnanoModuleContentHandler, HtmlnanoModuleNodeHandler, HtmlnanoOptions, HtmlnanoOptionsConfigFile, HtmlnanoPredefinedPreset, HtmlnanoPredefinedPresets, HtmlnanoPreset, HtmlnanoTemplateRule, MinifyHtmlTemplateOptions, PostHTMLNodeLike, PostHTMLTreeLike };
package/dist/index.js CHANGED
@@ -18,8 +18,8 @@ const presets = {
18
18
  ampSafe: ampSafePreset__default.default,
19
19
  max: maxPreset__default.default
20
20
  };
21
- function loadConfig(options, preset, configPath) {
22
- const { skipConfigLoading = false, ...rest } = options || {};
21
+ function loadConfig(options, preset) {
22
+ const { skipConfigLoading = false, configPath, ...rest } = options || {};
23
23
  let restConfig = rest;
24
24
  if (!skipConfigLoading) {
25
25
  const explorer = cosmiconfig.cosmiconfigSync('htmlnano');
@@ -32,9 +32,10 @@ function loadConfig(options, preset, configPath) {
32
32
  }
33
33
  delete rc.config.preset;
34
34
  }
35
- if (!options) {
36
- restConfig = rc.config;
37
- }
35
+ restConfig = {
36
+ ...rc.config,
37
+ ...restConfig
38
+ };
38
39
  }
39
40
  }
40
41
  return [
@@ -124,14 +125,17 @@ const modules = {
124
125
  mergeStyles: ()=>interop(import('./_modules/mergeStyles.js')),
125
126
  minifyConditionalComments: ()=>interop(import('./_modules/minifyConditionalComments.js')),
126
127
  minifyCss: ()=>interop(import('./_modules/minifyCss.js')),
128
+ minifyHtmlTemplate: ()=>interop(import('./_modules/minifyHtmlTemplate.js')),
127
129
  minifyJs: ()=>interop(import('./_modules/minifyJs.js')),
128
130
  minifyJson: ()=>interop(import('./_modules/minifyJson.js')),
131
+ minifyAttributes: ()=>interop(import('./_modules/minifyAttributes.js')),
129
132
  minifySvg: ()=>interop(import('./_modules/minifySvg.js')),
130
133
  minifyUrls: ()=>interop(import('./_modules/minifyUrls.js')),
131
134
  normalizeAttributeValues: ()=>interop(import('./_modules/normalizeAttributeValues.js')),
132
135
  removeAttributeQuotes: ()=>interop(import('./_modules/removeAttributeQuotes.js')),
133
136
  removeComments: ()=>interop(import('./_modules/removeComments.js')),
134
137
  removeEmptyAttributes: ()=>interop(import('./_modules/removeEmptyAttributes.js')),
138
+ removeEmptyElements: ()=>interop(import('./_modules/removeEmptyElements.js')),
135
139
  removeOptionalTags: ()=>interop(import('./_modules/removeOptionalTags.js')),
136
140
  removeRedundantAttributes: ()=>interop(import('./_modules/removeRedundantAttributes.js')),
137
141
  removeUnusedCss: ()=>interop(import('./_modules/removeUnusedCss.js')),
@@ -151,7 +155,13 @@ const htmlnano = Object.assign(function htmlnano(optionsRun = {}, presetRun) {
151
155
  ...options
152
156
  };
153
157
  let promise = Promise.resolve(tree);
158
+ const nonModuleOptions = new Set([
159
+ 'skipInternalWarnings'
160
+ ]);
154
161
  for (const [moduleName, moduleOptions] of Object.entries(options)){
162
+ if (nonModuleOptions.has(moduleName)) {
163
+ continue;
164
+ }
155
165
  if (!moduleOptions) {
156
166
  continue;
157
167
  }
@@ -178,15 +188,19 @@ const htmlnano = Object.assign(function htmlnano(optionsRun = {}, presetRun) {
178
188
  }
179
189
  const mod = moduleName in modules ? await modules[moduleName]() : await import(`./_modules/${moduleName}.mjs`);
180
190
  if (typeof mod.onAttrs === 'function') {
191
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- module options are generic
181
192
  attrsHandlers.push(mod.onAttrs(options, moduleOptions));
182
193
  }
183
194
  if (typeof mod.onContent === 'function') {
195
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- module options are generic
184
196
  contentsHandlers.push(mod.onContent(options, moduleOptions));
185
197
  }
186
198
  if (typeof mod.onNode === 'function') {
199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- module options are generic
187
200
  nodeHandlers.push(mod.onNode(options, moduleOptions));
188
201
  }
189
202
  if (typeof mod.default === 'function') {
203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- module options are generic
190
204
  promise = promise.then(async (tree)=>await mod.default(tree, options, moduleOptions));
191
205
  }
192
206
  }
@@ -273,4 +287,5 @@ exports.default = htmlnano;
273
287
  exports.getRequiredOptionalDependencies = getRequiredOptionalDependencies;
274
288
  exports.htmlMinimizerWebpackPluginMinify = htmlMinimizerWebpackPluginMinify;
275
289
  exports.loadConfig = loadConfig;
290
+ exports.presets = presets;
276
291
  exports.process = process;
package/dist/index.mjs CHANGED
@@ -9,8 +9,8 @@ const presets = {
9
9
  ampSafe: ampSafePreset,
10
10
  max: maxPreset
11
11
  };
12
- function loadConfig(options, preset, configPath) {
13
- const { skipConfigLoading = false, ...rest } = options || {};
12
+ function loadConfig(options, preset) {
13
+ const { skipConfigLoading = false, configPath, ...rest } = options || {};
14
14
  let restConfig = rest;
15
15
  if (!skipConfigLoading) {
16
16
  const explorer = cosmiconfigSync('htmlnano');
@@ -23,9 +23,10 @@ function loadConfig(options, preset, configPath) {
23
23
  }
24
24
  delete rc.config.preset;
25
25
  }
26
- if (!options) {
27
- restConfig = rc.config;
28
- }
26
+ restConfig = {
27
+ ...rc.config,
28
+ ...restConfig
29
+ };
29
30
  }
30
31
  }
31
32
  return [
@@ -115,14 +116,17 @@ const modules = {
115
116
  mergeStyles: ()=>interop(import('./_modules/mergeStyles.mjs')),
116
117
  minifyConditionalComments: ()=>interop(import('./_modules/minifyConditionalComments.mjs')),
117
118
  minifyCss: ()=>interop(import('./_modules/minifyCss.mjs')),
119
+ minifyHtmlTemplate: ()=>interop(import('./_modules/minifyHtmlTemplate.mjs')),
118
120
  minifyJs: ()=>interop(import('./_modules/minifyJs.mjs')),
119
121
  minifyJson: ()=>interop(import('./_modules/minifyJson.mjs')),
122
+ minifyAttributes: ()=>interop(import('./_modules/minifyAttributes.mjs')),
120
123
  minifySvg: ()=>interop(import('./_modules/minifySvg.mjs')),
121
124
  minifyUrls: ()=>interop(import('./_modules/minifyUrls.mjs')),
122
125
  normalizeAttributeValues: ()=>interop(import('./_modules/normalizeAttributeValues.mjs')),
123
126
  removeAttributeQuotes: ()=>interop(import('./_modules/removeAttributeQuotes.mjs')),
124
127
  removeComments: ()=>interop(import('./_modules/removeComments.mjs')),
125
128
  removeEmptyAttributes: ()=>interop(import('./_modules/removeEmptyAttributes.mjs')),
129
+ removeEmptyElements: ()=>interop(import('./_modules/removeEmptyElements.mjs')),
126
130
  removeOptionalTags: ()=>interop(import('./_modules/removeOptionalTags.mjs')),
127
131
  removeRedundantAttributes: ()=>interop(import('./_modules/removeRedundantAttributes.mjs')),
128
132
  removeUnusedCss: ()=>interop(import('./_modules/removeUnusedCss.mjs')),
@@ -142,7 +146,13 @@ const htmlnano = Object.assign(function htmlnano(optionsRun = {}, presetRun) {
142
146
  ...options
143
147
  };
144
148
  let promise = Promise.resolve(tree);
149
+ const nonModuleOptions = new Set([
150
+ 'skipInternalWarnings'
151
+ ]);
145
152
  for (const [moduleName, moduleOptions] of Object.entries(options)){
153
+ if (nonModuleOptions.has(moduleName)) {
154
+ continue;
155
+ }
146
156
  if (!moduleOptions) {
147
157
  continue;
148
158
  }
@@ -169,15 +179,19 @@ const htmlnano = Object.assign(function htmlnano(optionsRun = {}, presetRun) {
169
179
  }
170
180
  const mod = moduleName in modules ? await modules[moduleName]() : await import(`./_modules/${moduleName}.mjs`);
171
181
  if (typeof mod.onAttrs === 'function') {
182
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- module options are generic
172
183
  attrsHandlers.push(mod.onAttrs(options, moduleOptions));
173
184
  }
174
185
  if (typeof mod.onContent === 'function') {
186
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- module options are generic
175
187
  contentsHandlers.push(mod.onContent(options, moduleOptions));
176
188
  }
177
189
  if (typeof mod.onNode === 'function') {
190
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- module options are generic
178
191
  nodeHandlers.push(mod.onNode(options, moduleOptions));
179
192
  }
180
193
  if (typeof mod.default === 'function') {
194
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- module options are generic
181
195
  promise = promise.then(async (tree)=>await mod.default(tree, options, moduleOptions));
182
196
  }
183
197
  }
@@ -260,4 +274,4 @@ if (typeof module !== 'undefined') {
260
274
  module.exports = htmlnano;
261
275
  }
262
276
 
263
- export { htmlnano as default, getRequiredOptionalDependencies, htmlMinimizerWebpackPluginMinify, loadConfig, process };
277
+ export { htmlnano as default, getRequiredOptionalDependencies, htmlMinimizerWebpackPluginMinify, loadConfig, presets, process };
@@ -2,17 +2,26 @@ import PostHTML from 'posthtml';
2
2
  import { MinifyOptions } from 'terser';
3
3
  import { Options } from 'cssnano';
4
4
  import { Config } from 'svgo';
5
+ import { UserDefinedOptions } from 'purgecss';
5
6
 
6
7
  type PostHTMLTreeLike = [PostHTML.Node] & PostHTML.NodeAPI & {
7
8
  options?: {
8
9
  quoteAllAttributes?: boolean | undefined;
10
+ quoteStyle?: 0 | 1 | 2 | undefined;
11
+ replaceQuote?: boolean | undefined;
9
12
  } | undefined;
10
13
  render(): string;
11
14
  render(node: PostHTML.Node | PostHTMLTreeLike, renderOptions?: any): string;
12
15
  };
13
16
  type MaybeArray<T> = T | Array<T>;
17
+ type HtmlnanoTemplateRule = {
18
+ tag: string;
19
+ attrs?: Record<string, string | boolean | void>;
20
+ };
21
+ type MinifyHtmlTemplateOptions = boolean | HtmlnanoTemplateRule[];
14
22
  interface HtmlnanoOptions {
15
23
  skipConfigLoading?: boolean;
24
+ configPath?: string;
16
25
  skipInternalWarnings?: boolean;
17
26
  collapseAttributeWhitespace?: boolean;
18
27
  collapseBooleanAttributes?: {
@@ -25,21 +34,46 @@ interface HtmlnanoOptions {
25
34
  mergeStyles?: boolean;
26
35
  mergeScripts?: boolean;
27
36
  minifyCss?: Options | boolean;
37
+ minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
28
38
  minifyConditionalComments?: boolean;
29
39
  minifyJs?: MinifyOptions | boolean;
30
40
  minifyJson?: boolean;
41
+ minifyAttributes?: boolean | {
42
+ metaContent?: boolean;
43
+ redundantWhitespaces?: 'safe' | 'agressive' | false;
44
+ };
31
45
  minifySvg?: Config | boolean;
32
46
  normalizeAttributeValues?: boolean;
33
- removeAttributeQuotes?: boolean;
34
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
47
+ removeAttributeQuotes?: boolean | {
48
+ force?: boolean;
49
+ };
50
+ removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
35
51
  removeEmptyAttributes?: boolean;
52
+ removeEmptyElements?: boolean | {
53
+ removeWithAttributes?: boolean;
54
+ };
36
55
  removeRedundantAttributes?: boolean;
37
56
  removeOptionalTags?: boolean;
38
- removeUnusedCss?: boolean;
57
+ removeUnusedCss?: boolean | ({
58
+ tool: 'purgeCSS';
59
+ } & Omit<UserDefinedOptions, 'content' | 'css' | 'extractors'>) | {
60
+ banner?: boolean;
61
+ csspath?: string;
62
+ htmlroot?: string;
63
+ ignore?: (string | RegExp)[];
64
+ inject?: string;
65
+ jsdom?: object;
66
+ media?: string[];
67
+ report?: boolean;
68
+ strictSSL?: boolean;
69
+ timeout?: number;
70
+ uncssrc?: string;
71
+ userAgent?: string;
72
+ };
39
73
  sortAttributes?: boolean | 'alphabetical' | 'frequency';
40
74
  sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
41
75
  }
42
- interface HtmlnanoPreset extends Omit<HtmlnanoOptions, 'skipConfigLoading'> {
76
+ interface HtmlnanoPreset extends Omit<HtmlnanoOptions, 'skipConfigLoading' | 'configPath'> {
43
77
  }
44
78
 
45
79
  declare const _default: HtmlnanoPreset;