html-minifier-next 4.12.1 → 4.13.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 +23 -21
- package/dist/htmlminifier.cjs +1477 -1310
- package/dist/htmlminifier.esm.bundle.js +4136 -3969
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +29 -0
- package/dist/types/lib/attributes.d.ts.map +1 -0
- package/dist/types/lib/constants.d.ts +83 -0
- package/dist/types/lib/constants.d.ts.map +1 -0
- package/dist/types/lib/content.d.ts +7 -0
- package/dist/types/lib/content.d.ts.map +1 -0
- package/dist/types/lib/elements.d.ts +39 -0
- package/dist/types/lib/elements.d.ts.map +1 -0
- package/dist/types/lib/options.d.ts +17 -0
- package/dist/types/lib/options.d.ts.map +1 -0
- package/dist/types/lib/utils.d.ts +21 -0
- package/dist/types/lib/utils.d.ts.map +1 -0
- package/dist/types/lib/whitespace.d.ts +7 -0
- package/dist/types/lib/whitespace.d.ts.map +1 -0
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +91 -1223
- package/src/htmlparser.js +11 -11
- package/src/lib/attributes.js +511 -0
- package/src/lib/constants.js +213 -0
- package/src/lib/content.js +105 -0
- package/src/lib/elements.js +242 -0
- package/src/lib/index.js +20 -0
- package/src/lib/options.js +252 -0
- package/src/lib/utils.js +90 -0
- package/src/lib/whitespace.js +139 -0
- package/src/presets.js +0 -1
- package/src/tokenchain.js +1 -1
- package/dist/types/utils.d.ts +0 -2
- package/dist/types/utils.d.ts.map +0 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Imports
|
|
2
|
+
|
|
3
|
+
import RelateURL from 'relateurl';
|
|
4
|
+
import { stableStringify, identity, identityAsync, replaceAsync } from './utils.js';
|
|
5
|
+
import { RE_TRAILING_SEMICOLON } from './constants.js';
|
|
6
|
+
import { canCollapseWhitespace, canTrimWhitespace } from './whitespace.js';
|
|
7
|
+
import { wrapCSS, unwrapCSS } from './content.js';
|
|
8
|
+
|
|
9
|
+
// Helper functions
|
|
10
|
+
|
|
11
|
+
function shouldMinifyInnerHTML(options) {
|
|
12
|
+
return Boolean(
|
|
13
|
+
options.collapseWhitespace ||
|
|
14
|
+
options.removeComments ||
|
|
15
|
+
options.removeOptionalTags ||
|
|
16
|
+
options.minifyJS !== identity ||
|
|
17
|
+
options.minifyCSS !== identityAsync ||
|
|
18
|
+
options.minifyURLs !== identity
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Main options processor
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Partial<MinifierOptions>} inputOptions - User-provided options
|
|
26
|
+
* @param {Object} deps - Dependencies from htmlminifier.js
|
|
27
|
+
* @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
|
|
28
|
+
* @param {Function} deps.getTerser - Function to lazily load terser
|
|
29
|
+
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
30
|
+
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
31
|
+
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
32
|
+
*/
|
|
33
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
34
|
+
const options = {
|
|
35
|
+
name: function (name) {
|
|
36
|
+
return name.toLowerCase();
|
|
37
|
+
},
|
|
38
|
+
canCollapseWhitespace,
|
|
39
|
+
canTrimWhitespace,
|
|
40
|
+
continueOnMinifyError: true,
|
|
41
|
+
html5: true,
|
|
42
|
+
ignoreCustomComments: [
|
|
43
|
+
/^!/,
|
|
44
|
+
/^\s*#/
|
|
45
|
+
],
|
|
46
|
+
ignoreCustomFragments: [
|
|
47
|
+
/<%[\s\S]*?%>/,
|
|
48
|
+
/<\?[\s\S]*?\?>/
|
|
49
|
+
],
|
|
50
|
+
includeAutoGeneratedTags: true,
|
|
51
|
+
log: identity,
|
|
52
|
+
minifyCSS: identityAsync,
|
|
53
|
+
minifyJS: identity,
|
|
54
|
+
minifyURLs: identity
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
Object.keys(inputOptions).forEach(function (key) {
|
|
58
|
+
const option = inputOptions[key];
|
|
59
|
+
|
|
60
|
+
if (key === 'caseSensitive') {
|
|
61
|
+
if (option) {
|
|
62
|
+
options.name = identity;
|
|
63
|
+
}
|
|
64
|
+
} else if (key === 'log') {
|
|
65
|
+
if (typeof option === 'function') {
|
|
66
|
+
options.log = option;
|
|
67
|
+
}
|
|
68
|
+
} else if (key === 'minifyCSS' && typeof option !== 'function') {
|
|
69
|
+
if (!option) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
74
|
+
|
|
75
|
+
options.minifyCSS = async function (text, type) {
|
|
76
|
+
// Fast path: Nothing to minify
|
|
77
|
+
if (!text || !text.trim()) {
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
text = await replaceAsync(
|
|
81
|
+
text,
|
|
82
|
+
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
83
|
+
async function (match, prefix, dq, sq, unq, suffix) {
|
|
84
|
+
const quote = dq != null ? '"' : (sq != null ? "'" : '');
|
|
85
|
+
const url = dq ?? sq ?? unq ?? '';
|
|
86
|
+
try {
|
|
87
|
+
const out = await options.minifyURLs(url);
|
|
88
|
+
return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (!options.continueOnMinifyError) {
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
options.log && options.log(err);
|
|
94
|
+
return match;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
// Cache key: Wrapped content, type, options signature
|
|
99
|
+
const inputCSS = wrapCSS(text, type);
|
|
100
|
+
const cssSig = stableStringify({ type, opts: lightningCssOptions, cont: !!options.continueOnMinifyError });
|
|
101
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
102
|
+
const cssKey = inputCSS.length > 2048
|
|
103
|
+
? (inputCSS.length + '|' + inputCSS.slice(0, 50) + inputCSS.slice(-50) + '|' + type + '|' + cssSig)
|
|
104
|
+
: (inputCSS + '|' + type + '|' + cssSig);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const cached = cssMinifyCache.get(cssKey);
|
|
108
|
+
if (cached) {
|
|
109
|
+
return cached;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const transformCSS = await getLightningCSS();
|
|
113
|
+
const result = transformCSS({
|
|
114
|
+
filename: 'input.css',
|
|
115
|
+
code: Buffer.from(inputCSS),
|
|
116
|
+
minify: true,
|
|
117
|
+
errorRecovery: !!options.continueOnMinifyError,
|
|
118
|
+
...lightningCssOptions
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const outputCSS = unwrapCSS(result.code.toString(), type);
|
|
122
|
+
|
|
123
|
+
// If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
|
|
124
|
+
// This preserves:
|
|
125
|
+
// 1. Template code like `<?php ?>`, `<%= %>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
|
|
126
|
+
// 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
|
|
127
|
+
// CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
|
|
128
|
+
const isCDATA = text.includes('<![CDATA[');
|
|
129
|
+
const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
|
|
130
|
+
const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
|
|
131
|
+
const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
|
|
132
|
+
|
|
133
|
+
// Preserve if output is empty and input had template syntax or UIDs
|
|
134
|
+
// This catches cases where Lightning CSS removed content that should be preserved
|
|
135
|
+
const finalOutput = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
136
|
+
|
|
137
|
+
cssMinifyCache.set(cssKey, finalOutput);
|
|
138
|
+
return finalOutput;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
cssMinifyCache.delete(cssKey);
|
|
141
|
+
if (!options.continueOnMinifyError) {
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
options.log && options.log(err);
|
|
145
|
+
return text;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
} else if (key === 'minifyJS' && typeof option !== 'function') {
|
|
149
|
+
if (!option) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const terserOptions = typeof option === 'object' ? option : {};
|
|
154
|
+
|
|
155
|
+
terserOptions.parse = {
|
|
156
|
+
...terserOptions.parse,
|
|
157
|
+
bare_returns: false
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
options.minifyJS = async function (text, inline) {
|
|
161
|
+
const start = text.match(/^\s*<!--.*/);
|
|
162
|
+
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
163
|
+
|
|
164
|
+
terserOptions.parse.bare_returns = inline;
|
|
165
|
+
|
|
166
|
+
let jsKey;
|
|
167
|
+
try {
|
|
168
|
+
// Fast path: Avoid invoking Terser for empty/whitespace-only content
|
|
169
|
+
if (!code || !code.trim()) {
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
// Cache key: content, inline, options signature (subset)
|
|
173
|
+
const terserSig = stableStringify({
|
|
174
|
+
compress: terserOptions.compress,
|
|
175
|
+
mangle: terserOptions.mangle,
|
|
176
|
+
ecma: terserOptions.ecma,
|
|
177
|
+
toplevel: terserOptions.toplevel,
|
|
178
|
+
module: terserOptions.module,
|
|
179
|
+
keep_fnames: terserOptions.keep_fnames,
|
|
180
|
+
format: terserOptions.format,
|
|
181
|
+
cont: !!options.continueOnMinifyError,
|
|
182
|
+
});
|
|
183
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
184
|
+
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|')) + (inline ? '1' : '0') + '|' + terserSig;
|
|
185
|
+
const cached = jsMinifyCache.get(jsKey);
|
|
186
|
+
if (cached) {
|
|
187
|
+
return await cached;
|
|
188
|
+
}
|
|
189
|
+
const inFlight = (async () => {
|
|
190
|
+
const terser = await getTerser();
|
|
191
|
+
const result = await terser(code, terserOptions);
|
|
192
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
193
|
+
})();
|
|
194
|
+
jsMinifyCache.set(jsKey, inFlight);
|
|
195
|
+
const resolved = await inFlight;
|
|
196
|
+
jsMinifyCache.set(jsKey, resolved);
|
|
197
|
+
return resolved;
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (jsKey) jsMinifyCache.delete(jsKey);
|
|
200
|
+
if (!options.continueOnMinifyError) {
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
options.log && options.log(err);
|
|
204
|
+
return text;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
} else if (key === 'minifyURLs' && typeof option !== 'function') {
|
|
208
|
+
if (!option) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let relateUrlOptions = option;
|
|
213
|
+
|
|
214
|
+
if (typeof option === 'string') {
|
|
215
|
+
relateUrlOptions = { site: option };
|
|
216
|
+
} else if (typeof option !== 'object') {
|
|
217
|
+
relateUrlOptions = {};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Cache RelateURL instance for reuse (expensive to create)
|
|
221
|
+
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
222
|
+
|
|
223
|
+
options.minifyURLs = function (text) {
|
|
224
|
+
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
225
|
+
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
226
|
+
if (!/[/:?#\s]/.test(text)) {
|
|
227
|
+
return text;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
return relateUrlInstance.relate(text);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
if (!options.continueOnMinifyError) {
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
options.log && options.log(err);
|
|
237
|
+
return text;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
} else {
|
|
241
|
+
options[key] = option;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
return options;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Exports
|
|
248
|
+
|
|
249
|
+
export {
|
|
250
|
+
shouldMinifyInnerHTML,
|
|
251
|
+
processOptions
|
|
252
|
+
};
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Stringify for options signatures (sorted keys, shallow, nested objects)
|
|
2
|
+
|
|
3
|
+
function stableStringify(obj) {
|
|
4
|
+
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
5
|
+
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
6
|
+
const keys = Object.keys(obj).sort();
|
|
7
|
+
let out = '{';
|
|
8
|
+
for (let i = 0; i < keys.length; i++) {
|
|
9
|
+
const k = keys[i];
|
|
10
|
+
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
11
|
+
}
|
|
12
|
+
return out + '}';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// LRU cache for strings and promises
|
|
16
|
+
|
|
17
|
+
class LRU {
|
|
18
|
+
constructor(limit = 200) {
|
|
19
|
+
this.limit = limit;
|
|
20
|
+
this.map = new Map();
|
|
21
|
+
}
|
|
22
|
+
get(key) {
|
|
23
|
+
if (this.map.has(key)) {
|
|
24
|
+
const v = this.map.get(key);
|
|
25
|
+
this.map.delete(key);
|
|
26
|
+
this.map.set(key, v);
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
set(key, value) {
|
|
32
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
33
|
+
this.map.set(key, value);
|
|
34
|
+
if (this.map.size > this.limit) {
|
|
35
|
+
const first = this.map.keys().next().value;
|
|
36
|
+
this.map.delete(first);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
delete(key) { this.map.delete(key); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Unique ID generator
|
|
43
|
+
|
|
44
|
+
function uniqueId(value) {
|
|
45
|
+
let id;
|
|
46
|
+
do {
|
|
47
|
+
id = Math.random().toString(36).replace(/^0\.[0-9]*/, '');
|
|
48
|
+
} while (~value.indexOf(id));
|
|
49
|
+
return id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Identity functions
|
|
53
|
+
|
|
54
|
+
function identity(value) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function identityAsync(value) {
|
|
59
|
+
return Promise.resolve(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Replace async helper
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Asynchronously replace matches in a string
|
|
66
|
+
* @param {string} str - Input string
|
|
67
|
+
* @param {RegExp} regex - Regular expression with global flag
|
|
68
|
+
* @param {Function} asyncFn - Async function to process each match
|
|
69
|
+
* @returns {Promise<string>} Processed string
|
|
70
|
+
*/
|
|
71
|
+
async function replaceAsync(str, regex, asyncFn) {
|
|
72
|
+
const promises = [];
|
|
73
|
+
|
|
74
|
+
str.replace(regex, (match, ...args) => {
|
|
75
|
+
const promise = asyncFn(match, ...args);
|
|
76
|
+
promises.push(promise);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const data = await Promise.all(promises);
|
|
80
|
+
return str.replace(regex, () => data.shift());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Exports
|
|
84
|
+
|
|
85
|
+
export { stableStringify };
|
|
86
|
+
export { LRU };
|
|
87
|
+
export { uniqueId };
|
|
88
|
+
export { identity };
|
|
89
|
+
export { identityAsync };
|
|
90
|
+
export { replaceAsync };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Imports
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
RE_WS_START,
|
|
5
|
+
RE_WS_END,
|
|
6
|
+
RE_ALL_WS_NBSP,
|
|
7
|
+
RE_NBSP_LEADING_GROUP,
|
|
8
|
+
RE_NBSP_LEAD_GROUP,
|
|
9
|
+
RE_NBSP_TRAILING_GROUP,
|
|
10
|
+
RE_NBSP_TRAILING_STRIP,
|
|
11
|
+
inlineElementsToKeepWhitespace
|
|
12
|
+
} from './constants.js';
|
|
13
|
+
|
|
14
|
+
// Trim whitespace
|
|
15
|
+
|
|
16
|
+
const trimWhitespace = str => {
|
|
17
|
+
if (!str) return str;
|
|
18
|
+
// Fast path: If no whitespace at start or end, return early
|
|
19
|
+
if (!/^[ \n\r\t\f]/.test(str) && !/[ \n\r\t\f]$/.test(str)) {
|
|
20
|
+
return str;
|
|
21
|
+
}
|
|
22
|
+
return str.replace(RE_WS_START, '').replace(RE_WS_END, '');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Collapse all whitespace
|
|
26
|
+
|
|
27
|
+
function collapseWhitespaceAll(str) {
|
|
28
|
+
if (!str) return str;
|
|
29
|
+
// Fast path: If there are no common whitespace characters, return early
|
|
30
|
+
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
31
|
+
return str;
|
|
32
|
+
}
|
|
33
|
+
// No-break space is specifically handled inside the replacer function here:
|
|
34
|
+
return str.replace(RE_ALL_WS_NBSP, function (spaces) {
|
|
35
|
+
// Preserve standalone tabs
|
|
36
|
+
if (spaces === '\t') return '\t';
|
|
37
|
+
// Fast path: No no-break space, common case—just collapse to single space
|
|
38
|
+
// This avoids the nested regex for the majority of cases
|
|
39
|
+
if (spaces.indexOf('\xA0') === -1) return ' ';
|
|
40
|
+
// For no-break space handling, use the original regex approach
|
|
41
|
+
return spaces.replace(RE_NBSP_LEADING_GROUP, '$1 ');
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Collapse whitespace with options
|
|
46
|
+
|
|
47
|
+
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
48
|
+
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
49
|
+
|
|
50
|
+
if (!str) return str;
|
|
51
|
+
|
|
52
|
+
// Fast path: Nothing to do
|
|
53
|
+
if (!trimLeft && !trimRight && !collapseAll && !options.preserveLineBreaks) {
|
|
54
|
+
return str;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fast path: No whitespace at all
|
|
58
|
+
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
59
|
+
return str;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (options.preserveLineBreaks) {
|
|
63
|
+
str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function () {
|
|
64
|
+
lineBreakBefore = '\n';
|
|
65
|
+
return '';
|
|
66
|
+
}).replace(/[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*$/, function () {
|
|
67
|
+
lineBreakAfter = '\n';
|
|
68
|
+
return '';
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (trimLeft) {
|
|
73
|
+
// Non-breaking space is specifically handled inside the replacer function
|
|
74
|
+
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
75
|
+
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
76
|
+
if (conservative && spaces === '\t') {
|
|
77
|
+
return '\t';
|
|
78
|
+
}
|
|
79
|
+
return spaces.replace(/^[^\xA0]+/, '').replace(RE_NBSP_LEAD_GROUP, '$1 ') || (conservative ? ' ' : '');
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (trimRight) {
|
|
84
|
+
// Non-breaking space is specifically handled inside the replacer function
|
|
85
|
+
str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
|
|
86
|
+
const conservative = !lineBreakAfter && options.conservativeCollapse;
|
|
87
|
+
if (conservative && spaces === '\t') {
|
|
88
|
+
return '\t';
|
|
89
|
+
}
|
|
90
|
+
return spaces.replace(RE_NBSP_TRAILING_GROUP, ' $1').replace(RE_NBSP_TRAILING_STRIP, '') || (conservative ? ' ' : '');
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (collapseAll) {
|
|
95
|
+
// Strip non-space whitespace then compress spaces to one
|
|
96
|
+
str = collapseWhitespaceAll(str);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Avoid string concatenation when no line breaks (common case)
|
|
100
|
+
if (!lineBreakBefore && !lineBreakAfter) return str;
|
|
101
|
+
if (!lineBreakBefore) return str + lineBreakAfter;
|
|
102
|
+
if (!lineBreakAfter) return lineBreakBefore + str;
|
|
103
|
+
return lineBreakBefore + str + lineBreakAfter;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Collapse whitespace smartly based on surrounding tags
|
|
107
|
+
|
|
108
|
+
function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
|
|
109
|
+
let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
110
|
+
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
111
|
+
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
112
|
+
}
|
|
113
|
+
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
114
|
+
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
115
|
+
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
116
|
+
}
|
|
117
|
+
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Collapse/trim whitespace for given tag
|
|
121
|
+
|
|
122
|
+
function canCollapseWhitespace(tag) {
|
|
123
|
+
return !/^(?:script|style|pre|textarea)$/.test(tag);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function canTrimWhitespace(tag) {
|
|
127
|
+
return !/^(?:pre|textarea)$/.test(tag);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Exports
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
trimWhitespace,
|
|
134
|
+
collapseWhitespaceAll,
|
|
135
|
+
collapseWhitespace,
|
|
136
|
+
collapseWhitespaceSmart,
|
|
137
|
+
canCollapseWhitespace,
|
|
138
|
+
canTrimWhitespace
|
|
139
|
+
};
|
package/src/presets.js
CHANGED
package/src/tokenchain.js
CHANGED
|
@@ -60,7 +60,7 @@ class TokenChain {
|
|
|
60
60
|
const sorter = new Sorter();
|
|
61
61
|
sorter.sorterMap = new Map();
|
|
62
62
|
|
|
63
|
-
// Convert Map entries to array and sort
|
|
63
|
+
// Convert Map entries to array and sort by frequency (descending) then alphabetically
|
|
64
64
|
const entries = Array.from(this.map.entries()).sort((a, b) => {
|
|
65
65
|
const m = a[1].arrays.length;
|
|
66
66
|
const n = b[1].arrays.length;
|
package/dist/types/utils.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.js"],"names":[],"mappings":"AAAA,+EAUC"}
|