Output without JavaScript
Carve can produce interactive HTML, static HTML, Markdown, plain text, and ANSI terminal text. Tabs, collapsible sections, and browser-drawn diagrams cannot work identically in every format. This page states what replaces them and which content must remain. The specification calls this behavior graceful degradation; this page uses fallback output.
The principle
When an interactive feature is written to a non-interactive output format, the renderer MUST preserve the construct's content and structure and may drop only its interaction. Authored text - especially a label or title that distinguishes one panel from another - MUST NOT be silently discarded.
A reader of the PDF should be able to tell the tabs apart. A reader of the Markdown export should see every panel's heading. Losing the click is fine; losing the words is not.
Output formats
| Output | Interaction | Browser scripts | Typical use |
|---|---|---|---|
| Interactive HTML | yes | yes (KaTeX, mermaid, tab JS) | docs sites, blogs |
| Static HTML / PDF | no | no (print, weasyprint) | handouts, archives |
| Markdown | no | n/a (host may re-render) | export, interchange |
| Plain text / ANSI | no | n/a | terminals, emails, logs |
Non-interactive output must retain the author's text in a readable form.
Fallback output by feature
The table describes the JavaScript, PHP, and Rust implementations.
| Construct | Interactive HTML | Static / PDF / Markdown / Plain | Status |
|---|---|---|---|
| Tabs / code-group | clickable tabs; [label] is the tab header | each panel shown in sequence, its [label] as a caption heading | see the label rule below |
Disclosure (details) | native <details>/<summary> (interactive without scripts) | native <details open> - kept, not flattened | special case - see below |
| Spoiler | blurred until revealed | revealed | hiding is not retained |
| Mermaid / charts | script-drawn diagram | diagram source preserved (a ```mermaid fence in Markdown); for PDF the extension SHOULD pre-render to SVG/PNG at build time | source never lost; image needs build-time render |
Math ($\...``) | KaTeX / MathJax | source preserved ($...$ in Markdown); for PDF use server-side KaTeX to MathML/HTML | source never lost |
| Footnotes | jump links | print-native footnotes; [^id] preserved in Markdown | content retained |
| Links / autolinks | clickable | clickable in PDF; URL preserved in plain text | destination retained |
| Cross-references / table of contents | anchor links | internal PDF links; anchors preserved in Markdown | destination retained |
| Multiple table header rows | all rows keep header semantics | Markdown: first header row stays the header; later header rows stay as body rows | header semantics weaken; cell content survives |
Composite figure (::: figure) | one <figure> containing the panel figures and group caption | Markdown: panels in order, panel captions in italics, group caption last in bold; plain/terminal: group caption first, followed by each panel | required by Parts 9 and 11 of the specification |
| Frontmatter | omitted from the body | Markdown: emitted first, as --- carrying the format token wherever it is not yaml, content verbatim; plain/terminal: omitted | Markdown is the one target that spells it |
The composite figure's contract is the floor applied with no exceptions: every panel, every panel caption, any stray content between the panels, and the group caption (its number resolved) are content, and no target may silently discard or reorder any of them. Layout hints degrade to nothing - order is the only layout a non-HTML target keeps - but the text never does. Panel letters exist in cross-reference text only ("Figure 2a"); no target invents visible (a) labels the author did not write.
Most constructs already degrade well because their distinguishing text is a title (a quoted "..." node the renderer emits) or source (kept verbatim). The exception is tabs/code-group, whose distinguishing text is a grouping [label].
Disclosure is a special case (not flattened)
A disclosure (
details) renders as a native<details>/<summary>element in all modes; instaticmode it carries theopenattribute. It is not flattened, because<details>is interactive without scripts - browsers collapse and expand it natively. Flattening it to a plain section would only discard the disclosure semantics and its screen-reader affordance for no gain.
This is the line between disclosure and the script-dependent constructs: tabs/code-group/spoiler need a script or stylesheet to be interactive, so they flatten in static; details does not, so it stays itself. The only static requirement is the open attribute - without it a PDF renderer could render the disclosure collapsed and hide the body.
Email and other no-<details> targets. A few email clients ignore the <details> element. They do not lose content - a non-supporting client shows the summary and body as ordinary blocks (it just cannot toggle). If you need a target that has no <details> at all, flatten it as a post-step: rewrite <details open><summary>X</summary>...</details> to <section class="disclosure"><p class="disclosure-title">X</p>...</section> (a few lines of DOM rewriting on the static HTML), or render through the reserved email mode once an implementation defines it. Flattening here is a portability polish, not a correctness fix - the default <details open> already preserves all content everywhere.
The label problem
A ::: fenced div may carry a grouping [label]. By the block rules, core does not assign the label any meaning - only a group extension (tabs, code-group) consumes it, turning each panel's [label] into a clickable tab header. That is correct for interactive HTML.
The hazard is every other target. When the group extension is absent (static HTML, or any build that did not enable it) or cannot apply (Markdown, plain text, ANSI, PDF), a renderer without the fallback drops the label. The panels then stack with no indication of which is which:
:::: tabs
::: tab [Installation]
`composer require markup-carve/carve-php`
:::
::: tab [Usage]
`$converter->convert($carve)`
:::
::::rendered to Markdown without the fallback collapses to two unlabeled code spans - the reader cannot tell "Installation" from "Usage". The authored labels are gone.
How unused labels appear
A renderer that does not consume a fenced div's grouping
[label](because no group extension is active for the target) MUST render the label as a visible caption at the start of that div's content. The caption uses the same slot a quoted title would: an<p class="div-label">in HTML, a bold line in Markdown, and a standalone line in plain text and ANSI. If a block has both a title and a label, the title is rendered first.
This keeps the label visible whether or not the tabs extension is enabled. When the extension is enabled, it uses the label as the tab heading instead.
The same invariant generalizes: any future construct whose meaning lives in an extension-only token (a carousel index, an embed poster, a reveal trigger) MUST define a static caption-or-source fallback, or the renderer MUST surface the token rather than drop it.
Three kinds of fallback
"Not online" is two failure modes, not one, plus a third in the same family:
- Interaction-only - tabs, code-group, details, spoiler. The content is all present; only the UI (click, hover) is gone. Fallback: flatten - show every panel, surface its label, expand disclosures.
- Client-rendered visual - mermaid, charts, math. The content is source that needs a client script to become a visual. Offline you get raw source, not the diagram. Fallback: move rendering to build time - pre-render to an image (Mermaid or Chart.js) or render math during the build; otherwise show the source.
- Embedded - iframes, video, future embed extensions. Online is a live embed; offline is a poster image plus a link.
Class 1 is a cheap markup transform. Classes 2 and 3 need a build-time renderer and must be configured as part of the document build.
How the renderer chooses: the mode signal
The renderer does not guess how the output will be used. The application chooses "interactive" (the default) or "static", together with the output format:
- Output format. Markdown, plain text, and ANSI are inherently static and degrade unconditionally; the label-caption rule and source-fallbacks apply there whatever mode the caller passed.
- Mode (HTML only).
"interactive"renders the live forms;"static"renders through each extension'srenderStaticpath (and the core caption floor for any unconsumed label). See the Extensions Contract for therenderStatichook, the resolution order, and therenderersmap that supplies class-2/3 build-time renderers.
So the build chooses the mode by target, the same way it already chooses a profile:
- Online HTML -
mode: "interactive". Tabs are live, mermaid draws via its script, math via KaTeX. - Static HTML / PDF source / archival -
mode: "static"(with arenderersmap for diagrams/math). Tabs flatten to labeled sections, diagrams become images or source, math is server-rendered or source - self-contained, no client JS required.
There is no implicit detection: a given (format, mode, renderers) triple produces one deterministic result. A host that wants both an interactive site and a print/PDF artifact renders twice with the two modes, or keeps the interactive HTML and adds a print stylesheet (see below). "print", "email", and similar are reserved for future named presets layered on "static".
A PDF workflow
Two supported routes; both rely on the rules above so no authored content is lost:
- HTML to PDF. Render Carve to HTML in
mode: "static"with arenderersmap (so tabs become labeled stacked sections, disclosures expand, mermaid/charts pre-render to SVG/PNG, and math renders server-side). Pass that self-contained HTML to a PDF tool such as WeasyPrint or headless Chromium. Client scripts do not run in that pipeline, so disabling interactive extensions and pre-rendering visuals make the PDF complete. - Markdown to PDF. Render Carve to Markdown (the fallback is automatic), then hand it to a Markdown-to-PDF toolchain (for example pandoc). Diagram and math source survive as fenced blocks for that toolchain to handle.
The alternative to route 1's "disable extensions" step is to render the interactive HTML once and ship a print stylesheet that, under @media print, expands disclosures, shows every tab panel with its label, and reveals spoilers. That keeps a single HTML artifact serving both screen and print, at the cost of a stylesheet that the diagram/math extensions must still cooperate with (their output must be print-visible, i.e. pre-rendered, not script-drawn).
Recommendations for interactive extensions
- Tabs / code-group: rely on the label-caption fallback above; an extension targeting a non-HTML renderer SHOULD emit each panel as a labeled section.
- Mermaid / charts: for PDF and offline HTML, pre-render diagrams to SVG or PNG at build time (client scripts do not run in a print pipeline). For Markdown, keep the source fence - hosts such as GitHub render it.
- Math: for PDF, render server-side (KaTeX to MathML/HTML). Keep
$...$source in Markdown. - Print stylesheet: an HTML-to-PDF pipeline SHOULD ship a print stylesheet that force-expands disclosures, shows all tab panels with their labels, and reveals spoilers.
Lint
Silent loss of an authored label is exactly the class of problem the validator should catch. A grouping [label] (or title) that no active renderer consumes and that is not surfaced as a caption SHOULD be reported by carve lint, the same way broken cross-references and leaked block markers are.