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
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{{#if (@root.mrdocs.corpus.getUrl (@root.mrdocs.corpus.get id))~}}
{{#>markup/a href=(@root.mrdocs.corpus.getUrl (@root.mrdocs.corpus.get id))}}{{#>markup/code}}{{ literal }}{{/markup/code}}{{/markup/a}}
{{~else if href~}}
{{#>markup/a href=href}}{{#>markup/code}}{{ literal }}{{/markup/code}}{{/markup/a}}
{{~else~}}
{{#>markup/code}}{{ literal }}{{/markup/code}}
{{~/if}}
21 changes: 18 additions & 3 deletions docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,15 @@
"title": "Input directories to extract symbols from",
"type": "array"
},
"input-tagfiles": {
"additionalProperties": {
"type": "string"
},
"default": {},
"description": "A map of tagfile path to the base URL under which the documentation set it describes is published. A reference in a doc comment that names no symbol in this corpus, such as `@ref std::vector`, becomes a link into that set when one of these tagfiles lists the name. The name is resolved from the scope it is written in outward, as any other reference is, and is never shortened to reach an entry. Relative paths are resolved against the directory of the configuration file. On the command line, each entry is passed as `--input-tagfiles=<path>=<base-url>`, which may be repeated. Doxygen writes a tagfile for its own output with `GENERATE_TAGFILE`, MrDocs with the `output-tagfile` option.",
"title": "Tagfiles of other documentation sets to link against",
"type": "object"
},
"legible-names": {
"default": true,
"description": "Use legible names for IDs in the documentation. When set to true, MrDocs uses legible names for symbol anchors in the corpus. These are symbols that are legible but still safe for URLs. When the option is set to false, MrDocs uses a hash of the symbol ID to avoid conflicts.",
Expand Down Expand Up @@ -502,6 +511,12 @@
"title": "Output directory for the generated documentation",
"type": "string"
},
"output-tagfile": {
"default": "<output-dir>/reference.tag.xml",
"description": "Specifies the full path (filename) where the generated tagfile should be saved. If left empty, no tagfile will be generated. The tagfiles of other documentation sets are read with the `input-tagfiles` option.",
"title": "Path for the tagfile",
"type": "string"
},
"overloads": {
"default": true,
"description": "When set to `true`, MrDocs detects function overloads and groups them as a single symbol type. The documentation for this new symbol comes from the union of non-ambiguous metadata from the functions.",
Expand Down Expand Up @@ -689,9 +704,9 @@
"type": "array"
},
"tagfile": {
"default": "<output-dir>/reference.tag.xml",
"description": "Specifies the full path (filename) where the generated tagfile should be saved. If left empty, no tagfile will be generated.",
"title": "Path for the tagfile",
"default": "",
"description": "Deprecated: this is the old name for `output-tagfile`. Setting it still sets that option, with a warning. An empty value leaves `output-tagfile` alone.",
"title": "Path for the tagfile (deprecated)",
"type": "string"
},
"transform-options": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
<define name="S_2bdz8onDMx7PBYnExqtLgMxUqrPY"><zeroOrMore><choice><element name="kind"><text/></element></choice></zeroOrMore></define>
<define name="S_pbDQ6katbvPedvuztLHCiRNi4G8"><zeroOrMore><choice><element name="kind"><text/></element><element name="children"><zeroOrMore><ref name="AnyInline"/></zeroOrMore></element><element name="href"><text/></element></choice></zeroOrMore></define>
<define name="S_3PrdQDrzihPKEMMq8ZtDjd69UoMf"><zeroOrMore><choice><element name="kind"><text/></element><element name="literal"><text/></element></choice></zeroOrMore></define>
<define name="S_2yM1iLKZLVpSpfu7Dq5sFaDfnwLa"><zeroOrMore><choice><element name="kind"><text/></element><element name="id"><text/></element><element name="literal"><text/></element></choice></zeroOrMore></define>
<define name="S_2yM1iLKZLVpSpfu7Dq5sFaDfnwLa"><zeroOrMore><choice><element name="kind"><text/></element><element name="href"><text/></element><element name="id"><text/></element><element name="literal"><text/></element></choice></zeroOrMore></define>
<define name="S_22nFJc2hmNmmR5h1kq6NkgViKdEM"><zeroOrMore><choice><element name="kind"><text/></element></choice></zeroOrMore></define>
<define name="S_CJPbC4NwrPJeuv6wSnDmgGcZcQc"><zeroOrMore><choice><element name="kind"><text/></element><element name="children"><zeroOrMore><ref name="AnyInline"/></zeroOrMore></element></choice></zeroOrMore></define>
<define name="S_28xEnXQadr6mJnmktXwag2oH9muy"><zeroOrMore><choice><element name="kind"><text/></element><element name="children"><zeroOrMore><ref name="AnyInline"/></zeroOrMore></element></choice></zeroOrMore></define>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"S_2bdz8onDMx7PBYnExqtLgMxUqrPY": {"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string"}}},
"S_pbDQ6katbvPedvuztLHCiRNi4G8": {"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string"},"children":{"type":"array","items":{"$ref":"#/$defs/AnyInline"}},"href":{"type":"string"}}},
"S_3PrdQDrzihPKEMMq8ZtDjd69UoMf": {"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string"},"literal":{"type":"string"}}},
"S_2yM1iLKZLVpSpfu7Dq5sFaDfnwLa": {"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string"},"id":{"type":"string"},"literal":{"type":"string"}}},
"S_2yM1iLKZLVpSpfu7Dq5sFaDfnwLa": {"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string"},"href":{"type":"string"},"id":{"type":"string"},"literal":{"type":"string"}}},
"S_22nFJc2hmNmmR5h1kq6NkgViKdEM": {"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string"}}},
"S_CJPbC4NwrPJeuv6wSnDmgGcZcQc": {"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string"},"children":{"type":"array","items":{"$ref":"#/$defs/AnyInline"}}}},
"S_28xEnXQadr6mJnmktXwag2oH9muy": {"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string"},"children":{"type":"array","items":{"$ref":"#/$defs/AnyInline"}}}},
Expand Down
3 changes: 3 additions & 0 deletions docs/modules/ROOT/examples/configuration/input-tagfiles.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
input-tagfiles:
tagfiles/cppreference-doxygen-web.tag.xml: https://en.cppreference.com/w/
tagfiles/boost-url.tag.xml: https://www.boost.org/doc/libs/release/libs/url/doc/html/
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/commands/inlines.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ include::example$snippets/commands/link.adoc[tags=!footer]

== Cross-references

xref:commands/reference.adoc#cmd-cross-reference[`@ref`] links to another symbol by its qualified name; MrDocs resolves it against the corpus and emits a working link. This is the command to reach for whenever one symbol's documentation should point at another.
xref:commands/reference.adoc#cmd-cross-reference[`@ref`] links to another symbol by its qualified name; MrDocs resolves it and emits a working link. This is the command to reach for whenever one symbol's documentation should point at another. A name the corpus does not hold can still resolve into another project's documentation, through xref:configuration/output.adoc#_external_docs[`input-tagfiles`].

.Cross-references
[source,cpp]
Expand Down
16 changes: 14 additions & 2 deletions docs/modules/ROOT/pages/configuration/output.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,23 @@ include::example$snippets/options/base-url/base-url.adoc[tags=!footer]

== External docs

xref:configuration/reference.adoc#tagfile_option[`tagfile`] writes a Doxygen-compatible tag file alongside the documentation. The expected use is cross-linking: other MrDocs runs can read it to resolve `@ref` targets that live in your library, and Doxygen-generated docs can consume it for the same purpose. Reach for it when you publish a library that other people's docs need to link to.
xref:configuration/reference.adoc#output-tagfile_option[`output-tagfile`] writes a Doxygen-compatible tag file alongside the documentation. The expected use is cross-linking: other MrDocs runs can read it to resolve `@ref` targets that live in your library, and Doxygen-generated docs can consume it for the same purpose. Reach for it when you publish a library that other people's docs need to link to.

TIP: A few projects use the tag file in a less obvious way: they read it from their own site-generation scripts to build a navigation tree or a search index from the symbol list, rather than maintaining one by hand.

NOTE: The inverse direction (consuming tag files emitted by other projects so that your pages link to *their* symbols) is handled by the xref:extensions/antora.adoc#antora-cpp-tagfiles-extension[`antora-cpp-tagfiles-extension`]. Point it at one or more tag files in your Antora playbook, and references like `boost::asio::io_context` resolve to the Boost.Asio site automatically.
xref:configuration/reference.adoc#input-tagfiles_option[`input-tagfiles`] is the other direction: it reads the tag files of documentation sets you do not build, so a reference to a symbol MrDocs never extracted still becomes a link. Each key is a tag file, each value the base URL that set is published under:

.`mrdocs.yml`
[source,yaml]
----
include::example$configuration/input-tagfiles.yml[]
----

With that in place, `@ref std::vector` in a doc comment links to Cppreference instead of rendering as plain code. A name one of these sets documents resolves the way every other reference does, from the scope it is written in outward, so a name relative to its enclosing scope reaches an external symbol just as it would reach one of yours. What never happens is shortening: `vector` reaches `std::vector` only from inside `std`, because the shorter the name the likelier it is to collide with something unrelated in a foreign set, and a reference that silently points at the wrong page is worse than one that points nowhere. A name no scope accounts for is left as code.

NOTE: A tag file only says where the pages of that set are, so MrDocs can resolve no more than the set publishes. Cppreference, for one, gives `namespace std` itself no page, so `@ref std::chrono` stays plain text while `@ref std::chrono::duration` links. Either way, it is a reference that resolved to nothing, and xref:configuration/reference.adoc#warn-broken-ref_option[`warn-broken-ref`] reports it as one.

NOTE: For an Antora site there is also the xref:extensions/antora.adoc#antora-cpp-tagfiles-extension[`antora-cpp-tagfiles-extension`], which adds a `cpp:` macro and bundles the Cppreference tag file. The two are complementary, and reach different text: the extension resolves the names you write in your own pages, `input-tagfiles` resolves the ones inside doc comments, for every generator rather than for Asciidoctor alone.

== Styling

Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/extensions/antora.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
= Antora extensions

Two Antora extensions connect the xref:page$generators/adoc.adoc[Asciidoc Generator] into an Antora build. One runs Mr.Docs as a stage inside the Antora build. The other registers the resulting Mr.Docs xref:configuration/reference.adoc#tagfile_option[tagfile], so prose on the site can link to C++ symbols.
Two Antora extensions connect the xref:page$generators/adoc.adoc[Asciidoc Generator] into an Antora build. One runs Mr.Docs as a stage inside the Antora build. The other registers the resulting Mr.Docs xref:configuration/reference.adoc#output-tagfile_option[tagfile], so prose on the site can link to C++ symbols.

[#antora-cpp-reference-extension]
== C++ reference extension
Expand Down
4 changes: 4 additions & 0 deletions docs/modules/ROOT/partials/dom-schema.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2554,6 +2554,10 @@ A reference to a symbol.
|`string`
|Discriminator identifying which inline variant is active.

|`href`
|`string`
|URL documenting the symbol, when that's in another documentation set.

|`id`
|`string`
|Symbol being referenced.
Expand Down
2 changes: 1 addition & 1 deletion docs/website/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ for (let panel of data.panels) {
'--generator=html',
'--embedded=true',
'--show-namespaces=false',
'--tagfile=',
'--output-tagfile=',
];
const command = args.join(' ');
console.log(`Running command: ${command}`)
Expand Down
31 changes: 31 additions & 0 deletions include/mrdocs/Config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,43 @@ class MRDOCS_DECL
void
reportUnknownConfigKeys() const;

/** Warn about the deprecated options this configuration sets.

Reports each one as a warning, or as an error under
`warn-as-error`. Deferred and explicitly called for the same
reason as @ref reportUnknownConfigKeys.
*/
void
reportDeprecatedOptions() const;

private:
/** Keys found in the configuration file that match no known option.

Populated during load and surfaced by @ref reportUnknownConfigKeys.
*/
std::vector<std::string> unknownConfigKeys;

/** A deprecated option this configuration sets, and its advice.

Populated during @ref normalize, which is the pass that sees each
option's properties, and surfaced by
@ref reportDeprecatedOptions.
*/
struct DeprecatedOption
{
/** Name of the option, as the configuration spells it.
*/
std::string name;
/** What to use instead, as the option itself declares.
*/
std::string advice;
};

/** The deprecated options this configuration sets.
*/
std::vector<DeprecatedOption> deprecatedOptions;

friend struct ConfigSchemaVisitor;
};

// Config adds no reflected options of its own; it only inherits the
Expand Down
25 changes: 25 additions & 0 deletions include/mrdocs/Corpus.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
#include <mrdocs/Config.hpp>
#include <mrdocs/Dom.hpp>
#include <mrdocs/Metadata.hpp>
#include <mrdocs/Support/TagfileIndex.hpp>
#include <mrdocs/detail/Corpus.hpp>
#include <algorithm>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
Expand Down Expand Up @@ -135,6 +137,26 @@ class MRDOCS_VISIBLE
Expected<Symbol const&>
lookup(SymbolID const& context, std::string_view name) const;

/** Return the URL documenting a name this corpus does not hold.

The counterpart of @ref lookup for the symbols covered by
another documentation set: the name is resolved from the context
outward, so one written relative to its enclosing scope reaches
an external symbol just as it would reach one of ours. What
comes back is a URL rather than a Symbol, since nothing was
extracted to point at.

A name no enclosing scope accounts for is not matched against
a longer one: `vector` reaches `std::vector` only from inside
`std`, exactly as it would for a symbol of this corpus.

@param context The context the name is written in.
@param name The name of the symbol to look up.
@return The URL, or nothing if no tagfile documents the name.
*/
std::optional<std::string>
externalUrl(SymbolID const& context, std::string_view name) const;

/** Return the Symbol with the matching ID, or nullptr.
*/
Symbol const*
Expand Down Expand Up @@ -386,6 +408,9 @@ class MRDOCS_VISIBLE
// Undocumented symbols.
detail::UndocumentedSymbolSet undocumented_;

// Symbols documented elsewhere, read from the configured tagfiles.
TagfileIndex externalSymbols_;

// Lookup cache: context Symbol ID -> (name -> Info).
std::map<SymbolID, UnorderedStringMap<Symbol const*>> lookupCache_;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ struct ReferenceInline
/** Symbol being referenced.
*/
SymbolID id = SymbolID::invalid;
/** URL documenting the symbol, when that's in another documentation set.

Set when the reference names no symbol in this corpus and a
tagfile says where it is documented, which leaves @ref id
invalid. Empty for a symbol of this corpus.
*/
std::string href;

/** Construct a reference with optional display text.
*/
Expand All @@ -51,7 +58,7 @@ struct ReferenceInline
MRDOCS_DESCRIBE_STRUCT(
ReferenceInline,
(InlineCommonBase<InlineKind::Reference>),
(literal, id)
(literal, id, href)
)

} // mrdocs::doc
Expand Down
96 changes: 96 additions & 0 deletions include/mrdocs/Support/TagfileIndex.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// 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
//

#ifndef MRDOCS_API_SUPPORT_TAGFILEINDEX_HPP
#define MRDOCS_API_SUPPORT_TAGFILEINDEX_HPP

// The reading half of tagfile support.

#include <mrdocs/Platform.hpp>
#include <cstddef>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <string_view>

namespace mrdocs {

/** The symbols documented outside this corpus, and where to find them.

A reference in a doc comment can name a symbol MrDocs never
extracted. A tagfile says which symbols another documentation set
covers and which page each one is on, so a reference to one of them
can still become a link instead of plain text.
*/
class MRDOCS_DECL
TagfileIndex
{
public:
/** Where one symbol is documented.

The parts are what a tagfile offers, joined into a URL by
@ref find.
*/
struct Target
{
/** URL the documentation set is published under.
*/
std::string baseUrl;
/** Name of the page within that documentation set.
*/
std::string page;
/** Anchor on that page; empty for a whole-page entry.
*/
std::string anchor;
};

/** Record where a symbol is documented.

The first target recorded for a name is the one kept, so reading
a name a second time leaves the index as it was.

@return `true` if the target was recorded, `false` if the name
was already known or either the name or the page is empty.

@param qualifiedName The fully qualified name of the symbol.
@param target Where the symbol is documented.
*/
bool
insert(std::string_view qualifiedName, Target target);

/** Return the URL documenting a symbol, or nothing if it has none.

@param qualifiedName The fully qualified name to look for.
*/
std::optional<std::string>
find(std::string_view qualifiedName) const;

/** Return whether the index holds nothing.
*/
bool
empty() const noexcept;

/** Return how many symbols the index holds.

Reported per tagfile as it is read, since a tagfile that
contributes nothing is the first thing to suspect when a
reference to it stays unresolved.
*/
std::size_t
size() const noexcept;

private:
std::map<std::string, Target, std::less<>> targets_;
};

} // mrdocs

#endif // MRDOCS_API_SUPPORT_TAGFILEINDEX_HPP
Loading
Loading