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