buninu-browser 0.2.0 → 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 +19 -12
- package/lib/happy-dom/parser.js +24 -4
- package/lib/happy-dom/selector-matcher.js +83 -0
- package/lib/headless-shell.js +120 -28
- package/lib/input/hit-test.js +25 -7
- package/lib/layout/text-layout.js +365 -54
- 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 +326 -46
- package/lib/paint/display-list.js +322 -21
- package/lib/paint/gradient.js +11 -5
- package/lib/render-tree/index.js +62 -2
- package/lib/render-tree/svg.js +2 -1
- package/lib/renderer/bindings.js +989 -72
- package/lib/renderer/canvas-2d.js +92 -0
- package/lib/renderer/host.js +159 -9
- 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 +1407 -200
- package/lib/renderer/page-renderer.js +1899 -394
- 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 +463 -242
- package/lib/style/selector-keys.js +302 -0
- package/lib/style/selector-traits.js +240 -0
- 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 +98 -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 +29 -0
- package/test/hit-test.test.js +25 -1
- package/test/log.test.js +4 -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 +81 -0
- package/test/page-scripts.test.js +1094 -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/render-tree.test.js +22 -1
- 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/selector-keys.test.js +39 -0
- package/test/selector-traits.test.js +19 -0
- package/test/setup.js +5 -0
- package/test/text-layout.test.js +69 -1
- 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;
|
|
@@ -39,10 +40,11 @@ export function layoutText(tree, options = {}) {
|
|
|
39
40
|
boxes: [],
|
|
40
41
|
lineHeight: options.lineHeight == null ? null : positiveNumber(options.lineHeight, 31),
|
|
41
42
|
fallbackLineHeight: 31,
|
|
42
|
-
measure: cachedMeasure(measureText),
|
|
43
|
-
metrics: cachedMetrics(options.fontMetrics),
|
|
44
|
-
maxContent: new WeakMap(),
|
|
45
|
-
minContent: new WeakMap(),
|
|
43
|
+
measure: options.measureCache ?? cachedMeasure(measureText),
|
|
44
|
+
metrics: options.metricsCache ?? cachedMetrics(options.fontMetrics),
|
|
45
|
+
maxContent: options.maxContentCache ?? new WeakMap(),
|
|
46
|
+
minContent: options.minContentCache ?? new WeakMap(),
|
|
47
|
+
blockResults: options.blockResultCache ?? null,
|
|
46
48
|
// Paint layers: the root layer holds in-flow content; positioned boxes add
|
|
47
49
|
// more. A layer with an integer z-index is a stacking context; `parent` is
|
|
48
50
|
// the stacking context a layer is painted in (CSS 2 Appendix E).
|
|
@@ -50,14 +52,23 @@ export function layoutText(tree, options = {}) {
|
|
|
50
52
|
stackingContext: ROOT_LAYER,
|
|
51
53
|
layerCount: 1,
|
|
52
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,
|
|
58
|
+
// Static positions recorded for out-of-flow boxes, in order (see
|
|
59
|
+
// cacheBlockResult), and those boxes by node for replays.
|
|
60
|
+
staticPositions: [],
|
|
61
|
+
outOfFlowBoxes: new Map(),
|
|
53
62
|
};
|
|
54
63
|
|
|
55
64
|
containingBlocks = [];
|
|
65
|
+
outOfFlowBoxes = ctx.outOfFlowBoxes;
|
|
56
66
|
let root;
|
|
57
67
|
try {
|
|
58
68
|
root = buildBox(tree.root, inheritedFlags(tree.root, { preserve: false, wrap: true }));
|
|
59
69
|
} finally {
|
|
60
70
|
containingBlocks = null;
|
|
71
|
+
outOfFlowBoxes = null;
|
|
61
72
|
}
|
|
62
73
|
const containingWidth = options.measureText || viewportWidth
|
|
63
74
|
? Math.max(1, (viewportWidth || columns * columnWidth) - x * 2)
|
|
@@ -72,6 +83,7 @@ export function layoutText(tree, options = {}) {
|
|
|
72
83
|
// establishes the BFC, so body margins collapse with its children's.
|
|
73
84
|
const horizontal = resolveHorizontal(ctx, root, x, containingWidth, false);
|
|
74
85
|
const rootY = y + strutValue(topStrut(root));
|
|
86
|
+
ctx.containing = { width: containingWidth, height: ctx.initialContainingBlock.height || null };
|
|
75
87
|
const result = layoutBlock(ctx, root, horizontal.x, horizontal.width, rootY, collapsesThroughTop(root));
|
|
76
88
|
const height = rootY + result.height + strutValue(bottomStrut(root));
|
|
77
89
|
|
|
@@ -91,12 +103,43 @@ export function layoutText(tree, options = {}) {
|
|
|
91
103
|
});
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Long-lived layout entry point. Expensive shaping measurements are valid as
|
|
108
|
+
* long as their immutable computed-style identity is valid, so retain them
|
|
109
|
+
* across incremental updates instead of starting cold on every reflow.
|
|
110
|
+
* Formatting-context result caches are added to this same owner.
|
|
111
|
+
*/
|
|
112
|
+
export class LayoutEngine {
|
|
113
|
+
#measure;
|
|
114
|
+
#metrics;
|
|
115
|
+
#maxContent = new WeakMap();
|
|
116
|
+
#minContent = new WeakMap();
|
|
117
|
+
#blockResults = new WeakMap();
|
|
118
|
+
|
|
119
|
+
constructor({ measureText = (text) => Bun.stringWidth(text) * 13, fontMetrics = null } = {}) {
|
|
120
|
+
this.#measure = cachedMeasure(measureText);
|
|
121
|
+
this.#metrics = cachedMetrics(fontMetrics);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
layout(tree, options = {}) {
|
|
125
|
+
return layoutText(tree, {
|
|
126
|
+
...options,
|
|
127
|
+
measureCache: this.#measure,
|
|
128
|
+
metricsCache: this.#metrics,
|
|
129
|
+
maxContentCache: this.#maxContent,
|
|
130
|
+
minContentCache: this.#minContent,
|
|
131
|
+
blockResultCache: this.#blockResults,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
94
136
|
// ---------------------------------------------------------------------------
|
|
95
137
|
// Box tree construction
|
|
96
138
|
|
|
97
139
|
// Absolutely positioned boxes register with their containing block while the
|
|
98
140
|
// box tree is built. Building is synchronous and scoped to layoutText.
|
|
99
141
|
let containingBlocks = null;
|
|
142
|
+
let outOfFlowBoxes = null;
|
|
100
143
|
|
|
101
144
|
// Text nodes carry their parent's style, so only element boxes can be positioned.
|
|
102
145
|
const UNPOSITIONABLE = new Set(["root", "text", "line-break"]);
|
|
@@ -115,6 +158,7 @@ function outOfFlowBox(node, flags) {
|
|
|
115
158
|
const box = buildBox(node, inheritedFlags(node, flags), true);
|
|
116
159
|
box.outOfFlow = true;
|
|
117
160
|
holder.absolutes.push(box);
|
|
161
|
+
outOfFlowBoxes?.set(node, box);
|
|
118
162
|
return box;
|
|
119
163
|
}
|
|
120
164
|
|
|
@@ -279,8 +323,15 @@ function hasInlineContent(items) {
|
|
|
279
323
|
*/
|
|
280
324
|
function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = null) {
|
|
281
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;
|
|
282
331
|
const boxStart = ctx.boxes.length;
|
|
283
332
|
const fragmentStart = ctx.fragments.length;
|
|
333
|
+
const layerStart = ctx.layerCount;
|
|
334
|
+
const staticStart = ctx.staticPositions.length;
|
|
284
335
|
const previousLayer = ctx.layer;
|
|
285
336
|
const previousContext = ctx.stackingContext;
|
|
286
337
|
// Anonymous boxes reuse their container's node but are never positioned themselves.
|
|
@@ -300,10 +351,10 @@ function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = nu
|
|
|
300
351
|
if (box.absolutes?.length) layoutAbsolutes(ctx, box, record);
|
|
301
352
|
let dx = 0;
|
|
302
353
|
let dy = 0;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
dx =
|
|
306
|
-
dy =
|
|
354
|
+
const offset = relativeOffset(style, containing ?? { width, height: null });
|
|
355
|
+
if (offset) {
|
|
356
|
+
dx = offset.x;
|
|
357
|
+
dy = offset.y;
|
|
307
358
|
}
|
|
308
359
|
if (style?.transform) {
|
|
309
360
|
dx += numberOrZero(resolveLength(style.transform.x, record.width));
|
|
@@ -314,9 +365,137 @@ function layoutBlock(ctx, box, x, width, y, absorbTop = false, forcedHeight = nu
|
|
|
314
365
|
if (dx || dy) shiftOutput(ctx, boxStart, fragmentStart, dx, dy);
|
|
315
366
|
ctx.layer = previousLayer;
|
|
316
367
|
ctx.stackingContext = previousContext;
|
|
368
|
+
cacheBlockResult(ctx, box, key, x, y, result, {
|
|
369
|
+
boxStart, fragmentStart, layerStart, staticStart, layer: previousLayer, context: previousContext,
|
|
370
|
+
});
|
|
317
371
|
return result;
|
|
318
372
|
}
|
|
319
373
|
|
|
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;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Where an out-of-flow box would have been in flow; recorded so that a replayed block sets it again. */
|
|
394
|
+
function setStaticPosition(ctx, box, position) {
|
|
395
|
+
box.staticPosition = position;
|
|
396
|
+
ctx.staticPositions.push({ node: box.node, position });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Replays a retained formatting-context result at its new flow position. */
|
|
400
|
+
function cachedBlockResult(ctx, box, key, y, x) {
|
|
401
|
+
if (!ctx.blockResults || box.anonymous) return null;
|
|
402
|
+
const cached = ctx.blockResults.get(box.node)?.get(key);
|
|
403
|
+
if (!cached) return null;
|
|
404
|
+
const dx = x - cached.x;
|
|
405
|
+
const dy = y - cached.y;
|
|
406
|
+
// The block's own paint layers are made again, in their order, after the
|
|
407
|
+
// layers laid out before it; -1 is the layer and stacking context it is in.
|
|
408
|
+
const layers = [];
|
|
409
|
+
for (const layer of cached.layers) {
|
|
410
|
+
layers.push(Object.freeze({
|
|
411
|
+
z: layer.z,
|
|
412
|
+
order: ctx.layerCount++,
|
|
413
|
+
context: layer.context,
|
|
414
|
+
parent: layer.parent === -1 ? ctx.stackingContext : layers[layer.parent],
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
const layerAt = (index) => index === -1 ? ctx.layer : layers[index];
|
|
418
|
+
// Retained output is frozen: where it neither moved nor changed layer, the
|
|
419
|
+
// same objects are output again.
|
|
420
|
+
const moved = dx !== 0 || dy !== 0;
|
|
421
|
+
cached.boxes.forEach((record, index) => {
|
|
422
|
+
const layer = layerAt(cached.boxLayers[index]);
|
|
423
|
+
ctx.boxes.push(!moved && record.layer === layer ? record : { ...record, x: record.x + dx, y: record.y + dy, layer });
|
|
424
|
+
});
|
|
425
|
+
cached.fragments.forEach((fragment, index) => {
|
|
426
|
+
const layer = layerAt(cached.fragmentLayers[index]);
|
|
427
|
+
ctx.fragments.push(!moved && fragment.layer === layer ? fragment : { ...shiftedFragment(fragment, dx, dy), layer });
|
|
428
|
+
});
|
|
429
|
+
// Out-of-flow boxes whose containing block is outside this one still need their static positions.
|
|
430
|
+
for (const { node, x: staticX, y: staticY } of cached.staticPositions) {
|
|
431
|
+
const outOfFlow = ctx.outOfFlowBoxes.get(node);
|
|
432
|
+
const position = { x: staticX + dx, y: staticY + dy };
|
|
433
|
+
if (outOfFlow) outOfFlow.staticPosition = position;
|
|
434
|
+
ctx.staticPositions.push({ node, position });
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
height: cached.result.height,
|
|
438
|
+
baseline: cached.result.baseline == null ? null : cached.result.baseline + dy,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Retains a block's output for replay (cachedBlockResult). Positioned boxes
|
|
444
|
+
* inside it make paint layers of their own; those are kept by creation
|
|
445
|
+
* order relative to the layer and stacking context the block is in, and the
|
|
446
|
+
* static positions it recorded go along. Output in a layer made before the
|
|
447
|
+
* block (which should not occur) is not retained.
|
|
448
|
+
*/
|
|
449
|
+
function cacheBlockResult(ctx, box, key, x, y, result, start) {
|
|
450
|
+
if (!ctx.blockResults || box.anonymous) return;
|
|
451
|
+
const boxes = ctx.boxes.slice(start.boxStart);
|
|
452
|
+
const fragments = ctx.fragments.slice(start.fragmentStart);
|
|
453
|
+
const local = new Set();
|
|
454
|
+
const collect = (layer) => {
|
|
455
|
+
if (layer === start.layer || layer === start.context || local.has(layer)) return true;
|
|
456
|
+
if (!layer || layer.order < start.layerStart) return false;
|
|
457
|
+
local.add(layer);
|
|
458
|
+
return collect(layer.parent);
|
|
459
|
+
};
|
|
460
|
+
if (!boxes.every((record) => collect(record.layer)) || !fragments.every((fragment) => collect(fragment.layer))) return;
|
|
461
|
+
const ordered = [...local].sort((left, right) => left.order - right.order);
|
|
462
|
+
const indices = new Map(ordered.map((layer, index) => [layer, index]));
|
|
463
|
+
const layers = [];
|
|
464
|
+
for (const layer of ordered) {
|
|
465
|
+
const parent = indices.get(layer.parent) ?? (layer.parent === start.context ? -1 : null);
|
|
466
|
+
if (parent === null) return;
|
|
467
|
+
layers.push({ z: layer.z, context: layer.context, parent });
|
|
468
|
+
}
|
|
469
|
+
const indexOf = (layer) => layer === start.layer ? -1 : indices.get(layer) ?? null;
|
|
470
|
+
const boxLayers = boxes.map((record) => indexOf(record.layer));
|
|
471
|
+
const fragmentLayers = fragments.map((fragment) => indexOf(fragment.layer));
|
|
472
|
+
if (boxLayers.includes(null) || fragmentLayers.includes(null)) return;
|
|
473
|
+
let entries = ctx.blockResults.get(box.node);
|
|
474
|
+
if (!entries) ctx.blockResults.set(box.node, entries = new Map());
|
|
475
|
+
// A node normally sees one containing width. Bound unusual cases such as a
|
|
476
|
+
// reused table cell so the retained cache cannot grow without limit.
|
|
477
|
+
if (!entries.has(key) && entries.size >= 4) {
|
|
478
|
+
entries.delete(entries.keys().next().value);
|
|
479
|
+
}
|
|
480
|
+
entries.set(key, {
|
|
481
|
+
x,
|
|
482
|
+
y,
|
|
483
|
+
result: { ...result },
|
|
484
|
+
layers,
|
|
485
|
+
boxes: boxes.map(retained),
|
|
486
|
+
boxLayers,
|
|
487
|
+
fragments: fragments.map(retained),
|
|
488
|
+
fragmentLayers,
|
|
489
|
+
staticPositions: ctx.staticPositions.slice(start.staticStart)
|
|
490
|
+
.map(({ node, position }) => ({ node, x: position.x, y: position.y })),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** An output record as retained: frozen (records replayed from a retained block already are). */
|
|
495
|
+
function retained(record) {
|
|
496
|
+
return Object.isFrozen(record) ? record : Object.freeze({ ...record });
|
|
497
|
+
}
|
|
498
|
+
|
|
320
499
|
/** CSS 2 §10.3.7/§10.6.4 absolute placement against the padding box. */
|
|
321
500
|
function layoutAbsolutes(ctx, box, record) {
|
|
322
501
|
const border = borderWidths(box.style);
|
|
@@ -387,10 +566,11 @@ function justifyColumn(ctx, items, justify = "normal", free) {
|
|
|
387
566
|
});
|
|
388
567
|
}
|
|
389
568
|
|
|
569
|
+
/** Moves output already produced; records are replaced, as retained ones are frozen and shared. */
|
|
390
570
|
function shiftOutput(ctx, boxStart, fragmentStart, dx, dy) {
|
|
391
571
|
for (let index = boxStart; index < ctx.boxes.length; index++) {
|
|
392
|
-
ctx.boxes[index]
|
|
393
|
-
ctx.boxes[index].y
|
|
572
|
+
const record = ctx.boxes[index];
|
|
573
|
+
ctx.boxes[index] = { ...record, x: record.x + dx, y: record.y + dy };
|
|
394
574
|
}
|
|
395
575
|
for (let index = fragmentStart; index < ctx.fragments.length; index++) {
|
|
396
576
|
ctx.fragments[index] = shiftedFragment(ctx.fragments[index], dx, dy);
|
|
@@ -398,6 +578,7 @@ function shiftOutput(ctx, boxStart, fragmentStart, dx, dy) {
|
|
|
398
578
|
}
|
|
399
579
|
|
|
400
580
|
function shiftedFragment(fragment, dx, dy) {
|
|
581
|
+
if (dx === 0 && dy === 0) return fragment;
|
|
401
582
|
return {
|
|
402
583
|
...fragment,
|
|
403
584
|
x: fragment.x + dx,
|
|
@@ -418,7 +599,7 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
418
599
|
const record = { nodeId: box.node.id, x, y, width, height: 0, style };
|
|
419
600
|
ctx.boxes.push(record);
|
|
420
601
|
if (box.replaced) {
|
|
421
|
-
const dimensions = imageDimensions(box.node, width);
|
|
602
|
+
const dimensions = imageDimensions(box.node, width, ctx.containing?.height);
|
|
422
603
|
const imageX = x + border[3] + padding[3];
|
|
423
604
|
const imageY = y + border[0] + padding[0];
|
|
424
605
|
ctx.fragments.push(lineFragment(box.style, "", [box.node.id], [{
|
|
@@ -450,6 +631,21 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
450
631
|
let columnItems = null;
|
|
451
632
|
let gridResult = null;
|
|
452
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
|
+
};
|
|
453
649
|
|
|
454
650
|
if (box.table) {
|
|
455
651
|
const result = layoutTable(ctx, box, contentX, contentWidth, contentTop);
|
|
@@ -481,7 +677,9 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
481
677
|
: child.style.alignSelf;
|
|
482
678
|
const shrink = !["normal", "stretch"].includes(alignment);
|
|
483
679
|
const placed = resolveHorizontal(ctx, child, contentX, contentWidth, shrink);
|
|
484
|
-
|
|
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";
|
|
485
683
|
let childX = placed.x;
|
|
486
684
|
if (shrink && !autoMargins) {
|
|
487
685
|
const free = contentWidth - placed.width - placed.marginLeft - placed.marginRight;
|
|
@@ -500,7 +698,7 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
500
698
|
let firstInFlow = true;
|
|
501
699
|
box.children.forEach((child) => {
|
|
502
700
|
if (child.kind === "placeholder") {
|
|
503
|
-
child.box
|
|
701
|
+
setStaticPosition(ctx, child.box, { x: contentX, y: cursor + strutValue(pending ?? ZERO_STRUT) });
|
|
504
702
|
return;
|
|
505
703
|
}
|
|
506
704
|
const first = firstInFlow;
|
|
@@ -527,16 +725,12 @@ function layoutBlockContent(ctx, box, x, width, y, absorbTop = false, forcedHeig
|
|
|
527
725
|
contentBottom = collapsesThroughBottom(box) ? cursor : cursor + strutValue(pending ?? ZERO_STRUT);
|
|
528
726
|
}
|
|
529
727
|
|
|
728
|
+
ctx.containing = previousContaining;
|
|
530
729
|
let contentHeight = Math.max(0, contentBottom - contentTop);
|
|
531
|
-
const toContentHeight = (value) => style.boxSizing === "border-box"
|
|
532
|
-
? Math.max(0, value - border[0] - border[2] - padding[0] - padding[2])
|
|
533
|
-
: value;
|
|
534
|
-
const specifiedHeight = numberOrNull(style?.height);
|
|
535
730
|
if (specifiedHeight != null) contentHeight = toContentHeight(specifiedHeight);
|
|
536
|
-
|
|
537
|
-
const maxHeight = numberOrNull(style?.maxHeight);
|
|
731
|
+
const maxHeight = style?.maxHeight === "none" ? null : resolveHeight(style?.maxHeight);
|
|
538
732
|
if (maxHeight != null) contentHeight = Math.min(contentHeight, toContentHeight(maxHeight));
|
|
539
|
-
const minHeight =
|
|
733
|
+
const minHeight = resolveHeight(style?.minHeight);
|
|
540
734
|
if (minHeight != null) contentHeight = Math.max(contentHeight, toContentHeight(minHeight));
|
|
541
735
|
if (columnItems?.length) justifyColumn(ctx, columnItems, style?.justifyContent, contentHeight - (contentBottom - contentTop));
|
|
542
736
|
if (gridResult) alignGridContent(ctx, gridResult, style?.alignContent, contentHeight - (contentBottom - contentTop));
|
|
@@ -659,13 +853,9 @@ function layoutFlexRow(ctx, box, x, width, y) {
|
|
|
659
853
|
}
|
|
660
854
|
let crossSize = Math.max(0, ...lineItems.map((item) => item.height + item.marginTop + item.marginBottom));
|
|
661
855
|
// A single-line container with a definite height gives its line that cross size (§9.4.15).
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
crossSize = style.boxSizing === "border-box"
|
|
666
|
-
? Math.max(0, definiteHeight - padding[0] - padding[2] - borderWidths(style)[0] - borderWidths(style)[2])
|
|
667
|
-
: definiteHeight;
|
|
668
|
-
}
|
|
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;
|
|
669
859
|
|
|
670
860
|
// Main axis: auto margins absorb free space first, then justify-content.
|
|
671
861
|
let free = width - gaps - lineItems.reduce((sum, item) => sum + item.target + item.marginLeft + item.marginRight, 0);
|
|
@@ -902,7 +1092,10 @@ function alignGridContent(ctx, grid, alignment = "normal", free) {
|
|
|
902
1092
|
for (const placement of grid.placements) {
|
|
903
1093
|
const dy = offset(placement.row);
|
|
904
1094
|
if (!dy) continue;
|
|
905
|
-
for (let index = placement.boxStart; index < placement.boxEnd; index++)
|
|
1095
|
+
for (let index = placement.boxStart; index < placement.boxEnd; index++) {
|
|
1096
|
+
const record = ctx.boxes[index];
|
|
1097
|
+
ctx.boxes[index] = { ...record, y: record.y + dy };
|
|
1098
|
+
}
|
|
906
1099
|
for (let index = placement.fragmentStart; index < placement.fragmentEnd; index++) {
|
|
907
1100
|
ctx.fragments[index] = shiftedFragment(ctx.fragments[index], 0, dy);
|
|
908
1101
|
}
|
|
@@ -1532,7 +1725,8 @@ function resolveHorizontal(ctx, box, containingX, containingWidth, shrink, perce
|
|
|
1532
1725
|
}
|
|
1533
1726
|
|
|
1534
1727
|
function maxContentWidth(ctx, box) {
|
|
1535
|
-
const
|
|
1728
|
+
const cacheKey = box.node ?? box;
|
|
1729
|
+
const cached = ctx.maxContent.get(cacheKey);
|
|
1536
1730
|
if (cached != null) return cached;
|
|
1537
1731
|
const style = box.style;
|
|
1538
1732
|
const padding = style?.padding ?? ZERO_BOX;
|
|
@@ -1568,12 +1762,14 @@ function maxContentWidth(ctx, box) {
|
|
|
1568
1762
|
}
|
|
1569
1763
|
const maxWidth = numberOrNull(style?.maxWidth);
|
|
1570
1764
|
if (maxWidth != null) width = Math.min(width, style.boxSizing === "border-box" ? maxWidth : maxWidth + extra);
|
|
1571
|
-
|
|
1765
|
+
width = atLeastMinWidth(style, width, extra);
|
|
1766
|
+
ctx.maxContent.set(cacheKey, width);
|
|
1572
1767
|
return width;
|
|
1573
1768
|
}
|
|
1574
1769
|
|
|
1575
1770
|
function minContentWidth(ctx, box) {
|
|
1576
|
-
const
|
|
1771
|
+
const cacheKey = box.node ?? box;
|
|
1772
|
+
const cached = ctx.minContent.get(cacheKey);
|
|
1577
1773
|
if (cached != null) return cached;
|
|
1578
1774
|
const style = box.style;
|
|
1579
1775
|
const padding = style?.padding ?? ZERO_BOX;
|
|
@@ -1600,10 +1796,17 @@ function minContentWidth(ctx, box) {
|
|
|
1600
1796
|
? inlineMinContentWidth(ctx, child)
|
|
1601
1797
|
: outer(child))) + extra;
|
|
1602
1798
|
}
|
|
1603
|
-
|
|
1799
|
+
width = atLeastMinWidth(style, width, extra);
|
|
1800
|
+
ctx.minContent.set(cacheKey, width);
|
|
1604
1801
|
return width;
|
|
1605
1802
|
}
|
|
1606
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
|
+
|
|
1607
1810
|
function verticalMargins(style) {
|
|
1608
1811
|
const margin = style?.margin ?? ZERO_BOX;
|
|
1609
1812
|
return { top: numberOrZero(margin[0]), bottom: numberOrZero(margin[2]) };
|
|
@@ -1668,6 +1871,8 @@ function layoutInline(ctx, ifc, x, width, y) {
|
|
|
1668
1871
|
}
|
|
1669
1872
|
return atom;
|
|
1670
1873
|
};
|
|
1874
|
+
// Percentage sizes of inline images.
|
|
1875
|
+
atomFor.containing = ctx.containing;
|
|
1671
1876
|
const lines = breakLines(ctx, paragraph, width, atomFor);
|
|
1672
1877
|
let cursor = y;
|
|
1673
1878
|
let baseline = null;
|
|
@@ -1879,9 +2084,26 @@ function truncateForEllipsis(ctx, style, paragraph, segments, line, width) {
|
|
|
1879
2084
|
return { text, width: ellipsisWidth, run: lastText.run, metrics: textMetrics(ctx, lastText.run.style) };
|
|
1880
2085
|
}
|
|
1881
2086
|
|
|
2087
|
+
/** Space beside an inline replaced element's content: its margins, borders and padding. */
|
|
1882
2088
|
function inlineImageMargins(node) {
|
|
1883
2089
|
const margin = node.style?.margin ?? ZERO_BOX;
|
|
1884
|
-
|
|
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
|
+
};
|
|
1885
2107
|
}
|
|
1886
2108
|
|
|
1887
2109
|
/**
|
|
@@ -1929,7 +2151,8 @@ function runSliceWidth(ctx, run, start, end, text, atomFor) {
|
|
|
1929
2151
|
if (end <= start) return 0;
|
|
1930
2152
|
if (run.kind === "image") {
|
|
1931
2153
|
const margins = inlineImageMargins(run.node);
|
|
1932
|
-
|
|
2154
|
+
const containing = atomFor.containing;
|
|
2155
|
+
return imageDimensions(run.node, containing?.width ?? Infinity, containing?.height).width + margins.left + margins.right;
|
|
1933
2156
|
}
|
|
1934
2157
|
if (run.kind === "atomic") {
|
|
1935
2158
|
const atom = atomFor(run);
|
|
@@ -2009,13 +2232,20 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2009
2232
|
const strut = textMetrics(ctx, style);
|
|
2010
2233
|
let ascent = strut.ascent;
|
|
2011
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;
|
|
2012
2237
|
for (const segment of segments) {
|
|
2013
2238
|
const { run } = segment;
|
|
2014
|
-
if (run.kind === "
|
|
2015
|
-
|
|
2016
|
-
|
|
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);
|
|
2017
2247
|
ascent = Math.max(ascent, -segment.imageTop);
|
|
2018
|
-
descent = Math.max(descent, segment.imageTop +
|
|
2248
|
+
descent = Math.max(descent, segment.imageTop + outerHeight);
|
|
2019
2249
|
} else if (run.kind === "atomic") {
|
|
2020
2250
|
const atom = atomFor(run);
|
|
2021
2251
|
const atomAscent = atom.marginTop + (atom.baseline ?? atom.height) ;
|
|
@@ -2029,7 +2259,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2029
2259
|
}
|
|
2030
2260
|
}
|
|
2031
2261
|
const baselineY = y + ascent;
|
|
2032
|
-
const lineHeight = ascent + descent;
|
|
2262
|
+
const lineHeight = Math.max(ascent + descent, floatHeight);
|
|
2033
2263
|
|
|
2034
2264
|
// Horizontal alignment.
|
|
2035
2265
|
const alignment = style?.textAlign;
|
|
@@ -2042,6 +2272,22 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2042
2272
|
const edges = new Map();
|
|
2043
2273
|
const runs = [];
|
|
2044
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
|
+
};
|
|
2045
2291
|
// Right floats (approximated as atomic inlines) go to the end of the line.
|
|
2046
2292
|
const rightFloats = segments.filter((segment) => segment.run.kind === "atomic" && segment.run.style?.float === "right");
|
|
2047
2293
|
const rightFloatWidth = rightFloats.reduce((sum, segment) => sum + segment.width, 0);
|
|
@@ -2060,6 +2306,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2060
2306
|
}
|
|
2061
2307
|
}
|
|
2062
2308
|
const segmentWidth = segment.width;
|
|
2309
|
+
const shift = runOffset(run);
|
|
2063
2310
|
const common = {
|
|
2064
2311
|
nodeId: run.nodeId,
|
|
2065
2312
|
text: run.kind === "text" ? paragraph.text.slice(segment.start, segment.end) : "",
|
|
@@ -2068,11 +2315,27 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2068
2315
|
style: run.style ?? style,
|
|
2069
2316
|
};
|
|
2070
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
|
+
}
|
|
2071
2334
|
runs.push({
|
|
2072
2335
|
...common,
|
|
2073
2336
|
type: "image",
|
|
2074
|
-
x:
|
|
2075
|
-
y:
|
|
2337
|
+
x: imageX,
|
|
2338
|
+
y: imageY,
|
|
2076
2339
|
width: segment.dimensions.width,
|
|
2077
2340
|
height: segment.dimensions.height,
|
|
2078
2341
|
resource: run.node.resource,
|
|
@@ -2082,14 +2345,14 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2082
2345
|
});
|
|
2083
2346
|
} else if (run.kind === "atomic") {
|
|
2084
2347
|
const atom = atomFor(run);
|
|
2085
|
-
const atomY = baselineY - segment.atomAscent + atom.marginTop;
|
|
2086
|
-
runs.push({ ...common, type: "atomic", x: cursor, y: atomY, width: segmentWidth, height: atom.height });
|
|
2087
|
-
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 });
|
|
2088
2351
|
} else {
|
|
2089
2352
|
runs.push({
|
|
2090
2353
|
...common,
|
|
2091
|
-
x: cursor,
|
|
2092
|
-
y: baselineY - segment.metrics.contentAscent,
|
|
2354
|
+
x: cursor + shift.x,
|
|
2355
|
+
y: baselineY - segment.metrics.contentAscent + shift.y,
|
|
2093
2356
|
width: Math.max(1, segmentWidth),
|
|
2094
2357
|
height: null,
|
|
2095
2358
|
});
|
|
@@ -2138,10 +2401,11 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2138
2401
|
const metrics = textMetrics(ctx, nodeStyle);
|
|
2139
2402
|
const left = edge.left ?? edge.first;
|
|
2140
2403
|
const right = edge.right ?? edge.last;
|
|
2404
|
+
const shift = offsets.get(node) ?? ZERO_OFFSET;
|
|
2141
2405
|
ctx.boxes.push({
|
|
2142
2406
|
nodeId: node.id,
|
|
2143
|
-
x: left,
|
|
2144
|
-
y: baselineY - metrics.contentAscent - padding[0] - border[0],
|
|
2407
|
+
x: left + shift.x,
|
|
2408
|
+
y: baselineY - metrics.contentAscent - padding[0] - border[0] + shift.y,
|
|
2145
2409
|
width: right - left,
|
|
2146
2410
|
height: metrics.contentHeight + padding[0] + padding[2] + border[0] + border[2],
|
|
2147
2411
|
style: nodeStyle,
|
|
@@ -2164,6 +2428,10 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2164
2428
|
return { height: lineHeight, baseline: baselineY };
|
|
2165
2429
|
}
|
|
2166
2430
|
|
|
2431
|
+
function isFloat(style) {
|
|
2432
|
+
return style?.float === "left" || style?.float === "right";
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2167
2435
|
function measureAtomic(ctx, box, availableWidth) {
|
|
2168
2436
|
const placed = resolveHorizontal(ctx, box, 0, availableWidth, true);
|
|
2169
2437
|
const result = dryLayout(ctx, box, placed.width);
|
|
@@ -2190,7 +2458,7 @@ function textMetrics(ctx, style) {
|
|
|
2190
2458
|
|
|
2191
2459
|
function isDecorated(style) {
|
|
2192
2460
|
return Boolean(style && ((style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)")
|
|
2193
|
-
|| borderWidths(style).some((width) => width > 0)));
|
|
2461
|
+
|| style.backgroundImage || borderWidths(style).some((width) => width > 0)));
|
|
2194
2462
|
}
|
|
2195
2463
|
|
|
2196
2464
|
function borderWidths(style) {
|
|
@@ -2220,11 +2488,12 @@ function lineFragment(style, text, nodeIds, runs, x, y, width, height, baseline
|
|
|
2220
2488
|
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
2221
2489
|
|
|
2222
2490
|
function cachedMeasure(measureText) {
|
|
2223
|
-
const byStyle = new
|
|
2491
|
+
const byStyle = new WeakMap();
|
|
2492
|
+
const defaultStyle = new Map();
|
|
2224
2493
|
return (text, style) => {
|
|
2225
2494
|
if (!text) return 0;
|
|
2226
|
-
let cache = byStyle.get(style
|
|
2227
|
-
if (!cache) byStyle.set(style
|
|
2495
|
+
let cache = style && typeof style === "object" ? byStyle.get(style) : defaultStyle;
|
|
2496
|
+
if (!cache) byStyle.set(style, cache = new Map());
|
|
2228
2497
|
let width = cache.get(text);
|
|
2229
2498
|
if (width === undefined) cache.set(text, width = measureText(text, style ?? null));
|
|
2230
2499
|
return width;
|
|
@@ -2256,7 +2525,49 @@ function cachedMetrics(fontMetrics) {
|
|
|
2256
2525
|
};
|
|
2257
2526
|
}
|
|
2258
2527
|
|
|
2259
|
-
|
|
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);
|
|
2260
2571
|
const intrinsicWidth = node.resource?.width ?? node.widthAttribute ?? 300;
|
|
2261
2572
|
const intrinsicHeight = node.resource?.height ?? node.heightAttribute ?? 150;
|
|
2262
2573
|
const ratio = intrinsicWidth / intrinsicHeight;
|
|
@@ -2266,7 +2577,7 @@ function imageDimensions(node, referenceWidth) {
|
|
|
2266
2577
|
? resolveLength(cssWidth, referenceWidth)
|
|
2267
2578
|
: node.widthAttribute;
|
|
2268
2579
|
let height = cssHeight !== "auto" && cssHeight != null
|
|
2269
|
-
? resolveLength(cssHeight,
|
|
2580
|
+
? resolveLength(cssHeight, referenceHeight ?? NaN)
|
|
2270
2581
|
: node.heightAttribute;
|
|
2271
2582
|
if (!Number.isFinite(width)) width = null;
|
|
2272
2583
|
if (!Number.isFinite(height)) height = null;
|