@pi-r/chrome 0.11.2 → 0.12.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.
@@ -0,0 +1,423 @@
1
+ "use strict";
2
+
3
+ const { randomUUID } = require('node:crypto');
4
+ const Document = require('@e-mc/document');
5
+ const { DomWriter } = require('@e-mc/document/parse/dom');
6
+ const { escapePattern } = require('@e-mc/types');
7
+ const { PATTERN_PARENTHESIS, findClosingIndex, getNewlineString, replaceMatch, spliceSource, splitEnclosing } = require('@pi-r/chrome/util');
8
+ const PATTERN_STRING_SOME = '(?:\\s|' + DomWriter.PATTERN_COMMENT + '|`[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}`)*';
9
+ const PATTERN_STRING_MANY = PATTERN_STRING_SOME.slice(0, -1) + '+';
10
+ const PATTERN_VARIABLE = '--(?:[-\\w]+|\\\\\\S|[^\\u0000-\\u007F]+)+';
11
+ const CACHE_ATRULES = Object.create(null);
12
+ const CACHE_QUERY = new Map();
13
+ const CACHE_SELECTOR = new Map();
14
+ const REGEXP_COMMENT = new RegExp(DomWriter.PATTERN_COMMENT, 'g');
15
+ const REGEXP_QUOTEDVALUE = new RegExp(`\\b(?:@import${PATTERN_STRING_MANY}|content${PATTERN_STRING_SOME}:${PATTERN_STRING_SOME})(?:"[^"]*"|'[^']*')`, 'gi');
16
+ const REGEXP_VARIABLES = new RegExp(`(?<!style\\()(\\s*)(${PATTERN_VARIABLE})${PATTERN_STRING_SOME}:[^;}]*([;}])` + DomWriter.PATTERN_TRAILINGSPACE, 'g');
17
+ const REGEXP_FONTFACE = new RegExp(`(\\s*)@font-face${PATTERN_STRING_SOME}{([^}]+)}` + DomWriter.PATTERN_TRAILINGSPACE, 'gi');
18
+ const REGEXP_FONTFAMILY = new RegExp(`\\bfont-family${PATTERN_STRING_SOME}:([^;}]+)`, 'i');
19
+ const REGEXP_KEYFRAMES = new RegExp(`(\\s*)@keyframes${PATTERN_STRING_MANY}([^{]+){`, 'gi');
20
+ const REGEXP_PROPERTY = new RegExp(`(\\s*)@property${PATTERN_STRING_MANY}(${PATTERN_VARIABLE})${PATTERN_STRING_SOME}{`, 'gi');
21
+ const REGEXP_NTHCHILD = new RegExp(`\\(${PATTERN_STRING_SOME}([+-])?(?:(\\d*)[nN]|(0))?${PATTERN_STRING_SOME}([+-])?${PATTERN_STRING_SOME}(\\d*)(?:${PATTERN_STRING_MANY}[oO][fF]${PATTERN_STRING_MANY}(${PATTERN_PARENTHESIS})${PATTERN_STRING_SOME}\\)|${PATTERN_STRING_SOME}\\))`, 'g');
22
+ const REGEXP_LEADINGBRACE = new RegExp(`(.)${PATTERN_STRING_SOME}{$`);
23
+ const REGEXP_VARIABLES_UNSAFE = new RegExp(`(?<!style\\()(\\s*)(--[^\\s:]+)\\s*:[^;}]*([;}])` + DomWriter.PATTERN_TRAILINGSPACE, 'g');
24
+ const REGEXP_FONTFACE_UNSAFE = new RegExp(`(\\s*)@font-face\\s*{([^}]+)}` + DomWriter.PATTERN_TRAILINGSPACE, 'gi');
25
+ const REGEXP_FONTFAMILY_UNSAFE = /\bfont-family\s*:([^;}]+)/i;
26
+ const REGEXP_KEYFRAMES_UNSAFE = /(\s*)@keyframes\s+([^{]+){/gi;
27
+ const REGEXP_PROPERTY_UNSAFE = /(\s*)@property\s+(--\S+)\s*{/gi;
28
+ const REGEXP_NTHCHILD_UNSAFE = new RegExp(`\\(\\s*([+-])?(?:(\\d*)[nN]|(0))?\\s*([+-])?\\s*(\\d*)(?:\\s+[oO][fF]\\s+(${PATTERN_PARENTHESIS})\\s*\\)|\\s*\\))`, 'g');
29
+ const REGEXP_LEADINGBRACE_UNSAFE = /(.)\s*{$/;
30
+ const getEndIndex = (match) => match.index + match[0].length;
31
+ function cssUnused(source, options) {
32
+ const { usedVariables, usedFontFace, usedKeyframes, unusedStyles, unusedMedia, unusedSupports, unusedContainer, unusedScope, useUnsafeCssReplace, useSessionCache } = options.config;
33
+ if (!usedVariables && !usedFontFace && !usedKeyframes && !unusedStyles && !unusedMedia && !unusedSupports && !unusedContainer && !unusedScope) {
34
+ return;
35
+ }
36
+ const [stringSome, stringMany] = useUnsafeCssReplace ? ['\\s*', '\\s+'] : [PATTERN_STRING_SOME, PATTERN_STRING_MANY];
37
+ const replaceMap = [];
38
+ let current = source, modified = false, checkEmpty, match;
39
+ const parseSelector = (value) => {
40
+ const items = [];
41
+ const revised = value.replace(useUnsafeCssReplace ? REGEXP_NTHCHILD_UNSAFE : REGEXP_NTHCHILD, (...capture) => {
42
+ return '`%' + (items.push('\\(' + stringSome + (capture[1] === '-' ? '\\x2d' : '\\x2b?') + (capture[2] || '[01]?') + (capture[3] === '0' ? !capture[1] && !capture[2] && !capture[4] && !capture[5] ? '[+-]?0[nN]?' : '0' : '[nN]?') + stringSome + (capture[5] ? (capture[4] === '-' ? '\\x2d' : '\\x2b?') + stringSome + capture[5] : `(?:[+-]${stringSome}0)?`) + (capture[6] ? stringMany + '[oO][fF]' + stringMany + capture[6].trim() : '') + stringSome + '\\)') - 1) + '`';
43
+ });
44
+ let selector = '';
45
+ for (let i = 0, length = revised.length, casing = false, attr = false, quote = false; i < length; i++) {
46
+ let ch = revised[i], space = 0;
47
+ switch (ch) {
48
+ case '.':
49
+ case '#':
50
+ if (!attr) {
51
+ casing = true;
52
+ }
53
+ break;
54
+ case '[':
55
+ if (!quote) {
56
+ attr = true;
57
+ casing = false;
58
+ space = 2;
59
+ }
60
+ break;
61
+ case ']':
62
+ if (!quote || attr && revised[i - 1] === '"' && revised[i - 2] === '\\') {
63
+ if (quote) {
64
+ selector = selector.slice(0, -1) + '`%%%`';
65
+ quote = false;
66
+ }
67
+ attr = false;
68
+ casing = false;
69
+ space = 1;
70
+ }
71
+ break;
72
+ case '=':
73
+ if (attr && !quote) {
74
+ casing = true;
75
+ space = 3;
76
+ }
77
+ break;
78
+ case '\\':
79
+ ch = '`%%%%`';
80
+ break;
81
+ case '"':
82
+ if (attr) {
83
+ if (!quote) {
84
+ ch = '`%%%`';
85
+ quote = true;
86
+ }
87
+ else if (revised[i - 1] !== '\\') {
88
+ ch = '`%%%`';
89
+ quote = false;
90
+ casing = false;
91
+ }
92
+ }
93
+ break;
94
+ case '~':
95
+ case '^':
96
+ case '$':
97
+ case '*':
98
+ case '|':
99
+ if (attr && !quote) {
100
+ space = 1;
101
+ }
102
+ break;
103
+ case ' ':
104
+ if (attr && !quote) {
105
+ ch = '`%%`';
106
+ break;
107
+ }
108
+ case ':':
109
+ case ',':
110
+ if (!attr) {
111
+ casing = false;
112
+ }
113
+ break;
114
+ }
115
+ selector += (space & 1 ? '`%%`' : '') + (!casing && ch >= 'a' && ch <= 'z' ? '`' + ch + ch.toUpperCase() + '`' : ch) + (space & 2 ? '`%%`' : '');
116
+ }
117
+ selector = escapePattern(selector)
118
+ .replace(/\s*(>|~(?!=)|\\\+|\\\*(?!=)])\s*/g, (...capture) => stringSome + capture[1] + stringSome)
119
+ .replace(/`([a-z][A-Z]|%{2,4})`/g, (...capture) => {
120
+ switch (capture[1]) {
121
+ case '%%':
122
+ return stringSome;
123
+ case '%%%':
124
+ return `(?:["']|${stringSome})`;
125
+ case '%%%%':
126
+ return '\\\\{0,}';
127
+ default:
128
+ return `[${capture[1]}]`;
129
+ }
130
+ });
131
+ for (let i = 0, length = revised.length; i < length; ++i) {
132
+ selector = selector.replace('`%' + i + '`', items[i]);
133
+ }
134
+ return selector.replace(/ +/g, stringMany) + stringSome;
135
+ };
136
+ const replaceUnunsed = (name, items, nesting, increment = 1) => {
137
+ for (let i = 0; i < items.length; i += increment) {
138
+ const value = items[i].trim();
139
+ let valueTo;
140
+ const key = name + '_' + value + (increment === 2 && (valueTo = items[i + 1].trim()) ? '_' + valueTo : '') + (useUnsafeCssReplace ? '_1' : '');
141
+ let pattern = CACHE_QUERY.get(key), rule;
142
+ if (!pattern) {
143
+ let flags = 'gi', selector = '', revised = false;
144
+ if (name === 'container') {
145
+ for (let j = 0, length = value.length, casing = true; j < length; j++) {
146
+ let ch = value[j];
147
+ if (!casing) {
148
+ ch = ch.toLowerCase();
149
+ }
150
+ else if (ch === '(') {
151
+ casing = false;
152
+ }
153
+ if (casing || ch < 'a' || ch > 'z') {
154
+ selector += value[j];
155
+ if (selector.endsWith(' not ')) {
156
+ selector = selector.replace(/ not $/, ' `nN``oO``tT` ');
157
+ }
158
+ }
159
+ else {
160
+ selector += '`' + ch + ch.toUpperCase() + '`';
161
+ }
162
+ }
163
+ flags = 'g';
164
+ }
165
+ else if (name === 'scope') {
166
+ selector = `\\(${stringSome + parseSelector(value)}\\)`;
167
+ if (valueTo) {
168
+ selector += `${stringMany}[tT][oO]${stringMany}\\(${stringSome + parseSelector(valueTo)}\\)`;
169
+ }
170
+ flags = 'g';
171
+ revised = true;
172
+ }
173
+ else if (name === 'supports' && /^selector\(/i.test(value)) {
174
+ const startIndex = value.indexOf('(');
175
+ const endIndex = value.lastIndexOf(')');
176
+ if (startIndex !== -1 && endIndex !== -1) {
177
+ selector = '[sS][eE][lL][eE][cC][tT][oO][rR]\\(' + stringSome + parseSelector(value.slice(startIndex + 1, endIndex)) + '\\)';
178
+ flags = 'g';
179
+ revised = true;
180
+ }
181
+ }
182
+ if (!revised) {
183
+ selector = escapePattern(selector || value)
184
+ .replace(/\s*(\\[()])\s*/g, (...capture) => stringSome + capture[1] + stringSome)
185
+ .replace(/:\s+/g, stringSome + ':' + stringSome)
186
+ .replace(/\s+([<>/]|<=|>=|\\\*)\s+/g, (...capture) => stringSome + capture[1] + stringSome)
187
+ .replace(/ +/g, stringMany);
188
+ if (flags === 'g') {
189
+ selector = selector.replace(/`([a-z][A-Z])`/g, (...capture) => `[${capture[1]}]`);
190
+ }
191
+ }
192
+ pattern = new RegExp('(\\s*)@' + (flags === 'g' ? name.split('').map(ch => `[${ch + ch.toUpperCase()}]`).join('') : name) + stringMany + selector + stringSome + '{', flags);
193
+ if (useSessionCache !== false) {
194
+ CACHE_QUERY.set(key, pattern);
195
+ }
196
+ }
197
+ else {
198
+ pattern.lastIndex = 0;
199
+ }
200
+ while (rule = pattern.exec(current)) {
201
+ if (!nesting) {
202
+ const block = Array.from(current.slice(0, rule.index).matchAll(/[{}]/g));
203
+ if (block.filter(open => open[0] === '{').length !== block.filter(close => close[0] === '}').length) {
204
+ continue;
205
+ }
206
+ }
207
+ const [endIndex, trailing] = findClosingIndex(current, getEndIndex(rule), '{');
208
+ if (endIndex !== -1) {
209
+ current = spliceSource(current, rule.index, endIndex, pattern, rule[1], trailing);
210
+ (checkEmpty ||= {})[name] = true;
211
+ modified = true;
212
+ }
213
+ }
214
+ }
215
+ };
216
+ if (!useUnsafeCssReplace) {
217
+ const replaceBracket = (pattern) => {
218
+ let bracket;
219
+ while (bracket = pattern.exec(current)) {
220
+ const segment = bracket[0];
221
+ if (/[{}]/.test(segment)) {
222
+ const placeholder = '`' + randomUUID().slice(0, 18) + '`';
223
+ replaceMap.push([placeholder, segment]);
224
+ current = replaceMatch(bracket, current, placeholder, pattern);
225
+ }
226
+ }
227
+ pattern.lastIndex = 0;
228
+ };
229
+ replaceBracket(REGEXP_COMMENT);
230
+ replaceBracket(REGEXP_QUOTEDVALUE);
231
+ const values = splitEnclosing(current, /\b(?<!-)url/gi);
232
+ for (let i = 0, length = values.length; i < length; ++i) {
233
+ const seg = values[i];
234
+ if (/^url\([^{}]*[{}].*\)$/is.test(seg)) {
235
+ replaceMap.push([values[i] = randomUUID().slice(0, 18), seg]);
236
+ }
237
+ }
238
+ if (replaceMap.length > 0) {
239
+ current = values.join('');
240
+ }
241
+ }
242
+ if (unusedMedia) {
243
+ replaceUnunsed('media', unusedMedia, true);
244
+ }
245
+ if (unusedSupports) {
246
+ replaceUnunsed('supports', unusedSupports, true);
247
+ }
248
+ if (unusedContainer) {
249
+ replaceUnunsed('container', unusedContainer, false);
250
+ }
251
+ if (unusedScope) {
252
+ replaceUnunsed('scope', unusedScope.map(item => [item.start || '', item.end || '']).flat(), false, 2);
253
+ }
254
+ if (unusedStyles) {
255
+ current = (function recurse(target, child) {
256
+ let output = '', index = 0, pendingStart = 0, pendingEnd = 0, found = -1;
257
+ while ((found = target.indexOf('{', index)) !== -1) {
258
+ ++found;
259
+ const closing = findClosingIndex(target, found, '{')[0];
260
+ if (closing === -1) {
261
+ break;
262
+ }
263
+ let header = target.slice(index, found), invalid = 0, content;
264
+ for (const value of unusedStyles) {
265
+ const key = value + (child ? '_1' : '') + (useUnsafeCssReplace ? '_2' : '');
266
+ const pattern = CACHE_SELECTOR.get(key);
267
+ let single, group;
268
+ if (!pattern) {
269
+ const selector = parseSelector(value) + (child ? `(?:${stringSome}&${stringSome})*` : '');
270
+ single = new RegExp(`(^|;)(${stringSome})(?<!${child ? '&|' : ''}@[^};]*)${selector}\\{$`);
271
+ group = new RegExp(`((?:^|;)[^{]*${stringSome}(,${stringSome}|(?<!@[^};]*)))(${(child ? '(?:&\\s+|(?<!&))' : '') + selector})(,[^{]*)?\\{$`, 'g');
272
+ if (useSessionCache !== false) {
273
+ CACHE_SELECTOR.set(key, [single, group]);
274
+ }
275
+ }
276
+ else {
277
+ [single, group] = pattern;
278
+ }
279
+ if (match = single.exec(header)) {
280
+ content = match[1] === ';' ? header.slice(0, match.index + 1) + match[2].replace(/[^\n]+$/, '') : undefined;
281
+ invalid = 1;
282
+ break;
283
+ }
284
+ let leading = -1, first = true;
285
+ while (match = group.exec(header)) {
286
+ const start = match.index + match[1].length;
287
+ if (!child && first) {
288
+ if (match[1].trimEnd().endsWith(';')) {
289
+ leading = start;
290
+ }
291
+ first = false;
292
+ }
293
+ const sibling = match[2].startsWith(',');
294
+ const trailing = match[4];
295
+ const offset = trailing && !sibling ? /^,\s*/.exec(trailing)[0].length : 1;
296
+ header = header.slice(0, start - (sibling ? match[2].length : 0)) + header.slice(start + match[3].length + (trailing ? sibling ? 0 : offset : /\s$/.test(match[3]) ? -1 : 0));
297
+ group.lastIndex = match.index;
298
+ invalid = 2;
299
+ }
300
+ if (invalid) {
301
+ group.lastIndex = 0;
302
+ const empty = (useUnsafeCssReplace ? REGEXP_LEADINGBRACE_UNSAFE : REGEXP_LEADINGBRACE).exec(header);
303
+ if (empty) {
304
+ if (header.trim() === '{') {
305
+ content = undefined;
306
+ invalid = 1;
307
+ break;
308
+ }
309
+ if (leading !== -1 && empty[1] === ';') {
310
+ content = target.slice(index, found).slice(0, leading).replace(/[^\n;]+$/, '');
311
+ invalid = 1;
312
+ break;
313
+ }
314
+ }
315
+ }
316
+ }
317
+ if (invalid === 0 || invalid === 2) {
318
+ const trailing = target.slice(found, closing);
319
+ if (content = recurse(trailing, true)) {
320
+ content = header + content;
321
+ }
322
+ else if (invalid === 2) {
323
+ content = header + trailing;
324
+ }
325
+ }
326
+ if (content) {
327
+ if (pendingEnd) {
328
+ output = target.slice(pendingStart, pendingEnd) + content;
329
+ pendingEnd = 0;
330
+ }
331
+ else {
332
+ output += content;
333
+ }
334
+ }
335
+ else if (!invalid) {
336
+ if (output) {
337
+ output += target.slice(index, closing);
338
+ }
339
+ else {
340
+ pendingEnd = closing;
341
+ }
342
+ }
343
+ else if (pendingEnd) {
344
+ output = target.slice(pendingStart, pendingEnd);
345
+ pendingEnd = 0;
346
+ }
347
+ else {
348
+ pendingStart = closing;
349
+ }
350
+ index = closing;
351
+ }
352
+ if (output) {
353
+ modified = true;
354
+ return output + target.slice(index);
355
+ }
356
+ return child ? '' : target;
357
+ })(current, false);
358
+ }
359
+ if (usedVariables) {
360
+ let pattern = useUnsafeCssReplace ? REGEXP_VARIABLES_UNSAFE : REGEXP_VARIABLES;
361
+ while (match = pattern.exec(current)) {
362
+ if (!usedVariables.includes(match[2])) {
363
+ current = replaceMatch(match, current, match[3] === ';' ? getNewlineString(match[1], match[4]) : '', pattern);
364
+ modified = true;
365
+ }
366
+ }
367
+ pattern.lastIndex = 0;
368
+ pattern = useUnsafeCssReplace ? REGEXP_PROPERTY_UNSAFE : REGEXP_PROPERTY;
369
+ while (match = pattern.exec(current)) {
370
+ if (!usedVariables.includes(match[2])) {
371
+ const [endIndex, trailing] = findClosingIndex(current, getEndIndex(match), '{');
372
+ if (endIndex !== -1) {
373
+ current = spliceSource(current, match.index, endIndex, pattern, match[1], trailing);
374
+ modified = true;
375
+ }
376
+ }
377
+ }
378
+ pattern.lastIndex = 0;
379
+ }
380
+ if (usedFontFace) {
381
+ const pattern = useUnsafeCssReplace ? REGEXP_FONTFACE_UNSAFE : REGEXP_FONTFACE;
382
+ const fontFamily = useUnsafeCssReplace ? REGEXP_FONTFAMILY_UNSAFE : REGEXP_FONTFAMILY;
383
+ while (match = pattern.exec(current)) {
384
+ const font = fontFamily.exec(match[0]);
385
+ if (font && !usedFontFace.includes(font[1].trim().replace(/^(["'])(.+)\1$/s, (...content) => content[2]))) {
386
+ current = spliceSource(current, match.index, getEndIndex(match), pattern, match[1], match[3]);
387
+ modified = true;
388
+ }
389
+ }
390
+ pattern.lastIndex = 0;
391
+ }
392
+ if (usedKeyframes) {
393
+ const pattern = useUnsafeCssReplace ? REGEXP_KEYFRAMES_UNSAFE : REGEXP_KEYFRAMES;
394
+ while (match = pattern.exec(current)) {
395
+ if (!usedKeyframes.includes(match[2].trim())) {
396
+ const [endIndex, trailing] = findClosingIndex(current, getEndIndex(match), '{');
397
+ if (endIndex !== -1) {
398
+ current = spliceSource(current, match.index, endIndex, pattern, match[1], trailing);
399
+ modified = true;
400
+ }
401
+ }
402
+ }
403
+ pattern.lastIndex = 0;
404
+ }
405
+ if (modified) {
406
+ for (const name in checkEmpty) {
407
+ const pattern = CACHE_ATRULES[name + (useUnsafeCssReplace ? '_1' : '')] ||= new RegExp(`(\\s*)@${name}[^{]*{${stringSome}}` + DomWriter.PATTERN_TRAILINGSPACE, 'gi');
408
+ while (match = pattern.exec(current)) {
409
+ current = spliceSource(current, match.index, getEndIndex(match), pattern, match[1], match[2]);
410
+ }
411
+ pattern.lastIndex = 0;
412
+ }
413
+ if (replaceMap.length > 0) {
414
+ for (const [placeholder, content] of replaceMap.reverse()) {
415
+ current = current.replace(placeholder, content);
416
+ }
417
+ }
418
+ return current;
419
+ }
420
+ }
421
+ var index = Document.signExtensionType(cssUnused, 'css');
422
+
423
+ module.exports = index;
package/index.d.ts CHANGED
@@ -2,6 +2,6 @@ import type { IFileManager } from '@e-mc/types/lib';
2
2
 
3
3
  import type { ChromeDocumentConstructor, DocumentAsset } from './types';
4
4
 
5
- declare const Chrome: ChromeDocumentConstructor<IFileManager<DocumentAsset>>;
5
+ declare const Chrome: ChromeDocumentConstructor<IFileManager, DocumentAsset>;
6
6
 
7
7
  export = Chrome;