DocLang Strict Mode Syntax
DocLang has two dialects. This is the declared one: you write the structure instead of letting the parser infer it, and the normalizer never runs. What you read is what gets parsed — which is what makes a strict document reviewable in a pull request and meaningful to sign off on.
Use flex to draft. Use strict for the version that gets committed.
Opting in
Section titled “Opting in”Set mode: strict in the frontmatter. There is no CLI flag — the dialect
belongs to the document, so nothing on the command line can contradict it.
---mode: stricttitle: "Retention Policy"---
SECTION "Overview"
TEXT Every block is declared. Nothing is inferred.Sections
Section titled “Sections”A document is a sequence of SECTION blocks. The title is part of the opening
line and must be quoted:
SECTION "Introduction"Properties go on indented lines below the opening line — never inline.
SECTION "Intro" level: 2 is an error.
| Property | Accepted on | Meaning |
|---|---|---|
level: |
any section | 1–6. Defaults to 1. |
id: |
levels 2–6 only | Pins the anchor instead of deriving it from the title. |
level: declares the hierarchy — indentation does not. Sections are never
nested syntactically; an indented SECTION is an error, not a subsection.
SECTION "Guide"
TEXT Intro paragraph.
SECTION "Installation" level: 2 id: install
TEXT Steps go here.A level-1 section becomes a document section. Levels 2–6 become headings
inside the section above them — the same structure # and ## produce in
flex, which is why both dialects render identically.
A level-2 section with no level-1 section before it is an error: there is nothing to nest it under.
How id: is normalized
Section titled “How id: is normalized”id: is lowercased, spaces become hyphens, and the result is narrowed to
[a-z0-9_-]. id: Install_Steps becomes install_steps. That normalized form
is canonical, so doclang fmt emits it. An id: with no surviving characters
(only emoji, say) is an error rather than an empty anchor.
doclang fmt omits id: when it would be derivable from the title, and keeps
it when it would not.
Elements
Section titled “Elements”Inside a section body, indented two spaces, the element vocabulary is the same as SlideLang strict — see the SlideLang strict reference for the full syntax of each:
TEXT, POINTS, CODE <lang>, IMAGE "src" "alt", TABLE, QUOTE,
CHECKLIST, :::info / :::warning / :::danger / :::success,
:::code-group, <<mermaid>>, <<plantuml>>, <<chart: type>>, <<map>>,
<<grid>>, <<math>>, @directives.
The one thing to unlearn: there is no SLIDE, and the slide-only properties
(title:, heading:, subtitle:, logo:) are errors inside a SECTION. A
section’s title is the quoted string on its opening line.
Nothing is dropped silently
Section titled “Nothing is dropped silently”A line the parser does not recognize — a mistyped keyword, an invented marker — is an error, not a silent omission:
SECTION "A"
TEXXT this paragraph would have vanishedIn flex, stray content like this is tolerated and quietly ignored. In strict it fails the build, and every unrecognized line is named individually so you can see exactly what did not make it into the document.
What strict unlocks
Section titled “What strict unlocks”label: on tables and figures is only reachable in the strict dialect, so
cross-references work:
SECTION "Results"
TABLE headers: ["Metric", "Value"] rows: [ ["Throughput", "1200 rps"] ] caption: "Measured throughput" label: tbl-throughput
TEXT See \ref{tbl-throughput} for the numbers.This is the main functional reason to pick strict over flex, beyond auditability: in flex, a table cannot be labelled at all.
@include
Section titled “@include”@include expands textually, before the parser runs. In strict — where
indentation is syntax — that means the fragment’s own indentation decides
where it lands, not the directive’s:
- A fragment of complete sections is written at column 0.
- A fragment meant for a section body is written already indented.
- Indenting the
@includeline itself does not indent the fragment.
Formatting
Section titled “Formatting”# Keeps the document's own dialectdoclang fmt doc.doclang
# Promotes a flex draft to strict (transpiles)doclang fmt draft.doclang --strict --write--strict on a flex document is a transpilation, not a reformat: it
rewrites the document into the other dialect. It is not the default — unlike
slidelang fmt, where strict is the only canonical form — because DocLang’s two
dialects are both legitimate, and transpiling unasked would rewrite every
existing flex document.
Downgrading strict → flex is refused: it would discard the author’s declaration
with no way back. Edit mode: by hand if you really want that.
One known lossiness, shared with the flex formatter: inline formatting in a
section title (SECTION "The **important** part") degrades to plain text on a
round trip. The stored heading is not invertible — a hand-typed <strong>
produces the same result as **bold**. The output is stable and re-parses
clean; it just loses the emphasis. Element bodies are unaffected.
Not part of the grammar
Section titled “Not part of the grammar”Early design sketches of this dialect described constructs that no parser implements. They will not parse; do not use them:
| Sketched | Reality |
|---|---|
SECTION 1 "Title" (numbered) |
The title is the only argument: SECTION "Title" |
SUBSECTION "Title" |
Use SECTION "Title" with level: 2 |
numbered:, pagebreak: |
Not implemented by any renderer |
doctype:, output:, page: |
Not part of the parsed frontmatter schema |
<<toc>> |
Use the --toc CLI flag |
<<ref: id>> |
Use label: + \ref{label} (see above) |
<<footnote>>, <<bibliography>> |
Not implemented |
See Frontmatter for the keys that actually do something.
Migrating from flex
Section titled “Migrating from flex”| Flex | Strict |
|---|---|
# Section |
SECTION "Section" |
## Subsection |
SECTION "Subsection" + level: 2 |
| Text paragraph | TEXT block |
- item |
POINTS block |
```lang fenced block |
CODE lang block |
Or let the formatter do it: doclang fmt draft.doclang --strict --write.