@thejaredwilcurt/csslop 0.0.53 → 0.0.55

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.53",
5
+ "version": "0.0.55",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/pre/start.js",
@@ -32,15 +32,15 @@
32
32
  "codemirror": "^6.0.2",
33
33
  "eslint": "^10.14.0",
34
34
  "eslint-config-tjw-base": "^5.0.0",
35
- "eslint-config-tjw-import-x": "^1.0.1",
35
+ "eslint-config-tjw-import-x": "^1.0.2",
36
36
  "eslint-config-tjw-jsdoc": "^2.0.1",
37
- "eslint-plugin-import-x": "^4.17.0",
38
- "eslint-plugin-jsdoc": "^65.4.4",
37
+ "eslint-plugin-import-x": "^4.18.1",
38
+ "eslint-plugin-jsdoc": "^65.4.5",
39
39
  "fflate": "^0.8.3",
40
40
  "globals": "^17.14.0",
41
41
  "pretty-ms": "^10.0.0",
42
42
  "real-world-css-libraries": "^1.0.9",
43
- "vite": "^8.3.6"
43
+ "vite": "^8.3.7"
44
44
  },
45
45
  "author": "The Jared Wilcurt",
46
46
  "homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
@@ -12,13 +12,34 @@ import {
12
12
 
13
13
  import {
14
14
  BORDER_EDGE_PROPERTIES,
15
- CSS_WIDE_KEYWORDS
15
+ CSS_WIDE_KEYWORDS,
16
+ expandToLeafProperties
16
17
  } from './config.js';
17
18
  import { collectDeclaredProperties } from './lookup.js';
18
19
  import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
19
20
 
20
21
  const BORDER_TRIO_PROPERTIES = ['border-width', 'border-style', 'border-color'];
21
22
 
23
+ /**
24
+ * Every property the `border` shorthand sets: the physical edge shorthands, such
25
+ * as `border-top`, and the longhands they expand into, such as `border-top-width`.
26
+ * A declaration of one of them beside the trio competes with the shorthand.
27
+ *
28
+ * @type {Set<string>}
29
+ */
30
+ const BORDER_SHORTHAND_PROPERTIES = new Set([
31
+ ...BORDER_EDGE_PROPERTIES,
32
+ ...expandToLeafProperties('border')
33
+ ]);
34
+
35
+ /**
36
+ * Matches the logical border longhands, such as `border-inline-start-color`
37
+ * and `border-block-width`, which set the same edges as the physical ones.
38
+ *
39
+ * @type {RegExp}
40
+ */
41
+ const LOGICAL_BORDER_PATTERN = /^border-(?:inline|block)(?:-|$)/;
42
+
22
43
  /**
23
44
  * The keywords that state a border line style.
24
45
  *
@@ -140,7 +161,7 @@ function collapseBorderTrioWithPerEdgeOverrides (declarations, context) {
140
161
  return entry.property === property;
141
162
  }).length !== 1;
142
163
  }
143
- return property === 'border' || (property.startsWith('border-') && !property.startsWith('border-image'));
164
+ return property === 'border' || BORDER_SHORTHAND_PROPERTIES.has(property) || LOGICAL_BORDER_PATTERN.test(property);
144
165
  });
145
166
  if (hasCompetingBorderWrites) {
146
167
  return declarations;
@@ -179,10 +200,14 @@ function collapseBorderTrioWithPerEdgeOverrides (declarations, context) {
179
200
  }
180
201
 
181
202
  const [widthComponents, styleComponents, colorComponents] = componentsByProperty;
203
+ // The `border` shorthand resets its color to currentcolor, so a color that
204
+ // states currentcolor is left out of the shorthand rather than repeated.
205
+ const statesShorthandColor = colorComponents.length === 1 &&
206
+ colorComponents[0].toLowerCase() !== INITIAL_BORDER_EDGE_PARTS['border-color'];
182
207
  const borderValue = buildBorderShorthandValue(
183
208
  widthComponents.length === 1 ? width : '',
184
209
  styleComponents.length === 1 ? style : '',
185
- colorComponents.length === 1 ? colorComponents[0] : ''
210
+ statesShorthandColor ? colorComponents[0] : ''
186
211
  ) + importantSuffix;
187
212
  const overrides = BORDER_TRIO_PROPERTIES.flatMap((property, index) => {
188
213
  if (componentsByProperty[index].length === 1) {
@@ -155,22 +155,78 @@ function collectSharedParentCandidates (rule) {
155
155
  }
156
156
 
157
157
  /**
158
- * Counts how many characters wrapping a run behind a shared parent selector
159
- * saves: the parent is written once with its braces, and every wrapped
160
- * selector shrinks to its nested form.
158
+ * Counts the characters a list of rules takes up as selector text. Each rule
159
+ * counts its selectors, its braces, and every rule nested within it, while
160
+ * plain declarations count for nothing.
161
161
  *
162
- * @param {Array} run The rules of the run paired with their nested selectors.
163
- * @param {string} parent The shared parent selector string.
164
- * @return {number} The character savings of the wrapper, or a non-positive number when it costs more than it saves.
162
+ * @param {Array} rules The AST entries to measure.
163
+ * @return {number} The number of characters the rules take up.
165
164
  */
166
- function measureWrapperSavings (run, parent) {
167
- const charactersSaved = run.reduce((total, entry) => {
168
- return total + entry.sibling.selectors.reduce((sum, selector, position) => {
169
- return sum + selector.trim().length - entry.nestedSelectors[position].length;
170
- }, 0);
165
+ function measureRuleCharacters (rules) {
166
+ return rules.reduce((total, rule) => {
167
+ if (rule.type !== 'rule') {
168
+ return total;
169
+ }
170
+ const selectorText = (rule.selectors || []).map((selector) => {
171
+ return selector.trim();
172
+ }).join(',');
173
+ const nestedRules = (rule.declarations || []).filter((declaration) => {
174
+ return declaration.type === 'rule';
175
+ });
176
+ // The two braces wrap the rule's declarations
177
+ return total + selectorText.length + 2 + measureRuleCharacters(nestedRules);
171
178
  }, 0);
179
+ }
180
+
181
+ /**
182
+ * Counts how many characters wrapping a run behind a shared parent selector
183
+ * saves: the run is measured as it is written, against the declarations the
184
+ * wrapper would hold plus the parent written once with its braces.
185
+ *
186
+ * @param {Array} run The rules of the run paired with their nested selectors.
187
+ * @param {Array} declarations The declarations the wrapper would hold, including any factoring of the nested rules.
188
+ * @param {string} parent The shared parent selector string.
189
+ * @return {number} The character savings of the wrapper, or a non-positive number when it costs more than it saves.
190
+ */
191
+ function measureWrapperSavings (run, declarations, parent) {
192
+ const charactersBefore = measureRuleCharacters(run.map((entry) => {
193
+ return entry.sibling;
194
+ }));
172
195
  // The wrapper writes the shared selector once plus its surrounding braces.
173
- return charactersSaved - (parent.length + 2);
196
+ const charactersAfter = measureRuleCharacters(declarations) + parent.length + 2;
197
+ return charactersBefore - charactersAfter;
198
+ }
199
+
200
+ /**
201
+ * Expresses every selector of a sibling rule relative to a candidate parent.
202
+ * A nested selector must be shorter than the selector it replaces, so each
203
+ * factoring pass leaves the rules smaller and factoring them again terminates.
204
+ *
205
+ * @param {object} rule The sibling rule whose selectors are nested.
206
+ * @param {function(string): (string|null)} matchNestedSelector The matcher built for the candidate parent.
207
+ * @return {Array|null} The nested selectors, or null when any selector cannot nest under the parent.
208
+ */
209
+ function nestSelectorsUnderParent (rule, matchNestedSelector) {
210
+ const nestedSelectors = [];
211
+ for (const selector of rule.selectors) {
212
+ const trimmedSelector = selector.trim();
213
+ const nestedSelector = matchNestedSelector(trimmedSelector);
214
+ if (nestedSelector === null || nestedSelector.length >= trimmedSelector.length) {
215
+ return null;
216
+ }
217
+ nestedSelectors.push(nestedSelector);
218
+ }
219
+ return nestedSelectors;
220
+ }
221
+
222
+ /**
223
+ * Re-expresses a run entry as a rule whose selectors are the nested selectors.
224
+ *
225
+ * @param {object} entry A run entry pairing a sibling rule with its nested selectors.
226
+ * @return {object} The rule, with its selectors replaced by the nested ones.
227
+ */
228
+ function createNestedChild (entry) {
229
+ return { ...entry.sibling, selectors: entry.nestedSelectors };
174
230
  }
175
231
 
176
232
  /**
@@ -187,15 +243,49 @@ function orderRulesBySourcePosition (rules) {
187
243
  });
188
244
  }
189
245
 
246
+ /**
247
+ * Builds the declarations of a wrapper that holds a run of sibling rules nested
248
+ * under their shared parent. A first rule whose only nested selector is `&` is
249
+ * the parent itself, so its declarations are hoisted into the wrapper. Otherwise
250
+ * the nested rules are factored again, so the selectors they still share can be
251
+ * wrapped in turn.
252
+ *
253
+ * @param {Array} run The rules of the run paired with their nested selectors.
254
+ * @return {Array} The declarations of the wrapper rule.
255
+ */
256
+ function createWrapperDeclarations (run) {
257
+ const firstEntry = run[0];
258
+ const firstSelectorIsParent = (
259
+ firstEntry.nestedSelectors.length === 1 &&
260
+ firstEntry.nestedSelectors[0] === '&'
261
+ );
262
+ if (!firstSelectorIsParent) {
263
+ return factorCommonParents(run.map(createNestedChild), true);
264
+ }
265
+ const directDeclarations = firstEntry.sibling.declarations;
266
+ const directNestedRules = directDeclarations.filter((declaration) => {
267
+ return declaration.type === 'rule';
268
+ });
269
+ const directEntries = directDeclarations.filter((declaration) => {
270
+ return declaration.type !== 'rule';
271
+ });
272
+ const children = run.slice(1).map(createNestedChild);
273
+ return [
274
+ ...directEntries,
275
+ ...orderRulesBySourcePosition([...directNestedRules, ...children])
276
+ ];
277
+ }
278
+
190
279
  /**
191
280
  * Groups consecutive sibling rules that share a common leading selector
192
281
  * portion (such as `.foo` in `.foo .a`, `.foo .b`, or `.menu .item` in
193
282
  * `.menu .item:hover .x`) into a synthesized parent rule with the shared
194
283
  * portion factored out. Every viable prefix of the first rule's selector is
195
284
  * tried as the parent, and the grouping that saves the most characters wins,
196
- * but only when it saves more than the wrapper itself costs. Rules with a
197
- * comma-separated selector list can join a run when every one of their
198
- * selectors nests under the parent.
285
+ * measured with the rules nested under it factored in turn, but only when it
286
+ * saves more than the wrapper itself costs. Rules with a comma-separated
287
+ * selector list can join a run when every one of their selectors nests under
288
+ * the parent.
199
289
  *
200
290
  * @param {Array} rules The flat AST rule nodes to factor.
201
291
  * @param {boolean} allowTrailingCompound True when the rules are children of a factored wrapper, whose `&`-relative selectors may share a trailing compound that can be factored out in turn.
@@ -225,12 +315,8 @@ function factorCommonParents (rules, allowTrailingCompound = false) {
225
315
  if (sibling.type !== 'rule' || !sibling.selectors?.length) {
226
316
  break;
227
317
  }
228
- const nestedSelectors = sibling.selectors.map((selector) => {
229
- return matchNestedSelector(selector.trim());
230
- });
231
- if (nestedSelectors.some((nestedSelector) => {
232
- return nestedSelector === null;
233
- })) {
318
+ const nestedSelectors = nestSelectorsUnderParent(sibling, matchNestedSelector);
319
+ if (!nestedSelectors) {
234
320
  break;
235
321
  }
236
322
  run.push({ sibling, nestedSelectors });
@@ -243,12 +329,13 @@ function factorCommonParents (rules, allowTrailingCompound = false) {
243
329
  if (run.length < 2 && selectorCount < 2) {
244
330
  continue;
245
331
  }
246
- const savings = measureWrapperSavings(run, candidateParent);
332
+ const declarations = createWrapperDeclarations(run);
333
+ const savings = measureWrapperSavings(run, declarations, candidateParent);
247
334
  if (savings > 0 && (!bestGrouping || savings > bestGrouping.savings)) {
248
335
  bestGrouping = {
336
+ declarations,
249
337
  lookahead,
250
338
  parent: candidateParent,
251
- run,
252
339
  savings
253
340
  };
254
341
  }
@@ -262,39 +349,10 @@ function factorCommonParents (rules, allowTrailingCompound = false) {
262
349
  index++;
263
350
  continue;
264
351
  }
265
- const firstEntry = bestGrouping.run[0];
266
- const firstSelectorIsParent = (
267
- firstEntry.nestedSelectors.length === 1 &&
268
- firstEntry.nestedSelectors[0] === '&'
269
- );
270
- const directDeclarations = firstSelectorIsParent ?
271
- firstEntry.sibling.declarations :
272
- [];
273
- const entriesToNest = firstSelectorIsParent ?
274
- bestGrouping.run.slice(1) :
275
- bestGrouping.run;
276
- const children = entriesToNest.map((entry) => {
277
- return { ...entry.sibling, selectors: entry.nestedSelectors };
278
- });
279
- let declarations;
280
- if (firstSelectorIsParent) {
281
- const directNestedRules = directDeclarations.filter((declaration) => {
282
- return declaration.type === 'rule';
283
- });
284
- const directEntries = directDeclarations.filter((declaration) => {
285
- return declaration.type !== 'rule';
286
- });
287
- declarations = [
288
- ...directEntries,
289
- ...orderRulesBySourcePosition([...directNestedRules, ...children])
290
- ];
291
- } else {
292
- declarations = factorCommonParents(children, true);
293
- }
294
352
  result.push({
295
353
  type: 'rule',
296
354
  selectors: [bestGrouping.parent],
297
- declarations
355
+ declarations: bestGrouping.declarations
298
356
  });
299
357
  index = bestGrouping.lookahead;
300
358
  }
@@ -2,6 +2,8 @@
2
2
  * @file Nests flat rules under a preceding rule whose selector their own selector starts with, so CSS nesting writes the shared part once.
3
3
  */
4
4
 
5
+ import { findTopLevelDelimiter } from '../../parser/source-search.js';
6
+
5
7
  /**
6
8
  * Matches the part of a child selector that follows its parent when that part
7
9
  * starts with an explicit combinator. Optional whitespace, then the combinator
@@ -78,6 +80,46 @@ function isSelectorWhitespace (character) {
78
80
  return (/\s/).test(character);
79
81
  }
80
82
 
83
+ /**
84
+ * The characters that can end a parent selector: the colon of a pseudo-class,
85
+ * the whitespace of a descendant combinator, and the explicit combinators.
86
+ *
87
+ * @type {string}
88
+ */
89
+ const PARENT_BOUNDARY_CHARACTERS = ':>+~ \t\n\r\f';
90
+
91
+ /**
92
+ * Records the parent selector that a boundary character would end, when it
93
+ * ends one. Boundaries are only found at the top level of the selector, so
94
+ * whitespace inside an attribute selector or a pseudo-class argument, and
95
+ * characters escaped with a backslash, never end a parent.
96
+ *
97
+ * @param {string} childSelector The trimmed child selector.
98
+ * @param {number} boundaryIndex The index of the boundary character within the child selector.
99
+ * @param {Set} candidates The set the candidate parent selector is added to.
100
+ */
101
+ function addCandidateParentSelector (childSelector, boundaryIndex, candidates) {
102
+ const character = childSelector[boundaryIndex];
103
+ if (character === ':' || isSelectorWhitespace(character)) {
104
+ // The second colon of a pseudo-element is part of its `::` introducer,
105
+ // not a boundary between a potential parent and nested child.
106
+ if (character === ':' && childSelector[boundaryIndex - 1] === ':') {
107
+ return;
108
+ }
109
+ candidates.add(childSelector.slice(0, boundaryIndex));
110
+ return;
111
+ }
112
+ // A combinator may be separated from its parent by whitespace, which
113
+ // belongs to neither side, so the parent ends where that run begins.
114
+ let boundary = boundaryIndex;
115
+ while (boundary > 0 && isSelectorWhitespace(childSelector[boundary - 1])) {
116
+ boundary--;
117
+ }
118
+ if (boundary > 0) {
119
+ candidates.add(childSelector.slice(0, boundary));
120
+ }
121
+ }
122
+
81
123
  /**
82
124
  * Collects every prefix of a child selector that could act as its nesting
83
125
  * parent. A parent always ends immediately before a pseudo-class colon, a
@@ -91,29 +133,10 @@ function isSelectorWhitespace (character) {
91
133
  */
92
134
  function collectCandidateParentSelectors (childSelector) {
93
135
  const candidates = new Set();
94
- for (let index = 1; index < childSelector.length; index++) {
95
- const character = childSelector[index];
96
- if (character === ':' || isSelectorWhitespace(character)) {
97
- // The second colon of a pseudo-element is part of its `::` introducer,
98
- // not a boundary between a potential parent and nested child.
99
- if (character === ':' && childSelector[index - 1] === ':') {
100
- continue;
101
- }
102
- candidates.add(childSelector.slice(0, index));
103
- continue;
104
- }
105
- if (character !== '>' && character !== '+' && character !== '~') {
106
- continue;
107
- }
108
- // A combinator may be separated from its parent by whitespace, which
109
- // belongs to neither side, so the parent ends where that run begins.
110
- let boundary = index;
111
- while (boundary > 0 && isSelectorWhitespace(childSelector[boundary - 1])) {
112
- boundary--;
113
- }
114
- if (boundary > 0) {
115
- candidates.add(childSelector.slice(0, boundary));
116
- }
136
+ let boundaryIndex = findTopLevelDelimiter(childSelector, PARENT_BOUNDARY_CHARACTERS, 1);
137
+ while (boundaryIndex !== -1) {
138
+ addCandidateParentSelector(childSelector, boundaryIndex, candidates);
139
+ boundaryIndex = findTopLevelDelimiter(childSelector, PARENT_BOUNDARY_CHARACTERS, boundaryIndex + 1);
117
140
  }
118
141
  return candidates;
119
142
  }
@@ -306,6 +306,83 @@ function simplifyNthPseudoClasses (selector) {
306
306
  return minified;
307
307
  }
308
308
 
309
+ /**
310
+ * Matches an attribute selector's brackets: the attribute name, the matcher
311
+ * (such as `=` or `^=`), the value, and the optional case-sensitivity flag `i`
312
+ * or `s`. The value is a double-quoted string, a single-quoted string, or a run
313
+ * of unquoted characters in which escapes may stand in for spaces.
314
+ *
315
+ * @type {RegExp}
316
+ */
317
+ const ATTRIBUTE_SELECTOR_PATTERN = /\[\s*([^\s=\]]+?)\s*([~|^$*]?=)\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|(?:\\.|[^\s"'\\\]])+)(?:\s*([is]))?\s*\]/gi;
318
+
319
+ /**
320
+ * Matches a value that can be written without quotes: an identifier made of
321
+ * letters, digits, hyphens, underscores, and escapes. It cannot start with a
322
+ * digit, or with a hyphen followed by a digit.
323
+ *
324
+ * @type {RegExp}
325
+ */
326
+ const UNQUOTED_ATTRIBUTE_VALUE_PATTERN = /^(?:-?[a-zA-Z_]|\\.)(?:[\w-]|\\.)*$/;
327
+
328
+ /**
329
+ * Writes the text of a quoted attribute value without its quotes, escaping the
330
+ * characters that would otherwise end an unquoted identifier.
331
+ *
332
+ * @param {string} content The value's text, without its quotes.
333
+ * @return {string|null} The unquoted value, or null when it cannot be written without quotes.
334
+ */
335
+ function escapeAttributeValue (content) {
336
+ if (content.includes('\\')) {
337
+ return null;
338
+ }
339
+ // Escape special characters that require quoting (spaces, #, ., :, /, ;)
340
+ const escaped = content.replace(/([ #.:/;])/g, '\\$1');
341
+ if (!UNQUOTED_ATTRIBUTE_VALUE_PATTERN.test(escaped)) {
342
+ return null;
343
+ }
344
+ return escaped;
345
+ }
346
+
347
+ /**
348
+ * Writes an attribute value with double quotes, removing the escapes that only
349
+ * an unquoted value needs. A value that already has quotes keeps them, unless
350
+ * a single-quoted value holds a double quote that would have to be escaped.
351
+ *
352
+ * @param {string} rawValue The value as written, with or without quotes.
353
+ * @return {string} The value wrapped in double quotes.
354
+ */
355
+ function quoteAttributeValue (rawValue) {
356
+ if (rawValue[0] === '"' || rawValue[0] === '\'') {
357
+ const content = rawValue.slice(1, -1);
358
+ return content.includes('"') ? rawValue : '"' + content + '"';
359
+ }
360
+ // Unescape the characters that only an unquoted value needs escaped
361
+ return '"' + rawValue.replace(/\\([ #.:/;])/g, '$1') + '"';
362
+ }
363
+
364
+ /**
365
+ * Writes an attribute value and its case-sensitivity flag in whichever form is
366
+ * shorter. A value written with quotes stays quoted on a tie, and a value written
367
+ * without quotes stays unquoted. A flag after an unquoted value needs a space,
368
+ * but a flag after a quoted value does not.
369
+ *
370
+ * @param {string} rawValue The value as written, including its quotes when it has them.
371
+ * @param {string|null} flag The flag as written, or null when there is none.
372
+ * @return {string} The value followed by the flag, in its shortest form.
373
+ */
374
+ function formatAttributeValue (rawValue, flag) {
375
+ const isQuoted = rawValue[0] === '"' || rawValue[0] === '\'';
376
+ const quotedForm = quoteAttributeValue(rawValue) + (flag || '');
377
+ const unquotedValue = isQuoted ? escapeAttributeValue(rawValue.slice(1, -1)) : rawValue;
378
+ if (unquotedValue === null) {
379
+ return quotedForm;
380
+ }
381
+ const unquotedForm = unquotedValue + (flag ? ' ' + flag : '');
382
+ const useUnquoted = isQuoted ? unquotedForm.length < quotedForm.length : unquotedForm.length <= quotedForm.length;
383
+ return useUnquoted ? unquotedForm : quotedForm;
384
+ }
385
+
309
386
  /**
310
387
  * Writes each attribute selector's value quoted or escaped, whichever is
311
388
  * shorter, and removes the whitespace inside its brackets.
@@ -314,35 +391,9 @@ function simplifyNthPseudoClasses (selector) {
314
391
  * @return {string} The selector with minified attribute selectors.
315
392
  */
316
393
  function minifyAttributeSelectors (selector) {
317
- let minified = selector;
318
- // Minify double-quoted attribute selectors: remove inner whitespace and escape when shorter
319
- minified = minified.replace(/\[\s*([^=]+)\s*=\s*"(.*?)"\s*\]/g, (match, attribute, value) => {
320
- // Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
321
- let escaped = value.replace(/([ #.:/;])/g, '\\$1');
322
- if (escaped.length < value.length + 2) {
323
- return '[' + attribute + '=' + escaped + ']';
324
- }
325
- return '[' + attribute + '="' + value + '"]';
326
- });
327
- // Minify single-quoted attribute selectors: remove inner whitespace and escape when shorter
328
- minified = minified.replace(/\[\s*([^=]+)\s*=\s*'(.*?)'\s*\]/g, (match, attribute, value) => {
329
- // Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
330
- let escaped = value.replace(/([ #.:/;])/g, '\\$1');
331
- if (escaped.length < value.length + 2) {
332
- return '[' + attribute + '=' + escaped + ']';
333
- }
334
- return '[' + attribute + '="' + value + '"]';
335
- });
336
- // Minify unquoted attribute selectors: quote when unescaping produces a shorter result
337
- minified = minified.replace(/\[\s*([^=]+)\s*=\s*([^"'].*?)\s*\]/g, (match, attribute, value) => {
338
- // Unescape special characters (spaces, #, ., :, /, ;) and compare with quoted form
339
- let unescaped = value.replace(/\\([ #.:/;])/g, '$1');
340
- if (unescaped.length + 2 < value.length) {
341
- return '[' + attribute + '="' + unescaped + '"]';
342
- }
343
- return '[' + attribute + '=' + value + ']';
394
+ return selector.replace(ATTRIBUTE_SELECTOR_PATTERN, (match, attribute, matcher, rawValue, flag) => {
395
+ return '[' + attribute + matcher + formatAttributeValue(rawValue, flag) + ']';
344
396
  });
345
- return minified;
346
397
  }
347
398
 
348
399
  /**
@@ -169,15 +169,17 @@ function simplifyMathFunctionsOnce (value, property) {
169
169
  */
170
170
  function normalizeMathFunctions (value, property, originalValue = '') {
171
171
  let result = value;
172
- let previous;
173
-
174
- do {
175
- previous = result;
176
- // Resolving a math function leaves a plain value where it stood, which can
177
- // bring the function it was nested in within reach of the next pass, so
178
- // the value is simplified until it settles
179
- result = simplifyMathFunctionsOnce(result, property);
180
- } while (result !== previous);
172
+ if (value.includes('(')) {
173
+ let previous;
174
+
175
+ do {
176
+ previous = result;
177
+ // Resolving a math function leaves a plain value where it stood, which can
178
+ // bring the function it was nested in within reach of the next pass, so
179
+ // the value is simplified until it settles
180
+ result = simplifyMathFunctionsOnce(result, property);
181
+ } while (result !== previous);
182
+ }
181
183
 
182
184
  // When calc() folded to an absolute-length unit (pt, pc, in, cm, mm, q), convert to pixels
183
185
  if (originalValue.includes('calc(') && /^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i.test(result)) {
@@ -48,8 +48,13 @@ function rewriteMathFunctionContents (rewriteExpression, value) {
48
48
  break;
49
49
  }
50
50
  const functionName = value.slice(findFunctionNameStart(value, openIndex), openIndex).toLowerCase();
51
+ if (!MATH_FUNCTION_NAMES.has(functionName)) {
52
+ result += value.slice(index, openIndex + 1);
53
+ index = openIndex + 1;
54
+ continue;
55
+ }
51
56
  const closeIndex = findMatchingParenthesis(value, openIndex);
52
- if (!MATH_FUNCTION_NAMES.has(functionName) || closeIndex === -1) {
57
+ if (closeIndex === -1) {
53
58
  result += value.slice(index, openIndex + 1);
54
59
  index = openIndex + 1;
55
60
  continue;