Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* xref:config-file.adoc[]
* xref:commands.adoc[Documenting the Code]
* xref:generators.adoc[]
* xref:extensions.adoc[]
* xref:design-notes.adoc[]
* xref:reference:index.adoc[Library Reference]
* Contribute
Expand Down
121 changes: 121 additions & 0 deletions docs/modules/ROOT/pages/extensions.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
= Extensions

Extensions let you transform the corpus of extracted symbols before any generator runs.
A typical use case is rewriting metadata across many symbols at once: backfilling briefs from a naming convention, tagging symbols by group, or marking generated code as "see below" in the output.

Extensions are user-supplied scripts written in JavaScript or Lua.
They run between extraction (turning C++ source into a corpus of symbols) and rendering (turning the corpus into output files), so any change they make is visible to every generator.

== File layout

Drop a script into the extensions/ subdirectory of an addon root and Mr.Docs picks it up automatically:

[source,text]
----
<addons>/extensions/*.js
<addons>/extensions/*.lua
----

The same `addons` and `addons-supplemental` configuration options that govern templates apply here too: scripts under every existing addon root are discovered.

Scripts are loaded in alphabetical order by full path, with the two languages interleaved.
This means renaming a script changes the order in which it runs, but switching the language does not.

== The `transform_corpus` hook

A script extends Mr.Docs by exposing a function named `transform_corpus(corpus)`.
Mr.Docs calls it once with a flat read-only view of the corpus.
The script inspects symbols and calls mutation functions on the pre-registered `mrdocs` object to apply changes.

A script that does not define `transform_corpus` is silently ignored, so an extension file can be empty during development without breaking the build.

[source,javascript]
----
// <addons>/extensions/rebrief.js
function transform_corpus(corpus) {
for (var i = 0; i < corpus.symbols.length; ++i) {
var sym = corpus.symbols[i];
if (sym.kind === "function") {
mrdocs.set_brief(sym.id, "Rewritten: " + sym.name);
}
}
}
----

[source,lua]
----
-- <addons>/extensions/rebrief.lua
function transform_corpus(corpus)
local i = 0
while true do
local sym = corpus.symbols[i]
if sym == nil then break end
if sym.kind == "function" then
mrdocs.set_brief(
sym.id,
"Rewritten: " .. sym.name)
end
i = i + 1
end
end
----

== The `corpus` argument

The `corpus` argument is a read-only object with a single field, `symbols`, which is an array of every symbol Mr.Docs extracted.
Each entry in `corpus.symbols` exposes the following fields:

|===
|Field |Type |Description

|`id`
|string
|Stable opaque identifier. Pass this back to a mutation function to identify which symbol to act on.

|`kind`
|string
|The symbol kind, in lowercase kebab-case. For example: `function`, `record`, `namespace`, `enum`, `enum-constant`, `namespace-alias`.

|`name`
|string
|The unqualified symbol name. Empty for the global namespace.

|`qualifiedName`
|string
|The fully qualified name (for example, `std::vector::size`).

|`brief`
|string
|The current brief as plain text, or the empty string if the symbol has no brief.
|===

In JavaScript, iterate with `corpus.symbols.length` and `corpus.symbols[i]`, or with `for (var s of corpus.symbols)`.

In Lua, iterate by integer index starting at `0`; `corpus.symbols[i]` returns `nil` once you go past the last symbol, so a simple `while` loop terminates correctly.

== The `mrdocs` API

Mutations go through the pre-registered `mrdocs` global.
The current set is intentionally narrow; new entries land as concrete use cases surface.

=== `mrdocs.set_brief(symbol_id, text)`

Replace the symbol's brief with a single-paragraph plain-text block.

* `symbol_id`: the `id` value read from a symbol in `corpus.symbols`.
* `text`: the new brief text. The argument is treated as plain text; no markdown parsing is performed.

Each setter validates its arguments and raises an error on misuse.
An uncaught error inside an extension aborts the build with the script's path and the error message.

== Lifecycle

Extensions run between corpus finalization and the first generator invocation.
The order is:

. Mr.Docs walks the source files and extracts a corpus of symbols.
. Built-in finalizers post-process the corpus (for example, sorting members and resolving inheritance).
. Extensions run, in alphabetical order by full path.
. The selected generator renders the (possibly mutated) corpus.

Because step 3 happens before step 4, an extension that mutates a symbol's brief is visible to every output format, not just one.
50 changes: 50 additions & 0 deletions docs/modules/ROOT/pages/generators.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,56 @@ The layout template can include other partial templates to render the symbol dat

The Document Object Model (DOM) for each symbol includes all information about the symbol.One advantage of custom templates over post-processing XML files is the ability to access symbols as a graph.If symbol `A` refers to symbol `B`, some properties of symbol `B` are likely to be required in the documentation of `A`.All templates and generators can access a reference to `B` by searching the symbol tree or simply by accessing the elements `A` refers to.All references to other symbols are resolved in the templates.

[#custom-helpers]
=== Custom Helpers

Beyond the built-in helpers, an addon can register its own Handlebars helpers in JavaScript or Lua.
Drop a script alongside your partials and Mr.Docs picks it up automatically:

* `<addons>/generator/common/helpers/*.{js,lua}`: helpers available to every output format.
* `<addons>/generator/<generator>/helpers/*.{js,lua}`: helpers available only to the matching format (`html`, `adoc`, ...). A format-specific helper overrides a common one with the same name.

The file's stem (with the `.js` or `.lua` extension stripped) becomes the helper name.
Templates invoke it the same way they invoke a built-in helper:

[source,handlebars]
----
{{my_helper symbol.name}}
----

==== Helper resolution

Each script is expected to expose a single function, the helper:

* Return the function from the chunk (recommended):
+
[source,javascript]
----
return function(name) {
return "[" + name + "]";
};
----
+
[source,lua]
----
return function(name)
return "[" .. name .. "]"
end
----

* Or define a global with the same stem as the file (for example, `my_helper.lua` defining `function my_helper(name) ... end`).

==== Utility files

A file whose stem starts with an underscore (for example, `_utils.js`) is loaded first and is *not* registered as a helper.
Use these files to define globals that several helpers share, so a single utility script can set up state once instead of every helper duplicating it.

==== Arguments and the Handlebars options object

Mr.Docs strips Handlebars' trailing options object before forwarding arguments to the helper.
Helpers receive the positional arguments only and don't have to filter the options out themselves.
This also avoids expensive marshalling of symbol contexts, which contain circular references.

== Stylesheet Options

The HTML and AsciiDoc generators ship a bundled stylesheet that is inlined by default. You can replace or layer styles with the following options (available in config files and on the CLI):
Expand Down
29 changes: 18 additions & 11 deletions include/mrdocs/Support/Expected.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -388,22 +388,29 @@ namespace detail
# define MRDOCS_LABEL_(a) MRDOCS_MERGE_(expected_result_, a)
# define MRDOCS_UNIQUE_NAME MRDOCS_LABEL_(__LINE__)

// `detail::failed` and `detail::error` below are qualified with `::mrdocs::`
// so the macros remain correct when expanded inside another `detail`
// namespace (e.g. `mrdocs::lua::detail`): a qualified `detail::` lookup
// stops at the first matching nested `detail` and never falls through to
// `mrdocs::detail`. `Unexpected` and `Error` are left unqualified: ordinary
// scope walking finds them in `mrdocs::`.

/// Try to retrive expected-like type
# define MRDOCS_TRY_VOID(expr) \
auto MRDOCS_UNIQUE_NAME = expr; \
if (detail::failed(MRDOCS_UNIQUE_NAME)) { \
return Unexpected(detail::error(MRDOCS_UNIQUE_NAME)); \
if (::mrdocs::detail::failed(MRDOCS_UNIQUE_NAME)) { \
return Unexpected(::mrdocs::detail::error(MRDOCS_UNIQUE_NAME)); \
} \
void(0)
# define MRDOCS_TRY_VAR(var, expr) \
auto MRDOCS_UNIQUE_NAME = expr; \
if (detail::failed(MRDOCS_UNIQUE_NAME)) { \
return Unexpected(detail::error(MRDOCS_UNIQUE_NAME)); \
if (::mrdocs::detail::failed(MRDOCS_UNIQUE_NAME)) { \
return Unexpected(::mrdocs::detail::error(MRDOCS_UNIQUE_NAME)); \
} \
var = *std::move(MRDOCS_UNIQUE_NAME)
# define MRDOCS_TRY_MSG(var, expr, msg) \
auto MRDOCS_UNIQUE_NAME = expr; \
if (detail::failed(MRDOCS_UNIQUE_NAME)) { \
if (::mrdocs::detail::failed(MRDOCS_UNIQUE_NAME)) { \
return Unexpected(Error(msg)); \
} \
var = *std::move(MRDOCS_UNIQUE_NAME)
Expand All @@ -413,12 +420,12 @@ namespace detail

/// Check existing expected-like type
# define MRDOCS_CHECK_VOID(var) \
if (detail::failed(var)) { \
return Unexpected(detail::error(var)); \
if (::mrdocs::detail::failed(var)) { \
return Unexpected(::mrdocs::detail::error(var)); \
} \
void(0)
# define MRDOCS_CHECK_MSG(var, msg) \
if (detail::failed(var)) { \
if (::mrdocs::detail::failed(var)) { \
return Unexpected(Error(msg)); \
} \
void(0)
Expand All @@ -428,12 +435,12 @@ namespace detail

/// Check existing expected-like type and return custom value otherwise
# define MRDOCS_CHECK_OR_VOID(var) \
if (detail::failed(var)) { \
if (::mrdocs::detail::failed(var)) { \
return; \
} \
void(0)
# define MRDOCS_CHECK_OR_VALUE(var, value) \
if (detail::failed(var)) { \
if (::mrdocs::detail::failed(var)) { \
return value; \
} \
void(0)
Expand All @@ -442,7 +449,7 @@ namespace detail
MRDOCS_CHECK_GET_OR_MACRO(__VA_ARGS__, MRDOCS_CHECK_OR_VALUE, MRDOCS_CHECK_OR_VOID)(__VA_ARGS__)

# define MRDOCS_CHECK_OR_CONTINUE(var) \
if (detail::failed(var)) { \
if (::mrdocs::detail::failed(var)) { \
continue; \
} \
void(0)
Expand Down
67 changes: 67 additions & 0 deletions include/mrdocs/Support/Lua.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
// Copyright (c) 2023 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com)
//
// Official repository: https://github.com/cppalliance/mrdocs
//
Expand All @@ -21,6 +22,9 @@
#include <string_view>

namespace mrdocs {

class Handlebars;

/** Lua interop helpers for the optional scripting/backend integration.

This namespace contains glue for pushing/popping values, registering
Expand Down Expand Up @@ -117,6 +121,19 @@ class MRDOCS_DECL
/** Constructor.
*/
Context(Context const&) noexcept;

/** Return the underlying `lua_State*`.

Exposed as `void*` so callers don't have to drag `lua.h` into
the public API. Cast to `lua_State*` at the use site. The state
is owned by this Context and must not be `lua_close`d by the
caller; use this only when the wrapper does not yet abstract
the operation you need (for example, registering a native
C function that the script can call as a global).
*/
MRDOCS_DECL
void*
nativeState() const noexcept;
};

//------------------------------------------------
Expand Down Expand Up @@ -206,6 +223,20 @@ class Scope
std::string_view key,
source_location loc =
source_location::current());

/** Push a dom::Value onto the Lua stack.

Primitives (nil, boolean, integer, string) are pushed as their
Lua-native counterparts. Arrays and objects are pushed as
userdata wrapping the underlying dom container, with the same
lazy bindings used elsewhere in the wrapper.

@param value The DOM value to push.
@return A Value referring to the new stack slot.
*/
MRDOCS_DECL
Value
pushDom(dom::Value const& value);
};

//------------------------------------------------
Expand Down Expand Up @@ -607,6 +638,42 @@ class Table : public Value
Param value) const;
};

/** Register a Lua helper function

Register a Lua chunk as a Handlebars helper. The chunk source is
resolved to a callable in the following order:

1. **Chunk return value** - load and execute the chunk; if it returns
a function, use that. This is the idiomatic shape for a per-file
helper:
Example: `return function(x) return 'lua:' .. tostring(x) end`

2. **Global lookup** - if the chunk does not return a function, look
up the helper name on the global table. This handles chunks that
define a function as a side effect:
Example: `function helper_name(x) return tostring(x) end`

The resolved function is anchored in `LUA_REGISTRYINDEX` for the
lifetime of the registration. When the helper is invoked from a
template, positional arguments are converted from @ref dom::Value to
Lua values; the trailing Handlebars options object is dropped (matching
the JavaScript helper semantics) to avoid recursive marshalling of
symbol contexts.

@param hbs The Handlebars instance to register the helper into
@param name The name of the helper function
@param ctx The Lua context that anchors the helper closure
@param script The Lua source that defines the helper function
@return Success, or an error if the script could not be resolved to a function
*/
[[nodiscard]] MRDOCS_DECL
Expected<void, Error>
registerHelper(
mrdocs::Handlebars& hbs,
std::string_view name,
Context& ctx,
std::string_view script);

} // lua
} // mrdocs

Expand Down
8 changes: 8 additions & 0 deletions src/lib/CorpusImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <lib/Metadata/Finalizers/NamespacesFinalizer.hpp>
#include <lib/Metadata/Finalizers/OverloadsFinalizer.hpp>
#include <lib/Metadata/Finalizers/SortMembersFinalizer.hpp>
#include <lib/Extensions/RunExtensions.hpp>
#include <lib/Support/Chrono.hpp>
#include <lib/Support/Report.hpp>
#include <mrdocs/Metadata.hpp>
Expand Down Expand Up @@ -931,6 +932,13 @@ CorpusImpl::build(
// ------------------------------------------
corpus->finalize();

// ------------------------------------------
// Run user extension scripts
// ------------------------------------------
// Extensions fire after finalizers and before any generator runs,
// so any mutations they perform are visible to every output format.
MRDOCS_TRY(runExtensions(*corpus));

report::info(
"Extracted {} declarations in {}",
corpus->info_.size(),
Expand Down
Loading
Loading