Skip to content

feat: support Lua and JavaScript extensions#1196

Open
gennaroprota wants to merge 12 commits into
cppalliance:developfrom
gennaroprota:feat/support_scripting_extensions
Open

feat: support Lua and JavaScript extensions#1196
gennaroprota wants to merge 12 commits into
cppalliance:developfrom
gennaroprota:feat/support_scripting_extensions

Conversation

@gennaroprota
Copy link
Copy Markdown
Collaborator

@gennaroprota gennaroprota commented May 7, 2026

This mirrors the existing JavaScript helpers for Lua, and adds support for corpus-mutation extensions written either in Lua or Javascript.


Adds two related script-extension capabilities and the supporting infrastructure to expose mrdocs' reflection model to those scripts:

  1. Lua scripts as Handlebars helpers — mirrors the existing JavaScript helper surface, so generators can call into Lua from templates the same way they already call into JS, and helpers compose across addon directories (layering).
  2. Corpus-mutation extensions in Lua and JavaScript — scripts under addons/extensions/ run after the corpus is built and can rename, retag, or otherwise mutate symbols before generation.

To make the script surface coherent rather than a hand-maintained binding table, the extension layer is driven by reflection: a new Describe.hpp exposes types through their reflection metadata, a new MRDOCS_DESCRIBE_HIERARCHY macro lets polymorphic bases describe their derived hierarchy, and the existing Polymorphic<T> type is now usable from mrdocs.set. Scripts therefore see a typed view of the model that stays in sync with the C++ types automatically as new symbol/type kinds are added.

A small dom::Object bug surfaced during the work — field lookup was returning the key instead of the value — and is fixed in the same PR.

Changes

  • Source: New src/lib/Extensions/ (RunExtensions.{cpp,hpp}) runs Lua/JS corpus-mutation scripts; CorpusImpl.cpp invokes it after corpus construction. src/lib/Support/Lua.cpp and include/mrdocs/Support/Lua.hpp grow to support binding mrdocs' reflection types into Lua (including Polymorphic<T>). src/lib/Gen/hbs/Builder.cpp updated to load Lua helpers alongside the existing JS ones. New include/mrdocs/Support/Describe.hpp, the MRDOCS_DESCRIBE_HIERARCHY macro, and seven new *Hierarchy.hpp headers under include/mrdocs/Metadata/ apply the macro to symbols, types, names, template args/params, and doc-comment blocks/inlines. dom::Object field-lookup fix.
  • Tests: All exercised through golden tests; no separate unit tests added.
  • Golden tests: New extension test trees extensions/js-set-name/ and extensions/lua-set-name/ exercise corpus-rename scripts in JS and Lua. New generator/hbs/lua-helper/ and generator/hbs/lua-helper-layering/ exercise Lua Handlebars helpers, including helper layering across multiple addon directories. All intentional output additions, not regenerations.
  • Docs: New docs/modules/ROOT/pages/extensions.adoc documenting the scripting model; generators.adoc updated for the Lua helper surface and the layering model.
  • Third-party: third-party/patches/lua/CMakeLists.txt updated for the vendored Lua build.
  • Breaking changes: None on the user-facing CLI. Public reflection headers gain new entries (Describe.hpp, the *Hierarchy.hpp set) which is purely additive. Downstream code that specialises reflection traits manually should check whether MRDOCS_DESCRIBE_HIERARCHY now applies to types it was previously describing by hand.

Testing

  • The four golden-test trees added under test-files/golden-tests/extensions/ and test-files/golden-tests/generator/hbs/lua-helper*/ are the primary coverage: they execute real Lua and JavaScript scripts against fixed input fixtures and assert the resulting .xml / .html / .adoc output byte-for-byte. Any future regression in helper or extension behavior fails these tests.
  • No CI workflow changes needed — the existing golden-test job runs all of test-files/golden-tests/ on every build, so the new trees stay covered automatically going forward.

Documentation

  • New docs/modules/ROOT/pages/extensions.adoc documents corpus-mutation extensions in both Lua and JavaScript, the lookup paths (addons/extensions/), and the available script API.
  • docs/modules/ROOT/pages/generators.adoc updated to cover the Lua helper surface alongside the existing JS one, and the helper-layering model across multiple addon directories.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

✨ Highlights

  • 🧪 New golden tests added

🧾 Changes by Scope

Scope Lines Δ% Lines Δ Lines + Lines - Files Δ Files + Files ~ Files ↔ Files -
🛠️ Source 64% 2071 2015 56 18 9 9 - -
🥇 Golden Tests 27% 862 862 - 49 49 - - -
📄 Docs 8% 250 242 8 5 2 3 - -
🤝 Third-party 1% 31 18 13 1 - 1 - -
Total 100% 3214 3137 77 73 60 13 - -

Legend: Files + (added), Files ~ (modified), Files ↔ (renamed), Files - (removed)

🔝 Top Files

  • src/lib/Extensions/RunExtensions.cpp (Source): 1120 lines Δ (+1120 / -0)
  • src/lib/Support/Lua.cpp (Source): 276 lines Δ (+268 / -8)
  • src/lib/Gen/hbs/Builder.cpp (Source): 144 lines Δ (+111 / -33)

Generated by 🚫 dangerJS against f68a074

@gennaroprota gennaroprota force-pushed the feat/support_scripting_extensions branch from ef7ea6b to 2156b1c Compare May 7, 2026 10:42
@codecov
Copy link
Copy Markdown

codecov Bot commented May 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.12%. Comparing base (7e73057) to head (f68a074).
⚠️ Report is 8 commits behind head on develop.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #1196   +/-   ##
========================================
  Coverage    82.12%   82.12%           
========================================
  Files           33       33           
  Lines         3149     3149           
  Branches       734      734           
========================================
  Hits          2586     2586           
  Misses         387      387           
  Partials       176      176           
Flag Coverage Δ
bootstrap 82.12% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cppalliance-bot
Copy link
Copy Markdown

cppalliance-bot commented May 7, 2026

An automated preview of the documentation is available at https://1196.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-19 16:19:08 UTC

@gennaroprota gennaroprota changed the title feat(handlebars): support Lua scripts as Handlebars helpers feat(handlebars): support Lua and JavaScript extensions May 7, 2026
@gennaroprota gennaroprota changed the title feat(handlebars): support Lua and JavaScript extensions feat: support Lua and JavaScript extensions May 7, 2026
@gennaroprota gennaroprota force-pushed the feat/support_scripting_extensions branch 2 times, most recently from 63f542c to 8c6f453 Compare May 7, 2026 16:34
This mirrors the existing JS helpers for Lua. *.lua files placed in an
addon's generator/{common|<ext>}/helpers/ directory are auto-registered
as Handlebars helpers; files whose name starts with '_' run first as
utility scripts. Two golden fixtures (lua-helper/, lua-helper-layering/)
mirror their JS counterparts and cover the `addons-supplemental`
override.

Incidental fixes to issues uncovered by this patch:

- Added a qualification to `MRDOCS_TRY` / `MRDOCS_CHECK_*` /
  `MRDOCS_CHECK_OR_*` / `MRDOCS_CHECK_OR_CONTINUE` to make them work
  with nested namespaces named `detail`.

- Dropped onelua.c and ltests.c from the Lua build patch, because the
  former defines `main`, which conflicted with our `main`, and the
  latter is test scaffolding which shouldn't ship in a library build.

- Added `extern "C"` around the Lua includes.
The `__index` metamethod in `domObject_push_metatable()` retrieved the
value correctly via `Object::get(key)`, then called `lua_replace(L, 1)`
to move the result into the userdata's slot. `lua_replace` also pops the
top, so, on return, the key string was at the top of the stack and Lua
picked it up as the metamethod's single return value, making every field
access on a `dom::Object` userdata silently return the key it was asked
for.

This was latent until now because no Lua script in the test suite
previously read fields off a `dom::Object` userdata. Surfaced while
wiring corpus extensions: a script doing `corpus.symbols[i]` saw
`"symbols"` (the key) instead of the array.
@gennaroprota gennaroprota force-pushed the feat/support_scripting_extensions branch 2 times, most recently from ad08eed to db26b0d Compare May 11, 2026 10:39
This adds a hook that runs user-provided Lua scripts after corpus
extraction and finalization, before any generator runs. Extensions live
in <addon>/extensions/*.lua for each addon root in the configuration. A
script may define `transform_corpus(corpus)`, which is invoked once with
a flat DOM view of the corpus. The script may mutate the corpus by
calling pre-registered globals on the `mrdocs` table; currently:

- `mrdocs.set_brief(symbol_id, text)`: replace a symbol's brief with a
  single-paragraph plain-text block.

Each setter validates its arguments and raises a Lua error on misuse;
any uncaught error in a script aborts the build. Multiple extensions run
in alphabetical order by file path. The mutation surface is
intentionally narrow; additional setters will land as concrete use cases
surface.

A golden fixture (test-files/golden-tests/extensions/lua-set-brief/)
rewrites a function's brief from Lua and verifies the change reaches the
xml output.

Finally, this touches Lua wrapper for two new affordances:
`Scope::pushDom` for the corpus argument, and a `Context::nativeState()`
escape hatch for binding native C functions as Lua globals (the wrapper
doesn't abstract that yet).
This mirrors the Lua corpus-mutation hook for JavaScript. Extension
scripts under <addon>/extensions/*.js can now define
`transform_corpus(corpus)` and call `mrdocs.set_brief(symbol_id, text)`,
just like their Lua counterparts. The Lua and JS bindings now share a
language-agnostic `setBriefImpl` helper that takes already-extracted
`dom::Value` arguments. Each binding is a thin adapter:

- Lua: the existing C closure registered on the raw `lua_State*`.
- JS: a `dom::Function` exposed as a property of a `mrdocs` global
  object; the wrapper's `setGlobal` -> `toJsValue` ->
  `makeFunctionProxy` chain handles the rest.

Discovery picks up both *.lua and *.js, sorted together so script
ordering doesn't depend on the chosen language. A golden fixture mirrors
the Lua test.
@gennaroprota gennaroprota force-pushed the feat/support_scripting_extensions branch 3 times, most recently from e58fb63 to 075f7bc Compare May 13, 2026 14:36
Copy link
Copy Markdown
Collaborator

@alandefreitas alandefreitas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR is great. It's really nice to see we already have examples that transform the corpus via the scripts.

I don't see much value in putting Lua scripts as Handlebars helpers (a cheap improvement win in an extension type that already exists) with Corpus-mutation extensions in Lua and JavaScript (a completely new feature that's huge).

a new Describe.hpp

Doesn't it already exist? And wasn't the distinction between Describe.hpp and other headers related to reflection that Describe.hpp would only be about porting Boost.Describe?

MRDOCS_DESCRIBE_HIERARCHY

I believe this needs better in-source documentation. We even support that now (or are about to).

I also don't understand these *Hierarchy.hpp files that much. The PR describes that they exist, but not why they exist. All I know is they're using this pattern of including everything, which defeats the purpose of public headers. I also don't see why the user ever needs that information to be public.

They also seem to defeat the purpose of things in a sense. Because we're trying to avoid manually listing things with reflection, but we are still doing it there. The typical pattern we use for that is a single source of truth as inc files.

I also don't understand how a hierarchy can be described as a list unless there's some special convention about this list.

Tests

There's enough new functionality (even public functionality) here that seems to deserve unit tests.

Golden tests

Do they really cover everything that's possible? What are the typical use cases for this? Do we get errors on invalid transformations? How do we merge symbols in the corpus? How do you add symbols to the corpus?

Third-party: third-party/patches/lua/CMakeLists.txt updated for the vendored Lua build.

What do you mean by vendored Lua build here?

  • No CI workflow changes needed — the existing golden-test job runs all of test-files/golden-tests/ on every build, so the new trees stay covered automatically going forward.

Documentation

I think the documentation should make the same distinction you made here. It could maybe be described as corpus-mutation extensions or something. Because we have this concept of extensions. Then, things people understand as extensions are these customizations of the template system and the corpus-mutation extensions. They are very different layers and share a common logic of knowing where to put addons, etc. They also have to know about supplemental addons, which they're probably going to use the most. The documentation concerns the user journey.

Also, any reference should be generated automatically. I don’t understand the API very well. Is mrdocs.set the only function we have?

I also don’t see the pattern we had discussed about taking inspiration from Lua/Darktable. It seems the new extension system uses the same pattern as the helpers extension system, but that pattern is not appropriate there. What if an extension wants to provide different functionalities? At a minimum, it's important to include a comparison and an explanation of why the PR chose a different strategy.

Is that the best Python and Lua API we could have on the Lua/JS side? What are the alternatives? They look very different from what things look like on the C++ side. Are they extendable, or will we need to break the Lua/JS API every time there’s a new feature?

The "Corpus argument" section describes a subset of a symbol's API (I don't really understand why, since the full reference is elsewhere), but the API of the corpus object itself isn't described.

@gennaroprota
Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. Great stuff in there :-). Here's a point by point reply:

  • Splitting Lua scripts as Handlebars helpers: yes, I will.
  • A new Describe.hpp: there's no new Describe.hpp (AI hallucinated :-)). You are right that the file started as the Boost.Describe port, but the hierarchy machinery looked like a natural extension. I can move that part to a separate file, if you like.
  • Documentation of MRDOCS_DESCRIBE_HIERARCHY: yes, will expand it.
  • Why the various *Hierarchy.hpp exist: the macro static-asserts is_base_of<Base, D> for each derived class, so they must be complete at the macro's expansion point. We can't put it in the base's header (because derived classes are not visible there), and can't put it in each derived's header (because the macro needs them all at once).
  • Why are they public: good point. I'll move them to src/.
  • Driving lists from .inc files: yes, I will refactor.
  • "Hierarchy" as a flat list: fair, the term "hierarchy" is too broad. The macro actually describes a flat set of leaf derived classes sharing a base. Deeper inheritance isn't modeled (and we don't need it). I can rename it to "MRDOCS_DESCRIBE_DERIVED_LEAVES" or "MRDOCS_DESCRIBE_MOST_DERIVED", if you like.
  • Unit tests: agreed. Will add coverage.
  • Golden coverage:
    • Errors on invalid transformations: not golden tested (a golden of a failing build is awkward). I'll unit-test them.
    • Merging symbols: intentionally not supported, as structural changes can break corpus invariants.
    • Adding symbols: likewise.
  • "Vendored Lua build": I just meant that the Lua source is in-tree under third-party/lua/ and built as a part of MrDocs. third-party/patches/lua/CMakeLists.txt is MrDocs's CMake override for that in-tree build (upstream Lua doesn't ship a CMakeLists) and the PR diff is to that override, not to upstream Lua.
  • Documentation: agreed. The current single extensions.adoc conflates two layers. Will restructure into (a) template helpers (both JS and Lua), (b) corpus-mutation extensions (both languages), (c) a shared "Addons" section covering lookup paths, addons vs. addons-supplemental, and how both file types are discovered under the same roots.
  • Auto-generated reference: Yes.
  • Is mrdocs.set the only function? Yes (but see the next item).
  • Darktable comparison: Yes, you are right that I didn't follow through. Just wanted the simplest thing that worked first. But the simplest thing has obvious drawbacks. I'll switch, and add a comparison/rationale to the docs.
  • Is this the best API? Honest framing: it's defensible but not necessarily optimal. I considered the following alternatives:
    A. Domain-specific helpers (mrdocs.rename, mrdocs.deprecate, ...). Stable and readable, but each one hand-written and must be kept in sync with C++.
    B. Stable script-side schema independent of C++ names. A separate schema for the script API where field names aren't C++ identifiers. Scripts don't break when C++ fields are renamed. Cost: a schema to maintain alongside the C++ types.
    C. Direct DOM manipulation. Mutable corpus; scripts assign fields directly. Most "natural" shape, but no invariants protection.
    D. Reflection-driven generic setter (this PR): one function, dispatch by field name, allowlist gate, reflective sub-dispatch for nested and polymorphic types. Stays in sync with C++ for free; the contract is "the normalized C++ member name is the script API name."
    I went with (D) because it auto-tracks C++ changes and keeps the surface narrow. The weakness is what you'd expect: renaming an allowlisted member would break scripts (though aliases would mitigate).
  • Why it looks different from the C++ side: different consumers and constraints: dynamic-typed and sandboxed vs. strongly typed and refactor-safe. The read shape isn't bespoke: scripts see the same DOM the generators already use for rendering. Only the write surface is new, deliberately narrow, and it goes through one gated function rather than direct field assignment.
  • Extensibility and breakage:
    • New described C++ fields -> Automatically appear in the script DOM. No script API change.
    • New writable fields -> Need an allowlist addition.
    • New mrdocs.* functions -> Additive.
      Breakage only on rename or removal of an allowlisted field (aliases mitigate if needed).
  • "Corpus argument" section: fair point. The current section doesn't describe corpus itself. I'll rewrite.

Open decisions for you:

  • Describe.hpp split.
  • MRDOCS_DESCRIBE_DERIVED rename.

This completes the work on extensions by making both the read and the
write view of scripts lay on the describe machinery already used by the
rest of the code. Scripts can now read any described field, and write
any field on an (intentionally small) allowlist.
@gennaroprota gennaroprota force-pushed the feat/support_scripting_extensions branch from 4ae57c0 to 22b6462 Compare May 19, 2026 09:47
This adds a MRDOCS_DESCRIBE_HIERARCHY macro that registers the derived
classes of a polymorphic base. This lets generic code dispatch over the
closed set of derived types without the per-base X-macro boilerplate
every consumer would otherwise need.
This wires the macro added in the previous commit to every polymorphic
base in MrDocs. Each registration lives in a small private include
under src/lib/Metadata/. The hierarchy info is a compile-time
consumer-side concern, so no public header exposes it; consumers
include the relevant file directly.

This commit only registers the relationships. The next one will
introduce the first consumer.
The previous step's generic setter could navigate through `Optional`,
`vector` and described structs, but not `Polymorphic<T>`, the type used
throughout MrDocs for type-erased value pointers. It can now.

The generic setter dispatches a `kind:` field against the closed set of
derived classed registered via `MRDOCS_DESCRIBE_HIERARCHY`, forwards the
remaining DOM keys to the matched class, and writes the result into the
`Polymorphic<T>`.

The Lua adapter gains a recursive table-to-DOM converter; `doc` and
`loc` are added to the allowlist.
Some library types are better presented in documentation by their
semantic role than by their literal C++ form: e.g., a function returning
`capy::task<T>` is more useful to readers when documented as a coroutine
yielding `T` than when shown with its literal task-object return type.

Hence, this adds `returnType` to the script-side allowlist.

This is the substrate for semantic-rendering extensions, not the fully
declarative API one might eventually expose ("declare this type an
awaitable, MrDocs handles the rest"). That higher-level API can be built
on top of this hook.
Replace the duplicated subset of symbol-fields with a clear "what
`corpus` actually is" lead. Note explicitly that lookups, name
searches and other queries are not exposed on `corpus`; scripts
walk `corpus.symbols` and filter. For per-symbol fields, link out
to the templates documentation rather than duplicate a subset.
Template helpers (generators.adoc) and corpus-mutation extensions
(extensions.adoc) live at different layers but share the same
addon-discovery machinery. Both pages were duplicating the explanation
of `addons` and `addons-supplemental`, and neither covered the user
journey of "what is an addon, where does it sit?".

This adds `addons.adoc` as the canonical page for the addon concept: the
built-in addon, replacement via `addons`, supplementation via
`addons-supplemental`, the on-disk layout, and the discovery rule
(last-wins for templates/helpers, full aggregation for extensions).
`extensions.adoc` and `generators.adoc` drop their local explanations
and cross-link to it.
@alandefreitas
Copy link
Copy Markdown
Collaborator

Open decisions for you:

Up to you. I'll let you come up with the best design you can and we can iterate on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants