htmlnano 3.0.0 → 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 (115) hide show
  1. package/README.md +18 -17
  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 +99 -24
  25. package/dist/_modules/mergeScripts.mjs +99 -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 +56 -4
  29. package/dist/_modules/mergeStyles.mjs +56 -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 +94 -5
  49. package/dist/_modules/minifyJs.mjs +95 -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 +38 -13
  93. package/dist/_modules/removeUnusedCss.mjs +39 -14
  94. package/dist/_modules/sortAttributes.d.mts +36 -3
  95. package/dist/_modules/sortAttributes.d.ts +36 -3
  96. package/dist/_modules/sortAttributes.js +23 -5
  97. package/dist/_modules/sortAttributes.mjs +23 -5
  98. package/dist/_modules/sortAttributesWithLists.d.mts +36 -3
  99. package/dist/_modules/sortAttributesWithLists.d.ts +36 -3
  100. package/dist/_modules/sortAttributesWithLists.js +30 -8
  101. package/dist/_modules/sortAttributesWithLists.mjs +31 -9
  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 +4 -0
  111. package/dist/presets/max.mjs +4 -0
  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 +21 -13
@@ -118,21 +118,19 @@ let STORED_URL_BASE;
118
118
  for (const [attrName, attrValue] of Object.entries(node.attrs)){
119
119
  const attrNameLower = attrName.toLowerCase();
120
120
  if (isUriTypeAttribute(node.tag, attrNameLower)) {
121
- if (isJavaScriptUrl(attrValue)) {
122
- promises.push(minifyJavaScriptUrl(node, attrName, terser));
123
- } else {
124
- if (relateUrlInstance && attrValue) {
125
- // FIXME!
126
- // relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
127
- // the WHATWG URL API is very strict while attrValue might not be a valid URL
128
- // new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
129
- node.attrs[attrName] = relateUrlInstance.relate(attrValue);
130
- }
121
+ if (typeof attrValue !== 'string') continue;
122
+ const javascriptMatch = getJavaScriptUrlMatch(attrValue);
123
+ if (javascriptMatch) {
124
+ promises.push(minifyJavaScriptUrl(node, attrName, javascriptMatch, terser));
125
+ continue;
126
+ }
127
+ if (relateUrlInstance) {
128
+ node.attrs[attrName] = relateUrlValue(relateUrlInstance, attrValue);
131
129
  }
132
130
  continue;
133
131
  }
134
132
  if (isSrcsetAttribute(node.tag, attrNameLower)) {
135
- if (srcset && attrValue) {
133
+ if (srcset && typeof attrValue === 'string') {
136
134
  try {
137
135
  const parsedSrcset = srcset.parseSrcset(attrValue, {
138
136
  strict: true
@@ -140,7 +138,7 @@ let STORED_URL_BASE;
140
138
  node.attrs[attrName] = srcset.stringifySrcset(parsedSrcset.map((item)=>{
141
139
  if (relateUrlInstance) {
142
140
  // @ts-expect-error -- not actually readonly
143
- item.url = relateUrlInstance.relate(item.url);
141
+ item.url = relateUrlValue(relateUrlInstance, item.url);
144
142
  }
145
143
  return item;
146
144
  }));
@@ -157,24 +155,51 @@ let STORED_URL_BASE;
157
155
  return Promise.resolve(tree);
158
156
  }
159
157
  };
160
- function isJavaScriptUrl(url) {
161
- return typeof url === 'string' && url.toLowerCase().startsWith(JAVASCRIPT_URL_PROTOCOL);
162
- }
163
158
  const jsWrapperStart = 'function a(){';
164
159
  const jsWrapperEnd = '}a();';
165
- function minifyJavaScriptUrl(node, attrName, terser) {
166
- var _node_attrs;
167
- if (!terser) return Promise.resolve();
168
- let result = (_node_attrs = node.attrs) == null ? void 0 : _node_attrs[attrName];
169
- if (result) {
170
- result = jsWrapperStart + result.slice(JAVASCRIPT_URL_PROTOCOL.length) + jsWrapperEnd;
171
- return terser.minify(result, {}) // Default Option is good enough
172
- .then(({ code })=>{
173
- const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
174
- node.attrs[attrName] = JAVASCRIPT_URL_PROTOCOL + minifiedJs;
175
- });
160
+ const javascriptUrlRegex = /^(\s*)(javascript:)([\s\S]*)$/i;
161
+ function getJavaScriptUrlMatch(url) {
162
+ const match = javascriptUrlRegex.exec(url);
163
+ if (!match) return null;
164
+ return {
165
+ leadingWhitespace: match[1],
166
+ code: match[3]
167
+ };
168
+ }
169
+ function getUrlScheme(value) {
170
+ const match = /^[a-z][a-z0-9+.-]*:/i.exec(value);
171
+ if (!match) return null;
172
+ return match[0].slice(0, -1).toLowerCase();
173
+ }
174
+ function shouldRelateUrlValue(value) {
175
+ const trimmed = value.trim();
176
+ if (!trimmed) return false;
177
+ if (trimmed.startsWith('#') || trimmed.startsWith('?')) return false;
178
+ const scheme = getUrlScheme(trimmed);
179
+ if (scheme) return scheme === 'http' || scheme === 'https';
180
+ return true;
181
+ }
182
+ function relateUrlValue(relateUrl, value) {
183
+ if (!shouldRelateUrlValue(value)) return value;
184
+ // FIXME!
185
+ // relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
186
+ // the WHATWG URL API is very strict while attrValue might not be a valid URL
187
+ // new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
188
+ try {
189
+ return relateUrl.relate(value);
190
+ } catch (unused) {
191
+ return value;
176
192
  }
177
- return Promise.resolve();
193
+ }
194
+ function minifyJavaScriptUrl(node, attrName, match, terser) {
195
+ if (!terser) return Promise.resolve();
196
+ const result = jsWrapperStart + match.code + jsWrapperEnd;
197
+ return terser.minify(result, {}) // Default Option is good enough
198
+ .then(({ code })=>{
199
+ if (!code) return;
200
+ const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
201
+ node.attrs[attrName] = match.leadingWhitespace + JAVASCRIPT_URL_PROTOCOL + minifiedJs;
202
+ }).catch(()=>undefined);
178
203
  }
179
204
 
180
205
  exports.default = mod;
@@ -116,21 +116,19 @@ let STORED_URL_BASE;
116
116
  for (const [attrName, attrValue] of Object.entries(node.attrs)){
117
117
  const attrNameLower = attrName.toLowerCase();
118
118
  if (isUriTypeAttribute(node.tag, attrNameLower)) {
119
- if (isJavaScriptUrl(attrValue)) {
120
- promises.push(minifyJavaScriptUrl(node, attrName, terser));
121
- } else {
122
- if (relateUrlInstance && attrValue) {
123
- // FIXME!
124
- // relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
125
- // the WHATWG URL API is very strict while attrValue might not be a valid URL
126
- // new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
127
- node.attrs[attrName] = relateUrlInstance.relate(attrValue);
128
- }
119
+ if (typeof attrValue !== 'string') continue;
120
+ const javascriptMatch = getJavaScriptUrlMatch(attrValue);
121
+ if (javascriptMatch) {
122
+ promises.push(minifyJavaScriptUrl(node, attrName, javascriptMatch, terser));
123
+ continue;
124
+ }
125
+ if (relateUrlInstance) {
126
+ node.attrs[attrName] = relateUrlValue(relateUrlInstance, attrValue);
129
127
  }
130
128
  continue;
131
129
  }
132
130
  if (isSrcsetAttribute(node.tag, attrNameLower)) {
133
- if (srcset && attrValue) {
131
+ if (srcset && typeof attrValue === 'string') {
134
132
  try {
135
133
  const parsedSrcset = srcset.parseSrcset(attrValue, {
136
134
  strict: true
@@ -138,7 +136,7 @@ let STORED_URL_BASE;
138
136
  node.attrs[attrName] = srcset.stringifySrcset(parsedSrcset.map((item)=>{
139
137
  if (relateUrlInstance) {
140
138
  // @ts-expect-error -- not actually readonly
141
- item.url = relateUrlInstance.relate(item.url);
139
+ item.url = relateUrlValue(relateUrlInstance, item.url);
142
140
  }
143
141
  return item;
144
142
  }));
@@ -155,24 +153,51 @@ let STORED_URL_BASE;
155
153
  return Promise.resolve(tree);
156
154
  }
157
155
  };
158
- function isJavaScriptUrl(url) {
159
- return typeof url === 'string' && url.toLowerCase().startsWith(JAVASCRIPT_URL_PROTOCOL);
160
- }
161
156
  const jsWrapperStart = 'function a(){';
162
157
  const jsWrapperEnd = '}a();';
163
- function minifyJavaScriptUrl(node, attrName, terser) {
164
- var _node_attrs;
165
- if (!terser) return Promise.resolve();
166
- let result = (_node_attrs = node.attrs) == null ? void 0 : _node_attrs[attrName];
167
- if (result) {
168
- result = jsWrapperStart + result.slice(JAVASCRIPT_URL_PROTOCOL.length) + jsWrapperEnd;
169
- return terser.minify(result, {}) // Default Option is good enough
170
- .then(({ code })=>{
171
- const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
172
- node.attrs[attrName] = JAVASCRIPT_URL_PROTOCOL + minifiedJs;
173
- });
158
+ const javascriptUrlRegex = /^(\s*)(javascript:)([\s\S]*)$/i;
159
+ function getJavaScriptUrlMatch(url) {
160
+ const match = javascriptUrlRegex.exec(url);
161
+ if (!match) return null;
162
+ return {
163
+ leadingWhitespace: match[1],
164
+ code: match[3]
165
+ };
166
+ }
167
+ function getUrlScheme(value) {
168
+ const match = /^[a-z][a-z0-9+.-]*:/i.exec(value);
169
+ if (!match) return null;
170
+ return match[0].slice(0, -1).toLowerCase();
171
+ }
172
+ function shouldRelateUrlValue(value) {
173
+ const trimmed = value.trim();
174
+ if (!trimmed) return false;
175
+ if (trimmed.startsWith('#') || trimmed.startsWith('?')) return false;
176
+ const scheme = getUrlScheme(trimmed);
177
+ if (scheme) return scheme === 'http' || scheme === 'https';
178
+ return true;
179
+ }
180
+ function relateUrlValue(relateUrl, value) {
181
+ if (!shouldRelateUrlValue(value)) return value;
182
+ // FIXME!
183
+ // relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
184
+ // the WHATWG URL API is very strict while attrValue might not be a valid URL
185
+ // new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
186
+ try {
187
+ return relateUrl.relate(value);
188
+ } catch (unused) {
189
+ return value;
174
190
  }
175
- return Promise.resolve();
191
+ }
192
+ function minifyJavaScriptUrl(node, attrName, match, terser) {
193
+ if (!terser) return Promise.resolve();
194
+ const result = jsWrapperStart + match.code + jsWrapperEnd;
195
+ return terser.minify(result, {}) // Default Option is good enough
196
+ .then(({ code })=>{
197
+ if (!code) return;
198
+ const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
199
+ node.attrs[attrName] = match.leadingWhitespace + JAVASCRIPT_URL_PROTOCOL + minifiedJs;
200
+ }).catch(()=>undefined);
176
201
  }
177
202
 
178
203
  export { mod as default };
@@ -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
  }
@@ -213,15 +213,17 @@ const mod = {
213
213
  const newAttrs = attrs;
214
214
  Object.entries(attrs).forEach(([attrName, attrValue])=>{
215
215
  let newAttrValue = attrValue;
216
- if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName) && (caseInsensitiveAttributes[attrName] === null || node.tag && caseInsensitiveAttributes[attrName].includes(node.tag))) {
217
- newAttrValue = typeof attrValue === 'string' ? attrValue.toLowerCase() : attrValue;
216
+ const hasCaseInsensitive = Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName) && (caseInsensitiveAttributes[attrName] === null || node.tag && caseInsensitiveAttributes[attrName].includes(node.tag));
217
+ const hasInvalidValueDefault = Object.hasOwnProperty.call(invalidValueDefault, attrName);
218
+ const invalidMeta = hasInvalidValueDefault ? invalidValueDefault[attrName] : undefined;
219
+ const appliesInvalidValueDefault = Boolean(invalidMeta && (invalidMeta.tag === null || node && node.tag && invalidMeta.tag.includes(node.tag)));
220
+ const shouldNormalizeCase = hasCaseInsensitive || appliesInvalidValueDefault;
221
+ if (typeof attrValue === 'string' && shouldNormalizeCase) {
222
+ newAttrValue = attrValue.trim().toLowerCase();
218
223
  }
219
- if (Object.hasOwnProperty.call(invalidValueDefault, attrName)) {
220
- const meta = invalidValueDefault[attrName];
221
- if (meta.tag === null || node && node.tag && meta.tag.includes(node.tag)) {
222
- if (typeof newAttrValue !== 'string' || !meta.valid.includes(newAttrValue)) {
223
- newAttrValue = meta.default;
224
- }
224
+ if (appliesInvalidValueDefault && invalidMeta) {
225
+ if (typeof newAttrValue !== 'string' || !invalidMeta.valid.includes(newAttrValue)) {
226
+ newAttrValue = invalidMeta.default;
225
227
  }
226
228
  }
227
229
  newAttrs[attrName] = newAttrValue;
@@ -211,15 +211,17 @@ const mod = {
211
211
  const newAttrs = attrs;
212
212
  Object.entries(attrs).forEach(([attrName, attrValue])=>{
213
213
  let newAttrValue = attrValue;
214
- if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName) && (caseInsensitiveAttributes[attrName] === null || node.tag && caseInsensitiveAttributes[attrName].includes(node.tag))) {
215
- newAttrValue = typeof attrValue === 'string' ? attrValue.toLowerCase() : attrValue;
214
+ const hasCaseInsensitive = Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName) && (caseInsensitiveAttributes[attrName] === null || node.tag && caseInsensitiveAttributes[attrName].includes(node.tag));
215
+ const hasInvalidValueDefault = Object.hasOwnProperty.call(invalidValueDefault, attrName);
216
+ const invalidMeta = hasInvalidValueDefault ? invalidValueDefault[attrName] : undefined;
217
+ const appliesInvalidValueDefault = Boolean(invalidMeta && (invalidMeta.tag === null || node && node.tag && invalidMeta.tag.includes(node.tag)));
218
+ const shouldNormalizeCase = hasCaseInsensitive || appliesInvalidValueDefault;
219
+ if (typeof attrValue === 'string' && shouldNormalizeCase) {
220
+ newAttrValue = attrValue.trim().toLowerCase();
216
221
  }
217
- if (Object.hasOwnProperty.call(invalidValueDefault, attrName)) {
218
- const meta = invalidValueDefault[attrName];
219
- if (meta.tag === null || node && node.tag && meta.tag.includes(node.tag)) {
220
- if (typeof newAttrValue !== 'string' || !meta.valid.includes(newAttrValue)) {
221
- newAttrValue = meta.default;
222
- }
222
+ if (appliesInvalidValueDefault && invalidMeta) {
223
+ if (typeof newAttrValue !== 'string' || !invalidMeta.valid.includes(newAttrValue)) {
224
+ newAttrValue = invalidMeta.default;
223
225
  }
224
226
  }
225
227
  newAttrs[attrName] = newAttrValue;
@@ -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
  }
@@ -52,6 +85,9 @@ type HtmlnanoModule<Options = any> = {
52
85
  default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
53
86
  };
54
87
 
55
- declare const mod: HtmlnanoModule;
88
+ type RemoveAttributeQuotesOptions = {
89
+ force?: boolean;
90
+ };
91
+ declare const mod: HtmlnanoModule<RemoveAttributeQuotesOptions>;
56
92
 
57
93
  export { mod as default };
@@ -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
  }
@@ -52,6 +85,9 @@ type HtmlnanoModule<Options = any> = {
52
85
  default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
53
86
  };
54
87
 
55
- declare const mod: HtmlnanoModule;
88
+ type RemoveAttributeQuotesOptions = {
89
+ force?: boolean;
90
+ };
91
+ declare const mod: HtmlnanoModule<RemoveAttributeQuotesOptions>;
56
92
 
57
93
  export { mod as default };
@@ -2,11 +2,16 @@ Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  // Specification: https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
4
4
  // See also: https://github.com/posthtml/posthtml-render/pull/30
5
- // See also: https://github.com/posthtml/htmlnano/issues/6#issuecomment-707105334
5
+ // See also: https://github.com/maltsev/htmlnano/issues/6#issuecomment-707105334
6
6
  /** Disable quoteAllAttributes while not overriding the configuration */ const mod = {
7
- default: function removeAttributeQuotes(tree) {
8
- var _tree_options, _quoteAllAttributes;
9
- if (tree.options) (_quoteAllAttributes = (_tree_options = tree.options).quoteAllAttributes) != null ? _quoteAllAttributes : _tree_options.quoteAllAttributes = false;
7
+ default: function removeAttributeQuotes(tree, _options, moduleOptions) {
8
+ var _tree, _options1, _tree_options, _quoteAllAttributes;
9
+ (_options1 = (_tree = tree).options) != null ? _options1 : _tree.options = {};
10
+ if (moduleOptions && typeof moduleOptions === 'object' && moduleOptions.force) {
11
+ tree.options.quoteAllAttributes = false;
12
+ return tree;
13
+ }
14
+ (_quoteAllAttributes = (_tree_options = tree.options).quoteAllAttributes) != null ? _quoteAllAttributes : _tree_options.quoteAllAttributes = false;
10
15
  return tree;
11
16
  }
12
17
  };
@@ -1,10 +1,15 @@
1
1
  // Specification: https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
2
2
  // See also: https://github.com/posthtml/posthtml-render/pull/30
3
- // See also: https://github.com/posthtml/htmlnano/issues/6#issuecomment-707105334
3
+ // See also: https://github.com/maltsev/htmlnano/issues/6#issuecomment-707105334
4
4
  /** Disable quoteAllAttributes while not overriding the configuration */ const mod = {
5
- default: function removeAttributeQuotes(tree) {
6
- var _tree_options, _quoteAllAttributes;
7
- if (tree.options) (_quoteAllAttributes = (_tree_options = tree.options).quoteAllAttributes) != null ? _quoteAllAttributes : _tree_options.quoteAllAttributes = false;
5
+ default: function removeAttributeQuotes(tree, _options, moduleOptions) {
6
+ var _tree, _options1, _tree_options, _quoteAllAttributes;
7
+ (_options1 = (_tree = tree).options) != null ? _options1 : _tree.options = {};
8
+ if (moduleOptions && typeof moduleOptions === 'object' && moduleOptions.force) {
9
+ tree.options.quoteAllAttributes = false;
10
+ return tree;
11
+ }
12
+ (_quoteAllAttributes = (_tree_options = tree.options).quoteAllAttributes) != null ? _quoteAllAttributes : _tree_options.quoteAllAttributes = false;
8
13
  return tree;
9
14
  }
10
15
  };