buninu-browser 0.2.1 → 0.2.2
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/AGENTS.md +4 -0
- package/README.md +74 -9
- package/bunfig.toml +1 -0
- package/buninu-browser.js +4 -0
- package/index.js +5 -0
- package/lib/audio/piano.js +24 -1
- package/lib/audio/pulse-client.js +120 -12
- package/lib/cdp-server.js +16 -11
- package/lib/happy-dom/parser.js +24 -4
- package/lib/headless-shell.js +66 -19
- package/lib/input/hit-test.js +25 -7
- package/lib/layout/text-layout.js +196 -49
- package/lib/log.js +5 -2
- package/lib/media/byte-pipe.js +82 -0
- package/lib/media/position-audio.js +42 -0
- package/lib/network/network-service.js +161 -14
- package/lib/paint/display-list.js +300 -14
- package/lib/paint/gradient.js +11 -5
- package/lib/render-tree/index.js +17 -2
- package/lib/render-tree/svg.js +2 -1
- package/lib/renderer/bindings.js +985 -68
- package/lib/renderer/canvas-2d.js +92 -0
- package/lib/renderer/host.js +158 -8
- package/lib/renderer/interfaces.js +215 -0
- package/lib/renderer/main.js +160 -12
- package/lib/renderer/media-source.js +819 -0
- package/lib/renderer/messaging.js +411 -0
- package/lib/renderer/native-functions.js +113 -0
- package/lib/renderer/page-realm.js +1405 -198
- package/lib/renderer/page-renderer.js +1736 -193
- package/lib/renderer/rejections.js +31 -0
- package/lib/renderer/rewrite-worker.js +22 -0
- package/lib/renderer/script-rewrite.js +805 -57
- package/lib/renderer/trusted-types.js +129 -0
- package/lib/renderer/workers.js +327 -0
- package/lib/screenshot-cli.js +2 -0
- package/lib/style/computed-style.js +162 -22
- package/lib/user-agent.js +14 -2
- package/package.json +4 -1
- package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
- package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
- package/patches/happy-dom/README.md +26 -0
- package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
- package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
- package/patches/ses/README.md +4 -0
- package/test/canvas-2d.test.js +55 -0
- package/test/compositor.test.js +3 -1
- package/test/computed-style.test.js +38 -1
- package/test/display-list.test.js +74 -1
- package/test/download.test.js +35 -0
- package/test/fixtures/long-video.mp4 +0 -0
- package/test/fixtures/media-sync.js +57 -0
- package/test/happy-dom-platform-adapters.test.js +25 -0
- package/test/headless-shell.test.js +1 -1
- package/test/hit-test.test.js +25 -1
- package/test/media-source.test.js +156 -0
- package/test/media-sync.test.js +12 -0
- package/test/mobile.test.js +2 -2
- package/test/navigation.test.js +61 -0
- package/test/network-service.test.js +14 -0
- package/test/page-scripts.test.js +1045 -7
- package/test/position-audio.test.js +67 -0
- package/test/positioning.test.js +95 -1
- package/test/pulse-client.test.js +69 -0
- package/test/renderer-host.test.js +5 -2
- package/test/screenshot-cli.test.js +25 -1
- package/test/script-rewrite.test.js +111 -6
- package/test/setup.js +5 -0
- package/test/user-agent.test.js +60 -4
- package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
- package/vendor/ses/dist/ses.mjs +52 -6
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* and `fragments` as positioned line boxes sorted by y.
|
|
19
19
|
*/
|
|
20
20
|
const ZERO_BOX = Object.freeze([0, 0, 0, 0]);
|
|
21
|
+
const ZERO_OFFSET = Object.freeze({ x: 0, y: 0 });
|
|
21
22
|
const ZERO_STRUT = Object.freeze({ positive: 0, negative: 0 });
|
|
22
23
|
const DEFAULT_ASCENT = 0.93;
|
|
23
24
|
const DEFAULT_CONTENT_HEIGHT = 1.17;
|
|
@@ -51,6 +52,9 @@ export function layoutText(tree, options = {}) {
|
|
|
51
52
|
stackingContext: ROOT_LAYER,
|
|
52
53
|
layerCount: 1,
|
|
53
54
|
initialContainingBlock: null,
|
|
55
|
+
// The content box of the block being laid out: the containing block of
|
|
56
|
+
// its in-flow children, for percentages in their relative offsets.
|
|
57
|
+
containing: null,
|
|
54
58
|
// Static positions recorded for out-of-flow boxes, in order (see
|
|
55
59
|
// cacheBlockResult), and those boxes by node for replays.
|
|
56
60
|
staticPositions: [],
|
|
@@ -79,6 +83,7 @@ export function layoutText(tree, options = {}) {
|
|
|
79
83
|
// establishes the BFC, so body margins collapse with its children's.
|
|
80
84
|
const horizontal = resolveHorizontal(ctx, root, x, containingWidth, false);
|
|
81
85
|
const rootY = y + strutValue(topStrut(root));
|
|
86
|
+
ctx.containing = { width: containingWidth, height: ctx.initialContainingBlock.height || null };
|
|
82
87
|
const result = layoutBlock(ctx, root, horizontal.x, horizontal.width, rootY, collapsesThroughTop(root));
|
|
83
88
|
const height = rootY + result.height + strutValue(bottomStrut(root));
|
|
84
89
|
|
|
@@ -317,9 +322,12 @@ function hasInlineContent(items) {
|
|
|
317
322
|
* then applies relative offsets and translations to everything it produced.
|
|
318
323
|
*/
|
|
319
324
|
function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = null) {
|
|
320
|
-
const cached = cachedBlockResult(ctx, box, width, y, x, absorbTop, forcedHeight);
|
|
321
|
-
if (cached) return cached;
|
|
322
325
|
const style = box.style;
|
|
326
|
+
// Its containing block, for percentages in relative offsets.
|
|
327
|
+
const containing = ctx.containing;
|
|
328
|
+
const key = blockCacheKey(width, absorbTop, forcedHeight, style?.position === "relative" ? containing : null);
|
|
329
|
+
const cached = cachedBlockResult(ctx, box, key, y, x);
|
|
330
|
+
if (cached) return cached;
|
|
323
331
|
const boxStart = ctx.boxes.length;
|
|
324
332
|
const fragmentStart = ctx.fragments.length;
|
|
325
333
|
const layerStart = ctx.layerCount;
|
|
@@ -343,10 +351,10 @@ function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = nu
|
|
|
343
351
|
if (box.absolutes?.length) layoutAbsolutes(ctx, box, record);
|
|
344
352
|
let dx = 0;
|
|
345
353
|
let dy = 0;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
dx =
|
|
349
|
-
dy =
|
|
354
|
+
const offset = relativeOffset(style, containing ?? { width, height: null });
|
|
355
|
+
if (offset) {
|
|
356
|
+
dx = offset.x;
|
|
357
|
+
dy = offset.y;
|
|
350
358
|
}
|
|
351
359
|
if (style?.transform) {
|
|
352
360
|
dx += numberOrZero(resolveLength(style.transform.x, record.width));
|
|
@@ -357,14 +365,29 @@ function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = nu
|
|
|
357
365
|
if (dx || dy) shiftOutput(ctx, boxStart, fragmentStart, dx, dy);
|
|
358
366
|
ctx.layer = previousLayer;
|
|
359
367
|
ctx.stackingContext = previousContext;
|
|
360
|
-
cacheBlockResult(ctx, box,
|
|
368
|
+
cacheBlockResult(ctx, box, key, x, y, result, {
|
|
361
369
|
boxStart, fragmentStart, layerStart, staticStart, layer: previousLayer, context: previousContext,
|
|
362
370
|
});
|
|
363
371
|
return result;
|
|
364
372
|
}
|
|
365
373
|
|
|
366
|
-
function blockCacheKey(width, absorbTop, forcedHeight) {
|
|
367
|
-
|
|
374
|
+
function blockCacheKey(width, absorbTop, forcedHeight, containing) {
|
|
375
|
+
const key = `${width}\n${absorbTop ? 1 : 0}\n${forcedHeight ?? "auto"}`;
|
|
376
|
+
return containing ? `${key}\n${containing.width}\n${containing.height ?? "auto"}` : key;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The offset of a relatively positioned box (CSS 2 §9.4.3). Percentages are
|
|
381
|
+
* of its containing block; one of an indefinite height is auto.
|
|
382
|
+
*/
|
|
383
|
+
function relativeOffset(style, containing) {
|
|
384
|
+
if (style?.position !== "relative" || !style.inset) return null;
|
|
385
|
+
const [top, right, bottom, left] = style.inset.map((value, index) => value === "auto"
|
|
386
|
+
? null
|
|
387
|
+
: numberOrNull(resolveLength(value, (index % 2 ? containing?.width : containing?.height) ?? NaN)));
|
|
388
|
+
const x = left ?? (right != null ? -right : 0);
|
|
389
|
+
const y = top ?? (bottom != null ? -bottom : 0);
|
|
390
|
+
return x || y ? { x, y } : null;
|
|
368
391
|
}
|
|
369
392
|
|
|
370
393
|
/** Where an out-of-flow box would have been in flow; recorded so that a replayed block sets it again. */
|
|
@@ -374,9 +397,9 @@ function setStaticPosition(ctx, box, position) {
|
|
|
374
397
|
}
|
|
375
398
|
|
|
376
399
|
/** Replays a retained formatting-context result at its new flow position. */
|
|
377
|
-
function cachedBlockResult(ctx, box,
|
|
400
|
+
function cachedBlockResult(ctx, box, key, y, x) {
|
|
378
401
|
if (!ctx.blockResults || box.anonymous) return null;
|
|
379
|
-
const cached = ctx.blockResults.get(box.node)?.get(
|
|
402
|
+
const cached = ctx.blockResults.get(box.node)?.get(key);
|
|
380
403
|
if (!cached) return null;
|
|
381
404
|
const dx = x - cached.x;
|
|
382
405
|
const dy = y - cached.y;
|
|
@@ -423,7 +446,7 @@ function cachedBlockResult(ctx, box, width, y, x, absorbTop, forcedHeight) {
|
|
|
423
446
|
* static positions it recorded go along. Output in a layer made before the
|
|
424
447
|
* block (which should not occur) is not retained.
|
|
425
448
|
*/
|
|
426
|
-
function cacheBlockResult(ctx, box,
|
|
449
|
+
function cacheBlockResult(ctx, box, key, x, y, result, start) {
|
|
427
450
|
if (!ctx.blockResults || box.anonymous) return;
|
|
428
451
|
const boxes = ctx.boxes.slice(start.boxStart);
|
|
429
452
|
const fragments = ctx.fragments.slice(start.fragmentStart);
|
|
@@ -451,10 +474,10 @@ function cacheBlockResult(ctx, box, width, x, y, absorbTop, forcedHeight, result
|
|
|
451
474
|
if (!entries) ctx.blockResults.set(box.node, entries = new Map());
|
|
452
475
|
// A node normally sees one containing width. Bound unusual cases such as a
|
|
453
476
|
// reused table cell so the retained cache cannot grow without limit.
|
|
454
|
-
if (!entries.has(
|
|
477
|
+
if (!entries.has(key) && entries.size >= 4) {
|
|
455
478
|
entries.delete(entries.keys().next().value);
|
|
456
479
|
}
|
|
457
|
-
entries.set(
|
|
480
|
+
entries.set(key, {
|
|
458
481
|
x,
|
|
459
482
|
y,
|
|
460
483
|
result: { ...result },
|
|
@@ -576,7 +599,7 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
576
599
|
const record = { nodeId: box.node.id, x, y, width, height: 0, style };
|
|
577
600
|
ctx.boxes.push(record);
|
|
578
601
|
if (box.replaced) {
|
|
579
|
-
const dimensions = imageDimensions(box.node, width);
|
|
602
|
+
const dimensions = imageDimensions(box.node, width, ctx.containing?.height);
|
|
580
603
|
const imageX = x + border[3] + padding[3];
|
|
581
604
|
const imageY = y + border[0] + padding[0];
|
|
582
605
|
ctx.fragments.push(lineFragment(box.style, "", [box.node.id], [{
|
|
@@ -608,6 +631,21 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
608
631
|
let columnItems = null;
|
|
609
632
|
let gridResult = null;
|
|
610
633
|
let baseline = null;
|
|
634
|
+
const toContentHeight = (value) => style.boxSizing === "border-box"
|
|
635
|
+
? Math.max(0, value - border[0] - border[2] - padding[0] - padding[2])
|
|
636
|
+
: value;
|
|
637
|
+
// A percentage height (and min/max) resolves against the containing block
|
|
638
|
+
// only when that height is definite (CSS 2 §10.5); otherwise it is auto.
|
|
639
|
+
const containingHeight = ctx.containing?.height;
|
|
640
|
+
const resolveHeight = (value) => numberOrNull(resolveLength(value, containingHeight ?? NaN));
|
|
641
|
+
const specifiedHeight = resolveHeight(style?.height);
|
|
642
|
+
const previousContaining = ctx.containing;
|
|
643
|
+
ctx.containing = {
|
|
644
|
+
width: contentWidth,
|
|
645
|
+
height: forcedHeight != null
|
|
646
|
+
? Math.max(0, forcedHeight - border[0] - border[2] - padding[0] - padding[2])
|
|
647
|
+
: specifiedHeight != null ? toContentHeight(specifiedHeight) : null,
|
|
648
|
+
};
|
|
611
649
|
|
|
612
650
|
if (box.table) {
|
|
613
651
|
const result = layoutTable(ctx, box, contentX, contentWidth, contentTop);
|
|
@@ -639,7 +677,9 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
639
677
|
: child.style.alignSelf;
|
|
640
678
|
const shrink = !["normal", "stretch"].includes(alignment);
|
|
641
679
|
const placed = resolveHorizontal(ctx, child, contentX, contentWidth, shrink);
|
|
642
|
-
|
|
680
|
+
// Auto margins on the cross axis (left, right) align the item instead (Flexbox §8.1).
|
|
681
|
+
const crossMargins = child.style?.margin ?? ZERO_BOX;
|
|
682
|
+
const autoMargins = crossMargins[1] === "auto" || crossMargins[3] === "auto";
|
|
643
683
|
let childX = placed.x;
|
|
644
684
|
if (shrink && !autoMargins) {
|
|
645
685
|
const free = contentWidth - placed.width - placed.marginLeft - placed.marginRight;
|
|
@@ -685,16 +725,12 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
685
725
|
contentBottom = collapsesThroughBottom(box) ? cursor : cursor + strutValue(pending ?? ZERO_STRUT);
|
|
686
726
|
}
|
|
687
727
|
|
|
728
|
+
ctx.containing = previousContaining;
|
|
688
729
|
let contentHeight = Math.max(0, contentBottom - contentTop);
|
|
689
|
-
const toContentHeight = (value) => style.boxSizing === "border-box"
|
|
690
|
-
? Math.max(0, value - border[0] - border[2] - padding[0] - padding[2])
|
|
691
|
-
: value;
|
|
692
|
-
const specifiedHeight = numberOrNull(style?.height);
|
|
693
730
|
if (specifiedHeight != null) contentHeight = toContentHeight(specifiedHeight);
|
|
694
|
-
|
|
695
|
-
const maxHeight = numberOrNull(style?.maxHeight);
|
|
731
|
+
const maxHeight = style?.maxHeight === "none" ? null : resolveHeight(style?.maxHeight);
|
|
696
732
|
if (maxHeight != null) contentHeight = Math.min(contentHeight, toContentHeight(maxHeight));
|
|
697
|
-
const minHeight =
|
|
733
|
+
const minHeight = resolveHeight(style?.minHeight);
|
|
698
734
|
if (minHeight != null) contentHeight = Math.max(contentHeight, toContentHeight(minHeight));
|
|
699
735
|
if (columnItems?.length) justifyColumn(ctx, columnItems, style?.justifyContent, contentHeight - (contentBottom - contentTop));
|
|
700
736
|
if (gridResult) alignGridContent(ctx, gridResult, style?.alignContent, contentHeight - (contentBottom - contentTop));
|
|
@@ -817,13 +853,9 @@ function layoutFlexRow(ctx, box, x, width, y) {
|
|
|
817
853
|
}
|
|
818
854
|
let crossSize = Math.max(0, ...lineItems.map((item) => item.height + item.marginTop + item.marginBottom));
|
|
819
855
|
// A single-line container with a definite height gives its line that cross size (§9.4.15).
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
crossSize = style.boxSizing === "border-box"
|
|
824
|
-
? Math.max(0, definiteHeight - padding[0] - padding[2] - borderWidths(style)[0] - borderWidths(style)[2])
|
|
825
|
-
: definiteHeight;
|
|
826
|
-
}
|
|
856
|
+
// ctx.containing.height is that content-box height, percentages already resolved.
|
|
857
|
+
const definiteHeight = ctx.containing?.height;
|
|
858
|
+
if (lines.length === 1 && definiteHeight != null) crossSize = definiteHeight;
|
|
827
859
|
|
|
828
860
|
// Main axis: auto margins absorb free space first, then justify-content.
|
|
829
861
|
let free = width - gaps - lineItems.reduce((sum, item) => sum + item.target + item.marginLeft + item.marginRight, 0);
|
|
@@ -1730,6 +1762,7 @@ function maxContentWidth(ctx, box) {
|
|
|
1730
1762
|
}
|
|
1731
1763
|
const maxWidth = numberOrNull(style?.maxWidth);
|
|
1732
1764
|
if (maxWidth != null) width = Math.min(width, style.boxSizing === "border-box" ? maxWidth : maxWidth + extra);
|
|
1765
|
+
width = atLeastMinWidth(style, width, extra);
|
|
1733
1766
|
ctx.maxContent.set(cacheKey, width);
|
|
1734
1767
|
return width;
|
|
1735
1768
|
}
|
|
@@ -1763,10 +1796,17 @@ function minContentWidth(ctx, box) {
|
|
|
1763
1796
|
? inlineMinContentWidth(ctx, child)
|
|
1764
1797
|
: outer(child))) + extra;
|
|
1765
1798
|
}
|
|
1799
|
+
width = atLeastMinWidth(style, width, extra);
|
|
1766
1800
|
ctx.minContent.set(cacheKey, width);
|
|
1767
1801
|
return width;
|
|
1768
1802
|
}
|
|
1769
1803
|
|
|
1804
|
+
/** min-width wins over the intrinsic width (and max-width); a percentage is of an unknown width here. */
|
|
1805
|
+
function atLeastMinWidth(style, width, extra) {
|
|
1806
|
+
const minWidth = numberOrNull(style?.minWidth);
|
|
1807
|
+
return minWidth == null ? width : Math.max(width, style.boxSizing === "border-box" ? minWidth : minWidth + extra);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1770
1810
|
function verticalMargins(style) {
|
|
1771
1811
|
const margin = style?.margin ?? ZERO_BOX;
|
|
1772
1812
|
return { top: numberOrZero(margin[0]), bottom: numberOrZero(margin[2]) };
|
|
@@ -1831,6 +1871,8 @@ function layoutInline(ctx, ifc, x, width, y) {
|
|
|
1831
1871
|
}
|
|
1832
1872
|
return atom;
|
|
1833
1873
|
};
|
|
1874
|
+
// Percentage sizes of inline images.
|
|
1875
|
+
atomFor.containing = ctx.containing;
|
|
1834
1876
|
const lines = breakLines(ctx, paragraph, width, atomFor);
|
|
1835
1877
|
let cursor = y;
|
|
1836
1878
|
let baseline = null;
|
|
@@ -2042,9 +2084,26 @@ function truncateForEllipsis(ctx, style, paragraph, segments, line, width) {
|
|
|
2042
2084
|
return { text, width: ellipsisWidth, run: lastText.run, metrics: textMetrics(ctx, lastText.run.style) };
|
|
2043
2085
|
}
|
|
2044
2086
|
|
|
2087
|
+
/** Space beside an inline replaced element's content: its margins, borders and padding. */
|
|
2045
2088
|
function inlineImageMargins(node) {
|
|
2046
2089
|
const margin = node.style?.margin ?? ZERO_BOX;
|
|
2047
|
-
|
|
2090
|
+
const border = borderWidths(node.style);
|
|
2091
|
+
const padding = node.style?.padding ?? ZERO_BOX;
|
|
2092
|
+
return {
|
|
2093
|
+
left: numberOrZero(margin[3]) + border[3] + padding[3],
|
|
2094
|
+
right: numberOrZero(margin[1]) + border[1] + padding[1],
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
/** The same above and below its content (its margin box sits on the line). */
|
|
2099
|
+
function inlineImageVerticalInsets(node) {
|
|
2100
|
+
const margin = node.style?.margin ?? ZERO_BOX;
|
|
2101
|
+
const border = borderWidths(node.style);
|
|
2102
|
+
const padding = node.style?.padding ?? ZERO_BOX;
|
|
2103
|
+
return {
|
|
2104
|
+
top: numberOrZero(margin[0]) + border[0] + padding[0],
|
|
2105
|
+
bottom: numberOrZero(margin[2]) + border[2] + padding[2],
|
|
2106
|
+
};
|
|
2048
2107
|
}
|
|
2049
2108
|
|
|
2050
2109
|
/**
|
|
@@ -2092,7 +2151,8 @@ function runSliceWidth(ctx, run, start, end, text, atomFor) {
|
|
|
2092
2151
|
if (end <= start) return 0;
|
|
2093
2152
|
if (run.kind === "image") {
|
|
2094
2153
|
const margins = inlineImageMargins(run.node);
|
|
2095
|
-
|
|
2154
|
+
const containing = atomFor.containing;
|
|
2155
|
+
return imageDimensions(run.node, containing?.width ?? Infinity, containing?.height).width + margins.left + margins.right;
|
|
2096
2156
|
}
|
|
2097
2157
|
if (run.kind === "atomic") {
|
|
2098
2158
|
const atom = atomFor(run);
|
|
@@ -2172,13 +2232,20 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2172
2232
|
const strut = textMetrics(ctx, style);
|
|
2173
2233
|
let ascent = strut.ascent;
|
|
2174
2234
|
let descent = strut.descent;
|
|
2235
|
+
// Floats (approximated as atomic inlines) sit at the top of the line instead of on its baseline.
|
|
2236
|
+
let floatHeight = 0;
|
|
2175
2237
|
for (const segment of segments) {
|
|
2176
2238
|
const { run } = segment;
|
|
2177
|
-
if (run.kind === "
|
|
2178
|
-
|
|
2179
|
-
|
|
2239
|
+
if (run.kind === "atomic" && isFloat(run.style)) {
|
|
2240
|
+
const atom = atomFor(run);
|
|
2241
|
+
floatHeight = Math.max(floatHeight, atom.marginTop + atom.height + atom.marginBottom);
|
|
2242
|
+
} else if (run.kind === "image") {
|
|
2243
|
+
segment.dimensions = imageDimensions(run.node, width, ctx.containing?.height);
|
|
2244
|
+
segment.insets = inlineImageVerticalInsets(run.node);
|
|
2245
|
+
const outerHeight = segment.dimensions.height + segment.insets.top + segment.insets.bottom;
|
|
2246
|
+
segment.imageTop = inlineImageTop(run.node.style, outerHeight, strut);
|
|
2180
2247
|
ascent = Math.max(ascent, -segment.imageTop);
|
|
2181
|
-
descent = Math.max(descent, segment.imageTop +
|
|
2248
|
+
descent = Math.max(descent, segment.imageTop + outerHeight);
|
|
2182
2249
|
} else if (run.kind === "atomic") {
|
|
2183
2250
|
const atom = atomFor(run);
|
|
2184
2251
|
const atomAscent = atom.marginTop + (atom.baseline ?? atom.height) ;
|
|
@@ -2192,7 +2259,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2192
2259
|
}
|
|
2193
2260
|
}
|
|
2194
2261
|
const baselineY = y + ascent;
|
|
2195
|
-
const lineHeight = ascent + descent;
|
|
2262
|
+
const lineHeight = Math.max(ascent + descent, floatHeight);
|
|
2196
2263
|
|
|
2197
2264
|
// Horizontal alignment.
|
|
2198
2265
|
const alignment = style?.textAlign;
|
|
@@ -2205,6 +2272,22 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2205
2272
|
const edges = new Map();
|
|
2206
2273
|
const runs = [];
|
|
2207
2274
|
const atomsToPlace = [];
|
|
2275
|
+
// Relatively positioned inline boxes move their contents (CSS 2 §9.4.3);
|
|
2276
|
+
// an offset adds to those of the inline boxes around it.
|
|
2277
|
+
const containing = ctx.containing;
|
|
2278
|
+
const offsets = new Map();
|
|
2279
|
+
const offsetOf = (node, style, outer) => {
|
|
2280
|
+
if (!offsets.has(node)) {
|
|
2281
|
+
const own = relativeOffset(style, containing);
|
|
2282
|
+
offsets.set(node, own ? { x: outer.x + own.x, y: outer.y + own.y } : outer);
|
|
2283
|
+
}
|
|
2284
|
+
return offsets.get(node);
|
|
2285
|
+
};
|
|
2286
|
+
const runOffset = (run) => {
|
|
2287
|
+
let offset = ZERO_OFFSET;
|
|
2288
|
+
for (const node of run.inlines) offset = offsetOf(node, paragraph.inlineState.get(node).style, offset);
|
|
2289
|
+
return run.kind === "image" ? offsetOf(run.node, run.node.style, offset) : offset;
|
|
2290
|
+
};
|
|
2208
2291
|
// Right floats (approximated as atomic inlines) go to the end of the line.
|
|
2209
2292
|
const rightFloats = segments.filter((segment) => segment.run.kind === "atomic" && segment.run.style?.float === "right");
|
|
2210
2293
|
const rightFloatWidth = rightFloats.reduce((sum, segment) => sum + segment.width, 0);
|
|
@@ -2223,6 +2306,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2223
2306
|
}
|
|
2224
2307
|
}
|
|
2225
2308
|
const segmentWidth = segment.width;
|
|
2309
|
+
const shift = runOffset(run);
|
|
2226
2310
|
const common = {
|
|
2227
2311
|
nodeId: run.nodeId,
|
|
2228
2312
|
text: run.kind === "text" ? paragraph.text.slice(segment.start, segment.end) : "",
|
|
@@ -2231,11 +2315,27 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2231
2315
|
style: run.style ?? style,
|
|
2232
2316
|
};
|
|
2233
2317
|
if (run.kind === "image") {
|
|
2318
|
+
const imageX = cursor + inlineImageMargins(run.node).left + shift.x;
|
|
2319
|
+
const imageY = baselineY + segment.imageTop + segment.insets.top + shift.y;
|
|
2320
|
+
// Its background and borders, around the content box.
|
|
2321
|
+
if (isDecorated(run.node.style)) {
|
|
2322
|
+
const border = borderWidths(run.node.style);
|
|
2323
|
+
const padding = run.node.style.padding ?? ZERO_BOX;
|
|
2324
|
+
ctx.boxes.push({
|
|
2325
|
+
nodeId: run.nodeId,
|
|
2326
|
+
x: imageX - padding[3] - border[3],
|
|
2327
|
+
y: imageY - padding[0] - border[0],
|
|
2328
|
+
width: segment.dimensions.width + padding[1] + padding[3] + border[1] + border[3],
|
|
2329
|
+
height: segment.dimensions.height + padding[0] + padding[2] + border[0] + border[2],
|
|
2330
|
+
style: run.node.style,
|
|
2331
|
+
inline: true,
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2234
2334
|
runs.push({
|
|
2235
2335
|
...common,
|
|
2236
2336
|
type: "image",
|
|
2237
|
-
x:
|
|
2238
|
-
y:
|
|
2337
|
+
x: imageX,
|
|
2338
|
+
y: imageY,
|
|
2239
2339
|
width: segment.dimensions.width,
|
|
2240
2340
|
height: segment.dimensions.height,
|
|
2241
2341
|
resource: run.node.resource,
|
|
@@ -2245,14 +2345,14 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2245
2345
|
});
|
|
2246
2346
|
} else if (run.kind === "atomic") {
|
|
2247
2347
|
const atom = atomFor(run);
|
|
2248
|
-
const atomY = baselineY - segment.atomAscent + atom.marginTop;
|
|
2249
|
-
runs.push({ ...common, type: "atomic", x: cursor, y: atomY, width: segmentWidth, height: atom.height });
|
|
2250
|
-
atomsToPlace.push({ box: run.box, x: cursor + atom.marginLeft, y: atomY, width: atom.width });
|
|
2348
|
+
const atomY = isFloat(run.style) ? y + atom.marginTop : baselineY - segment.atomAscent + atom.marginTop;
|
|
2349
|
+
runs.push({ ...common, type: "atomic", x: cursor + shift.x, y: atomY + shift.y, width: segmentWidth, height: atom.height });
|
|
2350
|
+
atomsToPlace.push({ box: run.box, x: cursor + atom.marginLeft + shift.x, y: atomY + shift.y, width: atom.width });
|
|
2251
2351
|
} else {
|
|
2252
2352
|
runs.push({
|
|
2253
2353
|
...common,
|
|
2254
|
-
x: cursor,
|
|
2255
|
-
y: baselineY - segment.metrics.contentAscent,
|
|
2354
|
+
x: cursor + shift.x,
|
|
2355
|
+
y: baselineY - segment.metrics.contentAscent + shift.y,
|
|
2256
2356
|
width: Math.max(1, segmentWidth),
|
|
2257
2357
|
height: null,
|
|
2258
2358
|
});
|
|
@@ -2301,10 +2401,11 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2301
2401
|
const metrics = textMetrics(ctx, nodeStyle);
|
|
2302
2402
|
const left = edge.left ?? edge.first;
|
|
2303
2403
|
const right = edge.right ?? edge.last;
|
|
2404
|
+
const shift = offsets.get(node) ?? ZERO_OFFSET;
|
|
2304
2405
|
ctx.boxes.push({
|
|
2305
2406
|
nodeId: node.id,
|
|
2306
|
-
x: left,
|
|
2307
|
-
y: baselineY - metrics.contentAscent - padding[0] - border[0],
|
|
2407
|
+
x: left + shift.x,
|
|
2408
|
+
y: baselineY - metrics.contentAscent - padding[0] - border[0] + shift.y,
|
|
2308
2409
|
width: right - left,
|
|
2309
2410
|
height: metrics.contentHeight + padding[0] + padding[2] + border[0] + border[2],
|
|
2310
2411
|
style: nodeStyle,
|
|
@@ -2327,6 +2428,10 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2327
2428
|
return { height: lineHeight, baseline: baselineY };
|
|
2328
2429
|
}
|
|
2329
2430
|
|
|
2431
|
+
function isFloat(style) {
|
|
2432
|
+
return style?.float === "left" || style?.float === "right";
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2330
2435
|
function measureAtomic(ctx, box, availableWidth) {
|
|
2331
2436
|
const placed = resolveHorizontal(ctx, box, 0, availableWidth, true);
|
|
2332
2437
|
const result = dryLayout(ctx, box, placed.width);
|
|
@@ -2353,7 +2458,7 @@ function textMetrics(ctx, style) {
|
|
|
2353
2458
|
|
|
2354
2459
|
function isDecorated(style) {
|
|
2355
2460
|
return Boolean(style && ((style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)")
|
|
2356
|
-
|| borderWidths(style).some((width) => width > 0)));
|
|
2461
|
+
|| style.backgroundImage || borderWidths(style).some((width) => width > 0)));
|
|
2357
2462
|
}
|
|
2358
2463
|
|
|
2359
2464
|
function borderWidths(style) {
|
|
@@ -2420,7 +2525,49 @@ function cachedMetrics(fontMetrics) {
|
|
|
2420
2525
|
};
|
|
2421
2526
|
}
|
|
2422
2527
|
|
|
2423
|
-
|
|
2528
|
+
/**
|
|
2529
|
+
* An iframe's content box: like other replaced elements, but with no
|
|
2530
|
+
* intrinsic size or ratio of its own (its document's frame is drawn into
|
|
2531
|
+
* whatever box it gets), so each dimension falls back to 300x150 alone.
|
|
2532
|
+
*/
|
|
2533
|
+
function frameDimensions(node, referenceWidth, referenceHeight) {
|
|
2534
|
+
const resolve = (cssValue, attribute, fallback, reference) => {
|
|
2535
|
+
const value = cssValue !== "auto" && cssValue != null ? resolveLength(cssValue, reference ?? NaN) : attribute;
|
|
2536
|
+
return Number.isFinite(value) ? value : fallback;
|
|
2537
|
+
};
|
|
2538
|
+
let width = resolve(node.style?.width, node.widthAttribute, 300, referenceWidth);
|
|
2539
|
+
const height = resolve(node.style?.height, node.heightAttribute, 150, referenceHeight);
|
|
2540
|
+
const maximum = node.style?.maxWidth !== "none" && node.style?.maxWidth != null ? resolveLength(node.style.maxWidth, referenceWidth) : NaN;
|
|
2541
|
+
if (Number.isFinite(maximum) && width > maximum) width = maximum;
|
|
2542
|
+
return { width: Math.max(0, width), height: Math.max(0, height) };
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
/**
|
|
2546
|
+
* An inline <svg>'s size (SVG 2 §8.2): its CSS size, else its width and
|
|
2547
|
+
* height attributes, else 100% of its container's width; a missing height
|
|
2548
|
+
* follows the viewBox's ratio, or is 150px without one. Where the container
|
|
2549
|
+
* is still being measured (an indefinite width) it counts as 300px wide.
|
|
2550
|
+
*/
|
|
2551
|
+
function svgDimensions(node, referenceWidth, referenceHeight) {
|
|
2552
|
+
const css = (value, reference) => value !== "auto" && value != null ? numberOrNull(resolveLength(value, reference ?? NaN)) : null;
|
|
2553
|
+
const ratio = node.svgRatio ?? null;
|
|
2554
|
+
let width = css(node.style?.width, referenceWidth) ?? node.widthAttribute;
|
|
2555
|
+
let height = css(node.style?.height, referenceHeight) ?? node.heightAttribute;
|
|
2556
|
+
if (width == null && height != null && ratio) width = height * ratio;
|
|
2557
|
+
if (width == null) width = Number.isFinite(referenceWidth) ? referenceWidth : 300;
|
|
2558
|
+
if (height == null) height = ratio ? width / ratio : 150;
|
|
2559
|
+
const maximum = node.style?.maxWidth !== "none" && node.style?.maxWidth != null ? numberOrNull(resolveLength(node.style.maxWidth, referenceWidth)) : null;
|
|
2560
|
+
if (maximum != null && width > maximum) {
|
|
2561
|
+
height = ratio ? maximum / ratio : height;
|
|
2562
|
+
width = maximum;
|
|
2563
|
+
}
|
|
2564
|
+
return { width: Math.max(0, width), height: Math.max(0, height) };
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/** Percentages in width are of `referenceWidth`, in height of `referenceHeight` (auto if indefinite). */
|
|
2568
|
+
function imageDimensions(node, referenceWidth, referenceHeight = null) {
|
|
2569
|
+
if (node.tagName === "IFRAME") return frameDimensions(node, referenceWidth, referenceHeight);
|
|
2570
|
+
if (node.tagName === "SVG") return svgDimensions(node, referenceWidth, referenceHeight);
|
|
2424
2571
|
const intrinsicWidth = node.resource?.width ?? node.widthAttribute ?? 300;
|
|
2425
2572
|
const intrinsicHeight = node.resource?.height ?? node.heightAttribute ?? 150;
|
|
2426
2573
|
const ratio = intrinsicWidth / intrinsicHeight;
|
|
@@ -2430,7 +2577,7 @@ function imageDimensions(node, referenceWidth) {
|
|
|
2430
2577
|
? resolveLength(cssWidth, referenceWidth)
|
|
2431
2578
|
: node.widthAttribute;
|
|
2432
2579
|
let height = cssHeight !== "auto" && cssHeight != null
|
|
2433
|
-
? resolveLength(cssHeight,
|
|
2580
|
+
? resolveLength(cssHeight, referenceHeight ?? NaN)
|
|
2434
2581
|
: node.heightAttribute;
|
|
2435
2582
|
if (!Number.isFinite(width)) width = null;
|
|
2436
2583
|
if (!Number.isFinite(height)) height = null;
|
package/lib/log.js
CHANGED
|
@@ -34,9 +34,11 @@ export function installLog(path) {
|
|
|
34
34
|
logPath = path;
|
|
35
35
|
for (const level of ["log", "info", "warn", "error", "debug", "trace"]) {
|
|
36
36
|
const original = console[level].bind(console);
|
|
37
|
+
// The file first: whoever sees a line on the console (and then stops
|
|
38
|
+
// this process) finds it in the log too.
|
|
37
39
|
console[level] = (...args) => {
|
|
38
|
-
original(...args);
|
|
39
40
|
writeLog(level, formatWithOptions({ colors: false }, ...args));
|
|
41
|
+
original(...args);
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
writeLog("log", `started pid ${process.pid}, bun ${Bun.version}, cwd ${process.cwd()}, args ${JSON.stringify(Bun.argv.slice(2))}`);
|
|
@@ -69,13 +71,14 @@ export async function pipeToLog(stream, source, echo) {
|
|
|
69
71
|
let pending = "";
|
|
70
72
|
try {
|
|
71
73
|
for await (const chunk of stream) {
|
|
72
|
-
echo(chunk);
|
|
73
74
|
pending += decoder.decode(chunk, { stream: true });
|
|
74
75
|
const end = pending.lastIndexOf("\n");
|
|
75
76
|
if (end >= 0) {
|
|
76
77
|
writeLog(source, pending.slice(0, end));
|
|
77
78
|
pending = pending.slice(end + 1);
|
|
78
79
|
}
|
|
80
|
+
// Echoed after the complete lines are in the log (see installLog).
|
|
81
|
+
echo(chunk);
|
|
79
82
|
}
|
|
80
83
|
} catch {
|
|
81
84
|
// The child went away mid-read.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A growing media buffer shared by a progressive download and by MSE appends.
|
|
3
|
+
*
|
|
4
|
+
* wasmpeg can only open a decoder on a complete snapshot, so playback keeps
|
|
5
|
+
* this concatenation, reopens when the decoder runs off the end of what it
|
|
6
|
+
* was opened on, and seeks back. `generation` changes on every append and
|
|
7
|
+
* on finish/fail, which is how a decoder knows its snapshot is stale.
|
|
8
|
+
* `waitFor` resolves when `size` reaches the mark or the pipe is finished.
|
|
9
|
+
*/
|
|
10
|
+
export function createBytePipe() {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let size = 0;
|
|
13
|
+
let generation = 0;
|
|
14
|
+
let done = false;
|
|
15
|
+
let error = null;
|
|
16
|
+
/** @type {Array<() => void>} */
|
|
17
|
+
const waiters = [];
|
|
18
|
+
|
|
19
|
+
const wake = () => {
|
|
20
|
+
for (const waiter of waiters.splice(0)) waiter();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
append(bytes) {
|
|
25
|
+
if (done || bytes == null) return;
|
|
26
|
+
const chunk = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
27
|
+
if (!chunk.byteLength) return;
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
size += chunk.byteLength;
|
|
30
|
+
generation += 1;
|
|
31
|
+
wake();
|
|
32
|
+
},
|
|
33
|
+
finish() {
|
|
34
|
+
if (done) return;
|
|
35
|
+
done = true;
|
|
36
|
+
generation += 1;
|
|
37
|
+
wake();
|
|
38
|
+
},
|
|
39
|
+
fail(reason) {
|
|
40
|
+
if (done) return;
|
|
41
|
+
error = reason instanceof Error ? reason : new Error(String(reason ?? "media fetch failed"));
|
|
42
|
+
done = true;
|
|
43
|
+
generation += 1;
|
|
44
|
+
wake();
|
|
45
|
+
},
|
|
46
|
+
/** One contiguous snapshot. Collapses chunks so the next call is cheap. */
|
|
47
|
+
bytes() {
|
|
48
|
+
if (chunks.length === 1) return chunks[0];
|
|
49
|
+
const out = new Uint8Array(size);
|
|
50
|
+
let offset = 0;
|
|
51
|
+
for (const chunk of chunks) {
|
|
52
|
+
out.set(chunk, offset);
|
|
53
|
+
offset += chunk.byteLength;
|
|
54
|
+
}
|
|
55
|
+
chunks.length = 0;
|
|
56
|
+
if (out.byteLength) chunks.push(out);
|
|
57
|
+
return out;
|
|
58
|
+
},
|
|
59
|
+
get size() {
|
|
60
|
+
return size;
|
|
61
|
+
},
|
|
62
|
+
get generation() {
|
|
63
|
+
return generation;
|
|
64
|
+
},
|
|
65
|
+
get done() {
|
|
66
|
+
return done;
|
|
67
|
+
},
|
|
68
|
+
get error() {
|
|
69
|
+
return error;
|
|
70
|
+
},
|
|
71
|
+
waitFor(mark) {
|
|
72
|
+
if (size >= mark || done) return Promise.resolve();
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const check = () => {
|
|
75
|
+
if (size >= mark || done) resolve();
|
|
76
|
+
else waiters.push(check);
|
|
77
|
+
};
|
|
78
|
+
waiters.push(check);
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Advance a PCM decoder to an exact sample cursor, never playing discarded PCM.
|
|
2
|
+
* Reopened snapshots start at sample zero, including their pending first chunk.
|
|
3
|
+
* The caller holds output requests until this transaction has completed.
|
|
4
|
+
*/
|
|
5
|
+
export async function advanceAudio(getSound, seconds, { reopen, needsMore, active = () => true }) {
|
|
6
|
+
while (active()) {
|
|
7
|
+
const sound = getSound();
|
|
8
|
+
const { sampleRate, channels } = sound.decoder;
|
|
9
|
+
const target = Math.max(0, Math.round(seconds * sampleRate));
|
|
10
|
+
if (sound.position === target) return true;
|
|
11
|
+
if (sound.position > target) {
|
|
12
|
+
if (!await reopen()) return false;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
// This is also the first chunk of EVERY replacement decoder. Skipping it
|
|
16
|
+
// used to leave the opening sound queued at exact chunk boundaries.
|
|
17
|
+
let samples = sound.pending;
|
|
18
|
+
sound.pending = null;
|
|
19
|
+
if (!samples) {
|
|
20
|
+
try { samples = sound.decoder.nextSamples(); }
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (!needsMore(sound)) throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (samples?.length) {
|
|
26
|
+
const take = Math.min(target - sound.position, samples.length / channels);
|
|
27
|
+
sound.position += take;
|
|
28
|
+
if (take * channels < samples.length) sound.pending = samples.subarray(take * channels);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!needsMore(sound)) return false;
|
|
32
|
+
const pipe = sound.pipe;
|
|
33
|
+
if (pipe.error) throw pipe.error;
|
|
34
|
+
// Data may already have arrived while this snapshot was being decoded.
|
|
35
|
+
if (pipe.generation === sound.openedGeneration) await pipe.waitFor(pipe.size + 1);
|
|
36
|
+
if (!active()) return false;
|
|
37
|
+
if (pipe.error) throw pipe.error;
|
|
38
|
+
if (pipe.generation === sound.openedGeneration) return false;
|
|
39
|
+
if (!await reopen()) return false;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|