@thejaredwilcurt/csslop 0.0.38 → 0.0.40
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/README.md +7 -2
- package/package.json +12 -11
- package/src/declarations/background.js +29 -37
- package/src/declarations/border.js +1 -1
- package/src/declarations/fold.js +1 -1
- package/src/declarations/process.js +17 -8
- package/src/declarations/shorthand-values.js +1 -1
- package/src/index.js +15 -44
- package/src/parser/at-rules.js +291 -0
- package/src/parser/index.js +826 -0
- package/src/parser/positions.js +71 -0
- package/src/parser/scanner.js +229 -0
- package/src/parser/source-search.js +287 -0
- package/src/parser/tokenizer.js +396 -0
- package/src/preprocess.js +20 -228
- package/src/rules/custom-properties.js +86 -9
- package/src/rules/lang.js +20 -56
- package/src/rules/normalize.js +80 -0
- package/src/rules/optimize.js +271 -111
- package/src/rules/selectors.js +56 -221
- package/src/rules/stringify.js +4 -5
- package/src/value/color-mix.js +22 -47
- package/src/value/gradients.js +44 -71
- package/src/value/light-dark.js +9 -94
- package/src/value/math.js +271 -76
- package/src/value/minify.js +89 -54
- package/src/value/relative-colors.js +15 -78
- package/src/value/shared.js +53 -10
- package/src/value/tokens.js +4 -387
- package/src/value/transforms.js +20 -53
- package/src/value/syntax.js +0 -144
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* @file Evaluates and minifies CSS relative color syntax.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
findMatchingParenthesis,
|
|
7
|
+
findTopLevelDelimiter,
|
|
8
|
+
splitTopLevelComponents
|
|
9
|
+
} from '../parser/source-search.js';
|
|
10
|
+
|
|
5
11
|
import {
|
|
6
12
|
parseColor,
|
|
7
13
|
rgbaToHex,
|
|
@@ -48,69 +54,6 @@ const RELATIVE_COLOR_CHANNELS = {
|
|
|
48
54
|
oklch: ['l', 'c', 'h']
|
|
49
55
|
};
|
|
50
56
|
|
|
51
|
-
/**
|
|
52
|
-
* Finds the closing parenthesis matching the opening one at the given index,
|
|
53
|
-
* accounting for nested parentheses.
|
|
54
|
-
*
|
|
55
|
-
* @param {string} value The string being scanned.
|
|
56
|
-
* @param {number} openIndex Index of the opening parenthesis.
|
|
57
|
-
* @return {number} Index of the matching close parenthesis, or -1.
|
|
58
|
-
*/
|
|
59
|
-
function findClosingParenthesis (value, openIndex) {
|
|
60
|
-
let depth = 1;
|
|
61
|
-
let index = openIndex + 1;
|
|
62
|
-
while (index < value.length) {
|
|
63
|
-
const character = value[index];
|
|
64
|
-
if (character === '(') {
|
|
65
|
-
depth++;
|
|
66
|
-
} else if (character === ')') {
|
|
67
|
-
depth--;
|
|
68
|
-
if (depth === 0) {
|
|
69
|
-
return index;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
index++;
|
|
73
|
-
}
|
|
74
|
-
return -1;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Splits a string on top-level whitespace while treating each parenthesized
|
|
79
|
-
* group as part of a single token, keeping nested function arguments intact.
|
|
80
|
-
*
|
|
81
|
-
* @param {string} text The text to split.
|
|
82
|
-
* @return {Array} The list of top-level tokens.
|
|
83
|
-
*/
|
|
84
|
-
function splitTopLevelWhitespace (text) {
|
|
85
|
-
const tokens = [];
|
|
86
|
-
let current = '';
|
|
87
|
-
let depth = 0;
|
|
88
|
-
for (let index = 0; index < text.length; index++) {
|
|
89
|
-
const character = text[index];
|
|
90
|
-
if (character === '(') {
|
|
91
|
-
depth++;
|
|
92
|
-
current += character;
|
|
93
|
-
} else if (character === ')') {
|
|
94
|
-
if (depth > 0) {
|
|
95
|
-
depth--;
|
|
96
|
-
}
|
|
97
|
-
current += character;
|
|
98
|
-
// Match any single whitespace character at the top level
|
|
99
|
-
} else if (depth === 0 && /\s/.test(character)) {
|
|
100
|
-
if (current) {
|
|
101
|
-
tokens.push(current);
|
|
102
|
-
current = '';
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
current += character;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (current) {
|
|
109
|
-
tokens.push(current);
|
|
110
|
-
}
|
|
111
|
-
return tokens;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
57
|
/**
|
|
115
58
|
* Splits a relative-color body into its channel portion and its optional alpha
|
|
116
59
|
* portion at the top-level `/` separator.
|
|
@@ -119,20 +62,14 @@ function splitTopLevelWhitespace (text) {
|
|
|
119
62
|
* @return {Array} A two-element array of [channelsPart, alphaPart|null].
|
|
120
63
|
*/
|
|
121
64
|
function splitRelativeAlpha (body) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (character === '(') {
|
|
126
|
-
depth++;
|
|
127
|
-
} else if (character === ')') {
|
|
128
|
-
if (depth > 0) {
|
|
129
|
-
depth--;
|
|
130
|
-
}
|
|
131
|
-
} else if (character === '/' && depth === 0) {
|
|
132
|
-
return [body.slice(0, index).trim(), body.slice(index + 1).trim()];
|
|
133
|
-
}
|
|
65
|
+
const separatorIndex = findTopLevelDelimiter(body, '/', 0);
|
|
66
|
+
if (separatorIndex === -1) {
|
|
67
|
+
return [body.trim(), null];
|
|
134
68
|
}
|
|
135
|
-
return [
|
|
69
|
+
return [
|
|
70
|
+
body.slice(0, separatorIndex).trim(),
|
|
71
|
+
body.slice(separatorIndex + 1).trim()
|
|
72
|
+
];
|
|
136
73
|
}
|
|
137
74
|
|
|
138
75
|
/**
|
|
@@ -276,7 +213,7 @@ function rewriteRelativeColor (functionName, inner) {
|
|
|
276
213
|
// Strip the leading `from` keyword, allowing it to abut the base color
|
|
277
214
|
const body = inner.replace(/^\s*from\b\s*/i, '').trim();
|
|
278
215
|
const [channelsPart, alphaRaw] = splitRelativeAlpha(body);
|
|
279
|
-
const tokens =
|
|
216
|
+
const tokens = splitTopLevelComponents(channelsPart);
|
|
280
217
|
if (tokens.length < 2) {
|
|
281
218
|
return functionName + '(' + inner + ')';
|
|
282
219
|
}
|
|
@@ -316,7 +253,7 @@ function minifyRelativeColorSyntax (value) {
|
|
|
316
253
|
if (match && !precededByIdentifier) {
|
|
317
254
|
const functionName = match[1].toLowerCase();
|
|
318
255
|
const openParenIndex = index + match[1].length;
|
|
319
|
-
const closingParenIndex =
|
|
256
|
+
const closingParenIndex = findMatchingParenthesis(value, openParenIndex);
|
|
320
257
|
if (closingParenIndex !== -1) {
|
|
321
258
|
const inner = value.slice(openParenIndex + 1, closingParenIndex);
|
|
322
259
|
result += rewriteRelativeColor(functionName, inner);
|
package/src/value/shared.js
CHANGED
|
@@ -47,8 +47,10 @@ function roundCompactNumber (value, precision = 3) {
|
|
|
47
47
|
if (!Number.isFinite(number)) {
|
|
48
48
|
return String(value);
|
|
49
49
|
}
|
|
50
|
-
// Strip
|
|
51
|
-
|
|
50
|
+
// Strip the zeros trailing the fraction, then the decimal point they leave
|
|
51
|
+
// behind. Only zeros written after a decimal point are droppable, so the
|
|
52
|
+
// match has to start at one: the zeros of a whole number are digits.
|
|
53
|
+
let result = number.toFixed(precision).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '');
|
|
52
54
|
if (result.startsWith('0.')) {
|
|
53
55
|
result = result.slice(1);
|
|
54
56
|
}
|
|
@@ -58,6 +60,44 @@ function roundCompactNumber (value, precision = 3) {
|
|
|
58
60
|
return result;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The most characters the result of a resolved calculation is written with,
|
|
65
|
+
* counting the digits and the decimal point but not the sign. A calculation is
|
|
66
|
+
* only replaced by its result to save characters, and a result that repeats
|
|
67
|
+
* can always be written out longer, so its digits are cut off where the
|
|
68
|
+
* accuracy they add stops paying for the characters they cost.
|
|
69
|
+
*
|
|
70
|
+
* @type {number}
|
|
71
|
+
*/
|
|
72
|
+
const MAXIMUM_RESOLVED_NUMBER_CHARACTERS = 7;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Formats what a calculation resolved to as the number a stylesheet states it
|
|
76
|
+
* with: rounded to however many decimal places are left over once the whole
|
|
77
|
+
* part and the decimal point have taken their share of the character budget a
|
|
78
|
+
* resolved number is written within.
|
|
79
|
+
*
|
|
80
|
+
* @param {number|string} value The number the calculation resolved to.
|
|
81
|
+
* @param {number} maximumCharacters The most characters the written number may take.
|
|
82
|
+
* @return {string} The number as it is written into the stylesheet.
|
|
83
|
+
*/
|
|
84
|
+
function formatResolvedNumber (value, maximumCharacters = MAXIMUM_RESOLVED_NUMBER_CHARACTERS) {
|
|
85
|
+
const number = typeof value === 'number' ? value : parseFloat(value);
|
|
86
|
+
if (!Number.isFinite(number)) {
|
|
87
|
+
return String(value);
|
|
88
|
+
}
|
|
89
|
+
const wholePart = Math.floor(Math.abs(number));
|
|
90
|
+
// A magnitude below one is written without its leading zero, so nothing of
|
|
91
|
+
// its whole part is spent from the budget
|
|
92
|
+
let wholeDigitCount = 0;
|
|
93
|
+
if (wholePart !== 0) {
|
|
94
|
+
wholeDigitCount = String(wholePart).length;
|
|
95
|
+
}
|
|
96
|
+
// The decimal point itself takes one of the characters the digits could have
|
|
97
|
+
const decimalPlaces = Math.max(maximumCharacters - wholeDigitCount - 1, 0);
|
|
98
|
+
return roundCompactNumber(number, decimalPlaces);
|
|
99
|
+
}
|
|
100
|
+
|
|
61
101
|
/**
|
|
62
102
|
* Converts an absolute CSS length value (pt, pc, in, cm, mm, q) to its pixel equivalent using standard conversion factors.
|
|
63
103
|
*
|
|
@@ -84,15 +124,16 @@ function convertAbsoluteLengthToPx (value, unit) {
|
|
|
84
124
|
}
|
|
85
125
|
|
|
86
126
|
/**
|
|
87
|
-
* Formats a
|
|
127
|
+
* Formats what a calculation resolved to as the dimension a stylesheet states
|
|
128
|
+
* it with, rounding the number into the character budget a resolved number is
|
|
129
|
+
* written within.
|
|
88
130
|
*
|
|
89
|
-
* @param {number} value
|
|
90
|
-
* @param {string} unit
|
|
91
|
-
* @
|
|
92
|
-
* @return {string} The formatted dimension string.
|
|
131
|
+
* @param {number} value The numeric result of the calculation.
|
|
132
|
+
* @param {string} unit The CSS unit suffix the result carries (e.g. "px", "%").
|
|
133
|
+
* @return {string} The dimension as it is written into the stylesheet.
|
|
93
134
|
*/
|
|
94
|
-
function
|
|
95
|
-
return
|
|
135
|
+
function formatResolvedDimension (value, unit) {
|
|
136
|
+
return formatResolvedNumber(value) + unit;
|
|
96
137
|
}
|
|
97
138
|
|
|
98
139
|
/**
|
|
@@ -197,7 +238,9 @@ export {
|
|
|
197
238
|
collapseShorthandParts,
|
|
198
239
|
convertAbsoluteLengthToPx,
|
|
199
240
|
formatCompactNumber,
|
|
200
|
-
|
|
241
|
+
formatResolvedDimension,
|
|
242
|
+
formatResolvedNumber,
|
|
243
|
+
hasSubstitutedParts,
|
|
201
244
|
normalizeScaleComponent,
|
|
202
245
|
parseAlphaString,
|
|
203
246
|
parseAngleToDegrees,
|
package/src/value/tokens.js
CHANGED
|
@@ -1,397 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file
|
|
2
|
+
* @file Drops the whitespace of a minified CSS value that only separates two tokens which already mark the boundary between them.
|
|
3
3
|
*/
|
|
4
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
|
-
}
|
|
5
|
+
import { tokenizeCssValue } from '../parser/tokenizer.js';
|
|
137
6
|
|
|
138
7
|
/**
|
|
139
|
-
*
|
|
140
|
-
* starts one when a digit follows close enough behind it.
|
|
8
|
+
* One token of a value, as the tokenizer reads it.
|
|
141
9
|
*
|
|
142
|
-
* @
|
|
143
|
-
* @param {number} index The index to test.
|
|
144
|
-
* @return {boolean} Whether a number starts at the index.
|
|
10
|
+
* @typedef {import('../parser/tokenizer.js').CssToken} CssToken
|
|
145
11
|
*/
|
|
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
12
|
|
|
396
13
|
/**
|
|
397
14
|
* The operators that a math function requires whitespace on both sides of. Both
|