@thejaredwilcurt/csslop 0.0.22 → 0.0.24

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thejaredwilcurt/csslop",
3
3
  "main": "index.js",
4
4
  "type": "module",
5
- "version": "0.0.22",
5
+ "version": "0.0.24",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/prestart.js",
@@ -16,7 +16,7 @@
16
16
  "real": "node ./tests/realworld.test.js",
17
17
  "lint": "eslint *.js scripts src tests --fix",
18
18
  "fix": "npm run lint",
19
- "bump": "npx --yes -- @jsdevtools/version-bump-prompt && npm i"
19
+ "bump": "npx --yes -- @jsdevtools/version-bump-prompt patch && npm i"
20
20
  },
21
21
  "dependencies": {
22
22
  "@csstools/css-calc": "^3.3.0",
@@ -25,21 +25,21 @@
25
25
  "devDependencies": {
26
26
  "@codemirror/autocomplete": "^6.20.3",
27
27
  "@codemirror/lang-css": "^6.3.1",
28
- "@codemirror/view": "^6.43.8",
28
+ "@codemirror/view": "^6.43.9",
29
29
  "@eslint/js": "^10.0.1",
30
30
  "@stylistic/eslint-plugin": "^5.10.0",
31
31
  "codemirror": "^6.0.2",
32
- "eslint": "^10.8.1",
32
+ "eslint": "^10.9.0",
33
33
  "eslint-config-tjw-base": "^5.0.0",
34
34
  "eslint-config-tjw-import-x": "^1.0.1",
35
35
  "eslint-config-tjw-jsdoc": "^2.0.1",
36
36
  "eslint-plugin-import-x": "^4.17.0",
37
- "eslint-plugin-jsdoc": "^64.1.0",
37
+ "eslint-plugin-jsdoc": "^64.2.1",
38
38
  "fflate": "^0.8.3",
39
39
  "globals": "^17.11.0",
40
40
  "pretty-ms": "^9.3.0",
41
- "real-world-css-libraries": "^1.0.7",
42
- "vite": "^8.2.1"
41
+ "real-world-css-libraries": "^1.0.8",
42
+ "vite": "^8.2.2"
43
43
  },
44
44
  "author": "The Jared Wilcurt",
45
45
  "homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
package/src/charset.js ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * @file Resolves `@charset` labels against the Encoding Standard so charset rules can be deduplicated, hoisted, shortened, or removed.
3
+ */
4
+
5
+ /**
6
+ * The default encoding of a stylesheet, used when no `@charset` is declared.
7
+ *
8
+ * @type {string}
9
+ */
10
+ const DEFAULT_ENCODING = 'UTF-8';
11
+
12
+ /**
13
+ * Encodings that a stylesheet can never actually be in when labelled by a
14
+ * `@charset` rule. The rule itself is written in ASCII bytes, so a UTF-16
15
+ * label falls back to UTF-8 when determining the fallback encoding.
16
+ * https://drafts.csswg.org/css-syntax-3/#determine-the-fallback-encoding
17
+ *
18
+ * @type {Set<string>}
19
+ */
20
+ const UTF_16_ENCODINGS = new Set(['UTF-16BE', 'UTF-16LE']);
21
+
22
+ /**
23
+ * Every encoding name of the Encoding Standard mapped to all of its labels.
24
+ * A `@charset` label is looked up here (ASCII case-insensitively, after
25
+ * trimming whitespace) to find the encoding a browser will use, and any label
26
+ * of that encoding can be swapped in without changing how the file is decoded.
27
+ * https://encoding.spec.whatwg.org/#names-and-labels
28
+ *
29
+ * @type {object}
30
+ */
31
+ const ENCODING_LABELS = {
32
+ // The encoding
33
+ 'UTF-8': ['unicode-1-1-utf-8', 'unicode11utf8', 'unicode20utf8', 'utf-8', 'utf8', 'x-unicode20utf8'],
34
+ // Legacy single-byte encodings
35
+ IBM866: ['866', 'cp866', 'csibm866', 'ibm866'],
36
+ 'ISO-8859-2': ['csisolatin2', 'iso-8859-2', 'iso-ir-101', 'iso8859-2', 'iso88592', 'iso_8859-2', 'iso_8859-2:1987', 'l2', 'latin2'],
37
+ 'ISO-8859-3': ['csisolatin3', 'iso-8859-3', 'iso-ir-109', 'iso8859-3', 'iso88593', 'iso_8859-3', 'iso_8859-3:1988', 'l3', 'latin3'],
38
+ 'ISO-8859-4': ['csisolatin4', 'iso-8859-4', 'iso-ir-110', 'iso8859-4', 'iso88594', 'iso_8859-4', 'iso_8859-4:1988', 'l4', 'latin4'],
39
+ 'ISO-8859-5': ['csisolatincyrillic', 'cyrillic', 'iso-8859-5', 'iso-ir-144', 'iso8859-5', 'iso88595', 'iso_8859-5', 'iso_8859-5:1988'],
40
+ 'ISO-8859-6': ['arabic', 'asmo-708', 'csiso88596e', 'csiso88596i', 'csisolatinarabic', 'ecma-114', 'iso-8859-6', 'iso-8859-6-e', 'iso-8859-6-i', 'iso-ir-127', 'iso8859-6', 'iso88596', 'iso_8859-6', 'iso_8859-6:1987'],
41
+ 'ISO-8859-7': ['csisolatingreek', 'ecma-118', 'elot_928', 'greek', 'greek8', 'iso-8859-7', 'iso-ir-126', 'iso8859-7', 'iso88597', 'iso_8859-7', 'iso_8859-7:1987', 'sun_eu_greek'],
42
+ 'ISO-8859-8': ['csiso88598e', 'csisolatinhebrew', 'hebrew', 'iso-8859-8', 'iso-8859-8-e', 'iso-ir-138', 'iso8859-8', 'iso88598', 'iso_8859-8', 'iso_8859-8:1988', 'visual'],
43
+ 'ISO-8859-8-I': ['csiso88598i', 'iso-8859-8-i', 'logical'],
44
+ 'ISO-8859-10': ['csisolatin6', 'iso-8859-10', 'iso-ir-157', 'iso8859-10', 'iso885910', 'l6', 'latin6'],
45
+ 'ISO-8859-13': ['iso-8859-13', 'iso8859-13', 'iso885913'],
46
+ 'ISO-8859-14': ['iso-8859-14', 'iso8859-14', 'iso885914'],
47
+ 'ISO-8859-15': ['csisolatin9', 'iso-8859-15', 'iso8859-15', 'iso885915', 'iso_8859-15', 'l9'],
48
+ 'ISO-8859-16': ['iso-8859-16'],
49
+ 'KOI8-R': ['cskoi8r', 'koi', 'koi8', 'koi8-r', 'koi8_r'],
50
+ 'KOI8-U': ['koi8-ru', 'koi8-u'],
51
+ macintosh: ['csmacintosh', 'mac', 'macintosh', 'x-mac-roman'],
52
+ 'windows-874': ['dos-874', 'iso-8859-11', 'iso8859-11', 'iso885911', 'tis-620', 'windows-874'],
53
+ 'windows-1250': ['cp1250', 'windows-1250', 'x-cp1250'],
54
+ 'windows-1251': ['cp1251', 'windows-1251', 'x-cp1251'],
55
+ 'windows-1252': ['ansi_x3.4-1968', 'ascii', 'cp1252', 'cp819', 'csisolatin1', 'ibm819', 'iso-8859-1', 'iso-ir-100', 'iso8859-1', 'iso88591', 'iso_8859-1', 'iso_8859-1:1987', 'l1', 'latin1', 'us-ascii', 'windows-1252', 'x-cp1252'],
56
+ 'windows-1253': ['cp1253', 'windows-1253', 'x-cp1253'],
57
+ 'windows-1254': ['cp1254', 'csisolatin5', 'iso-8859-9', 'iso-ir-148', 'iso8859-9', 'iso88599', 'iso_8859-9', 'iso_8859-9:1989', 'l5', 'latin5', 'windows-1254', 'x-cp1254'],
58
+ 'windows-1255': ['cp1255', 'windows-1255', 'x-cp1255'],
59
+ 'windows-1256': ['cp1256', 'windows-1256', 'x-cp1256'],
60
+ 'windows-1257': ['cp1257', 'windows-1257', 'x-cp1257'],
61
+ 'windows-1258': ['cp1258', 'windows-1258', 'x-cp1258'],
62
+ 'x-mac-cyrillic': ['x-mac-cyrillic', 'x-mac-ukrainian'],
63
+ // Legacy multi-byte Chinese (simplified) encodings
64
+ GBK: ['chinese', 'csgb2312', 'csiso58gb231280', 'gb2312', 'gb_2312', 'gb_2312-80', 'gbk', 'iso-ir-58', 'x-gbk'],
65
+ gb18030: ['gb18030'],
66
+ // Legacy multi-byte Chinese (traditional) encodings
67
+ Big5: ['big5', 'big5-hkscs', 'cn-big5', 'csbig5', 'x-x-big5'],
68
+ // Legacy multi-byte Japanese encodings
69
+ 'EUC-JP': ['cseucpkdfmtjapanese', 'euc-jp', 'x-euc-jp'],
70
+ 'ISO-2022-JP': ['csiso2022jp', 'iso-2022-jp'],
71
+ Shift_JIS: ['csshiftjis', 'ms932', 'ms_kanji', 'shift-jis', 'shift_jis', 'sjis', 'windows-31j', 'x-sjis'],
72
+ // Legacy multi-byte Korean encodings
73
+ 'EUC-KR': ['cseuckr', 'csksc56011987', 'euc-kr', 'iso-ir-149', 'korean', 'ks_c_5601-1987', 'ks_c_5601-1989', 'ksc5601', 'ksc_5601', 'windows-949'],
74
+ // Legacy miscellaneous encodings
75
+ replacement: ['csiso2022kr', 'hz-gb-2312', 'iso-2022-cn', 'iso-2022-cn-ext', 'iso-2022-kr', 'replacement'],
76
+ 'UTF-16BE': ['unicodefffe', 'utf-16be'],
77
+ 'UTF-16LE': ['csunicode', 'iso-10646-ucs-2', 'ucs-2', 'unicode', 'unicodefeff', 'utf-16', 'utf-16le'],
78
+ 'x-user-defined': ['x-user-defined']
79
+ };
80
+
81
+ /**
82
+ * Builds the lookup of every Encoding Standard label to the encoding it names.
83
+ *
84
+ * @return {Map} Map of lowercase label to encoding name.
85
+ */
86
+ function createEncodingByLabelLookup () {
87
+ const encodingByLabel = new Map();
88
+ for (const encodingName in ENCODING_LABELS) {
89
+ for (const label of ENCODING_LABELS[encodingName]) {
90
+ encodingByLabel.set(label, encodingName);
91
+ }
92
+ }
93
+ return encodingByLabel;
94
+ }
95
+
96
+ /**
97
+ * Builds the lookup of every encoding to its fewest-bytes label, so a
98
+ * `@charset` rule can be rewritten with a shorter but equivalent label.
99
+ * Labels of the same length are ordered alphabetically to keep output stable.
100
+ *
101
+ * @return {Map} Map of encoding name to its shortest label.
102
+ */
103
+ function createShortestLabelLookup () {
104
+ const shortestLabels = new Map();
105
+ for (const encodingName in ENCODING_LABELS) {
106
+ const labelsByLength = [...ENCODING_LABELS[encodingName]].sort((labelA, labelB) => {
107
+ if (labelA.length !== labelB.length) {
108
+ return labelA.length - labelB.length;
109
+ }
110
+ return labelA.localeCompare(labelB);
111
+ });
112
+ shortestLabels.set(encodingName, labelsByLength[0]);
113
+ }
114
+ return shortestLabels;
115
+ }
116
+
117
+ const ENCODING_BY_LABEL = createEncodingByLabelLookup();
118
+ const SHORTEST_LABEL_BY_ENCODING = createShortestLabelLookup();
119
+
120
+ /**
121
+ * The byte sequence an engine looks for when a stylesheet declares its
122
+ * encoding: the lowercase `@charset` keyword, a single space, a double-quoted
123
+ * label, and a semicolon. Engines compare these raw bytes instead of parsing
124
+ * CSS syntax, so a rule written any other way (single quotes, an uppercased
125
+ * keyword, padded whitespace, escaped label bytes) declares nothing at all.
126
+ * https://drafts.csswg.org/css-syntax-3/#determine-the-fallback-encoding
127
+ *
128
+ * @type {RegExp}
129
+ */
130
+ // Matches `@charset "label";` anywhere in raw CSS text, capturing the quoted label
131
+ const ENCODING_DECLARATION_PATTERN = /@charset ("[^"]*");/;
132
+
133
+ /**
134
+ * The same byte sequence, but required to make up an entire at-rule, so that
135
+ * text surrounding the keyword disqualifies the rule as a declaration.
136
+ *
137
+ * @type {RegExp}
138
+ */
139
+ // Matches raw at-rule text that is exactly `@charset "label";`, capturing the quoted label
140
+ const ENCODING_DECLARATION_RULE_PATTERN = /^@charset ("[^"]*");$/;
141
+
142
+ /**
143
+ * Reads the quoted label of an encoding declaration out of raw CSS text.
144
+ * Text that does not contain the declaration byte sequence declares no
145
+ * encoding, so an empty string is returned for it.
146
+ *
147
+ * @param {string} text The raw CSS text to read the declaration from.
148
+ * @param {RegExp} pattern The byte sequence pattern to read the text with.
149
+ * @return {string} The declared value including its quotes, or empty string when nothing is declared.
150
+ */
151
+ function readEncodingDeclarationValue (text, pattern) {
152
+ if (!text) {
153
+ return '';
154
+ }
155
+ const match = String(text).match(pattern);
156
+ if (!match) {
157
+ return '';
158
+ }
159
+ return match[1];
160
+ }
161
+
162
+ /**
163
+ * Reads the value a `@charset` AST node declares, based on the raw text the
164
+ * rule was written with rather than its parsed value. A rule the engine never
165
+ * reads as an encoding declaration is inert, and reports no value.
166
+ *
167
+ * @param {object} rule A `@charset` AST rule node.
168
+ * @return {string} The declared value including its quotes, or empty string when the rule is inert.
169
+ */
170
+ function readCharsetRuleValue (rule) {
171
+ let ruleText = rule.rawSource;
172
+ if (!ruleText) {
173
+ ruleText = '@charset ' + rule.charset + ';';
174
+ }
175
+ return readEncodingDeclarationValue(String(ruleText).trim(), ENCODING_DECLARATION_RULE_PATTERN);
176
+ }
177
+
178
+ /**
179
+ * Reduces a raw `@charset` value to the label that gets looked up, by removing
180
+ * the surrounding quotes, trimming whitespace, and lowercasing it, since
181
+ * Encoding Standard labels are matched ASCII case-insensitively.
182
+ *
183
+ * @param {string} charsetValue The `@charset` value, with or without surrounding quotes.
184
+ * @return {string} The normalized label, or empty string when there is no value.
185
+ */
186
+ function normalizeCharsetLabel (charsetValue) {
187
+ if (!charsetValue) {
188
+ return '';
189
+ }
190
+ // Remove one layer of matching single or double quotes wrapping the label
191
+ const unquoted = String(charsetValue).trim().replace(/^(["'])([\s\S]*)\1$/, '$2');
192
+ return unquoted.trim().toLowerCase();
193
+ }
194
+
195
+ /**
196
+ * Resolves a `@charset` value to the encoding a browser will actually decode
197
+ * the stylesheet with. Labels that are not in the Encoding Standard are
198
+ * ignored, and UTF-16 labels fall back to UTF-8, so both leave the stylesheet
199
+ * in the default encoding.
200
+ *
201
+ * @param {string} charsetValue The `@charset` value, with or without surrounding quotes.
202
+ * @return {string} The effective encoding name.
203
+ */
204
+ function resolveEffectiveEncoding (charsetValue) {
205
+ const encodingName = ENCODING_BY_LABEL.get(normalizeCharsetLabel(charsetValue));
206
+ if (!encodingName || UTF_16_ENCODINGS.has(encodingName)) {
207
+ return DEFAULT_ENCODING;
208
+ }
209
+ return encodingName;
210
+ }
211
+
212
+ /**
213
+ * Determines whether a `@charset` value leaves the stylesheet in an encoding
214
+ * that can represent every unicode character, meaning unicode escapes may be
215
+ * safely replaced with the literal characters they resolve to.
216
+ *
217
+ * @param {string} charsetValue The `@charset` value, with or without surrounding quotes.
218
+ * @return {boolean} True when the effective encoding supports all unicode characters.
219
+ */
220
+ function isUnicodeCompatibleCharset (charsetValue) {
221
+ return resolveEffectiveEncoding(charsetValue) === DEFAULT_ENCODING;
222
+ }
223
+
224
+ /**
225
+ * Rewrites a `@charset` value as the shortest quoted label for the same
226
+ * encoding, or as an empty string when the rule has no effect at all and can
227
+ * be deleted, because the stylesheet is left in the default UTF-8 encoding.
228
+ * The declared label is kept when no shorter label exists for its encoding.
229
+ *
230
+ * @param {string} charsetValue The `@charset` value, with or without surrounding quotes.
231
+ * @return {string} The shortest equivalent quoted value, or empty string when the rule is removable.
232
+ */
233
+ function optimizeCharsetValue (charsetValue) {
234
+ const encodingName = resolveEffectiveEncoding(charsetValue);
235
+ if (encodingName === DEFAULT_ENCODING) {
236
+ return '';
237
+ }
238
+ const declaredLabel = normalizeCharsetLabel(charsetValue);
239
+ const shortestLabel = SHORTEST_LABEL_BY_ENCODING.get(encodingName);
240
+ if (shortestLabel.length < declaredLabel.length) {
241
+ return '"' + shortestLabel + '"';
242
+ }
243
+ return '"' + declaredLabel + '"';
244
+ }
245
+
246
+ /**
247
+ * Finds the `@charset` value that applies to a stylesheet by scanning the raw
248
+ * CSS text before it is parsed. Only the first `@charset` of a document has any
249
+ * effect, so later ones (usually the result of concatenating files) are ignored,
250
+ * and only text matching the declaration byte sequence declares an encoding.
251
+ *
252
+ * @param {string} css The raw CSS string to scan.
253
+ * @return {string} The first declared `@charset` value (with quotes), or empty string when none is declared.
254
+ */
255
+ function detectCharset (css) {
256
+ return readEncodingDeclarationValue(css, ENCODING_DECLARATION_PATTERN);
257
+ }
258
+
259
+ /**
260
+ * Applies the browser's `@charset` handling to the top-level rules of a
261
+ * stylesheet. Only the first `@charset` is meaningful, and it is only honored
262
+ * when it is the very first thing in the file, so it is shortened and hoisted
263
+ * to the front while every later `@charset` is dropped. A first `@charset` that
264
+ * leaves the stylesheet in the default UTF-8 encoding is dropped as well, as is
265
+ * any `@charset` that is not written as an encoding declaration, since engines
266
+ * discard those without ever reading an encoding from them.
267
+ *
268
+ * @param {Array} rules The top-level AST rule nodes to filter.
269
+ * @return {Array} A new array of rules, with at most one `@charset` rule, placed at the start.
270
+ */
271
+ function filterRedundantCharsets (rules) {
272
+ let hoistedCharset = null;
273
+ let foundCharset = false;
274
+
275
+ const filtered = rules.filter((rule) => {
276
+ if (rule.type !== 'charset') {
277
+ return true;
278
+ }
279
+ const declaredValue = readCharsetRuleValue(rule);
280
+ if (!declaredValue) {
281
+ return false;
282
+ }
283
+ if (!foundCharset) {
284
+ foundCharset = true;
285
+ const optimizedCharset = optimizeCharsetValue(declaredValue);
286
+ if (optimizedCharset) {
287
+ hoistedCharset = {
288
+ ...rule,
289
+ charset: optimizedCharset,
290
+ rawSource: '@charset ' + optimizedCharset + ';'
291
+ };
292
+ }
293
+ }
294
+ return false;
295
+ });
296
+
297
+ if (hoistedCharset) {
298
+ return [hoistedCharset, ...filtered];
299
+ }
300
+ return filtered;
301
+ }
302
+
303
+ export {
304
+ detectCharset,
305
+ filterRedundantCharsets,
306
+ isUnicodeCompatibleCharset
307
+ };
package/src/context.js CHANGED
@@ -2,6 +2,8 @@
2
2
  * @file Manages the shared minification context for tracking registered custom properties and their syntax.
3
3
  */
4
4
 
5
+ import { isUnicodeCompatibleCharset } from './charset.js';
6
+
5
7
  /**
6
8
  * Creates a fresh minification context used to track `@property`-registered custom properties and their declared syntax types across the entire stylesheet.
7
9
  *
@@ -22,18 +24,15 @@ function createMinifyContext () {
22
24
  let activeCharset = '';
23
25
 
24
26
  /**
25
- * Returns true when the active charset is a unicode-compatible encoding
26
- * (UTF-8, UTF-16, or the default when no `@charset` is declared), meaning
27
- * CSS unicode escapes can safely be resolved to literal characters.
27
+ * Returns true when the active charset leaves the stylesheet in a
28
+ * unicode-compatible encoding (UTF-8, a UTF-16 label that falls back to UTF-8,
29
+ * an unrecognized label, or the default when no `@charset` is declared),
30
+ * meaning CSS unicode escapes can safely be resolved to literal characters.
28
31
  *
29
32
  * @return {boolean} True if the active charset supports unicode characters.
30
33
  */
31
34
  function isUnicodeCharset () {
32
- if (!activeCharset) {
33
- return true;
34
- }
35
- const normalized = activeCharset.toLowerCase().replace(/["']/g, '');
36
- return normalized === 'utf-8' || normalized.startsWith('utf-16');
35
+ return isUnicodeCompatibleCharset(activeCharset);
37
36
  }
38
37
 
39
38
  /**
package/src/index.js CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  import { parse } from '@node-projects/css-parser';
6
6
 
7
+ import {
8
+ detectCharset,
9
+ filterRedundantCharsets
10
+ } from './charset.js';
7
11
  import {
8
12
  clearActiveCharset,
9
13
  createMinifyContext,
@@ -13,7 +17,6 @@ import {
13
17
  analyzePositionTryRules,
14
18
  cleanPositionTryRules,
15
19
  collectRuleMetadata,
16
- filterRedundantCharsets,
17
20
  filterUnusedPositionTry
18
21
  } from './position-try.js';
19
22
  import {
@@ -172,22 +175,6 @@ function mergeAdjacentRulesWithIdenticalBodies (ruleStrings) {
172
175
  return result;
173
176
  }
174
177
 
175
- /**
176
- * Extracts the first `@charset` value from raw CSS text before parsing.
177
- * Scans for `@charset` followed by a quoted string and semicolon.
178
- *
179
- * @param {string} css The raw CSS string to scan.
180
- * @return {string} The first charset value (with quotes), or empty string if none found.
181
- */
182
- function detectCharset (css) {
183
- // Match @charset followed by a quoted value and semicolon
184
- const match = css.match(/@charset\s+(["'][^"']+["'])\s*;/i);
185
- if (match) {
186
- return match[1];
187
- }
188
- return '';
189
- }
190
-
191
178
  /**
192
179
  * Parses, optimizes, and minifies a CSS string by applying rule merging, declaration deduplication, value compression, and dead-code elimination.
193
180
  *
@@ -163,41 +163,9 @@ function filterUnusedPositionTry (rules, positionTryRules, positionTryUsage) {
163
163
  });
164
164
  }
165
165
 
166
- /**
167
- * Removes duplicate and redundant UTF-8 `@charset` rules, keeping only the first
168
- * non-UTF-8 charset declaration and moving it to the top of the document.
169
- * Per the CSS specification, `@charset` must be the very first thing in a stylesheet.
170
- *
171
- * @param {Array} rules The top-level AST rule nodes to filter.
172
- * @return {Array} A new array of rules with the first non-UTF-8 `@charset` at the start and all others removed.
173
- */
174
- function filterRedundantCharsets (rules) {
175
- let keptCharset = null;
176
-
177
- const filtered = rules.filter((rule) => {
178
- if (rule.type !== 'charset') {
179
- return true;
180
- }
181
- if (!keptCharset) {
182
- // Strip surrounding quotes from the charset value for comparison
183
- const normalizedCharset = rule.charset?.toLowerCase().replace(/["']/g, '');
184
- if (normalizedCharset !== 'utf-8') {
185
- keptCharset = rule;
186
- }
187
- }
188
- return false;
189
- });
190
-
191
- if (keptCharset) {
192
- return [keptCharset, ...filtered];
193
- }
194
- return filtered;
195
- }
196
-
197
166
  export {
198
167
  analyzePositionTryRules,
199
168
  cleanPositionTryRules,
200
169
  collectRuleMetadata,
201
- filterRedundantCharsets,
202
170
  filterUnusedPositionTry
203
171
  };