MrDocs in the Wild

Portrait of Alan de Freitas Alan de Freitas · Apr 24, 2026

The questions changed. For a long time, people asked about MrDocs in the abstract: what formats will it support, how will it handle templates, when will it be ready. Then, gradually, the questions became specific. Jean-Louis Leroy, the author of Boost.OpenMethod, became one of our most active sources of feedback. His library exercises corners of C++ that most projects never touch, which means MrDocs gets tested in ways we would not have anticipated. He wanted to know why his template specializations were not sorted correctly. He wanted macro support because Boost libraries rely heavily on macros. He hit a crash when his doc comments contained HTML tables. These are not theoretical questions about a tool that might exist someday. These are questions from someone who already generated documentation with MrDocs and needs it to work better.

In our previous post, we described MrDocs transitioning from prototype to product. This post is about what happened when MrDocs went into the wild.

Real Projects, Real Problems

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#e4eee8", "primaryBorderColor": "#affbd6", "primaryTextColor": "#000000", "lineColor": "#baf9d9", "secondaryColor": "#f0eae4", "tertiaryColor": "#ebeaf4", "fontSize": "14px"}}}%% mindmap root((Feedback)) First impressions Unstyled demos Custom stylesheets Navigation Orphaned pages Breadcrumbs AST edge cases Parameter packs Friend targets Detail namespaces Rendering Description ordering Code blocks Anchor links Runtime JS engine switch Compiler fallback

The Demo Page

Right after the previous post, where we announced the MVP and encouraged people to try MrDocs, we noticed the demos page was not doing us any favors. Someone shared MrDocs on a developer community and the website started getting traffic. The landing page looked polished, but visitors clicked through to the demos and saw raw, unstyled HTML: no fonts, no spacing, no colors. The HTML generator produced correct semantic markup, and that is technically the point: users are supposed to customize the output with their own stylesheets. But on the demos page, there was no stylesheet at all, and the result looked broken rather than customizable.

The custom stylesheet system added five configuration options (stylesheets, linkcss, copycss, no-default-styles, stylesdir) so projects can match their own branding. A bundled default CSS now ships with MrDocs, and it was refined to remove gradients in favor of solid, readable backgrounds.

Stylesheet commits

MrDocs generates thousands of reference pages, one per C++ symbol. We maintain an Antora extension, the antora-cpp-reference-extension, that integrates these pages into Antora-based documentation sites. But the generated pages end up orphaned from the navigation tree. Users found the navigation confusing: clicking on “boost” in the breadcrumb did not go where expected, and reference pages had no trail showing where they belonged in the hierarchy.

The obvious fix would be to list every page in Antora’s nav.adoc, but maintaining a navigation file with thousands of entries that changes every time a symbol is added or removed is not practical. Worse, Antora renders the navigation file in the sidebar, so listing every reference page would flood the UI with thousands of entries. We discussed the problem extensively with the Antora maintainer on the Antora community chat. His position was clear: Antora was designed so that pages must be in the navigation file. Programmatic editing of navigation is not supported.

That was not acceptable for us. We needed breadcrumbs that work for thousands of generated pages without polluting the sidebar or requiring a hand-maintained navigation file. The Antora author’s position was reasonable from his perspective (Antora is a general-purpose documentation tool, not a reference generator), but our use case was fundamentally different from what Antora was designed for.

The antora-cpp-reference-extension now builds breadcrumbs independently from the navigation file. MrDocs generates reference pages in a directory structure that mirrors the C++ namespace hierarchy (boost/urls/segments_view.adoc lives inside boost/urls/). The extension uses this structure to reconstruct the breadcrumb trail: each directory maps to a namespace, and the page title (which is the symbol name) becomes the last breadcrumb entry. The result reads naturally: Reference > boost > urls > segments_view.

Zero changes to the nav file. The sidebar stays clean. Breadcrumbs appear automatically and update when symbols are added or removed.

Breadcrumb and reference extension commits

antora-cpp-reference-extension

  • ae95eb2 feat: synthesize reference breadcrumbs without nav files
  • 10a4019 feat: add auto base URL detection
  • 6a6c08b docs: auto-base-url option
  • 4f7c79f refactor: enhance release asset validation

Coordinating Two Independent Extensions

The antora-cpp-reference-extension generates reference pages and breadcrumbs. The antora-cpp-tagfiles-extension resolves cross-library symbol links (so a reference to boost::system::error_code in Boost.URL’s docs links to the correct page in Boost.System’s docs). These are two independent Antora extensions running as separate jobs.

The problem was that the reference extension generates tagfiles as a side effect of producing reference pages, and the tagfiles extension needs the most recent version of those tagfiles to resolve links correctly. MrDocs changes the tagfiles every time the corpus changes. Manually keeping them in sync was not sustainable: committing tagfiles to the repository meant they were always stale by the time the next build ran.

We made the extensions coordinate directly. The reference extension now hands its tagfile to the tagfiles extension at build time, so the links always reflect the current state of the documentation. The reference extension also gained auto base URL detection, removing the need for manual path configuration when switching between development and production builds.

Extension coordination commits

antora-cpp-reference-extension

  • 6e8ffcb feat: antora-cpp-tagfiles-extension coordination
  • 8f12576 chore: version is 0.1.0

antora-cpp-tagfiles-extension

  • 98eba40 feat: antora-cpp-reference-extension coordination
  • 5a1723c feat: add global log level control for missing symbols
  • 453f01b chore: version is 0.1.0

Edge Cases in the Wild

As more libraries adopted MrDocs, edge cases in C++ symbol extraction surfaced. Boost.Beast exposed a duplicate ellipsis in parameter pack rendering (#1108, #1129):

Before: T& emplace(Args...&&... args)

After: T& emplace(Args&&... args)

Boost.OpenMethod revealed that friend targets were not resolving correctly. Boost.Buffers uncovered a problem with detail namespaces: when a class inherits from a base in a hidden namespace, the inherited members appeared in the documentation but their doc comments were lost (#1107). We fixed this so derived classes inherit documentation from hidden bases.

Unnamed structs also sparked an extended design discussion. When C++ code declares constexpr struct {} f{};, MrDocs needs a stable, unique name for hyperlinks. The team established a collaborative design process using shared documents, with Peter Dimov contributing an insight about C compatibility (typedef struct {} T; makes the struct named in C++).

AST and metadata commits
  • c85be75 fix: remove duplicate ellipsis in parameter pack expansion
  • c3dbded fix(ast): prevent TU parent from including unmatched globals
  • 76b7b43 fix(ast): canonicalize friend targets
  • 05f5852 fix(metadata): copy impl-defined base docs
  • 35cf1f6 fix: UsingSymbol is SymbolParent
  • c406d57 fix: preserve extraction mode when copying members from derived classes
  • 4e7ef04 fix: prevent infinite recursion when extracting non-regular base class
  • 0a69301 fix: extract and fix some special member function helpers

Rendering and Output

Users noticed that the manual description of a symbol was buried below long member tables (#1105). On a class with many members, you had to scroll past the entire member listing before finding the author’s explanation of what the class does. We moved the description to appear immediately after the synopsis, matching what cppreference does.

Other rendering issues included HTML code blocks not wrapped in <pre> tags, anchor links appearing when the wrapper element was missing, and the Handlebars template engine accumulating special name re-mappings that conflated different symbols.

Rendering and output commits
  • d90eae6 fix: hide anchor links when wrapper is not included
  • 92491de fix: manual description comes before member lists
  • 58bf524 fix: remove all special name re-mappings for Handlebars
  • 2a75692 fix: HTML code blocks not wrapped in pre tags
  • 1ebff32 fix: bottomUpTraverse() skips ListBlock items
  • 7b118b1 fix: missing @return command in doc comment

Under the Hood

We fixed a compiler fallback issue where MrDocs failed when the compilation database referenced a compiler that was not available on the current machine, and corrected sanitizer flag propagation so that UBSan and TSan do not unnecessarily propagate to dependency builds.

Build and toolchain commits
  • 235f5c8 fix: fall back to system compilers when database compiler is unavailable
  • f320581 fix: don’t pass sanitizer to dependency builds for UBSan/TSan

The MrDocs Website

While we were fixing the generated output, Robert Beeston and Julio Estrada were redesigning the MrDocs website. Robert led the design direction, working with a team to develop a visual identity that balances a distinctive retro aesthetic with modern readability, including a dark theme. Julio handled the implementation: mobile-responsive layout, UI styling improvements, cleaner backgrounds and styles, Open Graph and Twitter meta tags for social sharing, and a close button for the docs navigation on smaller screens.

For a documentation tool, the website is the first thing potential users see. Having a polished, memorable landing page matters more than it might for other kinds of projects.

Exploring the Unknowns

The team made a deliberate choice: instead of following a traditional feature roadmap, we would focus on areas of uncertainty (#1113). These were open questions that blocked multiple design decisions at once:

  • MrDocs-as-compiler (#1073): should MrDocs emit “object” files for later “linking,” like a compiler?
  • Scripting extensions (#1128, #881): how should users extend and transform documentation output?
  • Plugins (#58, #1044): how should third-party code register new generators?
  • JSON-only MrDocs: should we add a JSON output format alongside (or replacing) the existing XML structured output?
  • Reflection (#1114): how do we reduce the maintenance burden of the growing metadata model?
  • Cross-linking (#1072): how do we reference symbols in other libraries?

The motivation was practical. Each Boost library that adopted MrDocs had its own needs that could not be met by the core tool alone. Boost.URL has implementation_defined namespaces with internal code that should be hidden or transformed in the documentation. Boost.Capy has detail types that should be presented as user-facing types. Coroutines are represented as types in the AST but should be documented as functions. We want MrDocs to be smart enough, with project-specific extensions, that library authors do not have to do workarounds in the source code just to get the documentation right.

Rather than hard-coding solutions for each library, the unknowns framework asked: what general mechanisms would let every library solve its own documentation problems?

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f7f9ff", "primaryBorderColor": "#9aa7e8", "primaryTextColor": "#1f2a44", "lineColor": "#b4bef2", "secondaryColor": "#fbf8ff", "tertiaryColor": "#ffffff", "fontSize": "14px"}}}%% mindmap root((Unknowns)) Scripting extensions JS helpers Lua Plugins Generator API DLL loading Reflection Boost.Describe MrDocs.Describe Cross-linking Tagfiles Antora coordination JSON-only MrDocs MrDocs-as-compiler

Reflection: Replacing Boilerplate with Introspection

MrDocs models many kinds of C++ symbols: functions, classes, namespaces, enums, typedefs, concepts, and more. Each symbol type has metadata, and every piece of code that touches that metadata had to enumerate all fields by hand. Adding a single field to a symbol type meant updating it in:

  • Schema files that describe the metadata format
  • Generators (HTML, AsciiDoc, XML) that produce the output
  • Templates that render individual pages
  • Operators like comparison functions, merge functions (e.g., merging symbols from different translation units when only one is documented), and equality checks
  • Documentation describing the metadata
  • The code itself that populates and transforms the metadata

That is roughly ten to fifteen places per field, and missing one caused CI failures that blocked everyone. This was one of the unknowns we identified: how to reduce the maintenance burden as the data model grows. Worse, downstream users who had their own templates and extensions also had to learn about the new fields and update everything accordingly.

Gennaro Prota, with his strong background in generic programming and metaprogramming, took ownership of the reflection problem. The work progressed through several stages:

  1. Integrate Boost.Describe into the metadata system, replacing hand-written serialization functions
  2. Add $meta.type and $meta.bases to all DOM objects so templates can introspect the corpus
  3. Replace the XML generator with a reflection-based one (no more hand-maintained XML output)
  4. Build a custom reflection system (MrDocs.Describe) tailored to our needs
  5. Replace per-type operators with a single generic template

The result eliminated the second step entirely: adding a new field to a symbol type no longer requires touching ten other files. The description drives everything, and the serialization, comparison, and merge logic derive from it automatically. Boost.Describe and Boost.Mp11 are private dependencies that do not appear in public headers.

Along the way, Gennaro also added function object support, fixed Markdown inline formatting and missing dependent array bounds.

Reflection and metadata commits

Reflection (Gennaro Prota)

  • d490880 refactor(metadata): integrate Boost.Describe
  • c4dd89a feat: add $meta.type and $meta.bases to all DOM objects
  • d4a64ef fix: replace the XML generator with a reflection-based one
  • 6ce961f refactor: add custom reflection facilities (MrDocs.Describe)
  • eb68494 refactor: migrate all reflection consumers to MrDocs.Describe
  • 8f5391b refactor: replace per-type merge() one-liners with a single generic template
  • e749144 feat: make the reflection consumers public
  • 1ed76ad refactor: replace most per-type tag_invoke overloads with a single generic template
  • 0246935 refactor: replace per-type operator==() and operator<=>() with a single generic template

Features and fixes (Gennaro Prota)

  • 93a5032 feat: add function object support
  • f35ebcd fix: rendering of Markdown inline formatting and bullet lists
  • 4ae305b fix: missing dependent array bounds in the output
  • 72fba40 test: add golden tests for a partial class template specialization

The reflection work is the foundation for everything that comes next: the extension system, the upcoming Lua scripting, and the metadata transformation pipeline.

First Steps Toward Extensions

MrDocs supports two extension points: JavaScript for Handlebars template helpers, and Lua for more powerful scripting. The JavaScript engine had been Duktape, but Duktape is no longer actively maintained and only supports ES5.1. We needed a replacement.

We evaluated several alternatives (#881):

Engine JS Support Windows/MSVC Size License
QuickJS ES2023 No (clang-cl only) ~370 KB MIT
PrimJS ES2019 No (POSIX only) ~370 KB MIT
JerryScript ES5.1 + ES2022 subset Yes ~200 KB Apache 2.0
Escargot ES2025 subset Yes ~400-500 KB LGPL 2.1
MuJS ES5.1 Yes ~200-300 KB ISC
Moddable XS ES2025 (~99%) Yes (via SDK) ~100-300 KB Apache/GPL/LGPL
mJS Restricted ES6 Yes ~50-60 KB GPL 2.0 / Commercial
Elk Minimal ES6 Yes ~20-30 KB GPL 2.0 / Commercial

We first experimented with QuickJS, which had the best ES support. But it requires C11 features like <stdatomic.h> and __int128 that plain MSVC does not support. On Windows, users would need Clang with the Visual Studio runtime. PrimJS was POSIX-only. We settled on JerryScript: it supports Windows and MSVC natively, has a small footprint (~200 KB), and covers enough of ES2022 for template helpers. Unlike most alternatives in the table, JerryScript was designed from the ground up to be embedded in other applications, which makes it more like Lua and less like engines that target browsers or standalone runtimes.

The JavaScript helpers extension was a single commit but a large one: 85 files changed, 4,287 insertions. The work included:

  • Replacing Duktape with JerryScript across the entire codebase, including build scripts, CMake recipes, and third-party patches
  • Rewriting the C++ JavaScript bindings (JavaScript.hpp and JavaScript.cpp) with shared context lifetime, safer value accessors, and clearer error messages
  • A layered addon system where projects provide JavaScript helpers in a directory structure (generator/common/helpers/ for shared helpers, generator/html/helpers/ for format-specific ones). Multiple addon directories can be layered, so a project’s helpers override or extend the defaults.
  • Golden tests for extension output (js-helper/, js-helper-layering/) to verify that helpers produce the expected documentation
  • 1,335 lines of new JavaScript binding tests covering the engine lifecycle, value conversion, error handling, and helper registration

Combined with the public API for registering custom generators, MrDocs now supports customization beyond templates. A library like Boost.Capy could write an extension that transforms its coroutine types into function documentation, without any changes to MrDocs itself.

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f7f9ff", "primaryBorderColor": "#9aa7e8", "primaryTextColor": "#1f2a44", "lineColor": "#b4bef2", "secondaryColor": "#fbf8ff", "tertiaryColor": "#ffffff", "fontSize": "14px"}}}%% flowchart LR A[Clang AST] --> B[Extraction] B --> C[Corpus] C --> D[Transformation Extensions] D --> E[Handlebars Generators] E --> F[Documentation Templates] F --> H[HTML / AsciiDoc] F --> G[Template Extensions] G --> F D -.-> I[XML]

The vision for extensions has two layers:

  • Transformation extensions operate on the corpus between extraction and generation. A library could transform its internal types into the documentation structure it wants. This layer is not yet implemented.
  • Template extensions (JavaScript helpers) operate inside the Handlebars templates that produce HTML and AsciiDoc output. This is the layer we shipped.
  • Lua scripts for more powerful scripting in both layers
Extension and generator commits
  • 0f3ecb4 feat: javascript helpers extension (85 files, 4,287 insertions)
  • 930a5ea fix: jerry_port_context_free wrong signature causes silent corruption
  • 8da0930 feat(lib): public API for generator registration
  • 788c1ba feat(generators): tables for symbols have headers

Why We Discarded MrDocs-as-Compiler

One unknown we explored and deliberately discarded was MrDocs-as-compiler (#1073). The idea, proposed by Peter Dimov, was to treat MrDocs like a compiler: emit “object” files per translation unit, then “link” them to produce the final reference. CMake would invoke MrDocs as if it were Clang, with identical command-line options.

We spent time studying tools that work this way: clang-tidy, clang-doc, include-what-you-use. What we found is that tricking CMake into thinking MrDocs is a real compiler is not trivial. Every tool that tries this approach ends up needing either a coordinator binary (reimplementing what MrDocs already has) or CMake helper scripts. Both add workflows rather than simplifying them.

The experience from the Boost ecosystem reinforced this: no Boost project uses any of these compiler-like tools for static analysis, and the reason is complexity. People who find the compilation database workflow too involved are going to be even less inclined to adopt a tool that requires them to pretend to be a compiler. We decided to keep MrDocs as a single-step tool that reads a compilation database and produces output, rather than splitting it into a multi-binary pipeline that would need its own coordination layer.

Contributor Experience

As more people contributed to MrDocs, the gap between “clone the repo” and “submit a useful PR” needed closing. The biggest change was the bootstrap script, which reduced the entire build setup to a single python bootstrap.py command (covered in a separate post). Beyond the bootstrap, we split the contributor guide into focused sections, added reference documentation for MrDocs comment syntax (so contributors know what @copydoc, @see, and other commands do), and created a run_all_tests script that runs the full test suite locally without needing to understand the CMake test configuration.

Onboarding commits
  • b103cba docs(reference): mrdocs comments
  • 9b7ec24 feat(util): run_all_tests script
  • 5902699 docs: update packages
  • 302f0a6 docs: split contribute.adoc guide

Automating PR Reviews

MrDocs PRs tend to be large and hard to review. A single PR might touch the AST visitor, the Handlebars templates, the Antora extension, the CI configuration, and hundreds of golden test files (when an intentional change to the output format updates the expected output for every test case). We found ourselves making the same review comments over and over.

We set up Danger.js to catch these patterns before human reviewers see the PR. The most important check is detecting when source code changes do not include corresponding tests: if someone changes extraction logic but does not update the golden tests, or changes a template without updating the expected output, Danger flags it. Beyond that:

  • Categorizes all file changes into scopes (source, tests, golden-tests, docs, CI, build, tooling) and generates a summary table showing churn per scope
  • Validates commit messages against Conventional Commits format
  • Warns when a single commit exceeds 2,000 lines of source churn (encouraging smaller, reviewable slices)
  • Flags mismatched commit types (e.g., a feat: commit that only touches test files suggests test: instead)
  • Rejects PR descriptions under 40 characters
  • Ignores the test check for refactor-only PRs where the tests are expected to remain unchanged

Even when there are no warnings, the scope summary table gives reviewers an immediate sense of what a large PR touches. On a PR that changes 500 lines of source and 3,000 lines of golden tests, the table makes it clear that the bulk of the diff is expected test output, not new logic.

Danger.js commits
  • 6f5f6e9 ci: setup danger.js
  • 5429b2e ci(danger): align report table and add top-files summary
  • 240921d ci(danger): split PR target ci workflows
  • 08c46b6 ci(danger): correct file delta calculation in reports
  • 2cfd081 ci(danger): adjust large commit threshold
  • 71845c8 ci(danger): map root files into explicit scopes
  • 17b0a57 ci(danger): ignore test check for refactor-only PRs
  • 6481fd3 ci(danger): simplify CI naming
  • fd7d248 ci(danger): omit empty sections from report
  • 7502961 ci(danger): categorize util/bootstrap as build scope
  • 57e191e ci(danger): better markdown format

CI Infrastructure

We integrated Codecov for tracking test coverage across PRs and switched from GCC to Clang for coverage (more accurate AST-based measurement). CI speed was a recurring concern: we skipped remote documentation generation on PRs, sped up release demos, and skipped long tests that were not catching new bugs. LLVM cache keys were unified to avoid redundant builds, and CTest timeouts were increased for sanitizer jobs that run significantly slower. Matheus Izvekov contributed the Clang coverage switch, fixed an infinite recursion in extraction, and moved the project to use system libs by default.

CI infrastructure commits
  • ed6b3bc ci: add codecov configuration
  • 5426a0a ci: use clang for coverage
  • d629173 fix(ci): unify redundant LLVM cache keys
  • 36a3b51 ci: update actions to v1.9.1
  • 7b2103a ci: increase CTest timeout for MSan jobs
  • 086becc ci: increase the ctest timeout to 9000
  • adb6821 ci(cpp-matrix): remove the optimized-debug factor
  • 9507a38 ci: simplify CI workflow and upgrade cpp-actions to @develop
  • 9a5bd3c ci: skip remote documentation generation on PRs
  • 637011f ci: detect and report demo generation failures
  • 084322d ci: speed up release demos on PRs
  • 471951d ci: skip long tests to speed up CI
  • a5f160b ci: increase test coverage for the new XML generator
  • b1fc43c ci: exclude Reflection.hpp from coverage
  • a1f9a82 ci: accept any g++-14 version
  • c136a46 ci(website): preserve roadmap directory during deployment
  • 4763d86 revert(ci): remove premature roadmap report step
  • 3462996 ci: revert coverage changes
  • 8b2c3e9 ci: align llvm-sanitizer-config with archive basename
  • fdff573 ci: gitignore CI node_modules
  • 757d446 fix(ci): update the fmt branch reference from master to main
  • a3366b0 fix(ci): name rolling release packages after the branch

Test Infrastructure

MrDocs uses golden tests: the expected output for every test case is stored as a file, and the test runner compares the actual output against it. The most important change was adding multipage golden tests. Previously, all golden tests were single-page, but many bugs only manifested in multi-page output (cross-references between pages, navigation links, index generation). We were missing these entirely because we had no way to test them. We also added output normalization (so platform differences do not cause false failures) and regression categories so tests can be grouped and run selectively. A run_ci_with_act.py script lets contributors run the full CI pipeline locally using act.

Test infrastructure commits
  • bf78b1b test: support multipage golden tests
  • d7ad1ce test: output normalization
  • ccd7f71 test: check int tests results in ctest
  • 681b0cd chore: assign categories to regression tests
  • 9146125 test: cover additional paths in DocCommentFinalizer.cpp
  • 8326417 test: run_ci_with_act.py script
  • 5527e9c test: testClang_stdCxx default is C++26
  • 0dfdb02 test: –bad is disabled by default

Acknowledgments and Reflections

Going into the wild changed MrDocs. The edge cases, the customization requests, and the integration feedback shaped the direction more than any internal roadmap could.

Gennaro Prota drove the reflection integration that reduces maintenance burden across the entire codebase. Matheus Izvekov hardened CI with coverage, sanitizers, and warnings-as-errors, and migrated dependency management to the bootstrap script. Julio Estrada and Robert Beeston delivered the polished public face of MrDocs. Agustín Bergé contributed AST and metadata fixes including base member shadowing and alias SFINAE detection. Jean-Louis Leroy provided detailed feedback from Boost.OpenMethod that drove multiple improvements.

The most requested feature we have not solved yet is macro support (#1127). Macros are expanded before parsing and do not appear in the AST. Supporting them would require preprocessor-level integration with Clang. The work ahead also includes Lua scripting, metadata transforms, and deeper reflection, all direct responses to what users told us they need.

The biggest lesson from this period is that the problems worth solving are the ones users bring. We spent time on an unknowns framework to decide what to explore, but the most impactful work came from people who showed up with a broken demo page, a missing breadcrumb, or a duplicate ellipsis in their generated docs.

The complete set of changes is available in the MrDocs repository.

All Posts by This Author