@thejaredwilcurt/csslop 0.0.32 → 0.0.33
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 +4 -4
- package/src/declarations/config.js +88 -0
- package/src/value/minify.js +81 -141
- package/src/value/tokens.js +527 -0
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.33",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/prestart.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@codemirror/autocomplete": "^6.20.3",
|
|
29
29
|
"@codemirror/lang-css": "^6.3.1",
|
|
30
|
-
"@codemirror/view": "^6.43.
|
|
30
|
+
"@codemirror/view": "^6.43.10",
|
|
31
31
|
"@eslint/js": "^10.0.1",
|
|
32
32
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
33
33
|
"codemirror": "^6.0.2",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
37
37
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
38
38
|
"eslint-plugin-import-x": "^4.17.0",
|
|
39
|
-
"eslint-plugin-jsdoc": "^64.
|
|
39
|
+
"eslint-plugin-jsdoc": "^64.3.4",
|
|
40
40
|
"fflate": "^0.8.3",
|
|
41
|
-
"globals": "^17.
|
|
41
|
+
"globals": "^17.12.0",
|
|
42
42
|
"pretty-ms": "^9.3.1",
|
|
43
43
|
"real-world-css-libraries": "^1.0.8",
|
|
44
44
|
"vite": "^8.2.2"
|
|
@@ -199,6 +199,93 @@ function getOverridesOf (shorthandName) {
|
|
|
199
199
|
return OVERRIDES_BY_SHORTHAND.get(shorthandName) || NO_PROPERTIES;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* The words a longhand adds to its shorthand's name to say which side, axis,
|
|
204
|
+
* corner, or alignment dimension of the box that longhand applies to, such as
|
|
205
|
+
* the `top` of `padding-top` or the `row` of `row-gap`.
|
|
206
|
+
*
|
|
207
|
+
* @type {Set<string>}
|
|
208
|
+
*/
|
|
209
|
+
const BOX_PART_WORDS = new Set([
|
|
210
|
+
'align',
|
|
211
|
+
'block',
|
|
212
|
+
'bottom',
|
|
213
|
+
'column',
|
|
214
|
+
'end',
|
|
215
|
+
'inline',
|
|
216
|
+
'justify',
|
|
217
|
+
'left',
|
|
218
|
+
'right',
|
|
219
|
+
'row',
|
|
220
|
+
'start',
|
|
221
|
+
'top',
|
|
222
|
+
'x',
|
|
223
|
+
'y'
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Removes each of the shorthand's own words from a longhand's words, leaving
|
|
228
|
+
* only the words the longhand adds to name the part of the box it covers. Each
|
|
229
|
+
* shared word is removed once, so the `border` and the `radius` of
|
|
230
|
+
* `border-radius` leave `top` and `left` behind in `border-top-left-radius`.
|
|
231
|
+
*
|
|
232
|
+
* @param {Array} longhandWords The hyphen-separated words of the longhand's name.
|
|
233
|
+
* @param {Array} shorthandWords The hyphen-separated words of the shorthand's name.
|
|
234
|
+
* @return {Array} The words the longhand adds on top of the shorthand's.
|
|
235
|
+
*/
|
|
236
|
+
function subtractSharedWords (longhandWords, shorthandWords) {
|
|
237
|
+
const remainingWords = [...longhandWords];
|
|
238
|
+
for (const shorthandWord of shorthandWords) {
|
|
239
|
+
const wordIndex = remainingWords.indexOf(shorthandWord);
|
|
240
|
+
if (wordIndex !== -1) {
|
|
241
|
+
remainingWords.splice(wordIndex, 1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return remainingWords;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Whether each shorthand takes a positional list of components, computed on
|
|
249
|
+
* first use, since the shorthand tables never change.
|
|
250
|
+
*
|
|
251
|
+
* @type {Map<string, boolean>}
|
|
252
|
+
*/
|
|
253
|
+
const positionalComponentsByShorthand = new Map();
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Reports whether a shorthand's value is a positional list of same-typed
|
|
257
|
+
* components rather than an unordered set of components that its grammar tells
|
|
258
|
+
* apart by type. A shorthand is positional when its longhands are the very same
|
|
259
|
+
* property repeated for each part of the box, as `padding` repeats a length for
|
|
260
|
+
* each side and `gap` repeats one for each axis. Nothing but the order the
|
|
261
|
+
* components are written in says which part of the box each one lands on, so
|
|
262
|
+
* the whitespace between them delimits the list. A shorthand such as `border`
|
|
263
|
+
* or `font`, whose longhands each hold a different kind of value, is not
|
|
264
|
+
* positional: its grammar reads each component by type, in any order.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} shorthandName The CSS property name to test.
|
|
267
|
+
* @return {boolean} Whether the shorthand's components are positional.
|
|
268
|
+
*/
|
|
269
|
+
function hasPositionalComponents (shorthandName) {
|
|
270
|
+
const cachedAnswer = positionalComponentsByShorthand.get(shorthandName);
|
|
271
|
+
if (cachedAnswer !== undefined) {
|
|
272
|
+
return cachedAnswer;
|
|
273
|
+
}
|
|
274
|
+
const longhands = shorthandMap[shorthandName];
|
|
275
|
+
let isPositional = false;
|
|
276
|
+
if (Array.isArray(longhands)) {
|
|
277
|
+
const shorthandWords = shorthandName.split('-');
|
|
278
|
+
isPositional = longhands.every((longhand) => {
|
|
279
|
+
const addedWords = subtractSharedWords(longhand.split('-'), shorthandWords);
|
|
280
|
+
return Boolean(addedWords.length) && addedWords.every((word) => {
|
|
281
|
+
return BOX_PART_WORDS.has(word);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
positionalComponentsByShorthand.set(shorthandName, isPositional);
|
|
286
|
+
return isPositional;
|
|
287
|
+
}
|
|
288
|
+
|
|
202
289
|
/**
|
|
203
290
|
* The leaf longhands each property ultimately sets, computed on first use. The
|
|
204
291
|
* shorthand tables never change, so a property always expands the same way.
|
|
@@ -242,6 +329,7 @@ export {
|
|
|
242
329
|
expandToLeafProperties,
|
|
243
330
|
getLonghandsOf,
|
|
244
331
|
getOverridesOf,
|
|
332
|
+
hasPositionalComponents,
|
|
245
333
|
shorthandMap,
|
|
246
334
|
shorthandOverrideMap,
|
|
247
335
|
UNIFORM_VALUE_SHORTHANDS
|
package/src/value/minify.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { isUnicodeCharset } from '../context.js';
|
|
6
|
+
import { hasPositionalComponents } from '../declarations/config.js';
|
|
6
7
|
import { resolveUnicodeEscape } from '../utilities.js';
|
|
7
8
|
|
|
8
9
|
import { evaluateColorMix } from './color-mix.js';
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
roundCompactNumber
|
|
38
39
|
} from './shared.js';
|
|
39
40
|
import { findMatchingParenthesis } from './syntax.js';
|
|
41
|
+
import { elideRedundantSeparators } from './tokens.js';
|
|
40
42
|
import { minifyTransformValue } from './transforms.js';
|
|
41
43
|
import { optimizeUnicodeRange } from './unicode-range.js';
|
|
42
44
|
|
|
@@ -200,20 +202,6 @@ function splitValueSegments (value) {
|
|
|
200
202
|
return segments;
|
|
201
203
|
}
|
|
202
204
|
|
|
203
|
-
/**
|
|
204
|
-
* Reports whether a segment is a quoted string. Every literal segment is either
|
|
205
|
-
* a quoted string or a url() token, so anything else is one of the latter.
|
|
206
|
-
*
|
|
207
|
-
* @param {ValueSegment} [segment] The segment to test, when the value has one there.
|
|
208
|
-
* @return {boolean} Whether the segment is a quoted string.
|
|
209
|
-
*/
|
|
210
|
-
function isQuotedStringSegment (segment) {
|
|
211
|
-
if (!segment?.isLiteral) {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
return segment.text.startsWith('"') || segment.text.startsWith('\'');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
205
|
/**
|
|
218
206
|
* Applies a replacer function only to segments of a CSS value that are outside quoted strings and url() functions, preserving those literal segments unchanged.
|
|
219
207
|
*
|
|
@@ -246,106 +234,6 @@ function lowercaseHexColors (value) {
|
|
|
246
234
|
});
|
|
247
235
|
}
|
|
248
236
|
|
|
249
|
-
/**
|
|
250
|
-
* Removes whitespace that precedes a hex color token. A `#` unambiguously starts
|
|
251
|
-
* a hash token in CSS, so no separator is required between it and a preceding
|
|
252
|
-
* ident, keyword, or number (e.g. `1px solid #f00` becomes `1px solid#f00`).
|
|
253
|
-
*
|
|
254
|
-
* @param {string} value The CSS value string.
|
|
255
|
-
* @return {string} The value with spaces before hex colors removed.
|
|
256
|
-
*/
|
|
257
|
-
function elideSpaceBeforeHexColors (value) {
|
|
258
|
-
return replaceOutsideStringsAndUrls(value, (segment) => {
|
|
259
|
-
// Match whitespace followed by a hex color token of 3 to 8 hex digits
|
|
260
|
-
return segment.replace(/\s+#([0-9a-fA-F]{3,8})\b/gi, '#$1');
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Matches the separator whitespace at the head of a value segment. Whitespace
|
|
266
|
-
* that precedes a `+` or a `-` is left alone, because a math function requires
|
|
267
|
-
* those two operators to be surrounded by it.
|
|
268
|
-
*
|
|
269
|
-
* @type {RegExp}
|
|
270
|
-
*/
|
|
271
|
-
const LEADING_SEPARATOR_PATTERN = /^\s+(?![+-])/;
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Removes the separator whitespace that follows a closing parenthesis. A
|
|
275
|
-
* parenthesis ends its own token, so the whitespace after one never keeps two
|
|
276
|
-
* tokens from merging: `url(a.png) 30 round` holds the same tokens as
|
|
277
|
-
* `url(a.png)30 round`.
|
|
278
|
-
*
|
|
279
|
-
* @param {string} value The CSS value string.
|
|
280
|
-
* @return {string} The value without the redundant separators.
|
|
281
|
-
*/
|
|
282
|
-
function elideSpaceAfterParentheses (value) {
|
|
283
|
-
const segments = splitValueSegments(value);
|
|
284
|
-
return segments.map((segment, index) => {
|
|
285
|
-
if (segment.isLiteral) {
|
|
286
|
-
return segment.text;
|
|
287
|
-
}
|
|
288
|
-
let text = segment.text;
|
|
289
|
-
const previousSegment = segments[index - 1];
|
|
290
|
-
// A url() token ends with the parenthesis that closes it, so whitespace at
|
|
291
|
-
// the head of the segment after one is a separator of the same kind
|
|
292
|
-
if (previousSegment?.isLiteral && !isQuotedStringSegment(previousSegment)) {
|
|
293
|
-
text = text.replace(LEADING_SEPARATOR_PATTERN, '');
|
|
294
|
-
}
|
|
295
|
-
// Match whitespace that follows a closing parenthesis
|
|
296
|
-
return text.replace(/\)\s+(?![+-])/g, ')');
|
|
297
|
-
}).join('');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Removes the separator whitespace that follows a closing quote. A string ends
|
|
302
|
-
* its own token, so no separator is needed between it and the token that comes
|
|
303
|
-
* next: `"smcp" 1` holds the same tokens as `"smcp"1`.
|
|
304
|
-
*
|
|
305
|
-
* @param {string} value The CSS value string.
|
|
306
|
-
* @return {string} The value without the redundant separators.
|
|
307
|
-
*/
|
|
308
|
-
function elideSpaceAfterStrings (value) {
|
|
309
|
-
const segments = splitValueSegments(value);
|
|
310
|
-
return segments.map((segment, index) => {
|
|
311
|
-
if (segment.isLiteral || !isQuotedStringSegment(segments[index - 1])) {
|
|
312
|
-
return segment.text;
|
|
313
|
-
}
|
|
314
|
-
return segment.text.replace(LEADING_SEPARATOR_PATTERN, '');
|
|
315
|
-
}).join('');
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Reports whether a value separates top-level entries with commas, the way the
|
|
320
|
-
* repeatable values of properties such as `font-variation-settings` and
|
|
321
|
-
* `transition` do. Every literal is skipped, so a comma inside a string or a
|
|
322
|
-
* url does not count, and the parenthesis depth tells a function's argument
|
|
323
|
-
* separators apart from the value's own.
|
|
324
|
-
*
|
|
325
|
-
* @param {string} value The CSS value string.
|
|
326
|
-
* @return {boolean} Whether the value holds more than one comma-separated entry.
|
|
327
|
-
*/
|
|
328
|
-
function hasCommaSeparatedEntries (value) {
|
|
329
|
-
let depth = 0;
|
|
330
|
-
for (const segment of splitValueSegments(value)) {
|
|
331
|
-
if (segment.isLiteral) {
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
for (const character of segment.text) {
|
|
335
|
-
if (character === '(') {
|
|
336
|
-
depth++;
|
|
337
|
-
}
|
|
338
|
-
if (character === ')') {
|
|
339
|
-
depth = Math.max(0, depth - 1);
|
|
340
|
-
}
|
|
341
|
-
if (character === ',' && depth === 0) {
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
237
|
/**
|
|
350
238
|
* Restores the whitespace that a math function requires before a `+` or a `-`
|
|
351
239
|
* operator. Both operators have to be surrounded by whitespace to be read as
|
|
@@ -965,6 +853,18 @@ const PUNCTUATED_COMPONENT_PROPERTIES = new Set([
|
|
|
965
853
|
'transform'
|
|
966
854
|
]);
|
|
967
855
|
|
|
856
|
+
/**
|
|
857
|
+
* Reports whether a property is a custom property. A custom property holds an
|
|
858
|
+
* arbitrary token stream rather than a typed value, so every one of its tokens,
|
|
859
|
+
* whitespace included, is part of what it stores.
|
|
860
|
+
*
|
|
861
|
+
* @param {string} property The CSS property name.
|
|
862
|
+
* @return {boolean} Whether the property is a custom property.
|
|
863
|
+
*/
|
|
864
|
+
function isCustomProperty (property) {
|
|
865
|
+
return Boolean(property) && property.startsWith('--');
|
|
866
|
+
}
|
|
867
|
+
|
|
968
868
|
/**
|
|
969
869
|
* Matches one offset of a position: an edge keyword or a numeric distance.
|
|
970
870
|
*
|
|
@@ -1032,6 +932,46 @@ function normalizeImagePositionSeparator (value) {
|
|
|
1032
932
|
return joinedToUrl.replace(SEPARATED_IMAGE_SIZE_POSITION_PATTERN, ')$1');
|
|
1033
933
|
}
|
|
1034
934
|
|
|
935
|
+
/**
|
|
936
|
+
* Matches the keywords that state a border line style.
|
|
937
|
+
*
|
|
938
|
+
* @type {string}
|
|
939
|
+
*/
|
|
940
|
+
const BORDER_STYLE_KEYWORD_SOURCE = 'solid|dashed|dotted|double|groove|ridge|inset|outset|hidden|none';
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Matches the ways a border line width is stated: one of its three keywords, a
|
|
944
|
+
* length, or a math function that resolves to one.
|
|
945
|
+
*
|
|
946
|
+
* @type {string}
|
|
947
|
+
*/
|
|
948
|
+
const BORDER_WIDTH_SOURCE = 'thin|medium|thick|[+-]?(?:\\d+|\\d*\\.\\d+)[a-z]*|(?:calc|min|max|clamp)\\([^()]*\\)';
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Matches a `border` value that states its line style in front of its line
|
|
952
|
+
* width, with the two captured so they can be swapped.
|
|
953
|
+
*
|
|
954
|
+
* @type {RegExp}
|
|
955
|
+
*/
|
|
956
|
+
const BORDER_STYLE_BEFORE_WIDTH_PATTERN = new RegExp(
|
|
957
|
+
'^(' + BORDER_STYLE_KEYWORD_SOURCE + ')\\s+(' + BORDER_WIDTH_SOURCE + ')(?=\\s|$)',
|
|
958
|
+
'i'
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Rewrites a `border` value that leads with its line style into the canonical
|
|
963
|
+
* width-style-color order. The width has to be recognized before the two are
|
|
964
|
+
* swapped: `border` states its components in any order, so the component behind
|
|
965
|
+
* the style may be a color instead, as in `solid #8aadf4 1px`, and that one
|
|
966
|
+
* belongs behind the width rather than in front of it.
|
|
967
|
+
*
|
|
968
|
+
* @param {string} value The `border` value.
|
|
969
|
+
* @return {string} The value with its width stated before its style.
|
|
970
|
+
*/
|
|
971
|
+
function reorderBorderWidthBeforeStyle (value) {
|
|
972
|
+
return value.replace(BORDER_STYLE_BEFORE_WIDTH_PATTERN, '$2 $1');
|
|
973
|
+
}
|
|
974
|
+
|
|
1035
975
|
/**
|
|
1036
976
|
* Applies property-specific optimizations to a CSS value (transition, flex, font,
|
|
1037
977
|
* background, display, scale, border-radius, shorthand collapsing, etc.).
|
|
@@ -1142,10 +1082,8 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
|
|
|
1142
1082
|
val = convertBackgroundPositionKeywords(val);
|
|
1143
1083
|
}
|
|
1144
1084
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
// Reorder border shorthand from style-width-color to width-style-color
|
|
1148
|
-
val = val.replace(/^((?:solid|dashed|dotted|double|groove|ridge|inset|outset|hidden|none))\s+([^\s]+)\s+(.+)$/i, '$2 $1 $3');
|
|
1085
|
+
if (property === 'border') {
|
|
1086
|
+
val = reorderBorderWidthBeforeStyle(val);
|
|
1149
1087
|
}
|
|
1150
1088
|
|
|
1151
1089
|
if (property === 'flex-flow') {
|
|
@@ -1182,8 +1120,7 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
|
|
|
1182
1120
|
|
|
1183
1121
|
// Custom properties hold an arbitrary token stream rather than a typed value,
|
|
1184
1122
|
// so a unit-like token in one is not necessarily a length.
|
|
1185
|
-
|
|
1186
|
-
if (!isCustomProperty) {
|
|
1123
|
+
if (!isCustomProperty(property)) {
|
|
1187
1124
|
val = replaceOutsideStringsAndUrls(val, convertAbsoluteLengthsToPx);
|
|
1188
1125
|
}
|
|
1189
1126
|
|
|
@@ -1207,10 +1144,6 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
|
|
|
1207
1144
|
return shortenColorValues(segment, allowsSeparatorElision);
|
|
1208
1145
|
});
|
|
1209
1146
|
|
|
1210
|
-
// Remove space before hex colors (second pass after color evaluations)
|
|
1211
|
-
if (allowsSeparatorElision) {
|
|
1212
|
-
val = elideSpaceBeforeHexColors(val);
|
|
1213
|
-
}
|
|
1214
1147
|
if (!PUNCTUATED_COMPONENT_PROPERTIES.has(property)) {
|
|
1215
1148
|
// Restore space after close-paren when followed by an alphanumeric, hash, or hyphen
|
|
1216
1149
|
val = val.replace(/\)(?=[0-9a-zA-Z#-])/g, ') ');
|
|
@@ -1310,6 +1243,21 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
|
|
|
1310
1243
|
return val;
|
|
1311
1244
|
}
|
|
1312
1245
|
|
|
1246
|
+
/**
|
|
1247
|
+
* Reports whether a declaration holds a shorthand that was assembled by joining
|
|
1248
|
+
* already-minified longhand values with a separator, and whose grammar reads
|
|
1249
|
+
* those components by their position in the list. Nothing but that separator
|
|
1250
|
+
* says where one component ends and the next begins, so it is kept even where
|
|
1251
|
+
* the two components happen to be tokens that would survive being written
|
|
1252
|
+
* together.
|
|
1253
|
+
*
|
|
1254
|
+
* @param {object} declaration The CSS declaration object with property and value fields.
|
|
1255
|
+
* @return {boolean} Whether the assembled components keep their separators.
|
|
1256
|
+
*/
|
|
1257
|
+
function keepsAssembledComponentSeparators (declaration) {
|
|
1258
|
+
return Boolean(declaration.isAssembledShorthand) && hasPositionalComponents(declaration.property);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1313
1261
|
/**
|
|
1314
1262
|
* Minifies a CSS declaration's value by applying color conversion, math simplification, shorthand compression, gradient optimization, and other property-specific optimizations.
|
|
1315
1263
|
*
|
|
@@ -1327,10 +1275,7 @@ function computeMinifiedValue (declaration) {
|
|
|
1327
1275
|
return 'none';
|
|
1328
1276
|
}
|
|
1329
1277
|
let val = declaration.value;
|
|
1330
|
-
|
|
1331
|
-
// between their components, because those spaces delimit the shorthand's
|
|
1332
|
-
// parts rather than the authored whitespace of a single written value.
|
|
1333
|
-
const allowsSeparatorElision = !declaration.isAssembledShorthand;
|
|
1278
|
+
const allowsSeparatorElision = !keepsAssembledComponentSeparators(declaration);
|
|
1334
1279
|
|
|
1335
1280
|
if (typeof val === 'string') {
|
|
1336
1281
|
val = val.trim();
|
|
@@ -1374,16 +1319,13 @@ function computeMinifiedValue (declaration) {
|
|
|
1374
1319
|
}
|
|
1375
1320
|
|
|
1376
1321
|
val = lowercaseHexColors(val);
|
|
1377
|
-
if (allowsSeparatorElision) {
|
|
1378
|
-
val = elideSpaceBeforeHexColors(val);
|
|
1379
|
-
}
|
|
1380
1322
|
|
|
1381
1323
|
// Convert color functions to hex equivalents
|
|
1382
1324
|
val = convertColorsToHex(val);
|
|
1383
1325
|
|
|
1384
1326
|
// Shorten all color tokens (hex and named) to their shortest representation.
|
|
1385
|
-
//
|
|
1386
|
-
// spelling of the same length
|
|
1327
|
+
// A value that keeps the whitespace between its components saves nothing by
|
|
1328
|
+
// switching to a spelling of the same length, so its colors keep the
|
|
1387
1329
|
// spelling they were written with.
|
|
1388
1330
|
val = replaceOutsideStringsAndUrls(val, (segment) => {
|
|
1389
1331
|
return shortenColorValues(segment, allowsSeparatorElision);
|
|
@@ -1412,19 +1354,17 @@ function computeMinifiedValue (declaration) {
|
|
|
1412
1354
|
|
|
1413
1355
|
// Every earlier pass reads the value with its component separators in place,
|
|
1414
1356
|
// so the ones that turned out to be redundant are only dropped at the end.
|
|
1415
|
-
// The properties that punctuate their own components never got them back.
|
|
1416
1357
|
const elidesRedundantSeparators = (
|
|
1417
1358
|
typeof val === 'string' &&
|
|
1418
1359
|
allowsSeparatorElision &&
|
|
1419
|
-
!
|
|
1360
|
+
!isCustomProperty(declaration.property)
|
|
1420
1361
|
);
|
|
1421
1362
|
if (elidesRedundantSeparators) {
|
|
1422
|
-
|
|
1423
|
-
//
|
|
1424
|
-
//
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
}
|
|
1363
|
+
// A property that punctuates its own components already had the whitespace
|
|
1364
|
+
// after its closing parentheses taken out, and only the separators its
|
|
1365
|
+
// grammar still needs put back, so those are the ones left alone here.
|
|
1366
|
+
const elidesAfterSelfEndingTokens = !PUNCTUATED_COMPONENT_PROPERTIES.has(declaration.property);
|
|
1367
|
+
val = elideRedundantSeparators(val, elidesAfterSelfEndingTokens);
|
|
1428
1368
|
}
|
|
1429
1369
|
|
|
1430
1370
|
return val;
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tokenizes a CSS value the way the CSS Syntax tokenizer does, so that the whitespace which only separated two tokens that already end themselves can be told apart from the whitespace that holds two tokens apart.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Matches one CSS whitespace character. CSS counts a form feed and a carriage
|
|
7
|
+
* return as whitespace, so the shorter `\s` class would be too broad.
|
|
8
|
+
*
|
|
9
|
+
* @type {RegExp}
|
|
10
|
+
*/
|
|
11
|
+
const WHITESPACE_CHARACTER = /[ \t\n\r\f]/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Matches one decimal digit, which is what starts the numeric tokens.
|
|
15
|
+
*
|
|
16
|
+
* @type {RegExp}
|
|
17
|
+
*/
|
|
18
|
+
const DIGIT_CHARACTER = /[0-9]/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Matches one hexadecimal digit, which is what a unicode escape holds.
|
|
22
|
+
*
|
|
23
|
+
* @type {RegExp}
|
|
24
|
+
*/
|
|
25
|
+
const HEX_DIGIT_CHARACTER = /[0-9a-fA-F]/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Matches one character that may start an identifier: a letter, an underscore,
|
|
29
|
+
* or anything outside ASCII.
|
|
30
|
+
*
|
|
31
|
+
* @type {RegExp}
|
|
32
|
+
*/
|
|
33
|
+
const IDENT_START_CHARACTER = /[a-zA-Z_\u0080-\uFFFF]/;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Matches one character that may continue an identifier, which is everything an
|
|
37
|
+
* identifier may start with plus the digits and the hyphen.
|
|
38
|
+
*
|
|
39
|
+
* @type {RegExp}
|
|
40
|
+
*/
|
|
41
|
+
const NAME_CHARACTER = /[-a-zA-Z0-9_\u0080-\uFFFF]/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} CssToken
|
|
45
|
+
* @property {string} type The kind of token, such as `ident`, `number`, or `whitespace`.
|
|
46
|
+
* @property {string} text The exact slice of the value that the token spans.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reports whether the two characters at an index form a valid escape sequence.
|
|
51
|
+
* A backslash escapes the character after it, unless that character is the
|
|
52
|
+
* newline that ends the line.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} text The CSS value being read.
|
|
55
|
+
* @param {number} index The index of the possible backslash.
|
|
56
|
+
* @return {boolean} Whether an escape sequence starts at the index.
|
|
57
|
+
*/
|
|
58
|
+
function startsEscape (text, index) {
|
|
59
|
+
return text[index] === '\\' && text[index + 1] !== '\n';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reads past an escape sequence, which is either up to six hexadecimal digits
|
|
64
|
+
* and the single whitespace character that may end them, or one escaped
|
|
65
|
+
* character.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} text The CSS value being read.
|
|
68
|
+
* @param {number} index The index of the backslash that starts the escape.
|
|
69
|
+
* @return {number} The index just past the escape sequence.
|
|
70
|
+
*/
|
|
71
|
+
function readPastEscape (text, index) {
|
|
72
|
+
let end = index + 1;
|
|
73
|
+
let hexDigitCount = 0;
|
|
74
|
+
while (end < text.length && hexDigitCount < 6 && HEX_DIGIT_CHARACTER.test(text[end])) {
|
|
75
|
+
end++;
|
|
76
|
+
hexDigitCount++;
|
|
77
|
+
}
|
|
78
|
+
if (!hexDigitCount) {
|
|
79
|
+
return Math.min(end + 1, text.length);
|
|
80
|
+
}
|
|
81
|
+
if (end < text.length && WHITESPACE_CHARACTER.test(text[end])) {
|
|
82
|
+
end++;
|
|
83
|
+
}
|
|
84
|
+
return end;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reads past a name, the run of identifier characters and escapes that makes up
|
|
89
|
+
* an identifier, the body of a hash token, or the unit of a dimension.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} text The CSS value being read.
|
|
92
|
+
* @param {number} index The index the name starts at.
|
|
93
|
+
* @return {number} The index just past the name.
|
|
94
|
+
*/
|
|
95
|
+
function readPastName (text, index) {
|
|
96
|
+
let end = index;
|
|
97
|
+
while (end < text.length) {
|
|
98
|
+
if (NAME_CHARACTER.test(text[end])) {
|
|
99
|
+
end++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (startsEscape(text, end)) {
|
|
103
|
+
end = readPastEscape(text, end);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
return end;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reports whether an identifier starts at an index. A hyphen starts one only
|
|
113
|
+
* when a second hyphen, an identifier character, or an escape follows it, which
|
|
114
|
+
* is what makes `--custom` an identifier and a lone `-` a delimiter.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} text The CSS value being read.
|
|
117
|
+
* @param {number} index The index to test.
|
|
118
|
+
* @return {boolean} Whether an identifier starts at the index.
|
|
119
|
+
*/
|
|
120
|
+
function startsIdent (text, index) {
|
|
121
|
+
const character = text[index];
|
|
122
|
+
if (character === undefined) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (character === '-') {
|
|
126
|
+
const nextCharacter = text[index + 1];
|
|
127
|
+
if (nextCharacter === '-') {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return Boolean(nextCharacter) && (IDENT_START_CHARACTER.test(nextCharacter) || startsEscape(text, index + 1));
|
|
131
|
+
}
|
|
132
|
+
if (IDENT_START_CHARACTER.test(character)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return startsEscape(text, index);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reports whether a number starts at an index. A sign or a decimal point only
|
|
140
|
+
* starts one when a digit follows close enough behind it.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} text The CSS value being read.
|
|
143
|
+
* @param {number} index The index to test.
|
|
144
|
+
* @return {boolean} Whether a number starts at the index.
|
|
145
|
+
*/
|
|
146
|
+
function startsNumber (text, index) {
|
|
147
|
+
const character = text[index];
|
|
148
|
+
if (character === undefined) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (character === '+' || character === '-') {
|
|
152
|
+
const nextCharacter = text[index + 1] || '';
|
|
153
|
+
const followingCharacter = text[index + 2] || '';
|
|
154
|
+
return DIGIT_CHARACTER.test(nextCharacter) || (nextCharacter === '.' && DIGIT_CHARACTER.test(followingCharacter));
|
|
155
|
+
}
|
|
156
|
+
if (character === '.') {
|
|
157
|
+
return DIGIT_CHARACTER.test(text[index + 1] || '');
|
|
158
|
+
}
|
|
159
|
+
return DIGIT_CHARACTER.test(character);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Reads past the run of digits that starts at an index.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} text The CSS value being read.
|
|
166
|
+
* @param {number} index The index the digits start at.
|
|
167
|
+
* @return {number} The index just past the digits.
|
|
168
|
+
*/
|
|
169
|
+
function readPastDigits (text, index) {
|
|
170
|
+
let end = index;
|
|
171
|
+
while (end < text.length && DIGIT_CHARACTER.test(text[end])) {
|
|
172
|
+
end++;
|
|
173
|
+
}
|
|
174
|
+
return end;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Reads past a number: an optional sign, an integer part, an optional fraction,
|
|
179
|
+
* and an optional scientific exponent.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} text The CSS value being read.
|
|
182
|
+
* @param {number} index The index the number starts at.
|
|
183
|
+
* @return {number} The index just past the number.
|
|
184
|
+
*/
|
|
185
|
+
function readPastNumber (text, index) {
|
|
186
|
+
let end = index;
|
|
187
|
+
if (text[end] === '+' || text[end] === '-') {
|
|
188
|
+
end++;
|
|
189
|
+
}
|
|
190
|
+
end = readPastDigits(text, end);
|
|
191
|
+
if (text[end] === '.' && DIGIT_CHARACTER.test(text[end + 1] || '')) {
|
|
192
|
+
end = readPastDigits(text, end + 1);
|
|
193
|
+
}
|
|
194
|
+
const exponentCharacter = text[end];
|
|
195
|
+
if (exponentCharacter === 'e' || exponentCharacter === 'E') {
|
|
196
|
+
let exponentEnd = end + 1;
|
|
197
|
+
if (text[exponentEnd] === '+' || text[exponentEnd] === '-') {
|
|
198
|
+
exponentEnd++;
|
|
199
|
+
}
|
|
200
|
+
if (DIGIT_CHARACTER.test(text[exponentEnd] || '')) {
|
|
201
|
+
end = readPastDigits(text, exponentEnd);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return end;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Reads the numeric token at an index. A number takes a unit when an identifier
|
|
209
|
+
* follows it and becomes a percentage when a percent sign does.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} text The CSS value being read.
|
|
212
|
+
* @param {number} index The index the number starts at.
|
|
213
|
+
* @return {CssToken} The dimension, percentage, or number token.
|
|
214
|
+
*/
|
|
215
|
+
function readNumericToken (text, index) {
|
|
216
|
+
const numberEnd = readPastNumber(text, index);
|
|
217
|
+
if (startsIdent(text, numberEnd)) {
|
|
218
|
+
const unitEnd = readPastName(text, numberEnd);
|
|
219
|
+
return { type: 'dimension', text: text.slice(index, unitEnd) };
|
|
220
|
+
}
|
|
221
|
+
if (text[numberEnd] === '%') {
|
|
222
|
+
return { type: 'percentage', text: text.slice(index, numberEnd + 1) };
|
|
223
|
+
}
|
|
224
|
+
return { type: 'number', text: text.slice(index, numberEnd) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Reports whether the contents of a `url()` are quoted, which makes it an
|
|
229
|
+
* ordinary function token holding a string rather than a single url token.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} text The CSS value being read.
|
|
232
|
+
* @param {number} index The index just past the opening parenthesis.
|
|
233
|
+
* @return {boolean} Whether a quoted string opens the parentheses.
|
|
234
|
+
*/
|
|
235
|
+
function holdsQuotedUrl (text, index) {
|
|
236
|
+
let end = index;
|
|
237
|
+
while (end < text.length && WHITESPACE_CHARACTER.test(text[end])) {
|
|
238
|
+
end++;
|
|
239
|
+
}
|
|
240
|
+
return text[end] === '"' || text[end] === '\'';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Reads past the body of an unquoted url token, which runs to the parenthesis
|
|
245
|
+
* that closes it. An unquoted url holds no nested parentheses, so the first
|
|
246
|
+
* closing one ends the token.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} text The CSS value being read.
|
|
249
|
+
* @param {number} index The index just past the opening parenthesis.
|
|
250
|
+
* @return {number} The index just past the closing parenthesis.
|
|
251
|
+
*/
|
|
252
|
+
function readPastUrlBody (text, index) {
|
|
253
|
+
let end = index;
|
|
254
|
+
while (end < text.length) {
|
|
255
|
+
if (startsEscape(text, end)) {
|
|
256
|
+
end = readPastEscape(text, end);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (text[end] === ')') {
|
|
260
|
+
return end + 1;
|
|
261
|
+
}
|
|
262
|
+
end++;
|
|
263
|
+
}
|
|
264
|
+
return end;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reads the token that an identifier starts. An identifier directly followed by
|
|
269
|
+
* an opening parenthesis is a function token instead, and the `url()` written
|
|
270
|
+
* without quotes is a single token that swallows its own contents.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} text The CSS value being read.
|
|
273
|
+
* @param {number} index The index the identifier starts at.
|
|
274
|
+
* @return {CssToken} The ident, function, or url token.
|
|
275
|
+
*/
|
|
276
|
+
function readIdentLikeToken (text, index) {
|
|
277
|
+
const nameEnd = readPastName(text, index);
|
|
278
|
+
const name = text.slice(index, nameEnd);
|
|
279
|
+
if (text[nameEnd] !== '(') {
|
|
280
|
+
return { type: 'ident', text: name };
|
|
281
|
+
}
|
|
282
|
+
if (name.toLowerCase() === 'url' && !holdsQuotedUrl(text, nameEnd + 1)) {
|
|
283
|
+
const urlEnd = readPastUrlBody(text, nameEnd + 1);
|
|
284
|
+
return { type: 'url', text: text.slice(index, urlEnd) };
|
|
285
|
+
}
|
|
286
|
+
return { type: 'function', text: text.slice(index, nameEnd + 1) };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Reads the string token at an index, which runs to the matching quote and
|
|
291
|
+
* takes escaped quotes in its stride.
|
|
292
|
+
*
|
|
293
|
+
* @param {string} text The CSS value being read.
|
|
294
|
+
* @param {number} index The index of the opening quote.
|
|
295
|
+
* @return {CssToken} The string token.
|
|
296
|
+
*/
|
|
297
|
+
function readStringToken (text, index) {
|
|
298
|
+
const quote = text[index];
|
|
299
|
+
let end = index + 1;
|
|
300
|
+
while (end < text.length) {
|
|
301
|
+
if (startsEscape(text, end)) {
|
|
302
|
+
end = readPastEscape(text, end);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (text[end] === quote) {
|
|
306
|
+
end++;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (text[end] === '\n') {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
end++;
|
|
313
|
+
}
|
|
314
|
+
return { type: 'string', text: text.slice(index, end) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Reads the run of whitespace at an index as the single token that CSS treats
|
|
319
|
+
* it as.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} text The CSS value being read.
|
|
322
|
+
* @param {number} index The index the whitespace starts at.
|
|
323
|
+
* @return {CssToken} The whitespace token.
|
|
324
|
+
*/
|
|
325
|
+
function readWhitespaceToken (text, index) {
|
|
326
|
+
let end = index;
|
|
327
|
+
while (end < text.length && WHITESPACE_CHARACTER.test(text[end])) {
|
|
328
|
+
end++;
|
|
329
|
+
}
|
|
330
|
+
return { type: 'whitespace', text: text.slice(index, end) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The characters that stand on their own as structural punctuation, none of
|
|
335
|
+
* which can ever merge with the token beside it.
|
|
336
|
+
*
|
|
337
|
+
* @type {Set<string>}
|
|
338
|
+
*/
|
|
339
|
+
const PUNCTUATION_CHARACTERS = new Set(['(', ')', '[', ']', '{', '}', ',', ':', ';']);
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Reads the single token that starts at an index.
|
|
343
|
+
*
|
|
344
|
+
* @param {string} text The CSS value being read.
|
|
345
|
+
* @param {number} index The index the token starts at.
|
|
346
|
+
* @return {CssToken} The token found at the index.
|
|
347
|
+
*/
|
|
348
|
+
function readToken (text, index) {
|
|
349
|
+
const character = text[index];
|
|
350
|
+
|
|
351
|
+
if (WHITESPACE_CHARACTER.test(character)) {
|
|
352
|
+
return readWhitespaceToken(text, index);
|
|
353
|
+
}
|
|
354
|
+
if (character === '"' || character === '\'') {
|
|
355
|
+
return readStringToken(text, index);
|
|
356
|
+
}
|
|
357
|
+
if (PUNCTUATION_CHARACTERS.has(character)) {
|
|
358
|
+
return { type: 'punctuation', text: character };
|
|
359
|
+
}
|
|
360
|
+
if (character === '#') {
|
|
361
|
+
const startsHash = NAME_CHARACTER.test(text[index + 1] || '') || startsEscape(text, index + 1);
|
|
362
|
+
if (startsHash) {
|
|
363
|
+
return { type: 'hash', text: text.slice(index, readPastName(text, index + 1)) };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (character === '@' && startsIdent(text, index + 1)) {
|
|
367
|
+
return { type: 'at-keyword', text: text.slice(index, readPastName(text, index + 1)) };
|
|
368
|
+
}
|
|
369
|
+
if (startsNumber(text, index)) {
|
|
370
|
+
return readNumericToken(text, index);
|
|
371
|
+
}
|
|
372
|
+
if (startsIdent(text, index)) {
|
|
373
|
+
return readIdentLikeToken(text, index);
|
|
374
|
+
}
|
|
375
|
+
return { type: 'delim', text: character };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Splits a CSS value into the tokens that the CSS Syntax tokenizer would read
|
|
380
|
+
* out of it.
|
|
381
|
+
*
|
|
382
|
+
* @param {string} value The CSS value to tokenize.
|
|
383
|
+
* @return {Array} The tokens the value holds, in the order they appear.
|
|
384
|
+
*/
|
|
385
|
+
function tokenizeCssValue (value) {
|
|
386
|
+
const tokens = [];
|
|
387
|
+
let index = 0;
|
|
388
|
+
while (index < value.length) {
|
|
389
|
+
const token = readToken(value, index);
|
|
390
|
+
tokens.push(token);
|
|
391
|
+
index += Math.max(token.text.length, 1);
|
|
392
|
+
}
|
|
393
|
+
return tokens;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The operators that a math function requires whitespace on both sides of. Both
|
|
398
|
+
* of them double as the sign of a number, so an unseparated one is read as part
|
|
399
|
+
* of the term that follows it and the expression silently breaks.
|
|
400
|
+
*
|
|
401
|
+
* @type {Set<string>}
|
|
402
|
+
*/
|
|
403
|
+
const MATH_OPERATOR_DELIMITERS = new Set(['+', '-']);
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Reports whether a token is an operator that a math function needs kept apart
|
|
407
|
+
* from the terms around it.
|
|
408
|
+
*
|
|
409
|
+
* @param {CssToken} token The token to test.
|
|
410
|
+
* @return {boolean} Whether the token is a `+` or a `-` operator.
|
|
411
|
+
*/
|
|
412
|
+
function isMathOperator (token) {
|
|
413
|
+
return token.type === 'delim' && MATH_OPERATOR_DELIMITERS.has(token.text);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Reports whether two tokens written side by side still read as those same two
|
|
418
|
+
* tokens. Writing them together and tokenizing the result answers this for
|
|
419
|
+
* every pair at once: a pair that merges comes back as one token, such as the
|
|
420
|
+
* `4px` and `solid` of `4pxsolid`, while a pair that holds its own comes back
|
|
421
|
+
* unchanged, such as the `solid` and `#0000` of `solid#0000`.
|
|
422
|
+
*
|
|
423
|
+
* @param {CssToken} leftToken The token before the separator.
|
|
424
|
+
* @param {CssToken} rightToken The token after the separator.
|
|
425
|
+
* @return {boolean} Whether the two tokens survive being written together.
|
|
426
|
+
*/
|
|
427
|
+
function canJoinTokens (leftToken, rightToken) {
|
|
428
|
+
const rejoinedTokens = tokenizeCssValue(leftToken.text + rightToken.text);
|
|
429
|
+
return (
|
|
430
|
+
rejoinedTokens.length === 2 &&
|
|
431
|
+
rejoinedTokens[0].text === leftToken.text &&
|
|
432
|
+
rejoinedTokens[1].text === rightToken.text
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The characters that close a token and can never continue one. A token that
|
|
438
|
+
* ends with one of them, such as a `url()` or a quoted string, marks where it
|
|
439
|
+
* ends without any help from the whitespace behind it.
|
|
440
|
+
*
|
|
441
|
+
* @type {Set<string>}
|
|
442
|
+
*/
|
|
443
|
+
const TOKEN_CLOSING_CHARACTERS = new Set([')', ']', '}', '"', '\'']);
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The characters that can only ever open a token, never continue one. A token
|
|
447
|
+
* that starts with one of them, such as a hash color or a quoted string, marks
|
|
448
|
+
* where it starts without any help from the whitespace in front of it.
|
|
449
|
+
*
|
|
450
|
+
* @type {Set<string>}
|
|
451
|
+
*/
|
|
452
|
+
const TOKEN_OPENING_CHARACTERS = new Set(['#', '!', '"', '\'', '(', ')', '[', ']', '{', '}', ',', ':', ';']);
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Reports whether a token spells out its own end, so that the token after it
|
|
456
|
+
* reads as a separate one even with nothing written between them.
|
|
457
|
+
*
|
|
458
|
+
* @param {CssToken} token The token to test.
|
|
459
|
+
* @return {boolean} Whether the token's last character closes it.
|
|
460
|
+
*/
|
|
461
|
+
function marksItsOwnEnd (token) {
|
|
462
|
+
return TOKEN_CLOSING_CHARACTERS.has(token.text.slice(-1));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Reports whether a token spells out its own start, so that the token before it
|
|
467
|
+
* reads as a separate one even with nothing written between them.
|
|
468
|
+
*
|
|
469
|
+
* @param {CssToken} token The token to test.
|
|
470
|
+
* @return {boolean} Whether the token's first character opens it.
|
|
471
|
+
*/
|
|
472
|
+
function marksItsOwnStart (token) {
|
|
473
|
+
return TOKEN_OPENING_CHARACTERS.has(token.text.slice(0, 1));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Reports whether the whitespace between two tokens carries nothing. That takes
|
|
478
|
+
* three things: one of the two tokens has to spell out the boundary the
|
|
479
|
+
* whitespace would otherwise be drawing, the pair has to survive being written
|
|
480
|
+
* together as the same two tokens, and neither of them may be a math operator,
|
|
481
|
+
* which reads the whitespace around it as part of the expression's grammar.
|
|
482
|
+
*
|
|
483
|
+
* @param {CssToken} [previousToken] The token before the whitespace, when the whitespace does not lead the value.
|
|
484
|
+
* @param {CssToken} [nextToken] The token after the whitespace, when the whitespace does not trail the value.
|
|
485
|
+
* @param {boolean} elidesAfterSelfEndingTokens Whether a token that marks its own end is allowed to absorb the whitespace behind it.
|
|
486
|
+
* @return {boolean} Whether the whitespace may be dropped.
|
|
487
|
+
*/
|
|
488
|
+
function isRedundantSeparator (previousToken, nextToken, elidesAfterSelfEndingTokens) {
|
|
489
|
+
if (!previousToken || !nextToken) {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
if (isMathOperator(previousToken) || isMathOperator(nextToken)) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
const marksTheBoundary = (
|
|
496
|
+
(elidesAfterSelfEndingTokens && marksItsOwnEnd(previousToken)) ||
|
|
497
|
+
marksItsOwnStart(nextToken)
|
|
498
|
+
);
|
|
499
|
+
if (!marksTheBoundary) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
return canJoinTokens(previousToken, nextToken);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Removes every run of whitespace in a CSS value that separates two tokens
|
|
507
|
+
* already marking the boundary between them, leaving the runs that keep their
|
|
508
|
+
* neighbours from being read as a single different token.
|
|
509
|
+
*
|
|
510
|
+
* @param {string} value The minified CSS value.
|
|
511
|
+
* @param {boolean} elidesAfterSelfEndingTokens Whether the whitespace that follows a token marking its own end, such as a `url()` or a function call, may go. A grammar that reads a component by where it sits relative to a function, such as the `<position>` that may follow the image of a `background` layer, keeps those separators and drops only the ones the token after them marks.
|
|
512
|
+
* @return {string} The value without its redundant separators.
|
|
513
|
+
*/
|
|
514
|
+
function elideRedundantSeparators (value, elidesAfterSelfEndingTokens = true) {
|
|
515
|
+
const tokens = tokenizeCssValue(value);
|
|
516
|
+
return tokens.map((token, index) => {
|
|
517
|
+
if (token.type !== 'whitespace') {
|
|
518
|
+
return token.text;
|
|
519
|
+
}
|
|
520
|
+
if (isRedundantSeparator(tokens[index - 1], tokens[index + 1], elidesAfterSelfEndingTokens)) {
|
|
521
|
+
return '';
|
|
522
|
+
}
|
|
523
|
+
return token.text;
|
|
524
|
+
}).join('');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export { elideRedundantSeparators };
|