html-minifier-next 1.0.1 → 1.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/README.md +66 -3
- package/cli.js +3 -1
- package/package.json +11 -11
- package/src/htmlminifier.js +23 -2
- package/dist/htmlminifier.cjs +0 -1858
- package/dist/htmlminifier.esm.bundle.js +0 -59386
- package/dist/htmlminifier.umd.bundle.js +0 -59397
- package/dist/htmlminifier.umd.bundle.min.js +0 -9
package/dist/htmlminifier.cjs
DELETED
|
@@ -1,1858 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
|
-
var CleanCSS = require('clean-css');
|
|
6
|
-
var entities = require('entities');
|
|
7
|
-
var RelateURL = require('relateurl');
|
|
8
|
-
var terser = require('terser');
|
|
9
|
-
|
|
10
|
-
async function replaceAsync(str, regex, asyncFn) {
|
|
11
|
-
const promises = [];
|
|
12
|
-
|
|
13
|
-
str.replace(regex, (match, ...args) => {
|
|
14
|
-
const promise = asyncFn(match, ...args);
|
|
15
|
-
promises.push(promise);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const data = await Promise.all(promises);
|
|
19
|
-
return str.replace(regex, () => data.shift());
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/*!
|
|
23
|
-
* HTML Parser By John Resig (ejohn.org)
|
|
24
|
-
* Modified by Juriy "kangax" Zaytsev
|
|
25
|
-
* Original code by Erik Arvidsson, Mozilla Public License
|
|
26
|
-
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class CaseInsensitiveSet extends Set {
|
|
31
|
-
has(str) {
|
|
32
|
-
return super.has(str.toLowerCase());
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Regular Expressions for parsing tags and attributes
|
|
37
|
-
const singleAttrIdentifier = /([^\s"'<>/=]+)/;
|
|
38
|
-
const singleAttrAssigns = [/=/];
|
|
39
|
-
const singleAttrValues = [
|
|
40
|
-
// attr value double quotes
|
|
41
|
-
/"([^"]*)"+/.source,
|
|
42
|
-
// attr value, single quotes
|
|
43
|
-
/'([^']*)'+/.source,
|
|
44
|
-
// attr value, no quotes
|
|
45
|
-
/([^ \t\n\f\r"'`=<>]+)/.source
|
|
46
|
-
];
|
|
47
|
-
// https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
|
|
48
|
-
const qnameCapture = (function () {
|
|
49
|
-
// based on https://www.npmjs.com/package/ncname
|
|
50
|
-
const combiningChar = '\\u0300-\\u0345\\u0360\\u0361\\u0483-\\u0486\\u0591-\\u05A1\\u05A3-\\u05B9\\u05BB-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u064B-\\u0652\\u0670\\u06D6-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0901-\\u0903\\u093C\\u093E-\\u094D\\u0951-\\u0954\\u0962\\u0963\\u0981-\\u0983\\u09BC\\u09BE-\\u09C4\\u09C7\\u09C8\\u09CB-\\u09CD\\u09D7\\u09E2\\u09E3\\u0A02\\u0A3C\\u0A3E-\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A70\\u0A71\\u0A81-\\u0A83\\u0ABC\\u0ABE-\\u0AC5\\u0AC7-\\u0AC9\\u0ACB-\\u0ACD\\u0B01-\\u0B03\\u0B3C\\u0B3E-\\u0B43\\u0B47\\u0B48\\u0B4B-\\u0B4D\\u0B56\\u0B57\\u0B82\\u0B83\\u0BBE-\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCD\\u0BD7\\u0C01-\\u0C03\\u0C3E-\\u0C44\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C82\\u0C83\\u0CBE-\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5\\u0CD6\\u0D02\\u0D03\\u0D3E-\\u0D43\\u0D46-\\u0D48\\u0D4A-\\u0D4D\\u0D57\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F3E\\u0F3F\\u0F71-\\u0F84\\u0F86-\\u0F8B\\u0F90-\\u0F95\\u0F97\\u0F99-\\u0FAD\\u0FB1-\\u0FB7\\u0FB9\\u20D0-\\u20DC\\u20E1\\u302A-\\u302F\\u3099\\u309A';
|
|
51
|
-
const digit = '0-9\\u0660-\\u0669\\u06F0-\\u06F9\\u0966-\\u096F\\u09E6-\\u09EF\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0BE7-\\u0BEF\\u0C66-\\u0C6F\\u0CE6-\\u0CEF\\u0D66-\\u0D6F\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F29';
|
|
52
|
-
const extender = '\\xB7\\u02D0\\u02D1\\u0387\\u0640\\u0E46\\u0EC6\\u3005\\u3031-\\u3035\\u309D\\u309E\\u30FC-\\u30FE';
|
|
53
|
-
const letter = 'A-Za-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u0131\\u0134-\\u013E\\u0141-\\u0148\\u014A-\\u017E\\u0180-\\u01C3\\u01CD-\\u01F0\\u01F4\\u01F5\\u01FA-\\u0217\\u0250-\\u02A8\\u02BB-\\u02C1\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03CE\\u03D0-\\u03D6\\u03DA\\u03DC\\u03DE\\u03E0\\u03E2-\\u03F3\\u0401-\\u040C\\u040E-\\u044F\\u0451-\\u045C\\u045E-\\u0481\\u0490-\\u04C4\\u04C7\\u04C8\\u04CB\\u04CC\\u04D0-\\u04EB\\u04EE-\\u04F5\\u04F8\\u04F9\\u0531-\\u0556\\u0559\\u0561-\\u0586\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0621-\\u063A\\u0641-\\u064A\\u0671-\\u06B7\\u06BA-\\u06BE\\u06C0-\\u06CE\\u06D0-\\u06D3\\u06D5\\u06E5\\u06E6\\u0905-\\u0939\\u093D\\u0958-\\u0961\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8B\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AE0\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B36-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB5\\u0BB7-\\u0BB9\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CDE\\u0CE0\\u0CE1\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D28\\u0D2A-\\u0D39\\u0D60\\u0D61\\u0E01-\\u0E2E\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E45\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD\\u0EAE\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0F40-\\u0F47\\u0F49-\\u0F69\\u10A0-\\u10C5\\u10D0-\\u10F6\\u1100\\u1102\\u1103\\u1105-\\u1107\\u1109\\u110B\\u110C\\u110E-\\u1112\\u113C\\u113E\\u1140\\u114C\\u114E\\u1150\\u1154\\u1155\\u1159\\u115F-\\u1161\\u1163\\u1165\\u1167\\u1169\\u116D\\u116E\\u1172\\u1173\\u1175\\u119E\\u11A8\\u11AB\\u11AE\\u11AF\\u11B7\\u11B8\\u11BA\\u11BC-\\u11C2\\u11EB\\u11F0\\u11F9\\u1E00-\\u1E9B\\u1EA0-\\u1EF9\\u1F00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2126\\u212A\\u212B\\u212E\\u2180-\\u2182\\u3007\\u3021-\\u3029\\u3041-\\u3094\\u30A1-\\u30FA\\u3105-\\u312C\\u4E00-\\u9FA5\\uAC00-\\uD7A3';
|
|
54
|
-
const ncname = '[' + letter + '_][' + letter + digit + '\\.\\-_' + combiningChar + extender + ']*';
|
|
55
|
-
return '((?:' + ncname + '\\:)?' + ncname + ')';
|
|
56
|
-
})();
|
|
57
|
-
const startTagOpen = new RegExp('^<' + qnameCapture);
|
|
58
|
-
const startTagClose = /^\s*(\/?)>/;
|
|
59
|
-
const endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>');
|
|
60
|
-
const doctype = /^<!DOCTYPE\s?[^>]+>/i;
|
|
61
|
-
|
|
62
|
-
let IS_REGEX_CAPTURING_BROKEN = false;
|
|
63
|
-
'x'.replace(/x(.)?/g, function (m, g) {
|
|
64
|
-
IS_REGEX_CAPTURING_BROKEN = g === '';
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Empty Elements
|
|
68
|
-
const empty = new CaseInsensitiveSet(['area', 'base', 'basefont', 'br', 'col', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
69
|
-
|
|
70
|
-
// Inline Elements
|
|
71
|
-
const inline = new CaseInsensitiveSet(['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'noscript', 'object', 'q', 's', 'samp', 'script', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'tt', 'u', 'var']);
|
|
72
|
-
|
|
73
|
-
// Elements that you can, intentionally, leave open
|
|
74
|
-
// (and which close themselves)
|
|
75
|
-
const closeSelf = new CaseInsensitiveSet(['colgroup', 'dd', 'dt', 'li', 'option', 'p', 'td', 'tfoot', 'th', 'thead', 'tr', 'source']);
|
|
76
|
-
|
|
77
|
-
// Attributes that have their values filled in disabled='disabled'
|
|
78
|
-
const fillAttrs = new CaseInsensitiveSet(['checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected']);
|
|
79
|
-
|
|
80
|
-
// Special Elements (can contain anything)
|
|
81
|
-
const special = new CaseInsensitiveSet(['script', 'style']);
|
|
82
|
-
|
|
83
|
-
// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
|
|
84
|
-
// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
|
|
85
|
-
const nonPhrasing = new CaseInsensitiveSet(['address', 'article', 'aside', 'base', 'blockquote', 'body', 'caption', 'col', 'colgroup', 'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'legend', 'li', 'menuitem', 'meta', 'ol', 'optgroup', 'option', 'param', 'rp', 'rt', 'source', 'style', 'summary', 'tbody', 'td', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul']);
|
|
86
|
-
|
|
87
|
-
const reCache = {};
|
|
88
|
-
|
|
89
|
-
function attrForHandler(handler) {
|
|
90
|
-
let pattern = singleAttrIdentifier.source +
|
|
91
|
-
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
|
|
92
|
-
'[ \\t\\n\\f\\r]*(?:' + singleAttrValues.join('|') + '))?';
|
|
93
|
-
if (handler.customAttrSurround) {
|
|
94
|
-
const attrClauses = [];
|
|
95
|
-
for (let i = handler.customAttrSurround.length - 1; i >= 0; i--) {
|
|
96
|
-
attrClauses[i] = '(?:' +
|
|
97
|
-
'(' + handler.customAttrSurround[i][0].source + ')\\s*' +
|
|
98
|
-
pattern +
|
|
99
|
-
'\\s*(' + handler.customAttrSurround[i][1].source + ')' +
|
|
100
|
-
')';
|
|
101
|
-
}
|
|
102
|
-
attrClauses.push('(?:' + pattern + ')');
|
|
103
|
-
pattern = '(?:' + attrClauses.join('|') + ')';
|
|
104
|
-
}
|
|
105
|
-
return new RegExp('^\\s*' + pattern);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function joinSingleAttrAssigns(handler) {
|
|
109
|
-
return singleAttrAssigns.concat(
|
|
110
|
-
handler.customAttrAssign || []
|
|
111
|
-
).map(function (assign) {
|
|
112
|
-
return '(?:' + assign.source + ')';
|
|
113
|
-
}).join('|');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
class HTMLParser {
|
|
117
|
-
constructor(html, handler) {
|
|
118
|
-
this.html = html;
|
|
119
|
-
this.handler = handler;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async parse() {
|
|
123
|
-
let html = this.html;
|
|
124
|
-
const handler = this.handler;
|
|
125
|
-
|
|
126
|
-
const stack = []; let lastTag;
|
|
127
|
-
const attribute = attrForHandler(handler);
|
|
128
|
-
let last, prevTag, nextTag;
|
|
129
|
-
while (html) {
|
|
130
|
-
last = html;
|
|
131
|
-
// Make sure we're not in a script or style element
|
|
132
|
-
if (!lastTag || !special.has(lastTag)) {
|
|
133
|
-
let textEnd = html.indexOf('<');
|
|
134
|
-
if (textEnd === 0) {
|
|
135
|
-
// Comment:
|
|
136
|
-
if (/^<!--/.test(html)) {
|
|
137
|
-
const commentEnd = html.indexOf('-->');
|
|
138
|
-
|
|
139
|
-
if (commentEnd >= 0) {
|
|
140
|
-
if (handler.comment) {
|
|
141
|
-
await handler.comment(html.substring(4, commentEnd));
|
|
142
|
-
}
|
|
143
|
-
html = html.substring(commentEnd + 3);
|
|
144
|
-
prevTag = '';
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
|
|
150
|
-
if (/^<!\[/.test(html)) {
|
|
151
|
-
const conditionalEnd = html.indexOf(']>');
|
|
152
|
-
|
|
153
|
-
if (conditionalEnd >= 0) {
|
|
154
|
-
if (handler.comment) {
|
|
155
|
-
await handler.comment(html.substring(2, conditionalEnd + 1), true /* non-standard */);
|
|
156
|
-
}
|
|
157
|
-
html = html.substring(conditionalEnd + 2);
|
|
158
|
-
prevTag = '';
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Doctype:
|
|
164
|
-
const doctypeMatch = html.match(doctype);
|
|
165
|
-
if (doctypeMatch) {
|
|
166
|
-
if (handler.doctype) {
|
|
167
|
-
handler.doctype(doctypeMatch[0]);
|
|
168
|
-
}
|
|
169
|
-
html = html.substring(doctypeMatch[0].length);
|
|
170
|
-
prevTag = '';
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// End tag:
|
|
175
|
-
const endTagMatch = html.match(endTag);
|
|
176
|
-
if (endTagMatch) {
|
|
177
|
-
html = html.substring(endTagMatch[0].length);
|
|
178
|
-
await replaceAsync(endTagMatch[0], endTag, parseEndTag);
|
|
179
|
-
prevTag = '/' + endTagMatch[1].toLowerCase();
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Start tag:
|
|
184
|
-
const startTagMatch = parseStartTag(html);
|
|
185
|
-
if (startTagMatch) {
|
|
186
|
-
html = startTagMatch.rest;
|
|
187
|
-
await handleStartTag(startTagMatch);
|
|
188
|
-
prevTag = startTagMatch.tagName.toLowerCase();
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Treat `<` as text
|
|
193
|
-
if (handler.continueOnParseError) {
|
|
194
|
-
textEnd = html.indexOf('<', 1);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
let text;
|
|
199
|
-
if (textEnd >= 0) {
|
|
200
|
-
text = html.substring(0, textEnd);
|
|
201
|
-
html = html.substring(textEnd);
|
|
202
|
-
} else {
|
|
203
|
-
text = html;
|
|
204
|
-
html = '';
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// next tag
|
|
208
|
-
let nextTagMatch = parseStartTag(html);
|
|
209
|
-
if (nextTagMatch) {
|
|
210
|
-
nextTag = nextTagMatch.tagName;
|
|
211
|
-
} else {
|
|
212
|
-
nextTagMatch = html.match(endTag);
|
|
213
|
-
if (nextTagMatch) {
|
|
214
|
-
nextTag = '/' + nextTagMatch[1];
|
|
215
|
-
} else {
|
|
216
|
-
nextTag = '';
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (handler.chars) {
|
|
221
|
-
await handler.chars(text, prevTag, nextTag);
|
|
222
|
-
}
|
|
223
|
-
prevTag = '';
|
|
224
|
-
} else {
|
|
225
|
-
const stackedTag = lastTag.toLowerCase();
|
|
226
|
-
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)</' + stackedTag + '[^>]*>', 'i'));
|
|
227
|
-
|
|
228
|
-
html = await replaceAsync(html, reStackedTag, async (_, text) => {
|
|
229
|
-
if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
|
|
230
|
-
text = text
|
|
231
|
-
.replace(/<!--([\s\S]*?)-->/g, '$1')
|
|
232
|
-
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (handler.chars) {
|
|
236
|
-
await handler.chars(text);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return '';
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
await parseEndTag('</' + stackedTag + '>', stackedTag);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (html === last) {
|
|
246
|
-
throw new Error('Parse Error: ' + html);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (!handler.partialMarkup) {
|
|
251
|
-
// Clean up any remaining tags
|
|
252
|
-
await parseEndTag();
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function parseStartTag(input) {
|
|
256
|
-
const start = input.match(startTagOpen);
|
|
257
|
-
if (start) {
|
|
258
|
-
const match = {
|
|
259
|
-
tagName: start[1],
|
|
260
|
-
attrs: []
|
|
261
|
-
};
|
|
262
|
-
input = input.slice(start[0].length);
|
|
263
|
-
let end, attr;
|
|
264
|
-
while (!(end = input.match(startTagClose)) && (attr = input.match(attribute))) {
|
|
265
|
-
input = input.slice(attr[0].length);
|
|
266
|
-
match.attrs.push(attr);
|
|
267
|
-
}
|
|
268
|
-
if (end) {
|
|
269
|
-
match.unarySlash = end[1];
|
|
270
|
-
match.rest = input.slice(end[0].length);
|
|
271
|
-
return match;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function closeIfFound(tagName) {
|
|
277
|
-
if (findTag(tagName) >= 0) {
|
|
278
|
-
await parseEndTag('', tagName);
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async function handleStartTag(match) {
|
|
284
|
-
const tagName = match.tagName;
|
|
285
|
-
let unarySlash = match.unarySlash;
|
|
286
|
-
|
|
287
|
-
if (handler.html5) {
|
|
288
|
-
if (lastTag === 'p' && nonPhrasing.has(tagName)) {
|
|
289
|
-
await parseEndTag('', lastTag);
|
|
290
|
-
} else if (tagName === 'tbody') {
|
|
291
|
-
await closeIfFound('thead');
|
|
292
|
-
} else if (tagName === 'tfoot') {
|
|
293
|
-
if (!await closeIfFound('tbody')) {
|
|
294
|
-
await closeIfFound('thead');
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (tagName === 'col' && findTag('colgroup') < 0) {
|
|
298
|
-
lastTag = 'colgroup';
|
|
299
|
-
stack.push({ tag: lastTag, attrs: [] });
|
|
300
|
-
if (handler.start) {
|
|
301
|
-
await handler.start(lastTag, [], false, '');
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (!handler.html5 && !inline.has(tagName)) {
|
|
307
|
-
while (lastTag && inline.has(lastTag)) {
|
|
308
|
-
await parseEndTag('', lastTag);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (closeSelf.has(tagName) && lastTag === tagName) {
|
|
313
|
-
await parseEndTag('', tagName);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const unary = empty.has(tagName) || (tagName === 'html' && lastTag === 'head') || !!unarySlash;
|
|
317
|
-
|
|
318
|
-
const attrs = match.attrs.map(function (args) {
|
|
319
|
-
let name, value, customOpen, customClose, customAssign, quote;
|
|
320
|
-
const ncp = 7; // number of captured parts, scalar
|
|
321
|
-
|
|
322
|
-
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
|
|
323
|
-
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
|
|
324
|
-
if (args[3] === '') { delete args[3]; }
|
|
325
|
-
if (args[4] === '') { delete args[4]; }
|
|
326
|
-
if (args[5] === '') { delete args[5]; }
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function populate(index) {
|
|
330
|
-
customAssign = args[index];
|
|
331
|
-
value = args[index + 1];
|
|
332
|
-
if (typeof value !== 'undefined') {
|
|
333
|
-
return '"';
|
|
334
|
-
}
|
|
335
|
-
value = args[index + 2];
|
|
336
|
-
if (typeof value !== 'undefined') {
|
|
337
|
-
return '\'';
|
|
338
|
-
}
|
|
339
|
-
value = args[index + 3];
|
|
340
|
-
if (typeof value === 'undefined' && fillAttrs.has(name)) {
|
|
341
|
-
value = name;
|
|
342
|
-
}
|
|
343
|
-
return '';
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
let j = 1;
|
|
347
|
-
if (handler.customAttrSurround) {
|
|
348
|
-
for (let i = 0, l = handler.customAttrSurround.length; i < l; i++, j += ncp) {
|
|
349
|
-
name = args[j + 1];
|
|
350
|
-
if (name) {
|
|
351
|
-
quote = populate(j + 2);
|
|
352
|
-
customOpen = args[j];
|
|
353
|
-
customClose = args[j + 6];
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (!name && (name = args[j])) {
|
|
360
|
-
quote = populate(j + 1);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return {
|
|
364
|
-
name,
|
|
365
|
-
value,
|
|
366
|
-
customAssign: customAssign || '=',
|
|
367
|
-
customOpen: customOpen || '',
|
|
368
|
-
customClose: customClose || '',
|
|
369
|
-
quote: quote || ''
|
|
370
|
-
};
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
if (!unary) {
|
|
374
|
-
stack.push({ tag: tagName, attrs });
|
|
375
|
-
lastTag = tagName;
|
|
376
|
-
unarySlash = '';
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (handler.start) {
|
|
380
|
-
await handler.start(tagName, attrs, unary, unarySlash);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function findTag(tagName) {
|
|
385
|
-
let pos;
|
|
386
|
-
const needle = tagName.toLowerCase();
|
|
387
|
-
for (pos = stack.length - 1; pos >= 0; pos--) {
|
|
388
|
-
if (stack[pos].tag.toLowerCase() === needle) {
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return pos;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
async function parseEndTag(tag, tagName) {
|
|
396
|
-
let pos;
|
|
397
|
-
|
|
398
|
-
// Find the closest opened tag of the same type
|
|
399
|
-
if (tagName) {
|
|
400
|
-
pos = findTag(tagName);
|
|
401
|
-
} else { // If no tag name is provided, clean shop
|
|
402
|
-
pos = 0;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (pos >= 0) {
|
|
406
|
-
// Close all the open elements, up the stack
|
|
407
|
-
for (let i = stack.length - 1; i >= pos; i--) {
|
|
408
|
-
if (handler.end) {
|
|
409
|
-
handler.end(stack[i].tag, stack[i].attrs, i > pos || !tag);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Remove the open elements from the stack
|
|
414
|
-
stack.length = pos;
|
|
415
|
-
lastTag = pos && stack[pos - 1].tag;
|
|
416
|
-
} else if (tagName.toLowerCase() === 'br') {
|
|
417
|
-
if (handler.start) {
|
|
418
|
-
await handler.start(tagName, [], true, '');
|
|
419
|
-
}
|
|
420
|
-
} else if (tagName.toLowerCase() === 'p') {
|
|
421
|
-
if (handler.start) {
|
|
422
|
-
await handler.start(tagName, [], false, '', true);
|
|
423
|
-
}
|
|
424
|
-
if (handler.end) {
|
|
425
|
-
handler.end(tagName, []);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
class Sorter {
|
|
433
|
-
sort(tokens, fromIndex = 0) {
|
|
434
|
-
for (let i = 0, len = this.keys.length; i < len; i++) {
|
|
435
|
-
const key = this.keys[i];
|
|
436
|
-
const token = key.slice(1);
|
|
437
|
-
|
|
438
|
-
let index = tokens.indexOf(token, fromIndex);
|
|
439
|
-
|
|
440
|
-
if (index !== -1) {
|
|
441
|
-
do {
|
|
442
|
-
if (index !== fromIndex) {
|
|
443
|
-
tokens.splice(index, 1);
|
|
444
|
-
tokens.splice(fromIndex, 0, token);
|
|
445
|
-
}
|
|
446
|
-
fromIndex++;
|
|
447
|
-
} while ((index = tokens.indexOf(token, fromIndex)) !== -1);
|
|
448
|
-
|
|
449
|
-
return this[key].sort(tokens, fromIndex);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return tokens;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
class TokenChain {
|
|
457
|
-
add(tokens) {
|
|
458
|
-
tokens.forEach((token) => {
|
|
459
|
-
const key = '$' + token;
|
|
460
|
-
if (!this[key]) {
|
|
461
|
-
this[key] = [];
|
|
462
|
-
this[key].processed = 0;
|
|
463
|
-
}
|
|
464
|
-
this[key].push(tokens);
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
createSorter() {
|
|
469
|
-
const sorter = new Sorter();
|
|
470
|
-
|
|
471
|
-
sorter.keys = Object.keys(this).sort((j, k) => {
|
|
472
|
-
const m = this[j].length;
|
|
473
|
-
const n = this[k].length;
|
|
474
|
-
return m < n ? 1 : m > n ? -1 : j < k ? -1 : j > k ? 1 : 0;
|
|
475
|
-
}).filter((key) => {
|
|
476
|
-
if (this[key].processed < this[key].length) {
|
|
477
|
-
const token = key.slice(1);
|
|
478
|
-
const chain = new TokenChain();
|
|
479
|
-
|
|
480
|
-
this[key].forEach((tokens) => {
|
|
481
|
-
let index;
|
|
482
|
-
while ((index = tokens.indexOf(token)) !== -1) {
|
|
483
|
-
tokens.splice(index, 1);
|
|
484
|
-
}
|
|
485
|
-
tokens.forEach((token) => {
|
|
486
|
-
this['$' + token].processed++;
|
|
487
|
-
});
|
|
488
|
-
chain.add(tokens.slice(0));
|
|
489
|
-
});
|
|
490
|
-
sorter[key] = chain.createSorter();
|
|
491
|
-
return true;
|
|
492
|
-
}
|
|
493
|
-
return false;
|
|
494
|
-
});
|
|
495
|
-
return sorter;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function trimWhitespace(str) {
|
|
500
|
-
return str && str.replace(/^[ \n\r\t\f]+/, '').replace(/[ \n\r\t\f]+$/, '');
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function collapseWhitespaceAll(str) {
|
|
504
|
-
// Non-breaking space is specifically handled inside the replacer function here:
|
|
505
|
-
return str && str.replace(/[ \n\r\t\f\xA0]+/g, function (spaces) {
|
|
506
|
-
return spaces === '\t' ? '\t' : spaces.replace(/(^|\xA0+)[^\xA0]+/g, '$1 ');
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
511
|
-
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
512
|
-
|
|
513
|
-
if (options.preserveLineBreaks) {
|
|
514
|
-
str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function () {
|
|
515
|
-
lineBreakBefore = '\n';
|
|
516
|
-
return '';
|
|
517
|
-
}).replace(/[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*$/, function () {
|
|
518
|
-
lineBreakAfter = '\n';
|
|
519
|
-
return '';
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (trimLeft) {
|
|
524
|
-
// Non-breaking space is specifically handled inside the replacer function here:
|
|
525
|
-
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
526
|
-
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
527
|
-
if (conservative && spaces === '\t') {
|
|
528
|
-
return '\t';
|
|
529
|
-
}
|
|
530
|
-
return spaces.replace(/^[^\xA0]+/, '').replace(/(\xA0+)[^\xA0]+/g, '$1 ') || (conservative ? ' ' : '');
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (trimRight) {
|
|
535
|
-
// Non-breaking space is specifically handled inside the replacer function here:
|
|
536
|
-
str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
|
|
537
|
-
const conservative = !lineBreakAfter && options.conservativeCollapse;
|
|
538
|
-
if (conservative && spaces === '\t') {
|
|
539
|
-
return '\t';
|
|
540
|
-
}
|
|
541
|
-
return spaces.replace(/[^\xA0]+(\xA0+)/g, ' $1').replace(/[^\xA0]+$/, '') || (conservative ? ' ' : '');
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (collapseAll) {
|
|
546
|
-
// strip non space whitespace then compress spaces to one
|
|
547
|
-
str = collapseWhitespaceAll(str);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return lineBreakBefore + str + lineBreakAfter;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// non-empty tags that will maintain whitespace around them
|
|
554
|
-
const inlineTags = new Set(['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']);
|
|
555
|
-
// non-empty tags that will maintain whitespace within them
|
|
556
|
-
const inlineTextTags = new Set(['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']);
|
|
557
|
-
// self-closing tags that will maintain whitespace around them
|
|
558
|
-
const selfClosingInlineTags = new Set(['comment', 'img', 'input', 'wbr']);
|
|
559
|
-
|
|
560
|
-
function collapseWhitespaceSmart(str, prevTag, nextTag, options) {
|
|
561
|
-
let trimLeft = prevTag && !selfClosingInlineTags.has(prevTag);
|
|
562
|
-
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
563
|
-
trimLeft = prevTag.charAt(0) === '/' ? !inlineTags.has(prevTag.slice(1)) : !inlineTextTags.has(prevTag);
|
|
564
|
-
}
|
|
565
|
-
let trimRight = nextTag && !selfClosingInlineTags.has(nextTag);
|
|
566
|
-
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
567
|
-
trimRight = nextTag.charAt(0) === '/' ? !inlineTextTags.has(nextTag.slice(1)) : !inlineTags.has(nextTag);
|
|
568
|
-
}
|
|
569
|
-
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function isConditionalComment(text) {
|
|
573
|
-
return /^\[if\s[^\]]+]|\[endif]$/.test(text);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function isIgnoredComment(text, options) {
|
|
577
|
-
for (let i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
|
|
578
|
-
if (options.ignoreCustomComments[i].test(text)) {
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
return false;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function isEventAttribute(attrName, options) {
|
|
586
|
-
const patterns = options.customEventAttributes;
|
|
587
|
-
if (patterns) {
|
|
588
|
-
for (let i = patterns.length; i--;) {
|
|
589
|
-
if (patterns[i].test(attrName)) {
|
|
590
|
-
return true;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
return /^on[a-z]{3,}$/.test(attrName);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function canRemoveAttributeQuotes(value) {
|
|
599
|
-
// https://mathiasbynens.be/notes/unquoted-attribute-values
|
|
600
|
-
return /^[^ \t\n\f\r"'`=<>]+$/.test(value);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function attributesInclude(attributes, attribute) {
|
|
604
|
-
for (let i = attributes.length; i--;) {
|
|
605
|
-
if (attributes[i].name.toLowerCase() === attribute) {
|
|
606
|
-
return true;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
return false;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
613
|
-
attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
|
|
614
|
-
|
|
615
|
-
return (
|
|
616
|
-
(tag === 'script' &&
|
|
617
|
-
attrName === 'language' &&
|
|
618
|
-
attrValue === 'javascript') ||
|
|
619
|
-
|
|
620
|
-
(tag === 'form' &&
|
|
621
|
-
attrName === 'method' &&
|
|
622
|
-
attrValue === 'get') ||
|
|
623
|
-
|
|
624
|
-
(tag === 'input' &&
|
|
625
|
-
attrName === 'type' &&
|
|
626
|
-
attrValue === 'text') ||
|
|
627
|
-
|
|
628
|
-
(tag === 'script' &&
|
|
629
|
-
attrName === 'charset' &&
|
|
630
|
-
!attributesInclude(attrs, 'src')) ||
|
|
631
|
-
|
|
632
|
-
(tag === 'a' &&
|
|
633
|
-
attrName === 'name' &&
|
|
634
|
-
attributesInclude(attrs, 'id')) ||
|
|
635
|
-
|
|
636
|
-
(tag === 'area' &&
|
|
637
|
-
attrName === 'shape' &&
|
|
638
|
-
attrValue === 'rect')
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// https://mathiasbynens.be/demo/javascript-mime-type
|
|
643
|
-
// https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
|
|
644
|
-
const executableScriptsMimetypes = new Set([
|
|
645
|
-
'text/javascript',
|
|
646
|
-
'text/ecmascript',
|
|
647
|
-
'text/jscript',
|
|
648
|
-
'application/javascript',
|
|
649
|
-
'application/x-javascript',
|
|
650
|
-
'application/ecmascript',
|
|
651
|
-
'module'
|
|
652
|
-
]);
|
|
653
|
-
|
|
654
|
-
const keepScriptsMimetypes = new Set([
|
|
655
|
-
'module'
|
|
656
|
-
]);
|
|
657
|
-
|
|
658
|
-
function isScriptTypeAttribute(attrValue = '') {
|
|
659
|
-
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
660
|
-
return attrValue === '' || executableScriptsMimetypes.has(attrValue);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function keepScriptTypeAttribute(attrValue = '') {
|
|
664
|
-
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
665
|
-
return keepScriptsMimetypes.has(attrValue);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function isExecutableScript(tag, attrs) {
|
|
669
|
-
if (tag !== 'script') {
|
|
670
|
-
return false;
|
|
671
|
-
}
|
|
672
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
673
|
-
const attrName = attrs[i].name.toLowerCase();
|
|
674
|
-
if (attrName === 'type') {
|
|
675
|
-
return isScriptTypeAttribute(attrs[i].value);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
return true;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function isStyleLinkTypeAttribute(attrValue = '') {
|
|
682
|
-
attrValue = trimWhitespace(attrValue).toLowerCase();
|
|
683
|
-
return attrValue === '' || attrValue === 'text/css';
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function isStyleSheet(tag, attrs) {
|
|
687
|
-
if (tag !== 'style') {
|
|
688
|
-
return false;
|
|
689
|
-
}
|
|
690
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
691
|
-
const attrName = attrs[i].name.toLowerCase();
|
|
692
|
-
if (attrName === 'type') {
|
|
693
|
-
return isStyleLinkTypeAttribute(attrs[i].value);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
return true;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const isSimpleBoolean = new Set(['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
|
|
700
|
-
const isBooleanValue = new Set(['true', 'false']);
|
|
701
|
-
|
|
702
|
-
function isBooleanAttribute(attrName, attrValue) {
|
|
703
|
-
return isSimpleBoolean.has(attrName) || (attrName === 'draggable' && !isBooleanValue.has(attrValue));
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function isUriTypeAttribute(attrName, tag) {
|
|
707
|
-
return (
|
|
708
|
-
(/^(?:a|area|link|base)$/.test(tag) && attrName === 'href') ||
|
|
709
|
-
(tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName)) ||
|
|
710
|
-
(tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName)) ||
|
|
711
|
-
(tag === 'q' && attrName === 'cite') ||
|
|
712
|
-
(tag === 'blockquote' && attrName === 'cite') ||
|
|
713
|
-
((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
|
|
714
|
-
(tag === 'form' && attrName === 'action') ||
|
|
715
|
-
(tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
|
|
716
|
-
(tag === 'head' && attrName === 'profile') ||
|
|
717
|
-
(tag === 'script' && (attrName === 'src' || attrName === 'for'))
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
function isNumberTypeAttribute(attrName, tag) {
|
|
722
|
-
return (
|
|
723
|
-
(/^(?:a|area|object|button)$/.test(tag) && attrName === 'tabindex') ||
|
|
724
|
-
(tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
|
|
725
|
-
(tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
|
|
726
|
-
(tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName)) ||
|
|
727
|
-
(tag === 'colgroup' && attrName === 'span') ||
|
|
728
|
-
(tag === 'col' && attrName === 'span') ||
|
|
729
|
-
((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function isLinkType(tag, attrs, value) {
|
|
734
|
-
if (tag !== 'link') {
|
|
735
|
-
return false;
|
|
736
|
-
}
|
|
737
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
738
|
-
if (attrs[i].name === 'rel' && attrs[i].value === value) {
|
|
739
|
-
return true;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function isMediaQuery(tag, attrs, attrName) {
|
|
745
|
-
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const srcsetTags = new Set(['img', 'source']);
|
|
749
|
-
|
|
750
|
-
function isSrcset(attrName, tag) {
|
|
751
|
-
return attrName === 'srcset' && srcsetTags.has(tag);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
async function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
|
|
755
|
-
if (isEventAttribute(attrName, options)) {
|
|
756
|
-
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
|
757
|
-
return options.minifyJS(attrValue, true);
|
|
758
|
-
} else if (attrName === 'class') {
|
|
759
|
-
attrValue = trimWhitespace(attrValue);
|
|
760
|
-
if (options.sortClassName) {
|
|
761
|
-
attrValue = options.sortClassName(attrValue);
|
|
762
|
-
} else {
|
|
763
|
-
attrValue = collapseWhitespaceAll(attrValue);
|
|
764
|
-
}
|
|
765
|
-
return attrValue;
|
|
766
|
-
} else if (isUriTypeAttribute(attrName, tag)) {
|
|
767
|
-
attrValue = trimWhitespace(attrValue);
|
|
768
|
-
return isLinkType(tag, attrs, 'canonical') ? attrValue : options.minifyURLs(attrValue);
|
|
769
|
-
} else if (isNumberTypeAttribute(attrName, tag)) {
|
|
770
|
-
return trimWhitespace(attrValue);
|
|
771
|
-
} else if (attrName === 'style') {
|
|
772
|
-
attrValue = trimWhitespace(attrValue);
|
|
773
|
-
if (attrValue) {
|
|
774
|
-
if (/;$/.test(attrValue) && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
775
|
-
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
776
|
-
}
|
|
777
|
-
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
778
|
-
}
|
|
779
|
-
return attrValue;
|
|
780
|
-
} else if (isSrcset(attrName, tag)) {
|
|
781
|
-
// https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
|
|
782
|
-
attrValue = trimWhitespace(attrValue).split(/\s+,\s*|\s*,\s+/).map(function (candidate) {
|
|
783
|
-
let url = candidate;
|
|
784
|
-
let descriptor = '';
|
|
785
|
-
const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
|
786
|
-
if (match) {
|
|
787
|
-
url = url.slice(0, -match[0].length);
|
|
788
|
-
const num = +match[1].slice(0, -1);
|
|
789
|
-
const suffix = match[1].slice(-1);
|
|
790
|
-
if (num !== 1 || suffix !== 'x') {
|
|
791
|
-
descriptor = ' ' + num + suffix;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
return options.minifyURLs(url) + descriptor;
|
|
795
|
-
}).join(', ');
|
|
796
|
-
} else if (isMetaViewport(tag, attrs) && attrName === 'content') {
|
|
797
|
-
attrValue = attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function (numString) {
|
|
798
|
-
// "0.90000" -> "0.9"
|
|
799
|
-
// "1.0" -> "1"
|
|
800
|
-
// "1.0001" -> "1.0001" (unchanged)
|
|
801
|
-
return (+numString).toString();
|
|
802
|
-
});
|
|
803
|
-
} else if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') {
|
|
804
|
-
return collapseWhitespaceAll(attrValue);
|
|
805
|
-
} else if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
|
|
806
|
-
attrValue = trimWhitespace(attrValue.replace(/ ?[\n\r]+ ?/g, '').replace(/\s{2,}/g, options.conservativeCollapse ? ' ' : ''));
|
|
807
|
-
} else if (tag === 'script' && attrName === 'type') {
|
|
808
|
-
attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
|
|
809
|
-
} else if (isMediaQuery(tag, attrs, attrName)) {
|
|
810
|
-
attrValue = trimWhitespace(attrValue);
|
|
811
|
-
return options.minifyCSS(attrValue, 'media');
|
|
812
|
-
}
|
|
813
|
-
return attrValue;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function isMetaViewport(tag, attrs) {
|
|
817
|
-
if (tag !== 'meta') {
|
|
818
|
-
return false;
|
|
819
|
-
}
|
|
820
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
821
|
-
if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
|
|
822
|
-
return true;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
function isContentSecurityPolicy(tag, attrs) {
|
|
828
|
-
if (tag !== 'meta') {
|
|
829
|
-
return false;
|
|
830
|
-
}
|
|
831
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
832
|
-
if (attrs[i].name.toLowerCase() === 'http-equiv' && attrs[i].value.toLowerCase() === 'content-security-policy') {
|
|
833
|
-
return true;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function ignoreCSS(id) {
|
|
839
|
-
return '/* clean-css ignore:start */' + id + '/* clean-css ignore:end */';
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// Wrap CSS declarations for CleanCSS > 3.x
|
|
843
|
-
// See https://github.com/jakubpawlowicz/clean-css/issues/418
|
|
844
|
-
function wrapCSS(text, type) {
|
|
845
|
-
switch (type) {
|
|
846
|
-
case 'inline':
|
|
847
|
-
return '*{' + text + '}';
|
|
848
|
-
case 'media':
|
|
849
|
-
return '@media ' + text + '{a{top:0}}';
|
|
850
|
-
default:
|
|
851
|
-
return text;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
function unwrapCSS(text, type) {
|
|
856
|
-
let matches;
|
|
857
|
-
switch (type) {
|
|
858
|
-
case 'inline':
|
|
859
|
-
matches = text.match(/^\*\{([\s\S]*)\}$/);
|
|
860
|
-
break;
|
|
861
|
-
case 'media':
|
|
862
|
-
matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
|
|
863
|
-
break;
|
|
864
|
-
}
|
|
865
|
-
return matches ? matches[1] : text;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
async function cleanConditionalComment(comment, options) {
|
|
869
|
-
return options.processConditionalComments
|
|
870
|
-
? await replaceAsync(comment, /^(\[if\s[^\]]+]>)([\s\S]*?)(<!\[endif])$/, async function (match, prefix, text, suffix) {
|
|
871
|
-
return prefix + await minifyHTML(text, options, true) + suffix;
|
|
872
|
-
})
|
|
873
|
-
: comment;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
async function processScript(text, options, currentAttrs) {
|
|
877
|
-
for (let i = 0, len = currentAttrs.length; i < len; i++) {
|
|
878
|
-
if (currentAttrs[i].name.toLowerCase() === 'type' &&
|
|
879
|
-
options.processScripts.indexOf(currentAttrs[i].value) > -1) {
|
|
880
|
-
return await minifyHTML(text, options);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
return text;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
|
|
887
|
-
// with the following deviations:
|
|
888
|
-
// - retain <body> if followed by <noscript>
|
|
889
|
-
// - </rb>, </rt>, </rtc>, </rp> & </tfoot> follow https://www.w3.org/TR/html5/syntax.html#optional-tags
|
|
890
|
-
// - retain all tags which are adjacent to non-standard HTML tags
|
|
891
|
-
const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody']);
|
|
892
|
-
const optionalEndTags = new Set(['html', 'head', 'body', 'li', 'dt', 'dd', 'p', 'rb', 'rt', 'rtc', 'rp', 'optgroup', 'option', 'colgroup', 'caption', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th']);
|
|
893
|
-
const headerTags = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
|
|
894
|
-
const descriptionTags = new Set(['dt', 'dd']);
|
|
895
|
-
const pBlockTags = new Set(['address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul']);
|
|
896
|
-
const pInlineTags = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
|
|
897
|
-
const rubyTags = new Set(['rb', 'rt', 'rtc', 'rp']);
|
|
898
|
-
const rtcTag = new Set(['rb', 'rtc', 'rp']);
|
|
899
|
-
const optionTag = new Set(['option', 'optgroup']);
|
|
900
|
-
const tableContentTags = new Set(['tbody', 'tfoot']);
|
|
901
|
-
const tableSectionTags = new Set(['thead', 'tbody', 'tfoot']);
|
|
902
|
-
const cellTags = new Set(['td', 'th']);
|
|
903
|
-
const topLevelTags = new Set(['html', 'head', 'body']);
|
|
904
|
-
const compactTags = new Set(['html', 'body']);
|
|
905
|
-
const looseTags = new Set(['head', 'colgroup', 'caption']);
|
|
906
|
-
const trailingTags = new Set(['dt', 'thead']);
|
|
907
|
-
const htmlTags = new Set(['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'bgsound', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'content', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'image', 'img', 'input', 'ins', 'isindex', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'listing', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'multicol', 'nav', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'plaintext', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr', 'xmp']);
|
|
908
|
-
|
|
909
|
-
function canRemoveParentTag(optionalStartTag, tag) {
|
|
910
|
-
switch (optionalStartTag) {
|
|
911
|
-
case 'html':
|
|
912
|
-
case 'head':
|
|
913
|
-
return true;
|
|
914
|
-
case 'body':
|
|
915
|
-
return !headerTags.has(tag);
|
|
916
|
-
case 'colgroup':
|
|
917
|
-
return tag === 'col';
|
|
918
|
-
case 'tbody':
|
|
919
|
-
return tag === 'tr';
|
|
920
|
-
}
|
|
921
|
-
return false;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
function isStartTagMandatory(optionalEndTag, tag) {
|
|
925
|
-
switch (tag) {
|
|
926
|
-
case 'colgroup':
|
|
927
|
-
return optionalEndTag === 'colgroup';
|
|
928
|
-
case 'tbody':
|
|
929
|
-
return tableSectionTags.has(optionalEndTag);
|
|
930
|
-
}
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
935
|
-
switch (optionalEndTag) {
|
|
936
|
-
case 'html':
|
|
937
|
-
case 'head':
|
|
938
|
-
case 'body':
|
|
939
|
-
case 'colgroup':
|
|
940
|
-
case 'caption':
|
|
941
|
-
return true;
|
|
942
|
-
case 'li':
|
|
943
|
-
case 'optgroup':
|
|
944
|
-
case 'tr':
|
|
945
|
-
return tag === optionalEndTag;
|
|
946
|
-
case 'dt':
|
|
947
|
-
case 'dd':
|
|
948
|
-
return descriptionTags.has(tag);
|
|
949
|
-
case 'p':
|
|
950
|
-
return pBlockTags.has(tag);
|
|
951
|
-
case 'rb':
|
|
952
|
-
case 'rt':
|
|
953
|
-
case 'rp':
|
|
954
|
-
return rubyTags.has(tag);
|
|
955
|
-
case 'rtc':
|
|
956
|
-
return rtcTag.has(tag);
|
|
957
|
-
case 'option':
|
|
958
|
-
return optionTag.has(tag);
|
|
959
|
-
case 'thead':
|
|
960
|
-
case 'tbody':
|
|
961
|
-
return tableContentTags.has(tag);
|
|
962
|
-
case 'tfoot':
|
|
963
|
-
return tag === 'tbody';
|
|
964
|
-
case 'td':
|
|
965
|
-
case 'th':
|
|
966
|
-
return cellTags.has(tag);
|
|
967
|
-
}
|
|
968
|
-
return false;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
const reEmptyAttribute = new RegExp(
|
|
972
|
-
'^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
|
|
973
|
-
'?:down|up|over|move|out)|key(?:press|down|up)))$');
|
|
974
|
-
|
|
975
|
-
function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
976
|
-
const isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
|
|
977
|
-
if (!isValueEmpty) {
|
|
978
|
-
return false;
|
|
979
|
-
}
|
|
980
|
-
if (typeof options.removeEmptyAttributes === 'function') {
|
|
981
|
-
return options.removeEmptyAttributes(attrName, tag);
|
|
982
|
-
}
|
|
983
|
-
return (tag === 'input' && attrName === 'value') || reEmptyAttribute.test(attrName);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function hasAttrName(name, attrs) {
|
|
987
|
-
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
988
|
-
if (attrs[i].name === name) {
|
|
989
|
-
return true;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
return false;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
function canRemoveElement(tag, attrs) {
|
|
996
|
-
switch (tag) {
|
|
997
|
-
case 'textarea':
|
|
998
|
-
return false;
|
|
999
|
-
case 'audio':
|
|
1000
|
-
case 'script':
|
|
1001
|
-
case 'video':
|
|
1002
|
-
if (hasAttrName('src', attrs)) {
|
|
1003
|
-
return false;
|
|
1004
|
-
}
|
|
1005
|
-
break;
|
|
1006
|
-
case 'iframe':
|
|
1007
|
-
if (hasAttrName('src', attrs) || hasAttrName('srcdoc', attrs)) {
|
|
1008
|
-
return false;
|
|
1009
|
-
}
|
|
1010
|
-
break;
|
|
1011
|
-
case 'object':
|
|
1012
|
-
if (hasAttrName('data', attrs)) {
|
|
1013
|
-
return false;
|
|
1014
|
-
}
|
|
1015
|
-
break;
|
|
1016
|
-
case 'applet':
|
|
1017
|
-
if (hasAttrName('code', attrs)) {
|
|
1018
|
-
return false;
|
|
1019
|
-
}
|
|
1020
|
-
break;
|
|
1021
|
-
}
|
|
1022
|
-
return true;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
function canCollapseWhitespace(tag) {
|
|
1026
|
-
return !/^(?:script|style|pre|textarea)$/.test(tag);
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function canTrimWhitespace(tag) {
|
|
1030
|
-
return !/^(?:pre|textarea)$/.test(tag);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
async function normalizeAttr(attr, attrs, tag, options) {
|
|
1034
|
-
const attrName = options.name(attr.name);
|
|
1035
|
-
let attrValue = attr.value;
|
|
1036
|
-
|
|
1037
|
-
if (options.decodeEntities && attrValue) {
|
|
1038
|
-
attrValue = entities.decodeHTMLStrict(attrValue);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
if ((options.removeRedundantAttributes &&
|
|
1042
|
-
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
1043
|
-
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
1044
|
-
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
1045
|
-
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
1046
|
-
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))) {
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
if (attrValue) {
|
|
1051
|
-
attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
if (options.removeEmptyAttributes &&
|
|
1055
|
-
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
if (options.decodeEntities && attrValue) {
|
|
1060
|
-
attrValue = attrValue.replace(/&(#?[0-9a-zA-Z]+;)/g, '&$1');
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
return {
|
|
1064
|
-
attr,
|
|
1065
|
-
name: attrName,
|
|
1066
|
-
value: attrValue
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
1071
|
-
const attrName = normalized.name;
|
|
1072
|
-
let attrValue = normalized.value;
|
|
1073
|
-
const attr = normalized.attr;
|
|
1074
|
-
let attrQuote = attr.quote;
|
|
1075
|
-
let attrFragment;
|
|
1076
|
-
let emittedAttrValue;
|
|
1077
|
-
|
|
1078
|
-
if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
|
|
1079
|
-
~attrValue.indexOf(uidAttr) || !canRemoveAttributeQuotes(attrValue))) {
|
|
1080
|
-
if (!options.preventAttributesEscaping) {
|
|
1081
|
-
if (typeof options.quoteCharacter === 'undefined') {
|
|
1082
|
-
const apos = (attrValue.match(/'/g) || []).length;
|
|
1083
|
-
const quot = (attrValue.match(/"/g) || []).length;
|
|
1084
|
-
attrQuote = apos < quot ? '\'' : '"';
|
|
1085
|
-
} else {
|
|
1086
|
-
attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
|
|
1087
|
-
}
|
|
1088
|
-
if (attrQuote === '"') {
|
|
1089
|
-
attrValue = attrValue.replace(/"/g, '"');
|
|
1090
|
-
} else {
|
|
1091
|
-
attrValue = attrValue.replace(/'/g, ''');
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
emittedAttrValue = attrQuote + attrValue + attrQuote;
|
|
1095
|
-
if (!isLast && !options.removeTagWhitespace) {
|
|
1096
|
-
emittedAttrValue += ' ';
|
|
1097
|
-
}
|
|
1098
|
-
} else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
|
|
1099
|
-
// make sure trailing slash is not interpreted as HTML self-closing tag
|
|
1100
|
-
emittedAttrValue = attrValue;
|
|
1101
|
-
} else {
|
|
1102
|
-
emittedAttrValue = attrValue + ' ';
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
|
|
1106
|
-
isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase()))) {
|
|
1107
|
-
attrFragment = attrName;
|
|
1108
|
-
if (!isLast) {
|
|
1109
|
-
attrFragment += ' ';
|
|
1110
|
-
}
|
|
1111
|
-
} else {
|
|
1112
|
-
attrFragment = attrName + attr.customAssign + emittedAttrValue;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
return attr.customOpen + attrFragment + attr.customClose;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
function identity(value) {
|
|
1119
|
-
return value;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
function identityAsync(value) {
|
|
1123
|
-
return Promise.resolve(value);
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
const processOptions = (inputOptions) => {
|
|
1127
|
-
const options = {
|
|
1128
|
-
name: function (name) {
|
|
1129
|
-
return name.toLowerCase();
|
|
1130
|
-
},
|
|
1131
|
-
canCollapseWhitespace,
|
|
1132
|
-
canTrimWhitespace,
|
|
1133
|
-
html5: true,
|
|
1134
|
-
ignoreCustomComments: [
|
|
1135
|
-
/^!/,
|
|
1136
|
-
/^\s*#/
|
|
1137
|
-
],
|
|
1138
|
-
ignoreCustomFragments: [
|
|
1139
|
-
/<%[\s\S]*?%>/,
|
|
1140
|
-
/<\?[\s\S]*?\?>/
|
|
1141
|
-
],
|
|
1142
|
-
includeAutoGeneratedTags: true,
|
|
1143
|
-
log: identity,
|
|
1144
|
-
minifyCSS: identityAsync,
|
|
1145
|
-
minifyJS: identity,
|
|
1146
|
-
minifyURLs: identity
|
|
1147
|
-
};
|
|
1148
|
-
|
|
1149
|
-
Object.keys(inputOptions).forEach(function (key) {
|
|
1150
|
-
const option = inputOptions[key];
|
|
1151
|
-
|
|
1152
|
-
if (key === 'caseSensitive') {
|
|
1153
|
-
if (option) {
|
|
1154
|
-
options.name = identity;
|
|
1155
|
-
}
|
|
1156
|
-
} else if (key === 'log') {
|
|
1157
|
-
if (typeof option === 'function') {
|
|
1158
|
-
options.log = option;
|
|
1159
|
-
}
|
|
1160
|
-
} else if (key === 'minifyCSS' && typeof option !== 'function') {
|
|
1161
|
-
if (!option) {
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
const cleanCssOptions = typeof option === 'object' ? option : {};
|
|
1166
|
-
|
|
1167
|
-
options.minifyCSS = async function (text, type) {
|
|
1168
|
-
text = text.replace(/(url\s*\(\s*)("|'|)(.*?)\2(\s*\))/ig, function (match, prefix, quote, url, suffix) {
|
|
1169
|
-
return prefix + quote + options.minifyURLs(url) + quote + suffix;
|
|
1170
|
-
});
|
|
1171
|
-
|
|
1172
|
-
const inputCSS = wrapCSS(text, type);
|
|
1173
|
-
|
|
1174
|
-
return new Promise((resolve) => {
|
|
1175
|
-
new CleanCSS(cleanCssOptions).minify(inputCSS, (_err, output) => {
|
|
1176
|
-
if (output.errors.length > 0) {
|
|
1177
|
-
output.errors.forEach(options.log);
|
|
1178
|
-
resolve(text);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const outputCSS = unwrapCSS(output.styles, type);
|
|
1182
|
-
resolve(outputCSS);
|
|
1183
|
-
});
|
|
1184
|
-
});
|
|
1185
|
-
};
|
|
1186
|
-
} else if (key === 'minifyJS' && typeof option !== 'function') {
|
|
1187
|
-
if (!option) {
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
const terserOptions = typeof option === 'object' ? option : {};
|
|
1192
|
-
|
|
1193
|
-
terserOptions.parse = {
|
|
1194
|
-
...terserOptions.parse,
|
|
1195
|
-
bare_returns: false
|
|
1196
|
-
};
|
|
1197
|
-
|
|
1198
|
-
options.minifyJS = async function (text, inline) {
|
|
1199
|
-
const start = text.match(/^\s*<!--.*/);
|
|
1200
|
-
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
1201
|
-
|
|
1202
|
-
terserOptions.parse.bare_returns = inline;
|
|
1203
|
-
|
|
1204
|
-
try {
|
|
1205
|
-
const result = await terser.minify(code, terserOptions);
|
|
1206
|
-
return result.code.replace(/;$/, '');
|
|
1207
|
-
} catch (error) {
|
|
1208
|
-
options.log(error);
|
|
1209
|
-
return text;
|
|
1210
|
-
}
|
|
1211
|
-
};
|
|
1212
|
-
} else if (key === 'minifyURLs' && typeof option !== 'function') {
|
|
1213
|
-
if (!option) {
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
let relateUrlOptions = option;
|
|
1218
|
-
|
|
1219
|
-
if (typeof option === 'string') {
|
|
1220
|
-
relateUrlOptions = { site: option };
|
|
1221
|
-
} else if (typeof option !== 'object') {
|
|
1222
|
-
relateUrlOptions = {};
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
options.minifyURLs = function (text) {
|
|
1226
|
-
try {
|
|
1227
|
-
return RelateURL.relate(text, relateUrlOptions);
|
|
1228
|
-
} catch (err) {
|
|
1229
|
-
options.log(err);
|
|
1230
|
-
return text;
|
|
1231
|
-
}
|
|
1232
|
-
};
|
|
1233
|
-
} else {
|
|
1234
|
-
options[key] = option;
|
|
1235
|
-
}
|
|
1236
|
-
});
|
|
1237
|
-
return options;
|
|
1238
|
-
};
|
|
1239
|
-
|
|
1240
|
-
function uniqueId(value) {
|
|
1241
|
-
let id;
|
|
1242
|
-
do {
|
|
1243
|
-
id = Math.random().toString(36).replace(/^0\.[0-9]*/, '');
|
|
1244
|
-
} while (~value.indexOf(id));
|
|
1245
|
-
return id;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
const specialContentTags = new Set(['script', 'style']);
|
|
1249
|
-
|
|
1250
|
-
async function createSortFns(value, options, uidIgnore, uidAttr) {
|
|
1251
|
-
const attrChains = options.sortAttributes && Object.create(null);
|
|
1252
|
-
const classChain = options.sortClassName && new TokenChain();
|
|
1253
|
-
|
|
1254
|
-
function attrNames(attrs) {
|
|
1255
|
-
return attrs.map(function (attr) {
|
|
1256
|
-
return options.name(attr.name);
|
|
1257
|
-
});
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
function shouldSkipUID(token, uid) {
|
|
1261
|
-
return !uid || token.indexOf(uid) === -1;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
function shouldSkipUIDs(token) {
|
|
1265
|
-
return shouldSkipUID(token, uidIgnore) && shouldSkipUID(token, uidAttr);
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
async function scan(input) {
|
|
1269
|
-
let currentTag, currentType;
|
|
1270
|
-
const parser = new HTMLParser(input, {
|
|
1271
|
-
start: function (tag, attrs) {
|
|
1272
|
-
if (attrChains) {
|
|
1273
|
-
if (!attrChains[tag]) {
|
|
1274
|
-
attrChains[tag] = new TokenChain();
|
|
1275
|
-
}
|
|
1276
|
-
attrChains[tag].add(attrNames(attrs).filter(shouldSkipUIDs));
|
|
1277
|
-
}
|
|
1278
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1279
|
-
const attr = attrs[i];
|
|
1280
|
-
if (classChain && attr.value && options.name(attr.name) === 'class') {
|
|
1281
|
-
classChain.add(trimWhitespace(attr.value).split(/[ \t\n\f\r]+/).filter(shouldSkipUIDs));
|
|
1282
|
-
} else if (options.processScripts && attr.name.toLowerCase() === 'type') {
|
|
1283
|
-
currentTag = tag;
|
|
1284
|
-
currentType = attr.value;
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
},
|
|
1288
|
-
end: function () {
|
|
1289
|
-
currentTag = '';
|
|
1290
|
-
},
|
|
1291
|
-
chars: async function (text) {
|
|
1292
|
-
if (options.processScripts && specialContentTags.has(currentTag) &&
|
|
1293
|
-
options.processScripts.indexOf(currentType) > -1) {
|
|
1294
|
-
await scan(text);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
});
|
|
1298
|
-
|
|
1299
|
-
await parser.parse();
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
const log = options.log;
|
|
1303
|
-
options.log = identity;
|
|
1304
|
-
options.sortAttributes = false;
|
|
1305
|
-
options.sortClassName = false;
|
|
1306
|
-
await scan(await minifyHTML(value, options));
|
|
1307
|
-
options.log = log;
|
|
1308
|
-
if (attrChains) {
|
|
1309
|
-
const attrSorters = Object.create(null);
|
|
1310
|
-
for (const tag in attrChains) {
|
|
1311
|
-
attrSorters[tag] = attrChains[tag].createSorter();
|
|
1312
|
-
}
|
|
1313
|
-
options.sortAttributes = function (tag, attrs) {
|
|
1314
|
-
const sorter = attrSorters[tag];
|
|
1315
|
-
if (sorter) {
|
|
1316
|
-
const attrMap = Object.create(null);
|
|
1317
|
-
const names = attrNames(attrs);
|
|
1318
|
-
names.forEach(function (name, index) {
|
|
1319
|
-
(attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
|
|
1320
|
-
});
|
|
1321
|
-
sorter.sort(names).forEach(function (name, index) {
|
|
1322
|
-
attrs[index] = attrMap[name].shift();
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
if (classChain) {
|
|
1328
|
-
const sorter = classChain.createSorter();
|
|
1329
|
-
options.sortClassName = function (value) {
|
|
1330
|
-
return sorter.sort(value.split(/[ \n\f\r]+/)).join(' ');
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
async function minifyHTML(value, options, partialMarkup) {
|
|
1336
|
-
if (options.collapseWhitespace) {
|
|
1337
|
-
value = collapseWhitespace(value, options, true, true);
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
const buffer = [];
|
|
1341
|
-
let charsPrevTag;
|
|
1342
|
-
let currentChars = '';
|
|
1343
|
-
let hasChars;
|
|
1344
|
-
let currentTag = '';
|
|
1345
|
-
let currentAttrs = [];
|
|
1346
|
-
const stackNoTrimWhitespace = [];
|
|
1347
|
-
const stackNoCollapseWhitespace = [];
|
|
1348
|
-
let optionalStartTag = '';
|
|
1349
|
-
let optionalEndTag = '';
|
|
1350
|
-
const ignoredMarkupChunks = [];
|
|
1351
|
-
const ignoredCustomMarkupChunks = [];
|
|
1352
|
-
let uidIgnore;
|
|
1353
|
-
let uidAttr;
|
|
1354
|
-
let uidPattern;
|
|
1355
|
-
|
|
1356
|
-
// temporarily replace ignored chunks with comments,
|
|
1357
|
-
// so that we don't have to worry what's there.
|
|
1358
|
-
// for all we care there might be
|
|
1359
|
-
// completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
|
|
1360
|
-
value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
|
|
1361
|
-
if (!uidIgnore) {
|
|
1362
|
-
uidIgnore = uniqueId(value);
|
|
1363
|
-
const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
|
|
1364
|
-
if (options.ignoreCustomComments) {
|
|
1365
|
-
options.ignoreCustomComments = options.ignoreCustomComments.slice();
|
|
1366
|
-
} else {
|
|
1367
|
-
options.ignoreCustomComments = [];
|
|
1368
|
-
}
|
|
1369
|
-
options.ignoreCustomComments.push(pattern);
|
|
1370
|
-
}
|
|
1371
|
-
const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
|
|
1372
|
-
ignoredMarkupChunks.push(group1);
|
|
1373
|
-
return token;
|
|
1374
|
-
});
|
|
1375
|
-
|
|
1376
|
-
const customFragments = options.ignoreCustomFragments.map(function (re) {
|
|
1377
|
-
return re.source;
|
|
1378
|
-
});
|
|
1379
|
-
if (customFragments.length) {
|
|
1380
|
-
const reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g');
|
|
1381
|
-
// temporarily replace custom ignored fragments with unique attributes
|
|
1382
|
-
value = value.replace(reCustomIgnore, function (match) {
|
|
1383
|
-
if (!uidAttr) {
|
|
1384
|
-
uidAttr = uniqueId(value);
|
|
1385
|
-
uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
|
|
1386
|
-
|
|
1387
|
-
if (options.minifyCSS) {
|
|
1388
|
-
options.minifyCSS = (function (fn) {
|
|
1389
|
-
return function (text, type) {
|
|
1390
|
-
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
1391
|
-
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1392
|
-
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
1393
|
-
});
|
|
1394
|
-
|
|
1395
|
-
const ids = [];
|
|
1396
|
-
new CleanCSS().minify(wrapCSS(text, type)).warnings.forEach(function (warning) {
|
|
1397
|
-
const match = uidPattern.exec(warning);
|
|
1398
|
-
if (match) {
|
|
1399
|
-
const id = uidAttr + match[2] + uidAttr;
|
|
1400
|
-
text = text.replace(id, ignoreCSS(id));
|
|
1401
|
-
ids.push(id);
|
|
1402
|
-
}
|
|
1403
|
-
});
|
|
1404
|
-
|
|
1405
|
-
return fn(text, type).then(chunk => {
|
|
1406
|
-
ids.forEach(function (id) {
|
|
1407
|
-
chunk = chunk.replace(ignoreCSS(id), id);
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
return chunk;
|
|
1411
|
-
});
|
|
1412
|
-
};
|
|
1413
|
-
})(options.minifyCSS);
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
if (options.minifyJS) {
|
|
1417
|
-
options.minifyJS = (function (fn) {
|
|
1418
|
-
return function (text, type) {
|
|
1419
|
-
return fn(text.replace(uidPattern, function (match, prefix, index) {
|
|
1420
|
-
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1421
|
-
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
1422
|
-
}), type);
|
|
1423
|
-
};
|
|
1424
|
-
})(options.minifyJS);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
|
|
1429
|
-
ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
|
|
1430
|
-
return '\t' + token + '\t';
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
|
|
1435
|
-
(options.sortClassName && typeof options.sortClassName !== 'function')) {
|
|
1436
|
-
await createSortFns(value, options, uidIgnore, uidAttr);
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
function _canCollapseWhitespace(tag, attrs) {
|
|
1440
|
-
return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
function _canTrimWhitespace(tag, attrs) {
|
|
1444
|
-
return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
function removeStartTag() {
|
|
1448
|
-
let index = buffer.length - 1;
|
|
1449
|
-
while (index > 0 && !/^<[^/!]/.test(buffer[index])) {
|
|
1450
|
-
index--;
|
|
1451
|
-
}
|
|
1452
|
-
buffer.length = Math.max(0, index);
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
function removeEndTag() {
|
|
1456
|
-
let index = buffer.length - 1;
|
|
1457
|
-
while (index > 0 && !/^<\//.test(buffer[index])) {
|
|
1458
|
-
index--;
|
|
1459
|
-
}
|
|
1460
|
-
buffer.length = Math.max(0, index);
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// look for trailing whitespaces, bypass any inline tags
|
|
1464
|
-
function trimTrailingWhitespace(index, nextTag) {
|
|
1465
|
-
for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
|
|
1466
|
-
const str = buffer[index];
|
|
1467
|
-
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
1468
|
-
if (match) {
|
|
1469
|
-
endTag = match[1];
|
|
1470
|
-
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options))) {
|
|
1471
|
-
break;
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// look for trailing whitespaces from previously processed text
|
|
1477
|
-
// which may not be trimmed due to a following comment or an empty
|
|
1478
|
-
// element which has now been removed
|
|
1479
|
-
function squashTrailingWhitespace(nextTag) {
|
|
1480
|
-
let charsIndex = buffer.length - 1;
|
|
1481
|
-
if (buffer.length > 1) {
|
|
1482
|
-
const item = buffer[buffer.length - 1];
|
|
1483
|
-
if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) {
|
|
1484
|
-
charsIndex--;
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
trimTrailingWhitespace(charsIndex, nextTag);
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
const parser = new HTMLParser(value, {
|
|
1491
|
-
partialMarkup,
|
|
1492
|
-
continueOnParseError: options.continueOnParseError,
|
|
1493
|
-
customAttrAssign: options.customAttrAssign,
|
|
1494
|
-
customAttrSurround: options.customAttrSurround,
|
|
1495
|
-
html5: options.html5,
|
|
1496
|
-
|
|
1497
|
-
start: async function (tag, attrs, unary, unarySlash, autoGenerated) {
|
|
1498
|
-
if (tag.toLowerCase() === 'svg') {
|
|
1499
|
-
options = Object.create(options);
|
|
1500
|
-
options.caseSensitive = true;
|
|
1501
|
-
options.keepClosingSlash = true;
|
|
1502
|
-
options.name = identity;
|
|
1503
|
-
}
|
|
1504
|
-
tag = options.name(tag);
|
|
1505
|
-
currentTag = tag;
|
|
1506
|
-
charsPrevTag = tag;
|
|
1507
|
-
if (!inlineTextTags.has(tag)) {
|
|
1508
|
-
currentChars = '';
|
|
1509
|
-
}
|
|
1510
|
-
hasChars = false;
|
|
1511
|
-
currentAttrs = attrs;
|
|
1512
|
-
|
|
1513
|
-
let optional = options.removeOptionalTags;
|
|
1514
|
-
if (optional) {
|
|
1515
|
-
const htmlTag = htmlTags.has(tag);
|
|
1516
|
-
// <html> may be omitted if first thing inside is not comment
|
|
1517
|
-
// <head> may be omitted if first thing inside is an element
|
|
1518
|
-
// <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
|
|
1519
|
-
// <colgroup> may be omitted if first thing inside is <col>
|
|
1520
|
-
// <tbody> may be omitted if first thing inside is <tr>
|
|
1521
|
-
if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
|
|
1522
|
-
removeStartTag();
|
|
1523
|
-
}
|
|
1524
|
-
optionalStartTag = '';
|
|
1525
|
-
// end-tag-followed-by-start-tag omission rules
|
|
1526
|
-
if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
|
|
1527
|
-
removeEndTag();
|
|
1528
|
-
// <colgroup> cannot be omitted if preceding </colgroup> is omitted
|
|
1529
|
-
// <tbody> cannot be omitted if preceding </tbody>, </thead> or </tfoot> is omitted
|
|
1530
|
-
optional = !isStartTagMandatory(optionalEndTag, tag);
|
|
1531
|
-
}
|
|
1532
|
-
optionalEndTag = '';
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
// set whitespace flags for nested tags (eg. <code> within a <pre>)
|
|
1536
|
-
if (options.collapseWhitespace) {
|
|
1537
|
-
if (!stackNoTrimWhitespace.length) {
|
|
1538
|
-
squashTrailingWhitespace(tag);
|
|
1539
|
-
}
|
|
1540
|
-
if (!unary) {
|
|
1541
|
-
if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
|
|
1542
|
-
stackNoTrimWhitespace.push(tag);
|
|
1543
|
-
}
|
|
1544
|
-
if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
|
|
1545
|
-
stackNoCollapseWhitespace.push(tag);
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
const openTag = '<' + tag;
|
|
1551
|
-
const hasUnarySlash = unarySlash && options.keepClosingSlash;
|
|
1552
|
-
|
|
1553
|
-
buffer.push(openTag);
|
|
1554
|
-
|
|
1555
|
-
if (options.sortAttributes) {
|
|
1556
|
-
options.sortAttributes(tag, attrs);
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
const parts = [];
|
|
1560
|
-
for (let i = attrs.length, isLast = true; --i >= 0;) {
|
|
1561
|
-
const normalized = await normalizeAttr(attrs[i], attrs, tag, options);
|
|
1562
|
-
if (normalized) {
|
|
1563
|
-
parts.unshift(buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr));
|
|
1564
|
-
isLast = false;
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
if (parts.length > 0) {
|
|
1568
|
-
buffer.push(' ');
|
|
1569
|
-
buffer.push.apply(buffer, parts);
|
|
1570
|
-
} else if (optional && optionalStartTags.has(tag)) {
|
|
1571
|
-
// start tag must never be omitted if it has any attributes
|
|
1572
|
-
optionalStartTag = tag;
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
buffer.push(buffer.pop() + (hasUnarySlash ? '/' : '') + '>');
|
|
1576
|
-
|
|
1577
|
-
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
1578
|
-
removeStartTag();
|
|
1579
|
-
optionalStartTag = '';
|
|
1580
|
-
}
|
|
1581
|
-
},
|
|
1582
|
-
end: function (tag, attrs, autoGenerated) {
|
|
1583
|
-
if (tag.toLowerCase() === 'svg') {
|
|
1584
|
-
options = Object.getPrototypeOf(options);
|
|
1585
|
-
}
|
|
1586
|
-
tag = options.name(tag);
|
|
1587
|
-
|
|
1588
|
-
// check if current tag is in a whitespace stack
|
|
1589
|
-
if (options.collapseWhitespace) {
|
|
1590
|
-
if (stackNoTrimWhitespace.length) {
|
|
1591
|
-
if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
|
1592
|
-
stackNoTrimWhitespace.pop();
|
|
1593
|
-
}
|
|
1594
|
-
} else {
|
|
1595
|
-
squashTrailingWhitespace('/' + tag);
|
|
1596
|
-
}
|
|
1597
|
-
if (stackNoCollapseWhitespace.length &&
|
|
1598
|
-
tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
|
1599
|
-
stackNoCollapseWhitespace.pop();
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
let isElementEmpty = false;
|
|
1604
|
-
if (tag === currentTag) {
|
|
1605
|
-
currentTag = '';
|
|
1606
|
-
isElementEmpty = !hasChars;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
if (options.removeOptionalTags) {
|
|
1610
|
-
// <html>, <head> or <body> may be omitted if the element is empty
|
|
1611
|
-
if (isElementEmpty && topLevelTags.has(optionalStartTag)) {
|
|
1612
|
-
removeStartTag();
|
|
1613
|
-
}
|
|
1614
|
-
optionalStartTag = '';
|
|
1615
|
-
// </html> or </body> may be omitted if not followed by comment
|
|
1616
|
-
// </head> may be omitted if not followed by space or comment
|
|
1617
|
-
// </p> may be omitted if no more content in non-</a> parent
|
|
1618
|
-
// except for </dt> or </thead>, end tags may be omitted if no more content in parent element
|
|
1619
|
-
if (htmlTags.has(tag) && optionalEndTag && !trailingTags.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags.has(tag))) {
|
|
1620
|
-
removeEndTag();
|
|
1621
|
-
}
|
|
1622
|
-
optionalEndTag = optionalEndTags.has(tag) ? tag : '';
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
|
|
1626
|
-
// remove last "element" from buffer
|
|
1627
|
-
removeStartTag();
|
|
1628
|
-
optionalStartTag = '';
|
|
1629
|
-
optionalEndTag = '';
|
|
1630
|
-
} else {
|
|
1631
|
-
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
1632
|
-
optionalEndTag = '';
|
|
1633
|
-
} else {
|
|
1634
|
-
buffer.push('</' + tag + '>');
|
|
1635
|
-
}
|
|
1636
|
-
charsPrevTag = '/' + tag;
|
|
1637
|
-
if (!inlineTags.has(tag)) {
|
|
1638
|
-
currentChars = '';
|
|
1639
|
-
} else if (isElementEmpty) {
|
|
1640
|
-
currentChars += '|';
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
},
|
|
1644
|
-
chars: async function (text, prevTag, nextTag) {
|
|
1645
|
-
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
1646
|
-
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
1647
|
-
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
1648
|
-
text = entities.decodeHTML(text);
|
|
1649
|
-
}
|
|
1650
|
-
if (options.collapseWhitespace) {
|
|
1651
|
-
if (!stackNoTrimWhitespace.length) {
|
|
1652
|
-
if (prevTag === 'comment') {
|
|
1653
|
-
const prevComment = buffer[buffer.length - 1];
|
|
1654
|
-
if (prevComment.indexOf(uidIgnore) === -1) {
|
|
1655
|
-
if (!prevComment) {
|
|
1656
|
-
prevTag = charsPrevTag;
|
|
1657
|
-
}
|
|
1658
|
-
if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
|
|
1659
|
-
const charsIndex = buffer.length - 2;
|
|
1660
|
-
buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
|
|
1661
|
-
text = trailingSpaces + text;
|
|
1662
|
-
return '';
|
|
1663
|
-
});
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
if (prevTag) {
|
|
1668
|
-
if (prevTag === '/nobr' || prevTag === 'wbr') {
|
|
1669
|
-
if (/^\s/.test(text)) {
|
|
1670
|
-
let tagIndex = buffer.length - 1;
|
|
1671
|
-
while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
|
|
1672
|
-
tagIndex--;
|
|
1673
|
-
}
|
|
1674
|
-
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
1675
|
-
}
|
|
1676
|
-
} else if (inlineTextTags.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
1677
|
-
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
if (prevTag || nextTag) {
|
|
1681
|
-
text = collapseWhitespaceSmart(text, prevTag, nextTag, options);
|
|
1682
|
-
} else {
|
|
1683
|
-
text = collapseWhitespace(text, options, true, true);
|
|
1684
|
-
}
|
|
1685
|
-
if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
|
|
1686
|
-
trimTrailingWhitespace(buffer.length - 1, nextTag);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
|
|
1690
|
-
text = collapseWhitespace(text, options, false, false, true);
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
if (options.processScripts && specialContentTags.has(currentTag)) {
|
|
1694
|
-
text = await processScript(text, options, currentAttrs);
|
|
1695
|
-
}
|
|
1696
|
-
if (isExecutableScript(currentTag, currentAttrs)) {
|
|
1697
|
-
text = await options.minifyJS(text);
|
|
1698
|
-
}
|
|
1699
|
-
if (isStyleSheet(currentTag, currentAttrs)) {
|
|
1700
|
-
text = await options.minifyCSS(text);
|
|
1701
|
-
}
|
|
1702
|
-
if (options.removeOptionalTags && text) {
|
|
1703
|
-
// <html> may be omitted if first thing inside is not comment
|
|
1704
|
-
// <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
|
|
1705
|
-
if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
|
|
1706
|
-
removeStartTag();
|
|
1707
|
-
}
|
|
1708
|
-
optionalStartTag = '';
|
|
1709
|
-
// </html> or </body> may be omitted if not followed by comment
|
|
1710
|
-
// </head>, </colgroup> or </caption> may be omitted if not followed by space or comment
|
|
1711
|
-
if (compactTags.has(optionalEndTag) || (looseTags.has(optionalEndTag) && !/^\s/.test(text))) {
|
|
1712
|
-
removeEndTag();
|
|
1713
|
-
}
|
|
1714
|
-
optionalEndTag = '';
|
|
1715
|
-
}
|
|
1716
|
-
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
1717
|
-
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
1718
|
-
// Escape any `&` symbols that start either:
|
|
1719
|
-
// 1) a legacy named character reference (i.e. one that doesn't end with `;`)
|
|
1720
|
-
// 2) or any other character reference (i.e. one that does end with `;`)
|
|
1721
|
-
// Note that `&` can be escaped as `&`, without the semi-colon.
|
|
1722
|
-
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
1723
|
-
text = text.replace(/&((?:Iacute|aacute|uacute|plusmn|Otilde|otilde|agrave|Agrave|Yacute|yacute|Oslash|oslash|atilde|Atilde|brvbar|ccedil|Ccedil|Ograve|curren|divide|eacute|Eacute|ograve|Oacute|egrave|Egrave|Ugrave|frac12|frac14|frac34|ugrave|oacute|iacute|Ntilde|ntilde|Uacute|middot|igrave|Igrave|iquest|Aacute|cedil|laquo|micro|iexcl|Icirc|icirc|acirc|Ucirc|Ecirc|ocirc|Ocirc|ecirc|ucirc|Aring|aring|AElig|aelig|acute|pound|raquo|Acirc|times|THORN|szlig|thorn|COPY|auml|ordf|ordm|Uuml|macr|uuml|Auml|ouml|Ouml|para|nbsp|euml|quot|QUOT|Euml|yuml|cent|sect|copy|sup1|sup2|sup3|iuml|Iuml|ETH|shy|reg|not|yen|amp|AMP|REG|uml|eth|deg|gt|GT|LT|lt)(?!;)|(?:#?[0-9a-zA-Z]+;))/g, '&$1').replace(/</g, '<');
|
|
1724
|
-
}
|
|
1725
|
-
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
1726
|
-
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
1727
|
-
return ignoredCustomMarkupChunks[+index][0];
|
|
1728
|
-
});
|
|
1729
|
-
}
|
|
1730
|
-
currentChars += text;
|
|
1731
|
-
if (text) {
|
|
1732
|
-
hasChars = true;
|
|
1733
|
-
}
|
|
1734
|
-
buffer.push(text);
|
|
1735
|
-
},
|
|
1736
|
-
comment: async function (text, nonStandard) {
|
|
1737
|
-
const prefix = nonStandard ? '<!' : '<!--';
|
|
1738
|
-
const suffix = nonStandard ? '>' : '-->';
|
|
1739
|
-
if (isConditionalComment(text)) {
|
|
1740
|
-
text = prefix + await cleanConditionalComment(text, options) + suffix;
|
|
1741
|
-
} else if (options.removeComments) {
|
|
1742
|
-
if (isIgnoredComment(text, options)) {
|
|
1743
|
-
text = '<!--' + text + '-->';
|
|
1744
|
-
} else {
|
|
1745
|
-
text = '';
|
|
1746
|
-
}
|
|
1747
|
-
} else {
|
|
1748
|
-
text = prefix + text + suffix;
|
|
1749
|
-
}
|
|
1750
|
-
if (options.removeOptionalTags && text) {
|
|
1751
|
-
// preceding comments suppress tag omissions
|
|
1752
|
-
optionalStartTag = '';
|
|
1753
|
-
optionalEndTag = '';
|
|
1754
|
-
}
|
|
1755
|
-
buffer.push(text);
|
|
1756
|
-
},
|
|
1757
|
-
doctype: function (doctype) {
|
|
1758
|
-
buffer.push(options.useShortDoctype
|
|
1759
|
-
? '<!doctype' +
|
|
1760
|
-
(options.removeTagWhitespace ? '' : ' ') + 'html>'
|
|
1761
|
-
: collapseWhitespaceAll(doctype));
|
|
1762
|
-
}
|
|
1763
|
-
});
|
|
1764
|
-
|
|
1765
|
-
await parser.parse();
|
|
1766
|
-
|
|
1767
|
-
if (options.removeOptionalTags) {
|
|
1768
|
-
// <html> may be omitted if first thing inside is not comment
|
|
1769
|
-
// <head> or <body> may be omitted if empty
|
|
1770
|
-
if (topLevelTags.has(optionalStartTag)) {
|
|
1771
|
-
removeStartTag();
|
|
1772
|
-
}
|
|
1773
|
-
// except for </dt> or </thead>, end tags may be omitted if no more content in parent element
|
|
1774
|
-
if (optionalEndTag && !trailingTags.has(optionalEndTag)) {
|
|
1775
|
-
removeEndTag();
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
if (options.collapseWhitespace) {
|
|
1779
|
-
squashTrailingWhitespace('br');
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
return joinResultSegments(buffer, options, uidPattern
|
|
1783
|
-
? function (str) {
|
|
1784
|
-
return str.replace(uidPattern, function (match, prefix, index, suffix) {
|
|
1785
|
-
let chunk = ignoredCustomMarkupChunks[+index][0];
|
|
1786
|
-
if (options.collapseWhitespace) {
|
|
1787
|
-
if (prefix !== '\t') {
|
|
1788
|
-
chunk = prefix + chunk;
|
|
1789
|
-
}
|
|
1790
|
-
if (suffix !== '\t') {
|
|
1791
|
-
chunk += suffix;
|
|
1792
|
-
}
|
|
1793
|
-
return collapseWhitespace(chunk, {
|
|
1794
|
-
preserveLineBreaks: options.preserveLineBreaks,
|
|
1795
|
-
conservativeCollapse: !options.trimCustomFragments
|
|
1796
|
-
}, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk));
|
|
1797
|
-
}
|
|
1798
|
-
return chunk;
|
|
1799
|
-
});
|
|
1800
|
-
}
|
|
1801
|
-
: identity, uidIgnore
|
|
1802
|
-
? function (str) {
|
|
1803
|
-
return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function (match, index) {
|
|
1804
|
-
return ignoredMarkupChunks[+index];
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
: identity);
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
1811
|
-
let str;
|
|
1812
|
-
const maxLineLength = options.maxLineLength;
|
|
1813
|
-
const noNewlinesBeforeTagClose = options.noNewlinesBeforeTagClose;
|
|
1814
|
-
|
|
1815
|
-
if (maxLineLength) {
|
|
1816
|
-
let line = ''; const lines = [];
|
|
1817
|
-
while (results.length) {
|
|
1818
|
-
const len = line.length;
|
|
1819
|
-
const end = results[0].indexOf('\n');
|
|
1820
|
-
const isClosingTag = Boolean(results[0].match(endTag));
|
|
1821
|
-
const shouldKeepSameLine = noNewlinesBeforeTagClose && isClosingTag;
|
|
1822
|
-
|
|
1823
|
-
if (end < 0) {
|
|
1824
|
-
line += restoreIgnore(restoreCustom(results.shift()));
|
|
1825
|
-
} else {
|
|
1826
|
-
line += restoreIgnore(restoreCustom(results[0].slice(0, end)));
|
|
1827
|
-
results[0] = results[0].slice(end + 1);
|
|
1828
|
-
}
|
|
1829
|
-
if (len > 0 && line.length > maxLineLength && !shouldKeepSameLine) {
|
|
1830
|
-
lines.push(line.slice(0, len));
|
|
1831
|
-
line = line.slice(len);
|
|
1832
|
-
} else if (end >= 0) {
|
|
1833
|
-
lines.push(line);
|
|
1834
|
-
line = '';
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
if (line) {
|
|
1838
|
-
lines.push(line);
|
|
1839
|
-
}
|
|
1840
|
-
str = lines.join('\n');
|
|
1841
|
-
} else {
|
|
1842
|
-
str = restoreIgnore(restoreCustom(results.join('')));
|
|
1843
|
-
}
|
|
1844
|
-
return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str;
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
const minify = async function (value, options) {
|
|
1848
|
-
const start = Date.now();
|
|
1849
|
-
options = processOptions(options || {});
|
|
1850
|
-
const result = await minifyHTML(value, options);
|
|
1851
|
-
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
|
1852
|
-
return result;
|
|
1853
|
-
};
|
|
1854
|
-
|
|
1855
|
-
var htmlminifier = { minify };
|
|
1856
|
-
|
|
1857
|
-
exports.default = htmlminifier;
|
|
1858
|
-
exports.minify = minify;
|