Extensions

An extension is a Lua or JavaScript script that runs as part of a Mr.Docs build and shapes the documentation in a way templates alone cannot. There are two kinds, and a single script may declare either or both:

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

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

A script declares a transform with mrdocs.register_transform(id, fn) and a generator with mrdocs.register_generator(id, fn). Either way the registered function receives a single context object, ctx.

Languages and addon locations

Extensions are user-supplied scripts dropped under <addons>/extensions/ with the .js or .lua extension. The addon roots come from the addons and addons-supplemental options, the same roots that hold the Handlebars templates. Mr.Docs aggregates scripts across every root and runs them in alphabetical order by full path, with the two languages interleaved.

Both scripting languages reach the same mrdocs API. The choice is a trade-off, not a ranking.

  • JavaScript is more familiar to most developers. The runtime Mr.Docs embeds is a small ES engine, so scripts have the language proper (closures, destructuring, regex, classes) but not a Node-style standard library. There is no fs, no path, no process. Scripts that only manipulate the corpus do not miss any of that.

  • Lua is the language designed to be embedded. Mr.Docs links it whole, so scripts have access to the entire Lua standard library (string, table, math, io, os) and can do filesystem work or text munging without leaving the script. The cost is that fewer people read Lua at a glance than read JavaScript. If you’re already familiar with Lua, it is the more powerful choice.

The context object

Every registered function receives one argument, ctx:

  • ctx.corpus is the corpus. Its symbols field is a flat array of every extracted symbol; what a script can do with those symbols depends on the kind of extension, described in the sections below.

  • ctx.config is the resolved configuration: the same object the templates receive, holding every value from the config file and the command line. See the configuration reference for the available keys.

  • ctx.params is the extension’s own parameter block, keyed by the id it registered under: transform-options.<id> for a transform, generator-options.<id> for a generator. It is an empty object when the configuration sets none.

A generator’s ctx carries one more field, ctx.output, covered under Generators.

A script can register any number of transforms and generators, or none at all. If it registers nothing, Mr.Docs warns that the script had no effect and moves on.

Corpus transforms

A transform is a function registered with mrdocs.register_transform(id, fn); the id keys its transform-options.<id> parameters, delivered as ctx.params. 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.

  • JavaScript

  • Lua

addons/extensions/noop.js
mrdocs.register_transform("noop", function(ctx) {
    // walk ctx.corpus.symbols, assign to the fields you want to change
});
addons/extensions/noop.lua
mrdocs.register_transform("noop", function(ctx)
    -- walk ctx.corpus.symbols, assign to the fields you want to change
end)

The ctx.corpus object provides functions that expose the symbol graph. The ctx.corpus.symbols field is a flat array containing every extracted symbol. Scripts that need queries like "all members of `X`" simply walk the array and filter.

For instance, the following scripts count the symbols of each kind and report the totals at the end of the run:

  • JavaScript

  • Lua

addons/extensions/count_by_kind.js
mrdocs.register_transform("count-by-kind", function(ctx) {
    var counts = {};
    for (var i = 0; i < ctx.corpus.symbols.length; ++i) {
        var k = ctx.corpus.symbols[i].kind;
        counts[k] = (counts[k] || 0) + 1;
    }
    for (var k in counts) {
        console.log(k + ": " + counts[k]);
    }
});
addons/extensions/count_by_kind.lua
mrdocs.register_transform("count-by-kind", function(ctx)
    local counts = {}
    for _, sym in ipairs(ctx.corpus.symbols) do
        counts[sym.kind] = (counts[sym.kind] or 0) + 1
    end
    for k, v in pairs(counts) do
        print(k .. ": " .. v)
    end
end)

Each entry in ctx.corpus.symbols is a proxy for a live Mr.Docs symbol. The fields of each object are at the DOM reference.

When a script knows a symbol’s id and needs to act on that one symbol:

  • ctx.corpus.get(id) returns the proxy for it or null if the id is unknown

  • ctx.corpus.lookup(name) does a global-namespace name lookup and returns the proxy (or null)

subclass-tree.cpp
/// The root of the shape hierarchy.
struct Shape {};

/// A shape with straight sides.
struct Polygon : Shape {};

/// A three-sided polygon.
struct Triangle : Polygon {};

/// A four-sided polygon.
struct Quadrilateral : Polygon {};

/// A quadrilateral with equal sides.
struct Square : Quadrilateral {};

/// A round shape.
struct Circle : Shape {};
  • JavaScript

  • Lua

addons/extensions/subclass_tree.js
// Print the inheritance subtree rooted at a named class.
//
// `ctx.corpus.lookup(name)` resolves the entry point once. From there the
// only way down the tree is by id: each record carries a `derived`
// list of base16 ids, and `ctx.corpus.get(id)` turns each id back into a
// live symbol proxy. The recursion walks the graph that single-pass
// iteration over `ctx.corpus.symbols` cannot reconstruct.

function listSubclasses(corpus, sym, indent) {
    for (var i = 0; i < sym.derived.length; ++i) {
        var child = corpus.get(sym.derived[i]);
        if (child) {
            console.log(indent + child.name);
            listSubclasses(corpus, child, indent + "  ");
        }
    }
}

mrdocs.register_transform("subclass-tree", function(ctx) {
    var base = ctx.corpus.lookup("Shape");
    if (base) {
        console.log(base.name);
        listSubclasses(ctx.corpus, base, "  ");
    }
});
addons/extensions/subclass_tree.lua
-- Print the inheritance subtree rooted at a named class.
--
-- `ctx.corpus.lookup(name)` resolves the entry point once. From there the
-- only way down the tree is by id: each record carries a `derived`
-- list of base16 ids, and `ctx.corpus.get(id)` turns each id back into a
-- live symbol proxy. The recursion walks the graph that single-pass
-- iteration over `ctx.corpus.symbols` cannot reconstruct.

local function listSubclasses(corpus, sym, indent)
    for _, id in ipairs(sym.derived) do
        local child = corpus.get(id)
        if child then
            print(indent .. child.name)
            listSubclasses(corpus, child, indent .. "  ")
        end
    end
end

mrdocs.register_transform("subclass-tree", function(ctx)
    local base = ctx.corpus.lookup("Shape")
    if base then
        print(base.name)
        listSubclasses(ctx.corpus, base, "  ")
    end
end)

Running either script against the fixture above prints:

Shape
  Circle
  Polygon
    Quadrilateral
      Square
    Triangle

Modifying the corpus

Scripts modify the corpus by assigning to fields on a symbol proxy. Each assignment lands directly in the underlying Mr.Docs symbol. The runtime validates each assignment and raises an exception on an invalid value. An uncaught error in an extension aborts the build and includes the script’s path and the error message.

Most extensions read some attribute of a symbol and write back a string, an enumerator, or a small structured value. For instance, a codebase that follows "any function whose name starts with is_<name> is a predicate returning true if its <name> holds" encodes information that the brief could repeat verbatim:

brief-from-name.cpp
bool is_prime(int n);
bool is_palindrome(char const* s);
bool is_empty(char const* s);
  • JavaScript

  • Lua

addons/extensions/brief_from_name.js
// Synthesize documentation for every `is_*` predicate from its name.
//
// The convention: "any function whose name starts with `is_` is a
// predicate that returns `true` if its name (in plain English) holds".
// Both the brief and the lone parameter follow that template, so the
// script writes them once and frees authors from typing the same
// sentence on every declaration. Anything an author already wrote is
// preserved: only missing fields are filled in.

mrdocs.register_transform("brief-from-name", function(ctx) {
    for (var i = 0; i < ctx.corpus.symbols.length; ++i) {
        var sym = ctx.corpus.symbols[i];
        if (sym.kind === "function" && sym.name.indexOf("is_") === 0) {
            if (!sym.doc) { sym.doc = {}; }

            var subject = sym.name.slice(3).replace(/_/g, " ");

            if (!sym.doc.brief) {
                sym.doc.brief = "Returns true if " + subject + ".";
            }

            if (sym.params.length === 1
                && (!sym.doc.params || sym.doc.params.length === 0)) {
                sym.doc.params = [{
                    name: sym.params[0].name,
                    children: "The input examined for the " + subject + " property."
                }];
            }
        }
    }
});
addons/extensions/brief_from_name.lua
-- Synthesize documentation for every `is_*` predicate from its name.
--
-- The convention: "any function whose name starts with `is_` is a
-- predicate that returns `true` if its name (in plain English) holds".
-- Both the brief and the lone parameter follow that template, so the
-- script writes them once and frees authors from typing the same
-- sentence on every declaration. Anything an author already wrote is
-- preserved: only missing fields are filled in.

mrdocs.register_transform("brief-from-name", function(ctx)
    for _, sym in ipairs(ctx.corpus.symbols) do
        if sym.kind == "function"
           and sym.name:sub(1, 3) == "is_" then
            if not sym.doc then sym.doc = {} end

            local subject = sym.name:sub(4):gsub("_", " ")

            if not sym.doc.brief then
                sym.doc.brief = "Returns true if " .. subject .. "."
            end

            if #sym.params == 1
               and (not sym.doc.params or #sym.doc.params == 0) then
                sym.doc.params = {
                    {
                        name = sym.params[1].name,
                        children = "The input examined for the "
                            .. subject .. " property."
                    }
                }
            end
        end
    end
end)

Every is_foo_bar function then ships with "Returns true if foo bar." Authors only have to write a brief when the synthesized one is not the right one.

Preview
is_empty

Returns true if empty.

Synopsis

Declared in <brief‐from‐name.cpp>

bool
is_empty(char const* s);
Parameters

Name

Description

s

The input examined for the empty property.

is_palindrome

Returns true if palindrome.

Synopsis

Declared in <brief‐from‐name.cpp>

bool
is_palindrome(char const* s);
Parameters

Name

Description

s

The input examined for the palindrome property.

is_prime

Returns true if prime.

Synopsis

Declared in <brief‐from‐name.cpp>

bool
is_prime(int n);
Parameters

Name

Description

n

The input examined for the prime property.

Cross-linking Symbols

When the value being written needs to reference another symbol, the second symbol’s id is what makes the link clickable in the rendered output rather than a plain string.

For instance, consider a project where the parse_X and format_X free functions are symmetric. A reader landing on one almost always wants to see the other. The extension builds a name → id index in one pass, then walks it again to look up each partner:

parse-format-relates.cpp
/// An HTTP request as a structured value.
struct request;

/// Parse `text` into a request. Returns a valid request on success.
request parse_request(char const* text);

/// Format `r` as the wire-format text of an HTTP request.
char const* format_request(request const& r);

/// A user record.
struct user;

/// Parse `text` into a user.
user parse_user(char const* text);

/// Format `u` as the canonical wire-format text of a user.
char const* format_user(user const& u);
  • JavaScript

  • Lua

addons/extensions/parse_format_relates.js
// Cross-link symmetric IO helpers from a JavaScript extension.
//
// Every `parse_X` and `format_X` free function gets a `@see` entry
// pointing at its partner. The result is that each function's
// rendered page carries a "See Also" link to the other one, without
// anyone writing `@see` by hand.

function partnerName(name) {
    var partner = null;
    if (name.indexOf("parse_") === 0) {
        partner = "format_" + name.slice(6);
    } else if (name.indexOf("format_") === 0) {
        partner = "parse_" + name.slice(7);
    }
    return partner;
}

mrdocs.register_transform("parse-format-relates", function(ctx) {
    for (var i = 0; i < ctx.corpus.symbols.length; ++i) {
        var s = ctx.corpus.symbols[i];
        if (s.kind === "function") {
            var pname = partnerName(s.name);
            var partner = pname ? ctx.corpus.lookup(pname) : null;
            if (partner) {
                s.doc = {
                    sees: [{
                        children: [{
                            kind: "reference",
                            literal: pname,
                            id: partner.id
                        }]
                    }]
                };
            }
        }
    }
});
addons/extensions/parse_format_relates.lua
-- Cross-link symmetric IO helpers from a Lua extension.
--
-- Every `parse_X` and `format_X` free function gets a `@see` entry
-- pointing at its partner. The result is that each function's
-- rendered page carries a "See Also" link to the other one, without
-- anyone writing `@see` by hand.

local function partnerName(name)
    local partner = nil
    if name:sub(1, 6) == "parse_" then
        partner = "format_" .. name:sub(7)
    elseif name:sub(1, 7) == "format_" then
        partner = "parse_" .. name:sub(8)
    end
    return partner
end

mrdocs.register_transform("parse-format-relates", function(ctx)
    for _, s in ipairs(ctx.corpus.symbols) do
        if s.kind == "function" then
            local pname = partnerName(s.name)
            if pname then
                local partner = ctx.corpus.lookup(pname)
                if partner then
                    s.doc = {
                        sees = {
                            {
                                children = {
                                    { kind = "reference",
                                      literal = pname,
                                      id = partner.id }
                                }
                            }
                        }
                    }
                end
            end
        end
    end
end)
Preview
request

An HTTP request as a structured value.

Synopsis

Declared in <parse‐format‐relates.cpp>

struct request;
Non-Member Functions

Name

Description

format_request

Format r as the wire‐format text of an HTTP request.

parse_request

Parse text into a request. Returns a valid request on success.

user

A user record.

Synopsis

Declared in <parse‐format‐relates.cpp>

struct user;
Non-Member Functions

Name

Description

format_user

Format u as the canonical wire‐format text of a user.

parse_user

Parse text into a user.

format_request

Format r as the wire‐format text of an HTTP request.

Synopsis

Declared in <parse‐format‐relates.cpp>

char const*
format_request(request const& r);
Parameters

Name

Description

r

An HTTP request as a structured value.

See Also
format_user

Format u as the canonical wire‐format text of a user.

Synopsis

Declared in <parse‐format‐relates.cpp>

char const*
format_user(user const& u);
Parameters

Name

Description

u

A user record.

See Also
parse_request

Parse text into a request. Returns a valid request on success.

Synopsis

Declared in <parse‐format‐relates.cpp>

request
parse_request(char const* text);
Return Value

An HTTP request as a structured value.

See Also
parse_user

Parse text into a user.

Synopsis

Declared in <parse‐format‐relates.cpp>

user
parse_user(char const* text);
Return Value

A user record.

See Also

The two-pass shape (index, then look up) is the idiom whenever a write needs to refer to a symbol the script hasn’t yet seen during the walk.

Notice in this example that s.doc.sees receives a list of polymorphic types that represent a paragraph in s.doc.sees.children. These polymorphic objects accept an object with a kind: selector that names the concrete derived class to construct.

Generators

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.

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.

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

  • ctx.corpus.symbols is the array of every symbol. Each carries the same fields the template and helper layers see, plus a flat _id string suitable as a stable per-symbol URL fragment. A generator reads the corpus rather than mutating it.

  • ctx.config is the resolved configuration, as above.

  • ctx.output.write(relativePath, contents) writes one file under the configured output directory, which is the path specified with --output on the command line, or with the output key in the config file; that’s the same location the built-in generators write to. The path is resolved relative to that directory and may not escape it; an absolute path or one that climbs above the output directory is rejected. Parent directories are created as needed.

  • ctx.params is this generator’s own options: the generator-options.<id> block from the configuration, keyed by the id the generator registered under, or an empty object when none is set. It lets a generator be configured without adding command-line options. For example, generator-options: { search-index: { stop-words: [the, a, an] } } reaches the search-index generator as ctx.params.stop-words.

Because the script owns the output, it also owns what a per-page generator would otherwise do for it: the URLs it emits, and any escaping of the content it writes. Mr.Docs does not apply an escape map to a generator’s output.

Example: a search index

A complete, runnable example lives at examples/generators/script-driven/search-index/. The extension declares a search-index generator that emits a single search-index.json aggregating every symbol, an artifact no per-page generator can produce:

addons/extensions/search_index.lua
-- Declare a `search-index` generator: a script-defined generator that
-- aggregates every symbol into a single search-index.json, the kind of
-- artifact the per-page generators cannot produce.
--
-- `mrdocs.register_generator(id, fn)` declares it next to any
-- `mrdocs.register_transform` a script might also declare; selecting
-- `generator: <id>` runs `fn` with one `ctx`. `ctx.corpus.symbols` is
-- every symbol (each tagged with a flat `_id` so the generator can form
-- stable per-symbol URLs) and `ctx.output.write` emits files under the
-- output directory.

-- Quote a string as a JSON value.
local function json_string(s)
  s = s:gsub('\\', '\\\\'):gsub('"', '\\"')
  return '"' .. s .. '"'
end

mrdocs.register_generator("search-index", function(ctx)
  local entries = {}
  for _, sym in ipairs(ctx.corpus.symbols) do
    local name = sym.name or ""
    if name ~= "" then
      entries[#entries + 1] =
        '{"name":' .. json_string(name) ..
        ',"url":' .. json_string(sym._id .. ".html") .. "}"
    end
  end
  ctx.output.write(
    "search-index.json",
    "[" .. table.concat(entries, ",") .. "]")
end)

Select it with --generator=search-index; it writes search-index.json into the output directory.