machinalayout 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.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +1068 -0
- package/docs/forbidden-concepts.md +60 -0
- package/docs/frames-and-stack.md +135 -0
- package/docs/m0-contract.md +70 -0
- package/docs/machina-text-m2a-plan.md +319 -0
- package/docs/machina-text-parser.md +24 -0
- package/docs/machina-text-react.md +68 -0
- package/docs/npm-prepublish-m2z-audit.md +233 -0
- package/docs/npm-prepublish-m3c-cleanup.md +105 -0
- package/docs/react-adapter.md +110 -0
- package/docs/row-model.md +82 -0
- package/docs/z-order-and-containment.md +38 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1068 @@
|
|
|
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
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/validation.ts
|
|
12
|
+
function assertFiniteNumber(value, fieldName) {
|
|
13
|
+
if (!Number.isFinite(value)) {
|
|
14
|
+
throw new MachinaLayoutError(
|
|
15
|
+
"NonFiniteNumber",
|
|
16
|
+
`${fieldName} must be a finite number. Received: ${value}`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function assertNonNegativeSize(value, fieldName) {
|
|
21
|
+
assertFiniteNumber(value, fieldName);
|
|
22
|
+
if (value < 0) {
|
|
23
|
+
throw new MachinaLayoutError(
|
|
24
|
+
"NegativeSize",
|
|
25
|
+
`${fieldName} must be non-negative. Received: ${value}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function assertNonNegativeGap(value, fieldName = "gap") {
|
|
30
|
+
assertFiniteNumber(value, fieldName);
|
|
31
|
+
if (value < 0) {
|
|
32
|
+
throw new MachinaLayoutError(
|
|
33
|
+
"NegativeGap",
|
|
34
|
+
`${fieldName} must be non-negative. Received: ${value}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertNonNegativePadding(value, fieldName = "padding") {
|
|
39
|
+
assertFiniteNumber(value, fieldName);
|
|
40
|
+
if (value < 0) {
|
|
41
|
+
throw new MachinaLayoutError(
|
|
42
|
+
"NegativePadding",
|
|
43
|
+
`${fieldName} must be non-negative. Received: ${value}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/padding.ts
|
|
49
|
+
function normalizePadding(padding) {
|
|
50
|
+
const resolved = typeof padding === "number" ? { top: padding, right: padding, bottom: padding, left: padding } : padding === void 0 ? { top: 0, right: 0, bottom: 0, left: 0 } : {
|
|
51
|
+
top: padding.top,
|
|
52
|
+
right: padding.right,
|
|
53
|
+
bottom: padding.bottom,
|
|
54
|
+
left: padding.left
|
|
55
|
+
};
|
|
56
|
+
assertNonNegativePadding(resolved.top, "padding.top");
|
|
57
|
+
assertNonNegativePadding(resolved.right, "padding.right");
|
|
58
|
+
assertNonNegativePadding(resolved.bottom, "padding.bottom");
|
|
59
|
+
assertNonNegativePadding(resolved.left, "padding.left");
|
|
60
|
+
return {
|
|
61
|
+
top: resolved.top,
|
|
62
|
+
right: resolved.right,
|
|
63
|
+
bottom: resolved.bottom,
|
|
64
|
+
left: resolved.left
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/length.ts
|
|
69
|
+
function resolveUiLength(length, axisSize, fieldName = "length") {
|
|
70
|
+
assertFiniteNumber(axisSize, "axisSize");
|
|
71
|
+
if (typeof length === "number") {
|
|
72
|
+
assertFiniteNumber(length, fieldName);
|
|
73
|
+
return length;
|
|
74
|
+
}
|
|
75
|
+
if (!length || typeof length !== "object" || !("unit" in length) || !("value" in length)) {
|
|
76
|
+
throw new MachinaLayoutError("InvalidLengthUnit", `Invalid UiLength for ${fieldName}.`);
|
|
77
|
+
}
|
|
78
|
+
const { unit, value } = length;
|
|
79
|
+
assertFiniteNumber(value, `${fieldName}.value`);
|
|
80
|
+
if (unit === "px") {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (unit === "ui") {
|
|
84
|
+
return value * axisSize;
|
|
85
|
+
}
|
|
86
|
+
throw new MachinaLayoutError("InvalidLengthUnit", `Invalid UiLength unit for ${fieldName}: ${String(unit)}.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/offset.ts
|
|
90
|
+
function applyOffset(rect, parentRect, offset) {
|
|
91
|
+
const dx = offset?.x === void 0 ? 0 : resolveUiLength(offset.x, parentRect.width);
|
|
92
|
+
const dy = offset?.y === void 0 ? 0 : resolveUiLength(offset.y, parentRect.height);
|
|
93
|
+
return {
|
|
94
|
+
x: rect.x + dx,
|
|
95
|
+
y: rect.y + dy,
|
|
96
|
+
width: rect.width,
|
|
97
|
+
height: rect.height
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/compileLayoutRows.ts
|
|
102
|
+
function compileLayoutRows(rows) {
|
|
103
|
+
if (rows.length === 0) {
|
|
104
|
+
throw new MachinaLayoutError("EmptyRows", "rows must contain at least one row.");
|
|
105
|
+
}
|
|
106
|
+
const nodes = {};
|
|
107
|
+
const rowById = /* @__PURE__ */ new Map();
|
|
108
|
+
const rootCandidates = [];
|
|
109
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
110
|
+
const row = rows[rowIndex];
|
|
111
|
+
if (row.id.trim().length === 0) {
|
|
112
|
+
throw new MachinaLayoutError("InvalidId", `row at index ${rowIndex} has an invalid id.`);
|
|
113
|
+
}
|
|
114
|
+
if (rowById.has(row.id)) {
|
|
115
|
+
throw new MachinaLayoutError("DuplicateId", `duplicate id found: ${row.id}`);
|
|
116
|
+
}
|
|
117
|
+
if (row.order !== void 0) {
|
|
118
|
+
assertFiniteNumber(row.order, `rows[${rowIndex}].order`);
|
|
119
|
+
}
|
|
120
|
+
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.`);
|
|
122
|
+
}
|
|
123
|
+
if (row.z !== void 0) {
|
|
124
|
+
assertFiniteNumber(row.z, `rows[${rowIndex}].z`);
|
|
125
|
+
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`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
rowById.set(row.id, row);
|
|
130
|
+
nodes[row.id] = {
|
|
131
|
+
id: row.id,
|
|
132
|
+
z: row.z,
|
|
133
|
+
frame: row.frame,
|
|
134
|
+
arrange: row.arrange,
|
|
135
|
+
view: row.view,
|
|
136
|
+
slot: row.slot,
|
|
137
|
+
debugLabel: row.debugLabel,
|
|
138
|
+
offset: row.offset
|
|
139
|
+
};
|
|
140
|
+
if (row.parent === void 0) {
|
|
141
|
+
rootCandidates.push(row.id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (rootCandidates.length === 0) {
|
|
145
|
+
throw new MachinaLayoutError("MissingRoot", "exactly one root is required, found none.");
|
|
146
|
+
}
|
|
147
|
+
if (rootCandidates.length > 1) {
|
|
148
|
+
throw new MachinaLayoutError("MultipleRoots", "exactly one root is required, found multiple.");
|
|
149
|
+
}
|
|
150
|
+
const rootId = rootCandidates[0];
|
|
151
|
+
if (nodes[rootId].frame.kind === "fill") {
|
|
152
|
+
throw new MachinaLayoutError("FillFrameWithoutArranger", "FillFrame cannot be used as the root frame.");
|
|
153
|
+
}
|
|
154
|
+
if (nodes[rootId].frame.kind === "fixed") {
|
|
155
|
+
throw new MachinaLayoutError("FixedFrameWithoutArranger", "FixedFrame cannot be used as the root frame.");
|
|
156
|
+
}
|
|
157
|
+
const childrenEntries = /* @__PURE__ */ new Map();
|
|
158
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
159
|
+
const row = rows[rowIndex];
|
|
160
|
+
if (row.parent === void 0) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (row.parent === row.id) {
|
|
164
|
+
throw new MachinaLayoutError("SelfParent", `node ${row.id} cannot parent itself.`);
|
|
165
|
+
}
|
|
166
|
+
if (!rowById.has(row.parent) || row.parent.trim().length === 0) {
|
|
167
|
+
throw new MachinaLayoutError("UnknownParent", `node ${row.id} references unknown parent: ${row.parent}`);
|
|
168
|
+
}
|
|
169
|
+
const entry = {
|
|
170
|
+
childId: row.id,
|
|
171
|
+
orderValue: row.order ?? 0,
|
|
172
|
+
rowIndex
|
|
173
|
+
};
|
|
174
|
+
const list = childrenEntries.get(row.parent);
|
|
175
|
+
if (list) {
|
|
176
|
+
list.push(entry);
|
|
177
|
+
} else {
|
|
178
|
+
childrenEntries.set(row.parent, [entry]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const children = {};
|
|
182
|
+
for (const [parentId, list] of childrenEntries.entries()) {
|
|
183
|
+
list.sort((a, b) => a.orderValue - b.orderValue || a.rowIndex - b.rowIndex);
|
|
184
|
+
children[parentId] = list.map((item) => item.childId);
|
|
185
|
+
}
|
|
186
|
+
const parentById = /* @__PURE__ */ new Map();
|
|
187
|
+
for (const row of rows) {
|
|
188
|
+
parentById.set(row.id, row.parent);
|
|
189
|
+
}
|
|
190
|
+
const chainState = /* @__PURE__ */ new Map();
|
|
191
|
+
const detectParentChainCycle = (nodeId) => {
|
|
192
|
+
const state = chainState.get(nodeId) ?? 0;
|
|
193
|
+
if (state === 2) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (state === 1) {
|
|
197
|
+
throw new MachinaLayoutError("Cycle", `cycle detected at node ${nodeId}`);
|
|
198
|
+
}
|
|
199
|
+
chainState.set(nodeId, 1);
|
|
200
|
+
const parentId = parentById.get(nodeId);
|
|
201
|
+
if (parentId !== void 0) {
|
|
202
|
+
detectParentChainCycle(parentId);
|
|
203
|
+
}
|
|
204
|
+
chainState.set(nodeId, 2);
|
|
205
|
+
};
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
detectParentChainCycle(row.id);
|
|
208
|
+
}
|
|
209
|
+
const visitState = /* @__PURE__ */ new Map();
|
|
210
|
+
let visitedCount = 0;
|
|
211
|
+
const dfs = (nodeId) => {
|
|
212
|
+
const state = visitState.get(nodeId) ?? 0;
|
|
213
|
+
if (state === 1) {
|
|
214
|
+
throw new MachinaLayoutError("Cycle", `cycle detected at node ${nodeId}`);
|
|
215
|
+
}
|
|
216
|
+
if (state === 2) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
visitState.set(nodeId, 1);
|
|
220
|
+
visitedCount += 1;
|
|
221
|
+
for (const childId of children[nodeId] ?? []) {
|
|
222
|
+
dfs(childId);
|
|
223
|
+
}
|
|
224
|
+
visitState.set(nodeId, 2);
|
|
225
|
+
};
|
|
226
|
+
dfs(rootId);
|
|
227
|
+
if (visitedCount !== rows.length) {
|
|
228
|
+
throw new MachinaLayoutError(
|
|
229
|
+
"UnreachableNode",
|
|
230
|
+
"one or more nodes are unreachable from the root."
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return { rootId, nodes, children };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/resolveFrame.ts
|
|
237
|
+
function validateParentRect(parent) {
|
|
238
|
+
assertFiniteNumber(parent.x, "parent.x");
|
|
239
|
+
assertFiniteNumber(parent.y, "parent.y");
|
|
240
|
+
assertNonNegativeSize(parent.width, "parent.width");
|
|
241
|
+
assertNonNegativeSize(parent.height, "parent.height");
|
|
242
|
+
}
|
|
243
|
+
function hasLength(value) {
|
|
244
|
+
return value !== void 0;
|
|
245
|
+
}
|
|
246
|
+
function resolveAnchor(parent, frame) {
|
|
247
|
+
const hasLeft = hasLength(frame.left);
|
|
248
|
+
const hasRight = hasLength(frame.right);
|
|
249
|
+
const hasTop = hasLength(frame.top);
|
|
250
|
+
const hasBottom = hasLength(frame.bottom);
|
|
251
|
+
const hasWidth = hasLength(frame.width);
|
|
252
|
+
const hasHeight = hasLength(frame.height);
|
|
253
|
+
const left = hasLeft ? resolveUiLength(frame.left, parent.width, "frame.left") : void 0;
|
|
254
|
+
const right = hasRight ? resolveUiLength(frame.right, parent.width, "frame.right") : void 0;
|
|
255
|
+
const top = hasTop ? resolveUiLength(frame.top, parent.height, "frame.top") : void 0;
|
|
256
|
+
const bottom = hasBottom ? resolveUiLength(frame.bottom, parent.height, "frame.bottom") : void 0;
|
|
257
|
+
const explicitWidth = hasWidth ? resolveUiLength(frame.width, parent.width, "frame.width") : void 0;
|
|
258
|
+
const explicitHeight = hasHeight ? resolveUiLength(frame.height, parent.height, "frame.height") : void 0;
|
|
259
|
+
if (hasWidth) assertNonNegativeSize(explicitWidth, "frame.width");
|
|
260
|
+
if (hasHeight) assertNonNegativeSize(explicitHeight, "frame.height");
|
|
261
|
+
const horizontalCount = Number(hasLeft) + Number(hasRight) + Number(hasWidth);
|
|
262
|
+
if (horizontalCount !== 2) {
|
|
263
|
+
throw new MachinaLayoutError("InvalidAnchorHorizontal", "Anchor frame must specify exactly two horizontal constraints: left, right, width.");
|
|
264
|
+
}
|
|
265
|
+
const verticalCount = Number(hasTop) + Number(hasBottom) + Number(hasHeight);
|
|
266
|
+
if (verticalCount !== 2) {
|
|
267
|
+
throw new MachinaLayoutError("InvalidAnchorVertical", "Anchor frame must specify exactly two vertical constraints: top, bottom, height.");
|
|
268
|
+
}
|
|
269
|
+
let x;
|
|
270
|
+
let width;
|
|
271
|
+
if (hasLeft && hasWidth) {
|
|
272
|
+
x = parent.x + left;
|
|
273
|
+
width = explicitWidth;
|
|
274
|
+
} else if (hasRight && hasWidth) {
|
|
275
|
+
x = parent.x + parent.width - right - explicitWidth;
|
|
276
|
+
width = explicitWidth;
|
|
277
|
+
} else {
|
|
278
|
+
x = parent.x + left;
|
|
279
|
+
width = parent.width - left - right;
|
|
280
|
+
}
|
|
281
|
+
let y;
|
|
282
|
+
let height;
|
|
283
|
+
if (hasTop && hasHeight) {
|
|
284
|
+
y = parent.y + top;
|
|
285
|
+
height = explicitHeight;
|
|
286
|
+
} else if (hasBottom && hasHeight) {
|
|
287
|
+
y = parent.y + parent.height - bottom - explicitHeight;
|
|
288
|
+
height = explicitHeight;
|
|
289
|
+
} else {
|
|
290
|
+
y = parent.y + top;
|
|
291
|
+
height = parent.height - top - bottom;
|
|
292
|
+
}
|
|
293
|
+
if (width < 0 || height < 0) {
|
|
294
|
+
throw new MachinaLayoutError(
|
|
295
|
+
"NegativeResolvedSize",
|
|
296
|
+
`Resolved anchor frame size must be non-negative. Received width=${width}, height=${height}.`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return { x, y, width, height };
|
|
300
|
+
}
|
|
301
|
+
function resolveFrame(parent, frame) {
|
|
302
|
+
validateParentRect(parent);
|
|
303
|
+
switch (frame.kind) {
|
|
304
|
+
case "absolute": {
|
|
305
|
+
assertFiniteNumber(frame.x, "frame.x");
|
|
306
|
+
assertFiniteNumber(frame.y, "frame.y");
|
|
307
|
+
assertNonNegativeSize(frame.width, "frame.width");
|
|
308
|
+
assertNonNegativeSize(frame.height, "frame.height");
|
|
309
|
+
return { x: parent.x + frame.x, y: parent.y + frame.y, width: frame.width, height: frame.height };
|
|
310
|
+
}
|
|
311
|
+
case "anchor":
|
|
312
|
+
return resolveAnchor(parent, frame);
|
|
313
|
+
case "root":
|
|
314
|
+
throw new MachinaLayoutError("RootFrameWithoutRoot", "RootFrame can only be declared on the root row.");
|
|
315
|
+
case "fixed": {
|
|
316
|
+
assertNonNegativeSize(frame.width, "frame.width");
|
|
317
|
+
assertNonNegativeSize(frame.height, "frame.height");
|
|
318
|
+
throw new MachinaLayoutError("FixedFrameWithoutArranger", "Fixed frames require an arranger to determine placement.");
|
|
319
|
+
}
|
|
320
|
+
case "fill":
|
|
321
|
+
throw new MachinaLayoutError("FillFrameWithoutArranger", "Fill frames require a stack arranger to determine placement.");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/resolveLayoutDocument.ts
|
|
326
|
+
function validateRootRect(rootRect) {
|
|
327
|
+
assertFiniteNumber(rootRect.x, "rootRect.x");
|
|
328
|
+
assertFiniteNumber(rootRect.y, "rootRect.y");
|
|
329
|
+
assertNonNegativeSize(rootRect.width, "rootRect.width");
|
|
330
|
+
assertNonNegativeSize(rootRect.height, "rootRect.height");
|
|
331
|
+
}
|
|
332
|
+
function resolveStackChildRects(parentRect, arrange, childIds, document) {
|
|
333
|
+
const gap = arrange.gap ?? 0;
|
|
334
|
+
const justify = arrange.justify ?? "start";
|
|
335
|
+
const align = arrange.align ?? "start";
|
|
336
|
+
assertNonNegativeGap(gap, "gap");
|
|
337
|
+
const padding = normalizePadding(arrange.padding);
|
|
338
|
+
const content = {
|
|
339
|
+
x: parentRect.x + padding.left,
|
|
340
|
+
y: parentRect.y + padding.top,
|
|
341
|
+
width: parentRect.width - padding.left - padding.right,
|
|
342
|
+
height: parentRect.height - padding.top - padding.bottom
|
|
343
|
+
};
|
|
344
|
+
if (content.width < 0 || content.height < 0) {
|
|
345
|
+
throw new MachinaLayoutError("StackContentNegative", "stack content size cannot be negative after applying padding");
|
|
346
|
+
}
|
|
347
|
+
const isHorizontal = arrange.axis === "horizontal";
|
|
348
|
+
const contentMain = isHorizontal ? content.width : content.height;
|
|
349
|
+
const contentCross = isHorizontal ? content.height : content.width;
|
|
350
|
+
const childMainSizes = [];
|
|
351
|
+
const childCrossSizes = [];
|
|
352
|
+
const fillWeights = [];
|
|
353
|
+
for (const childId of childIds) {
|
|
354
|
+
const childNode = document.nodes[childId];
|
|
355
|
+
if (!childNode) {
|
|
356
|
+
throw new MachinaLayoutError("UnknownParent", `child id ${childId} referenced by arranged parent is missing`);
|
|
357
|
+
}
|
|
358
|
+
if (childNode.frame.kind === "fixed") {
|
|
359
|
+
assertNonNegativeSize(childNode.frame.width, `${childId}.frame.width`);
|
|
360
|
+
assertNonNegativeSize(childNode.frame.height, `${childId}.frame.height`);
|
|
361
|
+
childMainSizes.push(isHorizontal ? childNode.frame.width : childNode.frame.height);
|
|
362
|
+
childCrossSizes.push(isHorizontal ? childNode.frame.height : childNode.frame.width);
|
|
363
|
+
fillWeights.push(0);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (childNode.frame.kind !== "fill") {
|
|
367
|
+
throw new MachinaLayoutError("StackChildMustBeFixed", `stack child must use fixed or fill frame: ${childId}`);
|
|
368
|
+
}
|
|
369
|
+
const weight = childNode.frame.weight ?? 1;
|
|
370
|
+
assertFiniteNumber(weight, `${childId}.frame.weight`);
|
|
371
|
+
if (weight <= 0) {
|
|
372
|
+
throw new MachinaLayoutError("InvalidFillWeight", `${childId}.frame.weight must be greater than 0`);
|
|
373
|
+
}
|
|
374
|
+
const cross = childNode.frame.cross ?? "fill";
|
|
375
|
+
let childCross = contentCross;
|
|
376
|
+
if (cross !== "fill") {
|
|
377
|
+
assertNonNegativeSize(cross, `${childId}.frame.cross`);
|
|
378
|
+
childCross = cross;
|
|
379
|
+
}
|
|
380
|
+
childMainSizes.push(0);
|
|
381
|
+
childCrossSizes.push(childCross);
|
|
382
|
+
fillWeights.push(weight);
|
|
383
|
+
}
|
|
384
|
+
const fixedMainTotal = childIds.reduce((sum, _id, i) => sum + (fillWeights[i] === 0 ? childMainSizes[i] : 0), 0);
|
|
385
|
+
const totalGap = gap * Math.max(0, childIds.length - 1);
|
|
386
|
+
const remainingMain = contentMain - fixedMainTotal - totalGap;
|
|
387
|
+
if (remainingMain < 0) {
|
|
388
|
+
throw new MachinaLayoutError("StackOverflow", "stack main axis overflow");
|
|
389
|
+
}
|
|
390
|
+
const totalFillWeight = fillWeights.reduce((sum, w) => sum + w, 0);
|
|
391
|
+
if (totalFillWeight > 0) {
|
|
392
|
+
for (let i = 0; i < childMainSizes.length; i += 1) {
|
|
393
|
+
if (fillWeights[i] > 0) {
|
|
394
|
+
childMainSizes[i] = remainingMain * fillWeights[i] / totalFillWeight;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const childCross of childCrossSizes) {
|
|
399
|
+
if (childCross > contentCross) {
|
|
400
|
+
throw new MachinaLayoutError("StackOverflow", "stack cross axis overflow");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const occupiedMain = childMainSizes.reduce((sum, size) => sum + size, 0) + totalGap;
|
|
404
|
+
const remainingMainAfterFill = contentMain - occupiedMain;
|
|
405
|
+
let startOffset = 0;
|
|
406
|
+
let actualGap = gap;
|
|
407
|
+
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
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const rects = {};
|
|
429
|
+
let currentMain = startOffset;
|
|
430
|
+
childIds.forEach((childId, index) => {
|
|
431
|
+
const childMain = childMainSizes[index];
|
|
432
|
+
const childCross = childCrossSizes[index];
|
|
433
|
+
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 };
|
|
447
|
+
currentMain += childMain + actualGap;
|
|
448
|
+
});
|
|
449
|
+
return rects;
|
|
450
|
+
}
|
|
451
|
+
function resolveLayoutDocument(document, rootRect) {
|
|
452
|
+
validateRootRect(rootRect);
|
|
453
|
+
const rootNode = document.nodes[document.rootId];
|
|
454
|
+
if (!rootNode) {
|
|
455
|
+
throw new MachinaLayoutError("MissingRoot", `root node not found for id: ${document.rootId}`);
|
|
456
|
+
}
|
|
457
|
+
const resolvedNodes = {};
|
|
458
|
+
const resolvedChildren = {};
|
|
459
|
+
const visitState = /* @__PURE__ */ new Map();
|
|
460
|
+
let visitedCount = 0;
|
|
461
|
+
const resolveNode = (nodeId, rect) => {
|
|
462
|
+
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
|
+
}
|
|
469
|
+
const node = document.nodes[nodeId];
|
|
470
|
+
if (!node) {
|
|
471
|
+
throw new MachinaLayoutError("UnknownParent", `node referenced in children but missing from nodes: ${nodeId}`);
|
|
472
|
+
}
|
|
473
|
+
visitState.set(nodeId, 1);
|
|
474
|
+
visitedCount += 1;
|
|
475
|
+
resolvedNodes[nodeId] = {
|
|
476
|
+
id: node.id,
|
|
477
|
+
z: node.z,
|
|
478
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
479
|
+
frame: node.frame,
|
|
480
|
+
arrange: node.arrange,
|
|
481
|
+
view: node.view,
|
|
482
|
+
slot: node.slot,
|
|
483
|
+
debugLabel: node.debugLabel,
|
|
484
|
+
offset: node.offset
|
|
485
|
+
};
|
|
486
|
+
const childIds = document.children[nodeId] ?? [];
|
|
487
|
+
resolvedChildren[nodeId] = [...childIds];
|
|
488
|
+
const childRects = node.arrange?.kind === "stack" ? resolveStackChildRects(rect, node.arrange, childIds, document) : void 0;
|
|
489
|
+
for (const childId of childIds) {
|
|
490
|
+
const childNode = document.nodes[childId];
|
|
491
|
+
if (!childNode) {
|
|
492
|
+
throw new MachinaLayoutError("UnknownParent", `child id ${childId} referenced by ${nodeId} is missing`);
|
|
493
|
+
}
|
|
494
|
+
const normalChildRect = childRects?.[childId] ?? resolveFrame(rect, childNode.frame);
|
|
495
|
+
const childRect = applyOffset(normalChildRect, rect, childNode.offset);
|
|
496
|
+
resolveNode(childId, childRect);
|
|
497
|
+
}
|
|
498
|
+
visitState.set(nodeId, 2);
|
|
499
|
+
};
|
|
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
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/resolveLayoutRows.ts
|
|
512
|
+
function resolveLayoutRows(rows, rootRect) {
|
|
513
|
+
const document = compileLayoutRows(rows);
|
|
514
|
+
return resolveLayoutDocument(document, rootRect);
|
|
515
|
+
}
|
|
516
|
+
|
|
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
|
+
// src/flattenResolvedTree.ts
|
|
563
|
+
function flattenResolvedTree(tree) {
|
|
564
|
+
const out = [];
|
|
565
|
+
const visit = (node) => {
|
|
566
|
+
out.push({
|
|
567
|
+
id: node.id,
|
|
568
|
+
z: node.z,
|
|
569
|
+
rect: { ...node.rect },
|
|
570
|
+
frame: node.frame,
|
|
571
|
+
arrange: node.arrange,
|
|
572
|
+
view: node.view,
|
|
573
|
+
slot: node.slot,
|
|
574
|
+
debugLabel: node.debugLabel,
|
|
575
|
+
offset: node.offset
|
|
576
|
+
});
|
|
577
|
+
for (const child of node.children) {
|
|
578
|
+
visit(child);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
visit(tree);
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/formatRect.ts
|
|
586
|
+
function formatRect(rect) {
|
|
587
|
+
return `x=${rect.x} y=${rect.y} w=${rect.width} h=${rect.height}`;
|
|
588
|
+
}
|
|
589
|
+
|
|
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;
|
|
709
|
+
}
|
|
710
|
+
return lines;
|
|
711
|
+
}
|
|
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;
|
|
825
|
+
}
|
|
826
|
+
pushText(text.slice(cursor, next));
|
|
827
|
+
cursor = next;
|
|
828
|
+
}
|
|
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;
|
|
840
|
+
}
|
|
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] };
|
|
856
|
+
}
|
|
857
|
+
if (src.kind === "plain") {
|
|
858
|
+
return {
|
|
859
|
+
ok: true,
|
|
860
|
+
document: { blocks: [{ kind: "paragraph", inline: [{ kind: "text", text: src.text }] }] },
|
|
861
|
+
diagnostics: []
|
|
862
|
+
};
|
|
863
|
+
}
|
|
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;
|
|
925
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
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;
|
|
958
|
+
}
|
|
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));
|
|
963
|
+
}
|
|
964
|
+
function normalizeSpecPolicy(spec) {
|
|
965
|
+
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
|
|
974
|
+
};
|
|
975
|
+
}
|
|
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) };
|
|
981
|
+
}
|
|
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
|
+
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]
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
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
|
+
export {
|
|
1049
|
+
MachinaLayoutError,
|
|
1050
|
+
MachinaReactView,
|
|
1051
|
+
MachinaTextView,
|
|
1052
|
+
applyOffset,
|
|
1053
|
+
assertFiniteNumber,
|
|
1054
|
+
assertNonNegativeGap,
|
|
1055
|
+
assertNonNegativePadding,
|
|
1056
|
+
assertNonNegativeSize,
|
|
1057
|
+
compileLayoutRows,
|
|
1058
|
+
flattenResolvedTree,
|
|
1059
|
+
formatRect,
|
|
1060
|
+
normalizePadding,
|
|
1061
|
+
parseMachinaText,
|
|
1062
|
+
parseMachinaTextInline,
|
|
1063
|
+
resolveFrame,
|
|
1064
|
+
resolveLayoutDocument,
|
|
1065
|
+
resolveLayoutRows,
|
|
1066
|
+
resolveUiLength,
|
|
1067
|
+
toResolvedTree
|
|
1068
|
+
};
|