htmlnano 3.0.0 → 3.2.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 (115) hide show
  1. package/README.md +40 -25
  2. package/dist/_modules/collapseAttributeWhitespace.d.mts +41 -5
  3. package/dist/_modules/collapseAttributeWhitespace.d.ts +41 -5
  4. package/dist/_modules/collapseAttributeWhitespace.js +69 -16
  5. package/dist/_modules/collapseAttributeWhitespace.mjs +67 -17
  6. package/dist/_modules/collapseBooleanAttributes.d.mts +36 -3
  7. package/dist/_modules/collapseBooleanAttributes.d.ts +36 -3
  8. package/dist/_modules/collapseBooleanAttributes.js +18 -11
  9. package/dist/_modules/collapseBooleanAttributes.mjs +18 -11
  10. package/dist/_modules/collapseWhitespace.d.mts +36 -3
  11. package/dist/_modules/collapseWhitespace.d.ts +36 -3
  12. package/dist/_modules/collapseWhitespace.js +25 -2
  13. package/dist/_modules/collapseWhitespace.mjs +25 -2
  14. package/dist/_modules/custom.d.mts +36 -3
  15. package/dist/_modules/custom.d.ts +36 -3
  16. package/dist/_modules/deduplicateAttributeValues.d.mts +36 -3
  17. package/dist/_modules/deduplicateAttributeValues.d.ts +36 -3
  18. package/dist/_modules/deduplicateAttributeValues.js +20 -5
  19. package/dist/_modules/deduplicateAttributeValues.mjs +21 -6
  20. package/dist/_modules/example.d.mts +36 -3
  21. package/dist/_modules/example.d.ts +36 -3
  22. package/dist/_modules/mergeScripts.d.mts +36 -3
  23. package/dist/_modules/mergeScripts.d.ts +36 -3
  24. package/dist/_modules/mergeScripts.js +111 -24
  25. package/dist/_modules/mergeScripts.mjs +111 -24
  26. package/dist/_modules/mergeStyles.d.mts +36 -3
  27. package/dist/_modules/mergeStyles.d.ts +36 -3
  28. package/dist/_modules/mergeStyles.js +66 -4
  29. package/dist/_modules/mergeStyles.mjs +66 -4
  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 +36 -3
  35. package/dist/_modules/minifyConditionalComments.d.ts +36 -3
  36. package/dist/_modules/minifyConditionalComments.js +37 -19
  37. package/dist/_modules/minifyConditionalComments.mjs +37 -19
  38. package/dist/_modules/minifyCss.d.mts +36 -3
  39. package/dist/_modules/minifyCss.d.ts +36 -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 +36 -3
  47. package/dist/_modules/minifyJs.d.ts +36 -3
  48. package/dist/_modules/minifyJs.js +106 -5
  49. package/dist/_modules/minifyJs.mjs +107 -6
  50. package/dist/_modules/minifyJson.d.mts +36 -3
  51. package/dist/_modules/minifyJson.d.ts +36 -3
  52. package/dist/_modules/minifyJson.js +8 -11
  53. package/dist/_modules/minifyJson.mjs +8 -11
  54. package/dist/_modules/minifySvg.d.mts +36 -3
  55. package/dist/_modules/minifySvg.d.ts +36 -3
  56. package/dist/_modules/minifySvg.js +35 -4
  57. package/dist/_modules/minifySvg.mjs +35 -4
  58. package/dist/_modules/minifyUrls.d.mts +37 -4
  59. package/dist/_modules/minifyUrls.d.ts +37 -4
  60. package/dist/_modules/minifyUrls.js +52 -27
  61. package/dist/_modules/minifyUrls.mjs +52 -27
  62. package/dist/_modules/normalizeAttributeValues.d.mts +36 -3
  63. package/dist/_modules/normalizeAttributeValues.d.ts +36 -3
  64. package/dist/_modules/normalizeAttributeValues.js +10 -8
  65. package/dist/_modules/normalizeAttributeValues.mjs +10 -8
  66. package/dist/_modules/removeAttributeQuotes.d.mts +40 -4
  67. package/dist/_modules/removeAttributeQuotes.d.ts +40 -4
  68. package/dist/_modules/removeAttributeQuotes.js +9 -4
  69. package/dist/_modules/removeAttributeQuotes.mjs +9 -4
  70. package/dist/_modules/removeComments.d.mts +37 -4
  71. package/dist/_modules/removeComments.d.ts +37 -4
  72. package/dist/_modules/removeComments.js +44 -12
  73. package/dist/_modules/removeComments.mjs +44 -12
  74. package/dist/_modules/removeEmptyAttributes.d.mts +36 -3
  75. package/dist/_modules/removeEmptyAttributes.d.ts +36 -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 +36 -3
  83. package/dist/_modules/removeOptionalTags.d.ts +36 -3
  84. package/dist/_modules/removeOptionalTags.js +39 -28
  85. package/dist/_modules/removeOptionalTags.mjs +39 -28
  86. package/dist/_modules/removeRedundantAttributes.d.mts +36 -3
  87. package/dist/_modules/removeRedundantAttributes.d.ts +36 -3
  88. package/dist/_modules/removeRedundantAttributes.js +43 -28
  89. package/dist/_modules/removeRedundantAttributes.mjs +43 -28
  90. package/dist/_modules/removeUnusedCss.d.mts +37 -3
  91. package/dist/_modules/removeUnusedCss.d.ts +37 -3
  92. package/dist/_modules/removeUnusedCss.js +40 -14
  93. package/dist/_modules/removeUnusedCss.mjs +41 -15
  94. package/dist/_modules/sortAttributes.d.mts +39 -5
  95. package/dist/_modules/sortAttributes.d.ts +39 -5
  96. package/dist/_modules/sortAttributes.js +27 -8
  97. package/dist/_modules/sortAttributes.mjs +27 -8
  98. package/dist/_modules/sortAttributesWithLists.d.mts +39 -4
  99. package/dist/_modules/sortAttributesWithLists.d.ts +39 -4
  100. package/dist/_modules/sortAttributesWithLists.js +61 -52
  101. package/dist/_modules/sortAttributesWithLists.mjs +62 -53
  102. package/dist/helpers.d.ts +8 -1
  103. package/dist/helpers.js +48 -0
  104. package/dist/helpers.mjs +45 -1
  105. package/dist/index.d.ts +37 -4
  106. package/dist/index.js +13 -0
  107. package/dist/index.mjs +13 -0
  108. package/dist/presets/ampSafe.d.ts +36 -3
  109. package/dist/presets/max.d.ts +36 -3
  110. package/dist/presets/max.js +13 -5
  111. package/dist/presets/max.mjs +13 -5
  112. package/dist/presets/safe.d.ts +36 -3
  113. package/dist/presets/safe.js +6 -0
  114. package/dist/presets/safe.mjs +6 -0
  115. package/package.json +27 -15
@@ -2,16 +2,24 @@ 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;
17
25
  configPath?: string;
@@ -27,17 +35,42 @@ interface HtmlnanoOptions {
27
35
  mergeStyles?: boolean;
28
36
  mergeScripts?: boolean;
29
37
  minifyCss?: Options | boolean;
38
+ minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
30
39
  minifyConditionalComments?: boolean;
31
40
  minifyJs?: MinifyOptions | boolean;
32
41
  minifyJson?: boolean;
42
+ minifyAttributes?: boolean | {
43
+ metaContent?: boolean;
44
+ redundantWhitespaces?: 'safe' | 'agressive' | false;
45
+ };
33
46
  minifySvg?: Config | boolean;
34
47
  normalizeAttributeValues?: boolean;
35
- removeAttributeQuotes?: boolean;
36
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
48
+ removeAttributeQuotes?: boolean | {
49
+ force?: boolean;
50
+ };
51
+ removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
37
52
  removeEmptyAttributes?: boolean;
53
+ removeEmptyElements?: boolean | {
54
+ removeWithAttributes?: boolean;
55
+ };
38
56
  removeRedundantAttributes?: boolean;
39
57
  removeOptionalTags?: boolean;
40
- 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
+ };
41
74
  sortAttributes?: boolean | 'alphabetical' | 'frequency';
42
75
  sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
43
76
  }
@@ -2,16 +2,24 @@ 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;
17
25
  configPath?: string;
@@ -27,17 +35,42 @@ interface HtmlnanoOptions {
27
35
  mergeStyles?: boolean;
28
36
  mergeScripts?: boolean;
29
37
  minifyCss?: Options | boolean;
38
+ minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
30
39
  minifyConditionalComments?: boolean;
31
40
  minifyJs?: MinifyOptions | boolean;
32
41
  minifyJson?: boolean;
42
+ minifyAttributes?: boolean | {
43
+ metaContent?: boolean;
44
+ redundantWhitespaces?: 'safe' | 'agressive' | false;
45
+ };
33
46
  minifySvg?: Config | boolean;
34
47
  normalizeAttributeValues?: boolean;
35
- removeAttributeQuotes?: boolean;
36
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
48
+ removeAttributeQuotes?: boolean | {
49
+ force?: boolean;
50
+ };
51
+ removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
37
52
  removeEmptyAttributes?: boolean;
53
+ removeEmptyElements?: boolean | {
54
+ removeWithAttributes?: boolean;
55
+ };
38
56
  removeRedundantAttributes?: boolean;
39
57
  removeOptionalTags?: boolean;
40
- 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
+ };
41
74
  sortAttributes?: boolean | 'alphabetical' | 'frequency';
42
75
  sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
43
76
  }
@@ -7,7 +7,8 @@ const bodyStartTagCantBeOmittedWithFirstChildTags = new Set([
7
7
  'meta',
8
8
  'link',
9
9
  'script',
10
- 'style'
10
+ 'style',
11
+ 'template'
11
12
  ]);
12
13
  const tbodyStartTagCantBeOmittedWithPrecededTags = new Set([
13
14
  'tbody',
@@ -70,32 +71,40 @@ function getNextNode(tree, currentNodeIndex, nonEmpty = false) {
70
71
  }
71
72
  return null;
72
73
  }
74
+ function omitTag(node) {
75
+ const optionalTagNode = node;
76
+ optionalTagNode.optionalTagName = typeof node.tag === 'string' ? node.tag : undefined;
77
+ // @ts-expect-error -- deliberately set tag to false
78
+ node.tag = false;
79
+ }
80
+ function getNodeTagName(node) {
81
+ if (node.tag) return node.tag;
82
+ const optionalTagNode = node;
83
+ return optionalTagNode.optionalTagName || false;
84
+ }
73
85
  function removeOptionalTags(tree) {
74
86
  tree.forEach((node, index)=>{
75
87
  if (typeof node === 'string') return node;
76
88
  if (!node.tag) return node;
77
89
  if (node.attrs && Object.keys(node.attrs).length) return node;
78
90
  // const prevNode = getPrevNode(tree, index);
79
- const prevNonEmptyNode = getPrevNode(tree, index, true);
91
+ const prevNode = getPrevNode(tree, index);
80
92
  const nextNode = getNextNode(tree, index);
81
- const nextNonEmptyNode = getNextNode(tree, index, true);
82
93
  const firstChildNode = getFirstChildTag(node, false);
83
- const firstNonEmptyChildNode = getFirstChildTag(node);
84
94
  /**
85
95
  * An "html" element's start tag may be omitted if the first thing inside the "html" element is not a comment.
86
96
  * An "html" element's end tag may be omitted if the "html" element is not IMMEDIATELY followed by a comment.
87
97
  */ if (node.tag === 'html') {
88
98
  let isHtmlStartTagCanBeOmitted = true;
89
99
  let isHtmlEndTagCanBeOmitted = true;
90
- if (typeof firstNonEmptyChildNode === 'string' && helpers_js.isComment(firstNonEmptyChildNode)) {
100
+ if (typeof firstChildNode === 'string' && helpers_js.isComment(firstChildNode)) {
91
101
  isHtmlStartTagCanBeOmitted = false;
92
102
  }
93
- if (typeof nextNonEmptyNode === 'string' && helpers_js.isComment(nextNonEmptyNode)) {
103
+ if (typeof nextNode === 'string' && helpers_js.isComment(nextNode)) {
94
104
  isHtmlEndTagCanBeOmitted = false;
95
105
  }
96
106
  if (isHtmlStartTagCanBeOmitted && isHtmlEndTagCanBeOmitted) {
97
- // @ts-expect-error -- deliberately set tag to false
98
- node.tag = false;
107
+ omitTag(node);
99
108
  }
100
109
  }
101
110
  /**
@@ -104,15 +113,14 @@ function removeOptionalTags(tree) {
104
113
  */ if (node.tag === 'head') {
105
114
  let isHeadStartTagCanBeOmitted = false;
106
115
  let isHeadEndTagCanBeOmitted = true;
107
- if (isEmptyNode(node) || firstNonEmptyChildNode && typeof firstNonEmptyChildNode === 'object' && firstNonEmptyChildNode.tag) {
116
+ if (isEmptyNode(node) || firstChildNode && typeof firstChildNode === 'object' && firstChildNode.tag) {
108
117
  isHeadStartTagCanBeOmitted = true;
109
118
  }
110
- if (nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode) || nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && helpers_js.isComment(nextNode)) {
119
+ if (nextNode && typeof nextNode === 'string' && (startWithWhitespacePattern.test(nextNode) || helpers_js.isComment(nextNode))) {
111
120
  isHeadEndTagCanBeOmitted = false;
112
121
  }
113
122
  if (isHeadStartTagCanBeOmitted && isHeadEndTagCanBeOmitted) {
114
- // @ts-expect-error -- deliberately set tag to false
115
- node.tag = false;
123
+ omitTag(node);
116
124
  }
117
125
  }
118
126
  /**
@@ -121,18 +129,17 @@ function removeOptionalTags(tree) {
121
129
  */ if (node.tag === 'body') {
122
130
  let isBodyStartTagCanBeOmitted = true;
123
131
  let isBodyEndTagCanBeOmitted = true;
124
- if (typeof firstChildNode === 'string' && startWithWhitespacePattern.test(firstChildNode) || typeof firstNonEmptyChildNode === 'string' && helpers_js.isComment(firstNonEmptyChildNode)) {
132
+ if (typeof firstChildNode === 'string' && startWithWhitespacePattern.test(firstChildNode) || typeof firstChildNode === 'string' && helpers_js.isComment(firstChildNode)) {
125
133
  isBodyStartTagCanBeOmitted = false;
126
134
  }
127
- if (firstNonEmptyChildNode && typeof firstNonEmptyChildNode === 'object' && firstNonEmptyChildNode.tag && bodyStartTagCantBeOmittedWithFirstChildTags.has(firstNonEmptyChildNode.tag)) {
135
+ if (firstChildNode && typeof firstChildNode === 'object' && firstChildNode.tag && bodyStartTagCantBeOmittedWithFirstChildTags.has(firstChildNode.tag)) {
128
136
  isBodyStartTagCanBeOmitted = false;
129
137
  }
130
138
  if (nextNode && typeof nextNode === 'string' && helpers_js.isComment(nextNode)) {
131
139
  isBodyEndTagCanBeOmitted = false;
132
140
  }
133
141
  if (isBodyStartTagCanBeOmitted && isBodyEndTagCanBeOmitted) {
134
- // @ts-expect-error -- deliberately set tag to false
135
- node.tag = false;
142
+ omitTag(node);
136
143
  }
137
144
  }
138
145
  /**
@@ -141,18 +148,17 @@ function removeOptionalTags(tree) {
141
148
  */ if (node.tag === 'colgroup') {
142
149
  let isColgroupStartTagCanBeOmitted = false;
143
150
  let isColgroupEndTagCanBeOmitted = true;
144
- if (firstNonEmptyChildNode && typeof firstNonEmptyChildNode === 'object' && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'col') {
151
+ if (firstChildNode && typeof firstChildNode === 'object' && firstChildNode.tag && firstChildNode.tag === 'col') {
145
152
  isColgroupStartTagCanBeOmitted = true;
146
153
  }
147
- if (prevNonEmptyNode && typeof prevNonEmptyNode === 'object' && prevNonEmptyNode.tag && prevNonEmptyNode.tag === 'colgroup') {
154
+ if (prevNode && typeof prevNode === 'object' && getNodeTagName(prevNode) === 'colgroup') {
148
155
  isColgroupStartTagCanBeOmitted = false;
149
156
  }
150
- if (nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode) || nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && helpers_js.isComment(nextNonEmptyNode)) {
157
+ if (nextNode && typeof nextNode === 'string' && (startWithWhitespacePattern.test(nextNode) || helpers_js.isComment(nextNode))) {
151
158
  isColgroupEndTagCanBeOmitted = false;
152
159
  }
153
160
  if (isColgroupStartTagCanBeOmitted && isColgroupEndTagCanBeOmitted) {
154
- // @ts-expect-error -- deliberately set tag to false
155
- node.tag = false;
161
+ omitTag(node);
156
162
  }
157
163
  }
158
164
  /**
@@ -161,18 +167,23 @@ function removeOptionalTags(tree) {
161
167
  */ if (node.tag === 'tbody') {
162
168
  let isTbodyStartTagCanBeOmitted = false;
163
169
  let isTbodyEndTagCanBeOmitted = true;
164
- if (firstNonEmptyChildNode && typeof firstNonEmptyChildNode === 'object' && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'tr') {
170
+ if (firstChildNode && typeof firstChildNode === 'object' && firstChildNode.tag && firstChildNode.tag === 'tr') {
165
171
  isTbodyStartTagCanBeOmitted = true;
166
172
  }
167
- if (prevNonEmptyNode && typeof prevNonEmptyNode === 'object' && prevNonEmptyNode.tag && tbodyStartTagCantBeOmittedWithPrecededTags.has(prevNonEmptyNode.tag)) {
168
- isTbodyStartTagCanBeOmitted = false;
173
+ if (prevNode && typeof prevNode === 'object') {
174
+ const prevTagName = getNodeTagName(prevNode);
175
+ if (prevTagName && tbodyStartTagCantBeOmittedWithPrecededTags.has(prevTagName)) {
176
+ isTbodyStartTagCanBeOmitted = false;
177
+ }
169
178
  }
170
- if (nextNonEmptyNode && typeof nextNonEmptyNode === 'object' && nextNonEmptyNode.tag && tbodyEndTagCantBeOmittedWithFollowedTags.has(nextNonEmptyNode.tag)) {
171
- isTbodyEndTagCanBeOmitted = false;
179
+ if (nextNode && typeof nextNode === 'object') {
180
+ const nextTagName = getNodeTagName(nextNode);
181
+ if (nextTagName && tbodyEndTagCantBeOmittedWithFollowedTags.has(nextTagName)) {
182
+ isTbodyEndTagCanBeOmitted = false;
183
+ }
172
184
  }
173
185
  if (isTbodyStartTagCanBeOmitted && isTbodyEndTagCanBeOmitted) {
174
- // @ts-expect-error -- deliberately set tag to false
175
- node.tag = false;
186
+ omitTag(node);
176
187
  }
177
188
  }
178
189
  if (node.content && node.content.length) {
@@ -5,7 +5,8 @@ const bodyStartTagCantBeOmittedWithFirstChildTags = new Set([
5
5
  'meta',
6
6
  'link',
7
7
  'script',
8
- 'style'
8
+ 'style',
9
+ 'template'
9
10
  ]);
10
11
  const tbodyStartTagCantBeOmittedWithPrecededTags = new Set([
11
12
  'tbody',
@@ -68,32 +69,40 @@ function getNextNode(tree, currentNodeIndex, nonEmpty = false) {
68
69
  }
69
70
  return null;
70
71
  }
72
+ function omitTag(node) {
73
+ const optionalTagNode = node;
74
+ optionalTagNode.optionalTagName = typeof node.tag === 'string' ? node.tag : undefined;
75
+ // @ts-expect-error -- deliberately set tag to false
76
+ node.tag = false;
77
+ }
78
+ function getNodeTagName(node) {
79
+ if (node.tag) return node.tag;
80
+ const optionalTagNode = node;
81
+ return optionalTagNode.optionalTagName || false;
82
+ }
71
83
  function removeOptionalTags(tree) {
72
84
  tree.forEach((node, index)=>{
73
85
  if (typeof node === 'string') return node;
74
86
  if (!node.tag) return node;
75
87
  if (node.attrs && Object.keys(node.attrs).length) return node;
76
88
  // const prevNode = getPrevNode(tree, index);
77
- const prevNonEmptyNode = getPrevNode(tree, index, true);
89
+ const prevNode = getPrevNode(tree, index);
78
90
  const nextNode = getNextNode(tree, index);
79
- const nextNonEmptyNode = getNextNode(tree, index, true);
80
91
  const firstChildNode = getFirstChildTag(node, false);
81
- const firstNonEmptyChildNode = getFirstChildTag(node);
82
92
  /**
83
93
  * An "html" element's start tag may be omitted if the first thing inside the "html" element is not a comment.
84
94
  * An "html" element's end tag may be omitted if the "html" element is not IMMEDIATELY followed by a comment.
85
95
  */ if (node.tag === 'html') {
86
96
  let isHtmlStartTagCanBeOmitted = true;
87
97
  let isHtmlEndTagCanBeOmitted = true;
88
- if (typeof firstNonEmptyChildNode === 'string' && isComment(firstNonEmptyChildNode)) {
98
+ if (typeof firstChildNode === 'string' && isComment(firstChildNode)) {
89
99
  isHtmlStartTagCanBeOmitted = false;
90
100
  }
91
- if (typeof nextNonEmptyNode === 'string' && isComment(nextNonEmptyNode)) {
101
+ if (typeof nextNode === 'string' && isComment(nextNode)) {
92
102
  isHtmlEndTagCanBeOmitted = false;
93
103
  }
94
104
  if (isHtmlStartTagCanBeOmitted && isHtmlEndTagCanBeOmitted) {
95
- // @ts-expect-error -- deliberately set tag to false
96
- node.tag = false;
105
+ omitTag(node);
97
106
  }
98
107
  }
99
108
  /**
@@ -102,15 +111,14 @@ function removeOptionalTags(tree) {
102
111
  */ if (node.tag === 'head') {
103
112
  let isHeadStartTagCanBeOmitted = false;
104
113
  let isHeadEndTagCanBeOmitted = true;
105
- if (isEmptyNode(node) || firstNonEmptyChildNode && typeof firstNonEmptyChildNode === 'object' && firstNonEmptyChildNode.tag) {
114
+ if (isEmptyNode(node) || firstChildNode && typeof firstChildNode === 'object' && firstChildNode.tag) {
106
115
  isHeadStartTagCanBeOmitted = true;
107
116
  }
108
- if (nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode) || nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && isComment(nextNode)) {
117
+ if (nextNode && typeof nextNode === 'string' && (startWithWhitespacePattern.test(nextNode) || isComment(nextNode))) {
109
118
  isHeadEndTagCanBeOmitted = false;
110
119
  }
111
120
  if (isHeadStartTagCanBeOmitted && isHeadEndTagCanBeOmitted) {
112
- // @ts-expect-error -- deliberately set tag to false
113
- node.tag = false;
121
+ omitTag(node);
114
122
  }
115
123
  }
116
124
  /**
@@ -119,18 +127,17 @@ function removeOptionalTags(tree) {
119
127
  */ if (node.tag === 'body') {
120
128
  let isBodyStartTagCanBeOmitted = true;
121
129
  let isBodyEndTagCanBeOmitted = true;
122
- if (typeof firstChildNode === 'string' && startWithWhitespacePattern.test(firstChildNode) || typeof firstNonEmptyChildNode === 'string' && isComment(firstNonEmptyChildNode)) {
130
+ if (typeof firstChildNode === 'string' && startWithWhitespacePattern.test(firstChildNode) || typeof firstChildNode === 'string' && isComment(firstChildNode)) {
123
131
  isBodyStartTagCanBeOmitted = false;
124
132
  }
125
- if (firstNonEmptyChildNode && typeof firstNonEmptyChildNode === 'object' && firstNonEmptyChildNode.tag && bodyStartTagCantBeOmittedWithFirstChildTags.has(firstNonEmptyChildNode.tag)) {
133
+ if (firstChildNode && typeof firstChildNode === 'object' && firstChildNode.tag && bodyStartTagCantBeOmittedWithFirstChildTags.has(firstChildNode.tag)) {
126
134
  isBodyStartTagCanBeOmitted = false;
127
135
  }
128
136
  if (nextNode && typeof nextNode === 'string' && isComment(nextNode)) {
129
137
  isBodyEndTagCanBeOmitted = false;
130
138
  }
131
139
  if (isBodyStartTagCanBeOmitted && isBodyEndTagCanBeOmitted) {
132
- // @ts-expect-error -- deliberately set tag to false
133
- node.tag = false;
140
+ omitTag(node);
134
141
  }
135
142
  }
136
143
  /**
@@ -139,18 +146,17 @@ function removeOptionalTags(tree) {
139
146
  */ if (node.tag === 'colgroup') {
140
147
  let isColgroupStartTagCanBeOmitted = false;
141
148
  let isColgroupEndTagCanBeOmitted = true;
142
- if (firstNonEmptyChildNode && typeof firstNonEmptyChildNode === 'object' && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'col') {
149
+ if (firstChildNode && typeof firstChildNode === 'object' && firstChildNode.tag && firstChildNode.tag === 'col') {
143
150
  isColgroupStartTagCanBeOmitted = true;
144
151
  }
145
- if (prevNonEmptyNode && typeof prevNonEmptyNode === 'object' && prevNonEmptyNode.tag && prevNonEmptyNode.tag === 'colgroup') {
152
+ if (prevNode && typeof prevNode === 'object' && getNodeTagName(prevNode) === 'colgroup') {
146
153
  isColgroupStartTagCanBeOmitted = false;
147
154
  }
148
- if (nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode) || nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && isComment(nextNonEmptyNode)) {
155
+ if (nextNode && typeof nextNode === 'string' && (startWithWhitespacePattern.test(nextNode) || isComment(nextNode))) {
149
156
  isColgroupEndTagCanBeOmitted = false;
150
157
  }
151
158
  if (isColgroupStartTagCanBeOmitted && isColgroupEndTagCanBeOmitted) {
152
- // @ts-expect-error -- deliberately set tag to false
153
- node.tag = false;
159
+ omitTag(node);
154
160
  }
155
161
  }
156
162
  /**
@@ -159,18 +165,23 @@ function removeOptionalTags(tree) {
159
165
  */ if (node.tag === 'tbody') {
160
166
  let isTbodyStartTagCanBeOmitted = false;
161
167
  let isTbodyEndTagCanBeOmitted = true;
162
- if (firstNonEmptyChildNode && typeof firstNonEmptyChildNode === 'object' && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'tr') {
168
+ if (firstChildNode && typeof firstChildNode === 'object' && firstChildNode.tag && firstChildNode.tag === 'tr') {
163
169
  isTbodyStartTagCanBeOmitted = true;
164
170
  }
165
- if (prevNonEmptyNode && typeof prevNonEmptyNode === 'object' && prevNonEmptyNode.tag && tbodyStartTagCantBeOmittedWithPrecededTags.has(prevNonEmptyNode.tag)) {
166
- isTbodyStartTagCanBeOmitted = false;
171
+ if (prevNode && typeof prevNode === 'object') {
172
+ const prevTagName = getNodeTagName(prevNode);
173
+ if (prevTagName && tbodyStartTagCantBeOmittedWithPrecededTags.has(prevTagName)) {
174
+ isTbodyStartTagCanBeOmitted = false;
175
+ }
167
176
  }
168
- if (nextNonEmptyNode && typeof nextNonEmptyNode === 'object' && nextNonEmptyNode.tag && tbodyEndTagCantBeOmittedWithFollowedTags.has(nextNonEmptyNode.tag)) {
169
- isTbodyEndTagCanBeOmitted = false;
177
+ if (nextNode && typeof nextNode === 'object') {
178
+ const nextTagName = getNodeTagName(nextNode);
179
+ if (nextTagName && tbodyEndTagCantBeOmittedWithFollowedTags.has(nextTagName)) {
180
+ isTbodyEndTagCanBeOmitted = false;
181
+ }
170
182
  }
171
183
  if (isTbodyStartTagCanBeOmitted && isTbodyEndTagCanBeOmitted) {
172
- // @ts-expect-error -- deliberately set tag to false
173
- node.tag = false;
184
+ omitTag(node);
174
185
  }
175
186
  }
176
187
  if (node.content && node.content.length) {
@@ -2,16 +2,24 @@ 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;
17
25
  configPath?: string;
@@ -27,17 +35,42 @@ interface HtmlnanoOptions {
27
35
  mergeStyles?: boolean;
28
36
  mergeScripts?: boolean;
29
37
  minifyCss?: Options | boolean;
38
+ minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
30
39
  minifyConditionalComments?: boolean;
31
40
  minifyJs?: MinifyOptions | boolean;
32
41
  minifyJson?: boolean;
42
+ minifyAttributes?: boolean | {
43
+ metaContent?: boolean;
44
+ redundantWhitespaces?: 'safe' | 'agressive' | false;
45
+ };
33
46
  minifySvg?: Config | boolean;
34
47
  normalizeAttributeValues?: boolean;
35
- removeAttributeQuotes?: boolean;
36
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
48
+ removeAttributeQuotes?: boolean | {
49
+ force?: boolean;
50
+ };
51
+ removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
37
52
  removeEmptyAttributes?: boolean;
53
+ removeEmptyElements?: boolean | {
54
+ removeWithAttributes?: boolean;
55
+ };
38
56
  removeRedundantAttributes?: boolean;
39
57
  removeOptionalTags?: boolean;
40
- 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
+ };
41
74
  sortAttributes?: boolean | 'alphabetical' | 'frequency';
42
75
  sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
43
76
  }
@@ -2,16 +2,24 @@ 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;
17
25
  configPath?: string;
@@ -27,17 +35,42 @@ interface HtmlnanoOptions {
27
35
  mergeStyles?: boolean;
28
36
  mergeScripts?: boolean;
29
37
  minifyCss?: Options | boolean;
38
+ minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
30
39
  minifyConditionalComments?: boolean;
31
40
  minifyJs?: MinifyOptions | boolean;
32
41
  minifyJson?: boolean;
42
+ minifyAttributes?: boolean | {
43
+ metaContent?: boolean;
44
+ redundantWhitespaces?: 'safe' | 'agressive' | false;
45
+ };
33
46
  minifySvg?: Config | boolean;
34
47
  normalizeAttributeValues?: boolean;
35
- removeAttributeQuotes?: boolean;
36
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
48
+ removeAttributeQuotes?: boolean | {
49
+ force?: boolean;
50
+ };
51
+ removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
37
52
  removeEmptyAttributes?: boolean;
53
+ removeEmptyElements?: boolean | {
54
+ removeWithAttributes?: boolean;
55
+ };
38
56
  removeRedundantAttributes?: boolean;
39
57
  removeOptionalTags?: boolean;
40
- 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
+ };
41
74
  sortAttributes?: boolean | 'alphabetical' | 'frequency';
42
75
  sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
43
76
  }