react-x11 2.49.0 → 2.51.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/README.md +5 -1
- package/package.json +4 -4
- package/src/ClickToComponent.js +11 -132
- package/src/DevToolsIntegration.js +126 -42
- package/src/Reconciler.js +26 -10
- package/src/cli/dev.js +162 -19
- package/src/cli/errorwindow.js +368 -0
- package/src/cli/index.js +4 -0
- package/src/cocoa/app.js +22 -0
- package/src/cocoa/presenter.js +3 -1
- package/src/cocoa/promotion.js +10 -11
- package/src/cocoa/relaunch.js +114 -15
- package/src/cocoa/window.js +98 -15
- package/src/currentcolor.js +28 -0
- package/src/decorations.js +21 -9
- package/src/editor.js +151 -0
- package/src/foreignnodes.js +8 -1
- package/src/gloverlay.js +2 -1
- package/src/node.d.ts +5 -0
- package/src/nodes/animation.js +31 -3
- package/src/nodes/boxpaint.js +34 -8
- package/src/nodes/cascade.js +50 -3
- package/src/nodes/damage.js +2 -0
- package/src/nodes/layout.js +9 -17
- package/src/nodes/moveblit.js +5 -5
- package/src/nodes/node.js +8 -0
- package/src/nodes/paint.js +4 -2
- package/src/nodes/queries.js +46 -34
- package/src/nodes/scrollable.js +4 -1
- package/src/nodes/scrollblit.js +10 -7
- package/src/nodes/styling.js +176 -21
- package/src/nodes/window/capabilities.js +1 -1
- package/src/nodes/window/floors.js +63 -18
- package/src/nodes/window/size.js +94 -19
- package/src/refresh/loader.d.ts +16 -0
- package/src/refresh/loader.js +58 -8
- package/src/shorthands.js +1266 -0
- package/src/styles.js +370 -131
- package/src/types/elements.d.ts +5 -2
- package/src/types/style.d.ts +163 -39
- package/src/types/system.d.ts +4 -1
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
// CSS's shorthands, as the longhands they set — and the one grammar every
|
|
2
|
+
// length in a style is read by.
|
|
3
|
+
//
|
|
4
|
+
// Pure, like decorations.js and grid.js: strings in, longhands out. The
|
|
5
|
+
// style funnel expands with it (`expandStyle`, from styles.js's
|
|
6
|
+
// `resolveComputedStyle`) and development validates with the same readers
|
|
7
|
+
// (`checkShorthand`, `readLength`), so the two cannot disagree about what a
|
|
8
|
+
// value means.
|
|
9
|
+
//
|
|
10
|
+
// Three decisions here are easy to undo:
|
|
11
|
+
//
|
|
12
|
+
// - **The more specific property wins, wherever it is written.**
|
|
13
|
+
// `paddingLeft` beside `padding: '8px 16px'` is the left padding whether
|
|
14
|
+
// it comes before or after it in the style — the rule `paddingLeft` beside
|
|
15
|
+
// `padding: 8` has always followed, and the one `flex` follows. CSS decides
|
|
16
|
+
// by order, and a flattened style array has no order left to decide by: a
|
|
17
|
+
// longhand and its shorthand are two keys of one object. So a longhand
|
|
18
|
+
// beats a shorthand, a side beats all four, and a pair (`paddingBlock`)
|
|
19
|
+
// beats all four too. Among the shorthands that is `SHORTHAND_ORDER`.
|
|
20
|
+
//
|
|
21
|
+
// - **A length is a number of logical pixels however it is written.** `12`,
|
|
22
|
+
// `'12'` and `'12px'` are the same value by the time anything reads it.
|
|
23
|
+
// They were not: the display scale multiplies numbers only
|
|
24
|
+
// (`scaleResolvedStyle`), so `'12px'` was six logical pixels at 2x; yoga's
|
|
25
|
+
// border setter takes no strings, so `borderWidth: '2px'` drew nothing;
|
|
26
|
+
// and the layout's fast paths read any string as a percentage. A unit this
|
|
27
|
+
// file does not know is an error where the style is written, never
|
|
28
|
+
// `parseFloat`'s guess — yoga read `'2em'` as two pixels.
|
|
29
|
+
//
|
|
30
|
+
// - **An omitted part is left unset, not reset.** CSS resets every longhand
|
|
31
|
+
// a shorthand leaves out; here the longhand beside it would win anyway (the
|
|
32
|
+
// first rule), so a reset could only ever reach a property nobody named.
|
|
33
|
+
// Three exceptions, all CSS's: a border or an outline with no width is
|
|
34
|
+
// `medium`, 3px — a border with no width draws nothing here, where CSS
|
|
35
|
+
// would draw one — `none` is a width of 0, the way `border: none` and
|
|
36
|
+
// `outline: none` are written to take one away, and a line with no colour
|
|
37
|
+
// is `currentColor`. That last one is the weakest thing in the style: a
|
|
38
|
+
// colour the style names for the same side anywhere else — `borderColor`
|
|
39
|
+
// beside `borderBottom: '1px solid'` — is the one it takes
|
|
40
|
+
// (`expandStyle`), which is what such a style meant before there was a
|
|
41
|
+
// `currentColor` to fall back on.
|
|
42
|
+
import { cssColorStraight } from 'ntk/color';
|
|
43
|
+
|
|
44
|
+
import { isCurrentColor } from './currentcolor.js';
|
|
45
|
+
import { parseLinearGradient, splitTop } from './decorations.js';
|
|
46
|
+
|
|
47
|
+
/** What a length property takes besides pixels. */
|
|
48
|
+
const PERCENT = 1;
|
|
49
|
+
const AUTO = 2;
|
|
50
|
+
/** CSS's `thin`, `medium` and `thick`: a line's width only. */
|
|
51
|
+
const WIDTH_WORDS = 4;
|
|
52
|
+
/** `fontSize`'s percentage, which is CSS's: of the size it inherits — the
|
|
53
|
+
* same thing `em` means there — and so a number, never a string. */
|
|
54
|
+
const OF_FONT = 8;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Every style property that is a distance on the screen, and what each takes
|
|
58
|
+
* besides pixels. The same set the display scale multiplies
|
|
59
|
+
* (`SCALED_LENGTH_PROPS` in styles.js is these keys), because they are the
|
|
60
|
+
* same question: which values are lengths.
|
|
61
|
+
*
|
|
62
|
+
* `'auto'` and the percentages are what yoga's own setters take, which is the
|
|
63
|
+
* source of truth for the layout half. Of the paint half, a radius takes a
|
|
64
|
+
* percentage too — of the box's shorter side, read where the corner is drawn
|
|
65
|
+
* (`radiusOf` in styles.js), since a corner here is a circle and CSS's
|
|
66
|
+
* elliptical one is not drawn — and the rest take lengths only. Border widths
|
|
67
|
+
* refuse a percentage the way CSS's do; yoga's border setter coerced one to a
|
|
68
|
+
* number.
|
|
69
|
+
*
|
|
70
|
+
* Every one of them takes the relative units as well (`RELATIVE`).
|
|
71
|
+
*/
|
|
72
|
+
export const LENGTH_FORMS = new Map([
|
|
73
|
+
['width', PERCENT | AUTO],
|
|
74
|
+
['height', PERCENT | AUTO],
|
|
75
|
+
['minWidth', PERCENT],
|
|
76
|
+
['minHeight', PERCENT],
|
|
77
|
+
['maxWidth', PERCENT],
|
|
78
|
+
['maxHeight', PERCENT],
|
|
79
|
+
['flexBasis', PERCENT | AUTO],
|
|
80
|
+
['top', PERCENT | AUTO],
|
|
81
|
+
['right', PERCENT | AUTO],
|
|
82
|
+
['bottom', PERCENT | AUTO],
|
|
83
|
+
['left', PERCENT | AUTO],
|
|
84
|
+
['start', PERCENT | AUTO],
|
|
85
|
+
['end', PERCENT | AUTO],
|
|
86
|
+
['margin', PERCENT | AUTO],
|
|
87
|
+
['marginTop', PERCENT | AUTO],
|
|
88
|
+
['marginRight', PERCENT | AUTO],
|
|
89
|
+
['marginBottom', PERCENT | AUTO],
|
|
90
|
+
['marginLeft', PERCENT | AUTO],
|
|
91
|
+
['marginStart', PERCENT | AUTO],
|
|
92
|
+
['marginEnd', PERCENT | AUTO],
|
|
93
|
+
['padding', PERCENT],
|
|
94
|
+
['paddingTop', PERCENT],
|
|
95
|
+
['paddingRight', PERCENT],
|
|
96
|
+
['paddingBottom', PERCENT],
|
|
97
|
+
['paddingLeft', PERCENT],
|
|
98
|
+
['paddingStart', PERCENT],
|
|
99
|
+
['paddingEnd', PERCENT],
|
|
100
|
+
['gap', PERCENT],
|
|
101
|
+
['rowGap', PERCENT],
|
|
102
|
+
['columnGap', PERCENT],
|
|
103
|
+
['borderWidth', WIDTH_WORDS],
|
|
104
|
+
['borderTopWidth', WIDTH_WORDS],
|
|
105
|
+
['borderRightWidth', WIDTH_WORDS],
|
|
106
|
+
['borderBottomWidth', WIDTH_WORDS],
|
|
107
|
+
['borderLeftWidth', WIDTH_WORDS],
|
|
108
|
+
['borderStartWidth', WIDTH_WORDS],
|
|
109
|
+
['borderEndWidth', WIDTH_WORDS],
|
|
110
|
+
['borderRadius', PERCENT],
|
|
111
|
+
['outlineWidth', WIDTH_WORDS],
|
|
112
|
+
['outlineOffset', 0],
|
|
113
|
+
['fontSize', OF_FONT],
|
|
114
|
+
['letterSpacing', 0],
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const WIDTHS = { thin: 1, medium: 3, thick: 5 };
|
|
118
|
+
/** What a border or an outline with no width of its own is: CSS's `medium`. */
|
|
119
|
+
const MEDIUM = WIDTHS.medium;
|
|
120
|
+
|
|
121
|
+
const NUMBER_AND_UNIT = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)$/i;
|
|
122
|
+
/** A string the funnel can hand on as it is: a percentage, or `'auto'`. */
|
|
123
|
+
const AS_WRITTEN = /^(?:[+-]?(?:\d+\.?\d*|\.\d+)%|auto)$/;
|
|
124
|
+
const FUNCTION = /^(calc|min|max|clamp)\(/i;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* What a relative length is relative to, as the bits a node records so that
|
|
128
|
+
* it resolves the length again when that moves (nodes/styling.js):
|
|
129
|
+
*
|
|
130
|
+
* - `UNIT_EM` — a font size. `em` is this node's own, and on `fontSize`
|
|
131
|
+
* itself the one it inherits, as in CSS; it moves when an ancestor's size
|
|
132
|
+
* does, which the text cascade already walks (`_retext`).
|
|
133
|
+
* - `UNIT_REM` — the palette's `fontSize`, which is the root font size an
|
|
134
|
+
* app here has: the size every label that names none is set in. It moves
|
|
135
|
+
* when the theme does (`_themeChanged`).
|
|
136
|
+
* - `UNIT_VIEWPORT` — the window the node is in: the `<window>`, or the
|
|
137
|
+
* `<popup>`, the same one a `'@width >= 600'` block asks about, in the
|
|
138
|
+
* same logical pixels. It moves on a resize, through the same registry.
|
|
139
|
+
*/
|
|
140
|
+
export const UNIT_EM = 1;
|
|
141
|
+
export const UNIT_REM = 2;
|
|
142
|
+
export const UNIT_VIEWPORT = 4;
|
|
143
|
+
|
|
144
|
+
/** Each relative unit, as what one of it is in logical pixels. On a desktop
|
|
145
|
+
* the small, large and dynamic viewports are the one window. */
|
|
146
|
+
const RELATIVE = {
|
|
147
|
+
em: (ctx, key) => (key === 'fontSize' ? ctx.parentEm : ctx.em),
|
|
148
|
+
rem: (ctx) => ctx.rem,
|
|
149
|
+
vw: (ctx) => ctx.viewport.width / 100,
|
|
150
|
+
vh: (ctx) => ctx.viewport.height / 100,
|
|
151
|
+
vmin: (ctx) => Math.min(ctx.viewport.width, ctx.viewport.height) / 100,
|
|
152
|
+
vmax: (ctx) => Math.max(ctx.viewport.width, ctx.viewport.height) / 100,
|
|
153
|
+
};
|
|
154
|
+
for (const prefix of ['s', 'l', 'd']) {
|
|
155
|
+
RELATIVE[`${prefix}vw`] = RELATIVE.vw;
|
|
156
|
+
RELATIVE[`${prefix}vh`] = RELATIVE.vh;
|
|
157
|
+
RELATIVE[`${prefix}vmin`] = RELATIVE.vmin;
|
|
158
|
+
RELATIVE[`${prefix}vmax`] = RELATIVE.vmax;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The units CSS has that a length here does not read, each with the reason
|
|
162
|
+
* — so the error says which one it met, and why, rather than "invalid". */
|
|
163
|
+
const NOT_READ = {
|
|
164
|
+
ex: "it needs the font's x-height, which nothing measures yet — 0.5em is the usual stand-in",
|
|
165
|
+
ch: "it needs the font's 0 advance, which nothing measures yet — 0.5em is the usual stand-in",
|
|
166
|
+
lh: 'a line height relative to itself is circular here — use em',
|
|
167
|
+
rlh: 'a line height relative to itself is circular here — use rem',
|
|
168
|
+
};
|
|
169
|
+
for (const unit of ['cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax']) {
|
|
170
|
+
NOT_READ[unit] =
|
|
171
|
+
"a container's size is known only after a layout pass, and a length in " +
|
|
172
|
+
'it would lay the tree out again on every frame the container moves — a ' +
|
|
173
|
+
"percentage of the parent, or an '@container' block, says it without that";
|
|
174
|
+
}
|
|
175
|
+
const PRINT_UNITS = new Set(['pt', 'pc', 'in', 'cm', 'mm', 'q']);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A context that resolves every relative unit to 1 and records nothing:
|
|
179
|
+
* development's, which asks whether a value reads, not what it comes to.
|
|
180
|
+
*/
|
|
181
|
+
export const PROBE = Object.freeze({
|
|
182
|
+
em: 16,
|
|
183
|
+
parentEm: 16,
|
|
184
|
+
rem: 16,
|
|
185
|
+
viewport: Object.freeze({ width: 100, height: 100 }),
|
|
186
|
+
deps: 0,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const isToken = (v) =>
|
|
190
|
+
typeof v === 'string' && /^\$[A-Za-z_][A-Za-z0-9_-]*$/.test(v);
|
|
191
|
+
const mentionsToken = (v) => typeof v === 'string' && v.includes('$');
|
|
192
|
+
/** A colour as the grammar reads one: anything ntk parses, and CSS's
|
|
193
|
+
* `currentColor`, which the paint resolves to the node's ink. */
|
|
194
|
+
const isColor = (v) =>
|
|
195
|
+
typeof v === 'string' && (cssColorStraight(v) !== null || isCurrentColor(v));
|
|
196
|
+
|
|
197
|
+
/** What a line the shorthand gives no colour is drawn in. */
|
|
198
|
+
const CURRENT_COLOR = 'currentColor';
|
|
199
|
+
|
|
200
|
+
/** What a length property takes, said the way the errors say it. */
|
|
201
|
+
function expected(forms) {
|
|
202
|
+
const words = ["a number of logical pixels, like 12 or '12px'"];
|
|
203
|
+
if (forms & PERCENT) words.push("a percentage like '50%'");
|
|
204
|
+
if (forms & OF_FONT)
|
|
205
|
+
words.push("a percentage of the inherited size, like '120%'");
|
|
206
|
+
if (forms & AUTO) words.push("'auto'");
|
|
207
|
+
if (forms & WIDTH_WORDS) words.push("'thin', 'medium' or 'thick'");
|
|
208
|
+
words.push('em, rem, vw or vh');
|
|
209
|
+
return words.join(', or ');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A value that does not read. The message says what was written, why it
|
|
214
|
+
* does not read and what would; `in(where)` puts the place it was written
|
|
215
|
+
* where every other style error has it — `invalid width "2ex" in <box
|
|
216
|
+
* style>: …` — once the caller knows the place, which a parser never does.
|
|
217
|
+
*/
|
|
218
|
+
export class StyleValueError extends Error {
|
|
219
|
+
constructor(key, value, why, expectation) {
|
|
220
|
+
super('');
|
|
221
|
+
this.key = key;
|
|
222
|
+
this.value = value;
|
|
223
|
+
this.why = why;
|
|
224
|
+
this.expectation = expectation;
|
|
225
|
+
this.where = null;
|
|
226
|
+
this.message = this.text();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
text() {
|
|
230
|
+
return (
|
|
231
|
+
`react-x11: invalid ${this.key} ${JSON.stringify(this.value)}` +
|
|
232
|
+
(this.where ? ` in ${this.where}` : '') +
|
|
233
|
+
(this.why ? `: ${this.why}` : '') +
|
|
234
|
+
(this.expectation ? ` (expected ${this.expectation})` : '')
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Said of `key` instead: a piece of a shorthand is named after the
|
|
239
|
+
* property that was written, not the longhand it was read as. */
|
|
240
|
+
as(key) {
|
|
241
|
+
this.key = key;
|
|
242
|
+
this.message = this.text();
|
|
243
|
+
return this;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
in(where) {
|
|
247
|
+
this.where = where;
|
|
248
|
+
this.message = this.text();
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Put `where` into an error a reader threw: in its place for one of ours,
|
|
254
|
+
* and on a line of its own for one from a parser that speaks its own
|
|
255
|
+
* sentences (a gradient's). */
|
|
256
|
+
export function locate(err, where) {
|
|
257
|
+
if (err instanceof StyleValueError) return err.in(where);
|
|
258
|
+
err.message += `\n in ${where}`;
|
|
259
|
+
return err;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const invalid = (key, value, why, forms) =>
|
|
263
|
+
new StyleValueError(key, value, why, expected(forms));
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* One length, as everything downstream reads it: a number of logical pixels,
|
|
267
|
+
* a percentage string, or `'auto'`. Throws naming what was wrong — the unit
|
|
268
|
+
* it does not read, the percentage this property does not take — and `key`'s
|
|
269
|
+
* forms decide which those are.
|
|
270
|
+
*
|
|
271
|
+
* A relative unit — `em`, `rem`, `vw` and the rest — and `calc()`, `min()`,
|
|
272
|
+
* `max()` and `clamp()` over them are resolved against `ctx`: the node's
|
|
273
|
+
* font sizes and its window, in logical pixels, each read recording the
|
|
274
|
+
* dependency on `ctx.deps`. Without one there is nothing to resolve against,
|
|
275
|
+
* and a relative length is an error: a style reaches the layout through the
|
|
276
|
+
* funnel, which always has its node, so one that arrives unresolved is a
|
|
277
|
+
* path that skipped it. Development reads with `PROBE`.
|
|
278
|
+
*
|
|
279
|
+
* `value` is one value: a property that takes several goes through its
|
|
280
|
+
* shorthand reader, which calls this per piece.
|
|
281
|
+
*/
|
|
282
|
+
export function readLength(
|
|
283
|
+
key,
|
|
284
|
+
value,
|
|
285
|
+
forms = LENGTH_FORMS.get(key) ?? 0,
|
|
286
|
+
ctx = null,
|
|
287
|
+
) {
|
|
288
|
+
if (typeof value === 'number') {
|
|
289
|
+
if (Number.isFinite(value)) return value;
|
|
290
|
+
throw invalid(key, value, 'it is not a finite number', forms);
|
|
291
|
+
}
|
|
292
|
+
if (typeof value !== 'string') {
|
|
293
|
+
throw invalid(key, value, null, forms);
|
|
294
|
+
}
|
|
295
|
+
const text = value.trim();
|
|
296
|
+
if (forms & AUTO && text === 'auto') return 'auto';
|
|
297
|
+
if (forms & WIDTH_WORDS && Object.hasOwn(WIDTHS, text)) return WIDTHS[text];
|
|
298
|
+
if (FUNCTION.test(text)) return readExpression(key, value, forms, ctx);
|
|
299
|
+
const m = NUMBER_AND_UNIT.exec(text);
|
|
300
|
+
if (!m) {
|
|
301
|
+
const fn = /^([a-z-]+)\(/i.exec(text);
|
|
302
|
+
if (fn) {
|
|
303
|
+
throw invalid(
|
|
304
|
+
key,
|
|
305
|
+
value,
|
|
306
|
+
`${fn[1]}() is not read — calc, min, max and clamp are`,
|
|
307
|
+
forms,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (splitTop(text, false).length > 1) {
|
|
311
|
+
throw invalid(
|
|
312
|
+
key,
|
|
313
|
+
value,
|
|
314
|
+
key === 'borderRadius'
|
|
315
|
+
? 'a radius per corner is not supported — one radius rounds all four'
|
|
316
|
+
: `${key} takes one value`,
|
|
317
|
+
forms,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
throw invalid(key, value, null, forms);
|
|
321
|
+
}
|
|
322
|
+
return unitValue(key, value, Number(m[1]), m[2].toLowerCase(), forms, ctx);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** A number with its unit, resolved: the one place a unit means anything. */
|
|
326
|
+
function unitValue(key, value, n, unit, forms, ctx) {
|
|
327
|
+
if (unit === '' || unit === 'px') return n;
|
|
328
|
+
if (unit === '%') {
|
|
329
|
+
if (forms & OF_FONT)
|
|
330
|
+
return (n * relative('em', key, value, forms, ctx)) / 100;
|
|
331
|
+
if (forms & PERCENT) return `${n}%`;
|
|
332
|
+
throw invalid(key, value, `${key} does not take a percentage`, forms);
|
|
333
|
+
}
|
|
334
|
+
if (Object.hasOwn(RELATIVE, unit)) {
|
|
335
|
+
return n * relative(unit, key, value, forms, ctx);
|
|
336
|
+
}
|
|
337
|
+
if (Object.hasOwn(NOT_READ, unit)) {
|
|
338
|
+
throw invalid(key, value, `${unit} is not read — ${NOT_READ[unit]}`, forms);
|
|
339
|
+
}
|
|
340
|
+
if (PRINT_UNITS.has(unit)) {
|
|
341
|
+
throw invalid(
|
|
342
|
+
key,
|
|
343
|
+
value,
|
|
344
|
+
`${unit} is a print unit — lengths here are logical pixels, which the ` +
|
|
345
|
+
'display scale turns into device pixels',
|
|
346
|
+
forms,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
throw invalid(key, value, `"${unit}" is not a unit`, forms);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** What one of `unit` is, in logical pixels, here. */
|
|
353
|
+
function relative(unit, key, value, forms, ctx) {
|
|
354
|
+
if (ctx === null) {
|
|
355
|
+
throw invalid(
|
|
356
|
+
key,
|
|
357
|
+
value,
|
|
358
|
+
`${unit} is resolved for the node it is written on, and this value ` +
|
|
359
|
+
'reached the layout without being resolved',
|
|
360
|
+
forms,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
const one = RELATIVE[unit](ctx, key);
|
|
364
|
+
if (!Number.isFinite(one)) {
|
|
365
|
+
throw invalid(
|
|
366
|
+
key,
|
|
367
|
+
value,
|
|
368
|
+
`there is no ${unit} to resolve it against`,
|
|
369
|
+
forms,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return one;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// calc(), min(), max(), clamp().
|
|
377
|
+
//
|
|
378
|
+
// Resolved where the style is, to one number: every term that is a length
|
|
379
|
+
// here becomes logical pixels against the node's context, which is what lets
|
|
380
|
+
// `clamp(14px, 1rem + 0.5vw, 20px)` — fluid type — mean something without a
|
|
381
|
+
// layout pass. A percentage is the exception, because yoga takes one unit per
|
|
382
|
+
// value and what a percentage is of is decided inside the layout: an
|
|
383
|
+
// expression of percentages alone stays one (`calc(100% / 3)` is `'33.3…%'`),
|
|
384
|
+
// and one that mixes a percentage with a length is an error that says so,
|
|
385
|
+
// since `calc(100% - 16px)` cannot be handed to yoga as anything.
|
|
386
|
+
|
|
387
|
+
/** A value in an expression: a length in pixels, a percentage, or a bare
|
|
388
|
+
* number — which a length property reads as pixels, as everywhere here. */
|
|
389
|
+
const LEN = 1;
|
|
390
|
+
const PCT = 2;
|
|
391
|
+
const NUM = 3;
|
|
392
|
+
|
|
393
|
+
function tokenize(text) {
|
|
394
|
+
const tokens = [];
|
|
395
|
+
const re =
|
|
396
|
+
/\s*(?:([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)|([a-z-]+)\(|([()+\-*/,]))/giy;
|
|
397
|
+
let at = 0;
|
|
398
|
+
while (at < text.length) {
|
|
399
|
+
re.lastIndex = at;
|
|
400
|
+
const m = re.exec(text);
|
|
401
|
+
if (!m) {
|
|
402
|
+
if (/^\s*$/.test(text.slice(at))) break;
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
// a sign belongs to the number only where an operand is expected — after
|
|
406
|
+
// an operator, an opening parenthesis or a comma, or at the start
|
|
407
|
+
if (m[1] !== undefined && /^[+-]/.test(m[1])) {
|
|
408
|
+
const prev = tokens[tokens.length - 1];
|
|
409
|
+
if (prev && (prev.type === 'value' || prev.op === ')')) {
|
|
410
|
+
tokens.push({ type: 'op', op: m[1][0] });
|
|
411
|
+
at = re.lastIndex - m[0].length + m[0].indexOf(m[1]) + 1;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (m[1] !== undefined) {
|
|
416
|
+
tokens.push({ type: 'value', n: Number(m[1]), unit: m[2].toLowerCase() });
|
|
417
|
+
} else if (m[3] !== undefined) {
|
|
418
|
+
tokens.push({ type: 'fn', name: m[3].toLowerCase() });
|
|
419
|
+
} else {
|
|
420
|
+
tokens.push({ type: 'op', op: m[4] });
|
|
421
|
+
}
|
|
422
|
+
at = re.lastIndex;
|
|
423
|
+
}
|
|
424
|
+
return tokens;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function readExpression(key, value, forms, ctx) {
|
|
428
|
+
const bad = (why) => invalid(key, value, why, forms);
|
|
429
|
+
const tokens = tokenize(value.trim());
|
|
430
|
+
if (!tokens) throw bad('it is not an expression this reads');
|
|
431
|
+
let i = 0;
|
|
432
|
+
const peek = () => tokens[i];
|
|
433
|
+
const take = (op) => {
|
|
434
|
+
const t = tokens[i];
|
|
435
|
+
if (!t || t.type !== 'op' || t.op !== op) {
|
|
436
|
+
throw bad(`expected "${op}"${t ? '' : ' before the end'}`);
|
|
437
|
+
}
|
|
438
|
+
i++;
|
|
439
|
+
};
|
|
440
|
+
const mixed = () =>
|
|
441
|
+
bad(
|
|
442
|
+
'it mixes a percentage with a length, and yoga takes one unit per ' +
|
|
443
|
+
'value — what a percentage is of is decided inside the layout. ' +
|
|
444
|
+
"Pad a '100%' box rather than subtracting from it, or give the room " +
|
|
445
|
+
'to flexGrow',
|
|
446
|
+
);
|
|
447
|
+
// a bare number is pixels where it meets a length, as everywhere here
|
|
448
|
+
const unify = (a, b) => {
|
|
449
|
+
if (a.kind === b.kind) return a.kind;
|
|
450
|
+
if (a.kind === PCT || b.kind === PCT) throw mixed();
|
|
451
|
+
return LEN;
|
|
452
|
+
};
|
|
453
|
+
const operand = (t) => {
|
|
454
|
+
if (t.unit === '') return { kind: NUM, v: t.n };
|
|
455
|
+
if (t.unit === '%' && !(forms & OF_FONT)) {
|
|
456
|
+
if (!(forms & PERCENT)) {
|
|
457
|
+
throw bad(`${key} does not take a percentage`);
|
|
458
|
+
}
|
|
459
|
+
return { kind: PCT, v: t.n };
|
|
460
|
+
}
|
|
461
|
+
return { kind: LEN, v: unitValue(key, value, t.n, t.unit, forms, ctx) };
|
|
462
|
+
};
|
|
463
|
+
const call = (name) => {
|
|
464
|
+
const args = [sum()];
|
|
465
|
+
while (peek()?.op === ',') {
|
|
466
|
+
i++;
|
|
467
|
+
args.push(sum());
|
|
468
|
+
}
|
|
469
|
+
take(')');
|
|
470
|
+
if (name === 'calc') {
|
|
471
|
+
if (args.length !== 1) throw bad('calc() takes one expression');
|
|
472
|
+
return args[0];
|
|
473
|
+
}
|
|
474
|
+
if (name === 'clamp' && args.length !== 3) {
|
|
475
|
+
throw bad('clamp() takes a minimum, a preferred value and a maximum');
|
|
476
|
+
}
|
|
477
|
+
if (args.length < 1) throw bad(`${name}() takes at least one value`);
|
|
478
|
+
let kind = args[0].kind;
|
|
479
|
+
for (const arg of args) kind = unify({ kind }, arg);
|
|
480
|
+
const vs = args.map((a) => a.v);
|
|
481
|
+
if (name === 'min') return { kind, v: Math.min(...vs) };
|
|
482
|
+
if (name === 'max') return { kind, v: Math.max(...vs) };
|
|
483
|
+
return { kind, v: Math.max(vs[0], Math.min(vs[1], vs[2])) };
|
|
484
|
+
};
|
|
485
|
+
const factor = () => {
|
|
486
|
+
const t = tokens[i++];
|
|
487
|
+
if (!t) throw bad('it ends where a value was expected');
|
|
488
|
+
if (t.type === 'value') return operand(t);
|
|
489
|
+
if (t.type === 'fn') {
|
|
490
|
+
if (!['calc', 'min', 'max', 'clamp'].includes(t.name)) {
|
|
491
|
+
throw bad(`${t.name}() is not read — calc, min, max and clamp are`);
|
|
492
|
+
}
|
|
493
|
+
return call(t.name);
|
|
494
|
+
}
|
|
495
|
+
if (t.op === '(') {
|
|
496
|
+
const inner = sum();
|
|
497
|
+
take(')');
|
|
498
|
+
return inner;
|
|
499
|
+
}
|
|
500
|
+
throw bad(`"${t.op}" where a value was expected`);
|
|
501
|
+
};
|
|
502
|
+
const product = () => {
|
|
503
|
+
let a = factor();
|
|
504
|
+
while (peek()?.op === '*' || peek()?.op === '/') {
|
|
505
|
+
const op = tokens[i++].op;
|
|
506
|
+
const b = factor();
|
|
507
|
+
if (op === '*') {
|
|
508
|
+
if (a.kind !== NUM && b.kind !== NUM) {
|
|
509
|
+
throw bad('one side of a multiplication has to be a plain number');
|
|
510
|
+
}
|
|
511
|
+
a = { kind: a.kind === NUM ? b.kind : a.kind, v: a.v * b.v };
|
|
512
|
+
} else {
|
|
513
|
+
if (b.kind !== NUM) throw bad('a division is by a plain number');
|
|
514
|
+
if (b.v === 0) throw bad('it divides by zero');
|
|
515
|
+
a = { kind: a.kind, v: a.v / b.v };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return a;
|
|
519
|
+
};
|
|
520
|
+
const sum = () => {
|
|
521
|
+
let a = product();
|
|
522
|
+
while (peek()?.op === '+' || peek()?.op === '-') {
|
|
523
|
+
const op = tokens[i++].op;
|
|
524
|
+
const b = product();
|
|
525
|
+
a = { kind: unify(a, b), v: op === '+' ? a.v + b.v : a.v - b.v };
|
|
526
|
+
}
|
|
527
|
+
return a;
|
|
528
|
+
};
|
|
529
|
+
const result = factor();
|
|
530
|
+
if (i !== tokens.length) throw bad('there is more after the expression');
|
|
531
|
+
if (!Number.isFinite(result.v)) throw bad('it does not come to a number');
|
|
532
|
+
if (result.kind === PCT) return `${Math.round(result.v * 1e4) / 1e4}%`;
|
|
533
|
+
return result.v;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
// lineHeight: a multiple of the font's own line height, written any way CSS
|
|
538
|
+
// writes one.
|
|
539
|
+
|
|
540
|
+
const MULTIPLE = /^\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?\s*$/i;
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* A `lineHeight` as the multiple the text engines take, or undefined for
|
|
544
|
+
* `'normal'` — the font's own. CSS's forms all come to one: `1.5`, `'1.5'`,
|
|
545
|
+
* `'1.5em'` and `'150%'` are the multiple as written, and an absolute length
|
|
546
|
+
* — `'24px'`, `'1.5rem'`, a `calc()` — is divided by the node's own font size
|
|
547
|
+
* (`ctx.em`), the one size the multiple is applied to, which is what makes a
|
|
548
|
+
* design's "16 on 24" the line it asks for.
|
|
549
|
+
*/
|
|
550
|
+
export function readLineHeight(value, ctx = null) {
|
|
551
|
+
if (typeof value === 'number') {
|
|
552
|
+
if (Number.isFinite(value) && value >= 0) return value;
|
|
553
|
+
throw new StyleValueError(
|
|
554
|
+
'lineHeight',
|
|
555
|
+
value,
|
|
556
|
+
'it is not a multiple',
|
|
557
|
+
LINE_HEIGHT,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (typeof value !== 'string') {
|
|
561
|
+
throw new StyleValueError('lineHeight', value, null, LINE_HEIGHT);
|
|
562
|
+
}
|
|
563
|
+
const text = value.trim();
|
|
564
|
+
if (text === 'normal') return undefined;
|
|
565
|
+
if (MULTIPLE.test(text)) return readLineHeight(Number(text), ctx);
|
|
566
|
+
const m = NUMBER_AND_UNIT.exec(text);
|
|
567
|
+
if (m && m[2].toLowerCase() === 'em')
|
|
568
|
+
return readLineHeight(Number(m[1]), ctx);
|
|
569
|
+
if (m && m[2] === '%') return readLineHeight(Number(m[1]) / 100, ctx);
|
|
570
|
+
let px;
|
|
571
|
+
try {
|
|
572
|
+
px = readLength('lineHeight', value, 0, ctx);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
throw err instanceof StyleValueError
|
|
575
|
+
? new StyleValueError('lineHeight', value, err.why, LINE_HEIGHT)
|
|
576
|
+
: err;
|
|
577
|
+
}
|
|
578
|
+
if (ctx === null) {
|
|
579
|
+
throw new StyleValueError(
|
|
580
|
+
'lineHeight',
|
|
581
|
+
value,
|
|
582
|
+
'a line height in pixels is resolved against the font size of the ' +
|
|
583
|
+
'node it is written on',
|
|
584
|
+
LINE_HEIGHT,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return readLineHeight(px / ctx.em, ctx);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const LINE_HEIGHT =
|
|
591
|
+
"a multiple of the font's own line height — 1.5, '150%', '1.5em' — a " +
|
|
592
|
+
"length like '24px' or '1.5rem', or 'normal'";
|
|
593
|
+
|
|
594
|
+
/** Whether the funnel has to read a `lineHeight` rather than hand it on. */
|
|
595
|
+
const lineHeightNeedsReading = (value) =>
|
|
596
|
+
typeof value === 'string' && !MULTIPLE.test(value);
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* `readLength`, or the value as written where it would throw: the funnel's
|
|
600
|
+
* reading. A layout property's applier reads it again and refuses it, which
|
|
601
|
+
* is what reports it — thrown at a commit, dropped and said in a frame
|
|
602
|
+
* (`applyLayoutStyle`) — and a paint property's reader passes over what is
|
|
603
|
+
* not a number, as it always has.
|
|
604
|
+
*/
|
|
605
|
+
function lengthOrAsWritten(key, value, ctx) {
|
|
606
|
+
try {
|
|
607
|
+
return readLength(key, value, undefined, ctx);
|
|
608
|
+
} catch {
|
|
609
|
+
return value;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Whether the funnel has to read a length value rather than hand it on: a
|
|
614
|
+
* percentage goes on as it is, except on `fontSize`, where it is the
|
|
615
|
+
* inherited size's and comes to a number. */
|
|
616
|
+
const needsReading = (key, value) =>
|
|
617
|
+
typeof value === 'string' &&
|
|
618
|
+
(!AS_WRITTEN.test(value) || LENGTH_FORMS.get(key) & OF_FONT) &&
|
|
619
|
+
value !== 'auto';
|
|
620
|
+
|
|
621
|
+
const pieces = (value) => splitTop(String(value).trim(), false);
|
|
622
|
+
|
|
623
|
+
/** Whether a value is several, the way a top-level space separates them. A
|
|
624
|
+
* hover asks this of every `borderColor`, so a value with no space in it is
|
|
625
|
+
* answered without the split. */
|
|
626
|
+
const SPACE = /\S\s+\S/;
|
|
627
|
+
const isSeveral = (value) =>
|
|
628
|
+
typeof value === 'string' && SPACE.test(value) && pieces(value).length > 1;
|
|
629
|
+
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
// The shorthands.
|
|
632
|
+
|
|
633
|
+
/** `padding: '8px 16px'` and the rest of CSS's one-to-four rule: top, right,
|
|
634
|
+
* bottom, left, a missing one taken from the side opposite. */
|
|
635
|
+
const EDGE_LISTS = {
|
|
636
|
+
padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
|
|
637
|
+
margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
|
|
638
|
+
inset: ['top', 'right', 'bottom', 'left'],
|
|
639
|
+
borderWidth: [
|
|
640
|
+
'borderTopWidth',
|
|
641
|
+
'borderRightWidth',
|
|
642
|
+
'borderBottomWidth',
|
|
643
|
+
'borderLeftWidth',
|
|
644
|
+
],
|
|
645
|
+
borderColor: [
|
|
646
|
+
'borderTopColor',
|
|
647
|
+
'borderRightColor',
|
|
648
|
+
'borderBottomColor',
|
|
649
|
+
'borderLeftColor',
|
|
650
|
+
],
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
/** The two-value shorthands: the first value and the second, the second
|
|
654
|
+
* taken from the first when it is left out. `gap` is row then column, as
|
|
655
|
+
* CSS writes it; the logical pairs are start then end. */
|
|
656
|
+
const PAIRS = {
|
|
657
|
+
gap: ['rowGap', 'columnGap'],
|
|
658
|
+
paddingInline: ['paddingStart', 'paddingEnd'],
|
|
659
|
+
paddingBlock: ['paddingTop', 'paddingBottom'],
|
|
660
|
+
marginInline: ['marginStart', 'marginEnd'],
|
|
661
|
+
marginBlock: ['marginTop', 'marginBottom'],
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
/** `border` and its sides: which width and which colour each one sets. */
|
|
665
|
+
const LINES = {
|
|
666
|
+
border: ['borderWidth', 'borderColor'],
|
|
667
|
+
borderTop: ['borderTopWidth', 'borderTopColor'],
|
|
668
|
+
borderRight: ['borderRightWidth', 'borderRightColor'],
|
|
669
|
+
borderBottom: ['borderBottomWidth', 'borderBottomColor'],
|
|
670
|
+
borderLeft: ['borderLeftWidth', 'borderLeftColor'],
|
|
671
|
+
borderStart: ['borderStartWidth', 'borderStartColor'],
|
|
672
|
+
borderEnd: ['borderEndWidth', 'borderEndColor'],
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
/** The line styles that paint, and the ones that take the line away. */
|
|
676
|
+
export const LINE_STYLES = new Set(['solid', 'dashed']);
|
|
677
|
+
const NO_LINE = new Set(['none', 'hidden']);
|
|
678
|
+
const UNPAINTED_STYLES = new Set([
|
|
679
|
+
'dotted',
|
|
680
|
+
'double',
|
|
681
|
+
'groove',
|
|
682
|
+
'ridge',
|
|
683
|
+
'inset',
|
|
684
|
+
'outset',
|
|
685
|
+
]);
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Every shorthand, most specific first: when two write the same longhand,
|
|
689
|
+
* the first one wins (a longhand the style names beats them all). `border`
|
|
690
|
+
* leads because the style it names is the box's — `borderStyle` is one per
|
|
691
|
+
* box, and a side's own style only says something when `border` did not.
|
|
692
|
+
* Then the sides, which beat the four-side lists; then the pairs, which beat
|
|
693
|
+
* them too; then the lists; then the ones that share no longhand with
|
|
694
|
+
* anything.
|
|
695
|
+
*/
|
|
696
|
+
export const SHORTHAND_ORDER = [
|
|
697
|
+
'border',
|
|
698
|
+
'borderTop',
|
|
699
|
+
'borderRight',
|
|
700
|
+
'borderBottom',
|
|
701
|
+
'borderLeft',
|
|
702
|
+
'borderStart',
|
|
703
|
+
'borderEnd',
|
|
704
|
+
'paddingInline',
|
|
705
|
+
'paddingBlock',
|
|
706
|
+
'marginInline',
|
|
707
|
+
'marginBlock',
|
|
708
|
+
'padding',
|
|
709
|
+
'margin',
|
|
710
|
+
'inset',
|
|
711
|
+
'borderWidth',
|
|
712
|
+
'borderColor',
|
|
713
|
+
'gap',
|
|
714
|
+
'outline',
|
|
715
|
+
'background',
|
|
716
|
+
'flex',
|
|
717
|
+
];
|
|
718
|
+
|
|
719
|
+
/** The names that are only ever a shorthand — no yoga setter, no paint
|
|
720
|
+
* reads them. A style property in its own right (styles.js `STYLE_PROPS`). */
|
|
721
|
+
export const SHORTHAND_PROPS = new Set(
|
|
722
|
+
SHORTHAND_ORDER.filter(
|
|
723
|
+
(key) => !LENGTH_FORMS.has(key) && key !== 'borderColor',
|
|
724
|
+
),
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
/** …and the ones that are a longhand with one value and a shorthand with
|
|
728
|
+
* several: `padding: 8` is EDGE_ALL, `padding: '8px 16px'` is four sides. */
|
|
729
|
+
const SEVERAL_FORM = new Set([
|
|
730
|
+
'padding',
|
|
731
|
+
'margin',
|
|
732
|
+
'borderWidth',
|
|
733
|
+
'borderColor',
|
|
734
|
+
'gap',
|
|
735
|
+
]);
|
|
736
|
+
|
|
737
|
+
/** Which shorthands set this longhand, nearest first — so a `transition`
|
|
738
|
+
* written against the shorthand reaches the longhands it expands into
|
|
739
|
+
* (`transitionFor`). */
|
|
740
|
+
export const SHORTHANDS_OF = (() => {
|
|
741
|
+
const of = new Map();
|
|
742
|
+
const add = (longhand, shorthand) => {
|
|
743
|
+
if (!of.has(longhand)) of.set(longhand, []);
|
|
744
|
+
if (!of.get(longhand).includes(shorthand)) of.get(longhand).push(shorthand);
|
|
745
|
+
};
|
|
746
|
+
for (const key of SHORTHAND_ORDER) {
|
|
747
|
+
for (const longhand of longhandsOf(key)) add(longhand, key);
|
|
748
|
+
}
|
|
749
|
+
// `border` reaches a side's width and colour through the four-side
|
|
750
|
+
// longhands it sets, which a side falls back to
|
|
751
|
+
for (const [key, [width, color]] of Object.entries(LINES)) {
|
|
752
|
+
if (key === 'border') continue;
|
|
753
|
+
add(width, 'borderWidth');
|
|
754
|
+
add(width, 'border');
|
|
755
|
+
add(color, 'borderColor');
|
|
756
|
+
add(color, 'border');
|
|
757
|
+
}
|
|
758
|
+
return of;
|
|
759
|
+
})();
|
|
760
|
+
|
|
761
|
+
function longhandsOf(key) {
|
|
762
|
+
if (EDGE_LISTS[key]) return EDGE_LISTS[key];
|
|
763
|
+
if (PAIRS[key]) return PAIRS[key];
|
|
764
|
+
if (LINES[key]) return [...LINES[key], 'borderStyle'];
|
|
765
|
+
switch (key) {
|
|
766
|
+
case 'outline':
|
|
767
|
+
return ['outlineWidth', 'outlineColor'];
|
|
768
|
+
case 'background':
|
|
769
|
+
return ['backgroundColor', 'backgroundImage'];
|
|
770
|
+
case 'flex':
|
|
771
|
+
return ['flexGrow', 'flexShrink', 'flexBasis'];
|
|
772
|
+
default:
|
|
773
|
+
return [];
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Whether `style` holds anything the funnel has to rewrite: a shorthand, or a
|
|
779
|
+
* length written as something other than a number, a percentage or `'auto'`.
|
|
780
|
+
* The question `resolveComputedStyle` asks before allocating anything, so a
|
|
781
|
+
* style written in numbers — nearly every one — costs a walk of its keys.
|
|
782
|
+
*/
|
|
783
|
+
export function needsExpanding(style) {
|
|
784
|
+
for (const key in style) {
|
|
785
|
+
if (SHORTHAND_PROPS.has(key)) return true;
|
|
786
|
+
const value = style[key];
|
|
787
|
+
if (typeof value !== 'string') continue;
|
|
788
|
+
if (key === 'borderColor') {
|
|
789
|
+
if (isSeveral(value)) return true;
|
|
790
|
+
} else if (key === 'lineHeight') {
|
|
791
|
+
if (lineHeightNeedsReading(value)) return true;
|
|
792
|
+
} else if (LENGTH_FORMS.has(key) && needsReading(key, value)) return true;
|
|
793
|
+
}
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* The style with every shorthand expanded into its longhands and every
|
|
799
|
+
* length read into a number: what the layout, the paint and the display
|
|
800
|
+
* scale are handed. Never throws — this runs from a frame as often as from
|
|
801
|
+
* a commit. A length that does not read, alone or as a piece of a list, goes
|
|
802
|
+
* on as written for its longhand's reader to refuse, which is what reports
|
|
803
|
+
* it (`lengthOrAsWritten`); a line, a background or a `flex` that does not
|
|
804
|
+
* read is dropped whole, as CSS drops a declaration it cannot parse, since
|
|
805
|
+
* which of its parts was meant is not something to guess. Development has
|
|
806
|
+
* said so where the style was written either way.
|
|
807
|
+
*/
|
|
808
|
+
export function expandStyle(style, ctx = null) {
|
|
809
|
+
const out = {};
|
|
810
|
+
let found = null;
|
|
811
|
+
for (const key of Object.keys(style)) {
|
|
812
|
+
const value = style[key];
|
|
813
|
+
if (
|
|
814
|
+
SHORTHAND_PROPS.has(key) ||
|
|
815
|
+
(SEVERAL_FORM.has(key) && isSeveral(value))
|
|
816
|
+
) {
|
|
817
|
+
(found ??= new Set()).add(key);
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (LENGTH_FORMS.has(key) && needsReading(key, value)) {
|
|
821
|
+
out[key] = lengthOrAsWritten(key, value, ctx);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (key === 'lineHeight' && lineHeightNeedsReading(value)) {
|
|
825
|
+
// one that does not read goes on as written, and the text inherits
|
|
826
|
+
// past it, as it always did with a value it could not use
|
|
827
|
+
try {
|
|
828
|
+
out[key] = readLineHeight(value, ctx);
|
|
829
|
+
} catch {
|
|
830
|
+
out[key] = value;
|
|
831
|
+
}
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
out[key] = value;
|
|
835
|
+
}
|
|
836
|
+
if (found === null) return out;
|
|
837
|
+
let implied = null;
|
|
838
|
+
for (const key of SHORTHAND_ORDER) {
|
|
839
|
+
if (!found.has(key)) continue;
|
|
840
|
+
let longhands;
|
|
841
|
+
try {
|
|
842
|
+
longhands = readShorthand(key, style[key], ctx, false);
|
|
843
|
+
} catch {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const colorKey = impliedColorKey(key, longhands);
|
|
847
|
+
if (colorKey !== null) (implied ??= []).push(colorKey);
|
|
848
|
+
for (const longhand of Object.keys(longhands)) {
|
|
849
|
+
const value = longhands[longhand];
|
|
850
|
+
if (value !== undefined && out[longhand] == null) out[longhand] = value;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
// A line that named no colour is drawn in `currentColor` — after every
|
|
854
|
+
// other shorthand has had its say, since it is the weakest thing in the
|
|
855
|
+
// style: a colour named for that side anywhere, a side falling back to
|
|
856
|
+
// `borderColor` included, is the one the line takes.
|
|
857
|
+
for (const colorKey of implied ?? []) {
|
|
858
|
+
if (out[colorKey] != null) continue;
|
|
859
|
+
if (colorKey !== 'outlineColor' && out.borderColor != null) continue;
|
|
860
|
+
out[colorKey] = CURRENT_COLOR;
|
|
861
|
+
}
|
|
862
|
+
return out;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* The colour key a line left to `currentColor`, or null: a border, a side
|
|
867
|
+
* or an outline that drew a line and named no colour for it. `none`, or a
|
|
868
|
+
* width of 0, draws no line and needs none.
|
|
869
|
+
*/
|
|
870
|
+
function impliedColorKey(key, longhands) {
|
|
871
|
+
const [widthKey, colorKey] =
|
|
872
|
+
key === 'outline' ? ['outlineWidth', 'outlineColor'] : (LINES[key] ?? []);
|
|
873
|
+
if (colorKey === undefined || !(colorKey in longhands)) return null;
|
|
874
|
+
if (longhands[colorKey] !== undefined || longhands[widthKey] === 0) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
return colorKey;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* One shorthand's longhands, or a throw naming what did not read. A
|
|
882
|
+
* longhand the value left out is absent from the result.
|
|
883
|
+
*
|
|
884
|
+
* `lenient` is development's reading of a value whose `$token`s have not
|
|
885
|
+
* resolved yet: a token stands for one value of whatever kind the slot
|
|
886
|
+
* wants, and nothing is returned for it.
|
|
887
|
+
*/
|
|
888
|
+
export function expandShorthand(key, value, ctx = null, lenient = false) {
|
|
889
|
+
const longhands = readShorthand(key, value, ctx, lenient);
|
|
890
|
+
// a token may be the colour a lenient reading skipped, so only a value
|
|
891
|
+
// read whole says a line has none
|
|
892
|
+
const colorKey = lenient ? null : impliedColorKey(key, longhands);
|
|
893
|
+
if (colorKey !== null) longhands[colorKey] = CURRENT_COLOR;
|
|
894
|
+
return longhands;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** …as written: a line's colour left out is left out, for the funnel to
|
|
898
|
+
* weigh against the rest of the style (`expandStyle`). */
|
|
899
|
+
function readShorthand(key, value, ctx, lenient) {
|
|
900
|
+
if (value == null || value === '') return {};
|
|
901
|
+
if (EDGE_LISTS[key]) return readEdges(key, value, ctx, lenient);
|
|
902
|
+
if (PAIRS[key]) return readPair(key, value, ctx, lenient);
|
|
903
|
+
if (LINES[key]) return readLine(key, value, ctx, lenient);
|
|
904
|
+
switch (key) {
|
|
905
|
+
case 'outline':
|
|
906
|
+
return readOutline(value, ctx, lenient);
|
|
907
|
+
case 'background':
|
|
908
|
+
return readBackground(value, lenient);
|
|
909
|
+
case 'flex':
|
|
910
|
+
return readFlex(value, ctx, lenient);
|
|
911
|
+
default:
|
|
912
|
+
throw new Error(`react-x11: ${key} is not a shorthand`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** Development's check of a shorthand as written: the same reading as the
|
|
917
|
+
* funnel's, tokens standing in for whatever they will resolve to. */
|
|
918
|
+
export function checkShorthand(key, value) {
|
|
919
|
+
expandShorthand(key, value, PROBE, true);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** Whether `key` is read by `checkShorthand` for this value — every
|
|
923
|
+
* shorthand, and a several-value longhand written with several. */
|
|
924
|
+
export const isShorthandValue = (key, value) =>
|
|
925
|
+
SHORTHAND_PROPS.has(key) || (SEVERAL_FORM.has(key) && isSeveral(value));
|
|
926
|
+
|
|
927
|
+
/** One piece of an edge list or a pair: a length of the longhand's kind, or
|
|
928
|
+
* for `borderColor`, a colour. */
|
|
929
|
+
function readPiece(shorthand, longhand, piece, ctx, lenient) {
|
|
930
|
+
if (lenient && mentionsToken(piece)) return undefined;
|
|
931
|
+
if (shorthand === 'borderColor') {
|
|
932
|
+
if (isColor(piece) || !lenient) return piece;
|
|
933
|
+
throw new StyleValueError(
|
|
934
|
+
'borderColor',
|
|
935
|
+
piece,
|
|
936
|
+
null,
|
|
937
|
+
"a CSS colour per side — 'red', '#2980b9 transparent', or a $token",
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
return readLength(longhand, piece, undefined, ctx);
|
|
942
|
+
} catch (err) {
|
|
943
|
+
// In the funnel the piece goes on as written — one a `$token` resolved
|
|
944
|
+
// to — for the longhand's own reader to refuse, which is what reports
|
|
945
|
+
// it; in development it is named after the property that was written.
|
|
946
|
+
if (!lenient) return piece;
|
|
947
|
+
throw err instanceof StyleValueError ? err.as(shorthand) : err;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function readEdges(key, value, ctx, lenient) {
|
|
952
|
+
const sides = EDGE_LISTS[key];
|
|
953
|
+
const list = typeof value === 'number' ? [value] : pieces(value);
|
|
954
|
+
if (list.length > 4) {
|
|
955
|
+
throw new StyleValueError(
|
|
956
|
+
key,
|
|
957
|
+
value,
|
|
958
|
+
`${list.length} values, where CSS takes one to four — top, right, ` +
|
|
959
|
+
'bottom, left',
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const read = list.map((piece, i) =>
|
|
963
|
+
readPiece(key, sides[i] ?? sides[0], piece, ctx, lenient),
|
|
964
|
+
);
|
|
965
|
+
// CSS's rule: a missing side is the one opposite it
|
|
966
|
+
const [top, right = top, bottom = top, left = right] = read;
|
|
967
|
+
return {
|
|
968
|
+
[sides[0]]: top,
|
|
969
|
+
[sides[1]]: right,
|
|
970
|
+
[sides[2]]: bottom,
|
|
971
|
+
[sides[3]]: left,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function readPair(key, value, ctx, lenient) {
|
|
976
|
+
const [first, second] = PAIRS[key];
|
|
977
|
+
const list = typeof value === 'number' ? [value] : pieces(value);
|
|
978
|
+
if (list.length > 2) {
|
|
979
|
+
throw new StyleValueError(
|
|
980
|
+
key,
|
|
981
|
+
value,
|
|
982
|
+
`${list.length} values, where it takes one or two — ${first}, then ` +
|
|
983
|
+
second,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
const a = readPiece(key, first, list[0], ctx, lenient);
|
|
987
|
+
const b = list.length > 1 ? readPiece(key, second, list[1], ctx, lenient) : a;
|
|
988
|
+
return { [first]: a, [second]: b };
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* A line — `border`, a side, an `outline` — as its width, its style and its
|
|
993
|
+
* colour, each at most once and in any order: `'1px solid $border'`,
|
|
994
|
+
* `'dashed red 2px'`, `'none'`. A number is the width.
|
|
995
|
+
*/
|
|
996
|
+
function readParts(key, value, ctx, lenient) {
|
|
997
|
+
const parts = {
|
|
998
|
+
width: undefined,
|
|
999
|
+
style: undefined,
|
|
1000
|
+
color: undefined,
|
|
1001
|
+
none: false,
|
|
1002
|
+
};
|
|
1003
|
+
const bad = (why) =>
|
|
1004
|
+
new StyleValueError(
|
|
1005
|
+
key,
|
|
1006
|
+
value,
|
|
1007
|
+
why,
|
|
1008
|
+
"a width, a style and a colour in any order — '1px solid #d0d0d0', " +
|
|
1009
|
+
"'2px dashed $accent' — or 'none'",
|
|
1010
|
+
);
|
|
1011
|
+
if (typeof value === 'number') {
|
|
1012
|
+
parts.width = readLength(`${key} width`, value, WIDTH_WORDS, ctx);
|
|
1013
|
+
if (parts.width < 0) throw bad('a width cannot be negative');
|
|
1014
|
+
return parts;
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof value !== 'string') throw bad('it is not a line');
|
|
1017
|
+
const list = pieces(value);
|
|
1018
|
+
if (list.length > 3)
|
|
1019
|
+
throw bad(`${list.length} values, where it takes three at most`);
|
|
1020
|
+
for (const piece of list) {
|
|
1021
|
+
const word = piece.toLowerCase();
|
|
1022
|
+
if (lenient && mentionsToken(piece)) continue;
|
|
1023
|
+
if (
|
|
1024
|
+
NO_LINE.has(word) ||
|
|
1025
|
+
LINE_STYLES.has(word) ||
|
|
1026
|
+
UNPAINTED_STYLES.has(word)
|
|
1027
|
+
) {
|
|
1028
|
+
if (parts.style !== undefined || parts.none)
|
|
1029
|
+
throw bad('it names two styles');
|
|
1030
|
+
if (NO_LINE.has(word)) parts.none = true;
|
|
1031
|
+
else if (UNPAINTED_STYLES.has(word)) {
|
|
1032
|
+
throw bad(`${word} lines are not drawn here — solid and dashed are`);
|
|
1033
|
+
} else parts.style = word;
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
if (
|
|
1037
|
+
Object.hasOwn(WIDTHS, word) ||
|
|
1038
|
+
NUMBER_AND_UNIT.test(piece) ||
|
|
1039
|
+
FUNCTION.test(piece)
|
|
1040
|
+
) {
|
|
1041
|
+
if (parts.width !== undefined) throw bad('it names two widths');
|
|
1042
|
+
parts.width = readLength(`${key} width`, piece, WIDTH_WORDS, ctx);
|
|
1043
|
+
if (parts.width < 0) throw bad('a width cannot be negative');
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (isColor(piece)) {
|
|
1047
|
+
if (parts.color !== undefined) throw bad('it names two colours');
|
|
1048
|
+
parts.color = piece;
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
throw bad(`"${piece}" is neither a width, a line style nor a colour`);
|
|
1052
|
+
}
|
|
1053
|
+
return parts;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function readLine(key, value, ctx, lenient) {
|
|
1057
|
+
const [widthKey, colorKey] = LINES[key];
|
|
1058
|
+
const parts = readParts(key, value, ctx, lenient);
|
|
1059
|
+
if (parts.none) return { [widthKey]: 0 };
|
|
1060
|
+
return {
|
|
1061
|
+
[widthKey]: parts.width ?? MEDIUM,
|
|
1062
|
+
[colorKey]: parts.color,
|
|
1063
|
+
borderStyle: parts.style,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function readOutline(value, ctx, lenient) {
|
|
1068
|
+
const parts = readParts('outline', value, ctx, lenient);
|
|
1069
|
+
if (parts.none) return { outlineWidth: 0 };
|
|
1070
|
+
if (parts.style === 'dashed') {
|
|
1071
|
+
throw new StyleValueError(
|
|
1072
|
+
'outline',
|
|
1073
|
+
value,
|
|
1074
|
+
"an outline is drawn solid — leave the style out, or write 'solid'",
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
return { outlineWidth: parts.width ?? MEDIUM, outlineColor: parts.color };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* `background`: a colour, a gradient, or both — the two things a box paints
|
|
1082
|
+
* behind itself. Everything else CSS's `background` says (a `url()`, a size,
|
|
1083
|
+
* a repeat, a second layer) is something a box does not paint, and says so.
|
|
1084
|
+
*/
|
|
1085
|
+
function readBackground(value, lenient) {
|
|
1086
|
+
const bad = (why) =>
|
|
1087
|
+
new StyleValueError(
|
|
1088
|
+
'background',
|
|
1089
|
+
value,
|
|
1090
|
+
why,
|
|
1091
|
+
"a colour, a linear-gradient(…), both, or 'none'",
|
|
1092
|
+
);
|
|
1093
|
+
if (typeof value !== 'string') throw bad('it is not a colour');
|
|
1094
|
+
if (splitTop(value, true).length > 1) {
|
|
1095
|
+
throw bad('a box paints one layer — one gradient over one colour');
|
|
1096
|
+
}
|
|
1097
|
+
let color;
|
|
1098
|
+
let image;
|
|
1099
|
+
for (const piece of pieces(value)) {
|
|
1100
|
+
if (lenient && isToken(piece)) continue;
|
|
1101
|
+
const word = piece.toLowerCase();
|
|
1102
|
+
if (word === 'none') {
|
|
1103
|
+
if (image !== undefined) throw bad('it names two images');
|
|
1104
|
+
image = 'none';
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
if (/^[a-z-]*gradient\(/.test(word)) {
|
|
1108
|
+
if (image !== undefined) throw bad('it names two images');
|
|
1109
|
+
// Its own parser, and its own message — a radial gradient is a
|
|
1110
|
+
// `<canvas onDraw>`, a stop that is not a colour names the stop. In
|
|
1111
|
+
// development only: the paint parses what it is handed, and drops a
|
|
1112
|
+
// gradient that does not parse, so the funnel need not do it twice.
|
|
1113
|
+
if (lenient && !mentionsToken(piece)) parseLinearGradient(piece);
|
|
1114
|
+
image = piece;
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (word.startsWith('url(')) {
|
|
1118
|
+
throw bad('an image is an <image src> element here, not a background');
|
|
1119
|
+
}
|
|
1120
|
+
if (isColor(piece)) {
|
|
1121
|
+
if (color !== undefined) throw bad('it names two colours');
|
|
1122
|
+
color = piece;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
throw bad(`"${piece}" is neither a colour nor a gradient`);
|
|
1126
|
+
}
|
|
1127
|
+
// `none` takes the colour away with the image, as it does in CSS — the
|
|
1128
|
+
// way a `:hover` block says "no fill here"
|
|
1129
|
+
if (image === 'none' && color === undefined) color = 'transparent';
|
|
1130
|
+
return { backgroundColor: color, backgroundImage: image };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const FLEX_KEYWORDS = {
|
|
1134
|
+
none: { flexGrow: 0, flexShrink: 0, flexBasis: 'auto' },
|
|
1135
|
+
auto: { flexGrow: 1, flexShrink: 1, flexBasis: 'auto' },
|
|
1136
|
+
initial: { flexGrow: 0, flexShrink: 1, flexBasis: 'auto' },
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
const FACTOR = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* CSS's `flex`. A number is `flex: <grow> 1 0` — "take this share of what is
|
|
1143
|
+
* left, from a base size of nothing" — which is what `flex: 1` means
|
|
1144
|
+
* everywhere it is written; the keywords are CSS's own. The rest is CSS's
|
|
1145
|
+
* grammar: one or two unitless factors (grow, then shrink) and a basis that
|
|
1146
|
+
* is a length or `auto`, before them or after: `'1 1 0'`, `'2 200px'`,
|
|
1147
|
+
* `'0 0 auto'`, `'240px'`.
|
|
1148
|
+
*/
|
|
1149
|
+
function readFlex(value, ctx, lenient) {
|
|
1150
|
+
const bad = (why) =>
|
|
1151
|
+
new StyleValueError(
|
|
1152
|
+
'flex',
|
|
1153
|
+
value,
|
|
1154
|
+
why,
|
|
1155
|
+
'a number — flex: 1 is flexGrow: 1, flexShrink: 1, flexBasis: 0 — ' +
|
|
1156
|
+
"'auto', 'none', or CSS's '<grow> <shrink> <basis>', like '1 1 0' " +
|
|
1157
|
+
"or '0 0 240px'",
|
|
1158
|
+
);
|
|
1159
|
+
if (typeof value === 'number') {
|
|
1160
|
+
if (!(Number.isFinite(value) && value >= 0)) {
|
|
1161
|
+
throw bad('a grow factor cannot be negative');
|
|
1162
|
+
}
|
|
1163
|
+
return { flexGrow: value, flexShrink: 1, flexBasis: 0 };
|
|
1164
|
+
}
|
|
1165
|
+
if (typeof value !== 'string') throw bad('it is not a number');
|
|
1166
|
+
const text = value.trim();
|
|
1167
|
+
if (Object.hasOwn(FLEX_KEYWORDS, text)) return { ...FLEX_KEYWORDS[text] };
|
|
1168
|
+
const list = pieces(text);
|
|
1169
|
+
if (list.length > 3)
|
|
1170
|
+
throw bad(`${list.length} values, where it takes three at most`);
|
|
1171
|
+
const factors = [];
|
|
1172
|
+
let basis;
|
|
1173
|
+
let basisAt = -1;
|
|
1174
|
+
for (let i = 0; i < list.length; i++) {
|
|
1175
|
+
const piece = list[i];
|
|
1176
|
+
if (lenient && mentionsToken(piece)) {
|
|
1177
|
+
factors.push(undefined);
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
// a unitless zero is a factor unless both factors came before it, which
|
|
1181
|
+
// is the one place CSS reads it as the basis
|
|
1182
|
+
if (FACTOR.test(piece) && !(factors.length === 2 && basisAt === -1)) {
|
|
1183
|
+
const n = Number(piece);
|
|
1184
|
+
if (n < 0) throw bad('a factor cannot be negative');
|
|
1185
|
+
factors.push(n);
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
if (basisAt !== -1) throw bad('it names two bases');
|
|
1189
|
+
// a word that is no length at all is a flex value nobody wrote — `'fill'`
|
|
1190
|
+
// — and is said of `flex`; a length in a unit the grammar does not read
|
|
1191
|
+
// is said of the basis, naming the unit
|
|
1192
|
+
if (
|
|
1193
|
+
piece !== 'auto' &&
|
|
1194
|
+
!NUMBER_AND_UNIT.test(piece) &&
|
|
1195
|
+
!FUNCTION.test(piece)
|
|
1196
|
+
) {
|
|
1197
|
+
throw bad(`"${piece}" is neither a factor nor a basis`);
|
|
1198
|
+
}
|
|
1199
|
+
basis = readLength('flex basis', piece, LENGTH_FORMS.get('flexBasis'), ctx);
|
|
1200
|
+
basisAt = i;
|
|
1201
|
+
}
|
|
1202
|
+
if (factors.length > 2) throw bad('it names three factors');
|
|
1203
|
+
// a basis between the two factors is not CSS's grammar
|
|
1204
|
+
if (basisAt === 1 && factors.length === 2)
|
|
1205
|
+
throw bad('the basis splits the factors');
|
|
1206
|
+
const [grow, shrink] = factors;
|
|
1207
|
+
return {
|
|
1208
|
+
flexGrow: grow ?? (lenient ? undefined : 1),
|
|
1209
|
+
flexShrink: shrink ?? (lenient && factors.length > 1 ? undefined : 1),
|
|
1210
|
+
// CSS: a basis left out of a shorthand that names a factor is 0
|
|
1211
|
+
flexBasis: basis ?? (factors.length > 0 ? 0 : undefined),
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// ---------------------------------------------------------------------------
|
|
1216
|
+
// Whole-style checks: what no single value can be wrong about on its own.
|
|
1217
|
+
|
|
1218
|
+
/** Every block of a style a value can sit in: the style itself, and its
|
|
1219
|
+
* state and query blocks, one level down. */
|
|
1220
|
+
function* blocksOf(style) {
|
|
1221
|
+
yield style;
|
|
1222
|
+
for (const key of Object.keys(style)) {
|
|
1223
|
+
const c = key.charCodeAt(0);
|
|
1224
|
+
if ((c === 58 /* : */ || c === 64) /* @ */ && style[key]) yield style[key];
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* What development says about a style's borders taken together, or null: a
|
|
1230
|
+
* box has one `borderStyle`, so two shorthands naming different ones cannot
|
|
1231
|
+
* both be drawn.
|
|
1232
|
+
*/
|
|
1233
|
+
export function borderProblem(style) {
|
|
1234
|
+
for (const block of blocksOf(style)) {
|
|
1235
|
+
// one block is one box's style at one moment; two blocks are two
|
|
1236
|
+
// moments, which may draw the line two ways
|
|
1237
|
+
let styleKey = null;
|
|
1238
|
+
let lineStyle = null;
|
|
1239
|
+
for (const key of Object.keys(LINES)) {
|
|
1240
|
+
const value = block[key];
|
|
1241
|
+
if (typeof value !== 'string') continue;
|
|
1242
|
+
let parts;
|
|
1243
|
+
try {
|
|
1244
|
+
parts = readParts(key, value, PROBE, true);
|
|
1245
|
+
} catch {
|
|
1246
|
+
continue; // its own check names it
|
|
1247
|
+
}
|
|
1248
|
+
if (parts.none || parts.style === undefined) continue;
|
|
1249
|
+
if (
|
|
1250
|
+
lineStyle !== null &&
|
|
1251
|
+
parts.style !== lineStyle &&
|
|
1252
|
+
block.borderStyle == null
|
|
1253
|
+
) {
|
|
1254
|
+
return (
|
|
1255
|
+
`react-x11: ${styleKey} and ${key} name different line styles ` +
|
|
1256
|
+
`("${lineStyle}" and "${parts.style}"), and a box has one ` +
|
|
1257
|
+
'borderStyle — say which with borderStyle, or draw the side that ' +
|
|
1258
|
+
'differs as a box of its own'
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
styleKey ??= key;
|
|
1262
|
+
lineStyle ??= parts.style;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|