htmlnano 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +40 -25
  2. package/dist/_modules/collapseAttributeWhitespace.d.mts +41 -5
  3. package/dist/_modules/collapseAttributeWhitespace.d.ts +41 -5
  4. package/dist/_modules/collapseAttributeWhitespace.js +69 -16
  5. package/dist/_modules/collapseAttributeWhitespace.mjs +67 -17
  6. package/dist/_modules/collapseBooleanAttributes.d.mts +36 -3
  7. package/dist/_modules/collapseBooleanAttributes.d.ts +36 -3
  8. package/dist/_modules/collapseBooleanAttributes.js +18 -11
  9. package/dist/_modules/collapseBooleanAttributes.mjs +18 -11
  10. package/dist/_modules/collapseWhitespace.d.mts +36 -3
  11. package/dist/_modules/collapseWhitespace.d.ts +36 -3
  12. package/dist/_modules/collapseWhitespace.js +25 -2
  13. package/dist/_modules/collapseWhitespace.mjs +25 -2
  14. package/dist/_modules/custom.d.mts +36 -3
  15. package/dist/_modules/custom.d.ts +36 -3
  16. package/dist/_modules/deduplicateAttributeValues.d.mts +36 -3
  17. package/dist/_modules/deduplicateAttributeValues.d.ts +36 -3
  18. package/dist/_modules/deduplicateAttributeValues.js +20 -5
  19. package/dist/_modules/deduplicateAttributeValues.mjs +21 -6
  20. package/dist/_modules/example.d.mts +36 -3
  21. package/dist/_modules/example.d.ts +36 -3
  22. package/dist/_modules/mergeScripts.d.mts +36 -3
  23. package/dist/_modules/mergeScripts.d.ts +36 -3
  24. package/dist/_modules/mergeScripts.js +111 -24
  25. package/dist/_modules/mergeScripts.mjs +111 -24
  26. package/dist/_modules/mergeStyles.d.mts +36 -3
  27. package/dist/_modules/mergeStyles.d.ts +36 -3
  28. package/dist/_modules/mergeStyles.js +66 -4
  29. package/dist/_modules/mergeStyles.mjs +66 -4
  30. package/dist/_modules/minifyAttributes.d.mts +95 -0
  31. package/dist/_modules/minifyAttributes.d.ts +95 -0
  32. package/dist/_modules/minifyAttributes.js +159 -0
  33. package/dist/_modules/minifyAttributes.mjs +157 -0
  34. package/dist/_modules/minifyConditionalComments.d.mts +36 -3
  35. package/dist/_modules/minifyConditionalComments.d.ts +36 -3
  36. package/dist/_modules/minifyConditionalComments.js +37 -19
  37. package/dist/_modules/minifyConditionalComments.mjs +37 -19
  38. package/dist/_modules/minifyCss.d.mts +36 -3
  39. package/dist/_modules/minifyCss.d.ts +36 -3
  40. package/dist/_modules/minifyCss.js +13 -27
  41. package/dist/_modules/minifyCss.mjs +14 -28
  42. package/dist/_modules/minifyHtmlTemplate.d.mts +91 -0
  43. package/dist/_modules/minifyHtmlTemplate.d.ts +91 -0
  44. package/dist/_modules/minifyHtmlTemplate.js +231 -0
  45. package/dist/_modules/minifyHtmlTemplate.mjs +228 -0
  46. package/dist/_modules/minifyJs.d.mts +36 -3
  47. package/dist/_modules/minifyJs.d.ts +36 -3
  48. package/dist/_modules/minifyJs.js +106 -5
  49. package/dist/_modules/minifyJs.mjs +107 -6
  50. package/dist/_modules/minifyJson.d.mts +36 -3
  51. package/dist/_modules/minifyJson.d.ts +36 -3
  52. package/dist/_modules/minifyJson.js +8 -11
  53. package/dist/_modules/minifyJson.mjs +8 -11
  54. package/dist/_modules/minifySvg.d.mts +36 -3
  55. package/dist/_modules/minifySvg.d.ts +36 -3
  56. package/dist/_modules/minifySvg.js +35 -4
  57. package/dist/_modules/minifySvg.mjs +35 -4
  58. package/dist/_modules/minifyUrls.d.mts +37 -4
  59. package/dist/_modules/minifyUrls.d.ts +37 -4
  60. package/dist/_modules/minifyUrls.js +52 -27
  61. package/dist/_modules/minifyUrls.mjs +52 -27
  62. package/dist/_modules/normalizeAttributeValues.d.mts +36 -3
  63. package/dist/_modules/normalizeAttributeValues.d.ts +36 -3
  64. package/dist/_modules/normalizeAttributeValues.js +10 -8
  65. package/dist/_modules/normalizeAttributeValues.mjs +10 -8
  66. package/dist/_modules/removeAttributeQuotes.d.mts +40 -4
  67. package/dist/_modules/removeAttributeQuotes.d.ts +40 -4
  68. package/dist/_modules/removeAttributeQuotes.js +9 -4
  69. package/dist/_modules/removeAttributeQuotes.mjs +9 -4
  70. package/dist/_modules/removeComments.d.mts +37 -4
  71. package/dist/_modules/removeComments.d.ts +37 -4
  72. package/dist/_modules/removeComments.js +44 -12
  73. package/dist/_modules/removeComments.mjs +44 -12
  74. package/dist/_modules/removeEmptyAttributes.d.mts +36 -3
  75. package/dist/_modules/removeEmptyAttributes.d.ts +36 -3
  76. package/dist/_modules/removeEmptyAttributes.js +37 -16
  77. package/dist/_modules/removeEmptyAttributes.mjs +37 -16
  78. package/dist/_modules/removeEmptyElements.d.mts +95 -0
  79. package/dist/_modules/removeEmptyElements.d.ts +95 -0
  80. package/dist/_modules/removeEmptyElements.js +90 -0
  81. package/dist/_modules/removeEmptyElements.mjs +88 -0
  82. package/dist/_modules/removeOptionalTags.d.mts +36 -3
  83. package/dist/_modules/removeOptionalTags.d.ts +36 -3
  84. package/dist/_modules/removeOptionalTags.js +39 -28
  85. package/dist/_modules/removeOptionalTags.mjs +39 -28
  86. package/dist/_modules/removeRedundantAttributes.d.mts +36 -3
  87. package/dist/_modules/removeRedundantAttributes.d.ts +36 -3
  88. package/dist/_modules/removeRedundantAttributes.js +43 -28
  89. package/dist/_modules/removeRedundantAttributes.mjs +43 -28
  90. package/dist/_modules/removeUnusedCss.d.mts +37 -3
  91. package/dist/_modules/removeUnusedCss.d.ts +37 -3
  92. package/dist/_modules/removeUnusedCss.js +40 -14
  93. package/dist/_modules/removeUnusedCss.mjs +41 -15
  94. package/dist/_modules/sortAttributes.d.mts +39 -5
  95. package/dist/_modules/sortAttributes.d.ts +39 -5
  96. package/dist/_modules/sortAttributes.js +27 -8
  97. package/dist/_modules/sortAttributes.mjs +27 -8
  98. package/dist/_modules/sortAttributesWithLists.d.mts +39 -4
  99. package/dist/_modules/sortAttributesWithLists.d.ts +39 -4
  100. package/dist/_modules/sortAttributesWithLists.js +61 -52
  101. package/dist/_modules/sortAttributesWithLists.mjs +62 -53
  102. package/dist/helpers.d.ts +8 -1
  103. package/dist/helpers.js +48 -0
  104. package/dist/helpers.mjs +45 -1
  105. package/dist/index.d.ts +37 -4
  106. package/dist/index.js +13 -0
  107. package/dist/index.mjs +13 -0
  108. package/dist/presets/ampSafe.d.ts +36 -3
  109. package/dist/presets/max.d.ts +36 -3
  110. package/dist/presets/max.js +13 -5
  111. package/dist/presets/max.mjs +13 -5
  112. package/dist/presets/safe.d.ts +36 -3
  113. package/dist/presets/safe.js +6 -0
  114. package/dist/presets/safe.mjs +6 -0
  115. package/package.json +27 -15
@@ -2,16 +2,24 @@ import PostHTML from 'posthtml';
2
2
  import { MinifyOptions } from 'terser';
3
3
  import { Options } from 'cssnano';
4
4
  import { Config } from 'svgo';
5
+ import { UserDefinedOptions } from 'purgecss';
5
6
 
6
7
  type PostHTMLTreeLike = [PostHTML.Node] & PostHTML.NodeAPI & {
7
8
  options?: {
8
9
  quoteAllAttributes?: boolean | undefined;
10
+ quoteStyle?: 0 | 1 | 2 | undefined;
11
+ replaceQuote?: boolean | undefined;
9
12
  } | undefined;
10
13
  render(): string;
11
14
  render(node: PostHTML.Node | PostHTMLTreeLike, renderOptions?: any): string;
12
15
  };
13
16
  type MaybeArray<T> = T | Array<T>;
14
17
  type PostHTMLNodeLike = PostHTML.Node | string;
18
+ type HtmlnanoTemplateRule = {
19
+ tag: string;
20
+ attrs?: Record<string, string | boolean | void>;
21
+ };
22
+ type MinifyHtmlTemplateOptions = boolean | HtmlnanoTemplateRule[];
15
23
  interface HtmlnanoOptions {
16
24
  skipConfigLoading?: boolean;
17
25
  configPath?: string;
@@ -27,17 +35,42 @@ interface HtmlnanoOptions {
27
35
  mergeStyles?: boolean;
28
36
  mergeScripts?: boolean;
29
37
  minifyCss?: Options | boolean;
38
+ minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
30
39
  minifyConditionalComments?: boolean;
31
40
  minifyJs?: MinifyOptions | boolean;
32
41
  minifyJson?: boolean;
42
+ minifyAttributes?: boolean | {
43
+ metaContent?: boolean;
44
+ redundantWhitespaces?: 'safe' | 'agressive' | false;
45
+ };
33
46
  minifySvg?: Config | boolean;
34
47
  normalizeAttributeValues?: boolean;
35
- removeAttributeQuotes?: boolean;
36
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
48
+ removeAttributeQuotes?: boolean | {
49
+ force?: boolean;
50
+ };
51
+ removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
37
52
  removeEmptyAttributes?: boolean;
53
+ removeEmptyElements?: boolean | {
54
+ removeWithAttributes?: boolean;
55
+ };
38
56
  removeRedundantAttributes?: boolean;
39
57
  removeOptionalTags?: boolean;
40
- removeUnusedCss?: boolean;
58
+ removeUnusedCss?: boolean | ({
59
+ tool: 'purgeCSS';
60
+ } & Omit<UserDefinedOptions, 'content' | 'css' | 'extractors'>) | {
61
+ banner?: boolean;
62
+ csspath?: string;
63
+ htmlroot?: string;
64
+ ignore?: (string | RegExp)[];
65
+ inject?: string;
66
+ jsdom?: object;
67
+ media?: string[];
68
+ report?: boolean;
69
+ strictSSL?: boolean;
70
+ timeout?: number;
71
+ uncssrc?: string;
72
+ userAgent?: string;
73
+ };
41
74
  sortAttributes?: boolean | 'alphabetical' | 'frequency';
42
75
  sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
43
76
  }
@@ -52,7 +85,8 @@ type HtmlnanoModule<Options = any> = {
52
85
  default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
53
86
  };
54
87
 
55
- type ValidOptions = 'alphabetical' | 'frequency';
56
- declare const mod: HtmlnanoModule<boolean | ValidOptions>;
88
+ type SortAttributesOption = 'alphabetical' | 'frequency';
89
+
90
+ declare const mod: HtmlnanoModule<boolean | SortAttributesOption>;
57
91
 
58
92
  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,7 +85,8 @@ type HtmlnanoModule<Options = any> = {
52
85
  default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
53
86
  };
54
87
 
55
- type ValidOptions = 'alphabetical' | 'frequency';
56
- declare const mod: HtmlnanoModule<boolean | ValidOptions>;
88
+ type SortAttributesOption = 'alphabetical' | 'frequency';
89
+
90
+ declare const mod: HtmlnanoModule<boolean | SortAttributesOption>;
57
91
 
58
92
  export { mod as default };
@@ -4,11 +4,12 @@ const validOptions = new Set([
4
4
  'frequency',
5
5
  'alphabetical'
6
6
  ]);
7
- const processModuleOptions = (options)=>{
7
+ function resolveSortType(options) {
8
8
  if (options === true) return 'alphabetical';
9
9
  if (options === false) return false;
10
10
  return validOptions.has(options) ? options : false;
11
- };
11
+ }
12
+
12
13
  class AttributeTokenChain {
13
14
  addFromNodeAttrs(nodeAttrs) {
14
15
  Object.keys(nodeAttrs).forEach((attrName)=>{
@@ -24,23 +25,41 @@ class AttributeTokenChain {
24
25
  const _sortOrder = [
25
26
  ...this.freqData.entries()
26
27
  ];
27
- _sortOrder.sort((a, b)=>b[1] - a[1]);
28
+ _sortOrder.sort((a, b)=>{
29
+ const freqDiff = b[1] - a[1];
30
+ if (freqDiff !== 0) return freqDiff;
31
+ return a[0].localeCompare(b[0]);
32
+ });
28
33
  this.sortOrder = _sortOrder.map((i)=>i[0]);
29
34
  }
30
35
  sortFromNodeAttrs(nodeAttrs) {
31
36
  const newAttrs = {};
32
- // Convert node.attrs attrName into lower case.
37
+ // Convert node.attrs attrName into lower case while preserving originals.
33
38
  const loweredNodeAttrs = {};
34
39
  Object.entries(nodeAttrs).forEach(([attrName, attrValue])=>{
35
- loweredNodeAttrs[attrName.toLowerCase()] = attrValue;
40
+ const attrNameLower = attrName.toLowerCase();
41
+ if (!loweredNodeAttrs[attrNameLower]) {
42
+ loweredNodeAttrs[attrNameLower] = {
43
+ name: attrName,
44
+ value: attrValue
45
+ };
46
+ }
36
47
  });
37
48
  if (!this.sortOrder) {
38
49
  this.createSortOrder();
39
50
  }
51
+ const seen = new Set();
40
52
  this.sortOrder.forEach((attrNameLower)=>{
41
53
  // The attrName inside "sortOrder" has been lowered
42
- if (loweredNodeAttrs[attrNameLower] != null) {
43
- newAttrs[attrNameLower] = loweredNodeAttrs[attrNameLower];
54
+ const originalAttr = loweredNodeAttrs[attrNameLower];
55
+ if (originalAttr != null) {
56
+ newAttrs[originalAttr.name] = originalAttr.value;
57
+ seen.add(attrNameLower);
58
+ }
59
+ });
60
+ Object.entries(loweredNodeAttrs).forEach(([attrNameLower, originalAttr])=>{
61
+ if (!seen.has(attrNameLower)) {
62
+ newAttrs[originalAttr.name] = originalAttr.value;
44
63
  }
45
64
  });
46
65
  return newAttrs;
@@ -52,7 +71,7 @@ class AttributeTokenChain {
52
71
  }
53
72
  /** Sort attibutes */ const mod = {
54
73
  default (tree, options, moduleOptions) {
55
- const sortType = processModuleOptions(moduleOptions);
74
+ const sortType = resolveSortType(moduleOptions);
56
75
  if (sortType === 'alphabetical') {
57
76
  return sortAttributesInAlphabeticalOrder(tree);
58
77
  }
@@ -2,11 +2,12 @@ const validOptions = new Set([
2
2
  'frequency',
3
3
  'alphabetical'
4
4
  ]);
5
- const processModuleOptions = (options)=>{
5
+ function resolveSortType(options) {
6
6
  if (options === true) return 'alphabetical';
7
7
  if (options === false) return false;
8
8
  return validOptions.has(options) ? options : false;
9
- };
9
+ }
10
+
10
11
  class AttributeTokenChain {
11
12
  addFromNodeAttrs(nodeAttrs) {
12
13
  Object.keys(nodeAttrs).forEach((attrName)=>{
@@ -22,23 +23,41 @@ class AttributeTokenChain {
22
23
  const _sortOrder = [
23
24
  ...this.freqData.entries()
24
25
  ];
25
- _sortOrder.sort((a, b)=>b[1] - a[1]);
26
+ _sortOrder.sort((a, b)=>{
27
+ const freqDiff = b[1] - a[1];
28
+ if (freqDiff !== 0) return freqDiff;
29
+ return a[0].localeCompare(b[0]);
30
+ });
26
31
  this.sortOrder = _sortOrder.map((i)=>i[0]);
27
32
  }
28
33
  sortFromNodeAttrs(nodeAttrs) {
29
34
  const newAttrs = {};
30
- // Convert node.attrs attrName into lower case.
35
+ // Convert node.attrs attrName into lower case while preserving originals.
31
36
  const loweredNodeAttrs = {};
32
37
  Object.entries(nodeAttrs).forEach(([attrName, attrValue])=>{
33
- loweredNodeAttrs[attrName.toLowerCase()] = attrValue;
38
+ const attrNameLower = attrName.toLowerCase();
39
+ if (!loweredNodeAttrs[attrNameLower]) {
40
+ loweredNodeAttrs[attrNameLower] = {
41
+ name: attrName,
42
+ value: attrValue
43
+ };
44
+ }
34
45
  });
35
46
  if (!this.sortOrder) {
36
47
  this.createSortOrder();
37
48
  }
49
+ const seen = new Set();
38
50
  this.sortOrder.forEach((attrNameLower)=>{
39
51
  // The attrName inside "sortOrder" has been lowered
40
- if (loweredNodeAttrs[attrNameLower] != null) {
41
- newAttrs[attrNameLower] = loweredNodeAttrs[attrNameLower];
52
+ const originalAttr = loweredNodeAttrs[attrNameLower];
53
+ if (originalAttr != null) {
54
+ newAttrs[originalAttr.name] = originalAttr.value;
55
+ seen.add(attrNameLower);
56
+ }
57
+ });
58
+ Object.entries(loweredNodeAttrs).forEach(([attrNameLower, originalAttr])=>{
59
+ if (!seen.has(attrNameLower)) {
60
+ newAttrs[originalAttr.name] = originalAttr.value;
42
61
  }
43
62
  });
44
63
  return newAttrs;
@@ -50,7 +69,7 @@ class AttributeTokenChain {
50
69
  }
51
70
  /** Sort attibutes */ const mod = {
52
71
  default (tree, options, moduleOptions) {
53
- const sortType = processModuleOptions(moduleOptions);
72
+ const sortType = resolveSortType(moduleOptions);
54
73
  if (sortType === 'alphabetical') {
55
74
  return sortAttributesInAlphabeticalOrder(tree);
56
75
  }
@@ -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,8 @@ 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<boolean | 'alphabetical' | 'frequency'>;
88
+ type SortAttributesOption = 'alphabetical' | 'frequency';
89
+
90
+ declare const mod: HtmlnanoModule<boolean | SortAttributesOption>;
56
91
 
57
92
  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,8 @@ 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<boolean | 'alphabetical' | 'frequency'>;
88
+ type SortAttributesOption = 'alphabetical' | 'frequency';
89
+
90
+ declare const mod: HtmlnanoModule<boolean | SortAttributesOption>;
56
91
 
57
92
  export { mod as default };
@@ -2,53 +2,69 @@ Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  var collapseAttributeWhitespace_js = require('./collapseAttributeWhitespace.js');
4
4
 
5
- // class, rel, ping
6
5
  const validOptions = new Set([
7
6
  'frequency',
8
7
  'alphabetical'
9
8
  ]);
10
- const processModuleOptions = (options)=>{
9
+ function resolveSortType(options) {
11
10
  if (options === true) return 'alphabetical';
12
11
  if (options === false) return false;
13
12
  return validOptions.has(options) ? options : false;
14
- };
15
- class AttributeTokenChain {
13
+ }
14
+
15
+ // class, rel, ping
16
+ class ListAttributeTokenChain {
16
17
  addFromNodeAttrsArray(attrValuesArray) {
17
18
  attrValuesArray.forEach((attrValue)=>{
18
- if (this.freqData.has(attrValue)) {
19
- this.freqData.set(attrValue, this.freqData.get(attrValue) + 1);
19
+ if (!attrValue) {
20
+ return;
21
+ }
22
+ if (this.tokenCounts.has(attrValue)) {
23
+ this.tokenCounts.set(attrValue, this.tokenCounts.get(attrValue) + 1);
20
24
  } else {
21
- this.freqData.set(attrValue, 1);
25
+ this.tokenCounts.set(attrValue, 1);
22
26
  }
23
27
  });
24
28
  }
25
29
  createSortOrder() {
26
- const _sortOrder = [
27
- ...this.freqData.entries()
30
+ const nextSortOrder = [
31
+ ...this.tokenCounts.entries()
28
32
  ];
29
- _sortOrder.sort((a, b)=>b[1] - a[1]);
30
- this.sortOrder = _sortOrder.map((i)=>i[0]);
33
+ nextSortOrder.sort((a, b)=>b[1] - a[1] || a[0].localeCompare(b[0]));
34
+ this.sortedTokens = nextSortOrder.map((i)=>i[0]);
31
35
  }
32
36
  sortFromNodeAttrsArray(attrValuesArray) {
33
37
  const resultArray = [];
34
- if (!this.sortOrder) {
38
+ const tokenCounts = new Map();
39
+ attrValuesArray.forEach((attrValue)=>{
40
+ var _tokenCounts_get;
41
+ if (!attrValue) {
42
+ return;
43
+ }
44
+ tokenCounts.set(attrValue, ((_tokenCounts_get = tokenCounts.get(attrValue)) != null ? _tokenCounts_get : 0) + 1);
45
+ });
46
+ if (!this.sortedTokens) {
35
47
  this.createSortOrder();
36
48
  }
37
- this.sortOrder.forEach((k)=>{
38
- if (attrValuesArray.includes(k)) {
49
+ this.sortedTokens.forEach((k)=>{
50
+ const count = tokenCounts.get(k);
51
+ if (!count) {
52
+ return;
53
+ }
54
+ for(let i = 0; i < count; i += 1){
39
55
  resultArray.push(k);
40
56
  }
41
57
  });
42
58
  return resultArray;
43
59
  }
44
60
  constructor(){
45
- /** <attr, frequency> */ this.freqData = new Map();
46
- this.sortOrder = null;
61
+ /** <attr, frequency> */ this.tokenCounts = new Map();
62
+ this.sortedTokens = null;
47
63
  }
48
64
  }
49
65
  /** Sort values inside list-like attributes (e.g. class, rel) */ const mod = {
50
66
  default (tree, options, moduleOptions) {
51
- const sortType = processModuleOptions(moduleOptions);
67
+ const sortType = resolveSortType(moduleOptions);
52
68
  if (sortType === 'alphabetical') {
53
69
  return sortAttributesWithListsInAlphabeticalOrder(tree);
54
70
  }
@@ -59,58 +75,51 @@ class AttributeTokenChain {
59
75
  return tree;
60
76
  }
61
77
  };
62
- function sortAttributesWithListsInAlphabeticalOrder(tree) {
78
+ const splitListAttributeValues = (attrValue)=>attrValue.split(/\s+/).filter(Boolean);
79
+ function walkListAttributes(tree, walkFn) {
63
80
  tree.walk((node)=>{
64
81
  if (!node.attrs) {
65
82
  return node;
66
83
  }
67
- Object.keys(node.attrs).forEach((attrName)=>{
84
+ const tagName = node.tag ? node.tag.toLowerCase() : undefined;
85
+ Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
68
86
  const attrNameLower = attrName.toLowerCase();
69
- if (!collapseAttributeWhitespace_js.attributesWithLists.has(attrNameLower)) {
87
+ if (!collapseAttributeWhitespace_js.isListAttribute(attrNameLower, tagName) || typeof attrValues !== 'string') {
70
88
  return;
71
89
  }
72
- const attrValues = node.attrs[attrName].split(/\s/);
73
- node.attrs[attrName] = attrValues.sort((a, b)=>{
74
- // @ts-expect-error -- deliberately use minus operator to sort things
75
- return typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b;
76
- }).join(' ');
90
+ walkFn(node.attrs, attrName, attrValues);
77
91
  });
78
92
  return node;
79
93
  });
94
+ }
95
+ function sortAttributesWithListsInAlphabeticalOrder(tree) {
96
+ walkListAttributes(tree, (nodeAttrs, attrName, attrValues)=>{
97
+ const values = splitListAttributeValues(attrValues);
98
+ if (values.length < 2) {
99
+ return;
100
+ }
101
+ nodeAttrs[attrName] = values.sort((a, b)=>{
102
+ // @ts-expect-error -- deliberately use minus operator to sort things
103
+ return typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b;
104
+ }).join(' ');
105
+ });
80
106
  return tree;
81
107
  }
82
108
  function sortAttributesWithListsByFrequency(tree) {
83
- const tokenChainObj = {}; // <attrNameLower: AttributeTokenChain>[]
109
+ const tokenChainObj = {};
84
110
  // Traverse through tree to get frequency
85
- tree.walk((node)=>{
86
- if (!node.attrs) {
87
- return node;
88
- }
89
- Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
90
- const attrNameLower = attrName.toLowerCase();
91
- if (!collapseAttributeWhitespace_js.attributesWithLists.has(attrNameLower)) {
92
- return;
93
- }
94
- tokenChainObj[attrNameLower] = tokenChainObj[attrNameLower] || new AttributeTokenChain();
95
- tokenChainObj[attrNameLower].addFromNodeAttrsArray(attrValues.split(/\s/));
96
- });
97
- return node;
111
+ walkListAttributes(tree, (_nodeAttrs, attrName, attrValues)=>{
112
+ const attrNameLower = attrName.toLowerCase();
113
+ tokenChainObj[attrNameLower] = tokenChainObj[attrNameLower] || new ListAttributeTokenChain();
114
+ tokenChainObj[attrNameLower].addFromNodeAttrsArray(splitListAttributeValues(attrValues));
98
115
  });
99
116
  // Traverse through tree again, this time sort the attribute values
100
- tree.walk((node)=>{
101
- if (!node.attrs) {
102
- return node;
117
+ walkListAttributes(tree, (nodeAttrs, attrName, attrValues)=>{
118
+ const attrNameLower = attrName.toLowerCase();
119
+ if (!tokenChainObj[attrNameLower]) {
120
+ return;
103
121
  }
104
- Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
105
- const attrNameLower = attrName.toLowerCase();
106
- if (!collapseAttributeWhitespace_js.attributesWithLists.has(attrNameLower)) {
107
- return;
108
- }
109
- if (tokenChainObj[attrNameLower]) {
110
- node.attrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(attrValues.split(/\s/)).join(' ');
111
- }
112
- });
113
- return node;
122
+ nodeAttrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(splitListAttributeValues(attrValues)).join(' ');
114
123
  });
115
124
  return tree;
116
125
  }