How to Contribute a Feature
Each section below is a procedure for one kind of change, with a worked case from the code. Each ends with the tests the change needs and the pages it must update. Testing & fixtures says how to run the tests and write a golden fixture.
Run pipeline
A run goes from options to generated output in these stages:
| Stage | Where it lives |
|---|---|
Options |
|
Compilation database |
|
Extraction |
|
Corpus and finalization |
|
Generators |
|
Add or change an option
An option is one entry of src/mrdocs/ConfigOptions.json.
The build generates ConfigSchema.hpp from it into the build tree, and the generated struct holds one field per option, in camel case.
The configuration file, the command line and --help all read that schema, so a new option needs no code in them.
The worked case is sort-members-ctors-1st, a boolean that puts constructors first in a sorted list of members.
-
Add the entry to its group in
src/mrdocs/ConfigOptions.json, withname,brief,details,typeanddefault. -
For an enum, list its
values. For an option only the command line can set, addcommand-line-only. -
Build. The build runs
generate-config-info.pyandgenerate-yaml-schema.pyfromutils/codegen/. They writeConfigSchema.hppand the JSON schema underdocs/modules/ROOT/attachments/schemas/config/. -
Read the value where it matters.
sort-members-ctors-1stisconfig_.sortMembersCtors1StinSymbolCompare.hpp, undersrc/mrdocs/Metadata/Finalizers/. -
Add a check, a dependent default, or a path to resolve against the config directory to
Config::normalizeinsrc/mrdocs/Config.cpp. -
When the option changes the corpus after extraction, act on it in a finalizer (see Add a finalizer).
The option reference comes from the schema, so its entry appears with the build.
Keep the name of a changed option unless a rename is the point, because users have it in their mrdocs.yml.
Tests the change needs
-
Add a golden fixture per behavior, under
tests/golden/fixtures/config/<option>/, with a<stem>.ymlthat sets the option. -
For
sort-members-ctors-1st, the fixture is intests/golden/fixtures/snippets/options/sort-members-ctors-1st/, and its docs page embeds it. -
Pass
yaml-schema-check, which fails while the committed JSON schema is stale. Runpython3 utils/codegen/generate-yaml-schema.pyfrom the root, then the check again.
Add a finalizer
A finalizer is a pass over the whole corpus after extraction and merging.
Each lives in src/mrdocs/Metadata/Finalizers/ as a class with a build() member, and Corpus::finalize in src/mrdocs/Corpus.cpp runs them one after another.
Because the order matters (sorting needs the inherited members to exist), the position of your call is part of the change.
The worked case is OverloadsFinalizer, which folds overloaded functions into sets. It starts at the global namespace, has one operator() per kind of symbol it handles, and runs only when config.overloads is set.
-
Create
<Name>Finalizer.hppand<Name>Finalizer.cppbeside the others. The build globs the directory, so no build file changes. -
Give the class a
Corpus&and aConfig const&, both taken by the constructor. -
Add a
build()member that finds the global namespace withcorpus_.find(SymbolID::global)and visits down from it, asOverloadsFinalizerdoes. -
Give
operator()an overload for each kind of symbol you care about, and a no-opoperator()(Symbol&) {}for the rest. -
Add a block to
Corpus::finalize, after the finalizers it depends on, that creates the finalizer and callsbuild(). -
Guard that block with the option that enables it, as the block for
OverloadsFinalizerdoes withif (config.overloads)inCorpus.cpp. -
Befriend the finalizer in
include/mrdocs/Corpus.hpponly when it needs private access, asSortMembersFinalizerdoes.
Tests the change needs
-
Add a golden fixture that shows the pass at work, in the directory of the option that controls it (
tests/golden/fixtures/config/overloads/forOverloadsFinalizer). -
Add a unit test under
tests/unit/Metadata/Finalizers/when the pass has logic a fixture cannot reach, asDocComment/parseInlines.cppdoes.
Pages to update
-
Update the page of the option that controls it, under
docs/modules/ROOT/pages/configuration/. -
Update src/ only if you add a directory.
Add a kind of symbol, or a field to symbols
Every symbol is a struct in include/mrdocs/Metadata/Symbol/, derived from SymbolCommonBase<SymbolKind::X>, with a MRDOCS_DESCRIBE_STRUCT that lists its fields.
The describe list is the single source of the DOM projection that templates read, the XML output, and the RELAX NG and JSON schemas.
Symbol/SymbolNodes.inc lists the kinds, and each place that handles every kind expands it.
The worked case for a field is IsUsing of TypedefSymbol in Symbol/Typedef.hpp, which tells using aliases from C typedefs.
Adding a field
-
Declare it in the struct, with a default and a doc comment.
-
Add its name to the describe list at the end of the header.
-
When it takes part in comparison, add it to
operator<⇒in the matching file undersrc/mrdocs/Metadata/Symbol/. -
Set it where extraction builds the symbol.
ASTVisitor::populateinsrc/mrdocs/AST/ASTVisitor.cppsetsIsUsingfor aTypedefSymbol. -
Show it in the templates when users should see it. The
typedef.hbspartial underdata/mrdocs/addons/generator/common/partials/symbol/signature/printstypedeforusingfrom it. -
Build, then run
MRDOCS=<build-dir>/mrdocs utils/codegen/generate-schema.sh, with themrdocsyou just built (it looks formrdocsonPATHotherwise). It rewritesmrdocs.rng,mrdocs.schema.jsonand thedom-schema.adocreference, whereisUsinghas its row.
Adding a kind
Do the field steps for a new struct and add these:
-
Add
INFO(<Name>)toSymbol/SymbolNodes.inc. -
Create
Symbol/<Name>.hppandsrc/mrdocs/Metadata/Symbol/<Name>.cpp(the comparison), and include the header frominclude/mrdocs/Metadata.hpp. -
Handle the kind where the code expands
SymbolNodes.incundersrc/mrdocs/Generators/:xml/XMLWriter.cpp, andhbs/TagfileWriter.cpp,hbs/Builder.cpp,hbs/MultiPageVisitor.cppandhbs/SinglePageVisitor.cpp. -
Extract the kind in
ASTVisitor(see Extraction code map). -
Add a template for it under
symbol/indata/mrdocs/addons/generator/common/partials/.
Tests the change needs
-
Add a golden fixture per case in
tests/golden/fixtures/symbols/<kind>/. -
Validate first and read each difference before you update, because every XML fixture that holds the new field changes. The golden workflow says how.
-
Pass
schema-check, which fails while the committed schemas anddom-schema.adocare stale. -
Pass
xml-lint, which validates every fixture against the schema.
Extraction code map
No common contribution starts in the Clang visitor, so this is a map and not a procedure.
Extraction runs Clang over each source file of the compilation database, and src/mrdocs/AST/ turns each declaration into a symbol.
Corpus::build in src/mrdocs/Corpus.cpp drives it, then merges the results of all translation units.
| File | Role |
|---|---|
|
The Clang entry points that hand each translation unit to the visitor. |
|
The visitor. |
|
Turns a Clang comment into the |
|
Build the |
|
Parses the references in documentation, such as |
|
Collects the macros of a translation unit. |
Pick the file by the kind of fix:
-
Change a
populateoverload when the fix concerns what one kind of symbol records. -
Change
TypeBuilderorNameBuilderwhen it concerns how the code writes a type or a name. -
Leave everything after extraction, such as merging and ordering, to the corpus and its finalizers.
Add a built-in generator
A generator turns the finished corpus into files.
Each built-in one has a directory under src/mrdocs/Generators/ and a subclass of Generator from include/mrdocs/Generator.hpp.
For a new output that is only a different template over the same data, use a data-driven generator instead, as Data-Driven Generators explains.
The worked case is the noop generator, the smallest one, in src/mrdocs/Generators/noop/.
-
Create
<Name>Generator.hppand.cppin a new directorysrc/mrdocs/Generators/<id>/. The build globs the tree, so no build file changes. -
Derive from
Generatorand overrideid(),displayName(),fileExtension()andbuild(). -
Make the
ida short lowercase string. Users write it ingenerator:and ingenerator-options.<id>, and the registry takes one generator per id. -
Make the
fileExtensionlowercase, without the leading dot. An empty one means the generator writes no file. -
End the
.cppwith amake<Name>Generator()function that returns astd::unique_ptr<Generator>. -
In
BuiltinGenerators.cppundersrc/mrdocs/Generators/, declare that function and add oneregistry.install(…)line toregisterBuiltinGenerators. -
Add the id to the text of the
generatoroption insrc/mrdocs/ConfigOptions.json, which lists the built-in generators.
Tests the change needs
-
Add a golden fixture per behavior under
tests/golden/fixtures/generators/<id>/, made of:-
a
<stem>.cpp -
a
<stem>.ymlthat setsgenerator: <id> -
the expected
<stem>.<extension>that the create target writes
-
-
Use
tests/golden/fixtures/generators/json/as the model. -
Add a unit test in
tests/unit/Generators/ortests/unit/Extensions/GeneratorRegistry.cppfor registration rules.