Corpus Transforms
A corpus transform is a pipeline extension that rewrites metadata across many symbols at once to incorporate project conventions into the corpus. You can use it to do things like:
-
Backfill documentation fields from naming conventions,
-
Create and identify relations between fields,
-
Tag symbols by group, and
-
Mark generated code as "implementation defined" according to project conventions.
Transforms run once, after the corpus is built and finalized and before any generator runs, so every output format sees the change. After a transform runs, the corpus forgets the lookups by name it remembered, so the generators see a symbol that a transform renames under its new name.
|
As specified in Defining Extensions, you can write an extension in Lua, JavaScript, or C++. A transform must be independent and not rely on the order in which extensions are executed. |
Every transform works on the same corpus. The transforms run one after another, and each sees what the earlier ones wrote. Each receives its own transform-options.<id> block. The transforms of scripts and of plugins run in one pipeline. There is nothing to select: once a plugin is installed, its transform runs in every build.
Registering a transform
A transform defined is a function registered under an id. A script registers it with mrdocs. and a plugin registers it with register_transform(id, fn)registerTransform, as Defining Extensions describes:
-
JavaScript
-
Lua
-
C++
addons/extensions/hello_world_transform.jsmrdocs.register_transform("hello-world-transform", function(ctx) {
ctx.report.warn("Hello from a transform");
});
addons/extensions/hello_world_transform.luamrdocs.register_transform("hello-world-transform", function(ctx)
ctx.report.warn("Hello from a transform")
end)
hello-world-transform/plugin.cpp#include <mrdocs/Extensions/TransformExtensionContext.hpp>
#include <mrdocs/Plugin.hpp>
#include <mrdocs/Support/Reporter.hpp>
#include <mrdocs/Transform.hpp>
MRDOCS_PLUGIN_MAIN(plugin)
{
return plugin.registerTransform("hello-world-transform",
[](mrdocs::TransformExtensionContext const& ctx) -> mrdocs::Expected<void>
{
ctx.report->warn("Hello from a transform");
return {};
});
}
A transform that keeps state across calls is a subclass of Transform instead, with Transform::id and Transform::apply. A plugin passes an object of the class to the other overload of registerTransform, the way Programmatic Generators describes the Generator subclass.
The id keys the transform-options.<id> block of the configuration file. The block arrives as ctx.params in a script. In C++ a callable reads it as the params field of its TransformExtensionContext, and a subclass of Transform calls getTransformParams from Transform::apply.
The function body is where the work goes, and the sections below fill it in. The C++ tabs of the later sections show the callable a plugin passes to registerTransform and the helpers it uses.
Options
The block is an empty object when the configuration has none. With this configuration, the transform with the id brief-filler gets { "text": "TODO" }, and the one registered as brief-checker gets { "suffix": "checked" }:
transform-options:
brief-filler:
text: TODO
brief-checker:
suffix: checked
The context
Every transform registered as a function receives a TransformExtensionContext, which holds the corpus, the configuration, the transform’s own options block and the reporter. A plugin receives the object itself. A script receives a proxy of it as ctx, whose keys are the fields of the object (ctx.corpus, ctx.config, ctx.params, ctx.report), as Defining Extensions says.
Errors
An error from a transform stops the run before any generator sees the corpus. The diagnostic names the transform by its id. Here a script transform registered as brief-filler reads transform-options.brief-filler.text and raises because the block has no text:
the transform "brief-filler" failed: [script]:2: transform-options.brief-filler.text is missing
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
-
C++
addons/extensions/count_by_kind.jsmrdocs.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.luamrdocs.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)
count-by-kind/plugin/plugin.cppreturn plugin.registerTransform("count-by-kind",
[](mrdocs::TransformExtensionContext const& ctx) -> mrdocs::Expected<void>
{
std::map<std::string_view, int> counts;
for (mrdocs::Symbol const& symbol : *ctx.corpus)
{
++counts[mrdocs::toString(symbol.Kind)];
}
for (auto const& [kind, count] : counts)
{
ctx.report->info("{}: {}", kind, count);
}
return {};
});
Run on the shapes of the next section, the C++ version reports:
namespace: 1
record: 6
Each entry in ctx.corpus.symbols is a proxy for a live Mr.Docs symbol, and the fields of each object are at the DOM reference. In C++, iterating the corpus yields the Symbol objects themselves, and symbol.Kind and symbol.Name are plain members. The array itself (internally represented by a set) is fixed in size: read it, iterate it, filter it, but copy the entries you keep into your own array or table before trimming or reordering them.
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 ornullif the id is unknown -
ctx.corpus.lookup(name)does a global-namespace name lookup and returns the proxy (ornull)
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
-
C++
addons/extensions/subclass_tree.jsmrdocs.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.lualocal 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
subclass-tree/plugin/plugin.cppreturn plugin.registerTransform("subclass-tree",
[](mrdocs::TransformExtensionContext const& ctx) -> mrdocs::Expected<void>
{
mrdocs::Corpus const& corpus = *ctx.corpus;
auto base = corpus.lookup("Shape");
if (base)
{
ctx.report->info("{}", base->Name);
if (auto const* record = base->asRecordPtr())
{
listSubclasses(*ctx.report, corpus, *record, " ");
}
}
return {};
});
subclass-tree/plugin/plugin.cppvoid
listSubclasses(mrdocs::report::Reporter& report, mrdocs::Corpus const& corpus,
mrdocs::RecordSymbol const& record, std::string const& indent)
{
for (mrdocs::SymbolID const& id : record.Derived)
{
auto const* child = corpus.find(id);
if (!child)
{
continue;
}
report.info("{}{}", indent, child->Name);
if (auto const* derived = child->asRecordPtr())
{
listSubclasses(report, corpus, *derived, indent + " ");
}
}
}
Running any of them 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 the problems it finds through ctx.report, the reporter of the run.
A script calls ctx.report.warn(…), ctx.report.error(…) or ctx.report.info(…) with the same free-form arguments as console.log. A plugin calls ctx.report→warn(…) and the others with a format string and its arguments, as std::format takes them. Either way, Mr.Docs counts each message in the totals of the run like one of its own, and under warn-as-error a warning counts as an error.
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
-
C++
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 ctx.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)) {
ctx.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.lualocal 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 ctx.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
ctx.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
project-conventions/plugin/plugin.cppreturn plugin.registerTransform("project-conventions",
[](mrdocs::TransformExtensionContext const& ctx) -> mrdocs::Expected<void>
{
for (mrdocs::Symbol const& symbol : *ctx.corpus)
{
warnIfLowQuality(*ctx.report, symbol);
}
return {};
});
project-conventions/plugin/plugin.cppvoid
warnIfLowQuality(mrdocs::report::Reporter& report, mrdocs::Symbol const& symbol)
{
if (!symbol.doc || !symbol.doc->brief)
{
return;
}
std::string brief = mrdocs::doc::getAsPlainText(*symbol.doc->brief);
brief.erase(0, brief.find_first_not_of(" \t\n\r\f\v"));
brief.erase(brief.find_last_not_of(" \t\n\r\f\v") + 1);
if (isLowQuality(brief))
{
report.warn(
"{}: low-quality brief: \"{}\"", symbol.Name, brief);
}
}
project-conventions/plugin/plugin.cppbool
isLowQuality(std::string const& brief)
{
std::size_t special = 0;
std::size_t total = 0;
for (char c : brief)
{
if (c == ' ')
{
continue;
}
++total;
if (!std::isalnum(static_cast<unsigned char>(c)))
{
++special;
}
}
return !brief.empty() && (brief.size() < 4 || special > total / 2);
}
Running any of them against the fixture above warns:
flush: low-quality brief: "x"
sync: low-quality brief: ">>>>>>>>>>>>"
Modifying the corpus
Assigning to a field of a symbol writes to the corpus, which a transform gets mutable and a generator gets as const. A C++ transform writes through the mutable reference it asks the corpus for by id.
Most extensions read some attribute of a symbol and write back a string, an enumerator, or a small structured value. For instance, a codebase where a function named is_<name> is a predicate returning true if its <name> holds can repeat that in the brief:
brief-from-name.cppbool is_prime(int n);
bool is_palindrome(char const* s);
bool is_empty(char const* s);
-
JavaScript
-
Lua
-
C++
addons/extensions/brief_from_name.jsmrdocs.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.lualocal 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
brief-from-name/plugin/plugin.cppreturn plugin.registerTransform("brief-from-name",
[](mrdocs::TransformExtensionContext const& ctx) -> mrdocs::Expected<void>
{
for (mrdocs::Symbol const& symbol : *ctx.corpus)
{
describePredicate(*ctx.corpus, symbol);
}
return {};
});
brief-from-name/plugin/plugin.cppvoid
describePredicate(mrdocs::Corpus& corpus, mrdocs::Symbol const& symbol)
{
auto const* function = symbol.asFunctionPtr();
if (!function || !symbol.Name.starts_with("is_"))
{
return;
}
std::string subject = symbol.Name.substr(3);
std::ranges::replace(subject, '_', ' ');
mrdocs::Symbol* const target = corpus.find(symbol.id);
fillBrief(*target, subject);
if (function->Params.size() == 1 && target->doc->params.empty())
{
fillParam(*target, *function, subject);
}
}
brief-from-name/plugin/plugin.cppvoid
fillBrief(mrdocs::Symbol& symbol, std::string const& subject)
{
if (!symbol.doc)
{
symbol.doc.emplace();
}
if (!symbol.doc->brief)
{
symbol.doc->brief = mrdocs::doc::BriefBlock(
"Returns true if " + subject + ".");
}
}
brief-from-name/plugin/plugin.cppvoid
fillParam(mrdocs::Symbol& symbol, mrdocs::FunctionSymbol const& function,
std::string const& subject)
{
auto const& name = function.Params[0].Name;
symbol.doc->params.emplace_back(name ? *name : std::string(),
"The input examined for the " + subject + " property.");
}
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.
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. |
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.
Corpus shape invariants
What a transform changes has to leave the corpus in the shape finalization left it in:
-
A symbol keeps its id and its kind.
-
Every symbol id written into the corpus names a symbol the corpus has, so a transform cannot create or destroy a symbol.
-
A transform does not redo what finalization derived: sorted member lists, overload sets, copied briefs and resolved references.
Within those limits a transform changes what it likes: doc comments, attributes, and which members a list holds and in what order.
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 walks the functions once and looks up the partner of each by name:
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
-
C++
addons/extensions/parse_format_relates.jsmrdocs.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.lualocal 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
parse-format-relates/plugin/plugin.cppreturn plugin.registerTransform("parse-format-relates",
[](mrdocs::TransformExtensionContext const& ctx) -> mrdocs::Expected<void>
{
for (mrdocs::Symbol const& symbol : *ctx.corpus)
{
relatePartner(*ctx.corpus, symbol);
}
return {};
});
parse-format-relates/plugin/plugin.cppvoid
relatePartner(mrdocs::Corpus& corpus, mrdocs::Symbol const& symbol)
{
if (!symbol.isFunction())
{
return;
}
std::string const name = partnerName(symbol.Name);
if (name.empty())
{
return;
}
auto const partner = corpus.lookup(name);
if (partner)
{
addSee(*corpus.find(symbol.id), name, partner->id);
}
}
parse-format-relates/plugin/plugin.cppstd::string
partnerName(std::string_view name)
{
if (name.starts_with("parse_"))
{
return "format_" + std::string(name.substr(6));
}
if (name.starts_with("format_"))
{
return "parse_" + std::string(name.substr(7));
}
return {};
}
parse-format-relates/plugin/plugin.cppvoid
addSee(mrdocs::Symbol& target, std::string const& name,
mrdocs::SymbolID const& id)
{
mrdocs::doc::ReferenceInline reference(name);
reference.id = id;
mrdocs::doc::SeeBlock see;
see.children.emplace_back(std::move(reference));
if (!target.doc)
{
target.doc.emplace();
}
target.doc->sees.clear();
target.doc->sees.push_back(std::move(see));
}
request
An HTTP request as a structured value.
Synopsis
Declared in <parse‐format‐relates.cpp>
struct request;
Non-Member Functions
Name |
Description |
Format |
|
Parse |
user
A user record.
Synopsis
Declared in <parse‐format‐relates.cpp>
struct user;
Non-Member Functions
Name |
Description |
Format |
|
Parse |
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
Looking the other symbol up by name is the idiom whenever a write needs to refer to a symbol other than the one the walk is on.
|
Notice in this example that |
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 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 fills in each symbol’s description from the corresponding file. A complete, runnable example lives at examples/extensions/ghostwriter/.
-
Transform
-
Input
-
External docs
-
Config
addons/extensions/ghostwriter.lualocal 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.mdA `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.mdA `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.ymladdons-supplemental:
- addons
# tag::generator[]
generator: adoc
# end::generator[]
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, and 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:
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 |
The enclosed area. |
Data Members
Name |
Description |
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 |
The Euclidean length. |
Data Members
Name |
Description |
The x component. |
|
The y component. |
Non-Member Functions
Name |
Description |
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>
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. |