arrmatura 6.3.2 → 6.5.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 (72) hide show
  1. package/README.md +59 -25
  2. package/dist/index.cjs +4 -0
  3. package/dist/index.js +4 -2
  4. package/docs/architecture.md +151 -0
  5. package/docs/built-ins.md +223 -0
  6. package/docs/cml.md +342 -0
  7. package/index.ts +1 -22
  8. package/package.json +18 -16
  9. package/src/{registry → compiler}/index.ts +23 -22
  10. package/src/controls/Iterative.ts +127 -0
  11. package/src/controls/block.ts +25 -0
  12. package/src/{registry → controls}/composition.ts +9 -15
  13. package/src/controls/conditionals.ts +79 -0
  14. package/src/controls/connector.ts +75 -0
  15. package/src/{registry → controls}/elementary.ts +3 -6
  16. package/src/{registry → controls}/fragment.ts +3 -4
  17. package/src/controls/index.ts +10 -0
  18. package/src/controls/root.ts +47 -0
  19. package/src/controls/routing.ts +45 -0
  20. package/src/{registry → controls}/selection.ts +4 -9
  21. package/src/{registry → controls}/slot.ts +12 -10
  22. package/src/core/Arrmatron.ts +648 -411
  23. package/src/core/Component.ts +108 -53
  24. package/src/core/ManifestNode.ts +295 -141
  25. package/src/core/NativeElement.ts +89 -0
  26. package/src/core/Platform.ts +116 -0
  27. package/src/core/Registry.ts +162 -0
  28. package/src/core/consts.ts +3 -0
  29. package/src/core/launch.ts +18 -0
  30. package/src/expr/Expression.ts +59 -0
  31. package/src/expr/ExpressionParser.ts +216 -0
  32. package/src/expr/ExpressionParserBase.ts +120 -0
  33. package/src/expr/ParserContext.ts +17 -0
  34. package/src/expr/compileEmitterExpression.ts +54 -0
  35. package/src/expr/compilePlaceholder.ts +14 -0
  36. package/src/expr/operations.ts +53 -0
  37. package/src/expr/prepareConnectedProps.ts +16 -0
  38. package/src/expr/resolveExpression.ts +66 -0
  39. package/src/index.ts +8 -0
  40. package/src/types.ts +168 -0
  41. package/src/utils/applyStateChangedToImpl.ts +2 -9
  42. package/src/utils/applyTemplate.ts +20 -0
  43. package/src/utils/asyncValueCall.ts +11 -0
  44. package/src/utils/capitalize.ts +7 -0
  45. package/src/utils/createRegisterTypes.ts +67 -0
  46. package/src/utils/defineCalculatedProperty.ts +48 -0
  47. package/src/utils/defineObjectRef.ts +18 -0
  48. package/src/utils/hashCodeOf.ts +147 -0
  49. package/src/utils/index.ts +9 -0
  50. package/src/utils/isEquals.ts +74 -0
  51. package/src/utils/mapEntries.ts +9 -0
  52. package/src/utils/mergeObject.ts +25 -0
  53. package/src/utils/narrowData.ts +30 -0
  54. package/src/utils/scalarParse.ts +41 -0
  55. package/src/utils/splitTopLevel.ts +28 -0
  56. package/src/utils/stringify.ts +4 -3
  57. package/src/utils/toNativeTree.ts +21 -0
  58. package/src/utils/xmlParse.ts +120 -0
  59. package/dist/index.js.map +0 -7
  60. package/docs/glossary.md +0 -19
  61. package/docs/hello.md +0 -46
  62. package/docs/manual.md +0 -312
  63. package/src/core/resolveExpression.ts +0 -25
  64. package/src/registry/conditionals.ts +0 -77
  65. package/src/registry/connector.ts +0 -47
  66. package/src/registry/iterations.ts +0 -134
  67. package/src/registry/root.ts +0 -48
  68. package/src/registry/routing.ts +0 -32
  69. package/src/utils/FingerprintMashine.ts +0 -128
  70. package/src/utils/compileExpression.ts +0 -215
  71. package/src/utils/objectFingerprint.spec.ts +0 -70
  72. package/types.ts +0 -82
@@ -0,0 +1,223 @@
1
+ ---
2
+ title: "Built-ins"
3
+ description: "Collection of Intrinsic built-in components."
4
+ keywords: [arrmatura]
5
+ ---
6
+
7
+ Built-ins are the components the runtime implements itself.
8
+
9
+ ## Element — native tags
10
+
11
+ Lowercase tags and `div`.
12
+
13
+ ```xml
14
+ <div class="card {active ? 'active'}" click="-> select(id)">{title}</div>
15
+ ```
16
+
17
+ - All attributes compile through `compileAttribute`; element text compiles into the `#text` property.
18
+ - Children are compiled lazily on first `getSubNodes`.
19
+ - The component is built as `platform.createComponent({ tag, native: true })`.
20
+
21
+ ## Composite — registered components
22
+
23
+ Any other capitalised tag.
24
+
25
+ ```xml
26
+ <Card Ref="card" title="{doc.title}" />
27
+ ```
28
+
29
+ - Content comes from the *registered template* of the tag, not from the children written at the call
30
+ site — those become slot content (see `Slot`).
31
+ - `Ref` is appended to the manifest `uid`, so two usages differing only by `Ref` stay distinct.
32
+ - Tag lookup falls back through dotted prefixes (`Page.Docs.Header` → `Page.Docs` → `Page`).
33
+
34
+ ## If / Then / Else
35
+
36
+ ```xml
37
+ <div If="isReady">ready</div>
38
+
39
+ <Fragment If="count > 0">
40
+ <Then><List items="{items}" /></Then>
41
+ <Else><Empty /></Else>
42
+ </Fragment>
43
+ ```
44
+
45
+ - The expression may be written bare or fully wrapped in `{…}`; a partial template is not supported.
46
+ - The condition is coerced with `!!`.
47
+ - Branches are resolved once and cached:
48
+
49
+ | Children | then-branch | else-branch |
50
+ | --- | --- | --- |
51
+ | neither `Then` nor `Else` | the element itself, minus `If` | nothing |
52
+ | `Else` only | the element minus the `Else` child | children of `Else` |
53
+ | `Then` only | children of `Then` | nothing |
54
+ | both | children of `Then` | children of `Else` |
55
+
56
+ As soon as an explicit `<Then>` is present the host element is dropped and only its children render.
57
+ Use `<Fragment If>` for if/else, and the bare form for a single branch.
58
+
59
+ ### Slot presence check
60
+
61
+ ```xml
62
+ <div If="Slot(header)"><Slot Key="header" /></div>
63
+ ```
64
+
65
+ `Slot(key)` is a compile-time special form, not an expression: the condition becomes "the caller
66
+ passed non-empty content for that slot". It reads the enclosing `Composite` directly and must not be
67
+ placed inside an iteration.
68
+
69
+ ## Each
70
+
71
+ ```xml
72
+ <Item Each="item of items" data="{item}" />
73
+ <Row Each="row of data | mapEntries" row="{row}" />
74
+ ```
75
+
76
+ Syntax is `<name> <word> <expression>`: the first token names the item, the second is ignored (`of`
77
+ by convention), the rest is the expression.
78
+
79
+ The value is normalised by `narrowData`:
80
+
81
+ | Input | Items |
82
+ | --- | --- |
83
+ | falsy | `[]` |
84
+ | `"a, b"` | `{ id, name }` per comma-separated token |
85
+ | array | objects kept, `id` forced to string; scalars become `{ id, name }` |
86
+ | iterable | spread as-is |
87
+ | object | entries become items keyed by their key |
88
+
89
+ Then:
90
+
91
+ - The item key is `datum.id`, falling back to `hashCodeOf(datum)`. A **duplicate key is logged as an
92
+ error and skipped**, so the row disappears.
93
+ - Each item gets a cloned manifest with `uid = <uid>#<key>`, which is what makes reuse across renders
94
+ stable.
95
+ - `IterativeItem` opens a scope: `item` and `item.*` resolve against itself, everything else and all
96
+ `emit` calls delegate to the enclosing scope.
97
+ - Item data is re-read from the live map on every render, so mutating an existing item propagates
98
+ into the reused child.
99
+
100
+ ## Fragment / Then / Else
101
+
102
+ ```xml
103
+ <Fragment If="showContent">
104
+ <Header />
105
+ <Content />
106
+ </Fragment>
107
+ ```
108
+
109
+ Transparent grouping: no component of its own beyond a plain `Component`, no new scope, children
110
+ render in place. `Then` and `Else` compile to the same node and are meaningful only as direct
111
+ children of an `If` element.
112
+
113
+ ## Dynamic
114
+
115
+ ```xml
116
+ <Dynamic As="{viewMode == 'list' ? 'ListView' : 'GridView'}" items="{items}" />
117
+ ```
118
+
119
+ - `As` compiles into the internal `_dynamicTag` property; every other attribute and all children pass
120
+ through to the resolved tag.
121
+ - The resolved element is compiled once per distinct tag value and cached on the node.
122
+ - Missing `As` compiles to the tag `Error.Dynamic`; an expression resolving to `undefined` falls back
123
+ to the compiler default, a `div`.
124
+
125
+ ## Slot
126
+
127
+ Content projection. `Composite` groups the children written at the call site by their `Slot`
128
+ attribute, defaulting to the group `default`.
129
+
130
+ ```xml
131
+ <!-- definition -->
132
+ <Component id="Card">
133
+ <div class="card">
134
+ <Slot Key="header" />
135
+ <Slot />
136
+ <Slot Key="footer" />
137
+ </div>
138
+ </Component>
139
+
140
+ <!-- usage -->
141
+ <Card>
142
+ <Fragment Slot="header"><h1>{title}</h1></Fragment>
143
+ <p>body</p>
144
+ <Fragment Slot="footer"><Btn label="OK" /></Fragment>
145
+ </Card>
146
+ ```
147
+
148
+ - `Key` selects the group; no `Key` means `default`.
149
+ - An unfilled slot renders nothing.
150
+ - Projected content resolves properties and refs **at the call site** (`scopeForChildren` is the
151
+ grandparent scope), so it sees the caller's data, not the component's internals.
152
+ - Slot groups are compiled once per component usage and cached.
153
+
154
+ ## Selector
155
+
156
+ Multi-way switch on one string key.
157
+
158
+ ```xml
159
+ <Selector On="{node | typeOf}">
160
+ <Case When="object"><XmlElement node="{node}" /></Case>
161
+ <Case When="string"><span>{node}</span></Case>
162
+ <Case When="default">—</Case>
163
+ </Selector>
164
+ ```
165
+
166
+ - Children are grouped by their `When` attribute; the child tag name is never inspected, `Case` is
167
+ convention only.
168
+ - Matching is exact string equality against `On`, falling back to the group `default`.
169
+ - No match and no `default` renders nothing.
170
+ - `On` is commonly composed from several fragments (`"{data|typeOf}{isValuable(data) ? '' : '-empty'}"`).
171
+
172
+ ## Connector
173
+
174
+ A component with no output that forwards values. Two modes, chosen by the presence of `trigger`.
175
+
176
+ | Mode | Attributes | Behaviour |
177
+ | --- | --- | --- |
178
+ | data | `data`, `change` | every time `data` resolves to a value other than `undefined`, call `change` with it |
179
+ | trigger | `trigger`, `data`, `change` | every time `trigger` resolves to a value other than `undefined`, resolve `data` and call `change` with the resolved data |
180
+
181
+ ```xml
182
+ <Connector data="@query.data" change="-> options" />
183
+ <Connector data="{find(R.enums[typeSpec], value)}" change="-> item" />
184
+ <Connector trigger="1" change="-> @service.nextQuestion()" />
185
+ <Connector data-value="{value}" trigger="{value}" change="{onChange}" />
186
+ ```
187
+
188
+ - Promises are unwrapped on both `data` and `trigger`, so async sources need no extra handling.
189
+ - `change` is either a `->` emitter or a function-valued expression.
190
+ - Attributes are re-ordered at compile time — `change` first, `trigger` last — because state applies
191
+ in that order and the firing setter must find its target already set.
192
+ - `trigger="1"` is the idiom for "run once when this subtree mounts".
193
+
194
+ ## Block
195
+
196
+ Renders XML nodes produced at runtime.
197
+
198
+ ```xml
199
+ <Block Nodes="{@mdService.nodes}" />
200
+ ```
201
+
202
+ - `Nodes` holds an `XmlNode[]` (as produced by `xmlParse` or by a service, e.g. a markdown renderer).
203
+ - The array is compiled on every render — array input is not memoised, unlike compilation by tag.
204
+ - `Ref` is appended to the manifest `uid`, as for `Composite`.
205
+ - A missing or empty `Nodes` renders nothing.
206
+
207
+ ## Root
208
+
209
+ `CRootNode` / `RootCtx` have no tag and are not reachable from CML. `launch()` and `applyTemplate()`
210
+ wrap a template string in one: it parses the XML, opens the outermost scope, exposes no slots, and
211
+ builds a bare `Component`.
212
+
213
+ ## Gotchas
214
+
215
+ - `Each` and `If` on the same element: the loop is outer, the condition is evaluated per item.
216
+ - An explicit `<Then>` drops the host element; only its children render.
217
+ - Duplicate `id` values inside `Each` silently lose rows (logged as an error).
218
+ - `Slot(key)` in an `If` must not be used inside an iteration.
219
+ - Slot content sees the caller's scope, not the component's — passing data *into* a slot means passing
220
+ it through properties.
221
+ - `Selector` matches strings exactly; a numeric `On` will not match `When="1"` unless it stringifies
222
+ to exactly that.
223
+ - A tag whose first character is a digit is treated as a component and will be reported as unknown.
package/docs/cml.md ADDED
@@ -0,0 +1,342 @@
1
+ ---
2
+ title: "Component Markup Language"
3
+ description: "CML syntax: lexical rules, attribute forms, expressions, references and event bindings."
4
+ keywords: [arrmatura, cml, syntax]
5
+ ---
6
+
7
+ CML is the XML dialect Arrmatura templates are written in.
8
+
9
+ A `.xml` source registers one or more component types; the runtime parses it once, compiles each element into a manifest node, and instantiates that node per live component.
10
+
11
+ This document is the **syntax** reference.
12
+
13
+ - for the semantics of each built-in element see [built-ins.md](./built-ins.md);
14
+ - for the runtime model see [architecture.md](./architecture.md).
15
+
16
+ ## Lexical structure
17
+
18
+ CML is a strict XML subset, parsed by `xmlParse` (`src/utils/xmlParse.ts`).
19
+
20
+ | Production | Rule |
21
+ | --- | --- |
22
+ | Tag name | `[A-Za-z][A-Za-z0-9._:-]*` — dots and dashes are legal and meaningful (`Page.Speaking`, `This-Stat`) |
23
+ | Attribute name | `[A-Za-z][A-Za-z0-9:$-]*` — **no `_`, no `.`** |
24
+ | Attribute value | double quotes only; `'single'` and bare values are not valid XML here |
25
+ | Bare attribute | `<input disabled>` — a valueless attribute compiles to `true` |
26
+ | Self-closing | `<Btn />`; also implicit for `img`, `input`, `br`, `hr`, `col`, `source` |
27
+ | Comment | `<!-- ... -->`, stripped before compilation |
28
+ | Entity | `&amp;` `&lt;` `&gt;` `&quot;` `&nbsp;` `&#65;` `&#x41;` are decoded; an unknown 2–5 char entity becomes a space |
29
+ | Mismatched close | throws; the platform degrades the whole template into a `div` showing the error |
30
+
31
+ ### Attribute values are scalar-coerced
32
+
33
+ Before any expression handling, a quoted value is passed through `scalarParse`:
34
+
35
+ | Source | Value | Type |
36
+ | --- | --- | --- |
37
+ | `size="3"` | `3` | number |
38
+ | `open="true"` / `open="false"` | `true` / `false` | boolean |
39
+ | `x="null"` / `x="undefined"` | `null` / `undefined` | — |
40
+ | `name="John"` | `"John"` | string |
41
+ | `id="12abc"` | `"12abc"` | string (NaN falls back) |
42
+ | `code="12345678901234567890"` | string | over 17 chars stays a string |
43
+
44
+ A non-string result skips expression compilation entirely and is applied once at init.
45
+
46
+ ### Text content
47
+
48
+ - `<div>Hello</div>` — the text becomes the element's `#text` attribute and follows the normal attribute rules, so `<div>Hello {name}</div>` interpolates.
49
+ - Mixed content — text interleaved with child elements is wrapped in a `Text` node carrying a `text` attribute: `<div>Hi <b>x</b></div>` yields `<Text text="Hi " />` plus `<b>`. A `Text` component must be registered for it to render (`arrmatura-web` provides one).
50
+
51
+ ## Component definition
52
+
53
+ ```xml
54
+ <Component id="UserCard">
55
+ <Signature purpose="Display a user" group="Data">
56
+ <Prop id="name" type="string" required="true" />
57
+ <Prop id="size" type="enum" enum="sm,md,lg" />
58
+ </Signature>
59
+
60
+ <div class="card">{name}</div>
61
+ </Component>
62
+ ```
63
+
64
+ - `id` is the **tag** the component is used under; it is required and must be double-quoted.
65
+ - `<Signature>` declares the component's contract; it is stripped at registration and never reaches the compiler — see [Signature](#signature).
66
+ - One file may hold several `<Component>` blocks.
67
+ - A tag whose first character is lowercase compiles to a native element; uppercase or digit means a registered component. A `div` is always native.
68
+ - Tag lookup falls back through dotted prefixes: `Page.Speaking.Header` tries `Page.Speaking`, then `Page`. A component registered at a short prefix silently captures every longer tag.
69
+
70
+ ### Subcomponents
71
+
72
+ ```xml
73
+ <Component id="Page">
74
+ <This-Stat value="1" />
75
+
76
+ <Subcomponent id="Stat">
77
+ <div>{value}</div>
78
+ </Subcomponent>
79
+ </Component>
80
+ ```
81
+
82
+ `<Subcomponent id="Stat">` registers as the flat tag `Page-Stat`, and `This-` inside the enclosing `<Component>` rewrites to that prefix. Subcomponents register flat, so never nest one inside another; siblings reference each other with `This-`.
83
+
84
+ ## Signature
85
+
86
+ `<Signature>` declares a component's public contract. It is removed by `createRegisterTypes` before compilation, so it costs nothing at runtime and cannot affect rendering. It exists for tooling: `catalog.json`, the component gallery, editor completion, and agents reading the source.
87
+
88
+ ```xml
89
+ <Component id="Forma">
90
+ <Signature purpose="Renders a whole data form from a field-metadata list"
91
+ description="The form engine: hands `meta` to FormController, which dispatches each field to `Field.{type}`."
92
+ group="Fields">
93
+ <Prop id="meta" type="array" required="true" description="Field definitions" />
94
+ <Prop id="disabled" type="boolean" default="false" description="Makes every field read-only" />
95
+ <Slot name="default" description="Content pinned into the tab strip" />
96
+ </Signature>
97
+
98
+ <div class="flex flex-col-reverse {class}">...</div>
99
+ </Component>
100
+ ```
101
+
102
+ ### Placement
103
+
104
+ - One `<Signature>` per `<Component>`, written as its first child. The stripper removes every `<Signature>` block wherever it sits, so a second one is silently tolerated rather than reported.
105
+ - A `<Subcomponent>` may carry its own; it is stripped with the subcomponent and never leaks into the parent template.
106
+ - Both the self-closing `<Signature ... />` and the paired form are recognised.
107
+
108
+ ### Signature attributes
109
+
110
+ | Attribute | Required | Meaning |
111
+ | --- | --- | --- |
112
+ | `purpose` | yes, by convention | one line — what the component is for |
113
+ | `description` | recommended | prose: behaviour, caveats, when to reach for it |
114
+ | `group` | recommended | catalog grouping (`Fields`, `Layout`, `Table`, `Markdown`, `Platform`, …) |
115
+ | `kind` | services only | `"service"` marks a non-rendering component; omit it on UI components |
116
+ | `name` | rarely | human-readable label; defaults to the component `id` |
117
+
118
+ ### Child elements
119
+
120
+ | Element | Declares | Attributes |
121
+ | --- | --- | --- |
122
+ | `<Prop>` | an input property | `id`, `type`, `description`, `required`, `default`, `enum`, `alias` |
123
+ | `<State>` | a property the component publishes back | `id`, `type`, `description`, `enum` |
124
+ | `<Method>` | a method reachable through a `->` binding | `id`, `args`, `description` |
125
+ | `<Slot>` | a named content slot | `name`, `description` |
126
+
127
+ `<Method args="...">` is free text describing the call shape — a bare name (`args="id"`) or a destructured object (`args="{ fieldId, value }"`).
128
+
129
+ The signature `<Slot name="…">` documents a slot; the runtime `<Slot Key="…">` renders one. Same tag, different attribute, different context.
130
+
131
+ Only `<Prop>` is consumed by `generate-catalog.ts` today; `<State>`, `<Method>` and `<Slot>` are read by people and agents.
132
+
133
+ ### Property types
134
+
135
+ `ComponentPropertyType` in `src/types.ts` declares `string | number | boolean | object | array | function | any`. Nothing validates against it, and the vocabulary actually in use is wider:
136
+
137
+ | Type | Use |
138
+ | --- | --- |
139
+ | `string`, `number`, `boolean`, `object`, `array`, `any` | as expected |
140
+ | `action` | a `->` handler property — the most common type after `string` |
141
+ | `enum` | a closed set; pair with `enum="sm,md,lg"` |
142
+ | `class` | a CSS class list |
143
+
144
+ `enum="a,b,c"` is a comma-separated list, split by the catalog generator. `required="true"` and `default="..."` are plain attribute values, so they pass through scalar coercion like any other.
145
+
146
+ ### Services
147
+
148
+ A TypeScript service is declared in CML as a signature-only component. Its body is the diagnostic shown when the class was not registered:
149
+
150
+ ```xml
151
+ <Component id="OnMount">
152
+ <Signature kind="service" purpose="Fires one action once, when the component mounts"
153
+ description="Arrmatura exposes no DOM mount event, so one-shot side effects wire through __init. Renders nothing."
154
+ group="Platform">
155
+ <Prop id="action" type="action" required="true" description="Handler invoked once on mount" />
156
+ </Signature>
157
+ <p class="error">Service OnMount is not imported</p>
158
+ </Component>
159
+ ```
160
+
161
+ ### Escaping
162
+
163
+ Descriptions are ordinary attribute values, so `<`, `>` and `"` must be written as entities: `filter_&lt;fieldId&gt;`, `action=&quot;-&gt; @docs.refresh()&quot;`. Backticks and `{}` are safe — the stripped block is never compiled.
164
+
165
+ ## Attribute forms
166
+
167
+ `ManifestNode.compileAttribute` classifies every attribute. The first matching row wins.
168
+
169
+ | # | Form | Example | Compiled as |
170
+ | --- | --- | --- | --- |
171
+ | 1 | `data-*` (except `data-theme`) | `data-row="{row}"` | merged into the `data` object under `row` |
172
+ | 2 | `Ref` | `Ref="api"` | reference name, not state |
173
+ | 3 | `Props` | `Props="{item}"` | object spread — each entry becomes a property |
174
+ | 4 | non-string value | `size="3"`, `open` | literal, applied once at init |
175
+ | 5 | `'…'`-wrapped value | `args="'key=value'"` | literal string, applied once at init — **the escape hatch for text containing `{` or `->`** |
176
+ | 6 | `@ref` (no `{`) | `data="@ctrl.options"` | reference or bound expression (see below) |
177
+ | 7 | `-> …` (no `{`) | `click="-> save()"` | event handler, applied once at init |
178
+ | 8 | contains `{` | `title="Hi {name}"` | expression, re-resolved on every render pass |
179
+ | 9 | anything else | `class="card"` | constant string |
180
+
181
+ Rows 6 and 7 are tested **only when the value contains no `{`**. `click="-> pick({id})"` is therefore not a handler — it is form 8, an interpolated string.
182
+
183
+ ## Expressions
184
+
185
+ A value containing `{` is an expression. Two shapes:
186
+
187
+ | Shape | Example | Result |
188
+ | --- | --- | --- |
189
+ | Whole-attribute placeholder | `count="{items.length}"` | the **raw value** — number, object, array, promise |
190
+ | Interpolated template | `title="{first} {last}"` | a **string**; each part is stringified, `null`/`undefined` render as `""` |
191
+
192
+ A whole-attribute placeholder must have no nested `{`; otherwise the template path is taken and the value becomes a string.
193
+
194
+ ### Primary forms
195
+
196
+ | Form | Example |
197
+ | --- | --- |
198
+ | Literals | `true`, `false`, `null`, `undefined`, `42`, `-5`, `'text'`, `"text"` |
199
+ | Identifier | `name` — resolved against the enclosing scope's component |
200
+ | Event payload | `it`, `event` — the handler argument; `undefined` outside a `->` binding |
201
+ | Resources | `R.app.name`, `R.enums[typeSpec]` — `R` is the whole resource tree |
202
+ | Member access | `obj.prop`, `arr[0]`, `obj["a" + b]` |
203
+ | Call | `find(items, id)`, `slice(data, 0, 5)` |
204
+ | Grouping | `(a + b) * c` |
205
+
206
+ Every `.prop` access is optional-chained, so a missing link yields `undefined` rather than throwing.
207
+
208
+ Functions resolve from `resources.functions` — an app helper cannot shadow a library one.
209
+
210
+ Identifier characters are `[A-Za-z0-9_$-]`, which **includes `-`**. `count-1` is one identifier; subtraction needs spaces: `count - 1`.
211
+
212
+ ### Pipes
213
+
214
+ ```xml
215
+ {value | upper}
216
+ {value | pipe:arg1:arg2 | pipe2 | pipe3}
217
+ {@histo.data | filter : 'isSelected' : true}
218
+ ```
219
+
220
+ A pipe is a function call with the piped value as the first argument. Pipes apply to the **whole placeholder only** — `{(name | upper)}` is a parse error.
221
+
222
+ ### Operator precedence
223
+
224
+ Loosest to tightest:
225
+
226
+ | Level | Operators | Notes |
227
+ | --- | --- | --- |
228
+ | 1 | `\|` (pipe) | top level of a placeholder only |
229
+ | 2 | `? :` | the `:` branch is optional; `{a ? b}` yields `undefined` when false |
230
+ | 3 | `??` | |
231
+ | 4 | `\|\|`, `OR` | |
232
+ | 5 | `&&`, `AND` | |
233
+ | 6 | `===`, `==`, `!=` | does not chain |
234
+ | 7 | `>=`, `>`, `<=`, `<`, `GTE`, `GT`, `LT` | does not chain |
235
+ | 8 | `+`, `-` | |
236
+ | 9 | `*`, `/`, `%` | |
237
+ | 10 | `!` | the only prefix operator |
238
+ | 11 | literal, identifier, `.`, `[]`, `(args)`, `(…)` | |
239
+
240
+ Word forms exist for use where `<` and `&` are awkward in XML.
241
+
242
+ **Not supported**: `**`, `!==`, `LTE`, unary `-` on an expression (only on a numeric literal), assignment, object and array literals, arrow functions, `new`, template literals.
243
+
244
+ ### Divergences from JavaScript
245
+
246
+ | Expression | CML | JS |
247
+ | --- | --- | --- |
248
+ | `1 == '1'` | `false` — `==` is strict | `true` |
249
+ | `10 - 3 - 2` | `9` — binary operators are **right**-associative | `5` |
250
+ | `12 / 6 / 2` | `4` | `1` |
251
+ | `'a' + 1 + 2` | `"a3"` | `"a12"` |
252
+ | `a == b == c` | parse error | `false` |
253
+
254
+ Parenthesise anything that chains a `-` or a `/`: `{(10 - 3) - 2}`.
255
+
256
+ ## References
257
+
258
+ `@` addresses another component by its `Ref=` name. Names resolve up the scope chain — current scope, then each parent — so a reference is visible only inside the subtree of the component that declares it.
259
+
260
+ ```xml
261
+ <Service Ref="api" />
262
+
263
+ <Display data="{@api.result}" /> <!-- reactive: a connector -->
264
+ <Editor service="@api" /> <!-- inject the component instance itself -->
265
+ <button click="-> @api.refresh()">Go</button>
266
+ ```
267
+
268
+ | Form | Meaning |
269
+ | --- | --- |
270
+ | `@ref` (alone, no dot) | inject the referenced component instance, read once at init |
271
+ | `@ref.prop` | a **connector** — subscribes to `ref` and re-applies the value whenever it changes |
272
+ | `@ref.prop` inside `->` | a plain read at call time, compiled to `$RefProp('ref','prop')`; no subscription |
273
+ | `@ref.method()` inside `->` | a call on the referenced component |
274
+
275
+ `this` / `This` are always resolvable and mean the current component.
276
+
277
+ `@` is rewritten by a regex that ignores quoting, so `@word.word` inside a string literal also becomes a reference: `'example@gmail.com'` resolves a phantom `gmail` ref.
278
+
279
+ ## Event bindings
280
+
281
+ A handler is an attribute whose value starts with `->` and contains no `{`.
282
+
283
+ ```xml
284
+ <button click="-> count = count + 1">+1</button>
285
+ <button click="-> @api.refresh()">Refresh</button>
286
+ <button click="-> save(it)">Save</button>
287
+ <input change="-> value = it; touched = true" />
288
+ ```
289
+
290
+ | Form | Effect |
291
+ | --- | --- |
292
+ | `-> key = expr` | `up({ key: <expr> })` on the enclosing component |
293
+ | `-> @ref.key = expr` | same, on the referenced component |
294
+ | `-> method(args)` | calls `method` on the enclosing component; no args passes the event payload |
295
+ | `-> @ref.method(args)` | calls the method on the referenced component |
296
+ | `-> key` | `up({ key: <payload> })` on the enclosing component |
297
+ | `a; b; c` | statements run in order, each awaited |
298
+
299
+ `it` and `event` inside a handler are the payload the platform passed in. A single argument is awaited before the call; two or more are passed as-is. A failing handler is logged, never rethrown.
300
+
301
+ ## Control flow
302
+
303
+ Control flow is expressed by reserved attributes and reserved tags. Syntax only here — see [built-ins.md](./built-ins.md) for behaviour.
304
+
305
+ | Syntax | Rule |
306
+ | --- | --- |
307
+ | `If="expr"` | expression; surrounding `{}` optional. Tested **before** the tag, so it works on any element |
308
+ | `If="Slot(name)"` | compile-time special form: true when slot `name` has content |
309
+ | `Each="item of expr"` | literally `<name> of <expression>`; **no braces**, the second word is skipped |
310
+ | `Each` + `If` together | legal: `Each` is tested first and wraps the loop, `If` then applies per item |
311
+ | `<Then>` / `<Else>` | branch wrappers, valid only as direct children of an `If` element |
312
+ | `<Fragment>` | groups nodes without emitting an element |
313
+ | `<Dynamic As="{expr}" Props="{obj}" />` | tag chosen at runtime; `As` also accepts a template: `As="Field.{type \| capitalize}"` |
314
+ | `<Block Nodes="{nodes}" />` | renders a runtime `XmlNode[]` |
315
+ | `<Slot Key="name" />` / `Slot="name"` | slot declaration / assignment of a child to a slot |
316
+ | `<Selector On="{expr}">` with `<Case When="value">` | exact string match; `When="default"` is the fallback |
317
+ | `<Connector data="{…}" change="-> …" />` | data-driven; add `trigger="{…}"` for the trigger-driven variant |
318
+
319
+ ```xml
320
+ <div If="isReady">Content</div>
321
+
322
+ <Fragment If="{@docs.loading}">
323
+ <Then><Spinner /></Then>
324
+ <Else><Article /></Else>
325
+ </Fragment>
326
+
327
+ <Row Each="row of pivot.rows" data="{row}" />
328
+ <Tag Each="tag of slice(data, 0, data.length > 6 ? 5 : 6)" data="{tag}" />
329
+ ```
330
+
331
+ Inside an `Each` body the item name is the only identifier resolved locally; everything else falls through to the enclosing scope. Items are keyed by the datum's own `id` field (not by any `id` attribute) — a duplicate is logged and **skipped**, so a list without unique ids renders one row.
332
+
333
+ ## Gotchas
334
+
335
+ - **`If="false"` is ignored.** `false`, `0` and `""` are scalar-coerced to falsy values, and the compiler tests `attrs.If` for truthiness — so the attribute is dropped and the element always renders. Use `If="{false}"`.
336
+ - **An unbraced value is a constant, not an expression.** `disabled="isBusy"` is the string `"isBusy"` (permanently truthy) and `click="save"` is a string, not a call. Only `If`, `Each`, `Props`, `As` and `Ref` read their value as an expression without braces.
337
+ - **Literal `{` needs the quote escape.** `<p>{id}</p>` compiles an expression; `<p args="'{id}'" />` keeps the braces. Text patterns containing braces are better supplied from resources via `Props=`.
338
+ - **`==` is strict.** Compare like with like — an id typed as a number never equals its string form.
339
+ - **`-` and `/` are right-associative**, and `a-b` without spaces is one identifier.
340
+ - **Exponentiation is not a CML operator.** `a ** b` silently yields `NaN` instead of a parse error.
341
+ - **Attribute names cannot contain `_` or `.`** — the attribute regex drops them.
342
+ - **A `Ref` is subtree-scoped.** `@foo` is unreachable from a sibling subtree; hoist the declaration or repeat it.
package/index.ts CHANGED
@@ -1,22 +1 @@
1
- import { IArrmatron, IPlatform } from "arrmatura/types";
2
-
3
- import { CRootNode } from "./src/registry/root";
4
-
5
- export * from "./src/registry";
6
- export { CRootNode } from "./src/registry/root";
7
- export * from "./src/core/Component";
8
-
9
- /**
10
- * Launches the runtime with given top-level template on the specified platform.
11
- *
12
- * @param {IPlatform} platform - The platform on which to launch the template.
13
- * @param {string} template - The template to launch with.
14
- * @return {IArrmatron} The root context object.
15
- */
16
- export const launch = (platform: IPlatform, template: string): IArrmatron => {
17
- const root = new CRootNode(template).createArrmatron(platform);
18
-
19
- root.up({}, true);
20
-
21
- return root;
22
- };
1
+ export * from "./src";
package/package.json CHANGED
@@ -1,27 +1,29 @@
1
1
  {
2
2
  "name": "arrmatura",
3
- "version": "6.3.2",
4
- "description": "Arrmatura runtime engine",
5
- "author": "alitskevich@gmail.com",
6
- "license": "ISC",
3
+ "version": "6.5.0",
4
+ "description": "General-purpose FRP framework",
5
+ "author": "alitskevich",
6
+ "license": "MIT",
7
7
  "type": "module",
8
+ "types": "./index.ts",
8
9
  "exports": {
9
- "import": "./index.ts",
10
- "require": "./dist/index.js"
10
+ "types": "./src/index.ts",
11
+ "require": "./dist/index.js",
12
+ "import": "./index.ts"
11
13
  },
12
14
  "files": [
13
- "src",
14
15
  "dist",
16
+ "docs",
15
17
  "index.ts",
16
- "types.ts",
17
- "docs/**/*"
18
+ "src"
18
19
  ],
19
- "dependencies": {
20
- "arrmatura": "6.3.2",
21
- "ultimus": "2.1.10"
22
- },
23
20
  "scripts": {
24
- "esbuild": "esbuild index.ts --bundle --outdir=./dist --platform=browser --format=esm --sourcemap --target=esnext --external:* --minify",
25
- "pnpm:publish": "pnpm publish --no-git-checks"
21
+ "build": "esbuild index.ts --bundle --outdir=dist --format=cjs --platform=node --target=esnext --packages=external --minify --keep-names",
22
+ "lint": "biome check ./src",
23
+ "lint:fix": "biome check ./src --fix",
24
+ "test": "vitest",
25
+ "typecheck": "tsc",
26
+ "prepublish": "npm run build",
27
+ "publish": "npm publish"
26
28
  }
27
- }
29
+ }