htmlnano 2.0.3 → 2.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.
- package/.eslintignore +3 -2
- package/CHANGELOG.md +43 -3
- package/docs/docs/040-presets.md +4 -4
- package/docs/docs/050-modules.md +1 -1
- package/docs/docs/060-contribute.md +1 -1
- package/docs/docusaurus.config.js +5 -0
- package/docs/package-lock.json +7592 -11615
- package/docs/package.json +4 -3
- package/docs/versioned_docs/version-1.1.1/040-presets.md +4 -4
- package/docs/versioned_docs/version-1.1.1/050-modules.md +1 -2
- package/docs/versioned_docs/version-1.1.1/060-contribute.md +1 -1
- package/docs/versioned_docs/version-2.0.0/040-presets.md +4 -4
- package/docs/versioned_docs/version-2.0.0/050-modules.md +1 -1
- package/docs/versioned_docs/version-2.0.0/060-contribute.md +1 -1
- package/index.cjs +11 -0
- package/index.d.cts +3 -0
- package/index.d.mts +3 -0
- package/index.d.ts +3 -3
- package/index.mjs +2 -0
- package/lib/helpers.cjs +78 -0
- package/lib/helpers.mjs +52 -0
- package/lib/htmlnano.cjs +200 -0
- package/lib/htmlnano.mjs +196 -0
- package/lib/modules/{collapseAttributeWhitespace.js → collapseAttributeWhitespace.cjs} +2 -3
- package/lib/modules/collapseAttributeWhitespace.mjs +104 -0
- package/lib/modules/collapseBooleanAttributes.mjs +175 -0
- package/lib/modules/{collapseWhitespace.js → collapseWhitespace.cjs} +3 -2
- package/lib/modules/collapseWhitespace.mjs +132 -0
- package/lib/modules/custom.mjs +16 -0
- package/lib/modules/{deduplicateAttributeValues.js → deduplicateAttributeValues.cjs} +1 -1
- package/lib/modules/deduplicateAttributeValues.mjs +40 -0
- package/lib/modules/example.cjs +85 -0
- package/lib/modules/example.mjs +75 -0
- package/lib/modules/{mergeScripts.js → mergeScripts.cjs} +3 -1
- package/lib/modules/mergeScripts.mjs +56 -0
- package/lib/modules/{mergeStyles.js → mergeStyles.cjs} +4 -2
- package/lib/modules/mergeStyles.mjs +36 -0
- package/lib/modules/{minifyConditionalComments.js → minifyConditionalComments.cjs} +2 -2
- package/lib/modules/minifyConditionalComments.mjs +49 -0
- package/lib/modules/{minifyCss.js → minifyCss.cjs} +12 -8
- package/lib/modules/minifyCss.mjs +88 -0
- package/lib/modules/{minifyJs.js → minifyJs.cjs} +25 -10
- package/lib/modules/minifyJs.mjs +121 -0
- package/lib/modules/{minifyJson.js → minifyJson.cjs} +4 -0
- package/lib/modules/minifyJson.mjs +21 -0
- package/lib/modules/minifySvg.cjs +37 -0
- package/lib/modules/minifySvg.mjs +30 -0
- package/lib/modules/{minifyUrls.js → minifyUrls.cjs} +7 -8
- package/lib/modules/minifyUrls.mjs +229 -0
- package/lib/modules/{normalizeAttributeValues.js → normalizeAttributeValues.cjs} +0 -5
- package/lib/modules/normalizeAttributeValues.mjs +140 -0
- package/lib/modules/removeAttributeQuotes.mjs +12 -0
- package/lib/modules/{removeComments.js → removeComments.cjs} +1 -1
- package/lib/modules/removeComments.mjs +92 -0
- package/lib/modules/{removeEmptyAttributes.js → removeEmptyAttributes.cjs} +1 -1
- package/lib/modules/removeEmptyAttributes.mjs +121 -0
- package/lib/modules/{removeOptionalTags.js → removeOptionalTags.cjs} +1 -1
- package/lib/modules/removeOptionalTags.mjs +225 -0
- package/lib/modules/{removeRedundantAttributes.js → removeRedundantAttributes.cjs} +1 -2
- package/lib/modules/removeRedundantAttributes.mjs +141 -0
- package/lib/modules/{removeUnusedCss.js → removeUnusedCss.cjs} +12 -13
- package/lib/modules/removeUnusedCss.mjs +122 -0
- package/lib/modules/sortAttributes.mjs +121 -0
- package/lib/modules/{sortAttributesWithLists.js → sortAttributesWithLists.cjs} +1 -1
- package/lib/modules/sortAttributesWithLists.mjs +135 -0
- package/lib/presets/{ampSafe.js → ampSafe.cjs} +4 -9
- package/lib/presets/ampSafe.mjs +11 -0
- package/lib/presets/{max.js → max.cjs} +4 -9
- package/lib/presets/max.mjs +20 -0
- package/lib/presets/{safe.js → safe.cjs} +2 -3
- package/lib/presets/safe.mjs +65 -0
- package/package.json +40 -12
- package/test.js +48 -0
- package/index.js +0 -1
- package/lib/helpers.js +0 -52
- package/lib/htmlnano.js +0 -147
- package/lib/modules/minifySvg.js +0 -38
- /package/lib/modules/{collapseBooleanAttributes.js → collapseBooleanAttributes.cjs} +0 -0
- /package/lib/modules/{custom.js → custom.cjs} +0 -0
- /package/lib/modules/{removeAttributeQuotes.js → removeAttributeQuotes.cjs} +0 -0
- /package/lib/modules/{sortAttributes.js → sortAttributes.cjs} +0 -0
package/lib/htmlnano.mjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import posthtml from 'posthtml';
|
|
2
|
+
import { cosmiconfigSync } from 'cosmiconfig';
|
|
3
|
+
import safePreset from './presets/safe.mjs';
|
|
4
|
+
import ampSafePreset from './presets/ampSafe.mjs';
|
|
5
|
+
import maxPreset from './presets/max.mjs';
|
|
6
|
+
|
|
7
|
+
const presets = {
|
|
8
|
+
safe: safePreset,
|
|
9
|
+
ampSafe: ampSafePreset,
|
|
10
|
+
max: maxPreset,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function loadConfig(options, preset, configPath) {
|
|
14
|
+
let { skipConfigLoading = false, ...rest } = options || {};
|
|
15
|
+
|
|
16
|
+
if (!skipConfigLoading) {
|
|
17
|
+
const explorer = cosmiconfigSync('htmlnano');
|
|
18
|
+
const rc = configPath ? explorer.load(configPath) : explorer.search();
|
|
19
|
+
if (rc) {
|
|
20
|
+
const { preset: presetName } = rc.config;
|
|
21
|
+
if (presetName) {
|
|
22
|
+
if (!preset && presets[presetName]) {
|
|
23
|
+
preset = presets[presetName];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
delete rc.config.preset;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!options) {
|
|
30
|
+
rest = rc.config;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
rest || {},
|
|
37
|
+
preset || safePreset,
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const optionalDependencies = {
|
|
42
|
+
minifyCss: ['cssnano', 'postcss'],
|
|
43
|
+
minifyJs: ['terser'],
|
|
44
|
+
minifyUrl: ['relateurl', 'srcset', 'terser'],
|
|
45
|
+
minifySvg: ['svgo'],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
const modules = {
|
|
50
|
+
collapseAttributeWhitespace: () => import('./modules/collapseAttributeWhitespace.mjs'),
|
|
51
|
+
collapseBooleanAttributes: () => import('./modules/collapseBooleanAttributes.mjs'),
|
|
52
|
+
collapseWhitespace: () => import('./modules/collapseWhitespace.mjs'),
|
|
53
|
+
custom: () => import('./modules/custom.mjs'),
|
|
54
|
+
deduplicateAttributeValues: () => import('./modules/deduplicateAttributeValues.mjs'),
|
|
55
|
+
example: () => import('./modules/example.mjs'),
|
|
56
|
+
mergeScripts: () => import('./modules/mergeScripts.mjs'),
|
|
57
|
+
mergeStyles: () => import('./modules/mergeStyles.mjs'),
|
|
58
|
+
minifyConditionalComments: () => import('./modules/minifyConditionalComments.mjs'),
|
|
59
|
+
minifyCss: () => import('./modules/minifyCss.mjs'),
|
|
60
|
+
minifyJs: () => import('./modules/minifyJs.mjs'),
|
|
61
|
+
minifyJson: () => import('./modules/minifyJson.mjs'),
|
|
62
|
+
minifySvg: () => import('./modules/minifySvg.mjs'),
|
|
63
|
+
minifyUrls: () => import('./modules/minifyUrls.mjs'),
|
|
64
|
+
normalizeAttributeValues: () => import('./modules/normalizeAttributeValues.mjs'),
|
|
65
|
+
removeAttributeQuotes: () => import('./modules/removeAttributeQuotes.mjs'),
|
|
66
|
+
removeComments: () => import('./modules/removeComments.mjs'),
|
|
67
|
+
removeEmptyAttributes: () => import('./modules/removeEmptyAttributes.mjs'),
|
|
68
|
+
removeOptionalTags: () => import('./modules/removeOptionalTags.mjs'),
|
|
69
|
+
removeRedundantAttributes: () => import('./modules/removeRedundantAttributes.mjs'),
|
|
70
|
+
removeUnusedCss: () => import('./modules/removeUnusedCss.mjs'),
|
|
71
|
+
sortAttributes: () => import('./modules/sortAttributes.mjs'),
|
|
72
|
+
sortAttributesWithLists: () => import('./modules/sortAttributesWithLists.mjs'),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function htmlnano(optionsRun, presetRun) {
|
|
76
|
+
let [options, preset] = loadConfig(optionsRun, presetRun);
|
|
77
|
+
|
|
78
|
+
return async function minifier(tree) {
|
|
79
|
+
const nodeHandlers = [];
|
|
80
|
+
const attrsHandlers = [];
|
|
81
|
+
const contentsHandlers = [];
|
|
82
|
+
|
|
83
|
+
options = { ...preset, ...options };
|
|
84
|
+
let promise = Promise.resolve(tree);
|
|
85
|
+
|
|
86
|
+
for (const [moduleName, moduleOptions] of Object.entries(options)) {
|
|
87
|
+
if (!moduleOptions) {
|
|
88
|
+
// The module is disabled
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (safePreset[moduleName] === undefined) {
|
|
93
|
+
throw new Error('Module "' + moduleName + '" is not defined');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
(optionalDependencies[moduleName] || []).forEach(async dependency => {
|
|
97
|
+
try {
|
|
98
|
+
await import(dependency);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if (e.code === 'MODULE_NOT_FOUND' || e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
101
|
+
console.warn(`You have to install "${dependency}" in order to use htmlnano's "${moduleName}" module`);
|
|
102
|
+
} else {
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const module = moduleName in modules ?
|
|
109
|
+
await (modules[moduleName]()) :
|
|
110
|
+
await import(`./modules/${moduleName}.mjs`);
|
|
111
|
+
|
|
112
|
+
if (typeof module.onAttrs === 'function') {
|
|
113
|
+
attrsHandlers.push(module.onAttrs(options, moduleOptions));
|
|
114
|
+
}
|
|
115
|
+
if (typeof module.onContent === 'function') {
|
|
116
|
+
contentsHandlers.push(module.onContent(options, moduleOptions));
|
|
117
|
+
}
|
|
118
|
+
if (typeof module.onNode === 'function') {
|
|
119
|
+
nodeHandlers.push(module.onNode(options, moduleOptions));
|
|
120
|
+
}
|
|
121
|
+
if (typeof module.default === 'function') {
|
|
122
|
+
promise = promise.then(async tree => await module.default(tree, options, moduleOptions));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
|
|
127
|
+
return promise;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return promise.then(tree => {
|
|
131
|
+
tree.walk(node => {
|
|
132
|
+
if (node) {
|
|
133
|
+
if (node.attrs && typeof node.attrs === 'object') {
|
|
134
|
+
// Convert all attrs' key to lower case
|
|
135
|
+
let newAttrsObj = {};
|
|
136
|
+
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
137
|
+
newAttrsObj[attrName.toLowerCase()] = attrValue;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
for (const handler of attrsHandlers) {
|
|
141
|
+
newAttrsObj = handler(newAttrsObj, node);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
node.attrs = newAttrsObj;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (node.content) {
|
|
148
|
+
node.content = typeof node.content === 'string' ? [node.content] : node.content;
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(node.content) && node.content.length > 0) {
|
|
151
|
+
for (const handler of contentsHandlers) {
|
|
152
|
+
const result = handler(node.content, node);
|
|
153
|
+
node.content = typeof result === 'string' ? [result] : result;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const handler of nodeHandlers) {
|
|
159
|
+
node = handler(node);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return node;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return tree;
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
|
|
172
|
+
const [options] = loadConfig(optionsRun, presetRun);
|
|
173
|
+
|
|
174
|
+
return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
htmlnano.process = function (html, options, preset, postHtmlOptions) {
|
|
179
|
+
return posthtml([htmlnano(options, preset)])
|
|
180
|
+
.process(html, postHtmlOptions);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// https://github.com/webpack-contrib/html-minimizer-webpack-plugin/blob/faca00f2219514bc671c5942685721f0b5dbaa70/src/utils.js#L74
|
|
184
|
+
htmlnano.htmlMinimizerWebpackPluginMinify = function htmlNano(input, minimizerOptions = {}) {
|
|
185
|
+
const [[, code]] = Object.entries(input);
|
|
186
|
+
return htmlnano.process(code, minimizerOptions, presets.safe)
|
|
187
|
+
.then(result => {
|
|
188
|
+
return {
|
|
189
|
+
code: result.html
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
htmlnano.presets = presets;
|
|
195
|
+
|
|
196
|
+
export default htmlnano;
|
|
@@ -5,8 +5,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.attributesWithLists = void 0;
|
|
7
7
|
exports.onAttrs = onAttrs;
|
|
8
|
-
var _helpers = require("../helpers");
|
|
9
|
-
const attributesWithLists = new Set(['class', 'dropzone', 'rel',
|
|
8
|
+
var _helpers = require("../helpers.cjs");
|
|
9
|
+
const attributesWithLists = exports.attributesWithLists = new Set(['class', 'dropzone', 'rel',
|
|
10
10
|
// a, area, link
|
|
11
11
|
'ping',
|
|
12
12
|
// a, area
|
|
@@ -23,7 +23,6 @@ const attributesWithLists = new Set(['class', 'dropzone', 'rel',
|
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
25
|
/** @type Record<string, string[] | null> */
|
|
26
|
-
exports.attributesWithLists = attributesWithLists;
|
|
27
26
|
const attributesWithSingleValue = {
|
|
28
27
|
accept: ['input'],
|
|
29
28
|
action: ['form'],
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { isEventHandler } from '../helpers.mjs';
|
|
2
|
+
|
|
3
|
+
export const attributesWithLists = new Set([
|
|
4
|
+
'class',
|
|
5
|
+
'dropzone',
|
|
6
|
+
'rel', // a, area, link
|
|
7
|
+
'ping', // a, area
|
|
8
|
+
'sandbox', // iframe
|
|
9
|
+
/**
|
|
10
|
+
* https://github.com/posthtml/htmlnano/issues/180
|
|
11
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes
|
|
12
|
+
*
|
|
13
|
+
* "sizes" of <img> should not be modified, while "sizes" of <link> will only have one entry in most cases.
|
|
14
|
+
*/
|
|
15
|
+
// 'sizes', // link
|
|
16
|
+
'headers' // td, th
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/** @type Record<string, string[] | null> */
|
|
20
|
+
const attributesWithSingleValue = {
|
|
21
|
+
accept: ['input'],
|
|
22
|
+
action: ['form'],
|
|
23
|
+
accesskey: null,
|
|
24
|
+
'accept-charset': ['form'],
|
|
25
|
+
cite: ['blockquote', 'del', 'ins', 'q'],
|
|
26
|
+
cols: ['textarea'],
|
|
27
|
+
colspan: ['td', 'th'],
|
|
28
|
+
data: ['object'],
|
|
29
|
+
dropzone: null,
|
|
30
|
+
formaction: ['button', 'input'],
|
|
31
|
+
height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
|
32
|
+
high: ['meter'],
|
|
33
|
+
href: ['a', 'area', 'base', 'link'],
|
|
34
|
+
itemid: null,
|
|
35
|
+
low: ['meter'],
|
|
36
|
+
manifest: ['html'],
|
|
37
|
+
max: ['meter', 'progress'],
|
|
38
|
+
maxlength: ['input', 'textarea'],
|
|
39
|
+
media: ['source'],
|
|
40
|
+
min: ['meter'],
|
|
41
|
+
minlength: ['input', 'textarea'],
|
|
42
|
+
optimum: ['meter'],
|
|
43
|
+
ping: ['a', 'area'],
|
|
44
|
+
poster: ['video'],
|
|
45
|
+
profile: ['head'],
|
|
46
|
+
rows: ['textarea'],
|
|
47
|
+
rowspan: ['td', 'th'],
|
|
48
|
+
size: ['input', 'select'],
|
|
49
|
+
span: ['col', 'colgroup'],
|
|
50
|
+
src: [
|
|
51
|
+
'audio',
|
|
52
|
+
'embed',
|
|
53
|
+
'iframe',
|
|
54
|
+
'img',
|
|
55
|
+
'input',
|
|
56
|
+
'script',
|
|
57
|
+
'source',
|
|
58
|
+
'track',
|
|
59
|
+
'video'
|
|
60
|
+
],
|
|
61
|
+
start: ['ol'],
|
|
62
|
+
step: ['input'],
|
|
63
|
+
style: null,
|
|
64
|
+
tabindex: null,
|
|
65
|
+
usemap: ['img', 'object'],
|
|
66
|
+
value: ['li', 'meter', 'progress'],
|
|
67
|
+
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
|
|
71
|
+
export function onAttrs() {
|
|
72
|
+
return (attrs, node) => {
|
|
73
|
+
const newAttrs = attrs;
|
|
74
|
+
|
|
75
|
+
Object.entries(attrs).forEach(([attrName, attrValue]) => {
|
|
76
|
+
if (typeof attrValue !== 'string') return;
|
|
77
|
+
|
|
78
|
+
if (attributesWithLists.has(attrName)) {
|
|
79
|
+
const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
|
|
80
|
+
newAttrs[attrName] = newAttrValue;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
isEventHandler(attrName)
|
|
86
|
+
|| (
|
|
87
|
+
Object.prototype.hasOwnProperty.call(attributesWithSingleValue, attrName)
|
|
88
|
+
&& (
|
|
89
|
+
attributesWithSingleValue[attrName] === null
|
|
90
|
+
|| attributesWithSingleValue[attrName].includes(node.tag)
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
newAttrs[attrName] = minifySingleAttributeValue(attrValue);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return newAttrs;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function minifySingleAttributeValue(value) {
|
|
103
|
+
return typeof value === 'string' ? value.trim() : value;
|
|
104
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Source: https://github.com/kangax/html-minifier/issues/63
|
|
2
|
+
// https://html.spec.whatwg.org/#boolean-attribute
|
|
3
|
+
// https://html.spec.whatwg.org/#attributes-1
|
|
4
|
+
const htmlBooleanAttributes = new Set([
|
|
5
|
+
'allowfullscreen',
|
|
6
|
+
'allowpaymentrequest',
|
|
7
|
+
'allowtransparency',
|
|
8
|
+
'async',
|
|
9
|
+
'autofocus',
|
|
10
|
+
'autoplay',
|
|
11
|
+
'checked',
|
|
12
|
+
'compact',
|
|
13
|
+
'controls',
|
|
14
|
+
'declare',
|
|
15
|
+
'default',
|
|
16
|
+
'defaultchecked',
|
|
17
|
+
'defaultmuted',
|
|
18
|
+
'defaultselected',
|
|
19
|
+
'defer',
|
|
20
|
+
'disabled',
|
|
21
|
+
'enabled',
|
|
22
|
+
'formnovalidate',
|
|
23
|
+
'hidden',
|
|
24
|
+
'indeterminate',
|
|
25
|
+
'inert',
|
|
26
|
+
'ismap',
|
|
27
|
+
'itemscope',
|
|
28
|
+
'loop',
|
|
29
|
+
'multiple',
|
|
30
|
+
'muted',
|
|
31
|
+
'nohref',
|
|
32
|
+
'nomodule',
|
|
33
|
+
'noresize',
|
|
34
|
+
'noshade',
|
|
35
|
+
'novalidate',
|
|
36
|
+
'nowrap',
|
|
37
|
+
'open',
|
|
38
|
+
'pauseonexit',
|
|
39
|
+
'playsinline',
|
|
40
|
+
'readonly',
|
|
41
|
+
'required',
|
|
42
|
+
'reversed',
|
|
43
|
+
'scoped',
|
|
44
|
+
'seamless',
|
|
45
|
+
'selected',
|
|
46
|
+
'sortable',
|
|
47
|
+
'truespeed',
|
|
48
|
+
'typemustmatch',
|
|
49
|
+
'visible'
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const amphtmlBooleanAttributes = new Set([
|
|
53
|
+
'⚡',
|
|
54
|
+
'amp',
|
|
55
|
+
'⚡4ads',
|
|
56
|
+
'amp4ads',
|
|
57
|
+
'⚡4email',
|
|
58
|
+
'amp4email',
|
|
59
|
+
|
|
60
|
+
'amp-custom',
|
|
61
|
+
'amp-boilerplate',
|
|
62
|
+
'amp4ads-boilerplate',
|
|
63
|
+
'amp4email-boilerplate',
|
|
64
|
+
|
|
65
|
+
'allow-blocked-ranges',
|
|
66
|
+
'amp-access-hide',
|
|
67
|
+
'amp-access-template',
|
|
68
|
+
'amp-keyframes',
|
|
69
|
+
'animate',
|
|
70
|
+
'arrows',
|
|
71
|
+
'data-block-on-consent',
|
|
72
|
+
'data-enable-refresh',
|
|
73
|
+
'data-multi-size',
|
|
74
|
+
'date-template',
|
|
75
|
+
'disable-double-tap',
|
|
76
|
+
'disable-session-states',
|
|
77
|
+
'disableremoteplayback',
|
|
78
|
+
'dots',
|
|
79
|
+
'expand-single-section',
|
|
80
|
+
'expanded',
|
|
81
|
+
'fallback',
|
|
82
|
+
'first',
|
|
83
|
+
'fullscreen',
|
|
84
|
+
'inline',
|
|
85
|
+
'lightbox',
|
|
86
|
+
'noaudio',
|
|
87
|
+
'noautoplay',
|
|
88
|
+
'noloading',
|
|
89
|
+
'once',
|
|
90
|
+
'open-after-clear',
|
|
91
|
+
'open-after-select',
|
|
92
|
+
'open-button',
|
|
93
|
+
'placeholder',
|
|
94
|
+
'preload',
|
|
95
|
+
'reset-on-refresh',
|
|
96
|
+
'reset-on-resize',
|
|
97
|
+
'resizable',
|
|
98
|
+
'rotate-to-fullscreen',
|
|
99
|
+
'second',
|
|
100
|
+
'standalone',
|
|
101
|
+
'stereo',
|
|
102
|
+
'submit-error',
|
|
103
|
+
'submit-success',
|
|
104
|
+
'submitting',
|
|
105
|
+
'subscriptions-actions',
|
|
106
|
+
'subscriptions-dialog'
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const missingValueDefaultEmptyStringAttributes = {
|
|
110
|
+
// https://html.spec.whatwg.org/#attr-media-preload
|
|
111
|
+
audio: {
|
|
112
|
+
preload: 'auto'
|
|
113
|
+
},
|
|
114
|
+
video: {
|
|
115
|
+
preload: 'auto'
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const tagsHasMissingValueDefaultEmptyStringAttributes = new Set(Object.keys(missingValueDefaultEmptyStringAttributes));
|
|
120
|
+
|
|
121
|
+
export function onAttrs(options, moduleOptions) {
|
|
122
|
+
return (attrs, node) => {
|
|
123
|
+
if (!node.tag) return attrs;
|
|
124
|
+
|
|
125
|
+
const newAttrs = attrs;
|
|
126
|
+
|
|
127
|
+
if (tagsHasMissingValueDefaultEmptyStringAttributes.has(node.tag)) {
|
|
128
|
+
const tagAttributesCanBeReplacedWithEmptyString = missingValueDefaultEmptyStringAttributes[node.tag];
|
|
129
|
+
|
|
130
|
+
for (const attributesCanBeReplacedWithEmptyString of Object.keys(tagAttributesCanBeReplacedWithEmptyString)) {
|
|
131
|
+
if (
|
|
132
|
+
Object.prototype.hasOwnProperty.call(attrs, attributesCanBeReplacedWithEmptyString)
|
|
133
|
+
&& attrs[attributesCanBeReplacedWithEmptyString] === tagAttributesCanBeReplacedWithEmptyString[attributesCanBeReplacedWithEmptyString]
|
|
134
|
+
) {
|
|
135
|
+
attrs[attributesCanBeReplacedWithEmptyString] = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const attrName of Object.keys(attrs)) {
|
|
141
|
+
if (attrName === 'visible' && node.tag.startsWith('a-')) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (htmlBooleanAttributes.has(attrName)) {
|
|
146
|
+
newAttrs[attrName] = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fast path optimization.
|
|
150
|
+
// The rest of tranformations are only for string type attrValue.
|
|
151
|
+
if (typeof newAttrs[attrName] !== 'string') continue;
|
|
152
|
+
|
|
153
|
+
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && attrs[attrName] === '') {
|
|
154
|
+
newAttrs[attrName] = true;
|
|
155
|
+
}
|
|
156
|
+
// https://html.spec.whatwg.org/#a-quick-introduction-to-html
|
|
157
|
+
// The value, along with the "=" character, can be omitted altogether if the value is the empty string.
|
|
158
|
+
if (attrs[attrName] === '') {
|
|
159
|
+
newAttrs[attrName] = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// collapse crossorigin attributes
|
|
163
|
+
// Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
|
|
164
|
+
if (
|
|
165
|
+
attrName.toLowerCase() === 'crossorigin' && (
|
|
166
|
+
attrs[attrName] === 'anonymous'
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
newAttrs[attrName] = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return newAttrs;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = collapseWhitespace;
|
|
7
|
-
var _helpers = require("../helpers");
|
|
7
|
+
var _helpers = require("../helpers.cjs");
|
|
8
8
|
const noWhitespaceCollapseElements = new Set(['script', 'style', 'pre', 'textarea']);
|
|
9
9
|
const noTrimWhitespacesArroundElements = new Set([
|
|
10
10
|
// non-empty tags that will maintain whitespace around them
|
|
@@ -16,7 +16,8 @@ const noTrimWhitespacesInsideElements = new Set([
|
|
|
16
16
|
'a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var']);
|
|
17
17
|
const startsWithWhitespacePattern = /^\s/;
|
|
18
18
|
const endsWithWhitespacePattern = /\s$/;
|
|
19
|
-
|
|
19
|
+
// See https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace and https://infra.spec.whatwg.org/#ascii-whitespace
|
|
20
|
+
const multipleWhitespacePattern = /[\t\n\f\r ]+/g;
|
|
20
21
|
const NONE = '';
|
|
21
22
|
const SINGLE_SPACE = ' ';
|
|
22
23
|
const validOptions = ['all', 'aggressive', 'conservative'];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { isComment } from '../helpers.mjs';
|
|
2
|
+
|
|
3
|
+
const noWhitespaceCollapseElements = new Set([
|
|
4
|
+
'script',
|
|
5
|
+
'style',
|
|
6
|
+
'pre',
|
|
7
|
+
'textarea'
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const noTrimWhitespacesArroundElements = new Set([
|
|
11
|
+
// non-empty tags that will maintain whitespace around them
|
|
12
|
+
'a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'ins', 'kbd', 'label', 'mark', 'math', 'nobr', 'object', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var',
|
|
13
|
+
// self-closing tags that will maintain whitespace around them
|
|
14
|
+
'comment', 'img', 'input', 'wbr'
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const noTrimWhitespacesInsideElements = new Set([
|
|
18
|
+
// non-empty tags that will maintain whitespace within them
|
|
19
|
+
'a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var'
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const startsWithWhitespacePattern = /^\s/;
|
|
23
|
+
const endsWithWhitespacePattern = /\s$/;
|
|
24
|
+
// See https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace and https://infra.spec.whatwg.org/#ascii-whitespace
|
|
25
|
+
const multipleWhitespacePattern = /[\t\n\f\r ]+/g;
|
|
26
|
+
const NONE = '';
|
|
27
|
+
const SINGLE_SPACE = ' ';
|
|
28
|
+
const validOptions = ['all', 'aggressive', 'conservative'];
|
|
29
|
+
|
|
30
|
+
/** Collapses redundant whitespaces */
|
|
31
|
+
export default function collapseWhitespace(tree, options, collapseType, parent) {
|
|
32
|
+
collapseType = validOptions.includes(collapseType) ? collapseType : 'conservative';
|
|
33
|
+
|
|
34
|
+
tree.forEach((node, index) => {
|
|
35
|
+
const prevNode = tree[index - 1];
|
|
36
|
+
const nextNode = tree[index + 1];
|
|
37
|
+
|
|
38
|
+
if (typeof node === 'string') {
|
|
39
|
+
const parentNodeTag = parent && parent.node && parent.node.tag;
|
|
40
|
+
const isTopLevel = !parentNodeTag || parentNodeTag === 'html' || parentNodeTag === 'head';
|
|
41
|
+
const shouldTrim = (
|
|
42
|
+
collapseType === 'all' ||
|
|
43
|
+
isTopLevel ||
|
|
44
|
+
/*
|
|
45
|
+
* When collapseType is set to 'aggressive', and the tag is not inside 'noTrimWhitespacesInsideElements'.
|
|
46
|
+
* the first & last space inside the tag will be trimmed
|
|
47
|
+
*/
|
|
48
|
+
collapseType === 'aggressive'
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
node = collapseRedundantWhitespaces(node, collapseType, shouldTrim, parent, prevNode, nextNode);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isAllowCollapseWhitespace = !noWhitespaceCollapseElements.has(node.tag);
|
|
55
|
+
if (node.content && node.content.length && isAllowCollapseWhitespace) {
|
|
56
|
+
node.content = collapseWhitespace(node.content, options, collapseType, {
|
|
57
|
+
node,
|
|
58
|
+
prevNode,
|
|
59
|
+
nextNode
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
tree[index] = node;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return tree;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false, parent, prevNode, nextNode) {
|
|
71
|
+
if (!text || text.length === 0) {
|
|
72
|
+
return NONE;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!isComment(text)) {
|
|
76
|
+
text = text.replace(multipleWhitespacePattern, SINGLE_SPACE);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (shouldTrim) {
|
|
80
|
+
if (collapseType === 'aggressive') {
|
|
81
|
+
if (!noTrimWhitespacesInsideElements.has(parent && parent.node && parent.node.tag)) {
|
|
82
|
+
if (
|
|
83
|
+
// It is the first child node of the parent
|
|
84
|
+
!prevNode
|
|
85
|
+
// It is not the first child node, and prevNode not a text node, and prevNode is safe to trim around
|
|
86
|
+
|| prevNode && prevNode.tag && !noTrimWhitespacesArroundElements.has(prevNode.tag)
|
|
87
|
+
) {
|
|
88
|
+
text = text.trimStart();
|
|
89
|
+
} else {
|
|
90
|
+
// previous node is a "no trim whitespaces arround element"
|
|
91
|
+
if (
|
|
92
|
+
// but previous node ends with a whitespace
|
|
93
|
+
prevNode && prevNode.content && prevNode.content.length
|
|
94
|
+
&& endsWithWhitespacePattern.test(prevNode.content[prevNode.content.length - 1])
|
|
95
|
+
&& (
|
|
96
|
+
!nextNode // either the current node is the last child of the parent
|
|
97
|
+
|| (
|
|
98
|
+
// or the next node starts with a white space
|
|
99
|
+
nextNode && nextNode.content && nextNode.content.length
|
|
100
|
+
&& !startsWithWhitespacePattern.test(nextNode.content[0])
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
) {
|
|
104
|
+
text = text.trimStart();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
!nextNode
|
|
110
|
+
|| nextNode && nextNode.tag && !noTrimWhitespacesArroundElements.has(nextNode.tag)
|
|
111
|
+
) {
|
|
112
|
+
text = text.trimEnd();
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// now it is a textNode inside a "no trim whitespaces inside elements" node
|
|
116
|
+
if (
|
|
117
|
+
!prevNode // it the textnode is the first child of the node
|
|
118
|
+
&& startsWithWhitespacePattern.test(text[0]) // it starts with white space
|
|
119
|
+
&& typeof parent.prevNode === 'string' // the prev of the node is a textNode as well
|
|
120
|
+
&& endsWithWhitespacePattern.test(parent.prevNode[parent.prevNode.length - 1]) // that prev is ends with a white
|
|
121
|
+
) {
|
|
122
|
+
text = text.trimStart();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// collapseType is 'all', trim spaces
|
|
127
|
+
text = text.trim();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return text;
|
|
132
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Meta-module that runs custom modules */
|
|
2
|
+
export default function custom(tree, options, customModules) {
|
|
3
|
+
if (! customModules) {
|
|
4
|
+
return tree;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (! Array.isArray(customModules)) {
|
|
8
|
+
customModules = [customModules];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
customModules.forEach(customModule => {
|
|
12
|
+
tree = customModule(tree, options);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return tree;
|
|
16
|
+
}
|
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.onAttrs = onAttrs;
|
|
7
|
-
var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace");
|
|
7
|
+
var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace.cjs");
|
|
8
8
|
/** Deduplicate values inside list-like attributes (e.g. class, rel) */
|
|
9
9
|
function onAttrs() {
|
|
10
10
|
return attrs => {
|