Skip to content

Commit da1f43c

Browse files
committed
refactor(extensions): register scripts through a mrdocs object, not globals
`register_transform` and `register_generator` were bare globals in the extension environment. This moves them onto a `mrdocs` global object, the way `console` is already provided, so a script writes `mrdocs.register_transform(fn)` / `mrdocs.register_generator(id, fn)`. Among Lua-embedding applications that expose a "register an extension" call, a host namespace object is the prevailing convention - darktable uses `dt.register_*`, Aegisub `aegisub.register_*`, Redis `redis.register_function` - while bare globals are the exception. The main advantage of exposing an object is that it makes the surface discoverable. Note that the per-invocation `ctx` (`ctx.corpus`, `ctx.output`, `ctx.config`) is unchanged: only load-time declarations go through `mrdocs`.
1 parent d6fc2cd commit da1f43c

30 files changed

Lines changed: 96 additions & 82 deletions

File tree

docs/modules/ROOT/pages/extensions/corpus-extensions.adoc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ An extension is a Lua or JavaScript script that runs as part of a Mr.Docs build
55
* A *corpus transform* rewrites metadata across many symbols at once: backfill briefs from a naming convention, tag symbols by group, mark generated code as "see below" in the output. Transforms run between extraction and rendering, so every generator sees the change.
66
* A *generator* owns the whole emit: instead of rendering one page per symbol, it traverses the corpus and writes whatever files it wants. That lets it produce shapes the per-page generators cannot, such as a single artifact aggregated across every symbol.
77
8-
A script declares a transform with `register_transform(fn)` and a generator with `register_generator(id, fn)`. Either way the registered function receives a single context object, `ctx`.
8+
A script declares a transform with `mrdocs.register_transform(fn)` and a generator with `mrdocs.register_generator(id, fn)`. Either way the registered function receives a single context object, `ctx`.
99

1010
== Languages and addon locations
1111

@@ -33,7 +33,7 @@ A script can register any number of transforms and generators, or none at all. I
3333
[#corpus-transforms]
3434
== Corpus transforms
3535

36-
A transform is a function passed to `register_transform`. Mr.Docs invokes each registered function once, in registration order, with the `ctx` object. A flat view of the corpus reaches the script through `ctx.corpus`.
36+
A transform is a function passed to `mrdocs.register_transform`. Mr.Docs invokes each registered function once, in registration order, with the `ctx` object. A flat view of the corpus reaches the script through `ctx.corpus`.
3737

3838
[tabs]
3939
======
@@ -65,7 +65,7 @@ JavaScript::
6565
.`addons/extensions/count_by_kind.js`
6666
[source,javascript]
6767
----
68-
register_transform(function(ctx) {
68+
mrdocs.register_transform(function(ctx) {
6969
var counts = {};
7070
for (var i = 0; i < ctx.corpus.symbols.length; ++i) {
7171
var k = ctx.corpus.symbols[i].kind;
@@ -82,7 +82,7 @@ Lua::
8282
.`addons/extensions/count_by_kind.lua`
8383
[source,lua]
8484
----
85-
register_transform(function(ctx)
85+
mrdocs.register_transform(function(ctx)
8686
local counts = {}
8787
for _, sym in ipairs(ctx.corpus.symbols) do
8888
counts[sym.kind] = (counts[sym.kind] or 0) + 1
@@ -225,7 +225,7 @@ Notice in this example that `s.doc.sees` receives a list of polymorphic types th
225225

226226
The built-in generators render one page per symbol. When you need a different output structure, e.g. one file per namespace, or a single artifact aggregated across every symbol such as a search index, that page-per-symbol shape cannot express it. A generator hands the whole emit to the script instead: it traverses the corpus and writes whatever files it wants. No C++ and no templates are involved.
227227

228-
A script declares a generator with `register_generator(id, fn)`. The `id` is the name you select on the command line with `--generator=<id>`; a registered generator takes precedence over a built-in of the same name. Selecting a generator is a request for output, so its function does the work directly, the page-per-symbol fallback the built-ins provide does not apply.
228+
A script declares a generator with `mrdocs.register_generator(id, fn)`. The `id` is the name you select on the command line with `--generator=<id>`; a registered generator takes precedence over a built-in of the same name. Selecting a generator is a request for output, so its function does the work directly, the page-per-symbol fallback the built-ins provide does not apply.
229229

230230
The function receives the same `ctx` a transform does, plus `ctx.output`:
231231

docs/mrdocs.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@
282282
"default": [
283283
"html"
284284
],
285-
"description": "The generator is responsible for creating the documentation from the extracted symbols. The generator uses the extracted symbols and the templates to create the documentation. The built-in generators include `adoc`, `html`, `xml`, and `noop` (which extracts but writes no output, useful for checking extraction warnings); data-driven generators can be added by dropping a template folder under <addon>/generator/<name>/; a script-driven generator is declared by an extension with `register_generator` and produces the output itself. This option accepts a single generator (`generator: xml`), a list (`generator: [xml, adoc]`), or a comma-separated string (`generator: \"xml,adoc\"`); when more than one is given the documentation is produced once per generator.",
285+
"description": "The generator is responsible for creating the documentation from the extracted symbols. The generator uses the extracted symbols and the templates to create the documentation. The built-in generators include `adoc`, `html`, `xml`, and `noop` (which extracts but writes no output, useful for checking extraction warnings); data-driven generators can be added by dropping a template folder under <addon>/generator/<name>/; a script-driven generator is declared by an extension with `mrdocs.register_generator` and produces the output itself. This option accepts a single generator (`generator: xml`), a list (`generator: [xml, adoc]`), or a comma-separated string (`generator: \"xml,adoc\"`); when more than one is given the documentation is produced once per generator.",
286286
"title": "Generator(s) used to create the documentation"
287287
},
288288
"global-namespace-index": {

examples/generators/script-driven/search-index/addons/extensions/search_index.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
-- aggregates every symbol into a single search-index.json, the kind of
33
-- artifact the per-page generators cannot produce.
44
--
5-
-- `register_generator(id, fn)` declares it next to any
6-
-- `register_transform` a script might also declare; selecting
5+
-- `mrdocs.register_generator(id, fn)` declares it next to any
6+
-- `mrdocs.register_transform` a script might also declare; selecting
77
-- `generator: <id>` runs `fn` with one `ctx`. `ctx.corpus.symbols` is
88
-- every symbol (each tagged with a flat `_id` so the generator can form
99
-- stable per-symbol URLs) and `ctx.output.write` emits files under the
@@ -15,7 +15,7 @@ local function json_string(s)
1515
return '"' .. s .. '"'
1616
end
1717

18-
register_generator("search-index", function(ctx)
18+
mrdocs.register_generator("search-index", function(ctx)
1919
local entries = {}
2020
for _, sym in ipairs(ctx.corpus.symbols) do
2121
local name = sym.name or ""

src/lib/ConfigOptions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@
443443
{
444444
"name": "generator",
445445
"brief": "Generator(s) used to create the documentation",
446-
"details": "The generator is responsible for creating the documentation from the extracted symbols. The generator uses the extracted symbols and the templates to create the documentation. The built-in generators include `adoc`, `html`, `xml`, and `noop` (which extracts but writes no output, useful for checking extraction warnings); data-driven generators can be added by dropping a template folder under <addon>/generator/<name>/; a script-driven generator is declared by an extension with `register_generator` and produces the output itself. This option accepts a single generator (`generator: xml`), a list (`generator: [xml, adoc]`), or a comma-separated string (`generator: \"xml,adoc\"`); when more than one is given the documentation is produced once per generator.",
446+
"details": "The generator is responsible for creating the documentation from the extracted symbols. The generator uses the extracted symbols and the templates to create the documentation. The built-in generators include `adoc`, `html`, `xml`, and `noop` (which extracts but writes no output, useful for checking extraction warnings); data-driven generators can be added by dropping a template folder under <addon>/generator/<name>/; a script-driven generator is declared by an extension with `mrdocs.register_generator` and produces the output itself. This option accepts a single generator (`generator: xml`), a list (`generator: [xml, adoc]`), or a comma-separated string (`generator: \"xml,adoc\"`); when more than one is given the documentation is produced once per generator.",
447447
"type": "string-list",
448448
"default": ["html"]
449449
},

src/lib/CorpusImpl.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class CorpusImpl final : public Corpus
6262
std::map<SymbolID, UnorderedStringMap<Symbol const*>> lookupCache_;
6363

6464
// Output generators an extension script defined via
65-
// `register_generator(id, fn)`. Each `fn` is a `dom::Function` that
65+
// `mrdocs.register_generator(id, fn)`. Each `fn` is a `dom::Function` that
6666
// stays runnable until this corpus is destroyed (after extensions run,
6767
// when a generator is selected). Its scripting VM is kept alive either
6868
// by the function itself or by a matching entry in
@@ -217,8 +217,8 @@ class CorpusImpl final : public Corpus
217217

218218
/** Register a script-defined output generator.
219219
220-
Called from an extension's `register_generator(id, fn)`. The first
221-
registration of a given id wins; later ones are ignored.
220+
Called from an extension's `mrdocs.register_generator(id, fn)`.
221+
The first registration of a given id wins; later ones are ignored.
222222
*/
223223
void
224224
registerScriptGenerator(std::string id, dom::Function fn);
@@ -230,7 +230,7 @@ class CorpusImpl final : public Corpus
230230

231231
/** Keep a scripting VM alive for the lifetime of this corpus.
232232
233-
A generator registered via `register_generator` may hold only a
233+
A generator registered via `mrdocs.register_generator` may hold only a
234234
weak reference to the VM that defined it. The extension binding
235235
hands the VM over here so it outlives the extension run and stays
236236
usable when the generator is selected.

src/lib/Extensions/JsBinding.cpp

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,20 @@ namespace mrdocs {
2727

2828
namespace {
2929

30-
// Bind the `register_transform` and `register_generator` entry points
31-
// before the script runs. A JavaScript function bridges to a
32-
// `dom::Function`: transforms are captured in `transforms` (invoked once
33-
// the script has run), generators are handed to the corpus, which keeps
34-
// them runnable past this VM's lifetime.
30+
// Install the `mrdocs` global carrying the `register_transform` and
31+
// `register_generator` entry points before the script runs. A JavaScript
32+
// function bridges to a `dom::Function`: transforms are captured in
33+
// `transforms` (invoked once the script has run), generators are handed to
34+
// the corpus, which keeps them runnable past this VM's lifetime.
3535
void
3636
registerJsExtensionApi(
3737
js::Scope& scope,
3838
CorpusImpl& corpus,
3939
dom::Array& transforms,
4040
std::size_t& generators)
4141
{
42-
scope.setGlobal(
42+
dom::Object api;
43+
api.set(
4344
"register_transform",
4445
dom::Value(dom::makeVariadicInvocable(
4546
[&transforms](dom::Array const& args)
@@ -49,7 +50,8 @@ registerJsExtensionApi(
4950
if (args.empty() || !args.get(0).isFunction())
5051
{
5152
result = Unexpected(Error(
52-
"register_transform: expected a function argument"));
53+
"mrdocs.register_transform: expected a function "
54+
"argument"));
5355
}
5456
else
5557
{
@@ -58,7 +60,7 @@ registerJsExtensionApi(
5860
return result;
5961
})));
6062

61-
scope.setGlobal(
63+
api.set(
6264
"register_generator",
6365
dom::Value(dom::makeVariadicInvocable(
6466
[&corpus, &generators](dom::Array const& args)
@@ -70,7 +72,8 @@ registerJsExtensionApi(
7072
!args.get(1).isFunction())
7173
{
7274
result = Unexpected(Error(
73-
"register_generator: expected (string id, function)"));
75+
"mrdocs.register_generator: expected (string id, "
76+
"function)"));
7477
}
7578
else
7679
{
@@ -81,6 +84,8 @@ registerJsExtensionApi(
8184
}
8285
return result;
8386
})));
87+
88+
scope.setGlobal("mrdocs", dom::Value(std::move(api)));
8489
}
8590

8691
// Invoke one registered transform with the `ctx` object, tagging any
@@ -127,7 +132,7 @@ runOneJsExtension(CorpusImpl& corpus, std::string const& scriptPath)
127132

128133
MRDOCS_TRY(std::string script, files::getFileText(scriptPath));
129134

130-
// Running the script is what calls `register_transform`.
135+
// Running the script is what calls `mrdocs.register_transform`.
131136
Expected<void> result = scope.script(script);
132137
if (!result.has_value())
133138
{
@@ -138,7 +143,8 @@ runOneJsExtension(CorpusImpl& corpus, std::string const& scriptPath)
138143
else if (transforms.empty() && generators == 0)
139144
{
140145
// A discovered script that registers nothing is almost always a
141-
// mistake (a misspelled `register_transform` / `register_generator`,
146+
// mistake (a misspelled `mrdocs.register_transform` /
147+
// `mrdocs.register_generator`,
142148
// or a guard that skipped it), so flag it rather than silently
143149
// doing nothing.
144150
report::warn("extension '{}' registered nothing", scriptPath);

src/lib/Extensions/JsBinding.hpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ class CorpusImpl;
2222
/** Run one JavaScript extension script against the corpus.
2323
2424
Build a fresh JS context and evaluate the script. The script declares
25-
corpus transforms with `register_transform(fn)` and output generators
26-
with `register_generator(id, fn)`, in either combination. Each transform
27-
is invoked once, in registration order, with a navigable DOM view of the
28-
corpus it can read and mutate in place; each generator is handed to the
25+
corpus transforms with `mrdocs.register_transform(fn)` and output
26+
generators with `mrdocs.register_generator(id, fn)`, in either
27+
combination. Each transform is invoked once, in registration order,
28+
with a navigable DOM view of the corpus it can read and mutate in
29+
place; each generator is handed to the
2930
corpus to run later, once one is selected. A script that registers
3031
nothing causes a warning and otherwise has no effect, so an empty .js
3132
file is tolerated.

src/lib/Extensions/LuaBinding.cpp

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ struct LuaRegistrations
4848
std::size_t generators = 0;
4949
};
5050

51-
// `register_transform(fn)`: anchor `fn` in the registry and record it as
51+
// `mrdocs.register_transform(fn)`: anchor `fn` in the registry and record it as
5252
// a callable, so the host can invoke it once the chunk has run.
5353
int
5454
luaRegisterTransform(lua_State* L)
@@ -59,7 +59,7 @@ luaRegisterTransform(lua_State* L)
5959
if (lua_type(L, 1) != LUA_TFUNCTION)
6060
{
6161
result = luaL_error(L,
62-
"register_transform: expected a function argument");
62+
"mrdocs.register_transform: expected a function argument");
6363
}
6464
else
6565
{
@@ -70,9 +70,10 @@ luaRegisterTransform(lua_State* L)
7070
return result;
7171
}
7272

73-
// `register_generator(id, fn)`: anchor `fn` in the registry and hand it to
74-
// the corpus under `id`. The corpus keeps it runnable until a generator is
75-
// selected and run, long after this chunk's stack has unwound.
73+
// `mrdocs.register_generator(id, fn)`: anchor `fn` in the registry and
74+
// hand it to the corpus under `id`. The corpus keeps it runnable until a
75+
// generator is selected and run, long after this chunk's stack has
76+
// unwound.
7677
int
7778
luaRegisterGenerator(lua_State* L)
7879
{
@@ -82,7 +83,7 @@ luaRegisterGenerator(lua_State* L)
8283
if (lua_type(L, 1) != LUA_TSTRING || lua_type(L, 2) != LUA_TFUNCTION)
8384
{
8485
result = luaL_error(L,
85-
"register_generator: expected (string id, function)");
86+
"mrdocs.register_generator: expected (string id, function)");
8687
}
8788
else
8889
{
@@ -98,22 +99,24 @@ luaRegisterGenerator(lua_State* L)
9899
return result;
99100
}
100101

101-
// Bind the `register_transform` and `register_generator` entry points
102-
// before the chunk runs. The shared registrations pointer is carried as
103-
// each closure's single upvalue.
102+
// Install the `mrdocs` global carrying the `register_transform` and
103+
// `register_generator` entry points before the chunk runs. The shared
104+
// registrations pointer is carried as each closure's single upvalue.
104105
void
105106
registerLuaExtensionApi(
106107
lua::Context& ctx, CorpusImpl& corpus, LuaRegistrations& regs)
107108
{
108109
regs.ctx = &ctx;
109110
regs.corpus = &corpus;
110111
lua_State* L = static_cast<lua_State*>(ctx.nativeState());
112+
lua_newtable(L);
111113
lua_pushlightuserdata(L, &regs);
112114
lua_pushcclosure(L, &luaRegisterTransform, 1);
113-
lua_setglobal(L, "register_transform");
115+
lua_setfield(L, -2, "register_transform");
114116
lua_pushlightuserdata(L, &regs);
115117
lua_pushcclosure(L, &luaRegisterGenerator, 1);
116-
lua_setglobal(L, "register_generator");
118+
lua_setfield(L, -2, "register_generator");
119+
lua_setglobal(L, "mrdocs");
117120
}
118121

119122
// Invoke one registered transform with the `ctx` object, tagging any
@@ -158,11 +161,12 @@ runOneLuaExtension(CorpusImpl& corpus, std::string const& scriptPath)
158161
MRDOCS_TRY(lua::Function chunk, scope.loadChunk(script, scriptPath));
159162

160163
// Running the chunk's top-level code is what calls
161-
// `register_transform`; the chunk's own return value is unused.
164+
// `mrdocs.register_transform`; the chunk's own return value is unused.
162165
MRDOCS_TRY(chunk.call());
163166

164167
// A discovered script that registers nothing is almost always a
165-
// mistake (a misspelled `register_transform` / `register_generator`,
168+
// mistake (a misspelled `mrdocs.register_transform` /
169+
// `mrdocs.register_generator`,
166170
// or a guard that skipped it), so flag it rather than silently doing
167171
// nothing.
168172
if (regs.transforms.empty() && regs.generators == 0)

src/lib/Extensions/LuaBinding.hpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ class CorpusImpl;
2222
/** Run one Lua extension script against the corpus.
2323
2424
Build a fresh Lua context and evaluate the script. The script declares
25-
corpus transforms with `register_transform(fn)` and output generators
26-
with `register_generator(id, fn)`, in either combination. Each transform
27-
is invoked once, in registration order, with a navigable DOM view of the
28-
corpus it can read and mutate in place; each generator is handed to the
25+
corpus transforms with `mrdocs.register_transform(fn)` and output
26+
generators with `mrdocs.register_generator(id, fn)`, in either
27+
combination. Each transform is invoked once, in registration order,
28+
with a navigable DOM view of the corpus it can read and mutate in
29+
place; each generator is handed to the
2930
corpus to run later, once one is selected. A script that registers
3031
nothing causes a warning and otherwise has no effect, so an empty .lua
3132
file is tolerated.

src/lib/Extensions/RunExtensions.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class CorpusImpl;
2323
Extensions are discovered under each addon root's extensions/
2424
directory (the primary addons plus addons-supplemental): a .lua or
2525
.js file is an extension. Each script declares corpus transforms by
26-
calling `register_transform(fn)`; every registered function is
26+
calling `mrdocs.register_transform(fn)`; every registered function is
2727
invoked once, in registration order, with a navigable DOM view of the
2828
corpus. A transform reads the corpus through that view and mutates it
2929
by assigning to symbol fields (for example `sym.name = "..."`), which

0 commit comments

Comments
 (0)