@svelterm/core 0.1.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/src/components/spinner.d.ts +11 -0
  4. package/dist/src/components/spinner.js +19 -0
  5. package/dist/src/components/text-buffer.d.ts +21 -0
  6. package/dist/src/components/text-buffer.js +87 -0
  7. package/dist/src/css/animation-runner.d.ts +17 -0
  8. package/dist/src/css/animation-runner.js +72 -0
  9. package/dist/src/css/animation.d.ts +5 -0
  10. package/dist/src/css/animation.js +6 -0
  11. package/dist/src/css/calc.d.ts +5 -0
  12. package/dist/src/css/calc.js +130 -0
  13. package/dist/src/css/color.d.ts +1 -0
  14. package/dist/src/css/color.js +157 -0
  15. package/dist/src/css/compute.d.ts +63 -0
  16. package/dist/src/css/compute.js +606 -0
  17. package/dist/src/css/defaults.d.ts +8 -0
  18. package/dist/src/css/defaults.js +44 -0
  19. package/dist/src/css/incremental.d.ts +9 -0
  20. package/dist/src/css/incremental.js +46 -0
  21. package/dist/src/css/index.d.ts +5 -0
  22. package/dist/src/css/index.js +3 -0
  23. package/dist/src/css/media.d.ts +11 -0
  24. package/dist/src/css/media.js +59 -0
  25. package/dist/src/css/parser.d.ts +20 -0
  26. package/dist/src/css/parser.js +241 -0
  27. package/dist/src/css/selector.d.ts +17 -0
  28. package/dist/src/css/selector.js +272 -0
  29. package/dist/src/css/specificity.d.ts +7 -0
  30. package/dist/src/css/specificity.js +89 -0
  31. package/dist/src/css/values.d.ts +17 -0
  32. package/dist/src/css/values.js +58 -0
  33. package/dist/src/css/variables.d.ts +6 -0
  34. package/dist/src/css/variables.js +42 -0
  35. package/dist/src/debug/console.d.ts +16 -0
  36. package/dist/src/debug/console.js +65 -0
  37. package/dist/src/debug/server.d.ts +22 -0
  38. package/dist/src/debug/server.js +90 -0
  39. package/dist/src/headless.d.ts +21 -0
  40. package/dist/src/headless.js +26 -0
  41. package/dist/src/index.d.ts +18 -0
  42. package/dist/src/index.js +485 -0
  43. package/dist/src/input/dispatch.d.ts +18 -0
  44. package/dist/src/input/dispatch.js +70 -0
  45. package/dist/src/input/focus.d.ts +18 -0
  46. package/dist/src/input/focus.js +81 -0
  47. package/dist/src/input/hit.d.ts +3 -0
  48. package/dist/src/input/hit.js +29 -0
  49. package/dist/src/input/keyboard.d.ts +9 -0
  50. package/dist/src/input/keyboard.js +100 -0
  51. package/dist/src/input/mouse.d.ts +7 -0
  52. package/dist/src/input/mouse.js +35 -0
  53. package/dist/src/input/scroll.d.ts +2 -0
  54. package/dist/src/input/scroll.js +24 -0
  55. package/dist/src/layout/cache.d.ts +4 -0
  56. package/dist/src/layout/cache.js +8 -0
  57. package/dist/src/layout/engine.d.ts +9 -0
  58. package/dist/src/layout/engine.js +455 -0
  59. package/dist/src/layout/flex.d.ts +4 -0
  60. package/dist/src/layout/flex.js +30 -0
  61. package/dist/src/layout/incremental.d.ts +8 -0
  62. package/dist/src/layout/incremental.js +58 -0
  63. package/dist/src/layout/size.d.ts +2 -0
  64. package/dist/src/layout/size.js +25 -0
  65. package/dist/src/layout/text.d.ts +7 -0
  66. package/dist/src/layout/text.js +52 -0
  67. package/dist/src/render/ansi.d.ts +23 -0
  68. package/dist/src/render/ansi.js +108 -0
  69. package/dist/src/render/border.d.ts +4 -0
  70. package/dist/src/render/border.js +60 -0
  71. package/dist/src/render/buffer.d.ts +23 -0
  72. package/dist/src/render/buffer.js +70 -0
  73. package/dist/src/render/context.d.ts +19 -0
  74. package/dist/src/render/context.js +98 -0
  75. package/dist/src/render/diff.d.ts +2 -0
  76. package/dist/src/render/diff.js +53 -0
  77. package/dist/src/render/incremental-paint.d.ts +10 -0
  78. package/dist/src/render/incremental-paint.js +94 -0
  79. package/dist/src/render/paint-text.d.ts +29 -0
  80. package/dist/src/render/paint-text.js +120 -0
  81. package/dist/src/render/paint.d.ts +5 -0
  82. package/dist/src/render/paint.js +220 -0
  83. package/dist/src/render/queue.d.ts +24 -0
  84. package/dist/src/render/queue.js +54 -0
  85. package/dist/src/render/scrollbar.d.ts +3 -0
  86. package/dist/src/render/scrollbar.js +19 -0
  87. package/dist/src/render/snapshot.d.ts +18 -0
  88. package/dist/src/render/snapshot.js +126 -0
  89. package/dist/src/renderer/default.d.ts +3 -0
  90. package/dist/src/renderer/default.js +3 -0
  91. package/dist/src/renderer/index.d.ts +11 -0
  92. package/dist/src/renderer/index.js +116 -0
  93. package/dist/src/renderer/node.d.ts +44 -0
  94. package/dist/src/renderer/node.js +153 -0
  95. package/dist/src/terminal/screen.d.ts +10 -0
  96. package/dist/src/terminal/screen.js +31 -0
  97. package/dist/src/terminal/stdin-router.d.ts +31 -0
  98. package/dist/src/terminal/stdin-router.js +133 -0
  99. package/package.json +64 -0
@@ -0,0 +1,455 @@
1
+ import { computeMainStart, computeItemGap, computeCrossOffset } from './flex.js';
2
+ import { measureText } from './text.js';
3
+ import { resolveSize, constrain } from './size.js';
4
+ /** Flatten display:contents elements, promoting their children. */
5
+ function flattenContents(children, styles) {
6
+ const result = [];
7
+ for (const child of children) {
8
+ if (child.nodeType === 'element' && styles.get(child.id)?.display === 'contents') {
9
+ result.push(...flattenContents(child.children, styles));
10
+ }
11
+ else {
12
+ result.push(child);
13
+ }
14
+ }
15
+ return result;
16
+ }
17
+ export function computeLayout(root, styles, availWidth, availHeight) {
18
+ const boxes = new Map();
19
+ layoutNode(root, styles, boxes, 0, 0, availWidth, availHeight);
20
+ return boxes;
21
+ }
22
+ function layoutNode(node, styles, boxes, x, y, availWidth, availHeight) {
23
+ if (node.nodeType === 'text')
24
+ return layoutText(node, boxes, x, y, availWidth, styles);
25
+ if (node.nodeType === 'comment')
26
+ return { width: 0, height: 0 };
27
+ if (node.nodeType === 'fragment')
28
+ return layoutFragment(node, styles, boxes, x, y, availWidth, availHeight);
29
+ return layoutElement(node, styles, boxes, x, y, availWidth, availHeight);
30
+ }
31
+ function layoutText(node, boxes, x, y, availWidth = Infinity, styles) {
32
+ const text = node.text ?? '';
33
+ const parentStyle = node.parent ? styles?.get(node.parent.id) : undefined;
34
+ const preserveWhitespace = parentStyle?.whiteSpace === 'pre';
35
+ // Skip empty text and inter-element whitespace.
36
+ // Whitespace-only text between element siblings is formatting, not content.
37
+ // Preserve whitespace-only text that is the sole child (e.g. grid rows of spaces).
38
+ const isInterElementWhitespace = !preserveWhitespace
39
+ && text.trim() === ''
40
+ && node.parent?.children.some(c => c.nodeType === 'element');
41
+ if (text === '' || isInterElementWhitespace) {
42
+ boxes.set(node.id, { x, y, width: 0, height: 0 });
43
+ return { width: 0, height: 0 };
44
+ }
45
+ // Check parent's whiteSpace
46
+ const noWrap = parentStyle?.whiteSpace === 'nowrap';
47
+ const wrapWidth = noWrap ? Infinity : (availWidth > 0 ? availWidth : Infinity);
48
+ const measured = measureText(text, wrapWidth);
49
+ boxes.set(node.id, { x, y, width: measured.width, height: measured.height });
50
+ return measured;
51
+ }
52
+ function layoutFragment(node, styles, boxes, x, y, availWidth, availHeight) {
53
+ return layoutBlockFlow(node.children, styles, boxes, x, y, availWidth, availHeight);
54
+ }
55
+ function layoutElement(node, styles, boxes, x, y, availWidth, availHeight) {
56
+ const style = styles.get(node.id);
57
+ if (style?.display === 'none')
58
+ return { width: 0, height: 0 };
59
+ // display: contents — element is invisible to layout, children promoted
60
+ if (style?.display === 'contents') {
61
+ return layoutBlockFlow(node.children, styles, boxes, x, y, availWidth, availHeight);
62
+ }
63
+ // Absolute positioning: use top/left offsets relative to parent, don't consume space in flow
64
+ if (style?.position === 'absolute' || style?.position === 'fixed') {
65
+ const absX = x + (style.left ?? 0);
66
+ const absY = y + (style.top ?? 0);
67
+ return layoutAbsolute(node, styles, boxes, absX, absY, availWidth, availHeight, style);
68
+ }
69
+ let margin = {
70
+ top: resolvePadding(style?.marginTop, availWidth),
71
+ right: resolvePadding(style?.marginRight, availWidth),
72
+ bottom: resolvePadding(style?.marginBottom, availWidth),
73
+ left: resolvePadding(style?.marginLeft, availWidth),
74
+ };
75
+ const borderWidth = (style?.borderStyle && style.borderStyle !== 'none') ? 1 : 0;
76
+ const inset = {
77
+ top: resolvePadding(style?.paddingTop, availWidth) + borderWidth,
78
+ right: resolvePadding(style?.paddingRight, availWidth) + borderWidth,
79
+ bottom: resolvePadding(style?.paddingBottom, availWidth) + borderWidth,
80
+ left: resolvePadding(style?.paddingLeft, availWidth) + borderWidth,
81
+ };
82
+ // Resolve auto margins for centering
83
+ const nodeWidthForAutoMargin = resolveSize(style?.width, availWidth);
84
+ if (margin.left === -1 && margin.right === -1 && nodeWidthForAutoMargin !== null) {
85
+ const remaining = availWidth - nodeWidthForAutoMargin;
86
+ margin = { ...margin, left: Math.floor(remaining / 2), right: Math.ceil(remaining / 2) };
87
+ }
88
+ else {
89
+ if (margin.left === -1)
90
+ margin = { ...margin, left: 0 };
91
+ if (margin.right === -1)
92
+ margin = { ...margin, right: 0 };
93
+ }
94
+ const boxX = x + margin.left;
95
+ const boxY = y + margin.top;
96
+ const explicitWidth = resolveSize(style?.width, availWidth - margin.left - margin.right);
97
+ const nodeWidth = explicitWidth !== null ? Math.min(explicitWidth, availWidth - margin.left - margin.right) : null;
98
+ const nodeHeight = resolveSize(style?.height, availHeight - margin.top - margin.bottom);
99
+ const innerW = (nodeWidth ?? (availWidth - margin.left - margin.right)) - inset.left - inset.right;
100
+ const innerH = (nodeHeight ?? (availHeight - margin.top - margin.bottom)) - inset.top - inset.bottom;
101
+ const display = style?.display ?? 'block';
102
+ let content;
103
+ if (display === 'flex') {
104
+ content = positionChildren(node.children, 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
+ }
106
+ else if (display === 'table') {
107
+ content = layoutTable(node, styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH);
108
+ }
109
+ else if (display === 'grid' && style) {
110
+ content = layoutGrid(node, styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH, style);
111
+ }
112
+ else {
113
+ // block or inline — use block flow (inline children flow horizontally within)
114
+ content = layoutBlockFlow(node.children, styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH);
115
+ }
116
+ // Block elements fill parent width; inline/inline-block shrink-wrap to content.
117
+ // 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
+ const isBlock = (display === 'block' || display === 'flex' || display === 'grid' || display === 'table')
121
+ && !isFlexOrGridChild;
122
+ const autoWidth = isBlock
123
+ ? (availWidth - margin.left - margin.right)
124
+ : content.width + inset.left + inset.right;
125
+ // Input/textarea have intrinsic minimum height of 1 row for the value text
126
+ const intrinsicHeight = (node.tag === 'input' || node.tag === 'textarea')
127
+ ? Math.max(content.height, 1)
128
+ : content.height;
129
+ const autoHeight = intrinsicHeight + inset.top + inset.bottom;
130
+ const finalWidth = constrain(nodeWidth ?? autoWidth, style?.minWidth, style?.maxWidth);
131
+ const finalHeight = constrain(nodeHeight ?? autoHeight, style?.minHeight, style?.maxHeight);
132
+ boxes.set(node.id, { x: boxX, y: boxY, width: finalWidth, height: finalHeight });
133
+ // Return outer size including margin
134
+ return { width: finalWidth + margin.left + margin.right, height: finalHeight + margin.top + margin.bottom };
135
+ }
136
+ function layoutAbsolute(node, styles, boxes, x, y, availWidth, availHeight, style) {
137
+ const borderWidth = (style.borderStyle && style.borderStyle !== 'none') ? 1 : 0;
138
+ const inset = {
139
+ top: resolvePadding(style.paddingTop, availWidth) + borderWidth,
140
+ right: resolvePadding(style.paddingRight, availWidth) + borderWidth,
141
+ bottom: resolvePadding(style.paddingBottom, availWidth) + borderWidth,
142
+ left: resolvePadding(style.paddingLeft, availWidth) + borderWidth,
143
+ };
144
+ const nodeWidth = resolveSize(style.width, availWidth);
145
+ const nodeHeight = resolveSize(style.height, availHeight);
146
+ const innerW = (nodeWidth ?? availWidth) - inset.left - inset.right;
147
+ const innerH = (nodeHeight ?? availHeight) - inset.top - inset.bottom;
148
+ const content = positionChildren(node.children, styles, boxes, x + inset.left, y + inset.top, innerW, innerH, style.flexDirection ?? 'column', style.gap ?? 0, style.justifyContent ?? 'start', style.alignItems ?? 'start');
149
+ const finalWidth = constrain(nodeWidth ?? (content.width + inset.left + inset.right), style.minWidth, style.maxWidth);
150
+ const finalHeight = constrain(nodeHeight ?? (content.height + inset.top + inset.bottom), style.minHeight, style.maxHeight);
151
+ boxes.set(node.id, { x, y, width: finalWidth, height: finalHeight });
152
+ // Return zero size — absolute elements don't consume space in flow
153
+ return { width: 0, height: 0 };
154
+ }
155
+ /** Resolve a padding/margin value that may be a number or a % string */
156
+ function resolvePadding(value, availWidth) {
157
+ if (value === undefined)
158
+ return 0;
159
+ if (typeof value === 'number')
160
+ return value;
161
+ return resolveSize(value, availWidth) ?? 0;
162
+ }
163
+ function layoutBlockFlow(children, styles, boxes, x, y, availW, availH) {
164
+ // Layout absolute children first
165
+ for (const child of children) {
166
+ const s = styles.get(child.id);
167
+ if (s?.position === 'absolute' || s?.position === 'fixed') {
168
+ layoutNode(child, styles, boxes, x, y, availW, availH);
169
+ }
170
+ }
171
+ let cursorX = x;
172
+ let cursorY = y;
173
+ let lineHeight = 0;
174
+ let maxWidth = 0;
175
+ let prevBlockMarginBottom = 0;
176
+ const flatChildren = flattenContents(children, styles);
177
+ for (const child of flatChildren) {
178
+ if (child.nodeType === 'comment')
179
+ continue;
180
+ const s = styles.get(child.id);
181
+ if (s?.display === 'none')
182
+ continue;
183
+ if (s?.position === 'absolute' || s?.position === 'fixed')
184
+ continue;
185
+ const isInline = child.nodeType === 'text' || s?.display === 'inline' || s?.display === 'inline-block';
186
+ if (isInline) {
187
+ // Flow horizontally
188
+ const size = layoutNode(child, styles, boxes, cursorX, cursorY, availW - (cursorX - x), availH);
189
+ cursorX += size.width;
190
+ lineHeight = Math.max(lineHeight, size.height);
191
+ maxWidth = Math.max(maxWidth, cursorX - x);
192
+ prevBlockMarginBottom = 0;
193
+ }
194
+ else {
195
+ // Block element — new line first if we have inline content
196
+ if (cursorX > x) {
197
+ cursorY += lineHeight;
198
+ cursorX = x;
199
+ lineHeight = 0;
200
+ }
201
+ // Margin collapsing: adjacent vertical margins collapse to the larger
202
+ const childMarginTop = resolvePadding(s?.marginTop, availW);
203
+ if (prevBlockMarginBottom > 0 && childMarginTop > 0) {
204
+ const collapsed = Math.max(prevBlockMarginBottom, childMarginTop);
205
+ const overlap = prevBlockMarginBottom + childMarginTop - collapsed;
206
+ cursorY -= overlap;
207
+ }
208
+ const size = layoutNode(child, styles, boxes, x, cursorY, availW, availH - (cursorY - y));
209
+ cursorY += size.height;
210
+ maxWidth = Math.max(maxWidth, size.width);
211
+ prevBlockMarginBottom = resolvePadding(s?.marginBottom, availW);
212
+ }
213
+ }
214
+ // Account for trailing inline content
215
+ if (cursorX > x) {
216
+ cursorY += lineHeight;
217
+ }
218
+ return { width: maxWidth, height: cursorY - y };
219
+ }
220
+ function layoutTable(node, styles, boxes, x, y, availW, availH) {
221
+ // Collect rows and cells
222
+ const rows = [];
223
+ for (const child of node.children) {
224
+ if (child.tag === 'tr') {
225
+ const cells = child.children.filter(c => c.tag === 'td' || c.tag === 'th');
226
+ rows.push(cells);
227
+ }
228
+ }
229
+ if (rows.length === 0)
230
+ return { width: 0, height: 0 };
231
+ const numCols = Math.max(...rows.map(r => r.length));
232
+ const colWidths = new Array(numCols).fill(0);
233
+ // First pass: measure all cells to find max column widths
234
+ for (const row of rows) {
235
+ for (let col = 0; col < row.length; col++) {
236
+ const cell = row[col];
237
+ const size = layoutNode(cell, styles, boxes, 0, 0, availW, availH);
238
+ colWidths[col] = Math.max(colWidths[col], size.width);
239
+ }
240
+ }
241
+ // Add 2 cells padding between columns
242
+ const colGap = 2;
243
+ // Second pass: position cells with aligned columns
244
+ let rowY = y;
245
+ for (const row of rows) {
246
+ let colX = x;
247
+ let rowHeight = 0;
248
+ // Layout the tr element
249
+ const trNode = node.children.find(c => c.tag === 'tr' && c.children.includes(row[0]));
250
+ for (let col = 0; col < row.length; col++) {
251
+ const cell = row[col];
252
+ const size = layoutNode(cell, styles, boxes, colX, rowY, colWidths[col], availH);
253
+ // Table cells fill their column width
254
+ const cellBox = boxes.get(cell.id);
255
+ if (cellBox && cellBox.width < colWidths[col])
256
+ cellBox.width = colWidths[col];
257
+ rowHeight = Math.max(rowHeight, size.height);
258
+ colX += colWidths[col] + colGap;
259
+ }
260
+ if (trNode) {
261
+ const trWidth = colWidths.reduce((sum, w) => sum + w, 0) + colGap * (numCols - 1);
262
+ boxes.set(trNode.id, { x, y: rowY, width: trWidth, height: rowHeight });
263
+ }
264
+ rowY += rowHeight;
265
+ }
266
+ const totalWidth = colWidths.reduce((sum, w) => sum + w, 0) + colGap * Math.max(0, numCols - 1);
267
+ return { width: totalWidth, height: rowY - y };
268
+ }
269
+ function layoutGrid(node, styles, boxes, x, y, availW, availH, style) {
270
+ const children = node.children.filter(c => c.nodeType === 'element' && styles.get(c.id)?.display !== 'none');
271
+ if (children.length === 0)
272
+ return { width: 0, height: 0 };
273
+ const colWidths = parseGridTemplate(style.gridTemplateColumns ?? '', availW);
274
+ const rowHeights = parseGridTemplate(style.gridTemplateRows ?? '', availH);
275
+ const numCols = colWidths.length || 1;
276
+ const gap = style.gap ?? 0;
277
+ let rowY = y;
278
+ let maxWidth = 0;
279
+ let col = 0;
280
+ let rowIdx = 0;
281
+ let currentRowHeight = 0; // auto-computed from content
282
+ for (const child of children) {
283
+ if (col >= numCols) {
284
+ const explicitH = rowHeights[rowIdx];
285
+ rowY += (explicitH ?? currentRowHeight) + gap;
286
+ col = 0;
287
+ rowIdx++;
288
+ currentRowHeight = 0;
289
+ }
290
+ const colX = x + colWidths.slice(0, col).reduce((sum, w) => sum + w + gap, 0);
291
+ const colW = colWidths[col] ?? availW;
292
+ const explicitRowH = rowHeights[rowIdx];
293
+ const size = layoutNode(child, styles, boxes, colX, rowY, colW, explicitRowH ?? (availH - (rowY - y)));
294
+ // Grid children fill their column width
295
+ const childBox = boxes.get(child.id);
296
+ if (childBox && childBox.width < colW)
297
+ childBox.width = colW;
298
+ currentRowHeight = Math.max(currentRowHeight, size.height);
299
+ maxWidth = Math.max(maxWidth, colX - x + colW);
300
+ col++;
301
+ }
302
+ const finalRowH = rowHeights[rowIdx] ?? currentRowHeight;
303
+ return { width: maxWidth, height: (rowY - y) + finalRowH };
304
+ }
305
+ function parseGridTemplate(template, availW) {
306
+ if (!template)
307
+ return [];
308
+ const parts = template.trim().split(/\s+/);
309
+ const widths = [];
310
+ const frParts = [];
311
+ let fixedTotal = 0;
312
+ for (let i = 0; i < parts.length; i++) {
313
+ const part = parts[i];
314
+ if (part.endsWith('cell')) {
315
+ const w = Math.round(parseFloat(part));
316
+ widths.push(w);
317
+ fixedTotal += w;
318
+ }
319
+ else if (part.endsWith('%')) {
320
+ const w = Math.floor(availW * parseFloat(part) / 100);
321
+ widths.push(w);
322
+ fixedTotal += w;
323
+ }
324
+ else if (part.endsWith('fr')) {
325
+ const fr = parseFloat(part);
326
+ widths.push(0); // placeholder
327
+ frParts.push({ index: i, fr });
328
+ }
329
+ else {
330
+ widths.push(0);
331
+ }
332
+ }
333
+ // Distribute remaining space to fr units
334
+ if (frParts.length > 0) {
335
+ const totalFr = frParts.reduce((sum, p) => sum + p.fr, 0);
336
+ const remaining = Math.max(0, availW - fixedTotal);
337
+ for (const { index, fr } of frParts) {
338
+ widths[index] = Math.floor(remaining * fr / totalFr);
339
+ }
340
+ }
341
+ return widths;
342
+ }
343
+ function positionChildren(children, styles, boxes, innerX, innerY, innerW, innerH, dir, gap, justify, align, wrap = 'nowrap') {
344
+ // Layout absolute children first (they don't affect flow)
345
+ for (const child of children) {
346
+ const s = styles.get(child.id);
347
+ if (s?.position === 'absolute' || s?.position === 'fixed') {
348
+ layoutNode(child, styles, boxes, innerX, innerY, innerW, innerH);
349
+ }
350
+ }
351
+ // Flatten display:contents children into the list
352
+ const flatChildren = flattenContents(children, styles);
353
+ const visible = flatChildren.filter(c => {
354
+ if (c.nodeType === 'comment')
355
+ return false;
356
+ const s = styles.get(c.id);
357
+ if (s?.display === 'none')
358
+ return false;
359
+ if (s?.position === 'absolute' || s?.position === 'fixed')
360
+ return false;
361
+ return true;
362
+ });
363
+ if (visible.length === 0)
364
+ return { width: 0, height: 0 };
365
+ // Pre-measure to filter out zero-size items (e.g. whitespace text nodes)
366
+ const measured = visible.map(child => ({
367
+ child,
368
+ size: layoutNode(child, styles, boxes, 0, 0, innerW, innerH),
369
+ }));
370
+ const nonEmpty = measured.filter(({ size }) => size.width > 0 || size.height > 0);
371
+ if (nonEmpty.length === 0)
372
+ return { width: 0, height: 0 };
373
+ // Sort by order property, then handle reverse
374
+ const sorted = [...nonEmpty].sort((a, b) => {
375
+ const orderA = styles.get(a.child.id)?.order ?? 0;
376
+ const orderB = styles.get(b.child.id)?.order ?? 0;
377
+ return orderA - orderB;
378
+ });
379
+ const isReverse = dir === 'row-reverse' || dir === 'column-reverse';
380
+ const baseDir = (dir === 'row' || dir === 'row-reverse') ? 'row' : 'column';
381
+ const orderedItems = isReverse ? sorted.reverse() : sorted;
382
+ const ordered = orderedItems.map(item => item.child);
383
+ // Use pre-measured sizes, overridden by flex-basis when set
384
+ const sizes = orderedItems.map(item => {
385
+ const s = styles.get(item.child.id);
386
+ const basis = s?.flexBasis;
387
+ if (basis !== undefined && basis !== 'auto') {
388
+ const basisValue = typeof basis === 'number' ? basis : 0;
389
+ return baseDir === 'row'
390
+ ? { width: basisValue, height: item.size.height }
391
+ : { width: item.size.width, height: basisValue };
392
+ }
393
+ return item.size;
394
+ });
395
+ const growValues = ordered.map(child => styles.get(child.id)?.flexGrow ?? 0);
396
+ const shrinkValues = ordered.map(child => styles.get(child.id)?.flexShrink ?? 1);
397
+ const totalGrow = growValues.reduce((a, b) => a + b, 0);
398
+ const totalMain = sizes.reduce((sum, s, i) => {
399
+ return sum + (baseDir === 'row' ? s.width : s.height) + (i > 0 ? gap : 0);
400
+ }, 0);
401
+ const availMain = baseDir === 'row' ? innerW : innerH;
402
+ const freeSpace = Math.max(0, availMain - totalMain);
403
+ const overflow = Math.max(0, totalMain - availMain);
404
+ const hasGrow = totalGrow > 0;
405
+ const totalShrink = overflow > 0 ? shrinkValues.reduce((a, b) => a + b, 0) : 0;
406
+ // Position
407
+ let mainPos = computeMainStart(justify, freeSpace, ordered.length, hasGrow);
408
+ const itemGap = computeItemGap(justify, gap, freeSpace, ordered.length, hasGrow);
409
+ let contentWidth = 0;
410
+ let contentHeight = 0;
411
+ let crossPos = 0;
412
+ let lineHeight = 0;
413
+ for (let i = 0; i < ordered.length; i++) {
414
+ let mainSize = baseDir === 'row' ? sizes[i].width : sizes[i].height;
415
+ if (hasGrow && growValues[i] > 0) {
416
+ mainSize += Math.floor(freeSpace * growValues[i] / totalGrow);
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
+ }
422
+ // Wrap check
423
+ if (wrap === 'wrap' && mainPos + mainSize > availMain && i > 0) {
424
+ crossPos += lineHeight + gap;
425
+ mainPos = 0;
426
+ lineHeight = 0;
427
+ }
428
+ const crossSize = baseDir === 'row' ? sizes[i].height : sizes[i].width;
429
+ const crossAvail = baseDir === 'row' ? innerH : innerW;
430
+ // Check align-self
431
+ const childStyle = styles.get(ordered[i].id);
432
+ const selfAlign = childStyle?.alignSelf !== 'auto'
433
+ ? childStyle?.alignSelf ?? align
434
+ : align;
435
+ const crossOffset = computeCrossOffset(selfAlign, crossAvail, crossSize);
436
+ const finalCx = baseDir === 'row' ? innerX + mainPos : innerX + crossOffset;
437
+ const finalCy = baseDir === 'row' ? innerY + crossPos + crossOffset : innerY + mainPos;
438
+ const childAvailW = baseDir === 'row' ? mainSize : innerW;
439
+ const childAvailH = baseDir === 'row' ? innerH : mainSize;
440
+ layoutNode(ordered[i], styles, boxes, finalCx, finalCy, childAvailW, childAvailH);
441
+ // Override main-axis size for flex-grown/shrunk items
442
+ const box = boxes.get(ordered[i].id);
443
+ if (box) {
444
+ if (baseDir === 'row' && box.width !== mainSize)
445
+ box.width = mainSize;
446
+ if (baseDir === 'column' && box.height !== mainSize)
447
+ box.height = mainSize;
448
+ }
449
+ lineHeight = Math.max(lineHeight, baseDir === 'row' ? sizes[i].height : sizes[i].width);
450
+ mainPos += mainSize + (i < ordered.length - 1 ? itemGap : 0);
451
+ contentWidth = baseDir === 'row' ? Math.max(contentWidth, mainPos) : Math.max(contentWidth, sizes[i].width);
452
+ contentHeight = baseDir === 'row' ? crossPos + lineHeight : mainPos;
453
+ }
454
+ return { width: contentWidth, height: contentHeight };
455
+ }
@@ -0,0 +1,4 @@
1
+ import { ResolvedStyle } from '../css/compute.js';
2
+ export declare function computeMainStart(justify: ResolvedStyle['justifyContent'], freeSpace: number, count: number, hasGrow: boolean): number;
3
+ export declare function computeItemGap(justify: ResolvedStyle['justifyContent'], gap: number, freeSpace: number, count: number, hasGrow: boolean): number;
4
+ export declare function computeCrossOffset(align: ResolvedStyle['alignItems'], crossAvail: number, crossSize: number): number;
@@ -0,0 +1,30 @@
1
+ export function computeMainStart(justify, freeSpace, count, hasGrow) {
2
+ if (hasGrow || count === 0)
3
+ return 0;
4
+ switch (justify) {
5
+ case 'end': return freeSpace;
6
+ case 'center': return Math.floor(freeSpace / 2);
7
+ case 'space-around': return Math.floor(freeSpace / (count * 2));
8
+ case 'space-evenly': return Math.floor(freeSpace / (count + 1));
9
+ default: return 0;
10
+ }
11
+ }
12
+ export function computeItemGap(justify, gap, freeSpace, count, hasGrow) {
13
+ if (hasGrow)
14
+ return gap;
15
+ if (count <= 1)
16
+ return 0;
17
+ switch (justify) {
18
+ case 'space-between': return Math.floor(freeSpace / (count - 1));
19
+ case 'space-around': return Math.floor(freeSpace / count);
20
+ case 'space-evenly': return Math.floor(freeSpace / (count + 1));
21
+ default: return gap;
22
+ }
23
+ }
24
+ export function computeCrossOffset(align, crossAvail, crossSize) {
25
+ switch (align) {
26
+ case 'end': return crossAvail - crossSize;
27
+ case 'center': return Math.floor((crossAvail - crossSize) / 2);
28
+ default: return 0;
29
+ }
30
+ }
@@ -0,0 +1,8 @@
1
+ import { TermNode } from '../renderer/node.js';
2
+ import { ResolvedStyle } from '../css/compute.js';
3
+ import { LayoutBox } from './engine.js';
4
+ /**
5
+ * Incrementally re-layout only dirty subtrees.
6
+ * Falls back to full layout when dirty nodes affect auto-sized ancestors.
7
+ */
8
+ export declare function computeLayoutIncremental(root: TermNode, styles: Map<number, ResolvedStyle>, existingLayout: Map<number, LayoutBox>, dirtyNodes: Set<TermNode>, availWidth: number, availHeight: number, onLayout?: () => void): Map<number, LayoutBox>;
@@ -0,0 +1,58 @@
1
+ import { computeLayout } from './engine.js';
2
+ /**
3
+ * Incrementally re-layout only dirty subtrees.
4
+ * Falls back to full layout when dirty nodes affect auto-sized ancestors.
5
+ */
6
+ export function computeLayoutIncremental(root, styles, existingLayout, dirtyNodes, availWidth, availHeight, onLayout) {
7
+ if (dirtyNodes.size === 0)
8
+ return existingLayout;
9
+ // Check if any dirty node has an auto-sized ancestor — if so, full re-layout
10
+ // because the size change may propagate up
11
+ const needsFullLayout = [...dirtyNodes].some(node => hasAutoSizedAncestor(node, styles));
12
+ if (needsFullLayout) {
13
+ onLayout?.();
14
+ return computeLayout(root, styles, availWidth, availHeight);
15
+ }
16
+ // All dirty nodes are within fixed-size containers — only re-layout those subtrees
17
+ const result = new Map(existingLayout);
18
+ for (const dirtyNode of dirtyNodes) {
19
+ const boundary = findLayoutBoundary(dirtyNode, styles);
20
+ const boundaryBox = existingLayout.get(boundary.id);
21
+ if (!boundaryBox)
22
+ continue;
23
+ onLayout?.();
24
+ const subtreeLayout = computeLayout(boundary, styles, boundaryBox.width, boundaryBox.height);
25
+ // Merge subtree layout into result, adjusting positions
26
+ for (const [id, box] of subtreeLayout) {
27
+ result.set(id, {
28
+ x: box.x + boundaryBox.x,
29
+ y: box.y + boundaryBox.y,
30
+ width: box.width,
31
+ height: box.height,
32
+ });
33
+ }
34
+ // The boundary itself keeps its original position
35
+ result.set(boundary.id, boundaryBox);
36
+ }
37
+ return result;
38
+ }
39
+ function hasAutoSizedAncestor(node, styles) {
40
+ let current = node.parent;
41
+ while (current) {
42
+ const style = styles.get(current.id);
43
+ if (!style || style.width === null || style.height === null)
44
+ return true;
45
+ current = current.parent;
46
+ }
47
+ return false;
48
+ }
49
+ function findLayoutBoundary(node, styles) {
50
+ let current = node.parent;
51
+ while (current?.parent) {
52
+ const style = styles.get(current.id);
53
+ if (style && style.width !== null && style.height !== null)
54
+ return current;
55
+ current = current.parent;
56
+ }
57
+ return current ?? node;
58
+ }
@@ -0,0 +1,2 @@
1
+ export declare function resolveSize(value: number | string | null | undefined, available: number): number | null;
2
+ export declare function constrain(value: number, min: number | null | undefined, max: number | null | undefined): number;
@@ -0,0 +1,25 @@
1
+ import { evaluateCalc } from '../css/calc.js';
2
+ export function resolveSize(value, available) {
3
+ if (value === null || value === undefined)
4
+ return null;
5
+ if (typeof value === 'number')
6
+ return value;
7
+ if (typeof value === 'string') {
8
+ if (value.endsWith('%')) {
9
+ return Math.floor(available * parseFloat(value) / 100);
10
+ }
11
+ // calc(), min(), max(), clamp()
12
+ const calcResult = evaluateCalc(value, available);
13
+ if (calcResult !== null)
14
+ return calcResult;
15
+ }
16
+ return null;
17
+ }
18
+ export function constrain(value, min, max) {
19
+ let result = value;
20
+ if (min != null)
21
+ result = Math.max(result, min);
22
+ if (max != null)
23
+ result = Math.min(result, max);
24
+ return result;
25
+ }
@@ -0,0 +1,7 @@
1
+ export declare function wrapText(text: string, width: number): string[];
2
+ export declare function truncateText(text: string, width: number): string;
3
+ export declare function truncateMiddle(text: string, width: number): string;
4
+ export declare function measureText(text: string, availWidth: number): {
5
+ width: number;
6
+ height: number;
7
+ };
@@ -0,0 +1,52 @@
1
+ export function wrapText(text, width) {
2
+ if (text === '')
3
+ return [''];
4
+ if (text.length <= width)
5
+ return [text];
6
+ const lines = [];
7
+ let remaining = text;
8
+ while (remaining.length > 0) {
9
+ if (remaining.length <= width) {
10
+ lines.push(remaining);
11
+ break;
12
+ }
13
+ // Find last space within width
14
+ let breakAt = remaining.lastIndexOf(' ', width);
15
+ if (breakAt <= 0) {
16
+ // No space found — hard break at width
17
+ breakAt = width;
18
+ lines.push(remaining.substring(0, breakAt));
19
+ remaining = remaining.substring(breakAt);
20
+ }
21
+ else {
22
+ lines.push(remaining.substring(0, breakAt));
23
+ remaining = remaining.substring(breakAt + 1); // skip the space
24
+ }
25
+ }
26
+ return lines;
27
+ }
28
+ export function truncateText(text, width) {
29
+ if (width <= 0)
30
+ return '';
31
+ if (text.length <= width)
32
+ return text;
33
+ if (width === 1)
34
+ return '…';
35
+ return text.substring(0, width - 1) + '…';
36
+ }
37
+ export function truncateMiddle(text, width) {
38
+ if (width <= 0)
39
+ return '';
40
+ if (text.length <= width)
41
+ return text;
42
+ if (width <= 3)
43
+ return text.substring(0, width - 1) + '…';
44
+ const half = Math.floor((width - 1) / 2);
45
+ const endLen = width - 1 - half;
46
+ return text.substring(0, half) + '…' + text.substring(text.length - endLen);
47
+ }
48
+ export function measureText(text, availWidth) {
49
+ const lines = wrapText(text, availWidth);
50
+ const maxLineWidth = lines.reduce((max, line) => Math.max(max, line.length), 0);
51
+ return { width: maxLineWidth, height: lines.length };
52
+ }
@@ -0,0 +1,23 @@
1
+ export declare function moveTo(col: number, row: number): string;
2
+ export declare function clearScreen(): string;
3
+ export declare function hideCursor(): string;
4
+ export declare function showCursor(): string;
5
+ export declare function enterAltScreen(): string;
6
+ export declare function exitAltScreen(): string;
7
+ export declare function resetStyle(): string;
8
+ export declare function bold(): string;
9
+ export declare function dim(): string;
10
+ export declare function italic(): string;
11
+ export declare function underline(): string;
12
+ export declare function strikethrough(): string;
13
+ export declare function fgColor(color: string): string;
14
+ export declare function bgColor(color: string): string;
15
+ export declare function hyperlinkOpen(url: string): string;
16
+ export declare function hyperlinkClose(): string;
17
+ export declare function enableMouse(): string;
18
+ export declare function disableMouse(): string;
19
+ export declare function setCursorShape(shape: 'block' | 'underline' | 'bar'): string;
20
+ export declare function enableBracketedPaste(): string;
21
+ export declare function disableBracketedPaste(): string;
22
+ export declare function beginSyncUpdate(): string;
23
+ export declare function endSyncUpdate(): string;