machinalayout 0.1.0 → 0.2.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 (45) hide show
  1. package/README.md +280 -49
  2. package/dist/chunk-BJOQRPPX.js +382 -0
  3. package/dist/chunk-HU6XYOH7.js +133 -0
  4. package/dist/chunk-KYWOCAHK.js +205 -0
  5. package/dist/chunk-RJYRJ3LD.js +0 -0
  6. package/dist/chunk-TR24ERZT.js +66 -0
  7. package/dist/dispatch/index.d.ts +49 -0
  8. package/dist/dispatch/index.js +217 -0
  9. package/dist/index.d.ts +15 -238
  10. package/dist/index.js +596 -591
  11. package/dist/react/index.d.ts +33 -0
  12. package/dist/react/index.js +7 -0
  13. package/dist/react-native/index.d.ts +30 -0
  14. package/dist/react-native/index.js +83 -0
  15. package/dist/text/index.d.ts +10 -0
  16. package/dist/text/index.js +9 -0
  17. package/dist/text/react/index.d.ts +14 -0
  18. package/dist/text/react/index.js +7 -0
  19. package/dist/text/react-native/index.d.ts +16 -0
  20. package/dist/text/react-native/index.js +155 -0
  21. package/dist/text/vue/index.d.ts +113 -0
  22. package/dist/text/vue/index.js +202 -0
  23. package/dist/types-BudfpzZX.d.ts +184 -0
  24. package/dist/types-C4poVJpR.d.ts +74 -0
  25. package/dist/vue/index.d.ts +173 -0
  26. package/dist/vue/index.js +111 -0
  27. package/docs/adapter-packaging-a0-plan.md +352 -0
  28. package/docs/adapters.md +19 -0
  29. package/docs/api-coherence-m8-audit.md +397 -0
  30. package/docs/error-codes.md +84 -0
  31. package/docs/grid-arrange-m5a-contract.md +480 -0
  32. package/docs/grid-arrange.md +51 -0
  33. package/docs/layout-interpolation.md +52 -0
  34. package/docs/machina-dispatch-d0-contract.md +496 -0
  35. package/docs/machina-dispatch.md +143 -0
  36. package/docs/named-layers.md +40 -0
  37. package/docs/react-adapter.md +51 -69
  38. package/docs/react-native-adapter.md +56 -0
  39. package/docs/react-native-text-renderer.md +50 -0
  40. package/docs/reference-alignment-m7a-contract.md +384 -0
  41. package/docs/reference-alignment.md +44 -0
  42. package/docs/responsive-variants.md +54 -0
  43. package/docs/vue-adapter.md +55 -0
  44. package/docs/vue-text-renderer.md +55 -0
  45. package/package.json +60 -5
package/dist/index.js CHANGED
@@ -1,12 +1,18 @@
1
- // src/errors.ts
2
- var MachinaLayoutError = class extends Error {
3
- code;
4
- constructor(code, message) {
5
- super(message);
6
- this.name = "MachinaLayoutError";
7
- this.code = code;
8
- }
9
- };
1
+ import {
2
+ MachinaReactView
3
+ } from "./chunk-HU6XYOH7.js";
4
+ import "./chunk-RJYRJ3LD.js";
5
+ import {
6
+ MachinaTextView
7
+ } from "./chunk-KYWOCAHK.js";
8
+ import {
9
+ parseMachinaText,
10
+ parseMachinaTextInline
11
+ } from "./chunk-BJOQRPPX.js";
12
+ import {
13
+ MachinaLayoutError,
14
+ toResolvedTree
15
+ } from "./chunk-TR24ERZT.js";
10
16
 
11
17
  // src/validation.ts
12
18
  function assertFiniteNumber(value, fieldName) {
@@ -83,7 +89,10 @@ function resolveUiLength(length, axisSize, fieldName = "length") {
83
89
  if (unit === "ui") {
84
90
  return value * axisSize;
85
91
  }
86
- throw new MachinaLayoutError("InvalidLengthUnit", `Invalid UiLength unit for ${fieldName}: ${String(unit)}.`);
92
+ throw new MachinaLayoutError(
93
+ "InvalidLengthUnit",
94
+ `Invalid UiLength unit for ${fieldName}: ${String(unit)}.`
95
+ );
87
96
  }
88
97
 
89
98
  // src/offset.ts
@@ -118,12 +127,18 @@ function compileLayoutRows(rows) {
118
127
  assertFiniteNumber(row.order, `rows[${rowIndex}].order`);
119
128
  }
120
129
  if (row.frame.kind === "root" && row.parent !== void 0) {
121
- throw new MachinaLayoutError("RootFrameNotRoot", `row ${row.id} uses RootFrame but is not a root row.`);
130
+ throw new MachinaLayoutError(
131
+ "RootFrameNotRoot",
132
+ `row ${row.id} uses RootFrame but is not a root row.`
133
+ );
122
134
  }
123
135
  if (row.z !== void 0) {
124
136
  assertFiniteNumber(row.z, `rows[${rowIndex}].z`);
125
137
  if (!Number.isInteger(row.z) || row.z < -5 || row.z > 5) {
126
- throw new MachinaLayoutError("InvalidZ", `rows[${rowIndex}].z must be an integer in range -5..5`);
138
+ throw new MachinaLayoutError(
139
+ "InvalidZ",
140
+ `rows[${rowIndex}].z must be an integer in range -5..5`
141
+ );
127
142
  }
128
143
  }
129
144
  rowById.set(row.id, row);
@@ -135,6 +150,7 @@ function compileLayoutRows(rows) {
135
150
  view: row.view,
136
151
  slot: row.slot,
137
152
  debugLabel: row.debugLabel,
153
+ layer: row.layer,
138
154
  offset: row.offset
139
155
  };
140
156
  if (row.parent === void 0) {
@@ -149,10 +165,16 @@ function compileLayoutRows(rows) {
149
165
  }
150
166
  const rootId = rootCandidates[0];
151
167
  if (nodes[rootId].frame.kind === "fill") {
152
- throw new MachinaLayoutError("FillFrameWithoutArranger", "FillFrame cannot be used as the root frame.");
168
+ throw new MachinaLayoutError(
169
+ "FillFrameWithoutArranger",
170
+ "FillFrame cannot be used as the root frame."
171
+ );
153
172
  }
154
173
  if (nodes[rootId].frame.kind === "fixed") {
155
- throw new MachinaLayoutError("FixedFrameWithoutArranger", "FixedFrame cannot be used as the root frame.");
174
+ throw new MachinaLayoutError(
175
+ "FixedFrameWithoutArranger",
176
+ "FixedFrame cannot be used as the root frame."
177
+ );
156
178
  }
157
179
  const childrenEntries = /* @__PURE__ */ new Map();
158
180
  for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
@@ -164,7 +186,10 @@ function compileLayoutRows(rows) {
164
186
  throw new MachinaLayoutError("SelfParent", `node ${row.id} cannot parent itself.`);
165
187
  }
166
188
  if (!rowById.has(row.parent) || row.parent.trim().length === 0) {
167
- throw new MachinaLayoutError("UnknownParent", `node ${row.id} references unknown parent: ${row.parent}`);
189
+ throw new MachinaLayoutError(
190
+ "UnknownParent",
191
+ `node ${row.id} references unknown parent: ${row.parent}`
192
+ );
168
193
  }
169
194
  const entry = {
170
195
  childId: row.id,
@@ -233,6 +258,101 @@ function compileLayoutRows(rows) {
233
258
  return { rootId, nodes, children };
234
259
  }
235
260
 
261
+ // src/selectLayoutRowsForRoot.ts
262
+ function validateRootRect(rootRect) {
263
+ assertFiniteNumber(rootRect.x, "rootRect.x");
264
+ assertFiniteNumber(rootRect.y, "rootRect.y");
265
+ assertNonNegativeSize(rootRect.width, "rootRect.width");
266
+ assertNonNegativeSize(rootRect.height, "rootRect.height");
267
+ }
268
+ function validateCondition(condition, rowIndex, variantIndex) {
269
+ if (condition.minWidth !== void 0) {
270
+ assertFiniteNumber(
271
+ condition.minWidth,
272
+ `rows[${rowIndex}].variants[${variantIndex}].when.minWidth`
273
+ );
274
+ }
275
+ if (condition.maxWidth !== void 0) {
276
+ assertFiniteNumber(
277
+ condition.maxWidth,
278
+ `rows[${rowIndex}].variants[${variantIndex}].when.maxWidth`
279
+ );
280
+ }
281
+ if (condition.minHeight !== void 0) {
282
+ assertFiniteNumber(
283
+ condition.minHeight,
284
+ `rows[${rowIndex}].variants[${variantIndex}].when.minHeight`
285
+ );
286
+ }
287
+ if (condition.maxHeight !== void 0) {
288
+ assertFiniteNumber(
289
+ condition.maxHeight,
290
+ `rows[${rowIndex}].variants[${variantIndex}].when.maxHeight`
291
+ );
292
+ }
293
+ if (condition.minWidth !== void 0 && condition.maxWidth !== void 0 && condition.minWidth > condition.maxWidth) {
294
+ throw new MachinaLayoutError(
295
+ "InvalidVariantCondition",
296
+ `rows[${rowIndex}].variants[${variantIndex}].when has minWidth > maxWidth`
297
+ );
298
+ }
299
+ if (condition.minHeight !== void 0 && condition.maxHeight !== void 0 && condition.minHeight > condition.maxHeight) {
300
+ throw new MachinaLayoutError(
301
+ "InvalidVariantCondition",
302
+ `rows[${rowIndex}].variants[${variantIndex}].when has minHeight > maxHeight`
303
+ );
304
+ }
305
+ }
306
+ function conditionMatches(condition, rootRect) {
307
+ if (condition.minWidth !== void 0 && rootRect.width < condition.minWidth) return false;
308
+ if (condition.maxWidth !== void 0 && rootRect.width > condition.maxWidth) return false;
309
+ if (condition.minHeight !== void 0 && rootRect.height < condition.minHeight) return false;
310
+ if (condition.maxHeight !== void 0 && rootRect.height > condition.maxHeight) return false;
311
+ return true;
312
+ }
313
+ function validateVariantZ(variant, rowIndex, variantIndex) {
314
+ if (variant.z === void 0) return;
315
+ assertFiniteNumber(variant.z, `rows[${rowIndex}].variants[${variantIndex}].z`);
316
+ if (!Number.isInteger(variant.z) || variant.z < -5 || variant.z > 5) {
317
+ throw new MachinaLayoutError(
318
+ "InvalidZ",
319
+ `rows[${rowIndex}].variants[${variantIndex}].z must be an integer in range -5..5`
320
+ );
321
+ }
322
+ }
323
+ function selectLayoutRowsForRoot(rows, rootRect) {
324
+ validateRootRect(rootRect);
325
+ return rows.map((row, rowIndex) => {
326
+ const baseRow = { ...row };
327
+ delete baseRow.variants;
328
+ const variants = row.variants;
329
+ if (!variants || variants.length === 0) {
330
+ return baseRow;
331
+ }
332
+ for (let variantIndex = 0; variantIndex < variants.length; variantIndex += 1) {
333
+ const variant = variants[variantIndex];
334
+ validateCondition(variant.when, rowIndex, variantIndex);
335
+ validateVariantZ(variant, rowIndex, variantIndex);
336
+ if (!conditionMatches(variant.when, rootRect)) {
337
+ continue;
338
+ }
339
+ const selected = {
340
+ ...baseRow,
341
+ frame: variant.frame ?? baseRow.frame,
342
+ arrange: variant.arrange ?? baseRow.arrange,
343
+ offset: variant.offset ?? baseRow.offset,
344
+ z: variant.z ?? baseRow.z,
345
+ view: variant.view ?? baseRow.view,
346
+ slot: variant.slot ?? baseRow.slot,
347
+ debugLabel: variant.debugLabel ?? baseRow.debugLabel,
348
+ layer: variant.layer ?? baseRow.layer
349
+ };
350
+ return selected;
351
+ }
352
+ return baseRow;
353
+ });
354
+ }
355
+
236
356
  // src/resolveFrame.ts
237
357
  function validateParentRect(parent) {
238
358
  assertFiniteNumber(parent.x, "parent.x");
@@ -260,11 +380,17 @@ function resolveAnchor(parent, frame) {
260
380
  if (hasHeight) assertNonNegativeSize(explicitHeight, "frame.height");
261
381
  const horizontalCount = Number(hasLeft) + Number(hasRight) + Number(hasWidth);
262
382
  if (horizontalCount !== 2) {
263
- throw new MachinaLayoutError("InvalidAnchorHorizontal", "Anchor frame must specify exactly two horizontal constraints: left, right, width.");
383
+ throw new MachinaLayoutError(
384
+ "InvalidAnchorHorizontal",
385
+ "Anchor frame must specify exactly two horizontal constraints: left, right, width."
386
+ );
264
387
  }
265
388
  const verticalCount = Number(hasTop) + Number(hasBottom) + Number(hasHeight);
266
389
  if (verticalCount !== 2) {
267
- throw new MachinaLayoutError("InvalidAnchorVertical", "Anchor frame must specify exactly two vertical constraints: top, bottom, height.");
390
+ throw new MachinaLayoutError(
391
+ "InvalidAnchorVertical",
392
+ "Anchor frame must specify exactly two vertical constraints: top, bottom, height."
393
+ );
268
394
  }
269
395
  let x;
270
396
  let width;
@@ -306,29 +432,159 @@ function resolveFrame(parent, frame) {
306
432
  assertFiniteNumber(frame.y, "frame.y");
307
433
  assertNonNegativeSize(frame.width, "frame.width");
308
434
  assertNonNegativeSize(frame.height, "frame.height");
309
- return { x: parent.x + frame.x, y: parent.y + frame.y, width: frame.width, height: frame.height };
435
+ return {
436
+ x: parent.x + frame.x,
437
+ y: parent.y + frame.y,
438
+ width: frame.width,
439
+ height: frame.height
440
+ };
310
441
  }
311
442
  case "anchor":
312
443
  return resolveAnchor(parent, frame);
313
444
  case "root":
314
- throw new MachinaLayoutError("RootFrameWithoutRoot", "RootFrame can only be declared on the root row.");
445
+ throw new MachinaLayoutError(
446
+ "RootFrameWithoutRoot",
447
+ "RootFrame can only be declared on the root row."
448
+ );
315
449
  case "fixed": {
316
450
  assertNonNegativeSize(frame.width, "frame.width");
317
451
  assertNonNegativeSize(frame.height, "frame.height");
318
- throw new MachinaLayoutError("FixedFrameWithoutArranger", "Fixed frames require an arranger to determine placement.");
452
+ throw new MachinaLayoutError(
453
+ "FixedFrameWithoutArranger",
454
+ "Fixed frames require an arranger to determine placement."
455
+ );
319
456
  }
320
457
  case "fill":
321
- throw new MachinaLayoutError("FillFrameWithoutArranger", "Fill frames require a stack arranger to determine placement.");
458
+ throw new MachinaLayoutError(
459
+ "FillFrameWithoutArranger",
460
+ "Fill frames require a stack arranger to determine placement."
461
+ );
462
+ case "cell":
463
+ throw new MachinaLayoutError(
464
+ "CellFrameWithoutGrid",
465
+ "Cell frames require a grid arranger to determine placement."
466
+ );
467
+ case "guide":
468
+ throw new MachinaLayoutError(
469
+ "GuideTargetUnresolved",
470
+ "Guide frames require document-level dependency resolution and cannot be resolved directly."
471
+ );
322
472
  }
323
473
  }
324
474
 
325
475
  // src/resolveLayoutDocument.ts
326
- function validateRootRect(rootRect) {
476
+ function validateRootRect2(rootRect) {
327
477
  assertFiniteNumber(rootRect.x, "rootRect.x");
328
478
  assertFiniteNumber(rootRect.y, "rootRect.y");
329
479
  assertNonNegativeSize(rootRect.width, "rootRect.width");
330
480
  assertNonNegativeSize(rootRect.height, "rootRect.height");
331
481
  }
482
+ var H_EDGES = /* @__PURE__ */ new Set(["left", "right", "centerX"]);
483
+ var V_EDGES = /* @__PURE__ */ new Set(["top", "bottom", "centerY"]);
484
+ var ALL_EDGES = /* @__PURE__ */ new Set(["left", "right", "top", "bottom", "centerX", "centerY"]);
485
+ var isEdgeRef = (value) => Boolean(value && typeof value === "object" && "ref" in value && "edge" in value);
486
+ function getRectEdgeValue(rect, edge) {
487
+ switch (edge) {
488
+ case "left":
489
+ return rect.x;
490
+ case "right":
491
+ return rect.x + rect.width;
492
+ case "centerX":
493
+ return rect.x + rect.width / 2;
494
+ case "top":
495
+ return rect.y;
496
+ case "bottom":
497
+ return rect.y + rect.height;
498
+ case "centerY":
499
+ return rect.y + rect.height / 2;
500
+ default:
501
+ throw new MachinaLayoutError("InvalidGuideFrame", `unknown guide edge: ${String(edge)}`);
502
+ }
503
+ }
504
+ function validateGuideFrame(nodeId, frame, document) {
505
+ const hCount = Number(frame.left !== void 0) + Number(frame.right !== void 0) + Number(frame.width !== void 0);
506
+ const vCount = Number(frame.top !== void 0) + Number(frame.bottom !== void 0) + Number(frame.height !== void 0);
507
+ if (hCount !== 2 || vCount !== 2)
508
+ throw new MachinaLayoutError(
509
+ "InvalidGuideFrame",
510
+ `guide frame must provide exactly two constraints per axis: ${nodeId}`
511
+ );
512
+ const hRefs = [frame.left, frame.right].filter(isEdgeRef);
513
+ const vRefs = [frame.top, frame.bottom].filter(isEdgeRef);
514
+ if (hRefs.length > 1 || vRefs.length > 1)
515
+ throw new MachinaLayoutError(
516
+ "GuideTooManyReferencesPerAxis",
517
+ `guide has too many refs on one axis: ${nodeId}`
518
+ );
519
+ for (const ref of [...hRefs, ...vRefs]) {
520
+ if (ref.ref === nodeId)
521
+ throw new MachinaLayoutError(
522
+ "GuideSelfReference",
523
+ `guide cannot reference itself: ${nodeId}`
524
+ );
525
+ if (!document.nodes[ref.ref])
526
+ throw new MachinaLayoutError("GuideTargetNotFound", `guide target not found: ${ref.ref}`);
527
+ if (!ALL_EDGES.has(ref.edge))
528
+ throw new MachinaLayoutError("InvalidGuideFrame", `unknown edge: ${String(ref.edge)}`);
529
+ }
530
+ for (const ref of hRefs)
531
+ if (!H_EDGES.has(ref.edge))
532
+ throw new MachinaLayoutError(
533
+ "GuideInvalidEdgeForAxis",
534
+ `horizontal guide ref must use horizontal edge: ${nodeId}`
535
+ );
536
+ for (const ref of vRefs)
537
+ if (!V_EDGES.has(ref.edge))
538
+ throw new MachinaLayoutError(
539
+ "GuideInvalidEdgeForAxis",
540
+ `vertical guide ref must use vertical edge: ${nodeId}`
541
+ );
542
+ }
543
+ function resolveGuidePosition(parentRect, side, value, resolvedNodes) {
544
+ if (isEdgeRef(value)) {
545
+ const target = resolvedNodes[value.ref];
546
+ if (!target)
547
+ throw new MachinaLayoutError(
548
+ "GuideTargetUnresolved",
549
+ `guide target unresolved: ${value.ref}`
550
+ );
551
+ const axisSize2 = side === "left" || side === "right" ? parentRect.width : parentRect.height;
552
+ const offset = value.offset === void 0 ? 0 : resolveUiLength(value.offset, axisSize2, `frame.${side}.offset`);
553
+ return getRectEdgeValue(target.rect, value.edge) + offset;
554
+ }
555
+ const axisSize = side === "left" || side === "right" ? parentRect.width : parentRect.height;
556
+ const scalar = resolveUiLength(value, axisSize, `frame.${side}`);
557
+ if (side === "left") return parentRect.x + scalar;
558
+ if (side === "right") return parentRect.x + parentRect.width - scalar;
559
+ if (side === "top") return parentRect.y + scalar;
560
+ return parentRect.y + parentRect.height - scalar;
561
+ }
562
+ function resolveGuideFrame(parentRect, frame, resolvedNodes) {
563
+ const hasLeft = frame.left !== void 0;
564
+ const hasRight = frame.right !== void 0;
565
+ const hasWidth = frame.width !== void 0;
566
+ const hasTop = frame.top !== void 0;
567
+ const hasBottom = frame.bottom !== void 0;
568
+ const hasHeight = frame.height !== void 0;
569
+ const left = hasLeft ? resolveGuidePosition(parentRect, "left", frame.left, resolvedNodes) : void 0;
570
+ const right = hasRight ? resolveGuidePosition(parentRect, "right", frame.right, resolvedNodes) : void 0;
571
+ const top = hasTop ? resolveGuidePosition(parentRect, "top", frame.top, resolvedNodes) : void 0;
572
+ const bottom = hasBottom ? resolveGuidePosition(parentRect, "bottom", frame.bottom, resolvedNodes) : void 0;
573
+ const explicitWidth = hasWidth ? resolveUiLength(frame.width, parentRect.width, "frame.width") : void 0;
574
+ const explicitHeight = hasHeight ? resolveUiLength(frame.height, parentRect.height, "frame.height") : void 0;
575
+ if (hasWidth) assertNonNegativeSize(explicitWidth, "frame.width");
576
+ if (hasHeight) assertNonNegativeSize(explicitHeight, "frame.height");
577
+ const x = hasLeft && hasWidth ? left : hasRight && hasWidth ? right - explicitWidth : left;
578
+ const width = hasWidth ? explicitWidth : right - left;
579
+ const y = hasTop && hasHeight ? top : hasBottom && hasHeight ? bottom - explicitHeight : top;
580
+ const height = hasHeight ? explicitHeight : bottom - top;
581
+ if (width < 0 || height < 0)
582
+ throw new MachinaLayoutError(
583
+ "NegativeResolvedSize",
584
+ `Resolved guide frame size must be non-negative. Received width=${width}, height=${height}.`
585
+ );
586
+ return { x, y, width, height };
587
+ }
332
588
  function resolveStackChildRects(parentRect, arrange, childIds, document) {
333
589
  const gap = arrange.gap ?? 0;
334
590
  const justify = arrange.justify ?? "start";
@@ -341,9 +597,11 @@ function resolveStackChildRects(parentRect, arrange, childIds, document) {
341
597
  width: parentRect.width - padding.left - padding.right,
342
598
  height: parentRect.height - padding.top - padding.bottom
343
599
  };
344
- if (content.width < 0 || content.height < 0) {
345
- throw new MachinaLayoutError("StackContentNegative", "stack content size cannot be negative after applying padding");
346
- }
600
+ if (content.width < 0 || content.height < 0)
601
+ throw new MachinaLayoutError(
602
+ "StackContentNegative",
603
+ "stack content size cannot be negative after applying padding"
604
+ );
347
605
  const isHorizontal = arrange.axis === "horizontal";
348
606
  const contentMain = isHorizontal ? content.width : content.height;
349
607
  const contentCross = isHorizontal ? content.height : content.width;
@@ -352,9 +610,11 @@ function resolveStackChildRects(parentRect, arrange, childIds, document) {
352
610
  const fillWeights = [];
353
611
  for (const childId of childIds) {
354
612
  const childNode = document.nodes[childId];
355
- if (!childNode) {
356
- throw new MachinaLayoutError("UnknownParent", `child id ${childId} referenced by arranged parent is missing`);
357
- }
613
+ if (!childNode)
614
+ throw new MachinaLayoutError(
615
+ "UnknownParent",
616
+ `child id ${childId} referenced by arranged parent is missing`
617
+ );
358
618
  if (childNode.frame.kind === "fixed") {
359
619
  assertNonNegativeSize(childNode.frame.width, `${childId}.frame.width`);
360
620
  assertNonNegativeSize(childNode.frame.height, `${childId}.frame.height`);
@@ -363,14 +623,18 @@ function resolveStackChildRects(parentRect, arrange, childIds, document) {
363
623
  fillWeights.push(0);
364
624
  continue;
365
625
  }
366
- if (childNode.frame.kind !== "fill") {
367
- throw new MachinaLayoutError("StackChildMustBeFixed", `stack child must use fixed or fill frame: ${childId}`);
368
- }
626
+ if (childNode.frame.kind !== "fill")
627
+ throw new MachinaLayoutError(
628
+ "StackChildMustBeFixed",
629
+ `stack child must use fixed or fill frame: ${childId}`
630
+ );
369
631
  const weight = childNode.frame.weight ?? 1;
370
632
  assertFiniteNumber(weight, `${childId}.frame.weight`);
371
- if (weight <= 0) {
372
- throw new MachinaLayoutError("InvalidFillWeight", `${childId}.frame.weight must be greater than 0`);
373
- }
633
+ if (weight <= 0)
634
+ throw new MachinaLayoutError(
635
+ "InvalidFillWeight",
636
+ `${childId}.frame.weight must be greater than 0`
637
+ );
374
638
  const cross = childNode.frame.cross ?? "fill";
375
639
  let childCross = contentCross;
376
640
  if (cross !== "fill") {
@@ -381,49 +645,31 @@ function resolveStackChildRects(parentRect, arrange, childIds, document) {
381
645
  childCrossSizes.push(childCross);
382
646
  fillWeights.push(weight);
383
647
  }
384
- const fixedMainTotal = childIds.reduce((sum, _id, i) => sum + (fillWeights[i] === 0 ? childMainSizes[i] : 0), 0);
648
+ const fixedMainTotal = childIds.reduce(
649
+ (sum, _id, i) => sum + (fillWeights[i] === 0 ? childMainSizes[i] : 0),
650
+ 0
651
+ );
385
652
  const totalGap = gap * Math.max(0, childIds.length - 1);
386
653
  const remainingMain = contentMain - fixedMainTotal - totalGap;
387
- if (remainingMain < 0) {
388
- throw new MachinaLayoutError("StackOverflow", "stack main axis overflow");
389
- }
654
+ if (remainingMain < 0) throw new MachinaLayoutError("StackOverflow", "stack main axis overflow");
390
655
  const totalFillWeight = fillWeights.reduce((sum, w) => sum + w, 0);
391
656
  if (totalFillWeight > 0) {
392
- for (let i = 0; i < childMainSizes.length; i += 1) {
393
- if (fillWeights[i] > 0) {
657
+ for (let i = 0; i < childMainSizes.length; i += 1)
658
+ if (fillWeights[i] > 0)
394
659
  childMainSizes[i] = remainingMain * fillWeights[i] / totalFillWeight;
395
- }
396
- }
397
660
  }
398
- for (const childCross of childCrossSizes) {
399
- if (childCross > contentCross) {
661
+ for (const childCross of childCrossSizes)
662
+ if (childCross > contentCross)
400
663
  throw new MachinaLayoutError("StackOverflow", "stack cross axis overflow");
401
- }
402
- }
403
664
  const occupiedMain = childMainSizes.reduce((sum, size) => sum + size, 0) + totalGap;
404
665
  const remainingMainAfterFill = contentMain - occupiedMain;
405
666
  let startOffset = 0;
406
667
  let actualGap = gap;
407
668
  if (totalFillWeight === 0) {
408
- switch (justify) {
409
- case "start":
410
- break;
411
- case "center":
412
- startOffset = remainingMainAfterFill / 2;
413
- break;
414
- case "end":
415
- startOffset = remainingMainAfterFill;
416
- break;
417
- case "space-between":
418
- if (childIds.length <= 1) {
419
- actualGap = 0;
420
- } else {
421
- actualGap = gap + remainingMainAfterFill / (childIds.length - 1);
422
- }
423
- break;
424
- default:
425
- throw new Error(`Unsupported stack justify: ${String(justify)}`);
426
- }
669
+ if (justify === "center") startOffset = remainingMainAfterFill / 2;
670
+ else if (justify === "end") startOffset = remainingMainAfterFill;
671
+ else if (justify === "space-between")
672
+ actualGap = childIds.length <= 1 ? 0 : gap + remainingMainAfterFill / (childIds.length - 1);
427
673
  }
428
674
  const rects = {};
429
675
  let currentMain = startOffset;
@@ -431,134 +677,247 @@ function resolveStackChildRects(parentRect, arrange, childIds, document) {
431
677
  const childMain = childMainSizes[index];
432
678
  const childCross = childCrossSizes[index];
433
679
  let crossOffset = 0;
434
- switch (align) {
435
- case "start":
436
- break;
437
- case "center":
438
- crossOffset = (contentCross - childCross) / 2;
439
- break;
440
- case "end":
441
- crossOffset = contentCross - childCross;
442
- break;
443
- default:
444
- throw new Error(`Unsupported stack align: ${String(align)}`);
445
- }
446
- rects[childId] = isHorizontal ? { x: content.x + currentMain, y: content.y + crossOffset, width: childMain, height: childCross } : { x: content.x + crossOffset, y: content.y + currentMain, width: childCross, height: childMain };
680
+ if (align === "center") crossOffset = (contentCross - childCross) / 2;
681
+ else if (align === "end") crossOffset = contentCross - childCross;
682
+ rects[childId] = isHorizontal ? {
683
+ x: content.x + currentMain,
684
+ y: content.y + crossOffset,
685
+ width: childMain,
686
+ height: childCross
687
+ } : {
688
+ x: content.x + crossOffset,
689
+ y: content.y + currentMain,
690
+ width: childCross,
691
+ height: childMain
692
+ };
447
693
  currentMain += childMain + actualGap;
448
694
  });
449
695
  return rects;
450
696
  }
697
+ function validateGridTrack(track, axis, index) {
698
+ if (track.kind === "fixed") {
699
+ if (!Number.isFinite(track.size) || track.size < 0)
700
+ throw new MachinaLayoutError(
701
+ "InvalidGridTrack",
702
+ `${axis}[${index}].size must be finite and non-negative`
703
+ );
704
+ return;
705
+ }
706
+ if (track.kind === "fill") {
707
+ const weight = track.weight ?? 1;
708
+ if (!Number.isFinite(weight) || weight <= 0)
709
+ throw new MachinaLayoutError(
710
+ "InvalidGridTrack",
711
+ `${axis}[${index}].weight must be finite and greater than 0`
712
+ );
713
+ return;
714
+ }
715
+ throw new MachinaLayoutError("InvalidGridTrack", `${axis}[${index}] has unknown track kind`);
716
+ }
717
+ function resolveGridTracks(contentAxisSize, tracks, gap, axis) {
718
+ if (!Number.isFinite(gap) || gap < 0 || tracks.length === 0)
719
+ throw new MachinaLayoutError("InvalidGridTrack", `invalid ${axis} configuration`);
720
+ tracks.forEach((t, i) => {
721
+ validateGridTrack(t, axis, i);
722
+ });
723
+ const fixedTotal = tracks.reduce((s, t) => s + (t.kind === "fixed" ? t.size : 0), 0);
724
+ const gapTotal = gap * Math.max(0, tracks.length - 1);
725
+ const remaining = contentAxisSize - fixedTotal - gapTotal;
726
+ if (remaining < 0) throw new MachinaLayoutError("GridOverflow", `grid ${axis} overflow`);
727
+ const totalWeight = tracks.reduce((s, t) => s + (t.kind === "fill" ? t.weight ?? 1 : 0), 0);
728
+ const sizes = tracks.map(
729
+ (t) => t.kind === "fixed" ? t.size : totalWeight <= 0 ? 0 : remaining * (t.weight ?? 1) / totalWeight
730
+ );
731
+ let current = 0;
732
+ return sizes.map((size) => {
733
+ const r = { start: current, size };
734
+ current += size + gap;
735
+ return r;
736
+ });
737
+ }
738
+ function resolveGridChildRect(childNode, columns, rows, columnGap, rowGap, content) {
739
+ if (childNode.frame.kind !== "cell")
740
+ throw new MachinaLayoutError(
741
+ "GridChildMustBeCell",
742
+ `grid child must use cell frame: ${childNode.id}`
743
+ );
744
+ const { row, col } = childNode.frame;
745
+ const rowSpan = childNode.frame.rowSpan ?? 1;
746
+ const colSpan = childNode.frame.colSpan ?? 1;
747
+ if (!Number.isInteger(row) || row < 0 || !Number.isInteger(col) || col < 0 || !Number.isInteger(rowSpan) || rowSpan <= 0 || !Number.isInteger(colSpan) || colSpan <= 0)
748
+ throw new MachinaLayoutError(
749
+ "InvalidGridCell",
750
+ `invalid cell coordinates/spans for node ${childNode.id}`
751
+ );
752
+ if (row + rowSpan > rows.length || col + colSpan > columns.length)
753
+ throw new MachinaLayoutError(
754
+ "InvalidGridCell",
755
+ `cell exceeds grid bounds for node ${childNode.id}`
756
+ );
757
+ const x = content.x + columns[col].start;
758
+ const y = content.y + rows[row].start;
759
+ let width = columnGap * (colSpan - 1);
760
+ for (let i = col; i < col + colSpan; i += 1) width += columns[i].size;
761
+ let height = rowGap * (rowSpan - 1);
762
+ for (let i = row; i < row + rowSpan; i += 1) height += rows[i].size;
763
+ return { x, y, width, height };
764
+ }
451
765
  function resolveLayoutDocument(document, rootRect) {
452
- validateRootRect(rootRect);
766
+ validateRootRect2(rootRect);
453
767
  const rootNode = document.nodes[document.rootId];
454
- if (!rootNode) {
768
+ if (!rootNode)
455
769
  throw new MachinaLayoutError("MissingRoot", `root node not found for id: ${document.rootId}`);
456
- }
457
770
  const resolvedNodes = {};
458
771
  const resolvedChildren = {};
459
772
  const visitState = /* @__PURE__ */ new Map();
460
- let visitedCount = 0;
773
+ const pendingGuides = /* @__PURE__ */ new Map();
461
774
  const resolveNode = (nodeId, rect) => {
462
775
  const state = visitState.get(nodeId) ?? 0;
463
- if (state === 1) {
464
- throw new MachinaLayoutError("Cycle", `cycle detected at node ${nodeId}`);
465
- }
466
- if (state === 2) {
467
- return;
468
- }
776
+ if (state === 1) throw new MachinaLayoutError("Cycle", `cycle detected at node ${nodeId}`);
777
+ if (state === 2) return;
469
778
  const node = document.nodes[nodeId];
470
- if (!node) {
471
- throw new MachinaLayoutError("UnknownParent", `node referenced in children but missing from nodes: ${nodeId}`);
472
- }
779
+ if (!node)
780
+ throw new MachinaLayoutError(
781
+ "UnknownParent",
782
+ `node referenced in children but missing from nodes: ${nodeId}`
783
+ );
473
784
  visitState.set(nodeId, 1);
474
- visitedCount += 1;
475
785
  resolvedNodes[nodeId] = {
476
786
  id: node.id,
477
787
  z: node.z,
478
- rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
788
+ rect: { ...rect },
479
789
  frame: node.frame,
480
790
  arrange: node.arrange,
481
791
  view: node.view,
482
792
  slot: node.slot,
483
793
  debugLabel: node.debugLabel,
794
+ layer: node.layer,
484
795
  offset: node.offset
485
796
  };
486
797
  const childIds = document.children[nodeId] ?? [];
487
798
  resolvedChildren[nodeId] = [...childIds];
488
- const childRects = node.arrange?.kind === "stack" ? resolveStackChildRects(rect, node.arrange, childIds, document) : void 0;
799
+ let childRects;
800
+ if (node.arrange?.kind === "stack")
801
+ childRects = resolveStackChildRects(rect, node.arrange, childIds, document);
802
+ else if (node.arrange?.kind === "grid") {
803
+ const columnGap = node.arrange.columnGap ?? 0;
804
+ const rowGap = node.arrange.rowGap ?? 0;
805
+ const padding = normalizePadding(node.arrange.padding);
806
+ const content = {
807
+ x: rect.x + padding.left,
808
+ y: rect.y + padding.top,
809
+ width: rect.width - padding.left - padding.right,
810
+ height: rect.height - padding.top - padding.bottom
811
+ };
812
+ if (content.width < 0 || content.height < 0)
813
+ throw new MachinaLayoutError(
814
+ "GridContentNegative",
815
+ "grid content size cannot be negative after applying padding"
816
+ );
817
+ const columns = resolveGridTracks(content.width, node.arrange.columns, columnGap, "columns");
818
+ const rows = resolveGridTracks(content.height, node.arrange.rows, rowGap, "rows");
819
+ childRects = {};
820
+ for (const childId of childIds) {
821
+ const childNode = document.nodes[childId];
822
+ if (!childNode)
823
+ throw new MachinaLayoutError(
824
+ "UnknownParent",
825
+ `child id ${childId} referenced by ${nodeId} is missing`
826
+ );
827
+ childRects[childId] = resolveGridChildRect(
828
+ childNode,
829
+ columns,
830
+ rows,
831
+ columnGap,
832
+ rowGap,
833
+ content
834
+ );
835
+ }
836
+ }
489
837
  for (const childId of childIds) {
490
838
  const childNode = document.nodes[childId];
491
- if (!childNode) {
492
- throw new MachinaLayoutError("UnknownParent", `child id ${childId} referenced by ${nodeId} is missing`);
839
+ if (!childNode)
840
+ throw new MachinaLayoutError(
841
+ "UnknownParent",
842
+ `child id ${childId} referenced by ${nodeId} is missing`
843
+ );
844
+ if (childNode.frame.kind === "guide" && !childRects) {
845
+ validateGuideFrame(childId, childNode.frame, document);
846
+ pendingGuides.set(childId, { nodeId: childId, parentId: nodeId });
847
+ continue;
493
848
  }
494
849
  const normalChildRect = childRects?.[childId] ?? resolveFrame(rect, childNode.frame);
495
- const childRect = applyOffset(normalChildRect, rect, childNode.offset);
496
- resolveNode(childId, childRect);
850
+ resolveNode(childId, applyOffset(normalChildRect, rect, childNode.offset));
497
851
  }
498
852
  visitState.set(nodeId, 2);
499
853
  };
500
- resolveNode(document.rootId, { x: rootRect.x, y: rootRect.y, width: rootRect.width, height: rootRect.height });
501
- if (visitedCount !== Object.keys(document.nodes).length) {
502
- throw new MachinaLayoutError("UnreachableNode", "one or more nodes are unreachable from the root.");
503
- }
504
- return {
505
- rootId: document.rootId,
506
- nodes: resolvedNodes,
507
- children: resolvedChildren
854
+ const processPending = () => {
855
+ while (pendingGuides.size > 0) {
856
+ let progressed = false;
857
+ for (const [id, pending] of [...pendingGuides.entries()]) {
858
+ const parentResolved = resolvedNodes[pending.parentId];
859
+ const node = document.nodes[id];
860
+ if (!parentResolved || !node || node.frame.kind !== "guide") continue;
861
+ const refs = [node.frame.left, node.frame.right, node.frame.top, node.frame.bottom].filter(
862
+ isEdgeRef
863
+ );
864
+ if (refs.some((r) => !resolvedNodes[r.ref])) continue;
865
+ const rect = resolveGuideFrame(parentResolved.rect, node.frame, resolvedNodes);
866
+ resolveNode(id, applyOffset(rect, parentResolved.rect, node.offset));
867
+ pendingGuides.delete(id);
868
+ progressed = true;
869
+ }
870
+ if (pendingGuides.size === 0) return;
871
+ if (progressed) continue;
872
+ const remaining = /* @__PURE__ */ new Set([...pendingGuides.keys()]);
873
+ const visiting = /* @__PURE__ */ new Set();
874
+ const visited = /* @__PURE__ */ new Set();
875
+ const hasCycle = (id) => {
876
+ if (visiting.has(id)) return true;
877
+ if (visited.has(id)) return false;
878
+ visiting.add(id);
879
+ const node = document.nodes[id];
880
+ if (node?.frame.kind === "guide") {
881
+ for (const ref of [
882
+ node.frame.left,
883
+ node.frame.right,
884
+ node.frame.top,
885
+ node.frame.bottom
886
+ ].filter(isEdgeRef)) {
887
+ if (remaining.has(ref.ref) && hasCycle(ref.ref)) return true;
888
+ }
889
+ }
890
+ visiting.delete(id);
891
+ visited.add(id);
892
+ return false;
893
+ };
894
+ for (const id of remaining) {
895
+ if (hasCycle(id))
896
+ throw new MachinaLayoutError("GuideReferenceCycle", "guide reference cycle detected");
897
+ }
898
+ throw new MachinaLayoutError(
899
+ "GuideTargetUnresolved",
900
+ "one or more guide targets could not be resolved"
901
+ );
902
+ }
508
903
  };
904
+ resolveNode(document.rootId, { ...rootRect });
905
+ processPending();
906
+ if (Object.keys(resolvedNodes).length !== Object.keys(document.nodes).length)
907
+ throw new MachinaLayoutError(
908
+ "UnreachableNode",
909
+ "one or more nodes are unreachable from the root."
910
+ );
911
+ return { rootId: document.rootId, nodes: resolvedNodes, children: resolvedChildren };
509
912
  }
510
913
 
511
914
  // src/resolveLayoutRows.ts
512
915
  function resolveLayoutRows(rows, rootRect) {
513
- const document = compileLayoutRows(rows);
916
+ const selectedRows = selectLayoutRowsForRoot(rows, rootRect);
917
+ const document = compileLayoutRows(selectedRows);
514
918
  return resolveLayoutDocument(document, rootRect);
515
919
  }
516
920
 
517
- // src/toResolvedTree.ts
518
- function toResolvedTree(document) {
519
- const root = document.nodes[document.rootId];
520
- if (!root) {
521
- throw new MachinaLayoutError("MissingRoot", `root node '${document.rootId}' is missing`);
522
- }
523
- const visiting = /* @__PURE__ */ new Set();
524
- const visited = /* @__PURE__ */ new Set();
525
- const build = (node) => {
526
- if (visiting.has(node.id)) {
527
- throw new MachinaLayoutError("Cycle", `cycle detected at '${node.id}'`);
528
- }
529
- visiting.add(node.id);
530
- visited.add(node.id);
531
- const childIds = document.children[node.id] ?? [];
532
- const children = childIds.map((childId) => {
533
- const child = document.nodes[childId];
534
- if (!child) {
535
- throw new MachinaLayoutError("UnknownParent", `missing child node '${childId}' referenced by '${node.id}'`);
536
- }
537
- return build(child);
538
- });
539
- visiting.delete(node.id);
540
- return {
541
- id: node.id,
542
- z: node.z,
543
- rect: { ...node.rect },
544
- frame: node.frame,
545
- arrange: node.arrange,
546
- view: node.view,
547
- slot: node.slot,
548
- debugLabel: node.debugLabel,
549
- offset: node.offset,
550
- children
551
- };
552
- };
553
- const tree = build(root);
554
- for (const nodeId of Object.keys(document.nodes)) {
555
- if (!visited.has(nodeId)) {
556
- throw new MachinaLayoutError("UnreachableNode", `node '${nodeId}' is unreachable from root '${document.rootId}'`);
557
- }
558
- }
559
- return tree;
560
- }
561
-
562
921
  // src/flattenResolvedTree.ts
563
922
  function flattenResolvedTree(tree) {
564
923
  const out = [];
@@ -572,6 +931,7 @@ function flattenResolvedTree(tree) {
572
931
  view: node.view,
573
932
  slot: node.slot,
574
933
  debugLabel: node.debugLabel,
934
+ layer: node.layer,
575
935
  offset: node.offset
576
936
  });
577
937
  for (const child of node.children) {
@@ -587,464 +947,105 @@ function formatRect(rect) {
587
947
  return `x=${rect.x} y=${rect.y} w=${rect.width} h=${rect.height}`;
588
948
  }
589
949
 
590
- // src/react/MachinaReactView.tsx
591
- import React from "react";
592
- import { jsx, jsxs } from "react/jsx-runtime";
593
- function renderNode(node, parentRect, views, viewData, nodeData, nodeClassName, debug, nodeContainment, nodeContentVisibility, nodeContainIntrinsicSize, nodesById) {
594
- const viewKey = node.view ?? node.slot;
595
- const View = viewKey ? views[viewKey] : void 0;
596
- const selectedViewData = viewKey ? viewData?.[viewKey] : void 0;
597
- const selectedNodeData = nodeData?.[node.id];
598
- const left = node.rect.x - parentRect.x;
599
- const top = node.rect.y - parentRect.y;
600
- const style = {
601
- position: "absolute",
602
- left,
603
- top,
604
- width: node.rect.width,
605
- height: node.rect.height,
606
- boxSizing: "border-box",
607
- zIndex: node.z ?? 0,
608
- ...nodeContainment === "layout-paint" ? { contain: "layout paint" } : null,
609
- ...nodeContainment === "strict" ? { contain: "strict" } : null,
610
- ...nodeContentVisibility === "auto" ? { contentVisibility: "auto" } : null,
611
- ...nodeContainIntrinsicSize !== void 0 ? { containIntrinsicSize: nodeContainIntrinsicSize } : null,
612
- ...debug ? { outline: "1px dashed rgba(59, 130, 246, 0.9)" } : null
613
- };
614
- const renderedSlot = View && nodesById[node.id] ? React.createElement(View, {
615
- id: node.id,
616
- rect: { ...node.rect },
617
- debugLabel: node.debugLabel,
618
- node: { ...nodesById[node.id], rect: { ...nodesById[node.id].rect } },
619
- viewKey,
620
- viewData: selectedViewData,
621
- nodeData: selectedNodeData
622
- }) : null;
623
- return /* @__PURE__ */ jsxs(
624
- "div",
625
- {
626
- "data-testid": `machina-node-${node.id}`,
627
- className: nodeClassName,
628
- style,
629
- "data-machina-node-id": node.id,
630
- "data-machina-slot": node.slot,
631
- "data-machina-view": viewKey,
632
- "data-machina-debug-label": node.debugLabel,
633
- children: [
634
- debug ? /* @__PURE__ */ jsx("small", { children: node.debugLabel ?? node.id }) : null,
635
- renderedSlot,
636
- [...node.children].map((child, index) => ({ child, index })).sort((a, b) => (a.child.z ?? 0) - (b.child.z ?? 0) || a.index - b.index).map(
637
- ({ child }) => renderNode(
638
- child,
639
- node.rect,
640
- views,
641
- viewData,
642
- nodeData,
643
- nodeClassName,
644
- debug,
645
- nodeContainment,
646
- nodeContentVisibility,
647
- nodeContainIntrinsicSize,
648
- nodesById
649
- )
650
- )
651
- ]
652
- },
653
- node.id
654
- );
655
- }
656
- function MachinaReactView(props) {
657
- const {
658
- layout,
659
- views = {},
660
- viewData,
661
- nodeData,
662
- className,
663
- style,
664
- nodeClassName,
665
- debug,
666
- nodeContainment = "layout-paint",
667
- nodeContentVisibility = "none",
668
- nodeContainIntrinsicSize
669
- } = props;
670
- const tree = toResolvedTree(layout);
671
- const wrapperStyle = {
672
- position: "relative",
673
- width: tree.rect.width,
674
- height: tree.rect.height,
675
- ...style
676
- };
677
- return /* @__PURE__ */ jsx("div", { className, style: wrapperStyle, "data-machina-root-id": tree.id, children: renderNode(
678
- tree,
679
- tree.rect,
680
- views,
681
- viewData,
682
- nodeData,
683
- nodeClassName,
684
- debug,
685
- nodeContainment,
686
- nodeContentVisibility,
687
- nodeContainIntrinsicSize,
688
- layout.nodes
689
- ) });
690
- }
691
-
692
- // src/text/parseMachinaText.ts
693
- function makeDiagnostic(code, message, index, length, line, column) {
694
- return { code, message, index, length, line, column, level: "error" };
695
- }
696
- function toLines(source) {
697
- const lines = [];
698
- let i = 0;
699
- let line = 1;
700
- while (i <= source.length) {
701
- const start = i;
702
- while (i < source.length && source[i] !== "\n" && source[i] !== "\r") i += 1;
703
- const text = source.slice(start, i);
704
- lines.push({ text, index: start, line });
705
- if (i >= source.length) break;
706
- if (source[i] === "\r" && source[i + 1] === "\n") i += 2;
707
- else i += 1;
708
- line += 1;
950
+ // src/lerp.ts
951
+ function assertFiniteNumber2(value) {
952
+ if (!Number.isFinite(value)) {
953
+ throw new MachinaLayoutError(
954
+ "NonFiniteNumber",
955
+ `Expected finite number, got ${String(value)}.`
956
+ );
709
957
  }
710
- return lines;
711
958
  }
712
- function parseInline(text, lineIndex, line) {
713
- const diagnostics = [];
714
- const inline = [];
715
- let cursor = 0;
716
- const pushText = (t) => {
717
- if (!t) return;
718
- const prev = inline[inline.length - 1];
719
- if (prev?.kind === "text") prev.text += t;
720
- else inline.push({ kind: "text", text: t });
721
- };
722
- const allowedEscapes = /* @__PURE__ */ new Set(["\\", "*", "`", "[", "]", "(", ")", "-"]);
723
- const consumeEscape = () => {
724
- if (text[cursor] !== "\\") return false;
725
- if (cursor === text.length - 1) {
726
- diagnostics.push(makeDiagnostic("invalid_escape", "Dangling escape sequence.", lineIndex + cursor, 1, line, cursor + 1));
727
- pushText("\\");
728
- cursor += 1;
729
- return true;
730
- }
731
- const escaped = text[cursor + 1];
732
- if (allowedEscapes.has(escaped)) {
733
- pushText(escaped);
734
- cursor += 2;
735
- return true;
736
- }
737
- diagnostics.push(makeDiagnostic("invalid_escape", `Unsupported escape sequence: \\${escaped}`, lineIndex + cursor, 2, line, cursor + 1));
738
- pushText(escaped);
739
- cursor += 2;
740
- return true;
741
- };
742
- while (cursor < text.length) {
743
- if (consumeEscape()) continue;
744
- if (text.startsWith("![", cursor)) {
745
- diagnostics.push(makeDiagnostic("unsupported_syntax", "Images are not supported.", lineIndex + cursor, 2, line, cursor + 1));
746
- pushText("![");
747
- cursor += 2;
748
- continue;
749
- }
750
- if (text[cursor] === "`") {
751
- const close = text.indexOf("`", cursor + 1);
752
- if (close < 0) {
753
- diagnostics.push(makeDiagnostic("unclosed_inline", "Unclosed inline code marker.", lineIndex + cursor, text.length - cursor, line, cursor + 1));
754
- pushText(text.slice(cursor));
755
- break;
756
- }
757
- inline.push({ kind: "code", text: text.slice(cursor + 1, close) });
758
- cursor = close + 1;
759
- continue;
760
- }
761
- if (text.startsWith("**", cursor)) {
762
- const close = text.indexOf("**", cursor + 2);
763
- if (close < 0) {
764
- diagnostics.push(makeDiagnostic("unclosed_inline", "Unclosed strong marker.", lineIndex + cursor, text.length - cursor, line, cursor + 1));
765
- pushText(text.slice(cursor));
766
- break;
767
- }
768
- const children = parseInline(text.slice(cursor + 2, close), lineIndex + cursor + 2, line);
769
- diagnostics.push(...children.diagnostics);
770
- inline.push({ kind: "strong", children: children.inline });
771
- cursor = close + 2;
772
- continue;
773
- }
774
- if (text[cursor] === "*") {
775
- const close = text.indexOf("*", cursor + 1);
776
- if (close < 0) {
777
- diagnostics.push(makeDiagnostic("unclosed_inline", "Unclosed emphasis marker.", lineIndex + cursor, text.length - cursor, line, cursor + 1));
778
- pushText(text.slice(cursor));
779
- break;
780
- }
781
- const children = parseInline(text.slice(cursor + 1, close), lineIndex + cursor + 1, line);
782
- diagnostics.push(...children.diagnostics);
783
- inline.push({ kind: "emphasis", children: children.inline });
784
- cursor = close + 1;
785
- continue;
786
- }
787
- if (text[cursor] === "[") {
788
- const closeBracket = text.indexOf("]", cursor + 1);
789
- if (closeBracket < 0 || text[closeBracket + 1] !== "(") {
790
- diagnostics.push(makeDiagnostic("malformed_link", "Malformed link syntax.", lineIndex + cursor, Math.max(1, text.length - cursor), line, cursor + 1));
791
- pushText("[");
792
- cursor += 1;
793
- continue;
794
- }
795
- const closeParen = text.indexOf(")", closeBracket + 2);
796
- if (closeParen < 0) {
797
- diagnostics.push(makeDiagnostic("malformed_link", "Malformed link syntax.", lineIndex + cursor, text.length - cursor, line, cursor + 1));
798
- pushText(text.slice(cursor));
799
- break;
800
- }
801
- const label = text.slice(cursor + 1, closeBracket);
802
- const href = text.slice(closeBracket + 2, closeParen);
803
- if (label.length === 0) {
804
- diagnostics.push(makeDiagnostic("malformed_link", "Link label cannot be empty.", lineIndex + cursor, closeParen - cursor + 1, line, cursor + 1));
805
- pushText(text.slice(cursor, closeParen + 1));
806
- cursor = closeParen + 1;
807
- continue;
808
- }
809
- const labelInline = parseInline(label, lineIndex + cursor + 1, line);
810
- diagnostics.push(...labelInline.diagnostics);
811
- inline.push({ kind: "link", href, children: labelInline.inline });
812
- cursor = closeParen + 1;
813
- continue;
814
- }
815
- const specials = ["![", "`", "**", "*", "[", "\\"];
816
- let next = text.length;
817
- for (const special of specials) {
818
- const p = text.indexOf(special, cursor);
819
- if (p >= 0 && p < next) next = p;
820
- }
821
- if (next === cursor) {
822
- pushText(text[cursor]);
823
- cursor += 1;
824
- continue;
959
+ function sameStringArray(a, b) {
960
+ if (a.length !== b.length) {
961
+ return false;
962
+ }
963
+ for (let index = 0; index < a.length; index += 1) {
964
+ if (a[index] !== b[index]) {
965
+ return false;
825
966
  }
826
- pushText(text.slice(cursor, next));
827
- cursor = next;
828
967
  }
829
- return { inline, diagnostics };
830
- }
831
- function classifyForbiddenBlock(line) {
832
- if (/^#{1,6}\s+/.test(line)) return "heading_forbidden";
833
- if (/^\d+\.\s+/.test(line)) return "unsupported_syntax";
834
- if (/^\s*-\s+\[[ xX]\]\s+/.test(line)) return "unsupported_syntax";
835
- if (/^>\s+/.test(line)) return "unsupported_syntax";
836
- if (/^```/.test(line)) return "unsupported_syntax";
837
- if (/^\s*<\/?[a-zA-Z][^>]*>/.test(line)) return "unsupported_syntax";
838
- if (/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line)) return "unsupported_syntax";
839
- return void 0;
968
+ return true;
840
969
  }
841
- function parseBulletLine(line) {
842
- if (line.startsWith("\\- ")) return void 0;
843
- if (line.startsWith("- ")) return { depth: 1, text: line.slice(2) };
844
- if (line.startsWith(" - ")) return { depth: 2, text: line.slice(4) };
845
- if (line.startsWith(" - ")) return { depth: 3, text: line.slice(6) };
846
- return void 0;
847
- }
848
- function parseMachinaTextInline(text) {
849
- return parseInline(text, 0, 1);
850
- }
851
- function parseMachinaText(source) {
852
- const src = typeof source === "string" ? { kind: "machina-text", text: source } : source;
853
- if (src?.kind !== "plain" && src?.kind !== "machina-text") {
854
- const diagnostic = makeDiagnostic("unsupported_syntax", "Unsupported MachinaText source kind.", 0, 0, 1, 1);
855
- return { ok: false, document: { blocks: [] }, diagnostics: [diagnostic] };
970
+ function assertCompatibleResolvedLayouts(a, b) {
971
+ if (a.rootId !== b.rootId) {
972
+ throw new MachinaLayoutError(
973
+ "IncompatibleLayouts",
974
+ `Layout roots differ: ${a.rootId} !== ${b.rootId}.`
975
+ );
856
976
  }
857
- if (src.kind === "plain") {
858
- return {
859
- ok: true,
860
- document: { blocks: [{ kind: "paragraph", inline: [{ kind: "text", text: src.text }] }] },
861
- diagnostics: []
862
- };
977
+ if (!(a.rootId in a.nodes) || !(b.rootId in b.nodes)) {
978
+ throw new MachinaLayoutError(
979
+ "IncompatibleLayouts",
980
+ `Root id ${a.rootId} must exist in both node maps.`
981
+ );
863
982
  }
864
- const blocks = [];
865
- const diagnostics = [];
866
- const lines = toLines(src.text);
867
- let i = 0;
868
- while (i < lines.length) {
869
- const lineInfo = lines[i];
870
- const trimmed = lineInfo.text.trim();
871
- if (trimmed.length === 0) {
872
- i += 1;
873
- continue;
874
- }
875
- const forbiddenCode = classifyForbiddenBlock(lineInfo.text);
876
- if (forbiddenCode) {
877
- const code = forbiddenCode;
878
- diagnostics.push(makeDiagnostic(code, "Unsupported block syntax.", lineInfo.index, lineInfo.text.length || 1, lineInfo.line, 1));
879
- blocks.push({ kind: "paragraph", inline: [{ kind: "text", text: lineInfo.text }] });
880
- i += 1;
881
- continue;
882
- }
883
- const bullet = parseBulletLine(lineInfo.text);
884
- if (bullet) {
885
- const items = [];
886
- let lastTop;
887
- while (i < lines.length) {
888
- const current = lines[i];
889
- if (current.text.trim().length === 0) break;
890
- const currentBullet = parseBulletLine(current.text);
891
- if (!currentBullet) break;
892
- if (/^\s*-\s+\[[ xX]\]\s+/.test(current.text)) {
893
- diagnostics.push(makeDiagnostic("unsupported_syntax", "Task lists are not supported.", current.index, current.text.length || 1, current.line, 1));
894
- }
895
- if (currentBullet.depth > 2) {
896
- diagnostics.push(makeDiagnostic("max_list_depth_exceeded", "Maximum bullet depth is 2.", current.index, current.text.length || 1, current.line, 1));
897
- const parsed3 = parseInline(current.text.trim(), current.index + (current.text.length - current.text.trimStart().length), current.line);
898
- diagnostics.push(...parsed3.diagnostics);
899
- blocks.push({ kind: "paragraph", inline: parsed3.inline.length ? parsed3.inline : [{ kind: "text", text: current.text }] });
900
- i += 1;
901
- continue;
902
- }
903
- const parsed2 = parseInline(currentBullet.text, current.index + (currentBullet.depth === 1 ? 2 : 4), current.line);
904
- diagnostics.push(...parsed2.diagnostics);
905
- const item = { inline: parsed2.inline };
906
- if (currentBullet.depth === 1) {
907
- items.push(item);
908
- lastTop = item;
909
- } else if (lastTop) {
910
- if (!lastTop.children) lastTop.children = [];
911
- lastTop.children.push(item);
912
- } else {
913
- diagnostics.push(makeDiagnostic("unsupported_syntax", "Nested bullet requires a parent bullet.", current.index, current.text.length || 1, current.line, 1));
914
- blocks.push({ kind: "paragraph", inline: [{ kind: "text", text: current.text }] });
915
- }
916
- i += 1;
917
- }
918
- blocks.push({ kind: "bulletList", items });
919
- continue;
920
- }
921
- const paragraphLines = [];
922
- while (i < lines.length && lines[i].text.trim().length > 0 && !parseBulletLine(lines[i].text) && !classifyForbiddenBlock(lines[i].text)) {
923
- paragraphLines.push(lines[i]);
924
- i += 1;
983
+ const aNodeIds = Object.keys(a.nodes).sort();
984
+ const bNodeIds = Object.keys(b.nodes).sort();
985
+ if (!sameStringArray(aNodeIds, bNodeIds)) {
986
+ throw new MachinaLayoutError(
987
+ "IncompatibleLayouts",
988
+ "Resolved layouts must have the same node ids."
989
+ );
990
+ }
991
+ const aParentIds = Object.keys(a.children).sort();
992
+ const bParentIds = Object.keys(b.children).sort();
993
+ if (!sameStringArray(aParentIds, bParentIds)) {
994
+ throw new MachinaLayoutError(
995
+ "IncompatibleLayouts",
996
+ "Resolved layouts must have the same parent-child map."
997
+ );
998
+ }
999
+ for (const parentId of aParentIds) {
1000
+ const aChildren = a.children[parentId] ?? [];
1001
+ const bChildren = b.children[parentId] ?? [];
1002
+ if (!sameStringArray(aChildren, bChildren)) {
1003
+ throw new MachinaLayoutError(
1004
+ "IncompatibleLayouts",
1005
+ `Child order differs for parent ${parentId}.`
1006
+ );
925
1007
  }
926
- const paragraphText = paragraphLines.map((line) => line.text).join("\n");
927
- const first = paragraphLines[0];
928
- const parsed = parseInline(paragraphText, first?.index ?? 0, first?.line ?? 1);
929
- diagnostics.push(...parsed.diagnostics);
930
- blocks.push({ kind: "paragraph", inline: parsed.inline });
931
1008
  }
932
- return { ok: diagnostics.every((d) => d.level !== "error"), document: { blocks }, diagnostics };
933
- }
934
-
935
- // src/text/react/MachinaTextView.tsx
936
- import React2 from "react";
937
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
938
- var DEFAULT_POLICY = { variant: "body", wrap: "word", overflow: "clip", align: "start", leading: "normal", blockGap: 8, listGap: 2, valign: "top" };
939
- var INLINE_CODE_FONT = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
940
- var VARIANT_STYLE = {
941
- body: { fontSize: "14px", fontWeight: 400, lineHeight: 1.4 },
942
- label: { fontSize: "12px", fontWeight: 500, lineHeight: 1.3 },
943
- caption: { fontSize: "11px", fontWeight: 400, lineHeight: 1.25, opacity: 0.8 },
944
- title: { fontSize: "18px", fontWeight: 700, lineHeight: 1.25 },
945
- mono: { fontSize: "12px", lineHeight: 1.35, fontFamily: INLINE_CODE_FONT }
946
- };
947
- function isMachinaTextDocument(value) {
948
- return typeof value === "object" && value !== null && "blocks" in value;
949
- }
950
- function isMachinaTextSpec(value) {
951
- return typeof value === "object" && value !== null && "kind" in value && value.kind === "text";
952
1009
  }
953
- function normalizePositive(value, fallback) {
954
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
955
- }
956
- function normalizeNonNegative(value, fallback) {
957
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
1010
+ function copyChildren(children) {
1011
+ const copied = {};
1012
+ for (const [parentId, childIds] of Object.entries(children)) {
1013
+ copied[parentId] = [...childIds];
1014
+ }
1015
+ return copied;
958
1016
  }
959
- function normalizeLeading(value) {
960
- if (value === void 0) return DEFAULT_POLICY.leading;
961
- if (value === "tight" || value === "normal" || value === "loose") return value;
962
- return normalizePositive(value, resolveLineHeight(DEFAULT_POLICY));
1017
+ function lerpNumber(a, b, t) {
1018
+ assertFiniteNumber2(a);
1019
+ assertFiniteNumber2(b);
1020
+ assertFiniteNumber2(t);
1021
+ return a + (b - a) * t;
963
1022
  }
964
- function normalizeSpecPolicy(spec) {
1023
+ function lerpRect(a, b, t) {
965
1024
  return {
966
- variant: spec.variant ?? DEFAULT_POLICY.variant,
967
- wrap: spec.wrap ?? DEFAULT_POLICY.wrap,
968
- overflow: spec.overflow ?? DEFAULT_POLICY.overflow,
969
- align: spec.align ?? DEFAULT_POLICY.align,
970
- leading: normalizeLeading(spec.leading),
971
- blockGap: normalizeNonNegative(spec.blockGap, DEFAULT_POLICY.blockGap),
972
- listGap: normalizeNonNegative(spec.listGap, DEFAULT_POLICY.listGap),
973
- valign: spec.valign ?? DEFAULT_POLICY.valign
1025
+ x: lerpNumber(a.x, b.x, t),
1026
+ y: lerpNumber(a.y, b.y, t),
1027
+ width: lerpNumber(a.width, b.width, t),
1028
+ height: lerpNumber(a.height, b.height, t)
974
1029
  };
975
1030
  }
976
- function normalizeText(text) {
977
- if (isMachinaTextDocument(text)) return { document: text, diagnostics: [], policy: DEFAULT_POLICY };
978
- if (isMachinaTextSpec(text)) {
979
- const result2 = parseMachinaText(text.source);
980
- return { document: result2.document, diagnostics: result2.diagnostics, policy: normalizeSpecPolicy(text) };
1031
+ function lerpResolvedLayouts(a, b, t) {
1032
+ assertFiniteNumber2(t);
1033
+ assertCompatibleResolvedLayouts(a, b);
1034
+ const nodes = {};
1035
+ for (const id of Object.keys(b.nodes)) {
1036
+ const aNode = a.nodes[id];
1037
+ const bNode = b.nodes[id];
1038
+ nodes[id] = {
1039
+ ...bNode,
1040
+ rect: lerpRect(aNode.rect, bNode.rect, t)
1041
+ };
981
1042
  }
982
- const result = parseMachinaText(typeof text === "string" ? { kind: "machina-text", text } : text);
983
- return { document: result.document, diagnostics: result.diagnostics, policy: DEFAULT_POLICY };
984
- }
985
- function resolveLineHeight(policy) {
986
- if (policy.leading === "tight") return 1.15;
987
- if (policy.leading === "loose") return 1.6;
988
- if (typeof policy.leading === "number") return policy.leading;
989
- return VARIANT_STYLE[policy.variant].lineHeight;
990
- }
991
- function policyStyle(policy) {
992
- const wrapStyle = { word: { whiteSpace: "normal", overflowWrap: "anywhere" }, none: { whiteSpace: "nowrap" } };
993
- const overflowStyle = {
994
- clip: { overflow: "hidden" },
995
- ellipsis: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
996
- scroll: { overflow: "auto" }
997
- };
998
- const alignStyle = { start: { textAlign: "left" }, center: { textAlign: "center" }, end: { textAlign: "right" } };
999
- const justifyContent = {
1000
- top: "flex-start",
1001
- center: "center",
1002
- bottom: "flex-end"
1003
- };
1004
1043
  return {
1005
- width: "100%",
1006
- height: "100%",
1007
- boxSizing: "border-box",
1008
- display: "flex",
1009
- flexDirection: "column",
1010
- justifyContent: justifyContent[policy.valign],
1011
- minWidth: 0,
1012
- ...VARIANT_STYLE[policy.variant],
1013
- lineHeight: resolveLineHeight(policy),
1014
- ...wrapStyle[policy.wrap],
1015
- ...overflowStyle[policy.overflow],
1016
- ...alignStyle[policy.align]
1044
+ rootId: b.rootId,
1045
+ nodes,
1046
+ children: copyChildren(b.children)
1017
1047
  };
1018
1048
  }
1019
- function renderInline(inline, key, props) {
1020
- switch (inline.kind) {
1021
- case "text":
1022
- return /* @__PURE__ */ jsx2(React2.Fragment, { children: inline.text }, key);
1023
- case "strong":
1024
- return /* @__PURE__ */ jsx2("strong", { children: inline.children.map((c, i) => renderInline(c, `${key}-s-${i}`, props)) }, key);
1025
- case "emphasis":
1026
- return /* @__PURE__ */ jsx2("em", { children: inline.children.map((c, i) => renderInline(c, `${key}-e-${i}`, props)) }, key);
1027
- case "code":
1028
- return /* @__PURE__ */ jsx2("code", { style: { fontFamily: INLINE_CODE_FONT, backgroundColor: "rgba(127, 127, 127, 0.15)", borderRadius: 3, padding: "0 0.25em" }, children: inline.text }, key);
1029
- case "link": {
1030
- const rel = props.linkTarget === "_blank" ? "noreferrer noopener" : void 0;
1031
- return /* @__PURE__ */ jsx2("a", { href: inline.href, target: props.linkTarget, rel, onClick: (event) => props.onLinkClick?.(inline.href, event), children: inline.children.map((c, i) => renderInline(c, `${key}-l-${i}`, props)) }, key);
1032
- }
1033
- }
1034
- }
1035
- function renderBulletItem(item, path, props, listGap) {
1036
- return /* @__PURE__ */ jsxs2("li", { style: { marginBottom: listGap }, children: [
1037
- item.inline.map((i, idx) => renderInline(i, `${path}-i-${idx}`, props)),
1038
- item.children?.length ? /* @__PURE__ */ jsx2("ul", { style: { margin: "0.25em 0 0 0", paddingLeft: "1.25em" }, children: item.children.map((c, idx) => renderBulletItem(c, `${path}-c-${idx}`, props, listGap)) }) : null
1039
- ] }, path);
1040
- }
1041
- function MachinaTextView(props) {
1042
- const normalized = normalizeText(props.text);
1043
- return /* @__PURE__ */ jsx2("div", { className: props.className, style: { ...policyStyle(normalized.policy), ...props.style }, children: /* @__PURE__ */ jsxs2("div", { style: { minWidth: 0 }, children: [
1044
- normalized.document.blocks.map((block, index) => block.kind === "paragraph" ? /* @__PURE__ */ jsx2("p", { style: { margin: index === normalized.document.blocks.length - 1 ? "0" : `0 0 ${normalized.policy.blockGap}px 0` }, children: block.inline.map((i, idx) => renderInline(i, `b-${index}-${idx}`, props)) }, `b-${index}`) : /* @__PURE__ */ jsx2("ul", { style: { margin: index === normalized.document.blocks.length - 1 ? "0" : `0 0 ${normalized.policy.blockGap}px 0`, paddingLeft: "1.25em" }, children: block.items.map((item, itemIndex) => renderBulletItem(item, `b-${index}-item-${itemIndex}`, props, normalized.policy.listGap)) }, `b-${index}`)),
1045
- props.showDiagnostics && normalized.diagnostics.length > 0 ? /* @__PURE__ */ jsx2("pre", { style: { margin: `${normalized.policy.blockGap}px 0 0 0`, padding: "0.5em", fontSize: "11px", fontFamily: INLINE_CODE_FONT, whiteSpace: "pre-wrap", background: "rgba(127, 127, 127, 0.12)" }, children: normalized.diagnostics.map((d) => `${d.code} (${d.line}:${d.column}) ${d.message}`).join("\n") }) : null
1046
- ] }) });
1047
- }
1048
1049
  export {
1049
1050
  MachinaLayoutError,
1050
1051
  MachinaReactView,
@@ -1057,6 +1058,9 @@ export {
1057
1058
  compileLayoutRows,
1058
1059
  flattenResolvedTree,
1059
1060
  formatRect,
1061
+ lerpNumber,
1062
+ lerpRect,
1063
+ lerpResolvedLayouts,
1060
1064
  normalizePadding,
1061
1065
  parseMachinaText,
1062
1066
  parseMachinaTextInline,
@@ -1064,5 +1068,6 @@ export {
1064
1068
  resolveLayoutDocument,
1065
1069
  resolveLayoutRows,
1066
1070
  resolveUiLength,
1071
+ selectLayoutRowsForRoot,
1067
1072
  toResolvedTree
1068
1073
  };