Docs
Language reference
Core surface of @ruledwdl/core: REGISTRY, COMPONENTS, DATA, Layers syntax, binding, layouts, and reserved keys.
Page model
A WDL page is JSON with three keys. The host-agnostic renderer turns that JSON into HTML given an injected store.
Shape
{
"REGISTRY": { /* semantic_id → attribute tokens / },
"COMPONENTS": [ / layers trees or component refs / ],
"DATA": { / content + reserved keys */ }
}COMPONENTS & Layers
Each COMPONENTS entry is either a layers tree or a component reference. Layers nodes use tag.semantic_id (exactly one semantic id per node). Child >, sibling +, de-indent < (and <<), numeric repeat *N, and data loops *items.
Forms
// Layers tree
{ "layers": "section>div.hero>h1.title+p.lead",
"attr": {
".title": { "text": "Hello ${name}" },
".lead": { "text": "${tagline}" }
}
}
// Stored component (resolved via store.getComponent)
{ "component": "card",
"data_overrides": { "title": "…" },
"style_overrides": { ".card": { "class": "…" } }
}
// De-indent: footer is sibling of main under shell
// div.shell>main>article>h1<<footerLayers restrictions
Parse throws with a hint on unsupported syntax. Prefer REGISTRY or attr.class for extra CSS classes — never multiple dots on one node.
Not allowed
.class1.class2 // multiple semantic ids — use REGISTRY / attr.class
{ } // inline text — use attr.text or DATA
[ ] // inline attributes — use the attr object
^ // climb-up — use < instead
( ) // grouping — split into multiple COMPONENTS entriesREGISTRY
Keys are bare class names only. Values are attribute objects merged onto every element with that class. Wrong keys are silently ignored.
Tokens
"REGISTRY": {
"site-header": {
"class": "bg-white border-b px-6 py-4 flex items-center"
}
}
// ✓ "site-header"
// ✗ "header.site-header" — never matches
// ✗ "header" — only bare <header> with no classesThe attr object
Keyed by .semantic_id (preferred) or tag name. No allowlist — any HTML attribute passes through. Special keys: class, text (inline Markdown → safe HTML; raw for script/style), attr-ref, alpine, htmx.
Matchers
attr: {
".project-title": { "text": "${title}" }, // prefer .class
"meta": { } // applies to ALL <meta>
}
// Same-tag siblings — use distinct semantic ids
{ "layers": "meta.charset+meta.viewport",
"attr": {
".charset": { "charset": "UTF-8" },
".viewport": { "name": "viewport",
"content": "width=device-width,initial-scale=1.0" }
}
}DATA binding & loops
${key} and ${nested.path} bind values. Loops use *loopKey in layers; inside the loop, ${field} resolves per item (not ${item.field}). Loop arrays must be arrays of objects. ${_index} is the zero-based clone index.
Binding
DATA: {
"name": "World",
"tags": [
{ "label": "Workers" },
{ "label": "Edge" }
]
}
// layers: "span.tag*tags"
// attr: { ".tag": { "text": "${label}" } }
// ✗ "tags": ["Workers", "Edge"] — primitives break field binding
// ✓ wrap strings: [{ "item": "Workers" }] → ${item}Layouts & fullPage
Page may set layout: "name". Layouts chain with extends. Slot injection is literal {{content}} (not ${}). fullPage: true skips the default html/head/Tailwind shell — only when your layout is a complete document.
Slot
// Layout COMPONENTS
{ "layers": "div.shell",
"attr": { ".shell": { "text": "{{content}}" } }
}
// Page
{ "layout": "base", "COMPONENTS": [ … ], "DATA": { … } }Reserved DATA keys
Processed before render — not available as ordinary ${var} bindings. __head, __design_tokens, and __brand_tokens collect across the layout chain (page appends). Tokens emit as <style>; brand wins over design. Skipped when fullPage: true.
Keys
__seo object // SEO meta for wrapPage __head string[] // raw head tags __design_tokens string|string[] // CSS custom properties __brand_tokens string|string[] // same, emitted after design tokens // Head emission order: // __design_tokens → __brand_tokens → __head → opts.headInject
Try it
Paste samples into the home playground, or read the full specification in the repository.