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:

wcl
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"   },
]

§ 1Components

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%

wcl
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.

§ 2Repeating 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%

wcl
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:

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

§ 3Repeaters 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:

§ 3.1Fleet

web-1

Currently at 42%

db-1

Currently at 88%

cache

Currently at 25%

wcl
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 }

§ 4Generating 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:

wcl
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.

§ 5Render 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%

wcl
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.

§ 6Content 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:

§ 6.1Notes

Anything nested in the instance renders at wdoc_content.

wcl
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" }
}

§ 7Partials (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.

wcl
// 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:

wcl
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).

§ 8Content fragments on data (body & project)

Partials gather content by tag. A body does something different: it attaches a chunk of renderable content to a data record as a *property* — without that record being a renderable block — and a project renders it elsewhere by reference. This is the pattern when you generate pages from structured data and want each record to carry its own prose.

Declare your own block type with a @child("body") slot, author the content inside each record, then project it from a repeater. Because body is @by_ref, reading a record as data turns its body slot into a reference, so from = s.overview resolves to *that* record's fragment — and ${…} inside the body resolves against the record:

wcl
@document
type Infra { @children("server") servers: list<Server> }

@block("server")
type Server {
  @inline(0) name: identifier
  region: utf8?
  @child("body") overview: WdocAddressableBody?   // content rides on the record
}

server web01 { region = "us-east"
  body { p $"Frontend in ${region}." }            // NOT a renderable block here
}
server web02 { region = "eu-west"
  body { p $"Frontend in ${region}." }
}

page fleet {
  wdoc_repeater { each = servers  as = :s
    h2 $"${s.name}"
    project { from = s.overview }                 // render THIS record's body
  }
}

Addressing

A single @child("body") slot is addressed by its slot, so the body needs no name. A body in a @children("body") list, or one declared standalone at the document root, is addressed by its @inline(0) name — give each a label (body intro { … }). A direct page-scope project names the full path from a root field (from = servers.web01.overview). The record carrying a body may be nested (a step inside a tutorial) and may use a numeric label — a tstep 1's body resolves via st.body from a nested repeater, or statically via from = tuts.t1.steps.1.body. Note steps.1 matches the step *labelled* 1, not the first element — it's label matching, not positional indexing. A body never renders where it's declared, only where projected; a body that projects itself terminates (the inner self-reference is dropped).

§ 9Tables 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%
wcl
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):

wcl
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}%"] }) }

§ 10Charts from data

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

Utilisationcpumem022446688web-1db-1cache%
wcl
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 }) },
  ]
}

§ 11Connections 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
wcl
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.

§ 12Documenting 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.

§ 13Documenting 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:

wcl
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:

wcl
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.

§ 14The 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.