@svelterm/core 0.1.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +465 -0
- package/README.md +42 -29
- package/dist/src/cli/build.d.ts +13 -0
- package/dist/src/cli/build.js +119 -0
- package/dist/src/cli/bundle.d.ts +25 -0
- package/dist/src/cli/bundle.js +61 -0
- package/dist/src/cli/dev.d.ts +10 -0
- package/dist/src/cli/dev.js +152 -0
- package/dist/src/cli/devtools.d.ts +9 -0
- package/dist/src/cli/devtools.js +47 -0
- package/dist/src/cli/init.d.ts +8 -0
- package/dist/src/cli/init.js +153 -0
- package/dist/src/cli/main.d.ts +9 -0
- package/dist/src/cli/main.js +52 -0
- package/dist/src/cli/svt-bin.d.ts +2 -0
- package/dist/src/cli/svt-bin.js +6 -0
- package/dist/src/cli/svt.d.ts +14 -0
- package/dist/src/cli/svt.js +76 -0
- package/dist/src/components/text-buffer.js +8 -5
- package/dist/src/css/animation-runner.d.ts +15 -6
- package/dist/src/css/animation-runner.js +80 -29
- package/dist/src/css/animation.d.ts +12 -0
- package/dist/src/css/animation.js +21 -0
- package/dist/src/css/calc.js +4 -3
- package/dist/src/css/color.d.ts +19 -0
- package/dist/src/css/color.js +371 -62
- package/dist/src/css/compute.d.ts +31 -4
- package/dist/src/css/compute.js +273 -34
- package/dist/src/css/defaults.d.ts +1 -1
- package/dist/src/css/defaults.js +9 -0
- package/dist/src/css/easing.d.ts +9 -0
- package/dist/src/css/easing.js +95 -0
- package/dist/src/css/incremental.d.ts +1 -1
- package/dist/src/css/incremental.js +2 -2
- package/dist/src/css/interpolate.d.ts +13 -0
- package/dist/src/css/interpolate.js +41 -0
- package/dist/src/css/parser.js +59 -3
- package/dist/src/css/pseudo-elements.d.ts +9 -0
- package/dist/src/css/pseudo-elements.js +97 -0
- package/dist/src/css/selector.d.ts +17 -2
- package/dist/src/css/selector.js +128 -13
- package/dist/src/css/specificity.js +17 -6
- package/dist/src/css/values.d.ts +6 -1
- package/dist/src/css/values.js +13 -6
- package/dist/src/debug/context.d.ts +13 -0
- package/dist/src/debug/context.js +11 -0
- package/dist/src/debug/css.d.ts +12 -0
- package/dist/src/debug/css.js +28 -0
- package/dist/src/debug/dom.d.ts +17 -0
- package/dist/src/debug/dom.js +92 -0
- package/dist/src/devtools/DevTools.compiled.js +327 -0
- package/dist/src/devtools/DevTools.css.js +1 -0
- package/dist/src/devtools/client.d.ts +36 -0
- package/dist/src/devtools/client.js +76 -0
- package/dist/src/framelog.d.ts +54 -0
- package/dist/src/framelog.js +99 -0
- package/dist/src/headless.js +12 -4
- package/dist/src/index.d.ts +66 -3
- package/dist/src/index.js +610 -81
- package/dist/src/input/checkable.d.ts +8 -0
- package/dist/src/input/checkable.js +66 -0
- package/dist/src/input/details.d.ts +6 -0
- package/dist/src/input/details.js +34 -0
- package/dist/src/input/focus.d.ts +6 -0
- package/dist/src/input/focus.js +27 -9
- package/dist/src/input/keyboard.d.ts +2 -2
- package/dist/src/input/keyboard.js +32 -5
- package/dist/src/input/label.d.ts +8 -0
- package/dist/src/input/label.js +53 -0
- package/dist/src/input/modal.d.ts +9 -0
- package/dist/src/input/modal.js +28 -0
- package/dist/src/input/mouse.d.ts +2 -2
- package/dist/src/input/mouse.js +15 -2
- package/dist/src/input/select.d.ts +12 -0
- package/dist/src/input/select.js +63 -0
- package/dist/src/input/selection.d.ts +48 -0
- package/dist/src/input/selection.js +150 -0
- package/dist/src/layout/engine.d.ts +2 -0
- package/dist/src/layout/engine.js +1092 -142
- package/dist/src/layout/flex.js +4 -4
- package/dist/src/layout/size.js +3 -2
- package/dist/src/layout/text.d.ts +3 -2
- package/dist/src/layout/text.js +96 -17
- package/dist/src/layout/unicode.d.ts +20 -0
- package/dist/src/layout/unicode.js +121 -0
- package/dist/src/render/animation-clock.d.ts +57 -0
- package/dist/src/render/animation-clock.js +221 -0
- package/dist/src/render/ansi-text.d.ts +26 -0
- package/dist/src/render/ansi-text.js +131 -0
- package/dist/src/render/ansi.d.ts +18 -0
- package/dist/src/render/ansi.js +64 -19
- package/dist/src/render/border.js +166 -17
- package/dist/src/render/buffer.d.ts +1 -0
- package/dist/src/render/buffer.js +5 -2
- package/dist/src/render/clock.d.ts +35 -0
- package/dist/src/render/clock.js +67 -0
- package/dist/src/render/color-depth.d.ts +8 -0
- package/dist/src/render/color-depth.js +59 -0
- package/dist/src/render/context.d.ts +1 -0
- package/dist/src/render/context.js +17 -21
- package/dist/src/render/cursor-emit.d.ts +18 -0
- package/dist/src/render/cursor-emit.js +50 -0
- package/dist/src/render/diff.d.ts +12 -0
- package/dist/src/render/diff.js +120 -0
- package/dist/src/render/generation.d.ts +9 -0
- package/dist/src/render/generation.js +14 -0
- package/dist/src/render/graphics-layer.d.ts +27 -0
- package/dist/src/render/graphics-layer.js +86 -0
- package/dist/src/render/image.d.ts +27 -0
- package/dist/src/render/image.js +113 -0
- package/dist/src/render/incremental-paint.d.ts +7 -3
- package/dist/src/render/incremental-paint.js +52 -79
- package/dist/src/render/inline.d.ts +59 -0
- package/dist/src/render/inline.js +219 -0
- package/dist/src/render/kitty-graphics.d.ts +24 -0
- package/dist/src/render/kitty-graphics.js +58 -0
- package/dist/src/render/paint-text.js +68 -22
- package/dist/src/render/paint.d.ts +8 -1
- package/dist/src/render/paint.js +358 -31
- package/dist/src/render/png.d.ts +13 -0
- package/dist/src/render/png.js +145 -0
- package/dist/src/render/scrollbar.d.ts +8 -2
- package/dist/src/render/scrollbar.js +71 -14
- package/dist/src/render/snapshot.js +3 -1
- package/dist/src/renderer/default.d.ts +7 -0
- package/dist/src/renderer/default.js +11 -0
- package/dist/src/renderer/index.d.ts +8 -2
- package/dist/src/renderer/index.js +4 -2
- package/dist/src/renderer/node.d.ts +109 -0
- package/dist/src/renderer/node.js +165 -1
- package/dist/src/terminal/capabilities.d.ts +33 -0
- package/dist/src/terminal/capabilities.js +66 -0
- package/dist/src/terminal/clipboard.d.ts +9 -0
- package/dist/src/terminal/clipboard.js +39 -0
- package/dist/src/terminal/io.d.ts +82 -0
- package/dist/src/terminal/io.js +155 -0
- package/dist/src/terminal/screen.d.ts +3 -10
- package/dist/src/terminal/screen.js +5 -28
- package/dist/src/terminal/stdin-router.d.ts +8 -5
- package/dist/src/terminal/stdin-router.js +22 -11
- package/dist/src/utils/node-map.d.ts +24 -0
- package/dist/src/utils/node-map.js +75 -0
- package/dist/src/vite/config.d.ts +62 -0
- package/dist/src/vite/config.js +191 -0
- package/docs/compatibility.md +67 -0
- package/docs/debug/devtools.md +40 -0
- package/docs/debug/svt.md +50 -0
- package/docs/distribution.md +106 -0
- package/docs/elements.md +120 -0
- package/docs/getting-started.md +177 -0
- package/docs/guide/css.md +187 -0
- package/docs/guide/input.md +143 -0
- package/docs/guide/layout.md +171 -0
- package/docs/guide/theming.md +94 -0
- package/docs/how-it-works.md +115 -0
- package/docs/inline-mode.md +77 -0
- package/docs/layout.md +112 -0
- package/docs/motion.md +91 -0
- package/docs/reference/README.md +65 -0
- package/docs/reference/css/properties/border-corner.md +82 -0
- package/docs/reference/css/properties/border-style.md +168 -0
- package/docs/reference.md +227 -0
- package/docs/selectors.md +80 -0
- package/docs/terminal-css.md +149 -0
- package/docs/terminals.md +83 -0
- package/package.json +28 -7
|
@@ -1,6 +1,80 @@
|
|
|
1
|
+
import { SvtRegionNode, childrenWithPseudos } from '../renderer/node.js';
|
|
2
|
+
import { NodeMap } from '../utils/node-map.js';
|
|
1
3
|
import { computeMainStart, computeItemGap, computeCrossOffset } from './flex.js';
|
|
2
4
|
import { measureText } from './text.js';
|
|
5
|
+
import { measureAnsiText } from '../render/ansi-text.js';
|
|
6
|
+
import { imageIntrinsicSize } from '../render/image.js';
|
|
3
7
|
import { resolveSize, constrain } from './size.js';
|
|
8
|
+
import { parseCellLength } from '../css/values.js';
|
|
9
|
+
/**
|
|
10
|
+
* Check if two adjacent siblings both have borders on their shared edge.
|
|
11
|
+
* Returns true if the gap between them should be reduced by 1 to account
|
|
12
|
+
* for the visual spacing inherent in box-drawing border characters.
|
|
13
|
+
*/
|
|
14
|
+
function shouldAdjustBorderGap(prevStyle, nextStyle, direction) {
|
|
15
|
+
if (!prevStyle || !nextStyle)
|
|
16
|
+
return false;
|
|
17
|
+
if (prevStyle.borderStyle === 'none' || nextStyle.borderStyle === 'none')
|
|
18
|
+
return false;
|
|
19
|
+
if (direction === 'vertical') {
|
|
20
|
+
return prevStyle.borderBottom && nextStyle.borderTop;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
return prevStyle.borderRight && nextStyle.borderLeft;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Approximate auto min-size in the main flex axis (CSS Flexbox §4.5).
|
|
28
|
+
* Returns the smallest size the item can be without losing essential content:
|
|
29
|
+
* borders that occupy main-axis space + (1 cell if the item has children, else 0).
|
|
30
|
+
* overflow:hidden items can collapse to 0.
|
|
31
|
+
*/
|
|
32
|
+
function autoMinMainSize(node, style, baseDir) {
|
|
33
|
+
if (!style)
|
|
34
|
+
return 0;
|
|
35
|
+
if (style.overflow === 'hidden' || style.overflow === 'scroll')
|
|
36
|
+
return 0;
|
|
37
|
+
if (node.children.length === 0)
|
|
38
|
+
return 0;
|
|
39
|
+
const hasBorder = style.borderStyle && style.borderStyle !== 'none';
|
|
40
|
+
const borderMain = hasBorder
|
|
41
|
+
? (baseDir === 'row'
|
|
42
|
+
? (style.borderLeft ? 1 : 0) + (style.borderRight ? 1 : 0)
|
|
43
|
+
: (style.borderTop ? 1 : 0) + (style.borderBottom ? 1 : 0))
|
|
44
|
+
: 0;
|
|
45
|
+
return borderMain + 1;
|
|
46
|
+
}
|
|
47
|
+
/** Table-internal display values that need an anonymous table wrapper when
|
|
48
|
+
* they appear in normal block flow (§17.2.1). */
|
|
49
|
+
function isTableInternal(node, styles) {
|
|
50
|
+
if (node.nodeType !== 'element')
|
|
51
|
+
return false;
|
|
52
|
+
const display = styles.get(node.id)?.display;
|
|
53
|
+
return display === 'table-row' || display === 'table-cell'
|
|
54
|
+
|| display === 'table-row-group' || display === 'table-header-group'
|
|
55
|
+
|| display === 'table-footer-group';
|
|
56
|
+
}
|
|
57
|
+
/** Inter-element whitespace and comments don't break a run of consecutive
|
|
58
|
+
* table-internal siblings. */
|
|
59
|
+
function absorbsIntoTableRun(node, styles) {
|
|
60
|
+
if (isTableInternal(node, styles))
|
|
61
|
+
return true;
|
|
62
|
+
if (node.nodeType === 'comment')
|
|
63
|
+
return true;
|
|
64
|
+
return node.nodeType === 'text' && (node.text ?? '').trim() === '';
|
|
65
|
+
}
|
|
66
|
+
/** Collect the run of consecutive table-internal siblings starting at
|
|
67
|
+
* `start`, returning the run and the index of its last absorbed child. */
|
|
68
|
+
function gatherTableRun(children, start, styles) {
|
|
69
|
+
const run = [];
|
|
70
|
+
let i = start;
|
|
71
|
+
while (i < children.length && absorbsIntoTableRun(children[i], styles)) {
|
|
72
|
+
if (isTableInternal(children[i], styles))
|
|
73
|
+
run.push(children[i]);
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
return { run, end: i - 1 };
|
|
77
|
+
}
|
|
4
78
|
/** Flatten display:contents elements, promoting their children. */
|
|
5
79
|
function flattenContents(children, styles) {
|
|
6
80
|
const result = [];
|
|
@@ -15,7 +89,7 @@ function flattenContents(children, styles) {
|
|
|
15
89
|
return result;
|
|
16
90
|
}
|
|
17
91
|
export function computeLayout(root, styles, availWidth, availHeight) {
|
|
18
|
-
const boxes = new
|
|
92
|
+
const boxes = new NodeMap();
|
|
19
93
|
layoutNode(root, styles, boxes, 0, 0, availWidth, availHeight);
|
|
20
94
|
return boxes;
|
|
21
95
|
}
|
|
@@ -33,19 +107,38 @@ function layoutText(node, boxes, x, y, availWidth = Infinity, styles) {
|
|
|
33
107
|
const parentStyle = node.parent ? styles?.get(node.parent.id) : undefined;
|
|
34
108
|
const preserveWhitespace = parentStyle?.whiteSpace === 'pre';
|
|
35
109
|
// Skip empty text and inter-element whitespace.
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
&& node.parent?.children.some(c => c.nodeType === 'element');
|
|
41
|
-
if (text === '' || isInterElementWhitespace) {
|
|
110
|
+
// Preserve whitespace between inline siblings (matching browser behaviour),
|
|
111
|
+
// but collapse between block-level siblings or inside flex/grid containers
|
|
112
|
+
// (where children are blockified).
|
|
113
|
+
if (text === '') {
|
|
42
114
|
boxes.set(node.id, { x, y, width: 0, height: 0 });
|
|
43
115
|
return { width: 0, height: 0 };
|
|
44
116
|
}
|
|
117
|
+
if (!preserveWhitespace && text.trim() === '' && node.parent?.children.some(c => c.nodeType === 'element')) {
|
|
118
|
+
const parentDisplay = parentStyle?.display ?? 'block';
|
|
119
|
+
const isFlexOrGrid = parentDisplay === 'flex' || parentDisplay === 'grid';
|
|
120
|
+
const hasBlockSibling = node.parent.children.some(c => {
|
|
121
|
+
if (c.nodeType !== 'element')
|
|
122
|
+
return false;
|
|
123
|
+
const d = styles?.get(c.id)?.display ?? 'block';
|
|
124
|
+
return d !== 'inline';
|
|
125
|
+
});
|
|
126
|
+
if (isFlexOrGrid || hasBlockSibling) {
|
|
127
|
+
boxes.set(node.id, { x, y, width: 0, height: 0 });
|
|
128
|
+
return { width: 0, height: 0 };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// <svt-ansi> content is pre-styled and pre-formatted: measure with
|
|
132
|
+
// escape sequences stripped, no wrapping.
|
|
133
|
+
if (node.parent?.tag === 'svt-ansi') {
|
|
134
|
+
const measured = measureAnsiText(text);
|
|
135
|
+
boxes.set(node.id, { x, y, width: measured.width, height: measured.height });
|
|
136
|
+
return measured;
|
|
137
|
+
}
|
|
45
138
|
// Check parent's whiteSpace
|
|
46
139
|
const noWrap = parentStyle?.whiteSpace === 'nowrap';
|
|
47
140
|
const wrapWidth = noWrap ? Infinity : (availWidth > 0 ? availWidth : Infinity);
|
|
48
|
-
const measured = measureText(text, wrapWidth);
|
|
141
|
+
const measured = measureText(text, wrapWidth, parentStyle?.wordBreak ?? 'normal');
|
|
49
142
|
boxes.set(node.id, { x, y, width: measured.width, height: measured.height });
|
|
50
143
|
return measured;
|
|
51
144
|
}
|
|
@@ -58,7 +151,7 @@ function layoutElement(node, styles, boxes, x, y, availWidth, availHeight) {
|
|
|
58
151
|
return { width: 0, height: 0 };
|
|
59
152
|
// display: contents — element is invisible to layout, children promoted
|
|
60
153
|
if (style?.display === 'contents') {
|
|
61
|
-
return layoutBlockFlow(node
|
|
154
|
+
return layoutBlockFlow(childrenWithPseudos(node), styles, boxes, x, y, availWidth, availHeight);
|
|
62
155
|
}
|
|
63
156
|
// Absolute positioning: use top/left offsets relative to parent, don't consume space in flow
|
|
64
157
|
if (style?.position === 'absolute' || style?.position === 'fixed') {
|
|
@@ -66,6 +159,14 @@ function layoutElement(node, styles, boxes, x, y, availWidth, availHeight) {
|
|
|
66
159
|
const absY = y + (style.top ?? 0);
|
|
67
160
|
return layoutAbsolute(node, styles, boxes, absX, absY, availWidth, availHeight, style);
|
|
68
161
|
}
|
|
162
|
+
// position: relative — a visual shift of the box and its descendants;
|
|
163
|
+
// flow position and size behave as if unshifted (the return value,
|
|
164
|
+
// which advances the flow, is untouched). left beats right, top beats
|
|
165
|
+
// bottom, per LTR CSS.
|
|
166
|
+
if (style?.position === 'relative') {
|
|
167
|
+
x += style.left ?? -(style.right ?? 0);
|
|
168
|
+
y += style.top ?? -(style.bottom ?? 0);
|
|
169
|
+
}
|
|
69
170
|
let margin = {
|
|
70
171
|
top: resolvePadding(style?.marginTop, availWidth),
|
|
71
172
|
right: resolvePadding(style?.marginRight, availWidth),
|
|
@@ -73,12 +174,22 @@ function layoutElement(node, styles, boxes, x, y, availWidth, availHeight) {
|
|
|
73
174
|
left: resolvePadding(style?.marginLeft, availWidth),
|
|
74
175
|
};
|
|
75
176
|
const borderWidth = (style?.borderStyle && style.borderStyle !== 'none') ? 1 : 0;
|
|
177
|
+
const collapsesPadding = isOuterFacingBorder(style?.borderStyle);
|
|
178
|
+
const collapsesMargin = isInnerFacingBorder(style?.borderStyle);
|
|
76
179
|
const inset = {
|
|
77
|
-
top:
|
|
78
|
-
right:
|
|
79
|
-
bottom:
|
|
80
|
-
left:
|
|
180
|
+
top: insetWithCollapse(style?.paddingTop, availWidth, style?.borderTop, borderWidth, collapsesPadding),
|
|
181
|
+
right: insetWithCollapse(style?.paddingRight, availWidth, style?.borderRight, borderWidth, collapsesPadding),
|
|
182
|
+
bottom: insetWithCollapse(style?.paddingBottom, availWidth, style?.borderBottom, borderWidth, collapsesPadding),
|
|
183
|
+
left: insetWithCollapse(style?.paddingLeft, availWidth, style?.borderLeft, borderWidth, collapsesPadding),
|
|
81
184
|
};
|
|
185
|
+
if (collapsesMargin) {
|
|
186
|
+
margin = {
|
|
187
|
+
top: collapseMargin(margin.top, style?.borderTop, borderWidth),
|
|
188
|
+
right: collapseMargin(margin.right, style?.borderRight, borderWidth),
|
|
189
|
+
bottom: collapseMargin(margin.bottom, style?.borderBottom, borderWidth),
|
|
190
|
+
left: collapseMargin(margin.left, style?.borderLeft, borderWidth),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
82
193
|
// Resolve auto margins for centering
|
|
83
194
|
const nodeWidthForAutoMargin = resolveSize(style?.width, availWidth);
|
|
84
195
|
if (margin.left === -1 && margin.right === -1 && nodeWidthForAutoMargin !== null) {
|
|
@@ -93,17 +204,39 @@ function layoutElement(node, styles, boxes, x, y, availWidth, availHeight) {
|
|
|
93
204
|
}
|
|
94
205
|
const boxX = x + margin.left;
|
|
95
206
|
const boxY = y + margin.top;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
207
|
+
// Walk past display:contents ancestors to find the actual layout parent
|
|
208
|
+
let layoutParent = node.parent;
|
|
209
|
+
while (layoutParent && styles.get(layoutParent.id)?.display === 'contents') {
|
|
210
|
+
layoutParent = layoutParent.parent;
|
|
211
|
+
}
|
|
212
|
+
const parentDisplay = layoutParent ? styles.get(layoutParent.id)?.display : undefined;
|
|
213
|
+
const isFlexOrGridChild = parentDisplay === 'flex' || parentDisplay === 'grid';
|
|
214
|
+
const isContentBox = style?.boxSizing === 'content-box';
|
|
215
|
+
let explicitWidth = resolveSize(style?.width, availWidth - margin.left - margin.right);
|
|
216
|
+
if (explicitWidth !== null && isContentBox)
|
|
217
|
+
explicitWidth += inset.left + inset.right;
|
|
218
|
+
// Flex/grid children are sized by their parent algorithm, not clamped to available width
|
|
219
|
+
const nodeWidth = explicitWidth !== null
|
|
220
|
+
? (isFlexOrGridChild ? explicitWidth : Math.min(explicitWidth, availWidth - margin.left - margin.right))
|
|
221
|
+
: null;
|
|
222
|
+
let nodeHeight = resolveSize(style?.height, availHeight - margin.top - margin.bottom);
|
|
223
|
+
if (nodeHeight !== null && isContentBox)
|
|
224
|
+
nodeHeight += inset.top + inset.bottom;
|
|
225
|
+
// Apply max-width/max-height to available space so children (e.g. flex-wrap) respect constraints
|
|
226
|
+
let effectiveW = nodeWidth ?? (availWidth - margin.left - margin.right);
|
|
227
|
+
if (style?.maxWidth != null && effectiveW > style.maxWidth)
|
|
228
|
+
effectiveW = style.maxWidth;
|
|
229
|
+
let effectiveH = nodeHeight ?? (availHeight - margin.top - margin.bottom);
|
|
230
|
+
if (style?.maxHeight != null && effectiveH > style.maxHeight)
|
|
231
|
+
effectiveH = style.maxHeight;
|
|
232
|
+
const innerW = effectiveW - inset.left - inset.right;
|
|
233
|
+
const innerH = effectiveH - inset.top - inset.bottom;
|
|
101
234
|
const display = style?.display ?? 'block';
|
|
102
235
|
let content;
|
|
103
236
|
if (display === 'flex') {
|
|
104
|
-
content = positionChildren(node
|
|
237
|
+
content = positionChildren(childrenWithPseudos(node), styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH, style?.flexDirection ?? 'column', style?.gap ?? 0, style?.justifyContent ?? 'start', style?.alignItems ?? 'start', style?.flexWrap ?? 'nowrap');
|
|
105
238
|
}
|
|
106
|
-
else if (display === 'table') {
|
|
239
|
+
else if (display === 'table' || display === 'inline-table') {
|
|
107
240
|
content = layoutTable(node, styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH);
|
|
108
241
|
}
|
|
109
242
|
else if (display === 'grid' && style) {
|
|
@@ -111,41 +244,74 @@ function layoutElement(node, styles, boxes, x, y, availWidth, availHeight) {
|
|
|
111
244
|
}
|
|
112
245
|
else {
|
|
113
246
|
// block or inline — use block flow (inline children flow horizontally within)
|
|
114
|
-
content = layoutBlockFlow(node
|
|
247
|
+
content = layoutBlockFlow(childrenWithPseudos(node), styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH);
|
|
115
248
|
}
|
|
116
249
|
// Block elements fill parent width; inline/inline-block shrink-wrap to content.
|
|
117
250
|
// Flex/grid children are sized by the flex/grid algorithm, so they shrink-wrap.
|
|
118
|
-
const parentDisplay = node.parent ? styles.get(node.parent.id)?.display : undefined;
|
|
119
|
-
const isFlexOrGridChild = parentDisplay === 'flex' || parentDisplay === 'grid';
|
|
120
251
|
const isBlock = (display === 'block' || display === 'flex' || display === 'grid' || display === 'table')
|
|
121
252
|
&& !isFlexOrGridChild;
|
|
122
|
-
|
|
253
|
+
// Paint primitives (svt-region) have no meaningful content size — they
|
|
254
|
+
// exist to fill an allocated cell area. Default to the parent's available
|
|
255
|
+
// box on both axes when no explicit dimension was given, like an
|
|
256
|
+
// intrinsically sized replaced element rather than a content-sized div.
|
|
257
|
+
const isFillPrimitive = node instanceof SvtRegionNode;
|
|
258
|
+
let autoWidth = isFillPrimitive || isBlock
|
|
123
259
|
? (availWidth - margin.left - margin.right)
|
|
124
260
|
: content.width + inset.left + inset.right;
|
|
125
|
-
//
|
|
126
|
-
|
|
261
|
+
// <img> is a replaced element: intrinsic size from its pixels
|
|
262
|
+
// (1 px per column, 2 px per row for half-block rendering)
|
|
263
|
+
const intrinsicImage = node.tag === 'img' ? imageIntrinsicSize(node) : null;
|
|
264
|
+
if (intrinsicImage) {
|
|
265
|
+
autoWidth = Math.min(intrinsicImage.width + inset.left + inset.right, availWidth - margin.left - margin.right);
|
|
266
|
+
}
|
|
267
|
+
// A select shrink-wraps to its longest option label plus the " ▾" indicator
|
|
268
|
+
if (node.tag === 'select') {
|
|
269
|
+
autoWidth = Math.max(autoWidth, longestOptionLength(node) + 2 + inset.left + inset.right);
|
|
270
|
+
}
|
|
271
|
+
// Input/textarea/select have intrinsic minimum height of 1 row
|
|
272
|
+
const intrinsicHeight = (node.tag === 'input' || node.tag === 'textarea' || node.tag === 'select')
|
|
127
273
|
? Math.max(content.height, 1)
|
|
128
274
|
: content.height;
|
|
129
|
-
const autoHeight =
|
|
275
|
+
const autoHeight = isFillPrimitive
|
|
276
|
+
? (availHeight - margin.top - margin.bottom)
|
|
277
|
+
: intrinsicImage
|
|
278
|
+
? intrinsicImage.height + inset.top + inset.bottom
|
|
279
|
+
: intrinsicHeight + inset.top + inset.bottom;
|
|
130
280
|
const finalWidth = constrain(nodeWidth ?? autoWidth, style?.minWidth, style?.maxWidth);
|
|
131
281
|
const finalHeight = constrain(nodeHeight ?? autoHeight, style?.minHeight, style?.maxHeight);
|
|
132
282
|
boxes.set(node.id, { x: boxX, y: boxY, width: finalWidth, height: finalHeight });
|
|
133
283
|
// Return outer size including margin
|
|
134
284
|
return { width: finalWidth + margin.left + margin.right, height: finalHeight + margin.top + margin.bottom };
|
|
135
285
|
}
|
|
286
|
+
function longestOptionLength(select) {
|
|
287
|
+
let longest = 0;
|
|
288
|
+
const walk = (node) => {
|
|
289
|
+
for (const child of node.children) {
|
|
290
|
+
if (child.nodeType !== 'element')
|
|
291
|
+
continue;
|
|
292
|
+
if (child.tag === 'option')
|
|
293
|
+
longest = Math.max(longest, child.textContent.trim().length);
|
|
294
|
+
else if (child.tag === 'optgroup')
|
|
295
|
+
walk(child);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
walk(select);
|
|
299
|
+
return longest;
|
|
300
|
+
}
|
|
136
301
|
function layoutAbsolute(node, styles, boxes, x, y, availWidth, availHeight, style) {
|
|
137
302
|
const borderWidth = (style.borderStyle && style.borderStyle !== 'none') ? 1 : 0;
|
|
303
|
+
const collapsesPadding = isOuterFacingBorder(style.borderStyle);
|
|
138
304
|
const inset = {
|
|
139
|
-
top:
|
|
140
|
-
right:
|
|
141
|
-
bottom:
|
|
142
|
-
left:
|
|
305
|
+
top: insetWithCollapse(style.paddingTop, availWidth, style.borderTop, borderWidth, collapsesPadding),
|
|
306
|
+
right: insetWithCollapse(style.paddingRight, availWidth, style.borderRight, borderWidth, collapsesPadding),
|
|
307
|
+
bottom: insetWithCollapse(style.paddingBottom, availWidth, style.borderBottom, borderWidth, collapsesPadding),
|
|
308
|
+
left: insetWithCollapse(style.paddingLeft, availWidth, style.borderLeft, borderWidth, collapsesPadding),
|
|
143
309
|
};
|
|
144
310
|
const nodeWidth = resolveSize(style.width, availWidth);
|
|
145
311
|
const nodeHeight = resolveSize(style.height, availHeight);
|
|
146
312
|
const innerW = (nodeWidth ?? availWidth) - inset.left - inset.right;
|
|
147
313
|
const innerH = (nodeHeight ?? availHeight) - inset.top - inset.bottom;
|
|
148
|
-
const content = positionChildren(node
|
|
314
|
+
const content = positionChildren(childrenWithPseudos(node), styles, boxes, x + inset.left, y + inset.top, innerW, innerH, style.flexDirection ?? 'column', style.gap ?? 0, style.justifyContent ?? 'start', style.alignItems ?? 'start');
|
|
149
315
|
const finalWidth = constrain(nodeWidth ?? (content.width + inset.left + inset.right), style.minWidth, style.maxWidth);
|
|
150
316
|
const finalHeight = constrain(nodeHeight ?? (content.height + inset.top + inset.bottom), style.minHeight, style.maxHeight);
|
|
151
317
|
boxes.set(node.id, { x, y, width: finalWidth, height: finalHeight });
|
|
@@ -160,6 +326,42 @@ function resolvePadding(value, availWidth) {
|
|
|
160
326
|
return value;
|
|
161
327
|
return resolveSize(value, availWidth) ?? 0;
|
|
162
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Block-character border styles whose stroke faces outward.
|
|
331
|
+
* The unused (inner) portion of the border cell collapses with `padding`.
|
|
332
|
+
*/
|
|
333
|
+
function isOuterFacingBorder(borderStyle) {
|
|
334
|
+
return borderStyle === 'eighth-cell-outer' || borderStyle === 'half-cell-outer';
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Block-character border styles whose stroke faces inward.
|
|
338
|
+
* The unused (outer) portion of the border cell collapses with `margin`.
|
|
339
|
+
*/
|
|
340
|
+
function isInnerFacingBorder(borderStyle) {
|
|
341
|
+
return borderStyle === 'eighth-cell-inner' || borderStyle === 'half-cell-inner';
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Compute total inset on one side, applying the padding/border collapse rule when applicable.
|
|
345
|
+
* When the border absorbs padding, total inset = max(padding, borderWidth).
|
|
346
|
+
* Otherwise total inset = padding + borderWidth.
|
|
347
|
+
*/
|
|
348
|
+
function insetWithCollapse(padding, availWidth, sideHasBorder, borderWidth, collapses) {
|
|
349
|
+
const p = resolvePadding(padding, availWidth);
|
|
350
|
+
const sideBorder = sideHasBorder ? borderWidth : 0;
|
|
351
|
+
if (collapses && sideHasBorder)
|
|
352
|
+
return Math.max(p, sideBorder);
|
|
353
|
+
return p + sideBorder;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Apply the margin collapse rule for inner-facing borders on a single side.
|
|
357
|
+
* When the border absorbs margin: effective margin = max(margin - 1, 0).
|
|
358
|
+
* Caller must check the side actually has a border before calling.
|
|
359
|
+
*/
|
|
360
|
+
function collapseMargin(marginValue, sideHasBorder, borderWidth) {
|
|
361
|
+
if (!sideHasBorder || marginValue <= 0)
|
|
362
|
+
return marginValue;
|
|
363
|
+
return Math.max(marginValue - borderWidth, 0);
|
|
364
|
+
}
|
|
163
365
|
function layoutBlockFlow(children, styles, boxes, x, y, availW, availH) {
|
|
164
366
|
// Layout absolute children first
|
|
165
367
|
for (const child of children) {
|
|
@@ -173,8 +375,10 @@ function layoutBlockFlow(children, styles, boxes, x, y, availW, availH) {
|
|
|
173
375
|
let lineHeight = 0;
|
|
174
376
|
let maxWidth = 0;
|
|
175
377
|
let prevBlockMarginBottom = 0;
|
|
378
|
+
let prevBlockStyle;
|
|
176
379
|
const flatChildren = flattenContents(children, styles);
|
|
177
|
-
for (
|
|
380
|
+
for (let i = 0; i < flatChildren.length; i++) {
|
|
381
|
+
const child = flatChildren[i];
|
|
178
382
|
if (child.nodeType === 'comment')
|
|
179
383
|
continue;
|
|
180
384
|
const s = styles.get(child.id);
|
|
@@ -182,7 +386,26 @@ function layoutBlockFlow(children, styles, boxes, x, y, availW, availH) {
|
|
|
182
386
|
continue;
|
|
183
387
|
if (s?.position === 'absolute' || s?.position === 'fixed')
|
|
184
388
|
continue;
|
|
185
|
-
|
|
389
|
+
// Stray table-internal content (a <tr>/<td>/row-group outside a
|
|
390
|
+
// table) wraps in an anonymous table together with its consecutive
|
|
391
|
+
// table-internal siblings (§17.2.1).
|
|
392
|
+
if (isTableInternal(child, styles)) {
|
|
393
|
+
const { run, end } = gatherTableRun(flatChildren, i, styles);
|
|
394
|
+
i = end;
|
|
395
|
+
if (cursorX > x) {
|
|
396
|
+
cursorY += lineHeight;
|
|
397
|
+
cursorX = x;
|
|
398
|
+
lineHeight = 0;
|
|
399
|
+
}
|
|
400
|
+
const size = layoutTableChildren(run, undefined, styles, boxes, x, cursorY, availW, availH - (cursorY - y));
|
|
401
|
+
cursorY += size.height;
|
|
402
|
+
maxWidth = Math.max(maxWidth, size.width);
|
|
403
|
+
prevBlockMarginBottom = 0;
|
|
404
|
+
prevBlockStyle = undefined;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const isInline = child.nodeType === 'text' || s?.display === 'inline'
|
|
408
|
+
|| s?.display === 'inline-block' || s?.display === 'inline-table';
|
|
186
409
|
if (isInline) {
|
|
187
410
|
// Flow horizontally
|
|
188
411
|
const size = layoutNode(child, styles, boxes, cursorX, cursorY, availW - (cursorX - x), availH);
|
|
@@ -205,10 +428,15 @@ function layoutBlockFlow(children, styles, boxes, x, y, availW, availH) {
|
|
|
205
428
|
const overlap = prevBlockMarginBottom + childMarginTop - collapsed;
|
|
206
429
|
cursorY -= overlap;
|
|
207
430
|
}
|
|
431
|
+
// Border collapse: adjacent bordered blocks overlap by 1
|
|
432
|
+
if (shouldAdjustBorderGap(prevBlockStyle, s, 'vertical')) {
|
|
433
|
+
cursorY -= 1;
|
|
434
|
+
}
|
|
208
435
|
const size = layoutNode(child, styles, boxes, x, cursorY, availW, availH - (cursorY - y));
|
|
209
436
|
cursorY += size.height;
|
|
210
437
|
maxWidth = Math.max(maxWidth, size.width);
|
|
211
438
|
prevBlockMarginBottom = resolvePadding(s?.marginBottom, availW);
|
|
439
|
+
prevBlockStyle = s;
|
|
212
440
|
}
|
|
213
441
|
}
|
|
214
442
|
// Account for trailing inline content
|
|
@@ -217,128 +445,632 @@ function layoutBlockFlow(children, styles, boxes, x, y, availW, availH) {
|
|
|
217
445
|
}
|
|
218
446
|
return { width: maxWidth, height: cursorY - y };
|
|
219
447
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
448
|
+
function rawColspan(cell) {
|
|
449
|
+
const raw = cell.attributes.get('colspan');
|
|
450
|
+
if (!raw)
|
|
451
|
+
return 1;
|
|
452
|
+
const n = parseInt(raw);
|
|
453
|
+
// colspan=0 means "span remaining columns"; resolved against the table's
|
|
454
|
+
// numCols in buildTableGrid.
|
|
455
|
+
if (n === 0)
|
|
456
|
+
return 0;
|
|
457
|
+
return n > 0 ? n : 1;
|
|
458
|
+
}
|
|
459
|
+
function cellRowspan(cell) {
|
|
460
|
+
const raw = cell.attributes.get('rowspan');
|
|
461
|
+
if (!raw)
|
|
462
|
+
return 1;
|
|
463
|
+
const n = parseInt(raw);
|
|
464
|
+
return n > 0 ? n : 1;
|
|
465
|
+
}
|
|
466
|
+
function buildTableGrid(rows) {
|
|
467
|
+
// First pass: numCols treating colspan=0 as 1.
|
|
468
|
+
let numCols = 0;
|
|
469
|
+
const provisional = rows.map(() => []);
|
|
470
|
+
for (let r = 0; r < rows.length; r++) {
|
|
471
|
+
let col = 0;
|
|
472
|
+
for (const cell of rows[r].cells) {
|
|
473
|
+
while (provisional[r][col])
|
|
474
|
+
col++;
|
|
475
|
+
const cs = rawColspan(cell);
|
|
476
|
+
const span = cs === 0 ? 1 : cs;
|
|
477
|
+
const rspan = cellRowspan(cell);
|
|
478
|
+
for (let dr = 1; dr < rspan && r + dr < rows.length; dr++) {
|
|
479
|
+
for (let dc = 0; dc < span; dc++)
|
|
480
|
+
provisional[r + dr][col + dc] = true;
|
|
481
|
+
}
|
|
482
|
+
col += span;
|
|
483
|
+
if (col > numCols)
|
|
484
|
+
numCols = col;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Second pass: rebuild grid with colspan=0 expanded to fill remaining columns.
|
|
488
|
+
const occupied = rows.map(() => []);
|
|
489
|
+
const span = new Map();
|
|
490
|
+
for (let r = 0; r < rows.length; r++) {
|
|
491
|
+
let col = 0;
|
|
492
|
+
for (const cell of rows[r].cells) {
|
|
493
|
+
while (occupied[r][col])
|
|
494
|
+
col++;
|
|
495
|
+
const cs = rawColspan(cell);
|
|
496
|
+
const resolved = cs === 0 ? Math.max(1, numCols - col) : cs;
|
|
497
|
+
span.set(cell.id, resolved);
|
|
498
|
+
const rspan = cellRowspan(cell);
|
|
499
|
+
for (let dr = 1; dr < rspan && r + dr < rows.length; dr++) {
|
|
500
|
+
for (let dc = 0; dc < resolved; dc++)
|
|
501
|
+
occupied[r + dr][col + dc] = true;
|
|
502
|
+
}
|
|
503
|
+
col += resolved;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return { occupied, numCols, span };
|
|
507
|
+
}
|
|
508
|
+
function cellColspan(grid, cell) {
|
|
509
|
+
return grid.span.get(cell.id) ?? 1;
|
|
510
|
+
}
|
|
511
|
+
/** True for content that acts as a table cell inside a row: real cells, plus
|
|
512
|
+
* stray text / elements that get an anonymous cell box per §17.2.1. */
|
|
513
|
+
function isCellContent(node, styles) {
|
|
514
|
+
if (node.nodeType === 'text')
|
|
515
|
+
return (node.text ?? '').trim() !== '';
|
|
516
|
+
if (node.nodeType !== 'element')
|
|
517
|
+
return false;
|
|
518
|
+
const display = styles.get(node.id)?.display;
|
|
519
|
+
return display !== 'none' && !isRowLevelDisplay(display);
|
|
520
|
+
}
|
|
521
|
+
function isRowLevelDisplay(display) {
|
|
522
|
+
return display === 'table-row'
|
|
523
|
+
|| display === 'table-row-group' || display === 'table-header-group'
|
|
524
|
+
|| display === 'table-footer-group'
|
|
525
|
+
|| display === 'table-caption' || display === 'table-column'
|
|
526
|
+
|| display === 'table-column-group';
|
|
527
|
+
}
|
|
528
|
+
function cellsOfRow(trNode, styles) {
|
|
529
|
+
return trNode.children.filter(c => isCellContent(c, styles));
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Group the children of a table or row-group into rows. Explicit table-rows
|
|
533
|
+
* keep their cells; a run of consecutive stray cell-content children forms
|
|
534
|
+
* one anonymous row (§17.2.1). Simplification vs the spec: each stray child
|
|
535
|
+
* is its own anonymous cell rather than coalescing consecutive inline
|
|
536
|
+
* content into one.
|
|
537
|
+
*/
|
|
538
|
+
function groupIntoRows(children, styles) {
|
|
222
539
|
const rows = [];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
rows.push(cells);
|
|
540
|
+
let anonymous = [];
|
|
541
|
+
const flushAnonymous = () => {
|
|
542
|
+
if (anonymous.length > 0) {
|
|
543
|
+
rows.push({ trNode: null, cells: anonymous });
|
|
544
|
+
anonymous = [];
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
for (const child of children) {
|
|
548
|
+
if (child.nodeType === 'element' && styles.get(child.id)?.display === 'table-row') {
|
|
549
|
+
flushAnonymous();
|
|
550
|
+
rows.push({ trNode: child, cells: cellsOfRow(child, styles) });
|
|
551
|
+
}
|
|
552
|
+
else if (isCellContent(child, styles)) {
|
|
553
|
+
anonymous.push(child);
|
|
227
554
|
}
|
|
228
555
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
556
|
+
flushAnonymous();
|
|
557
|
+
return rows;
|
|
558
|
+
}
|
|
559
|
+
function collectTableRows(children, styles) {
|
|
560
|
+
// Per CSS 2.2 §17.5.2: header → bodies (in source order) → footer.
|
|
561
|
+
// Bare <tr> (or stray cell content) children of <table> form an implicit
|
|
562
|
+
// body, mirroring the browser HTML parser's auto-tbody insertion.
|
|
563
|
+
const headerRows = [];
|
|
564
|
+
const bodyRows = [];
|
|
565
|
+
const footerRows = [];
|
|
566
|
+
let strayRun = [];
|
|
567
|
+
const flushStray = () => {
|
|
568
|
+
if (strayRun.length > 0) {
|
|
569
|
+
bodyRows.push(...groupIntoRows(strayRun, styles));
|
|
570
|
+
strayRun = [];
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
for (const child of children) {
|
|
574
|
+
const display = child.nodeType === 'element' ? styles.get(child.id)?.display : undefined;
|
|
575
|
+
if (display === 'table-header-group') {
|
|
576
|
+
flushStray();
|
|
577
|
+
headerRows.push(...groupIntoRows(child.children, styles));
|
|
578
|
+
}
|
|
579
|
+
else if (display === 'table-footer-group') {
|
|
580
|
+
flushStray();
|
|
581
|
+
footerRows.push(...groupIntoRows(child.children, styles));
|
|
582
|
+
}
|
|
583
|
+
else if (display === 'table-row-group') {
|
|
584
|
+
flushStray();
|
|
585
|
+
bodyRows.push(...groupIntoRows(child.children, styles));
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
// table-rows and stray cell content accumulate; captions/columns
|
|
589
|
+
// are filtered out inside groupIntoRows.
|
|
590
|
+
strayRun.push(child);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
flushStray();
|
|
594
|
+
return [...headerRows, ...bodyRows, ...footerRows];
|
|
595
|
+
}
|
|
596
|
+
function findCaption(children, styles) {
|
|
597
|
+
return children.find(c => styles.get(c.id)?.display === 'table-caption');
|
|
598
|
+
}
|
|
599
|
+
function colSpanAttr(colNode) {
|
|
600
|
+
const raw = colNode.attributes.get('span');
|
|
601
|
+
if (!raw)
|
|
602
|
+
return 1;
|
|
603
|
+
const n = parseInt(raw);
|
|
604
|
+
return n > 0 ? n : 1;
|
|
605
|
+
}
|
|
606
|
+
function explicitWidth(node, styles) {
|
|
607
|
+
const w = styles.get(node.id)?.width;
|
|
608
|
+
return typeof w === 'number' && w > 0 ? w : 0;
|
|
609
|
+
}
|
|
610
|
+
function applyColHint(hints, col, span, width) {
|
|
611
|
+
if (width <= 0)
|
|
612
|
+
return;
|
|
613
|
+
for (let i = 0; i < span; i++) {
|
|
614
|
+
hints[col + i] = Math.max(hints[col + i] ?? 0, width);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function collectColHints(children, styles) {
|
|
618
|
+
// Walks <col> and <colgroup> in document order; returns per-column width
|
|
619
|
+
// hints (sparse). <col span> covers multiple columns; a <colgroup> with no
|
|
620
|
+
// <col> children covers `span` columns itself.
|
|
621
|
+
const hints = [];
|
|
622
|
+
let col = 0;
|
|
623
|
+
for (const child of children) {
|
|
624
|
+
const display = styles.get(child.id)?.display;
|
|
625
|
+
if (display === 'table-column') {
|
|
626
|
+
const span = colSpanAttr(child);
|
|
627
|
+
applyColHint(hints, col, span, explicitWidth(child, styles));
|
|
628
|
+
col += span;
|
|
629
|
+
}
|
|
630
|
+
else if (display === 'table-column-group') {
|
|
631
|
+
const colChildren = child.children.filter(c => styles.get(c.id)?.display === 'table-column');
|
|
632
|
+
if (colChildren.length === 0) {
|
|
633
|
+
const span = colSpanAttr(child);
|
|
634
|
+
applyColHint(hints, col, span, explicitWidth(child, styles));
|
|
635
|
+
col += span;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
for (const colNode of colChildren) {
|
|
639
|
+
const span = colSpanAttr(colNode);
|
|
640
|
+
applyColHint(hints, col, span, explicitWidth(colNode, styles));
|
|
641
|
+
col += span;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return hints;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Horizontal/vertical gap between table tracks. The separate model uses
|
|
650
|
+
* border-spacing; the collapsed model overlaps tracks by one cell where
|
|
651
|
+
* adjacent border strokes would otherwise double up, so they coincide and
|
|
652
|
+
* merge into shared grid lines. A track boundary only collapses when cells
|
|
653
|
+
* are bordered on both of its sides (e.g. left+right for columns) — one-sided
|
|
654
|
+
* borders such as row separators already draw a single line and need no overlap.
|
|
655
|
+
*/
|
|
656
|
+
function tableGaps(tableStyle, rows, styles) {
|
|
657
|
+
if (tableStyle?.borderCollapse === 'collapse') {
|
|
658
|
+
return {
|
|
659
|
+
col: allCellsBordered(rows, styles, 'borderLeft', 'borderRight') ? -1 : 0,
|
|
660
|
+
row: allCellsBordered(rows, styles, 'borderTop', 'borderBottom') ? -1 : 0,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
return { col: tableStyle?.borderSpacingH ?? 0, row: tableStyle?.borderSpacingV ?? 0 };
|
|
664
|
+
}
|
|
665
|
+
function allCellsBordered(rows, styles, ...sides) {
|
|
666
|
+
return rows.every(row => row.cells.every(cell => {
|
|
667
|
+
const style = styles.get(cell.id);
|
|
668
|
+
return style !== undefined && style.borderStyle !== 'none'
|
|
669
|
+
&& sides.every(side => style[side]);
|
|
670
|
+
}));
|
|
671
|
+
}
|
|
672
|
+
function measureColumnWidths(rows, grid, styles, boxes, availW, availH, mode = 'auto') {
|
|
673
|
+
const colWidths = new Array(grid.numCols).fill(0);
|
|
674
|
+
// Only single-column cells contribute directly to column widths in this
|
|
675
|
+
// pass; colspan>1 cells are positioned later using the resolved widths.
|
|
676
|
+
// table-layout: fixed only consults the first row.
|
|
677
|
+
const lastRow = mode === 'fixed' ? Math.min(1, rows.length) : rows.length;
|
|
678
|
+
for (let r = 0; r < lastRow; r++) {
|
|
679
|
+
let col = 0;
|
|
680
|
+
for (const cell of rows[r].cells) {
|
|
681
|
+
while (grid.occupied[r][col])
|
|
682
|
+
col++;
|
|
683
|
+
const span = cellColspan(grid, cell);
|
|
684
|
+
if (span === 1) {
|
|
685
|
+
const size = layoutNode(cell, styles, boxes, 0, 0, availW, availH);
|
|
686
|
+
colWidths[col] = Math.max(colWidths[col], size.width);
|
|
687
|
+
}
|
|
688
|
+
col += span;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return colWidths;
|
|
692
|
+
}
|
|
693
|
+
function placeCaption(caption, styles, boxes, x, y, tableWidth, availH) {
|
|
694
|
+
const size = layoutNode(caption, styles, boxes, x, y, tableWidth, availH);
|
|
695
|
+
const captionBox = boxes.get(caption.id);
|
|
696
|
+
if (captionBox)
|
|
697
|
+
captionBox.width = tableWidth;
|
|
698
|
+
return size.height;
|
|
699
|
+
}
|
|
700
|
+
function spannedWidth(colWidths, col, span, colGap) {
|
|
701
|
+
let w = 0;
|
|
702
|
+
for (let i = 0; i < span; i++)
|
|
703
|
+
w += colWidths[col + i] ?? 0;
|
|
704
|
+
return w + colGap * Math.max(0, span - 1);
|
|
705
|
+
}
|
|
706
|
+
function shiftSubtreeY(node, boxes, dy) {
|
|
707
|
+
for (const child of node.children) {
|
|
708
|
+
const box = boxes.get(child.id);
|
|
709
|
+
if (box)
|
|
710
|
+
box.y += dy;
|
|
711
|
+
shiftSubtreeY(child, boxes, dy);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function applyVerticalAlign(cell, totalHeight, contentHeight, styles, boxes) {
|
|
715
|
+
const cellBox = boxes.get(cell.id);
|
|
716
|
+
if (cellBox)
|
|
717
|
+
cellBox.height = totalHeight;
|
|
718
|
+
const slack = totalHeight - contentHeight;
|
|
719
|
+
if (slack <= 0)
|
|
720
|
+
return;
|
|
721
|
+
const va = styles.get(cell.id)?.verticalAlign ?? 'top';
|
|
722
|
+
if (va === 'top')
|
|
723
|
+
return;
|
|
724
|
+
const dy = va === 'middle' ? Math.floor(slack / 2) : slack;
|
|
725
|
+
shiftSubtreeY(cell, boxes, dy);
|
|
726
|
+
}
|
|
727
|
+
function placeRows(rows, grid, styles, boxes, x, startY, colWidths, tableWidth, availH, gaps) {
|
|
728
|
+
const rowHeights = [];
|
|
729
|
+
const placed = [];
|
|
730
|
+
// Pass 1: lay out each cell, accumulate per-row height from non-rowspan cells.
|
|
731
|
+
let rowY = startY;
|
|
732
|
+
for (let r = 0; r < rows.length; r++) {
|
|
733
|
+
const { trNode, cells } = rows[r];
|
|
734
|
+
let col = 0;
|
|
246
735
|
let colX = x;
|
|
247
736
|
let rowHeight = 0;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
737
|
+
for (const cell of cells) {
|
|
738
|
+
while (grid.occupied[r][col]) {
|
|
739
|
+
colX += colWidths[col] + gaps.col;
|
|
740
|
+
col++;
|
|
741
|
+
}
|
|
742
|
+
const span = cellColspan(grid, cell);
|
|
743
|
+
const rspan = cellRowspan(cell);
|
|
744
|
+
const cellWidth = spannedWidth(colWidths, col, span, gaps.col);
|
|
745
|
+
const size = layoutNode(cell, styles, boxes, colX, rowY, cellWidth, availH);
|
|
254
746
|
const cellBox = boxes.get(cell.id);
|
|
255
|
-
if (cellBox && cellBox.width <
|
|
256
|
-
cellBox.width =
|
|
257
|
-
|
|
258
|
-
|
|
747
|
+
if (cellBox && cellBox.width < cellWidth)
|
|
748
|
+
cellBox.width = cellWidth;
|
|
749
|
+
if (rspan === 1)
|
|
750
|
+
rowHeight = Math.max(rowHeight, size.height);
|
|
751
|
+
placed.push({ cell, rowIdx: r, rspan, contentHeight: size.height });
|
|
752
|
+
colX += cellWidth + gaps.col;
|
|
753
|
+
col += span;
|
|
259
754
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
755
|
+
const trMinHeight = trNode ? styles.get(trNode.id)?.height : undefined;
|
|
756
|
+
if (typeof trMinHeight === 'number' && trMinHeight > rowHeight)
|
|
757
|
+
rowHeight = trMinHeight;
|
|
758
|
+
rowHeights.push(rowHeight);
|
|
759
|
+
if (trNode)
|
|
760
|
+
boxes.set(trNode.id, { x, y: rowY, width: tableWidth, height: rowHeight });
|
|
761
|
+
rowY += rowHeight + gaps.row;
|
|
762
|
+
}
|
|
763
|
+
// Pass 2: stretch each cell to its row's total height and apply vertical-align.
|
|
764
|
+
for (const { cell, rowIdx, rspan, contentHeight } of placed) {
|
|
765
|
+
let totalH = 0;
|
|
766
|
+
let spanRows = 0;
|
|
767
|
+
for (let r = 0; r < rspan && rowIdx + r < rows.length; r++) {
|
|
768
|
+
totalH += rowHeights[rowIdx + r];
|
|
769
|
+
spanRows++;
|
|
263
770
|
}
|
|
264
|
-
|
|
771
|
+
totalH += gaps.row * Math.max(0, spanRows - 1);
|
|
772
|
+
applyVerticalAlign(cell, totalH, contentHeight, styles, boxes);
|
|
773
|
+
}
|
|
774
|
+
return rowY - startY - (rows.length > 0 ? gaps.row : 0);
|
|
775
|
+
}
|
|
776
|
+
function layoutTable(node, styles, boxes, x, y, availW, availH) {
|
|
777
|
+
return layoutTableChildren(node.children, styles.get(node.id), styles, boxes, x, y, availW, availH);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Table layout over a list of children. Called with a table element's
|
|
781
|
+
* children and style, or — for anonymous tables wrapped around stray
|
|
782
|
+
* table-internal content (§17.2.1) — with the run of stray siblings and no
|
|
783
|
+
* style (anonymous tables get initial values, e.g. border-spacing 0).
|
|
784
|
+
*/
|
|
785
|
+
function layoutTableChildren(children, tableStyle, styles, boxes, x, y, availW, availH) {
|
|
786
|
+
const rows = collectTableRows(children, styles);
|
|
787
|
+
const caption = findCaption(children, styles);
|
|
788
|
+
if (rows.length === 0 && !caption)
|
|
789
|
+
return { width: 0, height: 0 };
|
|
790
|
+
const grid = buildTableGrid(rows);
|
|
791
|
+
const gaps = tableGaps(tableStyle, rows, styles);
|
|
792
|
+
const colWidths = measureColumnWidths(rows, grid, styles, boxes, availW, availH, tableStyle?.tableLayout ?? 'auto');
|
|
793
|
+
// <col>/<colgroup> widths act as a minimum (auto) or as the source of
|
|
794
|
+
// truth (fixed) for the column.
|
|
795
|
+
const colHints = collectColHints(children, styles);
|
|
796
|
+
for (let i = 0; i < colWidths.length; i++) {
|
|
797
|
+
if (colHints[i] !== undefined)
|
|
798
|
+
colWidths[i] = Math.max(colWidths[i], colHints[i]);
|
|
799
|
+
}
|
|
800
|
+
const colsWidth = colWidths.reduce((sum, w) => sum + w, 0)
|
|
801
|
+
+ gaps.col * Math.max(0, colWidths.length - 1);
|
|
802
|
+
// Measure caption against availW so the table can grow to fit it.
|
|
803
|
+
let captionWidth = 0;
|
|
804
|
+
if (caption) {
|
|
805
|
+
const size = layoutNode(caption, styles, boxes, 0, 0, availW, availH);
|
|
806
|
+
captionWidth = size.width;
|
|
807
|
+
}
|
|
808
|
+
const tableWidth = Math.max(colsWidth, captionWidth);
|
|
809
|
+
const captionSide = caption ? styles.get(caption.id)?.captionSide ?? 'top' : 'top';
|
|
810
|
+
let rowY = y;
|
|
811
|
+
if (caption && captionSide === 'top') {
|
|
812
|
+
rowY += placeCaption(caption, styles, boxes, x, rowY, tableWidth, availH);
|
|
813
|
+
}
|
|
814
|
+
rowY += placeRows(rows, grid, styles, boxes, x, rowY, colWidths, tableWidth, availH, gaps);
|
|
815
|
+
if (caption && captionSide === 'bottom') {
|
|
816
|
+
rowY += placeCaption(caption, styles, boxes, x, rowY, tableWidth, availH);
|
|
265
817
|
}
|
|
266
|
-
|
|
267
|
-
return { width: totalWidth, height: rowY - y };
|
|
818
|
+
return { width: tableWidth, height: rowY - y };
|
|
268
819
|
}
|
|
269
820
|
function layoutGrid(node, styles, boxes, x, y, availW, availH, style) {
|
|
270
|
-
const children = node.
|
|
821
|
+
const children = childrenWithPseudos(node).filter(c => c.nodeType === 'element' && styles.get(c.id)?.display !== 'none');
|
|
271
822
|
if (children.length === 0)
|
|
272
823
|
return { width: 0, height: 0 };
|
|
273
|
-
const
|
|
824
|
+
const areas = style.gridTemplateAreas ? parseTemplateAreas(style.gridTemplateAreas) : null;
|
|
825
|
+
let colWidths = parseGridTemplate(style.gridTemplateColumns ?? '', availW);
|
|
826
|
+
if (colWidths.length === 0 && areas && areas.columnCount > 0) {
|
|
827
|
+
// Areas without a column template: split the width evenly
|
|
828
|
+
colWidths = Array(areas.columnCount).fill(Math.floor(availW / areas.columnCount));
|
|
829
|
+
}
|
|
274
830
|
const rowHeights = parseGridTemplate(style.gridTemplateRows ?? '', availH);
|
|
275
831
|
const numCols = colWidths.length || 1;
|
|
276
832
|
const gap = style.gap ?? 0;
|
|
277
|
-
|
|
278
|
-
let
|
|
279
|
-
let
|
|
280
|
-
|
|
281
|
-
|
|
833
|
+
// Pre-compute border-adjusted gaps for grid children
|
|
834
|
+
let hGap = gap;
|
|
835
|
+
let vGap = gap;
|
|
836
|
+
if (children.length >= 2) {
|
|
837
|
+
// Check first two adjacent children for horizontal collapse
|
|
838
|
+
if (numCols >= 2 && shouldAdjustBorderGap(styles.get(children[0].id), styles.get(children[1].id), 'horizontal')) {
|
|
839
|
+
hGap = Math.max(-1, gap - 1);
|
|
840
|
+
}
|
|
841
|
+
// Check first child and first child of second row for vertical collapse
|
|
842
|
+
if (children.length > numCols && shouldAdjustBorderGap(styles.get(children[0].id), styles.get(children[numCols].id), 'vertical')) {
|
|
843
|
+
vGap = Math.max(-1, gap - 1);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// Pass 1: assign each child to a row/col and compute content-based row heights
|
|
847
|
+
const placements = [];
|
|
848
|
+
const computedRowHeights = [];
|
|
849
|
+
const cursor = { col: 0, row: 0 };
|
|
282
850
|
for (const child of children) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
851
|
+
const childStyle = styles.get(child.id);
|
|
852
|
+
const area = childStyle?.gridArea ? areas?.byName.get(childStyle.gridArea) : undefined;
|
|
853
|
+
const placed = resolveGridPlacement(childStyle, area, cursor, numCols);
|
|
854
|
+
const colW = trackSpanSize(colWidths, placed.col, placed.span, hGap);
|
|
855
|
+
// Measure content height with unconstrained available height
|
|
856
|
+
const size = layoutNode(child, styles, boxes, 0, 0, colW, availH);
|
|
857
|
+
placements.push({ child, ...placed });
|
|
858
|
+
// Spanning content doesn't stretch individual tracks (matches columns)
|
|
859
|
+
if (placed.rowSpan === 1) {
|
|
860
|
+
computedRowHeights[placed.row] = Math.max(computedRowHeights[placed.row] ?? 0, size.height);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
// Resolve final row track heights: template first, content otherwise
|
|
864
|
+
const totalRows = placements.reduce((max, p) => Math.max(max, p.row + p.rowSpan), 0);
|
|
865
|
+
const trackHeights = [];
|
|
866
|
+
for (let r = 0; r < totalRows; r++) {
|
|
867
|
+
trackHeights[r] = rowHeights[r] ?? computedRowHeights[r] ?? 0;
|
|
868
|
+
}
|
|
869
|
+
// Pass 2: layout each child at its final position
|
|
870
|
+
let maxWidth = 0;
|
|
871
|
+
for (const { child, col, span, row, rowSpan } of placements) {
|
|
872
|
+
const colX = x + trackOffset(colWidths, col, hGap);
|
|
873
|
+
const colW = trackSpanSize(colWidths, col, span, hGap);
|
|
874
|
+
const rowY = y + trackOffset(trackHeights, row, vGap);
|
|
875
|
+
const rh = trackSpanSize(trackHeights, row, rowSpan, vGap);
|
|
876
|
+
layoutNode(child, styles, boxes, colX, rowY, colW, rh);
|
|
295
877
|
const childBox = boxes.get(child.id);
|
|
296
|
-
if (childBox
|
|
878
|
+
if (childBox) {
|
|
297
879
|
childBox.width = colW;
|
|
298
|
-
|
|
880
|
+
childBox.height = rh;
|
|
881
|
+
}
|
|
299
882
|
maxWidth = Math.max(maxWidth, colX - x + colW);
|
|
300
|
-
col++;
|
|
301
883
|
}
|
|
302
|
-
const
|
|
303
|
-
return { width: maxWidth, height:
|
|
884
|
+
const totalHeight = totalRows === 0 ? 0 : trackOffset(trackHeights, totalRows, vGap) - vGap;
|
|
885
|
+
return { width: maxWidth, height: totalHeight };
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Where one grid item lands: a named area wins outright; otherwise
|
|
889
|
+
* explicit lines/spans combine with the auto-flow cursor, which only
|
|
890
|
+
* auto-placed and column-placed items advance.
|
|
891
|
+
*/
|
|
892
|
+
function resolveGridPlacement(childStyle, area, cursor, numCols) {
|
|
893
|
+
if (area) {
|
|
894
|
+
return {
|
|
895
|
+
col: area.colStart, span: area.colEnd - area.colStart,
|
|
896
|
+
row: area.rowStart, rowSpan: area.rowEnd - area.rowStart,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
const span = childStyle?.gridColumnSpan ?? 1;
|
|
900
|
+
if (cursor.col >= numCols) {
|
|
901
|
+
cursor.col = 0;
|
|
902
|
+
cursor.row++;
|
|
903
|
+
}
|
|
904
|
+
const colStart = resolveGridColumnStart(childStyle, cursor.col, numCols);
|
|
905
|
+
const colEnd = resolveGridColumnEnd(childStyle, colStart, span, numCols);
|
|
906
|
+
const actualSpan = colEnd - colStart;
|
|
907
|
+
if (colStart > cursor.col)
|
|
908
|
+
cursor.col = colStart;
|
|
909
|
+
if (cursor.col + actualSpan > numCols) {
|
|
910
|
+
cursor.col = 0;
|
|
911
|
+
cursor.row++;
|
|
912
|
+
}
|
|
913
|
+
const row = childStyle?.gridRowStart != null ? childStyle.gridRowStart - 1 : cursor.row;
|
|
914
|
+
const rowSpan = childStyle?.gridRowEnd != null
|
|
915
|
+
? Math.max(1, childStyle.gridRowEnd - 1 - row)
|
|
916
|
+
: (childStyle?.gridRowSpan ?? 1);
|
|
917
|
+
const col = cursor.col;
|
|
918
|
+
cursor.col += actualSpan;
|
|
919
|
+
return { col, span: actualSpan, row, rowSpan };
|
|
304
920
|
}
|
|
305
|
-
|
|
921
|
+
/**
|
|
922
|
+
* Parse the quoted rows of grid-template-areas into per-name rectangles.
|
|
923
|
+
* `.` cells are holes; a name repeated across cells spans their extent.
|
|
924
|
+
*/
|
|
925
|
+
function parseTemplateAreas(value) {
|
|
926
|
+
const byName = new Map();
|
|
927
|
+
let columnCount = 0;
|
|
928
|
+
const rows = [...value.matchAll(/"([^"]*)"|'([^']*)'/g)].map(m => (m[1] ?? m[2] ?? '').trim());
|
|
929
|
+
rows.forEach((rowText, rowIndex) => {
|
|
930
|
+
const names = rowText.split(/\s+/);
|
|
931
|
+
columnCount = Math.max(columnCount, names.length);
|
|
932
|
+
names.forEach((name, colIndex) => {
|
|
933
|
+
if (name === '.' || name === '')
|
|
934
|
+
return;
|
|
935
|
+
const area = byName.get(name);
|
|
936
|
+
if (!area) {
|
|
937
|
+
byName.set(name, { rowStart: rowIndex, rowEnd: rowIndex + 1, colStart: colIndex, colEnd: colIndex + 1 });
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
area.rowStart = Math.min(area.rowStart, rowIndex);
|
|
941
|
+
area.rowEnd = Math.max(area.rowEnd, rowIndex + 1);
|
|
942
|
+
area.colStart = Math.min(area.colStart, colIndex);
|
|
943
|
+
area.colEnd = Math.max(area.colEnd, colIndex + 1);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
return { byName, columnCount };
|
|
948
|
+
}
|
|
949
|
+
/** Resolve the start column for a grid item (0-indexed) */
|
|
950
|
+
function resolveGridColumnStart(style, autoCol, numCols) {
|
|
951
|
+
if (!style)
|
|
952
|
+
return autoCol;
|
|
953
|
+
if (style.gridColumnStart != null)
|
|
954
|
+
return style.gridColumnStart - 1; // CSS lines are 1-indexed
|
|
955
|
+
return autoCol;
|
|
956
|
+
}
|
|
957
|
+
/** Resolve the end column for a grid item (0-indexed, exclusive) */
|
|
958
|
+
function resolveGridColumnEnd(style, start, span, numCols) {
|
|
959
|
+
if (!style)
|
|
960
|
+
return start + span;
|
|
961
|
+
if (style.gridColumnEnd != null)
|
|
962
|
+
return style.gridColumnEnd - 1; // CSS lines are 1-indexed
|
|
963
|
+
return start + span;
|
|
964
|
+
}
|
|
965
|
+
/** Offset to the start of a grid track (works for either axis) */
|
|
966
|
+
function trackOffset(trackSizes, track, gap) {
|
|
967
|
+
let offset = 0;
|
|
968
|
+
for (let i = 0; i < track; i++) {
|
|
969
|
+
offset += (trackSizes[i] ?? 0) + gap;
|
|
970
|
+
}
|
|
971
|
+
return offset;
|
|
972
|
+
}
|
|
973
|
+
/** Total size spanning multiple grid tracks including gaps between them */
|
|
974
|
+
function trackSpanSize(trackSizes, startTrack, span, gap) {
|
|
975
|
+
let size = 0;
|
|
976
|
+
for (let i = startTrack; i < startTrack + span && i < trackSizes.length; i++) {
|
|
977
|
+
if (i > startTrack)
|
|
978
|
+
size += gap;
|
|
979
|
+
size += trackSizes[i];
|
|
980
|
+
}
|
|
981
|
+
return size;
|
|
982
|
+
}
|
|
983
|
+
function parseGridTemplate(template, availSize) {
|
|
306
984
|
if (!template)
|
|
307
985
|
return [];
|
|
308
|
-
|
|
309
|
-
const
|
|
986
|
+
// Expand repeat() before splitting
|
|
987
|
+
const expanded = expandRepeat(template);
|
|
988
|
+
return resolveTrackSizes(splitTracks(expanded), availSize);
|
|
989
|
+
}
|
|
990
|
+
/** Split a track list on whitespace, keeping function arguments (minmax(a, b)) together */
|
|
991
|
+
function splitTracks(input) {
|
|
992
|
+
const tracks = [];
|
|
993
|
+
let current = '';
|
|
994
|
+
let depth = 0;
|
|
995
|
+
for (const ch of input.trim()) {
|
|
996
|
+
if (ch === '(')
|
|
997
|
+
depth++;
|
|
998
|
+
if (ch === ')')
|
|
999
|
+
depth--;
|
|
1000
|
+
if (/\s/.test(ch) && depth === 0) {
|
|
1001
|
+
if (current) {
|
|
1002
|
+
tracks.push(current);
|
|
1003
|
+
current = '';
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
current += ch;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (current)
|
|
1011
|
+
tracks.push(current);
|
|
1012
|
+
return tracks;
|
|
1013
|
+
}
|
|
1014
|
+
/** Expand repeat(N, tracks...) into flat track list */
|
|
1015
|
+
function expandRepeat(template) {
|
|
1016
|
+
return template.replace(/repeat\(\s*(\d+)\s*,\s*([^)]+)\)/g, (_match, countStr, tracks) => {
|
|
1017
|
+
const count = parseInt(countStr);
|
|
1018
|
+
const trackList = tracks.trim();
|
|
1019
|
+
return Array(count).fill(trackList).join(' ');
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
function resolveTrackSizes(parts, availSize) {
|
|
1023
|
+
const sizes = [];
|
|
310
1024
|
const frParts = [];
|
|
311
1025
|
let fixedTotal = 0;
|
|
312
1026
|
for (let i = 0; i < parts.length; i++) {
|
|
313
1027
|
const part = parts[i];
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
1028
|
+
const minmax = /^minmax\(([^,]+),(.+)\)$/.exec(part);
|
|
1029
|
+
if (minmax) {
|
|
1030
|
+
const min = resolveTrackLength(minmax[1].trim(), availSize) ?? 0;
|
|
1031
|
+
const maxRaw = minmax[2].trim();
|
|
1032
|
+
if (maxRaw.endsWith('fr')) {
|
|
1033
|
+
sizes.push(0); // placeholder
|
|
1034
|
+
frParts.push({ index: i, fr: parseFloat(maxRaw), min });
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
const w = Math.max(min, resolveTrackLength(maxRaw, availSize) ?? 0);
|
|
1038
|
+
sizes.push(w);
|
|
1039
|
+
fixedTotal += w;
|
|
1040
|
+
}
|
|
1041
|
+
continue;
|
|
318
1042
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
fixedTotal +=
|
|
1043
|
+
const fixed = resolveTrackLength(part, availSize);
|
|
1044
|
+
if (fixed !== null) {
|
|
1045
|
+
sizes.push(fixed);
|
|
1046
|
+
fixedTotal += fixed;
|
|
323
1047
|
}
|
|
324
1048
|
else if (part.endsWith('fr')) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
frParts.push({ index: i, fr });
|
|
1049
|
+
sizes.push(0); // placeholder
|
|
1050
|
+
frParts.push({ index: i, fr: parseFloat(part), min: 0 });
|
|
328
1051
|
}
|
|
329
1052
|
else {
|
|
330
|
-
|
|
1053
|
+
sizes.push(0);
|
|
331
1054
|
}
|
|
332
1055
|
}
|
|
333
|
-
// Distribute remaining space to fr units
|
|
1056
|
+
// Distribute remaining space to fr units, honouring minmax() minimums
|
|
334
1057
|
if (frParts.length > 0) {
|
|
335
1058
|
const totalFr = frParts.reduce((sum, p) => sum + p.fr, 0);
|
|
336
|
-
const remaining = Math.max(0,
|
|
337
|
-
for (const { index, fr } of frParts) {
|
|
338
|
-
|
|
1059
|
+
const remaining = Math.max(0, availSize - fixedTotal);
|
|
1060
|
+
for (const { index, fr, min } of frParts) {
|
|
1061
|
+
sizes[index] = Math.max(min, Math.floor(remaining * fr / totalFr));
|
|
339
1062
|
}
|
|
340
1063
|
}
|
|
341
|
-
return
|
|
1064
|
+
return sizes;
|
|
1065
|
+
}
|
|
1066
|
+
/** A fixed track length in cells (cell/ch or %), or null for fr/auto/keywords */
|
|
1067
|
+
function resolveTrackLength(part, availSize) {
|
|
1068
|
+
const cellLength = parseCellLength(part);
|
|
1069
|
+
if (cellLength !== null)
|
|
1070
|
+
return Math.round(cellLength);
|
|
1071
|
+
if (part.endsWith('%'))
|
|
1072
|
+
return Math.floor(availSize * parseFloat(part) / 100);
|
|
1073
|
+
return null;
|
|
342
1074
|
}
|
|
343
1075
|
function positionChildren(children, styles, boxes, innerX, innerY, innerW, innerH, dir, gap, justify, align, wrap = 'nowrap') {
|
|
344
1076
|
// Layout absolute children first (they don't affect flow)
|
|
@@ -362,6 +1094,8 @@ function positionChildren(children, styles, boxes, innerX, innerY, innerW, inner
|
|
|
362
1094
|
});
|
|
363
1095
|
if (visible.length === 0)
|
|
364
1096
|
return { width: 0, height: 0 };
|
|
1097
|
+
const isReverse = dir === 'row-reverse' || dir === 'column-reverse';
|
|
1098
|
+
const baseDir = (dir === 'row' || dir === 'row-reverse') ? 'row' : 'column';
|
|
365
1099
|
// Pre-measure to filter out zero-size items (e.g. whitespace text nodes)
|
|
366
1100
|
const measured = visible.map(child => ({
|
|
367
1101
|
child,
|
|
@@ -376,8 +1110,6 @@ function positionChildren(children, styles, boxes, innerX, innerY, innerW, inner
|
|
|
376
1110
|
const orderB = styles.get(b.child.id)?.order ?? 0;
|
|
377
1111
|
return orderA - orderB;
|
|
378
1112
|
});
|
|
379
|
-
const isReverse = dir === 'row-reverse' || dir === 'column-reverse';
|
|
380
|
-
const baseDir = (dir === 'row' || dir === 'row-reverse') ? 'row' : 'column';
|
|
381
1113
|
const orderedItems = isReverse ? sorted.reverse() : sorted;
|
|
382
1114
|
const ordered = orderedItems.map(item => item.child);
|
|
383
1115
|
// Use pre-measured sizes, overridden by flex-basis when set
|
|
@@ -395,61 +1127,279 @@ function positionChildren(children, styles, boxes, innerX, innerY, innerW, inner
|
|
|
395
1127
|
const growValues = ordered.map(child => styles.get(child.id)?.flexGrow ?? 0);
|
|
396
1128
|
const shrinkValues = ordered.map(child => styles.get(child.id)?.flexShrink ?? 1);
|
|
397
1129
|
const totalGrow = growValues.reduce((a, b) => a + b, 0);
|
|
1130
|
+
// Compute per-pair gap, adjusting for border collapse
|
|
1131
|
+
const borderDir = baseDir === 'column' ? 'vertical' : 'horizontal';
|
|
1132
|
+
const pairGaps = ordered.map((child, i) => {
|
|
1133
|
+
if (i === 0)
|
|
1134
|
+
return 0;
|
|
1135
|
+
const adjust = shouldAdjustBorderGap(styles.get(ordered[i - 1].id), styles.get(child.id), borderDir) ? 1 : 0;
|
|
1136
|
+
return Math.max(-1, gap - adjust);
|
|
1137
|
+
});
|
|
398
1138
|
const totalMain = sizes.reduce((sum, s, i) => {
|
|
399
|
-
return sum + (baseDir === 'row' ? s.width : s.height) +
|
|
1139
|
+
return sum + (baseDir === 'row' ? s.width : s.height) + pairGaps[i];
|
|
400
1140
|
}, 0);
|
|
401
1141
|
const availMain = baseDir === 'row' ? innerW : innerH;
|
|
402
|
-
const
|
|
403
|
-
const
|
|
1142
|
+
const rawFreeSpace = availMain - totalMain;
|
|
1143
|
+
const freeSpace = Math.max(0, rawFreeSpace);
|
|
1144
|
+
// With wrapping enabled, items wrap instead of shrinking
|
|
1145
|
+
const overflow = wrap === 'wrap' ? 0 : Math.max(0, -rawFreeSpace);
|
|
404
1146
|
const hasGrow = totalGrow > 0;
|
|
405
1147
|
const totalShrink = overflow > 0 ? shrinkValues.reduce((a, b) => a + b, 0) : 0;
|
|
1148
|
+
// Pre-compute grow/shrink adjustments with correct rounding
|
|
1149
|
+
const mainAdjust = new Array(ordered.length).fill(0);
|
|
1150
|
+
if (hasGrow && freeSpace > 0) {
|
|
1151
|
+
let distributed = 0;
|
|
1152
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
1153
|
+
if (growValues[i] > 0) {
|
|
1154
|
+
const share = Math.floor(freeSpace * growValues[i] / totalGrow);
|
|
1155
|
+
mainAdjust[i] = share;
|
|
1156
|
+
distributed += share;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
// Distribute remainder 1px each to items with largest fractional parts
|
|
1160
|
+
let remainder = freeSpace - distributed;
|
|
1161
|
+
if (remainder > 0) {
|
|
1162
|
+
const fractions = ordered.map((_, i) => growValues[i] > 0 ? (freeSpace * growValues[i] / totalGrow) % 1 : 0);
|
|
1163
|
+
const indices = ordered.map((_, i) => i)
|
|
1164
|
+
.filter(i => growValues[i] > 0)
|
|
1165
|
+
.sort((a, b) => fractions[b] - fractions[a]);
|
|
1166
|
+
for (const idx of indices) {
|
|
1167
|
+
if (remainder <= 0)
|
|
1168
|
+
break;
|
|
1169
|
+
mainAdjust[idx] += 1;
|
|
1170
|
+
remainder--;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (overflow > 0 && totalShrink > 0) {
|
|
1175
|
+
let distributed = 0;
|
|
1176
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
1177
|
+
if (shrinkValues[i] > 0) {
|
|
1178
|
+
const childStyle = styles.get(ordered[i].id);
|
|
1179
|
+
const explicitMain = baseDir === 'row' ? childStyle?.width : childStyle?.height;
|
|
1180
|
+
if (explicitMain != null) {
|
|
1181
|
+
const share = Math.floor(overflow * shrinkValues[i] / totalShrink);
|
|
1182
|
+
mainAdjust[i] = -share;
|
|
1183
|
+
distributed += share;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
// Distribute remainder to last shrinking item with explicit size
|
|
1188
|
+
let remainder = overflow - distributed;
|
|
1189
|
+
for (let i = ordered.length - 1; i >= 0 && remainder > 0; i--) {
|
|
1190
|
+
if (shrinkValues[i] > 0) {
|
|
1191
|
+
const childStyle = styles.get(ordered[i].id);
|
|
1192
|
+
const explicitMain = baseDir === 'row' ? childStyle?.width : childStyle?.height;
|
|
1193
|
+
if (explicitMain != null) {
|
|
1194
|
+
mainAdjust[i] -= remainder;
|
|
1195
|
+
remainder = 0;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
// Apply min-width/min-height constraints to shrink adjustments.
|
|
1201
|
+
// CSS Flexbox §4.5: items have auto min-size = min(content-size, specified-size).
|
|
1202
|
+
// Approximate content-min as: borders in main axis + (1 if has children, else 0).
|
|
1203
|
+
// overflow:hidden allows min to be 0 (per spec).
|
|
1204
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
1205
|
+
if (mainAdjust[i] < 0) {
|
|
1206
|
+
const baseSize = baseDir === 'row' ? sizes[i].width : sizes[i].height;
|
|
1207
|
+
const childStyle = styles.get(ordered[i].id);
|
|
1208
|
+
const minMain = baseDir === 'row' ? childStyle?.minWidth : childStyle?.minHeight;
|
|
1209
|
+
if (minMain != null) {
|
|
1210
|
+
const adjusted = baseSize + mainAdjust[i];
|
|
1211
|
+
if (adjusted < minMain)
|
|
1212
|
+
mainAdjust[i] = minMain - baseSize;
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
const autoMin = autoMinMainSize(ordered[i], childStyle, baseDir);
|
|
1216
|
+
if (baseSize + mainAdjust[i] < autoMin)
|
|
1217
|
+
mainAdjust[i] = autoMin - baseSize;
|
|
1218
|
+
}
|
|
1219
|
+
// Never shrink below 0
|
|
1220
|
+
if (baseSize + mainAdjust[i] < 0)
|
|
1221
|
+
mainAdjust[i] = -baseSize;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
// Apply max-width/max-height constraints to grow adjustments
|
|
1225
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
1226
|
+
if (mainAdjust[i] > 0) {
|
|
1227
|
+
const baseSize = baseDir === 'row' ? sizes[i].width : sizes[i].height;
|
|
1228
|
+
const childStyle = styles.get(ordered[i].id);
|
|
1229
|
+
const maxMain = baseDir === 'row' ? childStyle?.maxWidth : childStyle?.maxHeight;
|
|
1230
|
+
if (maxMain != null) {
|
|
1231
|
+
const adjusted = baseSize + mainAdjust[i];
|
|
1232
|
+
if (adjusted > maxMain) {
|
|
1233
|
+
const excess = adjusted - maxMain;
|
|
1234
|
+
mainAdjust[i] -= excess;
|
|
1235
|
+
// Redistribute excess to other growing items
|
|
1236
|
+
for (let j = 0; j < ordered.length; j++) {
|
|
1237
|
+
if (j !== i && growValues[j] > 0) {
|
|
1238
|
+
mainAdjust[j] += excess;
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
// §9.8.1 Auto margins absorb free space before justify-content
|
|
1247
|
+
const autoMargins = ordered.map(() => ({ before: 0, after: 0 }));
|
|
1248
|
+
if (freeSpace > 0 && !hasGrow) {
|
|
1249
|
+
const marginProp = baseDir === 'row'
|
|
1250
|
+
? { before: 'marginLeft', after: 'marginRight' }
|
|
1251
|
+
: { before: 'marginTop', after: 'marginBottom' };
|
|
1252
|
+
let autoCount = 0;
|
|
1253
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
1254
|
+
const s = styles.get(ordered[i].id);
|
|
1255
|
+
if (s && s[marginProp.before] === -1)
|
|
1256
|
+
autoCount++;
|
|
1257
|
+
if (s && s[marginProp.after] === -1)
|
|
1258
|
+
autoCount++;
|
|
1259
|
+
}
|
|
1260
|
+
if (autoCount > 0) {
|
|
1261
|
+
const perAuto = Math.floor(freeSpace / autoCount);
|
|
1262
|
+
let distributed = 0;
|
|
1263
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
1264
|
+
const s = styles.get(ordered[i].id);
|
|
1265
|
+
if (s && s[marginProp.before] === -1) {
|
|
1266
|
+
autoMargins[i].before = perAuto;
|
|
1267
|
+
distributed += perAuto;
|
|
1268
|
+
}
|
|
1269
|
+
if (s && s[marginProp.after] === -1) {
|
|
1270
|
+
autoMargins[i].after = perAuto;
|
|
1271
|
+
distributed += perAuto;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
// Distribute remainder to last auto margin
|
|
1275
|
+
let remainder = freeSpace - distributed;
|
|
1276
|
+
for (let i = ordered.length - 1; i >= 0 && remainder > 0; i--) {
|
|
1277
|
+
const s = styles.get(ordered[i].id);
|
|
1278
|
+
if (s && s[marginProp.after] === -1) {
|
|
1279
|
+
autoMargins[i].after += remainder;
|
|
1280
|
+
remainder = 0;
|
|
1281
|
+
}
|
|
1282
|
+
else if (s && s[marginProp.before] === -1) {
|
|
1283
|
+
autoMargins[i].before += remainder;
|
|
1284
|
+
remainder = 0;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
const hasAutoMargins = autoMargins.some(m => m.before > 0 || m.after > 0);
|
|
1290
|
+
// For wrapping, first determine line breaks and per-line cross sizes
|
|
1291
|
+
const itemLine = []; // which line each item is on
|
|
1292
|
+
const lineHeights = []; // cross size per line
|
|
1293
|
+
if (wrap === 'wrap') {
|
|
1294
|
+
let lineMainPos = 0;
|
|
1295
|
+
let currentLine = 0;
|
|
1296
|
+
let currentLineHeight = 0;
|
|
1297
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
1298
|
+
const contentMainSize = baseDir === 'row' ? sizes[i].width : sizes[i].height;
|
|
1299
|
+
if (lineMainPos + contentMainSize > availMain && i > 0) {
|
|
1300
|
+
lineHeights.push(currentLineHeight);
|
|
1301
|
+
currentLine++;
|
|
1302
|
+
lineMainPos = 0;
|
|
1303
|
+
currentLineHeight = 0;
|
|
1304
|
+
}
|
|
1305
|
+
itemLine.push(currentLine);
|
|
1306
|
+
const crossSize = baseDir === 'row' ? sizes[i].height : sizes[i].width;
|
|
1307
|
+
currentLineHeight = Math.max(currentLineHeight, crossSize);
|
|
1308
|
+
lineMainPos += contentMainSize + (i < ordered.length - 1 ? pairGaps[i + 1] : 0);
|
|
1309
|
+
}
|
|
1310
|
+
lineHeights.push(currentLineHeight);
|
|
1311
|
+
}
|
|
406
1312
|
// Position
|
|
407
|
-
let mainPos = computeMainStart(justify,
|
|
408
|
-
const
|
|
1313
|
+
let mainPos = hasAutoMargins ? 0 : computeMainStart(justify, rawFreeSpace, ordered.length, hasGrow);
|
|
1314
|
+
const baseItemGap = hasAutoMargins ? gap : computeItemGap(justify, gap, freeSpace, ordered.length, hasGrow);
|
|
409
1315
|
let contentWidth = 0;
|
|
410
1316
|
let contentHeight = 0;
|
|
411
1317
|
let crossPos = 0;
|
|
412
1318
|
let lineHeight = 0;
|
|
1319
|
+
let currentLine = 0;
|
|
1320
|
+
let naturalMain = 0;
|
|
413
1321
|
for (let i = 0; i < ordered.length; i++) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
if (overflow > 0 && totalShrink > 0 && shrinkValues[i] > 0) {
|
|
419
|
-
mainSize -= Math.floor(overflow * shrinkValues[i] / totalShrink);
|
|
420
|
-
mainSize = Math.max(0, mainSize);
|
|
421
|
-
}
|
|
1322
|
+
mainPos += autoMargins[i].before;
|
|
1323
|
+
let mainSize = (baseDir === 'row' ? sizes[i].width : sizes[i].height) + mainAdjust[i];
|
|
1324
|
+
mainSize = Math.max(0, mainSize);
|
|
422
1325
|
// Wrap check
|
|
423
|
-
|
|
424
|
-
|
|
1326
|
+
const contentMainSize = baseDir === 'row' ? sizes[i].width : sizes[i].height;
|
|
1327
|
+
if (wrap === 'wrap' && mainPos + contentMainSize > availMain && i > 0) {
|
|
1328
|
+
// Border collapse between wrap lines: reduce gap when adjacent
|
|
1329
|
+
// items have borders on the shared edge
|
|
1330
|
+
const crossDir = baseDir === 'row' ? 'vertical' : 'horizontal';
|
|
1331
|
+
const prevLineItem = ordered[i - 1];
|
|
1332
|
+
const prevStyle = styles.get(prevLineItem.id);
|
|
1333
|
+
const curStyle = styles.get(ordered[i].id);
|
|
1334
|
+
const collapseGap = shouldAdjustBorderGap(prevStyle, curStyle, crossDir) ? 1 : 0;
|
|
1335
|
+
crossPos += lineHeight + Math.max(-1, gap - collapseGap);
|
|
425
1336
|
mainPos = 0;
|
|
426
1337
|
lineHeight = 0;
|
|
1338
|
+
currentLine++;
|
|
427
1339
|
}
|
|
428
1340
|
const crossSize = baseDir === 'row' ? sizes[i].height : sizes[i].width;
|
|
429
|
-
|
|
430
|
-
|
|
1341
|
+
// In wrap mode, cross available is the line height; otherwise full container
|
|
1342
|
+
const lineCrossSize = wrap === 'wrap' ? lineHeights[currentLine] : 0;
|
|
1343
|
+
const crossAvail = wrap === 'wrap' ? lineCrossSize : (baseDir === 'row' ? innerH : innerW);
|
|
431
1344
|
const childStyle = styles.get(ordered[i].id);
|
|
432
1345
|
const selfAlign = childStyle?.alignSelf !== 'auto'
|
|
433
1346
|
? childStyle?.alignSelf ?? align
|
|
434
1347
|
: align;
|
|
435
|
-
|
|
1348
|
+
// Stretch only applies to items whose cross-axis size is auto (§8.3);
|
|
1349
|
+
// an explicit width/height wins and the item aligns to the line start.
|
|
1350
|
+
const crossSizeIsAuto = baseDir === 'row'
|
|
1351
|
+
? childStyle?.height == null
|
|
1352
|
+
: childStyle?.width == null;
|
|
1353
|
+
const isStretch = selfAlign === 'stretch' && crossSizeIsAuto;
|
|
1354
|
+
const crossOffset = isStretch ? 0 : computeCrossOffset(selfAlign, crossAvail, crossSize);
|
|
436
1355
|
const finalCx = baseDir === 'row' ? innerX + mainPos : innerX + crossOffset;
|
|
437
1356
|
const finalCy = baseDir === 'row' ? innerY + crossPos + crossOffset : innerY + mainPos;
|
|
438
1357
|
const childAvailW = baseDir === 'row' ? mainSize : innerW;
|
|
439
|
-
const childAvailH = baseDir === 'row' ? innerH : mainSize;
|
|
1358
|
+
const childAvailH = baseDir === 'row' ? (isStretch ? crossAvail : innerH) : mainSize;
|
|
440
1359
|
layoutNode(ordered[i], styles, boxes, finalCx, finalCy, childAvailW, childAvailH);
|
|
441
|
-
// Override main-axis size for flex-grown/shrunk items
|
|
442
1360
|
const box = boxes.get(ordered[i].id);
|
|
443
1361
|
if (box) {
|
|
444
1362
|
if (baseDir === 'row' && box.width !== mainSize)
|
|
445
1363
|
box.width = mainSize;
|
|
446
1364
|
if (baseDir === 'column' && box.height !== mainSize)
|
|
447
1365
|
box.height = mainSize;
|
|
1366
|
+
if (isStretch) {
|
|
1367
|
+
if (baseDir === 'row' && box.height < crossAvail) {
|
|
1368
|
+
const stretchH = constrain(crossAvail, childStyle?.minHeight, childStyle?.maxHeight);
|
|
1369
|
+
box.height = stretchH;
|
|
1370
|
+
layoutNode(ordered[i], styles, boxes, finalCx, finalCy, mainSize, stretchH);
|
|
1371
|
+
const rebox = boxes.get(ordered[i].id);
|
|
1372
|
+
if (rebox) {
|
|
1373
|
+
rebox.width = mainSize;
|
|
1374
|
+
rebox.height = stretchH;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (baseDir === 'column' && box.width < crossAvail) {
|
|
1378
|
+
const stretchW = constrain(crossAvail, childStyle?.minWidth, childStyle?.maxWidth);
|
|
1379
|
+
box.width = stretchW;
|
|
1380
|
+
layoutNode(ordered[i], styles, boxes, finalCx, finalCy, stretchW, mainSize);
|
|
1381
|
+
const rebox = boxes.get(ordered[i].id);
|
|
1382
|
+
if (rebox) {
|
|
1383
|
+
rebox.width = stretchW;
|
|
1384
|
+
rebox.height = mainSize;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
448
1388
|
}
|
|
449
|
-
lineHeight = Math.max(lineHeight,
|
|
450
|
-
|
|
1389
|
+
lineHeight = Math.max(lineHeight, crossSize);
|
|
1390
|
+
const usesJustifySpacing = justify === 'space-between' || justify === 'space-around' || justify === 'space-evenly';
|
|
1391
|
+
const pairItemGap = i < ordered.length - 1
|
|
1392
|
+
? (usesJustifySpacing && !hasGrow ? baseItemGap : pairGaps[i + 1])
|
|
1393
|
+
: 0;
|
|
1394
|
+
mainPos += mainSize + autoMargins[i].after + pairItemGap;
|
|
1395
|
+
naturalMain += mainSize + (i > 0 ? pairGaps[i] : 0);
|
|
451
1396
|
contentWidth = baseDir === 'row' ? Math.max(contentWidth, mainPos) : Math.max(contentWidth, sizes[i].width);
|
|
452
1397
|
contentHeight = baseDir === 'row' ? crossPos + lineHeight : mainPos;
|
|
453
1398
|
}
|
|
454
|
-
|
|
1399
|
+
// Return natural size for shrink-wrap auto-sizing (no justify offset).
|
|
1400
|
+
// For wrapping containers, use positioned extent (wrapping already happened).
|
|
1401
|
+
const mainResult = wrap === 'wrap' ? (baseDir === 'row' ? contentWidth : contentHeight) : naturalMain;
|
|
1402
|
+
return baseDir === 'row'
|
|
1403
|
+
? { width: mainResult, height: contentHeight }
|
|
1404
|
+
: { width: contentWidth, height: mainResult };
|
|
455
1405
|
}
|