Getting Started
mdbook-structured is an external preprocessor for mdBook. Listed JSON and
YAML chapters become structured HTML pages while mdBook retains its normal
book structure, navigation, themes, search, and stock HTML renderer.
Prerequisites
Install Cargo and mdBook 0.5.x, and start with a book containing a
SUMMARY.md.
Install
cargo install mdbook-structured
Configure
Add both preprocessors to your book’s book.toml:
[preprocessor.structured]
command = "mdbook-structured render"
after = ["index"]
before = ["links"]
renderers = ["html"]
max-input-bytes = 1048576
max-nodes = 10000
max-depth = 64
large-container-threshold = 100
[preprocessor.structured-links]
command = "mdbook-structured rewrite-links"
after = ["links"]
renderers = ["html"]
Install assets and build
From your book root:
cd path/to/book
mdbook-structured install .
mdbook build
mdbook serve
The installer writes starter CSS and JavaScript only; it never edits
book.toml. Append the generated paths to additional-css and
additional-js as directed by the command.
Add structured chapters
List a source file in SUMMARY.md and link to that source path, for example
[Runtime configuration](config/runtime.yaml). mdBook then publishes an
extension-preserving page such as config/runtime.yaml.html.
Only chapters listed in SUMMARY.md are transformed. Unlisted files remain
ordinary files, and mdbook test is not a structured-book acceptance command
in v1 because it uses a different renderer.
Further reading
Configuration
Register the renderer and link rewriter in book.toml:
[preprocessor.structured]
command = "mdbook-structured render"
after = ["index"]
before = ["links"]
renderers = ["html"]
max-input-bytes = 1048576
max-nodes = 10000
max-depth = 64
large-container-threshold = 100
[preprocessor.structured-links]
command = "mdbook-structured rewrite-links"
after = ["links"]
renderers = ["html"]
The limits are deliberately explicit:
| Option | Default |
|---|---|
max-input-bytes | 1048576 |
max-nodes | 10000 |
max-depth | 64 |
large-container-threshold | 100 |
render runs after mdBook’s index preprocessor and before links.
rewrite-links runs after links; both commands are restricted to html.
See configuration and operations, routes,
and link rewriting for the normative contracts.
Assets
Run mdbook-structured install . in the book root. Add the printed paths to
output.html.additional-css and output.html.additional-js; existing arrays
are preserved. The installer never edits book.toml.
What changes at build time
Listed .json, .yaml, and .yml chapters are transformed into structured
HTML. Other chapters and unlisted source files are copied by mdBook unchanged.
The source remains authoritative on disk.
Diagnostics and troubleshooting
The preprocessor fails fast on malformed protocol input, parser errors, duplicate keys, route collisions, invalid options, and resource-limit violations. Diagnostics include the source filename and location when known.
If a page is missing, verify that its source appears in SUMMARY.md, the
renderer is html, and both registrations are present. If links do not point
to .json.html or .yaml.html pages, keep the rewriter after links and link
to the source path. For unsupported mdbook test behavior, use mdbook build
with the stock HTML renderer instead.
Authoring
Structured pages are selected by SUMMARY.md. Entries can point directly to
JSON or YAML sources:
- [Runtime YAML](config/runtime.yaml)
- [Runtime JSON](config/runtime.json)
Links in ordinary Markdown should use those source paths. The generated pages
preserve the extension, for example config/runtime.yaml.html and
config/runtime.json.html.
README.yaml and README.json are distinct routes, as are
index.yaml.html and index.json.html. Exact source-path links always win.
Convenience README/index/directory aliases are created only when unique; an
ambiguous alias is an authored error and fails the build rather than choosing
silently.
See the formal authoring contract, route rules, and chapter link rewriting chapters for complete precedence and collision behavior.
Maintainer Guide
Local checks
Run the same checks required by CI before merging or tagging:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --locked
cargo doc --workspace --all-features --locked --no-deps
cargo package --locked -p mdbook-structured-core
cargo package --locked -p mdbook-structured
npm ci
npx playwright install --with-deps chromium
npm run test:browser
mdbook build .
Versions and releases
Both crates share the workspace version and the CLI’s registry dependency on
mdbook-structured-core must match it. From green main, create an annotated
vX.Y.Z tag. Versions on crates.io are immutable.
For the one-time v0.1.0 bootstrap, protect the crates-io-bootstrap
environment and provide its CARGO_REGISTRY_TOKEN secret. Publish in order:
mdbook-structured-core, wait for its exact registry visibility, then
mdbook-structured. Check each package with:
curl -sS https://crates.io/api/v1/crates/mdbook-structured-core/0.1.0
curl -sS https://crates.io/api/v1/crates/mdbook-structured/0.1.0
Configure Trusted Publishers for both crates with these exact fields:
owner: OptimalCNC
repository: mdbook-structured
workflow: release.yml
environment: crates-io
After testing OIDC publication, remove the bootstrap workflow and revoke or delete its long-lived token. The normal release workflow publishes core before the CLI and retries index visibility delays with bounded polling.
Hosted publication proof
Enable GitHub Pages with Build and deployment: GitHub Actions. The book is published at optimalcnc.github.io/mdbook-structured. Inspect that URL after deployment, and inspect both mdbook-structured docs.rs and core docs.rs after their asynchronous builds complete. A local command or green workflow is not proof that a hosted endpoint is live.
If a publish is interrupted, inspect the exact crates.io API version before retrying. A transient index delay is retryable; a published version cannot be overwritten. Yanking follows crates.io procedure and is not rollback.
Design Reference
Status: Approved design for v1
Date: 2026-08-28
mdbook-structured is an external Rust mdBook preprocessor backed by a reusable, format-extensible library. The library exposes a parser-neutral model for JSON-compatible structured documents. It renders selected JSON and YAML source files as readable, styled pages in mdBook’s stock HTML output. The source files remain authoritative on disk, and each generated page remains part of the ordinary mdBook book: navigation, themes, search, edit links, and the stock renderer continue to own their usual responsibilities.
The architecture is intentionally general, but the v1 input set is fixed to
.json, .yaml, and .yml. A file is eligible only when mdBook has loaded it
as a chapter listed in SUMMARY.md. The implementation does not enumerate the
source tree to discover unlisted chapters.
V1 includes:
- JSON and YAML parsing through selected parser libraries;
- a parser-neutral
StructuredDocumentmodel with source provenance; - build-time semantic HTML rendering and progressive JavaScript enhancement;
- book-wide Markdown link rewriting for registered structured chapters;
- chapter-route collision preflight and fail-fast diagnostics;
- an installer that generates starter CSS and JavaScript without editing
book.toml; and - tests that verify semantic behavior at the parser, renderer, preprocessor, and installer seams.
V1 does not include a custom mdBook backend, a fork of mdBook, JSON/YAML parsers in mdBook core, implicit file discovery, non-HTML renderer support, schema-aware or domain-specific views, generated JSON Pointer anchors, persistent expansion state, print-specific styling, automatic configuration file edits, or special presentation for non-displayable control characters.
The ownership split is:
- mdBook core owns book structure, processing order, interpretation of final chapter paths, URL generation, and rendering infrastructure;
- mdbook-structured-core owns parser adapters, lossless model projection, diagnostics, and structured-page rendering; and
- mdbook-structured owns the mdBook subprocess protocol, chapter selection, the structured logical-path shim, book-wide link mapping, and the install command.
This keeps the external seam small while allowing future format adapters and renderers to evolve without coupling their parser types to mdBook. The authoring contract starts from the book author’s point of view; the processing architecture then explains how the components cooperate.
Authoring Contract
An author may list a structured source directly in SUMMARY.md, alongside
ordinary Markdown chapters:
# Summary
- [Introduction](README.md)
- [Runtime configuration](config/runtime.yaml)
- [Protocol manifest](schemas/manifest.json)
- [Deployment guide](deployment.md)
mdBook loads each listed target as UTF-8 chapter text. The structured
preprocessor dispatches from Chapter.source_path, which is the real source
path, and leaves that field unchanged. The title supplied in SUMMARY.md
continues to be the chapter title.
Structured chapter URLs preserve the source extension. For example,
config/runtime.yaml is published as config/runtime.yaml.html. The
route contract defines the logical-path shim, README/index
handling, and collision policy.
Only listed chapters are transformed. A Markdown link to an unlisted JSON or
YAML file does not create a structured page. Stock mdBook may nevertheless
copy any non-.md file under the source directory, including listed and
unlisted JSON/YAML files, into the output as a static file. V1 accepts this
stock publication behavior.
Links to listed structured chapters use the source path in authored Markdown:
[Runtime configuration](config/runtime.yaml)
The separate chapter-link rewriter maps that destination to the generated HTML page. Relative paths, README/index handling, query strings, and fragments otherwise follow mdBook’s path conventions.
Extension-qualified README links remain distinct. If both README.yaml and
README.json are listed from one directory, authors can link to either source
name and reach its corresponding page. Their shared index.md and
directory-style convenience aliases are ambiguous when no actual chapter owns
the destination, however, so an authored link using either alias stops the
build instead of selecting a target by SUMMARY.md order.
Authors register the two preprocessor phases and the generated assets in
book.toml. The installer prints the required entries but never inserts them.
Asset registration remains an author decision. The complete configuration and
installer behavior are defined in Configuration and operations.
Processing Architecture
The stock mdBook pipeline is extended with two registrations of the same executable:
SUMMARY.md
|
v
mdBook loads listed UTF-8 files as Chapter.content
|
v
mdBook's index preprocessor
|
v
mdbook-structured render
| parse eligible sources, project, assign routes, render semantic HTML
v
mdBook's links preprocessor
| expand the normal Markdown helpers/includes
v
mdbook-structured rewrite-links
| resolve Markdown links to registered chapter outputs
v
stock mdBook HTML renderer
The phases are separate because mdBook’s built-in links preprocessor scans
chapter text for helper syntax. render runs after index, so it sees the
final logical chapter arrangement while the original source_path is still
available. It replaces eligible chapter content before helper expansion, so
encoded generated values and source text cannot be mistaken for mdBook helper
directives. rewrite-links runs after links, so ordinary Markdown links
introduced by includes are covered too.
The pipeline delegates through focused contracts:
- the structured core parses source text and exposes the parser-neutral document model;
- route projection assigns deterministic structured chapter paths and rejects duplicate final chapter routes before mutation;
- chapter-link rewriting resolves or diagnoses authored destinations against the projected chapter map; and
- HTML presentation defines the raw-HTML framing, semantic markup, styling hooks, and progressive behavior.
The executable is intentionally thin. It loads configuration, invokes the core library, projects errors into protocol-friendly diagnostics, and writes the resulting book. The same core library may later be used by an in-process build driver without changing its interface, but an in-process integration is not a v1 requirement.
The exact subprocess protocol, phase registration, renderer support, and installer commands belong to Configuration and operations.
Structured Core
mdbook-structured-core is the deep module at the format seam. Callers provide
a format, UTF-8 source text, a source name, and limits; they receive a
StructuredDocument or a structured diagnostic. Parser-library types do not
cross this interface.
Public model
The conceptual public model is:
#![allow(unused)]
fn main() {
struct StructuredDocument {
format: StructuredFormat,
loaded_source: String,
root: Node,
stats: DocumentStats,
}
struct Node {
span: SourceSpan,
value: NodeValue,
}
enum NodeValue {
Mapping(Vec<MappingEntry>),
Sequence(Vec<Node>),
String(String),
Number(NumberLexeme),
Boolean(bool),
Null,
}
struct MappingEntry {
decoded_key: String,
key_span: SourceSpan,
value: Node,
}
}
SourceSpan is a zero-based, half-open UTF-8 byte range. Human-facing line
and column locations are one-based, with columns counted in Unicode scalar
values rather than bytes or display cells. Every span and source location
refers to loaded_source: the UTF-8 text supplied in Chapter.content after
mdBook’s loading and BOM handling. Adapters normalize parser-native
coordinates at this boundary. A span records parser-reported provenance; it
does not promise that its source slice includes every syntactic delimiter.
loaded_source remains the authority for the exact source text.
Sequence positions are represented by their ordered position, and mapping
entries retain insertion order. Decoded keys are unique strings. Numbers
retain their validated lexical form rather than being converted through
f64. Strings retain their decoded value exactly: the core performs no
trimming, case normalization, or simplification. The loaded source is retained
separately so comments, quoting, whitespace, line endings, and formatting
remain available to the Original source view.
Format adapters
Internally, format adapters satisfy a small parser seam equivalent to:
#![allow(unused)]
fn main() {
trait FormatAdapter {
fn parse(
&self,
source: &str,
source_name: &Path,
limits: &Limits,
) -> Result<StructuredDocument, Diagnostic>;
}
}
The initial adapters use Rust parser libraries that expose the ordering and
source provenance required by the public model. The JSON adapter uses
json-syntax 0.12.x. The YAML adapter uses rlsp-yaml-parser with an exact
=0.11.1 version requirement. Parser-library types remain private to their
adapters.
The YAML adapter first consumes the library’s event stream as a bounded
preflight, then uses its lossless loader with the library’s default Core schema
to build the parsed tree. The library therefore owns YAML grammar, scalar
decoding, tree construction, and scalar meaning. The adapter owns resource
preflight, ordered projection into StructuredDocument, and conversion of
parser errors and provenance. When projecting an implicitly resolved number,
it retains the parser-returned plain-scalar spelling rather than converting it
through a Rust numeric type. V1 does not define a separate YAML schema or an
exhaustive YAML acceptance and rejection matrix.
rlsp-yaml-parser is a young, pre-1.0 dependency, so the exact pin is part of
the v1 design. A version change must requalify the adapter through the public
core contract tests. Keeping the dependency behind FormatAdapter allows it
to be replaced without changing StructuredDocument or its callers.
After parsing succeeds, an adapter must project the parser result losslessly
into StructuredDocument. Duplicate decoded mapping keys are rejected so the
public mapping invariant remains unambiguous. Any other parsed construct that
the model cannot represent without coercion, omission, or simplification
causes an explicit projection diagnostic. The adapter must not silently
discard data or substitute a reduced representation merely to complete the
conversion.
Limits and diagnostics
The parser and renderer enforce bounded input, node, and depth limits. The v1 defaults are:
max-input-bytes = 1048576
max-nodes = 10000
max-depth = 64
The input-byte limit is checked before parser entry. The YAML event preflight counts nodes and open containers, so configured node and depth limits stop the input before the loader builds its tree or rendering begins. Parser-owned hard safety limits may reject an input earlier. Limits cause a diagnostic before unbounded expansion or output generation. A diagnostic identifies the source name, category, line and column when available, and the structured key path when the adapter can establish one.
The verification strategy treats this public interface—not parser-specific representations—as the test surface.
Routes and Collisions
The route shim preserves a structured source’s extension while continuing to
use the stock HTML renderer. render takes the logical path established after
mdBook’s index phase and replaces its extension with
<source-extension>.md. The stock renderer then replaces only that final
.md:
source_path post-index path structured path HTML output
config/runtime.yaml config/runtime.yaml config/runtime.yaml.md config/runtime.yaml.html
schemas/manifest.json schemas/manifest.json schemas/manifest.json.md schemas/manifest.json.html
README.yaml index.md index.yaml.md index.yaml.html
README.json index.md index.json.md index.json.html
The final .md is an integration shim for the stock renderer’s
with_extension("html") rule. It is applied consistently to every structured
chapter, not only when a collision is detected, so published URLs do not
depend on the presence of another file.
The real source remains in Chapter.source_path, so edit links and diagnostics
continue to point to config/runtime.yaml or schemas/manifest.json. An
ordinary settings.md chapter and a structured settings.yaml chapter
therefore produce settings.html and settings.yaml.html.
Likewise, README.yaml and README.json in one directory produce distinct
final routes even though mdBook’s index phase gives them the same interim
index.md path. Their shared index and directory-style link aliases may be
ambiguous, but alias ambiguity is diagnosed only when such a link is authored;
it is not an output-route collision. Direct links to the two source paths
remain distinct.
Preflight
Before mutating any chapter, render projects every final chapter
destination. Ordinary chapters use their existing logical path; structured
chapters use the source-extension shim. The preflight then applies mdBook’s
with_extension("html") rule and rejects duplicate final chapter routes,
naming every conflicting source chapter.
Exact projected-route collisions remain possible, most notably between
settings.yaml and an authored settings.yaml.md chapter. The preflight fails
with every conflicting source listed. Routes are deterministic and never
change in response to a collision.
A projected structured HTML route that still contains a literal .md
substring is also rejected because the stock renderer would corrupt generated
navigation links to it. Finally, the preflight rejects the narrow case in
which a projected chapter destination exactly matches a non-.md source file
that stock mdBook would copy to that destination. Exact candidate-path probes
for this check do not discover or transform unlisted chapters.
The preflight does not claim to inventory every stock renderer artifact, theme asset, redirect, or internal route. Those remain mdBook’s responsibility; the plugin’s guarantee covers its projected chapter routes and exact static-source conflicts with those routes.
The resulting route map is the authority consumed by chapter-link rewriting.
Chapter Link Rewriting
rewrite-links builds exact chapter lookups and convenience-alias candidate
sets from the source paths and logical chapter paths in the received Book.
Every chapter source path is exact. Independently authored logical paths are
also exact, while paths synthesized from a README source by mdBook’s index
phase are convenience aliases. Both kinds of lookup resolve to the outputs
established by route projection.
The rewriter resolves a relative Markdown destination from the current chapter using mdBook’s path conventions, then consults the exact lookup before any convenience aliases.
For example:
[Runtime configuration](config/runtime.yaml)
becomes a link to config/runtime.yaml.html, with the relative URL calculated
from the current chapter in the same manner as mdBook.
README and index aliases
For a structured source whose file stem is README, the map also registers
mdBook’s post-index index.md path and its parent-directory form as aliases
for the projected index.<source-extension>.html page. The source path itself,
such as README.yaml, remains an exact chapter lookup even though render has
already replaced Chapter.path with the shimmed path.
An actual registered chapter at a destination takes precedence over a
convenience alias. Otherwise, an alias with one candidate is rewritten, while
an alias with multiple candidates fails with a diagnostic naming each source
and projected output. Candidate sets retain every README target: the tool does
not reject otherwise valid chapters or choose one by SUMMARY.md order.
For example, direct links to README.yaml and README.json in the same
directory resolve to index.yaml.html and index.json.html. A link to their
shared index.md or directory-style alias is ambiguous, in the absence of an
exact chapter at that destination, and fails only when that link appears in
authored Markdown.
Rewriting rules
The rewriter edits Markdown link spans using mdBook’s Markdown parser. It does not reserialize an entire chapter. It preserves query strings, percent encoding, and fragment text verbatim. Fragments are opaque: the tool does not parse JSON Pointer syntax and does not generate or validate pointer anchors in v1.
The stock HTML renderer subsequently rewrites any local URL containing a
literal .md substring, even when that substring is not the final extension.
The route preflight therefore rejects a projected path
such as guide.md.yaml.html. rewrite-links also fails if a query or fragment
would introduce literal .md into an otherwise valid rewritten destination.
These checks prevent silent corruption while leaving already encoded text
untouched. V1 does not invent a URL encoding to work around this mdBook
behavior.
The following are left unchanged:
- images and image destinations;
- raw HTML links, code blocks, and ordinary scalar text;
- external, protocol-relative, and other non-local URI schemes;
- fragment-only links;
- destinations with neither an exact chapter nor an alias candidate; and
- destinations that already end in
.html.
Reference-style links are supported by rewriting their definition once. An image-only reference definition is unchanged. If one definition is shared by an image and a normal link and rewriting it would change the image, the preprocessor fails with a clear diagnostic rather than guessing. Strings inside JSON/YAML values are never scanned as Markdown and are never rewritten.
Authored fragments remain available for future or user-provided anchors, but the structured renderer does not create JSON Pointer-based anchors in v1.
HTML Presentation
The preprocessor emits complete semantic HTML at build time. JavaScript is an enhancement layer: it never parses the source or owns the data model. The output remains readable when JavaScript is disabled.
Renderer boundary
Generated chapter content is framed as one contiguous raw HTML block with no blank line that could terminate the block in mdBook’s Markdown parser. Source-derived line breaks are entity-encoded where necessary rather than allowed to split that block. This framing ensures that generated elements and data cannot be reinterpreted as Markdown before the stock HTML renderer sees them.
The structured-page renderer uses context-specific, one-pass encoding that both HTML-escapes data and neutralizes mdBook helper-token delimiters in generated text and attributes. Ordinary displayable browser text retains its value while mdBook’s helper scanner cannot recognize it.
Each page starts with a visible h1 whose text comes from Chapter.name,
followed by the structured data tree. The heading is chapter framing, not a
node in the data model.
Structured tree
The visual reference is Firefox’s JSON Viewer: compact monospaced rows, indentation, twisties, key/value alignment, restrained hover states, and distinct scalar coloring. Those interaction and styling ideas are independently implemented so the page remains part of the mdBook theme.
The data root is rendered directly as an always-visible node; no synthetic
data-root row, label, or disclosure is added. Nested mappings and sequences are
container rows using native HTML details and summary elements. Containers
at model depth two start open, while deeper containers start closed. A nested
container with more than 100 immediate children starts closed, even at depth
two.
At every model depth, each non-empty mapping or sequence group is classified from its immediate rendered model/DOM children, including children inside a closed disclosure. A mixed group has at least one foldable mapping or sequence child, so every row reserves a disclosure gutter before its label: foldable rows place their native marker there and scalar rows leave it empty. An all-scalar group has only string, number, boolean, or null children; it omits only that marker-specific gutter and any root marker clearance while preserving ordinary structural nesting indentation and one sibling label column. A nested container’s summary remains a foldable row in its parent group, while its own children are classified separately. Empty containers have no child label group, so their existing disclosure behavior is preserved. The alignment anchor is the start of the decoded rendered label (a mapping key or sequence index), not the first visible glyph and not raw source indentation or quoting. The heading, action buttons, and Original source disclosure are outside this data-row alignment system.
The generated page includes controls to expand or collapse all containers on the active page. They do not affect other pages and do not persist state between visits.
Keys and string values are decoded and displayed without surrounding quotes.
CSS type markers distinguish strings, numbers, booleans, and null, so a string
true remains visibly different from boolean true. Empty keys and empty
strings receive an explicit visual marker. Long strings retain their complete
content and source whitespace; CSS handles readable wrapping rather than
truncating or simplifying values. Each authored line-break sequence in a
rendered string value or mapping key has a muted ↵ marker immediately before
the preserved break, so it remains distinguishable from responsive wrapping.
The marker is absent from copied text, accessibility text, and Original source.
Mapping and sequence order are preserved.
Original source
A collapsed Original source section contains
loaded_source without tool-level trimming,
normalization, or simplification, in a format-labelled code block. It supports
copy/paste, syntax-oriented review, and comparison when the structured view is
not the right representation. It is excluded from expand/collapse-all.
Every value inserted into HTML is encoded for its HTML context in one pass;
source content is never interpreted as markup or Markdown. For ordinary
displayable text, this encoding changes markup bytes without changing the
browser-visible characters. V1 defines no special presentation or
browser-text fidelity contract for non-displayable control characters. The
model and loaded_source still retain the parser and mdBook inputs without
tool-level stripping.
CSS and JavaScript
The supplied CSS and JavaScript expose a small semantic hook vocabulary for the document root, node kinds, scalar types, and containers. Those hooks are the stable contract used by the enhancement script and tests. Incidental wrapper nesting, attribute order, and additional styling classes remain changeable.
Authors register or override the starter assets through mdBook’s normal theme configuration, as described in Configuration and operations. Print-specific behavior is not a v1 requirement.
Configuration and Operations
The v1 executable exposes render, rewrite-links, and install.
Preprocessor protocol
render and rewrite-links use the standard mdBook external-preprocessor
protocol: a [PreprocessorContext, Book] JSON tuple enters on stdin, exactly
one transformed Book JSON value leaves on stdout, and human diagnostics go
only to stderr.
Both commands are restricted to the stock html renderer. Their nested
capability forms are:
mdbook-structured render supports html
mdbook-structured rewrite-links supports html
Those forms succeed; other renderer names are unsupported. During a normal run, either command rejects a malformed protocol value or an unexpected renderer instead of silently passing through a partially processed book.
mdBook registration
The recommended complete configuration is:
[preprocessor.structured]
command = "mdbook-structured render"
after = ["index"]
before = ["links"]
renderers = ["html"]
max-input-bytes = 1048576
max-nodes = 10000
max-depth = 64
large-container-threshold = 100
[preprocessor.structured-links]
command = "mdbook-structured rewrite-links"
after = ["links"]
renderers = ["html"]
The ordering implements the processing pipeline: render
converts eligible JSON/YAML chapters, assigns their source-extension route
shims, and does so before helper expansion. rewrite-links runs afterward and
covers links in ordinary Markdown, including links introduced by includes.
Files not present as chapters are never transformed as structured chapters. Each registered structured chapter must parse and render successfully. Parser-reported malformed input, duplicate decoded keys, lossless-projection failures, route errors, and resource-limit violations stop the build with a filename, source location, and, when available, structured path. There is no silent fallback that leaves a broken registered source as raw text.
The fixed v1 extension set is .json, .yaml, and .yml; unrelated chapters
pass through unchanged. Limits are explicit configuration, not silent
truncation. command, after, before, and renderers are mdBook-owned
registration keys rather than plugin options. The same applies to mdBook’s
standard optional key. After those keys are excluded, an unknown or invalid
plugin-specific configuration field is an error.
Books containing structured chapters support mdbook build with the stock
HTML renderer. mdbook test uses a different renderer and is unsupported for
such books in v1. With the recommended renderer filters, mdBook skips both
commands during mdbook test and applies its default test behavior to the
untransformed structured text, so the result is content-dependent and carries
no tool guarantee. The tool does not remove or replace structured chapters to
create a test-renderer mode.
Asset installation
mdbook-structured install [DIR]
The command writes mdbook-structured.css and mdbook-structured.js under
DIR, which defaults to the current book root. It never reads or modifies
book.toml. It prints instructions showing where authors should add the asset
paths in additional-css and additional-js. When either array already
exists, authors append the printed path rather than replacing existing
entries. Registration remains an author decision.
The installer processes the two files sequentially. It creates an absent file, leaves a byte-identical existing file unchanged, and stops on the first different existing file or unexpected write error. Partial side effects are allowed; rollback and atomic multi-file updates are intentionally out of scope. The generated assets are starter implementations using mdBook theme variables and can be edited or replaced by the author.
Verification
Verification targets consumer-visible behavior rather than the incidental serialization of HTML or JSON. This follows mature artifact-producing test patterns: mdBook decodes generated search data and checks selected fields, its protocol tests deserialize and round-trip typed book values, its browser tests assert selectors, text, counts, and attributes, and parser crates use table-driven or specification corpora with focused regression properties.
Test inputs are small, hand-authored source fixtures supplemented by bounded generated valid documents. Expected behavior is expressed as typed assertions and invariants. V1 does not use whole-file HTML snapshots, generated-book byte comparisons, or approval files for ordinary renderer changes.
Structured core
The core test surface is the public
StructuredDocument interface.
Table-driven positive fixtures cover representative JSON and YAML documents
accepted by the selected parser libraries: mappings and sequences, nesting,
insertion order, decoded keys and strings, empty values, booleans versus
strings, lexical numbers, source spans, and exact loaded_source retention.
YAML scalar-kind expectations come from rlsp-yaml-parser 0.11.1’s Core
schema rather than a project-owned classification table.
A property-based generator produces bounded JSON-compatible trees and serializes them into valid JSON. YAML positive coverage uses small curated parser-accepted fixtures; the core does not add a YAML serializer or duplicate the parser library’s general grammar suite solely for tests. Successful fixtures must project losslessly, retain their structural statistics, and satisfy node, order, type, and normalized-coordinate invariants, including with multibyte UTF-8 before a span.
For JSON, an independent semantic projection through serde_json::Value may
corroborate basic value meaning; it does not replace span or lexical-form
assertions. Parser-library conformance suites remain the authority for general
grammar coverage. A change to the exact YAML parser pin must pass the same
public core contract tests before adoption.
HTML and JavaScript
The renderer is tested through mdBook’s Markdown parser followed by a
standards-compliant HTML parser, never by comparing serialized HTML bytes. A
small test-only semantic probe walks the .structured-document subtree and
records an ObservedDocument: heading text, node kind, mapping key or sequence
index, scalar text and type, child count, container-open state, hard-break
marker placement, and raw-source text.
Tests assert this observation against concise expected values. They verify
that Chapter.name becomes the visible h1, source nodes appear in order,
scalar types remain distinguishable, ordinary displayable text is escaped and
complete, the data root is rendered directly without a synthetic disclosure,
authored breaks remain distinct from responsive wrapping without changing
selected text, the nested-container disclosure rules are applied, and
loaded_source is retained without tool-level modification. Passing generated
content through Markdown also verifies the raw HTML
framing.
The probe uses only documented semantic hooks; wrapper nesting, whitespace
between tags, attribute order, and ordinary CSS classes remain free to change.
Expected observations are written independently of renderer internals, so the
probe cannot merely reproduce the renderer’s output. One positive fixture
contains literal mdBook helper-looking text in both a scalar and
loaded_source, plus HTML-looking text in a scalar. The observed browser text
must retain both without invoking a helper or creating injected elements. V1
has no control-character-specific presentation acceptance test.
JavaScript is tested at the behavior seam. One browser-level smoke suite,
following mdBook’s selector-oriented browser tests, loads a built fixture and
asserts visible node counts and text, initial disclosure state, expand-all and
collapse-all behavior, active-page scoping, and the absence of persistent
state after a fresh load. It also checks that scalar and foldable sibling rows
share their rendered label column and that a native root marker does not escape
the structured-document boundary. Geometry assertions cover the all-scalar
direct root, the nested all-scalar display mapping, and the nested all-scalar
ports sequence after expansion, as well as mixed-root and mixed nested
alignment. They use the start of each decoded rendered label as the anchor and
verify that all-scalar groups omit only marker clearance while retaining
structural indentation. No screenshot or pixel baseline is required. CSS
receives semantic-hook coverage and a maintained manual visual check; visual
styling is intentionally author-overridable.
Preprocessor and mdBook integration
The preprocessor is tested first with in-memory Book values. These tests
assert typed protocol results: only eligible chapters change, source_path is
preserved, structured logical paths gain the source-extension shim, unrelated
chapters and metadata are untouched, projected chapter-route collisions and
exact static-source conflicts are reported, and rewritten destinations follow
the chapter map. Two structured README chapters with different source
extensions but the same post-index logical path must project to distinct
outputs. Direct source links to both must rewrite independently, and unused
shared convenience aliases must not fail preprocessing. A separate case adds
an actual index.md chapter at a shared alias destination and verifies that
the exact chapter wins without ambiguity. Link rewriting is tested against the
Markdown AST and spans, including reference definitions. Capability tests
exercise render supports html and rewrite-links supports html.
One small mdBook build fixture verifies the actual ordering with index,
links, includes, and the stock HTML renderer. It is inspected through parsed
output paths, DOM links, and semantic probes rather than expected HTML files.
It verifies runtime.yaml.html plus distinct index.yaml.html and
index.json.html routes, direct source-path links to both README pages, a
unique README/index/directory alias, and stock publication of the raw
runtime.yaml file. The fixture includes a literal helper-looking value to
verify render-before-links protection. A single end-to-end fixture proves
wiring; it does not duplicate every core or renderer case.
High-value failures
Negative tests are added only when they protect a critical semantic or
security boundary. V1 covers a malformed registered source with
location-bearing diagnostics, duplicate decoded keys, a parsed construct that
cannot be projected losslessly, each resource-limit boundary, an exact
projected-route collision, an exact static-source conflict, and a projected
route or rewritten destination that stock mdBook would corrupt because it
contains a literal .md. It also covers an authored convenience alias with
multiple chapter candidates, which must fail without rejecting the same book
when that alias is unused.
These tests do not duplicate every malformed syntax variant already covered by parser libraries or define an independent YAML behavior matrix. Assertions check error category, source path, location, and structured path rather than brittle prose. A new negative case is added when a regression, data-loss risk, or security requirement makes it valuable.
Installer
Installer tests observe filesystem state transitions: creation,
byte-identical reruns, refusal to overwrite differing files, sequential
stopping, printed add-path instructions, and unchanged book.toml. They
compare hashes or selected required markers only; the complete generated asset
text is not a test oracle.
Acceptance criterion
Representative and generated valid documents render completely and in order; the stock mdBook build exposes the expected links and source provenance; the small set of critical invalid cases fails loudly; and style or markup refactoring does not require rewriting large snapshots.
Evolution
Every discovered bug adds the smallest regression at the seam where it is observable. Parser upgrades rerun the positive corpus and properties. Future format adapters and renderer layers must satisfy the same semantic contract; they do not add parser-specific assertions to the mdBook protocol.
Format extensions
Future adapters for tree-shaped, JSON-compatible formats may target the
existing StructuredDocument. They must
preserve its ordering, scalar, provenance, and error invariants without
exposing parser-specific representations. A format whose parsed values cannot
be represented losslessly requires a different model or rendering layer
rather than a lossy adapter.
Deferred features
JSON Pointer-generated anchors, schema-aware and domain-specific views, persistent expansion state, alternate backends, print-specific behavior, and deeper mdBook integration remain deferred. These exclusions preserve the v1 boundary until implementation evidence establishes a need to change it.
Possible mdBook contributions
Any proposed mdBook core change remains a separate generic contribution. Candidates include chapter-aware link resolution, duplicate output-path diagnostics, and an explicit UTF-8 textual-source contract.
A dependency-reporting API is considered only after a prototype establishes a
reproducible mdbook serve rebuild problem. No first-class content-kind field
or format-specific behavior enters mdBook core without a versioned protocol
design.