@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.
|
|
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.
|
|
35
|
+
"eslint-config-tjw-import-x": "^1.0.2",
|
|
36
36
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
37
|
-
"eslint-plugin-import-x": "^4.
|
|
38
|
-
"eslint-plugin-jsdoc": "^65.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.
|
|
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
|
|
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
|
-
|
|
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
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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}
|
|
163
|
-
* @
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return
|
|
170
|
-
}
|
|
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
|
-
|
|
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
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* selectors nests under
|
|
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
|
|
229
|
-
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
}
|
package/src/rules/stringify.js
CHANGED
|
@@ -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
|
-
|
|
318
|
-
|
|
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
|
/**
|
package/src/value/math/index.js
CHANGED
|
@@ -169,15 +169,17 @@ function simplifyMathFunctionsOnce (value, property) {
|
|
|
169
169
|
*/
|
|
170
170
|
function normalizeMathFunctions (value, property, originalValue = '') {
|
|
171
171
|
let result = value;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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 (
|
|
57
|
+
if (closeIndex === -1) {
|
|
53
58
|
result += value.slice(index, openIndex + 1);
|
|
54
59
|
index = openIndex + 1;
|
|
55
60
|
continue;
|