Data Views

A *data view* renders document content — cards, tables, charts, diagrams — from a WCL data structure rather than hand-authored blocks. It's how you present data gathered about systems: declare the data once, then derive every view from it. The primary tool is a component: a reusable fragment of ordinary wdoc markup with named slots.

Every example draws from one inventory:

let inventory = [
  { name: "web-1", role: "frontend", cpu: 42.0, mem: 60.0, up: true,  status: "note"    },
  { name: "db-1",  role: "database", cpu: 88.0, mem: 75.0, up: true,  status: "warning" },
  { name: "cache", role: "cache",    cpu: 25.0, mem: 40.0, up: false, status: "error"   },
]

Components

Declare a wdoc_component with wdoc_slots and a wdoc_body of ordinary markup. Reference slots in any {slot}…" interpolated string or as a bare identifier in a field (class = [status]). A slot with a default is optional. Instantiate the component by its own name:

CPU

Currently at 42%

Memory

Currently at 88%

wdoc_component dv_metric {
  wdoc_slot label
  wdoc_slot value
  wdoc_slot status { default = "note" }
  wdoc_body {
    callout $"${label}" { class = [status]  body = $"Currently at **${value}%**" }
  }
}

// ... then, anywhere a block is allowed:
dv_metric { label = "CPU" value = 42 status = "warning" }
dv_metric { label = "Memory" value = 88 }    // status defaults to "note"

Interpolating slots into text

Slot values land in text via WCL's $"…" interpolated strings — note the $ prefix. A plain "…" string is literal. Bare references in a field (like class = [status]) need no prefix.

Repeating over data

wdoc_repeater renders its body once per element of each, binding the element to the symbol named by as. Combined with a component, it stamps one card per data row:

web-1

Currently at 42%

db-1

Currently at 88%

cache

Currently at 25%

wdoc_repeater { each = inventory  as = :h
  dv_metric { label = h.name  value = h.cpu  status = h.status }
}

A repeater needs no component — its body is just markup with the loop variable in scope:

wdoc_repeater { each = inventory  as = :h
  h3 $"${h.name}"
  p  $"CPU ${h.cpu}% · Mem ${h.mem}%"
}

Repeaters inside components

A slot can hold a whole list, and a wdoc_repeater inside the body can iterate it. Bindings stack, so the loop body sees both the loop variable and the component's other slots:

Fleet

web-1

Currently at 42%

db-1

Currently at 88%

cache

Currently at 25%

wdoc_component dv_hosts {
  wdoc_slot heading
  wdoc_slot rows                       // a list-typed slot
  wdoc_body {
    h3 $"${heading}"
    wdoc_repeater { each = rows  as = :h
      dv_metric { label = h.name  value = h.cpu  status = h.status }
    }
  }
}

dv_hosts { heading = "Fleet"  rows = inventory }

Generating pages and navigation

A wdoc_repeater is the single iteration concept at every level. At the document root, give it a page block as its body and it emits one rendered page per element — the page's interpolated label becomes the route (<name>.html). Inside a toc (or a chapter), give it a chapter block and it emits one navigation entry per element. So a whole catalogue site — one page per service, table, or endpoint — comes from data, with no hand-authored page or chapter boilerplate:

let containers = [
  { id: "web", name: "Web App" },
  { id: "api", name: "API" },
  { id: "db",  name: "Database" },
]

// One rendered page per element. The interpolated label is the route.
wdoc_repeater { each = containers  as = :c
  page $"cont_${c.id}" {
    sites = [:docbook]
    title = c.name
    h1 $"${c.name}"
    p  $"See the [Web App](cont_web) container."   // links resolve against
  }                                                // the generated page set
}

site docbook {
  default_template = :book
  toc {
    chapter "Containers" {
      // One TOC entry per element, linking to the generated page.
      wdoc_repeater { each = containers  as = :c
        chapter $"${c.name}" { page = $"cont_${c.id}" }
      }
    }
  }
}

Routes must be slug-safe and unique

A generated route is its interpolated label, so it must be non-empty, contain only the characters A-Za-z0-9_-, and be unique within its site — colliding or space-bearing routes are a build error. Build a slug from prose with to_lower(replace(s, " ", "-")). A cross-page link whose target is computed from the same data resolves automatically, because the generated pages exist before links are checked.

Render by reference

Instantiating a component writes its name as a block, so the *choice* of component is fixed in the source. A wdoc_instance instead renders the component named by the value of its component field — so a repeater can emit a *different* component per element. The instance's like-named fields fill the target's slots (falling back to each slot's default):

CPU

Currently at 42%

Memory

Currently at 91%

let widgets = [
  { kind: "dv_metric", label: "CPU",    value: 42, status: "ok" },
  { kind: "dv_metric", label: "Memory", value: 91, status: "warning" },
]
wdoc_repeater { each = widgets  as = :row
  // `component` is data, so each element picks its own component.
  wdoc_instance { component = row.kind  label = row.label  value = row.value  status = row.status }
}

A repeater works in any container

wdoc_repeater / wdoc_instance / component instances expand wherever a container iterates its children — a page, a diagram, a wf_* wireframe frame, a node_table, or the CSS-collection pass. A repeater inside a device frame composes its widgets from data; a repeater that emits class blocks generates stylesheet rules from data. Whatever a generator's body contains is what gets spliced in.

Content slots (layout wrappers)

A wdoc_content block in a component body marks where the instance's *own* nested blocks render — so a component can frame arbitrary content:

Notes

Anything nested in the instance renders at wdoc_content.

wdoc_component dv_panel {
  wdoc_slot title
  wdoc_body {
    h3 $"${title}"
    wdoc_content          // the caller's nested blocks render here
  }
}

dv_panel { title = "Notes"
  p "Anything nested in the instance renders at wdoc_content."
  list { li "including lists" li "and more" }
}

Partials (scatter & collect)

Components and repeaters compose content at one spot. A partial does the opposite: it lets you *scatter* tagged content throughout a document — even across imported files — and have one place gather it. It's the appendix / glossary / collected-sidebars pattern.

A partial tags a body of blocks; a collect with the same tag gathers every matching partial — across the whole document and its imported files — and renders their bodies, in document order, at the collect site. A partial is invisible where it's defined unless you set show_here = true.

Some prose lives between the deposits…

…and the collect below gathers both of the dv_aside deposits above, in order:

From section one

A point worth collecting later.

From section two

Another collected point.

// Scatter tagged deposits anywhere — different blocks, even imported files:
partial aside { callout "From section one" { body = "A point to collect later." } }
// ... prose, other blocks ...
partial aside { callout "From section two" { body = "Another point." } }

// Gather every `aside` partial here, in document order:
collect aside

Set show_here = true to render a partial both where it's defined *and* where it's collected:

partial aside { show_here = true  p "Pinned in place and also collected." }

Scope & limits

Collection is document-global: a collect gathers matching partials from the root document and every file pulled in by a top-level import — so tag deposits distinctly to avoid cross-page bleed. Partials in block-scoped (lazily imported) files aren't reached, nested partials inside a partial body aren't separately collected, and a collected body should avoid ids (per-page id checks run before collection). A collect whose collected content contains another collect of the same tag is a no-op (the cycle is broken).

Tables from data

Set a table's rows to a list of cell-lists — mapped from your data — with an optional header row. utf8 cells run through the inline-pattern engine (so bold, :icons:, links work); other scalars stringify:

HostRoleCPU %
web-1frontend42%
db-1database88%
cachecache25%
table {
  header = ["Host", "Role", "CPU %"]
  rows = map(inventory, fn(h: DvHost) -> list<utf8> { [h.name, h.role, $"${h.cpu}%"] })
}

Wrap a table in a component to reuse the layout, feeding the rows through a slot. Name the slot apart from the table's own rows field (a field that references its own name is a cycle, and renders nothing):

wdoc_component host_table {
  wdoc_slot data
  wdoc_body { table { header = ["Host", "Role", "CPU %"]  rows = data } }
}

host_table { data = map(inventory, fn(h: DvHost) -> list<utf8> { [h.name, h.role, $"${h.cpu}%"] }) }

Charts from data

Charts read their data from value fields, so you map the inventory straight into them — no wrapper needed:

Utilisationcpumem022446688web-1db-1cache%
bar_chart {
  categories = map(inventory, fn(h: DvHost) -> utf8 { h.name })
  series = [
    { name: "cpu", values: map(inventory, fn(h: DvHost) -> f64 { h.cpu }) },
    { name: "mem", values: map(inventory, fn(h: DvHost) -> f64 { h.mem }) },
  ]
}

Connections from data

A wdoc_repeater inside a diagram generates one shape per data element; give each a data-derived id. Then set the diagram's edges to a computed list — mapped from the data's own relationships — and layout = :layered (or :force) positions the graph automatically. Connect by id: each edge's source/destination names a node's id.

WebAPIDBCache
type DvSvc { key: utf8  name: utf8  deps: list<utf8> }

let services = [
  { key: "web",   name: "Web",   deps: ["api"] },
  { key: "api",   name: "API",   deps: ["db", "cache"] },
  { key: "db",    name: "DB",    deps: [] },
  { key: "cache", name: "Cache", deps: [] },
]

// One edge per (service → dependency) pair, derived from the data.
let service_links = flatten(map(services, fn(s: DvSvc) -> list<Edge> {
  map(s.deps, fn(d: utf8) -> Edge { { source: s.key, destination: d } })
}))

diagram { width = 460  height = 300  layout = :layered
  wdoc_repeater { each = services  as = :s
    process $"${s.name}" { id = s.key }   // node label from data, id = key
  }
  edges = service_links
}

Name the binding apart from `edges`

Assign the computed edges from a differently-named binding (edges = service_links), not edges = edges — a field that references its own name is a cycle and renders nothing.

Documenting types

The built-in type_table component documents a schema type by reflecting it — no hand-maintained field lists. type_table { type = Image } renders a table of the type's properties (name, type, whether it's required, and a description), including fields inherited via extends; a second "Child blocks" table lists its @child / @children slots. It's built on the type_fields reflection builtin.

Descriptions and visibility are authored on the schema itself:

DecoratorEffect
@doc("…")Sets the description shown for the field.
@hiddenDrops the field from the generated table.

Function-typed fields (every block's lower hook) are skipped automatically. This very page's Pages and Images field tables are generated this way.

Documenting your own schema

type_table reflects *any* type, so it documents the blocks you declare just as well as wdoc's built-ins. Declare your own root @document alongside import <wdoc.wcl> (the two schemas merge), give each block type a @block("…") kind and @doc descriptions, then reflect them:

import <wdoc.wcl>

@document
type MyDoc {
  @children("project_meta") metas: list<ProjectMeta>
  @child("settings") settings: Settings
}
@block("project_meta") type ProjectMeta { @inline(0) id: identifier  @doc("the owner") owner: utf8 }
@block("settings")     type Settings     { @doc("UI theme") theme: utf8 }

page reference {
  h2 "Top-level blocks"
  block_reference { type = MyDoc }    // one heading + property table per block
}

block_reference { type = MyDoc } walks the document's @child / @children slots and emits an h3 (the block's @block kind) plus a type_table for each — no hand-maintained list, and the reference can't drift from the schema. It's a thin wrapper over the child_types reflection builtin, which returns the element type references of a type's block slots. Drop to the repeater directly when you want a different layout:

wdoc_repeater { each = child_types(MyDoc)  as = :b
  type_table { type = b }
}

Union and interface slots

child_types resolves a @child / @children slot to its declared element type. A slot that accepts a union or interface resolves to that type's name; since type_table documents a single concrete type / interface, such slots aren't expanded into a table per variant — document those members individually.

The pattern

ToolUse it for
wdoc_componentA reusable fragment of wdoc markup with slots — cards, panels, sections. The everyday tool.
wdoc_repeaterRendering a body (or a component) once per element of a data list.
wdoc_contentA component that wraps arbitrary caller-provided content.
partial + collectScattering tagged content across a document (or imported files) and gathering it by tag at one site — appendices, glossaries, collected sidebars.
table computed rowsA data-driven <table>table { header = […] rows = map(data, …) }.
wdoc_repeater + computed edgesA data-driven diagram — repeater-generated nodes (id from data) wired by a computed edges list, auto-positioned by :layered/:force.
Chart value fieldsFeeding mapped data straight into bar_chart / line_chart / pie_chart.
type_tableAuto-documenting a schema type's fields — reflected via type_fields, described with @doc / @hidden.
block_referenceAuto-documenting every top-level block your @document declares — a heading + type_table per block, via child_types.