@muten/core 0.0.1
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 +97 -0
- package/dist/bin/muten.js +3 -0
- package/dist/build.js +12 -0
- package/dist/engine/compile/compile.js +10 -0
- package/dist/engine/compile/emit.js +92 -0
- package/dist/engine/compile/helpers.js +1 -0
- package/dist/engine/compile/logic.js +8 -0
- package/dist/engine/ir/compose.js +1 -0
- package/dist/engine/ir/flatten.js +1 -0
- package/dist/engine/ir/validate.js +1 -0
- package/dist/engine/lang/grammar.js +2 -0
- package/dist/engine/lang/lexer.js +4 -0
- package/dist/engine/lang/manifest.js +15 -0
- package/dist/engine/lang/parse.js +1 -0
- package/dist/engine/project/analyze.js +1 -0
- package/dist/engine/project/load.js +5 -0
- package/dist/engine/project/routes.js +5 -0
- package/dist/engine/project/styles.js +1 -0
- package/dist/engine/shared/diagnostics.js +1 -0
- package/dist/engine/shared/types.js +0 -0
- package/dist/engine/shared/vocab.js +1 -0
- package/dist/engine/style/tokens.js +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +6 -0
- package/dist/lint.js +2 -0
- package/dist/runtime.js +1 -0
- package/dist/vite-plugin-muten.js +15 -0
- package/package.json +56 -0
- package/spec/grammar.md +153 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present, Muten
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# muten
|
|
2
|
+
|
|
3
|
+
An **AI-first** frontend framework. You write `.muten` files; muten compiles them to vanilla JS
|
|
4
|
+
with fine-grained signals — **no virtual DOM, no framework runtime to ship**. The language is small,
|
|
5
|
+
semantic and analyzable on purpose: an AI (or a person) can **locate and mutate** an app cheaply.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm create muten@latest my-app # scaffold a new app (cross-platform: Windows + macOS)
|
|
9
|
+
cd my-app && npm install && npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## The app, by convention
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
my-app/
|
|
16
|
+
├─ src/
|
|
17
|
+
│ ├─ app.muten # the ROOT: routes (+ optional persistent shell)
|
|
18
|
+
│ ├─ pages/
|
|
19
|
+
│ │ └─ home/home.muten # a page; the folder name is its route target
|
|
20
|
+
│ ├─ parts/ # reusable .muten components (object + action params)
|
|
21
|
+
│ └─ components/ # host-written Custom JS (the escape hatch)
|
|
22
|
+
├─ theme.muten # the project's token scale (md=16px, breakpoints, …)
|
|
23
|
+
└─ src/styles.css # the look (muten ships structure + layout; the skin is yours)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`src/app.muten` is the single source of truth the AI reads first:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
routes {
|
|
30
|
+
/ -> home
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## CLI
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
muten build [dir] # compile → ./dist/<route>/index.html (+ dist/app.map.json, the app graph)
|
|
38
|
+
muten lint [dir] # parse + validate every page, no compile
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`build`/`lint` default to the current directory; pass a path to target another. The `muten` bin ships
|
|
42
|
+
with the app (it's a dependency). To scaffold a *new* app, use `npm create muten@latest` (the separate
|
|
43
|
+
[`create-muten`](https://www.npmjs.com/package/create-muten) scaffolder).
|
|
44
|
+
|
|
45
|
+
## Dev server (Vite)
|
|
46
|
+
|
|
47
|
+
The Vite plugin gives a Muten app a dev server + HMR + client-side routing while authoring stays the
|
|
48
|
+
DSL. `npm create muten` wires it up; `npm run dev` runs it.
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// vite.config.mjs
|
|
52
|
+
import muten from '@muten/core/vite-plugin-muten.js';
|
|
53
|
+
export default { plugins: [muten()] }; // theme.muten is auto-loaded
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Programmatic API
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { buildApp, compile, parse, validate, toDoc } from '@muten/core';
|
|
60
|
+
|
|
61
|
+
await buildApp('./my-app'); // same as `muten build ./my-app`
|
|
62
|
+
const html = compile(toDoc(parse(src))); // drive the compiler directly (embedding)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Architecture
|
|
66
|
+
|
|
67
|
+
The compiler is a straight pipeline of small, single-purpose stages:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
.muten ─[lang]→ IR ─[ir: compose]→ tree ─[ir: flatten]→ Doc ─[ir: validate]→ ✓ ─[compile]→ JS
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The source is TypeScript under `src/`, organized by **domain** — each has its own README:
|
|
74
|
+
|
|
75
|
+
| Domain | Role |
|
|
76
|
+
|---|---|
|
|
77
|
+
| [`src/engine/shared`](src/engine/shared/README.md) | contracts: types, the vocabulary (no magic strings), diagnostics |
|
|
78
|
+
| [`src/engine/lang`](src/engine/lang/README.md) | front-end: `.muten` text → IR (lexer · grammar · parser · manifest) |
|
|
79
|
+
| [`src/engine/ir`](src/engine/ir/README.md) | IR transforms + validation (compose · flatten · validate) |
|
|
80
|
+
| [`src/engine/compile`](src/engine/compile/README.md) | back-end: Doc → runnable JS (DOM + logic + emit + helpers) |
|
|
81
|
+
| [`src/engine/style`](src/engine/style/README.md) | the styling token vocabulary (the engine ships no values) |
|
|
82
|
+
| [`src/engine/project`](src/engine/project/README.md) | filesystem + whole-app awareness (load · analyze · routes · styles) |
|
|
83
|
+
|
|
84
|
+
The runtime (the only thing shipped to the browser), the Vite plugin, the CLI and the build/lint
|
|
85
|
+
orchestration also live in `src/`. See [`src/engine/README.md`](src/engine/README.md) for the
|
|
86
|
+
file-level conventions (≤500 lines, honest types, data-table dispatch, no magic strings).
|
|
87
|
+
|
|
88
|
+
## Build
|
|
89
|
+
|
|
90
|
+
`npm run build` = `tsc` (strict type-check) + `esbuild` → `dist/**/*.js`, **minified, per-file**
|
|
91
|
+
(modules preserved, so nothing bundles into a heavy monolith). `dist/` is generated — edit `src/`.
|
|
92
|
+
|
|
93
|
+
## Styling & escape hatch
|
|
94
|
+
|
|
95
|
+
muten imposes no theme. A page lays itself out with `style(…)` tokens (analyzable, resolved against
|
|
96
|
+
`theme.muten`) and skins itself via `class("…")` (your CSS / Tailwind / anything). For behavior the
|
|
97
|
+
primitives can't express, drop to a `Custom` component (`src/components/<Name>.js`).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{resolve as r}from"node:path";import{buildApp as s}from"../build.js";import{lintApp as i}from"../lint.js";const[t,o]=process.argv.slice(2);try{t==="build"?await s(r(o||process.cwd())):t==="lint"?process.exit(await i(r(o||process.cwd()))?1:0):(console.error(`usage: muten <build|lint> [dir] (default: the current directory)
|
|
3
|
+
to create an app: npm create muten@latest <dir>`),process.exit(1))}catch(e){console.error("\u2716 "+(e instanceof Error?e.message:String(e))),process.exit(1)}
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import{writeFileSync as u,mkdirSync as j,readFileSync as A,existsSync as S,rmSync as E}from"node:fs";import{join as s,relative as N}from"node:path";import{Nt as M}from"#engine/shared/vocab.js";import{readRoutes as C}from"#engine/project/routes.js";import{load as F,loadAllParts as V}from"#engine/project/load.js";import{validate as v}from"#engine/ir/validate.js";import{compile as H}from"#engine/compile/compile.js";import{formatDiagnostic as $,ParseError as I}from"#engine/shared/diagnostics.js";const J=r=>typeof r=="string"?r:r&&typeof r=="object"&&!Array.isArray(r)&&typeof r.url=="string"?r.url:"";async function T(r,n=s(r,"dist")){const i=t=>N(r,t);E(n,{recursive:!0,force:!0});const a=await V(r);Object.keys(a).length&&console.log(`Parts: ${Object.keys(a).join(", ")}`);const f=C(r);console.log(`Host app: ${r}`),console.log(`Pages: ${f.map(t=>"/"+t.route).join(", ")}
|
|
2
|
+
`);const l=[],g={app:r.split(/[\\/]/).pop()||"",parts:Object.keys(a),routes:{}};for(const t of f){let d;try{d=await F(t.screenPath,a)}catch(e){if(!(e instanceof I))throw e;const o={code:e.code,severity:"error",message:e.message,loc:e.loc,suggestion:null};throw new Error(`/${t.route}
|
|
3
|
+
`+$(o,i(t.screenPath)))}const{doc:c,data:O,sources:h,styles:p,partNames:w}=d,{ok:P,diagnostics:k}=v(c,{parts:w});if(!P)throw new Error(`/${t.route}
|
|
4
|
+
`+k.map(e=>" "+$(e,i(t.screenPath))).join(`
|
|
5
|
+
`));const x=[...new Set(Object.values(c.nodes).filter(e=>e.type===M.Custom).map(e=>e.props?.component))],y={};for(const e of x){if(!e)continue;const o=s(r,"src","components",e+".js");if(!S(o))throw new Error(`/${t.route}: Custom component not found: src/components/${e}.js`);y[e]=A(o,"utf8")}const m=s(n,t.route);j(m,{recursive:!0}),u(s(m,"index.html"),H(c,O,p.css,y,h)),console.log(`\u2713 /${t.route} \u2192 ${i(s(m,"index.html"))} (${Object.keys(c.nodes).length} nodes${p.from?", + "+p.from:""})`),l.push(t.route),g.routes["/"+t.route]={file:i(t.screenPath),models:Object.keys(c.entities),state:Object.fromEntries(Object.entries(c.state).map(([e,o])=>[e,typeof o.source=="string"?o.source:o.initial??null])),sources:Object.fromEntries(Object.entries(h).map(([e,o])=>[e,J(o)]))}}j(n,{recursive:!0});const b=l.map(t=>`<li><a href="./${t}/">/${t}</a></li>`).join(`
|
|
6
|
+
`);return u(s(n,"index.html"),`<!doctype html><meta charset="utf-8"><title>app</title>
|
|
7
|
+
<h1>Routes</h1>
|
|
8
|
+
<ul>
|
|
9
|
+
${b}
|
|
10
|
+
</ul>
|
|
11
|
+
`),u(s(n,"app.map.json"),JSON.stringify(g,null,2)),console.log(`
|
|
12
|
+
\u2713 ${i(s(n,"index.html"))} \u2192 route index`),console.log(`\u2713 ${i(s(n,"app.map.json"))} \u2192 app graph (the root the AI reads)`),{routes:l,outDir:n}}export{T as buildApp};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import{tokenClass as V,resolveToken as fe,defaultTheme as $e}from"#engine/style/tokens.js";import{Nt as a,Ek as me,Fmt as C,Fk as P}from"#engine/shared/vocab.js";import{customValue as he,CONTAINERS as X,parseClause as ge,editableFields as _e}from"#engine/compile/helpers.js";import{emitStore as de,emitStatic as be,emitModule as ye,emitHtml as Se}from"#engine/compile/emit.js";import{Logic as Ne}from"#engine/compile/logic.js";function ke(h,E={},v="",O={},x={},g={}){return Y(h,E,v,O,x,{...g,format:C.Module})}function we(h={},E={},v={}){const{state:O={},gets:x={},actions:g={},effects:d=[],entities:S={}}=h;return Y({screen:"store",entities:S,state:O,actions:g,gets:x,effects:d,consts:{},constraints:{},rootId:void 0,nodes:{}},E,"",{},v,{format:C.Store})}function Y(h,E={},v="",O={},x={},g={}){const{nodes:d,rootId:S,state:k,entities:L,screen:Z}=h,R=g.theme||$e;let t=[],q=!1;const A=e=>{const s=t;t=[],e();const o=t;return t=s,o},B=new Set,$=(e,s)=>{for(const o of s.style||[])B.add(o);return[e,...(s.style||[]).map(V),...s.class||[]].join(" ")},ee=new Set(Object.keys(k)),M=new Set(Object.entries(k).filter(([,e])=>typeof e.source=="string"&&e.source.startsWith("query:")).map(([e])=>e)),H=new Set,te={state:k,entities:L,actions:h.actions,consts:h.consts||{},gets:h.gets||{},effects:h.effects||[],stateKeys:ee,queryStates:M,stores:g.stores||{},usedStores:H,format:g.format},f=new Ne(te),w={locals:new Set},J=(e,s)=>{for(const o of d[e].children)K(o,s)},W=e=>e.parts.map(s=>typeof s=="string"?JSON.stringify(s):`String(${f.compileExpr(s,w)} ?? '')`).join(" + ");function D(e,s,o,n,p){if(t.push(`const el_${e} = document.createElement('${s}');`),t.push(`el_${e}.className = ${JSON.stringify(o)};`),typeof n=="string")t.push(`el_${e}.textContent = ${JSON.stringify(n)};`);else if(n&&"kind"in n){const c=W(n),l=n.parts.some(u=>typeof u!="string");t.push(l?`effect(() => { el_${e}.textContent = ${c}; });`:`el_${e}.textContent = ${c};`)}t.push(`${p}.appendChild(el_${e});`)}function T(e,s,o){if(typeof o=="string")t.push(`el_${e}.${s} = ${JSON.stringify(o)};`);else if(o&&"kind"in o){const n=W(o),p=o.parts.some(c=>typeof c!="string");t.push(p?`effect(() => { el_${e}.${s} = ${n}; });`:`el_${e}.${s} = ${n};`)}}function K(e,s){const o=d[e],n=o.props||{},p=X[o.type];if(p){const[c,l]=p;t.push(`const el_${e} = document.createElement('${c}');`),t.push(`el_${e}.className = ${JSON.stringify($(l,n))};`),o.type===a.Nav&&typeof n.label=="string"&&t.push(`el_${e}.setAttribute('aria-label', ${JSON.stringify(n.label)});`),t.push(`${s}.appendChild(el_${e});`),J(e,`el_${e}`);return}switch(o.type){case a.SearchField:{const c=f.bindSig(n.bind);t.push(`const el_${e} = document.createElement('input');`),t.push(`el_${e}.type = 'search';`),t.push(`el_${e}.className = ${JSON.stringify($("search",n))};`),typeof n.placeholder=="string"&&t.push(`el_${e}.placeholder = ${JSON.stringify(n.placeholder)};`),t.push(`el_${e}.value = ${c}.get();`),t.push(`el_${e}.addEventListener('input', (e) => ${c}.set(e.target.value));`),t.push(`${s}.appendChild(el_${e});`);break}case a.DataTable:{const c=f.bindSig(n.data),l=M.has(c)?`${c}.get().data`:`${c}.get()`,u=n.columns||[],_=(n.where||[]).map(ge),j=_.filter(r=>!r.dynamic).map(r=>`.filter((row) => ${r.expr})`).join(""),b=_.filter(r=>r.dynamic).map(r=>`.filter((row) => ${r.expr})`).join(""),i=o.children.map(r=>d[r]).filter(r=>r.type===a.RowAction);t.push(`const el_${e} = document.createElement('table');`),t.push(`el_${e}.className = ${JSON.stringify($("datatable",n))};`),t.push(`const head_${e} = el_${e}.createTHead().insertRow();`);for(const r of u)t.push(`{ const th = document.createElement('th'); th.textContent = ${JSON.stringify(r)}; head_${e}.appendChild(th); }`);i.length&&t.push(`head_${e}.appendChild(document.createElement('th'));`),t.push(`const body_${e} = el_${e}.createTBody();`),t.push(`${s}.appendChild(el_${e});`),t.push(`function renderRow_${e}(row) {`),t.push(" const tr = document.createElement('tr');");for(const r of u)t.push(` { const td = document.createElement('td'); td.textContent = row[${JSON.stringify(r)}] ?? ''; tr.appendChild(td); }`);for(const r of i){const m=r.props||{},pe=m.arg!==void 0?f.compileExpr(m.arg,{locals:new Set(["row"])}):"";t.push(` { const td = document.createElement('td'); const b = document.createElement('button'); b.className = ${JSON.stringify($("row-action",m))}; b.textContent = ${JSON.stringify(m.label)}; b.addEventListener('click', () => ${f.actionRef(m.action)}(${pe})); td.appendChild(b); tr.appendChild(td); }`)}t.push(" return tr;"),t.push("}"),t.push(`function base_${e}() { return ${l}${j}; }`),t.push("effect(() => {"),t.push(` const rows = base_${e}()${b};`),t.push(` body_${e}.replaceChildren(...rows.map(renderRow_${e}));`),t.push("});");break}case a.Form:{const c=f.bindSig(n.bind),l=k[c].type,u=_e(L[l]),_=(h.constraints||{})[l]||{};t.push(`const el_${e} = document.createElement('form');`),t.push(`el_${e}.className = ${JSON.stringify($("form",n))};`),t.push(`{ const t = document.createElement('div'); t.className = 'form-title'; t.textContent = ${JSON.stringify("New "+l)}; el_${e}.appendChild(t); }`);const j=[];for(const i of u){const r=`f_${e}_${i.name}`;if(j.push({...i,var:r,c:_[i.name]}),i.kind===P.Enum){t.push(`const ${r} = document.createElement('select');`),t.push(`${r}.className = 'field';`);for(const m of i.options)t.push(`{ const o = document.createElement('option'); o.value = ${JSON.stringify(m)}; o.textContent = ${JSON.stringify(m)}; ${r}.appendChild(o); }`)}else t.push(`const ${r} = document.createElement('input');`),t.push(`${r}.type = ${JSON.stringify(i.kind===P.Email?"email":"text")};`),t.push(`${r}.className = 'field';`),t.push(`${r}.placeholder = ${JSON.stringify(i.name)};`);t.push(`${r}.addEventListener('input', (e) => ${c}.set({ ...${c}.get(), ${JSON.stringify(i.name)}: e.target.value }));`),t.push(`el_${e}.appendChild(${r});`),_[i.name]&&t.push(`const err_${r} = document.createElement('small'); err_${r}.className = 'field-error'; el_${e}.appendChild(err_${r});`)}t.push(`{ const sb = document.createElement('button'); sb.type = 'submit'; sb.className = 'submit'; sb.textContent = ${JSON.stringify(typeof n.submitLabel=="string"?n.submitLabel:"Submit")}; el_${e}.appendChild(sb); }`);const b=[];for(const i of j){if(!i.c)continue;const r=`err_${i.var}`,m=`String(__d[${JSON.stringify(i.name)}] ?? '')`;b.push(`${r}.textContent = '';`),i.c.required&&b.push(`if (!${m}.trim()) { ${r}.textContent = 'Required'; __ok = false; }`),i.c.min!=null&&b.push(`if (${m} && ${m}.length < ${i.c.min}) { ${r}.textContent = 'Min ${i.c.min} characters'; __ok = false; }`),i.c.max!=null&&b.push(`if (${m}.length > ${i.c.max}) { ${r}.textContent = 'Max ${i.c.max} characters'; __ok = false; }`)}b.length?t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); const __d = ${c}.get(); let __ok = true; ${b.join(" ")} if (__ok) ${f.actionRef(n.submit)}(); });`):t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); ${f.actionRef(n.submit)}(); });`),t.push("effect(() => {"),t.push(` const d = ${c}.get();`);for(const i of j){const r=i.kind===P.Enum?JSON.stringify(i.options[0]):"''";t.push(` { const v = d[${JSON.stringify(i.name)}] ?? ${r}; if (${i.var}.value !== v) ${i.var}.value = v; }`)}t.push("});"),t.push(`${s}.appendChild(el_${e});`);break}case a.Text:D(e,"p",$("text",n),n.value,s);break;case a.Span:D(e,"span",$("span",n),n.value,s);break;case a.Title:D(e,n.level||"h1",$("title",n),n.value,s);break;case a.Image:{t.push(`const el_${e} = document.createElement('img');`),t.push(`el_${e}.className = ${JSON.stringify($("image",n))};`),T(e,"src",n.src),T(e,"alt",n.alt??""),t.push(`${s}.appendChild(el_${e});`);break}case a.When:{if(!n.cond)throw new Error("when without a condition");const c=f.compileExpr(n.cond,w),l=A(()=>J(e,"__p"));t.push(`function build_${e}(__p) {`);for(const u of l)t.push(" "+u);t.push("}"),t.push(`const anchor_${e} = document.createComment('when');`),t.push(`${s}.appendChild(anchor_${e});`),t.push(`let shown_${e} = [];`),t.push("effect(() => {"),t.push(` if (${c}) {`),t.push(` if (!shown_${e}.length) { const __f = document.createDocumentFragment(); build_${e}(__f); shown_${e} = [...__f.childNodes]; anchor_${e}.parentNode.insertBefore(__f, anchor_${e}); }`),t.push(` } else if (shown_${e}.length) { for (const __n of shown_${e}) __n.remove(); shown_${e} = []; }`),t.push("});");break}case a.Each:{if(!n.list||!n.as)throw new Error("each without a list or item variable");const c=f.compileExpr(n.list,w),l=A(()=>J(e,"__p"));t.push(`function buildItem_${e}(__p, ${n.as}) {`);for(const u of l)t.push(" "+u);t.push("}"),t.push(`const anchor_${e} = document.createComment('each');`),t.push(`${s}.appendChild(anchor_${e});`),t.push(`let items_${e} = [];`),t.push("effect(() => {"),t.push(` for (const __n of items_${e}) __n.remove();`),t.push(" const __f = document.createDocumentFragment();"),t.push(` for (const ${n.as} of (${c} ?? [])) buildItem_${e}(__f, ${n.as});`),t.push(` items_${e} = [...__f.childNodes];`),t.push(` anchor_${e}.parentNode.insertBefore(__f, anchor_${e});`),t.push("});");break}case a.Custom:{t.push(`const el_${e} = document.createElement('div');`),t.push(`el_${e}.className = ${JSON.stringify($("custom",n))};`),t.push(`${s}.appendChild(el_${e});`);const c=Object.entries(n.inputs||{}).map(([u,_])=>`${JSON.stringify(u)}: ${he(_)}`).join(", "),l=Object.entries(n.on||{}).map(([u,_])=>`${JSON.stringify(u)}: (...__a) => ${f.actionRef(typeof _=="string"?_:"")}(...__a)`).join(", ");t.push(`if (typeof __custom_${n.component} === 'function') __custom_${n.component}(el_${e}, { ${c} }, { ${l} });`);break}case a.Button:{if(t.push(`const el_${e} = document.createElement('button');`),t.push(`el_${e}.className = ${JSON.stringify($("button",n))};`),o.children&&o.children.length?J(e,`el_${e}`):n.label!==void 0&&T(e,"textContent",n.label),n.action){const c=n.arg!==void 0?f.compileExpr(n.arg,w):"";t.push(`el_${e}.addEventListener('click', () => ${f.actionRef(n.action)}(${c}));`)}t.push(`${s}.appendChild(el_${e});`);break}case a.Link:{t.push(`const el_${e} = document.createElement('a');`),t.push(`el_${e}.className = ${JSON.stringify($("link",n))};`),t.push(`el_${e}.href = ${JSON.stringify("#"+(n.to||"/"))};`),o.children&&o.children.length?J(e,`el_${e}`):n.label!==void 0&&T(e,"textContent",n.label),t.push(`${s}.appendChild(el_${e});`);break}case a.Slot:{q=!0,t.push("const __outlet = document.createElement('div');"),t.push("__outlet.className = 'muten-outlet';"),t.push(`${s}.appendChild(__outlet);`);break}default:throw new Error("unsupported primitive: "+o.type)}}const N=e=>String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"),F=e=>N(e).replace(/"/g,"""),y=e=>typeof e=="string"?e:"";function ne(){if(g.format!==C.Module)return!1;const e=new Set([a.When,a.Each,a.Custom,a.Form,a.SearchField,a.DataTable,a.Slot]),s=["action","bind","submit","on","inputs","data"],o=["value","src","alt","label"];for(const n of Object.keys(d)){const p=d[n],c=p.props||{};if(e.has(p.type)||s.some(l=>c[l]!==void 0)||o.some(l=>{const u=c[l];return!!u&&typeof u=="object"&&"kind"in u&&u.kind===me.Interp}))return!1}return!0}function U(e){const s=d[e],o=s.props||{},n=()=>(d[e].children||[]).map(U).join(""),p=l=>` class="${F($(l,o))}"`,c=X[s.type];if(c){const[l,u]=c;return`<${l}${p(u)}>${n()}</${l}>`}switch(s.type){case a.Text:return`<p${p("text")}>${N(y(o.value))}</p>`;case a.Span:return`<span${p("span")}>${N(y(o.value))}</span>`;case a.Title:{const l=o.level||"h1";return`<${l}${p("title")}>${N(y(o.value))}</${l}>`}case a.Image:return`<img${p("image")} src="${F(y(o.src))}" alt="${F(y(o.alt))}">`;case a.Link:return`<a${p("link")} href="${F("#"+(o.to||"/"))}">${s.children&&s.children.length?n():N(y(o.label))}</a>`;case a.Button:return`<button${p("button")}>${s.children&&s.children.length?n():N(y(o.label))}</button>`;default:return""}}const z=ne(),se=f.genState(),oe=f.genActions(),re=Object.entries(h.gets||{}).map(([e,s])=>`export const ${e} = computed(() => ${f.compileExpr(s,w)});`).join(`
|
|
2
|
+
`),ce=f.genEffects();let G=null;z?G=S?U(S):"":g.format!==C.Store&&S&&K(S,"app");const ie=t.join(`
|
|
3
|
+
`),Q={};for(const e of Object.values(k))if(typeof e.source=="string"&&e.source.startsWith("query:")){const s=e.source.slice(6),o=(e.type.match(/^list<(.+)>$/)||[])[1];Q[s]=o?f.uuidFields(o):[]}const ae=[...B].map(e=>{const s=fe(e,R);if(!s)return"";const o=`.${V(e)}{${s}}`,n=e.indexOf(":"),p=n>0&&R.breakpoints[e.slice(0,n)];return p?`@media (min-width:${p}){${o}}`:o}).filter(Boolean).join(`
|
|
4
|
+
`),le=Object.entries(O).map(([e,s])=>`const __custom_${e} = (function () {
|
|
5
|
+
${s}
|
|
6
|
+
return mount;
|
|
7
|
+
})();`).join(`
|
|
8
|
+
|
|
9
|
+
`),ue=[...H].map(e=>`import * as __store_${e} from 'virtual:muten/store/${e}';`).join(`
|
|
10
|
+
`),I={screen:Z,tokenCss:ae,projectCss:v,data:E,sources:x,queryUuids:Q,stateDecls:se,actionDecls:oe,getDecls:re,effectDecls:ce,componentDecls:le,storeImports:ue,renderBody:ie,staticHtml:G??"",hasSlot:q};return g.format===C.Store?de(I):z?be(I):g.format===C.Module?ye(I):Se(I)}export{Y as compile,ke as compileModule,we as compileStore};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const n=`// \u2500\u2500 fine-grained signals runtime (~18 lines, no dependencies) \u2500\u2500
|
|
2
|
+
let __current = null;
|
|
3
|
+
function signal(value) {
|
|
4
|
+
const subs = new Set();
|
|
5
|
+
return {
|
|
6
|
+
get() { if (__current) subs.add(__current); return value; },
|
|
7
|
+
set(next) { if (next === value) return; value = next; for (const e of [...subs]) e(); },
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function effect(fn) {
|
|
11
|
+
const run = () => { const prev = __current; __current = run; try { fn(); } finally { __current = prev; } };
|
|
12
|
+
run();
|
|
13
|
+
}
|
|
14
|
+
function __has(a, b) { return Array.isArray(a) ? a.includes(b) : String(a ?? '').toLowerCase().includes(String(b ?? '').toLowerCase()); }`;function t(e){return`const __DATA = ${JSON.stringify(e.data)};
|
|
15
|
+
const __SOURCES = ${JSON.stringify(e.sources)};
|
|
16
|
+
const __UUIDS = ${JSON.stringify(e.queryUuids)};
|
|
17
|
+
const __DELAY = 450;
|
|
18
|
+
const __fill = (name, rows) => { const ids = __UUIDS[name] || []; return rows.map((r) => { const o = { ...r }; for (const f of ids) if (o[f] === null || o[f] === undefined) o[f] = __id(); return o; }); };
|
|
19
|
+
function __fetch(name) { const s = __SOURCES[name]; if (s) { const url = typeof s === 'string' ? s : s.url; const at = typeof s === 'string' ? null : s.at; return fetch(url).then((r) => r.json()).then((j) => __fill(name, at ? (j[at] ?? []) : (Array.isArray(j) ? j : []))); } return new Promise((res) => setTimeout(() => res(__fill(name, __DATA[name] ?? [])), __DELAY)); }
|
|
20
|
+
function query(name) { const sig = signal({ data: [], loading: true, error: null }); __fetch(name).then((d) => sig.set({ data: d, loading: false, error: null })).catch((e) => sig.set({ data: [], loading: false, error: String(e) })); return sig; }`}function r(e){return`import { signal, computed, effect, __id, __has } from 'virtual:muten/runtime';
|
|
21
|
+
|
|
22
|
+
${t(e)}
|
|
23
|
+
|
|
24
|
+
${e.stateDecls}
|
|
25
|
+
|
|
26
|
+
${e.getDecls}
|
|
27
|
+
|
|
28
|
+
${e.actionDecls}
|
|
29
|
+
|
|
30
|
+
${e.effectDecls}
|
|
31
|
+
`}function o(e){return`export const screen = ${JSON.stringify(e.screen)};
|
|
32
|
+
export const css = ${JSON.stringify(`${e.tokenCss}
|
|
33
|
+
${e.projectCss}`)};
|
|
34
|
+
export function mount(app) { app.innerHTML = ${JSON.stringify(e.staticHtml)}; return app; }
|
|
35
|
+
`}function s(e){return`import { signal, effect, __id, __has } from 'virtual:muten/runtime';
|
|
36
|
+
${e.storeImports}
|
|
37
|
+
export const screen = ${JSON.stringify(e.screen)};
|
|
38
|
+
export const css = ${JSON.stringify(`${e.tokenCss}
|
|
39
|
+
${e.projectCss}`)};
|
|
40
|
+
|
|
41
|
+
export function mount(app) {
|
|
42
|
+
${t(e)}
|
|
43
|
+
|
|
44
|
+
${e.stateDecls}
|
|
45
|
+
|
|
46
|
+
${e.actionDecls}
|
|
47
|
+
|
|
48
|
+
${e.componentDecls}
|
|
49
|
+
|
|
50
|
+
${e.renderBody}
|
|
51
|
+
return ${e.hasSlot?"__outlet":"app"};
|
|
52
|
+
}
|
|
53
|
+
`}function i(e){return`<!doctype html>
|
|
54
|
+
<html lang="en">
|
|
55
|
+
<head>
|
|
56
|
+
<meta charset="utf-8">
|
|
57
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
58
|
+
<title>${e.screen}</title>
|
|
59
|
+
<style>
|
|
60
|
+
/* engine: only the used tokens \u2014 no base styles (those are the project's stylesheet) */
|
|
61
|
+
${e.tokenCss}
|
|
62
|
+
/* project: overrides the above via the cascade (bring-your-own-theme) */
|
|
63
|
+
${e.projectCss}
|
|
64
|
+
</style>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<div id="app"></div>
|
|
68
|
+
<script type="module">
|
|
69
|
+
${n}
|
|
70
|
+
|
|
71
|
+
// \u2500\u2500 dynamic ids (nothing hardcoded) \u2500\u2500
|
|
72
|
+
let __seq = 0;
|
|
73
|
+
function __id() { return (globalThis.crypto && crypto.randomUUID) ? crypto.randomUUID() : 'id-' + (++__seq); }
|
|
74
|
+
|
|
75
|
+
${t(e)}
|
|
76
|
+
|
|
77
|
+
// \u2500\u2500 declared state (state from the IR) \u2500\u2500
|
|
78
|
+
${e.stateDecls}
|
|
79
|
+
|
|
80
|
+
// \u2500\u2500 actions (actions from the IR) \u2500\u2500
|
|
81
|
+
${e.actionDecls}
|
|
82
|
+
|
|
83
|
+
// \u2500\u2500 custom components (host-written, opaque to the IR) \u2500\u2500
|
|
84
|
+
${e.componentDecls}
|
|
85
|
+
|
|
86
|
+
// \u2500\u2500 render: imperative DOM + fine-grained effects \u2500\u2500
|
|
87
|
+
const app = document.getElementById('app');
|
|
88
|
+
${e.renderBody}
|
|
89
|
+
<\/script>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
92
|
+
`}export{n as RUNTIME,i as emitHtml,s as emitModule,o as emitStatic,r as emitStore};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Nt as s,BOp as e,Fk as a}from"#engine/shared/vocab.js";const u=t=>typeof t=="string"&&t.startsWith("@")?`${t.slice(1)}.get()`:JSON.stringify(t),f={[s.Shell]:["div","shell"],[s.Header]:["header","header"],[s.Nav]:["nav","nav"],[s.Sidebar]:["aside","sidebar"],[s.Footer]:["footer","footer"],[s.Page]:["main","page"],[s.Stack]:["div","stack"]},m={[e.Eq]:"===",[e.Neq]:"!==",[e.Lt]:"<",[e.Gt]:">",[e.Lte]:"<=",[e.Gte]:">=",[e.And]:"&&",[e.Or]:"||",[e.Add]:"+",[e.Sub]:"-",[e.Mul]:"*",[e.Div]:"/"};function h(t){let n,r,i;if(t.includes(" contains "))n="contains",[r,i]=t.split(" contains ").map(o=>o.trim());else if(t.includes("=="))n="eq",[r,i]=t.split("==").map(o=>o.trim());else throw new Error("unsupported where clause: "+t);const l=i.startsWith("@"),d=l?`${i.slice(1)}.get()`:JSON.stringify(i),p=JSON.stringify(r),c=n==="eq"?`row[${p}] === ${d}`:`String(row[${p}] ?? '').toLowerCase().includes(String(${d}).toLowerCase())`;return{dynamic:l,expr:c}}function y(t){const n=[];for(const[r,i]of Object.entries(t))i!=="uuid"&&(i.startsWith("enum:")?n.push({name:r,kind:a.Enum,options:i.slice(5).split("|")}):i==="email"?n.push({name:r,kind:a.Email}):n.push({name:r,kind:a.Text}));return n}export{f as CONTAINERS,m as JS_BINOP,u as customValue,y as editableFields,h as parseClause};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{Ek as c,StOp as p,BOp as f,UOp as h,Fmt as u}from"#engine/shared/vocab.js";import{JS_BINOP as l}from"#engine/compile/helpers.js";class x{constructor(t){this.ctx=t}ctx;inStore(t,i,e){const r=this.ctx.stores[t];return!!r&&(r[e]||[]).includes(i)}actionRef(t){if(!t)return"";const[i,e]=t.split(".");return e&&this.inStore(i,e,"actions")?(this.ctx.usedStores.add(i),`__store_${i}.${e}`):t}bindSig(t){if(typeof t!="string")return"";if(t.startsWith("@"))return t.slice(1);const[i,e]=t.split(".");return e&&this.ctx.stores[i]?(this.ctx.usedStores.add(i),`__store_${i}.${e}`):t}uuidFields(t){const i=this.ctx.entities[t]||{};return Object.entries(i).filter(([,e])=>e==="uuid").map(([e])=>e)}resolveRef(t,i){const[e,...r]=t.split("."),s=r.length?"."+r.join("."):"";if(i.locals.has(e))return e+s;if(this.ctx.queryStates.has(e))return r[0]==="loading"||r[0]==="error"?`${e}.get()${s}`:`${e}.get().data${s}`;if(this.ctx.stateKeys.has(e))return`${e}.get()`+s;if(this.ctx.stores[e]){const n=r[0],a=r.length>1?"."+r.slice(1).join("."):"";if(this.inStore(e,n,"state")||this.inStore(e,n,"gets"))return this.ctx.usedStores.add(e),`__store_${e}.${n}.get()${a}`}return this.ctx.consts[e]!==void 0?JSON.stringify(r.length?null:this.ctx.consts[e]):e+s}compileExpr(t,i){if(t.kind===c.Lit)return JSON.stringify(t.value);if(t.kind===c.Ref)return this.resolveRef(t.name,i);if(t.kind===c.Tern)return`(${this.compileExpr(t.cond,i)} ? ${this.compileExpr(t.then,i)} : ${this.compileExpr(t.else,i)})`;if(t.kind===c.Un){if(t.op===h.Not)return`!(${this.compileExpr(t.operand,i)})`;throw new Error("unsupported unary operator")}if(t.kind===c.Bin){const e=this.compileExpr(t.left,i),r=this.compileExpr(t.right,i);if(t.op===f.Contains)return`__has(${e}, ${r})`;const s=l[t.op];if(s)return`(${e} ${s} ${r})`;throw new Error("unsupported operator: "+t.op)}throw new Error("unsupported expression")}stmtLines(t,i){const e=this.ctx,r=[];if(t.op===p.If){r.push(`if (${this.compileExpr(t.cond,i)}) {`);for(const s of t.then||[])for(const n of this.stmtLines(s,i))r.push(" "+n);if(t.else){r.push("} else {");for(const s of t.else)for(const n of this.stmtLines(s,i))r.push(" "+n)}return r.push("}"),r}if(t.op===p.Reset)r.push(`${t.target}.set(${JSON.stringify(e.state[t.target].initial??null)});`);else if(t.op===p.Set)r.push(`${t.target}.set(${this.compileExpr(t.arg,i)});`);else if(t.op===p.Push){const s=(e.state[t.target].type.match(/^list<(.+)>$/)||[])[1],n=s&&e.entities[s],a=o=>e.queryStates.has(t.target)?`${t.target}.set({ ...${t.target}.get(), data: [...${t.target}.get().data, ${o}] });`:`${t.target}.set([...${t.target}.get(), ${o}]);`;if(n){r.push(`{ const __it = { ...${this.compileExpr(t.arg,i)} };`);for(const o of this.uuidFields(s))r.push(` if (__it.${o} === null || __it.${o} === undefined) __it.${o} = __id(); // auto uuid`);r.push(` ${a("__it")} }`)}else r.push(`${a(this.compileExpr(t.arg,i))}`)}else if(t.op===p.Remove){const s={...i,locals:new Set([...i.locals,t.param])},n=this.compileExpr(t.pred,s);r.push(e.queryStates.has(t.target)?`${t.target}.set({ ...${t.target}.get(), data: ${t.target}.get().data.filter((${t.param}) => !(${n})) });`:`${t.target}.set(${t.target}.get().filter((${t.param}) => !(${n})));`)}return r}genState(){const t=this.ctx.format===u.Store?"export ":"",i=[];for(const[e,r]of Object.entries(this.ctx.state))typeof r.source=="string"&&r.source.startsWith("query:")?i.push(`${t}const ${e} = query(${JSON.stringify(r.source.slice(6))}); // async: ${e}.loading / .error / .data`):i.push(`${t}const ${e} = signal(${JSON.stringify(r.initial??null)});`);return i.join(`
|
|
2
|
+
`)}genActions(){const t=this.ctx.format===u.Store?"export ":"",i=[];for(const[e,r]of Object.entries(this.ctx.actions)){const s=this.ctx.stateKeys.has(r.input),n={locals:new Set,input:r.input,inputIsState:s};i.push(`${t}function ${e}(${s?"":r.input}) {`);for(const a of r.body||[])for(const o of this.stmtLines(a,n))i.push(" "+o);i.push("}")}return i.join(`
|
|
3
|
+
`)}genEffects(){const t={locals:new Set};return this.ctx.effects.map(i=>`effect(() => {
|
|
4
|
+
${i.map(e=>this.stmtLines(e,t).map(r=>" "+r).join(`
|
|
5
|
+
`)).join(`
|
|
6
|
+
`)}
|
|
7
|
+
});`).join(`
|
|
8
|
+
`)}}export{x as Logic};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Ek as f}from"#engine/shared/vocab.js";function A(t,n){const e=new Set;return{tree:t&&o(t,n,e),used:[...e]}}function o(t,n,e){const u=n[t.type];if(u){e.add(t.type);const c=s(u.tree,t.args||{});return o(c,n,e)}const i={type:t.type};return t.loc&&(i.loc=t.loc),t.args&&(i.args=t.args),t.props&&(i.props=t.props),t.children&&(i.children=t.children.map(c=>o(c,n,e))),i}function s(t,n){const e={type:t.type};return t.props&&(e.props=b(t.props,n)),t.args&&(e.args=a(t.args,n)),t.children&&(e.children=t.children.map(u=>s(u,n))),e}function b(t,n){const e={...t};return t.value!==void 0&&(e.value=d(t.value,n)),t.label!==void 0&&(e.label=d(t.label,n)),t.src!==void 0&&(e.src=d(t.src,n)),t.alt!==void 0&&(e.alt=d(t.alt,n)),t.placeholder!==void 0&&(e.placeholder=d(t.placeholder,n)),t.submitLabel!==void 0&&(e.submitLabel=d(t.submitLabel,n)),t.cond!==void 0&&(e.cond=r(t.cond,n)),t.list!==void 0&&(e.list=r(t.list,n)),t.arg!==void 0&&(e.arg=r(t.arg,n)),t.action!==void 0&&(e.action=l(t.action,n)),t.submit!==void 0&&(e.submit=l(t.submit,n)),t.bind!==void 0&&(e.bind=l(t.bind,n)),t.to!==void 0&&(e.to=l(t.to,n)),t.inputs!==void 0&&(e.inputs=a(t.inputs,n)),t.on!==void 0&&(e.on=a(t.on,n)),e}function d(t,n){if(typeof t=="string")return t;if("$param"in t){const e=n[t.$param];return typeof e=="number"?String(e):e}return g(t,n)}function r(t,n){return t.kind===f.Ref?{kind:f.Ref,name:l(t.name,n)}:t.kind===f.Bin?{...t,left:r(t.left,n),right:r(t.right,n)}:t.kind===f.Un?{...t,operand:r(t.operand,n)}:t.kind===f.Tern?{...t,cond:r(t.cond,n),then:r(t.then,n),else:r(t.else,n)}:t}function g(t,n){return{kind:f.Interp,parts:t.parts.map(e=>typeof e=="string"?e:r(e,n))}}function a(t,n){const e={};for(const[u,i]of Object.entries(t))e[u]=m(i,n);return e}function m(t,n){return typeof t=="string"?t.startsWith("$")?l(t,n):t:typeof t=="number"?t:n[t.$param]}function l(t,n){if(!t.startsWith("$"))return t;const e=t.slice(1).split(".")[0],u=t.slice(1+e.length),i=n[e];return(typeof i=="string"?i:e)+u}export{A as compose};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function i(t){const e={};let n=0;const c=o=>{const r="n"+ ++n,s={id:r,type:o.type,props:o.props||{},children:[]};return o.loc&&(s.loc=o.loc),o.args&&(s.args=o.args),e[r]=s,s.children=(o.children||[]).map(c),r};return{rootId:c(t),nodes:e}}function a(t){const{rootId:e,nodes:n}=t.tree?i(t.tree):{rootId:void 0,nodes:{}};return{screen:t.screen,entities:t.entities,state:t.state,actions:t.actions,consts:t.consts||{},constraints:t.constraints||{},rootId:e,nodes:n}}export{a as toDoc};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolveToken as V,SUGGESTED as b,defaultTheme as v,isKnownTokenShape as x}from"#engine/style/tokens.js";import{diag as l,closest as p}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as A,ACTION_OPS as D,PRIMITIVES as W}from"#engine/lang/manifest.js";import{Nt as d,Ek as y,StOp as _}from"#engine/shared/vocab.js";const O=new Set(A),C=["bind","data"],E=new Set(D),I=["text","number","bool","uuid","email","string"];function u(s,c=[]){return s.kind===y.Ref?c.push(s.name):s.kind===y.Un?u(s.operand,c):s.kind===y.Bin?(u(s.left,c),u(s.right,c)):s.kind===y.Tern&&(u(s.cond,c),u(s.then,c),u(s.else,c)),c}function G(s,c={}){const r=[],g=new Set(Object.keys(s.state||{})),N=new Set(c.stores||[]),j=new Set(Object.keys(s.consts||{})),T=s.nodes||{},S=Object.keys(s.entities||{});for(const[o,i]of Object.entries(s.state||{})){const t=i.type;if(t==="list")r.push(l("untyped-list",`state "${o}" is an untyped "list" \u2014 declare the element type, e.g. list<uuid> or list<User>`,{loc:i.loc,suggestion:"list<uuid>"}));else if(t.startsWith("list<")){const e=t.slice(5,-1);!I.includes(e)&&!S.includes(e)&&r.push(l("unknown-type",`list element "${e}" is not a known entity or scalar type`,{loc:i.loc,suggestion:p(e,[...S,...I])}))}}const R=(o,i)=>{if(typeof o=="string"&&o.startsWith("@")){const t=o.slice(1).split(".")[0];if(!g.has(t)){const e=p(t,[...g]);r.push(l("unknown-ref",`"@${t}" is not a declared state`,{loc:i.loc,suggestion:e?"@"+e:null}))}}},k=(o,i,t)=>{for(const e of u(o)){const n=e.split(".")[0];if(t.has(n)||g.has(n)||N.has(n)||j.has(n))continue;const h=p(n,[...g,...t]);r.push(l("unknown-ref",`"${n}" is not a known state or item variable here`,{loc:i.loc,suggestion:h}))}},w=new Set,$=(o,i)=>{const t=T[o];if(!t){r.push(l("missing-node",`node ${o} does not exist`));return}if(w.has(o)){r.push(l("dup-node",`${o} is referenced twice`,{loc:t.loc}));return}if(w.add(o),!O.has(t.type))t.args?r.push(l("unknown-part",`"${t.type}" is not a known part`,{loc:t.loc,suggestion:p(t.type,c.parts||[])})):r.push(l("unknown-type",`"${t.type}" is not a known primitive`,{loc:t.loc,suggestion:p(t.type,[...O])}));else{const a=W[t.type],m=a?a.props:{};for(const[f,P]of Object.entries(m))!P.endsWith("?")&&!(f in(t.props||{}))&&r.push(l("missing-prop",`${t.type} is missing the required "${f}"`,{loc:t.loc}))}const e=t.props||{};for(const a of C)a in e&&R(e[a],t);if(Array.isArray(e.style)){const a=c.theme||v,m=Object.keys(a.space||{}).length>0;for(const f of e.style)x(f)?m&&V(f,a)===null&&r.push(l("unknown-token",`"${f}": that step isn't in your theme scale`,{loc:t.loc,suggestion:p(f,b)})):r.push(l("unknown-token",`"${f}" is not an accepted style token`,{loc:t.loc,suggestion:p(f,b)}))}t.type===d.When&&e.cond&&k(e.cond,t,i),t.type===d.Each&&e.list&&k(e.list,t,i);const n=[];(t.type===d.Text||t.type===d.Title||t.type===d.Span)&&e.value&&n.push(e.value),t.type===d.Image&&(e.src&&n.push(e.src),e.alt&&n.push(e.alt));for(const a of n)if(typeof a=="object"&&"kind"in a&&a.kind===y.Interp)for(const m of a.parts)typeof m!="string"&&k(m,t,i);const h=t.type===d.Each&&e.as?new Set([...i,e.as]):i;for(const a of t.children||[])$(a,h)};if(s.rootId?$(s.rootId,new Set):c.kind!=="store"&&r.push(l("no-root","the doc is missing a rootId")),c.kind==="store")for(const[o,i]of Object.entries(s.gets||{}))for(const t of u(i)){const e=t.split(".")[0];g.has(e)||r.push(l("unknown-ref",`get "${o}": "${e}" is not a state of this store`,{suggestion:p(e,[...g])}))}for(const[o,i]of Object.entries(s.actions||{})){const t=new Set(i.mutates||[]),e=n=>{if(n.op===_.If){for(const h of n.then||[])e(h);for(const h of n.else||[])e(h);return}E.has(n.op)||r.push(l("unknown-op",`action "${o}" uses unknown op "${n.op}"`,{suggestion:p(n.op,[...E])})),n.target&&!t.has(n.target)&&r.push(l("undeclared-mutation",`action "${o}" mutates "${n.target}" but only declares mutates(${[...t].join(", ")||"\u2205"})`,{suggestion:p(n.target,[...t])}))};for(const n of i.body||[])e(n)}return{ok:r.length===0,diagnostics:r}}export{G as validate};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{ParseError as l}from"#engine/shared/diagnostics.js";import{tokenize as d}from"#engine/lang/lexer.js";import{Tk as n,Pn as r,Kw as o,BOp as s,UOp as f,Ek as a}from"#engine/shared/vocab.js";const k=[[n.Eq,void 0,s.Eq],[n.Neq,void 0,s.Neq],[n.Lte,void 0,s.Lte],[n.Gte,void 0,s.Gte],[n.Punct,r.Lt,s.Lt],[n.Punct,r.Gt,s.Gt],[n.Ident,o.Contains,s.Contains]],p=new Map([[o.True,!0],[o.False,!1],[o.Null,null]]);class c{toks;pos=0;lineStarts=[0];constructor(t){this.toks=d(t);for(let e=0;e<t.length;e++)t[e]===`
|
|
2
|
+
`&&this.lineStarts.push(e+1)}peek(){return this.toks[this.pos]}at(t,e){const i=this.peek();return i.t===t&&(e===void 0||i.v===e)}next(){return this.toks[this.pos++]}eat(t,e){if(!this.at(t,e)){const i=this.peek();throw new l(`expected ${t}${e?' "'+e+'"':""}, got ${i.t} "${i.v}"`,this.locOf(i.pos))}return this.next()}locOf(t){let e=0,i=this.lineStarts.length-1;for(;e<i;){const h=e+i+1>>1;this.lineStarts[h]<=t?e=h:i=h-1}return{line:e+1,col:t-this.lineStarts[e]+1}}parseExpr(){return this.ternary()}ternary(){const t=this.or();if(!this.at(n.Punct,r.Question))return t;this.next();const e=this.ternary();return this.eat(n.Punct,r.Colon),{kind:a.Tern,cond:t,then:e,else:this.ternary()}}or(){let t=this.and();for(;this.at(n.Ident,o.Or);)this.next(),t={kind:a.Bin,op:s.Or,left:t,right:this.and()};return t}and(){let t=this.cmp();for(;this.at(n.Ident,o.And);)this.next(),t={kind:a.Bin,op:s.And,left:t,right:this.cmp()};return t}comparison(){for(const[t,e,i]of k)if(this.at(t,e))return i;return null}cmp(){let t=this.add();for(let e=this.comparison();e;e=this.comparison())this.next(),t={kind:a.Bin,op:e,left:t,right:this.add()};return t}add(){let t=this.mul();for(;this.at(n.Punct,r.Plus)||this.at(n.Punct,r.Dash);){const e=this.peek().v===r.Plus?s.Add:s.Sub;this.next(),t={kind:a.Bin,op:e,left:t,right:this.mul()}}return t}mul(){let t=this.unary();for(;this.at(n.Punct,r.Star)||this.at(n.Punct,r.Slash);){const e=this.peek().v===r.Star?s.Mul:s.Div;this.next(),t={kind:a.Bin,op:e,left:t,right:this.unary()}}return t}unary(){return this.at(n.Ident,o.Not)?(this.next(),{kind:a.Un,op:f.Not,operand:this.unary()}):this.primary()}primary(){if(this.at(n.Punct,r.ParenL)){this.next();const i=this.ternary();return this.eat(n.Punct,r.ParenR),i}if(this.at(n.String))return{kind:a.Lit,value:this.next().v};if(this.at(n.Number))return{kind:a.Lit,value:Number(this.next().v)};let t=this.at(n.Param)?"$"+this.next().v:this.eat(n.Ident).v;const e=p.get(t);if(e!==void 0)return{kind:a.Lit,value:e};for(;this.at(n.Punct,r.Dot);)this.next(),t+="."+this.eat(n.Ident).v;return{kind:a.Ref,name:t}}parseInterpolation(t){if(!t.includes("{"))return t;const e=[];let i=0;for(;i<t.length;){const h=t.indexOf("{",i);if(h<0){e.push(t.slice(i));break}h>i&&e.push(t.slice(i,h));const u=t.indexOf("}",h);if(u<0){e.push(t.slice(h));break}e.push(new c(t.slice(h+1,u)).parseExpr()),i=u+1}return{kind:a.Interp,parts:e}}parseScalar(){if(this.at(n.String))return this.next().v;if(this.at(n.Number))return Number(this.next().v);const t=this.eat(n.Ident).v,e=p.get(t);return e!==void 0?e:t}parseValue(){return this.at(n.Punct,r.BrackL)?this.parseArray():this.at(n.Punct,r.BraceL)?this.parseObject():this.parseScalar()}parseArray(){this.eat(n.Punct,r.BrackL);const t=[];for(;!this.at(n.Punct,r.BrackR);)t.push(this.parseValue()),this.at(n.Punct,r.Comma)&&this.next();return this.eat(n.Punct,r.BrackR),t}parseObject(){this.eat(n.Punct,r.BraceL);const t={};for(;!this.at(n.Punct,r.BraceR);){const e=this.at(n.String)?this.next().v:this.eat(n.Ident).v;this.eat(n.Punct,r.Colon),t[e]=this.parseValue(),this.at(n.Punct,r.Comma)&&this.next()}return this.eat(n.Punct,r.BraceR),t}}export{c as Grammar};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{Tk as s,Pn as c}from"#engine/shared/vocab.js";import{ParseError as a}from"#engine/shared/diagnostics.js";const p=Object.values(c).join(""),x=[["->",s.Arrow],["<-",s.LArrow],["==",s.Eq],["=>",s.FatArrow],["!=",s.Neq],["<=",s.Lte],[">=",s.Gte]],g=e=>e===" "||e===" "||e==="\r"||e===`
|
|
2
|
+
`,h=e=>e>="0"&&e<="9",d=e=>e>="a"&&e<="z"||e>="A"&&e<="Z"||e==="_",l=e=>d(e)||h(e);function f(e,n){let t=1,i=1;for(let o=0;o<n&&o<e.length;o++)e[o]===`
|
|
3
|
+
`?(t++,i=1):i++;return{line:t,col:i}}class m{constructor(n){this.source=n}source;index=0;tokens=[];tokenize(){const{source:n}=this;for(;this.index<n.length;){const t=this.index,i=n[this.index];if(g(i)){this.index++;continue}if(i==="#"){this.skipComment();continue}if(i==='"'){this.scanString(t);continue}if(i==="@"){this.scanSigil(t,s.Ref,"@");continue}if(i==="$"){this.scanSigil(t,s.Param,"");continue}if(!this.scanOperator(t)){if(h(i)||i===c.Dash&&h(n[this.index+1])){this.scanNumber(t);continue}if(p.includes(i)){this.push(s.Punct,i,t),this.index++;continue}if(d(i)){this.scanWord(t);continue}throw new a(`unexpected character ${JSON.stringify(i)}`,f(n,this.index))}}return this.push(s.Eof,"",this.index),this.tokens}push(n,t,i){this.tokens.push({t:n,v:t,pos:i})}skipComment(){for(;this.index<this.source.length&&this.source[this.index]!==`
|
|
4
|
+
`;)this.index++}scanString(n){const{source:t}=this;let i=this.index+1,o="";for(;i<t.length&&t[i]!=='"';)o+=t[i],i++;this.push(s.String,o,n),this.index=i+1}scanSigil(n,t,i){const{source:o}=this;let r=this.index+1,u="";for(;r<o.length&&l(o[r]);)u+=o[r],r++;this.push(t,i+u,n),this.index=r}scanOperator(n){const t=this.source.slice(this.index,this.index+2),i=x.find(([o])=>o===t);return i?(this.push(i[1],t,n),this.index+=2,!0):!1}scanNumber(n){const{source:t}=this;let i=this.index+(t[this.index]===c.Dash?1:0);for(;i<t.length&&h(t[i]);)i++;if(t[i]===c.Dot)for(i++;i<t.length&&h(t[i]);)i++;this.push(s.Number,t.slice(this.index,i),n),this.index=i}scanWord(n){const{source:t}=this;let i=this.index;for(;i<t.length&&l(t[i]);)i++;this.push(s.Ident,t.slice(this.index,i),n),this.index=i}}function k(e){return new m(e).tokenize()}export{k as tokenize};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import{SUGGESTED as e,resolveToken as t}from"#engine/style/tokens.js";const a={Stack:{props:{style:"tokens?"},children:!0,doc:"Vertical stack (flex column) \u2014 its identity. For a horizontal layout, use a region with style(row). Card look: class(card).",snippet:`Stack {
|
|
2
|
+
$0
|
|
3
|
+
}`},Header:{props:{style:"tokens?"},children:!0,doc:"Page header landmark (<header>). Lay it out with style(row, between, center, \u2026).",snippet:`Header style(row, between, center) {
|
|
4
|
+
$0
|
|
5
|
+
}`},Nav:{string:"label",props:{label:"text?",style:"tokens?"},children:!0,doc:'Navigation landmark (<nav>). Optional label \u2192 aria-label (disambiguates multiple navs). `Nav "Primary" style(row, gap.md) { \u2026 }`.',snippet:`Nav style(row, gap.md) {
|
|
6
|
+
$0
|
|
7
|
+
}`},Sidebar:{props:{style:"tokens?"},children:!0,doc:"Complementary landmark (<aside>). Position with style(left) or style(right).",snippet:`Sidebar style(left) {
|
|
8
|
+
$0
|
|
9
|
+
}`},Footer:{props:{style:"tokens?"},children:!0,doc:"Footer landmark (<footer>).",snippet:`Footer style(padding.md) {
|
|
10
|
+
$0
|
|
11
|
+
}`},Page:{props:{style:"tokens?"},children:!0,doc:"The page content root (<main>) \u2014 one per route, mounts into the shell\u2019s `slot`. No imposed look; lay it out with style().",snippet:`Page {
|
|
12
|
+
$0
|
|
13
|
+
}`},Text:{string:"value",props:{value:"text",style:"tokens?"},children:!1,interp:!0,doc:'Paragraph text (<p>). Interpolates state reactively: `Text "Hi, {user.name}"`.',snippet:'Text "$1"'},Title:{string:"value",props:{value:"text",style:"tokens?"},children:!1,interp:!0,doc:'Heading. Level via keyword: `Title "Hi" h2` \u2192 <h2> (h1\u2026h6; default h1). Prefer one h1 per page. Interpolates state.',snippet:'Title "$1"'},Span:{string:"value",props:{value:"text",style:"tokens?"},children:!1,interp:!0,doc:'Inline text (<span>). Interpolates state: `Span "{cart.total}"`.',snippet:'Span "$1"'},Image:{string:"src",props:{src:"text",alt:"text",style:"tokens?"},children:!1,interp:!0,doc:'Image (<img>). `alt` is required (a11y/SEO): `Image "{p.image}" alt "{p.title}"`. Use alt "" for decorative images.',snippet:'Image "{${1:item.image}}" alt "${2:description}"'},SearchField:{string:"placeholder",props:{bind:"state",placeholder:"text?"},children:!1,doc:"Search input two-way bound to a text state.",snippet:'SearchField bind @${1:search} "${2:Search by name}"'},DataTable:{props:{data:"state",where:"clauses?",columns:"fields",style:"tokens?"},children:!0,doc:"Reactive table over a list/query. Static `where` filters are pushed to the query; dynamic ones stay reactive.",snippet:"DataTable @${1:items}\n columns(${2:name})"},RowAction:{string:"label",props:{label:"text",action:"action",arg:"expr?"},children:!1,doc:'A button rendered in each DataTable row: `RowAction "Delete" -> deleteItem(row.id)`.',snippet:'RowAction "${1:Delete}" -> ${2:action}(row.id)'},Button:{string:"label",props:{label:"text?",action:"action?",arg:"expr?",style:"tokens?"},children:!0,interp:!0,doc:'Clickable button \u2192 runs an action. Label interpolates (`Button "{x}"`) OR use `{ }` children for a clickable card. `-> action(arg)`; arg may be a ref or a literal.',snippet:'Button "${1:label}" -> ${2:action}($3)'},Form:{string:"submitLabel",props:{bind:"state",submit:"action",submitLabel:"text?"},children:!1,doc:"Auto-form: one field per entity field, two-way bound to a draft state.",snippet:'Form bind @${1:draft} submit ${2:createItem} "${3:Save}"'},Link:{string:"label",props:{label:"text?",to:"route",style:"tokens?"},children:!0,interp:!0,doc:'Navigation link: `Link "Catalog" -> /catalog`. Label interpolates, OR use `{ }` children for a clickable card that navigates. Client-side (no full reload).',snippet:'Link "${1:label}" -> /${2:route}'},slot:{props:{},children:!1,doc:"The outlet in a `shell { }` where the active route\u2019s page mounts.",snippet:"slot"},When:{props:{cond:"expr"},children:!0,control:!0,doc:"Conditional render: `when <expr> { ... }`. Mounts/unmounts reactively.",snippet:`when \${1:cond} {
|
|
14
|
+
$0
|
|
15
|
+
}`},Each:{props:{list:"expr",as:"ident"},children:!0,control:!0,doc:"List render: `each <list> as <item> { ... }`. The item is a scope variable in the template.",snippet:"each ${1:items} as ${2:item} {\n $0\n}"},Custom:{props:{component:"name",inputs:"map?",on:"map?"},children:!1,doc:"Escape hatch (\xA77): mount a host component from `src/components/<Name>.js`. Opaque to the IR; connected via inputs/on.",snippet:"Custom ${1:Name} inputs(${2:prop}: ${3}) on(${4:event}: ${5:action})"}},n=["bind","submit","where","columns","style","class","alt","inputs","on"],r={bind:"Two-way bind to a @state, e.g. `bind @search`.",submit:"Action to run on form submit, e.g. `submit createUser`.",where:"Filter clauses: `where(role == admin, name contains @q)`.",columns:"Columns to show: `columns(name, email, role)`.",style:"Layout & typography tokens (Muten builds, doesn\u2019t skin): `style(row, gap.md, text.lg)`.",class:'Raw CSS class(es) for LOOK \u2014 your CSS or a third-party like Tailwind: `class(card)` or `class("flex gap-4")`. Muten stays agnostic about appearance.',alt:'Required accessible/SEO text for an Image: `alt "{p.title}"`. Use "" for decorative images.',inputs:"Custom component inputs: `inputs(data: @sales)`.",on:"Custom component events wired to actions: `on(select: pick)`."},o=["screen","entity","state","store","const","theme","get","effect","action","mutates","mock","sources","routes","shell","guard","else","part","query","if","when","each","as","and","or","not","contains"],i={screen:"Declares the screen name: `screen users_dashboard`.",entity:"Declares a data shape + validation: `entity User { name text required email email required password text min:8 }` (implicit uuid id). Constraints: `required`, `min:N`, `max:N`.",state:'Declares reactive state: `state { search = "" : text users = query listUsers : list<User> }`.',store:"App-GLOBAL reactive state (shared across pages, no prop drilling): `store { cart = [] : list<number> }`. Referenced by name like local state.",const:"A compile-time IMMUTABLE scalar, inlined (never reactive): `const TAX = 0.21`. Scalars only \u2014 structured config uses a block (e.g. theme).",theme:'The project theme block (theme.muten): `theme { space { md "16px" } breakpoints { md "768px" } }`. Supplies the token SCALE; the engine owns only the vocabulary. The reset/base CSS lives in your stylesheet.',get:"A `.store` derived/memoized value (getter): `get total = items.length`. Read as `domain.total`, recomputes when deps change.",effect:"A `.store` reactive side-effect (Angular-style): `effect { ... }`. Re-runs automatically when the store state it reads changes.",action:"Declares a mutation: `action delete mutates users <- id { users.remove(u => u.id == id) }`.",mutates:"Lists the state an action may mutate \u2014 the linter enforces it.",mock:'Inline mock data for queries: `mock { listUsers: [ { name: "Ana", role: admin } ] }`.',sources:'Real data sources for queries: `sources { listChars: { url: "https://api...", at: "results" } }`.',routes:"App root (app.muten): maps URLs to pages, `routes { /url -> page }`. The single source of truth the AI reads.",shell:"Persistent app chrome in app.muten: `shell { Header { \u2026 } slot Footer { \u2026 } }`. Wraps every route; `slot` is where the active Page (<main>) mounts.",guard:"Route guard in app.muten: `routes { /cart -> cart guard auth.loggedIn else /login }`. If the store boolean is false on navigation, redirect. Guest-only page: `guard not auth.loggedIn else /catalog`.",else:"The redirect target of a route `guard`: `guard auth.loggedIn else /login`.",part:"Reusable composition: `part Card(item: Item, onPick: action) { ... }`. Pass OBJECTS (`$item.field`) and ACTION callbacks (`-> $onPick(...)`). Inlined at build time.",query:"An async data source. The state exposes `.loading`, `.error` and `.data`.",if:"Conditional INSIDE an action body: `if <expr> { \u2026 } else { \u2026 }` \u2014 the only branching in actions (toggles, validation, add-or-remove).",when:"Conditional render: `when <expr> { ... }`.",each:"List render: `each <list> as <item> { ... }`.",as:"Names the item variable in an `each`.",and:"Logical AND.",or:"Logical OR.",not:"Logical NOT, e.g. `when not (cart.isEmpty)`.",contains:"Case-insensitive substring match: `name contains @q`."},l=["push","remove","reset","set"],c={push:"Append to a list state: `users.push(draft)` (auto-fills uuid fields).",remove:"Remove matching items: `users.remove(u => u.id == id)`.",reset:"Reset a state to its declared initial: `draft.reset()`.",set:"Set a state value: `rating.set(v)`."},p=Object.keys(a),d=e;export{l as ACTION_OPS,c as ACTION_OP_DOCS,o as KEYWORDS,i as KEYWORD_DOCS,n as MODIFIERS,r as MODIFIER_DOCS,a as PRIMITIVES,p as PRIMITIVE_NAMES,d as TOKEN_NAMES,t as resolveToken};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ParseError as f}from"#engine/shared/diagnostics.js";import{PRIMITIVES as g}from"#engine/lang/manifest.js";import{Grammar as R}from"#engine/lang/grammar.js";import{Tk as t,Pn as i,Kw as n,Nt as P,Mod as p,StOp as u}from"#engine/shared/vocab.js";const m={};for(const[c,e]of Object.entries(g))e.string&&(m[c]=e.string);const S=new Set(Object.entries(g).filter(([,c])=>c.interp).map(([c])=>c)),x=c=>c==="text"?"string":c,y=c=>/^h[1-6]$/.test(c);class w extends R{modifiers;statements;constructor(e){super(e),this.modifiers=new Map([[p.Bind,s=>{s.bind=this.at(t.Ref)?this.eat(t.Ref).v:this.parseDotted()}],[p.Submit,s=>{s.submit=this.parseDotted()}],[p.Where,s=>{s.where=this.parseParenList(()=>this.rebuildClause())}],[p.Columns,s=>{s.columns=this.parseParenList(()=>this.eat(t.Ident).v)}],[p.Style,s=>{s.style=this.parseParenList(()=>this.parseStyleToken())}],[p.Class,s=>{s.class=this.parseParenList(()=>this.at(t.String)?this.next().v:this.eat(t.Ident).v)}],[p.Alt,s=>{s.alt=this.parseInterpolation(this.eat(t.String).v)}],[p.Inputs,s=>{s.inputs=this.parseArgs()}],[p.On,s=>{s.on=this.parseArgs()}]]),this.statements=new Map([[u.Push,s=>({op:u.Push,target:s,arg:this.parseExpr()})],[u.Set,s=>({op:u.Set,target:s,arg:this.parseExpr()})],[u.Reset,s=>({op:u.Reset,target:s})],[u.Remove,s=>{const a=this.eat(t.Ident).v;return this.eat(t.FatArrow),{op:u.Remove,target:s,param:a,pred:this.parseExpr()}}]])}parse(){const e={screen:"",entities:{},state:{},actions:{},tree:null},s=new Map([[n.Screen,()=>{this.next(),e.screen=this.eat(t.Ident).v}],[n.Entity,()=>this.parseEntity(e)],[n.State,()=>this.parseState(n.State,e.state)],[n.Store,()=>{e.store=e.store||{},this.parseState(n.Store,e.store)}],[n.Get,()=>this.parseGet(e)],[n.Effect,()=>{this.next(),(e.effects=e.effects||[]).push(this.parseActionBody())}],[n.Action,()=>this.parseAction(e)],[n.Mock,()=>this.parseMock(e)],[n.Sources,()=>this.parseSources(e)],[n.Routes,()=>this.parseRoutes(e)],[n.Shell,()=>this.parseShell(e)],[n.Part,()=>this.parsePart(e)],[n.Const,()=>this.parseConst(e)],[n.Theme,()=>this.parseTheme(e)]]);for(;!this.at(t.Eof);){const a=this.peek(),r=a.t===t.Ident?s.get(a.v):void 0;r?r():e.tree=this.parseNode()}return e}parseEntity(e){this.eat(t.Ident,n.Entity);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.BraceL);const a={id:"uuid"},r={};for(;!this.at(t.Punct,i.BraceR);){const o=this.eat(t.Ident).v,h=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Pipe);)this.next(),h.push(this.eat(t.Ident).v);a[o]=h.length>1?"enum:"+h.join("|"):x(h[0]);const d=this.parseConstraints();Object.keys(d).length&&(r[o]=d)}this.eat(t.Punct,i.BraceR),e.entities[s]=a,Object.keys(r).length&&((e.constraints=e.constraints||{})[s]=r)}parseConstraints(){const e={};for(;this.at(t.Ident,n.Required)||this.at(t.Ident,n.Min)||this.at(t.Ident,n.Max);){const s=this.next().v;if(s===n.Required){e.required=!0;continue}this.eat(t.Punct,i.Colon);const a=Number(this.eat(t.Number).v);s===n.Min?e.min=a:e.max=a}return e}parseState(e,s){for(this.eat(t.Ident,e),this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const a=this.eat(t.Ident);this.eat(t.Punct,i.Assign);let r,o,h=!1;this.at(t.Ident,n.Query)?(this.next(),r="query:"+this.eat(t.Ident).v):this.at(t.Punct,i.BraceL)||this.at(t.Punct,i.BrackL)?(o=this.parseValue(),h=!0):this.at(t.String)?(o=this.next().v,h=!0):this.at(t.Number)?(o=Number(this.next().v),h=!0):this.at(t.Ident,n.True)||this.at(t.Ident,n.False)?(o=this.next().v===n.True,h=!0):(o=this.next().v,h=!0),this.eat(t.Punct,i.Colon);const d=this.parseType(),v=this.locOf(a.pos);s[a.v]=r?{type:d,source:r,loc:v}:{type:d,initial:h?o:null,loc:v}}this.eat(t.Punct,i.BraceR)}parseGet(e){this.eat(t.Ident,n.Get);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Assign),(e.gets=e.gets||{})[s]=this.parseExpr()}parseAction(e){this.eat(t.Ident,n.Action);const s=this.eat(t.Ident).v;this.eat(t.Ident,n.Mutates);const a=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Comma);)this.next(),a.push(this.eat(t.Ident).v);this.eat(t.LArrow);const r=this.eat(t.Ident).v;e.actions[s]={mutates:a,input:r,body:this.parseActionBody()}}parseActionBody(){this.eat(t.Punct,i.BraceL);const e=[];for(;!this.at(t.Punct,i.BraceR);)e.push(this.parseStatement());return this.eat(t.Punct,i.BraceR),e}parseIf(){this.eat(t.Ident,n.If);const e=this.parseExpr(),s=this.parseActionBody(),a=this.at(t.Ident,n.Else)?(this.next(),this.parseActionBody()):null;return{op:u.If,cond:e,then:s,else:a}}parseStatement(){if(this.at(t.Ident,n.If))return this.parseIf();const e=this.eat(t.Ident).v;this.eat(t.Punct,i.Dot);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.ParenL);const a=this.statements.get(s);if(!a)throw new f(`unknown action method "${s}" on "${e}"`,this.locOf(this.peek().pos));const r=a(e);return this.eat(t.Punct,i.ParenR),r}parseMock(e){this.eat(t.Ident,n.Mock);const s=e.mock||{};this.parseEntries(a=>{s[a]=this.parseValue()}),e.mock=s}parseSources(e){this.eat(t.Ident,n.Sources);const s=e.sources||{};this.parseEntries(a=>{s[a]=this.parseValue()}),e.sources=s}parseRoutes(e){this.eat(t.Ident,n.Routes),this.eat(t.Punct,i.BraceL);const s=e.routes||[];for(;!this.at(t.Punct,i.BraceR);){const a=this.peek(),r=this.locOf(a.pos).line,o=this.pathOnLine(r);this.eat(t.Arrow);const h={url:o,page:this.eat(t.Ident).v,loc:this.locOf(a.pos)};this.at(t.Ident,n.Guard)&&(this.next(),h.guardNeg=this.at(t.Ident,n.Not)?(this.next(),!0):!1,h.guard=this.parseDotted(),this.eat(t.Ident,n.Else),h.redirect=this.pathOnLine(r)),s.push(h)}this.eat(t.Punct,i.BraceR),e.routes=s}parseShell(e){this.eat(t.Ident,n.Shell),e.shell={type:P.Shell,props:{},children:this.parseChildren()}}parseConst(e){this.eat(t.Ident,n.Const);const s=this.eat(t.Ident).v;if(this.eat(t.Punct,i.Assign),this.at(t.Punct,i.BraceL)||this.at(t.Punct,i.BrackL))throw new f("const holds a single value (string/number/bool) \u2014 use a block like `theme { \u2026 }` for structured data",this.locOf(this.peek().pos));(e.consts=e.consts||{})[s]=this.parseScalar()}parseTheme(e){this.eat(t.Ident,n.Theme),this.eat(t.Punct,i.BraceL);const s={};for(;!this.at(t.Punct,i.BraceR);){const a=this.eat(t.Ident).v;this.eat(t.Punct,i.BraceL);const r={};for(;!this.at(t.Punct,i.BraceR);)r[this.eat(t.Ident).v]=this.eat(t.String).v;this.eat(t.Punct,i.BraceR),s[a]=r}this.eat(t.Punct,i.BraceR),e.theme=s}parsePart(e){this.eat(t.Ident,n.Part);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.ParenL);const a=[];for(;!this.at(t.Punct,i.ParenR);){const o=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),a.push({name:o,type:this.parseType()}),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.ParenR);const r=this.parseChildren();e.parts=e.parts||{},e.parts[s]={params:a,tree:r.length===1?r[0]:{type:P.Stack,props:{},children:r}}}parseWhen(){const e=this.eat(t.Ident,n.When),s=this.parseExpr();return{type:P.When,props:{cond:s},children:this.parseChildren(),loc:this.locOf(e.pos)}}parseEach(){const e=this.eat(t.Ident,n.Each),s=this.parseExpr();this.eat(t.Ident,n.As);const a=this.eat(t.Ident).v;return{type:P.Each,props:{list:s,as:a},children:this.parseChildren(),loc:this.locOf(e.pos)}}parseNode(){if(this.at(t.Ident,n.When))return this.parseWhen();if(this.at(t.Ident,n.Each))return this.parseEach();const e=this.eat(t.Ident),s=e.v,a=this.locOf(e.pos);if(this.at(t.Punct,i.ParenL))return{type:s,args:this.parseArgs(),loc:a};const r={},o=[];s===P.Custom&&(r.component=this.eat(t.Ident).v);let h=!0;for(;h;){const v=this.peek();switch(v.t){case t.String:{const l=m[s]||"label";r[l]=S.has(s)?this.parseInterpolation(this.next().v):this.next().v;break}case t.Param:{const l=m[s]||"label";r[l]={$param:this.next().v};break}case t.Ref:r.data=this.next().v;break;case t.Arrow:this.parseArrow(s,r);break;case t.Ident:{const l=v.v;if(s===P.Title&&y(l)){this.next(),r.level=l;break}const I=this.modifiers.get(l);if(!I){h=!1;break}this.next(),I(r);break}case t.Punct:if(v.v===i.BraceL){for(this.next();!this.at(t.Punct,i.BraceR);)o.push(this.parseNode());this.eat(t.Punct,i.BraceR)}h=!1;break;default:h=!1}}const d={type:s,props:r,loc:a};return o.length&&(d.children=o),d}parseArrow(e,s){if(this.next(),e===P.Link){s.to=this.parsePath();return}s.action=this.parseDotted(),this.at(t.Punct,i.ParenL)&&(this.next(),this.at(t.Punct,i.ParenR)||(s.arg=this.parseExpr()),this.eat(t.Punct,i.ParenR))}parseChildren(){this.eat(t.Punct,i.BraceL);const e=[];for(;!this.at(t.Punct,i.BraceR);)e.push(this.parseNode());return this.eat(t.Punct,i.BraceR),e}parseType(){let e=this.eat(t.Ident).v;return this.at(t.Punct,i.Lt)&&(this.next(),e+="<"+this.eat(t.Ident).v+">",this.eat(t.Punct,i.Gt)),e}parseStyleToken(){const e=()=>this.at(t.Number)?this.next().v:this.eat(t.Ident).v;let s=e();for(this.at(t.Punct,i.Colon)&&(this.next(),s+=":"+e());this.at(t.Punct,i.Dot);)this.next(),s+="."+e();return s}parseDotted(){let e=this.at(t.Param)?"$"+this.next().v:this.eat(t.Ident).v;for(;this.at(t.Punct,i.Dot);)this.next(),e+="."+this.eat(t.Ident).v;return e}parseParenList(e){this.eat(t.Punct,i.ParenL);const s=[];for(;!this.at(t.Punct,i.ParenR);)s.push(e()),this.at(t.Punct,i.Comma)&&this.next();return this.eat(t.Punct,i.ParenR),s}rebuildClause(){const e=[];for(;!this.at(t.Punct,i.Comma)&&!this.at(t.Punct,i.ParenR);)e.push(this.next().v);return e.join(" ")}parseEntries(e){for(this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),e(s),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.BraceR)}parsePath(){let e="";for(;this.at(t.Punct,i.Slash);){const s=this.next();e+="/",this.at(t.Ident)&&this.peek().pos===s.pos+1&&(e+=this.eat(t.Ident).v)}return e}pathOnLine(e){let s="";for(;this.at(t.Punct,i.Slash)&&this.locOf(this.peek().pos).line===e;)this.next(),s+="/",this.at(t.Ident)&&(s+=this.eat(t.Ident).v);return s}parseArgs(){this.eat(t.Punct,i.ParenL);const e={};for(;!this.at(t.Punct,i.ParenR);){const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),e[s]=this.parseArgValue(),this.at(t.Punct,i.Comma)&&this.next()}return this.eat(t.Punct,i.ParenR),e}parseArgValue(){return this.at(t.String)?this.next().v:this.at(t.Number)?Number(this.next().v):this.at(t.Ref)?this.next().v:this.at(t.Param)?{$param:this.next().v}:this.parseDotted()}}function b(c){return new w(c).parse()}export{w as Parser,b as parse};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import f from"node:fs";import{dirname as d,join as u}from"node:path";import{parse as l}from"#engine/lang/parse.js";import{toDoc as b}from"#engine/ir/flatten.js";import{compose as k}from"#engine/ir/compose.js";import{validate as j}from"#engine/ir/validate.js";import{closest as O,diag as y,ParseError as T}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as w}from"#engine/lang/manifest.js";import{mergeTheme as h}from"#engine/style/tokens.js";function S(r){const e=m(r);if(!e)return h({});try{return h(l(f.readFileSync(u(e,"theme.muten"),"utf8")).theme||{})}catch{return h({})}}function R(r){const e={};let t;try{t=f.readdirSync(r)}catch{return e}for(const n of t){if(!n.endsWith(".muten"))continue;let o;try{o=l(f.readFileSync(u(r,n),"utf8"))}catch{continue}for(const[i,c]of Object.entries(o.parts||{}))e[i]={...c,state:o.state||{},entities:o.entities||{}}}return e}function m(r){let e=d(r);for(let t=0;t<30;t++){if(f.existsSync(u(e,"src","pages")))return e;const n=d(e);if(n===e)break;e=n}return null}function x(r){const e=[],t=n=>{let o;try{o=f.readdirSync(n,{withFileTypes:!0})}catch{return}for(const i of o){if(!i.isDirectory())continue;const c=u(n,i.name);i.name==="parts"?e.push(c):t(c)}};return t(u(r,"src")),e}function v(r){const e=m(r);if(!e)return[];const t=[],n=o=>{let i;try{i=f.readdirSync(o,{withFileTypes:!0})}catch{return}for(const c of i){const s=u(o,c.name);c.isDirectory()?n(s):c.name.endsWith(".store")&&t.push(c.name.slice(0,-6))}};return n(u(e,"src")),t}function D(r){const e=m(r);if(!e)return R(u(d(r),"parts"));const t={};for(const n of x(e))Object.assign(t,R(n));return t}function P(r,e){const t=m(r),n=t?u(t,"src","pages"):null;let o=[];if(n)try{o=f.readdirSync(n,{withFileTypes:!0}).filter(s=>s.isDirectory()).map(s=>s.name)}catch{}const i=[],c=new Set;for(const s of e)c.has(s.url)&&i.push(y("dup-route",`duplicate route "${s.url}"`,{loc:s.loc})),c.add(s.url),n&&!o.includes(s.page)&&i.push(y("unknown-page",`route "${s.url}" \u2192 page "${s.page}" not found in src/pages/`,{loc:s.loc,suggestion:O(s.page,o)}));return{ok:i.length===0,diagnostics:i}}function z(r,e){let t;try{t=l(e)}catch(p){return p instanceof T&&p.loc?{ok:!1,diagnostics:[y("syntax",p.message,{loc:p.loc})]}:{ok:!0,diagnostics:[]}}if(t.routes)return P(r,t.routes);if(t.theme)return{ok:!0,diagnostics:[]};if(r.endsWith(".store"))return j({screen:"store",state:t.state||{},actions:t.actions||{},entities:t.entities||{},gets:t.gets||{},consts:{},constraints:{},rootId:void 0,nodes:{}},{kind:"store"});const n=D(r),{tree:o,used:i}=k(t.tree,n),c={...t.entities},s={...t.state};for(const p of i){const g=n[p];g&&(Object.assign(c,g.entities),Object.assign(s,g.state))}const a=b({screen:t.screen,entities:c,state:s,actions:t.actions,consts:t.consts,constraints:t.constraints,tree:o});return j(a,{parts:Object.keys(n),stores:v(r),theme:S(r)})}function A(r,e){let t=null;try{t=l(e)}catch{}const n=D(r),o=Object.entries(n).map(([s,a])=>({name:s,params:a.params||[]})),i=[],c=(s,a)=>{i.push({name:s,type:a.type||"",query:typeof a.source=="string"&&a.source.startsWith("query:")})};for(const[s,a]of Object.entries(t?.state||{}))c(s,a);for(const s of Object.values(n))for(const[a,p]of Object.entries(s.state||{}))c(a,p);return{parts:o,state:i,actions:Object.keys(t?.actions||{}),primitives:w,theme:S(r)}}export{z as analyze,A as completion,D as projectParts,v as projectStores,S as projectTheme};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{readFileSync as p,existsSync as g,readdirSync as j}from"node:fs";import{join as m,dirname as x}from"node:path";import{parse as k}from"#engine/lang/parse.js";import{toDoc as V}from"#engine/ir/flatten.js";import{resolveStyles as O}from"#engine/project/styles.js";import{compose as v}from"#engine/ir/compose.js";async function b(e){const o={};if(!g(e))return o;for(const t of j(e)){if(!t.endsWith(".muten"))continue;const r=m(e,t),s=k(p(r,"utf8")),{css:n}=await O(r);for(const[a,i]of Object.entries(s.parts||{}))o[a]={...i,state:s.state||{},entities:s.entities||{},mock:s.mock||{},css:n}}return o}async function J(e){const o={},t=[],r=s=>{let n;try{n=j(s,{withFileTypes:!0})}catch{return}for(const a of n){if(!a.isDirectory())continue;const i=m(s,a.name);a.name==="parts"?t.push(i):r(i)}};r(m(e,"src"));for(const s of t)Object.assign(o,await b(s));return o}async function T(e,o={}){const t=k(p(e,"utf8")),r=await b(m(x(e),"parts")),s={};for(const[f,c]of Object.entries(t.parts||{}))s[f]={...c,state:{},entities:{},mock:{},css:""};const n={...o,...r,...s},{tree:a,used:i}=v(t.tree,n),u={...t.entities},d={...t.state};let l={...t.mock||{}};for(const f of i){const c=n[f];c&&(Object.assign(u,c.entities),Object.assign(d,c.state),l={...l,...c.mock})}const w=V({screen:t.screen,entities:u,state:d,actions:t.actions,consts:t.consts,constraints:t.constraints,tree:a}),y=e.replace(/\.muten$/,".data.json"),D={...g(y)?JSON.parse(p(y,"utf8")):{},...l},P=await O(e),S=i.map(f=>n[f]?.css).filter(Boolean).join(`
|
|
2
|
+
|
|
3
|
+
`),h=[P.css,S].filter(Boolean).join(`
|
|
4
|
+
|
|
5
|
+
`);return{ir:t,doc:w,data:D,sources:t.sources||{},styles:{css:h,from:P.from},partNames:Object.keys(n)}}export{T as load,J as loadAllParts,b as loadParts};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{readFileSync as i,existsSync as p}from"node:fs";import{join as s,relative as c}from"node:path";import{parse as g}from"#engine/lang/parse.js";function w(o){const t=r=>c(o,r),e=s(o,"src","app.muten");if(!p(e))throw new Error(`No app.muten at ${t(e)}
|
|
2
|
+
Every app needs a root. Create src/app.muten with:
|
|
3
|
+
routes {
|
|
4
|
+
/ -> home
|
|
5
|
+
}`);let a;try{a=g(i(e,"utf8"))}catch(r){throw new Error(`${t(e)}: ${r instanceof Error?r.message:String(r)}`)}const u=s(o,"src","pages"),n=(a.routes||[]).map(r=>({route:r.url.replace(/^\//,""),page:r.page,screenPath:s(u,r.page,r.page+".muten")}));if(!n.length)throw new Error(`${t(e)} has no routes. Add: routes { /url -> page }`);for(const r of n)if(!p(r.screenPath))throw new Error(`route /${r.route} -> ${r.page}: page not found at ${t(r.screenPath)}`);return n}export{w as readRoutes};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as c,existsSync as o}from"node:fs";import{basename as r}from"node:path";async function l(t){const s=t.replace(/\.muten$/,".scss"),e=t.replace(/\.muten$/,".css");return o(s)?{css:(await import("sass").catch(()=>{throw new Error(`To compile ${r(s)} install sass: npm i -D sass`)})).compile(s).css,from:r(s)}:o(e)?{css:c(e,"utf8"),from:r(e)}:{css:"",from:null}}export{l as resolveStyles};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class c extends Error{code;loc;constructor(r,n){super(r),this.name="ParseError",this.code="syntax",this.loc=n||null}}function g(t,r,n={}){const{loc:e=null,suggestion:o=null,severity:s="error"}=n;return{code:t,severity:s,message:r,loc:e,suggestion:o}}function u(t,r,n=3){let e=null,o=1/0;for(const s of r){const i=l(t,s);i<o&&(o=i,e=s)}return o<=n?e:null}function l(t,r){const n=Array.from({length:r.length+1},(e,o)=>o);for(let e=1;e<=t.length;e++){let o=n[0];n[0]=e;for(let s=1;s<=r.length;s++){const i=n[s];n[s]=t[e-1]===r[s-1]?o:1+Math.min(o,n[s],n[s-1]),o=i}}return n[r.length]}function a(t,r){const n=t.loc?`${r}:${t.loc.line}:${t.loc.col}`:r,e=t.suggestion?` \u2192 did you mean "${t.suggestion}"?`:"";return`${n} ${t.severity} [${t.code}] ${t.message}${e}`}export{c as ParseError,u as closest,g as diag,a as formatDiagnostic};
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var m=(a=>(a.Ident="ident",a.String="string",a.Number="number",a.Ref="ref",a.Param="param",a.Punct="punct",a.Arrow="arrow",a.LArrow="larrow",a.Eq="eq",a.Neq="neq",a.Lte="lte",a.Gte="gte",a.FatArrow="fatarrow",a.Eof="eof",a))(m||{}),c=(r=>(r.BraceL="{",r.BraceR="}",r.ParenL="(",r.ParenR=")",r.BrackL="[",r.BrackR="]",r.Comma=",",r.Pipe="|",r.Colon=":",r.Assign="=",r.Lt="<",r.Gt=">",r.Dot=".",r.Slash="/",r.Plus="+",r.Star="*",r.Question="?",r.Dash="-",r))(c||{}),h=(e=>(e.Screen="screen",e.Entity="entity",e.State="state",e.Store="store",e.Get="get",e.Effect="effect",e.Action="action",e.Mutates="mutates",e.Mock="mock",e.Sources="sources",e.Routes="routes",e.Shell="shell",e.Part="part",e.Const="const",e.Theme="theme",e.Query="query",e.When="when",e.Each="each",e.As="as",e.If="if",e.Else="else",e.Guard="guard",e.Not="not",e.And="and",e.Or="or",e.Contains="contains",e.Required="required",e.Min="min",e.Max="max",e.True="true",e.False="false",e.Null="null",e))(h||{}),S=(t=>(t.Shell="Shell",t.Header="Header",t.Nav="Nav",t.Sidebar="Sidebar",t.Footer="Footer",t.Page="Page",t.Stack="Stack",t.Text="Text",t.Title="Title",t.Span="Span",t.Image="Image",t.Link="Link",t.Button="Button",t.Form="Form",t.SearchField="SearchField",t.DataTable="DataTable",t.RowAction="RowAction",t.Custom="Custom",t.When="When",t.Each="Each",t.Slot="slot",t))(S||{}),x=(u=>(u.Module="module",u.Store="store",u.Html="html",u))(x||{}),f=(u=>(u.Text="text",u.Email="email",u.Enum="enum",u))(f||{}),d=(o=>(o.Or="or",o.And="and",o.Eq="==",o.Neq="!=",o.Lte="<=",o.Gte=">=",o.Lt="<",o.Gt=">",o.Contains="contains",o.Add="+",o.Sub="-",o.Mul="*",o.Div="/",o))(d||{}),A=(i=>(i.Not="not",i))(A||{}),b=(s=>(s.Lit="lit",s.Ref="ref",s.Un="un",s.Bin="bin",s.Tern="tern",s.Interp="interp",s))(b||{}),L=(l=>(l.Push="push",l.Set="set",l.Reset="reset",l.Remove="remove",l.If="if",l))(L||{}),R=(n=>(n.Bind="bind",n.Submit="submit",n.Where="where",n.Columns="columns",n.Style="style",n.Class="class",n.Alt="alt",n.Inputs="inputs",n.On="on",n))(R||{});export{d as BOp,b as Ek,f as Fk,x as Fmt,h as Kw,R as Mod,S as Nt,c as Pn,L as StOp,m as Tk,A as UOp};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const g=new Map([["start","flex-start"],["center","center"],["end","flex-end"],["between","space-between"],["around","space-around"],["evenly","space-evenly"]]),d=new Map([["start","flex-start"],["center","center"],["end","flex-end"],["stretch","stretch"],["baseline","baseline"]]),r=new Map([["row","display:flex;flex-direction:row"],["column","display:flex;flex-direction:column"],["wrap","flex-wrap:wrap"],["grid","display:grid"],["grow","flex:1"],["center","align-items:center"],["between","justify-content:space-between"],["bold","font-weight:700"],["italic","font-style:italic"]]),i=(e,t)=>t[e]??(/^\d+$/.test(e)?e+"px":null),c=(e,t,n)=>{if(e.startsWith("x.")){const l=i(e.slice(2),n);return l?`${t}-left:${l};${t}-right:${l}`:null}if(e.startsWith("y.")){const l=i(e.slice(2),n);return l?`${t}-top:${l};${t}-bottom:${l}`:null}const s=i(e,n);return s?`${t}:${s}`:null},o={gap:(e,t)=>{const n=i(e,t.space);return n?`gap:${n}`:null},padding:(e,t)=>c(e,"padding",t.space),margin:(e,t)=>c(e,"margin",t.space),cols:e=>e==="auto"?"grid-template-columns:repeat(auto-fill,minmax(160px,1fr))":/^\d+$/.test(e)?`grid-template-columns:repeat(${e},1fr)`:null,rows:e=>/^\d+$/.test(e)?`grid-template-rows:repeat(${e},1fr)`:null,text:(e,t)=>{const n=i(e,t.font);return n?`font-size:${n}`:null},weight:(e,t)=>{const n=t.weight[e]??(/^\d+$/.test(e)?e:null);return n?`font-weight:${n}`:null},leading:(e,t)=>{const n=t.leading[e]??(/^[\d.]+$/.test(e)?e:null);return n?`line-height:${n}`:null},align:e=>["left","center","right","justify"].includes(e)?`text-align:${e}`:null,justify:e=>{const t=g.get(e);return t?`justify-content:${t}`:null},items:e=>{const t=d.get(e);return t?`align-items:${t}`:null},width:(e,t)=>e==="full"?"width:100%":i(e,t.space)?`width:${i(e,t.space)}`:null,height:(e,t)=>e==="full"?"height:100%":i(e,t.space)?`height:${i(e,t.space)}`:null},u=Object.keys(o),x=[...r.keys()],f=["sm","md","lg","xl"],p={space:{},font:{},weight:{},leading:{},breakpoints:{}};function w(e={}){return{space:{...e.space||{}},font:{...e.font||{}},weight:{...e.weight||{}},leading:{...e.leading||{}},breakpoints:{...e.breakpoints||{}}}}function h(e,t=p){const n=e.indexOf(":");if(n>0&&t.breakpoints[e.slice(0,n)])return h(e.slice(n+1),t);const s=r.get(e);if(s)return s;const l=e.indexOf(".");if(l<0)return null;const a=o[e.slice(0,l)];return a&&a(e.slice(l+1),t)||null}function m(e){const t=e.indexOf(":"),n=t>0&&f.includes(e.slice(0,t))?e.slice(t+1):e;if(r.has(n))return!0;const s=n.indexOf(".");return s>0&&u.includes(n.slice(0,s))}const $=e=>"t-"+e.replace(/[.:]/g,"-"),y=["row","column","wrap","grid","grow","center","between","bold","italic","gap.sm","gap.md","gap.lg","padding.md","padding.lg","padding.x.md","padding.y.md","margin.md","cols.2","cols.3","cols.auto","rows.2","text.sm","text.md","text.lg","text.xl","weight.medium","weight.bold","leading.normal","align.left","align.center","align.right","justify.center","justify.between","items.center","items.start","width.full","height.full","md:row","md:cols.2","md:cols.3","lg:cols.4"];export{x as ATOM_NAMES,f as BREAKPOINT_NAMES,u as FAMILY_NAMES,y as SUGGESTED,p as defaultTheme,m as isKnownTokenShape,w as mergeTheme,h as resolveToken,$ as tokenClass};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildApp as p}from"./build.js";import{lintApp as e}from"./lint.js";import{parse as m}from"#engine/lang/parse.js";import{toDoc as x}from"#engine/ir/flatten.js";import{validate as d}from"#engine/ir/validate.js";import{compile as A}from"#engine/compile/compile.js";import{load as s,loadAllParts as b}from"#engine/project/load.js";export{p as buildApp,A as compile,e as lintApp,s as load,b as loadAllParts,m as parse,x as toDoc,d as validate};
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{cpSync as m,existsSync as o,readdirSync as p,readFileSync as i,writeFileSync as g}from"node:fs";import{fileURLToPath as l}from"node:url";import{dirname as f,join as r,resolve as u,basename as d}from"node:path";const a=f(l(import.meta.url)),c=r(a,"..","templates","starter"),S=JSON.parse(i(r(a,"..","package.json"),"utf8")).version;function v(n){const e=u(n);if(o(e)&&p(e).length>0)throw new Error(`target "${n}" is not empty`);if(!o(c))throw new Error("starter template missing from the muten package");m(c,e,{recursive:!0});const s=r(e,"package.json"),t=JSON.parse(i(s,"utf8"));t.name=d(e),t.dependencies.muten="^"+S,g(s,JSON.stringify(t,null,2)+`
|
|
2
|
+
`),console.log(`\u2713 created ${n}
|
|
3
|
+
|
|
4
|
+
cd ${n}
|
|
5
|
+
npm install
|
|
6
|
+
npm run dev`)}export{v as initApp};
|
package/dist/lint.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{join as m,relative as l}from"node:path";import{readRoutes as g}from"#engine/project/routes.js";import{load as p,loadParts as f}from"#engine/project/load.js";import{validate as d}from"#engine/ir/validate.js";import{formatDiagnostic as h,ParseError as u}from"#engine/shared/diagnostics.js";async function D(t){const a=s=>l(t,s),n=await f(m(t,"src","parts")),i=g(t);let r=0;for(const s of i){let e=[];try{const{doc:o,partNames:c}=await p(s.screenPath,n);e=d(o,{parts:c}).diagnostics}catch(o){if(!(o instanceof u))throw o;e=[{code:o.code,severity:"error",message:o.message,loc:o.loc,suggestion:null}]}for(const o of e)console.log(h(o,a(s.screenPath))),r++}return console.log(r?`
|
|
2
|
+
\u2716 ${r} problem(s)`:"\u2713 no problems"),r}export{D as lintApp};
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
let i=null,c=null;function p(n){const e=new Set;return{get(){return i&&(e.add(i),i.deps.add(e)),n},set(o){if(o!==n){n=o;for(const t of[...e])t()}}}}function f(n){const e=Object.assign(()=>{if(e.disposed)return;for(const t of e.deps)t.delete(e);e.deps.clear();const o=i;i=e;try{n()}finally{i=o}},{deps:new Set,disposed:!1});return c&&c.push(e),e(),e}function g(n){const e=c,o=[];c=o;try{n()}finally{c=e}return()=>{for(const t of o){t.disposed=!0;for(const s of t.deps)s.delete(t);t.deps.clear()}}}function E(n,e){return Array.isArray(n)?n.includes(e):String(n??"").toLowerCase().includes(String(e??"").toLowerCase())}function T(n){const e=p(n());return f(()=>e.set(n())),e}let h=0;function m(){return globalThis.crypto&&crypto.randomUUID?crypto.randomUUID():"id-"+ ++h}const a=new Set;function y(n){if(!n||a.has(n))return;a.add(n);const e=document.createElement("style");e.textContent=n,document.head.appendChild(e)}function R(n,e){const o=Object.keys(e);let t=null,s=null;const u=()=>{const d=(location.hash||"").replace(/^#/,"")||o[0],r=e[d]||e[o[0]];if(r.guard&&!r.guard()){const l="#"+(r.redirect??"");location.hash!==l&&(location.hash=r.redirect??"");return}d!==t&&(t=d,s&&s(),s=null,n.replaceChildren(),r.load().then(l=>{y(l.css),s=g(()=>{l.mount(n)})}))};addEventListener("hashchange",()=>{t=null,u()}),f(()=>{for(const d of o){const r=e[d].guard;r&&r()}u()})}export{E as __has,m as __id,T as computed,f as effect,y as injectCss,R as route,g as scope,p as signal};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import{readFileSync as p,existsSync as h,readdirSync as D}from"node:fs";import{fileURLToPath as T}from"node:url";import{dirname as k,join as c,basename as E}from"node:path";import{parse as j}from"#engine/lang/parse.js";import{toDoc as P}from"#engine/ir/flatten.js";import{load as W,loadAllParts as H}from"#engine/project/load.js";import{validate as F}from"#engine/ir/validate.js";import{compileModule as b,compileStore as M}from"#engine/compile/compile.js";import{mergeTheme as w}from"#engine/style/tokens.js";import{Nt as R}from"#engine/shared/vocab.js";const O="virtual:muten/runtime",y="virtual:muten/store/",_="virtual:muten/shell",J=k(T(import.meta.url)),L=p(c(J,"runtime.js"),"utf8");function N(l,m={}){let a;try{a=D(l,{withFileTypes:!0})}catch{return m}for(const o of a){const u=c(l,o.name);o.isDirectory()?N(u,m):o.name.endsWith(".store")&&(m[E(o.name,".store")]=j(p(u,"utf8")))}return m}function U(l={}){const m=l.store!==!1;let a=w(l.theme),o=process.cwd(),u={},S={};const d={};let $,v=null;const C=()=>{const e=new Set,t=($?.routes||[]).map(s=>{const n=JSON.stringify("/"+s.url.replace(/^\//,"")),g=`() => import(${JSON.stringify("/src/pages/"+s.page+"/"+s.page+".muten")})`;if(s.guard){const[f,i]=s.guard.split(".");return e.add(f),` ${n}: { load: ${g}, guard: () => ${s.guardNeg?"!":""}__store_${f}.${i}.get(), redirect: ${JSON.stringify(s.redirect)} },`}return` ${n}: { load: ${g} },`}).join(`
|
|
2
|
+
`),r=[...e].map(s=>`import * as __store_${s} from '${y}${s}';`).join(`
|
|
3
|
+
`);return`import * as __shell from '${_}';
|
|
4
|
+
import { route, injectCss } from '${O}';
|
|
5
|
+
${v?`import ${JSON.stringify(v)};
|
|
6
|
+
`:""}${r}
|
|
7
|
+
const routes = {
|
|
8
|
+
${t}
|
|
9
|
+
};
|
|
10
|
+
const root = document.getElementById('app');
|
|
11
|
+
if (root) {
|
|
12
|
+
injectCss(__shell.css);
|
|
13
|
+
const outlet = __shell.mount(root);
|
|
14
|
+
route(outlet, routes);
|
|
15
|
+
}`};return{name:"vite-plugin-muten",enforce:"pre",async configResolved(e){if(o=e.root,u=await H(o),m){S=N(c(o,"src"));for(const[s,n]of Object.entries(S))d[s]={state:Object.keys(n.state||{}),gets:Object.keys(n.gets||{}),actions:Object.keys(n.actions||{})}}const t=c(o,"src","app.muten");h(t)&&($=j(p(t,"utf8")));const r=c(o,"theme.muten");h(r)&&(a=w(j(p(r,"utf8")).theme||{}));for(const s of["styles.css","styles.scss"])if(h(c(o,"src",s))){v="/src/"+s;break}},resolveId(e){if(e===O||e===_||e.startsWith(y))return"\0"+e},load(e){if(e==="\0"+O)return L;if(e.startsWith("\0"+y)){const t=S[e.slice(("\0"+y).length)];if(t)return M({state:t.state||{},gets:t.gets||{},actions:t.actions||{},effects:t.effects||[],entities:t.entities||{}},t.mock||{},t.sources||{})}if(e==="\0"+_){const t=$?.shell||{type:R.Shell,props:{},children:[{type:R.Slot,props:{}}]},r=P({screen:"shell",entities:{},state:{},actions:{},tree:t});return b(r,{},"",{},{},{stores:d,theme:a})}},async transform(e,t){if(!t.endsWith(".muten"))return null;if(t.replace(/\\/g,"/").endsWith("/src/app.muten"))return{code:C(),map:null};const r=await W(t,u),{ok:s,diagnostics:n}=F(r.doc,{parts:r.partNames,stores:Object.keys(d),theme:a});if(!s)throw new Error("muten: "+n.map(i=>i.message).join(" \xB7 "));const g=[...new Set(Object.values(r.doc.nodes).filter(i=>i.type===R.Custom).map(i=>i.props?.component))],f={};for(const i of g){if(!i)continue;const I=c(o,"src","components",i+".js");h(I)&&(f[i]=p(I,"utf8"))}return{code:b(r.doc,r.data,r.styles.css,f,r.sources,{stores:d,theme:a}),map:null}},handleHotUpdate(e){(e.file.endsWith(".muten")||e.file.endsWith(".store"))&&e.server.ws.send({type:"full-reload"})},configureServer(e){e.middlewares.use((t,r,s)=>{if((t.url||"").split("?")[0]!=="/src/app.muten"){s();return}e.transformRequest("/src/app.muten").then(n=>{if(!n){s();return}r.setHeader("Content-Type","text/javascript"),r.end(n.code)},s)})}}}export{U as default};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@muten/core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "AI-first frontend framework — compiles .muten files to vanilla JS + signals.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/karttofer/muten.git"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"bin": {
|
|
14
|
+
"muten": "./dist/bin/muten.js"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/index.js",
|
|
19
|
+
"./vite-plugin-muten.js": "./dist/vite-plugin-muten.js"
|
|
20
|
+
},
|
|
21
|
+
"imports": {
|
|
22
|
+
"#engine/*": "./dist/engine/*"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"spec",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"ai",
|
|
35
|
+
"frontend",
|
|
36
|
+
"framework",
|
|
37
|
+
"dsl",
|
|
38
|
+
"signals",
|
|
39
|
+
"compiler"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"prepare": "npm run build",
|
|
44
|
+
"build": "tsc && node --experimental-strip-types esbuild.ts",
|
|
45
|
+
"test": "npm run build && node --experimental-strip-types test/parse.ts && node --experimental-strip-types test/expr.ts && node --experimental-strip-types test/parts.ts && node --experimental-strip-types test/diagnostics.ts && node --experimental-strip-types test/routes.ts && node --experimental-strip-types test/runtime.ts && node --experimental-strip-types test/lang.ts && node --experimental-strip-types test/forms.ts && node --experimental-strip-types test/smoke.ts"
|
|
46
|
+
},
|
|
47
|
+
"optionalDependencies": {
|
|
48
|
+
"sass": "^1.101.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^26.0.0",
|
|
52
|
+
"esbuild": "^0.28.1",
|
|
53
|
+
"typescript": "^6.0.3",
|
|
54
|
+
"vite": "^8.0.16"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/spec/grammar.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# The `.muten` language
|
|
2
|
+
|
|
3
|
+
> The authoritative source of the **syntax**. The **vocabulary** (primitives, their props, the style
|
|
4
|
+
> tokens, the keywords) lives in the manifest ([`src/engine/lang/manifest.ts`](../src/engine/lang/manifest.ts))
|
|
5
|
+
> and the enums ([`src/engine/shared/vocab.ts`](../src/engine/shared/vocab.ts)) — the other half of the
|
|
6
|
+
> rules. From these derive the lexer/parser (`src/engine/lang/`), the validator (`src/engine/ir/`), and
|
|
7
|
+
> the editor's highlight + autocomplete. **Changing the language = changing these rules + the vocabulary.**
|
|
8
|
+
|
|
9
|
+
## 1. Lexical (tokens)
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
comment = "#" … (to end of line) # ignored
|
|
13
|
+
string = '"' … '"'
|
|
14
|
+
ref = "@" ident # a state reference: @users
|
|
15
|
+
param = "$" ident # a part-param reference: $title
|
|
16
|
+
number = "-"? digit+ ( "." digit+ )?
|
|
17
|
+
ident = (letter | "_") (letter | digit | "_")*
|
|
18
|
+
operators = "->" "<-" "=>" "==" "!=" "<=" ">="
|
|
19
|
+
punctuation = { } ( ) [ ] , | : = < > . / + * ? -
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 2. File structure
|
|
23
|
+
|
|
24
|
+
A file is a sequence of top-level declarations, in any order:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
file = declaration* ;
|
|
28
|
+
declaration = screen | entity | state | store | get | effect | const | theme
|
|
29
|
+
| action | mock | sources | routes | shell | part | node ;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- A **page** (`src/pages/<route>/<route>.muten`): `screen` + state/entity/action/const/… + one **root node** (the tree).
|
|
33
|
+
- The **app root** (`src/app.muten`): `routes` (+ an optional persistent `shell`).
|
|
34
|
+
- A **store slice** (`*.store`): `store` / `get` / `action` / `effect` — app-global state, no tree.
|
|
35
|
+
- A **part** (`src/**/parts/*.muten`): `part` (+ its own entity/state/mock, *hoisted* when used).
|
|
36
|
+
- The **theme** (`theme.muten`): a single `theme` block (the token scale).
|
|
37
|
+
|
|
38
|
+
## 3. Declarations
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
screen = "screen" ident ;
|
|
42
|
+
|
|
43
|
+
entity = "entity" ident "{" field* "}" ; # implicit `id uuid`
|
|
44
|
+
field = ident type constraint* ; # `role admin | member` = enum
|
|
45
|
+
type = ident ( "<" ident ">" )? ; # text|number|bool|email|uuid | EntityName | list<T>
|
|
46
|
+
constraint = "required" | "min" ":" number | "max" ":" number ;
|
|
47
|
+
|
|
48
|
+
state = "state" "{" binding* "}" ; # `store { … }` is identical, but app-global
|
|
49
|
+
binding = ident "=" ( "query" ident | value ) ":" type ; # query → async { data, loading, error }
|
|
50
|
+
|
|
51
|
+
get = "get" ident "=" expr ; # .store derived/memoized value
|
|
52
|
+
effect = "effect" actionBody ; # .store reactive side-effect
|
|
53
|
+
const = "const" ident "=" scalar ; # compile-time immutable (scalars only)
|
|
54
|
+
|
|
55
|
+
action = "action" ident "mutates" ident ( "," ident )* "<-" ident actionBody ;
|
|
56
|
+
actionBody = "{" statement* "}" ;
|
|
57
|
+
statement = ident "." "push" "(" expr ")"
|
|
58
|
+
| ident "." "set" "(" expr ")"
|
|
59
|
+
| ident "." "reset" "(" ")"
|
|
60
|
+
| ident "." "remove" "(" ident "=>" expr ")"
|
|
61
|
+
| "if" expr actionBody ( "else" actionBody )? ;
|
|
62
|
+
|
|
63
|
+
mock = "mock" "{" ( ident ":" value )* "}" ;
|
|
64
|
+
sources = "sources" "{" ( ident ":" value )* "}" ; # value = "url" or { url: "…", at: "…" }
|
|
65
|
+
value = scalar | array | object ;
|
|
66
|
+
scalar = string | number | "true" | "false" | "null" | ident ; # a bare ident = an enum value
|
|
67
|
+
array = "[" ( value ( "," value )* )? "]" ;
|
|
68
|
+
object = "{" ( ( ident | string ) ":" value ( "," … )* )? "}" ;
|
|
69
|
+
|
|
70
|
+
routes = "routes" "{" route* "}" ; # one route per line
|
|
71
|
+
route = path "->" ident ( "guard" "not"? dotted "else" path )? ;
|
|
72
|
+
path = ( "/" ident? )+ ; # / · /cart · /
|
|
73
|
+
|
|
74
|
+
shell = "shell" "{" node* "}" ; # persistent chrome; must contain a `slot`
|
|
75
|
+
theme = "theme" "{" ( ident "{" ( ident string )* "}" )* "}" ; # space { md "16px" } …
|
|
76
|
+
part = "part" ident "(" ( param ( "," param )* )? ")" "{" node* "}" ;
|
|
77
|
+
param = ident ":" type ;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 4. Expressions
|
|
81
|
+
|
|
82
|
+
Used in `when` / `each` conditions, `if`, `get`, and `{ }` string interpolation. Precedence, lowest
|
|
83
|
+
binding first (each level parses the level below, then folds left while its operator keeps appearing):
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
expr = ternary ;
|
|
87
|
+
ternary = or ( "?" ternary ":" ternary )? ;
|
|
88
|
+
or = and ( "or" and )* ;
|
|
89
|
+
and = cmp ( "and" cmp )* ;
|
|
90
|
+
cmp = add ( ( "==" | "!=" | "<" | ">" | "<=" | ">=" | "contains" ) add )* ;
|
|
91
|
+
add = mul ( ( "+" | "-" ) mul )* ;
|
|
92
|
+
mul = unary ( ( "*" | "/" ) unary )* ;
|
|
93
|
+
unary = "not" unary | primary ;
|
|
94
|
+
primary = "(" ternary ")" | string | number | "true" | "false" | "null" | refName ;
|
|
95
|
+
refName = ( ident | param ) ( "." ident )* ; # user.name · cart.total · $item.field
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`contains` is list-membership OR case-insensitive substring (one operator, both meanings).
|
|
99
|
+
|
|
100
|
+
## 5. Nodes (the UI tree)
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
node = control | primitive | partInstance ;
|
|
104
|
+
control = "when" expr "{" node* "}"
|
|
105
|
+
| "each" expr "as" ident "{" node* "}" ;
|
|
106
|
+
primitive = TYPE nodePart* block? ; # TYPE ∈ manifest PRIMITIVES
|
|
107
|
+
partInstance = ident "(" ( ident ":" argValue ( "," … )* )? ")" ; # ident is NOT a primitive
|
|
108
|
+
argValue = string | number | ref | param | dotted ;
|
|
109
|
+
|
|
110
|
+
nodePart = positional | "->" target | modifier | level ;
|
|
111
|
+
positional = string | ref | param ; # → the primitive's string-prop / `data`
|
|
112
|
+
target = path # Link -> /route
|
|
113
|
+
| dotted ( "(" expr? ")" )? ; # action -> add(arg)
|
|
114
|
+
level = "h1" | "h2" | … | "h6" ; # Title only (heading level, not style)
|
|
115
|
+
modifier = "bind" ( ref | dotted ) # bind @draft · bind cart.query
|
|
116
|
+
| "submit" dotted
|
|
117
|
+
| "where" "(" clause* ")" # where(role == admin, name contains @q)
|
|
118
|
+
| "columns" "(" ident* ")"
|
|
119
|
+
| "style" "(" token* ")" # analyzable layout/typography tokens
|
|
120
|
+
| "class" "(" ( string | ident )* ")" # raw look classes (your CSS / Tailwind)
|
|
121
|
+
| "alt" string # Image (required, a11y/SEO)
|
|
122
|
+
| "inputs" "(" arg* ")" | "on" "(" arg* ")" ; # Custom
|
|
123
|
+
block = "{" node* "}" ;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- A **style token** is `family.step` or an atom, with an optional breakpoint prefix: `cols.3`, `gap.md`,
|
|
127
|
+
`padding.x.lg`, `md:cols.4`. Shapes come from the engine; the step values come from `theme.muten`.
|
|
128
|
+
- A positional string **interpolates** `{expr}` on Text/Title/Span/Image/Button/Link.
|
|
129
|
+
|
|
130
|
+
**Disambiguation:** if `(` comes right after the `TYPE`, it's a **part instance**; otherwise it's a
|
|
131
|
+
**primitive** with its parts. Primitives never take `(args)` after the name.
|
|
132
|
+
|
|
133
|
+
## 6. From rules to IR
|
|
134
|
+
|
|
135
|
+
- A node → `{ type, props, children?, loc }`; a part instance → `{ type, args, loc }`.
|
|
136
|
+
- The positional string is assigned to the prop named by the manifest's `string` for that primitive.
|
|
137
|
+
- `compose` inlines part instances (substituting `$param` with the args) **before** `flatten`, so the
|
|
138
|
+
part disappears and a tree of primitives remains; `flatten` numbers it into the flat `Doc`.
|
|
139
|
+
- `validate` requires: known types/parts, declared `@refs`, accepted style tokens, required props, and
|
|
140
|
+
actions that mutate only what `mutates` declares.
|
|
141
|
+
|
|
142
|
+
## 7. One source, many tools
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
these rules + the manifest (vocabulary) + vocab (tokens · keywords)
|
|
146
|
+
│
|
|
147
|
+
┌────────────┬──────────────┼──────────────┬───────────────┐
|
|
148
|
+
lexer/parse validate highlight linter autocomplete
|
|
149
|
+
(lang/) (ir/) (extension) (project/) (project/)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Everything reads the same two sources. Adding a primitive, a token or a keyword = edit the manifest /
|
|
153
|
+
vocab (and its codegen in `compile/`); the parser, validator and editor stay consistent on their own.
|