@muten/core 0.0.7 → 0.0.9
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/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
<img width="157" height="157" alt="Group 21" src="https://github.com/user-attachments/assets/fe9a02e6-483d-4788-9286-142c1ddb7057" />
|
|
3
3
|
<br/>
|
|
4
|
+
|
|
4
5
|
An **AI-first** frontend framework. You write `.muten` files; muten compiles them to vanilla JS
|
|
5
6
|
with fine-grained signals — **no virtual DOM, no framework runtime to ship**. The language is small,
|
|
6
7
|
semantic and analyzable on purpose: an AI (or a person) can **locate and mutate** an app cheaply.
|
|
@@ -10,19 +11,23 @@ npm create muten@latest my-app # scaffold a new app (cross-platform: Windows +
|
|
|
10
11
|
cd my-app && npm install && npm run dev
|
|
11
12
|
```
|
|
12
13
|
|
|
13
|
-
## Why muten
|
|
14
|
+
## Why muten
|
|
14
15
|
|
|
15
|
-
For an AI the cost of
|
|
16
|
-
|
|
16
|
+
For an AI the cost of working on a codebase is **context + mistakes + edit-radius**. muten is built to cut
|
|
17
|
+
all three *by construction* — these are properties of how it compiles, not marketing:
|
|
17
18
|
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- **
|
|
23
|
-
|
|
19
|
+
- **Almost nothing to ship** — no virtual DOM, no framework runtime. The *same* todo app, scaffolded by each
|
|
20
|
+
framework's official CLI and built, ships **~2.8 KB gzip** of JS in muten vs **~14 KB (Svelte) · ~24 KB (Vue)
|
|
21
|
+
· ~59 KB (React)** — **5–21× less** (a static page ships *zero*). Source is the most compact too (445 B), on
|
|
22
|
+
par with Svelte. *(Reproducible: the `bench/` folder + `node bench.mjs` in the source repo.)*
|
|
23
|
+
- **A deterministic oracle** — `muten check --json` validates every page at compile time (unknown
|
|
24
|
+
state/action/part, bad style token, illegal mutation) in milliseconds, no browser — a feedback loop the
|
|
25
|
+
others don't have. A *bounded* language is what makes that possible.
|
|
26
|
+
- **The whole app as data** — `app.map.json` is a compact index of routes + structure an agent reads first,
|
|
27
|
+
instead of grepping a component tree.
|
|
28
|
+
- **Small edit radius** — the UI is declarative, so a change is usually a few lines in one file.
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
The trade is deliberate: a small, analyzable language an AI can hold in its head — not a general-purpose one it can't.
|
|
26
31
|
|
|
27
32
|
## Capabilities
|
|
28
33
|
|
|
@@ -3,7 +3,7 @@ import{tokenClass as ne,resolveToken as Ee,defaultTheme as Oe}from"#engine/style
|
|
|
3
3
|
`),he=u.genEffects();let Z=null;Y?Z=C?X(C):"":$.format!==b.Store&&C&&Q(C,"app");const _e=t.join(`
|
|
4
4
|
`),ee={};for(const e of Object.values(I))if(typeof e.source=="string"&&e.source.startsWith("query:")){const s=e.source.slice(6),o=(e.type.match(/^list<(.+)>$/)||[])[1];ee[s]=o?u.uuidFields(o):[]}const ge=[...U].map(e=>{const s=Ee(e,M);if(!s)return"";const o=`.${ne(e)}{${s}}`,n=e.indexOf(":"),f=n>0&&M.breakpoints[e.slice(0,n)];return f?`@media (min-width:${f}){${o}}`:o}).filter(Boolean).join(`
|
|
5
5
|
`),de=Object.entries(J).map(([e,s])=>`const __custom_${e} = (function () {
|
|
6
|
-
${s}
|
|
6
|
+
${s.replace(/^[ \t]*export[ \t]+(default[ \t]+)?/gm,"")}
|
|
7
7
|
return mount;
|
|
8
8
|
})();`).join(`
|
|
9
9
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveToken as
|
|
1
|
+
import{resolveToken as U,SUGGESTED as W,defaultTheme as B,isKnownTokenShape as G}from"#engine/style/tokens.js";import{diag as c,closest as p}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as Y,ACTION_OPS as z,PRIMITIVES as H}from"#engine/lang/manifest.js";import{Nt as d,Ek as h,StOp as J}from"#engine/shared/vocab.js";const A=new Set([...Y,d.Shell]),Q=["bind","data"],C=new Set(z),O=["text","number","bool","uuid","email","string"];function m(o,a=[]){if(o.kind===h.Ref)a.push(o.name);else if(o.kind===h.Un)m(o.operand,a);else if(o.kind===h.Bin)m(o.left,a),m(o.right,a);else if(o.kind===h.Tern)m(o.cond,a),m(o.then,a),m(o.else,a);else if(o.kind===h.Call)for(const r of o.args)m(r,a);return a}function y(o,a=[]){if(o.kind===h.Call){a.push(o.fn);for(const r of o.args)y(r,a)}else o.kind===h.Un?y(o.operand,a):o.kind===h.Bin?(y(o.left,a),y(o.right,a)):o.kind===h.Tern&&(y(o.cond,a),y(o.then,a),y(o.else,a));return a}function et(o,a={}){const r=[],k=new Set(Object.keys(o.state||{})),F=new Set(a.stores||[]),D=new Set(Object.keys(o.consts||{})),q=new Set(o.params||[]),w=new Set(Object.keys(o.actions||{})),E=e=>/^(svelte|react):/.test(e),N=new Set((o.imports||[]).filter(e=>!E(e.from)).flatMap(e=>e.names)),j=new Set((o.imports||[]).filter(e=>E(e.from)).flatMap(e=>e.names)),_=o.nodes||{},S=(e,s)=>{for(const t of y(e))N.has(t)||r.push(c("unknown-function",`"${t}" is not a use'd function`,{loc:s,suggestion:p(t,[...N]),from:t}))},M=new Map;for(const[e,s]of Object.entries(o.state||{})){if(!s.source?.startsWith("query:"))continue;const t=new Set(["loading","error","data"]),n=o.entities?.[s.type];if(n){t.add("id");for(const i of Object.keys(n))t.add(i)}M.set(e,t)}const I=new Map;for(const[e,s]of Object.entries(a.storeMembers||{}))I.set(e,new Set(s));const x=(e,s,t)=>{const n=M.get(e);if(n){n.has(s)||r.push(c("unknown-member",`"${s}" is not a member of query "${e}"`,{loc:t.loc,suggestion:p(s,[...n]),from:s}));return}const i=I.get(e);i&&!i.has(s)&&r.push(c("unknown-member",`"${s}" is not a member of store "${e}"`,{loc:t.loc,suggestion:p(s,[...i]),from:s}))},T=Object.keys(o.entities||{});for(const[e,s]of Object.entries(o.state||{})){const t=s.type;if(t==="list")r.push(c("untyped-list",`state "${e}" is an untyped "list" \u2014 declare the element type, e.g. list<uuid> or list<User>`,{loc:s.loc,suggestion:"list<uuid>"}));else if(t.startsWith("list<")){const n=t.slice(5,-1);!O.includes(n)&&!T.includes(n)&&r.push(c("unknown-type",`list element "${n}" is not a known entity or scalar type`,{loc:s.loc,suggestion:p(n,[...T,...O]),from:n}))}else if(s.initial!==void 0&&s.initial!==null){const n=t==="number"?"number":t==="bool"?"boolean":["text","string","email","uuid"].includes(t)?"string":"";n&&typeof s.initial!==n&&r.push(c("type-mismatch",`state "${e}" is typed "${t}" but its initial value is a ${typeof s.initial}`,{loc:s.loc}))}}const v=e=>{const s=o.entities?.[e];return s?new Set(["id",...Object.keys(s)]):null},K=e=>{if(!e||e.kind!==h.Ref)return"";const s=o.state?.[e.name.split(".")[0]]?.type||"";return s.startsWith("list<")?s.slice(5,-1):""},L=(e,s)=>{if(typeof e=="string"&&e.startsWith("@")){const t=e.slice(1).split(".")[0];if(!k.has(t)){const n=p(t,[...k]);r.push(c("unknown-ref",`"@${t}" is not a declared state`,{loc:s.loc,suggestion:n?"@"+n:null,from:"@"+t,related:n?o.state?.[n]?.loc??null:null}))}}},R=(e,s)=>{if(!(!e||e.startsWith("$"))){if(e.includes(".")){const t=e.indexOf(".");x(e.slice(0,t),e.slice(t+1).split(".")[0],s);return}w.has(e)||r.push(c("unknown-action",`"${e}" is not a declared action`,{loc:s.loc,suggestion:p(e,[...w]),from:e}))}},b=(e,s,t)=>{S(e,s.loc);for(const n of m(e)){const i=n.indexOf("."),f=i===-1?n:n.slice(0,i);if(!(t.has(f)||k.has(f)||F.has(f)||D.has(f)||q.has(f)||w.has(f))){r.push(c("unknown-ref",`"${f}" is not a known state or item variable here`,{loc:s.loc,suggestion:p(f,[...k,...t.keys()]),from:f}));continue}if(i===-1)continue;const l=n.slice(i+1).split(".")[0],g=t.has(f)?t.get(f)||"":o.state?.[f]?.type||"",u=v(g);if(u)u.has(l)||r.push(c("unknown-member",`"${l}" is not a field of ${g} (${t.has(f)?"item":"state"} "${f}")`,{loc:s.loc,suggestion:p(l,[...u]),from:l}));else if(O.includes(g))r.push(c("unknown-member",`"${f}" is a ${g} \u2014 it has no field "${l}"`,{loc:s.loc}));else if(w.has(f)&&!k.has(f)){const $=new Set(["pending","error"]);$.has(l)||r.push(c("unknown-member",`action "${f}" exposes only .pending / .error, not "${l}"`,{loc:s.loc,suggestion:p(l,[...$]),from:l}))}else x(f,l,s)}},P=new Set,V=(e,s)=>{const t=_[e];if(!t){r.push(c("missing-node",`node ${e} does not exist`));return}if(P.has(e)){r.push(c("dup-node",`${e} is referenced twice`,{loc:t.loc}));return}if(P.add(e),!j.has(t.type))if(!A.has(t.type))t.args?r.push(c("unknown-part",`"${t.type}" is not a known part`,{loc:t.loc,suggestion:p(t.type,[...a.parts||[],...j]),from:t.type})):r.push(c("unknown-type",`"${t.type}" is not a known primitive`,{loc:t.loc,suggestion:p(t.type,[...A]),from:t.type}));else{const l=H[t.type],g=l?l.props:{};for(const[u,$]of Object.entries(g))!$.endsWith("?")&&!(u in(t.props||{}))&&r.push(c("missing-prop",`${t.type} is missing the required "${u}"`,{loc:t.loc}))}const n=t.props||{};for(const l of Q)l in n&&L(n[l],t);if(n.action&&R(n.action,t),n.submit&&R(n.submit,t),Array.isArray(n.style)){const l=a.theme||B,g=Object.keys(l.space||{}).length>0;for(const u of n.style)G(u)?g&&U(u,l)===null&&r.push(c("unknown-token",`"${u}": that step isn't in your theme scale`,{loc:t.loc,suggestion:p(u,W),from:u})):r.push(c("unknown-token",`"${u}" is not an accepted style token`,{loc:t.loc,suggestion:p(u,W),from:u}))}t.type===d.When&&n.cond&&b(n.cond,t,s),t.type===d.Each&&n.list&&b(n.list,t,s);const i=[];(t.type===d.Text||t.type===d.Title||t.type===d.Span)&&n.value&&i.push(n.value),t.type===d.Image&&(n.src&&i.push(n.src),n.alt&&i.push(n.alt)),t.type===d.Link&&n.to&&i.push(n.to),n.label&&i.push(n.label);for(const l of i)if(typeof l=="object"&&"kind"in l&&l.kind===h.Interp)for(const g of l.parts)typeof g!="string"&&b(g,t,s);const f=t.type===d.Each&&n.as?new Map([...s,[n.as,K(n.list)]]):s;for(const l of t.children||[])V(l,f)};if(o.rootId?V(o.rootId,new Map):a.kind!=="store"&&r.push(c("no-root","the doc is missing a rootId")),a.kind==="store")for(const[e,s]of Object.entries(o.gets||{})){S(s);for(const t of m(s)){const n=t.split(".")[0];k.has(n)||r.push(c("unknown-ref",`get "${e}": "${n}" is not a state of this store`,{suggestion:p(n,[...k]),from:n}))}}for(const[e,s]of Object.entries(o.actions||{})){const t=new Set(s.mutates||[]),n=i=>{if(i.op===J.If){S(i.cond);for(const f of i.then||[])n(f);for(const f of i.else||[])n(f);return}C.has(i.op)||r.push(c("unknown-op",`action "${e}" uses unknown op "${i.op}"`,{suggestion:p(i.op,[...C]),from:i.op})),"target"in i&&i.target&&!t.has(i.target)&&r.push(c("undeclared-mutation",`action "${e}" mutates "${i.target}" but only declares mutates(${[...t].join(", ")||"\u2205"})`,{suggestion:p(i.target,[...t]),from:i.target})),"arg"in i&&i.arg&&S(i.arg)};for(const i of s.body||[])n(i)}return{ok:r.length===0,diagnostics:r}}export{et as validate};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
class
|
|
1
|
+
class g extends Error{code;loc;constructor(r,n){super(r),this.name="ParseError",this.code="syntax",this.loc=n||null}}function u(t,r,n={}){const{loc:o=null,suggestion:e=null,severity:s="error",from:l=null,related:i=null}=n;return{code:t,severity:s,message:r,loc:o,suggestion:e,fix:l&&e?{from:l,to:e}:null,related:i}}function a(t,r,n=3){let o=null,e=1/0;for(const s of r){const l=c(t,s);l<e&&(e=l,o=s)}return e<=n?o:null}function c(t,r){const n=Array.from({length:r.length+1},(o,e)=>e);for(let o=1;o<=t.length;o++){let e=n[0];n[0]=o;for(let s=1;s<=r.length;s++){const l=n[s];n[s]=t[o-1]===r[s-1]?e:1+Math.min(e,n[s],n[s-1]),e=l}}return n[r.length]}function f(t,r){const n=t.loc?`${r}:${t.loc.line}:${t.loc.col}`:r,o=t.suggestion?` \u2192 did you mean "${t.suggestion}"?`:"";return`${n} ${t.severity} [${t.code}] ${t.message}${o}`}export{g as ParseError,a as closest,u as diag,f as formatDiagnostic};
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import{readFileSync as
|
|
2
|
-
`),
|
|
3
|
-
`);return`import * as __shell from '${
|
|
4
|
-
import { route, injectCss } from '${
|
|
5
|
-
${
|
|
6
|
-
`:""}${
|
|
1
|
+
import{readFileSync as f,existsSync as g}from"node:fs";import{fileURLToPath as P}from"node:url";import{dirname as C,join as l}from"node:path";import{parse as b}from"#engine/lang/parse.js";import{toDoc as D}from"#engine/ir/flatten.js";import{load as M,loadAllParts as E,findStores as H}from"#engine/project/load.js";import{validate as F}from"#engine/ir/validate.js";import{compileModule as k,compileStore as J}from"#engine/compile/compile.js";import{mergeTheme as j}from"#engine/style/tokens.js";import{Nt as $}from"#engine/shared/vocab.js";const O="virtual:muten/runtime",h="virtual:muten/store/",R="virtual:muten/shell",L=C(P(import.meta.url)),U=f(l(L,"runtime.js"),"utf8");function V(y={}){const I=y.store!==!1;let p=j(y.theme),i=process.cwd(),w={},S={};const c={};let m,d=null;const N=()=>{const e=new Set,t=(m?.routes||[]).map(o=>{const r=JSON.stringify("/"+o.url.replace(/^\//,"")),a=`() => import(${JSON.stringify("/src/pages/"+o.page+"/"+o.page+".muten")})`;if(o.guard){const[u,n]=o.guard.split(".");return e.add(u),` ${r}: { load: ${a}, guard: () => ${o.guardNeg?"!":""}__store_${u}.${n}.get(), redirect: ${JSON.stringify(o.redirect)} },`}return` ${r}: { load: ${a} },`}).join(`
|
|
2
|
+
`),s=[...e].map(o=>`import * as __store_${o} from '${h}${o}';`).join(`
|
|
3
|
+
`);return`import * as __shell from '${R}';
|
|
4
|
+
import { route, injectCss } from '${O}';
|
|
5
|
+
${d?`import ${JSON.stringify(d)};
|
|
6
|
+
`:""}${s}
|
|
7
7
|
const routes = {
|
|
8
|
-
${
|
|
8
|
+
${t}
|
|
9
9
|
};
|
|
10
10
|
const root = document.getElementById('app');
|
|
11
11
|
if (root) {
|
|
12
12
|
injectCss(__shell.css);
|
|
13
13
|
const outlet = __shell.mount(root);
|
|
14
14
|
route(outlet, routes);
|
|
15
|
-
}`},
|
|
15
|
+
}`},T=async()=>{w=await E(i);for(const s of Object.keys(c))delete c[s];if(I){S=H(l(i,"src"));for(const[s,o]of Object.entries(S))c[s]={state:Object.keys(o.state||{}),gets:Object.keys(o.gets||{}),actions:Object.keys(o.actions||{})}}const e=l(i,"src","app.muten");m=g(e)?b(f(e,"utf8")):void 0;const t=l(i,"theme.muten");p=g(t)?j(b(f(t,"utf8")).theme||{}):j(y.theme),d=null;for(const s of["styles.css","styles.scss"])if(g(l(i,"src",s))){d="/src/"+s;break}};let v=null;const _=e=>{v&&clearTimeout(v),v=setTimeout(()=>{T().then(()=>{for(const t of e.moduleGraph.idToModuleMap.values()){const s=t.id||"";(s.endsWith(".muten")||s.includes("virtual:muten/shell")||s.includes("virtual:muten/store/"))&&e.moduleGraph.invalidateModule(t)}e.ws.send({type:"full-reload"})})},30)};return{name:"vite-plugin-muten",enforce:"pre",async configResolved(e){i=e.root,await T()},resolveId(e){if(e===O||e===R||e.startsWith(h))return"\0"+e},load(e){if(e==="\0"+O)return U;if(e.startsWith("\0"+h)){const t=S[e.slice(("\0"+h).length)];if(t)return J({state:t.state||{},gets:t.gets||{},actions:t.actions||{},effects:t.effects||[],entities:t.entities||{}},t.mock||{},t.sources||{})}if(e==="\0"+R){const t=m?.shell||{type:$.Shell,props:{},children:[{type:$.Slot,props:{}}]},s=D({...m||{},screen:"shell",entities:{},state:{},actions:{},tree:t});return k(s,{},"",{},{},{stores:c,theme:p})}},async transform(e,t){if(!t.endsWith(".muten"))return null;if(t.replace(/\\/g,"/").endsWith("/src/app.muten"))return{code:N(),map:null};const s=await M(t,w),{ok:o,diagnostics:r}=F(s.doc,{parts:s.partNames,stores:Object.keys(c),theme:p});if(!o)throw new Error("muten: "+r.map(n=>n.message).join(" \xB7 "));const a=[...new Set(Object.values(s.doc.nodes).filter(n=>n.type===$.Custom).map(n=>n.props?.component))],u={};for(const n of a){if(!n)continue;const W=l(i,"src","components",n+".js");g(W)&&(u[n]=f(W,"utf8"))}return{code:k(s.doc,s.data,s.styles.css,u,s.sources,{stores:c,theme:p,api:m?.api||{}}),map:null}},handleHotUpdate(e){if(e.file.endsWith(".muten")||e.file.endsWith(".store"))return _(e.server),[]},configureServer(e){const t=s=>{const o=s.replace(/\\/g,"/");(o.endsWith(".muten")||o.endsWith(".store")||o.endsWith("/styles.css")||o.endsWith("/styles.scss")||o.includes("/components/")&&o.endsWith(".js"))&&_(e)};e.watcher.on("add",t),e.watcher.on("change",t),e.watcher.on("unlink",t),e.middlewares.use((s,o,r)=>{if((s.url||"").split("?")[0]!=="/src/app.muten"){r();return}e.transformRequest("/src/app.muten").then(a=>{if(!a){r();return}o.setHeader("Content-Type","text/javascript"),o.end(a.code)},r)})}}}export{V as default};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# muten.gbnf — GENERATED from @muten/core by scripts/gen-gbnf.mjs. DO NOT EDIT BY HAND.
|
|
2
|
+
# A constrained-decoding grammar for ONE screen/page. Covers screen/state/store/const/get/entity/
|
|
3
|
+
# meta/use/param/action + the node tree + expressions. (app.muten: routes/shell/api/sources/theme/part
|
|
4
|
+
# are a separate file — out of scope for v1.)
|
|
5
|
+
|
|
6
|
+
root ::= ws "screen" sp ident ws decls node ws
|
|
7
|
+
|
|
8
|
+
decls ::= (decl ws)*
|
|
9
|
+
decl ::= use-decl | param-decl | const-decl | entity-decl | state-decl | store-decl | get-decl | meta-decl | action-decl
|
|
10
|
+
|
|
11
|
+
use-decl ::= "use" sp ident (ws "," ws ident)* sp "from" sp string
|
|
12
|
+
param-decl ::= "param" sp ident
|
|
13
|
+
const-decl ::= "const" sp ident ws "=" ws scalar
|
|
14
|
+
entity-decl ::= "entity" sp ident ws "{" ws (field ws)* "}"
|
|
15
|
+
field ::= ident sp ident (ws "|" ws ident)* (sp constraint)*
|
|
16
|
+
constraint ::= "required" | ("min" | "max") ws ":" ws number
|
|
17
|
+
state-decl ::= "state" ws "{" ws (statevar ws)* "}"
|
|
18
|
+
store-decl ::= "store" ws "{" ws (statevar ws)* "}"
|
|
19
|
+
statevar ::= ident ws "=" ws stateinit ws ":" ws type
|
|
20
|
+
stateinit ::= "query" sp ident | value
|
|
21
|
+
type ::= ident ("<" ident ">")?
|
|
22
|
+
get-decl ::= "get" sp ident ws "=" ws expr
|
|
23
|
+
meta-decl ::= "meta" ws "{" ws (ident sp string ws)* "}"
|
|
24
|
+
action-decl ::= "action" sp ident mutates? input? ws actionbody
|
|
25
|
+
mutates ::= sp "mutates" sp ident (ws "," ws ident)*
|
|
26
|
+
input ::= ws "<-" ws ident
|
|
27
|
+
actionbody ::= "{" ws (stmt ws)* "}"
|
|
28
|
+
stmt ::= ifstmt | requeststmt | callstmt
|
|
29
|
+
ifstmt ::= "if" sp expr ws actionbody (ws "else" ws actionbody)?
|
|
30
|
+
requeststmt ::= ("post" | "put" | "delete") sp string (sp "body" sp expr)?
|
|
31
|
+
callstmt ::= ident "." actionop "(" ws callargs ws ")"
|
|
32
|
+
actionop ::= "push" | "remove" | "reset" | "set" | "create" | "update" | "delete" | "refetch"
|
|
33
|
+
callargs ::= refetcharg (ws "," ws refetcharg)* | predarg | expr | ""
|
|
34
|
+
refetcharg ::= ident ws ":" ws expr
|
|
35
|
+
predarg ::= ident ws "=>" ws expr
|
|
36
|
+
|
|
37
|
+
node ::= whennode | eachnode | linknode | actionnode | customnode | plainnode
|
|
38
|
+
whennode ::= "when" sp expr ws block
|
|
39
|
+
eachnode ::= "each" sp expr sp "as" sp ident ws block
|
|
40
|
+
block ::= "{" ws (node ws)* "}"
|
|
41
|
+
linknode ::= "Link" (sp (commonpart | "->" ws path))* (ws block)?
|
|
42
|
+
actionnode ::= ("RowAction" | "Button") (sp (commonpart | actionarrow))* (ws block)?
|
|
43
|
+
actionarrow ::= "->" ws dotted (ws "(" ws expr? ws ")")?
|
|
44
|
+
customnode ::= "Custom" sp ident (sp modifier)*
|
|
45
|
+
plainnode ::= ("Stack" | "Header" | "Nav" | "Sidebar" | "Footer" | "Page" | "Text" | "Title" | "Span" | "Image" | "SearchField" | "DataTable" | "Form" | "slot") (sp commonpart)* (ws block)?
|
|
46
|
+
commonpart ::= string | ref | level | modifier
|
|
47
|
+
level ::= "h" [1-6]
|
|
48
|
+
|
|
49
|
+
modifier ::= stylemod | classmod | bindmod | submitmod | wheremod | columnsmod | altmod | inputsmod | onmod
|
|
50
|
+
stylemod ::= "style" ws "(" ws styletoken (ws "," ws styletoken)* ws ")"
|
|
51
|
+
classmod ::= "class" ws "(" ws classitem (ws "," ws classitem)* ws ")"
|
|
52
|
+
classitem ::= (string | ident) (sp "when" sp expr)?
|
|
53
|
+
bindmod ::= "bind" sp (ref | dotted)
|
|
54
|
+
submitmod ::= "submit" sp dotted
|
|
55
|
+
wheremod ::= "where" ws "(" ws clause (ws "," ws clause)* ws ")"
|
|
56
|
+
clause ::= dotted ws cmpop ws (ref | value)
|
|
57
|
+
columnsmod ::= "columns" ws "(" ws ident (ws "," ws ident)* ws ")"
|
|
58
|
+
altmod ::= "alt" sp string
|
|
59
|
+
inputsmod ::= "inputs" ws "(" ws argpairs ws ")"
|
|
60
|
+
onmod ::= "on" ws "(" ws argpairs ws ")"
|
|
61
|
+
argpairs ::= argpair (ws "," ws argpair)*
|
|
62
|
+
argpair ::= ident ws ":" ws argval
|
|
63
|
+
argval ::= string | number | ref | dotted
|
|
64
|
+
|
|
65
|
+
styletoken ::= (breakpoint ":")? (atom | family "." tokenmod)
|
|
66
|
+
breakpoint ::= "sm" | "md" | "lg" | "xl"
|
|
67
|
+
atom ::= "row" | "column" | "wrap" | "grid" | "grow" | "center" | "between" | "bold" | "italic"
|
|
68
|
+
family ::= "gap" | "padding" | "margin" | "cols" | "rows" | "text" | "weight" | "leading" | "align" | "justify" | "items" | "width" | "height"
|
|
69
|
+
tokenmod ::= ("x." | "y.")? scaleseg
|
|
70
|
+
scaleseg ::= ident | number
|
|
71
|
+
|
|
72
|
+
path ::= ("/" pathseg?)+
|
|
73
|
+
pathseg ::= ident | "{" ws expr ws "}"
|
|
74
|
+
|
|
75
|
+
# expressions — the parser's precedence ladder (no left recursion: iterate, don't self-reference first)
|
|
76
|
+
expr ::= ternary
|
|
77
|
+
ternary ::= orexpr (ws "?" ws ternary ws ":" ws ternary)?
|
|
78
|
+
orexpr ::= andexpr (sp "or" sp andexpr)*
|
|
79
|
+
andexpr ::= cmpexpr (sp "and" sp cmpexpr)*
|
|
80
|
+
cmpexpr ::= addexpr (ws cmpop ws addexpr)*
|
|
81
|
+
cmpop ::= "==" | "!=" | "<=" | ">=" | "<" | ">" | "contains"
|
|
82
|
+
addexpr ::= mulexpr (ws ("+" | "-") ws mulexpr)*
|
|
83
|
+
mulexpr ::= unary (ws ("*" | "/") ws unary)*
|
|
84
|
+
unary ::= ("not" sp)? primary
|
|
85
|
+
primary ::= "(" ws ternary ws ")" | string | number | bool | "null" | callorref
|
|
86
|
+
callorref ::= dotted (ws "(" ws (ternary (ws "," ws ternary)*)? ws ")")?
|
|
87
|
+
|
|
88
|
+
value ::= array | object | scalar
|
|
89
|
+
array ::= "[" ws (value (ws "," ws value)*)? ws "]"
|
|
90
|
+
object ::= "{" ws (objpair (ws "," ws objpair)*)? ws "}"
|
|
91
|
+
objpair ::= (ident | string) ws ":" ws value
|
|
92
|
+
scalar ::= string | number | bool | "null" | ident
|
|
93
|
+
|
|
94
|
+
bool ::= "true" | "false"
|
|
95
|
+
ref ::= "@" ident ("." ident)*
|
|
96
|
+
dotted ::= "$"? ident ("." ident)*
|
|
97
|
+
ident ::= [a-zA-Z_] [a-zA-Z0-9_]*
|
|
98
|
+
number ::= [0-9]+ ("." [0-9]+)?
|
|
99
|
+
string ::= "\"" [^"]* "\""
|
|
100
|
+
|
|
101
|
+
# whitespace — NO comments in the generation grammar on purpose: a weak model degenerates into
|
|
102
|
+
# comment spam (free whitespace) and never closes the page. Humans add comments by hand after.
|
|
103
|
+
ws ::= wschar*
|
|
104
|
+
sp ::= wschar+
|
|
105
|
+
wschar ::= [ \t\r\n]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muten/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "AI-first frontend framework — compiles .muten files to vanilla JS + signals.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"dist",
|
|
26
26
|
"spec",
|
|
27
|
+
"grammar",
|
|
27
28
|
"README.md",
|
|
28
29
|
"LICENSE"
|
|
29
30
|
],
|
|
@@ -41,8 +42,9 @@
|
|
|
41
42
|
"license": "MIT",
|
|
42
43
|
"scripts": {
|
|
43
44
|
"prepare": "npm run build",
|
|
44
|
-
"build": "tsc && node --experimental-strip-types esbuild.ts",
|
|
45
|
-
"
|
|
45
|
+
"build": "tsc && node --experimental-strip-types esbuild.ts && node scripts/gen-gbnf.mjs",
|
|
46
|
+
"gbnf": "node scripts/gen-gbnf.mjs",
|
|
47
|
+
"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/params.ts && node --experimental-strip-types test/ssr.ts && node --experimental-strip-types test/writes.ts && node --experimental-strip-types test/dynamics.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 && node --experimental-strip-types test/print.ts && node --experimental-strip-types test/externs.ts && node --experimental-strip-types test/islands.ts && node test/gbnf.mjs"
|
|
46
48
|
},
|
|
47
49
|
"optionalDependencies": {
|
|
48
50
|
"sass": "^1.101.0"
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
"devDependencies": {
|
|
51
53
|
"@types/node": "^26.0.0",
|
|
52
54
|
"esbuild": "^0.28.1",
|
|
55
|
+
"gbnf": "^0.1.41",
|
|
53
56
|
"typescript": "^6.0.3",
|
|
54
57
|
"vite": "^8.0.16"
|
|
55
58
|
}
|