@react-x11/components 0.13.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 (128) hide show
  1. package/dist/html/controls.d.ts +29 -2
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +70 -2
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts.map +1 -1
  6. package/dist/html/css/cascade.js +26 -4
  7. package/dist/html/css/cascade.js.map +1 -1
  8. package/dist/html/css/color.d.ts +18 -0
  9. package/dist/html/css/color.d.ts.map +1 -1
  10. package/dist/html/css/color.js +27 -0
  11. package/dist/html/css/color.js.map +1 -1
  12. package/dist/html/css/parse.d.ts +18 -4
  13. package/dist/html/css/parse.d.ts.map +1 -1
  14. package/dist/html/css/parse.js +66 -28
  15. package/dist/html/css/parse.js.map +1 -1
  16. package/dist/html/css/style.d.ts +128 -15
  17. package/dist/html/css/style.d.ts.map +1 -1
  18. package/dist/html/css/style.js +561 -144
  19. package/dist/html/css/style.js.map +1 -1
  20. package/dist/html/css/transform.d.ts +85 -0
  21. package/dist/html/css/transform.d.ts.map +1 -0
  22. package/dist/html/css/transform.js +425 -0
  23. package/dist/html/css/transform.js.map +1 -0
  24. package/dist/html/css/ua.d.ts.map +1 -1
  25. package/dist/html/css/ua.js +14 -2
  26. package/dist/html/css/ua.js.map +1 -1
  27. package/dist/html/css/values.d.ts +4 -0
  28. package/dist/html/css/values.d.ts.map +1 -1
  29. package/dist/html/css/values.js.map +1 -1
  30. package/dist/html/fonts.d.ts +55 -5
  31. package/dist/html/fonts.d.ts.map +1 -1
  32. package/dist/html/fonts.js +284 -23
  33. package/dist/html/fonts.js.map +1 -1
  34. package/dist/html/layout/axes.d.ts +14 -0
  35. package/dist/html/layout/axes.d.ts.map +1 -0
  36. package/dist/html/layout/axes.js +28 -0
  37. package/dist/html/layout/axes.js.map +1 -0
  38. package/dist/html/layout/block.d.ts +93 -12
  39. package/dist/html/layout/block.d.ts.map +1 -1
  40. package/dist/html/layout/block.js +389 -39
  41. package/dist/html/layout/block.js.map +1 -1
  42. package/dist/html/layout/boxes.d.ts +59 -1
  43. package/dist/html/layout/boxes.d.ts.map +1 -1
  44. package/dist/html/layout/boxes.js +22 -2
  45. package/dist/html/layout/boxes.js.map +1 -1
  46. package/dist/html/layout/cache.d.ts +1 -1
  47. package/dist/html/layout/cache.d.ts.map +1 -1
  48. package/dist/html/layout/cache.js +6 -0
  49. package/dist/html/layout/cache.js.map +1 -1
  50. package/dist/html/layout/css-grid.d.ts.map +1 -1
  51. package/dist/html/layout/css-grid.js +90 -18
  52. package/dist/html/layout/css-grid.js.map +1 -1
  53. package/dist/html/layout/flex.d.ts.map +1 -1
  54. package/dist/html/layout/flex.js +570 -83
  55. package/dist/html/layout/flex.js.map +1 -1
  56. package/dist/html/layout/inline.d.ts +3 -0
  57. package/dist/html/layout/inline.d.ts.map +1 -1
  58. package/dist/html/layout/inline.js +144 -54
  59. package/dist/html/layout/inline.js.map +1 -1
  60. package/dist/html/layout/multicol.d.ts +42 -0
  61. package/dist/html/layout/multicol.d.ts.map +1 -0
  62. package/dist/html/layout/multicol.js +897 -0
  63. package/dist/html/layout/multicol.js.map +1 -0
  64. package/dist/html/layout/shaping.d.ts +7 -0
  65. package/dist/html/layout/shaping.d.ts.map +1 -1
  66. package/dist/html/layout/shaping.js +25 -20
  67. package/dist/html/layout/shaping.js.map +1 -1
  68. package/dist/html/layout/table.d.ts.map +1 -1
  69. package/dist/html/layout/table.js +62 -18
  70. package/dist/html/layout/table.js.map +1 -1
  71. package/dist/html/node.d.ts +3 -0
  72. package/dist/html/node.d.ts.map +1 -1
  73. package/dist/html/node.js +263 -36
  74. package/dist/html/node.js.map +1 -1
  75. package/dist/html/paint.d.ts +79 -4
  76. package/dist/html/paint.d.ts.map +1 -1
  77. package/dist/html/paint.js +1213 -141
  78. package/dist/html/paint.js.map +1 -1
  79. package/dist/html/svg.d.ts +15 -2
  80. package/dist/html/svg.d.ts.map +1 -1
  81. package/dist/html/svg.js +249 -17
  82. package/dist/html/svg.js.map +1 -1
  83. package/dist/html/widgets.js +87 -7
  84. package/dist/html/widgets.js.map +1 -1
  85. package/dist/html/woff2.d.ts +14 -0
  86. package/dist/html/woff2.d.ts.map +1 -0
  87. package/dist/html/woff2.js +608 -0
  88. package/dist/html/woff2.js.map +1 -0
  89. package/dist/maps/controller.d.ts +17 -1
  90. package/dist/maps/controller.d.ts.map +1 -1
  91. package/dist/maps/controller.js +27 -7
  92. package/dist/maps/controller.js.map +1 -1
  93. package/dist/richtext/node.d.ts +8 -1
  94. package/dist/richtext/node.d.ts.map +1 -1
  95. package/dist/richtext/node.js +5 -1
  96. package/dist/richtext/node.js.map +1 -1
  97. package/dist/richtext/runs.d.ts +25 -6
  98. package/dist/richtext/runs.d.ts.map +1 -1
  99. package/dist/richtext/runs.js +240 -24
  100. package/dist/richtext/runs.js.map +1 -1
  101. package/package.json +1 -1
  102. package/src/html/controls.ts +85 -2
  103. package/src/html/css/cascade.ts +23 -0
  104. package/src/html/css/color.ts +29 -0
  105. package/src/html/css/parse.ts +71 -31
  106. package/src/html/css/style.ts +664 -147
  107. package/src/html/css/transform.ts +459 -0
  108. package/src/html/css/ua.ts +15 -2
  109. package/src/html/css/values.ts +4 -0
  110. package/src/html/fonts.ts +312 -29
  111. package/src/html/layout/axes.ts +62 -0
  112. package/src/html/layout/block.ts +444 -37
  113. package/src/html/layout/boxes.ts +65 -3
  114. package/src/html/layout/cache.ts +6 -0
  115. package/src/html/layout/css-grid.ts +95 -22
  116. package/src/html/layout/flex.ts +671 -77
  117. package/src/html/layout/inline.ts +149 -55
  118. package/src/html/layout/multicol.ts +977 -0
  119. package/src/html/layout/shaping.ts +27 -18
  120. package/src/html/layout/table.ts +62 -19
  121. package/src/html/node.ts +300 -37
  122. package/src/html/paint.ts +1406 -206
  123. package/src/html/svg.ts +277 -19
  124. package/src/html/widgets.ts +95 -9
  125. package/src/html/woff2.ts +612 -0
  126. package/src/maps/controller.ts +28 -7
  127. package/src/richtext/node.ts +13 -2
  128. 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',
@@ -758,6 +956,9 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
758
956
  listStylePosition: 'outside',
759
957
  listStyleImage: null,
760
958
  cursor: null,
959
+ pointerEvents: 'auto',
960
+ fill: null,
961
+ stroke: null,
761
962
  borderCollapse: 'separate',
762
963
  // CSS's initial value; a `<table>` gets its 2px from the UA sheet, and
763
964
  // an anonymous table, which no sheet names, has none
@@ -793,6 +994,14 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
793
994
  webkitBoxAlign: null,
794
995
  webkitBoxFlex: 0,
795
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,
796
1005
  textOverflow: 'clip',
797
1006
  aspectRatio: null,
798
1007
  objectFit: 'fill',
@@ -802,9 +1011,13 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
802
1011
  overflowX: 'visible',
803
1012
  overflowY: 'visible',
804
1013
  clip: null,
1014
+ clipPath: null,
805
1015
  opacity: 1,
806
1016
  translate: null,
807
- transformTranslate: null,
1017
+ rotate: null,
1018
+ scale: null,
1019
+ transform: null,
1020
+ transformOrigin: [{ pct: 50 }, { pct: 50 }],
808
1021
  zIndex: AUTO,
809
1022
  verticalAlign: 'baseline',
810
1023
 
@@ -894,6 +1107,8 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
894
1107
  underlineThickness: null,
895
1108
  underlineOffset: null,
896
1109
  lineThrough: null,
1110
+ lineThroughStyle: 'solid',
1111
+ lineThroughThickness: null,
897
1112
 
898
1113
  flexDirection: 'row',
899
1114
  flexWrap: 'nowrap',
@@ -1003,6 +1218,8 @@ export function inherit(
1003
1218
  out.fontFeatureSettings = parent.fontFeatureSettings;
1004
1219
  out.tabSize = parent.tabSize;
1005
1220
  out.tabSizeIsLength = parent.tabSizeIsLength;
1221
+ out.orphans = parent.orphans;
1222
+ out.widows = parent.widows;
1006
1223
  out.whiteSpace = parent.whiteSpace;
1007
1224
  out.overflowWrap = parent.overflowWrap;
1008
1225
  out.wordBreak = parent.wordBreak;
@@ -1014,6 +1231,9 @@ export function inherit(
1014
1231
  out.listStylePosition = parent.listStylePosition;
1015
1232
  out.listStyleImage = parent.listStyleImage;
1016
1233
  out.cursor = parent.cursor;
1234
+ out.pointerEvents = parent.pointerEvents;
1235
+ out.fill = parent.fill;
1236
+ out.stroke = parent.stroke;
1017
1237
  out.borderCollapse = parent.borderCollapse;
1018
1238
  out.borderSpacing = parent.borderSpacing;
1019
1239
  out.borderSpacingY = parent.borderSpacingY;
@@ -1033,6 +1253,8 @@ export function inherit(
1033
1253
  out.underlineThickness = parent.underlineThickness;
1034
1254
  out.underlineOffset = parent.underlineOffset;
1035
1255
  out.lineThrough = parent.lineThrough;
1256
+ out.lineThroughStyle = parent.lineThroughStyle;
1257
+ out.lineThroughThickness = parent.lineThroughThickness;
1036
1258
  return out;
1037
1259
  }
1038
1260
 
@@ -1067,6 +1289,8 @@ export const FIRST_LINE_INHERITED = [
1067
1289
  'underlineThickness',
1068
1290
  'underlineOffset',
1069
1291
  'lineThrough',
1292
+ 'lineThroughStyle',
1293
+ 'lineThroughThickness',
1070
1294
  ] as const satisfies readonly (keyof ComputedStyle)[];
1071
1295
 
1072
1296
  /**
@@ -1142,6 +1366,15 @@ const BORDER_WIDTH_KEYWORDS: Record<string, number> = {
1142
1366
  thick: 5,
1143
1367
  };
1144
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
+
1145
1378
  /**
1146
1379
  * Apply one declaration to a style, in place.
1147
1380
  *
@@ -1157,7 +1390,12 @@ export function applyDeclaration(
1157
1390
  rawValue: string,
1158
1391
  ctx: UnitContext,
1159
1392
  ): void {
1160
- 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;
1161
1399
  let value = rawValue.trim();
1162
1400
  if (!value) return;
1163
1401
  // a logical property is the physical one it stands for, the CSS-wide
@@ -1285,17 +1523,63 @@ export function applyDeclaration(
1285
1523
  case 'column-count':
1286
1524
  case 'column-width':
1287
1525
  case 'columns': {
1288
- // `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`
1289
1528
  const bit = name === 'column-count' ? 1 : name === 'column-width' ? 2 : 3;
1290
- let set = 0;
1529
+ let count: number | null = null;
1530
+ let width: number | null = null;
1291
1531
  for (const part of splitValue(value)) {
1292
1532
  const v = part.toLowerCase();
1293
1533
  if (v === 'auto') continue;
1294
- if (/^\d+$/.test(v) && Number(v) >= 1) set |= 1;
1295
- else if (parseLength(part, ctx) !== null) set |= 2;
1296
- 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';
1297
1582
  }
1298
- style.columns = (style.columns & ~bit) | (set & bit);
1299
1583
  return;
1300
1584
  }
1301
1585
  case '-webkit-box-flex': {
@@ -1534,6 +1818,12 @@ export function applyDeclaration(
1534
1818
  if (clip !== undefined) style.clip = clip;
1535
1819
  return;
1536
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
+ }
1537
1827
  case 'opacity': {
1538
1828
  const a = parseAlpha(value);
1539
1829
  if (a !== null) style.opacity = a;
@@ -1553,9 +1843,31 @@ export function applyDeclaration(
1553
1843
  style.translate = [x, y ?? 0];
1554
1844
  return;
1555
1845
  }
1556
- case 'transform': {
1557
- const moved = transformTranslation(value, ctx);
1558
- 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;
1559
1871
  return;
1560
1872
  }
1561
1873
  case 'visibility': {
@@ -1818,19 +2130,8 @@ export function applyDeclaration(
1818
2130
  return;
1819
2131
  }
1820
2132
  case 'border-radius': {
1821
- // one to four radii, and after a `/` one to four more for the
1822
- // vertical radii where they differ: elliptical corners
1823
- const halves = value.split('/');
1824
- if (halves.length > 2) return;
1825
- const [horizontal, vertical] = halves.map((half) =>
1826
- splitValue(half).map((p) => radiusOf(p, ctx)),
1827
- );
1828
- for (const parts of vertical ? [horizontal, vertical] : [horizontal]) {
1829
- if (!parts.length || parts.length > 4 || parts.includes(null)) return;
1830
- }
1831
- style.borderRadius = fourSides(horizontal as Len[]);
1832
- const y = vertical ? fourSides(vertical as Len[]) : null;
1833
- style.borderRadiusY = y && !sameRadii(style.borderRadius, y) ? y : null;
2133
+ const radii = parseRadii(value, ctx);
2134
+ if (radii) [style.borderRadius, style.borderRadiusY] = radii;
1834
2135
  return;
1835
2136
  }
1836
2137
 
@@ -2073,7 +2374,20 @@ export function applyDeclaration(
2073
2374
  )
2074
2375
  .filter(Boolean)
2075
2376
  .join(', ');
2076
- 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;
2077
2391
  return;
2078
2392
  }
2079
2393
  case 'font-size': {
@@ -2479,6 +2793,20 @@ export function applyDeclaration(
2479
2793
  style.cursor = splitCommas(value)[0]?.trim().toLowerCase() || null;
2480
2794
  return;
2481
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
+ }
2482
2810
 
2483
2811
  // --- lists --------------------------------------------------------------
2484
2812
  case 'list-style': {
@@ -2651,7 +2979,11 @@ export function applyDeclaration(
2651
2979
  const col = parts.length > 1 ? gap(parts[1]) : row;
2652
2980
  if (row === null || col === null) return;
2653
2981
  if (which !== 'column-gap') style.rowGap = row;
2654
- 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
+ }
2655
2987
  return;
2656
2988
  }
2657
2989
  case 'grid-template-columns':
@@ -3128,6 +3460,12 @@ function alignKeyword(value: string): string | null {
3128
3460
  return 'flex-end';
3129
3461
  case 'first baseline':
3130
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';
3131
3469
  case 'center':
3132
3470
  return 'center';
3133
3471
  case 'stretch':
@@ -3799,7 +4137,7 @@ function backgroundImageOf(
3799
4137
  ctx: UnitContext,
3800
4138
  ): BackgroundImage | undefined {
3801
4139
  const v = text.trim();
3802
- if (IMAGE_FUNCTION.test(v.toLowerCase())) return parseLinearGradient(v, ctx);
4140
+ if (IMAGE_FUNCTION.test(v.toLowerCase())) return parseGradient(v, ctx);
3803
4141
  return parseUrl(v);
3804
4142
  }
3805
4143
 
@@ -3821,7 +4159,7 @@ function layerValues<T>(
3821
4159
  interface BackgroundLayer {
3822
4160
  color: string | null;
3823
4161
  image: string | null;
3824
- gradient: LinearGradient | null;
4162
+ gradient: Gradient | null;
3825
4163
  repeat: ComputedStyle['backgroundRepeat'];
3826
4164
  size: ComputedStyle['backgroundSize'];
3827
4165
  attachment: ComputedStyle['backgroundAttachment'];
@@ -3837,7 +4175,7 @@ interface BackgroundLayer {
3837
4175
  * is dropped rather than resetting the background it meant to replace
3838
4176
  * (CSS 2.1 4.2). What CSS3 adds is read too, so that a declaration a
3839
4177
  * browser keeps is kept: `/ cover` after the position, `space` and `round`,
3840
- * a gradient — drawn as nothing, over the layer's colour.
4178
+ * and a gradient, over the layer's colour.
3841
4179
  */
3842
4180
  function readBackgroundLayer(
3843
4181
  text: string,
@@ -3872,7 +4210,7 @@ function readBackgroundLayer(
3872
4210
  const url = parseUrl(part);
3873
4211
  if (url === undefined) return null;
3874
4212
  layer.image = url;
3875
- } else if (v !== 'none') layer.gradient = parseLinearGradient(part, ctx);
4213
+ } else if (v !== 'none') layer.gradient = parseGradient(part, ctx);
3876
4214
  i += 1;
3877
4215
  } else if (v === 'repeat-x' || v === 'repeat-y' || REPEATS.has(v)) {
3878
4216
  if (!once(2)) return null;
@@ -3936,10 +4274,8 @@ function readBackgroundLayer(
3936
4274
 
3937
4275
  /**
3938
4276
  * A `linear-gradient()`, or null for any other image function and for one
3939
- * that is no gradient: a direction by angle, by side or by corner, with a
3940
- * colour interpolation method (`in oklab`, which Tailwind 4 writes) read and
3941
- * not honoured, and at least two stops, each with no position, one, or two;
3942
- * 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.
3943
4279
  */
3944
4280
  function parseLinearGradient(
3945
4281
  text: string,
@@ -3948,14 +4284,13 @@ function parseLinearGradient(
3948
4284
  const m = /^linear-gradient\((.*)\)$/is.exec(text.trim());
3949
4285
  if (!m) return null;
3950
4286
  const args = splitCommas(m[1]).map((a) => a.trim());
3951
- const out: LinearGradient = { angle: Math.PI, corner: null, stops: [] };
3952
- const first = (args[0] ?? '')
3953
- .toLowerCase()
3954
- .replace(
3955
- /\bin\s+[a-z-]+(?:\s+(?:shorter|longer|increasing|decreasing)\s+hue)?/,
3956
- '',
3957
- )
3958
- .trim();
4287
+ const out: LinearGradient = {
4288
+ kind: 'linear',
4289
+ angle: Math.PI,
4290
+ corner: null,
4291
+ stops: [],
4292
+ };
4293
+ const first = withoutInterpolation(args[0] ?? '');
3959
4294
  let from = 0;
3960
4295
  if (!first || first.startsWith('to ')) {
3961
4296
  from = 1;
@@ -3991,7 +4326,120 @@ function parseLinearGradient(
3991
4326
  from = 1;
3992
4327
  }
3993
4328
  }
3994
- 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) {
3995
4443
  const parts = splitValue(arg);
3996
4444
  const color = parseColor(parts[0] ?? '');
3997
4445
  if (color === null) {
@@ -4002,14 +4450,19 @@ function parseLinearGradient(
4002
4450
  if (parts.length > 3) return null;
4003
4451
  const at = parts.slice(1).map((p) => parseLength(p, ctx));
4004
4452
  if (at.some((p) => p === null || p === AUTO)) return null;
4005
- if (!at.length) out.stops.push({ color, at: null });
4006
- 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 });
4007
4455
  }
4008
- 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);
4009
4462
  }
4010
4463
 
4011
4464
  /** The images CSS3 has beyond `url()`, which a layer may name: a linear
4012
- * gradient is drawn, and the rest draw as nothing. */
4465
+ * and a radial gradient are drawn, and the rest draw as nothing. */
4013
4466
  const IMAGE_FUNCTION =
4014
4467
  /^(?:-(?:webkit|moz|o|ms)-)?(?:(?:repeating-)?(?:linear|radial|conic)-gradient|gradient|image-set|cross-fade|element|paint)\(/;
4015
4468
 
@@ -4050,87 +4503,6 @@ function whiteSpaceOf(collapse: Collapse, wrap: boolean): WhiteSpace {
4050
4503
  return 'pre-line';
4051
4504
  }
4052
4505
 
4053
- /**
4054
- * The translation a `transform` makes: the sum of its translate functions'
4055
- * and its matrices' — `translate(-50%, -50%)`, Tailwind 3's `translate(
4056
- * var(--tw-translate-x), var(--tw-translate-y)) rotate(…) skewX(…) …` —
4057
- * with the functions that rotate, scale, skew or add perspective read and
4058
- * not drawn. Null for `none`, and undefined for what is no transform list,
4059
- * which drops the declaration.
4060
- */
4061
- function transformTranslation(
4062
- value: string,
4063
- ctx: UnitContext,
4064
- ): [Len, Len] | null | undefined {
4065
- if (value.trim().toLowerCase() === 'none') return null;
4066
- let x: Len = 0;
4067
- let y: Len = 0;
4068
- const parts = splitValue(value);
4069
- if (!parts.length) return undefined;
4070
- for (const part of parts) {
4071
- const m = /^([a-z0-9]+)\((.*)\)$/is.exec(part.trim());
4072
- if (!m) return undefined;
4073
- const name = m[1].toLowerCase();
4074
- const args = splitCommas(m[2]).map((a) => a.trim());
4075
- let dx: Len | null = 0;
4076
- let dy: Len | null = 0;
4077
- if (name === 'translate' || name === 'translate3d') {
4078
- dx = parseLength(args[0] ?? '', ctx);
4079
- dy = args[1] === undefined ? 0 : parseLength(args[1], ctx);
4080
- } else if (name === 'translatex') dx = parseLength(args[0] ?? '', ctx);
4081
- else if (name === 'translatey') dy = parseLength(args[0] ?? '', ctx);
4082
- else if (name === 'matrix' || name === 'matrix3d') {
4083
- // the translation is the last column's
4084
- const n = args.map(Number);
4085
- if (n.length !== (name === 'matrix' ? 6 : 16) || n.some(isNaN)) {
4086
- return undefined;
4087
- }
4088
- [dx, dy] = name === 'matrix' ? [n[4], n[5]] : [n[12], n[13]];
4089
- } else if (!TRANSFORMS.has(name)) return undefined;
4090
- if (dx === null || dx === AUTO || dy === null || dy === AUTO) {
4091
- return undefined;
4092
- }
4093
- const sx = addLen(x, dx);
4094
- const sy = addLen(y, dy);
4095
- if (sx === null || sy === null) return undefined;
4096
- x = sx;
4097
- y = sy;
4098
- }
4099
- return [x, y];
4100
- }
4101
-
4102
- /** The transform functions read and not drawn. */
4103
- const TRANSFORMS = new Set([
4104
- 'translatez',
4105
- 'rotate',
4106
- 'rotatex',
4107
- 'rotatey',
4108
- 'rotatez',
4109
- 'rotate3d',
4110
- 'scale',
4111
- 'scalex',
4112
- 'scaley',
4113
- 'scalez',
4114
- 'scale3d',
4115
- 'skew',
4116
- 'skewx',
4117
- 'skewy',
4118
- 'perspective',
4119
- ]);
4120
-
4121
- /** Two lengths added, a percentage and a length kept apart; null where
4122
- * one is a `min()` or a `max()`, which a sum cannot be kept of. */
4123
- function addLen(a: Len, b: Len): Len | null {
4124
- if (a === AUTO || b === AUTO) return null;
4125
- if (typeof a === 'number' && typeof b === 'number') return a + b;
4126
- if (a === 0) return b;
4127
- if (b === 0) return a;
4128
- const pa: Pct = typeof a === 'number' ? { pct: 0, px: a } : a;
4129
- const pb: Pct = typeof b === 'number' ? { pct: 0, px: b } : b;
4130
- if (pa.of || pb.of) return null;
4131
- return { pct: pa.pct + pb.pct, px: (pa.px ?? 0) + (pb.px ?? 0) };
4132
- }
4133
-
4134
4506
  /** An intrinsic size keyword, with the prefixes browsers still read, or
4135
4507
  * `fit-content()` of a length or a percentage (CSS Sizing 3, 3.1) — a
4136
4508
  * width's, where a height's is its content height as the keyword is;
@@ -4769,6 +5141,33 @@ function splitTopLevelSlash(value: string): string[] {
4769
5141
 
4770
5142
  // --- logical properties ----------------------------------------------------
4771
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
+
4772
5171
  const OBJECT_FITS = new Set(['fill', 'contain', 'cover', 'none', 'scale-down']);
4773
5172
 
4774
5173
  /**
@@ -4860,6 +5259,29 @@ function radiusOf(text: string, ctx: UnitContext): Len | null {
4860
5259
  return len.pct < 0 && !len.px && !len.of ? null : len;
4861
5260
  }
4862
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
+
4863
5285
  /** Whether two corners' radii are the same values. */
4864
5286
  function sameRadii(a: readonly Len[], b: readonly Len[]): boolean {
4865
5287
  for (let i = 0; i < 4; i += 1) {
@@ -5102,10 +5524,15 @@ const INHERITED_NAMES = new Set<string>([
5102
5524
  'list-style-position',
5103
5525
  'list-style-image',
5104
5526
  'cursor',
5527
+ 'pointer-events',
5528
+ 'fill',
5529
+ 'stroke',
5105
5530
  'border-collapse',
5106
5531
  'border-spacing',
5107
5532
  'empty-cells',
5108
5533
  'quotes',
5534
+ 'orphans',
5535
+ 'widows',
5109
5536
  ]);
5110
5537
 
5111
5538
  export function isInherited(name: string): boolean {
@@ -5260,6 +5687,9 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
5260
5687
  'list-style-position': ['listStylePosition'],
5261
5688
  'list-style-image': ['listStyleImage'],
5262
5689
  cursor: ['cursor'],
5690
+ 'pointer-events': ['pointerEvents'],
5691
+ fill: ['fill'],
5692
+ stroke: ['stroke'],
5263
5693
  'border-collapse': ['borderCollapse'],
5264
5694
  'caption-side': ['captionSide'],
5265
5695
  'empty-cells': ['emptyCells'],
@@ -5328,9 +5758,16 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
5328
5758
  '-webkit-box-pack': ['webkitBoxPack'],
5329
5759
  '-webkit-box-align': ['webkitBoxAlign'],
5330
5760
  '-webkit-box-flex': ['webkitBoxFlex'],
5331
- columns: ['columns'],
5332
- 'column-count': ['columns'],
5333
- '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'],
5334
5771
  'text-overflow': ['textOverflow'],
5335
5772
  // a flex box's and its items', and a grid's
5336
5773
  'flex-direction': ['flexDirection'],
@@ -5345,12 +5782,12 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
5345
5782
  'flex-shrink': ['flexShrink'],
5346
5783
  'flex-basis': ['flexBasis'],
5347
5784
  order: ['order'],
5348
- gap: ['rowGap', 'columnGap'],
5785
+ gap: ['rowGap', 'columnGap', 'columnGapNormal'],
5349
5786
  'row-gap': ['rowGap'],
5350
- 'column-gap': ['columnGap'],
5351
- 'grid-gap': ['rowGap', 'columnGap'],
5787
+ 'column-gap': ['columnGap', 'columnGapNormal'],
5788
+ 'grid-gap': ['rowGap', 'columnGap', 'columnGapNormal'],
5352
5789
  'grid-row-gap': ['rowGap'],
5353
- 'grid-column-gap': ['columnGap'],
5790
+ 'grid-column-gap': ['columnGap', 'columnGapNormal'],
5354
5791
  'grid-template-columns': ['gridColumns'],
5355
5792
  'grid-template-rows': ['gridRows'],
5356
5793
  'grid-auto-rows': ['gridAutoRows'],
@@ -5445,11 +5882,18 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
5445
5882
  clear: ['clear'],
5446
5883
  overflow: ['overflowX', 'overflowY'],
5447
5884
  clip: ['clip'],
5885
+ 'clip-path': ['clipPath'],
5886
+ '-webkit-clip-path': ['clipPath'],
5448
5887
  'overflow-x': ['overflowX'],
5449
5888
  'overflow-y': ['overflowY'],
5450
5889
  opacity: ['opacity'],
5451
5890
  translate: ['translate'],
5452
- transform: ['transformTranslate'],
5891
+ rotate: ['rotate'],
5892
+ scale: ['scale'],
5893
+ transform: ['transform'],
5894
+ '-webkit-transform': ['transform'],
5895
+ 'transform-origin': ['transformOrigin'],
5896
+ '-webkit-transform-origin': ['transformOrigin'],
5453
5897
  'z-index': ['zIndex'],
5454
5898
  'vertical-align': ['verticalAlign'],
5455
5899
  'text-decoration': [
@@ -5512,7 +5956,7 @@ export function decorate(style: ComputedStyle): void {
5512
5956
  style.color,
5513
5957
  );
5514
5958
  style.underlineStyle = style.textDecorationStyle;
5515
- style.underlineThickness = style.textDecorationThickness;
5959
+ style.underlineThickness = usedThickness(style);
5516
5960
  style.underlineOffset = style.textUnderlineOffset;
5517
5961
  }
5518
5962
  if (lines.includes('line-through')) {
@@ -5520,9 +5964,30 @@ export function decorate(style: ComputedStyle): void {
5520
5964
  style.textDecorationColor ?? 'currentColor',
5521
5965
  style.color,
5522
5966
  );
5967
+ style.lineThroughStyle = style.textDecorationStyle;
5968
+ style.lineThroughThickness = usedThickness(style);
5523
5969
  }
5524
5970
  }
5525
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
+
5526
5991
  /**
5527
5992
  * The absolutely positioned styles that were inline-level before they were
5528
5993
  * made blocks: where such a box would have been in flow is on its line,
@@ -5638,3 +6103,55 @@ export function blockify(style: ComputedStyle, inFlexContainer: boolean): void {
5638
6103
  return;
5639
6104
  }
5640
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
+ ]);