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