Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

OptionDefault
max-input-bytes1048576
max-nodes10000
max-depth64
large-container-threshold100

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 StructuredDocument model 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.