@react-x11/components 0.14.0 → 0.16.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 (97) hide show
  1. package/dist/code-editor/node.d.ts +13 -0
  2. package/dist/code-editor/node.d.ts.map +1 -1
  3. package/dist/code-editor/node.js +23 -5
  4. package/dist/code-editor/node.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +209 -23
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +873 -175
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +25 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +307 -19
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/content.d.ts +11 -6
  14. package/dist/html/css/content.d.ts.map +1 -1
  15. package/dist/html/css/content.js +171 -12
  16. package/dist/html/css/content.js.map +1 -1
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +62 -9
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/shapes.d.ts +62 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -0
  22. package/dist/html/css/shapes.js +214 -0
  23. package/dist/html/css/shapes.js.map +1 -0
  24. package/dist/html/css/style.d.ts +42 -5
  25. package/dist/html/css/style.d.ts.map +1 -1
  26. package/dist/html/css/style.js +75 -27
  27. package/dist/html/css/style.js.map +1 -1
  28. package/dist/html/css/ua.js +15 -0
  29. package/dist/html/css/ua.js.map +1 -1
  30. package/dist/html/css/values.d.ts +5 -0
  31. package/dist/html/css/values.d.ts.map +1 -1
  32. package/dist/html/css/values.js +18 -9
  33. package/dist/html/css/values.js.map +1 -1
  34. package/dist/html/index.d.ts.map +1 -1
  35. package/dist/html/index.js +7 -0
  36. package/dist/html/index.js.map +1 -1
  37. package/dist/html/layout/block.d.ts +38 -7
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +160 -50
  40. package/dist/html/layout/block.js.map +1 -1
  41. package/dist/html/layout/boxes.d.ts +29 -2
  42. package/dist/html/layout/boxes.d.ts.map +1 -1
  43. package/dist/html/layout/boxes.js +159 -20
  44. package/dist/html/layout/boxes.js.map +1 -1
  45. package/dist/html/layout/css-grid.d.ts.map +1 -1
  46. package/dist/html/layout/css-grid.js +69 -19
  47. package/dist/html/layout/css-grid.js.map +1 -1
  48. package/dist/html/layout/flex.d.ts.map +1 -1
  49. package/dist/html/layout/flex.js +1648 -225
  50. package/dist/html/layout/flex.js.map +1 -1
  51. package/dist/html/layout/inline.d.ts +15 -3
  52. package/dist/html/layout/inline.d.ts.map +1 -1
  53. package/dist/html/layout/inline.js +23 -8
  54. package/dist/html/layout/inline.js.map +1 -1
  55. package/dist/html/layout/table.d.ts +27 -1
  56. package/dist/html/layout/table.d.ts.map +1 -1
  57. package/dist/html/layout/table.js +907 -323
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +36 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +174 -37
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts +4 -1
  64. package/dist/html/paint.d.ts.map +1 -1
  65. package/dist/html/paint.js +769 -204
  66. package/dist/html/paint.js.map +1 -1
  67. package/dist/html/resources.js +14 -1
  68. package/dist/html/resources.js.map +1 -1
  69. package/dist/html/svg.d.ts +50 -17
  70. package/dist/html/svg.d.ts.map +1 -1
  71. package/dist/html/svg.js +501 -83
  72. package/dist/html/svg.js.map +1 -1
  73. package/dist/html/widgets.d.ts.map +1 -1
  74. package/dist/html/widgets.js +57 -4
  75. package/dist/html/widgets.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/code-editor/node.ts +24 -5
  78. package/src/html/css/cascade.ts +987 -176
  79. package/src/html/css/color.ts +345 -18
  80. package/src/html/css/content.ts +173 -15
  81. package/src/html/css/parse.ts +60 -8
  82. package/src/html/css/shapes.ts +273 -0
  83. package/src/html/css/style.ts +123 -30
  84. package/src/html/css/ua.ts +15 -0
  85. package/src/html/css/values.ts +23 -9
  86. package/src/html/index.ts +7 -0
  87. package/src/html/layout/block.ts +169 -56
  88. package/src/html/layout/boxes.ts +198 -24
  89. package/src/html/layout/css-grid.ts +89 -22
  90. package/src/html/layout/flex.ts +1863 -219
  91. package/src/html/layout/inline.ts +27 -7
  92. package/src/html/layout/table.ts +989 -333
  93. package/src/html/node.ts +204 -37
  94. package/src/html/paint.ts +999 -252
  95. package/src/html/resources.ts +13 -1
  96. package/src/html/svg.ts +600 -78
  97. package/src/html/widgets.ts +61 -4
@@ -16,7 +16,7 @@
16
16
  // reason richtext's node gives for answering from what it draws: an index
17
17
  // built by a second traversal can disagree with the boxes, and a selection
18
18
  // that disagrees with the glyphs is worse than no selection.
19
- import type { Element } from 'domhandler';
19
+ import type { ChildNode, Element } from 'domhandler';
20
20
 
21
21
  import type { TextRun } from '../../richtext/index.js';
22
22
  import {
@@ -28,6 +28,7 @@ import {
28
28
  NON_RENDERED,
29
29
  tagOf,
30
30
  } from '../dom.js';
31
+ import { ShapeCopies, languageOf, metaColorScheme } from '../css/cascade.js';
31
32
  import type {
32
33
  Cascade,
33
34
  FirstLetterRules,
@@ -38,6 +39,7 @@ import type { CollapsedTable } from './collapse.js';
38
39
  import { quoteAt } from '../css/content.js';
39
40
  import type { CounterStyles } from '../css/counter-styles.js';
40
41
  import type { ContentItem } from '../css/content.js';
42
+ import type { ShapeStyles } from '../css/shapes.js';
41
43
  import {
42
44
  CONTAIN_STYLE,
43
45
  FIRST_LINE_INHERITED,
@@ -47,7 +49,7 @@ import {
47
49
  inherit,
48
50
  } from '../css/style.js';
49
51
  import { AUTO } from '../css/values.js';
50
- import { svgIntrinsics } from '../svg.js';
52
+ import { concreteSize, svgIntrinsics } from '../svg.js';
51
53
  import type { IntrinsicSize } from '../svg.js';
52
54
  import type { ComputedStyle } from '../css/style.js';
53
55
  import type { GridLines } from './grid-lines.js';
@@ -81,15 +83,32 @@ export interface Marker {
81
83
  /** Its own style where a `::marker` rule gives it one; the item's where
82
84
  * none does. */
83
85
  style: ComputedStyle | null;
84
- /** The image it is, `list-style-image`'s, at its size in device pixels,
85
- * where that has arrived; its text is then not drawn. */
86
- image?: { url: string; width: number; height: number };
86
+ /** The image it is, `list-style-image`'s, where that has arrived, at its
87
+ * size in device pixels — its own, or the one a marker image with none
88
+ * is given (`Builder._markerImageSize`); its text is then not drawn.
89
+ * `gap` is the room between it and the content, in device pixels
90
+ * (`MARKER_IMAGE_GAP`). */
91
+ image?: { url: string; width: number; height: number; gap: number };
87
92
  /** Its text is its own — a string `list-style-type`, a `::marker`'s
88
93
  * `content` — and ends at the content's edge, with no gap a number
89
94
  * or a bullet is set apart by. */
90
95
  flush?: true;
91
96
  }
92
97
 
98
+ /**
99
+ * The room between a list marker's image and the item's content, in CSS
100
+ * pixels: between the image and the content's edge outside the item, and
101
+ * after the image inside it. CSS 2.1 (12.5.1) leaves where a marker goes
102
+ * to the user agent, and this is Blink's `kCMarkerPaddingPx`
103
+ * (`ListMarker::InlineMarginsForOutside` and `…ForInside`), which WebKit
104
+ * has too. Outside, ours was the gap a bullet has, 0.4em — 16 pixels at
105
+ * 40px text where Chrome has 7 — and inside the space a marker's text ends
106
+ * in, a space's width. Blink writes it into the marker's margins unzoomed,
107
+ * so at a device scale of 2 Chrome's is 7 device pixels; here it is 7 CSS
108
+ * pixels at any scale, so that a page at 2x is the page at 1x doubled.
109
+ */
110
+ const MARKER_IMAGE_GAP = 7;
111
+
93
112
  /** An out-of-flow box's static position, as an offset from the box whose
94
113
  * flow it was taken from, which may yet move. */
95
114
  export interface StaticPosition {
@@ -713,6 +732,15 @@ export const COLUMN_ROWS = new WeakMap<
713
732
  * that would ask the three maps above of every box ask first. */
714
733
  export const columned = { any: false };
715
734
 
735
+ /** What the document's rules give the elements inside an inline `<svg>`
736
+ * (`Cascade.shapeStyles`), null where they give none. */
737
+ export type ShapeStyler = (box: Box) => ShapeStyles | null;
738
+
739
+ /** What `BoxTree.shapeStyler` answered for each drawing's box it has been
740
+ * asked of, which are the ones painted: a box restyled where it is has
741
+ * its answer put here again (`HtmlViewNode._hoverInPlace`). */
742
+ export const SHAPE_STYLES = new WeakMap<Box, ShapeStyles | null>();
743
+
716
744
  /** The element a pseudo-element's box is generated from, which has no
717
745
  * element of its own (`Box.pseudo`): whose the images its styles name are,
718
746
  * when the host is asked for them. */
@@ -774,6 +802,16 @@ export interface BoxTree {
774
802
  * one: worked out as a selection is first painted over it, not as the
775
803
  * tree is built, since most builds are never selected in. */
776
804
  selectionStyler: SelectionStyler | null;
805
+ /** What the rules give the elements in each inline `<svg>`
806
+ * (`ShapeStyler`), which have no boxes and no styles, where any rule
807
+ * could reach one: worked out as a drawing is first painted, since a
808
+ * page of icons shows few of them at once. */
809
+ shapeStyler: ShapeStyler | null;
810
+ /** What the drawings of this build share in `Cascade.shapeStyles`: the
811
+ * document's elements by their ids, indexed when first asked, where a
812
+ * `<use>` finds the icon a sprite has for it; and what the rules matched
813
+ * in each copy a `<use>` makes. Null where `shapeStyler` is. */
814
+ shapeCopies: ShapeCopies | null;
777
815
  /** Whether a float or an out-of-flow box sits in an inline box: where
778
816
  * none does, layout looks for them among a block's own children and
779
817
  * goes through no inline box to find them. */
@@ -811,6 +849,10 @@ export interface BuildOptions {
811
849
  imageSize(el: Element): IntrinsicSize | null;
812
850
  /** The same for an image named by url — generated content's. */
813
851
  urlSize?(url: string): IntrinsicSize | null;
852
+ /** How far a style's first face reaches above its baseline, in device
853
+ * pixels: what a list marker's image with no size of its own is sized
854
+ * by (`_markerImageSize`). Where it is not known, 0.8em. */
855
+ faceAscent?(style: ComputedStyle): number | undefined;
814
856
  /** The size a real widget wants, so the box in the flow is the size the
815
857
  * control will be drawn at. */
816
858
  controlSize(
@@ -832,6 +874,28 @@ export function buildBoxes(
832
874
  return builder.run(root as Element);
833
875
  }
834
876
 
877
+ /** A document's first element with an id: one walk of it, the first time
878
+ * an id is asked for (`ShapeCopies`). */
879
+ function idIndex(root: Element): (id: string) => Element | null {
880
+ let index: Map<string, Element> | null = null;
881
+ return (id) => {
882
+ if (!index) {
883
+ index = new Map();
884
+ const stack: ChildNode[] = [...root.children].reverse();
885
+ for (let node = stack.pop(); node; node = stack.pop()) {
886
+ if (!isElement(node)) continue;
887
+ const own = node.attribs.id;
888
+ if (own && !index.has(own)) index.set(own, node);
889
+ const children = node.children;
890
+ for (let i = children.length - 1; i >= 0; i -= 1) {
891
+ stack.push(children[i]);
892
+ }
893
+ }
894
+ }
895
+ return index.get(id) ?? null;
896
+ };
897
+ }
898
+
835
899
  /**
836
900
  * How deep the box tree may go. Everything downstream of the builder — the
837
901
  * fix-up pass, layout, paint, the accessor walks — recurses on box depth, so
@@ -839,8 +903,17 @@ export function buildBoxes(
839
903
  * output, a runaway template) from a stack overflow five phases later.
840
904
  * Blink's parser flattens at 512 for the same reason; content past the cap
841
905
  * is dropped, which beats the alternative of crashing the application.
906
+ *
842
907
  * The parser keeps a document to 256 elements (`dom.ts`), so what reaches
843
- * this now is the anonymous boxes a table builds round each of them.
908
+ * this is the anonymous boxes a table builds round them, which the builder
909
+ * counts as it goes (`wrappersAround`), since the fix-up makes them after
910
+ * it. Counted as elements, a `display: table-cell` box in another was one
911
+ * level where it is four boxes — a table, a row group, a row and the cell
912
+ * — and every pass pays for a box: painting one takes some 800 bytes of
913
+ * the stack. Two hundred and fifty cells deep, a thousand boxes, layout
914
+ * and paint each ran out of the 984 KB V8 gives the main thread, and the
915
+ * document came out blank. 512 boxes paint in under half of it, and are a
916
+ * cell in a cell 128 deep.
844
917
  */
845
918
  const MAX_DEPTH = 512;
846
919
 
@@ -901,11 +974,15 @@ class Builder {
901
974
  run(root: Element): BoxTree {
902
975
  const cascade = this._options.cascade;
903
976
  cascade.beginSharing(this._options.kept ?? null);
977
+ cascade.pageColorScheme = metaColorScheme(root);
904
978
  const { style: rootStyle, html: impliedHtml } = cascade.rootStyle(
905
979
  hasBody(root),
906
980
  hasHtml(root),
907
981
  );
908
982
  const rootBox = new Box('block', null, rootStyle);
983
+ // the root standing in for a `<body>` has its background clipped to
984
+ // the document's text as the body's would be
985
+ if (rootStyle.backgroundClipText) this._clipText = true;
909
986
  // a fragment's root stands in for a `<body>`, counters and all
910
987
  this._scopes.open();
911
988
  this._counterChanges(rootStyle, {
@@ -923,6 +1000,7 @@ class Builder {
923
1000
  const anonymous = anonymousStyles(cascade.initial);
924
1001
  fixUp(rootBox, anonymous);
925
1002
  assignSubtreeRanges(rootBox);
1003
+ const copies = cascade.stylesShapes ? new ShapeCopies(idIndex(root)) : null;
926
1004
  return {
927
1005
  root: rootBox,
928
1006
  anonymous,
@@ -939,6 +1017,8 @@ class Builder {
939
1017
  firstLine: this._firstLine,
940
1018
  firstLineStyler: this._firstLine ? this._firstLineStyler() : null,
941
1019
  selectionStyler: cascade.hasSelection ? this._selectionStyler() : null,
1020
+ shapeStyler: copies ? this._shapeStyler(copies) : null,
1021
+ shapeCopies: copies,
942
1022
  nestedOutOfLine: this._nestedOutOfLine,
943
1023
  movedInline: this._movedInline,
944
1024
  clipText: this._clipText,
@@ -971,6 +1051,20 @@ class Builder {
971
1051
  };
972
1052
  }
973
1053
 
1054
+ /** `BoxTree.shapeStyler`, over this build's cascade: each drawing's
1055
+ * once, from its box's style as it is when first asked. */
1056
+ private _shapeStyler(copies: ShapeCopies): ShapeStyler {
1057
+ const cascade = this._options.cascade;
1058
+ return (box) => {
1059
+ let shapes = SHAPE_STYLES.get(box);
1060
+ if (shapes === undefined) {
1061
+ shapes = box.el ? cascade.shapeStyles(box.el, box.style, copies) : null;
1062
+ SHAPE_STYLES.set(box, shapes);
1063
+ }
1064
+ return shapes;
1065
+ };
1066
+ }
1067
+
974
1068
  /** `BoxTree.firstLineStyler`, over this build's cascade. */
975
1069
  private _firstLineStyler(): FirstLineStyler {
976
1070
  const cascade = this._options.cascade;
@@ -1160,8 +1254,10 @@ class Builder {
1160
1254
  return;
1161
1255
  }
1162
1256
 
1163
- if (this._depth >= MAX_DEPTH) return;
1164
1257
  const kind = boxKindFor(style.display);
1258
+ // the box, and the anonymous boxes the fix-up will put round it
1259
+ const depth = 1 + wrappersAround(into.kind, kind, style);
1260
+ if (this._depth + depth > MAX_DEPTH) return;
1165
1261
  const box = new Box(kind, el, style);
1166
1262
  into.append(box);
1167
1263
  if (
@@ -1191,7 +1287,7 @@ class Builder {
1191
1287
  let markerStyle: ComputedStyle | null = null;
1192
1288
  let ownMarker = false;
1193
1289
  let markerImage: { url: string; size: IntrinsicSize } | null = null;
1194
- let insideImage: string | null = null;
1290
+ let insideImage: NonNullable<Marker['image']> | null = null;
1195
1291
  if (style.display === 'list-item' && style.listStyleImage) {
1196
1292
  // asked for as generated content's images are, and the tree built
1197
1293
  // again when it arrives; until it has, and where it never does, the
@@ -1226,20 +1322,21 @@ class Builder {
1226
1322
  flush = true;
1227
1323
  }
1228
1324
  if (markerImage && !ownMarker) {
1229
- if (style.listStylePosition === 'inside') insideImage = markerImage.url;
1325
+ const [width, height] = this._markerImageSize(
1326
+ markerImage.size,
1327
+ markerStyle ?? style,
1328
+ );
1329
+ const gap = MARKER_IMAGE_GAP * (this._options.scale ?? 1);
1330
+ const image = { url: markerImage.url, width, height, gap };
1331
+ if (style.listStylePosition === 'inside') insideImage = image;
1230
1332
  else {
1231
- const scale = this._options.scale ?? 1;
1232
1333
  box.marker = {
1233
1334
  text: '',
1234
1335
  layout: null,
1235
1336
  x: 0,
1236
1337
  y: 0,
1237
1338
  style: markerStyle,
1238
- image: {
1239
- url: markerImage.url,
1240
- width: (markerImage.size.width ?? 0) * scale,
1241
- height: (markerImage.size.height ?? 0) * scale,
1242
- },
1339
+ image,
1243
1340
  };
1244
1341
  }
1245
1342
  } else if (text && style.listStylePosition === 'inside') {
@@ -1272,19 +1369,23 @@ class Builder {
1272
1369
  : null;
1273
1370
  const ownLetter = ownRules ? { rules: ownRules, punctuation: [] } : null;
1274
1371
  if (ownLetter) this._firstLetter = ownLetter;
1275
- this._depth += 1;
1372
+ this._depth += depth;
1276
1373
  if (insideImage) {
1277
1374
  // an inline image at the start of the first line, as a generated
1278
- // image is, and the space a marker's text ends in
1279
- this._contentImage(insideImage, box, style, el);
1280
- this._textNode(' ', box, style, el);
1375
+ // image is, and the room after it Blink gives it rather than a
1376
+ // space: the content's own white space after it is kept, as it is
1377
+ // after an image (`MARKER_IMAGE_GAP`)
1378
+ const { url, gap } = insideImage;
1379
+ const image = this._contentImage(url, box, style, el, insideImage);
1380
+ if (style.direction === 'rtl') image.style.marginLeft = gap;
1381
+ else image.style.marginRight = gap;
1281
1382
  } else if (insideMarker) {
1282
1383
  this._insideMarker(insideMarker, style, markerStyle, box, el);
1283
1384
  }
1284
1385
  this._pseudo(el, 'before', style, box);
1285
1386
  this._children(el, box, style, childInFlex, el, key);
1286
1387
  this._pseudo(el, 'after', style, box);
1287
- this._depth -= 1;
1388
+ this._depth -= depth;
1288
1389
  this._letterAfter(flow, skipped, outerLetter, ownLetter);
1289
1390
  if (flow !== 'inline') this._endLine();
1290
1391
  this._ws = after(flow, this._ws, around);
@@ -1724,14 +1825,14 @@ class Builder {
1724
1825
  .join(item.separator);
1725
1826
  break;
1726
1827
  case 'open-quote':
1727
- text += quoteAt(style.quotes, this._quoteDepth, 0);
1828
+ text += quoteAt(style.quotes, this._quoteDepth, 0, languageOf(el));
1728
1829
  this._quoteDepth += 1;
1729
1830
  break;
1730
1831
  case 'close-quote':
1731
1832
  // a close with nothing open writes nothing and closes nothing
1732
1833
  if (this._quoteDepth > 0) {
1733
1834
  this._quoteDepth -= 1;
1734
- text += quoteAt(style.quotes, this._quoteDepth, 1);
1835
+ text += quoteAt(style.quotes, this._quoteDepth, 1, languageOf(el));
1735
1836
  }
1736
1837
  break;
1737
1838
  case 'no-open-quote':
@@ -1757,7 +1858,10 @@ class Builder {
1757
1858
  into: Box,
1758
1859
  style: ComputedStyle,
1759
1860
  owner: Element,
1760
- ): void {
1861
+ /** Its size where it is a list marker's, which has one whatever the
1862
+ * image says (`_markerImageSize`). */
1863
+ marker?: { width: number; height: number },
1864
+ ): Box {
1761
1865
  const box = new Box('replaced', null, {
1762
1866
  ...inherit(style, this._options.cascade.initial),
1763
1867
  display: 'inline',
@@ -1767,11 +1871,40 @@ class Builder {
1767
1871
  CONTENT_IMAGES.set(box, url);
1768
1872
  const size = this._options.urlSize?.(url) ?? null;
1769
1873
  this._contentImages.push({ url, element: owner, sized: !!size });
1770
- if (size) setIntrinsics(box, size, this._options.scale ?? 1);
1874
+ if (marker) {
1875
+ const { width, height } = marker;
1876
+ const ratio = width > 0 && height > 0 ? width / height : 0;
1877
+ box.intrinsic = { width, height, missing: 0, ratio };
1878
+ } else if (size) setIntrinsics(box, size, this._options.scale ?? 1);
1771
1879
  else box.intrinsic = { width: 0, height: 0, missing: 0, ratio: 0 };
1772
1880
  this._ws = after('atomic', this._ws, this._ws);
1773
1881
  this._lettered = false;
1774
1882
  this._abandonLetter();
1883
+ return box;
1884
+ }
1885
+
1886
+ /**
1887
+ * A list marker image's size, in device pixels: its own where it has one,
1888
+ * and where it lacks some — an SVG with only a `viewBox`, or with no size
1889
+ * and no ratio — CSS Images' default sizing in a square half its face's
1890
+ * ascent across. CSS Lists 3 (3.3) makes the square 1em, which both
1891
+ * engines find too large (w3c/csswg-drafts#4207): Blink's is half the
1892
+ * ascent, rounded to a whole pixel first
1893
+ * (`LayoutListMarkerImage::DefaultSize`), Gecko's 0.4em, and this is
1894
+ * Blink's. A `viewBox` alone came to 0 by 0 and drew no marker at all,
1895
+ * and inside the item to the 300 by 150 a generated image defaults to.
1896
+ */
1897
+ private _markerImageSize(
1898
+ size: IntrinsicSize,
1899
+ style: ComputedStyle,
1900
+ ): [number, number] {
1901
+ let side = 0;
1902
+ // CoreText reads a size of 0 as its default twelve points
1903
+ if ((size.width === null || size.height === null) && style.fontSize > 0) {
1904
+ const ascent = this._options.faceAscent?.(style) ?? style.fontSize * 0.8;
1905
+ side = Math.round(ascent) / 2;
1906
+ }
1907
+ return concreteSize(size, side, side, this._options.scale ?? 1);
1775
1908
  }
1776
1909
 
1777
1910
  /** A text node, whitespace-processed per the inherited `white-space`. */
@@ -2437,6 +2570,47 @@ function after(
2437
2570
  }
2438
2571
  }
2439
2572
 
2573
+ /**
2574
+ * How many anonymous boxes the fix-up will put between a box of `kind` and
2575
+ * the box it is appended to, `parent` (`fixUpTable`, `wrapOrphans`,
2576
+ * `wrapTableParts`): a table part outside a table is given what it is
2577
+ * missing of one, and what is in a table and no part of it is given a
2578
+ * cell, a row and a row group. A cell in a block is in a table, a row group
2579
+ * and a row of its own, and a block in a row is in a cell. Counted against
2580
+ * `MAX_DEPTH`, which bounds the boxes and not the elements.
2581
+ */
2582
+ function wrappersAround(
2583
+ parent: BoxKind,
2584
+ kind: BoxKind,
2585
+ style: ComputedStyle,
2586
+ ): number {
2587
+ const column =
2588
+ style.display === 'table-column' || style.display === 'table-column-group';
2589
+ switch (parent) {
2590
+ case 'table':
2591
+ if (kind === 'table-row-group' || kind === 'table-caption' || column) {
2592
+ return 0;
2593
+ }
2594
+ return kind === 'table-row' ? 1 : kind === 'table-cell' ? 2 : 3;
2595
+ case 'table-row-group':
2596
+ return kind === 'table-row' ? 0 : kind === 'table-cell' ? 1 : 2;
2597
+ case 'table-row':
2598
+ return kind === 'table-cell' ? 0 : 1;
2599
+ }
2600
+ // a part out of flow is no part (`isTablePart`)
2601
+ if (
2602
+ style.position === 'absolute' ||
2603
+ style.position === 'fixed' ||
2604
+ style.float !== 'none'
2605
+ ) {
2606
+ return 0;
2607
+ }
2608
+ if (kind === 'table-row-group' || kind === 'table-caption' || column) {
2609
+ return 1;
2610
+ }
2611
+ return kind === 'table-row' ? 2 : kind === 'table-cell' ? 3 : 0;
2612
+ }
2613
+
2440
2614
  function boxKindFor(display: ComputedStyle['display']): BoxKind {
2441
2615
  switch (display) {
2442
2616
  case 'inline':
@@ -31,6 +31,7 @@ import { Box, GRID_TRACKS, isBlank } from './boxes.js';
31
31
  import {
32
32
  FLEXED_HEIGHT,
33
33
  MIN_CONTENT_PROBE,
34
+ STRETCHED_ACROSS,
34
35
  USED_HEIGHT,
35
36
  centreButton,
36
37
  clampHeight,
@@ -60,6 +61,9 @@ interface Item {
60
61
  /** Min-content and max-content widths, margin box, taken when asked. */
61
62
  min: number;
62
63
  max: number;
64
+ /** Whether it is a table stretched across its area (`STRETCHED_ACROSS`),
65
+ * which `layoutItem` decides. */
66
+ across: boolean;
63
67
  }
64
68
 
65
69
  /**
@@ -203,6 +207,11 @@ export function layoutGrid(
203
207
  const extra = style.boxSizing === 'border-box' ? 0 : box.horizontalExtra;
204
208
  widest = narrowest = Math.max(style.width + extra, box.horizontalExtra);
205
209
  } else {
210
+ // A percentage is of an area the columns are still being sized for,
211
+ // and what it comes to depends on what the item makes them: it is
212
+ // `auto` for that (CSS Sizing 3, 5.2.1), and the item's content says
213
+ // how wide it is. Measured at the width a percentage made of the
214
+ // probe's, an item `width: 100%` made its `auto` column nothing.
206
215
  const fresh = box.intrinsicMaxContent < 0;
207
216
  widest = maxContentOf(box, ctx);
208
217
  if (box.intrinsicMinContent < 0) {
@@ -210,8 +219,7 @@ export function layoutGrid(
210
219
  // which is the max-content one only if it was made just now
211
220
  const exact =
212
221
  fresh && ctx.fonts ? exactMinContent(box, ctx.fonts) : null;
213
- box.intrinsicMinContent =
214
- exact ?? measureIntrinsicWidth(box, ctx, MIN_CONTENT_PROBE);
222
+ box.intrinsicMinContent = exact ?? minContentOf(box, ctx);
215
223
  }
216
224
  narrowest = box.intrinsicMinContent;
217
225
  }
@@ -366,18 +374,19 @@ export function layoutGrid(
366
374
  } else if (align === 'center') dy = (tall - outerHeight) / 2;
367
375
  else if (align === 'flex-end') dy = tall - outerHeight;
368
376
  else if (
369
- (align === 'stretch' || align === 'baseline') &&
377
+ (align === 'stretch' || align === 'normal' || align === 'baseline') &&
370
378
  child.style.height === AUTO &&
371
379
  !child.style.heightKeyword &&
372
- (child.style.alignSelf === 'stretch' ||
380
+ (align === 'stretch' ||
373
381
  (child.kind !== 'replaced' && !child.style.aspectRatio))
374
382
  ) {
375
383
  // a stretched item is its area's height, within its own least and
376
384
  // greatest (10.3): taller than what it holds, or shorter where its
377
385
  // tracks let it be, what it holds then running past it. An image, a
378
- // control or a box with an `aspect-ratio` keeps its own height, as
379
- // `normal` has one with a natural size or a ratio — which is what
380
- // the grid's `stretch` may be, and the item's own may not.
386
+ // control or a box with an `aspect-ratio` keeps its own height under
387
+ // `normal`, which stretches only an item with neither a natural size
388
+ // nor a ratio, and is stretched by `stretch`, the grid's or its own
389
+ // (6.2, and CSS Box Alignment 3, 6.1).
381
390
  const stretched = tall - child.marginTop - child.marginBottom;
382
391
  const height = clampHeight(
383
392
  child,
@@ -401,21 +410,27 @@ export function layoutGrid(
401
410
  // purpose"): a card's `margin-top: auto` takes what its row is taller
402
411
  // than the card by, its `flex: 1` grows into it (CSS Flexbox 8.1,
403
412
  // 9.7). Its content's own height is the same layout, and kept.
413
+ // A table shares out among its rows what the area is taller than
414
+ // they are (`layoutTable`), and is no shorter than they are however
415
+ // short its area: its rows are the least it can be, as they are
416
+ // where it has a height of its own (CSS 2.1 17.5.3).
404
417
  if (
405
418
  child.kind !== 'replaced' &&
406
419
  (wide !== null ||
407
420
  percentHeightsIn(child) ||
408
- (child.kind === 'flex' && height !== child.height))
421
+ (child.kind === 'flex' && height !== child.height) ||
422
+ (child.kind === 'table' && height > child.height + 0.01))
409
423
  ) {
410
424
  FLEXED_HEIGHT.set(child, Math.max(0, height - child.verticalExtra));
411
425
  try {
412
- ctx.layoutSubtree(child, width, area);
426
+ layoutAcross(item, width, area, ctx);
413
427
  } finally {
414
428
  FLEXED_HEIGHT.delete(child);
415
429
  }
416
430
  }
417
431
  child.width = width;
418
- child.height = height;
432
+ child.height =
433
+ child.kind === 'table' ? Math.max(height, child.height) : height;
419
434
  centreButton(child);
420
435
  }
421
436
  moveTo(
@@ -596,6 +611,7 @@ function place(
596
611
  cols: p.bs,
597
612
  min: -1,
598
613
  max: -1,
614
+ across: false,
599
615
  }
600
616
  : {
601
617
  box: p.box,
@@ -605,6 +621,7 @@ function place(
605
621
  cols: p.as,
606
622
  min: -1,
607
623
  max: -1,
624
+ across: false,
608
625
  },
609
626
  );
610
627
  }
@@ -641,14 +658,37 @@ function extent(
641
658
  }
642
659
 
643
660
  /** An item's max-content width, its border box's: measured once in the
644
- * box's life, as a table cell's is (`Box.intrinsicMaxContent`). */
661
+ * box's life, as a table cell's is (`Box.intrinsicMaxContent`). Its
662
+ * content's, whatever width it has of its own (`minContentOf`). */
645
663
  function maxContentOf(box: Box, ctx: LayoutContext): number {
646
664
  if (box.intrinsicMaxContent < 0) {
647
- box.intrinsicMaxContent = measureIntrinsicWidth(box, ctx, Infinity);
665
+ box.intrinsicMaxContent = measureIntrinsicWidth(
666
+ box,
667
+ ctx,
668
+ Infinity,
669
+ box.kind !== 'replaced',
670
+ );
648
671
  }
649
672
  return box.intrinsicMaxContent;
650
673
  }
651
674
 
675
+ /**
676
+ * An item's min-content width, its border box's. What it is measured for is
677
+ * what it contributes to its columns, and the width a percentage makes of
678
+ * them is not known then: a box that is not replaced is as wide as its
679
+ * content for that, whatever its own `width` says, and a replaced element
680
+ * takes its percentage of nothing (CSS Sizing 3, 5.2.1). A width that is a
681
+ * length is weighed apart (`measure` in `layoutGrid`).
682
+ */
683
+ function minContentOf(box: Box, ctx: LayoutContext): number {
684
+ return measureIntrinsicWidth(
685
+ box,
686
+ ctx,
687
+ MIN_CONTENT_PROBE,
688
+ box.kind !== 'replaced',
689
+ );
690
+ }
691
+
652
692
  /**
653
693
  * Lay an item out in its area: stretched across it where its width is
654
694
  * `auto` and it is stretched, or at its content's width, no wider than
@@ -693,11 +733,7 @@ function layoutItem(
693
733
  probed = true;
694
734
  }
695
735
  if (child.intrinsicMinContent < 0) {
696
- child.intrinsicMinContent = measureIntrinsicWidth(
697
- child,
698
- ctx,
699
- MIN_CONTENT_PROBE,
700
- );
736
+ child.intrinsicMinContent = minContentOf(child, ctx);
701
737
  probed = true;
702
738
  }
703
739
  if (probed) resolveEdges(child, area);
@@ -757,7 +793,15 @@ function layoutItem(
757
793
  width = Math.min(widest, Math.max(narrowest, room));
758
794
  }
759
795
  if (child.kind !== 'replaced') {
760
- ctx.layoutSubtree(child, width, area);
796
+ // a table stretched across is as wide as its area, which in a block's
797
+ // flow it would shrink from to its columns (`STRETCHED_ACROSS`)
798
+ item.across =
799
+ across &&
800
+ child.kind === 'table' &&
801
+ style.width === AUTO &&
802
+ !keyword &&
803
+ through === null;
804
+ layoutAcross(item, width, area, ctx);
761
805
  return;
762
806
  }
763
807
  // a replaced element sizes itself from its style: then it is given the
@@ -776,6 +820,27 @@ function layoutItem(
776
820
  }
777
821
  }
778
822
 
823
+ /** Lay an item out at a width: a table stretched across its area at that
824
+ * width as its own (`STRETCHED_ACROSS`), for this layout alone. */
825
+ function layoutAcross(
826
+ item: Item,
827
+ width: number,
828
+ area: number,
829
+ ctx: LayoutContext,
830
+ ): void {
831
+ const child = item.box;
832
+ if (!item.across) {
833
+ ctx.layoutSubtree(child, width, area);
834
+ return;
835
+ }
836
+ STRETCHED_ACROSS.add(child);
837
+ try {
838
+ ctx.layoutSubtree(child, width, area);
839
+ } finally {
840
+ STRETCHED_ACROSS.delete(child);
841
+ }
842
+ }
843
+
779
844
  /** What the percentages in a box's padding come to across it, its edges
780
845
  * resolved: what a width of its content's, measured with them of nothing,
781
846
  * is short of its border box by. */
@@ -875,9 +940,9 @@ function percentOwn(box: Box): boolean {
875
940
  /**
876
941
  * An item's border-box height before the rows are sized, where it has one
877
942
  * then: a height of its own — a percentage of the rows it spans where they
878
- * have a length for a size — or the rows' where it is stretched down them
879
- * by its own `align-self`. Null where it has none, which is where its
880
- * height is its content's.
943
+ * have a length for a size — or the rows' where `stretch` stretches it
944
+ * down them, its own `align-self` or the grid's `align-items`. Null where
945
+ * it has none, which is where its height is its content's.
881
946
  */
882
947
  function heightBeforeRows(box: Box, rows: number): number | null {
883
948
  const style = box.style;
@@ -890,10 +955,12 @@ function heightBeforeRows(box: Box, rows: number): number | null {
890
955
  : own + box.verticalExtra,
891
956
  );
892
957
  }
958
+ const align =
959
+ style.alignSelf === AUTO ? box.parent?.style.alignItems : style.alignSelf;
893
960
  if (
894
961
  style.height === AUTO &&
895
962
  !style.heightKeyword &&
896
- style.alignSelf === 'stretch' &&
963
+ align === 'stretch' &&
897
964
  Number.isFinite(rows) &&
898
965
  !autoMargins(box, 'y').some(Boolean)
899
966
  ) {