@react-x11/components 0.12.0 → 0.14.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.
Files changed (150) hide show
  1. package/dist/html/controls.d.ts +41 -5
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +85 -57
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +134 -20
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +545 -126
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +18 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +27 -0
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/parse.d.ts +47 -6
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +336 -30
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/style.d.ts +142 -15
  18. package/dist/html/css/style.d.ts.map +1 -1
  19. package/dist/html/css/style.js +563 -147
  20. package/dist/html/css/style.js.map +1 -1
  21. package/dist/html/css/transform.d.ts +85 -0
  22. package/dist/html/css/transform.d.ts.map +1 -0
  23. package/dist/html/css/transform.js +425 -0
  24. package/dist/html/css/transform.js.map +1 -0
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -9
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +4 -0
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js.map +1 -1
  32. package/dist/html/dom.d.ts.map +1 -1
  33. package/dist/html/dom.js +3 -1
  34. package/dist/html/dom.js.map +1 -1
  35. package/dist/html/fonts.d.ts +78 -4
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +373 -19
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/form.d.ts +200 -0
  40. package/dist/html/form.d.ts.map +1 -0
  41. package/dist/html/form.js +801 -0
  42. package/dist/html/form.js.map +1 -0
  43. package/dist/html/index.d.ts +12 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +30 -301
  46. package/dist/html/index.js.map +1 -1
  47. package/dist/html/layout/axes.d.ts +14 -0
  48. package/dist/html/layout/axes.d.ts.map +1 -0
  49. package/dist/html/layout/axes.js +28 -0
  50. package/dist/html/layout/axes.js.map +1 -0
  51. package/dist/html/layout/block.d.ts +93 -12
  52. package/dist/html/layout/block.d.ts.map +1 -1
  53. package/dist/html/layout/block.js +393 -39
  54. package/dist/html/layout/block.js.map +1 -1
  55. package/dist/html/layout/boxes.d.ts +90 -2
  56. package/dist/html/layout/boxes.d.ts.map +1 -1
  57. package/dist/html/layout/boxes.js +74 -10
  58. package/dist/html/layout/boxes.js.map +1 -1
  59. package/dist/html/layout/cache.d.ts +1 -1
  60. package/dist/html/layout/cache.d.ts.map +1 -1
  61. package/dist/html/layout/cache.js +6 -0
  62. package/dist/html/layout/cache.js.map +1 -1
  63. package/dist/html/layout/css-grid.d.ts.map +1 -1
  64. package/dist/html/layout/css-grid.js +90 -18
  65. package/dist/html/layout/css-grid.js.map +1 -1
  66. package/dist/html/layout/flex.d.ts.map +1 -1
  67. package/dist/html/layout/flex.js +574 -84
  68. package/dist/html/layout/flex.js.map +1 -1
  69. package/dist/html/layout/inline.d.ts +3 -0
  70. package/dist/html/layout/inline.d.ts.map +1 -1
  71. package/dist/html/layout/inline.js +154 -54
  72. package/dist/html/layout/inline.js.map +1 -1
  73. package/dist/html/layout/multicol.d.ts +42 -0
  74. package/dist/html/layout/multicol.d.ts.map +1 -0
  75. package/dist/html/layout/multicol.js +897 -0
  76. package/dist/html/layout/multicol.js.map +1 -0
  77. package/dist/html/layout/shaping.d.ts +7 -0
  78. package/dist/html/layout/shaping.d.ts.map +1 -1
  79. package/dist/html/layout/shaping.js +25 -20
  80. package/dist/html/layout/shaping.js.map +1 -1
  81. package/dist/html/layout/table.d.ts.map +1 -1
  82. package/dist/html/layout/table.js +62 -18
  83. package/dist/html/layout/table.js.map +1 -1
  84. package/dist/html/node.d.ts +90 -15
  85. package/dist/html/node.d.ts.map +1 -1
  86. package/dist/html/node.js +771 -186
  87. package/dist/html/node.js.map +1 -1
  88. package/dist/html/paint.d.ts +101 -5
  89. package/dist/html/paint.d.ts.map +1 -1
  90. package/dist/html/paint.js +1502 -160
  91. package/dist/html/paint.js.map +1 -1
  92. package/dist/html/svg.d.ts +15 -2
  93. package/dist/html/svg.d.ts.map +1 -1
  94. package/dist/html/svg.js +249 -17
  95. package/dist/html/svg.js.map +1 -1
  96. package/dist/html/widgets.d.ts +29 -0
  97. package/dist/html/widgets.d.ts.map +1 -0
  98. package/dist/html/widgets.js +700 -0
  99. package/dist/html/widgets.js.map +1 -0
  100. package/dist/html/woff2.d.ts +14 -0
  101. package/dist/html/woff2.d.ts.map +1 -0
  102. package/dist/html/woff2.js +608 -0
  103. package/dist/html/woff2.js.map +1 -0
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/maps/controller.d.ts +17 -1
  108. package/dist/maps/controller.d.ts.map +1 -1
  109. package/dist/maps/controller.js +27 -7
  110. package/dist/maps/controller.js.map +1 -1
  111. package/dist/richtext/node.d.ts +8 -1
  112. package/dist/richtext/node.d.ts.map +1 -1
  113. package/dist/richtext/node.js +5 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/richtext/runs.d.ts +25 -6
  116. package/dist/richtext/runs.d.ts.map +1 -1
  117. package/dist/richtext/runs.js +240 -24
  118. package/dist/richtext/runs.js.map +1 -1
  119. package/package.json +2 -2
  120. package/src/html/controls.ts +114 -51
  121. package/src/html/css/cascade.ts +614 -140
  122. package/src/html/css/color.ts +29 -0
  123. package/src/html/css/parse.ts +388 -33
  124. package/src/html/css/style.ts +688 -150
  125. package/src/html/css/transform.ts +459 -0
  126. package/src/html/css/ua.ts +77 -10
  127. package/src/html/css/values.ts +4 -0
  128. package/src/html/dom.ts +3 -1
  129. package/src/html/fonts.ts +417 -24
  130. package/src/html/form.ts +962 -0
  131. package/src/html/index.ts +49 -338
  132. package/src/html/layout/axes.ts +62 -0
  133. package/src/html/layout/block.ts +447 -37
  134. package/src/html/layout/boxes.ts +143 -14
  135. package/src/html/layout/cache.ts +6 -0
  136. package/src/html/layout/css-grid.ts +95 -22
  137. package/src/html/layout/flex.ts +675 -78
  138. package/src/html/layout/inline.ts +159 -55
  139. package/src/html/layout/multicol.ts +977 -0
  140. package/src/html/layout/shaping.ts +27 -18
  141. package/src/html/layout/table.ts +62 -19
  142. package/src/html/node.ts +798 -172
  143. package/src/html/paint.ts +1742 -241
  144. package/src/html/svg.ts +277 -19
  145. package/src/html/widgets.ts +821 -0
  146. package/src/html/woff2.ts +612 -0
  147. package/src/index.ts +1 -0
  148. package/src/maps/controller.ts +28 -7
  149. package/src/richtext/node.ts +13 -2
  150. package/src/richtext/runs.ts +313 -35
@@ -40,6 +40,8 @@ import {
40
40
  } from './content.js';
41
41
  import type { ContentItem, CounterChange } from './content.js';
42
42
  import type { CustomProps } from './vars.js';
43
+ import { parseRotate, parseScale, parseTransform } from './transform.js';
44
+ import type { TransformFunction } from './transform.js';
43
45
  import { LIGHT_DARK, lightDark, usedColorScheme } from './color.js';
44
46
 
45
47
  export type Display =
@@ -108,15 +110,43 @@ export interface BoxShadow {
108
110
  inset: boolean;
109
111
  }
110
112
 
113
+ /** A gradient's colour stop: its colour, at a position along the gradient
114
+ * or, with none, where its neighbours put it. */
115
+ export interface GradientStop {
116
+ color: string;
117
+ at: Len | null;
118
+ }
119
+
111
120
  /** A `linear-gradient()` (CSS Images 3, 3.1): its direction, an angle
112
121
  * clockwise from up or a corner the box's shape turns into one, and its
113
- * colour stops, each at a position or where its neighbours put it. */
122
+ * colour stops. */
114
123
  export interface LinearGradient {
124
+ kind: 'linear';
115
125
  angle: number;
116
126
  corner: 'top left' | 'top right' | 'bottom left' | 'bottom right' | null;
117
- stops: { color: string; at: Len | null }[];
127
+ stops: GradientStop[];
118
128
  }
119
129
 
130
+ /** A `radial-gradient()` (CSS Images 3, 3.2): its ending shape, a circle or
131
+ * an ellipse; that shape's size, a keyword for the side or the corner of
132
+ * the box it reaches or the radii it was given, one for a circle and two
133
+ * for an ellipse; its centre, a position in the box; and its colour stops,
134
+ * along the ray from the centre to the shape's edge. */
135
+ export interface RadialGradient {
136
+ kind: 'radial';
137
+ circle: boolean;
138
+ extent: RadialExtent | null;
139
+ radii: [Len, Len] | null;
140
+ at: [Len, Len];
141
+ stops: GradientStop[];
142
+ }
143
+
144
+ export type RadialExtent =
145
+ 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner';
146
+
147
+ /** An image a gradient function draws. */
148
+ export type Gradient = LinearGradient | RadialGradient;
149
+
120
150
  /** Where a grid item starts or ends (CSS Grid 1, 8.3): a line, counted
121
151
  * among the lines of a name where it has one; a span, to a line of a name
122
152
  * where it has one; a name alone, an area's edge or a line's; or auto. */
@@ -174,6 +204,121 @@ function parseClip(
174
204
  return { top: edges[0], right: edges[1], bottom: edges[2], left: edges[3] };
175
205
  }
176
206
 
207
+ /** The box a `clip-path`'s shape is measured in, a `<geometry-box>` (CSS
208
+ * Masking 1, 5.1). */
209
+ export type ClipBox =
210
+ 'margin-box' | 'border-box' | 'padding-box' | 'content-box';
211
+
212
+ /**
213
+ * `clip-path` (CSS Masking 1, 5.1): a rectangle in a box of the element's,
214
+ * its corners rounded or not — a `<basic-shape-rect>` (CSS Shapes 1, 3.1),
215
+ * kept as the function it was written as, since each measures its four
216
+ * lengths its own way and a percentage among them has no pixels before
217
+ * layout. The element shows through it, and everything in it does.
218
+ */
219
+ export interface ClipPath {
220
+ box: ClipBox;
221
+ /** What the four lengths are: `inset()`'s, how far in from the box's
222
+ * top, right, bottom and left; `rect()`'s, those four edges from its top
223
+ * and its left, `auto` the box's own; `xywh()`'s, the rectangle's left
224
+ * and top from the box's, and its width and height. Null for a box
225
+ * named alone, which is the shape: the whole of it, rounded as the
226
+ * element's corners are. */
227
+ shape: 'inset' | 'rect' | 'xywh' | null;
228
+ lengths: readonly [Len, Len, Len, Len];
229
+ /** `round`: the corners' radii across, and down where they differ, as
230
+ * `border-radius` has them — a percentage is of the box, and not of the
231
+ * rectangle in it. Null for square corners. */
232
+ radii: readonly [Len, Len, Len, Len] | null;
233
+ radiiY: readonly [Len, Len, Len, Len] | null;
234
+ }
235
+
236
+ /** The boxes `clip-path` names; an SVG one is the CSS box that stands for
237
+ * it on an element with a CSS layout box (5.1). */
238
+ const CLIP_BOXES: Record<string, ClipBox> = {
239
+ 'margin-box': 'margin-box',
240
+ 'border-box': 'border-box',
241
+ 'padding-box': 'padding-box',
242
+ 'content-box': 'content-box',
243
+ 'fill-box': 'content-box',
244
+ 'stroke-box': 'border-box',
245
+ 'view-box': 'border-box',
246
+ };
247
+
248
+ /** The shapes that are no rectangle, and a reference to a `<clipPath>`
249
+ * element: a value, which sets the property, and none this draws. */
250
+ const UNDRAWN_CLIPS = /^(circle|ellipse|polygon|path|shape|url)\(/i;
251
+
252
+ const NO_LENGTHS = [0, 0, 0, 0] as const;
253
+
254
+ /**
255
+ * `none`; a rectangle's function, a box, or the two in either order; or a
256
+ * shape that is no rectangle, which is a value and clips nothing here.
257
+ * Undefined for anything else, which leaves the value as it was.
258
+ */
259
+ function parseClipPath(
260
+ value: string,
261
+ ctx: UnitContext,
262
+ ): ClipPath | null | undefined {
263
+ const v = value.trim();
264
+ if (/^none$/i.test(v)) return null;
265
+ let box: ClipBox | null = null;
266
+ let shape: Omit<ClipPath, 'box'> | null | undefined;
267
+ for (const part of splitValue(v)) {
268
+ const named = CLIP_BOXES[part.toLowerCase()];
269
+ if (named) {
270
+ if (box) return undefined;
271
+ box = named;
272
+ continue;
273
+ }
274
+ if (shape !== undefined) return undefined;
275
+ if (UNDRAWN_CLIPS.test(part) && part.endsWith(')')) shape = null;
276
+ else shape = rectShape(part, ctx);
277
+ if (shape === undefined) return undefined;
278
+ }
279
+ if (shape === null) return null;
280
+ if (shape) return { box: box ?? 'border-box', ...shape };
281
+ if (!box) return undefined;
282
+ return { box, shape: null, lengths: NO_LENGTHS, radii: null, radiiY: null };
283
+ }
284
+
285
+ /** `inset()`, `rect()` or `xywh()`, with the corners it rounds; undefined
286
+ * for anything else. */
287
+ function rectShape(
288
+ text: string,
289
+ ctx: UnitContext,
290
+ ): Omit<ClipPath, 'box'> | undefined {
291
+ const m = /^(inset|rect|xywh)\((.*)\)$/is.exec(text);
292
+ if (!m) return undefined;
293
+ const shape = m[1].toLowerCase() as 'inset' | 'rect' | 'xywh';
294
+ const parts = splitValue(m[2].trim());
295
+ const round = parts.findIndex((p) => p.toLowerCase() === 'round');
296
+ const sides = round < 0 ? parts : parts.slice(0, round);
297
+ let radii: ClipPath['radii'] = null;
298
+ let radiiY: ClipPath['radiiY'] = null;
299
+ if (round >= 0) {
300
+ const corners = parseRadii(parts.slice(round + 1).join(' '), ctx);
301
+ if (!corners) return undefined;
302
+ [radii, radiiY] = corners;
303
+ }
304
+ // one to four insets, as a margin's sides are written; the other two
305
+ // take all four
306
+ if (
307
+ shape === 'inset' ? !sides.length || sides.length > 4 : sides.length !== 4
308
+ )
309
+ return undefined;
310
+ const lengths: Len[] = [];
311
+ for (let i = 0; i < sides.length; i += 1) {
312
+ const len = parseLength(sides[i], ctx);
313
+ if (len === null) return undefined;
314
+ // only `rect()` has an edge that is `auto`, and a width is not negative
315
+ if (len === AUTO && shape !== 'rect') return undefined;
316
+ if (shape === 'xywh' && i > 1 && !notNegative(len)) return undefined;
317
+ lengths.push(len);
318
+ }
319
+ return { shape, lengths: fourSides(lengths), radii, radiiY };
320
+ }
321
+
177
322
  export type BorderStyle =
178
323
  | 'none'
179
324
  | 'hidden'
@@ -187,7 +332,7 @@ export type BorderStyle =
187
332
  | 'outset';
188
333
 
189
334
  /** A background layer's image: a url, a gradient, or none. */
190
- export type BackgroundImage = string | LinearGradient | null;
335
+ export type BackgroundImage = string | Gradient | null;
191
336
 
192
337
  /** An intrinsic size: `min-content`, `max-content` or `fit-content`, and
193
338
  * `fit-content()`, whose argument stands in for the room (`fit`). */
@@ -290,6 +435,15 @@ export interface ComputedStyle {
290
435
  * in place of its `list-style-type`'s; null for `none`. */
291
436
  listStyleImage: string | null;
292
437
  cursor: string | null;
438
+ /** `pointer-events`: `none` for an element the pointer passes through,
439
+ * to what is under it (CSS UI 4, 5.2). Its SVG values are `auto`. */
440
+ pointerEvents: 'auto' | 'none';
441
+ /** `fill` and `stroke`, what an `<svg>` in the document is painted with
442
+ * (SVG 2, 13.2): a colour, `currentColor`, `none`, or a `url()` of a
443
+ * paint server in the drawing — and null where nothing has set one,
444
+ * which leaves the drawing its own. */
445
+ fill: string | null;
446
+ stroke: string | null;
293
447
  borderCollapse: 'separate' | 'collapse';
294
448
  /** `border-spacing`: between columns, and between rows. */
295
449
  borderSpacing: number;
@@ -369,6 +523,26 @@ export interface ComputedStyle {
369
523
  * multicol container, which this lays out as one column and which
370
524
  * `line-clamp` does not clamp (CSS Overflow 4, 5.2). */
371
525
  columns: number;
526
+ /** `column-count` where it is a number, and `column-width` where it is
527
+ * a length; null for `auto` (CSS Multi-column 1, 2 and 3). */
528
+ columnCount: number | null;
529
+ columnWidth: number | null;
530
+ /** Whether `column-gap` is `normal`: nothing in a flex box or a grid,
531
+ * and an em between the columns of a multicol container (4.1). */
532
+ columnGapNormal: boolean;
533
+ /** The fewest lines of a block a column may end on, and the fewest it
534
+ * may start with (CSS Fragmentation 3, 4.3). Inherited. */
535
+ orphans: number;
536
+ widows: number;
537
+ /** `break-inside`: whether a column break may fall inside the box;
538
+ * `avoid-column` and `avoid-page` are read as `avoid`. */
539
+ breakInside: 'auto' | 'avoid';
540
+ /** `column-fill`: whether the columns are balanced, or each filled
541
+ * before the next is started; `balance-all` is `balance`. */
542
+ columnFill: 'balance' | 'auto';
543
+ /** `column-span: all`: whether the box is set across all the columns of
544
+ * the multicol container it is in (CSS Multi-column 1, 6). */
545
+ columnSpan: boolean;
372
546
  /** How a line cut by `overflow` ends: `clip`, or with an ellipsis. */
373
547
  textOverflow: 'clip' | 'ellipsis';
374
548
  /** `aspect-ratio`: a box's width over its height, where its height is
@@ -388,16 +562,28 @@ export interface ComputedStyle {
388
562
  * its edges measured from the border box's top left, a null edge the
389
563
  * border box's own (CSS 2.1 11.1.2). Null for `auto`. */
390
564
  clip: ClipRect | null;
565
+ /** `clip-path`: the rectangle the element and all it holds show through
566
+ * (CSS Masking 1, 5.1); null for `none`, and for a shape this does not
567
+ * draw. */
568
+ clipPath: ClipPath | null;
391
569
  opacity: number;
392
570
  /** Where `translate` moves the box after layout (CSS Transforms 2): a
393
571
  * length or a percentage of its own border box across and down; null
394
572
  * for `none`. */
395
573
  translate: [Len, Len] | null;
396
- /** The translation in its `transform`, the one part of a transform
397
- * this draws — rotating, scaling and skewing are not; null for `none`.
398
- * Either makes the box a containing block and paints it with the
399
- * positioned boxes, as a transform does. */
400
- transformTranslate: [Len, Len] | null;
574
+ /** `rotate`: a turn in the plane of the page, in degrees; null for
575
+ * `none`, and for a turn about an axis in the page. */
576
+ rotate: number | null;
577
+ /** `scale`: across and down; null for `none`. */
578
+ scale: [number, number] | null;
579
+ /** `transform`: its functions in the order written, the ones out of the
580
+ * plane left out (`css/transform.ts`); null for `none`. Any of the four
581
+ * makes the box a containing block and a stacking context, and paints it
582
+ * with the positioned boxes (CSS Transforms 1, 3; `transformed`). */
583
+ transform: TransformFunction[] | null;
584
+ /** `transform-origin`: the point a transform turns and scales about,
585
+ * from the border box's top left, a percentage of its width and height. */
586
+ transformOrigin: [Len, Len];
401
587
  zIndex: number | 'auto';
402
588
  /** A keyword, a length to raise the box by, or a percentage of its own
403
589
  * line height. */
@@ -484,7 +670,7 @@ export interface ComputedStyle {
484
670
  backgroundImage: string | null;
485
671
  /** A background drawn rather than fetched: the first layer's linear
486
672
  * gradient, where it is one. */
487
- backgroundGradient: LinearGradient | null;
673
+ backgroundGradient: Gradient | null;
488
674
  /** How the first layer repeats across and down (CSS Backgrounds 3, 3.4):
489
675
  * one of a few pairs made once, so a style copies no array. */
490
676
  backgroundRepeat: BackgroundRepeat;
@@ -543,10 +729,17 @@ export interface ComputedStyle {
543
729
  * set it (`decorate`). No property: the cascade works them out. */
544
730
  underline: string | null;
545
731
  underlineStyle: 'solid' | 'double' | 'dotted' | 'dashed' | 'wavy';
546
- /** And the underline's thickness and offset, the box's that set it. */
732
+ /** And the underline's thickness and offset, the box's that set it: the
733
+ * thickness the one used, which `auto` takes from that box's font size
734
+ * (`usedThickness`), and the offset null where it is `auto`. */
547
735
  underlineThickness: number | null;
548
736
  underlineOffset: number | null;
549
737
  lineThrough: string | null;
738
+ /** And the line through's style and thickness, the box's that set it, as
739
+ * the underline's are. It has no offset to set: it is drawn where each
740
+ * font it crosses puts it (`paintRunRules`). */
741
+ lineThroughStyle: 'solid' | 'double' | 'dotted' | 'dashed' | 'wavy';
742
+ lineThroughThickness: number | null;
550
743
 
551
744
  // flex — handed to yoga rather than interpreted here
552
745
  flexDirection: 'row' | 'row-reverse' | 'column' | 'column-reverse';
@@ -653,6 +846,8 @@ export const INHERITED = [
653
846
  'fontFeatureSettings',
654
847
  'tabSize',
655
848
  'tabSizeIsLength',
849
+ 'orphans',
850
+ 'widows',
656
851
  'whiteSpace',
657
852
  'overflowWrap',
658
853
  'wordBreak',
@@ -664,6 +859,9 @@ export const INHERITED = [
664
859
  'listStylePosition',
665
860
  'listStyleImage',
666
861
  'cursor',
862
+ 'pointerEvents',
863
+ 'fill',
864
+ 'stroke',
667
865
  'borderCollapse',
668
866
  'borderSpacing',
669
867
  'borderSpacingY',
@@ -700,6 +898,20 @@ export interface RootLook {
700
898
  controlPadY: number;
701
899
  controlBorder: number;
702
900
  controlRadius: number;
901
+ /**
902
+ * The size the palette's widgets set their text at, `theme.fontSize` — and
903
+ * so the size of a control's text, whatever the text around it is: a
904
+ * browser's controls take a system font rather than their parent's, and
905
+ * this is the system's. Without it, `fontSize`.
906
+ */
907
+ controlFontSize?: number;
908
+ /**
909
+ * The family they set it in, `theme.fontFamily`, for the same reason: a
910
+ * browser's system font is its own, Arial in Chrome whatever the page is
911
+ * set in. A `<textarea>` is `monoFamily`, a browser's `monospace`, as it
912
+ * is in Chrome. Without it, `fontFamily`.
913
+ */
914
+ controlFontFamily?: string;
703
915
  }
704
916
 
705
917
  export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
@@ -744,6 +956,9 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
744
956
  listStylePosition: 'outside',
745
957
  listStyleImage: null,
746
958
  cursor: null,
959
+ pointerEvents: 'auto',
960
+ fill: null,
961
+ stroke: null,
747
962
  borderCollapse: 'separate',
748
963
  // CSS's initial value; a `<table>` gets its 2px from the UA sheet, and
749
964
  // an anonymous table, which no sheet names, has none
@@ -779,6 +994,14 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
779
994
  webkitBoxAlign: null,
780
995
  webkitBoxFlex: 0,
781
996
  columns: 0,
997
+ columnCount: null,
998
+ columnWidth: null,
999
+ columnGapNormal: true,
1000
+ orphans: 2,
1001
+ widows: 2,
1002
+ breakInside: 'auto',
1003
+ columnFill: 'balance',
1004
+ columnSpan: false,
782
1005
  textOverflow: 'clip',
783
1006
  aspectRatio: null,
784
1007
  objectFit: 'fill',
@@ -788,9 +1011,13 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
788
1011
  overflowX: 'visible',
789
1012
  overflowY: 'visible',
790
1013
  clip: null,
1014
+ clipPath: null,
791
1015
  opacity: 1,
792
1016
  translate: null,
793
- transformTranslate: null,
1017
+ rotate: null,
1018
+ scale: null,
1019
+ transform: null,
1020
+ transformOrigin: [{ pct: 50 }, { pct: 50 }],
794
1021
  zIndex: AUTO,
795
1022
  verticalAlign: 'baseline',
796
1023
 
@@ -880,6 +1107,8 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
880
1107
  underlineThickness: null,
881
1108
  underlineOffset: null,
882
1109
  lineThrough: null,
1110
+ lineThroughStyle: 'solid',
1111
+ lineThroughThickness: null,
883
1112
 
884
1113
  flexDirection: 'row',
885
1114
  flexWrap: 'nowrap',
@@ -989,6 +1218,8 @@ export function inherit(
989
1218
  out.fontFeatureSettings = parent.fontFeatureSettings;
990
1219
  out.tabSize = parent.tabSize;
991
1220
  out.tabSizeIsLength = parent.tabSizeIsLength;
1221
+ out.orphans = parent.orphans;
1222
+ out.widows = parent.widows;
992
1223
  out.whiteSpace = parent.whiteSpace;
993
1224
  out.overflowWrap = parent.overflowWrap;
994
1225
  out.wordBreak = parent.wordBreak;
@@ -1000,6 +1231,9 @@ export function inherit(
1000
1231
  out.listStylePosition = parent.listStylePosition;
1001
1232
  out.listStyleImage = parent.listStyleImage;
1002
1233
  out.cursor = parent.cursor;
1234
+ out.pointerEvents = parent.pointerEvents;
1235
+ out.fill = parent.fill;
1236
+ out.stroke = parent.stroke;
1003
1237
  out.borderCollapse = parent.borderCollapse;
1004
1238
  out.borderSpacing = parent.borderSpacing;
1005
1239
  out.borderSpacingY = parent.borderSpacingY;
@@ -1019,6 +1253,8 @@ export function inherit(
1019
1253
  out.underlineThickness = parent.underlineThickness;
1020
1254
  out.underlineOffset = parent.underlineOffset;
1021
1255
  out.lineThrough = parent.lineThrough;
1256
+ out.lineThroughStyle = parent.lineThroughStyle;
1257
+ out.lineThroughThickness = parent.lineThroughThickness;
1022
1258
  return out;
1023
1259
  }
1024
1260
 
@@ -1053,6 +1289,8 @@ export const FIRST_LINE_INHERITED = [
1053
1289
  'underlineThickness',
1054
1290
  'underlineOffset',
1055
1291
  'lineThrough',
1292
+ 'lineThroughStyle',
1293
+ 'lineThroughThickness',
1056
1294
  ] as const satisfies readonly (keyof ComputedStyle)[];
1057
1295
 
1058
1296
  /**
@@ -1128,6 +1366,15 @@ const BORDER_WIDTH_KEYWORDS: Record<string, number> = {
1128
1366
  thick: 5,
1129
1367
  };
1130
1368
 
1369
+ /** `-webkit-` names that are the multicol properties' own. */
1370
+ const MULTICOL_ALIASES: Record<string, string> = {
1371
+ '-webkit-columns': 'columns',
1372
+ '-webkit-column-count': 'column-count',
1373
+ '-webkit-column-width': 'column-width',
1374
+ '-webkit-column-gap': 'column-gap',
1375
+ '-webkit-column-break-inside': 'break-inside',
1376
+ };
1377
+
1131
1378
  /**
1132
1379
  * Apply one declaration to a style, in place.
1133
1380
  *
@@ -1143,7 +1390,12 @@ export function applyDeclaration(
1143
1390
  rawValue: string,
1144
1391
  ctx: UnitContext,
1145
1392
  ): void {
1146
- const name = prop.toLowerCase();
1393
+ const written = prop.toLowerCase();
1394
+ // the multicol properties under the names WebKit had them by, which
1395
+ // Blink still reads, each an alias of the property (`-webkit-column-
1396
+ // count` in its `css_properties.json5`): a page written for it alone
1397
+ // names no other
1398
+ const name = MULTICOL_ALIASES[written] ?? written;
1147
1399
  let value = rawValue.trim();
1148
1400
  if (!value) return;
1149
1401
  // a logical property is the physical one it stands for, the CSS-wide
@@ -1271,17 +1523,63 @@ export function applyDeclaration(
1271
1523
  case 'column-count':
1272
1524
  case 'column-width':
1273
1525
  case 'columns': {
1274
- // `auto`, or a count, a width or both of them
1526
+ // `auto`, or a count, a width or both of them, in either order; what
1527
+ // the shorthand leaves out is `auto`
1275
1528
  const bit = name === 'column-count' ? 1 : name === 'column-width' ? 2 : 3;
1276
- let set = 0;
1529
+ let count: number | null = null;
1530
+ let width: number | null = null;
1277
1531
  for (const part of splitValue(value)) {
1278
1532
  const v = part.toLowerCase();
1279
1533
  if (v === 'auto') continue;
1280
- if (/^\d+$/.test(v) && Number(v) >= 1) set |= 1;
1281
- else if (parseLength(part, ctx) !== null) set |= 2;
1282
- else return;
1534
+ if (/^\d+$/.test(v) && Number(v) >= 1) {
1535
+ if (count !== null || !(bit & 1)) return;
1536
+ count = Number(v);
1537
+ continue;
1538
+ }
1539
+ const len = parseLength(part, ctx);
1540
+ if (typeof len !== 'number' || !(len > 0)) return;
1541
+ if (width !== null || !(bit & 2)) return;
1542
+ width = len;
1543
+ }
1544
+ if (bit & 1) style.columnCount = count;
1545
+ if (bit & 2) style.columnWidth = width;
1546
+ style.columns =
1547
+ (style.columnCount !== null ? 1 : 0) |
1548
+ (style.columnWidth !== null ? 2 : 0);
1549
+ return;
1550
+ }
1551
+ case 'orphans':
1552
+ case 'widows': {
1553
+ // a whole number of lines, one or more
1554
+ const v = value.trim();
1555
+ if (!/^\+?\d+$/.test(v) || Number(v) < 1) return;
1556
+ style[name] = Number(v);
1557
+ return;
1558
+ }
1559
+ case '-webkit-column-span':
1560
+ case 'column-span': {
1561
+ const v = value.trim().toLowerCase();
1562
+ if (v === 'all') style.columnSpan = true;
1563
+ else if (v === 'none') style.columnSpan = false;
1564
+ return;
1565
+ }
1566
+ case 'column-fill': {
1567
+ const v = value.trim().toLowerCase();
1568
+ if (v === 'auto') style.columnFill = 'auto';
1569
+ else if (v === 'balance' || v === 'balance-all') {
1570
+ style.columnFill = 'balance';
1571
+ }
1572
+ return;
1573
+ }
1574
+ case 'break-inside':
1575
+ case 'page-break-inside': {
1576
+ const v = value.trim().toLowerCase();
1577
+ if (v === 'auto') style.breakInside = 'auto';
1578
+ else if (/^avoid(?:-page|-column|-region)?$/.test(v)) {
1579
+ // the legacy property knows `avoid` alone
1580
+ if (name === 'page-break-inside' && v !== 'avoid') return;
1581
+ style.breakInside = 'avoid';
1283
1582
  }
1284
- style.columns = (style.columns & ~bit) | (set & bit);
1285
1583
  return;
1286
1584
  }
1287
1585
  case '-webkit-box-flex': {
@@ -1520,6 +1818,12 @@ export function applyDeclaration(
1520
1818
  if (clip !== undefined) style.clip = clip;
1521
1819
  return;
1522
1820
  }
1821
+ case 'clip-path':
1822
+ case '-webkit-clip-path': {
1823
+ const path = parseClipPath(value, ctx);
1824
+ if (path !== undefined) style.clipPath = path;
1825
+ return;
1826
+ }
1523
1827
  case 'opacity': {
1524
1828
  const a = parseAlpha(value);
1525
1829
  if (a !== null) style.opacity = a;
@@ -1539,9 +1843,31 @@ export function applyDeclaration(
1539
1843
  style.translate = [x, y ?? 0];
1540
1844
  return;
1541
1845
  }
1542
- case 'transform': {
1543
- const moved = transformTranslation(value, ctx);
1544
- if (moved !== undefined) style.transformTranslate = moved;
1846
+ case 'rotate': {
1847
+ const turn = parseRotate(value);
1848
+ if (turn !== undefined) style.rotate = turn;
1849
+ return;
1850
+ }
1851
+ case 'scale': {
1852
+ const factors = parseScale(value);
1853
+ if (factors !== undefined) style.scale = factors;
1854
+ return;
1855
+ }
1856
+ case 'transform':
1857
+ case '-webkit-transform': {
1858
+ const list = parseTransform(value, ctx);
1859
+ if (list !== undefined) style.transform = list;
1860
+ return;
1861
+ }
1862
+ case 'transform-origin':
1863
+ case '-webkit-transform-origin': {
1864
+ // across and down, as a position is, and a depth nothing here has
1865
+ const parts = splitValue(value);
1866
+ if (parts.length === 3) {
1867
+ if (typeof parseLength(parts.pop()!, ctx) !== 'number') return;
1868
+ } else if (parts.length > 2) return;
1869
+ const origin = positionPair(parts, ctx);
1870
+ if (origin) style.transformOrigin = origin;
1545
1871
  return;
1546
1872
  }
1547
1873
  case 'visibility': {
@@ -1804,19 +2130,8 @@ export function applyDeclaration(
1804
2130
  return;
1805
2131
  }
1806
2132
  case 'border-radius': {
1807
- // one to four radii, and after a `/` one to four more for the
1808
- // vertical radii where they differ: elliptical corners
1809
- const halves = value.split('/');
1810
- if (halves.length > 2) return;
1811
- const [horizontal, vertical] = halves.map((half) =>
1812
- splitValue(half).map((p) => radiusOf(p, ctx)),
1813
- );
1814
- for (const parts of vertical ? [horizontal, vertical] : [horizontal]) {
1815
- if (!parts.length || parts.length > 4 || parts.includes(null)) return;
1816
- }
1817
- style.borderRadius = fourSides(horizontal as Len[]);
1818
- const y = vertical ? fourSides(vertical as Len[]) : null;
1819
- style.borderRadiusY = y && !sameRadii(style.borderRadius, y) ? y : null;
2133
+ const radii = parseRadii(value, ctx);
2134
+ if (radii) [style.borderRadius, style.borderRadiusY] = radii;
1820
2135
  return;
1821
2136
  }
1822
2137
 
@@ -2059,7 +2374,20 @@ export function applyDeclaration(
2059
2374
  )
2060
2375
  .filter(Boolean)
2061
2376
  .join(', ');
2062
- style.fontFamily = ctx.families ? ctx.families(list) : list;
2377
+ // A list that ends in no generic family ends in the document's own
2378
+ // font: where no face in the list matches, the text is set in the
2379
+ // user agent's default (CSS Fonts 4, 5.1), which for a browser is its
2380
+ // standard font and here is the one the document is set in. The text
2381
+ // engine's own answer for a name nothing has is its platform's pick,
2382
+ // Verdana under fontconfig: the Zen Garden's 216 sets its summary in
2383
+ // Montserrat alone, which no machine here has, and Chrome sets it in
2384
+ // Times where ours was Verdana, a fifth wider
2385
+ const last = names[names.length - 1].trim();
2386
+ const generic =
2387
+ !/^['"]/.test(last) && GENERIC_FAMILY.has(last.toLowerCase());
2388
+ const fallback = generic ? null : (ctx.fallbackFamily?.() ?? null);
2389
+ const full = fallback ? `${list}, ${fallback}` : list;
2390
+ style.fontFamily = ctx.families ? ctx.families(full) : full;
2063
2391
  return;
2064
2392
  }
2065
2393
  case 'font-size': {
@@ -2067,7 +2395,11 @@ export function applyDeclaration(
2067
2395
  // handed a face at a size of millions shapes and caches glyphs that
2068
2396
  // size, and a nest of `larger`s gets there on its own
2069
2397
  const most = MAX_FONT_SIZE * ctx.scale;
2070
- const kw = keywordFontSize(value, parent.fontSize, ctx.rem);
2398
+ const kw = keywordFontSize(
2399
+ value,
2400
+ parent.fontSize,
2401
+ ctx.initial?.fontSize ?? ctx.rem,
2402
+ );
2071
2403
  if (kw !== null) {
2072
2404
  style.fontSize = Math.min(kw, most);
2073
2405
  return;
@@ -2461,6 +2793,20 @@ export function applyDeclaration(
2461
2793
  style.cursor = splitCommas(value)[0]?.trim().toLowerCase() || null;
2462
2794
  return;
2463
2795
  }
2796
+ case 'pointer-events': {
2797
+ // `none`, or one of the values SVG gives it, which are `auto` on
2798
+ // anything that is not SVG's to draw
2799
+ const v = value.trim().toLowerCase();
2800
+ if (v === 'none') style.pointerEvents = 'none';
2801
+ else if (POINTER_EVENTS.has(v)) style.pointerEvents = 'auto';
2802
+ return;
2803
+ }
2804
+ case 'fill':
2805
+ case 'stroke': {
2806
+ const paint = svgPaint(value);
2807
+ if (paint !== null) style[name] = paint;
2808
+ return;
2809
+ }
2464
2810
 
2465
2811
  // --- lists --------------------------------------------------------------
2466
2812
  case 'list-style': {
@@ -2633,7 +2979,11 @@ export function applyDeclaration(
2633
2979
  const col = parts.length > 1 ? gap(parts[1]) : row;
2634
2980
  if (row === null || col === null) return;
2635
2981
  if (which !== 'column-gap') style.rowGap = row;
2636
- if (which !== 'row-gap') style.columnGap = col;
2982
+ if (which !== 'row-gap') {
2983
+ style.columnGap = col;
2984
+ const word = parts[parts.length - 1].toLowerCase();
2985
+ style.columnGapNormal = word === 'normal';
2986
+ }
2637
2987
  return;
2638
2988
  }
2639
2989
  case 'grid-template-columns':
@@ -3110,6 +3460,12 @@ function alignKeyword(value: string): string | null {
3110
3460
  return 'flex-end';
3111
3461
  case 'first baseline':
3112
3462
  return 'baseline';
3463
+ // By last baselines, which nothing here aligns: set where that falls
3464
+ // back to, the end (CSS Box Alignment 3, 4.2). Dropped, a row that asks
3465
+ // for it stretched its items, and a flex box among them was as tall as
3466
+ // the row, its lines spread down it.
3467
+ case 'last baseline':
3468
+ return 'flex-end';
3113
3469
  case 'center':
3114
3470
  return 'center';
3115
3471
  case 'stretch':
@@ -3781,7 +4137,7 @@ function backgroundImageOf(
3781
4137
  ctx: UnitContext,
3782
4138
  ): BackgroundImage | undefined {
3783
4139
  const v = text.trim();
3784
- if (IMAGE_FUNCTION.test(v.toLowerCase())) return parseLinearGradient(v, ctx);
4140
+ if (IMAGE_FUNCTION.test(v.toLowerCase())) return parseGradient(v, ctx);
3785
4141
  return parseUrl(v);
3786
4142
  }
3787
4143
 
@@ -3803,7 +4159,7 @@ function layerValues<T>(
3803
4159
  interface BackgroundLayer {
3804
4160
  color: string | null;
3805
4161
  image: string | null;
3806
- gradient: LinearGradient | null;
4162
+ gradient: Gradient | null;
3807
4163
  repeat: ComputedStyle['backgroundRepeat'];
3808
4164
  size: ComputedStyle['backgroundSize'];
3809
4165
  attachment: ComputedStyle['backgroundAttachment'];
@@ -3819,7 +4175,7 @@ interface BackgroundLayer {
3819
4175
  * is dropped rather than resetting the background it meant to replace
3820
4176
  * (CSS 2.1 4.2). What CSS3 adds is read too, so that a declaration a
3821
4177
  * browser keeps is kept: `/ cover` after the position, `space` and `round`,
3822
- * a gradient — drawn as nothing, over the layer's colour.
4178
+ * and a gradient, over the layer's colour.
3823
4179
  */
3824
4180
  function readBackgroundLayer(
3825
4181
  text: string,
@@ -3854,7 +4210,7 @@ function readBackgroundLayer(
3854
4210
  const url = parseUrl(part);
3855
4211
  if (url === undefined) return null;
3856
4212
  layer.image = url;
3857
- } else if (v !== 'none') layer.gradient = parseLinearGradient(part, ctx);
4213
+ } else if (v !== 'none') layer.gradient = parseGradient(part, ctx);
3858
4214
  i += 1;
3859
4215
  } else if (v === 'repeat-x' || v === 'repeat-y' || REPEATS.has(v)) {
3860
4216
  if (!once(2)) return null;
@@ -3918,10 +4274,8 @@ function readBackgroundLayer(
3918
4274
 
3919
4275
  /**
3920
4276
  * A `linear-gradient()`, or null for any other image function and for one
3921
- * that is no gradient: a direction by angle, by side or by corner, with a
3922
- * colour interpolation method (`in oklab`, which Tailwind 4 writes) read and
3923
- * not honoured, and at least two stops, each with no position, one, or two;
3924
- * a bare percentage between stops, a hint, is passed over.
4277
+ * that is no gradient: a direction by angle, by side or by corner, and its
4278
+ * colour stops.
3925
4279
  */
3926
4280
  function parseLinearGradient(
3927
4281
  text: string,
@@ -3930,14 +4284,13 @@ function parseLinearGradient(
3930
4284
  const m = /^linear-gradient\((.*)\)$/is.exec(text.trim());
3931
4285
  if (!m) return null;
3932
4286
  const args = splitCommas(m[1]).map((a) => a.trim());
3933
- const out: LinearGradient = { angle: Math.PI, corner: null, stops: [] };
3934
- const first = (args[0] ?? '')
3935
- .toLowerCase()
3936
- .replace(
3937
- /\bin\s+[a-z-]+(?:\s+(?:shorter|longer|increasing|decreasing)\s+hue)?/,
3938
- '',
3939
- )
3940
- .trim();
4287
+ const out: LinearGradient = {
4288
+ kind: 'linear',
4289
+ angle: Math.PI,
4290
+ corner: null,
4291
+ stops: [],
4292
+ };
4293
+ const first = withoutInterpolation(args[0] ?? '');
3941
4294
  let from = 0;
3942
4295
  if (!first || first.startsWith('to ')) {
3943
4296
  from = 1;
@@ -3973,7 +4326,120 @@ function parseLinearGradient(
3973
4326
  from = 1;
3974
4327
  }
3975
4328
  }
3976
- for (const arg of args.slice(from)) {
4329
+ const stops = parseStops(args.slice(from), ctx);
4330
+ if (!stops) return null;
4331
+ out.stops = stops;
4332
+ return out;
4333
+ }
4334
+
4335
+ /**
4336
+ * A `radial-gradient()` (CSS Images 3, 3.2.1), or null for anything else:
4337
+ * an ending shape, `circle` or `ellipse`, and its size, in either order —
4338
+ * a keyword for the side or corner of the box it reaches, one length for a
4339
+ * circle's radius, or two lengths or percentages for an ellipse's — then
4340
+ * `at` and its centre, a position as `background-position` writes one;
4341
+ * each of the three may be left out, for an ellipse through the farthest
4342
+ * corner from the middle of the box. A colour interpolation method is read
4343
+ * and not honoured, and the stops are a linear gradient's, spaced along
4344
+ * the ray from the centre to the shape's edge.
4345
+ */
4346
+ function parseRadialGradient(
4347
+ text: string,
4348
+ ctx: UnitContext,
4349
+ ): RadialGradient | null {
4350
+ const m = /^radial-gradient\((.*)\)$/is.exec(text.trim());
4351
+ if (!m) return null;
4352
+ const args = splitCommas(m[1]).map((a) => a.trim());
4353
+ const out: RadialGradient = {
4354
+ kind: 'radial',
4355
+ circle: false,
4356
+ extent: null,
4357
+ radii: null,
4358
+ at: [{ pct: 50 }, { pct: 50 }],
4359
+ stops: [],
4360
+ };
4361
+ const first = withoutInterpolation(args[0] ?? '');
4362
+ let from = 0;
4363
+ if (!first) from = 1;
4364
+ else if (parseColor(splitValue(first)[0]) === null) {
4365
+ from = 1;
4366
+ const words = splitValue(first);
4367
+ const at = words.indexOf('at');
4368
+ if (at >= 0) {
4369
+ const position = positionPair(words.slice(at + 1), ctx);
4370
+ if (!position) return null;
4371
+ out.at = position;
4372
+ }
4373
+ let shape: string | null = null;
4374
+ const sizes: Len[] = [];
4375
+ // the size is one run of words, the shape's keyword before or after it
4376
+ let sized = false;
4377
+ for (const word of at >= 0 ? words.slice(0, at) : words) {
4378
+ if (word === 'circle' || word === 'ellipse') {
4379
+ if (shape) return null;
4380
+ shape = word;
4381
+ if (sizes.length || out.extent) sized = true;
4382
+ continue;
4383
+ }
4384
+ if (sized || out.extent) return null;
4385
+ if (RADIAL_EXTENTS.has(word)) {
4386
+ if (sizes.length) return null;
4387
+ out.extent = word as RadialExtent;
4388
+ continue;
4389
+ }
4390
+ const len = parseLength(word, ctx);
4391
+ if (len === null || len === AUTO || !notNegative(len)) return null;
4392
+ sizes.push(len);
4393
+ }
4394
+ if (sizes.length === 1) {
4395
+ // a circle's radius is a length: a percentage of what, the spec asks
4396
+ const [r] = sizes;
4397
+ if (shape === 'ellipse' || typeof r !== 'number') {
4398
+ return null;
4399
+ }
4400
+ out.circle = true;
4401
+ out.radii = [r, r];
4402
+ } else if (sizes.length === 2) {
4403
+ if (shape === 'circle') return null;
4404
+ out.radii = [sizes[0], sizes[1]];
4405
+ } else if (sizes.length) return null;
4406
+ else {
4407
+ out.circle = shape === 'circle';
4408
+ out.extent ??= 'farthest-corner';
4409
+ }
4410
+ }
4411
+ const stops = parseStops(args.slice(from), ctx);
4412
+ if (!stops) return null;
4413
+ out.stops = stops;
4414
+ return out;
4415
+ }
4416
+
4417
+ const RADIAL_EXTENTS = new Set([
4418
+ 'closest-side',
4419
+ 'closest-corner',
4420
+ 'farthest-side',
4421
+ 'farthest-corner',
4422
+ ]);
4423
+
4424
+ /** A gradient's first argument, lower-cased, without the colour
4425
+ * interpolation method CSS Images 4 adds to it (`in oklab`, which
4426
+ * Tailwind 4 writes): read, and not honoured. */
4427
+ function withoutInterpolation(arg: string): string {
4428
+ return arg
4429
+ .toLowerCase()
4430
+ .replace(
4431
+ /\bin\s+[a-z-]+(?:\s+(?:shorter|longer|increasing|decreasing)\s+hue)?/,
4432
+ '',
4433
+ )
4434
+ .trim();
4435
+ }
4436
+
4437
+ /** A gradient's colour stops, at least two, each with no position, one or
4438
+ * two; a bare length between two stops, a hint, is passed over. Null
4439
+ * where an argument is none of those. */
4440
+ function parseStops(args: string[], ctx: UnitContext): GradientStop[] | null {
4441
+ const stops: GradientStop[] = [];
4442
+ for (const arg of args) {
3977
4443
  const parts = splitValue(arg);
3978
4444
  const color = parseColor(parts[0] ?? '');
3979
4445
  if (color === null) {
@@ -3984,14 +4450,19 @@ function parseLinearGradient(
3984
4450
  if (parts.length > 3) return null;
3985
4451
  const at = parts.slice(1).map((p) => parseLength(p, ctx));
3986
4452
  if (at.some((p) => p === null || p === AUTO)) return null;
3987
- if (!at.length) out.stops.push({ color, at: null });
3988
- for (const p of at) out.stops.push({ color, at: p as Len });
4453
+ if (!at.length) stops.push({ color, at: null });
4454
+ for (const p of at) stops.push({ color, at: p as Len });
3989
4455
  }
3990
- return out.stops.length >= 2 ? out : null;
4456
+ return stops.length >= 2 ? stops : null;
4457
+ }
4458
+
4459
+ /** A gradient function's image, where it is one this draws. */
4460
+ function parseGradient(text: string, ctx: UnitContext): Gradient | null {
4461
+ return parseLinearGradient(text, ctx) ?? parseRadialGradient(text, ctx);
3991
4462
  }
3992
4463
 
3993
4464
  /** The images CSS3 has beyond `url()`, which a layer may name: a linear
3994
- * gradient is drawn, and the rest draw as nothing. */
4465
+ * and a radial gradient are drawn, and the rest draw as nothing. */
3995
4466
  const IMAGE_FUNCTION =
3996
4467
  /^(?:-(?:webkit|moz|o|ms)-)?(?:(?:repeating-)?(?:linear|radial|conic)-gradient|gradient|image-set|cross-fade|element|paint)\(/;
3997
4468
 
@@ -4032,87 +4503,6 @@ function whiteSpaceOf(collapse: Collapse, wrap: boolean): WhiteSpace {
4032
4503
  return 'pre-line';
4033
4504
  }
4034
4505
 
4035
- /**
4036
- * The translation a `transform` makes: the sum of its translate functions'
4037
- * and its matrices' — `translate(-50%, -50%)`, Tailwind 3's `translate(
4038
- * var(--tw-translate-x), var(--tw-translate-y)) rotate(…) skewX(…) …` —
4039
- * with the functions that rotate, scale, skew or add perspective read and
4040
- * not drawn. Null for `none`, and undefined for what is no transform list,
4041
- * which drops the declaration.
4042
- */
4043
- function transformTranslation(
4044
- value: string,
4045
- ctx: UnitContext,
4046
- ): [Len, Len] | null | undefined {
4047
- if (value.trim().toLowerCase() === 'none') return null;
4048
- let x: Len = 0;
4049
- let y: Len = 0;
4050
- const parts = splitValue(value);
4051
- if (!parts.length) return undefined;
4052
- for (const part of parts) {
4053
- const m = /^([a-z0-9]+)\((.*)\)$/is.exec(part.trim());
4054
- if (!m) return undefined;
4055
- const name = m[1].toLowerCase();
4056
- const args = splitCommas(m[2]).map((a) => a.trim());
4057
- let dx: Len | null = 0;
4058
- let dy: Len | null = 0;
4059
- if (name === 'translate' || name === 'translate3d') {
4060
- dx = parseLength(args[0] ?? '', ctx);
4061
- dy = args[1] === undefined ? 0 : parseLength(args[1], ctx);
4062
- } else if (name === 'translatex') dx = parseLength(args[0] ?? '', ctx);
4063
- else if (name === 'translatey') dy = parseLength(args[0] ?? '', ctx);
4064
- else if (name === 'matrix' || name === 'matrix3d') {
4065
- // the translation is the last column's
4066
- const n = args.map(Number);
4067
- if (n.length !== (name === 'matrix' ? 6 : 16) || n.some(isNaN)) {
4068
- return undefined;
4069
- }
4070
- [dx, dy] = name === 'matrix' ? [n[4], n[5]] : [n[12], n[13]];
4071
- } else if (!TRANSFORMS.has(name)) return undefined;
4072
- if (dx === null || dx === AUTO || dy === null || dy === AUTO) {
4073
- return undefined;
4074
- }
4075
- const sx = addLen(x, dx);
4076
- const sy = addLen(y, dy);
4077
- if (sx === null || sy === null) return undefined;
4078
- x = sx;
4079
- y = sy;
4080
- }
4081
- return [x, y];
4082
- }
4083
-
4084
- /** The transform functions read and not drawn. */
4085
- const TRANSFORMS = new Set([
4086
- 'translatez',
4087
- 'rotate',
4088
- 'rotatex',
4089
- 'rotatey',
4090
- 'rotatez',
4091
- 'rotate3d',
4092
- 'scale',
4093
- 'scalex',
4094
- 'scaley',
4095
- 'scalez',
4096
- 'scale3d',
4097
- 'skew',
4098
- 'skewx',
4099
- 'skewy',
4100
- 'perspective',
4101
- ]);
4102
-
4103
- /** Two lengths added, a percentage and a length kept apart; null where
4104
- * one is a `min()` or a `max()`, which a sum cannot be kept of. */
4105
- function addLen(a: Len, b: Len): Len | null {
4106
- if (a === AUTO || b === AUTO) return null;
4107
- if (typeof a === 'number' && typeof b === 'number') return a + b;
4108
- if (a === 0) return b;
4109
- if (b === 0) return a;
4110
- const pa: Pct = typeof a === 'number' ? { pct: 0, px: a } : a;
4111
- const pb: Pct = typeof b === 'number' ? { pct: 0, px: b } : b;
4112
- if (pa.of || pb.of) return null;
4113
- return { pct: pa.pct + pb.pct, px: (pa.px ?? 0) + (pb.px ?? 0) };
4114
- }
4115
-
4116
4506
  /** An intrinsic size keyword, with the prefixes browsers still read, or
4117
4507
  * `fit-content()` of a length or a percentage (CSS Sizing 3, 3.1) — a
4118
4508
  * width's, where a height's is its content height as the keyword is;
@@ -4307,8 +4697,11 @@ function applyFontShorthand(
4307
4697
  // dropped whole, as CSS drops any value it cannot read — a line height
4308
4698
  // below nought included: `font: 4em/-2em serif` set the text at 4em.
4309
4699
  const size =
4310
- keywordFontSize(sizeText ?? '', parent.fontSize, ctx.rem) ??
4311
- parseLength(sizeText ?? '', { ...ctx, em: parent.fontSize });
4700
+ keywordFontSize(
4701
+ sizeText ?? '',
4702
+ parent.fontSize,
4703
+ ctx.initial?.fontSize ?? ctx.rem,
4704
+ ) ?? parseLength(sizeText ?? '', { ...ctx, em: parent.fontSize });
4312
4705
  if (!family || size === null || size === AUTO) return;
4313
4706
  if (lineText && negativeLength(lineText, ctx)) return;
4314
4707
  // What the shorthand does not name goes back to its initial value rather
@@ -4748,6 +5141,33 @@ function splitTopLevelSlash(value: string): string[] {
4748
5141
 
4749
5142
  // --- logical properties ----------------------------------------------------
4750
5143
 
5144
+ /** `pointer-events`' values besides `none` (CSS UI 4, SVG 2 15.6). */
5145
+ const POINTER_EVENTS = new Set([
5146
+ 'auto',
5147
+ 'bounding-box',
5148
+ 'visiblepainted',
5149
+ 'visiblefill',
5150
+ 'visiblestroke',
5151
+ 'visible',
5152
+ 'painted',
5153
+ 'fill',
5154
+ 'stroke',
5155
+ 'all',
5156
+ ]);
5157
+ /**
5158
+ * A `fill` or a `stroke` (SVG 2, 13.2): `none`, a colour, or a `url()`
5159
+ * naming a gradient in the drawing, with what follows it, its fallback,
5160
+ * left off. Null for anything else, `context-fill` and `context-stroke`
5161
+ * among it, which only a marker or a `<use>` has a context for.
5162
+ */
5163
+ function svgPaint(value: string): string | null {
5164
+ const v = value.trim();
5165
+ if (v.toLowerCase() === 'none') return 'none';
5166
+ const url = /^url\(\s*(['"]?)(#[^'")\s]+)\1\s*\)/i.exec(v);
5167
+ if (url) return `url(${url[2]})`;
5168
+ return parseColor(v);
5169
+ }
5170
+
4751
5171
  const OBJECT_FITS = new Set(['fill', 'contain', 'cover', 'none', 'scale-down']);
4752
5172
 
4753
5173
  /**
@@ -4839,6 +5259,29 @@ function radiusOf(text: string, ctx: UnitContext): Len | null {
4839
5259
  return len.pct < 0 && !len.px && !len.of ? null : len;
4840
5260
  }
4841
5261
 
5262
+ /**
5263
+ * `border-radius`'s value: one to four radii, and after a `/` one to four
5264
+ * more for the vertical radii where they differ, elliptical corners — the
5265
+ * radii across, and the ones down, null where they are the same values.
5266
+ * Null for anything else.
5267
+ */
5268
+ function parseRadii(
5269
+ value: string,
5270
+ ctx: UnitContext,
5271
+ ): [[Len, Len, Len, Len], [Len, Len, Len, Len] | null] | null {
5272
+ const halves = value.split('/');
5273
+ if (halves.length > 2) return null;
5274
+ const [horizontal, vertical] = halves.map((half) =>
5275
+ splitValue(half).map((p) => radiusOf(p, ctx)),
5276
+ );
5277
+ for (const parts of vertical ? [horizontal, vertical] : [horizontal]) {
5278
+ if (!parts.length || parts.length > 4 || parts.includes(null)) return null;
5279
+ }
5280
+ const across = fourSides(horizontal as Len[]);
5281
+ const down = vertical ? fourSides(vertical as Len[]) : null;
5282
+ return [across, down && !sameRadii(across, down) ? down : null];
5283
+ }
5284
+
4842
5285
  /** Whether two corners' radii are the same values. */
4843
5286
  function sameRadii(a: readonly Len[], b: readonly Len[]): boolean {
4844
5287
  for (let i = 0; i < 4; i += 1) {
@@ -5081,10 +5524,15 @@ const INHERITED_NAMES = new Set<string>([
5081
5524
  'list-style-position',
5082
5525
  'list-style-image',
5083
5526
  'cursor',
5527
+ 'pointer-events',
5528
+ 'fill',
5529
+ 'stroke',
5084
5530
  'border-collapse',
5085
5531
  'border-spacing',
5086
5532
  'empty-cells',
5087
5533
  'quotes',
5534
+ 'orphans',
5535
+ 'widows',
5088
5536
  ]);
5089
5537
 
5090
5538
  export function isInherited(name: string): boolean {
@@ -5239,6 +5687,9 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
5239
5687
  'list-style-position': ['listStylePosition'],
5240
5688
  'list-style-image': ['listStyleImage'],
5241
5689
  cursor: ['cursor'],
5690
+ 'pointer-events': ['pointerEvents'],
5691
+ fill: ['fill'],
5692
+ stroke: ['stroke'],
5242
5693
  'border-collapse': ['borderCollapse'],
5243
5694
  'caption-side': ['captionSide'],
5244
5695
  'empty-cells': ['emptyCells'],
@@ -5307,9 +5758,16 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
5307
5758
  '-webkit-box-pack': ['webkitBoxPack'],
5308
5759
  '-webkit-box-align': ['webkitBoxAlign'],
5309
5760
  '-webkit-box-flex': ['webkitBoxFlex'],
5310
- columns: ['columns'],
5311
- 'column-count': ['columns'],
5312
- 'column-width': ['columns'],
5761
+ columns: ['columns', 'columnCount', 'columnWidth'],
5762
+ 'column-count': ['columns', 'columnCount'],
5763
+ 'column-width': ['columns', 'columnWidth'],
5764
+ orphans: ['orphans'],
5765
+ widows: ['widows'],
5766
+ 'break-inside': ['breakInside'],
5767
+ 'column-fill': ['columnFill'],
5768
+ 'column-span': ['columnSpan'],
5769
+ '-webkit-column-span': ['columnSpan'],
5770
+ 'page-break-inside': ['breakInside'],
5313
5771
  'text-overflow': ['textOverflow'],
5314
5772
  // a flex box's and its items', and a grid's
5315
5773
  'flex-direction': ['flexDirection'],
@@ -5324,12 +5782,12 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
5324
5782
  'flex-shrink': ['flexShrink'],
5325
5783
  'flex-basis': ['flexBasis'],
5326
5784
  order: ['order'],
5327
- gap: ['rowGap', 'columnGap'],
5785
+ gap: ['rowGap', 'columnGap', 'columnGapNormal'],
5328
5786
  'row-gap': ['rowGap'],
5329
- 'column-gap': ['columnGap'],
5330
- 'grid-gap': ['rowGap', 'columnGap'],
5787
+ 'column-gap': ['columnGap', 'columnGapNormal'],
5788
+ 'grid-gap': ['rowGap', 'columnGap', 'columnGapNormal'],
5331
5789
  'grid-row-gap': ['rowGap'],
5332
- 'grid-column-gap': ['columnGap'],
5790
+ 'grid-column-gap': ['columnGap', 'columnGapNormal'],
5333
5791
  'grid-template-columns': ['gridColumns'],
5334
5792
  'grid-template-rows': ['gridRows'],
5335
5793
  'grid-auto-rows': ['gridAutoRows'],
@@ -5424,11 +5882,18 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
5424
5882
  clear: ['clear'],
5425
5883
  overflow: ['overflowX', 'overflowY'],
5426
5884
  clip: ['clip'],
5885
+ 'clip-path': ['clipPath'],
5886
+ '-webkit-clip-path': ['clipPath'],
5427
5887
  'overflow-x': ['overflowX'],
5428
5888
  'overflow-y': ['overflowY'],
5429
5889
  opacity: ['opacity'],
5430
5890
  translate: ['translate'],
5431
- transform: ['transformTranslate'],
5891
+ rotate: ['rotate'],
5892
+ scale: ['scale'],
5893
+ transform: ['transform'],
5894
+ '-webkit-transform': ['transform'],
5895
+ 'transform-origin': ['transformOrigin'],
5896
+ '-webkit-transform-origin': ['transformOrigin'],
5432
5897
  'z-index': ['zIndex'],
5433
5898
  'vertical-align': ['verticalAlign'],
5434
5899
  'text-decoration': [
@@ -5491,7 +5956,7 @@ export function decorate(style: ComputedStyle): void {
5491
5956
  style.color,
5492
5957
  );
5493
5958
  style.underlineStyle = style.textDecorationStyle;
5494
- style.underlineThickness = style.textDecorationThickness;
5959
+ style.underlineThickness = usedThickness(style);
5495
5960
  style.underlineOffset = style.textUnderlineOffset;
5496
5961
  }
5497
5962
  if (lines.includes('line-through')) {
@@ -5499,9 +5964,30 @@ export function decorate(style: ComputedStyle): void {
5499
5964
  style.textDecorationColor ?? 'currentColor',
5500
5965
  style.color,
5501
5966
  );
5967
+ style.lineThroughStyle = style.textDecorationStyle;
5968
+ style.lineThroughThickness = usedThickness(style);
5502
5969
  }
5503
5970
  }
5504
5971
 
5972
+ /**
5973
+ * How thick the lines a box decorates its text with are. `auto` leaves it
5974
+ * to the user agent, which is to take it from the font where the font says
5975
+ * (CSS Text Decoration 4, 2.4.1); neither text engine here reports a face's
5976
+ * underline, so it is a part of the font size, Blink's tenth — of the size
5977
+ * of the box that set the decoration, which draws one line of one thickness
5978
+ * through everything inside it (2.9). A pixel for every size drew the dots
5979
+ * under the `<abbr>` of a 140px title as a hairline. `from-font` is `auto`
5980
+ * for the same want of a metric, a length is itself on whole pixels, and
5981
+ * no line is thinner than one.
5982
+ */
5983
+ function usedThickness(style: ComputedStyle): number {
5984
+ const length = style.textDecorationThickness;
5985
+ return Math.max(
5986
+ 1,
5987
+ length === null ? style.fontSize / 10 : Math.round(length),
5988
+ );
5989
+ }
5990
+
5505
5991
  /**
5506
5992
  * The absolutely positioned styles that were inline-level before they were
5507
5993
  * made blocks: where such a box would have been in flow is on its line,
@@ -5617,3 +6103,55 @@ export function blockify(style: ComputedStyle, inFlexContainer: boolean): void {
5617
6103
  return;
5618
6104
  }
5619
6105
  }
6106
+
6107
+ /**
6108
+ * The `display` a `<button>` is laid out with, which is not always the one
6109
+ * it was given (HTML's rendering section, button layout, 15.5.3): a flex
6110
+ * box or a grid is that, and of the rest, "a value such that the outer
6111
+ * display type is 'inline'" behaves as `inline-block` and anything else as
6112
+ * `flow-root`. Blink computes them so — `inline-block` for `inline` and
6113
+ * `inline-table`, `block` for `table` and `list-item` — and makes the one
6114
+ * block flow of each; the formatting context is the layout's to give every
6115
+ * button that is a block (`establishesBFC`).
6116
+ *
6117
+ * Left an inline box, a button around a block was broken in two around it,
6118
+ * as any inline box is: as wide as the line, its `height` applied to
6119
+ * nothing, and its border and padding drawn above and below the line it
6120
+ * opened. A table's part is left what it is, where the two disagree: a
6121
+ * block to the text, an inline-block in Blink. After `blockify`, since a
6122
+ * float, an absolute box and a flex item are blocks first.
6123
+ */
6124
+ export function settleButton(style: ComputedStyle): void {
6125
+ switch (style.display) {
6126
+ case 'inline':
6127
+ case 'inline-table':
6128
+ style.display = 'inline-block';
6129
+ return;
6130
+ case 'list-item':
6131
+ case 'table':
6132
+ style.display = 'block';
6133
+ return;
6134
+ default:
6135
+ return;
6136
+ }
6137
+ }
6138
+
6139
+ /** The generic font families, which a list ending in one falls back
6140
+ * through already, in the text engine (CSS Fonts 4, 4.2). */
6141
+ const GENERIC_FAMILY = new Set([
6142
+ 'serif',
6143
+ 'sans-serif',
6144
+ 'monospace',
6145
+ 'cursive',
6146
+ 'fantasy',
6147
+ 'system-ui',
6148
+ 'ui-serif',
6149
+ 'ui-sans-serif',
6150
+ 'ui-monospace',
6151
+ 'ui-rounded',
6152
+ 'math',
6153
+ 'emoji',
6154
+ 'fangsong',
6155
+ '-apple-system',
6156
+ 'blinkmacsystemfont',
6157
+ ]);