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.
- package/README.md +86 -1
- package/lib/cdp-server.js +47 -14
- package/lib/clipboard.js +22 -0
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +15 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +40 -11
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +213 -91
- package/lib/media/byte-pipe.js +2 -2
- package/lib/media/position-audio.js +10 -1
- package/lib/media/source-snapshots.js +126 -0
- package/lib/media/video-keyframe.js +17 -4
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +8 -28
- package/lib/paint/frame-cache.js +25 -0
- package/lib/paint/frame.js +77 -16
- package/lib/paint/paragraph-text.js +24 -0
- package/lib/paint/raster-readback.js +41 -0
- package/lib/paint/raw-frames.js +47 -0
- package/lib/paint/rounded-rect.js +82 -0
- package/lib/paint/shared-frame-pool.js +105 -0
- package/lib/render-tree/index.js +38 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +233 -80
- package/lib/renderer/broadcast-channel.js +76 -0
- package/lib/renderer/canvas-2d.js +257 -14
- package/lib/renderer/canvas-text.js +120 -0
- package/lib/renderer/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +34 -40
- package/lib/renderer/custom-element-reactions.js +25 -1
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/host.js +133 -98
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +52 -5
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +287 -99
- package/lib/renderer/page-renderer.js +663 -239
- package/lib/renderer/prepared-script-cache.js +50 -0
- package/lib/renderer/rendered-text.js +108 -0
- package/lib/renderer/rewrite-worker.js +4 -3
- package/lib/renderer/script-rewrite.js +13 -2
- package/lib/renderer/scroll-into-view.js +28 -0
- package/lib/renderer/slot-assignment.js +60 -0
- package/lib/renderer/video-presentation.js +23 -4
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +611 -104
- package/lib/style/selector-traits.js +49 -5
- package/package.json +1 -1
- package/test/broadcast-channel.test.js +26 -0
- package/test/canvas-2d.test.js +83 -2
- package/test/canvas-paint.test.js +9 -0
- package/test/canvas-text.test.js +41 -0
- package/test/cdp-stream.test.js +56 -0
- package/test/clipboard.test.js +35 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +258 -1
- package/test/display-list.test.js +12 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/fetch-lifecycle.test.js +161 -0
- package/test/fixtures/browser-raw-demo.html +36 -0
- package/test/fixtures/canvas-paint.js +117 -0
- package/test/fixtures/canvas-text.js +42 -0
- package/test/fixtures/image-lifetime.js +90 -0
- package/test/fixtures/media-seek-snapshot.js +77 -0
- package/test/fixtures/mse-audio-chirp.webm +0 -0
- package/test/fixtures/mse-switch-large.mp4 +0 -0
- package/test/fixtures/mse-switch-small.mp4 +0 -0
- package/test/fixtures/raw-capture-benchmark.json +76 -0
- package/test/fixtures/renderer-memory.js +110 -0
- package/test/fixtures/summary-style.js +28 -0
- package/test/font-loading.test.js +97 -0
- package/test/grid.test.js +58 -0
- package/test/hit-test.test.js +21 -1
- package/test/image-lifetime.test.js +12 -0
- package/test/image-resources.test.js +94 -0
- package/test/local-media.test.js +84 -0
- package/test/media-seek-snapshot.test.js +11 -0
- package/test/media-source-config.test.js +98 -0
- package/test/media-source-webm-accounting.test.js +98 -0
- package/test/media-source-webm.test.js +108 -0
- package/test/navigation.test.js +1 -0
- package/test/network-service.test.js +388 -0
- package/test/page-scripts.test.js +1 -1
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/position-audio.test.js +24 -0
- package/test/positioning.test.js +22 -0
- package/test/prepared-script-cache.test.js +45 -0
- package/test/pulse-client.test.js +10 -3
- package/test/raster-readback.test.js +40 -0
- package/test/raw-capture.test.js +152 -0
- package/test/render-tree.test.js +28 -0
- package/test/renderer-memory.test.js +11 -0
- package/test/selector-traits.test.js +24 -0
- package/test/shared-frame-pool.test.js +63 -0
- package/test/style-sheets.test.js +51 -0
- package/test/svg.test.js +29 -0
- package/test/text-layout.test.js +106 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +69 -0
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
- package/tools/browser-lab.js +78 -0
- package/tools/browser-soak.js +75 -0
- package/tools/network-reference.js +49 -0
- package/tools/process-memory.js +38 -0
- package/tools/script-benchmark.js +25 -0
- package/ai-debug.log +0 -5537
package/lib/input/hit-test.js
CHANGED
|
@@ -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
|
-
|
|
86
|
-
return domNode?.nodeType === 1 ? domNode : domNode?.parentElement ?? null;
|
|
91
|
+
return nodeId == null ? null : tree.nodesById.get(nodeId) ?? null;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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,
|
|
5
|
-
*
|
|
6
|
-
* layout (CSS Grid 2 §8 placement and §12 track sizing,
|
|
7
|
-
*
|
|
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 (
|
|
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
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
1167
|
-
else if (child.type === "table-footer-group") addRows(footer, child
|
|
1168
|
-
else if (child.type === "table-row-group") addRows(body, child
|
|
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
|
|
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
|
|
1366
|
-
|
|
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
|
-
|
|
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(
|
|
1429
|
-
|
|
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
|
-
|
|
1450
|
-
|
|
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,
|
|
2571
|
-
const value = cssValue !== "auto" && cssValue != null ? resolveLength(cssValue, reference ?? NaN) :
|
|
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,
|
|
2575
|
-
const height = resolve(node.style?.height,
|
|
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
|
-
|
|
2608
|
-
|
|
2609
|
-
const
|
|
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
|
-
:
|
|
2733
|
+
: null;
|
|
2615
2734
|
let height = cssHeight !== "auto" && cssHeight != null
|
|
2616
2735
|
? resolveLength(cssHeight, referenceHeight ?? NaN)
|
|
2617
|
-
:
|
|
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
|
-
|
|
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]));
|
package/lib/media/byte-pipe.js
CHANGED
|
@@ -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();
|