wdoc · explanation
Visibility
One document renders to three outputs: a website, a PDF and a folder of Markdown. It can also declare several sites, and lay each one out with a different template. A block that belongs in one of those and not in another says so with a decorator, rather than with a duplicate document. @only includes it. @except excludes it.
This chapter covers both decorators, their three axes, the rule that joins the axes, and the one place where visibility is not a preference but a requirement: waiving a native block on a target that cannot render it.
Two decorators, three axes
@only and @except attach to any block instance. Each takes up to three optional list<symbol> arguments:
@only(sites = [:handbook], templates = [:book], backends = [:html])
callout "Browse the search index" { class = ["tip"] body = "Type in the box." }
@except(backends = [:pdf])
video "assets/tour.mp4" { }
| Axis | Values | The current value is |
|---|---|---|
| sites | the label on a site block | the name of the site being rendered |
| templates | any template name. See below | the site's default_template symbol |
| backends | :html, :pdf, :markdown | the output target this build produces |
Three backends, not four. The :skill target left with the skill folder generator, so the axis names exactly the three outputs wcl wdoc still produces. wcl wdoc serve is an HTML build, so it matches :html.
A block with neither decorator renders everywhere. That is the default and it is what almost every block should be.
The rule
Two sentences decide every case, and they are worth reading closely because they are not symmetric.
- Within an axis, the values are OR'd. backends = [:pdf, :markdown] matches a PDF build or a Markdown build.
- Across axes, they are AND'd. @only(sites = [:docs], backends = [:html]) matches only when the site is docs and the target is HTML.
An axis you do not write does not constrain. @except(backends = [:pdf]) says nothing about sites, so it applies in every site.
The two decorators then combine like this: a block renders when @only is absent or matches, and @except is absent or does not fully match.
render = (no @only or @only matches)
and (no @except or @except does NOT match)
Write both on one block and @except wins where they overlap, because it is a veto. @only(backends = [:html, :pdf]) with @except(sites = [:internal]) means HTML and PDF, but never in the internal site.
An axis whose value is unknown never matches
This is the one asymmetry to remember. A constrained axis with no current value fails to match rather than matching vacuously.
It shows up in two places. A document with one unnamed site has no site name, so @only(sites = [:anything]) hides the block everywhere. And a site that declares no default_template has no template kind, so @only(templates = [:book]) hides the block there too.
The effect differs by decorator, and the difference follows from the rule rather than from a special case. On an @only an unknown axis hides the block, because @only failed to match. On an @except it shows the block, because @except failed to match either.
The backends axis
This is the axis you reach for most. Save this two-page document:
import <wdoc.wcl>
site docs {
default_template = :book
title = "Docs"
toc { chapter "Notes" { page = notes } }
}
page notes {
start = true
h1 "Notes"
@only(backends = [:html])
p "HTML-ONLY"
@only(backends = [:markdown])
p "MARKDOWN-ONLY"
@except(backends = [:markdown, :pdf])
p "NOT-MD-NOT-PDF"
}
Build it twice and read the two outputs. The Markdown target keeps one paragraph:
$ wcl wdoc build main.wcl --out _md --type markdown
wrote 1 page
$ cat _md/notes.md
# Notes
MARKDOWN-ONLY
The HTML target keeps the other two:
$ wcl wdoc build main.wcl --out _site
wrote 1 page
$ grep -o 'HTML-ONLY\|MARKDOWN-ONLY\|NOT-MD-NOT-PDF' _site/notes.html
HTML-ONLY
NOT-MD-NOT-PDF
Look at the third paragraph. @except(backends = [:markdown, :pdf]) and @only(backends = [:html]) select the same set today, because there are exactly three targets. They do not say the same thing, and a fourth target would separate them. Write @only when the block is for that target, and @except when the block is wrong on that one.
Reach for @except when the reason is a limitation
A live search box, a pan-and-zoom control, an embedded player: each is an HTML idea. Give each one @except naming the target it fails on, rather than @only(backends = [:html]). The @except form keeps working when the block starts rendering somewhere new. The @only form silently keeps hiding it.
The sites axis
In a multi-site document, sites scopes a block to some of them. The values are site block labels:
site docs { default_template = :book title = "Docs" }
site blog { default_template = :webpage title = "Blog" root = true }
page notes {
sites = [:docs, :blog]
h1 "Notes"
p "This paragraph is in both sites."
@only(sites = [:docs])
callout "Internal detail" { class = ["note"] body = "Only the docs site shows this." }
}
There are two ways to keep content out of a site, and they work at different scales. A page names its sites with the ordinary sites field. A page belongs to sites, so that is data. @only(sites = …) scopes one block inside a page that several sites share. Reach for the field when the whole page is site-specific and for the decorator when one paragraph is.
The styling blocks (class, base, media, keyframes, font_face) carry their own sites field for the same reason. Styling rules covers that one.
The templates axis
templates scopes a block to the layout it renders into. Use it for content that makes sense only in one shape: a "press Space for the next slide" note in a deck, a "use the sidebar" note in a book.
The value is a template name, not a fixed vocabulary. Four templates ship: :webpage, :book, :presentation and :website. You name a template block of your own here the same way. Templates and layouts covers writing one.
The axis reads the site, not the page
The current template kind is the site's default_template. A page that overrides its own layout with template = :book inside a :webpage site still matches templates = [:webpage], because the axis never looks at the page. Check that before you conclude a decorator is broken. Scope by sites when you meant the site, and by templates when you meant every site that uses one layout.
Waiving a native block on a target that cannot render it
Everything above is preference. This section is the one case where the backends axis is load-bearing, and it is why the axis exists at all.
Some blocks are native: wdoc renders them in Rust rather than through a WCL lower function, because their output is not expressible in WCL. Writing your own blocks covers what that means. A native block declares which targets implement it, and not every one covers all three.
file is the clear example. It ships a file into the output tree and optionally links to it. A PDF is one self-contained document: there is no output folder beside it to copy into, so a rendered link would point at something that was never shipped. file therefore declares :html and :markdown and stops there.
Put one on a page and build to PDF, and the build refuses:
$ wcl wdoc build main.wcl --out _pdf --type pdf
wcl::eval::user_error
× error: `file` has no :pdf implementation (it is native
│ on :html, :markdown); remove the block or waive it here with
│ `@except(backends = [:pdf])`
╭─[main.wcl:12:3]
11 │ h1 "Downloads"
12 │ file "notes.txt" { as = "the notes" }
· ──────────────────┬──────────────────
· ╰── error raised here
13 │ }
╰────
It refuses rather than rendering nothing, and that is the whole design. A block that silently vanished from one of your three outputs is a bug you find months later, from a reader.
The message names the fix. Add the waiver to that instance:
page dl {
h1 "Downloads"
@except(backends = [:pdf])
file "notes.txt" { as = "the notes" }
}
$ wcl wdoc build main.wcl --out _pdf --type pdf
wrote 1 pdf
$ wcl wdoc build main.wcl --out _md --type markdown
wrote 1 page
$ cat _md/dl.md
# Downloads
[the notes](_wdoc/notes.txt)
Capability says can't, intent says don't want to
The block's declaration states what the renderers can do. @except states what you want. The build refuses until the two agree. That refusal turns a silent hole in one output into an author's decision recorded in the source. The waiver is per instance on purpose: the next file block on the next page makes the same decision for itself.
Two targets have to cover the block
The build checks a block against two backends, because two are involved. One is the target the build is producing. The other is the renderer actually running, and those are not always the same: a card in a diagram draws its body as HTML in whichever target embeds the SVG.
So a file inside a card must not reach a PDF just because the card body happens to render as HTML. And markdown_source, which taps the Markdown emitter from inside the HTML build, is a rendering question rather than an output one. On an ordinary page the two are the same backend, and the check never surprises you. When they differ, the error says so.
Where a decorator goes
@only and @except sit on the line above the block instance they apply to, exactly like every other decorator:
@except(backends = [:pdf])
video "assets/tour.mp4" { poster = "assets/tour.jpg" }
They apply to one instance, and they take its whole subtree with it. Hide a callout and its body goes. Hide a diagram and every shape inside it goes. There is no way to hide a field, and no way to hide a page. A page belongs to sites through its sites field, and a page you do not want built is a page you do not declare.
One decorator of each kind per block, and that is a rule rather than a convention. A second @except on one instance is a schema violation:
$ wcl wdoc build main.wcl --out _site
wcl::eval::schema_violation
× decorator '@except' may appear at most once on one node
1 schema violation
So put every axis in the one decorator, which is also where the AND rule can read them together.
Where to go next
- Output targets. The three targets the backends axis names, and what each one can carry.
- Documents, pages and sites. The site block, the sites field on a page, and multi-site output.
- Writing your own blocks. What makes a block native, and how a block declares the targets it covers.
- Styling rules. The parallel sites field the styling blocks carry.
- Decorators. How a decorator is declared and read back, in the language rather than in wdoc.