Corpus Transforms

A Mr.Docs extension is a Lua or JavaScript script that runs as part of a 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, covered on this page, 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 script generator owns the whole emit: instead of rendering one page per symbol, it traverses the corpus and writes whatever files it wants.

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 locations

Extensions are scripts dropped under extensions/ inside a Mr.Docs addon directory, the same roots that hold the Handlebars templates, with the .js or .lua extension. Mr.Docs aggregates scripts across every addon 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:

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

Writing a transform

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 context object

Every registered function receives one argument, ctx:

  • ctx.corpus is the corpus: an object whose symbols field is a flat array of every extracted symbol, plus get(id) and lookup(name) functions that resolve a single symbol (see Iterating symbols).

  • 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 on the Script Generators page.

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.

Accessing the corpus

Iterating symbols

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.

Looking up a symbol

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
mrdocs.register_transform("subclass-tree", function(ctx) {
    const base = ctx.corpus.lookup("Shape");
    if (base) {
        console.log(base.name);
        listSubclasses(ctx.corpus, base, "  ");
    }
});

/**
 * Print the inheritance subtree below a record, one class per line, indented by
 * depth. Each record's `derived` list holds the ids of its direct subclasses;
 * `corpus.get` turns each id back into a symbol, and the recursion follows them
 * down the graph.
 * @param {object} corpus - `ctx.corpus`.
 * @param {object} sym - The record whose subclasses to print.
 * @param {string} indent - The leading whitespace for this level.
 */
function listSubclasses(corpus, sym, indent) {
    for (let i = 0; i < sym.derived.length; ++i) {
        const child = corpus.get(sym.derived[i]);
        if (child) {
            console.log(indent + child.name);
            listSubclasses(corpus, child, indent + "  ");
        }
    }
}
addons/extensions/subclass_tree.lua
local listSubclasses

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)

--- Print the inheritance subtree below a record, one class per line, indented
--- by depth. Each record's `derived` list holds the ids of its direct
--- subclasses; `corpus.get` turns each id back into a symbol, and the recursion
--- follows them down the graph.
--- @param corpus table `ctx.corpus`.
--- @param sym table The record whose subclasses to print.
--- @param indent string The leading whitespace for this level.
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

Running either script against the fixture above prints:

Shape
  Circle
  Polygon
    Quadrilateral
      Square
    Triangle

Emitting warnings

A read-only transform can do more than compute totals: it can report problems it finds back to Mr.Docs. mrdocs.report.warn(…​) and mrdocs.report.error(…​) route through the same diagnostic system as the rest of the tool, so their messages are formatted, counted in the run’s totals, and escalated by warn-as-error just like a built-in warning. They take the same free-form arguments as console.log.

A natural use is checking documentation against your project’s conventions. The example below checks for a "low-quality brief" that reads like a stray comment or is too short. A complete, runnable example lives at examples/extensions/project-conventions/.

project-conventions.cpp
/// Reset the parser to its initial state.
void reset();

/// x
void flush();

/// >>>>>>>>>>>>
void sync();
  • JavaScript

  • Lua

addons/extensions/project_conventions.js
// Check each symbol's documentation against the project's conventions. Every
// convention is a small predicate; this transform warns about each symbol that
// breaks one, routing through mrdocs.report so the messages count as real
// diagnostics. The one convention checked here flags a "low-quality brief": a
// brief that reads like a stray comment rather than a description.
mrdocs.register_transform("project-conventions", function(ctx) {
    for (let i = 0; i < ctx.corpus.symbols.length; ++i) {
        const sym = ctx.corpus.symbols[i];
        const brief = symbolBrief(sym);
        if (isLowQualityBrief(brief)) {
            mrdocs.report.warn(sym.name + ': low-quality brief: "' + brief + '"');
        }
    }
});

/**
 * The text of a documentation node, gathered by walking its children.
 * @param {object} node - A documentation node with `literal` and `children`.
 * @returns {string} The concatenated text of the node and its descendants.
 */
function nodeText(node) {
    let text = node.literal || "";
    if (node.children) {
        for (let i = 0; i < node.children.length; ++i) {
            text += nodeText(node.children[i]);
        }
    }
    return text;
}

/**
 * A symbol's brief as plain text, trimmed, or "" when it has none.
 * @param {object} sym - The symbol to read.
 * @returns {string} The brief text, or an empty string.
 */
function symbolBrief(sym) {
    if (!sym.doc || !sym.doc.brief) {
        return "";
    }
    return nodeText(sym.doc.brief).replace(/^\s+|\s+$/g, "");
}

/**
 * Whether a brief is low-quality: it reads like a stray comment rather than a
 * description because it is too short or more punctuation than words.
 * @param {string} brief - The brief text, already trimmed.
 * @returns {boolean} True when the brief breaks the convention.
 */
function isLowQualityBrief(brief) {
    if (brief.length === 0) {
        return false;
    }
    let special = 0, total = 0;
    for (let i = 0; i < brief.length; ++i) {
        if (brief[i] === " ") {
            continue;
        }
        total += 1;
        if (!/[A-Za-z0-9]/.test(brief[i])) {
            special += 1;
        }
    }
    return brief.length < 4 || special > total / 2;
}
addons/extensions/project_conventions.lua
local node_text, symbol_brief, is_low_quality_brief

-- Check each symbol's documentation against the project's conventions. Every
-- convention is a small predicate; this transform warns about each symbol that
-- breaks one, routing through mrdocs.report so the messages count as real
-- diagnostics. The one convention checked here flags a "low-quality brief": a
-- brief that reads like a stray comment rather than a description.
mrdocs.register_transform("project-conventions", function(ctx)
    for _, sym in ipairs(ctx.corpus.symbols) do
        local brief = symbol_brief(sym)
        if is_low_quality_brief(brief) then
            mrdocs.report.warn(sym.name .. ': low-quality brief: "' .. brief .. '"')
        end
    end
end)

--- The text of a documentation node, gathered by walking its children.
--- @param node table A documentation node with `literal` and `children`.
--- @return string The concatenated text of the node and its descendants.
function node_text(node)
    local text = node.literal or ""
    if node.children then
        for _, child in ipairs(node.children) do
            text = text .. node_text(child)
        end
    end
    return text
end

--- A symbol's brief as plain text, trimmed, or "" when it has none.
--- @param sym table The symbol to read.
--- @return string The brief text, or an empty string.
function symbol_brief(sym)
    if not (sym.doc and sym.doc.brief) then
        return ""
    end
    return node_text(sym.doc.brief):match("^%s*(.-)%s*$")
end

--- Whether a brief is low-quality: it reads like a stray comment rather than a
--- description because it is too short or more punctuation than words.
--- @param brief string The brief text, already trimmed.
--- @return boolean True when the brief breaks the convention.
function is_low_quality_brief(brief)
    if #brief == 0 then
        return false
    end
    local special, total = 0, 0
    for c in brief:gmatch(".") do
        if c ~= " " then
            total = total + 1
            if not c:match("%w") then special = special + 1 end
        end
    end
    return #brief < 4 or special > total / 2
end

Running either script against the fixture above warns:

flush: low-quality brief: "x"
sync: low-quality brief: ">>>>>>>>>>>>"

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
mrdocs.register_transform("brief-from-name", function(ctx) {
    for (let i = 0; i < ctx.corpus.symbols.length; ++i) {
        fillPredicateDoc(ctx.corpus.symbols[i]);
    }
});

/**
 * Fill in the brief and parameter doc of an `is_*` predicate from its name.
 * The subject is the name after `is_` with underscores turned to spaces, so
 * `is_prime` becomes "Returns true if prime." Does nothing for a symbol that is
 * not an `is_*` function, and never overwrites a field an author already wrote.
 * @param {object} sym - The symbol to document, mutated in place.
 */
function fillPredicateDoc(sym) {
    if (sym.kind !== "function" || sym.name.indexOf("is_") !== 0) {
        return;
    }
    if (!sym.doc) { sym.doc = {}; }

    const 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
local fillPredicateDoc

mrdocs.register_transform("brief-from-name", function(ctx)
    for _, sym in ipairs(ctx.corpus.symbols) do
        fillPredicateDoc(sym)
    end
end)

--- Fill in the brief and parameter doc of an `is_*` predicate from its name.
--- The subject is the name after `is_` with underscores turned to spaces, so
--- `is_prime` becomes "Returns true if prime." Does nothing for a symbol that
--- is not an `is_*` function, and never overwrites a field an author already
--- wrote.
--- @param sym table The symbol to document, mutated in place.
function fillPredicateDoc(sym)
    if sym.kind ~= "function" or sym.name:sub(1, 3) ~= "is_" then
        return
    end
    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

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
mrdocs.register_transform("parse-format-relates", function(ctx) {
    for (let i = 0; i < ctx.corpus.symbols.length; ++i) {
        const s = ctx.corpus.symbols[i];
        if (s.kind === "function") {
            const pname = partnerName(s.name);
            const partner = pname ? ctx.corpus.lookup(pname) : null;
            if (partner) {
                s.doc = {
                    sees: [{
                        children: [{
                            kind: "reference",
                            literal: pname,
                            id: partner.id
                        }]
                    }]
                };
            }
        }
    }
});

/**
 * The name of a function's symmetric IO partner: `format_X` for a `parse_X`,
 * and `parse_X` for a `format_X`. Returns null for any name that is neither.
 * @param {string} name - The function's name.
 * @returns {string|null} The partner's name, or null if `name` is not a
 *   `parse_`/`format_` helper.
 */
function partnerName(name) {
    let 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;
}
addons/extensions/parse_format_relates.lua
local partnerName

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)

--- The name of a function's symmetric IO partner: `format_X` for a `parse_X`,
--- and `parse_X` for a `format_X`. Returns nil for any name that is neither.
--- @param name string The function's name.
--- @return string|nil partner The partner's name, or nil if `name` is not a
---   `parse_`/`format_` helper.
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
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.

Reading files

After being presented with the arguments for generated reference documentation, a common objection is that the prose should be mostly written by technical writers, not developers. The rationale is not to clutter the headers, and that technical writers can focus on use cases the users are interested in and dedicate themselves to long tutorials.

A transform extension can bridge the two by also reading documentation from external sources. While developers write documentation in code that is verifiably correct and guaranteed never to drift, a writer keeps the extra documentation alongside the project. The ghostwrite transform extension fills in each symbol’s description from the corresponding file. A complete, runnable example lives at examples/extensions/ghostwriter/.

  • Transform

  • Input

  • Vec2 docs

  • Circle docs

  • distance docs

  • Config

addons/extensions/ghostwriter.lua
local read_file, doc_path, paragraphs, blocks, inlines

mrdocs.register_transform("ghostwriter", function(ctx)
  local root = (ctx.params and ctx.params.source) or "external-docs"
  for _, sym in ipairs(ctx.corpus.symbols) do
    -- Fill the description from the file when the symbol has none of its own.
    -- The brief written in the header is a separate field, so it is kept; the
    -- writer's file supplies the detailed description the developer left out.
    if sym.name ~= nil and (not sym.doc or #(sym.doc.document or {}) == 0) then
      local text = read_file(doc_path(ctx, sym, root))
      if text then
        local new_blocks = blocks(paragraphs(text))
        if #new_blocks > 0 then
          if not sym.doc then
            sym.doc = {}
          end
          sym.doc.document = new_blocks
        end
      end
    end
  end
end)

--- Read a whole file into a string.
--- @param path string Path to the file, relative to the run's working directory.
--- @return string|nil The file's contents, or nil when the file does not exist.
function read_file(path)
  local f = io.open(path, "r")
  if not f then
    return nil
  end
  local content = f:read("*a")
  f:close()
  return content
end

--- The external file that documents a symbol, mirroring its scope.
--- For example app::Vec2 maps to `<root>/app/Vec2.md`.
--- @param ctx table The transform context, used to walk `parent` ids.
--- @param sym table The symbol to locate a file for.
--- @param root string The source directory holding the writer's files.
--- @return string The path to the symbol's Markdown file.
function doc_path(ctx, sym, root)
  local parts = {}
  local cur = sym
  while cur and cur.name ~= nil do
    table.insert(parts, 1, cur.name)
    cur = cur.parent and ctx.corpus.get(cur.parent) or nil
  end
  return root .. "/" .. table.concat(parts, "/") .. ".md"
end

--- Split Markdown text into paragraphs.
--- Paragraphs are separated by blank lines; a soft line break inside a paragraph
--- is collapsed to a single space.
--- @param text string The raw Markdown.
--- @return string[] One entry per non-empty paragraph.
function paragraphs(text)
  local out = {}
  text = text:gsub("\r\n", "\n")
  for para in (text .. "\n\n"):gmatch("(.-)\n\n") do
    para = para:gsub("%s*\n%s*", " "):gsub("^%s+", ""):gsub("%s+$", "")
    if #para > 0 then
      out[#out + 1] = para
    end
  end
  return out
end

--- Wrap each paragraph string in a documentation paragraph block.
--- @param paras string[] Paragraphs, as returned by `paragraphs`.
--- @return table[] Paragraph block nodes, suitable for `sym.doc.document`.
function blocks(paras)
  local out = {}
  for _, p in ipairs(paras) do
    out[#out + 1] = { kind = "paragraph", children = inlines(p) }
  end
  return out
end

--- Parse one paragraph into documentation inline nodes.
--- Text wrapped in single backticks becomes an inline-code node; everything else
--- becomes plain text. Only backtick code spans are handled here; a richer
--- transform could parse more of Markdown.
--- @param text string A single paragraph, with no line breaks.
--- @return table[] Inline nodes, suitable as a block's `children`.
function inlines(text)
  local nodes = {}
  local is_code = false
  for segment in (text .. "`"):gmatch("(.-)`") do
    if #segment > 0 then
      if is_code then
        nodes[#nodes + 1] = { kind = "code", children = { { kind = "text", literal = segment } } }
      else
        nodes[#nodes + 1] = { kind = "text", literal = segment }
      end
    end
    is_code = not is_code
  end
  return nodes
end
simple.cpp
/// Application value types.
namespace app {

/// A point or displacement in the plane.
struct Vec2
{
    /// The x component.
    double x;
    /// The y component.
    double y;

    /// The Euclidean length.
    ///
    /// @return The straight-line length of the vector.
    double length() const;
};

/// A circle in the plane.
struct Circle
{
    /// The radius.
    double radius;

    /// The enclosed area.
    ///
    /// @return The area enclosed by the circle.
    double area() const;
};

/// The distance between two points.
///
/// @param a The first point.
/// @param b The second point.
/// @return The straight-line distance between `a` and `b`.
double distance(Vec2 const& a, Vec2 const& b);

}
external-docs/app/Vec2.md
A `Vec2` stores a pair of Cartesian coordinates and doubles as both a point
and a displacement. Arithmetic on it follows the usual rules for a 2-D vector
space, so adding two vectors adds their components and scaling multiplies them.

Prefer `Vec2` over a bare `std::pair<double, double>` when the values are
geometric: the named `x` and `y` members and the geometry helpers make intent
obvious at the call site, and keep unrelated pairs from being mixed in by
mistake.
external-docs/app/Circle.md
A `Circle` is defined entirely by its radius; its center is supplied by the
surrounding coordinate system rather than stored here. This keeps the type
small enough to pass by value.

The radius is assumed non-negative. Passing a negative radius is a precondition
violation, not a runtime error, so the geometry helpers do not check for it.
external-docs/app/distance.md
`distance` returns the Euclidean distance between two points: the length of the
straight segment joining them, or equivalently the square root of the summed
squared differences of their `x` and `y` components.

The value is symmetric in its arguments and is zero exactly when the two points
coincide, so it works well as the basis for an equality-with-tolerance check.
mrdocs.yml
addons-supplemental:
  - addons
generator: adoc
multipage: false
show-namespaces: false
warn-if-undocumented: false
source-root: .
input:
  - .
transform-options:
  ghostwriter:
    source: external-docs

The example is a Lua transform that reads the corresponding Markdown files by the technical writer. The code includes a brief for each symbol and its metadata, while the writer provides a longer description. The mrdocs.yml configuration file loads the transform and points it at the source directory through transform-options.ghostwriter.source.

The transform reads the file whose path matches each symbol’s scope and sets its description from the file’s paragraphs. It only touches the description; every other field the developer wrote stays. The distance function shows the split: its brief, its @param entries, and its @return all come from the header, while the writer’s file supplies just the longer description. The rendered page carries both sources at once:

Preview
app::Circle

A circle in the plane.

Synopsis

Declared in <simple.cpp>

struct Circle;
Description

A Circle is defined entirely by its radius; its center is supplied by the surrounding coordinate system rather than stored here. This keeps the type small enough to pass by value.

The radius is assumed non‐negative. Passing a negative radius is a precondition violation, not a runtime error, so the geometry helpers do not check for it.

Member Functions

Name

Description

area

The enclosed area.

Data Members

Name

Description

radius

The radius.

app::Circle::area

The enclosed area.

Synopsis

Declared in <simple.cpp>

double
area() const;
Return Value

The area enclosed by the circle.

app::Circle::radius

The radius.

Synopsis

Declared in <simple.cpp>

double radius;
app::Vec2

A point or displacement in the plane.

Synopsis

Declared in <simple.cpp>

struct Vec2;
Description

A Vec2 stores a pair of Cartesian coordinates and doubles as both a point and a displacement. Arithmetic on it follows the usual rules for a 2‐D vector space, so adding two vectors adds their components and scaling multiplies them.

Prefer Vec2 over a bare std::pair<double, double> when the values are geometric: the named x and y members and the geometry helpers make intent obvious at the call site, and keep unrelated pairs from being mixed in by mistake.

Member Functions

Name

Description

length

The Euclidean length.

Data Members

Name

Description

x

The x component.

y

The y component.

Non-Member Functions

Name

Description

distance

The distance between two points.

app::Vec2::length

The Euclidean length.

Synopsis

Declared in <simple.cpp>

double
length() const;
Return Value

The straight‐line length of the vector.

app::Vec2::x

The x component.

Synopsis

Declared in <simple.cpp>

double x;
app::Vec2::y

The y component.

Synopsis

Declared in <simple.cpp>

double y;
app::distance

The distance between two points.

Synopsis

Declared in <simple.cpp>

double
distance(
    Vec2 const& a,
    Vec2 const& b);
Description

distance returns the Euclidean distance between two points: the length of the straight segment joining them, or equivalently the square root of the summed squared differences of their x and y components.

The value is symmetric in its arguments and is zero exactly when the two points coincide, so it works well as the basis for an equality‐with‐tolerance check.

Return Value

The straight‐line distance between a and b.

Parameters

Name

Description

a

The first point.

b

The second point.