feat: support addon-defined Handlebars generators#1197
Conversation
🧾 Changes by Scope
🔝 Top Files
|
97fc42c to
1d5651d
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1197 +/- ##
========================================
Coverage 82.12% 82.12%
========================================
Files 33 33
Lines 3149 3149
Branches 734 734
========================================
Hits 2586 2586
Misses 387 387
Partials 176 176
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
An automated preview of the documentation is available at https://1197.mrdocs.prtest2.cppalliance.org/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-05-29 14:24:07 UTC |
1d5651d to
b08c8a0
Compare
|
Add a brief note about how this change was tested (or why tests are not needed). :) |
|
Thanks. The "data-driven extension" framing is great, and the addon path is exactly the shape we were asked for. Scope of the data-driven rework I'm not so sure about removing the built-in The The rule is a bit subtle: a directory is or isn't a generator depending on whether some file inside it happens to be named after it. Could we make it explicit instead, like "a directory under Process-global registry in the test runner The deferred lookup is the right fix. One thing to flag: displayName I'm not sure how much this actually shows up for an addon-defined generator. The commit message says "shown in Single-byte escape keys The single-byte limit is too tight for something meant as a general way to add output formats. A few real cases it rules out:
The performance argument doesn't need to keep us stuck with single-byte keys. Keep the 256-entry array as the fast path for single-byte rules. Only check a second structure (a small vector of patterns, or a trie) when a multi-byte rule actually starts at this position. The hot path stays the same array lookup; the secondary path only runs when there's a longer pattern to match. So we get the longer patterns working without paying for it when no one uses them. |
This introduces `EscapeMap`, a pattern-replacement table used to escape rendered output values. Single-byte sources live in a 256-entry array indexed by `unsigned char`; multi-byte sources go into a secondary array of buckets keyed by first byte, so the walk pays nothing extra for the multi-byte machinery in the common case (most buckets are empty). When a bucket is non-empty, the longest match wins, so a `**` rule takes precedence over a `*` rule at the same position. The default `HandlebarsGenerator::escape()` walks the map. This is the path addon-defined Handlebars generators use: each format's rules come from data (loaded later from `mrdocs-generator.yml`), rather than being hardcoded in a C++ subclass. The built-in `adoc` and `html` generators keep their existing hand-written `escape()` overrides, because the table lookup is slightly slower than the compiled switch they had and those generators sit on the hot path. `HandlebarsGenerator::escape()` is therefore left virtual so the built-ins can override it. Multi-byte support is what makes the map workable as a general escape mechanism: it accommodates tokens like Markdown's `**` versus a literal `*`, RST's `` `` literal `` `` versus `*emphasis*`, and UTF-8 codepoints past ASCII that want to be replaced as a unit instead of byte by byte. The pre-existing public `HTMLEscape` helper is refactored to read from the same shared character-to-entity table (`htmlEscapeEntities`), so the table can be used both there and by an `EscapeMap` for any addon-defined HTML-like generator.
This moves `id`, `fileExtension`, and `displayName` from per-subclass virtual overrides to base-class members set through the constructor. That's a prerequisite for letting users add a Handlebars-based generator without writing C++.
The function was failing on `!file.good()` which includes the `eof()` case, so an empty file caused a failure. Test for "fail() but not just because we hit EOF", instead.
An <addon>/generator/<name>/ directory that ships an mrdocs-generator.yml file is now installed as an additional `HandlebarsGenerator` at config-resolve time. The manifest's mere presence is the explicit opt-in; its content is read for escape rules. An empty file is valid. This way, a generator can be added without writing any C++.
Generator lookup happened once at `TestRunner` construction. With addon-defined Handlebars generators now possible, a generator contributed via a test's `addons-supplemental` was unreachable from the test binary: the lookup ran before that test's mrdocs.yml had been loaded, so the generator wasn't yet registered. The runner now defers the lookup. The built-in generators are not affected.
This adds a fixture under test-files/template-only-generators/mock-md that ships its own mock "Markdown-like" generator via an addon-local `addons-supplemental`: a one-line layout that emits the symbol name, plus a single escape rule mapping `_` to `\_`.
bbe59b8 to
bcb2e1f
Compare
|
Thank you for the thorough review, as usual. I addressed all of your points. Specifically: Scope of the data-driven rework Agreed and reverted. The built-in The Switched to manifest-as-discriminator. A directory under Process-global registry in the test runner Added comments at displayName You're right that the only runtime surface for an addon-defined generator's Single-byte escape keys Implemented your sketch. |
|
LGTM. Another set of non-blocking comments worth having in mind.
One point I forgot to make, but that's interesting to have in mind, is that we wrote this function before the mrdocs::Expected object existed. But nowadays it would make much more sense to just register the generator with a fimples Anyway, not a blocker but worth having in mind and fixing in passing, and there are more commits or if we're going to squash the commits anyway.
It's also good to have in mind this is some technical debt we have to deal with later. There are too many globals in mrdocs. Also, a lot of "semi-global" variables where all the mrdocs steps keep using references for everything instead of the data having an owner. |
|
Yes 🙂. Although I usually prefer writing "if and only if", which leaves no
doubt as to whether it's a typo or not. I used italics to mitigate that.
|

This adds support for Handlebars-based generators that don't involve any C++ code. <addon>/generator/<name>/ directories are picked up at config-resolve time and installed as additional generators; escape rules can be provided via <name>/mrdocs-generator.yml.
Adds support for addon-defined Handlebars generators: any
<addon>/generator/<name>/directory is picked up at config-resolve time and installed as a generator, with escape rules and other per-generator options described in<name>/mrdocs-generator.yml. New output formats can now be added entirely from configuration and template files, without touching the C++ generator infrastructure.To make this work cleanly the generator subsystem is made data-driven. Escape rules previously lived in a dedicated
AdocEscapesubclass; they now come from data, and the file is removed. Each output format used to have its ownHandlebarsGenerator/AdocGenerator/HTMLGeneratorsubclass; those classes are collapsed into thin wrappers over a shared data-driven implementation. The test runner was rearranged to match: generator lookup is deferred until the per-test settings load, since addon-defined generators now register dynamically rather than at static init.A small bug fix rides along:
getFileTextno longer fails on empty files.Changes
src/lib/Gen/hbs/AddonGenerators.{cpp,hpp}discovers and registers addon-defined generators at config-resolve time, wired intosrc/tool/GenerateAction.cpp. The manifest understands an optionalescapemapping (per-character replacement table) and an optionaldisplayNamescalar; both fall back to sensible defaults when absent.src/lib/Gen/adoc/AdocEscape.{cpp,hpp}removed; escape rules now come from data consumed by the existing generators.AdocGenerator,HTMLGenerator, andHandlebarsGeneratorreworked to read their options from data; the per-format subclass headers shrink correspondingly.src/lib/Support/Handlebars.cppand the publicinclude/mrdocs/Support/Handlebars.hppgain a small surface for the new flow.src/lib/ConfigOptions.jsonanddocs/mrdocs.schema.jsonupdated for the new config keys.src/lib/Support/Path.cppfix forgetFileTexton empty files.src/test/lib/Gen/hbs/AddonGenerators.cppunit test covers generator discovery and registration.src/test/Support/TestLayout.{cpp,hpp}andsrc/test/TestRunner.{cpp,hpp}reworked to defer generator lookup until per-test settings load, so dynamically registered generators are visible to the runner. New end-to-end fixture undertest-files/template-only-generators/mock-md/ships a complete addon (addons/generator/mock-md/withmrdocs-generator.ymland Handlebars layouts) and asimple.mock-mdexpected output.CMakeLists.txtupdated to install the new sources and pick up the new test fixture (+38 lines).util/generate-config-info.pyafter a config-option rename.AdocGenerator,HTMLGenerator,HandlebarsGenerator) and the removedAdocEscapeare internal — downstream code that included those headers directly will need to use the shared data-driven entry points. TheConfigOptions.json/mrdocs.schema.jsonschema gains keys but removes none.Testing
src/test/lib/Gen/hbs/AddonGenerators.cppis the unit-level coverage for the new discovery/registration logic.test-files/template-only-generators/mock-md/is the end-to-end fixture: a complete addon-defined generator (template-only, no C++) wired together with anmrdocs-generator.ymland asimple.cppinput that producessimple.mock-md. Any regression in discovery or in the data-driven generator pipeline fails this fixture.src/test/andtest-files/jobs already run both the unit test and the end-to-end fixture on every build.Documentation
docs/mrdocs.schema.jsonupdated to reflect the newmrdocs-generator.ymlkeys.docs/modules/ROOT/pages/generators.adoccovering the user workflow: directory layout, the*.<name>.hbsfilter that gates whether a directory is picked up as a generator, the optionalmrdocs-generator.ymlkeys, and the first-addon-wins layering rule.