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
@@ -0,0 +1,159 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var helpers_js = require('../helpers.js');
4
+ var collapseAttributeWhitespace_js = require('./collapseAttributeWhitespace.js');
5
+
6
+ const asciiWhitespace = new Set([
7
+ '\t',
8
+ '\n',
9
+ '\f',
10
+ '\r',
11
+ ' '
12
+ ]);
13
+ const defaultOptions = {
14
+ metaContent: true,
15
+ redundantWhitespaces: 'safe'
16
+ };
17
+ function isAsciiWhitespace(char) {
18
+ return asciiWhitespace.has(char);
19
+ }
20
+ function isAsciiDigit(char) {
21
+ return char >= '0' && char <= '9';
22
+ }
23
+ function skipAsciiWhitespace(input, start) {
24
+ let pos = start;
25
+ while(pos < input.length && isAsciiWhitespace(input[pos])){
26
+ pos += 1;
27
+ }
28
+ return pos;
29
+ }
30
+ function minifyMetaRefreshValue(value) {
31
+ const input = value;
32
+ let pos = skipAsciiWhitespace(input, 0);
33
+ const timeStart = pos;
34
+ while(pos < input.length && isAsciiDigit(input[pos])){
35
+ pos += 1;
36
+ }
37
+ if (pos === timeStart) return null;
38
+ const time = input.slice(timeStart, pos);
39
+ while(pos < input.length){
40
+ const ch = input[pos];
41
+ if (isAsciiDigit(ch) || ch === '.') {
42
+ pos += 1;
43
+ continue;
44
+ }
45
+ break;
46
+ }
47
+ pos = skipAsciiWhitespace(input, pos);
48
+ if (pos >= input.length) return time;
49
+ const separator = input[pos];
50
+ if (separator !== ';' && separator !== ',') return null;
51
+ pos += 1;
52
+ pos = skipAsciiWhitespace(input, pos);
53
+ if (pos >= input.length) return time;
54
+ let hasUrlPrefix = false;
55
+ if (input[pos] === 'u' || input[pos] === 'U') {
56
+ if (pos + 2 < input.length) {
57
+ const maybeUrl = input.slice(pos, pos + 3);
58
+ if (maybeUrl.toLowerCase() === 'url') {
59
+ let prefixPos = skipAsciiWhitespace(input, pos + 3);
60
+ if (prefixPos < input.length && input[prefixPos] === '=') {
61
+ prefixPos = skipAsciiWhitespace(input, prefixPos + 1);
62
+ hasUrlPrefix = true;
63
+ pos = prefixPos;
64
+ }
65
+ }
66
+ }
67
+ }
68
+ if (pos >= input.length) return time;
69
+ const firstUrlChar = input[pos];
70
+ if (firstUrlChar === '"' || firstUrlChar === '\'') {
71
+ if (!hasUrlPrefix) return null;
72
+ const quote = firstUrlChar;
73
+ const urlStart = pos + 1;
74
+ const quoteIndex = input.indexOf(quote, urlStart);
75
+ const url = quoteIndex === -1 ? input.slice(urlStart) : input.slice(urlStart, quoteIndex);
76
+ if (!url) return time;
77
+ const closingQuote = quoteIndex === -1 ? '' : quote;
78
+ return `${time}${separator} URL=${quote}${url}${closingQuote}`;
79
+ }
80
+ const url = input.slice(pos).trim();
81
+ if (!url) return time;
82
+ return `${time}${separator} ${url}`;
83
+ }
84
+ function isMetaRefresh(attrs, tagName) {
85
+ if (!tagName || tagName.toLowerCase() !== 'meta') return false;
86
+ const httpEquiv = attrs['http-equiv'];
87
+ if (typeof httpEquiv !== 'string') return false;
88
+ return httpEquiv.trim().toLowerCase() === 'refresh';
89
+ }
90
+ function normalizeOptions(moduleOptions) {
91
+ if (moduleOptions && typeof moduleOptions === 'object') {
92
+ let redundantWhitespaces = defaultOptions.redundantWhitespaces;
93
+ if (moduleOptions.redundantWhitespaces === 'aggressive') {
94
+ redundantWhitespaces = 'agressive';
95
+ } else if (moduleOptions.redundantWhitespaces === 'safe' || moduleOptions.redundantWhitespaces === 'agressive' || moduleOptions.redundantWhitespaces === false) {
96
+ redundantWhitespaces = moduleOptions.redundantWhitespaces;
97
+ }
98
+ return {
99
+ metaContent: moduleOptions.metaContent !== false,
100
+ redundantWhitespaces
101
+ };
102
+ }
103
+ return defaultOptions;
104
+ }
105
+ function collapseWhitespace(value) {
106
+ return value.replace(/\s+/g, ' ').trim();
107
+ }
108
+ function minifyAttributeWhitespace(mode, attrName, attrValue, tagName) {
109
+ if (!mode) {
110
+ return null;
111
+ }
112
+ const attrNameLower = attrName.toLowerCase();
113
+ if (collapseAttributeWhitespace_js.isListAttribute(attrNameLower, tagName)) {
114
+ const collapsed = collapseWhitespace(attrValue);
115
+ return collapsed === attrValue ? null : collapsed;
116
+ }
117
+ if (helpers_js.isEventHandler(attrName)) {
118
+ const trimmed = attrValue.trim();
119
+ return trimmed === attrValue ? null : trimmed;
120
+ }
121
+ if (collapseAttributeWhitespace_js.isSingleValueAttribute(attrNameLower, tagName)) {
122
+ const trimmed = attrValue.trim();
123
+ return trimmed === attrValue ? null : trimmed;
124
+ }
125
+ if (mode === 'agressive') {
126
+ const trimmed = attrValue.trim();
127
+ return trimmed === attrValue ? null : trimmed;
128
+ }
129
+ return null;
130
+ }
131
+ const mod = {
132
+ onAttrs (_options, moduleOptions) {
133
+ const normalizedOptions = normalizeOptions(moduleOptions);
134
+ return (attrs, node)=>{
135
+ if (normalizedOptions.metaContent && isMetaRefresh(attrs, node.tag)) {
136
+ const content = attrs.content;
137
+ if (typeof content === 'string') {
138
+ const minified = minifyMetaRefreshValue(content);
139
+ if (minified !== null && minified !== content) {
140
+ attrs.content = minified;
141
+ }
142
+ }
143
+ }
144
+ if (normalizedOptions.redundantWhitespaces) {
145
+ const tagName = node.tag ? node.tag.toLowerCase() : undefined;
146
+ Object.entries(attrs).forEach(([attrName, attrValue])=>{
147
+ if (typeof attrValue !== 'string') return;
148
+ const minified = minifyAttributeWhitespace(normalizedOptions.redundantWhitespaces, attrName, attrValue, tagName);
149
+ if (minified !== null) {
150
+ attrs[attrName] = minified;
151
+ }
152
+ });
153
+ }
154
+ return attrs;
155
+ };
156
+ }
157
+ };
158
+
159
+ exports.default = mod;
@@ -0,0 +1,157 @@
1
+ import { isEventHandler } from '../helpers.mjs';
2
+ import { isListAttribute, isSingleValueAttribute } from './collapseAttributeWhitespace.mjs';
3
+
4
+ const asciiWhitespace = new Set([
5
+ '\t',
6
+ '\n',
7
+ '\f',
8
+ '\r',
9
+ ' '
10
+ ]);
11
+ const defaultOptions = {
12
+ metaContent: true,
13
+ redundantWhitespaces: 'safe'
14
+ };
15
+ function isAsciiWhitespace(char) {
16
+ return asciiWhitespace.has(char);
17
+ }
18
+ function isAsciiDigit(char) {
19
+ return char >= '0' && char <= '9';
20
+ }
21
+ function skipAsciiWhitespace(input, start) {
22
+ let pos = start;
23
+ while(pos < input.length && isAsciiWhitespace(input[pos])){
24
+ pos += 1;
25
+ }
26
+ return pos;
27
+ }
28
+ function minifyMetaRefreshValue(value) {
29
+ const input = value;
30
+ let pos = skipAsciiWhitespace(input, 0);
31
+ const timeStart = pos;
32
+ while(pos < input.length && isAsciiDigit(input[pos])){
33
+ pos += 1;
34
+ }
35
+ if (pos === timeStart) return null;
36
+ const time = input.slice(timeStart, pos);
37
+ while(pos < input.length){
38
+ const ch = input[pos];
39
+ if (isAsciiDigit(ch) || ch === '.') {
40
+ pos += 1;
41
+ continue;
42
+ }
43
+ break;
44
+ }
45
+ pos = skipAsciiWhitespace(input, pos);
46
+ if (pos >= input.length) return time;
47
+ const separator = input[pos];
48
+ if (separator !== ';' && separator !== ',') return null;
49
+ pos += 1;
50
+ pos = skipAsciiWhitespace(input, pos);
51
+ if (pos >= input.length) return time;
52
+ let hasUrlPrefix = false;
53
+ if (input[pos] === 'u' || input[pos] === 'U') {
54
+ if (pos + 2 < input.length) {
55
+ const maybeUrl = input.slice(pos, pos + 3);
56
+ if (maybeUrl.toLowerCase() === 'url') {
57
+ let prefixPos = skipAsciiWhitespace(input, pos + 3);
58
+ if (prefixPos < input.length && input[prefixPos] === '=') {
59
+ prefixPos = skipAsciiWhitespace(input, prefixPos + 1);
60
+ hasUrlPrefix = true;
61
+ pos = prefixPos;
62
+ }
63
+ }
64
+ }
65
+ }
66
+ if (pos >= input.length) return time;
67
+ const firstUrlChar = input[pos];
68
+ if (firstUrlChar === '"' || firstUrlChar === '\'') {
69
+ if (!hasUrlPrefix) return null;
70
+ const quote = firstUrlChar;
71
+ const urlStart = pos + 1;
72
+ const quoteIndex = input.indexOf(quote, urlStart);
73
+ const url = quoteIndex === -1 ? input.slice(urlStart) : input.slice(urlStart, quoteIndex);
74
+ if (!url) return time;
75
+ const closingQuote = quoteIndex === -1 ? '' : quote;
76
+ return `${time}${separator} URL=${quote}${url}${closingQuote}`;
77
+ }
78
+ const url = input.slice(pos).trim();
79
+ if (!url) return time;
80
+ return `${time}${separator} ${url}`;
81
+ }
82
+ function isMetaRefresh(attrs, tagName) {
83
+ if (!tagName || tagName.toLowerCase() !== 'meta') return false;
84
+ const httpEquiv = attrs['http-equiv'];
85
+ if (typeof httpEquiv !== 'string') return false;
86
+ return httpEquiv.trim().toLowerCase() === 'refresh';
87
+ }
88
+ function normalizeOptions(moduleOptions) {
89
+ if (moduleOptions && typeof moduleOptions === 'object') {
90
+ let redundantWhitespaces = defaultOptions.redundantWhitespaces;
91
+ if (moduleOptions.redundantWhitespaces === 'aggressive') {
92
+ redundantWhitespaces = 'agressive';
93
+ } else if (moduleOptions.redundantWhitespaces === 'safe' || moduleOptions.redundantWhitespaces === 'agressive' || moduleOptions.redundantWhitespaces === false) {
94
+ redundantWhitespaces = moduleOptions.redundantWhitespaces;
95
+ }
96
+ return {
97
+ metaContent: moduleOptions.metaContent !== false,
98
+ redundantWhitespaces
99
+ };
100
+ }
101
+ return defaultOptions;
102
+ }
103
+ function collapseWhitespace(value) {
104
+ return value.replace(/\s+/g, ' ').trim();
105
+ }
106
+ function minifyAttributeWhitespace(mode, attrName, attrValue, tagName) {
107
+ if (!mode) {
108
+ return null;
109
+ }
110
+ const attrNameLower = attrName.toLowerCase();
111
+ if (isListAttribute(attrNameLower, tagName)) {
112
+ const collapsed = collapseWhitespace(attrValue);
113
+ return collapsed === attrValue ? null : collapsed;
114
+ }
115
+ if (isEventHandler(attrName)) {
116
+ const trimmed = attrValue.trim();
117
+ return trimmed === attrValue ? null : trimmed;
118
+ }
119
+ if (isSingleValueAttribute(attrNameLower, tagName)) {
120
+ const trimmed = attrValue.trim();
121
+ return trimmed === attrValue ? null : trimmed;
122
+ }
123
+ if (mode === 'agressive') {
124
+ const trimmed = attrValue.trim();
125
+ return trimmed === attrValue ? null : trimmed;
126
+ }
127
+ return null;
128
+ }
129
+ const mod = {
130
+ onAttrs (_options, moduleOptions) {
131
+ const normalizedOptions = normalizeOptions(moduleOptions);
132
+ return (attrs, node)=>{
133
+ if (normalizedOptions.metaContent && isMetaRefresh(attrs, node.tag)) {
134
+ const content = attrs.content;
135
+ if (typeof content === 'string') {
136
+ const minified = minifyMetaRefreshValue(content);
137
+ if (minified !== null && minified !== content) {
138
+ attrs.content = minified;
139
+ }
140
+ }
141
+ }
142
+ if (normalizedOptions.redundantWhitespaces) {
143
+ const tagName = node.tag ? node.tag.toLowerCase() : undefined;
144
+ Object.entries(attrs).forEach(([attrName, attrValue])=>{
145
+ if (typeof attrValue !== 'string') return;
146
+ const minified = minifyAttributeWhitespace(normalizedOptions.redundantWhitespaces, attrName, attrValue, tagName);
147
+ if (minified !== null) {
148
+ attrs[attrName] = minified;
149
+ }
150
+ });
151
+ }
152
+ return attrs;
153
+ };
154
+ }
155
+ };
156
+
157
+ export { mod as default };
@@ -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,17 +35,42 @@ 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
  }
@@ -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,17 +35,42 @@ 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
  }
@@ -8,14 +8,15 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
8
  var htmlnano__default = /*#__PURE__*/_interopDefault(htmlnano);
9
9
 
10
10
  // Spec: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/ms537512(v=vs.85)
11
- const CONDITIONAL_COMMENT_REGEXP = /(<!--\[if\s+?[^<>[\]]+?]>)([\s\S]+?)(<!\[endif\]-->)/gm;
11
+ const CONDITIONAL_COMMENT_HIDDEN_REGEXP = /(<!--\[if\s+?[^<>[\]]+?]>)([\s\S]*?)(<!\[endif\]-->)/gm;
12
+ const CONDITIONAL_COMMENT_REVEALED_REGEXP = /(<!--\[if\s+?[^<>[\]]+?\]><!-->)([\s\S]*?)(<!--<!\[endif\]-->)/gm;
12
13
  async function minifyConditionalComments(tree, htmlnanoOptions) {
13
14
  // forEach, tree.walk, tree.match just don't support Promise.
14
15
  for(let i = 0, len = tree.length; i < len; i++){
15
16
  const node = tree[i];
16
17
  if (typeof node === 'string') {
17
18
  if (helpers_js.isConditionalComment(node)) {
18
- tree[i] = await minifycontentInsideConditionalComments(node, htmlnanoOptions);
19
+ tree[i] = await minifyContentInsideConditionalComments(node, htmlnanoOptions);
19
20
  }
20
21
  } else if (node.content && node.content.length) {
21
22
  node.content = await minifyConditionalComments(node.content, htmlnanoOptions);
@@ -26,29 +27,46 @@ async function minifyConditionalComments(tree, htmlnanoOptions) {
26
27
  /** Minify content inside conditional comments */ const mod = {
27
28
  default: minifyConditionalComments
28
29
  };
29
- async function minifycontentInsideConditionalComments(text, htmlnanoOptions) {
30
- let match;
30
+ function collectConditionalCommentMatches(text, regexp) {
31
31
  const matches = [];
32
- // FIXME!
33
- // String#matchAll is supported since Node.js 12
34
- while((match = CONDITIONAL_COMMENT_REGEXP.exec(text)) !== null){
35
- matches.push([
36
- match[1],
37
- match[2],
38
- match[3]
39
- ]);
32
+ regexp.lastIndex = 0;
33
+ let match;
34
+ while((match = regexp.exec(text)) !== null){
35
+ matches.push({
36
+ start: match.index,
37
+ end: match.index + match[0].length,
38
+ open: match[1],
39
+ content: match[2],
40
+ close: match[3]
41
+ });
40
42
  }
43
+ return matches;
44
+ }
45
+ function hasHtmlOpeningWithoutClosing(content) {
46
+ return /<html\b/i.test(content) && !/<\/html>/i.test(content);
47
+ }
48
+ async function minifyContentInsideConditionalComments(text, htmlnanoOptions) {
49
+ const matches = [
50
+ ...collectConditionalCommentMatches(text, CONDITIONAL_COMMENT_HIDDEN_REGEXP),
51
+ ...collectConditionalCommentMatches(text, CONDITIONAL_COMMENT_REVEALED_REGEXP)
52
+ ].sort((a, b)=>a.start - b.start);
41
53
  if (!matches.length) {
42
54
  return Promise.resolve(text);
43
55
  }
44
- return Promise.all(matches.map(async (match)=>{
45
- const result = await htmlnano__default.default.process(match[1], htmlnanoOptions, {}, {});
46
- let minified = result.html;
47
- if (match[1].includes('<html') && minified.includes('</html>')) {
48
- minified = minified.replace('</html>', '');
56
+ let result = '';
57
+ let lastIndex = 0;
58
+ for (const match of matches){
59
+ result += text.slice(lastIndex, match.start);
60
+ const processed = await htmlnano__default.default.process(match.content, htmlnanoOptions, {}, {});
61
+ let minified = processed.html;
62
+ if (hasHtmlOpeningWithoutClosing(match.content) && /<\/html>/i.test(minified)) {
63
+ minified = minified.replace(/<\/html>/i, '');
49
64
  }
50
- return match[0] + minified + match[2];
51
- }));
65
+ result += match.open + minified + match.close;
66
+ lastIndex = match.end;
67
+ }
68
+ result += text.slice(lastIndex);
69
+ return result;
52
70
  }
53
71
 
54
72
  exports.default = mod;
@@ -2,14 +2,15 @@ import htmlnano from '../index.mjs';
2
2
  import { isConditionalComment } from '../helpers.mjs';
3
3
 
4
4
  // Spec: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/ms537512(v=vs.85)
5
- const CONDITIONAL_COMMENT_REGEXP = /(<!--\[if\s+?[^<>[\]]+?]>)([\s\S]+?)(<!\[endif\]-->)/gm;
5
+ const CONDITIONAL_COMMENT_HIDDEN_REGEXP = /(<!--\[if\s+?[^<>[\]]+?]>)([\s\S]*?)(<!\[endif\]-->)/gm;
6
+ const CONDITIONAL_COMMENT_REVEALED_REGEXP = /(<!--\[if\s+?[^<>[\]]+?\]><!-->)([\s\S]*?)(<!--<!\[endif\]-->)/gm;
6
7
  async function minifyConditionalComments(tree, htmlnanoOptions) {
7
8
  // forEach, tree.walk, tree.match just don't support Promise.
8
9
  for(let i = 0, len = tree.length; i < len; i++){
9
10
  const node = tree[i];
10
11
  if (typeof node === 'string') {
11
12
  if (isConditionalComment(node)) {
12
- tree[i] = await minifycontentInsideConditionalComments(node, htmlnanoOptions);
13
+ tree[i] = await minifyContentInsideConditionalComments(node, htmlnanoOptions);
13
14
  }
14
15
  } else if (node.content && node.content.length) {
15
16
  node.content = await minifyConditionalComments(node.content, htmlnanoOptions);
@@ -20,29 +21,46 @@ async function minifyConditionalComments(tree, htmlnanoOptions) {
20
21
  /** Minify content inside conditional comments */ const mod = {
21
22
  default: minifyConditionalComments
22
23
  };
23
- async function minifycontentInsideConditionalComments(text, htmlnanoOptions) {
24
- let match;
24
+ function collectConditionalCommentMatches(text, regexp) {
25
25
  const matches = [];
26
- // FIXME!
27
- // String#matchAll is supported since Node.js 12
28
- while((match = CONDITIONAL_COMMENT_REGEXP.exec(text)) !== null){
29
- matches.push([
30
- match[1],
31
- match[2],
32
- match[3]
33
- ]);
26
+ regexp.lastIndex = 0;
27
+ let match;
28
+ while((match = regexp.exec(text)) !== null){
29
+ matches.push({
30
+ start: match.index,
31
+ end: match.index + match[0].length,
32
+ open: match[1],
33
+ content: match[2],
34
+ close: match[3]
35
+ });
34
36
  }
37
+ return matches;
38
+ }
39
+ function hasHtmlOpeningWithoutClosing(content) {
40
+ return /<html\b/i.test(content) && !/<\/html>/i.test(content);
41
+ }
42
+ async function minifyContentInsideConditionalComments(text, htmlnanoOptions) {
43
+ const matches = [
44
+ ...collectConditionalCommentMatches(text, CONDITIONAL_COMMENT_HIDDEN_REGEXP),
45
+ ...collectConditionalCommentMatches(text, CONDITIONAL_COMMENT_REVEALED_REGEXP)
46
+ ].sort((a, b)=>a.start - b.start);
35
47
  if (!matches.length) {
36
48
  return Promise.resolve(text);
37
49
  }
38
- return Promise.all(matches.map(async (match)=>{
39
- const result = await htmlnano.process(match[1], htmlnanoOptions, {}, {});
40
- let minified = result.html;
41
- if (match[1].includes('<html') && minified.includes('</html>')) {
42
- minified = minified.replace('</html>', '');
50
+ let result = '';
51
+ let lastIndex = 0;
52
+ for (const match of matches){
53
+ result += text.slice(lastIndex, match.start);
54
+ const processed = await htmlnano.process(match.content, htmlnanoOptions, {}, {});
55
+ let minified = processed.html;
56
+ if (hasHtmlOpeningWithoutClosing(match.content) && /<\/html>/i.test(minified)) {
57
+ minified = minified.replace(/<\/html>/i, '');
43
58
  }
44
- return match[0] + minified + match[2];
45
- }));
59
+ result += match.open + minified + match.close;
60
+ lastIndex = match.end;
61
+ }
62
+ result += text.slice(lastIndex);
63
+ return result;
46
64
  }
47
65
 
48
66
  export { mod as default };