buninu-browser 0.2.3 → 0.3.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 (116) hide show
  1. package/README.md +86 -1
  2. package/lib/cdp-server.js +47 -14
  3. package/lib/clipboard.js +22 -0
  4. package/lib/happy-dom/document-base.js +33 -0
  5. package/lib/happy-dom/parser.js +15 -6
  6. package/lib/happy-dom/selector-matcher.js +2 -2
  7. package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
  8. package/lib/happy-dom/svg-attribute-adapter.js +50 -0
  9. package/lib/headless-shell.js +40 -11
  10. package/lib/input/hit-test.js +67 -19
  11. package/lib/layout/text-layout.js +213 -91
  12. package/lib/media/byte-pipe.js +2 -2
  13. package/lib/media/position-audio.js +10 -1
  14. package/lib/media/source-snapshots.js +126 -0
  15. package/lib/media/video-keyframe.js +17 -4
  16. package/lib/media/webm-stream.js +106 -0
  17. package/lib/network/network-service.js +268 -120
  18. package/lib/paint/display-list.js +8 -28
  19. package/lib/paint/frame-cache.js +25 -0
  20. package/lib/paint/frame.js +77 -16
  21. package/lib/paint/paragraph-text.js +24 -0
  22. package/lib/paint/raster-readback.js +41 -0
  23. package/lib/paint/raw-frames.js +47 -0
  24. package/lib/paint/rounded-rect.js +82 -0
  25. package/lib/paint/shared-frame-pool.js +105 -0
  26. package/lib/render-tree/index.js +38 -6
  27. package/lib/render-tree/svg.js +15 -7
  28. package/lib/renderer/bindings.js +233 -80
  29. package/lib/renderer/broadcast-channel.js +76 -0
  30. package/lib/renderer/canvas-2d.js +257 -14
  31. package/lib/renderer/canvas-text.js +120 -0
  32. package/lib/renderer/compositor-channel.js +103 -0
  33. package/lib/renderer/compositor.js +34 -40
  34. package/lib/renderer/custom-element-reactions.js +25 -1
  35. package/lib/renderer/flags.js +1 -1
  36. package/lib/renderer/font-loading.js +227 -0
  37. package/lib/renderer/host.js +133 -98
  38. package/lib/renderer/image-resources.js +65 -0
  39. package/lib/renderer/interfaces.js +28 -0
  40. package/lib/renderer/layout-observers.js +236 -0
  41. package/lib/renderer/main.js +52 -5
  42. package/lib/renderer/media-source.js +126 -55
  43. package/lib/renderer/page-realm.js +287 -99
  44. package/lib/renderer/page-renderer.js +663 -239
  45. package/lib/renderer/prepared-script-cache.js +50 -0
  46. package/lib/renderer/rendered-text.js +108 -0
  47. package/lib/renderer/rewrite-worker.js +4 -3
  48. package/lib/renderer/script-rewrite.js +13 -2
  49. package/lib/renderer/scroll-into-view.js +28 -0
  50. package/lib/renderer/slot-assignment.js +60 -0
  51. package/lib/renderer/video-presentation.js +23 -4
  52. package/lib/renderer/workers.js +2 -2
  53. package/lib/style/computed-style.js +611 -104
  54. package/lib/style/selector-traits.js +49 -5
  55. package/package.json +1 -1
  56. package/test/broadcast-channel.test.js +26 -0
  57. package/test/canvas-2d.test.js +83 -2
  58. package/test/canvas-paint.test.js +9 -0
  59. package/test/canvas-text.test.js +41 -0
  60. package/test/cdp-stream.test.js +56 -0
  61. package/test/clipboard.test.js +35 -0
  62. package/test/computed-style-binding.test.js +151 -0
  63. package/test/computed-style.test.js +258 -1
  64. package/test/display-list.test.js +12 -0
  65. package/test/document-base.test.js +87 -0
  66. package/test/dom-platform.test.js +461 -0
  67. package/test/fetch-lifecycle.test.js +161 -0
  68. package/test/fixtures/browser-raw-demo.html +36 -0
  69. package/test/fixtures/canvas-paint.js +117 -0
  70. package/test/fixtures/canvas-text.js +42 -0
  71. package/test/fixtures/image-lifetime.js +90 -0
  72. package/test/fixtures/media-seek-snapshot.js +77 -0
  73. package/test/fixtures/mse-audio-chirp.webm +0 -0
  74. package/test/fixtures/mse-switch-large.mp4 +0 -0
  75. package/test/fixtures/mse-switch-small.mp4 +0 -0
  76. package/test/fixtures/raw-capture-benchmark.json +76 -0
  77. package/test/fixtures/renderer-memory.js +110 -0
  78. package/test/fixtures/summary-style.js +28 -0
  79. package/test/font-loading.test.js +97 -0
  80. package/test/grid.test.js +58 -0
  81. package/test/hit-test.test.js +21 -1
  82. package/test/image-lifetime.test.js +12 -0
  83. package/test/image-resources.test.js +94 -0
  84. package/test/local-media.test.js +84 -0
  85. package/test/media-seek-snapshot.test.js +11 -0
  86. package/test/media-source-config.test.js +98 -0
  87. package/test/media-source-webm-accounting.test.js +98 -0
  88. package/test/media-source-webm.test.js +108 -0
  89. package/test/navigation.test.js +1 -0
  90. package/test/network-service.test.js +388 -0
  91. package/test/page-scripts.test.js +1 -1
  92. package/test/paint-frame.test.js +103 -0
  93. package/test/paint-paragraph-cache.test.js +87 -0
  94. package/test/position-audio.test.js +24 -0
  95. package/test/positioning.test.js +22 -0
  96. package/test/prepared-script-cache.test.js +45 -0
  97. package/test/pulse-client.test.js +10 -3
  98. package/test/raster-readback.test.js +40 -0
  99. package/test/raw-capture.test.js +152 -0
  100. package/test/render-tree.test.js +28 -0
  101. package/test/renderer-memory.test.js +11 -0
  102. package/test/selector-traits.test.js +24 -0
  103. package/test/shared-frame-pool.test.js +63 -0
  104. package/test/style-sheets.test.js +51 -0
  105. package/test/svg.test.js +29 -0
  106. package/test/text-layout.test.js +106 -0
  107. package/test/video-keyframe.test.js +61 -0
  108. package/test/video-presentation.test.js +69 -0
  109. package/test/window-resize.test.js +36 -0
  110. package/tools/bench-raw-capture.js +32 -0
  111. package/tools/browser-lab.js +78 -0
  112. package/tools/browser-soak.js +75 -0
  113. package/tools/network-reference.js +49 -0
  114. package/tools/process-memory.js +38 -0
  115. package/tools/script-benchmark.js +25 -0
  116. package/ai-debug.log +0 -5537
@@ -80,39 +80,82 @@ export function hitTest(tree, layout, x, y, scroll, viewport) {
80
80
  * pointer events. Null when the point hits nothing.
81
81
  */
82
82
  export function elementAtPoint(tree, displayList, x, y, scroll) {
83
+ const domNode = renderNodeAtPoint(tree, displayList, x, y, scroll)?.domNode;
84
+ return domNode?.nodeType === 1 ? domNode : domNode?.parentElement ?? null;
85
+ }
86
+
87
+ /** Also preserves generated controls, whose owning DOM element differs from their render node. */
88
+ export function renderNodeAtPoint(tree, displayList, x, y, scroll) {
83
89
  const hittable = hitTestable(tree);
84
90
  const nodeId = hitTarget(displayList, x, y, scroll, (id) => !hittable(tree.nodesById.get(id)));
85
- const domNode = nodeId == null ? null : tree.nodesById.get(nodeId)?.domNode;
86
- return domNode?.nodeType === 1 ? domNode : domNode?.parentElement ?? null;
91
+ return nodeId == null ? null : tree.nodesById.get(nodeId) ?? null;
87
92
  }
88
93
 
89
- /** Returns the content-coordinate bounds contributed by an element's text descendants. */
90
- export function elementBounds(tree, layout, element) {
91
- const rects = [];
94
+ // Layout objects are immutable after the layout pass. Keeping their lookup
95
+ // tables under a weak key releases old document nodes when a layout is dropped.
96
+ const geometryIndexes = new WeakMap();
97
+
98
+ function geometryIndex(tree, layout) {
99
+ let index = geometryIndexes.get(layout);
100
+ if (index?.tree === tree) return index;
101
+ const byNode = new Map(), boxes = new WeakMap();
102
+ for (const box of layout.boxes) if (!box.inline && !byNode.has(box.nodeId)) byNode.set(box.nodeId, box);
103
+ for (const node of tree.nodesById.values()) {
104
+ if (node.pseudo || node.domNode?.nodeType !== 1 || boxes.has(node.domNode)) continue;
105
+ // The first actual render node is authoritative, even if it is inline.
106
+ const box = byNode.get(node.id);
107
+ boxes.set(node.domNode, box ? { node, box } : null);
108
+ }
109
+ index = { tree, boxes, textBounds: null };
110
+ geometryIndexes.set(layout, index);
111
+ return index;
112
+ }
113
+
114
+ /** The first real render node and block box belonging to an element. */
115
+ export function elementBox(tree, layout, element) {
116
+ return tree && layout ? geometryIndex(tree, layout).boxes.get(element) ?? null : null;
117
+ }
118
+
119
+ function textBoundsIndex(tree, layout) {
120
+ const bounds = new WeakMap();
92
121
  for (const fragment of layout.fragments) {
93
122
  for (const run of fragment.runs) {
94
123
  const renderNode = tree.nodesById.get(run.nodeId);
95
124
  const owner = renderNode?.domNode?.nodeType === 1
96
125
  ? renderNode.domNode
97
126
  : renderNode?.domNode?.parentElement;
98
- if (!owner || (owner !== element && !element.contains(owner))) continue;
127
+ if (!owner) continue;
99
128
  // A replaced element (an image) spans its own box, not its line's height.
100
129
  const replaced = run.type === "image" && run.y != null;
101
130
  const top = replaced ? run.y : fragment.y;
102
- rects.push({
103
- left: run.x,
104
- top,
105
- right: run.x + run.width,
106
- bottom: top + (replaced ? run.height : fragment.height),
107
- });
131
+ const right = run.x + run.width;
132
+ const bottom = top + (replaced ? run.height : fragment.height);
133
+ for (let ancestor = owner; ancestor; ancestor = ancestor.parentElement) {
134
+ const rect = bounds.get(ancestor);
135
+ if (rect) {
136
+ rect.left = Math.min(rect.left, run.x);
137
+ rect.top = Math.min(rect.top, top);
138
+ rect.right = Math.max(rect.right, right);
139
+ rect.bottom = Math.max(rect.bottom, bottom);
140
+ } else bounds.set(ancestor, { left: run.x, top, right, bottom });
141
+ }
108
142
  }
109
143
  }
110
- if (!rects.length) return null;
111
- const left = Math.min(...rects.map((rect) => rect.left));
112
- const top = Math.min(...rects.map((rect) => rect.top));
113
- const right = Math.max(...rects.map((rect) => rect.right));
114
- const bottom = Math.max(...rects.map((rect) => rect.bottom));
115
- return Object.freeze({ left, top, right, bottom, width: right - left, height: bottom - top });
144
+ return bounds;
145
+ }
146
+
147
+ /** Returns the content-coordinate bounds contributed by an element's text descendants. */
148
+ export function elementBounds(tree, layout, element) {
149
+ const index = geometryIndex(tree, layout);
150
+ index.textBounds ??= textBoundsIndex(tree, layout);
151
+ const rect = index.textBounds.get(element);
152
+ if (!rect) return null;
153
+ if (rect.width === undefined) {
154
+ rect.width = rect.right - rect.left;
155
+ rect.height = rect.bottom - rect.top;
156
+ Object.freeze(rect);
157
+ }
158
+ return rect;
116
159
  }
117
160
 
118
161
  /**
@@ -120,19 +163,24 @@ export function elementBounds(tree, layout, element) {
120
163
  * visibility: hidden or pointer-events: none (both inherited, so a visible
121
164
  * or pointer-events: auto descendant still does). Text uses its element's style.
122
165
  */
166
+ const hitTestableIndexes = new WeakMap();
123
167
  function hitTestable(tree) {
168
+ const cached = hitTestableIndexes.get(tree);
169
+ if (cached) return cached;
124
170
  const styles = new Map();
125
171
  for (const node of tree.nodesById.values()) {
126
172
  if (node.domNode?.nodeType === 1 && node.style && !node.pseudo && !styles.has(node.domNode)) {
127
173
  styles.set(node.domNode, node.style);
128
174
  }
129
175
  }
130
- return (renderNode) => {
176
+ const accepts = (renderNode) => {
131
177
  const domNode = renderNode?.domNode;
132
178
  const element = domNode?.nodeType === 1 ? domNode : domNode?.parentElement;
133
179
  const style = (renderNode?.pseudo ? renderNode.style : null) ?? styles.get(element);
134
180
  return !style || (style.visibility !== "hidden" && style.pointerEvents !== "none");
135
181
  };
182
+ hitTestableIndexes.set(tree, accepts);
183
+ return accepts;
136
184
  }
137
185
 
138
186
  function closestInteractive(node) {
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Box-tree layout: block formatting contexts with margin collapsing, inline
3
3
  * formatting contexts with line boxes, atomic inlines, and flex layout
4
- * (CSS Flexbox §9: flexible length resolution, wrapping, alignment; column
5
- * containers currently stack items without main-axis flexing), and grid
6
- * layout (CSS Grid 2 §8 placement and §12 track sizing, without named lines,
7
- * areas, or column auto-flow). Relative, absolute and fixed positioning
4
+ * (CSS Flexbox §9: row/column flexible length resolution, row wrapping,
5
+ * alignment), and grid
6
+ * layout (CSS Grid 2 §8 placement and §12 track sizing, including named areas
7
+ * and row/column auto-flow). Relative, absolute and fixed positioning
8
8
  * place boxes after flow layout; positioned boxes get their own paint layer.
9
9
  *
10
10
  * The block/inline structure follows concepts from Dropflow
@@ -150,7 +150,7 @@ let containingBlocks = null;
150
150
  let outOfFlowBoxes = null;
151
151
 
152
152
  // Text nodes carry their parent's style, so only element boxes can be positioned.
153
- const UNPOSITIONABLE = new Set(["root", "text", "line-break"]);
153
+ const UNPOSITIONABLE = new Set(["root", "text", "line-break", "contents"]);
154
154
 
155
155
  function isPositioned(node) {
156
156
  return !UNPOSITIONABLE.has(node.type) && ["relative", "absolute", "fixed", "sticky"].includes(node.style?.position);
@@ -217,7 +217,10 @@ function blockChildren(node, flags) {
217
217
  };
218
218
 
219
219
  function walk(child, childFlags) {
220
- if (isOutOfFlow(child)) {
220
+ if (child.type === "contents") {
221
+ const flags = inheritedFlags(child, childFlags);
222
+ for (const grandchild of child.children) walk(grandchild, flags);
223
+ } else if (isOutOfFlow(child)) {
221
224
  const box = outOfFlowBox(child, childFlags);
222
225
  // Between blocks the placeholder records the static position; inline
223
226
  // placeholders fall back to the containing block's origin.
@@ -271,10 +274,10 @@ function flexItems(node, flags) {
271
274
  }
272
275
  text = [];
273
276
  };
274
- for (const child of node.children) {
277
+ for (const child of boxChildren(node)) {
275
278
  if (child.type === "line-break") continue;
276
279
  if (child.type === "text") {
277
- if (child.text.trim()) text.push(textItem(child, flags));
280
+ if (child.text.trim()) text.push(textItem(child, inheritedFlags(child, flags)));
278
281
  continue;
279
282
  }
280
283
  if (isOutOfFlow(child)) {
@@ -288,6 +291,14 @@ function flexItems(node, flags) {
288
291
  return items;
289
292
  }
290
293
 
294
+ /** display:contents contributes its descendants directly to the box tree. */
295
+ function* boxChildren(node) {
296
+ for (const child of node.children) {
297
+ if (child.type === "contents") yield* boxChildren(child);
298
+ else yield child;
299
+ }
300
+ }
301
+
291
302
  function textItem(node, flags) {
292
303
  return { kind: "text", node, style: node.style ?? null, text: node.text, preserve: flags.preserve, wrap: flags.wrap };
293
304
  }
@@ -364,9 +375,11 @@ function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = nu
364
375
  dx = offset.x;
365
376
  dy = offset.y;
366
377
  }
367
- if (style?.transform) {
368
- dx += numberOrZero(resolveLength(style.transform.x, record.width));
369
- dy += numberOrZero(resolveLength(style.transform.y, record.height));
378
+ // Individual translate precedes the transform list; its offset is in the
379
+ // parent's coordinates and is not multiplied by the list's own scale.
380
+ for (const translation of [style?.translate, style?.transform]) if (translation) {
381
+ dx += numberOrZero(resolveLength(translation.x, record.width));
382
+ dy += numberOrZero(resolveLength(translation.y, record.height));
370
383
  }
371
384
  for (let index = boxStart; index < ctx.boxes.length; index++) ctx.boxes[index].layer ??= ctx.layer;
372
385
  for (let index = fragmentStart; index < ctx.fragments.length; index++) ctx.fragments[index].layer ??= ctx.layer;
@@ -560,15 +573,23 @@ function layoutAbsolutes(ctx, box, record) {
560
573
  * item by item because shiftOutput moves everything after a start index.
561
574
  */
562
575
  function justifyColumn(ctx, items, justify = "normal", free) {
563
- if (!(free > 0) || ["normal", "start", "flex-start", "stretch"].includes(justify)) return;
576
+ const autoMargins = items.reduce((sum, item) => sum + Number(item.autoTop) + Number(item.autoBottom), 0);
577
+ if (!free || !autoMargins && ["normal", "start", "flex-start", "stretch"].includes(justify)) return;
564
578
  const count = items.length;
579
+ let autoOffset = 0;
565
580
  const offset = (index) => {
581
+ if (autoMargins && free > 0) {
582
+ autoOffset += Number(items[index].autoTop) * free / autoMargins;
583
+ const result = autoOffset;
584
+ autoOffset += Number(items[index].autoBottom) * free / autoMargins;
585
+ return result;
586
+ }
566
587
  switch (justify) {
567
588
  case "center": return free / 2;
568
589
  case "end": case "flex-end": return free;
569
- case "space-between": return count > 1 ? free * index / (count - 1) : 0;
570
- case "space-around": return free / count * (index + 0.5);
571
- case "space-evenly": return free / (count + 1) * (index + 1);
590
+ case "space-between": return free > 0 && count > 1 ? free * index / (count - 1) : 0;
591
+ case "space-around": return free > 0 ? free / count * (index + 0.5) : free / 2;
592
+ case "space-evenly": return free > 0 ? free / (count + 1) * (index + 1) : free / 2;
572
593
  default: return 0;
573
594
  }
574
595
  };
@@ -653,6 +674,8 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
653
674
  const containingHeight = ctx.containing?.height;
654
675
  const resolveHeight = (value) => numberOrNull(resolveLength(value, containingHeight ?? NaN));
655
676
  const specifiedHeight = resolveHeight(style?.height);
677
+ const minHeight = resolveHeight(style?.minHeight);
678
+ const maxHeight = style?.maxHeight === "none" ? null : resolveHeight(style?.maxHeight);
656
679
  const previousContaining = ctx.containing;
657
680
  ctx.containing = {
658
681
  width: contentWidth,
@@ -674,38 +697,11 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
674
697
  contentBottom = result.bottom;
675
698
  baseline = result.baseline;
676
699
  } else if (box.flex === "column") {
677
- const gap = style?.rowGap ?? style?.gap ?? 0;
678
- let cursor = contentTop;
679
- columnItems = [];
680
- box.children.forEach((child, index) => {
681
- if (index > 0) cursor += gap;
682
- columnItems.push({ boxStart: ctx.boxes.length, fragmentStart: ctx.fragments.length });
683
- if (child.kind === "ifc") {
684
- const result = layoutInline(ctx, child, contentX, contentWidth, cursor);
685
- cursor = result.bottom;
686
- baseline = result.baseline ?? baseline;
687
- return;
688
- }
689
- const alignment = ["auto", "normal", undefined].includes(child.style?.alignSelf)
690
- ? style?.alignItems ?? "normal"
691
- : child.style.alignSelf;
692
- const shrink = !["normal", "stretch"].includes(alignment);
693
- const placed = resolveHorizontal(ctx, child, contentX, contentWidth, shrink);
694
- // Auto margins on the cross axis (left, right) align the item instead (Flexbox §8.1).
695
- const crossMargins = child.style?.margin ?? ZERO_BOX;
696
- const autoMargins = crossMargins[1] === "auto" || crossMargins[3] === "auto";
697
- let childX = placed.x;
698
- if (shrink && !autoMargins) {
699
- const free = contentWidth - placed.width - placed.marginLeft - placed.marginRight;
700
- if (alignment === "center") childX = contentX + placed.marginLeft + free / 2;
701
- else if (alignment === "end") childX = contentX + placed.marginLeft + free;
702
- }
703
- const margin = verticalMargins(child.style);
704
- const result = layoutBlock(ctx, child, childX, placed.width, cursor + margin.top);
705
- cursor += margin.top + result.height + margin.bottom;
706
- baseline = result.baseline ?? baseline;
707
- });
708
- contentBottom = cursor;
700
+ const result = layoutFlexColumn(ctx, box, contentX, contentWidth, contentTop,
701
+ minHeight == null ? 0 : toContentHeight(minHeight), maxHeight == null ? Infinity : toContentHeight(maxHeight));
702
+ contentBottom = result.bottom;
703
+ baseline = result.baseline;
704
+ columnItems = result.items;
709
705
  } else {
710
706
  let cursor = contentTop;
711
707
  let pending = null;
@@ -747,14 +743,15 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
747
743
  ctx.containing = previousContaining;
748
744
  let contentHeight = Math.max(0, contentBottom - contentTop);
749
745
  if (specifiedHeight != null) contentHeight = toContentHeight(specifiedHeight);
750
- const maxHeight = style?.maxHeight === "none" ? null : resolveHeight(style?.maxHeight);
751
746
  if (maxHeight != null) contentHeight = Math.min(contentHeight, toContentHeight(maxHeight));
752
- const minHeight = resolveHeight(style?.minHeight);
753
747
  if (minHeight != null) contentHeight = Math.max(contentHeight, toContentHeight(minHeight));
748
+ if (forcedHeight != null) contentHeight = Math.max(0, forcedHeight - padding[0] - padding[2] - border[0] - border[2]);
754
749
  if (columnItems?.length) justifyColumn(ctx, columnItems, style?.justifyContent, contentHeight - (contentBottom - contentTop));
755
750
  if (gridResult) alignGridContent(ctx, gridResult, style?.alignContent, contentHeight - (contentBottom - contentTop));
756
751
  record.height = forcedHeight ?? contentHeight + padding[0] + padding[2] + border[0] + border[2];
757
- if (style?.display === "list-item") placeListMarker(ctx, box, contentX, contentTop, firstFragment);
752
+ // Summary's disclosure marker participates in its inline content.
753
+ const summaryMarker = box.node.tagName === "SUMMARY" && ["disclosure-open", "disclosure-closed"].includes(style?.listStyleType);
754
+ if (style?.display === "list-item" && !summaryMarker) placeListMarker(ctx, box, contentX, contentTop, firstFragment);
758
755
  return { height: record.height, baseline };
759
756
  }
760
757
 
@@ -802,6 +799,8 @@ function markerText(type, ordinal) {
802
799
  case "disc": return "\u2022 ";
803
800
  case "circle": return "\u25E6 ";
804
801
  case "square": return "\u25AA ";
802
+ case "disclosure-closed": return "\u25B6 ";
803
+ case "disclosure-open": return "\u25BC ";
805
804
  case "decimal": return `${ordinal}. `;
806
805
  case "decimal-leading-zero": return `${String(ordinal).padStart(2, "0")}. `;
807
806
  case "lower-alpha":
@@ -837,7 +836,97 @@ function roman(value) {
837
836
  }
838
837
 
839
838
  // ---------------------------------------------------------------------------
840
- // Flex row layout (CSS Flexbox §9)
839
+ // Flex layout (CSS Flexbox §9)
840
+
841
+ function layoutFlexColumn(ctx, box, x, width, y, minHeight, maxHeight) {
842
+ const style = box.style;
843
+ const gap = style?.rowGap ?? style?.gap ?? 0;
844
+ const definiteHeight = ctx.containing?.height;
845
+ // Most column containers have auto height. Without a main-axis constraint
846
+ // or definite basis there is no flexible free space to distribute, so a
847
+ // single layout pass suffices even for deeply nested application shells.
848
+ const unconstrained = definiteHeight == null && minHeight === 0 && maxHeight === Infinity
849
+ && box.children.every(child => numberOrNull(resolveLength(child.style?.flexBasis, NaN)) == null);
850
+ if (unconstrained) {
851
+ let cursor = y;
852
+ let baseline = null;
853
+ const items = [];
854
+ for (const [index, child] of box.children.entries()) {
855
+ if (index) cursor += gap;
856
+ const horizontal = columnHorizontal(ctx, child, style, x, width);
857
+ const margin = child.style?.margin ?? ZERO_BOX;
858
+ items.push({boxStart:ctx.boxes.length,fragmentStart:ctx.fragments.length,
859
+ autoTop:margin[0] === "auto",autoBottom:margin[2] === "auto"});
860
+ const result = layoutBlock(ctx, child, horizontal.x, horizontal.width, cursor + numberOrZero(margin[0]));
861
+ cursor += numberOrZero(margin[0]) + result.height + numberOrZero(margin[2]);
862
+ baseline ??= result.baseline;
863
+ }
864
+ return {bottom:cursor,baseline,items};
865
+ }
866
+ const items = box.children.map(child => {
867
+ const childStyle = child.style;
868
+ const horizontal = columnHorizontal(ctx, child, style, x, width);
869
+ const margin = childStyle?.margin ?? ZERO_BOX;
870
+ const padding = paddingBox(childStyle, width);
871
+ const border = borderWidths(childStyle);
872
+ const extra = padding[0] + padding[2] + border[0] + border[2];
873
+ const contentBox = value => Math.max(0, childStyle?.boxSizing === "border-box" ? value - extra : value);
874
+ const resolve = value => numberOrNull(resolveLength(value, definiteHeight ?? NaN));
875
+ const measured = dryLayout(ctx, child, horizontal.width);
876
+ const content = Math.max(0, measured.height - extra);
877
+ const specified = resolve(childStyle?.height);
878
+ const basis = childStyle?.flexBasis ?? "auto";
879
+ const definiteBasis = resolve(basis);
880
+ const base = definiteBasis != null ? contentBox(definiteBasis)
881
+ : basis === "auto" && specified != null ? contentBox(specified) : content;
882
+ const minSize = resolve(childStyle?.minHeight);
883
+ // The automatic minimum is content-based except for scrollable items.
884
+ const scrollable = ["auto", "scroll", "hidden"].includes(childStyle?.overflowScrollY);
885
+ let min = minSize != null ? contentBox(minSize) : scrollable ? 0
886
+ : Math.min(content, specified == null ? Infinity : contentBox(specified));
887
+ const maxSize = resolve(childStyle?.maxHeight);
888
+ const max = maxSize == null ? Infinity : contentBox(maxSize);
889
+ min = Math.min(min, max);
890
+ return { box:child, horizontal, extra, base, min, max, naturalHeight:measured.height,
891
+ definite:definiteHeight != null || definiteBasis != null,
892
+ hypothetical:Math.min(max, Math.max(min, base)), grow:childStyle?.flexGrow ?? 0, shrink:childStyle?.flexShrink ?? 1,
893
+ // The shared main-axis solver names these left/right; columns use top/bottom.
894
+ marginLeft:numberOrZero(margin[0]), marginRight:numberOrZero(margin[2]),
895
+ autoTop:margin[0] === "auto", autoBottom:margin[2] === "auto" };
896
+ });
897
+ const gaps = gap * Math.max(0, items.length - 1);
898
+ const natural = gaps + items.reduce((sum, item) => sum + item.hypothetical + item.extra + item.marginLeft + item.marginRight, 0);
899
+ const available = Math.max(minHeight, Math.min(maxHeight, definiteHeight ?? natural));
900
+ resolveFlexibleLengths(items, available - gaps);
901
+ let cursor = y;
902
+ let baseline = null;
903
+ for (const [index, item] of items.entries()) {
904
+ if (index) cursor += gap;
905
+ item.boxStart = ctx.boxes.length;
906
+ item.fragmentStart = ctx.fragments.length;
907
+ const targetHeight = item.target + item.extra;
908
+ const forcedHeight = item.definite || Math.abs(targetHeight - item.naturalHeight) > 1e-9 ? targetHeight : null;
909
+ const result = layoutBlock(ctx, item.box, item.horizontal.x, item.horizontal.width,
910
+ cursor + item.marginLeft, false, forcedHeight);
911
+ cursor += item.marginLeft + result.height + item.marginRight;
912
+ baseline ??= result.baseline;
913
+ }
914
+ return { bottom:cursor, baseline, items };
915
+ }
916
+
917
+ function columnHorizontal(ctx, child, style, x, width) {
918
+ const alignment = ["auto", "normal", undefined].includes(child.style?.alignSelf)
919
+ ? style?.alignItems ?? "normal" : child.style.alignSelf;
920
+ const shrink = !["normal", "stretch"].includes(alignment);
921
+ const horizontal = resolveHorizontal(ctx, child, x, width, shrink);
922
+ const margin = child.style?.margin ?? ZERO_BOX;
923
+ if (shrink && margin[1] !== "auto" && margin[3] !== "auto") {
924
+ const free = width - horizontal.width - horizontal.marginLeft - horizontal.marginRight;
925
+ if (alignment === "center") horizontal.x += free / 2;
926
+ else if (alignment === "end") horizontal.x += free;
927
+ }
928
+ return horizontal;
929
+ }
841
930
 
842
931
  function layoutFlexRow(ctx, box, x, width, y) {
843
932
  const style = box.style;
@@ -1143,7 +1232,7 @@ function tableStructure(node, flags) {
1143
1232
  target.push({
1144
1233
  node: child,
1145
1234
  style: child.style ?? null,
1146
- cells: child.children.filter((cell) => cell.type === "table-cell").map((cell) => tableCell(cell, rowFlags)),
1235
+ cells: [...boxChildren(child)].filter((cell) => cell.type === "table-cell").map((cell) => tableCell(cell, rowFlags)),
1147
1236
  });
1148
1237
  } else if (child.type === "table-cell") {
1149
1238
  if (!anonymous) target.push(anonymous = { node: null, style: null, cells: [] });
@@ -1156,16 +1245,16 @@ function tableStructure(node, flags) {
1156
1245
  addRows(body, loose, flags);
1157
1246
  loose = [];
1158
1247
  };
1159
- for (const child of node.children) {
1248
+ for (const child of boxChildren(node)) {
1160
1249
  if (child.type === "table-row" || child.type === "table-cell") {
1161
1250
  loose.push(child);
1162
1251
  continue;
1163
1252
  }
1164
1253
  flushLoose();
1165
1254
  if (child.type === "table-caption") captions.push(buildBox(child, inheritedFlags(child, flags), true));
1166
- else if (child.type === "table-header-group") addRows(header, child.children, inheritedFlags(child, flags));
1167
- else if (child.type === "table-footer-group") addRows(footer, child.children, inheritedFlags(child, flags));
1168
- else if (child.type === "table-row-group") addRows(body, child.children, inheritedFlags(child, flags));
1255
+ else if (child.type === "table-header-group") addRows(header, boxChildren(child), inheritedFlags(child, flags));
1256
+ else if (child.type === "table-footer-group") addRows(footer, boxChildren(child), inheritedFlags(child, flags));
1257
+ else if (child.type === "table-row-group") addRows(body, boxChildren(child), inheritedFlags(child, flags));
1169
1258
  }
1170
1259
  flushLoose();
1171
1260
  return { rows: [...header, ...body, ...footer], captions, grid: null };
@@ -1357,15 +1446,44 @@ function gridHorizontal(ctx, containerStyle, item, areaX, areaWidth) {
1357
1446
  return placed;
1358
1447
  }
1359
1448
 
1360
- /** Expands track lists and places items (§7.2, §8.5 row auto-flow). */
1449
+ /** Expands track lists and places items (§7.2, §8.5). */
1361
1450
  function gridStructure(ctx, box, availableWidth, columnGap, definite) {
1362
1451
  const style = box.style;
1363
1452
  const explicitColumns = expandTrackList(style?.gridTemplateColumns, definite ? availableWidth : null, columnGap);
1364
1453
  const explicitRows = expandTrackList(style?.gridTemplateRows, null, style?.rowGap ?? 0);
1365
- const items = box.children.map((child) => ({ box: child, ...gridLines(child.style, explicitColumns.tracks.length, "Column"),
1366
- ...rowLines(child.style, explicitRows.tracks.length) }));
1454
+ const areas = style?.gridTemplateAreas;
1455
+ const explicitColumnCount = Math.max(explicitColumns.tracks.length, areas?.columns ?? 0);
1456
+ const explicitRowCount = Math.max(explicitRows.tracks.length, areas?.rows ?? 0);
1457
+ const items = box.children.map((child) => ({ box: child,
1458
+ ...gridLines(child.style, explicitColumnCount, "Column", areas),
1459
+ ...gridLines(child.style, explicitRowCount, "Row", areas) }));
1460
+ // The column-flow algorithm is the row-flow algorithm with its axes exchanged.
1461
+ const columnFlow = style?.gridAutoFlow?.column === true;
1462
+ if (columnFlow) for (const item of items) transposeGridItem(item);
1463
+ let { columnCount, rowCount } = placeGridItems(items,
1464
+ columnFlow ? explicitRowCount : explicitColumnCount,
1465
+ columnFlow ? explicitColumnCount : explicitRowCount, style?.gridAutoFlow?.dense === true);
1466
+ if (columnFlow) {
1467
+ for (const item of items) transposeGridItem(item);
1468
+ [columnCount, rowCount] = [rowCount, columnCount];
1469
+ }
1470
+ const columnTracks = Array.from({ length: columnCount }, (_, index) =>
1471
+ explicitColumns.tracks[index] ?? cycle(style?.gridAutoColumns, index - explicitColumns.tracks.length));
1472
+ const rowTracks = Array.from({ length: rowCount }, (_, index) =>
1473
+ explicitRows.tracks[index] ?? cycle(style?.gridAutoRows, index - explicitRows.tracks.length));
1474
+ // auto-fit: repeated tracks nobody occupies collapse (§7.2.3.2).
1475
+ const columnCollapsed = columnTracks.map((_, index) => explicitColumns.autoFit.has(index)
1476
+ && !items.some((item) => index >= item.column && index < item.column + item.columnSpan));
1477
+ return { items, columnTracks, rowTracks, columnCollapsed };
1478
+ }
1479
+
1480
+ function transposeGridItem(item) {
1481
+ [item.row, item.column] = [item.column, item.row];
1482
+ [item.rowSpan, item.columnSpan] = [item.columnSpan, item.rowSpan];
1483
+ }
1367
1484
 
1368
- let columnCount = Math.max(1, explicitColumns.tracks.length);
1485
+ function placeGridItems(items, explicitColumnCount, explicitRowCount, dense) {
1486
+ let columnCount = Math.max(1, explicitColumnCount);
1369
1487
  for (const item of items) {
1370
1488
  if (item.column != null) columnCount = Math.max(columnCount, item.column + item.columnSpan);
1371
1489
  else columnCount = Math.max(columnCount, item.columnSpan);
@@ -1395,7 +1513,6 @@ function gridStructure(ctx, box, availableWidth, columnGap, definite) {
1395
1513
  occupy(item);
1396
1514
  }
1397
1515
  // 3. Auto-placed items walk a cursor forward (sparse) or restart (dense).
1398
- const dense = style?.gridAutoFlow?.dense === true;
1399
1516
  let cursorRow = 0;
1400
1517
  let cursorColumn = 0;
1401
1518
  for (const item of items) {
@@ -1425,15 +1542,8 @@ function gridStructure(ctx, box, availableWidth, columnGap, definite) {
1425
1542
  occupy(item);
1426
1543
  }
1427
1544
 
1428
- const rowCount = Math.max(explicitRows.tracks.length, ...items.map((item) => item.row + item.rowSpan), 0);
1429
- const columnTracks = Array.from({ length: columnCount }, (_, index) =>
1430
- explicitColumns.tracks[index] ?? cycle(style?.gridAutoColumns, index - explicitColumns.tracks.length));
1431
- const rowTracks = Array.from({ length: rowCount }, (_, index) =>
1432
- explicitRows.tracks[index] ?? cycle(style?.gridAutoRows, index - explicitRows.tracks.length));
1433
- // auto-fit: repeated tracks nobody occupies collapse (§7.2.3.2).
1434
- const columnCollapsed = columnTracks.map((_, index) => explicitColumns.autoFit.has(index)
1435
- && !items.some((item) => index >= item.column && index < item.column + item.columnSpan));
1436
- return { items, columnTracks, rowTracks, columnCollapsed };
1545
+ const rowCount = Math.max(explicitRowCount, ...items.map((item) => item.row + item.rowSpan), 0);
1546
+ return { columnCount, rowCount };
1437
1547
  }
1438
1548
 
1439
1549
  function cycle(tracks, index) {
@@ -1441,13 +1551,22 @@ function cycle(tracks, index) {
1441
1551
  return list[((index % list.length) + list.length) % list.length];
1442
1552
  }
1443
1553
 
1444
- function gridLines(style, explicitCount, axis) {
1554
+ function gridLines(style, explicitCount, axis, templateAreas) {
1445
1555
  const start = style?.[`grid${axis}Start`] ?? "auto";
1446
1556
  const end = style?.[`grid${axis}End`] ?? "auto";
1447
1557
  // Line numbers are 1-based; negatives count back from the explicit end line.
1448
1558
  const index = (line) => line > 0 ? line - 1 : Math.max(0, explicitCount + 1 + line);
1449
- let from = start.line != null ? index(start.line) : null;
1450
- let to = end.line != null ? index(end.line) : null;
1559
+ const areaLine = (name, end) => {
1560
+ if (!name || !templateAreas) return null;
1561
+ const key = axis.toLowerCase();
1562
+ const direct = templateAreas.areas[name];
1563
+ if (direct) return direct[end ? `${key}End` : key];
1564
+ const suffix = /^(.*)-(start|end)$/.exec(name);
1565
+ const area = suffix && templateAreas.areas[suffix[1]];
1566
+ return area ? area[suffix[2] === "end" ? `${key}End` : key] : null;
1567
+ };
1568
+ let from = start.line != null ? index(start.line) : areaLine(start.name, false);
1569
+ let to = end.line != null ? index(end.line) : areaLine(end.name, true);
1451
1570
  let span = 1;
1452
1571
  if (from != null && to != null) {
1453
1572
  if (to < from) [from, to] = [to, from];
@@ -1464,10 +1583,6 @@ function gridLines(style, explicitCount, axis) {
1464
1583
  return axis === "Column" ? { column: from, columnSpan: span } : { row: from, rowSpan: span };
1465
1584
  }
1466
1585
 
1467
- function rowLines(style, explicitCount) {
1468
- return gridLines(style, explicitCount, "Row");
1469
- }
1470
-
1471
1586
  /** Expands repeat() and resolves auto-fill/auto-fit repetitions (§7.2.3.2). */
1472
1587
  function expandTrackList(list, available, gap) {
1473
1588
  const tracks = [];
@@ -2542,7 +2657,7 @@ function cachedMetrics(fontMetrics) {
2542
2657
  const byStyle = new WeakMap();
2543
2658
  const metricsFor = (style) => {
2544
2659
  const fontSize = style?.fontSize ?? 16;
2545
- const key = `${fontSize}/${style?.fontWeight ?? 400}/${style?.fontFamily?.join(",")}`;
2660
+ const key = `${fontSize}/${style?.fontWeight ?? 400}/${style?.fontStyle ?? "normal"}/${style?.fontStretch ?? 100}/${style?.fontFamily?.join(",")}`;
2546
2661
  let metrics = cache.get(key);
2547
2662
  if (!metrics) {
2548
2663
  metrics = typeof fontMetrics === "function" ? fontMetrics(style ?? null) : null;
@@ -2567,12 +2682,12 @@ function cachedMetrics(fontMetrics) {
2567
2682
  * whatever box it gets), so each dimension falls back to 300x150 alone.
2568
2683
  */
2569
2684
  function frameDimensions(node, referenceWidth, referenceHeight) {
2570
- const resolve = (cssValue, attribute, fallback, reference) => {
2571
- const value = cssValue !== "auto" && cssValue != null ? resolveLength(cssValue, reference ?? NaN) : attribute;
2685
+ const resolve = (cssValue, fallback, reference) => {
2686
+ const value = cssValue !== "auto" && cssValue != null ? resolveLength(cssValue, reference ?? NaN) : null;
2572
2687
  return Number.isFinite(value) ? value : fallback;
2573
2688
  };
2574
- let width = resolve(node.style?.width, node.widthAttribute, 300, referenceWidth);
2575
- const height = resolve(node.style?.height, node.heightAttribute, 150, referenceHeight);
2689
+ let width = resolve(node.style?.width, 300, referenceWidth);
2690
+ const height = resolve(node.style?.height, 150, referenceHeight);
2576
2691
  const maximum = node.style?.maxWidth !== "none" && node.style?.maxWidth != null ? resolveLength(node.style.maxWidth, referenceWidth) : NaN;
2577
2692
  if (Number.isFinite(maximum) && width > maximum) width = maximum;
2578
2693
  return { width: Math.max(0, width), height: Math.max(0, height) };
@@ -2604,25 +2719,29 @@ function svgDimensions(node, referenceWidth, referenceHeight) {
2604
2719
  function imageDimensions(node, referenceWidth, referenceHeight = null) {
2605
2720
  if (node.tagName === "IFRAME") return frameDimensions(node, referenceWidth, referenceHeight);
2606
2721
  if (node.tagName === "SVG") return svgDimensions(node, referenceWidth, referenceHeight);
2607
- const intrinsicWidth = node.resource?.width ?? node.widthAttribute ?? 300;
2608
- const intrinsicHeight = node.resource?.height ?? node.heightAttribute ?? 150;
2609
- const ratio = intrinsicWidth / intrinsicHeight;
2722
+ // A canvas's attributes are its intrinsic bitmap dimensions. Its cached
2723
+ // paint snapshot can still describe the previous bitmap during layout.
2724
+ const canvas = node.tagName === "CANVAS";
2725
+ const intrinsicWidth = (canvas ? node.widthAttribute : node.resource?.width) ?? node.widthAttribute ?? 300;
2726
+ const intrinsicHeight = (canvas ? node.heightAttribute : node.resource?.height) ?? node.heightAttribute ?? 150;
2727
+ const emptyCanvas = node.tagName === "CANVAS" && !(intrinsicWidth > 0 && intrinsicHeight > 0);
2728
+ const ratio = emptyCanvas ? null : intrinsicWidth / intrinsicHeight;
2610
2729
  const cssWidth = node.style?.width;
2611
2730
  const cssHeight = node.style?.height;
2612
2731
  let width = cssWidth !== "auto" && cssWidth != null
2613
2732
  ? resolveLength(cssWidth, referenceWidth)
2614
- : node.widthAttribute;
2733
+ : null;
2615
2734
  let height = cssHeight !== "auto" && cssHeight != null
2616
2735
  ? resolveLength(cssHeight, referenceHeight ?? NaN)
2617
- : node.heightAttribute;
2736
+ : null;
2618
2737
  if (!Number.isFinite(width)) width = null;
2619
2738
  if (!Number.isFinite(height)) height = null;
2620
2739
  if (width == null && height == null) [width, height] = [intrinsicWidth, intrinsicHeight];
2621
- else if (width == null) width = height * ratio;
2622
- else if (height == null) height = width / ratio;
2740
+ else if (width == null) width = ratio == null ? intrinsicWidth : height * ratio;
2741
+ else if (height == null) height = ratio == null ? intrinsicHeight : width / ratio;
2623
2742
  if (node.style?.maxWidth !== "none" && node.style?.maxWidth != null) {
2624
2743
  const maximum = resolveLength(node.style.maxWidth, referenceWidth);
2625
- if (Number.isFinite(maximum) && width > maximum) [width, height] = [maximum, maximum / ratio];
2744
+ if (Number.isFinite(maximum) && width > maximum) [width, height] = [maximum, ratio == null ? height : maximum / ratio];
2626
2745
  }
2627
2746
  if (node.tagName === "VIDEO") {
2628
2747
  if (Number.isFinite(referenceWidth) && width > referenceWidth) [width, height] = [referenceWidth, referenceWidth / ratio];
@@ -2631,7 +2750,8 @@ function imageDimensions(node, referenceWidth, referenceHeight = null) {
2631
2750
  if (Number.isFinite(minHeight) && height < minHeight) height = minHeight;
2632
2751
  if (Number.isFinite(maxHeight) && height > maxHeight) [width, height] = [maxHeight * ratio, maxHeight];
2633
2752
  }
2634
- return { width: Math.max(1, width), height: Math.max(1, height) };
2753
+ const minimum = node.tagName === "CANVAS" ? 0 : 1;
2754
+ return { width: Math.max(minimum, width), height: Math.max(minimum, height) };
2635
2755
  }
2636
2756
 
2637
2757
  function positiveInteger(value, fallback) {
@@ -2658,8 +2778,10 @@ function resolveLength(value, reference) {
2658
2778
  if (!value || typeof value !== "object") return value;
2659
2779
  if (value.unit === "%") return reference * value.value / 100;
2660
2780
  if (value.unit === "math" && value.kind === "calc") return value.px + reference * value.percent / 100;
2781
+ if (value.unit === "math" && value.kind === "scale") return value.factor * resolveLength(value.value, reference);
2661
2782
  if (value.unit === "math") {
2662
2783
  const values = value.values.map((part) => resolveLength(part, reference));
2784
+ if (value.kind === "sum") return values.reduce((sum, part) => sum + part, 0);
2663
2785
  if (value.kind === "min") return Math.min(...values);
2664
2786
  if (value.kind === "max") return Math.max(...values);
2665
2787
  return Math.max(values[0], Math.min(values[1], values[2]));
@@ -7,7 +7,7 @@
7
7
  * on finish/fail, which is how a decoder knows its snapshot is stale.
8
8
  * `waitFor` resolves when `size` reaches the mark or the pipe is finished.
9
9
  */
10
- export function createBytePipe() {
10
+ export function createBytePipe({ retainBytes = true } = {}) {
11
11
  const chunks = [];
12
12
  let size = 0;
13
13
  let generation = 0;
@@ -25,7 +25,7 @@ export function createBytePipe() {
25
25
  if (done || bytes == null) return;
26
26
  const chunk = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
27
27
  if (!chunk.byteLength) return;
28
- chunks.push(chunk);
28
+ if (retainBytes) chunks.push(chunk);
29
29
  size += chunk.byteLength;
30
30
  generation += 1;
31
31
  wake();