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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Forbidden Concepts (M0/M1 Guardrails)
|
|
2
|
+
|
|
3
|
+
These concepts are forbidden in core M0/M1 unless a future milestone deliberately revisits them.
|
|
4
|
+
|
|
5
|
+
## Flexbox negotiation features
|
|
6
|
+
|
|
7
|
+
Forbidden: `flex-basis`, `flex-shrink`, flex grow/shrink/basis triangle, `align-content`, `align-self`, `space-around`, `space-evenly`, stretch defaults, baseline alignment. FillFrame weights are not flexbox.
|
|
8
|
+
|
|
9
|
+
Why: this would reintroduce hidden layout negotiation and makes layout harder to reason about from records.
|
|
10
|
+
|
|
11
|
+
## Margin-driven placement
|
|
12
|
+
|
|
13
|
+
Forbidden: auto margins, per-child margins, margin collapse. FillFrame does not add per-item margin behavior.
|
|
14
|
+
|
|
15
|
+
Why: margins introduce implicit inter-node coupling and hidden offsets that are not explicit in row geometry.
|
|
16
|
+
|
|
17
|
+
## Stack wrapping and implicit sizing behavior
|
|
18
|
+
|
|
19
|
+
Forbidden: wrap on `Stack`, percent units, implicit stretch behaviors.
|
|
20
|
+
|
|
21
|
+
Why: this belongs in a future separate primitive, not Stack.
|
|
22
|
+
|
|
23
|
+
## DOM/CSS-driven geometry authority
|
|
24
|
+
|
|
25
|
+
Forbidden: DOM measurement in core layout, selector-based layout, descendant selector semantics, CSS classes as layout inputs.
|
|
26
|
+
|
|
27
|
+
Why: renderer-dependent inputs break deterministic resolution and move authority away from typed records.
|
|
28
|
+
|
|
29
|
+
## Authoring model drift
|
|
30
|
+
|
|
31
|
+
Forbidden: nested JSX as canonical authoring, recursive `children` on `LayoutNode`.
|
|
32
|
+
|
|
33
|
+
Why: Machina’s contract is flat record authoring; tree shape is derived output.
|
|
34
|
+
|
|
35
|
+
## Out-of-scope platform concerns in core
|
|
36
|
+
|
|
37
|
+
Forbidden: routing inside layout core, state management inside layout core.
|
|
38
|
+
|
|
39
|
+
Why: layout core should stay a geometry engine, not an application framework.
|
|
40
|
+
|
|
41
|
+
## Ordering and layering misuse
|
|
42
|
+
|
|
43
|
+
Forbidden: global z-index solver, z-based layout ordering.
|
|
44
|
+
|
|
45
|
+
Why: z is sibling-local paint metadata; geometry and order resolution must remain independent.
|
|
46
|
+
|
|
47
|
+
## Silent correctness masking
|
|
48
|
+
|
|
49
|
+
Forbidden: silent clamping.
|
|
50
|
+
|
|
51
|
+
Why: silent repair hides authoring mistakes and weakens determinism guarantees.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Unit parsing guardrail
|
|
55
|
+
|
|
56
|
+
Forbidden: string percentages (e.g. `"50%"`), CSS unit parsing, and CSS `calc`.
|
|
57
|
+
|
|
58
|
+
Allowed in M1c: typed `UiLength` objects for `AnchorFrame` only (`px` and normalized `ui`), because conversion is explicit and deterministic.
|
|
59
|
+
|
|
60
|
+
M1d clarification: per-item margins remain forbidden. Use explicit node `offset` for local post-placement nudges when needed.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Frames and Stack
|
|
2
|
+
|
|
3
|
+
## Rects
|
|
4
|
+
|
|
5
|
+
Resolved rectangles are numeric records:
|
|
6
|
+
|
|
7
|
+
- `x`
|
|
8
|
+
- `y`
|
|
9
|
+
- `width`
|
|
10
|
+
- `height`
|
|
11
|
+
|
|
12
|
+
## `AbsoluteFrame`
|
|
13
|
+
|
|
14
|
+
Absolute frame gives explicit parent-local position and size.
|
|
15
|
+
|
|
16
|
+
- `x = parent.x + frame.x`
|
|
17
|
+
- `y = parent.y + frame.y`
|
|
18
|
+
- `width = frame.width`
|
|
19
|
+
- `height = frame.height`
|
|
20
|
+
|
|
21
|
+
## `AnchorFrame`
|
|
22
|
+
|
|
23
|
+
Anchor requires exactly two horizontal constraints from `left`, `right`, `width`, and exactly two vertical constraints from `top`, `bottom`, `height`.
|
|
24
|
+
|
|
25
|
+
Supported horizontal cases:
|
|
26
|
+
|
|
27
|
+
- `left + width`
|
|
28
|
+
- `right + width`
|
|
29
|
+
- `left + right`
|
|
30
|
+
|
|
31
|
+
Supported vertical cases:
|
|
32
|
+
|
|
33
|
+
- `top + height`
|
|
34
|
+
- `bottom + height`
|
|
35
|
+
- `top + bottom`
|
|
36
|
+
|
|
37
|
+
Invalid combinations fail. Negative resolved sizes fail.
|
|
38
|
+
|
|
39
|
+
## `FixedFrame`
|
|
40
|
+
|
|
41
|
+
Fixed frame is size-only.
|
|
42
|
+
|
|
43
|
+
- It cannot resolve by itself.
|
|
44
|
+
- It is valid as a direct child of `StackArrange`.
|
|
45
|
+
- It is invalid on the root row (same compile-time rejection code as non-arranged fixed usage): `FixedFrameWithoutArranger`.
|
|
46
|
+
- Otherwise resolution fails with `FixedFrameWithoutArranger`.
|
|
47
|
+
|
|
48
|
+
## `StackArrange`
|
|
49
|
+
|
|
50
|
+
Stack is ordered sequence arrangement over direct children.
|
|
51
|
+
|
|
52
|
+
- Direct children must be `FixedFrame` or `FillFrame`.
|
|
53
|
+
- Axis: `horizontal` or `vertical`.
|
|
54
|
+
- Supports `gap` and `padding`.
|
|
55
|
+
- `justify`: `start` | `center` | `end` | `space-between`
|
|
56
|
+
- `align`: `start` | `center` | `end`
|
|
57
|
+
|
|
58
|
+
Deliberate exclusions:
|
|
59
|
+
|
|
60
|
+
- no shrink
|
|
61
|
+
- no stretch
|
|
62
|
+
- no shrink/basis/min/max negotiation
|
|
63
|
+
- no wrap
|
|
64
|
+
- no margins
|
|
65
|
+
|
|
66
|
+
### Toolbar example
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
{
|
|
70
|
+
id: "toolbar",
|
|
71
|
+
frame: { kind: "anchor", left: 240, right: 0, top: 64, height: 56 },
|
|
72
|
+
arrange: {
|
|
73
|
+
kind: "stack",
|
|
74
|
+
axis: "horizontal",
|
|
75
|
+
gap: 8,
|
|
76
|
+
padding: { top: 8, right: 8, bottom: 8, left: 8 },
|
|
77
|
+
justify: "start",
|
|
78
|
+
align: "center",
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
{ id: "button-a", parent: "toolbar", frame: { kind: "fixed", width: 120, height: 40 } }
|
|
83
|
+
{ id: "button-b", parent: "toolbar", frame: { kind: "fixed", width: 96, height: 40 } }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Stack computes positions by arithmetic over order, fixed sizes, gap, and padding.
|
|
87
|
+
|
|
88
|
+
> Stack is ordered arithmetic, not Flexbox.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
## RootFrame
|
|
92
|
+
|
|
93
|
+
`RootFrame` is `{ kind: "root" }` and is valid only on the root row (`parent === undefined`).
|
|
94
|
+
|
|
95
|
+
- Root geometry is still copied from `rootRect` passed by the caller.
|
|
96
|
+
- `resolveFrame(parent, { kind: "root" })` throws `RootFrameWithoutRoot`.
|
|
97
|
+
- Non-root rows with `RootFrame` are rejected during compile with `RootFrameNotRoot`.
|
|
98
|
+
- Root should use `RootFrame`; legacy root `AbsoluteFrame` and `AnchorFrame` remain accepted for compatibility.
|
|
99
|
+
- Root `FixedFrame` and root `FillFrame` are invalid because both require arranger/stack context.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## `FillFrame`
|
|
103
|
+
|
|
104
|
+
`FillFrame` is valid only as a direct child of `StackArrange`.
|
|
105
|
+
|
|
106
|
+
- `weight` defaults to `1` and must be finite and `> 0`.
|
|
107
|
+
- `cross` defaults to `"fill"`.
|
|
108
|
+
- `cross: "fill"` uses stack content cross size.
|
|
109
|
+
- numeric `cross` is explicit cross size and must be finite/non-negative.
|
|
110
|
+
|
|
111
|
+
Fill children consume all remaining main-axis space after fixed sizes and gaps. When one or more fill children exist, `justify` has no additional free space to distribute.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## Anchor UiLength (M1c)
|
|
115
|
+
|
|
116
|
+
Anchor fields `left/right/top/bottom/width/height` accept `UiLength`:
|
|
117
|
+
- plain number means px
|
|
118
|
+
- `{ unit: "px", value }` explicit px
|
|
119
|
+
- `{ unit: "ui", value }` normalized unit resolved deterministically during frame resolution
|
|
120
|
+
|
|
121
|
+
Axis mapping:
|
|
122
|
+
- horizontal (`left/right/width`) uses `parent.width`
|
|
123
|
+
- vertical (`top/bottom/height`) uses `parent.height`
|
|
124
|
+
|
|
125
|
+
Notes:
|
|
126
|
+
- no string percentages
|
|
127
|
+
- no CSS `calc`
|
|
128
|
+
- fractional pixels are preserved
|
|
129
|
+
- negative `left/right/top/bottom` positional constraints are deliberately allowed for intentional overflow/bleed
|
|
130
|
+
- explicit negative width/height are rejected
|
|
131
|
+
- derived negative size from `left+right` or `top+bottom` is rejected
|
|
132
|
+
|
|
133
|
+
## Node offsets (M1d)
|
|
134
|
+
|
|
135
|
+
`offset` is applied after normal frame/stack placement and does not change stack distribution, sibling positions, or width/height. `offset.x` resolves against parent rect width; `offset.y` resolves against parent rect height.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# M0 Contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
M0 defines a deterministic, framework-independent rectangle resolution pipeline with a narrow, explicit scope.
|
|
6
|
+
|
|
7
|
+
## Architecture pipeline
|
|
8
|
+
|
|
9
|
+
`LayoutRow[]`
|
|
10
|
+
→ `compileLayoutRows`
|
|
11
|
+
→ `LayoutDocument`
|
|
12
|
+
→ `resolveLayoutDocument`
|
|
13
|
+
→ `ResolvedLayoutDocument`
|
|
14
|
+
→ optional `toResolvedTree`
|
|
15
|
+
→ renderer adapter
|
|
16
|
+
|
|
17
|
+
## Core model
|
|
18
|
+
|
|
19
|
+
- **Flat rows** are the canonical input model.
|
|
20
|
+
- **Indexed documents** (`nodes`, `children`, `rootId`) are the compiled representation.
|
|
21
|
+
- **Flat resolved output** is the canonical resolved geometry model.
|
|
22
|
+
- **Derived tree** is for rendering/debugging convenience, not authoring.
|
|
23
|
+
|
|
24
|
+
## Supported primitives
|
|
25
|
+
|
|
26
|
+
- `AbsoluteFrame`
|
|
27
|
+
- `AnchorFrame`
|
|
28
|
+
- `FixedFrame`
|
|
29
|
+
- `StackArrange`
|
|
30
|
+
|
|
31
|
+
## Supported metadata
|
|
32
|
+
|
|
33
|
+
- `slot`
|
|
34
|
+
- `debugLabel`
|
|
35
|
+
- `z`
|
|
36
|
+
|
|
37
|
+
## Adapter boundary
|
|
38
|
+
|
|
39
|
+
- The React adapter renders resolved rectangles as absolutely positioned wrappers.
|
|
40
|
+
- Slots are rendered via a slot registry (`views`).
|
|
41
|
+
- Coordinates are normalized to root-local space for embedding.
|
|
42
|
+
- Containment/content-visibility are adapter-level policy knobs.
|
|
43
|
+
|
|
44
|
+
## Sample
|
|
45
|
+
|
|
46
|
+
The Control Room sample (`samples/control-room`) demonstrates the M0 contract in a runnable app.
|
|
47
|
+
|
|
48
|
+
## Non-goals
|
|
49
|
+
|
|
50
|
+
- routing
|
|
51
|
+
- state framework
|
|
52
|
+
- full design system
|
|
53
|
+
- CSS replacement
|
|
54
|
+
- flexbox clone
|
|
55
|
+
- grid clone
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
> M1a note: Root rows may now declare `frame: { kind: "root" }`; root geometry still comes from caller-provided `rootRect`.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
> M1b note: `FillFrame` is now supported as a direct child of `StackArrange` for weighted remaining-space distribution.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## M1c implemented scope note
|
|
65
|
+
|
|
66
|
+
M1c adds typed `UiLength` support for `AnchorFrame` only; resolved rect outputs remain numeric pixels.
|
|
67
|
+
|
|
68
|
+
## M1d implemented scope note
|
|
69
|
+
|
|
70
|
+
M1d adds optional node-level `OffsetSpec` for deterministic post-placement translation. Offsets are not margins and do not participate in stack distribution.
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# MachinaText M2a Planning & Audit
|
|
2
|
+
|
|
3
|
+
## Status note (M2c.1)
|
|
4
|
+
|
|
5
|
+
M2c.1 is implemented in React renderer scope: explicit `leading`, `blockGap`, `listGap`, and `valign` policy fields now control vertical rhythm inside owned rectangles without changing layout resolver APIs or parser syntax.
|
|
6
|
+
|
|
7
|
+
## 1) Executive summary
|
|
8
|
+
|
|
9
|
+
MachinaText should be introduced as a **small, explicit text-content model** for authoring predictable text structures inside already-resolved Machina rectangles, without turning Machina into a document engine, Markdown engine, or font/layout measurement system.
|
|
10
|
+
|
|
11
|
+
For M2a, the least disruptive path is:
|
|
12
|
+
|
|
13
|
+
- add a **docs-first plan** (this file),
|
|
14
|
+
- keep MachinaText **independent from layout core resolution**,
|
|
15
|
+
- keep `LayoutRow` and resolver APIs unchanged,
|
|
16
|
+
- attach text policy at the **view payload layer** (application/renderer side) first,
|
|
17
|
+
- defer parser/runtime code to later milestones.
|
|
18
|
+
|
|
19
|
+
## 2) Goals
|
|
20
|
+
|
|
21
|
+
- Provide a brutally limited, machine-authorable text format for LLM-friendly edits.
|
|
22
|
+
- Keep text content model renderer-agnostic.
|
|
23
|
+
- Preserve Machina contract: layout owns rectangles, text stays inside resolved rectangles.
|
|
24
|
+
- Support a narrow set of content primitives:
|
|
25
|
+
- plain text
|
|
26
|
+
- paragraph breaks
|
|
27
|
+
- strong
|
|
28
|
+
- emphasis
|
|
29
|
+
- inline code
|
|
30
|
+
- links
|
|
31
|
+
- unordered bullets with max depth 2
|
|
32
|
+
- Make policy explicit (variant/wrap/overflow/align) rather than hidden in syntax tricks.
|
|
33
|
+
|
|
34
|
+
## 3) Non-goals
|
|
35
|
+
|
|
36
|
+
- No full Markdown compatibility.
|
|
37
|
+
- No headings in M2.
|
|
38
|
+
- No images/tables/raw HTML/blockquotes/ordered lists/task lists/footnotes.
|
|
39
|
+
- No routing/dispatch/state behavior.
|
|
40
|
+
- No rich text editor concerns (selection/caret/editing model).
|
|
41
|
+
- No glyph shaping/font fallback/bidi engine in Machina core.
|
|
42
|
+
- No intrinsic sizing or text-driven outer layout changes.
|
|
43
|
+
|
|
44
|
+
## 4) Proposed package/module placement
|
|
45
|
+
|
|
46
|
+
### Audit observations
|
|
47
|
+
|
|
48
|
+
- Core public API is exported from `src/index.ts`, and currently exports geometry + react adapter modules only. `src/index.ts`.
|
|
49
|
+
- Core types (`LayoutRow`, `FrameSpec`, resolved docs) are geometry-oriented and intentionally small. `src/types.ts`.
|
|
50
|
+
- React integration currently maps `view ?? slot` keys to app-provided components via `views` registry; this is the existing integration seam for payload rendering. `src/react/MachinaReactView.tsx`.
|
|
51
|
+
|
|
52
|
+
### Recommendation (least disruptive M2 path)
|
|
53
|
+
|
|
54
|
+
1. **Add `src/text/` in same package** for M2-series incubation (types/contracts/docs-aligned naming).
|
|
55
|
+
2. Keep React-specific text helpers under **`src/text/react/`** (not mixed into current generic `src/react` root) to keep text concerns modular.
|
|
56
|
+
3. Re-export from root only when implementation starts (M2b+), not required for M2a docs-only.
|
|
57
|
+
4. Consider separate package later (`@machina/text`) only after API stabilizes and at least one non-React renderer exists.
|
|
58
|
+
|
|
59
|
+
Rationale: this keeps churn low, avoids premature package splitting, and preserves current adapter surface.
|
|
60
|
+
|
|
61
|
+
## 5) Proposed public type surface
|
|
62
|
+
|
|
63
|
+
> M2a recommendation: define these in plan only; do not implement yet.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export type MachinaTextSource =
|
|
67
|
+
| { kind: "plain"; text: string }
|
|
68
|
+
| { kind: "machina-text"; text: string };
|
|
69
|
+
|
|
70
|
+
export type MachinaTextVariant = "body" | "label" | "caption" | "title" | "mono";
|
|
71
|
+
export type MachinaTextWrap = "word" | "none";
|
|
72
|
+
export type MachinaTextOverflow = "clip" | "ellipsis" | "scroll";
|
|
73
|
+
export type MachinaTextAlign = "start" | "center" | "end";
|
|
74
|
+
|
|
75
|
+
export type MachinaTextSpec = {
|
|
76
|
+
kind: "text";
|
|
77
|
+
source: MachinaTextSource;
|
|
78
|
+
variant?: MachinaTextVariant;
|
|
79
|
+
wrap?: MachinaTextWrap;
|
|
80
|
+
overflow?: MachinaTextOverflow;
|
|
81
|
+
align?: MachinaTextAlign;
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
AST recommendation:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
export type MachinaTextDocument = {
|
|
89
|
+
blocks: MachinaTextBlock[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type MachinaTextBlock =
|
|
93
|
+
| { kind: "paragraph"; inline: MachinaInline[] }
|
|
94
|
+
| { kind: "bulletList"; items: MachinaBulletItem[] };
|
|
95
|
+
|
|
96
|
+
export type MachinaBulletItem = {
|
|
97
|
+
inline: MachinaInline[];
|
|
98
|
+
children?: MachinaBulletItem[];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type MachinaInline =
|
|
102
|
+
| { kind: "text"; text: string }
|
|
103
|
+
| { kind: "strong"; children: MachinaInline[] }
|
|
104
|
+
| { kind: "emphasis"; children: MachinaInline[] }
|
|
105
|
+
| { kind: "code"; text: string }
|
|
106
|
+
| { kind: "link"; href: string; children: MachinaInline[] };
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Type-shape decisions:
|
|
110
|
+
|
|
111
|
+
- `strong` / `emphasis` should contain **children**, not raw text, to allow nested emphasis/code/link segments while keeping one consistent inline model.
|
|
112
|
+
- `link` should contain **inline children**, not label-only text, for parity and explicit structure.
|
|
113
|
+
- Bullets should allow nested inline formatting (same `MachinaInline[]` as paragraphs).
|
|
114
|
+
- Bullet max depth should be enforced by parser validation/diagnostics, not encoded in types.
|
|
115
|
+
- `variant/wrap/overflow/align` belong in `MachinaTextSpec` (content policy contract), while renderers decide exact visual implementation details.
|
|
116
|
+
- `plain` source should normalize into one-paragraph AST during parse.
|
|
117
|
+
|
|
118
|
+
## 6) Allowed syntax
|
|
119
|
+
|
|
120
|
+
MachinaText syntax (M2 target):
|
|
121
|
+
|
|
122
|
+
- plain text
|
|
123
|
+
- paragraph breaks (blank line separation)
|
|
124
|
+
- `**strong**`
|
|
125
|
+
- `*emphasis*`
|
|
126
|
+
- `` `code` ``
|
|
127
|
+
- `[label](href)` links
|
|
128
|
+
- unordered bullets (`-` prefix)
|
|
129
|
+
- nested bullets max depth 2
|
|
130
|
+
|
|
131
|
+
## 7) Forbidden syntax
|
|
132
|
+
|
|
133
|
+
- headings (`#`, `##`, etc.)
|
|
134
|
+
- ordered lists (`1.`)
|
|
135
|
+
- task list checkboxes
|
|
136
|
+
- blockquotes (`>`)
|
|
137
|
+
- fenced code blocks
|
|
138
|
+
- images (`![...]`)
|
|
139
|
+
- tables
|
|
140
|
+
- raw HTML
|
|
141
|
+
- extension syntax / custom directives
|
|
142
|
+
- CSS classes/styles inside text syntax
|
|
143
|
+
- any layout declarations
|
|
144
|
+
- route/action/dispatch syntax
|
|
145
|
+
|
|
146
|
+
## 8) Heading decision and rationale
|
|
147
|
+
|
|
148
|
+
**Decision: headings are explicitly out of scope in M2.**
|
|
149
|
+
|
|
150
|
+
Rationale:
|
|
151
|
+
|
|
152
|
+
- Heading markers smuggle hidden policy (typography scale, spacing, semantic levels).
|
|
153
|
+
- MachinaText should keep policy explicit and inspectable.
|
|
154
|
+
- “Title-like” rendering should come from explicit fields (`variant: "title"`) or explicit views, not punctuation shortcuts.
|
|
155
|
+
|
|
156
|
+
## 9) Bullet list decision and max-depth rationale
|
|
157
|
+
|
|
158
|
+
Decision:
|
|
159
|
+
|
|
160
|
+
- support unordered bullets only,
|
|
161
|
+
- allow nesting up to depth 2,
|
|
162
|
+
- reject/diagnose deeper nesting.
|
|
163
|
+
|
|
164
|
+
Rationale:
|
|
165
|
+
|
|
166
|
+
- Depth cap preserves predictable rendering across hosts.
|
|
167
|
+
- Prevents gradual drift toward document-authoring complexity.
|
|
168
|
+
- Meets common UI copy needs (short status/checklist-like structure) without introducing full markdown list semantics.
|
|
169
|
+
|
|
170
|
+
## 10) Parser behavior recommendation
|
|
171
|
+
|
|
172
|
+
Recommendation:
|
|
173
|
+
|
|
174
|
+
- Parser should return a **result object with diagnostics**, not throw for normal authoring errors.
|
|
175
|
+
- Hard throw only for programmer misuse (invalid API call types), not syntax mistakes in content strings.
|
|
176
|
+
|
|
177
|
+
Suggested shape:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
export type MachinaTextDiagnosticLevel = "error" | "warning";
|
|
181
|
+
|
|
182
|
+
export type MachinaTextDiagnostic = {
|
|
183
|
+
code:
|
|
184
|
+
| "unsupported_syntax"
|
|
185
|
+
| "heading_forbidden"
|
|
186
|
+
| "max_list_depth_exceeded"
|
|
187
|
+
| "malformed_link"
|
|
188
|
+
| "unclosed_inline"
|
|
189
|
+
| "invalid_escape";
|
|
190
|
+
message: string;
|
|
191
|
+
index: number;
|
|
192
|
+
length: number;
|
|
193
|
+
line: number;
|
|
194
|
+
column: number;
|
|
195
|
+
level: MachinaTextDiagnosticLevel;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export type ParseMachinaTextResult = {
|
|
199
|
+
ok: boolean;
|
|
200
|
+
document: MachinaTextDocument;
|
|
201
|
+
diagnostics: MachinaTextDiagnostic[];
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Fallback policy:
|
|
206
|
+
|
|
207
|
+
- Unsupported constructs should be preserved as literal text where possible, with diagnostics.
|
|
208
|
+
- Optional strict mode can fail `ok=false` on first unsupported construct.
|
|
209
|
+
|
|
210
|
+
## 11) Renderer boundary
|
|
211
|
+
|
|
212
|
+
Core MachinaText model owns:
|
|
213
|
+
|
|
214
|
+
- content AST semantics,
|
|
215
|
+
- small text policy flags (`variant`, `wrap`, `overflow`, `align`),
|
|
216
|
+
- parser diagnostics contract.
|
|
217
|
+
|
|
218
|
+
React/DOM renderer owns:
|
|
219
|
+
|
|
220
|
+
- actual text rendering via browser engine,
|
|
221
|
+
- mapping policy flags to CSS/DOM behavior,
|
|
222
|
+
- link interaction callbacks passed from app,
|
|
223
|
+
- accessibility annotations specific to host components.
|
|
224
|
+
|
|
225
|
+
Future non-DOM renderer owns:
|
|
226
|
+
|
|
227
|
+
- platform-specific shaping/layout internals,
|
|
228
|
+
- equivalent wrap/clip/ellipsis/scroll behavior inside rectangle,
|
|
229
|
+
- platform accessibility mapping.
|
|
230
|
+
|
|
231
|
+
## 12) Layout boundary
|
|
232
|
+
|
|
233
|
+
Hard boundary:
|
|
234
|
+
|
|
235
|
+
- MachinaLayout resolves outer rectangles.
|
|
236
|
+
- MachinaText operates inside resolved rectangle only.
|
|
237
|
+
|
|
238
|
+
Not allowed:
|
|
239
|
+
|
|
240
|
+
- requesting larger parent/sibling/root sizes,
|
|
241
|
+
- intrinsic measurement feedback into resolver,
|
|
242
|
+
- creating new layout nodes from text blocks.
|
|
243
|
+
|
|
244
|
+
Allowed:
|
|
245
|
+
|
|
246
|
+
- in-rectangle wrap/clip/ellipsis/scroll policy.
|
|
247
|
+
|
|
248
|
+
## 13) Link behavior boundary
|
|
249
|
+
|
|
250
|
+
Links are text semantics only:
|
|
251
|
+
|
|
252
|
+
- link node stores `href` + label children,
|
|
253
|
+
- no route syntax,
|
|
254
|
+
- no command dispatch tables,
|
|
255
|
+
- no state transition contract in MachinaText.
|
|
256
|
+
|
|
257
|
+
Application/renderers decide click behavior.
|
|
258
|
+
|
|
259
|
+
## 14) Suggested M2 milestone decomposition
|
|
260
|
+
|
|
261
|
+
- **M2a (this pass):** audit + plan doc + agreed type contracts (docs only).
|
|
262
|
+
- **M2b:** implement parser + diagnostics + unit tests (no renderer).
|
|
263
|
+
- **M2c:** React/DOM rendering adapter for MachinaText AST inside node views.
|
|
264
|
+
- **M2d:** sample integration in control-room-style demo with explicit text specs.
|
|
265
|
+
- **M2e:** docs polish, migration notes, edge-case hardening, non-DOM renderer contract notes.
|
|
266
|
+
|
|
267
|
+
## 15) Risks and mitigations
|
|
268
|
+
|
|
269
|
+
1. **Heading semantics creep**
|
|
270
|
+
Mitigation: explicit prohibition + diagnostic code `heading_forbidden`.
|
|
271
|
+
|
|
272
|
+
2. **Markdown dialect creep**
|
|
273
|
+
Mitigation: closed grammar and “unsupported as literal+diagnostic” policy.
|
|
274
|
+
|
|
275
|
+
3. **Text measurement creep into core**
|
|
276
|
+
Mitigation: hard boundary that no parser/model API takes font metrics or container reflow callbacks.
|
|
277
|
+
|
|
278
|
+
4. **Routing/event creep via links**
|
|
279
|
+
Mitigation: keep links as `href` semantics only, no action ids.
|
|
280
|
+
|
|
281
|
+
5. **HTML passthrough pressure**
|
|
282
|
+
Mitigation: explicit ban with diagnostics; no raw HTML node type.
|
|
283
|
+
|
|
284
|
+
6. **Intrinsic sizing pressure**
|
|
285
|
+
Mitigation: tests/docs asserting no resolver API change and no text->layout feedback path.
|
|
286
|
+
|
|
287
|
+
7. **Accessibility overpromises in core**
|
|
288
|
+
Mitigation: document that host renderers/apps own semantics and interaction behavior.
|
|
289
|
+
|
|
290
|
+
8. **Renderer leakage into core types**
|
|
291
|
+
Mitigation: keep core text types free of CSS/DOM-specific properties.
|
|
292
|
+
|
|
293
|
+
## 16) Recommended next implementation prompt
|
|
294
|
+
|
|
295
|
+
“Implement M2b parser-only MachinaText foundation (no renderer):
|
|
296
|
+
|
|
297
|
+
- Add `src/text/types.ts` and `src/text/parseMachinaText.ts`.
|
|
298
|
+
- Implement only allowed syntax: paragraphs, strong/emphasis/code/link, unordered bullets depth<=2.
|
|
299
|
+
- Return `ParseMachinaTextResult` with diagnostics; no throws for authoring errors.
|
|
300
|
+
- Forbid headings/raw HTML/ordered lists/etc. with explicit diagnostic codes.
|
|
301
|
+
- Add focused Vitest coverage for success and failure cases.
|
|
302
|
+
- Do not modify layout resolver APIs, `LayoutRow`, or React adapter behavior yet.”
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Repo-specific audit notes
|
|
307
|
+
|
|
308
|
+
- Current core shape is layout-only and should remain unchanged for M2a. `src/types.ts`.
|
|
309
|
+
- Current React integration seam (`view ?? slot` mapping) is a suitable place for app-level text view components in early milestones, avoiding immediate core API expansion. `src/react/MachinaReactView.tsx`.
|
|
310
|
+
- Current docs already enforce similar anti-creep guardrails (no intrinsic sizing/DOM authority), and MachinaText should align with that philosophy. `docs/forbidden-concepts.md`.
|
|
311
|
+
|
|
312
|
+
## M2b status note (implemented)
|
|
313
|
+
|
|
314
|
+
M2b parser-only foundation is now implemented in `src/text`:
|
|
315
|
+
|
|
316
|
+
- public MachinaText types are exported,
|
|
317
|
+
- `parseMachinaText` returns AST + diagnostics,
|
|
318
|
+
- no renderer integration added,
|
|
319
|
+
- no layout resolver API changes made.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# MachinaText Parser
|
|
2
|
+
|
|
3
|
+
MachinaText supports a narrow inline syntax: emphasis, strong, code spans, links, and bullets.
|
|
4
|
+
|
|
5
|
+
## Escapes (M3a)
|
|
6
|
+
|
|
7
|
+
Supported escapes in normal inline text:
|
|
8
|
+
|
|
9
|
+
- `\\` => `\`
|
|
10
|
+
- `\*` => `*`
|
|
11
|
+
- ``\` `` => `` ` ``
|
|
12
|
+
- `\[` => `[`
|
|
13
|
+
- `\]` => `]`
|
|
14
|
+
- `\(` => `(`
|
|
15
|
+
- `\)` => `)`
|
|
16
|
+
- `\-` => `-`
|
|
17
|
+
|
|
18
|
+
Escapes are processed before inline marker detection, so escaped markers stay literal (example: `\*not emphasis\*`).
|
|
19
|
+
|
|
20
|
+
Unsupported escapes (example: `\q`) emit diagnostic code `invalid_escape` and preserve visible content as literal text.
|
|
21
|
+
|
|
22
|
+
A trailing dangling backslash also emits `invalid_escape` and preserves a literal backslash.
|
|
23
|
+
|
|
24
|
+
Code spans do not process escapes. Content inside backticks is taken literally.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# MachinaText React Renderer (M2c)
|
|
2
|
+
|
|
3
|
+
`MachinaTextView` renders MachinaText content inside a rectangle already owned by a parent view. It does not participate in Machina layout resolution, sizing, or measurement.
|
|
4
|
+
|
|
5
|
+
## Component
|
|
6
|
+
|
|
7
|
+
- `MachinaTextView`
|
|
8
|
+
- `MachinaTextViewProps`
|
|
9
|
+
|
|
10
|
+
### `text` input forms
|
|
11
|
+
|
|
12
|
+
- `string`: parsed as `{ kind: "machina-text", text }`
|
|
13
|
+
- `MachinaTextSource`: parsed directly (`plain` stays literal)
|
|
14
|
+
- `MachinaTextSpec`: parses `spec.source` and applies text policy
|
|
15
|
+
- `MachinaTextDocument`: rendered directly with default policy
|
|
16
|
+
|
|
17
|
+
## Policy fields
|
|
18
|
+
|
|
19
|
+
Supported policy fields:
|
|
20
|
+
|
|
21
|
+
- `variant`: `body | label | caption | title | mono`
|
|
22
|
+
- `wrap`: `word | none`
|
|
23
|
+
- `overflow`: `clip | ellipsis | scroll`
|
|
24
|
+
- `align`: `start | center | end`
|
|
25
|
+
- `leading`: `tight | normal | loose | number`
|
|
26
|
+
- `blockGap`: non-negative number (pixels)
|
|
27
|
+
- `listGap`: non-negative number (pixels)
|
|
28
|
+
- `valign`: `top | center | bottom`
|
|
29
|
+
|
|
30
|
+
Defaults are `body`, `word`, `clip`, `start`, `normal`, `8`, `2`, `top`.
|
|
31
|
+
|
|
32
|
+
Vertical rhythm policy is internal to the text renderer inside an already-owned rectangle. It does not change outer Machina layout, resolver behavior, or sizing.
|
|
33
|
+
|
|
34
|
+
- `leading` maps to `line-height` (`tight=1.15`, `loose=1.6`, `normal=variant default`, numeric uses provided value when valid).
|
|
35
|
+
- `blockGap` controls spacing between top-level text blocks (paragraph/list blocks).
|
|
36
|
+
- `listGap` controls spacing between list items (including nested list items).
|
|
37
|
+
- `valign` controls vertical placement of content in the wrapper rectangle.
|
|
38
|
+
|
|
39
|
+
`MachinaTextView` may use internal flex layout for this vertical positioning only. This does not imply flex/grid-based Machina node placement.
|
|
40
|
+
|
|
41
|
+
## Link behavior boundary
|
|
42
|
+
|
|
43
|
+
Links render as standard `<a>` tags.
|
|
44
|
+
|
|
45
|
+
- optional `linkTarget`
|
|
46
|
+
- if `_blank`, renderer sets `rel="noreferrer noopener"`
|
|
47
|
+
- optional `onLinkClick(href, event)` callback
|
|
48
|
+
|
|
49
|
+
No routing, dispatch, or action semantics are implemented.
|
|
50
|
+
|
|
51
|
+
## Diagnostics
|
|
52
|
+
|
|
53
|
+
Parsing diagnostics never throw. Best-effort content still renders.
|
|
54
|
+
|
|
55
|
+
Set `showDiagnostics` to render parser diagnostics for development.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Overflow ellipsis policy (M3a)
|
|
59
|
+
|
|
60
|
+
`overflow: "ellipsis"` is defined as **single-line ellipsis** in M3.
|
|
61
|
+
|
|
62
|
+
When `overflow` is `ellipsis`, renderer normalization forces:
|
|
63
|
+
|
|
64
|
+
- `white-space: nowrap`
|
|
65
|
+
- `overflow: hidden`
|
|
66
|
+
- `text-overflow: ellipsis`
|
|
67
|
+
|
|
68
|
+
This overrides `wrap` for deterministic behavior. Multi-line ellipsis / line clamp is not supported.
|