@miaskiewicz/turbo-dom 0.7.1 → 0.8.0
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/Cargo.lock +7 -7
- package/Cargo.toml +7 -5
- package/README.md +13 -8
- package/package.json +1 -1
- package/pkg/package.json +2 -2
- package/pkg/turbo_dom_parser_bg.wasm +0 -0
- package/pkg-web/package.json +2 -2
- package/pkg-web/turbo_dom_parser_bg.wasm +0 -0
- package/src/runtime/cascade.mjs +70 -54
- package/src/runtime/css-shorthand.mjs +1217 -734
- package/src/runtime/custom-elements.mjs +8 -2
- package/src/runtime/dom.mjs +148 -95
- package/src/runtime/events.mjs +106 -85
- package/src/runtime/html-serialize.mjs +28 -24
- package/src/runtime/selectors.mjs +353 -237
- package/src/runtime/stubs.mjs +9 -6
- package/turbo-dom-parser.darwin-arm64.node +0 -0
- package/turbo-dom-parser.linux-arm64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-musl.node +0 -0
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
|
@@ -35,134 +35,155 @@ export function cssNumber(value) {
|
|
|
35
35
|
// hash, string, url, delim, comma, (, ), [, ], whitespace. `null` for bad strings / urls.
|
|
36
36
|
const isNameStart = (c) => /[A-Za-z_\u0080-]/.test(c);
|
|
37
37
|
const isName = (c) => /[A-Za-z0-9_\-\u0080-]/.test(c);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// CSS Syntax "consume an escaped code point": zero, a surrogate or past U+10FFFF is U+FFFD.
|
|
39
|
+
const escapedCodePoint = (hex) => {
|
|
40
|
+
const code = parseInt(hex, 16);
|
|
41
|
+
return String.fromCodePoint(code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff) ? 0xfffd : code);
|
|
42
|
+
};
|
|
43
|
+
const tokStartsNumber = (text, at) => {
|
|
44
|
+
const c = text[at], d = text[at + 1], e = text[at + 2];
|
|
45
|
+
if (c === '+' || c === '-') return /[0-9]/.test(d || '') || (d === '.' && /[0-9]/.test(e || ''));
|
|
46
|
+
if (c === '.') return /[0-9]/.test(d || '');
|
|
47
|
+
return /[0-9]/.test(c || '');
|
|
48
|
+
};
|
|
49
|
+
const tokStartsIdent = (text, at) => {
|
|
50
|
+
const c = text[at], d = text[at + 1];
|
|
51
|
+
const escape = (index) => text[index] === '\\' && text[index + 1] !== '\n';
|
|
52
|
+
if (c === '-') return isNameStart(d || '') || d === '-' || escape(at + 1);
|
|
53
|
+
if (c === '\\') return escape(at);
|
|
54
|
+
return isNameStart(c || '');
|
|
55
|
+
};
|
|
56
|
+
function tokReadName(text, state) {
|
|
41
57
|
const n = text.length;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
else { name += text[i]; i++; }
|
|
72
|
-
} else break;
|
|
73
|
-
}
|
|
74
|
-
return name;
|
|
75
|
-
};
|
|
76
|
-
while (i < n) {
|
|
77
|
-
const c = text[i];
|
|
78
|
-
if (c === '/' && text[i + 1] === '*') { const end = text.indexOf('*/', i + 2); i = end < 0 ? n : end + 2; continue; }
|
|
79
|
-
if (/\s/.test(c)) { while (i < n && /\s/.test(text[i])) i++; out.push({ type: 'ws' }); continue; }
|
|
80
|
-
if (c === '"' || c === "'") {
|
|
81
|
-
let value = ''; i++;
|
|
82
|
-
let closed = false;
|
|
83
|
-
while (i < n) {
|
|
84
|
-
const d = text[i];
|
|
85
|
-
if (d === c) { i++; closed = true; break; }
|
|
86
|
-
if (d === '\n') return null;
|
|
87
|
-
if (d === '\\') {
|
|
88
|
-
if (i + 1 >= n) { i++; continue; }
|
|
89
|
-
const e = text[i + 1];
|
|
90
|
-
if (e === '\n') { i += 2; continue; }
|
|
91
|
-
i++;
|
|
92
|
-
const hex = /^[0-9a-fA-F]{1,6}/.exec(text.slice(i));
|
|
93
|
-
if (hex) { value += escaped(hex[0]); i += hex[0].length; if (/\s/.test(text[i] || '')) i++; }
|
|
94
|
-
else { value += e; i++; }
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
value += d; i++;
|
|
98
|
-
}
|
|
99
|
-
void closed;
|
|
100
|
-
out.push({ type: 'string', value });
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (startsNumber(i)) {
|
|
104
|
-
const m = /^[+-]?(\d*\.\d+|\d+)([eE][+-]?\d+)?/.exec(text.slice(i));
|
|
105
|
-
const raw = m[0];
|
|
106
|
-
i += raw.length;
|
|
107
|
-
const value = Number(raw);
|
|
108
|
-
const integer = !/[.eE]/.test(raw);
|
|
109
|
-
if (text[i] === '%') { i++; out.push({ type: 'percentage', value, raw }); continue; }
|
|
110
|
-
if (startsIdent(i)) { out.push({ type: 'dimension', value, unit: readName() }); continue; }
|
|
111
|
-
out.push({ type: 'number', value, integer });
|
|
58
|
+
let name = '';
|
|
59
|
+
while (state.i < n) {
|
|
60
|
+
const c = text[state.i];
|
|
61
|
+
if (isName(c)) { name += c; state.i++; }
|
|
62
|
+
else if (c === '\\' && state.i + 1 >= n) { name += '\ufffd'; state.i++; }
|
|
63
|
+
else if (c === '\\' && text[state.i + 1] !== '\n') {
|
|
64
|
+
state.i++;
|
|
65
|
+
const hex = /^[0-9a-fA-F]{1,6}/.exec(text.slice(state.i));
|
|
66
|
+
if (hex) { name += escapedCodePoint(hex[0]); state.i += hex[0].length; if (/\s/.test(text[state.i] || '')) state.i++; }
|
|
67
|
+
else { name += text[state.i]; state.i++; }
|
|
68
|
+
} else break;
|
|
69
|
+
}
|
|
70
|
+
return name;
|
|
71
|
+
}
|
|
72
|
+
function tokReadString(text, state, quote) {
|
|
73
|
+
const n = text.length;
|
|
74
|
+
let value = ''; state.i++;
|
|
75
|
+
while (state.i < n) {
|
|
76
|
+
const d = text[state.i];
|
|
77
|
+
if (d === quote) { state.i++; return { type: 'string', value }; }
|
|
78
|
+
if (d === '\n') return null;
|
|
79
|
+
if (d === '\\') {
|
|
80
|
+
if (state.i + 1 >= n) { state.i++; continue; }
|
|
81
|
+
const e = text[state.i + 1];
|
|
82
|
+
if (e === '\n') { state.i += 2; continue; }
|
|
83
|
+
state.i++;
|
|
84
|
+
const hex = /^[0-9a-fA-F]{1,6}/.exec(text.slice(state.i));
|
|
85
|
+
if (hex) { value += escapedCodePoint(hex[0]); state.i += hex[0].length; if (/\s/.test(text[state.i] || '')) state.i++; }
|
|
86
|
+
else { value += e; state.i++; }
|
|
112
87
|
continue;
|
|
113
88
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
89
|
+
value += d; state.i++;
|
|
90
|
+
}
|
|
91
|
+
return { type: 'string', value };
|
|
92
|
+
}
|
|
93
|
+
function tokReadNumeric(text, state) {
|
|
94
|
+
const m = /^[+-]?(\d*\.\d+|\d+)([eE][+-]?\d+)?/.exec(text.slice(state.i));
|
|
95
|
+
const raw = m[0];
|
|
96
|
+
state.i += raw.length;
|
|
97
|
+
const value = Number(raw);
|
|
98
|
+
const integer = !/[.eE]/.test(raw);
|
|
99
|
+
if (text[state.i] === '%') { state.i++; return { type: 'percentage', value, raw }; }
|
|
100
|
+
if (tokStartsIdent(text, state.i)) return { type: 'dimension', value, unit: tokReadName(text, state) };
|
|
101
|
+
return { type: 'number', value, integer };
|
|
102
|
+
}
|
|
103
|
+
function tokReadUrlEscape(text, state) {
|
|
104
|
+
const n = text.length;
|
|
105
|
+
if (text[state.i + 1] === '\n') return null;
|
|
106
|
+
state.i++;
|
|
107
|
+
const hex = /^[0-9a-fA-F]{1,6}/.exec(text.slice(state.i));
|
|
108
|
+
if (hex) { const value = escapedCodePoint(hex[0]); state.i += hex[0].length; if (/\s/.test(text[state.i] || '')) state.i++; return value; }
|
|
109
|
+
if (state.i < n) { const value = text[state.i]; state.i++; return value; }
|
|
110
|
+
return '\ufffd';
|
|
111
|
+
}
|
|
112
|
+
function tokReadUrlBody(text, state) {
|
|
113
|
+
const n = text.length;
|
|
114
|
+
let value = '';
|
|
115
|
+
while (state.i < n && text[state.i] !== ')') {
|
|
116
|
+
const d = text[state.i];
|
|
117
|
+
if (/\s/.test(d)) { while (state.i < n && /\s/.test(text[state.i])) state.i++; if (state.i < n && text[state.i] !== ')') return null; break; }
|
|
118
|
+
if (d === '"' || d === "'" || d === '(') return null;
|
|
119
|
+
if (d === '\\') { const escaped = tokReadUrlEscape(text, state); if (escaped === null) return null; value += escaped; continue; }
|
|
120
|
+
value += d; state.i++;
|
|
121
|
+
}
|
|
122
|
+
if (state.i < n) state.i++;
|
|
123
|
+
return { type: 'url', value };
|
|
124
|
+
}
|
|
125
|
+
function tokReadIdentOrFunc(text, state) {
|
|
126
|
+
const n = text.length;
|
|
127
|
+
const name = tokReadName(text, state);
|
|
128
|
+
if (text[state.i] !== '(') return { type: 'ident', value: name };
|
|
129
|
+
state.i++;
|
|
130
|
+
if (name.toLowerCase() === 'url') {
|
|
131
|
+
let j = state.i;
|
|
132
|
+
while (j < n && /\s/.test(text[j])) j++;
|
|
133
|
+
if (text[j] !== '"' && text[j] !== "'") {
|
|
134
|
+
state.i = j;
|
|
135
|
+
return tokReadUrlBody(text, state);
|
|
151
136
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
137
|
+
}
|
|
138
|
+
return { type: 'function', name };
|
|
139
|
+
}
|
|
140
|
+
const PUNCT_TOKENS = { ',': { type: 'comma' }, '(': { type: 'open', value: '(' }, '[': { type: 'open', value: '[' }, '{': { type: 'open', value: '{' }, ')': { type: 'close', value: ')' }, ']': { type: 'close', value: ']' }, '}': { type: 'close', value: '}' }, ';': { type: 'semicolon' } };
|
|
141
|
+
function tokPunctuation(c) {
|
|
142
|
+
return PUNCT_TOKENS[c] ?? { type: 'delim', value: c };
|
|
143
|
+
}
|
|
144
|
+
function tokenizeComment(text, state, n) {
|
|
145
|
+
if (text[state.i] !== '/' || text[state.i + 1] !== '*') return false;
|
|
146
|
+
const end = text.indexOf('*/', state.i + 2);
|
|
147
|
+
state.i = end < 0 ? n : end + 2;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function tokenizeWhitespace(text, state, n, out) {
|
|
151
|
+
if (!/\s/.test(text[state.i])) return false;
|
|
152
|
+
while (state.i < n && /\s/.test(text[state.i])) state.i++;
|
|
153
|
+
out.push({ type: 'ws' });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
function tokenizeHash(text, state, n, out) {
|
|
157
|
+
if (text[state.i] !== '#' || state.i + 1 >= n || !(isName(text[state.i + 1]) || text[state.i + 1] === '\\')) return false;
|
|
158
|
+
state.i++;
|
|
159
|
+
out.push({ type: 'hash', value: tokReadName(text, state) });
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function tokenizeStep(text, state, n, out) {
|
|
163
|
+
if (tokenizeComment(text, state, n)) return true;
|
|
164
|
+
if (tokenizeWhitespace(text, state, n, out)) return true;
|
|
165
|
+
const c = text[state.i];
|
|
166
|
+
if (c === '"' || c === "'") { const token = tokReadString(text, state, c); if (!token) return false; out.push(token); return true; }
|
|
167
|
+
if (tokStartsNumber(text, state.i)) { out.push(tokReadNumeric(text, state)); return true; }
|
|
168
|
+
if (tokStartsIdent(text, state.i)) { const token = tokReadIdentOrFunc(text, state); if (!token) return false; out.push(token); return true; }
|
|
169
|
+
if (tokenizeHash(text, state, n, out)) return true;
|
|
170
|
+
state.i++;
|
|
171
|
+
out.push(tokPunctuation(c));
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
function tokenize(text) {
|
|
175
|
+
const out = [];
|
|
176
|
+
const state = { i: 0 };
|
|
177
|
+
const n = text.length;
|
|
178
|
+
while (state.i < n) {
|
|
179
|
+
if (!tokenizeStep(text, state, n, out)) return null;
|
|
159
180
|
}
|
|
160
181
|
return out;
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
// ------------------------------------------------------------- components ----
|
|
164
185
|
const LENGTH_UNITS = new Set(['px', 'em', 'rem', 'ex', 'rex', 'ch', 'rch', 'ic', 'ric', 'cap', 'rcap', 'lh', 'rlh', 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', 'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax', 'lvw', 'lvh', 'lvi', 'lvb', 'lvmin', 'lvmax', 'dvw', 'dvh', 'dvi', 'dvb', 'dvmin', 'dvmax', 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', 'cm', 'mm', 'q', 'in', 'pt', 'pc']);
|
|
165
|
-
const MATH_FUNCTIONS = new Set(['calc', 'min', 'max', 'clamp', 'round', 'mod', 'rem', 'abs', 'sign', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'calc-size']);
|
|
186
|
+
const MATH_FUNCTIONS = new Set(['calc', 'min', 'max', 'clamp', 'round', 'mod', 'rem', 'abs', 'sign', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'calc-size', 'sibling-index', 'sibling-count']);
|
|
166
187
|
const IMAGE_FUNCTIONS = new Set(['linear-gradient', 'radial-gradient', 'conic-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient', 'repeating-conic-gradient', '-webkit-linear-gradient', '-webkit-radial-gradient', '-webkit-repeating-linear-gradient', '-webkit-repeating-radial-gradient', '-webkit-gradient', 'image-set', '-webkit-image-set', 'cross-fade', '-webkit-cross-fade', 'paint', 'image']);
|
|
167
188
|
const COLOR_FUNCTIONS = new Set(['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color', 'color-mix', 'light-dark', 'contrast-color', '-webkit-device-cmyk']);
|
|
168
189
|
const SYSTEM_COLORS = new Set(['currentcolor', 'transparent', 'accentcolor', 'accentcolortext', 'activetext', 'buttonborder', 'buttonface', 'buttontext', 'canvas', 'canvastext', 'field', 'fieldtext', 'graytext', 'highlight', 'highlighttext', 'linktext', 'mark', 'marktext', 'selecteditem', 'selecteditemtext', 'visitedtext', 'activeborder', 'activecaption', 'appworkspace', 'background', 'buttonhighlight', 'buttonshadow', 'captiontext', 'inactiveborder', 'inactivecaption', 'inactivecaptiontext', 'infobackground', 'infotext', 'menu', 'menutext', 'scrollbar', 'threeddarkshadow', 'threedface', 'threedhighlight', 'threedlightshadow', 'threedshadow', 'window', 'windowframe', 'windowtext', '-webkit-link', '-webkit-focus-ring-color', '-internal-quirk-inherit']);
|
|
@@ -190,51 +211,65 @@ function join(comps) {
|
|
|
190
211
|
* parentheses / `calc()`, one-unit `min()` / `max()` / `clamp()` already simplified to
|
|
191
212
|
* `calc(...)`), absolute lengths combined in px; `null` when Chrome keeps it unsimplified.
|
|
192
213
|
*/
|
|
214
|
+
function sumOperand(comp) {
|
|
215
|
+
const terms = literalTerms(comp);
|
|
216
|
+
if (terms) return terms;
|
|
217
|
+
if (comp.kind === 'function' && comp.name === '(') { const nested = components(comp.text.slice(1, -1)); return nested && sumTerms(nested); }
|
|
218
|
+
// A nested math function Chrome simplified already spells `calc(...)`.
|
|
219
|
+
if (isMath(comp) && comp.text.startsWith('calc(')) {
|
|
220
|
+
const nested = components(comp.text.slice(5, -1));
|
|
221
|
+
return nested && sumTerms(nested);
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const sumScalar = (terms) => (terms.length === 1 && terms[0][0] === '' ? terms[0][1] : null);
|
|
226
|
+
const sumScale = (terms, factor) => terms.map(([unit, value]) => [unit, value * factor]);
|
|
227
|
+
function sumApplyOperator(op, current, next) {
|
|
228
|
+
if (op.kind === 'delim' && op.value === '*') {
|
|
229
|
+
const b = sumScalar(next);
|
|
230
|
+
return { current: b !== null ? sumScale(current, b) : sumScale(next, sumScalar(current)) };
|
|
231
|
+
}
|
|
232
|
+
if (op.kind === 'slash' || (op.kind === 'delim' && op.value === '/')) {
|
|
233
|
+
const b = sumScalar(next);
|
|
234
|
+
return b === null ? null : { current: sumScale(current, 1 / b) };
|
|
235
|
+
}
|
|
236
|
+
// The remaining case is +/- by precondition: every caller of sumTerms (the only caller of
|
|
237
|
+
// sumApplyOperator) runs after functionValid / checkedSum, which rejects any other operator
|
|
238
|
+
// at an odd position. Chrome 156 fixture-exercised in test/css-shorthand-chrome.test.mjs.
|
|
239
|
+
return { current: next, flush: op.value === '+' ? 1 : -1 };
|
|
240
|
+
}
|
|
193
241
|
function sumTerms(inner) {
|
|
194
242
|
// Operands alternate with operators; products bind first.
|
|
195
243
|
if (inner.length % 2 === 0) return null;
|
|
196
|
-
const CONSTANTS = { e: Math.E, pi: Math.PI, infinity: Infinity, '-infinity': -Infinity, nan: NaN };
|
|
197
|
-
const operand = (comp) => {
|
|
198
|
-
if (comp.kind === 'number') return [['', comp.value]];
|
|
199
|
-
if (comp.kind === 'percentage') return [['%', comp.value]];
|
|
200
|
-
if (comp.kind === 'dimension') return [[comp.unit, comp.value]];
|
|
201
|
-
if (comp.kind === 'ident') { const name = comp.value.toLowerCase(); return name in CONSTANTS ? [['', CONSTANTS[name]]] : null; }
|
|
202
|
-
if (comp.kind === 'function' && comp.name === '(') { const nested = components(comp.text.slice(1, -1)); return nested && sumTerms(nested); }
|
|
203
|
-
// A nested math function Chrome simplified already spells `calc(...)`.
|
|
204
|
-
if (isMath(comp) && comp.text.startsWith('calc(')) {
|
|
205
|
-
const nested = components(comp.text.slice(5, -1));
|
|
206
|
-
return nested && sumTerms(nested);
|
|
207
|
-
}
|
|
208
|
-
return null;
|
|
209
|
-
};
|
|
210
|
-
const scalar = (terms) => (terms.length === 1 && terms[0][0] === '' ? terms[0][1] : null);
|
|
211
|
-
const scale = (terms, factor) => terms.map(([unit, value]) => [unit, value * factor]);
|
|
212
244
|
const terms = [];
|
|
213
245
|
const push = (product, sign) => {
|
|
214
246
|
for (const [unit, value] of product) { const existing = terms.find((t) => t[0] === unit); if (existing) existing[1] += sign * value; else terms.push([unit, sign * value]); }
|
|
215
247
|
};
|
|
216
248
|
let sign = 1;
|
|
217
|
-
let current =
|
|
249
|
+
let current = sumOperand(inner[0]);
|
|
218
250
|
if (!current) return null;
|
|
219
251
|
for (let index = 1; index < inner.length; index += 2) {
|
|
220
|
-
const next =
|
|
252
|
+
const next = sumOperand(inner[index + 1]);
|
|
221
253
|
if (!next) return null;
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
current = b !== null ? scale(current, b) : scale(next, scalar(current));
|
|
227
|
-
} else if (op.kind === 'slash' || (op.kind === 'delim' && op.value === '/')) {
|
|
228
|
-
const b = scalar(next);
|
|
229
|
-
if (b === null) return null;
|
|
230
|
-
current = scale(current, 1 / b);
|
|
231
|
-
} else if (op.kind === 'delim' && (op.value === '+' || op.value === '-')) {
|
|
232
|
-
push(current, sign);
|
|
233
|
-
sign = op.value === '+' ? 1 : -1;
|
|
234
|
-
current = next;
|
|
235
|
-
} else return null;
|
|
254
|
+
const applied = sumApplyOperator(inner[index], current, next);
|
|
255
|
+
if (!applied) return null;
|
|
256
|
+
if (applied.flush !== undefined) { push(current, sign); sign = applied.flush; }
|
|
257
|
+
current = applied.current;
|
|
236
258
|
}
|
|
237
259
|
push(current, sign);
|
|
260
|
+
return canonicalTerms(terms);
|
|
261
|
+
}
|
|
262
|
+
const CONSTANTS = { e: Math.E, pi: Math.PI, infinity: Infinity, '-infinity': -Infinity, nan: NaN };
|
|
263
|
+
/** A literal `calc()` operand's one term (a number, a percentage, a dimension, a constant), else null. */
|
|
264
|
+
function literalTerms(comp) {
|
|
265
|
+
if (comp.kind === 'number') return [['', comp.value]];
|
|
266
|
+
if (comp.kind === 'percentage') return [['%', comp.value]];
|
|
267
|
+
if (comp.kind === 'dimension') return [[comp.unit, comp.value]];
|
|
268
|
+
if (comp.kind === 'ident') { const name = comp.value.toLowerCase(); return name in CONSTANTS ? [['', CONSTANTS[name]]] : null; }
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
/** Like-unit terms with absolute lengths in px and times, angles and frequencies in their canonical unit. */
|
|
272
|
+
function canonicalTerms(terms) {
|
|
238
273
|
// Absolute lengths combine in px.
|
|
239
274
|
const ABSOLUTE = { in: 96, cm: 96 / 2.54, mm: 96 / 25.4, q: 96 / 101.6, pt: 96 / 72, pc: 16 };
|
|
240
275
|
if (terms.some(([unit]) => unit in ABSOLUTE)) {
|
|
@@ -259,6 +294,7 @@ function sumTerms(inner) {
|
|
|
259
294
|
return terms;
|
|
260
295
|
}
|
|
261
296
|
function calcSum(inner) {
|
|
297
|
+
if (hasTreeCounting(inner)) return treeCalcText(inner);
|
|
262
298
|
const terms = sumTerms(inner);
|
|
263
299
|
return terms && calcText(terms);
|
|
264
300
|
}
|
|
@@ -268,36 +304,42 @@ const unitCategory = (unit) => (LENGTH_UNITS.has(unit) ? 0 : unit === 's' || uni
|
|
|
268
304
|
* math function), `null` when Chrome rejects it: a sum mixes numbers and dimensions or two
|
|
269
305
|
* dimension categories, a product multiplies two dimensions, or a divisor has a unit.
|
|
270
306
|
*/
|
|
307
|
+
function calcTypeOperand(comp) {
|
|
308
|
+
if (comp.kind === 'number') return 'number';
|
|
309
|
+
if (comp.kind === 'percentage') return 'percent';
|
|
310
|
+
if (comp.kind === 'dimension') { const category = unitCategory(comp.unit); return category === null ? null : `unit${category}`; }
|
|
311
|
+
if (comp.kind === 'function' && comp.name === '(') { const nested = components(comp.text.slice(1, -1)); return nested && calcType(nested); }
|
|
312
|
+
if (isMath(comp)) return mathType(comp);
|
|
313
|
+
if (comp.kind === 'ident' && ['e', 'pi', 'infinity', '-infinity', 'nan'].includes(comp.value.toLowerCase())) return 'number';
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
function calcTypeMultiply(product, next, total) {
|
|
317
|
+
if (unitType(product) && unitType(next)) return null;
|
|
318
|
+
return { product: (product === 'number' || product === 'unknown') ? next : product, total };
|
|
319
|
+
}
|
|
320
|
+
function calcTypeAdd(product, next, total) {
|
|
321
|
+
const newTotal = total === null ? product : sumType(total, product);
|
|
322
|
+
return newTotal === null ? null : { product: next, total: newTotal };
|
|
323
|
+
}
|
|
324
|
+
function calcTypeStep(op, product, next, total) {
|
|
325
|
+
if (op.kind === 'delim' && op.value === '*') return calcTypeMultiply(product, next, total);
|
|
326
|
+
if (op.kind === 'slash' || (op.kind === 'delim' && op.value === '/')) return unitType(next) ? null : { product, total };
|
|
327
|
+
if (op.kind === 'delim' && (op.value === '+' || op.value === '-')) return calcTypeAdd(product, next, total);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
271
330
|
function calcType(inner) {
|
|
272
|
-
const operand = (comp) => {
|
|
273
|
-
if (comp.kind === 'number') return 'number';
|
|
274
|
-
if (comp.kind === 'percentage') return 'percent';
|
|
275
|
-
if (comp.kind === 'dimension') { const category = unitCategory(comp.unit); return category === null ? null : `unit${category}`; }
|
|
276
|
-
if (comp.kind === 'function' && comp.name === '(') { const nested = components(comp.text.slice(1, -1)); return nested && calcType(nested); }
|
|
277
|
-
if (isMath(comp)) return mathType(comp);
|
|
278
|
-
if (comp.kind === 'ident' && ['e', 'pi', 'infinity', '-infinity', 'nan'].includes(comp.value.toLowerCase())) return 'number';
|
|
279
|
-
return null;
|
|
280
|
-
};
|
|
281
|
-
const unit = unitType, sum = sumType;
|
|
282
331
|
if (!inner.length) return null;
|
|
283
332
|
let total = null;
|
|
284
|
-
let product =
|
|
333
|
+
let product = calcTypeOperand(inner[0]);
|
|
285
334
|
if (product === null) return null;
|
|
286
335
|
for (let index = 1; index < inner.length; index += 2) {
|
|
287
|
-
const
|
|
336
|
+
const next = inner[index + 1] && calcTypeOperand(inner[index + 1]);
|
|
288
337
|
if (!next) return null;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
} else if (operator.kind === 'delim' && (operator.value === '+' || operator.value === '-')) {
|
|
295
|
-
total = total === null ? product : sum(total, product);
|
|
296
|
-
if (total === null) return null;
|
|
297
|
-
product = next;
|
|
298
|
-
} else return null;
|
|
299
|
-
}
|
|
300
|
-
return total === null ? product : sum(total, product);
|
|
338
|
+
const stepped = calcTypeStep(inner[index], product, next, total);
|
|
339
|
+
if (!stepped) return null;
|
|
340
|
+
product = stepped.product; total = stepped.total;
|
|
341
|
+
}
|
|
342
|
+
return total === null ? product : sumType(total, product);
|
|
301
343
|
}
|
|
302
344
|
const unitType = (type) => type === 'percent' || type.startsWith('unit');
|
|
303
345
|
/** The type of a sum of two types (`null`: Chrome rejects adding them). */
|
|
@@ -317,7 +359,7 @@ const isMath = (c) => c.kind === 'function' && MATH_FUNCTIONS.has(c.name);
|
|
|
317
359
|
function mathType(comp) {
|
|
318
360
|
const inner = innerOf(comp);
|
|
319
361
|
if (comp.text.startsWith('calc(')) return calcType(inner);
|
|
320
|
-
if (comp.name === 'sign') return 'number';
|
|
362
|
+
if (comp.name === 'sign' || comp.name === 'sibling-index' || comp.name === 'sibling-count') return 'number';
|
|
321
363
|
return ['min', 'max', 'clamp', 'abs'].includes(comp.name) ? argumentsType(inner) : 'unknown';
|
|
322
364
|
}
|
|
323
365
|
/** A function's comma-separated arguments. */
|
|
@@ -355,50 +397,52 @@ function argumentsType(inner) {
|
|
|
355
397
|
}
|
|
356
398
|
return total;
|
|
357
399
|
}
|
|
400
|
+
function rgbHslValid(name, inner) {
|
|
401
|
+
const commasCount = inner.filter((c) => c.kind === 'comma').length;
|
|
402
|
+
// Relative color syntax: `from <color>`, three channels, then `/ alpha`.
|
|
403
|
+
if (inner.length && isIdent(inner[0], ['from'])) return commasCount === 0 && (inner.length === 5 || (inner.length === 7 && inner[5].kind === 'slash'));
|
|
404
|
+
// A hue: a number, an angle, `none` (modern syntax) or a math function of those.
|
|
405
|
+
const hue = (c) => c.kind === 'number' || (c.kind === 'dimension' && unitCategory(c.unit) === 2) || (commasCount === 0 && isIdent(c, ['none'])) || (isMath(c) && ['number', 'unit2', 'unknown'].includes(mathType(c)));
|
|
406
|
+
if (name.startsWith('hsl') && inner.length && !hue(inner[0])) return false;
|
|
407
|
+
if (commasCount) return (commasCount === 2 || commasCount === 3) && inner.length === 2 * commasCount + 1 && inner.every((c, index) => (c.kind === 'comma') === (index % 2 === 1));
|
|
408
|
+
const slash = inner.findIndex((c) => c.kind === 'slash');
|
|
409
|
+
return slash < 0 ? inner.length === 3 : slash === 3 && inner.length === 5;
|
|
410
|
+
}
|
|
411
|
+
function linearValid(inner) {
|
|
412
|
+
// Each stop: a number output and at most two percentage positions.
|
|
413
|
+
return inner.some((c) => c.kind === 'comma') && argumentsOf(inner).every((stop) => stop.length >= 1 && stop.length <= 3 && stop[0].kind === 'number' && stop.slice(1).every((c) => c.kind === 'percentage'));
|
|
414
|
+
}
|
|
415
|
+
function stepsValid(inner) {
|
|
416
|
+
const count = (c, minimum) => (c.kind === 'number' ? c.integer && c.value >= minimum : c.kind === 'function' && MATH_FUNCTIONS.has(c.name));
|
|
417
|
+
if (inner.length === 1) return count(inner[0], 1);
|
|
418
|
+
if (inner.length === 3 && inner[1].kind === 'comma') {
|
|
419
|
+
const position = keyword(inner[2], ['start', 'end', 'jump-start', 'jump-end', 'jump-none', 'jump-both']);
|
|
420
|
+
return position !== null && count(inner[0], position === 'jump-none' ? 2 : 1);
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
function cubicBezierValid(inner) {
|
|
425
|
+
const args = argumentsOf(inner);
|
|
426
|
+
return args.length === 4 && args.every((arg, index) => arg.length === 1 && arg[0].kind === 'number' && (index % 2 === 1 || (arg[0].value >= 0 && arg[0].value <= 1)));
|
|
427
|
+
}
|
|
428
|
+
const COLOR_FN_NAMES = new Set(['rgb', 'rgba', 'hsl', 'hsla']);
|
|
358
429
|
/**
|
|
359
|
-
* Structural checks Chrome makes before keeping a function
|
|
360
|
-
* and operators and type-checks, `min()` / `max()` / `clamp()` have their arguments, `rgb()` /
|
|
361
|
-
* `hsl()` have three channels (and an alpha) in one syntax, `linear()` stops have at most two
|
|
362
|
-
* positions, `steps()` counts and `cubic-bezier()` x coordinates are in range.
|
|
430
|
+
* Structural checks Chrome makes before keeping a function.
|
|
363
431
|
*/
|
|
364
432
|
function functionValid(name, inner) {
|
|
365
433
|
if (name === 'calc') return checkedSum(inner) !== null;
|
|
366
434
|
if (name === 'min' || name === 'max') return !argumentsOf(inner).some(isNone) && argumentsType(inner) !== null;
|
|
367
435
|
if (name === 'clamp') { const args = argumentsOf(inner); return args.length === 3 && !isNone(args[1]) && argumentsType(inner) !== null; }
|
|
368
436
|
if (name === 'abs' || name === 'sign') return !inner.some(isComma) && checkedSum(inner) !== null;
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return slash < 0 ? inner.length === 3 : slash === 3 && inner.length === 5;
|
|
379
|
-
}
|
|
380
|
-
if (name === 'linear') {
|
|
381
|
-
// Each stop: a number output and at most two percentage positions (literals once `read`
|
|
382
|
-
// resolved the math functions).
|
|
383
|
-
return inner.some((c) => c.kind === 'comma') && argumentsOf(inner).every((stop) => stop.length >= 1 && stop.length <= 3 && stop[0].kind === 'number' && stop.slice(1).every((c) => c.kind === 'percentage'));
|
|
384
|
-
}
|
|
385
|
-
if (name === 'steps') {
|
|
386
|
-
// The step count: an integer literal, or a math function Chrome checks later.
|
|
387
|
-
const count = (c, minimum) => (c.kind === 'number' ? c.integer && c.value >= minimum : c.kind === 'function' && MATH_FUNCTIONS.has(c.name));
|
|
388
|
-
if (inner.length === 1) return count(inner[0], 1);
|
|
389
|
-
if (inner.length === 3 && inner[1].kind === 'comma') {
|
|
390
|
-
const position = keyword(inner[2], ['start', 'end', 'jump-start', 'jump-end', 'jump-none', 'jump-both']);
|
|
391
|
-
return position !== null && count(inner[0], position === 'jump-none' ? 2 : 1);
|
|
392
|
-
}
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
if (name === 'cubic-bezier') {
|
|
396
|
-
// Four numbers (literals once `read` resolved the math functions); the x coordinates are in [0, 1].
|
|
397
|
-
const args = argumentsOf(inner);
|
|
398
|
-
return args.length === 4 && args.every((arg, index) => arg.length === 1 && arg[0].kind === 'number' && (index % 2 === 1 || (arg[0].value >= 0 && arg[0].value <= 1)));
|
|
399
|
-
}
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
437
|
+
if (COLOR_FN_NAMES.has(name)) return rgbHslValid(name, inner);
|
|
438
|
+
if (name === 'linear') return linearValid(inner);
|
|
439
|
+
if (name === 'steps') return stepsValid(inner);
|
|
440
|
+
if (name === 'cubic-bezier') return cubicBezierValid(inner);
|
|
441
|
+
return treeArgumentsValid(name, inner);
|
|
442
|
+
}
|
|
443
|
+
/** The tree-counting functions take no arguments. */
|
|
444
|
+
const treeArgumentsValid = (name, inner) => !TREE_COUNTING.has(name) || inner.length === 0;
|
|
445
|
+
const TREE_COUNTING = new Set(['sibling-index', 'sibling-count']);
|
|
402
446
|
/** `calc()` of simplified terms: numbers, then percentages, then units in ASCII order. */
|
|
403
447
|
function calcText(terms) {
|
|
404
448
|
if (terms.length === 1 && !Number.isFinite(terms[0][1])) {
|
|
@@ -457,6 +501,227 @@ function absSign(name, inner) {
|
|
|
457
501
|
// `sign()` keeps a (signed) zero and NaN.
|
|
458
502
|
return name === 'abs' ? calcText([[term[0], Math.abs(term[1])]]) : calcText([['', term[1] === 0 || Number.isNaN(term[1]) ? term[1] : Math.sign(term[1])]]);
|
|
459
503
|
}
|
|
504
|
+
// ------------------------------------------------------ shape() (#338) ----
|
|
505
|
+
// Lockstep port of rtdom/css_shorthand/shape.rs: `shape(<fill-rule>? from <position>,
|
|
506
|
+
// <shape-command>#)` with Chrome 156's spelling (the default `nonzero` rule, `from origin` /
|
|
507
|
+
// `from start` anchors, `ccw` / `small` / `rotate 0deg` arc flags left out).
|
|
508
|
+
function shapePositionText(comps) {
|
|
509
|
+
if (comps.length === 3) return null;
|
|
510
|
+
const point = position(comps);
|
|
511
|
+
return point && `${point[0]} ${point[1]}`;
|
|
512
|
+
}
|
|
513
|
+
function coordinatePair(comps) {
|
|
514
|
+
if (comps.length !== 2) return null;
|
|
515
|
+
const [x, y] = comps.map((c) => lengthPercentage(c, true));
|
|
516
|
+
return x === null || y === null ? null : `${x} ${y}`;
|
|
517
|
+
}
|
|
518
|
+
/** A control point: a position (`to` commands) or a pair anchored `from start | end | origin`. */
|
|
519
|
+
function controlPoint(comps, absolute) {
|
|
520
|
+
if (comps.length === 4) {
|
|
521
|
+
const anchor = isIdent(comps[2], ['from']) ? keyword(comps[3], ['start', 'end', 'origin']) : null;
|
|
522
|
+
const pair = anchor && coordinatePair(comps.slice(0, 2));
|
|
523
|
+
if (!pair) return null;
|
|
524
|
+
return anchor === (absolute ? 'origin' : 'start') ? pair : `${pair} from ${anchor}`;
|
|
525
|
+
}
|
|
526
|
+
return absolute ? shapePositionText(comps) : coordinatePair(comps);
|
|
527
|
+
}
|
|
528
|
+
/** `to <position>` or `by <coordinate-pair>`: [text, absolute], or null. */
|
|
529
|
+
function endPoint(comps) {
|
|
530
|
+
const way = comps.length ? keyword(comps[0], ['to', 'by']) : null;
|
|
531
|
+
const text = way === 'to' ? shapePositionText(comps.slice(1)) : way === 'by' ? coordinatePair(comps.slice(1)) : null;
|
|
532
|
+
return text === null ? null : [`${way} ${text}`, way === 'to'];
|
|
533
|
+
}
|
|
534
|
+
/** `with <control-point> [/ <control-point>]?`. */
|
|
535
|
+
function shapeControls(comps, absolute, two) {
|
|
536
|
+
// splitEnd cut the end point at the `with`.
|
|
537
|
+
if (!comps.length) return null;
|
|
538
|
+
const points = split(comps.slice(1), isSlash);
|
|
539
|
+
if (points.length > (two ? 2 : 1)) return null;
|
|
540
|
+
const texts = points.map((point) => controlPoint(point, absolute));
|
|
541
|
+
return texts.includes(null) ? null : `with ${texts.join(' / ')}`;
|
|
542
|
+
}
|
|
543
|
+
/** The end point at the front of `comps` (up to a `stop` keyword) and the rest: [text, absolute, rest]. */
|
|
544
|
+
function splitEnd(comps, stop) {
|
|
545
|
+
const at = comps.findIndex((comp, index) => index > 0 && isIdent(comp, stop));
|
|
546
|
+
const end = endPoint(at < 0 ? comps : comps.slice(0, at));
|
|
547
|
+
return end && [...end, at < 0 ? [] : comps.slice(at)];
|
|
548
|
+
}
|
|
549
|
+
function axisLine(comps, sides) {
|
|
550
|
+
if (comps.length !== 2) return null;
|
|
551
|
+
const way = keyword(comps[0], ['to', 'by']);
|
|
552
|
+
const value = way === 'to' ? keyword(comps[1], sides) ?? lengthPercentage(comps[1], true) : way === 'by' ? lengthPercentage(comps[1], true) : null;
|
|
553
|
+
return value === null ? null : `${way} ${value}`;
|
|
554
|
+
}
|
|
555
|
+
/** `of <length-percentage>{1,2}` at the front of `rest`: the components it used, or 0. */
|
|
556
|
+
function arcRadii(rest, parts) {
|
|
557
|
+
const values = [];
|
|
558
|
+
for (const comp of rest.slice(1, 3)) { const value = lengthPercentage(comp, true); if (value === null) break; values.push(value); }
|
|
559
|
+
parts.radii = values.join(' ');
|
|
560
|
+
return values.length && values.length + 1;
|
|
561
|
+
}
|
|
562
|
+
/** One arc part at the front of `rest` into `parts`: the components it used, or 0. */
|
|
563
|
+
function arcPart(rest, parts) {
|
|
564
|
+
const word = ident(rest[0]);
|
|
565
|
+
if (word === 'of' && parts.radii === undefined) return arcRadii(rest, parts);
|
|
566
|
+
const flag = (word === 'cw' || word === 'ccw') ? 'sweep' : (word === 'large' || word === 'small') ? 'size' : null;
|
|
567
|
+
if (flag && parts[flag] === undefined) { parts[flag] = word; return 1; }
|
|
568
|
+
if (word === 'rotate' && parts.rotate === undefined && rest[1] && angleStrict(rest[1]) !== null) { parts.rotate = rest[1]; return 2; }
|
|
569
|
+
return 0;
|
|
570
|
+
}
|
|
571
|
+
/** `arc <end> [of <lp>{1,2}] && [cw | ccw]? && [large | small]? && [rotate <angle>]?`. */
|
|
572
|
+
function arcCommand(comps) {
|
|
573
|
+
const end = splitEnd(comps, ['of', 'cw', 'ccw', 'large', 'small', 'rotate']);
|
|
574
|
+
if (!end) return null;
|
|
575
|
+
const parts = {};
|
|
576
|
+
for (let rest = end[2]; rest.length;) { const used = arcPart(rest, parts); if (!used) return null; rest = rest.slice(used); }
|
|
577
|
+
if (!parts.radii) return null;
|
|
578
|
+
let out = `${end[0]} of ${parts.radii}`;
|
|
579
|
+
if (parts.sweep === 'cw') out += ' cw';
|
|
580
|
+
if (parts.size === 'large') out += ' large';
|
|
581
|
+
const rotate = parts.rotate;
|
|
582
|
+
return rotate && !(rotate.kind === 'dimension' && rotate.value === 0) ? `${out} rotate ${rotate.text}` : out;
|
|
583
|
+
}
|
|
584
|
+
/** `curve` / `smooth`: the end point, then the control point(s) (`smooth`'s optional). */
|
|
585
|
+
function curveCommand(rest, curve) {
|
|
586
|
+
const end = splitEnd(rest, ['with']);
|
|
587
|
+
if (!end) return null;
|
|
588
|
+
if (!curve && !end[2].length) return end[0];
|
|
589
|
+
const controls = shapeControls(end[2], end[1], curve);
|
|
590
|
+
return controls === null ? null : `${end[0]} ${controls}`;
|
|
591
|
+
}
|
|
592
|
+
const SHAPE_COMMANDS = {
|
|
593
|
+
close: (rest) => (rest.length ? null : ''),
|
|
594
|
+
move: (rest) => { const end = endPoint(rest); return end && end[0]; },
|
|
595
|
+
hline: (rest) => axisLine(rest, ['left', 'center', 'right', 'x-start', 'x-end']),
|
|
596
|
+
vline: (rest) => axisLine(rest, ['top', 'center', 'bottom', 'y-start', 'y-end']),
|
|
597
|
+
curve: (rest) => curveCommand(rest, true),
|
|
598
|
+
smooth: (rest) => curveCommand(rest, false),
|
|
599
|
+
arc: arcCommand,
|
|
600
|
+
};
|
|
601
|
+
SHAPE_COMMANDS.line = SHAPE_COMMANDS.move;
|
|
602
|
+
/** One `<shape-command>`. */
|
|
603
|
+
function shapeCommand(comps) {
|
|
604
|
+
const name = comps.length ? ident(comps[0]) : null;
|
|
605
|
+
const parse = name !== null && Object.hasOwn(SHAPE_COMMANDS, name) ? SHAPE_COMMANDS[name] : null;
|
|
606
|
+
const text = parse && parse(comps.slice(1));
|
|
607
|
+
if (text === null || text === undefined) return null;
|
|
608
|
+
return text ? `${name} ${text}` : name;
|
|
609
|
+
}
|
|
610
|
+
/** A `shape()` function in Chrome's spelling, or null when Chrome rejects it. */
|
|
611
|
+
function shapeText(comp) {
|
|
612
|
+
const items = split(innerOf(comp), isComma);
|
|
613
|
+
// One trailing comma is dropped.
|
|
614
|
+
if (items.length > 2 && !items[items.length - 1].length) items.pop();
|
|
615
|
+
let head = items[0];
|
|
616
|
+
const rule = head.length && isIdent(head[0], ['nonzero', 'evenodd']) ? ident(head[0]) : null;
|
|
617
|
+
if (rule) head = head.slice(1);
|
|
618
|
+
const from = head.length && isIdent(head[0], ['from']) ? shapePositionText(head.slice(1)) : null;
|
|
619
|
+
if (from === null || items.length < 2) return null;
|
|
620
|
+
const parts = [`from ${from}`];
|
|
621
|
+
for (const item of items.slice(1)) { const command = shapeCommand(item); if (command === null) return null; parts.push(command); }
|
|
622
|
+
return `shape(${rule === 'evenodd' ? 'evenodd ' : ''}${parts.join(', ')})`;
|
|
623
|
+
}
|
|
624
|
+
/** `shape() || <box>` (`fallback` left out), or null when `comps` hold no valid shape(). */
|
|
625
|
+
function shapeWithBox(comps, boxes, fallback) {
|
|
626
|
+
const isShape = (c) => functionNamed(c, ['shape']) !== null;
|
|
627
|
+
let shape = null, box = null;
|
|
628
|
+
if (comps.length === 1) [shape] = comps;
|
|
629
|
+
else if (comps.length === 2) [shape, box] = isShape(comps[0]) ? comps : [comps[1], comps[0]];
|
|
630
|
+
if (!shape || !isShape(shape)) return null;
|
|
631
|
+
const text = shapeText(shape);
|
|
632
|
+
const name = box && keyword(box, boxes);
|
|
633
|
+
if (text === null || (box && name === null)) return null;
|
|
634
|
+
return name && name !== fallback ? `${text} ${name}` : text;
|
|
635
|
+
}
|
|
636
|
+
// ----------------------------------------------- tree-counting math (#338) ----
|
|
637
|
+
// Math expressions Chrome keeps unresolved at parse time because an operand is only known at
|
|
638
|
+
// computed-value time: the tree-counting functions `sibling-index()` / `sibling-count()`.
|
|
639
|
+
// Lockstep port of rtdom/css_shorthand/math.rs: literal factors of a product multiply into one
|
|
640
|
+
// leading coefficient, literal terms of a sum add up in front, nested sums and products
|
|
641
|
+
// flatten, a product in a sum (or a sum in a product) is parenthesized.
|
|
642
|
+
/** Whether `comps` hold a tree-counting function, nested ones included. */
|
|
643
|
+
const hasTreeCounting = (comps) => comps.some((comp) => comp.kind === 'function' && (comp.text.includes('sibling-index()') || comp.text.includes('sibling-count()')));
|
|
644
|
+
/** Add like-unit `terms` into `into`, negated when `negative`. */
|
|
645
|
+
function addTerms(into, terms, negative) {
|
|
646
|
+
for (const [unit, value] of terms) {
|
|
647
|
+
const existing = into.find((term) => term[0] === unit);
|
|
648
|
+
if (existing) existing[1] += negative ? -value : value;
|
|
649
|
+
else into.push([unit, negative ? -value : value]);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function treeOperand(comp) {
|
|
653
|
+
const terms = literalTerms(comp);
|
|
654
|
+
if (terms) return { kind: 'literal', terms: canonicalTerms(terms) };
|
|
655
|
+
if (comp.kind === 'function' && comp.name === '(') return treeSum(components(comp.text.slice(1, -1)));
|
|
656
|
+
// A nested math function Chrome simplified already spells `calc(...)`.
|
|
657
|
+
if (comp.text.startsWith('calc(')) return treeSum(components(comp.text.slice(5, -1)));
|
|
658
|
+
return { kind: 'function', text: comp.text };
|
|
659
|
+
}
|
|
660
|
+
/** A checked `calc()` sum: operands at even positions, operators between them. */
|
|
661
|
+
function treeSum(comps) {
|
|
662
|
+
const literals = [], rest = [];
|
|
663
|
+
let start = 0, negative = false;
|
|
664
|
+
for (let index = 1; index <= comps.length; index += 2) {
|
|
665
|
+
const next = comps[index];
|
|
666
|
+
if (next && !(next.kind === 'delim' && (next.value === '+' || next.value === '-'))) continue;
|
|
667
|
+
const expr = treeProduct(comps.slice(start, index));
|
|
668
|
+
if (expr.kind === 'literal') addTerms(literals, expr.terms, negative);
|
|
669
|
+
else if (expr.kind === 'sum') { addTerms(literals, expr.literals, negative); for (const [minus, term] of expr.rest) rest.push([minus !== negative, term]); }
|
|
670
|
+
else rest.push([negative, expr]);
|
|
671
|
+
negative = !!next && next.value === '-';
|
|
672
|
+
start = index + 1;
|
|
673
|
+
}
|
|
674
|
+
if (!rest.length) return { kind: 'literal', terms: literals };
|
|
675
|
+
if (!literals.length && rest.length === 1 && !rest[0][0]) return rest[0][1];
|
|
676
|
+
return { kind: 'sum', literals, rest };
|
|
677
|
+
}
|
|
678
|
+
/** Fold one product operand into the coefficient and factors. */
|
|
679
|
+
function treeFactor(coefficient, factors, expr, divisor) {
|
|
680
|
+
if (expr.kind === 'literal' && expr.terms.length === 1) {
|
|
681
|
+
const [unit, value] = expr.terms[0];
|
|
682
|
+
if (divisor) coefficient[1] /= value;
|
|
683
|
+
else { coefficient[1] *= value; if (unit) coefficient[0] = unit; }
|
|
684
|
+
} else if (expr.kind === 'product' && !divisor) {
|
|
685
|
+
coefficient[1] *= expr.coefficient[1];
|
|
686
|
+
if (expr.coefficient[0]) coefficient[0] = expr.coefficient[0];
|
|
687
|
+
factors.push(...expr.factors);
|
|
688
|
+
} else factors.push([divisor, expr]);
|
|
689
|
+
}
|
|
690
|
+
/** A checked product: operands at even positions, `*` / `/` between them. */
|
|
691
|
+
function treeProduct(comps) {
|
|
692
|
+
const coefficient = ['', 1], factors = [];
|
|
693
|
+
for (let index = 0; index < comps.length; index += 2) {
|
|
694
|
+
const divisor = index > 0 && (comps[index - 1].kind === 'slash' || comps[index - 1].value === '/');
|
|
695
|
+
treeFactor(coefficient, factors, treeOperand(comps[index]), divisor);
|
|
696
|
+
}
|
|
697
|
+
if (!factors.length) return { kind: 'literal', terms: [coefficient] };
|
|
698
|
+
if (coefficient[0] === '' && coefficient[1] === 1 && factors.length === 1 && !factors[0][0]) return factors[0][1];
|
|
699
|
+
return { kind: 'product', coefficient, factors };
|
|
700
|
+
}
|
|
701
|
+
const literalText = (terms) => calcText(terms.map((term) => [...term])).slice(5, -1);
|
|
702
|
+
/** `calc()`'s text without the wrapper; `(...)` around a nested sum or product. */
|
|
703
|
+
function treeText(expr, grouped) {
|
|
704
|
+
if (expr.kind === 'function') return expr.text;
|
|
705
|
+
let body;
|
|
706
|
+
if (expr.kind === 'literal') body = literalText(expr.terms);
|
|
707
|
+
else if (expr.kind === 'sum') {
|
|
708
|
+
body = expr.literals.length ? literalText(expr.literals) : '';
|
|
709
|
+
// The first term is never subtracted.
|
|
710
|
+
for (const [minus, term] of expr.rest) body += (body ? (minus ? ' - ' : ' + ') : '') + treeText(term, true);
|
|
711
|
+
} else {
|
|
712
|
+
const [unit, value] = expr.coefficient;
|
|
713
|
+
// A unitless 1 is left out before a multiplied factor.
|
|
714
|
+
body = unit === '' && value === 1 && !expr.factors[0][0] ? '' : literalText([[unit, value]]);
|
|
715
|
+
for (const [divisor, factor] of expr.factors) body += (body ? (divisor ? ' / ' : ' * ') : '') + treeText(factor, true);
|
|
716
|
+
}
|
|
717
|
+
return grouped ? `(${body})` : body;
|
|
718
|
+
}
|
|
719
|
+
/** `calc(<sum>)` holding a tree-counting function: a lone function without the wrapper. */
|
|
720
|
+
function treeCalcText(inner) {
|
|
721
|
+
const expr = treeSum(inner);
|
|
722
|
+
return expr.kind === 'function' ? expr.text : `calc(${treeText(expr, false)})`;
|
|
723
|
+
}
|
|
724
|
+
const treeArgumentText = (argument) => treeText(treeSum(argument), false);
|
|
460
725
|
/**
|
|
461
726
|
* The arguments of a math function Chrome keeps: each a simplified sum without the `calc()`
|
|
462
727
|
* wrapper (`min(1px, calc(1px + 1em))` is `min(1px, 1em + 1px)`), or as written.
|
|
@@ -464,6 +729,7 @@ function absSign(name, inner) {
|
|
|
464
729
|
function sumArguments(inner) {
|
|
465
730
|
return argumentsOf(inner).map((argument) => {
|
|
466
731
|
const terms = isNone(argument) ? null : sumTerms(argument);
|
|
732
|
+
if (!terms && hasTreeCounting(argument)) return treeArgumentText(argument);
|
|
467
733
|
return terms ? calcText(terms).slice(5, -1) : join(argument);
|
|
468
734
|
}).join(', ');
|
|
469
735
|
}
|
|
@@ -571,74 +837,106 @@ function mathValue(comp) {
|
|
|
571
837
|
const terms = sumTerms(components(comp.text.slice(5, -1)));
|
|
572
838
|
return terms && terms.length === 1 ? terms[0] : null;
|
|
573
839
|
}
|
|
840
|
+
function readSimpleComponent(token) {
|
|
841
|
+
switch (token.type) {
|
|
842
|
+
case 'ident': return { kind: 'ident', value: token.value, text: token.value };
|
|
843
|
+
case 'number': return { kind: 'number', value: f32(token.value), integer: token.integer, text: cssNumber(token.value) };
|
|
844
|
+
case 'percentage': { const value = f32(Number(token.raw)); return { kind: 'percentage', value, text: `${cssNumber(Number(token.raw))}%` }; }
|
|
845
|
+
case 'dimension': {
|
|
846
|
+
// A length past f32's range (`1e39px`) is the largest float, as Chrome clamps it
|
|
847
|
+
// (`3.40282e+38px`), never infinite (#262; the Rust port's tokenizer likewise).
|
|
848
|
+
const number = Math.max(-F32_MAX, Math.min(F32_MAX, token.value));
|
|
849
|
+
const unit = token.unit.toLowerCase();
|
|
850
|
+
return { kind: 'dimension', value: f32(number), unit, text: `${cssNumber(number)}${unit}` };
|
|
851
|
+
}
|
|
852
|
+
case 'hash': return { kind: 'hash', value: token.value, text: `#${token.value}` };
|
|
853
|
+
case 'string': return { kind: 'string', value: token.value, text: quoted(token.value) };
|
|
854
|
+
case 'url': return { kind: 'url', value: token.value, text: `url(${quoted(token.value)})` };
|
|
855
|
+
case 'comma': return { kind: 'comma', text: ',' };
|
|
856
|
+
default: return null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function readDelim(token) {
|
|
860
|
+
if (token.value === '!') return null;
|
|
861
|
+
return token.value === '/' ? { kind: 'slash', text: '/' } : { kind: 'delim', value: token.value, text: token.value };
|
|
862
|
+
}
|
|
863
|
+
function simplifyMathLiterals(name, inner) {
|
|
864
|
+
// Keyword arguments are case-insensitive: Chrome writes them lowercase.
|
|
865
|
+
if (!NAMING_ARGUMENTS.has(name)) for (const comp of inner) if (comp.kind === 'ident' && !comp.value.startsWith('--')) comp.text = comp.value.toLowerCase();
|
|
866
|
+
if (name !== 'cubic-bezier' && name !== 'linear') return;
|
|
867
|
+
inner.forEach((comp, index) => {
|
|
868
|
+
const term = mathValue(comp);
|
|
869
|
+
if (term && term[0] === '') inner[index] = { kind: 'number', value: f32(term[1]), integer: false, text: cssNumber(term[1]) };
|
|
870
|
+
else if (term && term[0] === '%' && name === 'linear') inner[index] = { kind: 'percentage', value: f32(term[1]), text: `${cssNumber(term[1])}%` };
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
// Null prototype: a function named after an Object.prototype member (`constructor(…)`,
|
|
874
|
+
// `__proto__(…)`) is an ordinary unknown function, never a handler.
|
|
875
|
+
const SPECIAL_FUNCTION_HANDLERS = {
|
|
876
|
+
__proto__: null,
|
|
877
|
+
calc: (name, inner) => calcSum(inner),
|
|
878
|
+
min: (name, inner) => minMax(name, inner),
|
|
879
|
+
max: (name, inner) => minMax(name, inner),
|
|
880
|
+
clamp: (name, inner) => minMax(name, inner),
|
|
881
|
+
abs: (name, inner) => absSign(name, inner),
|
|
882
|
+
sign: (name, inner) => absSign(name, inner),
|
|
883
|
+
rgb: colorFunction,
|
|
884
|
+
rgba: colorFunction,
|
|
885
|
+
hsl: colorFunction,
|
|
886
|
+
hsla: colorFunction,
|
|
887
|
+
lab: labFunction,
|
|
888
|
+
lch: labFunction,
|
|
889
|
+
oklab: labFunction,
|
|
890
|
+
oklch: labFunction,
|
|
891
|
+
steps: (_n, inner) => stepsText(inner),
|
|
892
|
+
linear: (_n, inner) => linearText(inner),
|
|
893
|
+
};
|
|
894
|
+
function specialFunctionText(name, inner) {
|
|
895
|
+
const handler = SPECIAL_FUNCTION_HANDLERS[name];
|
|
896
|
+
return handler ? handler(name, inner) : null;
|
|
897
|
+
}
|
|
898
|
+
function readFunctionComponent(name, inner) {
|
|
899
|
+
if (name === 'url') {
|
|
900
|
+
if (inner.length !== 1 || inner[0].kind !== 'string') return null;
|
|
901
|
+
return { kind: 'url', value: inner[0].value, text: `url(${quoted(inner[0].value)})` };
|
|
902
|
+
}
|
|
903
|
+
simplifyMathLiterals(name, inner);
|
|
904
|
+
if (!functionValid(name, inner)) return null;
|
|
905
|
+
const special = specialFunctionText(name, inner);
|
|
906
|
+
const text = special ?? `${name}(${SUM_FUNCTIONS.includes(name) ? sumArguments(inner) : join(inner)})`;
|
|
907
|
+
return { kind: 'function', name, text };
|
|
908
|
+
}
|
|
909
|
+
|
|
574
910
|
/** Tokens -> components (a function / block is one component); `null` on bad input. */
|
|
575
911
|
function read(tokens, state, close) {
|
|
576
912
|
const out = [];
|
|
577
913
|
while (state.at < tokens.length) {
|
|
578
914
|
const token = tokens[state.at++];
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
case 'string': out.push({ kind: 'string', value: token.value, text: quoted(token.value) }); break;
|
|
598
|
-
case 'url': out.push({ kind: 'url', value: token.value, text: `url(${quoted(token.value)})` }); break;
|
|
599
|
-
case 'comma': out.push({ kind: 'comma', text: ',' }); break;
|
|
600
|
-
case 'delim':
|
|
601
|
-
if (token.value === '!') return null;
|
|
602
|
-
out.push(token.value === '/' ? { kind: 'slash', text: '/' } : { kind: 'delim', value: token.value, text: token.value });
|
|
603
|
-
break;
|
|
604
|
-
case 'function': {
|
|
605
|
-
const name = token.name.toLowerCase();
|
|
606
|
-
const inner = read(tokens, state, ')');
|
|
607
|
-
if (inner === null) return null;
|
|
608
|
-
if (name === 'url') {
|
|
609
|
-
if (inner.length !== 1 || inner[0].kind !== 'string') return null;
|
|
610
|
-
out.push({ kind: 'url', value: inner[0].value, text: `url(${quoted(inner[0].value)})` });
|
|
611
|
-
} else {
|
|
612
|
-
// Keyword arguments are case-insensitive: Chrome writes them lowercase.
|
|
613
|
-
if (!NAMING_ARGUMENTS.has(name)) for (const comp of inner) if (comp.kind === 'ident' && !comp.value.startsWith('--')) comp.text = comp.value.toLowerCase();
|
|
614
|
-
// A math argument Chrome resolves to a number (or, in linear(), a percentage) is that literal.
|
|
615
|
-
if (name === 'cubic-bezier' || name === 'linear') inner.forEach((comp, index) => {
|
|
616
|
-
const term = mathValue(comp);
|
|
617
|
-
if (term && term[0] === '') inner[index] = { kind: 'number', value: f32(term[1]), integer: false, text: cssNumber(term[1]) };
|
|
618
|
-
else if (term && term[0] === '%' && name === 'linear') inner[index] = { kind: 'percentage', value: f32(term[1]), text: `${cssNumber(term[1])}%` };
|
|
619
|
-
});
|
|
620
|
-
if (!functionValid(name, inner)) return null;
|
|
621
|
-
const special = name === 'calc' ? calcSum(inner) : name === 'min' || name === 'max' || name === 'clamp' ? minMax(name, inner) : name === 'abs' || name === 'sign' ? absSign(name, inner) : name === 'rgb' || name === 'rgba' || name === 'hsl' || name === 'hsla' ? colorFunction(name, inner) : name === 'lab' || name === 'lch' || name === 'oklab' || name === 'oklch' ? labFunction(name, inner) : name === 'steps' ? stepsText(inner) : name === 'linear' ? linearText(inner) : null;
|
|
622
|
-
const text = special ?? `${name}(${SUM_FUNCTIONS.includes(name) ? sumArguments(inner) : join(inner)})`;
|
|
623
|
-
out.push({ kind: 'function', name, text });
|
|
624
|
-
}
|
|
625
|
-
break;
|
|
626
|
-
}
|
|
627
|
-
case 'open': {
|
|
628
|
-
if (token.value === '{') return null;
|
|
629
|
-
const closer = token.value === '(' ? ')' : ']';
|
|
630
|
-
const inner = read(tokens, state, closer);
|
|
631
|
-
if (inner === null) return null;
|
|
632
|
-
out.push({ kind: 'function', name: token.value, text: `${token.value}${join(inner)}${closer}` });
|
|
633
|
-
break;
|
|
634
|
-
}
|
|
635
|
-
case 'close':
|
|
636
|
-
if (token.value === close) return out;
|
|
637
|
-
return null;
|
|
638
|
-
default: return null;
|
|
915
|
+
if (token.type === 'ws') continue;
|
|
916
|
+
if (token.type === 'delim') { const comp = readDelim(token); if (!comp) return null; out.push(comp); continue; }
|
|
917
|
+
if (token.type === 'function') {
|
|
918
|
+
const name = token.name.toLowerCase();
|
|
919
|
+
const inner = read(tokens, state, ')');
|
|
920
|
+
if (inner === null) return null;
|
|
921
|
+
const comp = readFunctionComponent(name, inner);
|
|
922
|
+
if (!comp) return null;
|
|
923
|
+
out.push(comp);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (token.type === 'open') {
|
|
927
|
+
if (token.value === '{') return null;
|
|
928
|
+
const closer = token.value === '(' ? ')' : ']';
|
|
929
|
+
const inner = read(tokens, state, closer);
|
|
930
|
+
if (inner === null) return null;
|
|
931
|
+
out.push({ kind: 'function', name: token.value, text: `${token.value}${join(inner)}${closer}` });
|
|
932
|
+
continue;
|
|
639
933
|
}
|
|
934
|
+
if (token.type === 'close') return token.value === close ? out : null;
|
|
935
|
+
const simple = readSimpleComponent(token);
|
|
936
|
+
if (!simple) return null;
|
|
937
|
+
out.push(simple);
|
|
640
938
|
}
|
|
641
|
-
return
|
|
939
|
+
return out;
|
|
642
940
|
}
|
|
643
941
|
|
|
644
942
|
/** Tokenize a declaration value into components; `null` for unbalanced or bad input. */
|
|
@@ -924,46 +1222,57 @@ function placeSelf(comps) {
|
|
|
924
1222
|
return [['align-self', a[1]], ['justify-self', justify]];
|
|
925
1223
|
}
|
|
926
1224
|
|
|
1225
|
+
const POS_LP = (c) => lengthPercentage(c, true);
|
|
1226
|
+
const POS_HORIZ = (c) => keyword(c, ['left', 'right']);
|
|
1227
|
+
const POS_VERT = (c) => keyword(c, ['top', 'bottom']);
|
|
1228
|
+
const POS_CENTER = (c) => keyword(c, ['center']);
|
|
1229
|
+
function positionOne(a) {
|
|
1230
|
+
const v = POS_VERT(a);
|
|
1231
|
+
if (v !== null) return ['center', v];
|
|
1232
|
+
const x = POS_HORIZ(a) ?? POS_CENTER(a) ?? POS_LP(a);
|
|
1233
|
+
return x === null ? null : [x, 'center'];
|
|
1234
|
+
}
|
|
1235
|
+
function positionTwo(a, b) {
|
|
1236
|
+
const ax = POS_HORIZ(a) ?? POS_CENTER(a) ?? POS_LP(a), by = POS_VERT(b) ?? POS_CENTER(b) ?? POS_LP(b);
|
|
1237
|
+
if (ax !== null && by !== null) return [ax, by];
|
|
1238
|
+
const ay = POS_VERT(a) ?? POS_CENTER(a), bx = POS_HORIZ(b) ?? POS_CENTER(b);
|
|
1239
|
+
return ay !== null && bx !== null ? [bx, ay] : null;
|
|
1240
|
+
}
|
|
1241
|
+
function positionManyAssign(slots, kw, text) {
|
|
1242
|
+
if (kw === 'left' || kw === 'right') { if (slots.x !== null) return false; slots.x = text; return true; }
|
|
1243
|
+
if (kw === 'top' || kw === 'bottom') { if (slots.y !== null) return false; slots.y = text; return true; }
|
|
1244
|
+
if (slots.middle !== null) return false;
|
|
1245
|
+
slots.middle = text;
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
function positionManyResolve(slots) {
|
|
1249
|
+
const { x, y, middle } = slots;
|
|
1250
|
+
if (x !== null && y !== null && middle === null) return [x, y];
|
|
1251
|
+
if (x !== null && y === null && middle !== null) return [x, middle];
|
|
1252
|
+
if (x === null && y !== null && middle !== null) return [middle, y];
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
function positionMany(comps) {
|
|
1256
|
+
// Keyword + offset pairs; a lone keyword takes no offset; `center` takes the axis the
|
|
1257
|
+
// other keyword leaves.
|
|
1258
|
+
const slots = { x: null, y: null, middle: null };
|
|
1259
|
+
let at = 0;
|
|
1260
|
+
while (at < comps.length) {
|
|
1261
|
+
const kw = keyword(comps[at], ['left', 'right', 'top', 'bottom', 'center']);
|
|
1262
|
+
if (kw === null) return null;
|
|
1263
|
+
const offset = comps[at + 1] ? POS_LP(comps[at + 1]) : null;
|
|
1264
|
+
const used = offset !== null && kw !== 'center' ? 2 : 1;
|
|
1265
|
+
const text = used === 2 ? `${kw} ${offset}` : kw;
|
|
1266
|
+
if (!positionManyAssign(slots, kw, text)) return null;
|
|
1267
|
+
at += used;
|
|
1268
|
+
}
|
|
1269
|
+
return positionManyResolve(slots);
|
|
1270
|
+
}
|
|
927
1271
|
/** One `<bg-position>`: its x and y specified values. */
|
|
928
1272
|
function position(comps) {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
const center = (c) => keyword(c, ['center']);
|
|
933
|
-
if (comps.length === 1) {
|
|
934
|
-
const [a] = comps;
|
|
935
|
-
const v = vertical(a);
|
|
936
|
-
if (v !== null) return ['center', v];
|
|
937
|
-
const x = horizontal(a) ?? center(a) ?? lp(a);
|
|
938
|
-
return x === null ? null : [x, 'center'];
|
|
939
|
-
}
|
|
940
|
-
if (comps.length === 2) {
|
|
941
|
-
const [a, b] = comps;
|
|
942
|
-
const ax = horizontal(a) ?? center(a) ?? lp(a), by = vertical(b) ?? center(b) ?? lp(b);
|
|
943
|
-
if (ax !== null && by !== null) return [ax, by];
|
|
944
|
-
const ay = vertical(a) ?? center(a), bx = horizontal(b) ?? center(b);
|
|
945
|
-
return ay !== null && bx !== null ? [bx, ay] : null;
|
|
946
|
-
}
|
|
947
|
-
if (comps.length === 3 || comps.length === 4) {
|
|
948
|
-
// Keyword + offset pairs; a lone keyword takes no offset; `center` takes the axis the
|
|
949
|
-
// other keyword leaves.
|
|
950
|
-
let at = 0, x = null, y = null, middle = null;
|
|
951
|
-
while (at < comps.length) {
|
|
952
|
-
const kw = keyword(comps[at], ['left', 'right', 'top', 'bottom', 'center']);
|
|
953
|
-
if (kw === null) return null;
|
|
954
|
-
const offset = comps[at + 1] ? lp(comps[at + 1]) : null;
|
|
955
|
-
const used = offset !== null && kw !== 'center' ? 2 : 1;
|
|
956
|
-
const text = used === 2 ? `${kw} ${offset}` : kw;
|
|
957
|
-
if (kw === 'left' || kw === 'right') { if (x !== null) return null; x = text; }
|
|
958
|
-
else if (kw === 'top' || kw === 'bottom') { if (y !== null) return null; y = text; }
|
|
959
|
-
else { if (middle !== null) return null; middle = text; }
|
|
960
|
-
at += used;
|
|
961
|
-
}
|
|
962
|
-
if (x !== null && y !== null && middle === null) return [x, y];
|
|
963
|
-
if (x !== null && y === null && middle !== null) return [x, middle];
|
|
964
|
-
if (x === null && y !== null && middle !== null) return [middle, y];
|
|
965
|
-
return null;
|
|
966
|
-
}
|
|
1273
|
+
if (comps.length === 1) return positionOne(comps[0]);
|
|
1274
|
+
if (comps.length === 2) return positionTwo(comps[0], comps[1]);
|
|
1275
|
+
if (comps.length === 3 || comps.length === 4) return positionMany(comps);
|
|
967
1276
|
return null;
|
|
968
1277
|
}
|
|
969
1278
|
/** A comma list of positions; `three`: three-value positions too (background only). */
|
|
@@ -1053,15 +1362,7 @@ function listStyle(comps) {
|
|
|
1053
1362
|
return namesOrInitial(['list-style-position', 'list-style-image', 'list-style-type'], parsed);
|
|
1054
1363
|
}
|
|
1055
1364
|
|
|
1056
|
-
function
|
|
1057
|
-
const parts = slashes(comps);
|
|
1058
|
-
let main, height = null;
|
|
1059
|
-
if (parts.length === 1) main = parts[0];
|
|
1060
|
-
else if (parts.length === 2 && parts[1].length === 1) {
|
|
1061
|
-
main = parts[0];
|
|
1062
|
-
height = length(parts[1][0], false) ?? keyword(parts[1][0], ['auto']);
|
|
1063
|
-
if (height === null) return null;
|
|
1064
|
-
} else return null;
|
|
1365
|
+
function columnsMain(main) {
|
|
1065
1366
|
if (!main.length || main.length > 2) return null;
|
|
1066
1367
|
let width = null, count = null;
|
|
1067
1368
|
for (const comp of main) {
|
|
@@ -1072,7 +1373,20 @@ function columns(comps) {
|
|
|
1072
1373
|
if (n !== null) { count = n; continue; }
|
|
1073
1374
|
return null;
|
|
1074
1375
|
}
|
|
1075
|
-
return
|
|
1376
|
+
return { width, count };
|
|
1377
|
+
}
|
|
1378
|
+
function columns(comps) {
|
|
1379
|
+
const parts = slashes(comps);
|
|
1380
|
+
let main, height = null;
|
|
1381
|
+
if (parts.length === 1) main = parts[0];
|
|
1382
|
+
else if (parts.length === 2 && parts[1].length === 1) {
|
|
1383
|
+
main = parts[0];
|
|
1384
|
+
height = length(parts[1][0], false) ?? keyword(parts[1][0], ['auto']);
|
|
1385
|
+
if (height === null) return null;
|
|
1386
|
+
} else return null;
|
|
1387
|
+
const parsed = columnsMain(main);
|
|
1388
|
+
if (!parsed) return null;
|
|
1389
|
+
return [['column-width', parsed.width ?? 'auto'], ['column-count', parsed.count ?? 'auto'], ['column-height', height ?? 'auto'], ['column-wrap', 'auto']];
|
|
1076
1390
|
}
|
|
1077
1391
|
|
|
1078
1392
|
function container(comps) {
|
|
@@ -1252,32 +1566,37 @@ function fontVariant(comps) {
|
|
|
1252
1566
|
return names.map((name, index) => [name, parsed[index] ?? 'normal']);
|
|
1253
1567
|
}
|
|
1254
1568
|
|
|
1569
|
+
const FLEX_BASIS = (c) => keyword(c, ['auto', 'content', 'min-content', 'max-content', 'fit-content']) ?? lengthPercentage(c, false);
|
|
1570
|
+
function flexStep(state, comps, at) {
|
|
1571
|
+
const comp = comps[at];
|
|
1572
|
+
if (state.grow === null) {
|
|
1573
|
+
const n = number(comp, false);
|
|
1574
|
+
if (n !== null) {
|
|
1575
|
+
state.grow = n;
|
|
1576
|
+
const next = comps[at + 1] ? number(comps[at + 1], false) : null;
|
|
1577
|
+
if (next !== null) { state.shrink = next; return at + 2; }
|
|
1578
|
+
return at + 1;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (state.basisValue === null) {
|
|
1582
|
+
const b = FLEX_BASIS(comp);
|
|
1583
|
+
if (b !== null) { state.basisValue = b; return at + 1; }
|
|
1584
|
+
}
|
|
1585
|
+
return -1;
|
|
1586
|
+
}
|
|
1255
1587
|
function flex(comps) {
|
|
1256
1588
|
const names = ['flex-grow', 'flex-shrink', 'flex-basis'];
|
|
1257
1589
|
if (comps.length === 1 && isIdent(comps[0], ['none'])) return named(names, ['0', '0', 'auto']);
|
|
1258
1590
|
if (comps.length === 1 && isIdent(comps[0], ['auto'])) return named(names, ['1', '1', 'auto']);
|
|
1259
|
-
const
|
|
1260
|
-
let
|
|
1591
|
+
const state = { grow: null, shrink: null, basisValue: null };
|
|
1592
|
+
let at = 0;
|
|
1261
1593
|
while (at < comps.length) {
|
|
1262
|
-
const
|
|
1263
|
-
if (
|
|
1264
|
-
|
|
1265
|
-
if (n !== null) {
|
|
1266
|
-
grow = n;
|
|
1267
|
-
const next = comps[at + 1] ? number(comps[at + 1], false) : null;
|
|
1268
|
-
if (next !== null) { shrink = next; at++; }
|
|
1269
|
-
at++;
|
|
1270
|
-
continue;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
if (basisValue === null) {
|
|
1274
|
-
const b = basis(comp);
|
|
1275
|
-
if (b !== null) { basisValue = b; at++; continue; }
|
|
1276
|
-
}
|
|
1277
|
-
return null;
|
|
1594
|
+
const next = flexStep(state, comps, at);
|
|
1595
|
+
if (next === -1) return null;
|
|
1596
|
+
at = next;
|
|
1278
1597
|
}
|
|
1279
|
-
if (grow === null && basisValue === null) return null;
|
|
1280
|
-
return named(names, [grow ?? '1', shrink ?? '1', basisValue ?? '0%']);
|
|
1598
|
+
if (state.grow === null && state.basisValue === null) return null;
|
|
1599
|
+
return named(names, [state.grow ?? '1', state.shrink ?? '1', state.basisValue ?? '0%']);
|
|
1281
1600
|
}
|
|
1282
1601
|
|
|
1283
1602
|
const SYSTEM_FONTS = ['caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'];
|
|
@@ -1335,32 +1654,41 @@ function oblique(c) {
|
|
|
1335
1654
|
if ((c.kind === 'dimension' || c.kind === 'number') && c.value === 0) return 'normal';
|
|
1336
1655
|
return `oblique ${text}`;
|
|
1337
1656
|
}
|
|
1338
|
-
function
|
|
1339
|
-
|
|
1657
|
+
function fontStyleSlot(comps, at) {
|
|
1658
|
+
const comp = comps[at];
|
|
1659
|
+
const value = keyword(comp, ['italic', 'oblique']);
|
|
1660
|
+
if (value === null) return null;
|
|
1661
|
+
if (value === 'oblique' && comps[at + 1] && comps[at + 1].kind === 'dimension' && angle(comps[at + 1]) !== null) {
|
|
1662
|
+
const o = oblique(comps[at + 1]);
|
|
1663
|
+
return o === null ? 'bad' : { value: o, used: 2 };
|
|
1664
|
+
}
|
|
1665
|
+
return { value, used: 1 };
|
|
1666
|
+
}
|
|
1667
|
+
function fontWeightSlot(comp) {
|
|
1668
|
+
const value = keyword(comp, ['bold', 'bolder', 'lighter']) ?? (comp.kind === 'number' && comp.value >= 1 && comp.value <= 1000 ? number(comp, false) : null);
|
|
1669
|
+
return value === null ? null : { value, used: 1 };
|
|
1670
|
+
}
|
|
1671
|
+
function fontLeading(comps) {
|
|
1340
1672
|
let style = null, caps = null, weight = null, stretch = null, at = 0;
|
|
1341
1673
|
while (at < comps.length && at < 4) {
|
|
1342
1674
|
const comp = comps[at];
|
|
1343
1675
|
if (isIdent(comp, ['normal'])) { at++; continue; }
|
|
1344
1676
|
if (style === null) {
|
|
1345
|
-
const
|
|
1346
|
-
if (
|
|
1347
|
-
|
|
1348
|
-
style = oblique(comps[at + 1]);
|
|
1349
|
-
if (style === null) return null;
|
|
1350
|
-
at += 2;
|
|
1351
|
-
continue;
|
|
1352
|
-
}
|
|
1353
|
-
style = value; at++; continue;
|
|
1354
|
-
}
|
|
1677
|
+
const s = fontStyleSlot(comps, at);
|
|
1678
|
+
if (s === 'bad') return null;
|
|
1679
|
+
if (s) { style = s.value; at += s.used; continue; }
|
|
1355
1680
|
}
|
|
1356
1681
|
if (caps === null) { const value = keyword(comp, ['small-caps']); if (value !== null) { caps = value; at++; continue; } }
|
|
1357
1682
|
if (weight === null) {
|
|
1358
|
-
const
|
|
1359
|
-
if (
|
|
1683
|
+
const w = fontWeightSlot(comp);
|
|
1684
|
+
if (w) { weight = w.value; at++; continue; }
|
|
1360
1685
|
}
|
|
1361
1686
|
if (stretch === null) { const value = keyword(comp, STRETCHES); if (value !== null) { stretch = value; at++; continue; } }
|
|
1362
1687
|
break;
|
|
1363
1688
|
}
|
|
1689
|
+
return { style, caps, weight, stretch, at };
|
|
1690
|
+
}
|
|
1691
|
+
function fontSizeAndLineHeight(comps, at) {
|
|
1364
1692
|
const sizeComp = comps[at];
|
|
1365
1693
|
if (!sizeComp) return null;
|
|
1366
1694
|
const size = keyword(sizeComp, FONT_SIZES) ?? lengthPercentage(sizeComp, false);
|
|
@@ -1374,29 +1702,38 @@ function font(comps) {
|
|
|
1374
1702
|
if (lineHeight === null) return null;
|
|
1375
1703
|
at += 2;
|
|
1376
1704
|
}
|
|
1377
|
-
|
|
1705
|
+
return { size, lineHeight, at };
|
|
1706
|
+
}
|
|
1707
|
+
function font(comps) {
|
|
1708
|
+
if (comps.length === 1) { const name = keyword(comps[0], SYSTEM_FONTS); if (name !== null) return { pending: name }; }
|
|
1709
|
+
const leading = fontLeading(comps);
|
|
1710
|
+
if (!leading) return null;
|
|
1711
|
+
const sized = fontSizeAndLineHeight(comps, leading.at);
|
|
1712
|
+
if (!sized) return null;
|
|
1713
|
+
const rest = comps.slice(sized.at);
|
|
1378
1714
|
if (!rest.length) return null;
|
|
1379
1715
|
const family = fontFamily(rest);
|
|
1380
1716
|
if (family === null) return null;
|
|
1381
|
-
return [['font-style', style ?? 'normal'], ['font-variant-caps', caps ?? 'normal'], ['font-variant-ligatures', 'normal'], ['font-variant-numeric', 'normal'], ['font-variant-east-asian', 'normal'], ['font-variant-alternates', 'normal'], ['font-size-adjust', 'none'], ['font-language-override', 'normal'], ['font-kerning', 'auto'], ['font-optical-sizing', 'auto'], ['font-feature-settings', 'normal'], ['font-variation-settings', 'normal'], ['font-variant-position', 'normal'], ['font-variant-emoji', 'normal'], ['font-weight', weight ?? 'normal'], ['font-stretch', stretch ?? 'normal'], ['font-size', size], ['line-height', lineHeight], ['font-family', family]];
|
|
1717
|
+
return [['font-style', leading.style ?? 'normal'], ['font-variant-caps', leading.caps ?? 'normal'], ['font-variant-ligatures', 'normal'], ['font-variant-numeric', 'normal'], ['font-variant-east-asian', 'normal'], ['font-variant-alternates', 'normal'], ['font-size-adjust', 'none'], ['font-language-override', 'normal'], ['font-kerning', 'auto'], ['font-optical-sizing', 'auto'], ['font-feature-settings', 'normal'], ['font-variation-settings', 'normal'], ['font-variant-position', 'normal'], ['font-variant-emoji', 'normal'], ['font-weight', leading.weight ?? 'normal'], ['font-stretch', leading.stretch ?? 'normal'], ['font-size', sized.size], ['line-height', sized.lineHeight], ['font-family', family]];
|
|
1382
1718
|
}
|
|
1383
1719
|
|
|
1384
1720
|
/** `<grid-line>` with Chrome's canonical part order (`span`, integer, name). */
|
|
1721
|
+
function gridLinePart(state, comp) {
|
|
1722
|
+
if (isIdent(comp, ['span']) && !state.span) { state.span = true; return true; }
|
|
1723
|
+
const n = state.int === null ? integer(comp, -Infinity) : null;
|
|
1724
|
+
if (n !== null) { if (n === '0') return false; state.int = n; return true; }
|
|
1725
|
+
const c = state.name === null ? customIdent(comp, ['auto', 'span']) : null;
|
|
1726
|
+
if (c !== null) { state.name = c; return true; }
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1385
1729
|
function gridLine(comps) {
|
|
1386
1730
|
if (comps.length === 1 && isIdent(comps[0], ['auto'])) return 'auto';
|
|
1387
1731
|
if (!comps.length) return null;
|
|
1388
|
-
|
|
1389
|
-
for (const comp of comps)
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
const c = name === null ? customIdent(comp, ['auto', 'span']) : null;
|
|
1394
|
-
if (c !== null) { name = c; continue; }
|
|
1395
|
-
return null;
|
|
1396
|
-
}
|
|
1397
|
-
if (int === null && name === null) return null;
|
|
1398
|
-
if (span && int !== null && int.startsWith('-')) return null;
|
|
1399
|
-
return [span ? 'span' : null, int, name].filter((v) => v !== null).join(' ');
|
|
1732
|
+
const state = { span: false, int: null, name: null };
|
|
1733
|
+
for (const comp of comps) if (!gridLinePart(state, comp)) return null;
|
|
1734
|
+
if (state.int === null && state.name === null) return null;
|
|
1735
|
+
if (state.span && state.int !== null && state.int.startsWith('-')) return null;
|
|
1736
|
+
return [state.span ? 'span' : null, state.int, state.name].filter((v) => v !== null).join(' ');
|
|
1400
1737
|
}
|
|
1401
1738
|
/** A line's default end / omitted value: a lone custom ident repeats, else `auto`. */
|
|
1402
1739
|
function lineDefault(start) {
|
|
@@ -1454,21 +1791,22 @@ function trackText(c, repeatCount) {
|
|
|
1454
1791
|
return c.text;
|
|
1455
1792
|
}
|
|
1456
1793
|
/** One track-list component: line names, a track size, or (at the top level) `repeat()`. */
|
|
1794
|
+
function trackOkRepeat(inner, repeatAllowed) {
|
|
1795
|
+
if (!repeatAllowed || !inner) return false;
|
|
1796
|
+
const comma = inner.findIndex(isComma);
|
|
1797
|
+
if (comma < 0) return false;
|
|
1798
|
+
const head = inner.slice(0, comma), tracks = inner.slice(comma + 1);
|
|
1799
|
+
const auto = head.length === 1 && isIdent(head[0], ['auto-fill', 'auto-fit']);
|
|
1800
|
+
const count = head.length === 1 && (head[0].kind === 'number' ? head[0].integer && head[0].value >= 1 : auto);
|
|
1801
|
+
return count && tracksValid(tracks) && tracks.every((track) => trackOk(track, false) && (!auto || trackFixed(track)));
|
|
1802
|
+
}
|
|
1457
1803
|
function trackOk(c, repeatAllowed) {
|
|
1458
1804
|
if (c.kind !== 'function') return trackBreadth(c, true);
|
|
1459
1805
|
if (c.name === '[') return true;
|
|
1460
1806
|
const inner = components(c.text.slice(c.name.length + 1, -1));
|
|
1461
1807
|
if (c.name === 'minmax') return !!inner && inner.length === 3 && isComma(inner[1]) && trackBreadth(inner[0], false) && trackBreadth(inner[2], true);
|
|
1462
1808
|
if (c.name === 'fit-content') return !!inner && inner.length === 1 && lengthPercentage(inner[0], false) !== null;
|
|
1463
|
-
if (c.name === 'repeat')
|
|
1464
|
-
if (!repeatAllowed || !inner) return false;
|
|
1465
|
-
const comma = inner.findIndex(isComma);
|
|
1466
|
-
if (comma < 0) return false;
|
|
1467
|
-
const head = inner.slice(0, comma), tracks = inner.slice(comma + 1);
|
|
1468
|
-
const auto = head.length === 1 && isIdent(head[0], ['auto-fill', 'auto-fit']);
|
|
1469
|
-
const count = head.length === 1 && (head[0].kind === 'number' ? head[0].integer && head[0].value >= 1 : auto);
|
|
1470
|
-
return count && tracksValid(tracks) && tracks.every((track) => trackOk(track, false) && (!auto || trackFixed(track)));
|
|
1471
|
-
}
|
|
1809
|
+
if (c.name === 'repeat') return trackOkRepeat(inner, repeatAllowed);
|
|
1472
1810
|
return lengthPercentage(c, false) !== null;
|
|
1473
1811
|
}
|
|
1474
1812
|
function trackList(comps) {
|
|
@@ -1509,44 +1847,59 @@ function areasText(rows) {
|
|
|
1509
1847
|
for (const box of boxes.values()) if ((box.x1 - box.x0 + 1) * (box.y1 - box.y0 + 1) !== box.count) return null;
|
|
1510
1848
|
return rows.map((row) => `"${row.join(' ')}"`).join(' ');
|
|
1511
1849
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
const rows = trackList(parts[0]), cols = trackList(parts[1]);
|
|
1519
|
-
return rows === null || cols === null ? null : [rows, cols, 'none'];
|
|
1850
|
+
const isNamesBlock = (c) => c.kind === 'function' && c.name === '[';
|
|
1851
|
+
function gridTemplateColumns(parts) {
|
|
1852
|
+
if (parts.length === 1) return { rows: parts[0], cols: null };
|
|
1853
|
+
if (parts.length === 2 && !parts[1].some((c) => functionNamed(c, ['repeat']) !== null)) {
|
|
1854
|
+
const cols = trackList(parts[1]);
|
|
1855
|
+
return cols === null ? null : { rows: parts[0], cols };
|
|
1520
1856
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
function gridTemplateTrackSize(c) {
|
|
1860
|
+
if (!c || string(c) !== null || isNamesBlock(c)) return null;
|
|
1861
|
+
return lengthPercentage(c, false) ?? keyword(c, ['auto', 'min-content', 'max-content']) ?? functionNamed(c, ['minmax', 'fit-content']) ?? (c.kind === 'dimension' && c.unit === 'fr' ? c.text : null);
|
|
1862
|
+
}
|
|
1863
|
+
function gridTemplateAreasAndTracks(rows) {
|
|
1526
1864
|
const areas = [], tracks = [];
|
|
1527
1865
|
let at = 0;
|
|
1528
|
-
const
|
|
1529
|
-
const names = () => { if (rows[at] && isNames(rows[at])) { tracks.push(rows[at].text); at++; } };
|
|
1866
|
+
const pushNames = () => { if (rows[at] && isNamesBlock(rows[at])) { tracks.push(rows[at].text); at++; } };
|
|
1530
1867
|
while (at < rows.length) {
|
|
1531
|
-
|
|
1868
|
+
pushNames();
|
|
1532
1869
|
const area = rows[at] ? areaRow(rows[at]) : null;
|
|
1533
1870
|
if (area === null) return null;
|
|
1534
1871
|
areas.push(area);
|
|
1535
1872
|
at++;
|
|
1536
|
-
const
|
|
1537
|
-
let size = null;
|
|
1538
|
-
if (c && string(c) === null && !isNames(c)) size = lengthPercentage(c, false) ?? keyword(c, ['auto', 'min-content', 'max-content']) ?? functionNamed(c, ['minmax', 'fit-content']) ?? (c.kind === 'dimension' && c.unit === 'fr' ? c.text : null);
|
|
1873
|
+
const size = gridTemplateTrackSize(rows[at]);
|
|
1539
1874
|
if (size !== null) { tracks.push(size); at++; } else tracks.push('auto');
|
|
1540
|
-
|
|
1875
|
+
pushNames();
|
|
1541
1876
|
}
|
|
1877
|
+
return { areas, tracks };
|
|
1878
|
+
}
|
|
1879
|
+
function mergeTrackNames(tracks) {
|
|
1542
1880
|
const merged = [];
|
|
1543
1881
|
for (const track of tracks) {
|
|
1544
1882
|
const last = merged[merged.length - 1];
|
|
1545
1883
|
if (last !== undefined && track.startsWith('[') && last.startsWith('[')) { merged[merged.length - 1] = `${last.slice(0, -1)} ${track.slice(1)}`; continue; }
|
|
1546
1884
|
merged.push(track);
|
|
1547
1885
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1886
|
+
return merged;
|
|
1887
|
+
}
|
|
1888
|
+
function gridTemplate(comps) {
|
|
1889
|
+
if (comps.length === 1 && isIdent(comps[0], ['none'])) return ['none', 'none', 'none'];
|
|
1890
|
+
const hasStrings = comps.some((c) => string(c) !== null);
|
|
1891
|
+
const parts = slashes(comps);
|
|
1892
|
+
if (!hasStrings) {
|
|
1893
|
+
if (parts.length !== 2) return null;
|
|
1894
|
+
const rows = trackList(parts[0]), cols = trackList(parts[1]);
|
|
1895
|
+
return rows === null || cols === null ? null : [rows, cols, 'none'];
|
|
1896
|
+
}
|
|
1897
|
+
const split = gridTemplateColumns(parts);
|
|
1898
|
+
if (!split) return null;
|
|
1899
|
+
const parsed = gridTemplateAreasAndTracks(split.rows);
|
|
1900
|
+
if (!parsed) return null;
|
|
1901
|
+
const areasValue = areasText(parsed.areas);
|
|
1902
|
+
return areasValue === null ? null : [mergeTrackNames(parsed.tracks).join(' '), split.cols ?? 'none', areasValue];
|
|
1550
1903
|
}
|
|
1551
1904
|
function grid(comps) {
|
|
1552
1905
|
const parts = slashes(comps);
|
|
@@ -1587,49 +1940,64 @@ function collapse4(values) {
|
|
|
1587
1940
|
const full = values.length === 1 ? [values[0], values[0], values[0], values[0]] : values.length === 2 ? [values[0], values[1], values[0], values[1]] : values.length === 3 ? [values[0], values[1], values[2], values[1]] : values.length === 4 ? values : null;
|
|
1588
1941
|
return full ? boxText(full) : values.join(' ');
|
|
1589
1942
|
}
|
|
1943
|
+
function borderImageRepeat(comps, at) {
|
|
1944
|
+
const kw = (c) => keyword(c, ['stretch', 'repeat', 'round', 'space']);
|
|
1945
|
+
const first = kw(comps[at]);
|
|
1946
|
+
if (first === null) return null;
|
|
1947
|
+
const second = comps[at + 1] ? kw(comps[at + 1]) : null;
|
|
1948
|
+
const used = second !== null ? 2 : 1;
|
|
1949
|
+
return { value: second !== null && second !== first ? `${first} ${second}` : first, used };
|
|
1950
|
+
}
|
|
1951
|
+
function borderImageSliceValues(comps, state) {
|
|
1952
|
+
const values = [];
|
|
1953
|
+
let fill = false;
|
|
1954
|
+
while (comps[state.at]) {
|
|
1955
|
+
const c = comps[state.at];
|
|
1956
|
+
if (isIdent(c, ['fill']) && !fill) fill = true;
|
|
1957
|
+
else if (fill && values.length) break; // `fill` goes before or after the numbers
|
|
1958
|
+
else { const value = values.length < 4 ? number(c, false) ?? percentage(c, false) : null; if (value === null) break; values.push(value); }
|
|
1959
|
+
state.at++;
|
|
1960
|
+
}
|
|
1961
|
+
return { values, fill };
|
|
1962
|
+
}
|
|
1963
|
+
function borderImageQuad(comps, state, ty) {
|
|
1964
|
+
const list = [];
|
|
1965
|
+
while (comps[state.at] && list.length < 4) { const value = ty(comps[state.at]); if (value === null) break; list.push(value); state.at++; }
|
|
1966
|
+
return list.length ? collapse4(list) : null;
|
|
1967
|
+
}
|
|
1968
|
+
function borderImageWidthOutset(comps, state) {
|
|
1969
|
+
if (!comps[state.at] || !isSlash(comps[state.at])) return { width: null, outset: null };
|
|
1970
|
+
state.at++;
|
|
1971
|
+
const w = borderImageQuad(comps, state, (c) => number(c, false) ?? lengthPercentage(c, false) ?? keyword(c, ['auto']));
|
|
1972
|
+
let o = null;
|
|
1973
|
+
if (comps[state.at] && isSlash(comps[state.at])) {
|
|
1974
|
+
state.at++;
|
|
1975
|
+
o = borderImageQuad(comps, state, (c) => number(c, false) ?? length(c, false));
|
|
1976
|
+
if (o === null) return null;
|
|
1977
|
+
} else if (w === null) return null;
|
|
1978
|
+
return { width: w, outset: o };
|
|
1979
|
+
}
|
|
1980
|
+
function borderImageSlice(comps, state, webkit) {
|
|
1981
|
+
const { values, fill } = borderImageSliceValues(comps, state);
|
|
1982
|
+
if (!values.length) return null;
|
|
1983
|
+
const slice = collapse4(values) + (fill || webkit ? ' fill' : '');
|
|
1984
|
+
const wo = borderImageWidthOutset(comps, state);
|
|
1985
|
+
return wo && { slice, width: wo.width, outset: wo.outset };
|
|
1986
|
+
}
|
|
1590
1987
|
function borderImage(comps, webkit) {
|
|
1591
|
-
let source = null, slice = null, width = null, outset = null, repeat = null
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1988
|
+
let source = null, slice = null, width = null, outset = null, repeat = null;
|
|
1989
|
+
const state = { at: 0 };
|
|
1990
|
+
while (state.at < comps.length) {
|
|
1991
|
+
const comp = comps[state.at];
|
|
1992
|
+
if (source === null) { const value = image(comp) ?? keyword(comp, ['none']); if (value !== null) { source = value; state.at++; continue; } }
|
|
1595
1993
|
if (repeat === null) {
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1598
|
-
if (first !== null) {
|
|
1599
|
-
const second = comps[at + 1] ? kw(comps[at + 1]) : null;
|
|
1600
|
-
at += second !== null ? 2 : 1;
|
|
1601
|
-
repeat = second !== null && second !== first ? `${first} ${second}` : first;
|
|
1602
|
-
continue;
|
|
1603
|
-
}
|
|
1994
|
+
const r = borderImageRepeat(comps, state.at);
|
|
1995
|
+
if (r) { repeat = r.value; state.at += r.used; continue; }
|
|
1604
1996
|
}
|
|
1605
1997
|
if (slice === null) {
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
const c = comps[at];
|
|
1610
|
-
if (isIdent(c, ['fill']) && !fill) fill = true;
|
|
1611
|
-
else if (fill && values.length) break; // `fill` goes before or after the numbers
|
|
1612
|
-
else { const value = values.length < 4 ? number(c, false) ?? percentage(c, false) : null; if (value === null) break; values.push(value); }
|
|
1613
|
-
at++;
|
|
1614
|
-
}
|
|
1615
|
-
if (!values.length) return null;
|
|
1616
|
-
slice = collapse4(values) + (fill || webkit ? ' fill' : '');
|
|
1617
|
-
const quad = (ty) => {
|
|
1618
|
-
const list = [];
|
|
1619
|
-
while (comps[at] && list.length < 4) { const value = ty(comps[at]); if (value === null) break; list.push(value); at++; }
|
|
1620
|
-
return list.length ? collapse4(list) : null;
|
|
1621
|
-
};
|
|
1622
|
-
if (comps[at] && isSlash(comps[at])) {
|
|
1623
|
-
at++;
|
|
1624
|
-
const w = quad((c) => number(c, false) ?? lengthPercentage(c, false) ?? keyword(c, ['auto']));
|
|
1625
|
-
let o = null;
|
|
1626
|
-
if (comps[at] && isSlash(comps[at])) {
|
|
1627
|
-
at++;
|
|
1628
|
-
o = quad((c) => number(c, false) ?? length(c, false));
|
|
1629
|
-
if (o === null) return null;
|
|
1630
|
-
} else if (w === null) return null;
|
|
1631
|
-
width = w; outset = o;
|
|
1632
|
-
}
|
|
1998
|
+
const parsed = borderImageSlice(comps, state, webkit);
|
|
1999
|
+
if (!parsed) return null;
|
|
2000
|
+
slice = parsed.slice; width = parsed.width; outset = parsed.outset;
|
|
1633
2001
|
continue;
|
|
1634
2002
|
}
|
|
1635
2003
|
return null;
|
|
@@ -1669,51 +2037,54 @@ function pathText(text) {
|
|
|
1669
2037
|
if (current) tokens.push(current);
|
|
1670
2038
|
return `${text.slice(0, start)}"${tokens.join(' ')}")`;
|
|
1671
2039
|
}
|
|
2040
|
+
function offsetAnchor(anchorPart) {
|
|
2041
|
+
if (anchorPart === null) return 'auto';
|
|
2042
|
+
if (anchorPart.length === 1 && isIdent(anchorPart[0], ['auto'])) return 'auto';
|
|
2043
|
+
// Three-value positions are background-only.
|
|
2044
|
+
if (anchorPart.length === 3) return null;
|
|
2045
|
+
const p = position(anchorPart);
|
|
2046
|
+
return p ? `${p[0]} ${p[1]}` : null;
|
|
2047
|
+
}
|
|
2048
|
+
const OFFSET_POS_KW = (c) => keyword(c, ['left', 'right', 'top', 'bottom', 'center']) !== null || lengthPercentage(c, true) !== null;
|
|
2049
|
+
function offsetPositionPart(main) {
|
|
2050
|
+
if (!main.length) return { value: 'normal', at: 0 };
|
|
2051
|
+
if (isIdent(main[0], ['auto', 'normal'])) return { value: ident(main[0]), at: 1 };
|
|
2052
|
+
for (let len = 4; len >= 1; len--) {
|
|
2053
|
+
if (main.length < len) continue;
|
|
2054
|
+
const slice = main.slice(0, len);
|
|
2055
|
+
if (slice.every(OFFSET_POS_KW)) {
|
|
2056
|
+
const p = position(slice);
|
|
2057
|
+
if (p) { if (len === 3) return null; return { value: `${p[0]} ${p[1]}`, at: len }; }
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
return { value: 'normal', at: 0 };
|
|
2061
|
+
}
|
|
2062
|
+
function offsetPathPart(main, at) {
|
|
2063
|
+
const comp = main[at];
|
|
2064
|
+
if (!comp) return { path: 'none', distance: '0px', rotate: 'auto', at };
|
|
2065
|
+
const value = isIdent(comp, ['none']) ? 'none' : functionNamed(comp, ['path', 'ray', 'url', 'circle', 'ellipse', 'inset', 'polygon', 'xywh', 'rect', 'shape']) ?? url(comp);
|
|
2066
|
+
if (value === null) return { path: 'none', distance: '0px', rotate: 'auto', at };
|
|
2067
|
+
if (value.startsWith('path(') && !pathDataValid(value)) return null;
|
|
2068
|
+
const path = value.startsWith('path(') ? pathText(value) : value;
|
|
2069
|
+
at++;
|
|
2070
|
+
const parsed = anyOrder(main.slice(at), [one((c) => lengthPercentage(c, true)), rotatePart]);
|
|
2071
|
+
if (!parsed) return null;
|
|
2072
|
+
return { path, distance: parsed[0] ?? '0px', rotate: parsed[1] ?? 'auto', at: main.length };
|
|
2073
|
+
}
|
|
1672
2074
|
function offset(comps) {
|
|
1673
2075
|
const parts = slashes(comps);
|
|
1674
2076
|
let main, anchorPart = null;
|
|
1675
2077
|
if (parts.length === 1) main = parts[0];
|
|
1676
2078
|
else if (parts.length === 2) { main = parts[0]; anchorPart = parts[1]; }
|
|
1677
2079
|
else return null;
|
|
1678
|
-
|
|
1679
|
-
if (
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
if (isIdent(main[0], ['auto', 'normal'])) { positionValue = ident(main[0]); at = 1; }
|
|
1687
|
-
else {
|
|
1688
|
-
for (let len = 4; len >= 1; len--) {
|
|
1689
|
-
if (main.length < len) continue;
|
|
1690
|
-
const slice = main.slice(0, len);
|
|
1691
|
-
if (slice.every((c) => keyword(c, ['left', 'right', 'top', 'bottom', 'center']) !== null || lengthPercentage(c, true) !== null)) {
|
|
1692
|
-
const p = position(slice);
|
|
1693
|
-
if (p) { if (len === 3) return null; positionValue = `${p[0]} ${p[1]}`; at = len; break; }
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
let path = 'none', distance = '0px', rotate = 'auto';
|
|
1699
|
-
const comp = main[at];
|
|
1700
|
-
if (comp) {
|
|
1701
|
-
{
|
|
1702
|
-
const value = isIdent(comp, ['none']) ? 'none' : functionNamed(comp, ['path', 'ray', 'url', 'circle', 'ellipse', 'inset', 'polygon', 'xywh', 'rect', 'shape']) ?? url(comp);
|
|
1703
|
-
if (value !== null) {
|
|
1704
|
-
if (value.startsWith('path(') && !pathDataValid(value)) return null;
|
|
1705
|
-
path = value.startsWith('path(') ? pathText(value) : value;
|
|
1706
|
-
at++;
|
|
1707
|
-
const parsed = anyOrder(main.slice(at), [one((c) => lengthPercentage(c, true)), rotatePart]);
|
|
1708
|
-
if (!parsed) return null;
|
|
1709
|
-
at = main.length;
|
|
1710
|
-
if (parsed[0] !== null) distance = parsed[0];
|
|
1711
|
-
if (parsed[1] !== null) rotate = parsed[1];
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
if (at !== main.length || (!main.length && parts.length === 1)) return null;
|
|
1716
|
-
return [['offset-position', positionValue], ['offset-path', path], ['offset-distance', distance], ['offset-rotate', rotate], ['offset-anchor', anchor]];
|
|
2080
|
+
const anchor = offsetAnchor(anchorPart);
|
|
2081
|
+
if (anchor === null) return null;
|
|
2082
|
+
const pos = offsetPositionPart(main);
|
|
2083
|
+
if (!pos) return null;
|
|
2084
|
+
const path = offsetPathPart(main, pos.at);
|
|
2085
|
+
if (!path) return null;
|
|
2086
|
+
if (path.at !== main.length || (!main.length && parts.length === 1)) return null;
|
|
2087
|
+
return [['offset-position', pos.value], ['offset-path', path.path], ['offset-distance', path.distance], ['offset-rotate', path.rotate], ['offset-anchor', anchor]];
|
|
1717
2088
|
}
|
|
1718
2089
|
function repeatStyle(comps) {
|
|
1719
2090
|
if (!comps.length) return null;
|
|
@@ -1747,39 +2118,72 @@ function bgSize(comps) {
|
|
|
1747
2118
|
// A single width other than `auto` spells its height `auto`.
|
|
1748
2119
|
return a === 'auto' ? [1, a] : [1, `${a} auto`];
|
|
1749
2120
|
}
|
|
2121
|
+
function layerPositionSize(out, comps, at) {
|
|
2122
|
+
const p = layerPosition(comps.slice(at));
|
|
2123
|
+
if (!p) return null;
|
|
2124
|
+
out.position = p[1]; at += p[0];
|
|
2125
|
+
if (comps[at] && isSlash(comps[at])) {
|
|
2126
|
+
const s = bgSize(comps.slice(at + 1));
|
|
2127
|
+
if (!s) return -1;
|
|
2128
|
+
out.size = s[1]; at += 1 + s[0];
|
|
2129
|
+
}
|
|
2130
|
+
return at;
|
|
2131
|
+
}
|
|
2132
|
+
function layerBoxSlot(out, value) {
|
|
2133
|
+
const clipOnly = value === 'text' || value === 'no-clip';
|
|
2134
|
+
if (out.origin === null && !clipOnly) out.origin = value;
|
|
2135
|
+
else if (out.clip === null) out.clip = value;
|
|
2136
|
+
else return false;
|
|
2137
|
+
return true;
|
|
2138
|
+
}
|
|
2139
|
+
function layerMaskSlot(out, comp) {
|
|
2140
|
+
if (out.composite === null) { const value = keyword(comp, ['add', 'subtract', 'intersect', 'exclude']); if (value !== null) { out.composite = value; return 1; } }
|
|
2141
|
+
if (out.mode === null) { const value = keyword(comp, ['alpha', 'luminance', 'match-source']); if (value !== null) { out.mode = value; return 1; } }
|
|
2142
|
+
return 0;
|
|
2143
|
+
}
|
|
2144
|
+
function layerStepImageOrPos(out, comps, at) {
|
|
2145
|
+
const comp = comps[at];
|
|
2146
|
+
if (out.image === null) { const value = image(comp) ?? keyword(comp, ['none']); if (value !== null) { out.image = value; return at + 1; } }
|
|
2147
|
+
if (out.position === null) {
|
|
2148
|
+
const next = layerPositionSize(out, comps, at);
|
|
2149
|
+
if (next === -1) return -1;
|
|
2150
|
+
if (next !== null) return next;
|
|
2151
|
+
}
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
function layerStepRepeatAttach(out, comps, at, mask) {
|
|
2155
|
+
const comp = comps[at];
|
|
2156
|
+
if (out.repeat === null) { const r = repeatStyle(comps.slice(at)); if (r) { out.repeat = r[1]; return at + r[0]; } }
|
|
2157
|
+
if (!mask && out.attachment === null) { const value = keyword(comp, ['scroll', 'fixed', 'local']); if (value !== null) { out.attachment = value; return at + 1; } }
|
|
2158
|
+
return null;
|
|
2159
|
+
}
|
|
2160
|
+
function layerStepTail(out, comps, at, mask, last, boxes) {
|
|
2161
|
+
const comp = comps[at];
|
|
2162
|
+
if (out.clip === null || out.origin === null) {
|
|
2163
|
+
const value = keyword(comp, boxes);
|
|
2164
|
+
if (value !== null) return layerBoxSlot(out, value) ? at + 1 : -1;
|
|
2165
|
+
}
|
|
2166
|
+
if (mask) { const used = layerMaskSlot(out, comp); if (used) return at + used; }
|
|
2167
|
+
else if (last && out.color === null) { const value = color(comp); if (value !== null) { out.color = value; return at + 1; } }
|
|
2168
|
+
return -1;
|
|
2169
|
+
}
|
|
2170
|
+
function layerStep(out, comps, at, mask, last, boxes) {
|
|
2171
|
+
const first = layerStepImageOrPos(out, comps, at);
|
|
2172
|
+
if (first !== null) return first;
|
|
2173
|
+
const second = layerStepRepeatAttach(out, comps, at, mask);
|
|
2174
|
+
if (second !== null) return second;
|
|
2175
|
+
return layerStepTail(out, comps, at, mask, last, boxes);
|
|
2176
|
+
}
|
|
2177
|
+
const LAYER_BOXES_MASK = ['border-box', 'padding-box', 'content-box', 'fill-box', 'stroke-box', 'view-box', 'no-clip'];
|
|
2178
|
+
const LAYER_BOXES_BG = ['border-box', 'padding-box', 'content-box', 'text', 'border-area'];
|
|
1750
2179
|
function layer(comps, mask, last) {
|
|
1751
2180
|
const out = { image: null, position: null, size: null, repeat: null, attachment: null, origin: null, clip: null, color: null, composite: null, mode: null };
|
|
1752
|
-
const boxes = mask ?
|
|
2181
|
+
const boxes = mask ? LAYER_BOXES_MASK : LAYER_BOXES_BG;
|
|
1753
2182
|
let at = 0;
|
|
1754
2183
|
while (at < comps.length) {
|
|
1755
|
-
const
|
|
1756
|
-
if (
|
|
1757
|
-
|
|
1758
|
-
const p = layerPosition(rest);
|
|
1759
|
-
if (p) {
|
|
1760
|
-
out.position = p[1]; at += p[0];
|
|
1761
|
-
if (comps[at] && isSlash(comps[at])) { const s = bgSize(comps.slice(at + 1)); if (!s) return null; out.size = s[1]; at += 1 + s[0]; }
|
|
1762
|
-
continue;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
if (out.repeat === null) { const r = repeatStyle(rest); if (r) { out.repeat = r[1]; at += r[0]; continue; } }
|
|
1766
|
-
if (!mask && out.attachment === null) { const value = keyword(comp, ['scroll', 'fixed', 'local']); if (value !== null) { out.attachment = value; at++; continue; } }
|
|
1767
|
-
if (out.clip === null || out.origin === null) {
|
|
1768
|
-
const value = keyword(comp, boxes);
|
|
1769
|
-
if (value !== null) {
|
|
1770
|
-
const clipOnly = value === 'text' || value === 'no-clip';
|
|
1771
|
-
if (out.origin === null && !clipOnly) out.origin = value;
|
|
1772
|
-
else if (out.clip === null) out.clip = value;
|
|
1773
|
-
else return null;
|
|
1774
|
-
at++;
|
|
1775
|
-
continue;
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
if (mask) {
|
|
1779
|
-
if (out.composite === null) { const value = keyword(comp, ['add', 'subtract', 'intersect', 'exclude']); if (value !== null) { out.composite = value; at++; continue; } }
|
|
1780
|
-
if (out.mode === null) { const value = keyword(comp, ['alpha', 'luminance', 'match-source']); if (value !== null) { out.mode = value; at++; continue; } }
|
|
1781
|
-
} else if (last && out.color === null) { const value = color(comp); if (value !== null) { out.color = value; at++; continue; } }
|
|
1782
|
-
return null;
|
|
2184
|
+
const next = layerStep(out, comps, at, mask, last, boxes);
|
|
2185
|
+
if (next === -1) return null;
|
|
2186
|
+
at = next;
|
|
1783
2187
|
}
|
|
1784
2188
|
if (out.origin !== null && out.clip === null) out.clip = out.origin;
|
|
1785
2189
|
return out;
|
|
@@ -2209,178 +2613,221 @@ const BOXES = {
|
|
|
2209
2613
|
'corner-shape': ['corner-top-left-shape', 'corner-top-right-shape', 'corner-bottom-right-shape', 'corner-bottom-left-shape'],
|
|
2210
2614
|
};
|
|
2211
2615
|
|
|
2616
|
+
// Per-shorthand serializers (lookup table below). Each takes (v, texts, grammar) and returns the
|
|
2617
|
+
// shorthand's spelling, '' when the values have no shorthand form.
|
|
2618
|
+
function borderRadiusText(v) {
|
|
2619
|
+
const corners = ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'].map(v);
|
|
2620
|
+
const horizontal = [], vertical = [];
|
|
2621
|
+
for (const corner of corners) { const space = corner.indexOf(' '); const h = space < 0 ? corner : corner.slice(0, space); horizontal.push(h); vertical.push(space < 0 ? h : corner.slice(space + 1)); }
|
|
2622
|
+
const h = boxText(horizontal);
|
|
2623
|
+
return horizontal.every((value, index) => value === vertical[index]) ? h : `${h} / ${boxText(vertical)}`;
|
|
2624
|
+
}
|
|
2625
|
+
function ruleInsetBoth(v) {
|
|
2626
|
+
const column = ruleInsetText(v, 'column');
|
|
2627
|
+
return column === ruleInsetText(v, 'row') ? column : '';
|
|
2628
|
+
}
|
|
2629
|
+
function ruleInsetPartText(v, part) {
|
|
2630
|
+
const row = pairText(v(`row-rule-inset-${part}-start`), v(`row-rule-inset-${part}-end`));
|
|
2631
|
+
return row === pairText(v(`column-rule-inset-${part}-start`), v(`column-rule-inset-${part}-end`)) ? row : '';
|
|
2632
|
+
}
|
|
2633
|
+
function positionShorthandText(texts) {
|
|
2634
|
+
const xs = layers(texts[0]), ys = layers(texts[1]);
|
|
2635
|
+
if (xs.length !== ys.length) {
|
|
2636
|
+
const part = (list, index) => (list[index] === undefined || list[index] === INITIAL ? '' : list[index]);
|
|
2637
|
+
return Array.from({ length: Math.max(xs.length, ys.length) }, (_, index) => joined([part(xs, index), part(ys, index)])).filter((layer) => layer).join(', ');
|
|
2638
|
+
}
|
|
2639
|
+
if ([...xs, ...ys].includes(INITIAL)) return '';
|
|
2640
|
+
return xs.map((x, index) => `${x} ${ys[index]}`).join(', ');
|
|
2641
|
+
}
|
|
2642
|
+
function borderSideShort(v, _texts, grammar) {
|
|
2643
|
+
return lineText(v(`${grammar}-width`), v(`${grammar}-style`), v(`${grammar}-color`));
|
|
2644
|
+
}
|
|
2645
|
+
function borderLogicalSideText(v, _texts, grammar) {
|
|
2646
|
+
return joined(['width', 'style', 'color'].map((part) => omitIf(v(`${grammar}-${part}`), null)));
|
|
2647
|
+
}
|
|
2648
|
+
function borderBothSidesText(v, _texts, grammar) {
|
|
2649
|
+
const [start, end] = grammar === 'rule' ? ['column-rule', 'row-rule'] : [`${grammar}-start`, `${grammar}-end`];
|
|
2650
|
+
const a = ['width', 'style', 'color'].map((part) => v(`${start}-${part}`));
|
|
2651
|
+
const b = ['width', 'style', 'color'].map((part) => v(`${end}-${part}`));
|
|
2652
|
+
return a.every((value, index) => value === b[index]) ? lineText(...a) : '';
|
|
2653
|
+
}
|
|
2654
|
+
function borderShort(v) {
|
|
2655
|
+
const image = ['border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'].map(v);
|
|
2656
|
+
if (image.some((value, index) => value !== BORDER_IMAGE_INITIAL[index])) return '';
|
|
2657
|
+
const parts = [];
|
|
2658
|
+
for (const part of ['width', 'style', 'color']) {
|
|
2659
|
+
const sides = ['top', 'right', 'bottom', 'left'].map((s) => v(`border-${s}-${part}`));
|
|
2660
|
+
if (sides.some((value) => value !== sides[0])) return '';
|
|
2661
|
+
parts.push(sides[0]);
|
|
2662
|
+
}
|
|
2663
|
+
return lineText(...parts);
|
|
2664
|
+
}
|
|
2665
|
+
function textDecorationShort(v) {
|
|
2666
|
+
const lineValue = v('text-decoration-line');
|
|
2667
|
+
const text = joined([omitIf(lineValue, 'none'), omitIf(v('text-decoration-thickness'), 'auto'), omitIf(v('text-decoration-style'), 'solid'), omitIf(v('text-decoration-color'), 'currentcolor')]);
|
|
2668
|
+
return text || (lineValue === INITIAL ? 'none' : lineValue);
|
|
2669
|
+
}
|
|
2670
|
+
function flexFlowText(v) {
|
|
2671
|
+
const direction = v('flex-direction'), wrap = v('flex-wrap');
|
|
2672
|
+
return wrap === 'nowrap' ? direction : direction === 'row' ? wrap : `${direction} ${wrap}`;
|
|
2673
|
+
}
|
|
2674
|
+
function columnsText(v) {
|
|
2675
|
+
const width = v('column-width'), count = v('column-count'), height = v('column-height');
|
|
2676
|
+
const main = width === 'auto' && count === 'auto' ? 'auto' : width === 'auto' ? count : count === 'auto' ? width : `${width} ${count}`;
|
|
2677
|
+
return height === 'auto' ? main : `${main} / ${height}`;
|
|
2678
|
+
}
|
|
2679
|
+
function textWrapText(v) {
|
|
2680
|
+
const mode = v('text-wrap-mode') === INITIAL ? 'wrap' : v('text-wrap-mode');
|
|
2681
|
+
const style = v('text-wrap-style') === INITIAL ? 'auto' : v('text-wrap-style');
|
|
2682
|
+
return style === 'auto' ? mode : mode === 'wrap' ? style : `${mode} ${style}`;
|
|
2683
|
+
}
|
|
2684
|
+
const WHITE_SPACE_MAP = { 'collapse wrap': 'normal', 'preserve nowrap': 'pre', 'collapse nowrap': 'nowrap', 'preserve wrap': 'pre-wrap', 'preserve-breaks wrap': 'pre-line', 'break-spaces wrap': 'break-spaces' };
|
|
2685
|
+
function whiteSpaceText(v) {
|
|
2686
|
+
const collapse = v('white-space-collapse') === INITIAL ? 'collapse' : v('white-space-collapse');
|
|
2687
|
+
const mode = v('text-wrap-mode') === INITIAL ? 'wrap' : v('text-wrap-mode');
|
|
2688
|
+
return WHITE_SPACE_MAP[`${collapse} ${mode}`] ?? `${collapse} ${mode}`;
|
|
2689
|
+
}
|
|
2690
|
+
function fontSynthesisText(v) {
|
|
2691
|
+
const parts = [];
|
|
2692
|
+
const values = ['font-synthesis-weight', 'font-synthesis-style', 'font-synthesis-small-caps'].map(v);
|
|
2693
|
+
for (const [index, name] of ['weight', 'style', 'small-caps'].entries()) {
|
|
2694
|
+
if (values[index] === 'auto') parts.push(name);
|
|
2695
|
+
else if (values[index] !== 'none') return '';
|
|
2696
|
+
}
|
|
2697
|
+
return parts.length ? parts.join(' ') : 'none';
|
|
2698
|
+
}
|
|
2699
|
+
function textBoxText(v) {
|
|
2700
|
+
const trim = v('text-box-trim'), edge = v('text-box-edge');
|
|
2701
|
+
if (trim === 'none' && edge === 'auto') return 'normal';
|
|
2702
|
+
if (trim === 'trim-both') return edge === 'auto' ? 'trim-both' : edge;
|
|
2703
|
+
return edge === 'auto' ? trim : `${trim} ${edge}`;
|
|
2704
|
+
}
|
|
2705
|
+
function scrollTimelineText(v) {
|
|
2706
|
+
const names = layers(v('scroll-timeline-name')), axes = layers(v('scroll-timeline-axis'));
|
|
2707
|
+
if (names.length !== axes.length) return '';
|
|
2708
|
+
return names.map((name, index) => (axes[index] === 'block' ? name : `${name} ${axes[index]}`)).join(', ');
|
|
2709
|
+
}
|
|
2710
|
+
function viewTimelineText(v) {
|
|
2711
|
+
const names = layers(v('view-timeline-name')), axes = layers(v('view-timeline-axis')), insets = layers(v('view-timeline-inset'));
|
|
2712
|
+
if (names.length !== axes.length || names.length !== insets.length) return '';
|
|
2713
|
+
return names.map((name, index) => joined([name, axes[index] === 'block' ? '' : axes[index], insets[index] === 'auto' ? '' : insets[index]])).join(', ');
|
|
2714
|
+
}
|
|
2715
|
+
function fontVariantText(v) {
|
|
2716
|
+
const values = ['font-variant-ligatures', 'font-variant-caps', 'font-variant-alternates', 'font-variant-numeric', 'font-variant-east-asian', 'font-variant-position', 'font-variant-emoji'].map(v);
|
|
2717
|
+
if (values.every((value) => value === 'normal')) return 'normal';
|
|
2718
|
+
if (values[0] === 'none') return values.slice(1).every((value) => value === 'normal') ? 'none' : '';
|
|
2719
|
+
return joined(values.map((value) => (value === 'normal' ? '' : value)));
|
|
2720
|
+
}
|
|
2721
|
+
function gridAreaText(v) {
|
|
2722
|
+
const [rs, cs, re, ce] = ['grid-row-start', 'grid-column-start', 'grid-row-end', 'grid-column-end'].map(v);
|
|
2723
|
+
const parts = [rs, cs, re, ce];
|
|
2724
|
+
if (ce === lineDefault(cs)) { parts.pop(); if (re === lineDefault(rs)) { parts.pop(); if (cs === lineDefault(rs)) parts.pop(); } }
|
|
2725
|
+
return parts.join(' / ');
|
|
2726
|
+
}
|
|
2727
|
+
function gridFlowText(rows, cols, flow, autoCols, autoRows) {
|
|
2728
|
+
const dense = flow.endsWith('dense');
|
|
2729
|
+
if ((flow.startsWith('row') || flow === 'dense') && rows === 'none' && autoCols === 'auto') return `${joined(['auto-flow', dense ? 'dense' : '', autoRows === 'auto' ? '' : autoRows])} / ${cols}`;
|
|
2730
|
+
if (flow.startsWith('column') && cols === 'none' && autoRows === 'auto') return `${rows} / ${joined(['auto-flow', dense ? 'dense' : '', autoCols === 'auto' ? '' : autoCols])}`;
|
|
2731
|
+
return '';
|
|
2732
|
+
}
|
|
2733
|
+
function gridText(v) {
|
|
2734
|
+
const rows = v('grid-template-rows'), cols = v('grid-template-columns'), areas = v('grid-template-areas'), flow = v('grid-auto-flow'), autoCols = v('grid-auto-columns'), autoRows = v('grid-auto-rows');
|
|
2735
|
+
if (flow === 'row' && autoRows === 'auto' && autoCols === 'auto') return gridTemplateText(rows, cols, areas);
|
|
2736
|
+
if (areas !== 'none') return '';
|
|
2737
|
+
return gridFlowText(rows, cols, flow, autoCols, autoRows);
|
|
2738
|
+
}
|
|
2739
|
+
function borderImageText(v) {
|
|
2740
|
+
const [source, slice, width, outsetValue, repeat] = ['border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'].map(v);
|
|
2741
|
+
if (slice === '100%' && width === '1' && outsetValue === '0' && repeat === 'stretch') return source;
|
|
2742
|
+
return `${source} ${slice} / ${width} / ${outsetValue} ${repeat}`;
|
|
2743
|
+
}
|
|
2744
|
+
function offsetText(v) {
|
|
2745
|
+
const [pos, path, distance, rotate, anchor] = ['offset-position', 'offset-path', 'offset-distance', 'offset-rotate', 'offset-anchor'].map(v);
|
|
2746
|
+
const moved = distance !== '0px' || rotate !== 'auto';
|
|
2747
|
+
const main = joined([pos === 'normal' ? '' : pos, path === 'none' && !moved ? '' : path, distance === '0px' ? '' : distance, rotate === 'auto' ? '' : rotate]) || 'normal';
|
|
2748
|
+
return anchor === 'auto' ? main : `${main} / ${anchor}`;
|
|
2749
|
+
}
|
|
2750
|
+
function timelineTriggerText(v) {
|
|
2751
|
+
const columns = ['timeline-trigger-name', 'timeline-trigger-source', 'timeline-trigger-activation-range-start', 'timeline-trigger-activation-range-end', 'timeline-trigger-active-range-start', 'timeline-trigger-active-range-end'].map((name) => layers(v(name)));
|
|
2752
|
+
const count = columns[0].length;
|
|
2753
|
+
if (columns.some((column) => column.length !== count)) return '';
|
|
2754
|
+
return Array.from({ length: count }, (_, index) => {
|
|
2755
|
+
const [name, source, as, ae, bs, be] = columns.map((column) => column[index]);
|
|
2756
|
+
return triggerText(name, source, [as, ae], [bs, be]);
|
|
2757
|
+
}).join(', ');
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
const SERIALIZERS = {
|
|
2761
|
+
'border-radius': (v) => borderRadiusText(v),
|
|
2762
|
+
'-webkit-border-radius': (v) => borderRadiusText(v),
|
|
2763
|
+
'column-rule-inset': (v) => ruleInsetText(v, 'column'),
|
|
2764
|
+
'row-rule-inset': (v) => ruleInsetText(v, 'row'),
|
|
2765
|
+
'rule-inset': ruleInsetBoth,
|
|
2766
|
+
'rule-inset-cap': (v) => ruleInsetPartText(v, 'cap'),
|
|
2767
|
+
'rule-inset-junction': (v) => ruleInsetPartText(v, 'junction'),
|
|
2768
|
+
'place-items': (v) => pairText(v('align-items'), v('justify-items')),
|
|
2769
|
+
'place-self': (v) => pairText(v('align-self'), v('justify-self')),
|
|
2770
|
+
'place-content': (v) => pairText(v('align-content'), v('justify-content')),
|
|
2771
|
+
'background-position': (_v, texts) => positionShorthandText(texts),
|
|
2772
|
+
'mask-position': (_v, texts) => positionShorthandText(texts),
|
|
2773
|
+
'-webkit-mask-position': (_v, texts) => positionShorthandText(texts),
|
|
2774
|
+
'animation-range': (v) => rangeText(v('animation-range-start'), v('animation-range-end'), 'normal'),
|
|
2775
|
+
'timeline-trigger-activation-range': (v) => rangesText(v('timeline-trigger-activation-range-start'), v('timeline-trigger-activation-range-end'), 'normal'),
|
|
2776
|
+
'timeline-trigger-active-range': (v) => rangesText(v('timeline-trigger-active-range-start'), v('timeline-trigger-active-range-end'), 'auto'),
|
|
2777
|
+
'border-top': borderSideShort,
|
|
2778
|
+
'border-right': borderSideShort,
|
|
2779
|
+
'border-bottom': borderSideShort,
|
|
2780
|
+
'border-left': borderSideShort,
|
|
2781
|
+
'column-rule': borderSideShort,
|
|
2782
|
+
'row-rule': borderSideShort,
|
|
2783
|
+
'border-block-start': borderLogicalSideText,
|
|
2784
|
+
'border-block-end': borderLogicalSideText,
|
|
2785
|
+
'border-inline-start': borderLogicalSideText,
|
|
2786
|
+
'border-inline-end': borderLogicalSideText,
|
|
2787
|
+
'border-block': borderBothSidesText,
|
|
2788
|
+
'border-inline': borderBothSidesText,
|
|
2789
|
+
rule: borderBothSidesText,
|
|
2790
|
+
border: borderShort,
|
|
2791
|
+
outline: (v) => joined(['outline-color', 'outline-style', 'outline-width'].map((name) => omitIf(v(name), null))),
|
|
2792
|
+
'-webkit-text-stroke': (v) => joined(['-webkit-text-stroke-width', '-webkit-text-stroke-color'].map((name) => omitIf(v(name), null))),
|
|
2793
|
+
'text-decoration': textDecorationShort,
|
|
2794
|
+
'text-emphasis': (v) => joined([omitIf(v('text-emphasis-style'), 'none'), omitIf(v('text-emphasis-color'), 'currentcolor')]) || 'none',
|
|
2795
|
+
'flex-flow': flexFlowText,
|
|
2796
|
+
'list-style': (v) => joined(['list-style-position', 'list-style-image', 'list-style-type'].map((name) => omitIf(v(name), null))),
|
|
2797
|
+
columns: columnsText,
|
|
2798
|
+
container: (v) => { const kind = v('container-type'); return kind === 'normal' ? v('container-name') : `${v('container-name')} / ${kind}`; },
|
|
2799
|
+
'text-wrap': textWrapText,
|
|
2800
|
+
'white-space': whiteSpaceText,
|
|
2801
|
+
'font-synthesis': fontSynthesisText,
|
|
2802
|
+
'text-box': textBoxText,
|
|
2803
|
+
'position-try': (v) => (v('position-try-order') === 'normal' ? v('position-try-fallbacks') : `${v('position-try-order')} ${v('position-try-fallbacks')}`),
|
|
2804
|
+
'scroll-timeline': scrollTimelineText,
|
|
2805
|
+
'view-timeline': viewTimelineText,
|
|
2806
|
+
'font-variant': fontVariantText,
|
|
2807
|
+
flex: (v) => `${v('flex-grow')} ${v('flex-shrink')} ${v('flex-basis')}`,
|
|
2808
|
+
font: (v) => fontText(v),
|
|
2809
|
+
'grid-area': gridAreaText,
|
|
2810
|
+
'grid-row': (_v, texts) => (texts[1] === lineDefault(texts[0]) ? texts[0] : `${texts[0]} / ${texts[1]}`),
|
|
2811
|
+
'grid-column': (_v, texts) => (texts[1] === lineDefault(texts[0]) ? texts[0] : `${texts[0]} / ${texts[1]}`),
|
|
2812
|
+
'grid-template': (v) => gridTemplateText(v('grid-template-rows'), v('grid-template-columns'), v('grid-template-areas')),
|
|
2813
|
+
grid: gridText,
|
|
2814
|
+
'border-image': borderImageText,
|
|
2815
|
+
offset: offsetText,
|
|
2816
|
+
background: (v) => backgroundText(v),
|
|
2817
|
+
mask: (v) => maskText(v),
|
|
2818
|
+
transition: (v) => transitionText(v),
|
|
2819
|
+
animation: (v) => animationText(v),
|
|
2820
|
+
'timeline-trigger': timelineTriggerText,
|
|
2821
|
+
};
|
|
2822
|
+
|
|
2212
2823
|
/** Serialize `grammar` from its longhands' values (`texts` in table order). */
|
|
2213
2824
|
function serializeValue(grammar, entry, texts) {
|
|
2214
2825
|
const v = (name) => { const index = entry.longhands.indexOf(name); return index < 0 ? '' : texts[index]; };
|
|
2215
2826
|
if (BOXES[grammar]) return boxText(BOXES[grammar].map(v));
|
|
2216
2827
|
if (PAIRS.has(grammar)) return pairText(texts[0], texts[1]);
|
|
2217
2828
|
if (SAME.has(grammar)) return allSame(texts);
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
case '-webkit-border-radius': {
|
|
2221
|
-
const corners = ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'].map(v);
|
|
2222
|
-
const horizontal = [], vertical = [];
|
|
2223
|
-
for (const corner of corners) { const space = corner.indexOf(' '); const h = space < 0 ? corner : corner.slice(0, space); horizontal.push(h); vertical.push(space < 0 ? h : corner.slice(space + 1)); }
|
|
2224
|
-
const h = boxText(horizontal);
|
|
2225
|
-
return horizontal.every((value, index) => value === vertical[index]) ? h : `${h} / ${boxText(vertical)}`;
|
|
2226
|
-
}
|
|
2227
|
-
case 'column-rule-inset': return ruleInsetText(v, 'column');
|
|
2228
|
-
case 'row-rule-inset': return ruleInsetText(v, 'row');
|
|
2229
|
-
case 'rule-inset': { const column = ruleInsetText(v, 'column'); return column === ruleInsetText(v, 'row') ? column : ''; }
|
|
2230
|
-
case 'rule-inset-cap':
|
|
2231
|
-
case 'rule-inset-junction': {
|
|
2232
|
-
const part = grammar === 'rule-inset-cap' ? 'cap' : 'junction';
|
|
2233
|
-
const row = pairText(v(`row-rule-inset-${part}-start`), v(`row-rule-inset-${part}-end`));
|
|
2234
|
-
return row === pairText(v(`column-rule-inset-${part}-start`), v(`column-rule-inset-${part}-end`)) ? row : '';
|
|
2235
|
-
}
|
|
2236
|
-
case 'place-items': return pairText(v('align-items'), v('justify-items'));
|
|
2237
|
-
case 'place-self': return pairText(v('align-self'), v('justify-self'));
|
|
2238
|
-
case 'place-content': return pairText(v('align-content'), v('justify-content'));
|
|
2239
|
-
case 'background-position':
|
|
2240
|
-
case 'mask-position':
|
|
2241
|
-
case '-webkit-mask-position': {
|
|
2242
|
-
const xs = layers(texts[0]), ys = layers(texts[1]);
|
|
2243
|
-
if (xs.length !== ys.length) {
|
|
2244
|
-
// Uneven lists: each layer's set parts; layers with none drop out.
|
|
2245
|
-
const part = (list, index) => (list[index] === undefined || list[index] === INITIAL ? '' : list[index]);
|
|
2246
|
-
return Array.from({ length: Math.max(xs.length, ys.length) }, (_, index) => joined([part(xs, index), part(ys, index)])).filter((layer) => layer).join(', ');
|
|
2247
|
-
}
|
|
2248
|
-
if ([...xs, ...ys].includes(INITIAL)) return '';
|
|
2249
|
-
return xs.map((x, index) => `${x} ${ys[index]}`).join(', ');
|
|
2250
|
-
}
|
|
2251
|
-
case 'animation-range': return rangeText(v('animation-range-start'), v('animation-range-end'), 'normal');
|
|
2252
|
-
case 'timeline-trigger-activation-range': return rangesText(v('timeline-trigger-activation-range-start'), v('timeline-trigger-activation-range-end'), 'normal');
|
|
2253
|
-
case 'timeline-trigger-active-range': return rangesText(v('timeline-trigger-active-range-start'), v('timeline-trigger-active-range-end'), 'auto');
|
|
2254
|
-
case 'border-top': case 'border-right': case 'border-bottom': case 'border-left': case 'column-rule': case 'row-rule':
|
|
2255
|
-
return lineText(v(`${grammar}-width`), v(`${grammar}-style`), v(`${grammar}-color`));
|
|
2256
|
-
// The logical sides leave omitted parts `initial` and keep every other value.
|
|
2257
|
-
case 'border-block-start': case 'border-block-end': case 'border-inline-start': case 'border-inline-end':
|
|
2258
|
-
return joined(['width', 'style', 'color'].map((part) => omitIf(v(`${grammar}-${part}`), null)));
|
|
2259
|
-
case 'border-block': case 'border-inline': case 'rule': {
|
|
2260
|
-
const [start, end] = grammar === 'rule' ? ['column-rule', 'row-rule'] : [`${grammar}-start`, `${grammar}-end`];
|
|
2261
|
-
const a = ['width', 'style', 'color'].map((part) => v(`${start}-${part}`)), b = ['width', 'style', 'color'].map((part) => v(`${end}-${part}`));
|
|
2262
|
-
return a.every((value, index) => value === b[index]) ? lineText(...a) : '';
|
|
2263
|
-
}
|
|
2264
|
-
case 'border': {
|
|
2265
|
-
const image = ['border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'].map(v);
|
|
2266
|
-
if (image.some((value, index) => value !== BORDER_IMAGE_INITIAL[index])) return '';
|
|
2267
|
-
const parts = [];
|
|
2268
|
-
for (const part of ['width', 'style', 'color']) {
|
|
2269
|
-
const sides = ['top', 'right', 'bottom', 'left'].map((s) => v(`border-${s}-${part}`));
|
|
2270
|
-
if (sides.some((value) => value !== sides[0])) return '';
|
|
2271
|
-
parts.push(sides[0]);
|
|
2272
|
-
}
|
|
2273
|
-
return lineText(...parts);
|
|
2274
|
-
}
|
|
2275
|
-
case 'outline': return joined(['outline-color', 'outline-style', 'outline-width'].map((name) => omitIf(v(name), null)));
|
|
2276
|
-
case '-webkit-text-stroke': return joined(['-webkit-text-stroke-width', '-webkit-text-stroke-color'].map((name) => omitIf(v(name), null)));
|
|
2277
|
-
case 'text-decoration': {
|
|
2278
|
-
const lineValue = v('text-decoration-line');
|
|
2279
|
-
const text = joined([omitIf(lineValue, 'none'), omitIf(v('text-decoration-thickness'), 'auto'), omitIf(v('text-decoration-style'), 'solid'), omitIf(v('text-decoration-color'), 'currentcolor')]);
|
|
2280
|
-
return text || (lineValue === INITIAL ? 'none' : lineValue);
|
|
2281
|
-
}
|
|
2282
|
-
case 'text-emphasis': return joined([omitIf(v('text-emphasis-style'), 'none'), omitIf(v('text-emphasis-color'), 'currentcolor')]) || 'none';
|
|
2283
|
-
case 'flex-flow': {
|
|
2284
|
-
const direction = v('flex-direction'), wrap = v('flex-wrap');
|
|
2285
|
-
return wrap === 'nowrap' ? direction : direction === 'row' ? wrap : `${direction} ${wrap}`;
|
|
2286
|
-
}
|
|
2287
|
-
case 'list-style': return joined(['list-style-position', 'list-style-image', 'list-style-type'].map((name) => omitIf(v(name), null)));
|
|
2288
|
-
case 'columns': {
|
|
2289
|
-
// Chrome 156 leaves `column-wrap` out of the spelling, whatever its value.
|
|
2290
|
-
const width = v('column-width'), count = v('column-count'), height = v('column-height');
|
|
2291
|
-
const main = width === 'auto' && count === 'auto' ? 'auto' : width === 'auto' ? count : count === 'auto' ? width : `${width} ${count}`;
|
|
2292
|
-
return height === 'auto' ? main : `${main} / ${height}`;
|
|
2293
|
-
}
|
|
2294
|
-
case 'container': { const kind = v('container-type'); return kind === 'normal' ? v('container-name') : `${v('container-name')} / ${kind}`; }
|
|
2295
|
-
case 'text-wrap': {
|
|
2296
|
-
const mode = v('text-wrap-mode') === INITIAL ? 'wrap' : v('text-wrap-mode');
|
|
2297
|
-
const style = v('text-wrap-style') === INITIAL ? 'auto' : v('text-wrap-style');
|
|
2298
|
-
return style === 'auto' ? mode : mode === 'wrap' ? style : `${mode} ${style}`;
|
|
2299
|
-
}
|
|
2300
|
-
case 'white-space': {
|
|
2301
|
-
const collapse = v('white-space-collapse') === INITIAL ? 'collapse' : v('white-space-collapse');
|
|
2302
|
-
const mode = v('text-wrap-mode') === INITIAL ? 'wrap' : v('text-wrap-mode');
|
|
2303
|
-
return { 'collapse wrap': 'normal', 'preserve nowrap': 'pre', 'collapse nowrap': 'nowrap', 'preserve wrap': 'pre-wrap', 'preserve-breaks wrap': 'pre-line', 'break-spaces wrap': 'break-spaces' }[`${collapse} ${mode}`] ?? `${collapse} ${mode}`;
|
|
2304
|
-
}
|
|
2305
|
-
case 'font-synthesis': {
|
|
2306
|
-
const parts = [];
|
|
2307
|
-
const values = ['font-synthesis-weight', 'font-synthesis-style', 'font-synthesis-small-caps'].map(v);
|
|
2308
|
-
for (const [index, name] of ['weight', 'style', 'small-caps'].entries()) {
|
|
2309
|
-
if (values[index] === 'auto') parts.push(name);
|
|
2310
|
-
else if (values[index] !== 'none') return '';
|
|
2311
|
-
}
|
|
2312
|
-
return parts.length ? parts.join(' ') : 'none';
|
|
2313
|
-
}
|
|
2314
|
-
case 'text-box': {
|
|
2315
|
-
const trim = v('text-box-trim'), edge = v('text-box-edge');
|
|
2316
|
-
if (trim === 'none' && edge === 'auto') return 'normal';
|
|
2317
|
-
if (trim === 'trim-both') return edge === 'auto' ? 'trim-both' : edge;
|
|
2318
|
-
return edge === 'auto' ? trim : `${trim} ${edge}`;
|
|
2319
|
-
}
|
|
2320
|
-
case 'position-try': return v('position-try-order') === 'normal' ? v('position-try-fallbacks') : `${v('position-try-order')} ${v('position-try-fallbacks')}`;
|
|
2321
|
-
case 'scroll-timeline': {
|
|
2322
|
-
const names = layers(v('scroll-timeline-name')), axes = layers(v('scroll-timeline-axis'));
|
|
2323
|
-
if (names.length !== axes.length) return '';
|
|
2324
|
-
return names.map((name, index) => (axes[index] === 'block' ? name : `${name} ${axes[index]}`)).join(', ');
|
|
2325
|
-
}
|
|
2326
|
-
case 'view-timeline': {
|
|
2327
|
-
const names = layers(v('view-timeline-name')), axes = layers(v('view-timeline-axis')), insets = layers(v('view-timeline-inset'));
|
|
2328
|
-
if (names.length !== axes.length || names.length !== insets.length) return '';
|
|
2329
|
-
return names.map((name, index) => joined([name, axes[index] === 'block' ? '' : axes[index], insets[index] === 'auto' ? '' : insets[index]])).join(', ');
|
|
2330
|
-
}
|
|
2331
|
-
case 'font-variant': {
|
|
2332
|
-
const values = ['font-variant-ligatures', 'font-variant-caps', 'font-variant-alternates', 'font-variant-numeric', 'font-variant-east-asian', 'font-variant-position', 'font-variant-emoji'].map(v);
|
|
2333
|
-
if (values.every((value) => value === 'normal')) return 'normal';
|
|
2334
|
-
if (values[0] === 'none') return values.slice(1).every((value) => value === 'normal') ? 'none' : '';
|
|
2335
|
-
return joined(values.map((value) => (value === 'normal' ? '' : value)));
|
|
2336
|
-
}
|
|
2337
|
-
case 'flex': return `${v('flex-grow')} ${v('flex-shrink')} ${v('flex-basis')}`;
|
|
2338
|
-
case 'font': return fontText(v);
|
|
2339
|
-
case 'grid-area': {
|
|
2340
|
-
const [rs, cs, re, ce] = ['grid-row-start', 'grid-column-start', 'grid-row-end', 'grid-column-end'].map(v);
|
|
2341
|
-
const parts = [rs, cs, re, ce];
|
|
2342
|
-
if (ce === lineDefault(cs)) { parts.pop(); if (re === lineDefault(rs)) { parts.pop(); if (cs === lineDefault(rs)) parts.pop(); } }
|
|
2343
|
-
return parts.join(' / ');
|
|
2344
|
-
}
|
|
2345
|
-
case 'grid-row':
|
|
2346
|
-
case 'grid-column': return texts[1] === lineDefault(texts[0]) ? texts[0] : `${texts[0]} / ${texts[1]}`;
|
|
2347
|
-
case 'grid-template': return gridTemplateText(v('grid-template-rows'), v('grid-template-columns'), v('grid-template-areas'));
|
|
2348
|
-
case 'grid': {
|
|
2349
|
-
const rows = v('grid-template-rows'), cols = v('grid-template-columns'), areas = v('grid-template-areas'), flow = v('grid-auto-flow'), autoCols = v('grid-auto-columns'), autoRows = v('grid-auto-rows');
|
|
2350
|
-
if (flow === 'row' && autoRows === 'auto' && autoCols === 'auto') return gridTemplateText(rows, cols, areas);
|
|
2351
|
-
if (areas !== 'none') return '';
|
|
2352
|
-
const dense = flow.endsWith('dense');
|
|
2353
|
-
if ((flow.startsWith('row') || flow === 'dense') && rows === 'none' && autoCols === 'auto') return `${joined(['auto-flow', dense ? 'dense' : '', autoRows === 'auto' ? '' : autoRows])} / ${cols}`;
|
|
2354
|
-
if (flow.startsWith('column') && cols === 'none' && autoRows === 'auto') return `${rows} / ${joined(['auto-flow', dense ? 'dense' : '', autoCols === 'auto' ? '' : autoCols])}`;
|
|
2355
|
-
return '';
|
|
2356
|
-
}
|
|
2357
|
-
case 'border-image': {
|
|
2358
|
-
const [source, slice, width, outsetValue, repeat] = ['border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'].map(v);
|
|
2359
|
-
if (slice === '100%' && width === '1' && outsetValue === '0' && repeat === 'stretch') return source;
|
|
2360
|
-
return `${source} ${slice} / ${width} / ${outsetValue} ${repeat}`;
|
|
2361
|
-
}
|
|
2362
|
-
case 'offset': {
|
|
2363
|
-
const [pos, path, distance, rotate, anchor] = ['offset-position', 'offset-path', 'offset-distance', 'offset-rotate', 'offset-anchor'].map(v);
|
|
2364
|
-
const moved = distance !== '0px' || rotate !== 'auto';
|
|
2365
|
-
const main = joined([pos === 'normal' ? '' : pos, path === 'none' && !moved ? '' : path, distance === '0px' ? '' : distance, rotate === 'auto' ? '' : rotate]) || 'normal';
|
|
2366
|
-
return anchor === 'auto' ? main : `${main} / ${anchor}`;
|
|
2367
|
-
}
|
|
2368
|
-
case 'background': return backgroundText(v);
|
|
2369
|
-
case 'mask': return maskText(v);
|
|
2370
|
-
case 'transition': return transitionText(v);
|
|
2371
|
-
case 'animation': return animationText(v);
|
|
2372
|
-
case 'timeline-trigger': {
|
|
2373
|
-
const columns = ['timeline-trigger-name', 'timeline-trigger-source', 'timeline-trigger-activation-range-start', 'timeline-trigger-activation-range-end', 'timeline-trigger-active-range-start', 'timeline-trigger-active-range-end'].map((name) => layers(v(name)));
|
|
2374
|
-
// Lists of different lengths have no shorthand spelling.
|
|
2375
|
-
const count = columns[0].length;
|
|
2376
|
-
if (columns.some((column) => column.length !== count)) return '';
|
|
2377
|
-
return Array.from({ length: count }, (_, index) => {
|
|
2378
|
-
const [name, source, as, ae, bs, be] = columns.map((column) => column[index]);
|
|
2379
|
-
return triggerText(name, source, [as, ae], [bs, be]);
|
|
2380
|
-
}).join(', ');
|
|
2381
|
-
}
|
|
2382
|
-
default: return '';
|
|
2383
|
-
}
|
|
2829
|
+
const serializer = SERIALIZERS[grammar];
|
|
2830
|
+
return serializer ? serializer(v, texts, grammar) : '';
|
|
2384
2831
|
}
|
|
2385
2832
|
|
|
2386
2833
|
// ------------------------------------------------------------------- table ----
|
|
@@ -2436,29 +2883,36 @@ export function expand(name, value) {
|
|
|
2436
2883
|
* getPropertyValue(shorthand) from `declarations` ([{ name, value, important, pending }]):
|
|
2437
2884
|
* '' when a longhand is missing, importance is mixed, or the values have no shorthand spelling.
|
|
2438
2885
|
*/
|
|
2439
|
-
|
|
2440
|
-
const entry = ENTRIES.get(name);
|
|
2441
|
-
if (!entry) return '';
|
|
2886
|
+
function collectLonghandValues(longhands, declarations) {
|
|
2442
2887
|
const values = [];
|
|
2443
|
-
for (const longhand of
|
|
2888
|
+
for (const longhand of longhands) {
|
|
2444
2889
|
let found = null;
|
|
2445
2890
|
for (let index = declarations.length - 1; index >= 0; index--) if (declarations[index].name === longhand) { found = declarations[index]; break; }
|
|
2446
|
-
if (!found) return
|
|
2891
|
+
if (!found) return null;
|
|
2447
2892
|
values.push(found);
|
|
2448
2893
|
}
|
|
2449
|
-
|
|
2894
|
+
return values;
|
|
2895
|
+
}
|
|
2896
|
+
const samePending = (a, b) => (a === null || a === undefined ? b === null || b === undefined : !!b && a[0] === b[0] && a[1] === b[1]);
|
|
2897
|
+
function pendingShorthandText(first, entry) {
|
|
2898
|
+
// A system font keyword leaves the font-variant longhands at `normal`.
|
|
2899
|
+
if (first.pending[0] === 'font' && entry.canonical === 'font-variant' && SYSTEM_FONTS.includes(first.pending[1])) return 'normal';
|
|
2900
|
+
return first.pending[0] === entry.canonical ? first.pending[1] : '';
|
|
2901
|
+
}
|
|
2902
|
+
function shorthandUniformText(values, entry) {
|
|
2450
2903
|
const first = values[0];
|
|
2451
|
-
|
|
2452
|
-
if (first.
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2904
|
+
if (!first.pending && !CSS_WIDE.has(first.value)) return null;
|
|
2905
|
+
if (!values.every((v) => v.value === first.value && samePending(first.pending, v.pending))) return null;
|
|
2906
|
+
return first.pending ? pendingShorthandText(first, entry) : first.value;
|
|
2907
|
+
}
|
|
2908
|
+
export function shorthandValue(name, declarations) {
|
|
2909
|
+
const entry = ENTRIES.get(name);
|
|
2910
|
+
if (!entry) return '';
|
|
2911
|
+
const values = collectLonghandValues(entry.longhands, declarations);
|
|
2912
|
+
if (!values) return '';
|
|
2913
|
+
if (values.some((v) => v.important) && values.some((v) => !v.important)) return '';
|
|
2914
|
+
const uniform = shorthandUniformText(values, entry);
|
|
2915
|
+
if (uniform !== null) return uniform;
|
|
2462
2916
|
for (const v of values) {
|
|
2463
2917
|
if (v.pending || (CSS_WIDE.has(v.value) && (v.value !== 'initial' || !entry.allowInitial))) return '';
|
|
2464
2918
|
}
|
|
@@ -2491,34 +2945,48 @@ export function blockText(declarations) {
|
|
|
2491
2945
|
}
|
|
2492
2946
|
|
|
2493
2947
|
/** A declaration list's `[name, value, important]` declarations, in order. */
|
|
2948
|
+
function declarationsScanInQuote(source, state, c) {
|
|
2949
|
+
if (c === '\\') state.index++;
|
|
2950
|
+
else if (c === state.quote) state.quote = '';
|
|
2951
|
+
}
|
|
2952
|
+
function declarationsScanBracket(state, c) {
|
|
2953
|
+
if (c === '(' || c === '[' || c === '{') { state.depth++; return true; }
|
|
2954
|
+
if (c === ')' || c === ']' || c === '}') { state.depth = Math.max(0, state.depth - 1); return true; }
|
|
2955
|
+
return false;
|
|
2956
|
+
}
|
|
2957
|
+
function declarationsScan(source, state) {
|
|
2958
|
+
const c = source[state.index];
|
|
2959
|
+
if (state.quote) { declarationsScanInQuote(source, state, c); return 'skip'; }
|
|
2960
|
+
if (c === '"' || c === "'") { state.quote = c; return 'skip'; }
|
|
2961
|
+
if (c === '/' && source[state.index + 1] === '*') { const end = source.indexOf('*/', state.index + 2); state.index = end < 0 ? source.length : end + 1; return 'skip'; }
|
|
2962
|
+
if (declarationsScanBracket(state, c)) return 'skip';
|
|
2963
|
+
if (c !== ';' || state.depth > 0) return 'skip';
|
|
2964
|
+
return 'split';
|
|
2965
|
+
}
|
|
2966
|
+
function parseDeclarationPart(part) {
|
|
2967
|
+
const colon = part.indexOf(':');
|
|
2968
|
+
if (colon < 0) return null;
|
|
2969
|
+
const name = part.slice(0, colon).trim();
|
|
2970
|
+
if (!/^-?[A-Za-z_-][A-Za-z0-9_-]*$/.test(name)) return null;
|
|
2971
|
+
let value = part.slice(colon + 1).trim();
|
|
2972
|
+
const important = /!\s*important\s*$/i.exec(value);
|
|
2973
|
+
if (important) value = value.slice(0, important.index).trim();
|
|
2974
|
+
else if (value.includes('!')) return null;
|
|
2975
|
+
return [name, value, !!important];
|
|
2976
|
+
}
|
|
2494
2977
|
export function declarations(text) {
|
|
2495
2978
|
const tokens = tokenize(String(text));
|
|
2496
2979
|
const out = [];
|
|
2497
2980
|
if (!tokens) return out;
|
|
2498
|
-
// Split at top-level semicolons, keeping each declaration's source span.
|
|
2499
2981
|
const source = String(text);
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
if (index < source.length)
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
if (c === ')' || c === ']' || c === '}') { depth = Math.max(0, depth - 1); continue; }
|
|
2509
|
-
if (c !== ';' || depth > 0) continue;
|
|
2510
|
-
}
|
|
2511
|
-
const part = source.slice(start, index).replace(/\/\*[\s\S]*?\*\//g, '');
|
|
2512
|
-
start = index + 1;
|
|
2513
|
-
const colon = part.indexOf(':');
|
|
2514
|
-
if (colon < 0) continue;
|
|
2515
|
-
const name = part.slice(0, colon).trim();
|
|
2516
|
-
if (!/^-?[A-Za-z_-][A-Za-z0-9_-]*$/.test(name)) continue;
|
|
2517
|
-
let value = part.slice(colon + 1).trim();
|
|
2518
|
-
const important = /!\s*important\s*$/i.exec(value);
|
|
2519
|
-
if (important) value = value.slice(0, important.index).trim();
|
|
2520
|
-
else if (value.includes('!')) continue;
|
|
2521
|
-
out.push([name, value, !!important]);
|
|
2982
|
+
const state = { depth: 0, quote: '', index: 0 };
|
|
2983
|
+
let start = 0;
|
|
2984
|
+
for (state.index = 0; state.index <= source.length; state.index++) {
|
|
2985
|
+
if (state.index < source.length && declarationsScan(source, state) === 'skip') continue;
|
|
2986
|
+
const part = source.slice(start, state.index).replace(/\/\*[\s\S]*?\*\//g, '');
|
|
2987
|
+
start = state.index + 1;
|
|
2988
|
+
const parsed = parseDeclarationPart(part);
|
|
2989
|
+
if (parsed) out.push(parsed);
|
|
2522
2990
|
}
|
|
2523
2991
|
return out;
|
|
2524
2992
|
}
|
|
@@ -2539,7 +3007,8 @@ function radiusPair(comps) {
|
|
|
2539
3007
|
}
|
|
2540
3008
|
const flexBasis = (comps) => single(comps, (c) => keyword(c, ['auto', 'content', 'min-content', 'max-content', 'fit-content']) ?? lengthPercentage(c, false));
|
|
2541
3009
|
const fontSize = (comps) => single(comps, (c) => keyword(c, FONT_SIZES) ?? lengthPercentage(c, false));
|
|
2542
|
-
const fontWeight = (comps) => single(comps, (c) => keyword(c, ['normal', 'bold', 'bolder', 'lighter']) ?? (c.kind
|
|
3010
|
+
const fontWeight = (comps) => single(comps, (c) => keyword(c, ['normal', 'bold', 'bolder', 'lighter']) ?? (c.kind !== 'number' || (c.value >= 1 && c.value <= 1000) ? number(c, false) : null));
|
|
3011
|
+
// (A literal weight is in [1, 1000]; a math function is checked when computed.)
|
|
2543
3012
|
function fontStyle(comps) {
|
|
2544
3013
|
if (comps.length === 1) return keyword(comps[0], ['normal', 'italic', 'oblique']);
|
|
2545
3014
|
if (comps.length === 2 && isIdent(comps[0], ['oblique'])) return oblique(comps[1]);
|
|
@@ -2584,12 +3053,20 @@ function gridAreas(comps) {
|
|
|
2584
3053
|
if (comps.length === 1 && isIdent(comps[0], ['none'])) return 'none';
|
|
2585
3054
|
return areasText(comps.map(areaRow));
|
|
2586
3055
|
}
|
|
2587
|
-
const
|
|
3056
|
+
const COORD_BOXES = ['content-box', 'padding-box', 'border-box', 'fill-box', 'stroke-box', 'view-box'];
|
|
3057
|
+
/** `clip-path`'s `shape() || <geometry-box>` and `path()`; the embedder's grammar decides the rest. */
|
|
3058
|
+
const clipPath = (comps) => shapeWithBox(comps, [...COORD_BOXES, 'margin-box'], 'border-box') ?? single(comps, (c) => {
|
|
3059
|
+
const value = functionNamed(c, ['path']);
|
|
3060
|
+
return value !== null && pathDataValid(value) ? pathText(value) : null;
|
|
3061
|
+
});
|
|
3062
|
+
/** `shape-outside`'s `shape() || <shape-box>`; the embedder's grammar decides the rest. */
|
|
3063
|
+
const shapeOutside = (comps) => shapeWithBox(comps, ['margin-box', 'border-box', 'padding-box', 'content-box'], 'margin-box');
|
|
3064
|
+
const offsetPath = (comps) => (comps.some((c) => functionNamed(c, ['shape']) !== null) ? shapeWithBox(comps, COORD_BOXES, 'border-box') : single(comps, (c) => {
|
|
2588
3065
|
if (isIdent(c, ['none'])) return 'none';
|
|
2589
|
-
const value = functionNamed(c, ['path', 'ray', 'circle', 'ellipse', 'inset', 'polygon', 'xywh', 'rect'
|
|
3066
|
+
const value = functionNamed(c, ['path', 'ray', 'circle', 'ellipse', 'inset', 'polygon', 'xywh', 'rect']) ?? url(c);
|
|
2590
3067
|
if (value === null || (value.startsWith('path(') && !pathDataValid(value))) return null;
|
|
2591
3068
|
return value.startsWith('path(') ? pathText(value) : value;
|
|
2592
|
-
});
|
|
3069
|
+
}));
|
|
2593
3070
|
// A percentage calc() stays as written.
|
|
2594
3071
|
const opacityV = (comps) => single(comps, (c) => number(c, true) ?? (c.kind === 'percentage' ? cssNumber(c.value / 100) : mathOf(c, isLengthUnit, true, false) && c.text.includes('%') ? c.text : null));
|
|
2595
3072
|
const DISPLAY_KEYWORDS = ['none', 'contents', 'block', 'inline', 'inline-block', 'flow-root', 'flex', 'inline-flex', 'grid', 'inline-grid', 'table', 'inline-table', 'table-row-group', 'table-header-group', 'table-footer-group', 'table-row', 'table-cell', 'table-column-group', 'table-column', 'table-caption', 'list-item', 'ruby', 'ruby-text', 'math', '-webkit-box', '-webkit-inline-box'];
|
|
@@ -2606,25 +3083,27 @@ const animationNames = (comps) => listed(comps, (item) => single(item, (c) => {
|
|
|
2606
3083
|
if (c.kind === 'string') return animationNameString(c);
|
|
2607
3084
|
return customIdent(c, ['none']);
|
|
2608
3085
|
}));
|
|
3086
|
+
function shadowItemStep(state, comp, box) {
|
|
3087
|
+
if (box && isIdent(comp, ['inset']) && !state.inset) { state.inset = true; return true; }
|
|
3088
|
+
// The blur radius (the third length) is not negative.
|
|
3089
|
+
const negative = comp.kind === 'dimension' && comp.value < 0;
|
|
3090
|
+
const l = state.lengths.length < (box ? 4 : 3) && !(state.lengths.length === 2 && negative) ? length(comp, true) : null;
|
|
3091
|
+
if (l !== null) { state.lengths.push(l); return true; }
|
|
3092
|
+
const c = state.shadowColor === null ? color(comp) : null;
|
|
3093
|
+
if (c === null) return false;
|
|
3094
|
+
state.shadowColor = c;
|
|
3095
|
+
return true;
|
|
3096
|
+
}
|
|
3097
|
+
function shadowItem(item, box) {
|
|
3098
|
+
const state = { shadowColor: null, inset: false, lengths: [] };
|
|
3099
|
+
for (const comp of item) if (!shadowItemStep(state, comp, box)) return null;
|
|
3100
|
+
if (state.lengths.length < 2) return null;
|
|
3101
|
+
return [...(state.shadowColor === null ? [] : [state.shadowColor]), ...state.lengths, ...(state.inset ? ['inset'] : [])].join(' ');
|
|
3102
|
+
}
|
|
2609
3103
|
/** `box-shadow` / `text-shadow` (`box`: a spread length and `inset`): each shadow as Chrome orders it (color, lengths, `inset`). */
|
|
2610
3104
|
function shadow(comps, box) {
|
|
2611
3105
|
if (comps.length === 1 && isIdent(comps[0], ['none'])) return 'none';
|
|
2612
|
-
return listed(comps, (item) =>
|
|
2613
|
-
let shadowColor = null, inset = false;
|
|
2614
|
-
const lengths = [];
|
|
2615
|
-
for (const comp of item) {
|
|
2616
|
-
if (box && isIdent(comp, ['inset']) && !inset) { inset = true; continue; }
|
|
2617
|
-
// The blur radius (the third length) is not negative.
|
|
2618
|
-
const negative = comp.kind === 'dimension' && comp.value < 0;
|
|
2619
|
-
const l = lengths.length < (box ? 4 : 3) && !(lengths.length === 2 && negative) ? length(comp, true) : null;
|
|
2620
|
-
if (l !== null) { lengths.push(l); continue; }
|
|
2621
|
-
const c = shadowColor === null ? color(comp) : null;
|
|
2622
|
-
if (c === null) return null;
|
|
2623
|
-
shadowColor = c;
|
|
2624
|
-
}
|
|
2625
|
-
if (lengths.length < 2) return null;
|
|
2626
|
-
return [...(shadowColor === null ? [] : [shadowColor]), ...lengths, ...(inset ? ['inset'] : [])].join(' ');
|
|
2627
|
-
});
|
|
3106
|
+
return listed(comps, (item) => shadowItem(item, box));
|
|
2628
3107
|
}
|
|
2629
3108
|
const SIDES4 = ['top', 'right', 'bottom', 'left'];
|
|
2630
3109
|
const LOGICAL = ['block-start', 'block-end', 'inline-start', 'inline-end'];
|
|
@@ -2699,6 +3178,7 @@ add(['text-decoration-style'], (c) => keywords(c, ['solid', 'double', 'dotted',
|
|
|
2699
3178
|
add(['list-style-position'], (c) => keywords(c, ['inside', 'outside']));
|
|
2700
3179
|
add(['list-style-image'], (c) => single(c, (x) => image(x) ?? keyword(x, ['none'])), false);
|
|
2701
3180
|
add(['z-index'], zIndex);
|
|
3181
|
+
add(['order'], (comps) => single(comps, (c) => integer(c, -Infinity)));
|
|
2702
3182
|
add(['width', 'height', 'min-width', 'min-height', 'block-size', 'inline-size', 'min-block-size', 'min-inline-size'], sizeV);
|
|
2703
3183
|
add(['max-width', 'max-height', 'max-block-size', 'max-inline-size'], maxSizeV);
|
|
2704
3184
|
add(['column-count'], (c) => single(c, (x) => keyword(x, ['auto']) ?? integer(x, 1)));
|
|
@@ -2745,6 +3225,8 @@ add(['text-wrap-style'], (c) => keywords(c, ['auto', 'balance', 'stable', 'prett
|
|
|
2745
3225
|
add(['overflow-wrap'], (c) => keywords(c, ['normal', 'break-word', 'anywhere']));
|
|
2746
3226
|
add(['grid-template-areas'], gridAreas);
|
|
2747
3227
|
add(['offset-path'], offsetPath);
|
|
3228
|
+
add(['clip-path'], clipPath, false);
|
|
3229
|
+
add(['shape-outside'], shapeOutside, false);
|
|
2748
3230
|
add(['opacity'], opacityV);
|
|
2749
3231
|
add(['display'], display, false);
|
|
2750
3232
|
add(['animation-name'], animationNames);
|
|
@@ -2799,5 +3281,6 @@ export function longhandValue(name, value) {
|
|
|
2799
3281
|
if (!grammar) return { valid: null, text: genericSpelling(name, comps) };
|
|
2800
3282
|
const text = grammar[0](comps);
|
|
2801
3283
|
if (text !== null) return { valid: true, text };
|
|
2802
|
-
|
|
3284
|
+
// A lenient grammar's rejection of a shape() is final: no embedder grammar knows shape() (#338).
|
|
3285
|
+
return grammar[1] || comps.some((c) => functionNamed(c, ['shape']) !== null) ? { valid: false } : { valid: null, text: genericSpelling(name, comps) };
|
|
2803
3286
|
}
|