From a9918d5708353a6dcdd8d5ae5c83f3923a78ac80 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Thu, 29 Oct 2026 16:35:21 +0100 Subject: [PATCH 1/5] fix: @sa and @arg keep their text The commands reference documents `@sa` as an alias of `@see` and `@arg` as an alias of `@li`, but the doc-comment extractor handled neither. Clang gives each of them its own command ID, which reached the branch that ignores unhandled block commands, so the paragraph after the command disappeared without a warning. We now handle them in the same `case` as `@see` and `@li`. --- src/mrdocs/AST/ExtractDocComment.cpp | 2 + tests/golden/fixtures/javadoc/lists/arg.cpp | 13 ++ tests/golden/fixtures/javadoc/lists/arg.xml | 156 +++++++++++++++ tests/golden/fixtures/javadoc/see/sa.adoc | 99 +++++++++ tests/golden/fixtures/javadoc/see/sa.cpp | 18 ++ tests/golden/fixtures/javadoc/see/sa.xml | 211 ++++++++++++++++++++ tests/golden/fixtures/javadoc/see/sa.yml | 2 + 7 files changed, 501 insertions(+) create mode 100644 tests/golden/fixtures/javadoc/lists/arg.cpp create mode 100644 tests/golden/fixtures/javadoc/lists/arg.xml create mode 100644 tests/golden/fixtures/javadoc/see/sa.adoc create mode 100644 tests/golden/fixtures/javadoc/see/sa.cpp create mode 100644 tests/golden/fixtures/javadoc/see/sa.xml create mode 100644 tests/golden/fixtures/javadoc/see/sa.yml diff --git a/src/mrdocs/AST/ExtractDocComment.cpp b/src/mrdocs/AST/ExtractDocComment.cpp index bb6c15609da..8b87a4d0428 100644 --- a/src/mrdocs/AST/ExtractDocComment.cpp +++ b/src/mrdocs/AST/ExtractDocComment.cpp @@ -1717,6 +1717,7 @@ class DocCommentVisitor return; } case T::KCI_li: + case T::KCI_arg: { if (jd_.Document.empty() || !jd_.Document.back()->isList()) { @@ -1735,6 +1736,7 @@ class DocCommentVisitor return; } case T::KCI_see: + case T::KCI_sa: { auto see = parseBlock(std::in_place_type); jd_.sees.push_back(std::move(see)); diff --git a/tests/golden/fixtures/javadoc/lists/arg.cpp b/tests/golden/fixtures/javadoc/lists/arg.cpp new file mode 100644 index 00000000000..58d1d869293 --- /dev/null +++ b/tests/golden/fixtures/javadoc/lists/arg.cpp @@ -0,0 +1,13 @@ +/** A function + + @arg Point 1 + @arg Point 2 + + Another paragraph. + + @li Point 3 + @arg Point 4 + @li Point 5 + \arg Point 6 +*/ +void f(); diff --git a/tests/golden/fixtures/javadoc/lists/arg.xml b/tests/golden/fixtures/javadoc/lists/arg.xml new file mode 100644 index 00000000000..6617445a02f --- /dev/null +++ b/tests/golden/fixtures/javadoc/lists/arg.xml @@ -0,0 +1,156 @@ + + + + index + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + 3WB9tVatMdT8usB7kG91HHDMUYwU + + + + f + f + + + + arg.cpp + arg.cpp + 13 + 1 + + + + + function + 3WB9tVatMdT8usB7kG91HHDMUYwU + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + list + + + + + paragraph + + + text + Point 1 + + + + + + + + + paragraph + + + text + Point 2 + + + + + + + unordered + + + paragraph + + + text + Another paragraph. + + + + + list + + + + + paragraph + + + text + Point 3 + + + + + + + + + paragraph + + + text + Point 4 + + + + + + + + + paragraph + + + text + Point 5 + + + + + + + + + paragraph + + + text + Point 6 + + + + + + + unordered + + + + brief + + + text + A function + + + + + + + named + + + identifier + void + + + void + + + normal + + diff --git a/tests/golden/fixtures/javadoc/see/sa.adoc b/tests/golden/fixtures/javadoc/see/sa.adoc new file mode 100644 index 00000000000..face7026d00 --- /dev/null +++ b/tests/golden/fixtures/javadoc/see/sa.adoc @@ -0,0 +1,99 @@ += Reference +:mrdocs: + +[#index] +== Global namespace + +=== Namespaces + +[cols="1"] +|=== +| Name +| link:#A[`A`] +|=== + + +=== Functions + +[cols="1,4"] +|=== +| Name| Description +| link:#f[`f`] +| A function +| link:#g[`g`] +| Another function +|=== + + +[#A] +== A + +=== Functions + +[cols="1,4"] +|=== +| Name| Description +| link:#A-h[`h`] +| A function in a namespace +|=== + + +[#A-h] +== link:#A[A]::h + +A function in a namespace + +=== Synopsis + +Declared in `<sa.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +void +h(); +---- + +[#f] +== f + +A function + +=== Synopsis + +Declared in `<sa.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +void +f(); +---- + +=== See Also + +g + +g + +A::h + +g + +link:#g[`g`] + +[#g] +== g + +Another function + +=== Synopsis + +Declared in `<sa.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +void +g(); +---- + + +[.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/tests/golden/fixtures/javadoc/see/sa.cpp b/tests/golden/fixtures/javadoc/see/sa.cpp new file mode 100644 index 00000000000..4c012aa5019 --- /dev/null +++ b/tests/golden/fixtures/javadoc/see/sa.cpp @@ -0,0 +1,18 @@ +/** A function + + @see g + @sa g + @sa A::h + \sa g + @sa @ref g +*/ +void f(); + +/** Another function */ +void g(); + +namespace A +{ + /** A function in a namespace */ + void h(); +} diff --git a/tests/golden/fixtures/javadoc/see/sa.xml b/tests/golden/fixtures/javadoc/see/sa.xml new file mode 100644 index 00000000000..5016f8784a0 --- /dev/null +++ b/tests/golden/fixtures/javadoc/see/sa.xml @@ -0,0 +1,211 @@ + + + + index + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + 2xwtzebgDk4MeocbdM1v5JoV4Xvj + 3WB9tVatMdT8usB7kG91HHDMUYwU 4An92kkqG8S7Ca73ZvRsDcPWQnS3 + + + + A + A + + + + sa.cpp + sa.cpp + 14 + 1 + + + + namespace + 2xwtzebgDk4MeocbdM1v5JoV4Xvj + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + 2xpyifs7TWj9zhL7iYX9ZhBtVXro + + + + h + h + + + + sa.cpp + sa.cpp + 17 + 5 + + + + + function + 2xpyifs7TWj9zhL7iYX9ZhBtVXro + regular + 2xwtzebgDk4MeocbdM1v5JoV4Xvj + + + brief + + + text + A function in a namespace + + + + + + + named + + + identifier + void + + + void + + + normal + + + f + f + + + + sa.cpp + sa.cpp + 9 + 1 + + + + + function + 3WB9tVatMdT8usB7kG91HHDMUYwU + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + brief + + + text + A function + + + + + + see + + + text + g + + + + + see + + + text + g + + + + + see + + + text + A::h + + + + + see + + + text + g + + + + + see + + + reference + g + 4An92kkqG8S7Ca73ZvRsDcPWQnS3 + + + + + + + + named + + + identifier + void + + + void + + + normal + + + g + g + + + + sa.cpp + sa.cpp + 12 + 1 + + + + + function + 4An92kkqG8S7Ca73ZvRsDcPWQnS3 + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + brief + + + text + Another function + + + + + + + named + + + identifier + void + + + void + + + normal + + diff --git a/tests/golden/fixtures/javadoc/see/sa.yml b/tests/golden/fixtures/javadoc/see/sa.yml new file mode 100644 index 00000000000..d5fd5e08fab --- /dev/null +++ b/tests/golden/fixtures/javadoc/see/sa.yml @@ -0,0 +1,2 @@ +generator: [adoc, xml] +multipage: false From 0b74fe018a4ca5a95d576b479610a65ad5892458 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Thu, 29 Oct 2026 16:35:21 +0100 Subject: [PATCH 2/5] docs: list only the commands the parser handles in the commands reference The commands registry, from which the commands reference is generated, documented three forms that don't work. `@link ... @endlink` is a verbatim block in Clang's command table that only HeaderDoc's `@/link` closes, so everything after `@link` becomes a code block. `@strong` is not a command, so `@strong word` renders as the text "strong word". And the quoted text in `@ref "text"` is printed literally after the link. We now document none of them, and the Cross-reference entry says to write `@ref ` in place of `@link`. The registry also gains `@dot`, `@msc` and `@startuml`, which the parser already renders as code blocks, and the Footnote Definition and Math Block entries now point to the examples that already show them. --- .../ROOT/partials/commands-registry.json | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/modules/ROOT/partials/commands-registry.json b/docs/modules/ROOT/partials/commands-registry.json index b387061dc6f..6593f9c3f65 100644 --- a/docs/modules/ROOT/partials/commands-registry.json +++ b/docs/modules/ROOT/partials/commands-registry.json @@ -96,14 +96,17 @@ "kind": "Block", "syntaxes": [ "@code[{.lang}] ... @endcode", - "@verbatim ... @endverbatim" + "@verbatim ... @endverbatim", + "@dot ... @enddot", + "@msc ... @endmsc", + "@startuml ... @enduml" ], "partial": "commands/code-block.adoc", "examples": [ "commands/code-block" ], "see_also": [], - "description": "Inserts a block of preformatted code into the documentation.\nEverything between the opening and closing tags is preserved verbatim, including whitespace and indentation.\n\n`@code` and `@verbatim` produce the same output.\nUse `@code` for source code and `@verbatim` for other preformatted text, though MrDocs treats them identically.", + "description": "Inserts a block of preformatted code into the documentation.\nEverything between the opening and closing tags is preserved verbatim, including whitespace and indentation.\n\n`@code` and `@verbatim` produce the same output.\nUse `@code` for source code and `@verbatim` for other preformatted text, though MrDocs treats them identically.\n\n`@dot`, `@msc`, and `@startuml` also produce a code block, tagged with the language `dot`, `msc`, and `plantuml` respectively.", "parameters": [ { "name": "lang", @@ -113,7 +116,7 @@ { "name": "code", "type": "Code", - "description": "The verbatim source between the opening command and `@endcode`/`@endverbatim`." + "description": "The verbatim source between the opening command and its closing command." } ] }, @@ -216,7 +219,9 @@ "[^label]: text" ], "partial": "commands/footnote-definition.adoc", - "examples": [], + "examples": [ + "commands/footnote" + ], "see_also": [ "Footnote Reference" ], @@ -243,7 +248,9 @@ "\\f[ expression \\f]" ], "partial": "commands/math-block.adoc", - "examples": [], + "examples": [ + "commands/math" + ], "see_also": [ "Math" ], @@ -582,7 +589,6 @@ "**text**", "__text__", "@b ", - "@strong ", "text" ], "partial": "commands/strong.adoc", @@ -592,7 +598,7 @@ "see_also": [ "Emphasis" ], - "description": "Renders its content in bold (strong) style.\nUse `**` or `__` around the bold text.\nThe Doxygen commands `@b` and `@strong` bold the single word that follows them.", + "description": "Renders its content in bold (strong) style.\nUse `**` or `__` around the bold text.\nThe Doxygen command `@b` bolds the single word that follows it.", "parameters": [ { "name": "word", @@ -816,8 +822,7 @@ "heading": "Cross-reference", "kind": "Inline", "syntaxes": [ - "@ref [\"text\"]", - "@link ... @endlink" + "@ref " ], "partial": "commands/cross-reference.adoc", "examples": [ @@ -827,17 +832,12 @@ "Link", "See-also" ], - "description": "Creates a cross-reference to another documented symbol.\nThe `@ref` command takes a symbol name (optionally namespace-qualified) and produces a link to that symbol's documentation page.\nQualified names use `::` separators (e.g., `@ref A::f1`, `@ref ::A::f1`).\nOperator names are supported (e.g., `@ref F::operator+`).", + "description": "Creates a cross-reference to another documented symbol.\nThe `@ref` command takes a symbol name (optionally namespace-qualified) and produces a link to that symbol's documentation page.\nQualified names use `::` separators (e.g., `@ref A::f1`, `@ref ::A::f1`).\nOperator names are supported (e.g., `@ref F::operator+`).\n\nDoxygen's `@link text @endlink` is not supported: Clang closes `@link` only at `@/link`, so `@endlink` never ends it and the rest of the comment becomes a code block. Write `@ref ` instead.", "parameters": [ { "name": "name", "type": "Symbol", "description": "Qualified name of the symbol to link to." - }, - { - "name": "text", - "type": "Text", - "description": "Optional display text for the link." } ] }, From 33d988faaec79348a5371123db531668dfd75405 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Thu, 29 Oct 2026 16:35:22 +0100 Subject: [PATCH 3/5] ci: check the commands registry against the parser utils/docs/check_commands_registry.py is meant to keep the commands registry in sync with the doc-comment parser, but no CI job ran it, and it had fallen out of step with the registry: it still expected the `node_types` and `preview` fields and the example locations of an earlier layout, and failed with 181 problems, nearly all of them false. We now run it in the utility tests job, with its checks updated to the registry's current shape. It also checked only one direction, that every command the parser handles is documented, which let the registry document `@sa`, `@arg`, `@strong` and `@link` while the parser ignored them. It now also checks that every command a documented syntax names is one the parser handles. --- .github/workflows/ci-utility-tests.yml | 3 + utils/docs/check_commands_registry.py | 389 +++++++++++++++---------- 2 files changed, 242 insertions(+), 150 deletions(-) diff --git a/.github/workflows/ci-utility-tests.yml b/.github/workflows/ci-utility-tests.yml index 04f23fc4bb2..f7343d81528 100644 --- a/.github/workflows/ci-utility-tests.yml +++ b/.github/workflows/ci-utility-tests.yml @@ -79,6 +79,9 @@ jobs: - name: Check bootstrap options partial is up to date run: python3 ./utils/docs/generate_bootstrap_options.py --check + - name: Check commands registry is in sync with the parser + run: python3 ./utils/docs/check_commands_registry.py + # When the Coverage matrix entry was skipped because the develop cache is # warm, fetch develop's LCOV report and re-upload it tagged with the PR's # commit SHA. Codecov sees zero patch lines and unchanged project totals, diff --git a/utils/docs/check_commands_registry.py b/utils/docs/check_commands_registry.py index 64ae6ef45e6..8230432223f 100644 --- a/utils/docs/check_commands_registry.py +++ b/utils/docs/check_commands_registry.py @@ -1,19 +1,32 @@ #!/usr/bin/env python3 """Check that the commands registry stays in sync with the source. -Four checks: +Five checks: 1. Every node type declared in BlockNodes.inc, BlockCommandNodes.inc, - or InlineNodes.inc has a registry entry (or is marked Planned). + or InlineNodes.inc has a registry entry. An entry names its node + type in `node_type`, spelled as the node name plus `Block` or + `Inline` (`Param` in BlockCommandNodes.inc is `ParamBlock`). Planned + entries count: the node exists even if the parser does not yet + produce it. 2. Every parser-dispatched command name from the C++ source appears in some entry's `syntaxes` list. - 3. Every entry carries the prose payload the page needs: a - `description` string for every entry, and a `preview` block - (with `title` and `caption`) for every non-Planned entry. - 4. Every non-Planned entry has at least one example, and each example - resolves to its golden-test mirror under tests/golden/fixtures/ - snippets/, its Antora source under docs/modules/ROOT/examples/, - and its rendered .adoc next to that source. + 3. Every entry carries what commands-registry-extension.js needs to + render it: a `heading`, a `description` string, and a `partial` of + the form commands/.adoc (the extension only generates a + per-entry partial for entries whose `partial` has that shape), with + each slug unique. + 4. Every non-Planned entry has at least one example, and its first + example resolves to the .cpp source and the golden-generated .adoc + output under tests/golden/fixtures/snippets/commands/, the two files + the generated partial includes (the Antora playbook mounts + tests/golden/fixtures/snippets as example$snippets). + 5. Every registry syntax that begins with `@` names a command + the parser dispatches (the reverse of check 2). Only the opening + command of a syntax is checked: in `@code ... @endcode` the closer + `@endcode` is part of the same construct and is not looked up. + Syntaxes that do not begin with `@` (Markdown, HTML tags, the + backslash math spellings) are not covered. Runs in the utility-tests CI step. Reads source files only; does not need mrdocs built. @@ -57,25 +70,26 @@ def parse_inc_file(path): return types -def collect_declared_types(root): - """Return the set of node type names declared in the three .inc files.""" - block_inc = os.path.join( - root, "include/mrdocs/Metadata/DocComment/Block/BlockNodes.inc" - ) - block_cmd_inc = os.path.join( - root, "include/mrdocs/Metadata/DocComment/Block/BlockCommandNodes.inc" - ) - inline_inc = os.path.join( - root, "include/mrdocs/Metadata/DocComment/Inline/InlineNodes.inc" - ) +# Each .inc file with the suffix that turns one of its names into the +# `node_type` spelling used by the registry. The suffix is needed because +# the block and inline node lists both declare `Code` and `Math`. +NODE_INC_FILES = ( + ("include/mrdocs/Metadata/DocComment/Block/BlockNodes.inc", "Block"), + ("include/mrdocs/Metadata/DocComment/Block/BlockCommandNodes.inc", "Block"), + ("include/mrdocs/Metadata/DocComment/Inline/InlineNodes.inc", "Inline"), +) + +def collect_declared_types(root): + """Return the registry `node_type` spelling of every declared node type.""" types = set() - for p in (block_inc, block_cmd_inc, inline_inc): - types.update(parse_inc_file(p)) + for rel_path, suffix in NODE_INC_FILES: + for name in parse_inc_file(os.path.join(root, rel_path)): + types.add(name + suffix) # `Text` holds plain text inside another node; users do not type it as # a command, so it has no registry entry. - types.discard("Text") + types.discard("TextInline") return types @@ -84,6 +98,33 @@ def collect_declared_types(root): # 2. Parse source files for parser-dispatched command strings # --------------------------------------------------------------------------- +# Commands that the registry documents and that ExtractDocComment.cpp +# handles without ever naming them, so the source scans below cannot see +# them and check 5 would wrongly report them. This is not a list of every +# command Clang delivers by comment kind (other verbatim block commands +# also end up as code blocks). Walking Clang's `CommandTraits` at script +# time would require building Clang, so each entry is listed here with the +# code path that handles it. Add an entry only after tracing that path. +# A command the extractor ignores must not be listed, whether Clang knows it +# but no `case` handles it, or Clang does not know it at all (`@strong`). +KIND_HANDLED_COMMANDS = { + # `ParamCommandComment`: `visitNode` -> `visitParam`. + "param", + # `TParamCommandComment`: `visitNode` -> `visitTParam`. + "tparam", + # `VerbatimBlockComment`: `visitNode` -> `visitVerbatimBlock`, whose final + # branch turns a verbatim block without a special name into a + # `CodeBlock` with no language. + "verbatim", + # `InlineCommandComment`: the default branch of `visitInlineCommand` maps + # Clang's render kind through `convertStyle`: bold to `StrongInline` + # for `@b`, monospaced to `CodeInline` for `@c` and `@p`. + "b", + "c", + "p", +} + + def extract_kci_commands(path): """Extract KCI_xxx command names from ExtractDocComment.cpp.""" cmds = set() @@ -99,9 +140,9 @@ def extract_runtime_commands(path): Two places to look: - 1. The customFlagCommands[] array (today: `functionobject`, `functor`). - Reading entries from the array body means adding a new flag command - to the array picks it up here too. + 1. The hand-written entries of the `customFlagCommands[]` array (today: + the `functor` alias). The array's other entries come from + FlagCommands.inc, which `extract_flag_commands` reads. 2. Inline `name == "xxx"` comparisons. In ExtractDocComment.cpp this hits the admonition checks (`name == "tip"`, `"important"`, @@ -127,6 +168,16 @@ def extract_runtime_commands(path): return cmds +def extract_flag_commands(path): + """Extract the lowercase command names from FlagCommands.inc. + + ExtractDocComment.cpp expands each `INFO(PascalName)` entry into a + `customFlagCommands[]` entry spelled in lowercase, so `FunctionObject` + becomes `functionobject`. + """ + return {name.lower() for name in parse_inc_file(path)} + + def extract_krules_syntaxes(path): """Pull inline syntax delimiters from the kRules table.""" with open(path, encoding="utf-8") as f: @@ -173,6 +224,9 @@ def collect_dispatched_commands(root): parse_inlines_path = os.path.join( root, "src/mrdocs/Metadata/Finalizers/DocComment/parseInlines.hpp" ) + flag_inc_path = os.path.join( + root, "include/mrdocs/Metadata/DocComment/FlagCommands.inc" + ) commands = set() @@ -182,18 +236,12 @@ def collect_dispatched_commands(root): # Runtime string checks (customFlagCommands plus inline name == "..." tests). commands.update(extract_runtime_commands(extract_path)) - # Render-kind commands (`@b`, `@strong`, `@c`, `@p`): the default branch - # in visitInlineCommand routes them by Clang's render kind, so there is - # no `KCI_` constant or string literal to grep. Hard-coded here because - # walking CommandTraits at script time would require building Clang. - commands.update({"b", "strong", "c", "p"}) + # Flag commands (`@functionobject`, `@seebelow`, ...) come from the + # FlagCommands.inc X-macro table. + commands.update(extract_flag_commands(flag_inc_path)) - # Clang aliases without their own KCI_ identifier: `@sa` aliases `@see` - # and `@arg` aliases `@li`. Clang routes both to the aliased command's - # KCI_ constant, so the dispatch never mentions them by name. Hard-coded - # for the same reason as above. (`@related` has its own `KCI_related` - # constant; extract_kci_commands finds it without help.) - commands.update({"sa", "arg"}) + # Commands routed by comment kind or render kind rather than by name. + commands.update(KIND_HANDLED_COMMANDS) # kRules carries the inline delimiters (Markdown plus HTML opening tags # like ``, ``, `
`). The Markdown ones (`*`, `**`, `~~`, @@ -238,12 +286,8 @@ def load_registry(root): def registry_node_types(entries): - """Return every `node_types` value across the registry as one set.""" - types = set() - for e in entries: - for nt in e.get("node_types", []): - types.add(nt) - return types + """Return every `node_type` value across the registry as one set.""" + return {e["node_type"] for e in entries if e.get("node_type")} def registry_all_syntaxes(entries): @@ -262,27 +306,30 @@ def syntax_to_command_name(syntax): "@brief" -> "brief" "@code / @endcode" -> "code" + "@param[] " -> "param" "text" -> "" "
" -> "
" "*text*" -> "*text*" (Markdown; left as-is) """ s = syntax.strip() - if s.startswith("@"): - # Strip the `@`; for `@code / @endcode`, keep the first command. - name = s[1:].split()[0].rstrip(",/") - return name + result = s - m = re.match(r"<(\w+)", s) + # Keep the first command only, without its `[...]` or `{...}` suffix. + m = re.match(r"@(\w+)", s) if m: - return f"<{m.group(1)}>" + result = m.group(1) + else: + m = re.match(r"<(\w+)", s) + if m: + result = f"<{m.group(1)}>" - # Markdown delimiter or freeform string: return unchanged. - return s + # Markdown delimiter or freeform string: returned unchanged. + return result # --------------------------------------------------------------------------- -# 4. The four checks +# 4. The checks # --------------------------------------------------------------------------- def check_node_types_covered(declared_types, entries): @@ -327,82 +374,114 @@ def check_commands_covered(dispatched_commands, html_commands, entries): return errors -def check_payload_present(entries, root): - """Check 3: every entry carries the prose payload the page needs. +def check_syntaxes_dispatched(dispatched_commands, entries): + """Check 5: every `@word` registry syntax names a dispatched command. - `description` is required everywhere. `preview.title` and - `preview.caption` are required for non-Planned entries (Planned - entries have no example, so they have no preview block). + The reverse of check 2: a registry entry must not advertise a command + the parser ignores. Only the opening command of each syntax is looked + up, so `@code ... @endcode` is checked as `code`. """ - del root # No filesystem reads here; the registry itself is the source. errors = [] + for e in entries: + for s in e.get("syntaxes", []): + cmd = syntax_to_command_name(s) + if s.strip().startswith("@") and cmd not in dispatched_commands: + errors.append( + f" Registry syntax '{s}' in entry " + f"'{e.get('heading', '?')}' names command '@{cmd}', " + f"which the parser does not dispatch" + ) + return errors + + +def check_payload_present(entries): + """Check 3: every entry carries what the registry extension renders. + + commands-registry-extension.js turns each entry into a partial named + after the slug in its `partial` field, headed by `heading` and + followed by `description`. An entry whose `partial` does not match + commands/.adoc gets no per-entry partial (the reference page + still renders it, through the aggregate), and when two entries share a + slug only one of them gets its per-entry partial. + """ + errors = [] + slugs = {} for e in entries: heading = e.get("heading", "?") + if not e.get("heading"): + errors.append(f" Entry '{heading}' has no `heading`") if not e.get("description"): errors.append(f" Entry '{heading}' has no `description`") - if "Planned" in e.get("status", ""): - continue - preview = e.get("preview") or {} - if not preview.get("title"): - errors.append(f" Entry '{heading}' has no `preview.title`") - if not preview.get("caption"): - errors.append(f" Entry '{heading}' has no `preview.caption`") + errors.extend(partial_errors(e, slugs)) + return errors + + +def partial_errors(entry, slugs): + """Return the problems with an entry's `partial`, recording its slug.""" + heading = entry.get("heading", "?") + errors = [] + m = re.match(r"^commands/(.+)\.adoc$", entry.get("partial") or "") + if not m: + errors.append( + f" Entry '{heading}' has no `partial` of the form " + f"commands/.adoc" + ) + elif m.group(1) in slugs: + errors.append( + f" Entry '{heading}' reuses the partial of entry " + f"'{slugs[m.group(1)]}'" + ) + else: + slugs[m.group(1)] = heading return errors def check_examples_exist(entries, root): - """Check 4: Every non-Planned entry has at least one example, and every - referenced example resolves to a real snippet under - tests/golden/fixtures/snippets/. - - Per spec R9, examples are mandatory for non-Planned entries; missing - examples are an error, not a warning. Each example also has to back - a partial that includes it via `include::example$commands/.cpp[]`, - so the Antora-side source at docs/modules/ROOT/examples/commands/ - must exist too. + """Check 4: every non-Planned entry has at least one example, and its + first example resolves to a .cpp source and a .adoc output under + tests/golden/fixtures/snippets/commands/. + + Examples are mandatory for non-Planned entries; a missing example is an + error, not a warning. The extension includes only the first example of + an entry, as example$snippets/commands/.cpp and .adoc, where + is the example minus a leading commands/ (the Antora playbook + mounts tests/golden/fixtures/snippets as example$snippets). The golden + tests regenerate the .adoc from the .cpp. """ errors = [] - snippets_dir = os.path.join(root, "tests/golden/fixtures/snippets") - antora_examples_dir = os.path.join( - root, "docs/modules/ROOT/examples") for e in entries: - examples = e.get("examples", []) - status = e.get("status", "") - heading = e.get("heading", "?") + if not e.get("planned"): + errors.extend(entry_example_errors(e, root)) + return errors + + +def entry_example_errors(entry, root): + """Return the problems with a non-Planned entry's example.""" + examples = entry.get("examples") or [] + if examples: + errors = example_file_errors( + examples[0], entry.get("heading", "?"), root) + else: + errors = [ + f" Entry '{entry.get('heading', '?')}' has no examples and is " + f"not Planned (at least one example is required)" + ] + return errors - # Planned entries are exempt from the example requirement - if "Planned" in status: - continue - if not examples: +def example_file_errors(example, heading, root): + """Return one error per missing file of an example. + + The path is built the way commands-registry-extension.js builds it. + """ + slug = example.removeprefix("commands/") + errors = [] + for ext in (".cpp", ".adoc"): + rel = f"tests/golden/fixtures/snippets/commands/{slug}{ext}" + if not os.path.isfile(os.path.join(root, rel)): errors.append( - f" Entry '{heading}' has no examples and is not Planned " - f"(at least one example is required)" + f" Example '{example}' for entry '{heading}' is missing {rel}" ) - continue - - for ex in examples: - mirror_cpp = os.path.join(snippets_dir, ex + ".cpp") - antora_cpp = os.path.join(antora_examples_dir, ex + ".cpp") - rendered = os.path.join(antora_examples_dir, ex + ".rendered.adoc") - if not os.path.isfile(mirror_cpp): - errors.append( - f" Example '{ex}' for entry '{heading}' is missing its " - f"golden-test mirror at " - f"tests/golden/fixtures/snippets/{ex}.cpp" - ) - if not os.path.isfile(antora_cpp): - errors.append( - f" Example '{ex}' for entry '{heading}' is missing its " - f"Antora source at docs/modules/ROOT/examples/{ex}.cpp" - ) - if not os.path.isfile(rendered): - errors.append( - f" Example '{ex}' for entry '{heading}' is missing its " - f"rendered output at " - f"docs/modules/ROOT/examples/{ex}.rendered.adoc " - f"(run `python utils/render_snippets.py --update`)" - ) return errors @@ -410,55 +489,65 @@ def check_examples_exist(entries, root): # Main # --------------------------------------------------------------------------- -def main(): - args = parse_args() - root = args.repo_root - - print("Commands registry coverage check") - print(f"Repository root: {root}") - print() +def build_checks(root): + """Return the checks as (title, errors, total, noun) rows. + `total` describes what was examined and `noun` names what the errors + count, so each row prints as `OK (, )`. + """ declared_types = collect_declared_types(root) - dispatched_commands, html_commands = collect_dispatched_commands(root) + dispatched, html_commands = collect_dispatched_commands(root) entries = load_registry(root) + return ( + ("declared node types have a registry entry", + check_node_types_covered(declared_types, entries), + f"{len(declared_types)} types", "gaps"), + ("parser-dispatched commands appear in the registry", + check_commands_covered(dispatched, html_commands, entries), + f"{len(dispatched) + len(html_commands)} commands", "gaps"), + ("entries carry what the registry extension renders", + check_payload_present(entries), + f"{len(entries)} entries", "missing"), + ("example files exist (source and rendered output)", + check_examples_exist(entries, root), + f"{len(entries)} entries", "missing"), + ("registry syntaxes name dispatched commands", + check_syntaxes_dispatched(dispatched, entries), + f"{len(entries)} entries", "undispatched"), + ) - all_errors = [] - print("Check 1: declared node types have a registry entry...") - errs = check_node_types_covered(declared_types, entries) - all_errors.extend(errs) - print(f" {'FAIL' if errs else 'OK'} ({len(declared_types)} types, {len(errs)} gaps)") - for e in errs: - print(e) - - print("Check 2: parser-dispatched commands appear in the registry...") - errs = check_commands_covered(dispatched_commands, html_commands, entries) - all_errors.extend(errs) - cmd_count = len(dispatched_commands) + len(html_commands) - print(f" {'FAIL' if errs else 'OK'} ({cmd_count} commands, {len(errs)} gaps)") - for e in errs: - print(e) - - print("Check 3: entries carry description + preview payload...") - errs = check_payload_present(entries, root) - all_errors.extend(errs) - print(f" {'FAIL' if errs else 'OK'} ({len(entries)} entries, {len(errs)} missing)") - for e in errs: - print(e) - - print("Check 4: example files exist (mirror, source, rendered)...") - errs = check_examples_exist(entries, root) - all_errors.extend(errs) - print(f" {'FAIL' if errs else 'OK'} ({len(entries)} entries, {len(errs)} missing)") - for e in errs: - print(e) +def report_check(number, check): + """Print one check's outcome and return its errors.""" + title, errors, total, noun = check + print(f"Check {number}: {title}...") + print(f" {'FAIL' if errors else 'OK'} ({total}, {len(errors)} {noun})") + for error in errors: + print(error) + return errors + +def report_summary(all_errors): + """Print the final verdict and return the process exit code.""" print() if all_errors: print(f"FAILED: {len(all_errors)} issue(s) found") - return 1 - print("PASSED: all checks passed") - return 0 + result = 1 + else: + print("PASSED: all checks passed") + result = 0 + return result + + +def main(): + args = parse_args() + print("Commands registry coverage check") + print(f"Repository root: {args.repo_root}") + print() + all_errors = [] + for number, check in enumerate(build_checks(args.repo_root), start=1): + all_errors.extend(report_check(number, check)) + return report_summary(all_errors) if __name__ == "__main__": From faf3cbe4aaf66a07599994ea43ade0f9450581c5 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Fri, 30 Oct 2026 11:44:13 +0100 Subject: [PATCH 4/5] docs: correct what the docs say about the brief, see-below and untitled headings The `auto-brief` option, the configuration and commands pages and the commands registry said that the first sentence of a doc comment becomes its brief, but we take the whole first paragraph. They now say so, as does the `auto-brief` example, and the option's description lists the commands that still set a brief when it is off. Likewise, the synopsis placeholder of a `@seebelow` symbol is `/* see-below */`, not `/* see below */`, and a `@par` with no title takes the first line of its body as the heading, where the registry said it emitted none. --- .../ROOT/attachments/schemas/config/mrdocs.schema.json | 4 ++-- docs/modules/ROOT/pages/commands/metadata.adoc | 4 ++-- docs/modules/ROOT/pages/configuration/extraction.adoc | 2 +- docs/modules/ROOT/pages/migration-notes.adoc | 4 ++-- docs/modules/ROOT/partials/commands-registry.json | 6 +++--- src/mrdocs/ConfigOptions.json | 4 ++-- src/mrdocs/Metadata/Finalizers/DocCommentFinalizer.hpp | 2 +- .../fixtures/snippets/options/auto-brief/auto-brief.adoc | 2 +- .../fixtures/snippets/options/auto-brief/auto-brief.cpp | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json b/docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json index 863a2929d78..f8713255d64 100644 --- a/docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json +++ b/docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json @@ -19,12 +19,12 @@ }, "auto-brief": { "default": true, - "description": "When set to `true`, Mr.Docs uses the first line (until the first dot, question mark, or exclamation mark) of the comment as the brief of the symbol. When set to `false`, a explicit @brief command is required.", + "description": "When set to `true`, Mr.Docs uses the first paragraph of the comment as the brief of the symbol. When set to `false`, only the `@brief`, `@short`, `@copybrief`, and `@copydoc` commands set a brief.", "enum": [ true, false ], - "title": "Use the first line of the comment as the brief", + "title": "Use the first paragraph of the comment as the brief", "type": "boolean" }, "auto-function-metadata": { diff --git a/docs/modules/ROOT/pages/commands/metadata.adoc b/docs/modules/ROOT/pages/commands/metadata.adoc index 5171b2938cb..bc773526461 100644 --- a/docs/modules/ROOT/pages/commands/metadata.adoc +++ b/docs/modules/ROOT/pages/commands/metadata.adoc @@ -4,7 +4,7 @@ A metadata command attaches a structured fact to the symbol instead of adding pr == Summary -The first sentence of a doc comment is the symbol's xref:commands/reference.adoc#cmd-summary[summary] by default. Reach for `@brief` (or `@short`) only when the opening sentence is not the summary you want. +The first paragraph of a doc comment is the symbol's xref:commands/reference.adoc#cmd-summary[summary] by default. Reach for `@brief` (or `@short`) only when the opening paragraph is not the summary you want. .Summary [source,cpp] @@ -77,7 +77,7 @@ include::example$snippets/commands/see-also.adoc[tags=!footer] == Symbol flags -Three commands set a flag on the symbol rather than producing visible text. xref:commands/reference.adoc#cmd-function-object[`@functionobject`] marks a `constexpr` variable as an https://en.cppreference.com/cpp/algorithm/ranges#Algorithm_function_objects[Algorithm Function Object (AFO)^]: put it on the variable, document the type's `operator()` (that is where the AFO's signature and text come from), and reach for it only when the type has extra members that defeat auto-detection. xref:commands/reference.adoc#cmd-see-below[`@seebelow`] renders a declaration's body as `pass:[/* see below */]`. xref:commands/reference.adoc#cmd-implementation-defined[`@implementationdefined`] hides a type whose definition is not part of the public interface, so its uses render as `pass:[/* implementation-defined */]`. +Three commands set a flag on the symbol rather than producing visible text. xref:commands/reference.adoc#cmd-function-object[`@functionobject`] marks a `constexpr` variable as an https://en.cppreference.com/cpp/algorithm/ranges#Algorithm_function_objects[Algorithm Function Object (AFO)^]: put it on the variable, document the type's `operator()` (that is where the AFO's signature and text come from), and reach for it only when the type has extra members that defeat auto-detection. xref:commands/reference.adoc#cmd-see-below[`@seebelow`] renders a declaration's body as `pass:[/* see-below */]`. xref:commands/reference.adoc#cmd-implementation-defined[`@implementationdefined`] hides a type whose definition is not part of the public interface, so its uses render as `pass:[/* implementation-defined */]`. .A small regex engine using all three flags [source,cpp] diff --git a/docs/modules/ROOT/pages/configuration/extraction.adoc b/docs/modules/ROOT/pages/configuration/extraction.adoc index 5a8df0c8890..9e30af732b7 100644 --- a/docs/modules/ROOT/pages/configuration/extraction.adoc +++ b/docs/modules/ROOT/pages/configuration/extraction.adoc @@ -98,7 +98,7 @@ MrDocs fills several kinds of metadata from the signature when the doc comment l === Briefs -By default, the first sentence of a doc comment becomes the brief and the rest becomes the detailed description. xref:configuration/reference.adoc#auto-brief_option[`auto-brief`] is on by default; turn it off if you want every brief written explicitly with xref:commands/reference.adoc#cmd-summary[`@brief`]. +By default, the first paragraph of a doc comment becomes the brief and the rest becomes the detailed description. xref:configuration/reference.adoc#auto-brief_option[`auto-brief`] is on by default; turn it off if you want every brief written explicitly with xref:commands/reference.adoc#cmd-summary[`@brief`]. .Example [source,cpp] diff --git a/docs/modules/ROOT/pages/migration-notes.adoc b/docs/modules/ROOT/pages/migration-notes.adoc index baceda0f3d1..e66dd6c3f64 100644 --- a/docs/modules/ROOT/pages/migration-notes.adoc +++ b/docs/modules/ROOT/pages/migration-notes.adoc @@ -148,7 +148,7 @@ The type is part of the public API, and callers may need to name it, but the doc Mr.Docs:: + -- -The type is declared normally and tagged with the `@seebelow` doc command. The synopsis is rendered as "see below" and the members are omitted, but the type itself remains a documented symbol in the corpus. No configuration entry is needed; the command on the symbol is enough: +The type is declared normally and tagged with the `@seebelow` doc command. The synopsis body is rendered as `+/* see-below */+` and the members are omitted, but the type itself remains a documented symbol in the corpus. No configuration entry is needed; the command on the symbol is enough: .`include/netz/buffer_view.hpp` [source,c++] @@ -167,7 +167,7 @@ include::example$snippets/options/see-below/include/netz/stream.hpp[tag=body] The `see-below` configuration option also accepts a glob of symbol names for cases where the doc-command treatment is not practical, for example when the same rule has to apply across many types or when the source can't be changed. The `@implementationdefined` command and the matching `implementation-defined` configuration option work the same way. ==== -.The type keeps its page, but the members collapse into a "see below" placeholder +.The type keeps its page, but the members collapse into a "see-below" placeholder [.adoc-preview] ======== include::example$snippets/options/see-below/see-below.adoc[tags=!footer] diff --git a/docs/modules/ROOT/partials/commands-registry.json b/docs/modules/ROOT/partials/commands-registry.json index 6593f9c3f65..e9d5f11035e 100644 --- a/docs/modules/ROOT/partials/commands-registry.json +++ b/docs/modules/ROOT/partials/commands-registry.json @@ -44,7 +44,7 @@ { "name": "title", "type": "Text", - "description": "The heading text, on the same line as `@par`. If omitted, no heading is emitted." + "description": "The heading text, on the same line as `@par`. If omitted, the first line of the section body becomes the heading." }, { "name": "content", @@ -280,7 +280,7 @@ "see_also": [ "Paragraph" ], - "description": "The `@brief` command marks its content as the brief description (summary) of the documented symbol.\nThe first sentence of a doc comment is automatically treated as the brief, so you only need `@brief` when you want to override that behavior or when the automatic detection does not capture what you intend.\n\n`@short` is an alias for `@brief`.", + "description": "The `@brief` command marks its content as the brief description (summary) of the documented symbol.\nThe first paragraph of a doc comment is automatically treated as the brief, so you only need `@brief` when you want to override that behavior or when the automatic detection does not capture what you intend.\n\n`@short` is an alias for `@brief`.", "parameters": [ { "name": "text", @@ -533,7 +533,7 @@ "commands/see-below" ], "see_also": [], - "description": "Marks the symbol so its synopsis renders as `/* see below */` instead of the full declaration, for interfaces whose exact form is unspecified." + "description": "Marks the symbol so its synopsis renders as `/* see-below */` instead of the full declaration, for interfaces whose exact form is unspecified." }, { "flag": "ImplementationDefined", diff --git a/src/mrdocs/ConfigOptions.json b/src/mrdocs/ConfigOptions.json index 71b8f940656..c6b8f563a7f 100644 --- a/src/mrdocs/ConfigOptions.json +++ b/src/mrdocs/ConfigOptions.json @@ -433,8 +433,8 @@ }, { "name": "auto-brief", - "brief": "Use the first line of the comment as the brief", - "details": "When set to `true`, Mr.Docs uses the first line (until the first dot, question mark, or exclamation mark) of the comment as the brief of the symbol. When set to `false`, a explicit @brief command is required.", + "brief": "Use the first paragraph of the comment as the brief", + "details": "When set to `true`, Mr.Docs uses the first paragraph of the comment as the brief of the symbol. When set to `false`, only the `@brief`, `@short`, `@copybrief`, and `@copydoc` commands set a brief.", "type": "bool", "default": true }, diff --git a/src/mrdocs/Metadata/Finalizers/DocCommentFinalizer.hpp b/src/mrdocs/Metadata/Finalizers/DocCommentFinalizer.hpp index 35b7ca90005..3d5666074a0 100644 --- a/src/mrdocs/Metadata/Finalizers/DocCommentFinalizer.hpp +++ b/src/mrdocs/Metadata/Finalizers/DocCommentFinalizer.hpp @@ -122,7 +122,7 @@ class DocCommentFinalizer { This might mean copying the brief from another symbol (when there's a copybrief command) or - populating it automatically (first sentence). + populating it automatically (first paragraph). */ void finalizeBrief(Symbol& I); diff --git a/tests/golden/fixtures/snippets/options/auto-brief/auto-brief.adoc b/tests/golden/fixtures/snippets/options/auto-brief/auto-brief.adoc index df4b0d1e42a..ad23dcc28e3 100644 --- a/tests/golden/fixtures/snippets/options/auto-brief/auto-brief.adoc +++ b/tests/golden/fixtures/snippets/options/auto-brief/auto-brief.adoc @@ -20,7 +20,7 @@ truncate( === Description -The first sentence of this comment becomes the brief; the rest is the description. +The first paragraph of this comment becomes the brief; the rest is the description. [.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/tests/golden/fixtures/snippets/options/auto-brief/auto-brief.cpp b/tests/golden/fixtures/snippets/options/auto-brief/auto-brief.cpp index 8f4b3ac4c5b..1db7bbe2d54 100644 --- a/tests/golden/fixtures/snippets/options/auto-brief/auto-brief.cpp +++ b/tests/golden/fixtures/snippets/options/auto-brief/auto-brief.cpp @@ -1,6 +1,6 @@ /** Truncate a string to the given length. - The first sentence of this comment becomes the + The first paragraph of this comment becomes the brief; the rest is the description. */ void truncate(char* s, int n); From e6904aac2577f6efafd0aecd5bf5e72430942d85 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Fri, 30 Oct 2026 10:45:15 +0100 Subject: [PATCH 5/5] docs: add a Doxygen compatibility page A project migrating from Doxygen had no way to tell which of its commands MrDocs honors short of reading the parser, and many of them are ignored without a warning: some lose their text, others publish text the author meant to hide. The new page lists every Doxygen command by family, with its status, what happens to its text, and the issue that tracks it where there is one. It also lists the comment styles MrDocs accepts and the configuration options that replace Doxygen markup. The commands registry check now also compares the page with the parser, so a command that starts or stops being handled fails CI until its row says so. Fixes #1342. --- .github/workflows/ci-utility-tests.yml | 2 +- docs/modules/ROOT/nav.adoc | 1 + docs/modules/ROOT/pages/commands/doxygen.adoc | 515 ++++++++++++++++++ docs/modules/ROOT/pages/commands/index.adoc | 2 + docs/modules/ROOT/pages/migration-notes.adoc | 2 +- utils/docs/check_commands_registry.py | 204 ++++++- 6 files changed, 709 insertions(+), 17 deletions(-) create mode 100644 docs/modules/ROOT/pages/commands/doxygen.adoc diff --git a/.github/workflows/ci-utility-tests.yml b/.github/workflows/ci-utility-tests.yml index f7343d81528..fd4ea4f1403 100644 --- a/.github/workflows/ci-utility-tests.yml +++ b/.github/workflows/ci-utility-tests.yml @@ -79,7 +79,7 @@ jobs: - name: Check bootstrap options partial is up to date run: python3 ./utils/docs/generate_bootstrap_options.py --check - - name: Check commands registry is in sync with the parser + - name: Check the commands registry and the Doxygen page against the parser run: python3 ./utils/docs/check_commands_registry.py # When the Coverage matrix entry was skipped because the develop cache is diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 344cc1b6943..ec9383f6958 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -9,6 +9,7 @@ ** xref:commands/metadata.adoc[Metadata Commands] ** xref:commands/inlines.adoc[Inline Commands] ** xref:commands/reference.adoc[Reference] +** xref:commands/doxygen.adoc[Doxygen Compatibility] * Configuration ** xref:configuration/index.adoc[Overview] ** xref:configuration/inputs.adoc[Inputs] diff --git a/docs/modules/ROOT/pages/commands/doxygen.adoc b/docs/modules/ROOT/pages/commands/doxygen.adoc new file mode 100644 index 00000000000..a7eb30129f1 --- /dev/null +++ b/docs/modules/ROOT/pages/commands/doxygen.adoc @@ -0,0 +1,515 @@ += Doxygen Compatibility + +// Maintainer note. The command tables on this page are checked against the +// parser by utils/docs/check_commands_registry.py. Each table row is four +// cells, one per line: the command spellings, the status, the effect or +// replacement, and a link, which may be empty. The header row is written on +// one line, and no cell wraps onto a second line. In the first cell every +// spelling sits in its own backtick span, and only the opening `@word` of a +// span is looked up, so `@code ... @endcode` names `code`. The status is +// exactly one of Supported, Different idiom, Not supported, or MrDocs only. +// A Supported or MrDocs only row must name commands the parser dispatches, +// and every dispatched command must have such a row. A dispatched command +// named in any other row is an error. The commands in `EXEMPT_COMMANDS` in +// the script are exempt from both rules, and the page lists them as Not +// supported. Spellings without a word, such as `@f$` or `@{`, are not +// looked up. + +MrDocs reads the doc comments of a C++ project that already uses Doxygen, but it is not a Doxygen clone. This page answers the question a migrating project asks first: which Doxygen commands does MrDocs understand, and what happens to the ones it does not. The xref:commands/reference.adoc[Commands Reference] lists what MrDocs understands. This page lists what Doxygen has. + +== Comment styles + +MrDocs accepts the same comment styles as Doxygen: + +* `pass:[/** ... */]`, the Javadoc style. +* `pass:[/*! ... */]`, the Qt style. +* `pass:[///]`, a run of line comments. +* `pass:[//!]`, a run of line comments in the Qt style. +* `pass:[///<]`, `pass:[//!<]`, `pass:[/**<]`, and `pass:[/*!<]`, which document the declaration to their left, such as a member variable or an enumerator. + +A command can start with a backslash or with an `@`. The two are the same, and the tables below use the `@` spelling. Plain `//` and `pass:[/* ... */]` comments are not doc comments and are ignored. + +The brief is the whole first paragraph of the comment, up to the first blank line or block command. With `JAVADOC_AUTOBRIEF` or `QT_AUTOBRIEF` on, Doxygen takes the first sentence; otherwise it needs `@brief` or a one-line `///` comment. Write a short first paragraph, or set the brief explicitly with `@brief`. With xref:configuration/reference.adoc#auto-brief_option[`auto-brief`] set to `false`, MrDocs sets no brief unless the comment uses `@brief`, `@short`, `@copybrief` or `@copydoc`. + +== Commands + +Each table row names one or more commands and gives their status: + +* *Supported*: MrDocs understands the command. The Effect column says what it produces, and the More column links the matching entry of the Commands Reference. +* *Different idiom*: MrDocs does the same job another way. The Effect column says how. +* *Not supported*: MrDocs does not understand the command. The Effect column says what happens to the text, because most of these commands do not fail loudly. The More column links the issue that tracks the gap, when there is one. +* *MrDocs only*: the command has no Doxygen counterpart. Only the last table uses this status. + +A command that closes another, such as `@endcode`, is listed on the row of the command that opens it. + +MrDocs does not warn about a command it ignores. The issue https://github.com/cppalliance/mrdocs/issues/1369[#1369^] asks for a warning. + +=== Documentation commands + +These commands shape the description of a symbol. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@brief`, `@short` +|Supported +|Sets the brief. When one of them is present, the first paragraph is no longer the brief. +|xref:commands/reference.adoc#cmd-summary[Summary] + +|`@details` +|Supported +|Adds a paragraph to the description. With no paragraph before it, that paragraph becomes the brief. +|xref:commands/reference.adoc#cmd-paragraph[Paragraph] + +|`@par` +|Supported +|The text on the line becomes a heading, and the lines beneath it become a paragraph. Unlike Doxygen, an untitled `@par` turns the first line of its body into the heading. A body that starts with a command, such as `@c`, is dropped. +|xref:commands/reference.adoc#cmd-heading[Heading] + +|`@li`, `@arg` +|Supported +|Adds a list item. +|xref:commands/reference.adoc#cmd-list[List] + +|`@code ... @endcode` +|Supported +|Code block. A language given as in `@code{.cpp}` is kept. +|xref:commands/reference.adoc#cmd-code-block[Code Block] + +|`@verbatim ... @endverbatim` +|Supported +|Code block with no language. +|xref:commands/reference.adoc#cmd-code-block[Code Block] + +|`@param`, `@tparam` +|Supported +|Documents a parameter. A direction in brackets, such as `[in]`, is kept. +|xref:commands/reference.adoc#cmd-parameters[Parameters], xref:commands/reference.adoc#cmd-template-parameters[Template Parameters] + +|`@return`, `@returns`, `@result` +|Supported +|Documents the return value. +|xref:commands/reference.adoc#cmd-returns[Returns] + +|`@throw`, `@throws`, `@exception` +|Supported +|Documents an exception type and when it is thrown. +|xref:commands/reference.adoc#cmd-exceptions[Exceptions] + +|`@pre` +|Supported +|Documents a precondition. +|xref:commands/reference.adoc#cmd-preconditions[Preconditions] + +|`@post` +|Supported +|Documents a postcondition. +|xref:commands/reference.adoc#cmd-postconditions[Postconditions] + +|`@see`, `@sa` +|Supported +|Adds an entry to the See also list. A bare name in its argument is plain text; put an `@ref` before it to make a link. +|xref:commands/reference.adoc#cmd-see-also[See-also] + +|`@note`, `@warning`, `@important` +|Supported +|Admonition. +|xref:commands/reference.adoc#cmd-admonitions[Admonitions] + +|`@deprecated` +|Not supported +|The command and the rest of its paragraph are dropped, so the deprecation notice does not appear. No warning is issued. +|https://github.com/cppalliance/mrdocs/issues/1368[#1368^] + +|`@retval` +|Not supported +|The command and the rest of its paragraph are dropped, without a warning. Describe the values in `@return` instead. +|https://github.com/cppalliance/mrdocs/issues/1368[#1368^] + +|`@xrefitem` +|Not supported +|The command and the rest of its paragraph are dropped, without a warning. +|https://github.com/cppalliance/mrdocs/issues/1368[#1368^] + +|`@attention`, `@author`, `@authors`, `@bug`, `@copyright`, `@date`, `@invariant`, `@remark`, `@remarks`, `@since`, `@test`, `@todo`, `@version` +|Not supported +|The command and the rest of its paragraph are dropped, without a warning. +|https://github.com/cppalliance/mrdocs/issues/1368[#1368^] + +|`@n` +|Not supported +|The command is accepted and ignored. It produces no line break. +| + +|`@noop`, `@addindex` +|Not supported +|The command and the rest of its line are dropped. +| + +|`@raisewarning`, `@qualifier`, `@showdate`, `@doxyconfig`, `@requirement`, `@satisfies`, `@verifies` +|Not supported +|Shown as plain text, including the command word. +| + +|`@parblock ... @endparblock` +|Not supported +|Shown as a code block, so the text inside is not formatted. +| + +|=== + +=== Structural commands + +A Doxygen comment can sit anywhere and name the entity it documents. MrDocs reads the C++ source, so the comment goes on the declaration and the entity is already known. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@class`, `@struct`, `@union`, `@interface`, `@protocol`, `@category`, `@enum`, `@namespace`, `@concept`, `@idlexcept` +|Different idiom +|Put the comment on the declaration. If the command is left in, it and the rest of its line are dropped. +|xref:commands/index.adoc[Documenting the Code] + +|`@fn`, `@var`, `@typedef`, `@def`, `@property` +|Different idiom +|Put the comment on the declaration. If the command is left in, it and the rest of its line are dropped. +|xref:commands/index.adoc[Documenting the Code] + +|`@overload` +|Different idiom +|MrDocs groups the overloads of a function by itself. The command and the rest of its line are dropped. +|xref:configuration/reference.adoc#overloads_option[`overloads`] + +|`@module`, `@package` +|Not supported +|Shown as plain text, including the command word. +| + +|`@headerfile` +|Not supported +|The command and the rest of its paragraph are dropped, without a warning. +| + +|`@example`, `@dir` +|Not supported +|The command and the rest of its line are dropped. +| + +|`@extends`, `@implements` +|Not supported +|The command word is dropped and its argument stays as plain text. +| + +|=== + +=== Grouping commands + +MrDocs has no groups or topics yet. Symbols appear under their namespace or record. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@defgroup`, `@ingroup`, `@addtogroup`, `@weakgroup` +|Not supported +|The command and the rest of its line are dropped. The symbol is not grouped. +|https://github.com/cppalliance/mrdocs/issues/1344[#1344^] + +|`@name` +|Not supported +|The command and the rest of its line are dropped. The members are not grouped. +|https://github.com/cppalliance/mrdocs/issues/229[#229^] + +|`+@{+`, `+@}+` +|Not supported +|Shown as plain text, including the `@` and the brace. +|https://github.com/cppalliance/mrdocs/issues/229[#229^] + +|`@nosubgrouping`, `@public`, `@protected`, `@private`, `@publicsection`, `@protectedsection`, `@privatesection`, `@static`, `@pure`, `@showinitializer`, `@hideinitializer` +|Not supported +|The command word is dropped and its argument stays as plain text. Access and storage come from the C++ declaration. +| + +|=== + +=== Conditional commands + +These commands hide or show text. MrDocs does not evaluate them, so the text they guard is published. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@cond ... @endcond` +|Not supported +|The lines with the commands are dropped, and the text between them is published as ordinary text. +|https://github.com/cppalliance/mrdocs/issues/1343[#1343^] + +|`@if ... @elseif ... @else ... @endif`, `@ifnot` +|Not supported +|The lines with the commands are dropped, and the text of every branch is published. +| + +|`@internal ... @endinternal` +|Not supported +|The text between them is published as a code block, so internal documentation is not hidden. +| + +|=== + +=== File-level commands + +These commands document a file, a page, or the project as a whole. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@file` +|Not supported +|The command and the rest of its line are dropped. The comment documents the next declaration when nothing such as an `#include` separates them, and is lost otherwise. +|https://github.com/cppalliance/mrdocs/issues/1248[#1248^] + +|`@mainpage`, `@page`, `@subpage` +|Not supported +|The command and the rest of its line are dropped. +|https://github.com/cppalliance/mrdocs/issues/1344[#1344^] + +|`@section`, `@subsection`, `@subsubsection`, `@paragraph`, `@tableofcontents` +|Not supported +|The command and the rest of its line are dropped. +| + +|`@subparagraph`, `@subsubparagraph` +|Not supported +|Shown as plain text, including the command word. +| + +|=== + +=== Include commands + +These commands insert the contents of another file. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@include`, `@snippet`, `@verbinclude`, `@htmlinclude`, `@latexinclude`, `@rtfinclude`, `@maninclude`, `@docbookinclude`, `@xmlinclude`, `@dontinclude`, `@skip`, `@skipline`, `@until`, `@line` +|Not supported +|The command and the rest of its line are dropped. The file is not read. This includes the `{lineno}` and `{doc}` forms. +| + +|`@includelineno`, `@includedoc`, `@snippetlineno`, `@snippetdoc` +|Not supported +|Shown as plain text, including the command word. +| + +|`@lineinfo`, `@fileinfo` +|Not supported +|Shown as plain text, including the command word. +| + +|=== + +=== Markup commands + +These commands format text or embed other content. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@a`, `@e`, `@em` +|Supported +|Emphasizes the next word. +|xref:commands/reference.adoc#cmd-emphasis[Emphasis] + +|`@b` +|Supported +|Makes the next word strong. +|xref:commands/reference.adoc#cmd-strong[Strong] + +|`@c`, `@p` +|Supported +|Sets the next word as code. +|xref:commands/reference.adoc#cmd-code-span[Code Span] + +|`@link ... @endlink` +|Different idiom +|Use `@ref `, without custom link text. Left in, `@link` turns the rest of the comment into a code block, because Clang only closes it at `@/link`. +|xref:commands/reference.adoc#cmd-cross-reference[Cross-reference] + +|`@image` +|Different idiom +|Use the Markdown form `![alt](src)`. The command and the rest of its line are dropped. +|xref:commands/reference.adoc#cmd-image[Image] + +|`@dot ... @enddot`, `@msc ... @endmsc`, `@startuml ... @enduml` +|Supported +|Code block tagged `dot`, `msc`, or `plantuml`. The diagram is not rendered. +|xref:commands/reference.adoc#cmd-code-block[Code Block] + +|`@dotfile`, `@mscfile`, `@diafile` +|Not supported +|The command and the rest of its line are dropped. +| + +|`@plantumlfile`, `@mermaidfile`, `@vhdlflow` +|Not supported +|Shown as plain text, including the command word. +| + +|`@mermaid ... @endmermaid` +|Not supported +|Shown as plain text, including the command words. +| + +|`@secreflist ... @endsecreflist` +|Not supported +|Shown as a code block. +| + +|`@anchor`, `@emoji` +|Not supported +|The command word is dropped and its argument stays as plain text. +| + +|`+@f$ ... @f$+` +|Supported +|Inline formula. +|xref:commands/reference.adoc#cmd-math[Math] + +|`+@f[ ... @f]+` +|Supported +|Display formula. +|xref:commands/reference.adoc#cmd-math-block[Math Block] + +|`+@f{ ... @f}+`, `+@f( ... @f)+` +|Not supported +|Shown as a code block. +| + +|=== + +=== Cross-reference commands + +These commands link to other symbols or copy their documentation. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@ref` +|Supported +|Links to a symbol. The quoted text after the name is not used as link text. It stays in the output as literal text. +|xref:commands/reference.adoc#cmd-cross-reference[Cross-reference] + +|`@relates`, `@related`, `@relatesalso`, `@relatedalso`, `@memberof` +|Supported +|Lists the function as a related symbol of the named record. +|xref:commands/reference.adoc#cmd-relates[Relates] + +|`@copydoc`, `@copybrief`, `@copydetails` +|Supported +|Copies the documentation, the brief, or the details of another symbol. +|xref:commands/reference.adoc#cmd-copy[Copy] + +|`@cite`, `@refitem` +|Not supported +|The command word is dropped and its argument stays as plain text. +| + +|=== + +=== Output-format commands + +These commands pass text to one output format. MrDocs renders every format from one model, so there is no such pass-through. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@htmlonly ... @endhtmlonly`, `@latexonly ... @endlatexonly`, `@manonly ... @endmanonly`, `@rtfonly ... @endrtfonly`, `@xmlonly ... @endxmlonly`, `@docbookonly ... @enddocbookonly` +|Not supported +|Shown as a code block tagged `html`, `latex`, `man`, `rtf`, `xml`, or `docbook`, in every output format. An option such as `[block]` becomes part of the code. +| + +|=== + +=== Graph and display switches + +These commands switch graphs and source listings on or off for a symbol. MrDocs draws no graphs. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@callgraph`, `@callergraph`, `@hidecallgraph`, `@hidecallergraph`, `@showrefby`, `@hiderefby`, `@showrefs`, `@hiderefs` +|Not supported +|The command word is dropped and its argument stays as plain text. +| + +|`@collaborationgraph`, `@hidecollaborationgraph`, `@includegraph`, `@hideincludegraph`, `@includedbygraph`, `@hideincludedbygraph`, `@directorygraph`, `@hidedirectorygraph`, `@groupgraph`, `@hidegroupgraph`, `@inheritancegraph`, `@hideinheritancegraph`, `@showenumvalues`, `@hideenumvalues`, `@showinlinesource`, `@hideinlinesource` +|Not supported +|Shown as plain text, including the command word. +| + +|=== + +=== Commands only MrDocs has + +These commands have no Doxygen counterpart. + +[cols="3,2,5,2",options="header"] +|=== +|Command |Status |Effect or replacement |More + +|`@tip`, `@caution` +|MrDocs only +|Admonitions, like `@note` and `@warning`. +|xref:commands/reference.adoc#cmd-admonitions[Admonitions] + +|`@seebelow` +|MrDocs only +|Replaces the synopsis body of a declaration with `+/* see-below */+`. +|xref:commands/reference.adoc#cmd-see-below[See Below] + +|`@implementationdefined` +|MrDocs only +|Hides a type whose definition is not part of the public interface. +|xref:commands/reference.adoc#cmd-implementation-defined[Implementation Defined] + +|`@functionobject`, `@functor` +|MrDocs only +|Marks a variable as an algorithm function object. +|xref:commands/reference.adoc#cmd-function-object[Function Object] + +|=== + +=== Escape sequences + +The escapes `+\@+`, `+\&+`, `+\#+`, `+\%+`, `+\"+`, `+\.+`, `+\::+`, `+\|+`, `+\--+`, `+\---+`, `+\=+`, `+\?+`, and `+\!+` are supported: each produces the text that follows the backslash. `+\$+`, `+\<+`, and `+\>+` are not: Clang removes the backslash first, so `+\$5 or \$6+` becomes inline math and `+\X\+` becomes emphasis. `+\\+` is not supported either: both characters are dropped. `+\~+` is not supported either: the tilde is shown, and the language that follows it is not used to filter the text. + +== Idioms that replace Doxygen markup + +Doxygen often needs markup in the comments or in the source to hide, shorten, or split documentation. MrDocs usually does the same job from the configuration. + +Hiding a symbol:: +Doxygen uses `@internal`, `@cond`, or `EXCLUDE_SYMBOLS`. In MrDocs, the xref:configuration/reference.adoc#exclude-symbols_option[`exclude-symbols`] option drops every symbol that matches one of its patterns, and xref:configuration/filters.adoc[Filters] describes the other filters. Because the symbol leaves the corpus, a reference to it renders as its literal name. + +Types callers should not name:: +Doxygen projects often use a documentation-only macro. In MrDocs, the xref:configuration/reference.adoc#implementation-defined_option[`implementation-defined`] option, or the xref:commands/reference.adoc#cmd-implementation-defined[`@implementationdefined`] command on the declaration, drops the page of the symbol and renders every reference to it as `+/* implementation-defined */+`. + +Exposition-only synopses:: +The xref:configuration/reference.adoc#see-below_option[`see-below`] option, or the xref:commands/reference.adoc#cmd-see-below[`@seebelow`] command, keeps the page of the symbol but collapses its synopsis to `+/* see-below */+`. + +Documentation for different audiences:: +Doxygen uses `ENABLED_SECTIONS` with `@if`, or a second Doxyfile. In MrDocs, write one configuration file per audience and run MrDocs once for each, with xref:configuration/reference.adoc#config_option[`--config`]. Each file has its own filters, such as `include-symbols`, `exclude-symbols`, and xref:configuration/reference.adoc#extract-private_option[`extract-private`], and its own output directory. The xref:migration-notes.adoc#idioms-without-workarounds[Migration Notes] show the idioms side by side. + +== Other differences + +* MrDocs links a name to its symbol only where a command asks for it. Doxygen links names it recognizes in plain text. See https://github.com/cppalliance/mrdocs/issues/1208[#1208^]. +* MrDocs reads part of Markdown and HTML, not all of it. The xref:commands/inlines.adoc[Inline Commands] page lists what works. See https://github.com/cppalliance/mrdocs/issues/1291[#1291^]. diff --git a/docs/modules/ROOT/pages/commands/index.adoc b/docs/modules/ROOT/pages/commands/index.adoc index c2934316021..d66e84fa61f 100644 --- a/docs/modules/ROOT/pages/commands/index.adoc +++ b/docs/modules/ROOT/pages/commands/index.adoc @@ -22,6 +22,8 @@ Each doc comment in the example above starts with a brief sentence, optionally f Doc comments can also carry special commands like `@param`, `@returns`, or `@throws` that attach structured information to the declaration. MrDocs supports most commands you would already use in a Javadoc or Doxygen comment. The xref:commands/reference.adoc[Commands Reference] lists every command MrDocs understands, grouped by category. +Coming from Doxygen? +The xref:commands/doxygen.adoc[Doxygen Compatibility] page lists every Doxygen command with its status, and says what happens to the text of those MrDocs does not understand. == Documentation Comment Formats diff --git a/docs/modules/ROOT/pages/migration-notes.adoc b/docs/modules/ROOT/pages/migration-notes.adoc index e66dd6c3f64..9f62c6a5f1e 100644 --- a/docs/modules/ROOT/pages/migration-notes.adoc +++ b/docs/modules/ROOT/pages/migration-notes.adoc @@ -391,7 +391,7 @@ PREDEFINED = JZON_DOCUMENTATION_BUILD=1 -- ====== -Many other idioms collapse into a one-line configuration option or a single doc command: exposition-only "see-below" synopses, hidden friend operators, related-symbol grouping, deprecated-but-documented overloads, and so on. The xref:commands/reference.adoc[Commands] and xref:configuration/reference.adoc[Options] references are the place to look when you find yourself reaching for a preprocessor conditional. +Many other idioms collapse into a one-line configuration option or a single doc command: exposition-only "see-below" synopses, hidden friend operators, related-symbol grouping, deprecated-but-documented overloads, and so on. The xref:commands/reference.adoc[Commands] and xref:configuration/reference.adoc[Options] references are the place to look when you find yourself reaching for a preprocessor conditional, and xref:commands/doxygen.adoc[Doxygen Compatibility] lists every Doxygen command with its status. [#handling-dependencies] == Handling dependencies diff --git a/utils/docs/check_commands_registry.py b/utils/docs/check_commands_registry.py index 8230432223f..3ee62b68a0f 100644 --- a/utils/docs/check_commands_registry.py +++ b/utils/docs/check_commands_registry.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -"""Check that the commands registry stays in sync with the source. +"""Check the commands registry and the Doxygen page against the source. -Five checks: +Six checks: 1. Every node type declared in BlockNodes.inc, BlockCommandNodes.inc, or InlineNodes.inc has a registry entry. An entry names its node @@ -10,7 +10,7 @@ entries count: the node exists even if the parser does not yet produce it. 2. Every parser-dispatched command name from the C++ source appears - in some entry's `syntaxes` list. + in some entry's `syntaxes` list, except those in `EXEMPT_COMMANDS`. 3. Every entry carries what commands-registry-extension.js needs to render it: a `heading`, a `description` string, and a `partial` of the form commands/.adoc (the extension only generates a @@ -27,6 +27,10 @@ `@endcode` is part of the same construct and is not looked up. Syntaxes that do not begin with `@` (Markdown, HTML tags, the backslash math spellings) are not covered. + 6. The command tables of the Doxygen compatibility page agree with the + parser, except for the commands in `EXEMPT_COMMANDS`. The row + format and the rules are described in the comment at the top of + the page. Runs in the utility-tests CI step. Reads source files only; does not need mrdocs built. @@ -100,11 +104,12 @@ def collect_declared_types(root): # Commands that the registry documents and that ExtractDocComment.cpp # handles without ever naming them, so the source scans below cannot see -# them and check 5 would wrongly report them. This is not a list of every -# command Clang delivers by comment kind (other verbatim block commands -# also end up as code blocks). Walking Clang's `CommandTraits` at script -# time would require building Clang, so each entry is listed here with the -# code path that handles it. Add an entry only after tracing that path. +# them, and checks 5 and 6 would wrongly report the registry syntaxes and +# page rows that name them. This is not a list of every command Clang +# delivers by comment kind (other verbatim block commands also end up as +# code blocks). Walking Clang's `CommandTraits` at script time would +# require building Clang, so each entry is listed here with the code path +# that handles it. Add an entry only after tracing that path. # A command the extractor ignores must not be listed, whether Clang knows it # but no `case` handles it, or Clang does not know it at all (`@strong`). KIND_HANDLED_COMMANDS = { @@ -125,6 +130,13 @@ def collect_declared_types(root): } +# Dispatched commands that have neither a registry entry (check 2) nor a +# `Supported` page row (check 6). The parser dispatches `@n` but emits only a +# newline text that merges into a space, so the page lists it as +# `Not supported`. +EXEMPT_COMMANDS = {"n"} + + def extract_kci_commands(path): """Extract KCI_xxx command names from ExtractDocComment.cpp.""" cmds = set() @@ -299,6 +311,12 @@ def registry_all_syntaxes(entries): return syntaxes +# The opening `@word` of a syntax or of a page spelling. The formula +# spellings `@f$`, `@f[`, `@f{` and `@f(` begin with `@f` but name no +# command, so they do not match. +OPENING_COMMAND_RE = re.compile(r"@(?!f[$\[{(])(\w+)") + + def syntax_to_command_name(syntax): """Reduce a registry syntax string to a key comparable with the dispatch. @@ -316,7 +334,7 @@ def syntax_to_command_name(syntax): result = s # Keep the first command only, without its `[...]` or `{...}` suffix. - m = re.match(r"@(\w+)", s) + m = OPENING_COMMAND_RE.match(s) if m: result = m.group(1) else: @@ -354,12 +372,7 @@ def check_commands_covered(dispatched_commands, html_commands, entries): errors = [] - for cmd in sorted(dispatched_commands): - # `@n` is parser-dispatched but only emits a newline inside a - # TextInline; it has no documentation entry, like the `Text` node - # type itself. - if cmd == "n": - continue + for cmd in sorted(dispatched_commands - EXEMPT_COMMANDS): if cmd not in registry_cmds: errors.append( f" Parser-dispatched command '@{cmd}' not found in registry syntaxes" @@ -485,6 +498,164 @@ def example_file_errors(example, heading, root): return errors +# --------------------------------------------------------------------------- +# 6. The Doxygen compatibility page +# --------------------------------------------------------------------------- + +PAGE_PATH = "docs/modules/ROOT/pages/commands/doxygen.adoc" + +# A row is one cell per line: spellings, status, effect, link. +PAGE_ROW_CELLS = 4 + +# The statuses that say the parser handles the command. +SUPPORTED_STATUSES = ("Supported", "MrDocs only") +PAGE_STATUSES = SUPPORTED_STATUSES + ("Different idiom", "Not supported") + + +def page_tables(text): + """Return (line number, cells, stray lines) for each table of the page. + + A table runs between two `|===` lines. Its first line is the header + row, written on one line, and is skipped. Every other line that starts + with `|` is one cell, kept with its line number. Any other non-blank + line is a stray (a wrapped cell), kept with its line number. + """ + tables = [] + in_table = False + header_pending = False + for number, line in enumerate(text.splitlines(), start=1): + if line.startswith("|==="): + in_table = not in_table + header_pending = in_table + if in_table: + tables.append((number, [], [])) + elif in_table and line.startswith("|") and header_pending: + header_pending = False + elif in_table and line.startswith("|"): + tables[-1][1].append((number, line[1:].strip())) + elif in_table and line.strip(): + tables[-1][2].append((number, line.strip())) + return tables + + +def table_rows(start, cells, strays): + """Return (rows, errors) for one table; a malformed table has no rows.""" + errors = [ + f" {PAGE_PATH}:{number}: line inside a table does not start with " + f"'|' (a cell must sit on one line): {text}" + for number, text in strays + ] + if len(cells) % PAGE_ROW_CELLS: + errors.append( + f" {PAGE_PATH}:{start}: the table has {len(cells)} cells, not " + f"a multiple of {PAGE_ROW_CELLS}: a row has the wrong number " + f"of cells" + ) + rows = [] + if not errors: + rows = [ + (cells[i][0], [c for _, c in cells[i:i + PAGE_ROW_CELLS]]) + for i in range(0, len(cells), PAGE_ROW_CELLS) + ] + return rows, errors + + +def page_rows(text): + """Return (rows, errors): each row is (line number, four cells).""" + rows = [] + errors = [] + for start, cells, strays in page_tables(text): + table, table_errors = table_rows(start, cells, strays) + rows.extend(table) + errors.extend(table_errors) + return rows, errors + + +def row_commands(first_cell): + """Return the command names looked up in a row's first cell.""" + spans = re.findall(r"`\+?([^`]+)`", first_cell) + matches = map(OPENING_COMMAND_RE.match, spans) + return [m.group(1) for m in matches if m] + + +def page_row_errors(dispatched, required, row): + """Return the problems with one table row.""" + number, (first, status, _effect, _link) = row + errors = [] + if status not in PAGE_STATUSES: + errors.append( + f" {PAGE_PATH}:{number}: unknown status '{status}' (expected " + f"one of: {', '.join(PAGE_STATUSES)})" + ) + else: + errors.extend( + status_errors(dispatched, required, number, first, status)) + return errors + + +def status_errors(dispatched, required, number, first, status): + """Return the commands of a row whose status contradicts the parser.""" + supported = status in SUPPORTED_STATUSES + errors = [] + for cmd in row_commands(first): + if supported and cmd not in dispatched: + errors.append( + f" {PAGE_PATH}:{number}: row marked '{status}' names " + f"'@{cmd}', which the parser does not dispatch" + ) + elif not supported and cmd in required: + errors.append( + f" {PAGE_PATH}:{number}: row marked '{status}' names " + f"'@{cmd}', which the parser dispatches" + ) + return errors + + +def page_supported_commands(rows): + """Return every command named by a `Supported` or `MrDocs only` row.""" + return { + cmd + for _, (first, status, _, _) in rows + if status in SUPPORTED_STATUSES + for cmd in row_commands(first) + } + + +def page_coverage_errors(required, rows): + """Return one error per required command without a supporting row.""" + supported = page_supported_commands(rows) + return [ + f" Parser-dispatched command '@{cmd}' has no Supported or " + f"MrDocs only row in {PAGE_PATH}" + for cmd in sorted(required - supported) + ] + + +def page_mismatches(dispatched, text): + """Return (errors, row count) for the text of the page.""" + required = dispatched - EXEMPT_COMMANDS + rows, errors = page_rows(text) + malformed = bool(errors) + for row in rows: + errors.extend(page_row_errors(dispatched, required, row)) + if not malformed: + # A malformed table hides its rows, so coverage would only repeat + # the damage as a list of missing commands. + errors.extend(page_coverage_errors(required, rows)) + return errors, len(rows) + + +def check_page_matches_parser(dispatched, root): + """Check 6: the page's command tables agree with the parser.""" + path = os.path.join(root, PAGE_PATH) + if os.path.isfile(path): + with open(path, encoding="utf-8") as f: + result = page_mismatches(dispatched, f.read()) + else: + result = ([f" {PAGE_PATH} does not exist"], 0) + return result + + # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- @@ -498,6 +669,7 @@ def build_checks(root): declared_types = collect_declared_types(root) dispatched, html_commands = collect_dispatched_commands(root) entries = load_registry(root) + page_errors, page_row_count = check_page_matches_parser(dispatched, root) return ( ("declared node types have a registry entry", check_node_types_covered(declared_types, entries), @@ -514,6 +686,8 @@ def build_checks(root): ("registry syntaxes name dispatched commands", check_syntaxes_dispatched(dispatched, entries), f"{len(entries)} entries", "undispatched"), + ("Doxygen page rows match the dispatched commands", + page_errors, f"{page_row_count} rows", "mismatches"), )