@thejaredwilcurt/csslop 0.0.23 → 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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/charset.js +72 -11
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.23",
5
+ "version": "0.0.24",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/prestart.js",
@@ -29,7 +29,7 @@
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",
package/src/charset.js CHANGED
@@ -117,6 +117,64 @@ function createShortestLabelLookup () {
117
117
  const ENCODING_BY_LABEL = createEncodingByLabelLookup();
118
118
  const SHORTEST_LABEL_BY_ENCODING = createShortestLabelLookup();
119
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
+
120
178
  /**
121
179
  * Reduces a raw `@charset` value to the label that gets looked up, by removing
122
180
  * the surrounding quotes, trimming whitespace, and lowercasing it, since
@@ -188,18 +246,14 @@ function optimizeCharsetValue (charsetValue) {
188
246
  /**
189
247
  * Finds the `@charset` value that applies to a stylesheet by scanning the raw
190
248
  * CSS text before it is parsed. Only the first `@charset` of a document has any
191
- * effect, so later ones (usually the result of concatenating files) are ignored.
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.
192
251
  *
193
252
  * @param {string} css The raw CSS string to scan.
194
- * @return {string} The first `@charset` value (with quotes), or empty string when none is declared.
253
+ * @return {string} The first declared `@charset` value (with quotes), or empty string when none is declared.
195
254
  */
196
255
  function detectCharset (css) {
197
- // Match @charset followed by a quoted value and semicolon
198
- const match = css.match(/@charset\s+(["'][^"']+["'])\s*;/i);
199
- if (match) {
200
- return match[1];
201
- }
202
- return '';
256
+ return readEncodingDeclarationValue(css, ENCODING_DECLARATION_PATTERN);
203
257
  }
204
258
 
205
259
  /**
@@ -207,7 +261,9 @@ function detectCharset (css) {
207
261
  * stylesheet. Only the first `@charset` is meaningful, and it is only honored
208
262
  * when it is the very first thing in the file, so it is shortened and hoisted
209
263
  * to the front while every later `@charset` is dropped. A first `@charset` that
210
- * leaves the stylesheet in the default UTF-8 encoding is dropped as well.
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.
211
267
  *
212
268
  * @param {Array} rules The top-level AST rule nodes to filter.
213
269
  * @return {Array} A new array of rules, with at most one `@charset` rule, placed at the start.
@@ -220,13 +276,18 @@ function filterRedundantCharsets (rules) {
220
276
  if (rule.type !== 'charset') {
221
277
  return true;
222
278
  }
279
+ const declaredValue = readCharsetRuleValue(rule);
280
+ if (!declaredValue) {
281
+ return false;
282
+ }
223
283
  if (!foundCharset) {
224
284
  foundCharset = true;
225
- const optimizedCharset = optimizeCharsetValue(rule.charset);
285
+ const optimizedCharset = optimizeCharsetValue(declaredValue);
226
286
  if (optimizedCharset) {
227
287
  hoistedCharset = {
228
288
  ...rule,
229
- charset: optimizedCharset
289
+ charset: optimizedCharset,
290
+ rawSource: '@charset ' + optimizedCharset + ';'
230
291
  };
231
292
  }
232
293
  }