buninu-browser 0.2.1 → 0.2.3
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/ai-debug.log +5537 -0
- 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/playback-buffer.js +27 -0
- package/lib/audio/pulse-client.js +137 -19
- package/lib/cdp-server.js +22 -11
- package/lib/happy-dom/parser.js +29 -4
- package/lib/happy-dom/selector-pseudo-adapter.js +14 -0
- package/lib/headless-shell.js +69 -19
- package/lib/input/hit-test.js +25 -7
- package/lib/layout/text-layout.js +268 -85
- package/lib/log.js +5 -2
- package/lib/media/byte-pipe.js +82 -0
- package/lib/media/position-audio.js +42 -0
- package/lib/media/video-keyframe.js +29 -0
- package/lib/network/network-service.js +161 -14
- package/lib/paint/display-list.js +316 -15
- package/lib/paint/gradient.js +11 -5
- package/lib/render-tree/index.js +19 -2
- package/lib/render-tree/svg.js +2 -1
- package/lib/renderer/bindings.js +995 -69
- package/lib/renderer/canvas-2d.js +92 -0
- package/lib/renderer/compositor.js +16 -1
- package/lib/renderer/custom-element-reactions.js +18 -0
- package/lib/renderer/history.js +42 -0
- package/lib/renderer/host.js +166 -9
- package/lib/renderer/image-loading.js +27 -0
- package/lib/renderer/interfaces.js +215 -0
- package/lib/renderer/keyboard-input.js +12 -0
- package/lib/renderer/main.js +162 -12
- package/lib/renderer/media-playback-events.js +13 -0
- 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 +1408 -242
- package/lib/renderer/page-renderer.js +1893 -223
- package/lib/renderer/piano-input.js +22 -0
- package/lib/renderer/pointer-move.js +29 -0
- 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/video-presentation.js +126 -0
- package/lib/renderer/workers.js +327 -0
- package/lib/screenshot-cli.js +2 -0
- package/lib/style/computed-style.js +186 -29
- 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 +41 -1
- package/test/computed-style.test.js +59 -1
- package/test/display-list.test.js +88 -1
- package/test/download.test.js +43 -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 +23 -1
- package/test/hit-test.test.js +25 -1
- package/test/media-playback-events.test.js +22 -0
- 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 +1203 -7
- package/test/piano-input.test.js +44 -0
- package/test/playback-buffer.test.js +31 -0
- package/test/position-audio.test.js +67 -0
- package/test/positioning.test.js +95 -1
- package/test/pulse-client.test.js +84 -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/text-layout.test.js +62 -0
- package/test/user-agent.test.js +60 -4
- package/test/video-presentation.test.js +67 -0
- 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,10 +18,19 @@
|
|
|
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;
|
|
24
25
|
|
|
26
|
+
// CSS 2 §8.4: all four percentage paddings use the containing block's width.
|
|
27
|
+
// Cyclic percentages contribute zero during intrinsic sizing.
|
|
28
|
+
function paddingBox(style, width = 0) {
|
|
29
|
+
const padding = style?.padding ?? ZERO_BOX;
|
|
30
|
+
return padding.every(Number.isFinite) ? padding
|
|
31
|
+
: padding.map(value => Math.max(0, numberOrZero(resolveLength(value, width))));
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
const ROOT_LAYER = Object.freeze({ z: 0, order: 0, context: true, parent: null });
|
|
26
35
|
|
|
27
36
|
export function layoutText(tree, options = {}) {
|
|
@@ -51,6 +60,9 @@ export function layoutText(tree, options = {}) {
|
|
|
51
60
|
stackingContext: ROOT_LAYER,
|
|
52
61
|
layerCount: 1,
|
|
53
62
|
initialContainingBlock: null,
|
|
63
|
+
// The content box of the block being laid out: the containing block of
|
|
64
|
+
// its in-flow children, for percentages in their relative offsets.
|
|
65
|
+
containing: null,
|
|
54
66
|
// Static positions recorded for out-of-flow boxes, in order (see
|
|
55
67
|
// cacheBlockResult), and those boxes by node for replays.
|
|
56
68
|
staticPositions: [],
|
|
@@ -79,6 +91,7 @@ export function layoutText(tree, options = {}) {
|
|
|
79
91
|
// establishes the BFC, so body margins collapse with its children's.
|
|
80
92
|
const horizontal = resolveHorizontal(ctx, root, x, containingWidth, false);
|
|
81
93
|
const rootY = y + strutValue(topStrut(root));
|
|
94
|
+
ctx.containing = { width: containingWidth, height: ctx.initialContainingBlock.height || null };
|
|
82
95
|
const result = layoutBlock(ctx, root, horizontal.x, horizontal.width, rootY, collapsesThroughTop(root));
|
|
83
96
|
const height = rootY + result.height + strutValue(bottomStrut(root));
|
|
84
97
|
|
|
@@ -317,9 +330,12 @@ function hasInlineContent(items) {
|
|
|
317
330
|
* then applies relative offsets and translations to everything it produced.
|
|
318
331
|
*/
|
|
319
332
|
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
333
|
const style = box.style;
|
|
334
|
+
// Its containing block, for percentages in relative offsets.
|
|
335
|
+
const containing = ctx.containing;
|
|
336
|
+
const key = blockCacheKey(width, absorbTop, forcedHeight, containing);
|
|
337
|
+
const cached = cachedBlockResult(ctx, box, key, y, x);
|
|
338
|
+
if (cached) return cached;
|
|
323
339
|
const boxStart = ctx.boxes.length;
|
|
324
340
|
const fragmentStart = ctx.fragments.length;
|
|
325
341
|
const layerStart = ctx.layerCount;
|
|
@@ -343,10 +359,10 @@ function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = nu
|
|
|
343
359
|
if (box.absolutes?.length) layoutAbsolutes(ctx, box, record);
|
|
344
360
|
let dx = 0;
|
|
345
361
|
let dy = 0;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
dx =
|
|
349
|
-
dy =
|
|
362
|
+
const offset = relativeOffset(style, containing ?? { width, height: null });
|
|
363
|
+
if (offset) {
|
|
364
|
+
dx = offset.x;
|
|
365
|
+
dy = offset.y;
|
|
350
366
|
}
|
|
351
367
|
if (style?.transform) {
|
|
352
368
|
dx += numberOrZero(resolveLength(style.transform.x, record.width));
|
|
@@ -357,14 +373,29 @@ function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = nu
|
|
|
357
373
|
if (dx || dy) shiftOutput(ctx, boxStart, fragmentStart, dx, dy);
|
|
358
374
|
ctx.layer = previousLayer;
|
|
359
375
|
ctx.stackingContext = previousContext;
|
|
360
|
-
cacheBlockResult(ctx, box,
|
|
376
|
+
cacheBlockResult(ctx, box, key, x, y, result, {
|
|
361
377
|
boxStart, fragmentStart, layerStart, staticStart, layer: previousLayer, context: previousContext,
|
|
362
378
|
});
|
|
363
379
|
return result;
|
|
364
380
|
}
|
|
365
381
|
|
|
366
|
-
function blockCacheKey(width, absorbTop, forcedHeight) {
|
|
367
|
-
|
|
382
|
+
function blockCacheKey(width, absorbTop, forcedHeight, containing) {
|
|
383
|
+
const key = `${width}\n${absorbTop ? 1 : 0}\n${forcedHeight ?? "auto"}`;
|
|
384
|
+
return containing ? `${key}\n${containing.width}\n${containing.height ?? "auto"}` : key;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The offset of a relatively positioned box (CSS 2 §9.4.3). Percentages are
|
|
389
|
+
* of its containing block; one of an indefinite height is auto.
|
|
390
|
+
*/
|
|
391
|
+
function relativeOffset(style, containing) {
|
|
392
|
+
if (style?.position !== "relative" || !style.inset) return null;
|
|
393
|
+
const [top, right, bottom, left] = style.inset.map((value, index) => value === "auto"
|
|
394
|
+
? null
|
|
395
|
+
: numberOrNull(resolveLength(value, (index % 2 ? containing?.width : containing?.height) ?? NaN)));
|
|
396
|
+
const x = left ?? (right != null ? -right : 0);
|
|
397
|
+
const y = top ?? (bottom != null ? -bottom : 0);
|
|
398
|
+
return x || y ? { x, y } : null;
|
|
368
399
|
}
|
|
369
400
|
|
|
370
401
|
/** Where an out-of-flow box would have been in flow; recorded so that a replayed block sets it again. */
|
|
@@ -374,9 +405,9 @@ function setStaticPosition(ctx, box, position) {
|
|
|
374
405
|
}
|
|
375
406
|
|
|
376
407
|
/** Replays a retained formatting-context result at its new flow position. */
|
|
377
|
-
function cachedBlockResult(ctx, box,
|
|
408
|
+
function cachedBlockResult(ctx, box, key, y, x) {
|
|
378
409
|
if (!ctx.blockResults || box.anonymous) return null;
|
|
379
|
-
const cached = ctx.blockResults.get(box.node)?.get(
|
|
410
|
+
const cached = ctx.blockResults.get(box.node)?.get(key);
|
|
380
411
|
if (!cached) return null;
|
|
381
412
|
const dx = x - cached.x;
|
|
382
413
|
const dy = y - cached.y;
|
|
@@ -423,7 +454,7 @@ function cachedBlockResult(ctx, box, width, y, x, absorbTop, forcedHeight) {
|
|
|
423
454
|
* static positions it recorded go along. Output in a layer made before the
|
|
424
455
|
* block (which should not occur) is not retained.
|
|
425
456
|
*/
|
|
426
|
-
function cacheBlockResult(ctx, box,
|
|
457
|
+
function cacheBlockResult(ctx, box, key, x, y, result, start) {
|
|
427
458
|
if (!ctx.blockResults || box.anonymous) return;
|
|
428
459
|
const boxes = ctx.boxes.slice(start.boxStart);
|
|
429
460
|
const fragments = ctx.fragments.slice(start.fragmentStart);
|
|
@@ -451,10 +482,10 @@ function cacheBlockResult(ctx, box, width, x, y, absorbTop, forcedHeight, result
|
|
|
451
482
|
if (!entries) ctx.blockResults.set(box.node, entries = new Map());
|
|
452
483
|
// A node normally sees one containing width. Bound unusual cases such as a
|
|
453
484
|
// reused table cell so the retained cache cannot grow without limit.
|
|
454
|
-
if (!entries.has(
|
|
485
|
+
if (!entries.has(key) && entries.size >= 4) {
|
|
455
486
|
entries.delete(entries.keys().next().value);
|
|
456
487
|
}
|
|
457
|
-
entries.set(
|
|
488
|
+
entries.set(key, {
|
|
458
489
|
x,
|
|
459
490
|
y,
|
|
460
491
|
result: { ...result },
|
|
@@ -488,31 +519,37 @@ function layoutAbsolutes(ctx, box, record) {
|
|
|
488
519
|
for (const absolute of box.absolutes) {
|
|
489
520
|
const style = absolute.style;
|
|
490
521
|
const containing = style?.position === "fixed" ? { ...icb, height: icb.height || block.height } : block;
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
522
|
+
const previousContaining = ctx.containing;
|
|
523
|
+
ctx.containing = containing;
|
|
524
|
+
try {
|
|
525
|
+
const [top, right, bottom, left] = (style?.inset ?? ["auto", "auto", "auto", "auto"])
|
|
526
|
+
.map((value, index) => value === "auto" ? null : resolveLength(value, index % 2 ? containing.width : containing.height));
|
|
527
|
+
const margin = style?.margin ?? ZERO_BOX;
|
|
528
|
+
const [marginTop, marginRight, marginBottom, marginLeft] = margin.map(numberOrZero);
|
|
529
|
+
const specifiedWidth = numberOrNull(resolveLength(style?.width, containing.width));
|
|
530
|
+
let width;
|
|
531
|
+
if (specifiedWidth == null && left != null && right != null) {
|
|
532
|
+
width = Math.max(0, containing.width - left - right - marginLeft - marginRight);
|
|
533
|
+
} else {
|
|
534
|
+
width = resolveHorizontal(ctx, absolute, 0, containing.width - (left ?? 0) - (right ?? 0), true, containing.width).width;
|
|
535
|
+
}
|
|
536
|
+
const staticPosition = absolute.staticPosition ?? containing;
|
|
537
|
+
const x = left != null ? containing.x + left + marginLeft
|
|
538
|
+
: right != null ? containing.x + containing.width - right - marginRight - width
|
|
539
|
+
: staticPosition.x + marginLeft;
|
|
540
|
+
const specifiedHeight = numberOrNull(resolveLength(style?.height, containing.height));
|
|
541
|
+
const forcedHeight = specifiedHeight == null && top != null && bottom != null
|
|
542
|
+
? Math.max(0, containing.height - top - bottom - marginTop - marginBottom)
|
|
543
|
+
: null;
|
|
544
|
+
let height = forcedHeight;
|
|
545
|
+
if (height == null && top == null && bottom != null) height = dryLayout(ctx, absolute, width).height;
|
|
546
|
+
const y = top != null ? containing.y + top + marginTop
|
|
547
|
+
: bottom != null ? containing.y + containing.height - bottom - marginBottom - height
|
|
548
|
+
: staticPosition.y + marginTop;
|
|
549
|
+
layoutBlock(ctx, absolute, x, width, y, false, forcedHeight);
|
|
550
|
+
} finally {
|
|
551
|
+
ctx.containing = previousContaining;
|
|
501
552
|
}
|
|
502
|
-
const staticPosition = absolute.staticPosition ?? containing;
|
|
503
|
-
const x = left != null ? containing.x + left + marginLeft
|
|
504
|
-
: right != null ? containing.x + containing.width - right - marginRight - width
|
|
505
|
-
: staticPosition.x + marginLeft;
|
|
506
|
-
const specifiedHeight = numberOrNull(resolveLength(style?.height, containing.height));
|
|
507
|
-
const forcedHeight = specifiedHeight == null && top != null && bottom != null
|
|
508
|
-
? Math.max(0, containing.height - top - bottom - marginTop - marginBottom)
|
|
509
|
-
: null;
|
|
510
|
-
let height = forcedHeight;
|
|
511
|
-
if (height == null && top == null && bottom != null) height = dryLayout(ctx, absolute, width).height;
|
|
512
|
-
const y = top != null ? containing.y + top + marginTop
|
|
513
|
-
: bottom != null ? containing.y + containing.height - bottom - marginBottom - height
|
|
514
|
-
: staticPosition.y + marginTop;
|
|
515
|
-
layoutBlock(ctx, absolute, x, width, y, false, forcedHeight);
|
|
516
553
|
}
|
|
517
554
|
}
|
|
518
555
|
|
|
@@ -570,13 +607,13 @@ function shiftedFragment(fragment, dx, dy) {
|
|
|
570
607
|
}
|
|
571
608
|
|
|
572
609
|
function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeight = null) {
|
|
573
|
-
const
|
|
610
|
+
const padding = paddingBox(box.style, ctx.containing?.width ?? width);
|
|
611
|
+
const style = box.style && padding !== box.style.padding ? { ...box.style, padding } : box.style;
|
|
574
612
|
const border = borderWidths(style);
|
|
575
|
-
const padding = style?.padding ?? ZERO_BOX;
|
|
576
613
|
const record = { nodeId: box.node.id, x, y, width, height: 0, style };
|
|
577
614
|
ctx.boxes.push(record);
|
|
578
615
|
if (box.replaced) {
|
|
579
|
-
const dimensions = imageDimensions(box.node, width);
|
|
616
|
+
const dimensions = imageDimensions(box.node, width, ctx.containing?.height);
|
|
580
617
|
const imageX = x + border[3] + padding[3];
|
|
581
618
|
const imageY = y + border[0] + padding[0];
|
|
582
619
|
ctx.fragments.push(lineFragment(box.style, "", [box.node.id], [{
|
|
@@ -608,6 +645,21 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
608
645
|
let columnItems = null;
|
|
609
646
|
let gridResult = null;
|
|
610
647
|
let baseline = null;
|
|
648
|
+
const toContentHeight = (value) => style.boxSizing === "border-box"
|
|
649
|
+
? Math.max(0, value - border[0] - border[2] - padding[0] - padding[2])
|
|
650
|
+
: value;
|
|
651
|
+
// A percentage height (and min/max) resolves against the containing block
|
|
652
|
+
// only when that height is definite (CSS 2 §10.5); otherwise it is auto.
|
|
653
|
+
const containingHeight = ctx.containing?.height;
|
|
654
|
+
const resolveHeight = (value) => numberOrNull(resolveLength(value, containingHeight ?? NaN));
|
|
655
|
+
const specifiedHeight = resolveHeight(style?.height);
|
|
656
|
+
const previousContaining = ctx.containing;
|
|
657
|
+
ctx.containing = {
|
|
658
|
+
width: contentWidth,
|
|
659
|
+
height: forcedHeight != null
|
|
660
|
+
? Math.max(0, forcedHeight - border[0] - border[2] - padding[0] - padding[2])
|
|
661
|
+
: specifiedHeight != null ? toContentHeight(specifiedHeight) : null,
|
|
662
|
+
};
|
|
611
663
|
|
|
612
664
|
if (box.table) {
|
|
613
665
|
const result = layoutTable(ctx, box, contentX, contentWidth, contentTop);
|
|
@@ -639,7 +691,9 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
639
691
|
: child.style.alignSelf;
|
|
640
692
|
const shrink = !["normal", "stretch"].includes(alignment);
|
|
641
693
|
const placed = resolveHorizontal(ctx, child, contentX, contentWidth, shrink);
|
|
642
|
-
|
|
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";
|
|
643
697
|
let childX = placed.x;
|
|
644
698
|
if (shrink && !autoMargins) {
|
|
645
699
|
const free = contentWidth - placed.width - placed.marginLeft - placed.marginRight;
|
|
@@ -678,23 +732,24 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
678
732
|
// Tables with width: auto are sized to their contents (CSS 2 §17.5.2).
|
|
679
733
|
const placed = resolveHorizontal(ctx, child, contentX, contentWidth, Boolean(child.table));
|
|
680
734
|
const result = layoutBlock(ctx, child, placed.x, placed.width, childY, collapsesThroughTop(child));
|
|
681
|
-
|
|
682
|
-
|
|
735
|
+
if (result.height === 0 && selfCollapsing(child)) {
|
|
736
|
+
// Empty blocks do not separate adjoining margins (CSS 2 §8.3.1).
|
|
737
|
+
pending = combineStruts(combined, bottomStrut(child));
|
|
738
|
+
} else {
|
|
739
|
+
cursor = childY + result.height;
|
|
740
|
+
pending = bottomStrut(child);
|
|
741
|
+
}
|
|
683
742
|
baseline = result.baseline ?? baseline;
|
|
684
743
|
});
|
|
685
744
|
contentBottom = collapsesThroughBottom(box) ? cursor : cursor + strutValue(pending ?? ZERO_STRUT);
|
|
686
745
|
}
|
|
687
746
|
|
|
747
|
+
ctx.containing = previousContaining;
|
|
688
748
|
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
749
|
if (specifiedHeight != null) contentHeight = toContentHeight(specifiedHeight);
|
|
694
|
-
|
|
695
|
-
const maxHeight = numberOrNull(style?.maxHeight);
|
|
750
|
+
const maxHeight = style?.maxHeight === "none" ? null : resolveHeight(style?.maxHeight);
|
|
696
751
|
if (maxHeight != null) contentHeight = Math.min(contentHeight, toContentHeight(maxHeight));
|
|
697
|
-
const minHeight =
|
|
752
|
+
const minHeight = resolveHeight(style?.minHeight);
|
|
698
753
|
if (minHeight != null) contentHeight = Math.max(contentHeight, toContentHeight(minHeight));
|
|
699
754
|
if (columnItems?.length) justifyColumn(ctx, columnItems, style?.justifyContent, contentHeight - (contentBottom - contentTop));
|
|
700
755
|
if (gridResult) alignGridContent(ctx, gridResult, style?.alignContent, contentHeight - (contentBottom - contentTop));
|
|
@@ -817,13 +872,9 @@ function layoutFlexRow(ctx, box, x, width, y) {
|
|
|
817
872
|
}
|
|
818
873
|
let crossSize = Math.max(0, ...lineItems.map((item) => item.height + item.marginTop + item.marginBottom));
|
|
819
874
|
// 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
|
-
}
|
|
875
|
+
// ctx.containing.height is that content-box height, percentages already resolved.
|
|
876
|
+
const definiteHeight = ctx.containing?.height;
|
|
877
|
+
if (lines.length === 1 && definiteHeight != null) crossSize = definiteHeight;
|
|
827
878
|
|
|
828
879
|
// Main axis: auto margins absorb free space first, then justify-content.
|
|
829
880
|
let free = width - gaps - lineItems.reduce((sum, item) => sum + item.target + item.marginLeft + item.marginRight, 0);
|
|
@@ -880,7 +931,7 @@ function flexItem(ctx, box, containerWidth) {
|
|
|
880
931
|
// Sizes below are content-box sizes, as in Blink's FlexItem
|
|
881
932
|
// (base_content_size, hypothetical_content_size); `extra` is padding+border.
|
|
882
933
|
const style = box.style;
|
|
883
|
-
const padding = style
|
|
934
|
+
const padding = paddingBox(style, containerWidth);
|
|
884
935
|
const extra = padding[1] + padding[3] + borderWidths(style)[1] + borderWidths(style)[3];
|
|
885
936
|
const contentBox = (value) => Math.max(0, style?.boxSizing === "border-box" ? value - extra : value);
|
|
886
937
|
const margin = style?.margin ?? ZERO_BOX;
|
|
@@ -1471,7 +1522,7 @@ function minimumContribution(box, minContent, available) {
|
|
|
1471
1522
|
const margins = numberOrZero(margin[1]) + numberOrZero(margin[3]);
|
|
1472
1523
|
const specified = numberOrNull(resolveLength(style?.minWidth, available));
|
|
1473
1524
|
if (specified != null) {
|
|
1474
|
-
const padding = style
|
|
1525
|
+
const padding = paddingBox(style, available);
|
|
1475
1526
|
const extra = padding[1] + padding[3] + borderWidths(style)[1] + borderWidths(style)[3];
|
|
1476
1527
|
return (style.boxSizing === "border-box" ? Math.max(specified, extra) : specified + extra) + margins;
|
|
1477
1528
|
}
|
|
@@ -1661,7 +1712,7 @@ function dryLayout(ctx, box, width) {
|
|
|
1661
1712
|
/** Resolves the used border-box width and horizontal margins of a block-level box. */
|
|
1662
1713
|
function resolveHorizontal(ctx, box, containingX, containingWidth, shrink, percentageBasis = containingWidth) {
|
|
1663
1714
|
const style = box.style;
|
|
1664
|
-
const padding = style
|
|
1715
|
+
const padding = paddingBox(style, percentageBasis);
|
|
1665
1716
|
const extra = padding[1] + padding[3] + borderWidths(style)[1] + borderWidths(style)[3];
|
|
1666
1717
|
const borderBox = (value) => style?.boxSizing === "border-box" ? value : value + extra;
|
|
1667
1718
|
const margin = style?.margin ?? ZERO_BOX;
|
|
@@ -1697,7 +1748,7 @@ function maxContentWidth(ctx, box) {
|
|
|
1697
1748
|
const cached = ctx.maxContent.get(cacheKey);
|
|
1698
1749
|
if (cached != null) return cached;
|
|
1699
1750
|
const style = box.style;
|
|
1700
|
-
const padding = style
|
|
1751
|
+
const padding = paddingBox(style);
|
|
1701
1752
|
const extra = padding[1] + padding[3] + borderWidths(style)[1] + borderWidths(style)[3];
|
|
1702
1753
|
let width;
|
|
1703
1754
|
const specified = numberOrNull(style?.width);
|
|
@@ -1730,6 +1781,7 @@ function maxContentWidth(ctx, box) {
|
|
|
1730
1781
|
}
|
|
1731
1782
|
const maxWidth = numberOrNull(style?.maxWidth);
|
|
1732
1783
|
if (maxWidth != null) width = Math.min(width, style.boxSizing === "border-box" ? maxWidth : maxWidth + extra);
|
|
1784
|
+
width = atLeastMinWidth(style, width, extra);
|
|
1733
1785
|
ctx.maxContent.set(cacheKey, width);
|
|
1734
1786
|
return width;
|
|
1735
1787
|
}
|
|
@@ -1739,7 +1791,7 @@ function minContentWidth(ctx, box) {
|
|
|
1739
1791
|
const cached = ctx.minContent.get(cacheKey);
|
|
1740
1792
|
if (cached != null) return cached;
|
|
1741
1793
|
const style = box.style;
|
|
1742
|
-
const padding = style
|
|
1794
|
+
const padding = paddingBox(style);
|
|
1743
1795
|
const extra = padding[1] + padding[3] + borderWidths(style)[1] + borderWidths(style)[3];
|
|
1744
1796
|
const specified = numberOrNull(style?.width);
|
|
1745
1797
|
const outer = (child) => {
|
|
@@ -1763,10 +1815,17 @@ function minContentWidth(ctx, box) {
|
|
|
1763
1815
|
? inlineMinContentWidth(ctx, child)
|
|
1764
1816
|
: outer(child))) + extra;
|
|
1765
1817
|
}
|
|
1818
|
+
width = atLeastMinWidth(style, width, extra);
|
|
1766
1819
|
ctx.minContent.set(cacheKey, width);
|
|
1767
1820
|
return width;
|
|
1768
1821
|
}
|
|
1769
1822
|
|
|
1823
|
+
/** min-width wins over the intrinsic width (and max-width); a percentage is of an unknown width here. */
|
|
1824
|
+
function atLeastMinWidth(style, width, extra) {
|
|
1825
|
+
const minWidth = numberOrNull(style?.minWidth);
|
|
1826
|
+
return minWidth == null ? width : Math.max(width, style.boxSizing === "border-box" ? minWidth : minWidth + extra);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1770
1829
|
function verticalMargins(style) {
|
|
1771
1830
|
const margin = style?.margin ?? ZERO_BOX;
|
|
1772
1831
|
return { top: numberOrZero(margin[0]), bottom: numberOrZero(margin[2]) };
|
|
@@ -1781,6 +1840,15 @@ function collapsesThroughTop(box) {
|
|
|
1781
1840
|
&& inFlowChildren(box)[0]?.kind === "block";
|
|
1782
1841
|
}
|
|
1783
1842
|
|
|
1843
|
+
function selfCollapsing(box) {
|
|
1844
|
+
return !box.bfc && box.kind === "block" && !box.replaced
|
|
1845
|
+
&& borderWidths(box.style).every(value => value === 0)
|
|
1846
|
+
&& (box.style?.padding ?? ZERO_BOX).every(value => value === 0)
|
|
1847
|
+
&& (box.style?.height === "auto" || box.style?.height === 0)
|
|
1848
|
+
&& (numberOrNull(box.style?.minHeight) ?? 0) <= 0
|
|
1849
|
+
&& inFlowChildren(box).every(selfCollapsing);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1784
1852
|
function collapsesThroughBottom(box) {
|
|
1785
1853
|
return !box.bfc && box.kind === "block" && !box.replaced
|
|
1786
1854
|
&& borderWidths(box.style)[2] === 0 && (box.style?.padding ?? ZERO_BOX)[2] === 0
|
|
@@ -1831,7 +1899,17 @@ function layoutInline(ctx, ifc, x, width, y) {
|
|
|
1831
1899
|
}
|
|
1832
1900
|
return atom;
|
|
1833
1901
|
};
|
|
1902
|
+
// Percentage sizes of inline images.
|
|
1903
|
+
atomFor.containing = ctx.containing;
|
|
1834
1904
|
const lines = breakLines(ctx, paragraph, width, atomFor);
|
|
1905
|
+
// Legacy line clamping applies to a vertical -webkit-box, not ordinary blocks.
|
|
1906
|
+
const clamp = ifc.style?.webkitBoxOrient === "vertical"
|
|
1907
|
+
&& ["-webkit-box", "-webkit-inline-box"].includes(ifc.style?.display)
|
|
1908
|
+
? ifc.style.webkitLineClamp : null;
|
|
1909
|
+
if (clamp && lines.length > clamp) {
|
|
1910
|
+
lines.length = clamp;
|
|
1911
|
+
lines[clamp - 1] = { ...lines[clamp - 1], clamped: true };
|
|
1912
|
+
}
|
|
1835
1913
|
let cursor = y;
|
|
1836
1914
|
let baseline = null;
|
|
1837
1915
|
for (const line of lines) {
|
|
@@ -2006,7 +2084,7 @@ function mayBreakWithinWords(paragraph, piece) {
|
|
|
2006
2084
|
* room for "…". Mutates `segments`; returns the ellipsis to append, or null.
|
|
2007
2085
|
*/
|
|
2008
2086
|
function truncateForEllipsis(ctx, style, paragraph, segments, line, width) {
|
|
2009
|
-
if (style?.textOverflow !== "ellipsis" || style.overflowX !== "clip" || line.width <= width + 0.5) return null;
|
|
2087
|
+
if (!line.clamped && (style?.textOverflow !== "ellipsis" || style.overflowX !== "clip" || line.width <= width + 0.5)) return null;
|
|
2010
2088
|
const lastText = [...segments].reverse().find((segment) => segment.run.kind === "text");
|
|
2011
2089
|
if (!lastText) return null;
|
|
2012
2090
|
const text = "\u2026";
|
|
@@ -2042,9 +2120,26 @@ function truncateForEllipsis(ctx, style, paragraph, segments, line, width) {
|
|
|
2042
2120
|
return { text, width: ellipsisWidth, run: lastText.run, metrics: textMetrics(ctx, lastText.run.style) };
|
|
2043
2121
|
}
|
|
2044
2122
|
|
|
2123
|
+
/** Space beside an inline replaced element's content: its margins, borders and padding. */
|
|
2045
2124
|
function inlineImageMargins(node) {
|
|
2046
2125
|
const margin = node.style?.margin ?? ZERO_BOX;
|
|
2047
|
-
|
|
2126
|
+
const border = borderWidths(node.style);
|
|
2127
|
+
const padding = paddingBox(node.style);
|
|
2128
|
+
return {
|
|
2129
|
+
left: numberOrZero(margin[3]) + border[3] + padding[3],
|
|
2130
|
+
right: numberOrZero(margin[1]) + border[1] + padding[1],
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
/** The same above and below its content (its margin box sits on the line). */
|
|
2135
|
+
function inlineImageVerticalInsets(node) {
|
|
2136
|
+
const margin = node.style?.margin ?? ZERO_BOX;
|
|
2137
|
+
const border = borderWidths(node.style);
|
|
2138
|
+
const padding = paddingBox(node.style);
|
|
2139
|
+
return {
|
|
2140
|
+
top: numberOrZero(margin[0]) + border[0] + padding[0],
|
|
2141
|
+
bottom: numberOrZero(margin[2]) + border[2] + padding[2],
|
|
2142
|
+
};
|
|
2048
2143
|
}
|
|
2049
2144
|
|
|
2050
2145
|
/**
|
|
@@ -2069,7 +2164,7 @@ function inlineImageTop(style, height, metrics) {
|
|
|
2069
2164
|
|
|
2070
2165
|
function inlineInsets(style) {
|
|
2071
2166
|
const margin = style?.margin ?? ZERO_BOX;
|
|
2072
|
-
const padding = style
|
|
2167
|
+
const padding = paddingBox(style);
|
|
2073
2168
|
const border = borderWidths(style);
|
|
2074
2169
|
return {
|
|
2075
2170
|
left: numberOrZero(margin[3]) + border[3] + padding[3],
|
|
@@ -2092,7 +2187,8 @@ function runSliceWidth(ctx, run, start, end, text, atomFor) {
|
|
|
2092
2187
|
if (end <= start) return 0;
|
|
2093
2188
|
if (run.kind === "image") {
|
|
2094
2189
|
const margins = inlineImageMargins(run.node);
|
|
2095
|
-
|
|
2190
|
+
const containing = atomFor.containing;
|
|
2191
|
+
return imageDimensions(run.node, containing?.width ?? Infinity, containing?.height).width + margins.left + margins.right;
|
|
2096
2192
|
}
|
|
2097
2193
|
if (run.kind === "atomic") {
|
|
2098
2194
|
const atom = atomFor(run);
|
|
@@ -2172,13 +2268,20 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2172
2268
|
const strut = textMetrics(ctx, style);
|
|
2173
2269
|
let ascent = strut.ascent;
|
|
2174
2270
|
let descent = strut.descent;
|
|
2271
|
+
// Floats (approximated as atomic inlines) sit at the top of the line instead of on its baseline.
|
|
2272
|
+
let floatHeight = 0;
|
|
2175
2273
|
for (const segment of segments) {
|
|
2176
2274
|
const { run } = segment;
|
|
2177
|
-
if (run.kind === "
|
|
2178
|
-
|
|
2179
|
-
|
|
2275
|
+
if (run.kind === "atomic" && isFloat(run.style)) {
|
|
2276
|
+
const atom = atomFor(run);
|
|
2277
|
+
floatHeight = Math.max(floatHeight, atom.marginTop + atom.height + atom.marginBottom);
|
|
2278
|
+
} else if (run.kind === "image") {
|
|
2279
|
+
segment.dimensions = imageDimensions(run.node, width, ctx.containing?.height);
|
|
2280
|
+
segment.insets = inlineImageVerticalInsets(run.node);
|
|
2281
|
+
const outerHeight = segment.dimensions.height + segment.insets.top + segment.insets.bottom;
|
|
2282
|
+
segment.imageTop = inlineImageTop(run.node.style, outerHeight, strut);
|
|
2180
2283
|
ascent = Math.max(ascent, -segment.imageTop);
|
|
2181
|
-
descent = Math.max(descent, segment.imageTop +
|
|
2284
|
+
descent = Math.max(descent, segment.imageTop + outerHeight);
|
|
2182
2285
|
} else if (run.kind === "atomic") {
|
|
2183
2286
|
const atom = atomFor(run);
|
|
2184
2287
|
const atomAscent = atom.marginTop + (atom.baseline ?? atom.height) ;
|
|
@@ -2192,7 +2295,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2192
2295
|
}
|
|
2193
2296
|
}
|
|
2194
2297
|
const baselineY = y + ascent;
|
|
2195
|
-
const lineHeight = ascent + descent;
|
|
2298
|
+
const lineHeight = Math.max(ascent + descent, floatHeight);
|
|
2196
2299
|
|
|
2197
2300
|
// Horizontal alignment.
|
|
2198
2301
|
const alignment = style?.textAlign;
|
|
@@ -2205,6 +2308,22 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2205
2308
|
const edges = new Map();
|
|
2206
2309
|
const runs = [];
|
|
2207
2310
|
const atomsToPlace = [];
|
|
2311
|
+
// Relatively positioned inline boxes move their contents (CSS 2 §9.4.3);
|
|
2312
|
+
// an offset adds to those of the inline boxes around it.
|
|
2313
|
+
const containing = ctx.containing;
|
|
2314
|
+
const offsets = new Map();
|
|
2315
|
+
const offsetOf = (node, style, outer) => {
|
|
2316
|
+
if (!offsets.has(node)) {
|
|
2317
|
+
const own = relativeOffset(style, containing);
|
|
2318
|
+
offsets.set(node, own ? { x: outer.x + own.x, y: outer.y + own.y } : outer);
|
|
2319
|
+
}
|
|
2320
|
+
return offsets.get(node);
|
|
2321
|
+
};
|
|
2322
|
+
const runOffset = (run) => {
|
|
2323
|
+
let offset = ZERO_OFFSET;
|
|
2324
|
+
for (const node of run.inlines) offset = offsetOf(node, paragraph.inlineState.get(node).style, offset);
|
|
2325
|
+
return run.kind === "image" ? offsetOf(run.node, run.node.style, offset) : offset;
|
|
2326
|
+
};
|
|
2208
2327
|
// Right floats (approximated as atomic inlines) go to the end of the line.
|
|
2209
2328
|
const rightFloats = segments.filter((segment) => segment.run.kind === "atomic" && segment.run.style?.float === "right");
|
|
2210
2329
|
const rightFloatWidth = rightFloats.reduce((sum, segment) => sum + segment.width, 0);
|
|
@@ -2223,6 +2342,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2223
2342
|
}
|
|
2224
2343
|
}
|
|
2225
2344
|
const segmentWidth = segment.width;
|
|
2345
|
+
const shift = runOffset(run);
|
|
2226
2346
|
const common = {
|
|
2227
2347
|
nodeId: run.nodeId,
|
|
2228
2348
|
text: run.kind === "text" ? paragraph.text.slice(segment.start, segment.end) : "",
|
|
@@ -2231,11 +2351,27 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2231
2351
|
style: run.style ?? style,
|
|
2232
2352
|
};
|
|
2233
2353
|
if (run.kind === "image") {
|
|
2354
|
+
const imageX = cursor + inlineImageMargins(run.node).left + shift.x;
|
|
2355
|
+
const imageY = baselineY + segment.imageTop + segment.insets.top + shift.y;
|
|
2356
|
+
// Its background and borders, around the content box.
|
|
2357
|
+
if (isDecorated(run.node.style)) {
|
|
2358
|
+
const border = borderWidths(run.node.style);
|
|
2359
|
+
const padding = paddingBox(run.node.style, ctx.containing?.width);
|
|
2360
|
+
ctx.boxes.push({
|
|
2361
|
+
nodeId: run.nodeId,
|
|
2362
|
+
x: imageX - padding[3] - border[3],
|
|
2363
|
+
y: imageY - padding[0] - border[0],
|
|
2364
|
+
width: segment.dimensions.width + padding[1] + padding[3] + border[1] + border[3],
|
|
2365
|
+
height: segment.dimensions.height + padding[0] + padding[2] + border[0] + border[2],
|
|
2366
|
+
style: run.node.style,
|
|
2367
|
+
inline: true,
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2234
2370
|
runs.push({
|
|
2235
2371
|
...common,
|
|
2236
2372
|
type: "image",
|
|
2237
|
-
x:
|
|
2238
|
-
y:
|
|
2373
|
+
x: imageX,
|
|
2374
|
+
y: imageY,
|
|
2239
2375
|
width: segment.dimensions.width,
|
|
2240
2376
|
height: segment.dimensions.height,
|
|
2241
2377
|
resource: run.node.resource,
|
|
@@ -2245,14 +2381,14 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2245
2381
|
});
|
|
2246
2382
|
} else if (run.kind === "atomic") {
|
|
2247
2383
|
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 });
|
|
2384
|
+
const atomY = isFloat(run.style) ? y + atom.marginTop : baselineY - segment.atomAscent + atom.marginTop;
|
|
2385
|
+
runs.push({ ...common, type: "atomic", x: cursor + shift.x, y: atomY + shift.y, width: segmentWidth, height: atom.height });
|
|
2386
|
+
atomsToPlace.push({ box: run.box, x: cursor + atom.marginLeft + shift.x, y: atomY + shift.y, width: atom.width });
|
|
2251
2387
|
} else {
|
|
2252
2388
|
runs.push({
|
|
2253
2389
|
...common,
|
|
2254
|
-
x: cursor,
|
|
2255
|
-
y: baselineY - segment.metrics.contentAscent,
|
|
2390
|
+
x: cursor + shift.x,
|
|
2391
|
+
y: baselineY - segment.metrics.contentAscent + shift.y,
|
|
2256
2392
|
width: Math.max(1, segmentWidth),
|
|
2257
2393
|
height: null,
|
|
2258
2394
|
});
|
|
@@ -2296,15 +2432,16 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2296
2432
|
for (const [node, edge] of edges) {
|
|
2297
2433
|
const nodeStyle = paragraph.inlineState.get(node)?.style;
|
|
2298
2434
|
if (!isDecorated(nodeStyle) || edge.first == null) continue;
|
|
2299
|
-
const padding = nodeStyle.
|
|
2435
|
+
const padding = paddingBox(nodeStyle, ctx.containing?.width);
|
|
2300
2436
|
const border = borderWidths(nodeStyle);
|
|
2301
2437
|
const metrics = textMetrics(ctx, nodeStyle);
|
|
2302
2438
|
const left = edge.left ?? edge.first;
|
|
2303
2439
|
const right = edge.right ?? edge.last;
|
|
2440
|
+
const shift = offsets.get(node) ?? ZERO_OFFSET;
|
|
2304
2441
|
ctx.boxes.push({
|
|
2305
2442
|
nodeId: node.id,
|
|
2306
|
-
x: left,
|
|
2307
|
-
y: baselineY - metrics.contentAscent - padding[0] - border[0],
|
|
2443
|
+
x: left + shift.x,
|
|
2444
|
+
y: baselineY - metrics.contentAscent - padding[0] - border[0] + shift.y,
|
|
2308
2445
|
width: right - left,
|
|
2309
2446
|
height: metrics.contentHeight + padding[0] + padding[2] + border[0] + border[2],
|
|
2310
2447
|
style: nodeStyle,
|
|
@@ -2327,6 +2464,10 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2327
2464
|
return { height: lineHeight, baseline: baselineY };
|
|
2328
2465
|
}
|
|
2329
2466
|
|
|
2467
|
+
function isFloat(style) {
|
|
2468
|
+
return style?.float === "left" || style?.float === "right";
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2330
2471
|
function measureAtomic(ctx, box, availableWidth) {
|
|
2331
2472
|
const placed = resolveHorizontal(ctx, box, 0, availableWidth, true);
|
|
2332
2473
|
const result = dryLayout(ctx, box, placed.width);
|
|
@@ -2353,7 +2494,7 @@ function textMetrics(ctx, style) {
|
|
|
2353
2494
|
|
|
2354
2495
|
function isDecorated(style) {
|
|
2355
2496
|
return Boolean(style && ((style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)")
|
|
2356
|
-
|| borderWidths(style).some((width) => width > 0)));
|
|
2497
|
+
|| style.backgroundImage || borderWidths(style).some((width) => width > 0)));
|
|
2357
2498
|
}
|
|
2358
2499
|
|
|
2359
2500
|
function borderWidths(style) {
|
|
@@ -2420,7 +2561,49 @@ function cachedMetrics(fontMetrics) {
|
|
|
2420
2561
|
};
|
|
2421
2562
|
}
|
|
2422
2563
|
|
|
2423
|
-
|
|
2564
|
+
/**
|
|
2565
|
+
* An iframe's content box: like other replaced elements, but with no
|
|
2566
|
+
* intrinsic size or ratio of its own (its document's frame is drawn into
|
|
2567
|
+
* whatever box it gets), so each dimension falls back to 300x150 alone.
|
|
2568
|
+
*/
|
|
2569
|
+
function frameDimensions(node, referenceWidth, referenceHeight) {
|
|
2570
|
+
const resolve = (cssValue, attribute, fallback, reference) => {
|
|
2571
|
+
const value = cssValue !== "auto" && cssValue != null ? resolveLength(cssValue, reference ?? NaN) : attribute;
|
|
2572
|
+
return Number.isFinite(value) ? value : fallback;
|
|
2573
|
+
};
|
|
2574
|
+
let width = resolve(node.style?.width, node.widthAttribute, 300, referenceWidth);
|
|
2575
|
+
const height = resolve(node.style?.height, node.heightAttribute, 150, referenceHeight);
|
|
2576
|
+
const maximum = node.style?.maxWidth !== "none" && node.style?.maxWidth != null ? resolveLength(node.style.maxWidth, referenceWidth) : NaN;
|
|
2577
|
+
if (Number.isFinite(maximum) && width > maximum) width = maximum;
|
|
2578
|
+
return { width: Math.max(0, width), height: Math.max(0, height) };
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
/**
|
|
2582
|
+
* An inline <svg>'s size (SVG 2 §8.2): its CSS size, else its width and
|
|
2583
|
+
* height attributes, else 100% of its container's width; a missing height
|
|
2584
|
+
* follows the viewBox's ratio, or is 150px without one. Where the container
|
|
2585
|
+
* is still being measured (an indefinite width) it counts as 300px wide.
|
|
2586
|
+
*/
|
|
2587
|
+
function svgDimensions(node, referenceWidth, referenceHeight) {
|
|
2588
|
+
const css = (value, reference) => value !== "auto" && value != null ? numberOrNull(resolveLength(value, reference ?? NaN)) : null;
|
|
2589
|
+
const ratio = node.svgRatio ?? null;
|
|
2590
|
+
let width = css(node.style?.width, referenceWidth) ?? node.widthAttribute;
|
|
2591
|
+
let height = css(node.style?.height, referenceHeight) ?? node.heightAttribute;
|
|
2592
|
+
if (width == null && height != null && ratio) width = height * ratio;
|
|
2593
|
+
if (width == null) width = Number.isFinite(referenceWidth) ? referenceWidth : 300;
|
|
2594
|
+
if (height == null) height = ratio ? width / ratio : 150;
|
|
2595
|
+
const maximum = node.style?.maxWidth !== "none" && node.style?.maxWidth != null ? numberOrNull(resolveLength(node.style.maxWidth, referenceWidth)) : null;
|
|
2596
|
+
if (maximum != null && width > maximum) {
|
|
2597
|
+
height = ratio ? maximum / ratio : height;
|
|
2598
|
+
width = maximum;
|
|
2599
|
+
}
|
|
2600
|
+
return { width: Math.max(0, width), height: Math.max(0, height) };
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/** Percentages in width are of `referenceWidth`, in height of `referenceHeight` (auto if indefinite). */
|
|
2604
|
+
function imageDimensions(node, referenceWidth, referenceHeight = null) {
|
|
2605
|
+
if (node.tagName === "IFRAME") return frameDimensions(node, referenceWidth, referenceHeight);
|
|
2606
|
+
if (node.tagName === "SVG") return svgDimensions(node, referenceWidth, referenceHeight);
|
|
2424
2607
|
const intrinsicWidth = node.resource?.width ?? node.widthAttribute ?? 300;
|
|
2425
2608
|
const intrinsicHeight = node.resource?.height ?? node.heightAttribute ?? 150;
|
|
2426
2609
|
const ratio = intrinsicWidth / intrinsicHeight;
|
|
@@ -2430,7 +2613,7 @@ function imageDimensions(node, referenceWidth) {
|
|
|
2430
2613
|
? resolveLength(cssWidth, referenceWidth)
|
|
2431
2614
|
: node.widthAttribute;
|
|
2432
2615
|
let height = cssHeight !== "auto" && cssHeight != null
|
|
2433
|
-
? resolveLength(cssHeight,
|
|
2616
|
+
? resolveLength(cssHeight, referenceHeight ?? NaN)
|
|
2434
2617
|
: node.heightAttribute;
|
|
2435
2618
|
if (!Number.isFinite(width)) width = null;
|
|
2436
2619
|
if (!Number.isFinite(height)) height = null;
|