@thejaredwilcurt/csslop 0.0.54 → 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 +3 -3
- package/src/rules/optimize/nest-rules.js +46 -23
- package/src/rules/stringify.js +79 -28
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",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"eslint-config-tjw-base": "^5.0.0",
|
|
35
35
|
"eslint-config-tjw-import-x": "^1.0.2",
|
|
36
36
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
37
|
-
"eslint-plugin-import-x": "^4.18.
|
|
37
|
+
"eslint-plugin-import-x": "^4.18.1",
|
|
38
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",
|
|
@@ -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
|
/**
|