RuledWDL

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<<footer

Layers 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 entries

REGISTRY

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 classes

The 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: &quot;name&quot;. 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 &lt;style&gt;; 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.