Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ option(MRDOCS_BUILD_HEADERS_ONLY "Build only public-headers for self-reference"
option(MRDOCS_GENERATE_REFERENCE "Generate MrDocs reference" ${MRDOCS_BUILD_DOCS})
option(MRDOCS_GENERATE_ANTORA_REFERENCE "Generate MrDocs reference in Antora module pages" OFF)

# A plugin is a shared module the tool loads while it runs. A build that
# links statically can neither produce one, since the static startup files
# carry relocations a shared object cannot hold, nor load one afterwards,
# so the plugin example and its link test are left out of such a build.
# The loader itself still builds; it simply finds nothing to load.
if (CMAKE_CXX_FLAGS MATCHES "(^| )-static($| )" OR
CMAKE_EXE_LINKER_FLAGS MATCHES "(^| )-static($| )")
set(MRDOCS_BUILD_PLUGIN_MODULES OFF)
else ()
set(MRDOCS_BUILD_PLUGIN_MODULES ON)
endif ()

set_ternary(MRDOCS_LINK_MODE MRDOCS_BUILD_SHARED SHARED "")
set_ternary(MRDOCS_LINK_MODE_DEFINITION MRDOCS_BUILD_SHARED MRDOCS_SHARED_LINK MRDOCS_STATIC_LINK)
set_ternary(MRDOCS_GCC "CMAKE_CXX_COMPILER_ID STREQUAL \"GNU\"" ON OFF)
Expand Down
9 changes: 8 additions & 1 deletion data/mrdocs/addons/plugins/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# data/mrdocs/addons/plugins/

Holds the DLLs or shared libraries that MrDocs loads when it is launched.
Holds the shared libraries that MrDocs loads when it is launched: every
`.dll`, `.so`, or `.dylib` directly inside it is loaded, in name order, and
asked what it provides. Any other file here, this one included, is ignored.

A supplemental addons directory can carry a `plugins` directory of its own, so
a plugin does not have to be installed next to MrDocs.

See the Plugins page of the documentation for how to write one.
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
** xref:extensions/corpus-extensions.adoc[Extensions]
** xref:extensions/handlebars-extensions.adoc[Handlebars Extensions]
** xref:extensions/data-driven-generators.adoc[Data-Driven Generators]
** xref:extensions/plugins.adoc[Plugins]
** xref:extensions/antora.adoc[Antora Extensions]
** xref:extensions/as-library.adoc[Mr.Docs as a Library]
** xref:reference:index.adoc[Library Reference]
Expand Down
119 changes: 119 additions & 0 deletions docs/modules/ROOT/pages/extensions/plugins.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
= Plugins
:url-native: https://github.com/cppalliance/mrdocs/tree/{page-origin-refname}/examples/generators/native

A plugin is a shared library that Mr.Docs loads as it starts up and hands a context to. Through that context, the plugin installs what it provides, so the rest of the run sees it as if it had shipped with the tool: generators, which turn the corpus into output, and transforms, which change the corpus before any generator sees it.

Reach for a plugin when the output needs {cpp}: a format whose rules are awkward to express in templates, a generator that depends on a system library, or one whose work is heavy enough to want compiled code. The lighter options remain xref:extensions/data-driven-generators.adoc[data-driven generators] (templates only) and script-driven generators (Lua or JavaScript, see xref:extensions/corpus-extensions.adoc[Extensions]). The difference from xref:extensions/as-library.adoc[Mr.Docs as a library] is that a plugin runs inside the ordinary `mrdocs` tool: you keep the command line, the configuration, and everything else Mr.Docs does, and you do not build Mr.Docs or LLVM.

== Where plugins live

Mr.Docs looks in the `plugins` subdirectory of every addon root, that is under both the xref:configuration/reference.adoc#addons_option[`addons`] directory and each xref:configuration/reference.adoc#addons-supplemental_option[`addons-supplemental`] directory, in that order:

[source]
----
my-addons/
└── plugins/
└── stats.dll <1>
----
<1> `.dll` on Windows, `.so` on Linux, `.dylib` or `.so` on macOS.

Every library in such a directory is loaded, in name order within a root, and roots are visited in the order the configuration lists them. One library reachable through several roots is loaded once. Anything that is not a library is ignored, so a directory can hold a README, an import library, or debug information without confusing the search.

A library that cannot be loaded, does not export the entry points, or reports an error of its own stops the run.

== The entry point

The `MRDOCS_PLUGIN_MAIN` macro defines the function Mr.Docs calls, naming the cpp:PluginContext[] parameter it receives:

.`plugin.cpp`
[source,cpp]
----
include::example$examples/generators/native/stats/plugin.cpp[tag=main]
----

The context is how a plugin reaches Mr.Docs: cpp:PluginContext::installGenerator[] adds a generator, cpp:PluginContext::installTransform[] adds a transform, and cpp:PluginContext::config[] returns the configuration the run was started with, already loaded and normalized, in case what the plugin installs depends on it. The reference is valid for the duration of the call.

Along with the entry point, the macro defines a function that reports the version of the plugin interface the library was compiled against. Mr.Docs compares it with its own and refuses to call a plugin that was built against a different one, rather than calling it with the wrong expectations. A plugin therefore has to be rebuilt when it is used with a Mr.Docs whose plugin interface has changed.

The macro reports the toolchain as well, and Mr.Docs checks that too, for the reason the next section explains.

== The generator

A plugin's generator is an ordinary cpp:Generator[] subclass: the same interface the built-in formats implement, and the same one the xref:extensions/as-library.adoc[library] examples use.

.`plugin.cpp`
[source,cpp]
----
include::example$examples/generators/native/stats/plugin.cpp[tag=generator]
----

cpp:Generator::build[] receives the corpus and the configuration, and owns everything from there: where its files go and what goes in them. This one counts the symbols by kind and writes a single file:

.`plugin.cpp`
[source,cpp]
----
include::example$examples/generators/native/stats/plugin.cpp[tag=build]
----

== Transforming the corpus

A plugin can also install a cpp:Transform[]: a pass that runs once, after the corpus is built and finalized and before any generator runs, so what it changes is what every output format sees. Where a generator is handed the corpus as `const`, a transform is handed it as it is, and may read it, change the symbols it finds, or both. What it cannot do is create a symbol or destroy one, since a corpus keeps its storage to itself. Everything a symbol holds is in reach, though, including the lists of members that the generators walk to decide what to write.

[source,cpp]
----
class BriefFiller
: public mrdocs::Transform
{
public:
std::string_view
id() const noexcept override
{
return "brief-filler";
}

mrdocs::Expected<void>
apply(
mrdocs::Corpus& corpus,
mrdocs::Config const& config) const override;
};

MRDOCS_PLUGIN_MAIN(context)
{
return context.installTransform(std::make_unique<BriefFiller>());
}
----

The transforms a plugin installs run before the ones an extension script registers with `mrdocs.register_transform`, and among themselves in the order they were installed. An error from a transform stops the run before any generator sees the corpus, and the diagnostic names the transform through cpp:Transform::id[].

== Building a plugin

A plugin is a CMake module library that links the `mrdocs` tool. Linking the tool is what resolves the Mr.Docs symbols the plugin calls, and it is the whole build dependency: no LLVM, no Clang, no separate library to ship alongside. That holds because the public headers only ever forward-declare the LLVM types they name; LLVM comes back only for a plugin that reaches past them into Mr.Docs's internal headers, as one supplying its own compilation database would.

[source,cmake]
----
find_package(mrdocs REQUIRED CONFIG)
add_library(stats MODULE plugin.cpp)
target_link_libraries(stats PRIVATE mrdocs::mrdocs)
target_compile_features(stats PRIVATE cxx_std_23)
----

A plugin and Mr.Docs pass {cpp} objects between them: the plugin allocates a generator that Mr.Docs destroys, and both inline the standard library types the API exposes. So the two have to be built with the same compiler and standard library, in a configuration that lays those types out the same way. On Windows that includes the debug or release choice, since it changes the iterator debug level.

That requirement is checked rather than assumed: the plugin reports its compiler, its standard library, and the settings that affect those layouts, and Mr.Docs refuses a plugin whose report differs from its own, naming both. So, a plugin built the wrong way is turned away at startup instead of crashing later, somewhere unrelated.

One configuration rules plugins out altogether: a statically linked Mr.Docs. Such a build has no dependable way to load a shared library while it runs, and cannot produce one either, since the startup files a static link brings in carry relocations a shared object cannot hold. Plugins therefore need a Mr.Docs that links dynamically.

Drop the resulting library into the `plugins` directory of an addon root and the generator it installs is selectable like any other, through the xref:configuration/reference.adoc#generator_option[`generator`] option:

[source,yaml]
----
generator: stats
----

For the {url-native}/stats[`stats`^] example, whose input describes a handful of symbols, the generator writes:

.`stats.txt`
[source,text]
----
include::example$examples/generators/native/stats/stats.txt[]
----
17 changes: 7 additions & 10 deletions docs/mrdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@ file-patterns:
- '*.hpp'
include-symbols:
- 'mrdocs::**'
# MrDocs's own macros are maintainer-facing implementation helpers, not
# public API: the MRDOCS_* support macros and the internal X-macros
# (INFO/LOG/F) and compat shims used across the headers. Keep them out of
# MrDocs's own reference.
exclude-macros:
- 'MRDOCS_*'
- 'INFO'
- 'LOG'
- 'F'
- 'FMT_CONSTEVAL'
# MrDocs's own macros are maintainer-facing implementation helpers rather
# than public API, except for the plugin interface, which is what a plugin
# author writes. Naming what to keep is what keeps the reference stable:
# a support macro added later stays out without anyone having to exclude
# it, which a list of exclusions cannot promise.
include-macros:
- 'MRDOCS_PLUGIN_*'
implementation-defined:
- '**::detail'
# Injected by the MRDOCS_DESCRIBE_* macros
Expand Down
29 changes: 29 additions & 0 deletions examples/generators/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,32 @@ foreach (script_driven IN ITEMS search-index json)
set_property(TEST mrdocs-generator-script-driven-${script_driven} PROPERTY
ENVIRONMENT "MRDOCS=$<TARGET_FILE:mrdocs>")
endforeach ()

if (MRDOCS_BUILD_PLUGIN_MODULES)
add_library(mrdocs-stats-plugin-example MODULE native/stats/plugin.cpp)
target_link_libraries(mrdocs-stats-plugin-example PRIVATE mrdocs)
target_compile_features(mrdocs-stats-plugin-example PRIVATE cxx_std_23)

# MrDocs looks for plugins in the `plugins` subdirectory of an addon root,
# so the library is built into one. The generator expression keeps CMake
# from appending a per-configuration subdirectory, which would put the
# library somewhere the test cannot name.
set(MRDOCS_STATS_ADDONS "${CMAKE_CURRENT_BINARY_DIR}/native/stats/addons")
set_target_properties(mrdocs-stats-plugin-example PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "$<1:${MRDOCS_STATS_ADDONS}/plugins>")

# Running the example is the test: the configuration asks for the `stats`
# generator, which exists only if the plugin was found, loaded, and
# installed it.
add_test(NAME mrdocs-generator-example-native-stats
COMMAND
mrdocs
"--config=${CMAKE_CURRENT_SOURCE_DIR}/native/stats/mrdocs.yml"
"--output=${CMAKE_CURRENT_BINARY_DIR}/native/stats/reference-output"
"--addons=${CMAKE_SOURCE_DIR}/data/mrdocs/addons"
"--addons-supplemental=${MRDOCS_STATS_ADDONS}"
"--stdlib-includes=${LIBCXX_DIR}"
"--libc-includes=${CMAKE_SOURCE_DIR}/data/mrdocs/headers/libc-stubs"
--log-level=warn
)
endif ()
4 changes: 4 additions & 0 deletions examples/generators/native/stats/mrdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
generator: stats
source-root: .
file-patterns:
- simple.cpp
121 changes: 121 additions & 0 deletions examples/generators/native/stats/plugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com)
//
// Official repository: https://github.com/cppalliance/mrdocs
//

// A MrDocs plugin: a shared library that MrDocs loads as it starts up
// and that installs a generator counting the extracted symbols by kind.

#include <mrdocs/Config.hpp>
#include <mrdocs/Corpus.hpp>
#include <mrdocs/Generator.hpp>
#include <mrdocs/Metadata.hpp>
#include <mrdocs/Plugin.hpp>
#include <mrdocs/Support/Error/Error.hpp>
#include <mrdocs/Support/Reflection/Describe.hpp>
#include <filesystem>
#include <fstream>
#include <map>
#include <memory>
#include <string_view>

namespace {

// tag::generator[]
// A generator that writes one line per symbol kind, indicating the kind and
// how many symbols of that kind the corpus has.
class StatsGenerator final
: public mrdocs::Generator
{
public:
std::string_view
id() const noexcept override
{
return "stats";
}

std::string_view
displayName() const noexcept override
{
return "Symbol statistics";
}

std::string_view
fileExtension() const noexcept override
{
return "txt";
}

mrdocs::Expected<void>
build(
mrdocs::Corpus const& corpus,
mrdocs::Config const& config) const override;
};
// end::generator[]

// Count the symbols of the corpus by kind, ordered by kind name.
std::map<std::string_view, int>
countByKind(mrdocs::Corpus const& corpus)
{
std::map<std::string_view, int> counts;
for (mrdocs::Symbol const& symbol : corpus)
{
++counts[mrdocs::toString(symbol.Kind)];
}
return counts;
}

// Resolve the directory the generator writes into.
std::filesystem::path
outputDir(mrdocs::Config const& config)
{
std::filesystem::path dir(config.configDir());
dir /= config.output;
return dir;
}

// tag::build[]
mrdocs::Expected<void>
StatsGenerator::
build(
mrdocs::Corpus const& corpus,
mrdocs::Config const& config) const
{
std::map<std::string_view, int> const counts =
countByKind(corpus);
std::filesystem::path const dir = outputDir(config);
std::error_code ec;
std::filesystem::create_directories(dir, ec);
std::filesystem::path const file = dir / "stats.txt";

mrdocs::Expected<void> result;
std::ofstream os(file);
if (!os)
{
result = mrdocs::Unexpected(mrdocs::formatError(
"could not open \"{}\" for writing", file.string()));
}
else
{
for (auto const& [kind, count] : counts)
{
os << kind << ' ' << count << '\n';
}
}
return result;
}
// end::build[]

} // (anon)

// tag::main[]
MRDOCS_PLUGIN_MAIN(context)
{
return context.installGenerator(std::make_unique<StatsGenerator>());
}
// end::main[]
36 changes: 36 additions & 0 deletions examples/generators/native/stats/simple.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/// A small library of geometric helpers.
namespace geometry {

/// The supported coordinate systems.
enum class System
{
/// Distances along two perpendicular axes.
cartesian,
/// A distance and an angle measure.
polar
};

/// A point in two dimensions.
struct Point
{
/** Compute the distance from the origin.

@return The distance from the origin.
*/
double length() const;

/** Translate by an offset.

@param dx The offset along the first axis.
@param dy The offset along the second axis.
*/
void translate(double dx, double dy);
};

/// A distance in the units of the coordinate system.
using Distance = double;

/// The coordinate system the helpers assume.
extern System defaultSystem;

} // namespace geometry
7 changes: 7 additions & 0 deletions examples/generators/native/stats/stats.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enum 1
enum-constant 2
function 2
namespace 2
record 1
typedef 1
variable 1
Loading
Loading