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
@@ -1,49 +1,136 @@
1
1
  import { extractTextContentFromNode } from '../helpers.mjs';
2
2
 
3
+ function normalizeAttrsForKey(attrs, config) {
4
+ const normalized = {
5
+ ...config.baseAttrs
6
+ };
7
+ for (const [key, value] of Object.entries(attrs || {})){
8
+ if (config.skippedAttrs.has(key) || value === undefined) {
9
+ continue;
10
+ }
11
+ if (config.booleanAttrs.has(key)) {
12
+ normalized[key] = true;
13
+ continue;
14
+ }
15
+ normalized[key] = value;
16
+ }
17
+ return normalized;
18
+ }
19
+
20
+ function normalizeAsyncAttr(attrs) {
21
+ if (!attrs) {
22
+ return;
23
+ }
24
+ if (attrs.async === '') {
25
+ attrs.async = true;
26
+ }
27
+ if (attrs.nomodule === '') {
28
+ attrs.nomodule = true;
29
+ }
30
+ }
31
+ function getScriptType(attrs) {
32
+ const type = attrs.type || 'text/javascript';
33
+ return typeof type === 'string' ? type.toLowerCase() : 'text/javascript';
34
+ }
35
+ function isMergeableScriptType(type) {
36
+ return type === 'text/javascript' || type === 'application/javascript';
37
+ }
38
+ const booleanAttrs = new Set([
39
+ 'async',
40
+ 'defer',
41
+ 'nomodule'
42
+ ]);
43
+ const skippedAttrs = new Set([
44
+ 'src',
45
+ 'integrity',
46
+ 'type'
47
+ ]);
48
+ function normalizeScriptAttrsForKey(attrs, scriptType) {
49
+ return normalizeAttrsForKey(attrs, {
50
+ baseAttrs: {
51
+ type: scriptType
52
+ },
53
+ booleanAttrs,
54
+ skippedAttrs
55
+ });
56
+ }
57
+ function buildScriptKey(attrs, scriptType, scriptSrcIndex) {
58
+ const normalizedAttrs = normalizeScriptAttrsForKey(attrs, scriptType);
59
+ const keyObject = {
60
+ index: scriptSrcIndex,
61
+ ...normalizedAttrs
62
+ };
63
+ const sortedKeys = Object.keys(keyObject).sort();
64
+ const sortedKeyObject = {};
65
+ for (const key of sortedKeys){
66
+ sortedKeyObject[key] = keyObject[key];
67
+ }
68
+ return JSON.stringify(sortedKeyObject);
69
+ }
70
+ function endsWithLineComment(scriptContent) {
71
+ const lastNewlineIndex = Math.max(scriptContent.lastIndexOf('\n'), scriptContent.lastIndexOf('\r'));
72
+ const lastLine = lastNewlineIndex === -1 ? scriptContent : scriptContent.slice(lastNewlineIndex + 1);
73
+ return /\/\/.*$/.test(lastLine);
74
+ }
75
+ function mergeScriptNodes(scriptNodesIndex, tracking) {
76
+ for (const scriptNodes of Object.values(scriptNodesIndex)){
77
+ if (scriptNodes.length < 2) {
78
+ continue;
79
+ }
80
+ const lastScriptNode = scriptNodes.pop();
81
+ tracking.mergedScriptNodes.add(lastScriptNode);
82
+ scriptNodes.reverse().forEach((scriptNode)=>{
83
+ let scriptContent = extractTextContentFromNode(scriptNode).trim();
84
+ if (!scriptContent) {
85
+ tracking.removedScriptNodes.add(scriptNode);
86
+ // @ts-expect-error -- remove node
87
+ scriptNode.tag = false;
88
+ scriptNode.content = [];
89
+ return;
90
+ }
91
+ if (endsWithLineComment(scriptContent)) {
92
+ scriptContent += '\n;';
93
+ } else if (scriptContent.slice(-1) !== ';') {
94
+ scriptContent += ';';
95
+ }
96
+ lastScriptNode.content = lastScriptNode.content || [];
97
+ lastScriptNode.content.unshift(scriptContent);
98
+ tracking.removedScriptNodes.add(scriptNode);
99
+ // @ts-expect-error -- remove node
100
+ scriptNode.tag = false;
101
+ scriptNode.content = [];
102
+ });
103
+ }
104
+ }
3
105
  /* Merge multiple <script> into one */ const mod = {
4
106
  default (tree) {
5
107
  const scriptNodesIndex = {};
108
+ const tracking = {
109
+ mergedScriptNodes: new WeakSet(),
110
+ removedScriptNodes: new WeakSet()
111
+ };
6
112
  let scriptSrcIndex = 1;
7
113
  tree.match({
8
114
  tag: 'script'
9
115
  }, (node)=>{
10
116
  const nodeAttrs = node.attrs || {};
117
+ normalizeAsyncAttr(nodeAttrs);
11
118
  if ('src' in nodeAttrs || 'integrity' in nodeAttrs) {
12
119
  scriptSrcIndex++;
13
120
  return node;
14
121
  }
15
- const scriptType = nodeAttrs.type || 'text/javascript';
16
- if (scriptType !== 'text/javascript' && scriptType !== 'application/javascript') {
122
+ const scriptType = getScriptType(nodeAttrs);
123
+ if (!isMergeableScriptType(scriptType)) {
17
124
  return node;
18
125
  }
19
- const scriptKey = JSON.stringify({
20
- id: nodeAttrs.id,
21
- class: nodeAttrs.class,
22
- type: scriptType,
23
- defer: nodeAttrs.defer !== undefined,
24
- async: nodeAttrs.async !== undefined,
25
- index: scriptSrcIndex
26
- });
126
+ const scriptKey = buildScriptKey(nodeAttrs, scriptType, scriptSrcIndex);
27
127
  if (!scriptNodesIndex[scriptKey]) {
28
128
  scriptNodesIndex[scriptKey] = [];
29
129
  }
30
130
  scriptNodesIndex[scriptKey].push(node);
31
131
  return node;
32
132
  });
33
- for (const scriptNodes of Object.values(scriptNodesIndex)){
34
- const lastScriptNode = scriptNodes.pop();
35
- scriptNodes.reverse().forEach((scriptNode)=>{
36
- let scriptContent = extractTextContentFromNode(scriptNode).trim();
37
- if (scriptContent.slice(-1) !== ';') {
38
- scriptContent += ';';
39
- }
40
- lastScriptNode.content = lastScriptNode.content || [];
41
- lastScriptNode.content.unshift(scriptContent);
42
- // @ts-expect-error -- remove node
43
- scriptNode.tag = false;
44
- scriptNode.content = [];
45
- });
46
- }
133
+ mergeScriptNodes(scriptNodesIndex, tracking);
47
134
  return tree;
48
135
  }
49
136
  };
@@ -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
  }
@@ -2,6 +2,70 @@ Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  var helpers_js = require('../helpers.js');
4
4
 
5
+ function normalizeAttrsForKey(attrs, config) {
6
+ const normalized = {
7
+ ...config.baseAttrs
8
+ };
9
+ for (const [key, value] of Object.entries(attrs || {})){
10
+ if (config.skippedAttrs.has(key) || value === undefined) {
11
+ continue;
12
+ }
13
+ if (config.booleanAttrs.has(key)) {
14
+ normalized[key] = true;
15
+ continue;
16
+ }
17
+ normalized[key] = value;
18
+ }
19
+ return normalized;
20
+ }
21
+
22
+ const booleanAttrs = new Set([
23
+ 'amp-custom',
24
+ 'disabled'
25
+ ]);
26
+ const skippedAttrs = new Set([
27
+ 'type',
28
+ 'media'
29
+ ]);
30
+ function normalizeStyleType(attrs) {
31
+ if (!attrs || typeof attrs.type !== 'string') {
32
+ return 'text/css';
33
+ }
34
+ const type = attrs.type.trim();
35
+ return type ? type.toLowerCase() : 'text/css';
36
+ }
37
+ function normalizeStyleMedia(attrs) {
38
+ if (!attrs || typeof attrs.media !== 'string') {
39
+ return 'all';
40
+ }
41
+ const media = attrs.media.trim();
42
+ return media ? media.replace(/\s+/g, ' ').toLowerCase() : 'all';
43
+ }
44
+ function normalizeStyleAttrsForKey(attrs) {
45
+ return normalizeAttrsForKey(attrs, {
46
+ booleanAttrs,
47
+ skippedAttrs
48
+ });
49
+ }
50
+ function buildStyleKey(attrs) {
51
+ const keyObject = {
52
+ type: normalizeStyleType(attrs),
53
+ media: normalizeStyleMedia(attrs),
54
+ ...normalizeStyleAttrsForKey(attrs)
55
+ };
56
+ const sortedKeys = Object.keys(keyObject).sort();
57
+ const sortedKeyObject = {};
58
+ for (const key of sortedKeys){
59
+ sortedKeyObject[key] = keyObject[key];
60
+ }
61
+ return JSON.stringify(sortedKeyObject);
62
+ }
63
+ function extractStyleTextContent(node) {
64
+ if (typeof node.content === 'string') {
65
+ return node.content;
66
+ }
67
+ return helpers_js.extractTextContentFromNode(node);
68
+ }
5
69
  /* Merge multiple <style> into one */ const mod = {
6
70
  default (tree) {
7
71
  const styleNodes = {};
@@ -20,12 +84,10 @@ var helpers_js = require('../helpers.js');
20
84
  if (helpers_js.isAmpBoilerplate(node)) {
21
85
  return node;
22
86
  }
23
- const styleType = nodeAttrs.type || 'text/css';
24
- const styleMedia = nodeAttrs.media || 'all';
25
- const styleKey = styleType + '_' + styleMedia;
87
+ const styleKey = buildStyleKey(nodeAttrs);
26
88
  if (styleKey in styleNodes) {
27
89
  var _styleNodes_styleKey, _content;
28
- const styleContent = helpers_js.extractTextContentFromNode(node);
90
+ const styleContent = extractStyleTextContent(node);
29
91
  (_content = (_styleNodes_styleKey = styleNodes[styleKey]).content) != null ? _content : _styleNodes_styleKey.content = [];
30
92
  styleNodes[styleKey].content.push(' ' + styleContent);
31
93
  return ''; // Remove node
@@ -1,5 +1,69 @@
1
1
  import { isAmpBoilerplate, extractTextContentFromNode } from '../helpers.mjs';
2
2
 
3
+ function normalizeAttrsForKey(attrs, config) {
4
+ const normalized = {
5
+ ...config.baseAttrs
6
+ };
7
+ for (const [key, value] of Object.entries(attrs || {})){
8
+ if (config.skippedAttrs.has(key) || value === undefined) {
9
+ continue;
10
+ }
11
+ if (config.booleanAttrs.has(key)) {
12
+ normalized[key] = true;
13
+ continue;
14
+ }
15
+ normalized[key] = value;
16
+ }
17
+ return normalized;
18
+ }
19
+
20
+ const booleanAttrs = new Set([
21
+ 'amp-custom',
22
+ 'disabled'
23
+ ]);
24
+ const skippedAttrs = new Set([
25
+ 'type',
26
+ 'media'
27
+ ]);
28
+ function normalizeStyleType(attrs) {
29
+ if (!attrs || typeof attrs.type !== 'string') {
30
+ return 'text/css';
31
+ }
32
+ const type = attrs.type.trim();
33
+ return type ? type.toLowerCase() : 'text/css';
34
+ }
35
+ function normalizeStyleMedia(attrs) {
36
+ if (!attrs || typeof attrs.media !== 'string') {
37
+ return 'all';
38
+ }
39
+ const media = attrs.media.trim();
40
+ return media ? media.replace(/\s+/g, ' ').toLowerCase() : 'all';
41
+ }
42
+ function normalizeStyleAttrsForKey(attrs) {
43
+ return normalizeAttrsForKey(attrs, {
44
+ booleanAttrs,
45
+ skippedAttrs
46
+ });
47
+ }
48
+ function buildStyleKey(attrs) {
49
+ const keyObject = {
50
+ type: normalizeStyleType(attrs),
51
+ media: normalizeStyleMedia(attrs),
52
+ ...normalizeStyleAttrsForKey(attrs)
53
+ };
54
+ const sortedKeys = Object.keys(keyObject).sort();
55
+ const sortedKeyObject = {};
56
+ for (const key of sortedKeys){
57
+ sortedKeyObject[key] = keyObject[key];
58
+ }
59
+ return JSON.stringify(sortedKeyObject);
60
+ }
61
+ function extractStyleTextContent(node) {
62
+ if (typeof node.content === 'string') {
63
+ return node.content;
64
+ }
65
+ return extractTextContentFromNode(node);
66
+ }
3
67
  /* Merge multiple <style> into one */ const mod = {
4
68
  default (tree) {
5
69
  const styleNodes = {};
@@ -18,12 +82,10 @@ import { isAmpBoilerplate, extractTextContentFromNode } from '../helpers.mjs';
18
82
  if (isAmpBoilerplate(node)) {
19
83
  return node;
20
84
  }
21
- const styleType = nodeAttrs.type || 'text/css';
22
- const styleMedia = nodeAttrs.media || 'all';
23
- const styleKey = styleType + '_' + styleMedia;
85
+ const styleKey = buildStyleKey(nodeAttrs);
24
86
  if (styleKey in styleNodes) {
25
87
  var _styleNodes_styleKey, _content;
26
- const styleContent = extractTextContentFromNode(node);
88
+ const styleContent = extractStyleTextContent(node);
27
89
  (_content = (_styleNodes_styleKey = styleNodes[styleKey]).content) != null ? _content : _styleNodes_styleKey.content = [];
28
90
  styleNodes[styleKey].content.push(' ' + styleContent);
29
91
  return ''; // Remove node
@@ -0,0 +1,95 @@
1
+ import PostHTML from 'posthtml';
2
+ import { MinifyOptions } from 'terser';
3
+ import { Options } from 'cssnano';
4
+ import { Config } from 'svgo';
5
+ import { UserDefinedOptions } from 'purgecss';
6
+
7
+ type PostHTMLTreeLike = [PostHTML.Node] & PostHTML.NodeAPI & {
8
+ options?: {
9
+ quoteAllAttributes?: boolean | undefined;
10
+ quoteStyle?: 0 | 1 | 2 | undefined;
11
+ replaceQuote?: boolean | undefined;
12
+ } | undefined;
13
+ render(): string;
14
+ render(node: PostHTML.Node | PostHTMLTreeLike, renderOptions?: any): string;
15
+ };
16
+ type MaybeArray<T> = T | Array<T>;
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[];
23
+ interface HtmlnanoOptions {
24
+ skipConfigLoading?: boolean;
25
+ configPath?: string;
26
+ skipInternalWarnings?: boolean;
27
+ collapseAttributeWhitespace?: boolean;
28
+ collapseBooleanAttributes?: {
29
+ amphtml?: boolean;
30
+ };
31
+ collapseWhitespace?: 'conservative' | 'all' | 'aggressive';
32
+ custom?: MaybeArray<(tree: PostHTMLTreeLike, options?: any) => (PostHTML.Node | PostHTMLTreeLike)>;
33
+ deduplicateAttributeValues?: boolean;
34
+ minifyUrls?: URL | string | false;
35
+ mergeStyles?: boolean;
36
+ mergeScripts?: boolean;
37
+ minifyCss?: Options | boolean;
38
+ minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
39
+ minifyConditionalComments?: boolean;
40
+ minifyJs?: MinifyOptions | boolean;
41
+ minifyJson?: boolean;
42
+ minifyAttributes?: boolean | {
43
+ metaContent?: boolean;
44
+ redundantWhitespaces?: 'safe' | 'agressive' | false;
45
+ };
46
+ minifySvg?: Config | boolean;
47
+ normalizeAttributeValues?: boolean;
48
+ removeAttributeQuotes?: boolean | {
49
+ force?: boolean;
50
+ };
51
+ removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
52
+ removeEmptyAttributes?: boolean;
53
+ removeEmptyElements?: boolean | {
54
+ removeWithAttributes?: boolean;
55
+ };
56
+ removeRedundantAttributes?: boolean;
57
+ removeOptionalTags?: 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
+ };
74
+ sortAttributes?: boolean | 'alphabetical' | 'frequency';
75
+ sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
76
+ }
77
+ type HtmlnanoModuleAttrsHandler = (attrs: Record<string, string | boolean | void>, node: PostHTML.Node) => Record<string, string | boolean | void>;
78
+ type HtmlnanoModuleContentHandler = (content: Array<PostHTMLNodeLike>, node: PostHTML.Node) => MaybeArray<PostHTMLNodeLike>;
79
+ type HtmlnanoModuleNodeHandler = (node: PostHTMLNodeLike) => PostHTML.Node | string;
80
+ type OptionalOptions<T> = T extends boolean | string | Function | number | null | undefined ? T : T extends object ? Partial<T> : T;
81
+ type HtmlnanoModule<Options = any> = {
82
+ onAttrs?: (options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => HtmlnanoModuleAttrsHandler;
83
+ onContent?: (options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => HtmlnanoModuleContentHandler;
84
+ onNode?: (options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => HtmlnanoModuleNodeHandler;
85
+ default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
86
+ };
87
+
88
+ type RedundantWhitespaceMode = 'safe' | 'agressive' | false;
89
+ type MinifyAttributesOptions = {
90
+ metaContent?: boolean;
91
+ redundantWhitespaces?: RedundantWhitespaceMode | 'aggressive';
92
+ };
93
+ declare const mod: HtmlnanoModule<MinifyAttributesOptions>;
94
+
95
+ export { mod as default };
@@ -0,0 +1,95 @@
1
+ import PostHTML from 'posthtml';
2
+ import { MinifyOptions } from 'terser';
3
+ import { Options } from 'cssnano';
4
+ import { Config } from 'svgo';
5
+ import { UserDefinedOptions } from 'purgecss';
6
+
7
+ type PostHTMLTreeLike = [PostHTML.Node] & PostHTML.NodeAPI & {
8
+ options?: {
9
+ quoteAllAttributes?: boolean | undefined;
10
+ quoteStyle?: 0 | 1 | 2 | undefined;
11
+ replaceQuote?: boolean | undefined;
12
+ } | undefined;
13
+ render(): string;
14
+ render(node: PostHTML.Node | PostHTMLTreeLike, renderOptions?: any): string;
15
+ };
16
+ type MaybeArray<T> = T | Array<T>;
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[];
23
+ interface HtmlnanoOptions {
24
+ skipConfigLoading?: boolean;
25
+ configPath?: string;
26
+ skipInternalWarnings?: boolean;
27
+ collapseAttributeWhitespace?: boolean;
28
+ collapseBooleanAttributes?: {
29
+ amphtml?: boolean;
30
+ };
31
+ collapseWhitespace?: 'conservative' | 'all' | 'aggressive';
32
+ custom?: MaybeArray<(tree: PostHTMLTreeLike, options?: any) => (PostHTML.Node | PostHTMLTreeLike)>;
33
+ deduplicateAttributeValues?: boolean;
34
+ minifyUrls?: URL | string | false;
35
+ mergeStyles?: boolean;
36
+ mergeScripts?: boolean;
37
+ minifyCss?: Options | boolean;
38
+ minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
39
+ minifyConditionalComments?: boolean;
40
+ minifyJs?: MinifyOptions | boolean;
41
+ minifyJson?: boolean;
42
+ minifyAttributes?: boolean | {
43
+ metaContent?: boolean;
44
+ redundantWhitespaces?: 'safe' | 'agressive' | false;
45
+ };
46
+ minifySvg?: Config | boolean;
47
+ normalizeAttributeValues?: boolean;
48
+ removeAttributeQuotes?: boolean | {
49
+ force?: boolean;
50
+ };
51
+ removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
52
+ removeEmptyAttributes?: boolean;
53
+ removeEmptyElements?: boolean | {
54
+ removeWithAttributes?: boolean;
55
+ };
56
+ removeRedundantAttributes?: boolean;
57
+ removeOptionalTags?: 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
+ };
74
+ sortAttributes?: boolean | 'alphabetical' | 'frequency';
75
+ sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
76
+ }
77
+ type HtmlnanoModuleAttrsHandler = (attrs: Record<string, string | boolean | void>, node: PostHTML.Node) => Record<string, string | boolean | void>;
78
+ type HtmlnanoModuleContentHandler = (content: Array<PostHTMLNodeLike>, node: PostHTML.Node) => MaybeArray<PostHTMLNodeLike>;
79
+ type HtmlnanoModuleNodeHandler = (node: PostHTMLNodeLike) => PostHTML.Node | string;
80
+ type OptionalOptions<T> = T extends boolean | string | Function | number | null | undefined ? T : T extends object ? Partial<T> : T;
81
+ type HtmlnanoModule<Options = any> = {
82
+ onAttrs?: (options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => HtmlnanoModuleAttrsHandler;
83
+ onContent?: (options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => HtmlnanoModuleContentHandler;
84
+ onNode?: (options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => HtmlnanoModuleNodeHandler;
85
+ default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
86
+ };
87
+
88
+ type RedundantWhitespaceMode = 'safe' | 'agressive' | false;
89
+ type MinifyAttributesOptions = {
90
+ metaContent?: boolean;
91
+ redundantWhitespaces?: RedundantWhitespaceMode | 'aggressive';
92
+ };
93
+ declare const mod: HtmlnanoModule<MinifyAttributesOptions>;
94
+
95
+ export { mod as default };