diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml index a67066dad3..773f927fdb 100644 --- a/docs/antora-playbook.yml +++ b/docs/antora-playbook.yml @@ -83,6 +83,11 @@ asciidoc: logo-img: '' logo-text: 'Mr. Docs' icon-img: '' + # Render LaTeX math. `stem:[...]` and `[stem]` blocks become MathJax + # delimiters (\(...\) inline, \[...\] display); the UI head loads + # MathJax to typeset them. MrDocs's own `$...$` / `\f$...\f$` doc-comment + # math emits `stem:[...]`, so this lights up the generated reference too. + stem: latexmath # Make every `[tabs]` block in the docs a synchronised tabset. # A tab click in one tabset selects the matching label in every # other tabset on the page, and the choice is persisted in diff --git a/docs/extensions/mrdocs-demos.js b/docs/extensions/mrdocs-demos.js index 6f95481494..3fa887a380 100644 --- a/docs/extensions/mrdocs-demos.js +++ b/docs/extensions/mrdocs-demos.js @@ -180,25 +180,25 @@ module.exports = function (registry) { // demos. The attribute is set by Antora. const pageVersion = parent.getDocument().getAttribute( 'page-component-version') - // Collect all demo URLs + // Collect all demo URLs. The layout on the server is + // demos////, where is one of + // adoc, adoc-asciidoc, html, or xml. Each format directory holds a + // full multipage tree (html, adoc, adoc-asciidoc) or a single file + // (xml); there is no separate single/multi level. let finalDemoDirs = []; const versions = getSubdirectoriesSync('https://mrdocs.com/demos/') .filter((v) => !pageVersion || v === pageVersion); for (const version of versions) { const demoLibraries = getSubdirectoriesSync(`https://mrdocs.com/demos/${version}/`); for (const demoLibrary of demoLibraries) { - const pageTypes = getSubdirectoriesSync(`https://mrdocs.com/demos/${version}/${demoLibrary}/`); - for (const pageType of pageTypes) { - const demoFormats = getSubdirectoriesSync(`https://mrdocs.com/demos/${version}/${demoLibrary}/${pageType}/`); - for (const demoFormat of demoFormats) { - finalDemoDirs.push({ - url: `https://mrdocs.com/demos/${version}/${demoLibrary}/${pageType}/${demoFormat}`, - version: version, - library: demoLibrary, - pageType: pageType, - format: demoFormat - }); - } + const demoFormats = getSubdirectoriesSync(`https://mrdocs.com/demos/${version}/${demoLibrary}/`); + for (const demoFormat of demoFormats) { + finalDemoDirs.push({ + url: `https://mrdocs.com/demos/${version}/${demoLibrary}/${demoFormat}`, + version: version, + library: demoLibrary, + format: demoFormat + }); } } } @@ -211,129 +211,59 @@ module.exports = function (registry) { } } - // Create tables. Each page renders only its own version (see - // the version filter above), so the per-version heading would - // just repeat the page title. + // Format columns, in display order. AsciiDoc links to the + // rendered (`adoc-asciidoc`) multipage output rather than the raw + // `.adoc` tree, so the reader sees a browsable page; each cell is + // the format icon linking to that demo. + const iconBase = 'https://raw.githubusercontent.com/cppalliance/mrdocs/refs/heads/develop/docs/modules/ROOT/images/icons/'; + const formatColumns = [ + { key: 'html', label: 'HTML', icon: 'html5.svg', entry: 'index.html' }, + { key: 'adoc-asciidoc', label: 'AsciiDoc', icon: 'asciidoc.svg', entry: 'index.html' }, + { key: 'xml', label: 'XML', icon: 'code_blocks.svg', entry: 'reference.xml' }, + ]; + + // Each page renders only its own version (see the version filter + // above), so a per-version heading would just repeat the title. let text = '' for (const version of allVersions) { - text += '\n' - text += `|===\n`; + // Libraries present in this version, sorted by display name. + const versionLibraries = [...new Set( + finalDemoDirs + .filter(d => d.version === version) + .map(d => d.library))] + .sort((a, b) => humanizeLibrary(a).localeCompare(humanizeLibrary(b))); - // Collect all unique page types, formats, and libraries for this version - let versionPageTypes = []; - let versionFormats = []; - let versionLibraries = []; - for (const demoDir of finalDemoDirs) { - if (demoDir.version !== version) { - continue - } - if (!versionPageTypes.includes(demoDir.pageType)) { - versionPageTypes.push(demoDir.pageType); - } - if (!versionFormats.includes(demoDir.format)) { - versionFormats.push(demoDir.format); - } - if (!versionLibraries.includes(demoDir.library)) { - versionLibraries.push(demoDir.library); - } + // Keep only the format columns that exist for this version, + // preserving the column order above. + const columns = formatColumns.filter(col => + finalDemoDirs.some(d => d.version === version && d.format === col.key)); + if (versionLibraries.length === 0 || columns.length === 0) { + continue; } - // Sort versionPageTypes so that multipage always comes first - versionPageTypes.sort((a, b) => { - if (a === 'multi' && b !== 'multi') return -1; - if (a !== 'multi' && b === 'multi') return 1; - return a.localeCompare(b); - }); - - // Drop the raw `.adoc` format and keep only the - // rendered AsciiDoc (`adoc-asciidoc`); the single - // AsciiDoc column links to the rendered version. - versionFormats = versionFormats.filter(format => format !== 'adoc'); - // Order columns HTML first, then AsciiDoc, then XML. - versionFormats.sort((a, b) => { - const order = ['html', 'adoc-asciidoc', 'xml']; - const aIndex = order.indexOf(a); - const bIndex = order.indexOf(b); - if (aIndex === -1 && bIndex === -1) return a.localeCompare(b); - if (aIndex === -1) return 1; - if (bIndex === -1) return -1; - return aIndex - bIndex; - }); - - // Sort versionLibraries alphabetically - versionLibraries.sort((a, b) => humanizeLibrary(a).localeCompare(humanizeLibrary(b))); - - // Ensure XML is never in the multipage formats - let multipageFormats = versionFormats.filter(format => format !== 'xml'); - let versionFormatColumns = versionFormats.map(format => `*${humanizeFormat(format)}*`).join(' | '); - let multipageFormatColumns = multipageFormats.map(format => `*${humanizeFormat(format)}*`).join(' | '); - - // Multicells for the page format taking as many formats as possible for that page type - const toPageTypeCell = pageType => `${(pageType === 'multi' ? multipageFormats : versionFormats).length}+| *${humanizePageType(pageType)}*`; - text += `| ${versionPageTypes.map(toPageTypeCell).join(' ')}\n`; - - text += `| *Library* | ${multipageFormatColumns} | ${versionFormatColumns}\n\n`; + const colCount = columns.length + 1; + text += `\n[cols="${colCount}*a",options="header"]\n`; + text += `|===\n`; + text += `| Library ${columns.map(col => `| ${col.label}`).join(' ')}\n\n`; for (const library of versionLibraries) { - text += `| ${libraryLink(library)}` - for (const pageType of versionPageTypes) { - const pageTypeFormats = pageType === 'multi' ? multipageFormats : versionFormats; - for (const format of pageTypeFormats) { - const demoDir = finalDemoDirs.find( - demoDir => demoDir.version === version && - demoDir.library === library && - demoDir.pageType === pageType && - demoDir.format === format); - if (demoDir) { - const demoUrlWithSuffix = demoDir.url + (() => { - if (format === 'xml') - { - return '/reference.xml'; - } - if (format === 'adoc') - { - if (pageType === 'multi') - { - return '/index.adoc' - } - else { - return '/reference.adoc' - } - } - if (format === 'html' || format === 'adoc-asciidoc') - { - if (pageType === 'multi') - { - return '/index.html' - } - else { - return '/reference.html' - } - } - return ''; - })() - if (['adoc', 'xml', 'html', 'adoc-asciidoc'].includes(format)) { - const formatIcons = { - adoc: 'https://raw.githubusercontent.com/cppalliance/mrdocs/refs/heads/develop/docs/modules/ROOT/images/icons/asciidoc.svg', - html: 'https://raw.githubusercontent.com/cppalliance/mrdocs/refs/heads/develop/docs/modules/ROOT/images/icons/html5.svg', - // The single AsciiDoc column now points at the rendered - // form, but the cell still represents the AsciiDoc format, - // so use the AsciiDoc icon. - 'adoc-asciidoc': 'https://raw.githubusercontent.com/cppalliance/mrdocs/refs/heads/develop/docs/modules/ROOT/images/icons/asciidoc.svg', - default: 'https://raw.githubusercontent.com/cppalliance/mrdocs/refs/heads/develop/docs/modules/ROOT/images/icons/code_blocks.svg' - }; - const icon = formatIcons[format] || formatIcons.default; - text += `| image:${icon}[${humanizeLibrary(library)} reference in ${humanizeFormat(format)} format,width=16,height=16,link=${demoUrlWithSuffix},window=_blank]` - } else { - text += `| ${demoUrlWithSuffix}[🔗,window=_blank]` - } - } else { - text += `| ` - } + text += `| ${libraryLink(library)}`; + for (const col of columns) { + const demoDir = finalDemoDirs.find(d => + d.version === version && + d.library === library && + d.format === col.key); + if (demoDir) { + const icon = iconBase + col.icon; + const url = `${demoDir.url}/${col.entry}`; + const alt = `${humanizeLibrary(library)} reference in ${col.label} format`; + text += ` | image:${icon}[${alt},width=24,height=24,link=${url},window=_blank]`; + } else { + text += ` |`; } } - text += `\n` + text += `\n`; } - text += `|===\n\n` + text += `|===\n\n`; } return self.parseContent(parent, text) diff --git a/docs/modules/ROOT/images/MrDocsBanner.jpg b/docs/modules/ROOT/images/MrDocsBanner.jpg deleted file mode 100644 index e9a23de516..0000000000 Binary files a/docs/modules/ROOT/images/MrDocsBanner.jpg and /dev/null differ diff --git a/docs/modules/ROOT/pages/commands/inlines.adoc b/docs/modules/ROOT/pages/commands/inlines.adoc index 4b5b270d3d..e5712051ba 100644 --- a/docs/modules/ROOT/pages/commands/inlines.adoc +++ b/docs/modules/ROOT/pages/commands/inlines.adoc @@ -11,6 +11,8 @@ MrDocs reads the usual Markdown spans, with Doxygen and HTML equivalents alongsi * xref:commands/reference.adoc#cmd-code-span[Code span]: `+`text`+`. * xref:commands/reference.adoc#cmd-strikethrough[Strikethrough]: `+~~text~~+`. * xref:commands/reference.adoc#cmd-highlight[Highlight]: `+==text==+`. +* xref:commands/reference.adoc#cmd-subscript[Subscript]: `+~text~+` or `+text+`. +* xref:commands/reference.adoc#cmd-superscript[Superscript]: `+^text^+` or `+text+`. .Text formatting [source,cpp] diff --git a/docs/modules/ROOT/pages/demos.adoc b/docs/modules/ROOT/pages/demos.adoc index 34380365e0..a22f85c46d 100644 --- a/docs/modules/ROOT/pages/demos.adoc +++ b/docs/modules/ROOT/pages/demos.adoc @@ -4,3 +4,13 @@ A few examples of reference documentation generated with Mr.Docs are available i mrdocs-demos::[] +== Libraries using Mr.Docs + +These libraries publish their reference documentation with Mr.Docs: + +* https://redboltz.github.io/async_mqtt/doc/latest/async_mqtt/reference.html[async_mqtt,window=_blank] +* https://www.boost.org/doc/libs/develop/libs/dynamic_bitset/doc/html/dynamic_bitset/reference/boost/dynamic_bitset.html[Boost.DynamicBitset,window=_blank] +* https://www.boost.org/doc/libs/develop/libs/openmethod/doc/html/openmethod/reference/boost/openmethod.html[Boost.OpenMethod,window=_blank] +* https://www.boost.org/doc/libs/develop/libs/redis/doc/html/redis/reference/boost/redis.html[Boost.Redis,window=_blank] +* https://www.boost.org/latest/doc/antora/url/reference/boost/urls.html[Boost.URL,window=_blank] + diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index e621d98791..d1d8b08d7c 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -47,6 +47,8 @@ Doc comments in the Javadoc and Doxygen tradition. Block-level commands describe include::example$snippets/landing/sqrt.cpp[tag=doc] ---- +Doc comments carry LaTeX math too. The `\f$...\f$` and `$...$` spans in the comment above render as real symbols, so a complexity like stem:[O(\log n)] or a definition like stem:[\lfloor\sqrt{value}\rfloor] reads the way it would in a paper. + == What Mr.Docs produces * Several generators emit documentation in formats ranging from rendered documents for human readers to structured data for downstream tooling. diff --git a/docs/modules/ROOT/partials/commands-registry.json b/docs/modules/ROOT/partials/commands-registry.json index f6808f9274..b387061dc6 100644 --- a/docs/modules/ROOT/partials/commands-registry.json +++ b/docs/modules/ROOT/partials/commands-registry.json @@ -211,7 +211,7 @@ { "node_type": "FootnoteDefinitionBlock", "heading": "Footnote Definition", - "kind": "Planned", + "kind": "Block", "syntaxes": [ "[^label]: text" ], @@ -220,8 +220,7 @@ "see_also": [ "Footnote Reference" ], - "planned": true, - "description": "The AST defines a FootnoteDefinition node, but no parser path produces it yet.\nWhen that lands, it will hold the body of a footnote that inline content can reference.", + "description": "Defines the body of a footnote, matched to references by its label.\nStart the paragraph with the label after a caret in square brackets, then a colon and the footnote text.", "parameters": [ { "name": "label", @@ -238,17 +237,17 @@ { "node_type": "MathBlock", "heading": "Math Block", - "kind": "Planned", + "kind": "Block", "syntaxes": [ - "$$ expression $$" + "$$ expression $$", + "\\f[ expression \\f]" ], "partial": "commands/math-block.adoc", "examples": [], "see_also": [ "Math" ], - "planned": true, - "description": "The AST defines a block-level Math node, but no parser path produces it yet.\nThis is the block form, separate from the inline `$...$` and `$$...$$` math that already works.\nWhen that lands, it will hold a standalone math expression as its own block.", + "description": "A standalone display math expression on its own line.\nA paragraph that is only `$$...$$`, or a `\\f[...\\f]` Doxygen block, becomes a math block; inline `$...$` and `\\f$...\\f$` stay in the surrounding text as a Math span.", "parameters": [ { "name": "expression", @@ -634,7 +633,8 @@ "kind": "Inline", "syntaxes": [ "~~text~~", - "text" + "text", + "text" ], "partial": "commands/strikethrough.adoc", "examples": [ @@ -681,7 +681,9 @@ "text" ], "partial": "commands/superscript.adoc", - "examples": [], + "examples": [ + "commands/superscript" + ], "see_also": [ "Subscript" ], @@ -692,8 +694,7 @@ "type": "Text", "description": "The text to raise as superscript." } - ], - "planned": true + ] }, { "node_type": "SubscriptInline", @@ -704,7 +705,9 @@ "text" ], "partial": "commands/subscript.adoc", - "examples": [], + "examples": [ + "commands/subscript" + ], "see_also": [ "Superscript" ], @@ -715,8 +718,7 @@ "type": "Text", "description": "The text to lower as subscript." } - ], - "planned": true + ] } ], "math": [ @@ -726,22 +728,25 @@ "kind": "Inline", "syntaxes": [ "$expression$ (inline)", - "$$expression$$ (display)" + "$$expression$$ (display)", + "\\f$expression\\f$ (inline)", + "\\f[expression\\f] (display)" ], "partial": "commands/math.adoc", - "examples": [], + "examples": [ + "commands/math" + ], "see_also": [ "Code Span" ], - "description": "Renders a mathematical expression.\nSingle dollar signs (`$...$`) produce an inline math span that flows within surrounding text.\nDouble dollar signs (`$$...$$`) produce a display math block, typically centered on its own line.\nThe content between the delimiters is treated as a literal math expression and is not parsed for Markdown or other formatting.", + "description": "Renders a mathematical expression.\nSingle dollar signs (`$...$`) produce an inline math span that flows within surrounding text; double dollar signs (`$$...$$`) produce a display math block on its own line.\nThe Doxygen delimiters `\\f$...\\f$` (inline) and `\\f[...\\f]` (display) are also recognized. Prefer them when the surrounding code is compiled with `-Wdocumentation`, since Clang parses them as known commands whereas `$...$` bodies can trip `-Wdocumentation-unknown-command`.\nThe content between the delimiters is treated as a literal math expression and is not parsed for Markdown or other formatting.", "parameters": [ { "name": "expression", "type": "Math", "description": "A LaTeX math expression; `$...$` inline, `$$...$$` display." } - ], - "planned": true + ] } ], "links_and_images": [ @@ -781,33 +786,28 @@ "kind": "Inline", "syntaxes": [ "![alt](src)", - "\"alt\"", - "@image [\"caption\"]" + "\"alt\"" ], "partial": "commands/image.adoc", - "examples": [], + "examples": [ + "commands/image" + ], "see_also": [ "Link" ], - "description": "Embeds an image inline.\nThe Markdown form is an exclamation mark, alt text in square brackets, then the source path in parentheses.\nThe HTML form uses an `` tag with `src` and `alt` attributes.", + "description": "Embeds an image inline.\nThe Markdown form is an exclamation mark, alt text in square brackets, then the source path in parentheses.\nThe HTML form uses an `` tag with `src` and `alt` attributes.\nBoth forms map to the same node, so pick whichever reads better. The Doxygen `@image` command is not supported, because it names a single output format and MrDocs renders every format from one model.", "parameters": [ { - "name": "format", - "type": "Text", - "description": "Output format the image applies to, e.g. `html`." - }, - { - "name": "file", + "name": "src", "type": "Path", - "description": "Path to the image file." + "description": "Path or URL of the image." }, { - "name": "caption", + "name": "alt", "type": "Text", - "description": "Optional caption." + "description": "Alternate text shown when the image cannot be displayed." } - ], - "planned": true + ] } ], "cross_references": [ @@ -844,17 +844,18 @@ { "node_type": "FootnoteReferenceInline", "heading": "Footnote Reference", - "kind": "Planned", + "kind": "Inline", "syntaxes": [ "[^label]" ], "partial": "commands/footnote-reference.adoc", - "examples": [], + "examples": [ + "commands/footnote" + ], "see_also": [ "Footnote Definition" ], - "planned": true, - "description": "The AST defines a FootnoteReference node, but no parser path produces it yet.\nWhen that lands, it will hold an inline reference to a footnote definition.", + "description": "Marks a superscript reference to a footnote, linking to the matching Footnote Definition.\nWrite the label after a caret inside square brackets. Any string works as the label; it just has to match the definition.", "parameters": [ { "name": "label", diff --git a/docs/ui/src/js/vendor/highlight.bundle.js b/docs/ui/src/js/vendor/highlight.bundle.js index 2541addbd7..d3538bfcde 100644 --- a/docs/ui/src/js/vendor/highlight.bundle.js +++ b/docs/ui/src/js/vendor/highlight.bundle.js @@ -1,408 +1,15 @@ ;(function () { 'use strict' + // The C++ doc-comment colouring and the other language tweaks live in a + // shared, DOM-free module so the marketing landing page + // (docs/website/render.js) can register the exact same languages without + // duplicating the logic. This bundle is just the browser entry point: it + // builds a highlight.js instance, installs the shared languages, and + // highlights every code block Antora emitted. var hljs = require('highlight.js/lib/highlight') - // The default bash language only paints keywords, builtins, strings and - // variables. Command-line invocations such as - // python bootstrap.py --yes --build-type Release - // contain none of those, so they would otherwise render as flat text. - // We wrap the upstream definition and add two more rules: the first - // word of each line is the command being run, and `-x` / `--xy` tokens - // are option flags. Both stay at relevance 0 so they do not pull - // language detection away from real bash scripts. - function commandAwareBash (hl) { - var def = require('highlight.js/lib/languages/bash')(hl) - def.contains = (def.contains || []).slice() - // Match GNU-style long flags and POSIX short flags only when the - // hyphen sits at the start of a token: preceded by start-of-line - // or whitespace, never inside a longer identifier. Without the - // lookbehind, `-party` in `../third-party` would be picked up as - // a flag and rendered in flag color, splitting the path. - def.contains.unshift({ - className: 'attr', - begin: /(?<=^|\s)--?[A-Za-z][A-Za-z0-9-]*/, - relevance: 0, - }) - // The first identifier on a line is the command being run. Skip - // continuation lines (those starting with whitespace + `-`, which - // are continued flag lists) by requiring the first non-space - // character to be a letter, digit, slash, or dot — not a hyphen. - def.contains.unshift({ - className: 'title', - begin: /^[ \t]*[A-Za-z0-9_./][A-Za-z0-9_./-]*/, - relevance: 0, - }) - return def - } - // Mr.Docs's own subject matter is documentation, so the snippets we - // show contain heavy Javadoc/Doxygen comments. By default highlight.js - // paints the whole comment in one muted gray, which makes the most - // informative part of the page the hardest to read. Wrap the cpp - // language so doc-block comments (those starting with `/**` or - // `/*!`) carry their own contained tokens: `@param`, `@return`, - // backtick code spans, and so on stand out against the prose. - // Fixes for highlight.js's upstream cpp language. Two of its - // heuristics paint C++ tokens with the wrong meaning and the result - // reads as random color rather than syntax: - // - // - CPP_PRIMITIVE_TYPES matches any identifier ending in `_t` as a - // keyword. So `enable_if_t` gets keyword color while the next - // line's `is_integral_v` (same kind of type alias from - // , just no `_t` suffix) gets no class at all. The - // asymmetry is the visible bug, but the deeper problem is that - // user-defined `*_t` names — `error_t`, `result_t`, anything in - // your own code — are *not* keywords. - // - The built-in list hard-codes `std`. The standard namespace is - // not a built-in; treating it as one means `std::foo` renders - // with `std` accented and `foo` plain, which is the opposite of - // the reading the eye does (everything after `::` is the - // interesting part). - // - // Strip both miscategorisations so type-alias names and namespace - // qualifiers all fall through to the plain foreground color. That - // keeps the right tokens (`int`, `template`, `requires`, `noexcept`) - // accented and lets the asymmetric pairs read as one block. - // Patches the upstream highlight.js cpp language so token output - // matches what a C++-aware reader expects. All changes are generic: - // they apply to any C++ source, not to a hardcoded stdlib list. - // - // 1. CPP_PRIMITIVE_TYPES (a regex tagging any `_t`-suffixed identifier - // as a keyword) is removed. `enable_if_t`, `error_t`, `result_t`, - // any user type ending in `_t` — none of these are keywords. - // 2. `std` is removed from `keywords.built_in`. It is a namespace, - // not a built-in; the new qualified-name rules below pick it up - // uniformly with every other namespace. - // 3. Two qualified-name rules added: the identifier immediately - // before `::`, and the identifier immediately after `::`. Both - // are tagged `type`, so `std::enable_if_t`, `boost::asio::ip`, - // `MyNamespace::Inner::leaf` all render symmetrically. No - // stdlib-specific behavior. - // 4. An attribute rule for `[[ ... ]]` so `[[nodiscard]]`, - // `[[noreturn]]`, and user attributes stand out as meta. - // 5. The injected rules are added to every nested `contains` array - // so they fire inside the FUNCTION_DECLARATION mode and inside - // the `params` paren-list mode — the two places hljs upstream - // would otherwise leave qualified names unclassified. - function fixCppLanguage (def) { - function isWildcardTypeMode (mode) { - return mode && - mode.className === 'keyword' && - typeof mode.begin === 'string' && - mode.begin === '\\b[a-z\\d_]*_t\\b' - } - // The upstream `class` mode fires on the keyword `class` or `struct` - // anywhere, and ends only at `{`, `;`, or `:`. Inside a template - // parameter list — `template void bar();` — there is no - // `{` after `T`, so the class mode swallows `T> void bar()` all the - // way to the trailing semicolon, tagging `T`, `void`, `bar`, and - // anything else inside as fake `title`s. Replace the begin pattern - // with one that demands the class name be followed by `{` or `:`, - // so it only matches real class/struct definitions. - function isClassMode (mode) { - return mode && - mode.className === 'class' && - typeof mode.beginKeywords === 'string' && - /\bclass\b/.test(mode.beginKeywords) && - /\bstruct\b/.test(mode.beginKeywords) - } - function patch (rules) { - if (!Array.isArray(rules)) return rules - return rules.filter(function (r) { - return !isWildcardTypeMode(r) - }).map(function (r) { - if (isClassMode(r)) { - r = Object.assign({}, r) - delete r.beginKeywords - r.begin = /\b(?:class|struct|union)\s+[A-Za-z_]\w*(?=\s*[:{])/ - // Put the matched text back so the inner contains can - // tokenise it. Without this the class wrapper opens but - // the class name renders plain because the begin regex - // already consumed it. - r.returnBegin = true - // hljs.TITLE_MODE matches any identifier — including - // `class`/`struct`/`union` themselves, which would - // otherwise get the function-name `title` color - // alongside the actual class name. Tag those keywords - // explicitly first so they stay red and only the class - // name gets the title color. - var classContains = (r.contains || []).slice() - classContains.unshift({ - className: 'keyword', - begin: /\b(?:class|struct|union)\b/, - relevance: 0, - }) - r.contains = classContains - } - if (r && Array.isArray(r.contains)) { - r = Object.assign({}, r, { contains: patch(r.contains) }) - } - return r - }) - } - def.contains = patch(def.contains) + require('./mrdocs-highlight-languages.js').register(hljs) - // Clear the entire built-in list. Upstream cpp.js hard-codes - // stdlib names (`vector`, `set`, `map`, `string`, `sqrt`, - // `printf`, the stream objects, math functions, etc.) as - // built-ins, so an unqualified `sqrt(x)` renders orange while a - // user's own `sqrt(x)` of the same shape would render plain. - // That contradicts the principle that stdlib names should get - // no treatment beyond what any other identifier does. The new - // qualified-name rules above still highlight `std::sqrt`, - // `std::vector`, etc. through the namespace/qualified-part - // tags — those fire on any `Name::Name`, stdlib or not. - if (def.keywords && typeof def.keywords.built_in === 'string') { - def.keywords.built_in = '' - } - - // Generic qualified-name rules. `\b[A-Za-z_]\w*(?=::)` catches the - // namespace/class name before any `::`; `(?<=::)[A-Za-z_]\w*` catches - // the identifier after. Both use `type` className so the two halves - // of every qualified name render symmetrically, regardless of - // whether the namespace is `std`, `boost`, or a user library. - var NAMESPACE_PART = { - className: 'type', - begin: /\b[A-Za-z_]\w*(?=::)/, - relevance: 0, - } - var QUALIFIED_PART = { - className: 'type', - begin: /(?<=::)[A-Za-z_]\w*/, - relevance: 0, - } - // Attributes like `[[nodiscard]]` and `[[deprecated("reason")]]`. - // Marked `meta` so they get the same blue tint as preprocessor - // directives. - var ATTRIBUTE_RULE = { - className: 'meta', - begin: /\[\[/, - end: /\]\]/, - relevance: 0, - } - // The upstream function-declaration mode's title sub-rule matches - // any `IDENT(` pattern and tags the identifier as `title`. Inside - // a function declaration this fires multiple times — for `noexcept(`, - // `requires(`, `explicit(`, etc. — and tags those keywords as - // titles (purple, like a function name). Pre-match those tokens as - // keywords before the title rule gets a chance, so `noexcept(B)`, - // `requires(...)`, `alignas(...)`, etc. stay red. - var KEYWORD_WITH_PAREN = { - className: 'keyword', - begin: /\b(?:noexcept|requires|explicit|alignas|alignof|decltype|sizeof|typeid|throw|return|if|while|for|switch|catch|co_await|co_yield|co_return|static_assert)\b(?=\s*\()/, - relevance: 0, - } - // Type-name heuristic for variable declarations: `TYPE name = ...`, - // `TYPE name;`, `TYPE name(...)`, `TYPE name{...}`. The - // identifier before another identifier + `=`/`;`/`{`/`,` is the - // type. This catches `sqrt_fn sqrt = {};`, `MyClass obj;`, etc., - // including user-defined types that aren't in any keyword list. - // The negative lookahead excludes keywords that themselves can - // sit before an identifier (`return value`, `throw err`, - // `delete x`, etc.) so they don't get misclassified as types. - var KW_LIST = ( - 'return throw delete new auto else case default do goto break continue ' + - 'sizeof alignof alignas noexcept requires explicit typename class struct ' + - 'union enum template namespace using typedef virtual override final ' + - 'static extern register mutable volatile const constexpr consteval ' + - 'constinit inline thread_local public private protected friend operator ' + - 'static_cast const_cast dynamic_cast reinterpret_cast ' + - 'true false nullptr this if while for switch catch try ' + - 'int float double char bool void short long signed unsigned ' + - 'wchar_t char8_t char16_t char32_t asm import module export decltype' - ).split(/\s+/).join('|') - var TYPE_IN_DECL = { - className: 'type', - begin: new RegExp( - '\\b(?!(?:' + KW_LIST + ')\\b)' + - '[A-Za-z_]\\w*' + - '(?=\\s+[A-Za-z_]\\w*\\s*[=;{,])' - ), - relevance: 0, - } - var INJECTED = [KEYWORD_WITH_PAREN, NAMESPACE_PART, QUALIFIED_PART, TYPE_IN_DECL, ATTRIBUTE_RULE] - - // Walk the grammar tree and prepend the injected rules into every - // `contains` array. Highlight.js scopes contains per-mode, so unless - // these rules live in the params mode and the function-declaration - // mode (as well as top level), they would never fire inside a - // function signature — the exact place qualified return types show - // up. The WeakSet guards against revisiting the same array twice - // when upstream cpp.js wires two modes to one shared `contains`. - // - // The opaque-mode check stops recursion into comment and string - // modes. Comments have their own contains (for inline `@TODO`, - // doctag, etc.) and so do strings (for escape sequences); if we - // inject our code-aware rules into those, `TYPE_IN_DECL` will fire - // on prose like `integral value` and paint `integral` orange. - function isOpaque (r) { - var c = r && r.className - return c === 'comment' || c === 'doc-comment' || - c === 'string' || c === 'regexp' || c === 'doctag' || - c === 'meta-string' - } - function inject (rules, seen) { - if (!Array.isArray(rules)) return - if (seen.has(rules)) return - seen.add(rules) - rules.unshift.apply(rules, INJECTED) - for (var i = 0; i < rules.length; i++) { - var r = rules[i] - if (r && Array.isArray(r.contains) && !isOpaque(r)) inject(r.contains, seen) - } - } - inject(def.contains, new WeakSet()) - return def - } - - function richCpp (hl) { - var def = fixCppLanguage(require('highlight.js/lib/languages/cpp')(hl)) - // Doc-comment children carry their own classes so `@param`, the - // parameter name after it, and inline `code` spans inside the - // prose each pick up their own color against the comment body. - var docCommentChildren = [ - { - className: 'doctag', - begin: /@\w+/, - relevance: 0, - }, - { - className: 'name', - begin: /(?<=@(?:param|tparam|throws|exception)\b\s+)[A-Za-z_]\w*/, - relevance: 0, - }, - { - className: 'string', - begin: /`[^`]+`/, - relevance: 0, - }, - ] - // Doxygen-style block doc comments: `/** ... */` and `/*! ... */`. - var docBlockComment = { - className: 'doc-comment', - begin: /\/\*[*!]/, - end: /\*\//, - contains: docCommentChildren, - } - // Doxygen-style line doc comments: `///`, `//!`, `///<`, `//!<`. - // The lookahead on `///` blocks plain `////` separators from - // matching as a (zero-content) doc comment. - var docLineComment = { - className: 'doc-comment', - begin: /\/\/[!/](?!\/), just no `_t` suffix) gets no class at all. The +// asymmetry is the visible bug, but the deeper problem is that +// user-defined `*_t` names — `error_t`, `result_t`, anything in +// your own code — are *not* keywords. +// - The built-in list hard-codes `std`. The standard namespace is +// not a built-in; treating it as one means `std::foo` renders +// with `std` accented and `foo` plain, which is the opposite of +// the reading the eye does (everything after `::` is the +// interesting part). +// +// Strip both miscategorisations so type-alias names and namespace +// qualifiers all fall through to the plain foreground color. That +// keeps the right tokens (`int`, `template`, `requires`, `noexcept`) +// accented and lets the asymmetric pairs read as one block. +// Patches the upstream highlight.js cpp language so token output +// matches what a C++-aware reader expects. All changes are generic: +// they apply to any C++ source, not to a hardcoded stdlib list. +// +// 1. CPP_PRIMITIVE_TYPES (a regex tagging any `_t`-suffixed identifier +// as a keyword) is removed. `enable_if_t`, `error_t`, `result_t`, +// any user type ending in `_t` — none of these are keywords. +// 2. `std` is removed from `keywords.built_in`. It is a namespace, +// not a built-in; the new qualified-name rules below pick it up +// uniformly with every other namespace. +// 3. Two qualified-name rules added: the identifier immediately +// before `::`, and the identifier immediately after `::`. Both +// are tagged `type`, so `std::enable_if_t`, `boost::asio::ip`, +// `MyNamespace::Inner::leaf` all render symmetrically. No +// stdlib-specific behavior. +// 4. An attribute rule for `[[ ... ]]` so `[[nodiscard]]`, +// `[[noreturn]]`, and user attributes stand out as meta. +// 5. The injected rules are added to every nested `contains` array +// so they fire inside the FUNCTION_DECLARATION mode and inside +// the `params` paren-list mode — the two places hljs upstream +// would otherwise leave qualified names unclassified. +function fixCppLanguage (def) { + function isWildcardTypeMode (mode) { + return mode && + mode.className === 'keyword' && + typeof mode.begin === 'string' && + mode.begin === '\\b[a-z\\d_]*_t\\b' + } + // The upstream `class` mode fires on the keyword `class` or `struct` + // anywhere, and ends only at `{`, `;`, or `:`. Inside a template + // parameter list — `template void bar();` — there is no + // `{` after `T`, so the class mode swallows `T> void bar()` all the + // way to the trailing semicolon, tagging `T`, `void`, `bar`, and + // anything else inside as fake `title`s. Replace the begin pattern + // with one that demands the class name be followed by `{` or `:`, + // so it only matches real class/struct definitions. + function isClassMode (mode) { + return mode && + mode.className === 'class' && + typeof mode.beginKeywords === 'string' && + /\bclass\b/.test(mode.beginKeywords) && + /\bstruct\b/.test(mode.beginKeywords) + } + function patch (rules) { + if (!Array.isArray(rules)) return rules + return rules.filter(function (r) { + return !isWildcardTypeMode(r) + }).map(function (r) { + if (isClassMode(r)) { + r = Object.assign({}, r) + delete r.beginKeywords + r.begin = /\b(?:class|struct|union)\s+[A-Za-z_]\w*(?=\s*[:{])/ + // Put the matched text back so the inner contains can + // tokenise it. Without this the class wrapper opens but + // the class name renders plain because the begin regex + // already consumed it. + r.returnBegin = true + // hljs.TITLE_MODE matches any identifier — including + // `class`/`struct`/`union` themselves, which would + // otherwise get the function-name `title` color + // alongside the actual class name. Tag those keywords + // explicitly first so they stay red and only the class + // name gets the title color. + var classContains = (r.contains || []).slice() + classContains.unshift({ + className: 'keyword', + begin: /\b(?:class|struct|union)\b/, + relevance: 0, + }) + r.contains = classContains + } + if (r && Array.isArray(r.contains)) { + r = Object.assign({}, r, { contains: patch(r.contains) }) + } + return r + }) + } + def.contains = patch(def.contains) + + // Clear the entire built-in list. Upstream cpp.js hard-codes + // stdlib names (`vector`, `set`, `map`, `string`, `sqrt`, + // `printf`, the stream objects, math functions, etc.) as + // built-ins, so an unqualified `sqrt(x)` renders orange while a + // user's own `sqrt(x)` of the same shape would render plain. + // That contradicts the principle that stdlib names should get + // no treatment beyond what any other identifier does. The new + // qualified-name rules above still highlight `std::sqrt`, + // `std::vector`, etc. through the namespace/qualified-part + // tags — those fire on any `Name::Name`, stdlib or not. + if (def.keywords && typeof def.keywords.built_in === 'string') { + def.keywords.built_in = '' + } + + // Generic qualified-name rules. `\b[A-Za-z_]\w*(?=::)` catches the + // namespace/class name before any `::`; `(?<=::)[A-Za-z_]\w*` catches + // the identifier after. Both use `type` className so the two halves + // of every qualified name render symmetrically, regardless of + // whether the namespace is `std`, `boost`, or a user library. + var NAMESPACE_PART = { + className: 'type', + begin: /\b[A-Za-z_]\w*(?=::)/, + relevance: 0, + } + var QUALIFIED_PART = { + className: 'type', + begin: /(?<=::)[A-Za-z_]\w*/, + relevance: 0, + } + // Attributes like `[[nodiscard]]` and `[[deprecated("reason")]]`. + // Marked `meta` so they get the same blue tint as preprocessor + // directives. + var ATTRIBUTE_RULE = { + className: 'meta', + begin: /\[\[/, + end: /\]\]/, + relevance: 0, + } + // The upstream function-declaration mode's title sub-rule matches + // any `IDENT(` pattern and tags the identifier as `title`. Inside + // a function declaration this fires multiple times — for `noexcept(`, + // `requires(`, `explicit(`, etc. — and tags those keywords as + // titles (purple, like a function name). Pre-match those tokens as + // keywords before the title rule gets a chance, so `noexcept(B)`, + // `requires(...)`, `alignas(...)`, etc. stay red. + var KEYWORD_WITH_PAREN = { + className: 'keyword', + begin: /\b(?:noexcept|requires|explicit|alignas|alignof|decltype|sizeof|typeid|throw|return|if|while|for|switch|catch|co_await|co_yield|co_return|static_assert)\b(?=\s*\()/, + relevance: 0, + } + // Type-name heuristic for variable declarations: `TYPE name = ...`, + // `TYPE name;`, `TYPE name(...)`, `TYPE name{...}`. The + // identifier before another identifier + `=`/`;`/`{`/`,` is the + // type. This catches `sqrt_fn sqrt = {};`, `MyClass obj;`, etc., + // including user-defined types that aren't in any keyword list. + // The negative lookahead excludes keywords that themselves can + // sit before an identifier (`return value`, `throw err`, + // `delete x`, etc.) so they don't get misclassified as types. + var KW_LIST = ( + 'return throw delete new auto else case default do goto break continue ' + + 'sizeof alignof alignas noexcept requires explicit typename class struct ' + + 'union enum template namespace using typedef virtual override final ' + + 'static extern register mutable volatile const constexpr consteval ' + + 'constinit inline thread_local public private protected friend operator ' + + 'static_cast const_cast dynamic_cast reinterpret_cast ' + + 'true false nullptr this if while for switch catch try ' + + 'int float double char bool void short long signed unsigned ' + + 'wchar_t char8_t char16_t char32_t asm import module export decltype' + ).split(/\s+/).join('|') + var TYPE_IN_DECL = { + className: 'type', + begin: new RegExp( + '\\b(?!(?:' + KW_LIST + ')\\b)' + + '[A-Za-z_]\\w*' + + '(?=\\s+[A-Za-z_]\\w*\\s*[=;{,])' + ), + relevance: 0, + } + var INJECTED = [KEYWORD_WITH_PAREN, NAMESPACE_PART, QUALIFIED_PART, TYPE_IN_DECL, ATTRIBUTE_RULE] + + // Walk the grammar tree and prepend the injected rules into every + // `contains` array. Highlight.js scopes contains per-mode, so unless + // these rules live in the params mode and the function-declaration + // mode (as well as top level), they would never fire inside a + // function signature — the exact place qualified return types show + // up. The WeakSet guards against revisiting the same array twice + // when upstream cpp.js wires two modes to one shared `contains`. + // + // The opaque-mode check stops recursion into comment and string + // modes. Comments have their own contains (for inline `@TODO`, + // doctag, etc.) and so do strings (for escape sequences); if we + // inject our code-aware rules into those, `TYPE_IN_DECL` will fire + // on prose like `integral value` and paint `integral` orange. + function isOpaque (r) { + var c = r && r.className + return c === 'comment' || c === 'doc-comment' || + c === 'string' || c === 'regexp' || c === 'doctag' || + c === 'meta-string' + } + function inject (rules, seen) { + if (!Array.isArray(rules)) return + if (seen.has(rules)) return + seen.add(rules) + rules.unshift.apply(rules, INJECTED) + for (var i = 0; i < rules.length; i++) { + var r = rules[i] + if (r && Array.isArray(r.contains) && !isOpaque(r)) inject(r.contains, seen) + } + } + inject(def.contains, new WeakSet()) + return def +} + +// Build the doc-comment-aware cpp language on top of an injected base cpp +// language definition. Injecting the base (instead of requiring highlight.js +// here) lets a Node caller such as docs/website/render.js supply it from its +// own highlight.js install, so this module never has to resolve highlight.js +// from docs/ui at runtime. The browser bundle and register() pass the base in +// from the same highlight.js they already load. +function makeRichCpp (cppLangDef) { + return function (hl) { return richCppImpl(hl, cppLangDef) } +} + +function richCppImpl (hl, cppLangDef) { + var def = fixCppLanguage(cppLangDef(hl)) + // Doc-comment children carry their own classes so `@param`, the + // parameter name after it, and inline `code` spans inside the + // prose each pick up their own color against the comment body. + var docCommentChildren = [ + { + className: 'doctag', + begin: /@\w+/, + relevance: 0, + }, + { + className: 'name', + begin: /(?<=@(?:param|tparam|throws|exception)\b\s+)[A-Za-z_]\w*/, + relevance: 0, + }, + { + className: 'string', + begin: /`[^`]+`/, + relevance: 0, + }, + ] + // Doxygen-style block doc comments: `/** ... */` and `/*! ... */`. + var docBlockComment = { + className: 'doc-comment', + begin: /\/\*[*!]/, + end: /\*\//, + contains: docCommentChildren, + } + // Doxygen-style line doc comments: `///`, `//!`, `///<`, `//!<`. + // The lookahead on `///` blocks plain `////` separators from + // matching as a (zero-content) doc comment. + var docLineComment = { + className: 'doc-comment', + begin: /\/\/[!/](?!\/) {{/with}} + {{! MathJax typesets the \(...\) and \[...\] delimiters that Asciidoctor + emits for stem/latexmath (enabled via the playbook `stem` attribute). }} + + {{!-- --}} diff --git a/docs/website/index.html.hbs b/docs/website/index.html.hbs index c82f5dc80c..0e2ac543de 100644 --- a/docs/website/index.html.hbs +++ b/docs/website/index.html.hbs @@ -31,8 +31,16 @@ - + + + diff --git a/docs/website/package-lock.json b/docs/website/package-lock.json index 3d942185a0..7e9e979bba 100644 --- a/docs/website/package-lock.json +++ b/docs/website/package-lock.json @@ -12,7 +12,7 @@ "asciidoctor": "^3.0.4", "cheerio": "^1.1.2", "handlebars": "^4.7.8", - "highlight.js": "^11.11.1" + "highlight.js": "9.18.3" } }, "node_modules/@asciidoctor/cli": { @@ -666,12 +666,13 @@ } }, "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "version": "9.18.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz", + "integrity": "sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==", + "deprecated": "Version no longer supported. Upgrade to @latest", "license": "BSD-3-Clause", "engines": { - "node": ">=12.0.0" + "node": "*" } }, "node_modules/htmlparser2": { diff --git a/docs/website/package.json b/docs/website/package.json index eae1b8a936..569b92fa8c 100644 --- a/docs/website/package.json +++ b/docs/website/package.json @@ -17,6 +17,6 @@ "asciidoctor": "^3.0.4", "cheerio": "^1.1.2", "handlebars": "^4.7.8", - "highlight.js": "^11.11.1" + "highlight.js": "9.18.3" } } diff --git a/docs/website/render.js b/docs/website/render.js index f5bf90e54e..342b4a065c 100644 --- a/docs/website/render.js +++ b/docs/website/render.js @@ -9,9 +9,16 @@ // const Handlebars = require('handlebars'); -const hljs = require('highlight.js/lib/core'); -hljs.registerLanguage('cpp', require('highlight.js/lib/languages/cpp')); -hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml')); +// Reuse the doc site's rich C++ language definition (the doc-comment +// colouring, etc.) instead of the stock cpp language, so the landing page +// snippets look identical to the documentation. highlight.js and its cpp +// language come from this package's own dependency (pinned to 9.x to match +// the UI bundle); we only borrow the doc-comment logic from docs/ui, passing +// our base cpp definition into it, so nothing here has to resolve +// highlight.js from docs/ui at run time. +const hljs = require('highlight.js/lib/highlight'); +const { makeRichCpp } = require('../ui/src/js/vendor/mrdocs-highlight-languages.js'); +hljs.registerLanguage('cpp', makeRichCpp(require('highlight.js/lib/languages/cpp'))); const fs = require('fs'); const os = require('os'); const assert = require('assert'); @@ -69,26 +76,63 @@ if (!fs.existsSync(mrdocsExecutable)) { const absSnippetsDir = process.env.SNIPPETS_PATH ? path.resolve(process.env.SNIPPETS_PATH) : path.resolve(__dirname, '..', '..', 'test-files', 'golden-tests', 'snippets') + +// The mrdocs HTML generator emits synopsis code blocks as +//
...
and leaves them +// unhighlighted; the doc site colours code through Antora's pipeline, not +// this generator, so nothing highlights them on the landing page. Run the +// same highlighter used for the raw snippet over those blocks (their +// contents are HTML-escaped C++, so unescape before highlighting and let +// highlight.js re-escape). +function highlightSynopsisBlocks (html) { + return html.replace( + /
([\s\S]*?)<\/code><\/pre>/g,
+        (_match, escaped) => {
+            const code = escaped
+                .replace(/</g, '<')
+                .replace(/>/g, '>')
+                .replace(/"/g, '"')
+                .replace(/'/g, "'")
+                .replace(/&/g, '&')
+            // Keep the original class (no `hljs`): the CDN github theme
+            // paints `.hljs` with a white background, which on this dark page
+            // turns the block into a white box. The inner hljs-* spans are
+            // styled by standalone class rules, so they colour without it,
+            // exactly like the raw snippet above.
+            return '
' +
+                hljs.highlight('cpp', code).value +
+                '
' + } + ) +} + for (let panel of data.panels) { console.log(`Generating documentation for panel ${panel.source}`) - // Find source file + // Find source file. The .cpp is a thin translation unit that only + // includes the same-named .hpp; the documented declarations live in + // that header, which is also what we show as the example (we document + // headers, not .cpp files). const sourcePath = path.join(absSnippetsDir, panel.source) assert(sourcePath.endsWith('.cpp')) assert(fs.existsSync(sourcePath)) const sourceBasename = path.basename(sourcePath, path.extname(sourcePath)) + const headerSource = panel.source.replace(/\.cpp$/, '.hpp') + const headerPath = path.join(absSnippetsDir, headerSource) + assert(fs.existsSync(headerPath)) // Run mrdocs in header-scan mode: no compilation database, the // config points `input` at the snippets directory, `file-patterns` - // narrows the scan to this panel's source file, and `recursive` - // is off so we never pick up identically named files in nested - // golden-test subdirectories. + // matches this panel's .cpp and its .hpp (the .cpp is the translation + // unit; the declarations live in the header), and `recursive` is off + // so we never pick up identically named files in nested golden-test + // subdirectories. const mrdocsConfig = path.join(absSnippetsDir, 'mrdocs.yml') const mrdocsOutput = path.join(os.tmpdir(), `mrdocs-website-${sourceBasename}.html`) const args = [ mrdocsExecutable, `--config=${mrdocsConfig}`, - `--file-patterns=${panel.source}`, + `--file-patterns=${sourceBasename}.*`, '--recursive=false', `--output=${mrdocsOutput}`, '--multipage=false', @@ -112,11 +156,12 @@ for (let panel of data.panels) { console.log('Failed to generate website panel documentation') process.exit(1) } - panel.documentation = fs.readFileSync(mrdocsOutput, 'utf8'); + panel.documentation = highlightSynopsisBlocks(fs.readFileSync(mrdocsOutput, 'utf8')); - // Also inject the contents of the source file as highlighted C++ - const snippetContents = fs.readFileSync(sourcePath, 'utf8'); - panel.snippet = hljs.highlight(snippetContents, {language: 'cpp'}).value; + // Also inject the header contents as highlighted C++ (the representative + // example is the header, not the thin .cpp that includes it). + const snippetContents = fs.readFileSync(headerPath, 'utf8'); + panel.snippet = hljs.highlight('cpp', snippetContents).value; // Delete the temporary output file fs.unlinkSync(mrdocsOutput); diff --git a/docs/website/styles.css b/docs/website/styles.css index bc30ed6a72..80aeb9f321 100644 --- a/docs/website/styles.css +++ b/docs/website/styles.css @@ -2659,7 +2659,7 @@ section#demo { } .documentation-panel p, -.documentation-panel span, +.documentation-panel span:not([class^="hljs-"]), .documentation-panel div { font-size: 0.875rem; color: #fff; @@ -2720,12 +2720,84 @@ section#demo { border-collapse: collapse; } -.hljs-string { - color: green; +/* + * Syntax highlighting for the landing page code blocks, ported from the doc + * site's theme (docs/ui/src/css/highlight.css, GitHub Primer dark dimmed) so + * the raw snippet and the mrdocs-rendered synopsis are coloured identically + * to the documentation. The markup comes from the shared richCpp language + * definition consumed by both the UI bundle and docs/website/render.js. + */ +.hljs-comment, +.hljs-quote { + color: #768390; + font-style: italic; } -.hljs-meta .hljs-string { - color: green; +.hljs-keyword, +.hljs-formula { + color: #f47067; +} + +.hljs-string, +.hljs-regexp { + color: #96d0ff; +} + +.hljs-number, +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-attr, +.hljs-attribute, +.hljs-template-variable, +.hljs-meta { + color: #6cb6ff; +} + +.hljs-built_in, +.hljs-type, +.hljs-class .hljs-title, +.hljs-title.class_, +.hljs-name, +.hljs-variable { + color: #f69d50; +} + +.hljs-title, +.hljs-function .hljs-title, +.hljs-title.function_ { + color: #dcbdfb; +} + +.hljs-tag, +.hljs-doctag, +.hljs-selector-tag, +.hljs-section { + color: #8ddb8c; +} + +/* + * Doxygen-style doc-block comments use a green pair: the prose is a darker + * green, and the `@tag` keywords, the identifier after `@param`/`@tparam`/ + * `@throws`, and backtick code spans share a lighter green so the parts that + * feed the rendered documentation stand out. + */ +.hljs-doc-comment { + color: rgb(94, 135, 79); + font-style: italic; +} + +.hljs-doc-comment .hljs-doctag { + color: rgb(164, 181, 67); + font-style: normal; + font-weight: 700; +} + +.hljs-doc-comment .hljs-name, +.hljs-doc-comment .hljs-string { + color: rgb(164, 181, 67); + font-style: italic; } html { diff --git a/include/mrdocs/Metadata/DocComment.hpp b/include/mrdocs/Metadata/DocComment.hpp index 8157232635..f87ad7b40d 100644 --- a/include/mrdocs/Metadata/DocComment.hpp +++ b/include/mrdocs/Metadata/DocComment.hpp @@ -91,6 +91,16 @@ struct MRDOCS_DECL DocComment { /// The list of postconditions. std::vector postconditions; + /** Footnote definitions. + + A footnote definition is written as a paragraph starting with + `[^label]: text` and is collected here as floating metadata, + regardless of where it appears in the comment. Inline + `[^label]` references point at it by label, and it is rendered + once in a footnotes section at the end of the symbol page. + */ + std::vector footnotes; + /** The list of "relates" references. These references are created with the @@ -142,7 +152,8 @@ struct MRDOCS_DECL DocComment { sees.empty() && related.empty() && preconditions.empty() && - postconditions.empty(); + postconditions.empty() && + footnotes.empty(); } /** Three-way comparison on the rendered block sequence. @@ -183,7 +194,7 @@ MRDOCS_DESCRIBE_STRUCT( (), (Document, brief, returns, params, tparams, exceptions, sees, preconditions, postconditions, - relates, related) + footnotes, relates, related) ) /** Append blocks from `other` into `I`, preserving order. @@ -323,6 +334,10 @@ traverseImpl(DocComment& doc, F&& func) { traverseImpl(postconditionsEl, std::forward(func)); } + for (auto& footnotesEl: doc.footnotes) + { + traverseImpl(footnotesEl, std::forward(func)); + } if constexpr (bottomUp && std::invocable) { func(doc); diff --git a/include/mrdocs/Metadata/DocComment/Block/MathBlock.hpp b/include/mrdocs/Metadata/DocComment/Block/MathBlock.hpp index f3967f2966..b243ce2298 100644 --- a/include/mrdocs/Metadata/DocComment/Block/MathBlock.hpp +++ b/include/mrdocs/Metadata/DocComment/Block/MathBlock.hpp @@ -44,6 +44,10 @@ struct MathBlock final /// Raw TeX math source std::string literal; + /** Construct an empty math block. + */ + MathBlock() = default; + /** Copy-construct a math block. */ MathBlock(MathBlock const& other) = default; diff --git a/include/mrdocs/Metadata/DocComment/Inline/HighlightInline.hpp b/include/mrdocs/Metadata/DocComment/Inline/HighlightInline.hpp index 462aa5e7a0..85cdd51aa8 100644 --- a/include/mrdocs/Metadata/DocComment/Inline/HighlightInline.hpp +++ b/include/mrdocs/Metadata/DocComment/Inline/HighlightInline.hpp @@ -32,6 +32,9 @@ struct HighlightInline final : InlineCommonBase , InlineContainer { + /** Inherit text container constructors. + */ + using InlineContainer::InlineContainer; }; MRDOCS_DESCRIBE_STRUCT( diff --git a/include/mrdocs/Metadata/DocComment/Inline/ImageInline.hpp b/include/mrdocs/Metadata/DocComment/Inline/ImageInline.hpp index e66ccffe73..7393e86d00 100644 --- a/include/mrdocs/Metadata/DocComment/Inline/ImageInline.hpp +++ b/include/mrdocs/Metadata/DocComment/Inline/ImageInline.hpp @@ -45,6 +45,20 @@ struct ImageInline final */ std::string alt; + /** Construct an empty image. + */ + ImageInline() = default; + + /** Construct an image with a source and alternate text. + + @param src Image source URL or path. + @param alt Alternate text when the image cannot be shown. + */ + ImageInline(std::string_view src, std::string_view alt) + : src(src) + , alt(alt) + {} + }; MRDOCS_DESCRIBE_STRUCT( diff --git a/include/mrdocs/Metadata/DocComment/Inline/StrikethroughInline.hpp b/include/mrdocs/Metadata/DocComment/Inline/StrikethroughInline.hpp index 1d6381d104..2f27963bd6 100644 --- a/include/mrdocs/Metadata/DocComment/Inline/StrikethroughInline.hpp +++ b/include/mrdocs/Metadata/DocComment/Inline/StrikethroughInline.hpp @@ -42,6 +42,9 @@ struct StrikethroughInline final : InlineCommonBase , InlineContainer { + /** Inherit text container constructors. + */ + using InlineContainer::InlineContainer; }; MRDOCS_DESCRIBE_STRUCT( diff --git a/include/mrdocs/Metadata/DocComment/Inline/SubscriptInline.hpp b/include/mrdocs/Metadata/DocComment/Inline/SubscriptInline.hpp index 3dda538ec8..6be59f3ecf 100644 --- a/include/mrdocs/Metadata/DocComment/Inline/SubscriptInline.hpp +++ b/include/mrdocs/Metadata/DocComment/Inline/SubscriptInline.hpp @@ -32,6 +32,9 @@ struct SubscriptInline final : InlineCommonBase , InlineContainer { + /** Inherit text container constructors. + */ + using InlineContainer::InlineContainer; }; MRDOCS_DESCRIBE_STRUCT( diff --git a/include/mrdocs/Metadata/DocComment/Inline/SuperscriptInline.hpp b/include/mrdocs/Metadata/DocComment/Inline/SuperscriptInline.hpp index eba06a32ad..3a9e32d28d 100644 --- a/include/mrdocs/Metadata/DocComment/Inline/SuperscriptInline.hpp +++ b/include/mrdocs/Metadata/DocComment/Inline/SuperscriptInline.hpp @@ -32,6 +32,9 @@ struct SuperscriptInline final : InlineCommonBase , InlineContainer { + /** Inherit text container constructors. + */ + using InlineContainer::InlineContainer; }; MRDOCS_DESCRIBE_STRUCT( diff --git a/mrdocs.rnc b/mrdocs.rnc index 5f526f5753..5ddcea2965 100644 --- a/mrdocs.rnc +++ b/mrdocs.rnc @@ -154,6 +154,13 @@ grammar | element code { ( Kind | Children | element literal { xsd:string } )* } | element strong { ( Kind | Children )* } | element emph { ( Kind | Children )* } + | element strikethrough { ( Kind | Children )* } + | element subscript { ( Kind | Children )* } + | element superscript { ( Kind | Children )* } + | element highlight { ( Kind | Children )* } + | element math { ( Kind | element literal { xsd:string } )* } + | element image { ( Kind | Children | element src { xsd:string } | element alt { xsd:string } )* } + | element footnoteReference { ( Kind | element label { xsd:string } )* } | element link { ( Kind | Children | element href { xsd:string } )* } | element reference { ( Kind | element literal { xsd:string } | element id { Id } )* } ) @@ -165,6 +172,8 @@ grammar | element list { ( Kind | element items { ( element listItem { element blocks { AnyBlock* }* } | TableRow )* } | element listKind { ListKind } )* } | element heading { ( Kind | Children | element level { xsd:integer } )* } | element code { ( Kind | Children | element literal { xsd:string } )* } + | element math { ( Kind | element literal { xsd:string } )* } + | element footnoteDefinition { ( Kind | element blocks { AnyBlock* } | element label { xsd:string } )* } | element table { ( Kind | element items { TableRow* } )* } ) TableRow = @@ -186,7 +195,8 @@ grammar | element exceptions { element throws { ( Kind | Children | element exception { ( Kind | element literal { xsd:string } | element id { Id } )* } )* }* } | element sees { element see { ( Kind | Children )* }* } | element preconditions { element precondition { ( Kind | Children )* }* } - | element postconditions { element postcondition { ( Kind | Children )* }* } )* + | element postconditions { element postcondition { ( Kind | Children )* }* } + | element footnotes { element footnoteDefinition { ( Kind | element blocks { AnyBlock* } | element label { xsd:string } )* }* } )* } #--------------------------------------------- diff --git a/share/mrdocs/addons/generator/adoc/partials/doc/block/footnote-definition.adoc.hbs b/share/mrdocs/addons/generator/adoc/partials/doc/block/footnote-definition.adoc.hbs new file mode 100644 index 0000000000..c9020e8f30 --- /dev/null +++ b/share/mrdocs/addons/generator/adoc/partials/doc/block/footnote-definition.adoc.hbs @@ -0,0 +1,2 @@ +[#fn-{{label}}] +^{{label}}^ {{> doc/block-container }} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/adoc/partials/doc/block/math.adoc.hbs b/share/mrdocs/addons/generator/adoc/partials/doc/block/math.adoc.hbs new file mode 100644 index 0000000000..8758cbbaf5 --- /dev/null +++ b/share/mrdocs/addons/generator/adoc/partials/doc/block/math.adoc.hbs @@ -0,0 +1,4 @@ +[stem] +++++ +{{{literal}}} +++++ diff --git a/share/mrdocs/addons/generator/adoc/partials/doc/inline/footnote-reference.adoc.hbs b/share/mrdocs/addons/generator/adoc/partials/doc/inline/footnote-reference.adoc.hbs new file mode 100644 index 0000000000..51c79ef0eb --- /dev/null +++ b/share/mrdocs/addons/generator/adoc/partials/doc/inline/footnote-reference.adoc.hbs @@ -0,0 +1 @@ +^<>^ \ No newline at end of file diff --git a/share/mrdocs/addons/generator/adoc/partials/doc/inline/math.adoc.hbs b/share/mrdocs/addons/generator/adoc/partials/doc/inline/math.adoc.hbs new file mode 100644 index 0000000000..3471cb8acf --- /dev/null +++ b/share/mrdocs/addons/generator/adoc/partials/doc/inline/math.adoc.hbs @@ -0,0 +1 @@ +stem:[{{{literal}}}] \ No newline at end of file diff --git a/share/mrdocs/addons/generator/adoc/partials/markup/hr.adoc.hbs b/share/mrdocs/addons/generator/adoc/partials/markup/hr.adoc.hbs new file mode 100644 index 0000000000..25d78085df --- /dev/null +++ b/share/mrdocs/addons/generator/adoc/partials/markup/hr.adoc.hbs @@ -0,0 +1,2 @@ +''' + diff --git a/share/mrdocs/addons/generator/adoc/partials/markup/img.adoc.hbs b/share/mrdocs/addons/generator/adoc/partials/markup/img.adoc.hbs new file mode 100644 index 0000000000..37695c0744 --- /dev/null +++ b/share/mrdocs/addons/generator/adoc/partials/markup/img.adoc.hbs @@ -0,0 +1 @@ +image:{{{src}}}[{{alt}}] \ No newline at end of file diff --git a/share/mrdocs/addons/generator/adoc/partials/markup/sub.adoc.hbs b/share/mrdocs/addons/generator/adoc/partials/markup/sub.adoc.hbs new file mode 100644 index 0000000000..e988ee8ecb --- /dev/null +++ b/share/mrdocs/addons/generator/adoc/partials/markup/sub.adoc.hbs @@ -0,0 +1 @@ +~{{> @partial-block }}~ \ No newline at end of file diff --git a/share/mrdocs/addons/generator/adoc/partials/markup/sup.adoc.hbs b/share/mrdocs/addons/generator/adoc/partials/markup/sup.adoc.hbs new file mode 100644 index 0000000000..b49e3d0fbc --- /dev/null +++ b/share/mrdocs/addons/generator/adoc/partials/markup/sup.adoc.hbs @@ -0,0 +1 @@ +^{{> @partial-block }}^ \ No newline at end of file diff --git a/share/mrdocs/addons/generator/common/partials/doc/block/footnote-definition.hbs b/share/mrdocs/addons/generator/common/partials/doc/block/footnote-definition.hbs index b1d95fd424..6f27906515 100644 --- a/share/mrdocs/addons/generator/common/partials/doc/block/footnote-definition.hbs +++ b/share/mrdocs/addons/generator/common/partials/doc/block/footnote-definition.hbs @@ -1 +1 @@ -{{> doc/inline-container }} \ No newline at end of file +{{label}}. {{> doc/block-container }} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/common/partials/doc/inline/emph.hbs b/share/mrdocs/addons/generator/common/partials/doc/inline/emph.hbs index 2bd7f82f10..5d98d43184 100644 --- a/share/mrdocs/addons/generator/common/partials/doc/inline/emph.hbs +++ b/share/mrdocs/addons/generator/common/partials/doc/inline/emph.hbs @@ -1 +1 @@ -{{#> markup/i }}{{> doc/inline-container }}{{/ markup/i }} \ No newline at end of file +{{#> markup/em }}{{> doc/inline-container }}{{/ markup/em }} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/common/partials/doc/inline/footnote-reference.hbs b/share/mrdocs/addons/generator/common/partials/doc/inline/footnote-reference.hbs index 31a6db315f..0aabc20fa4 100644 --- a/share/mrdocs/addons/generator/common/partials/doc/inline/footnote-reference.hbs +++ b/share/mrdocs/addons/generator/common/partials/doc/inline/footnote-reference.hbs @@ -1 +1 @@ -{{#> markup/i }}{{literal}}{{/ markup/i }} \ No newline at end of file +{{#> markup/sup }}{{label}}{{/ markup/sup }} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/common/partials/doc/inline/image.hbs b/share/mrdocs/addons/generator/common/partials/doc/inline/image.hbs index 2bd7f82f10..fc4ceec1b6 100644 --- a/share/mrdocs/addons/generator/common/partials/doc/inline/image.hbs +++ b/share/mrdocs/addons/generator/common/partials/doc/inline/image.hbs @@ -1 +1 @@ -{{#> markup/i }}{{> doc/inline-container }}{{/ markup/i }} \ No newline at end of file +{{> markup/img src=src alt=alt }} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/common/partials/doc/inline/subscript.hbs b/share/mrdocs/addons/generator/common/partials/doc/inline/subscript.hbs index 2bd7f82f10..0931070edf 100644 --- a/share/mrdocs/addons/generator/common/partials/doc/inline/subscript.hbs +++ b/share/mrdocs/addons/generator/common/partials/doc/inline/subscript.hbs @@ -1 +1 @@ -{{#> markup/i }}{{> doc/inline-container }}{{/ markup/i }} \ No newline at end of file +{{#> markup/sub }}{{> doc/inline-container }}{{/ markup/sub }} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/common/partials/doc/inline/superscript.hbs b/share/mrdocs/addons/generator/common/partials/doc/inline/superscript.hbs index 2bd7f82f10..dde34cfc9b 100644 --- a/share/mrdocs/addons/generator/common/partials/doc/inline/superscript.hbs +++ b/share/mrdocs/addons/generator/common/partials/doc/inline/superscript.hbs @@ -1 +1 @@ -{{#> markup/i }}{{> doc/inline-container }}{{/ markup/i }} \ No newline at end of file +{{#> markup/sup }}{{> doc/inline-container }}{{/ markup/sup }} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/common/partials/symbol.hbs b/share/mrdocs/addons/generator/common/partials/symbol.hbs index 5e0031d93b..9ab4195a35 100644 --- a/share/mrdocs/addons/generator/common/partials/symbol.hbs +++ b/share/mrdocs/addons/generator/common/partials/symbol.hbs @@ -36,4 +36,5 @@ {{> symbol/section/postconditions}} {{> symbol/section/see-also}} {{> symbol/section/recursive-index}} +{{> symbol/section/footnotes}} {{/markup/section}} diff --git a/share/mrdocs/addons/generator/common/partials/symbol/section/footnotes.hbs b/share/mrdocs/addons/generator/common/partials/symbol/section/footnotes.hbs new file mode 100644 index 0000000000..da4741759c --- /dev/null +++ b/share/mrdocs/addons/generator/common/partials/symbol/section/footnotes.hbs @@ -0,0 +1,12 @@ +{{! Footnotes: definitions written as `[^label]: text` are collected here + (see DocComment.footnotes) and rendered once at the end of the page, + under a horizontal rule rather than a heading. References `[^label]` + link to these anchors. }} +{{#if symbol.doc.footnotes}} +{{#> markup/section name="footnotes"}} +{{> markup/hr }} +{{#each symbol.doc.footnotes}} +{{> doc/block . }} +{{/each}} +{{/markup/section}} +{{/if}} diff --git a/share/mrdocs/addons/generator/html/layouts/wrapper.html.hbs b/share/mrdocs/addons/generator/html/layouts/wrapper.html.hbs index 71855f5c5c..f1fc40d519 100644 --- a/share/mrdocs/addons/generator/html/layouts/wrapper.html.hbs +++ b/share/mrdocs/addons/generator/html/layouts/wrapper.html.hbs @@ -21,6 +21,13 @@ {{/each}} {{/if}} +{{! Math rendering. Math nodes emit MathJax delimiters (\( \) inline, \[ \] + display); load MathJax so they render in the default template. Gated on + hasDefaultStyles so minimal/overridden output stays script-free, and users + who prefer KaTeX or a self-hosted copy can override this partial. }} +{{#if page.hasDefaultStyles}} +{{> math-renderer }} +{{/if}} {{! Title }} {{#if @root.config.multipage }} {{! Multipage documentation: symbol is available to the wrapper }} diff --git a/share/mrdocs/addons/generator/html/partials/doc/block/footnote-definition.html.hbs b/share/mrdocs/addons/generator/html/partials/doc/block/footnote-definition.html.hbs new file mode 100644 index 0000000000..4941ee3d80 --- /dev/null +++ b/share/mrdocs/addons/generator/html/partials/doc/block/footnote-definition.html.hbs @@ -0,0 +1 @@ +
{{label}} {{> doc/block-container }}
\ No newline at end of file diff --git a/share/mrdocs/addons/generator/html/partials/doc/block/math.html.hbs b/share/mrdocs/addons/generator/html/partials/doc/block/math.html.hbs new file mode 100644 index 0000000000..c165bff84c --- /dev/null +++ b/share/mrdocs/addons/generator/html/partials/doc/block/math.html.hbs @@ -0,0 +1 @@ +
\[{{{literal}}}\]
diff --git a/share/mrdocs/addons/generator/html/partials/doc/inline.html.hbs b/share/mrdocs/addons/generator/html/partials/doc/inline.html.hbs deleted file mode 100644 index bd6c0e327d..0000000000 --- a/share/mrdocs/addons/generator/html/partials/doc/inline.html.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{~#if (eq kind "text")~}}{{> doc/inline/text}}{{~/if~}} -{{~#if (eq kind "strong")~}}{{> doc/inline/strong}}{{~/if~}} -{{~#if (eq kind "emph")~}}{{> doc/inline/emph}}{{~/if~}} -{{~#if (eq kind "code")~}}{{> doc/inline/code}}{{~/if~}} -{{~#if (eq kind "link")~}}{{> doc/inline/link}}{{~/if~}} -{{~#if (eq kind "reference")~}}{{> doc/inline/reference}}{{~/if~}} -{{~#if (eq kind "copy-details")~}}{{> doc/inline/copy-details}}{{~/if~}} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/html/partials/doc/inline/footnote-reference.html.hbs b/share/mrdocs/addons/generator/html/partials/doc/inline/footnote-reference.html.hbs new file mode 100644 index 0000000000..0121891315 --- /dev/null +++ b/share/mrdocs/addons/generator/html/partials/doc/inline/footnote-reference.html.hbs @@ -0,0 +1 @@ +{{label}} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/html/partials/doc/inline/math.html.hbs b/share/mrdocs/addons/generator/html/partials/doc/inline/math.html.hbs new file mode 100644 index 0000000000..ae0cc1c0cb --- /dev/null +++ b/share/mrdocs/addons/generator/html/partials/doc/inline/math.html.hbs @@ -0,0 +1 @@ +\({{{literal}}}\) \ No newline at end of file diff --git a/share/mrdocs/addons/generator/html/partials/markup/hr.html.hbs b/share/mrdocs/addons/generator/html/partials/markup/hr.html.hbs new file mode 100644 index 0000000000..b5055ee674 --- /dev/null +++ b/share/mrdocs/addons/generator/html/partials/markup/hr.html.hbs @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/share/mrdocs/addons/generator/html/partials/markup/img.html.hbs b/share/mrdocs/addons/generator/html/partials/markup/img.html.hbs new file mode 100644 index 0000000000..86b9a6a4f0 --- /dev/null +++ b/share/mrdocs/addons/generator/html/partials/markup/img.html.hbs @@ -0,0 +1 @@ +{{alt}} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/html/partials/markup/sub.html.hbs b/share/mrdocs/addons/generator/html/partials/markup/sub.html.hbs new file mode 100644 index 0000000000..a150818d88 --- /dev/null +++ b/share/mrdocs/addons/generator/html/partials/markup/sub.html.hbs @@ -0,0 +1 @@ +{{> @partial-block }} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/html/partials/markup/sup.html.hbs b/share/mrdocs/addons/generator/html/partials/markup/sup.html.hbs new file mode 100644 index 0000000000..cdf1daa31b --- /dev/null +++ b/share/mrdocs/addons/generator/html/partials/markup/sup.html.hbs @@ -0,0 +1 @@ +{{> @partial-block }} \ No newline at end of file diff --git a/share/mrdocs/addons/generator/html/partials/math-renderer.html.hbs b/share/mrdocs/addons/generator/html/partials/math-renderer.html.hbs new file mode 100644 index 0000000000..3c2b2451ec --- /dev/null +++ b/share/mrdocs/addons/generator/html/partials/math-renderer.html.hbs @@ -0,0 +1,12 @@ + + diff --git a/src/lib/AST/ExtractDocComment.cpp b/src/lib/AST/ExtractDocComment.cpp index 8e1646bcfc..d3c9d9f06b 100644 --- a/src/lib/AST/ExtractDocComment.cpp +++ b/src/lib/AST/ExtractDocComment.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #ifdef _MSC_VER # pragma warning(push) @@ -292,6 +293,12 @@ class DocCommentVisitor doc::InlineContainer* curInlines_{ nullptr }; bool newline_blocks_merge_{ false }; + // A `\f$...\f$` inline formula is a verbatim block command to Clang, so it + // splits the surrounding paragraph in two. When that happens we record the + // source line just past the formula; a paragraph that starts on the next + // line (no blank line between) is merged back so the formula stays inline. + unsigned mergeParagraphAfterLine_{ 0 }; + // --- inline assembly template InlineTy, class... Args> @@ -1119,6 +1126,63 @@ class DocCommentVisitor C->hasTrailingNewline(), ensureUTF8(std::move(comps.text))); } + else if (comps.tag == "strong") + { + emplaceInline( + C->hasTrailingNewline(), + ensureUTF8(std::move(comps.text))); + } + else if (comps.tag == "mark") + { + emplaceInline( + C->hasTrailingNewline(), + ensureUTF8(std::move(comps.text))); + } + else if (comps.tag == "sub") + { + emplaceInline( + C->hasTrailingNewline(), + ensureUTF8(std::move(comps.text))); + } + else if (comps.tag == "sup") + { + emplaceInline( + C->hasTrailingNewline(), + ensureUTF8(std::move(comps.text))); + } + else if (comps.tag == "del" || comps.tag == "s") + { + emplaceInline( + C->hasTrailingNewline(), + ensureUTF8(std::move(comps.text))); + } + else if (comps.tag == "code") + { + emplaceInline( + C->hasTrailingNewline(), + ensureUTF8(std::move(comps.text))); + } + else if (comps.tag == "img") + { + // is a void tag: src/alt come from attributes, not + // enclosed text. A missing src makes the image meaningless, + // so warn and drop; a missing alt is allowed (empty alt). + auto srcAttr = getAttr("src"); + if (!srcAttr) + { + report::warn( + "{} at {} ({})", + srcAttr.error().message(), + filename, + loc.getLine()); + return; + } + std::string alt = getAttr("alt").value_or(std::string()); + emplaceInline( + C->hasTrailingNewline(), + ensureUTF8(std::move(*srcAttr)), + ensureUTF8(std::move(alt))); + } else { report::warn( @@ -1247,7 +1311,17 @@ class DocCommentVisitor // default rendering: concatenate all args and style accordingly std::string s; - s.reserve([&] { + // An unrecognized command carries its meaning in the command name, + // which Clang does not expose as an argument. Dropping it silently + // loses content: most importantly LaTeX macros inside math spans + // (e.g. \pi, \epsilon in `$\pi r^2$`), which Clang tokenizes as + // unknown commands. Preserve the command verbatim as literal text. + if (cmd->IsUnknownCommand) + { + s.push_back('\\'); + s.append(C->getCommandName(ctx_.getCommentCommandTraits())); + } + s.reserve(s.size() + [&] { size_t n = 0; for (unsigned i = 0; i < C->getNumArgs(); ++i) { @@ -1290,6 +1364,20 @@ class DocCommentVisitor visitChildrenWithCursor(C); return; } + // Merge back a paragraph split off by a preceding `\f$...\f$` inline + // formula, but only when it is contiguous (no blank line between). + unsigned const mergeLine = std::exchange(mergeParagraphAfterLine_, 0); + if (mergeLine != 0 + && !jd_.Document.empty() + && jd_.Document.back()->Kind == doc::BlockKind::Paragraph + && sm_.getPresumedLoc(C->getBeginLoc()).getLine() <= mergeLine + 1) + { + auto& inlines = static_cast( + jd_.Document.back()->asParagraph()); + auto scope = enterScope(inlines); + visitChildrenWithCursor(C); + return; + } doc::ParagraphBlock paragraph; auto scope = enterScope(paragraph); visitChildrenWithCursor(C); @@ -1600,7 +1688,6 @@ class DocCommentVisitor visitVerbatimBlock(clang::comments::VerbatimBlockComment const* C) { MRDOCS_COMMENT_TRACE(C, ctx_); - doc::CodeBlock code; std::string payload; unsigned n = C->getNumLines(); for (unsigned i = 0; i < n; ++i) @@ -1612,6 +1699,59 @@ class DocCommentVisitor payload.push_back('\n'); } } + + // Doxygen LaTeX formula commands, which Clang models as verbatim + // block commands. They give users a math syntax that does not trip + // Clang's -Wdocumentation-unknown-command (unlike the `$...$` / + // `$$...$$` Markdown forms, whose bodies Clang cannot parse). + // \f$ ... \f$ inline formula + // \f[ ... \f] displayed formula + auto const name = C->getCommandName(ctx_.getCommentCommandTraits()); + if (name == "f$" || name == "f[") + { + payload = std::string(trim(payload)); + } + if (name == "f$") + { + // Inline formula. Clang positions the verbatim block outside the + // surrounding paragraph, so emit into the current inline flow when + // one is open, append to the paragraph that was just closed, or + // otherwise wrap it in its own paragraph. + if (curInlines_) + { + emplaceInline(true, std::move(payload)); + return; + } + doc::InlineContainer* inlines = nullptr; + if (!jd_.Document.empty() + && jd_.Document.back()->Kind == doc::BlockKind::Paragraph) + { + inlines = &static_cast( + jd_.Document.back()->asParagraph()); + } + else + { + jd_.Document.emplace_back(doc::ParagraphBlock{}); + inlines = &static_cast( + jd_.Document.back()->asParagraph()); + } + inlines->children.emplace_back( + std::in_place_type, std::move(payload)); + mergeParagraphAfterLine_ + = sm_.getPresumedLoc(C->getEndLoc()).getLine(); + return; + } + if (name == "f[") + { + // Displayed formula: always a block. + flushCurrentParagraphAsBlock(); + doc::MathBlock math; + math.literal = std::move(payload); + jd_.Document.emplace_back(std::move(math)); + return; + } + + doc::CodeBlock code; code.literal = std::move(payload); jd_.Document.emplace_back(std::move(code)); } diff --git a/src/lib/Metadata/DocComment/Inline.cpp b/src/lib/Metadata/DocComment/Inline.cpp index 921c6dfcf1..776ae2a8a0 100644 --- a/src/lib/Metadata/DocComment/Inline.cpp +++ b/src/lib/Metadata/DocComment/Inline.cpp @@ -67,7 +67,13 @@ isEmpty(Polymorphic const& el) { return visit(*el, [](InlineTy const& N) -> bool { - if constexpr (std::derived_from) + if constexpr (std::same_as) + { + // An image carries its content in src/alt, not in children, so + // an empty child list does not make it empty. + return N.src.empty(); + } + else if constexpr (std::derived_from) { return N.children.empty(); } diff --git a/src/lib/Metadata/Finalizers/DocComment/parseInlines.hpp b/src/lib/Metadata/Finalizers/DocComment/parseInlines.hpp index cb24241273..68ee2b1916 100644 --- a/src/lib/Metadata/Finalizers/DocComment/parseInlines.hpp +++ b/src/lib/Metadata/Finalizers/DocComment/parseInlines.hpp @@ -1031,6 +1031,22 @@ parse(char const* first, char const* last, doc::InlineContainer& out_root) i += 2; continue; } + // Markdown footnote reference [^label] + if (c == '[' && i + 1 < s.size() && s[i + 1] == '^') { + std::size_t k = i + 2; + while (k < s.size() && s[k] != ']' && s[k] != '[') { + ++k; + } + if (k < s.size() && s[k] == ']' && k > i + 2) { + st.flush_text(); + st.cur->emplace_back(); + st.cur->back()->asFootnoteReference().label + = std::string(s.substr(i + 2, k - (i + 2))); + i = k + 1; + continue; + } + // Malformed: fall through to normal '[' handling below. + } if (c == '[') { st.flush_text(); st.brackets.push_back(Bracket{ diff --git a/src/lib/Metadata/Finalizers/DocCommentFinalizer.cpp b/src/lib/Metadata/Finalizers/DocCommentFinalizer.cpp index 5866d03c87..d2b511e372 100644 --- a/src/lib/Metadata/Finalizers/DocCommentFinalizer.cpp +++ b/src/lib/Metadata/Finalizers/DocCommentFinalizer.cpp @@ -178,6 +178,124 @@ splitParagraphsAtMarkers(BlockVec& blocks) } } +// Convert a paragraph whose whole text is a standalone display-math span +// ($$ ... $$) into a MathBlock, so display math renders as a block instead +// of an inline formula. Inline $...$ or $$...$$ mixed with other text is +// left untouched for parseInlinesInContainer to turn into a MathInline. +// Runs before inline parsing, while the paragraph still holds raw text. +void +promoteDisplayMathParagraphs(BlockVec& blocks) +{ + for (auto& block : blocks) + { + if (!block->isParagraph()) + { + continue; + } + doc::ParagraphBlock& para = block->asParagraph(); + std::string text; + bool pureText = true; + for (auto const& child : para.children) + { + if (child->isText()) + { + text += child->asText().literal; + } + else if (child->isSoftBreak() || child->isLineBreak()) + { + text += ' '; + } + else + { + pureText = false; + break; + } + } + if (!pureText) + { + continue; + } + std::string_view const sv = trim(text); + if (sv.size() < 5 + || !sv.starts_with("$$") + || !sv.ends_with("$$")) + { + continue; + } + std::string_view const inner = sv.substr(2, sv.size() - 4); + // Two spans on one line are two inline formulas, not one block. + if (inner.find("$$") != std::string_view::npos) + { + continue; + } + doc::MathBlock math; + math.literal = std::string(trim(inner)); + block = Polymorphic(std::move(math)); + } +} + +// Convert a paragraph that opens with a Markdown footnote definition +// (`[^label]: text`) into a FootnoteDefinitionBlock holding the text. The +// matching `[^label]` reference is produced by the inline parser. Runs on +// raw text, before inline parsing, so the definition body is inline-parsed +// later when the traversal descends into the new block. +void +extractFootnoteDefinitions(BlockVec& blocks) +{ + for (auto& block : blocks) + { + if (!block->isParagraph()) + { + continue; + } + doc::ParagraphBlock& para = block->asParagraph(); + if (para.children.empty() || !para.children.front()->isText()) + { + continue; + } + std::string_view const head = ltrim(para.children.front()->asText().literal); + if (!head.starts_with("[^")) + { + continue; + } + std::size_t const close = head.find("]:", 2); + if (close == std::string_view::npos || close == 2) + { + continue; + } + std::string label(head.substr(2, close - 2)); + std::string rest(ltrim(head.substr(close + 2))); + para.children.front()->asText().literal = std::move(rest); + + doc::FootnoteDefinitionBlock def; + def.label = std::move(label); + def.blocks.emplace_back( + Polymorphic(std::move(para))); + block = Polymorphic(std::move(def)); + } +} + +// Move footnote definitions out of the document flow into the DocComment's +// floating `footnotes` list. They are written inline (as a `[^label]: text` +// paragraph) but belong to the whole comment, and are rendered together in a +// footnotes section at the end of the page instead of where they appear. +void +hoistFootnoteDefinitions(DocComment& doc) +{ + for (auto it = doc.Document.begin(); it != doc.Document.end();) + { + if ((*it)->isFootnoteDefinition()) + { + doc.footnotes.push_back(std::move((*it)->asFootnoteDefinition())); + it = doc.Document.erase(it); + } + else + { + ++it; + } + } +} + // Parse inline Markdown (bold, italic, code, etc.) in a // single InlineContainer's immediate text children. // Does *not* recurse: topDownTraverse() already visits every @@ -1585,6 +1703,8 @@ parseInlines(DocComment& doc) }) { splitParagraphsAtMarkers(node.Document); + promoteDisplayMathParagraphs(node.Document); + extractFootnoteDefinitions(node.Document); } if constexpr ( requires { @@ -1593,12 +1713,16 @@ parseInlines(DocComment& doc) }) { splitParagraphsAtMarkers(node.blocks); + promoteDisplayMathParagraphs(node.blocks); + extractFootnoteDefinitions(node.blocks); } if constexpr (std::same_as) { for (doc::ListItem& item : node.items) { splitParagraphsAtMarkers(item.blocks); + promoteDisplayMathParagraphs(item.blocks); + extractFootnoteDefinitions(item.blocks); } } @@ -1610,6 +1734,10 @@ parseInlines(DocComment& doc) parseInlinesInContainer(node); } }); + + // Footnote definitions are parsed inline above; collect them into the + // floating `footnotes` list so they render once at the end of the page. + hoistFootnoteDefinitions(doc); } namespace { diff --git a/test-files/golden-tests/README.md b/test-files/golden-tests/README.md new file mode 100644 index 0000000000..095cdef679 --- /dev/null +++ b/test-files/golden-tests/README.md @@ -0,0 +1,25 @@ +# Golden tests + +Golden tests capture MrDocs output for a fixed input and compare it byte for byte on every run. Each test is a directory with one or more `.cpp` inputs, an `mrdocs.yml` config, and the expected output files. The runner (`mrdocs-test`) regenerates the output and diffs it against the committed goldens; the `mrdocs-update-test-fixtures-all` target rewrites them once a change is confirmed intentional. Never hand-edit the generated `.xml`/`.html`/`.adoc` files. + +## XML is the default regression format + +The XML generator is a direct reflection of the corpus, so it is cheap to produce and it records everything MrDocs extracted: symbols, relationships, and the full doc-comment tree. Most tests render XML only and rely on it to catch regressions, because rendering the Handlebars generators (adoc, html) for every case is expensive and mostly redundant once the XML is correct. A case that only needs to confirm the data was extracted belongs in an XML test, which generalizes across the generators. Add adoc/html goldens only where the rendered output itself is what you want to pin down. + +## javadoc/ — doc-comment extraction, one node kind per directory + +`javadoc/` tests how doc comments are parsed into the doc-comment tree. Each inline or block node kind (see `include/mrdocs/Metadata/DocComment/Inline/InlineNodes.inc` and `Block/BlockNodes.inc`) has a directory named after it, for example `javadoc/image`, `javadoc/code-span`, `javadoc/math`, `javadoc/footnote`. A node's directory exercises every input form that produces that node, so the extraction is pinned for all of them at once. For example `image` covers the Markdown `![alt](src)` form, the Markdown form with a title, and the HTML `` tag; `code-span` covers Markdown backticks, the Doxygen `@c` command, and the HTML `` tag; `math` covers inline `$...$` and `\f$...\f$` plus the display `$$...$$` and `\f[...\f]` blocks; `footnote` covers `[^label]` references and `[^label]: text` definitions. These tests are XML only (they inherit `generator: xml` from the tree-wide `mrdocs.yml`), because the point is to confirm each syntax lands as the right node in the corpus. The matching rendering checks live in `generator/hbs`. A node kind's directory name is the node kind (`image`, `math`); where a kind's name would collide with an existing block directory, the inline variant takes a descriptive name (inline `Code` is `code-span`, since `code` is the `@code` block). + +## generator/hbs — Handlebars generator tests + +`generator/hbs/` is where the Handlebars generators (adoc, html) are exercised. Each feature gets a small directory that renders the basics plus any case that was controversial or regressed at some point, so the templates do not silently break again. Keep these minimal: they are not the place for exhaustive coverage, which the XML tests already provide. Request the formats a test actually needs in its `mrdocs.yml` (for example `generator: [adoc, html, xml]`); a feature test usually includes xml too so the schema element is covered next to the rendering. + +## snippets/ — documentation snippets + +These are not general golden tests. Each `.cpp` here is a small, curated source whose MrDocs output is embedded directly in the documentation, so keep them short and user-friendly. Exhaustive, edge-case coverage across generators belongs in the real golden tests (for example under `generator/hbs/` or the XML tests), where a test is free to explore every possibility. A documentation snippet has the opposite job: read well on the page. + +The generator is set by `mrdocs.yml` (the `snippets/` directory defaults to `adoc`) and may be overridden per directory or per snippet. Each area renders only the format the documentation that embeds it actually uses: + +- `snippets/commands/` are embedded in the inline and block command reference pages under `docs/modules/ROOT/pages/commands/`, which `include::` the AsciiDoc output. These render adoc only. Do not add `.html`/`.xml` outputs (or a `.yml` requesting them) here to gain coverage; put that in a `generator/hbs/` or XML test instead. +- `snippets/landing/` is the Antora home page (`docs/modules/ROOT/pages/index.adoc`), which includes the AsciiDoc output, so it renders adoc and html. +- `snippets/*.cpp` in the snippets root are the panels on the marketing landing page, generated by `docs/website/render.js`, which embeds html. These should render html to match what the page uses. diff --git a/test-files/golden-tests/config/stylesheets/default-inline.html b/test-files/golden-tests/config/stylesheets/default-inline.html index 171e3f8756..e74a683a07 100644 --- a/test-files/golden-tests/config/stylesheets/default-inline.html +++ b/test-files/golden-tests/config/stylesheets/default-inline.html @@ -409,6 +409,18 @@ document.head.appendChild(script); })(); + + Reference diff --git a/test-files/golden-tests/generator/hbs/code-span/code-span.adoc b/test-files/golden-tests/generator/hbs/code-span/code-span.adoc new file mode 100644 index 0000000000..837496890d --- /dev/null +++ b/test-files/golden-tests/generator/hbs/code-span/code-span.adoc @@ -0,0 +1,39 @@ += Reference +:mrdocs: + +[#index] +== Global namespace + +=== Functions + +[cols="1,4"] +|=== +| Name| Description +| link:#set_value[`set_value`] +| Sets the value pointed to by `target`. +|=== + + +[#set_value] +== set_value + +Sets the value pointed to by `target`. + +=== Synopsis + +Declared in `<code‐span.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +void +set_value( + int* target, + int new_value); +---- + +=== Description + +The HTML form `*target` and the Doxygen `new_value` command render the same way as the Markdown backticks. + + +[.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/generator/hbs/code-span/code-span.cpp b/test-files/golden-tests/generator/hbs/code-span/code-span.cpp new file mode 100644 index 0000000000..6dad0594f7 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/code-span/code-span.cpp @@ -0,0 +1,6 @@ +/** Sets the value pointed to by `target`. + + The HTML form *target and the Doxygen @c new_value + command render the same way as the Markdown backticks. + */ +void set_value(int* target, int new_value); diff --git a/test-files/golden-tests/generator/hbs/code-span/code-span.html b/test-files/golden-tests/generator/hbs/code-span/code-span.html new file mode 100644 index 0000000000..035c27b3f8 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/code-span/code-span.html @@ -0,0 +1,49 @@ + + +Reference + + + +
+

Reference

+
+
+

Global namespace

+
+

Functions

+ + + + + + + +
NameDescription
set_value Sets the value pointed to by target.
+ +
+
+
+

set_value

+
+

Sets the value pointed to by target.

+
+
+

Synopsis

+

Declared in <code-span.cpp>

+
void
+set_value(
+    int* target,
+    int new_value);
+
+
+

Description

+

The HTML form *target and the Doxygen new_value command render the same way as the Markdown backticks.

+
+
+ +
+ + + \ No newline at end of file diff --git a/test-files/golden-tests/generator/hbs/code-span/code-span.xml b/test-files/golden-tests/generator/hbs/code-span/code-span.xml new file mode 100644 index 0000000000..0c1a0836ce --- /dev/null +++ b/test-files/golden-tests/generator/hbs/code-span/code-span.xml @@ -0,0 +1,141 @@ + + + + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + 3N5Wqc8NtBY1TvGYZykoTr252nsr + + + + set_value + + + + code-span.cpp + code-span.cpp + 6 + 1 + + + + + function + 3N5Wqc8NtBY1TvGYZykoTr252nsr + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + paragraph + + + text + The HTML form + + + code + + + text + *target + + + + + text + and the Doxygen + + + code + + + text + new_value + + + + + text + command render the same way as the Markdown backticks. + + + + + + brief + + + text + Sets the value pointed to by + + + code + + + text + target + + + + + text + . + + + + + + + named + + + identifier + void + + + void + + + + + + + pointer + + + named + + + identifier + int + + + int + + + + + target + + + + + named + + + identifier + int + + + int + + + new_value + + + normal + + diff --git a/test-files/golden-tests/generator/hbs/code-span/mrdocs.yml b/test-files/golden-tests/generator/hbs/code-span/mrdocs.yml new file mode 100644 index 0000000000..3cb41f10c7 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/code-span/mrdocs.yml @@ -0,0 +1,5 @@ +generator: [adoc, html, xml] +multipage: false +no-default-styles: true +warn-if-undocumented: false +source-root: . diff --git a/test-files/golden-tests/generator/hbs/footnotes/footnotes.adoc b/test-files/golden-tests/generator/hbs/footnotes/footnotes.adoc new file mode 100644 index 0000000000..9f25a91df4 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/footnotes/footnotes.adoc @@ -0,0 +1,41 @@ += Reference +:mrdocs: + +[#index] +== Global namespace + +=== Types + +[cols="1,4"] +|=== +| Name| Description +| link:#cache[`cache`] +| A cache with a bounded number of entries^<>^. +|=== + + +[#cache] +== cache + +A cache with a bounded number of entries^<>^. + +=== Synopsis + +Declared in `<footnotes.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +struct cache; +---- + +=== Description + +A lookup that misses falls through to the backing store^<>^. + +''' + +[#fn-lru] +^lru^ Least‐recently‐used entries are evicted first. + + +[.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/generator/hbs/footnotes/footnotes.cpp b/test-files/golden-tests/generator/hbs/footnotes/footnotes.cpp new file mode 100644 index 0000000000..50f6ca556d --- /dev/null +++ b/test-files/golden-tests/generator/hbs/footnotes/footnotes.cpp @@ -0,0 +1,7 @@ +/** A cache with a bounded number of entries[^lru]. + + A lookup that misses falls through to the backing store[^lru]. + + [^lru]: Least-recently-used entries are evicted first. + */ +struct cache { }; diff --git a/test-files/golden-tests/generator/hbs/footnotes/footnotes.html b/test-files/golden-tests/generator/hbs/footnotes/footnotes.html new file mode 100644 index 0000000000..b72f99b1b5 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/footnotes/footnotes.html @@ -0,0 +1,49 @@ + + +Reference + + + +
+

Reference

+
+
+

Global namespace

+
+

Types

+ + + + + + + +
NameDescription
cache A cache with a bounded number of entrieslru.
+ +
+
+
+

cache

+
+

A cache with a bounded number of entrieslru.

+
+
+

Synopsis

+

Declared in <footnotes.cpp>

+
struct cache;
+
+
+

Description

+

A lookup that misses falls through to the backing storelru.

+
+
+
lru

Least-recently-used entries are evicted first.

+
+
+ +
+ + + \ No newline at end of file diff --git a/test-files/golden-tests/generator/hbs/footnotes/footnotes.xml b/test-files/golden-tests/generator/hbs/footnotes/footnotes.xml new file mode 100644 index 0000000000..0d018c0d51 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/footnotes/footnotes.xml @@ -0,0 +1,84 @@ + + + + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + fyFCKEy9CiunXYqEi5ascGLee8F + + + + cache + + + footnotes.cpp + footnotes.cpp + 7 + 1 + + + + record + fyFCKEy9CiunXYqEi5ascGLee8F + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + paragraph + + + text + A lookup that misses falls through to the backing store + + + footnote-reference + + + + text + . + + + + + + brief + + + text + A cache with a bounded number of entries + + + footnote-reference + + + + text + . + + + + + + footnote-definition + + + paragraph + + + text + Least-recently-used entries are evicted first. + + + + + + + + + struct + + diff --git a/test-files/golden-tests/generator/hbs/footnotes/mrdocs.yml b/test-files/golden-tests/generator/hbs/footnotes/mrdocs.yml new file mode 100644 index 0000000000..3cb41f10c7 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/footnotes/mrdocs.yml @@ -0,0 +1,5 @@ +generator: [adoc, html, xml] +multipage: false +no-default-styles: true +warn-if-undocumented: false +source-root: . diff --git a/test-files/golden-tests/generator/hbs/image/image.adoc b/test-files/golden-tests/generator/hbs/image/image.adoc new file mode 100644 index 0000000000..78b5ccffd3 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/image/image.adoc @@ -0,0 +1,38 @@ += Reference +:mrdocs: + +[#index] +== Global namespace + +=== Types + +[cols="1,4"] +|=== +| Name| Description +| link:#widget[`widget`] +| A widget with an inline image. +|=== + + +[#widget] +== widget + +A widget with an inline image. + +=== Synopsis + +Declared in `<image.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +struct widget; +---- + +=== Description + +Markdown: image:status.png[status icon] + +HTML: image:banner.png[Banner] + + +[.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/generator/hbs/image/image.cpp b/test-files/golden-tests/generator/hbs/image/image.cpp new file mode 100644 index 0000000000..e4368ef20b --- /dev/null +++ b/test-files/golden-tests/generator/hbs/image/image.cpp @@ -0,0 +1,7 @@ +/** A widget with an inline image. + + Markdown: ![status icon](status.png) + + HTML: Banner + */ +struct widget { }; diff --git a/test-files/golden-tests/generator/hbs/image/image.html b/test-files/golden-tests/generator/hbs/image/image.html new file mode 100644 index 0000000000..e9bf2d29bd --- /dev/null +++ b/test-files/golden-tests/generator/hbs/image/image.html @@ -0,0 +1,47 @@ + + +Reference + + + +
+

Reference

+
+
+

Global namespace

+
+

Types

+ + + + + + + +
NameDescription
widget A widget with an inline image.
+ +
+
+
+

widget

+
+

A widget with an inline image.

+
+
+

Synopsis

+

Declared in <image.cpp>

+
struct widget;
+
+
+

Description

+

Markdown: status icon

+

HTML: Banner

+
+
+ +
+ + + \ No newline at end of file diff --git a/test-files/golden-tests/generator/hbs/image/image.xml b/test-files/golden-tests/generator/hbs/image/image.xml new file mode 100644 index 0000000000..11267e2a77 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/image/image.xml @@ -0,0 +1,70 @@ + + + + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + 3bKFAp8KD2cGZRzLRFok293VGkSJ + + + + widget + + + image.cpp + image.cpp + 7 + 1 + + + + record + 3bKFAp8KD2cGZRzLRFok293VGkSJ + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + paragraph + + + text + Markdown: + + + image + status.png + status icon + + + + + paragraph + + + text + HTML: + + + image + banner.png + Banner + + + + + + brief + + + text + A widget with an inline image. + + + + + struct + + diff --git a/test-files/golden-tests/generator/hbs/image/mrdocs.yml b/test-files/golden-tests/generator/hbs/image/mrdocs.yml new file mode 100644 index 0000000000..3cb41f10c7 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/image/mrdocs.yml @@ -0,0 +1,5 @@ +generator: [adoc, html, xml] +multipage: false +no-default-styles: true +warn-if-undocumented: false +source-root: . diff --git a/test-files/golden-tests/generator/hbs/math/math.adoc b/test-files/golden-tests/generator/hbs/math/math.adoc new file mode 100644 index 0000000000..6b0527f829 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/math/math.adoc @@ -0,0 +1,52 @@ += Reference +:mrdocs: + +[#index] +== Global namespace + +=== Functions + +[cols="1,4"] +|=== +| Name| Description +| link:#solve_quadratic[`solve_quadratic`] +| Solves a quadratic equation in stem:[O(1)] time. +|=== + + +[#solve_quadratic] +== solve_quadratic + +Solves a quadratic equation in stem:[O(1)] time. + +=== Synopsis + +Declared in `<math.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +double +solve_quadratic( + double a, + double b, + double c); +---- + +=== Description + +The roots are stem:[x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}]. + +A standalone Markdown display span is promoted to a block: + +[stem] +++++ +\int_0^1 x^2\,dx = \frac{1}{3} +++++ +The Doxygen delimiters produce the same block form: + +[stem] +++++ +e^{i\pi} + 1 = 0 +++++ + +[.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/generator/hbs/math/math.cpp b/test-files/golden-tests/generator/hbs/math/math.cpp new file mode 100644 index 0000000000..39e861ef6b --- /dev/null +++ b/test-files/golden-tests/generator/hbs/math/math.cpp @@ -0,0 +1,15 @@ +/** Solves a quadratic equation in $O(1)$ time. + + The roots are \f$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}\f$. + + A standalone Markdown display span is promoted to a block: + + $$\int_0^1 x^2\,dx = \frac{1}{3}$$ + + The Doxygen delimiters produce the same block form: + + \f[ + e^{i\pi} + 1 = 0 + \f] + */ +double solve_quadratic(double a, double b, double c); diff --git a/test-files/golden-tests/generator/hbs/math/math.html b/test-files/golden-tests/generator/hbs/math/math.html new file mode 100644 index 0000000000..ca12b9a35d --- /dev/null +++ b/test-files/golden-tests/generator/hbs/math/math.html @@ -0,0 +1,54 @@ + + +Reference + + + +
+

Reference

+
+
+

Global namespace

+
+

Functions

+ + + + + + + +
NameDescription
solve_quadratic Solves a quadratic equation in \(O(1)\) time.
+ +
+
+
+

solve_quadratic

+
+

Solves a quadratic equation in \(O(1)\) time.

+
+
+

Synopsis

+

Declared in <math.cpp>

+
double
+solve_quadratic(
+    double a,
+    double b,
+    double c);
+
+
+

Description

+

The roots are \(x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}\).

+

A standalone Markdown display span is promoted to a block:

+
\[\int_0^1 x^2\,dx = \frac{1}{3}\]
+

The Doxygen delimiters produce the same block form:

+
\[e^{i\pi} + 1 = 0\]
+
+
+ +
+ + + \ No newline at end of file diff --git a/test-files/golden-tests/generator/hbs/math/math.xml b/test-files/golden-tests/generator/hbs/math/math.xml new file mode 100644 index 0000000000..7ba6011e71 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/math/math.xml @@ -0,0 +1,154 @@ + + + + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + 2snLA3wNkrp2uBCNszvUX3UJ5h4s + + + + solve_quadratic + + + + math.cpp + math.cpp + 15 + 1 + + + + + function + 2snLA3wNkrp2uBCNszvUX3UJ5h4s + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + paragraph + + + text + The roots are + + + math + x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} + + + text + . + + + + + paragraph + + + text + A standalone Markdown display span is promoted to a block: + + + + + math + \int_0^1 x^2\,dx = \frac{1}{3} + + + paragraph + + + text + The Doxygen delimiters produce the same block form: + + + + + math + e^{i\pi} + 1 = 0 + + + + brief + + + text + Solves a quadratic equation in + + + math + O(1) + + + text + time. + + + + + + + named + + + identifier + double + + + double + + + + + + + named + + + identifier + double + + + double + + + a + + + + + named + + + identifier + double + + + double + + + b + + + + + named + + + identifier + double + + + double + + + c + + + normal + + diff --git a/test-files/golden-tests/generator/hbs/math/mrdocs.yml b/test-files/golden-tests/generator/hbs/math/mrdocs.yml new file mode 100644 index 0000000000..3cb41f10c7 --- /dev/null +++ b/test-files/golden-tests/generator/hbs/math/mrdocs.yml @@ -0,0 +1,5 @@ +generator: [adoc, html, xml] +multipage: false +no-default-styles: true +warn-if-undocumented: false +source-root: . diff --git a/test-files/golden-tests/javadoc/code-span/code-span.cpp b/test-files/golden-tests/javadoc/code-span/code-span.cpp new file mode 100644 index 0000000000..5f20102b31 --- /dev/null +++ b/test-files/golden-tests/javadoc/code-span/code-span.cpp @@ -0,0 +1,9 @@ +/** Sets the value pointed to by `target`. + + Markdown backticks: `target` and `new_value`. + + Doxygen @c command: @c new_value applies to one word. + + HTML code tag: *target. + */ +void set_value(int* target, int new_value); diff --git a/test-files/golden-tests/javadoc/code-span/code-span.xml b/test-files/golden-tests/javadoc/code-span/code-span.xml new file mode 100644 index 0000000000..4a84696ceb --- /dev/null +++ b/test-files/golden-tests/javadoc/code-span/code-span.xml @@ -0,0 +1,198 @@ + + + + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + 3N5Wqc8NtBY1TvGYZykoTr252nsr + + + + set_value + + + + code-span.cpp + code-span.cpp + 9 + 1 + + + + + function + 3N5Wqc8NtBY1TvGYZykoTr252nsr + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + paragraph + + + text + Markdown backticks: + + + code + + + text + target + + + + + text + and + + + code + + + text + new_value + + + + + text + . + + + + + paragraph + + + text + Doxygen + + + code + + + text + command + + + + + text + + + + code + + + text + new_value + + + + + text + applies to one word. + + + + + paragraph + + + text + HTML code tag: + + + code + + + text + *target + + + + + text + . + + + + + + brief + + + text + Sets the value pointed to by + + + code + + + text + target + + + + + text + . + + + + + + + named + + + identifier + void + + + void + + + + + + + pointer + + + named + + + identifier + int + + + int + + + + + target + + + + + named + + + identifier + int + + + int + + + new_value + + + normal + + diff --git a/test-files/golden-tests/javadoc/footnote/footnote.cpp b/test-files/golden-tests/javadoc/footnote/footnote.cpp new file mode 100644 index 0000000000..a7dfc0b826 --- /dev/null +++ b/test-files/golden-tests/javadoc/footnote/footnote.cpp @@ -0,0 +1,10 @@ +/** A cache with a bounded number of entries[^lru]. + + A lookup that misses falls through to the backing store[^lru], and a + second policy governs write-back[^wb]. + + [^lru]: Least-recently-used entries are evicted first. + + [^wb]: Dirty entries are flushed lazily. + */ +struct cache { }; diff --git a/test-files/golden-tests/javadoc/footnote/footnote.xml b/test-files/golden-tests/javadoc/footnote/footnote.xml new file mode 100644 index 0000000000..46fdc833f2 --- /dev/null +++ b/test-files/golden-tests/javadoc/footnote/footnote.xml @@ -0,0 +1,107 @@ + + + + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + fyFCKEy9CiunXYqEi5ascGLee8F + + + + cache + + + footnote.cpp + footnote.cpp + 10 + 1 + + + + record + fyFCKEy9CiunXYqEi5ascGLee8F + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + paragraph + + + text + A lookup that misses falls through to the backing store + + + footnote-reference + + + + text + , and a second policy governs write-back + + + footnote-reference + + + + text + . + + + + + + brief + + + text + A cache with a bounded number of entries + + + footnote-reference + + + + text + . + + + + + + footnote-definition + + + paragraph + + + text + Least-recently-used entries are evicted first. + + + + + + + + footnote-definition + + + paragraph + + + text + Dirty entries are flushed lazily. + + + + + + + + + struct + + diff --git a/test-files/golden-tests/javadoc/image/image.cpp b/test-files/golden-tests/javadoc/image/image.cpp new file mode 100644 index 0000000000..35fe67867b --- /dev/null +++ b/test-files/golden-tests/javadoc/image/image.cpp @@ -0,0 +1,9 @@ +/** An icon widget. + + Markdown image: ![status icon](status.png) + + Markdown image with a title: ![logo](logo.png "Project logo") + + HTML image tag: Banner + */ +struct icon { }; diff --git a/test-files/golden-tests/javadoc/image/image.xml b/test-files/golden-tests/javadoc/image/image.xml new file mode 100644 index 0000000000..b816b9cbdf --- /dev/null +++ b/test-files/golden-tests/javadoc/image/image.xml @@ -0,0 +1,84 @@ + + + + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + 2sDvCpwFFMAgQvrVkn5UDwsbLQh4 + + + + icon + + + image.cpp + image.cpp + 9 + 1 + + + + record + 2sDvCpwFFMAgQvrVkn5UDwsbLQh4 + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + paragraph + + + text + Markdown image: + + + image + status.png + status icon + + + + + paragraph + + + text + Markdown image with a title: + + + image + logo.png + logo + + + + + paragraph + + + text + HTML image tag: + + + image + banner.png + Banner + + + + + + brief + + + text + An icon widget. + + + + + struct + + diff --git a/test-files/golden-tests/javadoc/inline/inline-markers.adoc b/test-files/golden-tests/javadoc/inline/inline-markers.adoc new file mode 100644 index 0000000000..e55ce95b33 --- /dev/null +++ b/test-files/golden-tests/javadoc/inline/inline-markers.adoc @@ -0,0 +1,38 @@ += Reference +:mrdocs: + +[#index] +== Global namespace + +=== Functions + +[cols="1,4"] +|=== +| Name| Description +| link:#f[`f`] +| Exercise the inline markup kinds (HTML and Markdown forms): subscript, superscript, highlight, strikethrough. +|=== + +[#f] +== f + +Exercise the inline markup kinds (HTML and Markdown forms): subscript, superscript, highlight, strikethrough. + +=== Synopsis + +Declared in `<inline‐markers.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +void +f(); +---- + +=== Description + +HTML form (works in tight notation): H~2~O is water; x^2^ is x squared. *Bold*, #highlighted#, [.line-through]#struck‐through#, and [.line-through]#also struck# text. + +Markdown form (requires whitespace flanks): Subscript: ~i~ indicates an index. Superscript: raise to the ^n^ power. Strong: *bold*. Highlight: #highlighted#. Strikethrough: [.line-through]#struck‐through#. + + +[.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/javadoc/inline/inline-markers.cpp b/test-files/golden-tests/javadoc/inline/inline-markers.cpp new file mode 100644 index 0000000000..dd4f729e74 --- /dev/null +++ b/test-files/golden-tests/javadoc/inline/inline-markers.cpp @@ -0,0 +1,15 @@ +/** Exercise the inline markup kinds (HTML and Markdown forms): + subscript, superscript, highlight, strikethrough. + + HTML form (works in tight notation): + H2O is water; x2 is x squared. + Bold, highlighted, + struck-through, and also struck text. + + Markdown form (requires whitespace flanks): + Subscript: ~i~ indicates an index. Superscript: raise to the + ^n^ power. + Strong: **bold**. Highlight: ==highlighted==. + Strikethrough: ~~struck-through~~. + */ +void f(); diff --git a/test-files/golden-tests/javadoc/inline/inline-markers.html b/test-files/golden-tests/javadoc/inline/inline-markers.html new file mode 100644 index 0000000000..23f04d7c4c --- /dev/null +++ b/test-files/golden-tests/javadoc/inline/inline-markers.html @@ -0,0 +1,48 @@ + + +Reference + + + +
+

Reference

+
+
+

Global namespace

+
+

Functions

+ + + + + + + +
NameDescription
f Exercise the inline markup kinds (HTML and Markdown forms): subscript, superscript, highlight, strikethrough.
+ +
+
+
+

f

+
+

Exercise the inline markup kinds (HTML and Markdown forms): subscript, superscript, highlight, strikethrough.

+
+
+

Synopsis

+

Declared in <inline-markers.cpp>

+
void
+f();
+
+
+

Description

+

HTML form (works in tight notation): H2O is water; x2 is x squared. Bold, highlighted, struck-through, and also struck text.

+

Markdown form (requires whitespace flanks): Subscript: i indicates an index. Superscript: raise to the n power. Strong: bold. Highlight: highlighted. Strikethrough: struck-through.

+
+
+ +
+ + + \ No newline at end of file diff --git a/test-files/golden-tests/javadoc/inline/inline-markers.xml b/test-files/golden-tests/javadoc/inline/inline-markers.xml new file mode 100644 index 0000000000..86a5e43e9a --- /dev/null +++ b/test-files/golden-tests/javadoc/inline/inline-markers.xml @@ -0,0 +1,217 @@ + + + + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + 3WB9tVatMdT8usB7kG91HHDMUYwU + + + + f + + + + inline-markers.cpp + inline-markers.cpp + 15 + 1 + + + + + function + 3WB9tVatMdT8usB7kG91HHDMUYwU + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + paragraph + + + text + HTML form (works in tight notation): H + + + subscript + + + text + 2 + + + + + text + O is water; x + + + superscript + + + text + 2 + + + + + text + is x squared. + + + strong + + + text + Bold + + + + + text + , + + + highlight + + + text + highlighted + + + + + text + , + + + strikethrough + + + text + struck-through + + + + + text + , and + + + strikethrough + + + text + also struck + + + + + text + text. + + + + + paragraph + + + text + Markdown form (requires whitespace flanks): Subscript: + + + subscript + + + text + i + + + + + text + indicates an index. Superscript: raise to the + + + superscript + + + text + n + + + + + text + power. Strong: + + + strong + + + text + bold + + + + + text + . Highlight: + + + highlight + + + text + highlighted + + + + + text + . Strikethrough: + + + strikethrough + + + text + struck-through + + + + + text + . + + + + + + brief + + + text + Exercise the inline markup kinds (HTML and Markdown forms): subscript, superscript, highlight, strikethrough. + + + + + + + named + + + identifier + void + + + void + + + normal + + diff --git a/test-files/golden-tests/javadoc/inline/inline-markers.yml b/test-files/golden-tests/javadoc/inline/inline-markers.yml new file mode 100644 index 0000000000..0adfa7977d --- /dev/null +++ b/test-files/golden-tests/javadoc/inline/inline-markers.yml @@ -0,0 +1 @@ +generator: [xml, adoc, html] diff --git a/test-files/golden-tests/javadoc/math/math.cpp b/test-files/golden-tests/javadoc/math/math.cpp new file mode 100644 index 0000000000..25872a4f95 --- /dev/null +++ b/test-files/golden-tests/javadoc/math/math.cpp @@ -0,0 +1,15 @@ +/** Computes the area of a circle: $A = \pi r^2$. + + Inline math with Doxygen delimiters: \f$e^{i\pi} + 1 = 0\f$. + + A standalone Markdown display span is a block: + + $$\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}$$ + + A display block with Doxygen delimiters: + + \f[ + x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} + \f] + */ +double circle_area(double r); diff --git a/test-files/golden-tests/javadoc/math/math.xml b/test-files/golden-tests/javadoc/math/math.xml new file mode 100644 index 0000000000..18e7fbd075 --- /dev/null +++ b/test-files/golden-tests/javadoc/math/math.xml @@ -0,0 +1,124 @@ + + + + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + 34qLJpLx5zqxgfiGJzrnUXMvEF95 + + + + circle_area + + + + math.cpp + math.cpp + 15 + 1 + + + + + function + 34qLJpLx5zqxgfiGJzrnUXMvEF95 + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + paragraph + + + text + Inline math with Doxygen delimiters: + + + math + e^{i\pi} + 1 = 0 + + + text + . + + + + + paragraph + + + text + A standalone Markdown display span is a block: + + + + + math + \int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi} + + + paragraph + + + text + A display block with Doxygen delimiters: + + + + + math + x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} + + + + brief + + + text + Computes the area of a circle: + + + math + A = \pi r^2 + + + text + . + + + + + + + named + + + identifier + double + + + double + + + + + + + named + + + identifier + double + + + double + + + r + + + normal + + diff --git a/test-files/golden-tests/snippets/commands/code-span.adoc b/test-files/golden-tests/snippets/commands/code-span.adoc index 72f8fb6a52..6edce5f630 100644 --- a/test-files/golden-tests/snippets/commands/code-span.adoc +++ b/test-files/golden-tests/snippets/commands/code-span.adoc @@ -20,7 +20,7 @@ set_value( === Description -The previous value of `*target` is overwritten. +The previous value of `*target` is overwritten. The HTML form `*target` and the Doxygen `new_value` command render the same way as the Markdown backticks. [.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/snippets/commands/code-span.cpp b/test-files/golden-tests/snippets/commands/code-span.cpp index 6637ff6999..95257952f5 100644 --- a/test-files/golden-tests/snippets/commands/code-span.cpp +++ b/test-files/golden-tests/snippets/commands/code-span.cpp @@ -1,5 +1,7 @@ /** Sets the value pointed to by `target`. - The previous value of `*target` is overwritten. + The previous value of `*target` is overwritten. The HTML form + *target and the Doxygen @c new_value command render the + same way as the Markdown backticks. */ void set_value(int* target, int new_value); diff --git a/test-files/golden-tests/snippets/commands/footnote.adoc b/test-files/golden-tests/snippets/commands/footnote.adoc new file mode 100644 index 0000000000..87232fd39f --- /dev/null +++ b/test-files/golden-tests/snippets/commands/footnote.adoc @@ -0,0 +1,25 @@ += Reference +:mrdocs: + +[#configure] +== configure + +Uses the small‐buffer optimization^<>^ to avoid heap traffic. + +=== Synopsis + +Declared in `<footnote.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +void +configure(); +---- + +''' + +[#fn-sbo] +^sbo^ Short buffers live inline in the object, so no allocation happens until the capacity is exceeded. + + +[.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/snippets/commands/footnote.cpp b/test-files/golden-tests/snippets/commands/footnote.cpp new file mode 100644 index 0000000000..ab93efd3bb --- /dev/null +++ b/test-files/golden-tests/snippets/commands/footnote.cpp @@ -0,0 +1,6 @@ +/** Uses the small-buffer optimization[^sbo] to avoid heap traffic. + + [^sbo]: Short buffers live inline in the object, so no allocation happens + until the capacity is exceeded. + */ +void configure(); diff --git a/test-files/golden-tests/snippets/commands/image.adoc b/test-files/golden-tests/snippets/commands/image.adoc index bf2abadd8e..d48f12a4fe 100644 --- a/test-files/golden-tests/snippets/commands/image.adoc +++ b/test-files/golden-tests/snippets/commands/image.adoc @@ -1,10 +1,10 @@ = Reference :mrdocs: -[#status_widget] -== status_widget +[#banner] +== banner -A widget showing a status indicator. +The documentation banner. === Synopsis @@ -12,14 +12,14 @@ Declared in `<image.cpp>` [source,cpp,subs="verbatim,replacements,macros,-callouts"] ---- -struct status_widget; +struct banner; ---- === Description -Markdown image: +Markdown image: image:MrDocsBanner.png[MrDocs banner] -HTML image: +HTML image: image:MrDocsBanner.png[MrDocs banner] [.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/snippets/commands/image.cpp b/test-files/golden-tests/snippets/commands/image.cpp index 49e1d009f2..50555d1801 100644 --- a/test-files/golden-tests/snippets/commands/image.cpp +++ b/test-files/golden-tests/snippets/commands/image.cpp @@ -1,7 +1,7 @@ -/** A widget showing a status indicator. +/** The documentation banner. - Markdown image: ![Status icon](status.png) + Markdown image: ![MrDocs banner](MrDocsBanner.png) - HTML image: Banner + HTML image: MrDocs banner */ -struct status_widget { }; +struct banner { }; diff --git a/test-files/golden-tests/snippets/commands/math.adoc b/test-files/golden-tests/snippets/commands/math.adoc index e79e78b51c..42056d232d 100644 --- a/test-files/golden-tests/snippets/commands/math.adoc +++ b/test-files/golden-tests/snippets/commands/math.adoc @@ -1,10 +1,38 @@ = Reference :mrdocs: +[#binary_search] +== binary_search + +Looks up a key in a sorted range in stem:[O(\log n)] time. + +=== Synopsis + +Declared in `<math.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +int +binary_search( + int const* first, + int n, + int key); +---- + +=== Description + +The number of comparisons follows the recurrence: + +[stem] +++++ +T(n) = T(n/2) + O(1) +++++ +This comment uses the Doxygen formula delimiters, which Clang parses as known commands, so it compiles cleanly under ‐Wdocumentation. + [#circle_area] == circle_area -Computes the area of a circle: _A = rˆ2_. +Computes the area of a circle: stem:[A = \pi r^2]. === Synopsis @@ -18,7 +46,11 @@ circle_area(double r); === Description -For very large radii the result is taken modulo floating‐point precision: _A_{approx} = rˆ2 +_ +The Gaussian integral is the classic closed‐form result: +[stem] +++++ +\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi} +++++ [.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/snippets/commands/math.cpp b/test-files/golden-tests/snippets/commands/math.cpp index 4bfcd12771..3a863e64da 100644 --- a/test-files/golden-tests/snippets/commands/math.cpp +++ b/test-files/golden-tests/snippets/commands/math.cpp @@ -1,6 +1,18 @@ /** Computes the area of a circle: $A = \pi r^2$. - For very large radii the result is taken modulo - floating-point precision: $$A_{approx} = \pi r^2 + \epsilon$$ + The Gaussian integral is the classic closed-form result: + + $$\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}$$ */ double circle_area(double r); + +/** Looks up a key in a sorted range in \f$O(\log n)\f$ time. + + The number of comparisons follows the recurrence: + + \f[ T(n) = T(n/2) + O(1) \f] + + This comment uses the Doxygen formula delimiters, which Clang parses as + known commands, so it compiles cleanly under -Wdocumentation. + */ +int binary_search(int const* first, int n, int key); diff --git a/test-files/golden-tests/snippets/commands/subscript.adoc b/test-files/golden-tests/snippets/commands/subscript.adoc index 2b5028feb7..56d27d70e7 100644 --- a/test-files/golden-tests/snippets/commands/subscript.adoc +++ b/test-files/golden-tests/snippets/commands/subscript.adoc @@ -4,7 +4,7 @@ [#coefficient] == coefficient -Reads coefficient a~i~ from the polynomial. +Reads coefficient a~i~ from the polynomial. === Synopsis diff --git a/test-files/golden-tests/snippets/commands/subscript.cpp b/test-files/golden-tests/snippets/commands/subscript.cpp index 4c51fcab01..c71cab209c 100644 --- a/test-files/golden-tests/snippets/commands/subscript.cpp +++ b/test-files/golden-tests/snippets/commands/subscript.cpp @@ -1,3 +1,3 @@ -/** Reads coefficient a~i~ from the polynomial. +/** Reads coefficient ai from the polynomial. */ double coefficient(unsigned i); diff --git a/test-files/golden-tests/snippets/commands/superscript.adoc b/test-files/golden-tests/snippets/commands/superscript.adoc index 95d20422f5..242ae354a7 100644 --- a/test-files/golden-tests/snippets/commands/superscript.adoc +++ b/test-files/golden-tests/snippets/commands/superscript.adoc @@ -4,7 +4,7 @@ [#power_of_two] == power_of_two -Computes 2ˆnˆ in constant time. +Computes 2^n^ in constant time. === Synopsis diff --git a/test-files/golden-tests/snippets/commands/superscript.cpp b/test-files/golden-tests/snippets/commands/superscript.cpp index 83907e7be0..bc38ec6ec8 100644 --- a/test-files/golden-tests/snippets/commands/superscript.cpp +++ b/test-files/golden-tests/snippets/commands/superscript.cpp @@ -1,4 +1,4 @@ -/** Computes 2^n^ in constant time. +/** Computes 2n in constant time. Returns 1 when `n` is zero. */ diff --git a/test-files/golden-tests/snippets/distance.adoc b/test-files/golden-tests/snippets/distance.adoc index 8048bf0fc9..de46eda7b0 100644 --- a/test-files/golden-tests/snippets/distance.adoc +++ b/test-files/golden-tests/snippets/distance.adoc @@ -8,7 +8,7 @@ Return the distance between two points === Synopsis -Declared in `<distance.cpp>` +Declared in `<distance.hpp>` [source,cpp,subs="verbatim,replacements,macros,-callouts"] ---- diff --git a/test-files/golden-tests/snippets/distance.cpp b/test-files/golden-tests/snippets/distance.cpp index 8983087c6d..3c5e753b9b 100644 --- a/test-files/golden-tests/snippets/distance.cpp +++ b/test-files/golden-tests/snippets/distance.cpp @@ -1,13 +1 @@ -/** Return the distance between two points - - This function returns the distance between two points - according to the Euclidean distance formula. - - @param x0 The x-coordinate of the first point - @param y0 The y-coordinate of the first point - @param x1 The x-coordinate of the second point - @param y1 The y-coordinate of the second point - @return The distance between the two points -*/ -double -distance(double x0, double y0, double x1, double y1); \ No newline at end of file +#include "distance.hpp" diff --git a/test-files/golden-tests/snippets/distance.hpp b/test-files/golden-tests/snippets/distance.hpp new file mode 100644 index 0000000000..8983087c6d --- /dev/null +++ b/test-files/golden-tests/snippets/distance.hpp @@ -0,0 +1,13 @@ +/** Return the distance between two points + + This function returns the distance between two points + according to the Euclidean distance formula. + + @param x0 The x-coordinate of the first point + @param y0 The y-coordinate of the first point + @param x1 The x-coordinate of the second point + @param y1 The y-coordinate of the second point + @return The distance between the two points +*/ +double +distance(double x0, double y0, double x1, double y1); \ No newline at end of file diff --git a/test-files/golden-tests/snippets/function_object.adoc b/test-files/golden-tests/snippets/function_object.adoc index 639995f20f..5894277550 100644 --- a/test-files/golden-tests/snippets/function_object.adoc +++ b/test-files/golden-tests/snippets/function_object.adoc @@ -8,7 +8,7 @@ Compute the absolute value. === Synopsis -Declared in `<function_object.cpp>` +Declared in `<function_object.hpp>` [source,cpp,subs="verbatim,replacements,macros,-callouts"] ---- diff --git a/test-files/golden-tests/snippets/function_object.cpp b/test-files/golden-tests/snippets/function_object.cpp index 317cfe585d..2815819948 100644 --- a/test-files/golden-tests/snippets/function_object.cpp +++ b/test-files/golden-tests/snippets/function_object.cpp @@ -1,13 +1 @@ -struct abs_fn -{ - /** Compute the absolute value. - - @param x The input value. - @return The absolute value of x. - */ - double - operator()(double x) const noexcept; -}; - -/** Return the absolute value of a number. */ -constexpr abs_fn abs = {}; +#include "function_object.hpp" diff --git a/test-files/golden-tests/snippets/function_object.hpp b/test-files/golden-tests/snippets/function_object.hpp new file mode 100644 index 0000000000..317cfe585d --- /dev/null +++ b/test-files/golden-tests/snippets/function_object.hpp @@ -0,0 +1,13 @@ +struct abs_fn +{ + /** Compute the absolute value. + + @param x The input value. + @return The absolute value of x. + */ + double + operator()(double x) const noexcept; +}; + +/** Return the absolute value of a number. */ +constexpr abs_fn abs = {}; diff --git a/test-files/golden-tests/snippets/is_prime.adoc b/test-files/golden-tests/snippets/is_prime.adoc index c3a350253d..255f792d32 100644 --- a/test-files/golden-tests/snippets/is_prime.adoc +++ b/test-files/golden-tests/snippets/is_prime.adoc @@ -8,7 +8,7 @@ Return true if a number is prime. === Synopsis -Declared in `<is_prime.cpp>` +Declared in `<is_prime.hpp>` [source,cpp,subs="verbatim,replacements,macros,-callouts"] ---- diff --git a/test-files/golden-tests/snippets/is_prime.cpp b/test-files/golden-tests/snippets/is_prime.cpp index 9330eaf306..aad3558ecf 100644 --- a/test-files/golden-tests/snippets/is_prime.cpp +++ b/test-files/golden-tests/snippets/is_prime.cpp @@ -1,11 +1 @@ -/** Return true if a number is prime. - - @par Complexity - - Linear in n. - - @return Whether `n` is prime. - @param n The number to test -*/ -bool -is_prime(unsigned long long n) noexcept; \ No newline at end of file +#include "is_prime.hpp" diff --git a/test-files/golden-tests/snippets/is_prime.hpp b/test-files/golden-tests/snippets/is_prime.hpp new file mode 100644 index 0000000000..9330eaf306 --- /dev/null +++ b/test-files/golden-tests/snippets/is_prime.hpp @@ -0,0 +1,11 @@ +/** Return true if a number is prime. + + @par Complexity + + Linear in n. + + @return Whether `n` is prime. + @param n The number to test +*/ +bool +is_prime(unsigned long long n) noexcept; \ No newline at end of file diff --git a/test-files/golden-tests/snippets/landing/sqrt.adoc b/test-files/golden-tests/snippets/landing/sqrt.adoc index 658a0e6d27..741fd88968 100644 --- a/test-files/golden-tests/snippets/landing/sqrt.adoc +++ b/test-files/golden-tests/snippets/landing/sqrt.adoc @@ -27,11 +27,11 @@ This function is defined as an https://en.cppreference.com/cpp/algorithm/ranges# === Description -Returns the integer square root of `value` via bit manipulation. +Returns stem:[\lfloor\sqrt{value}\rfloor], the largest integer whose square does not exceed `value`, computed via bit manipulation. === Complexity -Logarithmic in `value`. +Logarithmic in `value`, about stem:[O(\log value)] iterations. [NOTE] ==== diff --git a/test-files/golden-tests/snippets/landing/sqrt.cpp b/test-files/golden-tests/snippets/landing/sqrt.cpp index 9a88a68c41..8916afec5e 100644 --- a/test-files/golden-tests/snippets/landing/sqrt.cpp +++ b/test-files/golden-tests/snippets/landing/sqrt.cpp @@ -5,11 +5,11 @@ struct sqrt_fn { /** Compute the integer square root. - Returns the integer square root of `value` - via bit manipulation. + Returns \f$\lfloor\sqrt{value}\rfloor\f$, the largest integer whose + square does not exceed `value`, computed via bit manipulation. @par Complexity - Logarithmic in `value`. + Logarithmic in `value`, about $O(\log value)$ iterations. @note Returns zero for zero input. diff --git a/test-files/golden-tests/snippets/landing/sqrt.html b/test-files/golden-tests/snippets/landing/sqrt.html index 9352ad2eaa..86e2d48a27 100644 --- a/test-files/golden-tests/snippets/landing/sqrt.html +++ b/test-files/golden-tests/snippets/landing/sqrt.html @@ -30,9 +30,9 @@

NOTE

Description

-

Returns the integer square root of value via bit manipulation.

+

Returns \(\lfloor\sqrt{value}\rfloor\), the largest integer whose square does not exceed value, computed via bit manipulation.

Complexity

-

Logarithmic in value.

+

Logarithmic in value, about \(O(\log value)\) iterations.

NOTE

diff --git a/test-files/golden-tests/snippets/sqrt.adoc b/test-files/golden-tests/snippets/sqrt.adoc index b31ffae6f8..e40b0e6810 100644 --- a/test-files/golden-tests/snippets/sqrt.adoc +++ b/test-files/golden-tests/snippets/sqrt.adoc @@ -4,11 +4,11 @@ [#sqrt] == sqrt -Computes the square root of an integral value. +Computes the integer square root stem:[\lfloor\sqrt{value}\rfloor]. === Synopsis -Declared in `<sqrt.cpp>` +Declared in `<sqrt.hpp>` [source,cpp,subs="verbatim,replacements,macros,-callouts"] ---- @@ -20,7 +20,7 @@ requires std::is_integral_v<T>; === Description -This function calculates the square root of a given integral value using bit manipulation. +This function calculates the square root of a given integral value using bit manipulation, in stem:[O(\log value)] time. === Exceptions diff --git a/test-files/golden-tests/snippets/sqrt.cpp b/test-files/golden-tests/snippets/sqrt.cpp index dde92f52c9..aefa77ac21 100644 --- a/test-files/golden-tests/snippets/sqrt.cpp +++ b/test-files/golden-tests/snippets/sqrt.cpp @@ -1,33 +1 @@ -#include - -/** Computes the square root of an integral value. - - This function calculates the square root of a - given integral value using bit manipulation. - - @throws std::invalid_argument if the input value is negative. - - @tparam T The type of the input value. Must be an integral type. - @param value The integral value to compute the square root of. - @return The square root of the input value. - */ -template -std::enable_if_t, T> sqrt(T value) { - if (value < 0) { - throw std::invalid_argument( - "Cannot compute square root of a negative number"); - } - T result = 0; - // The second-to-top bit is set - T bit = 1 << (sizeof(T) * 8 - 2); - while (bit > value) bit >>= 2; - while (bit != 0) { - if (value >= result + bit) { - value -= result + bit; - result += bit << 1; - } - result >>= 1; - bit >>= 2; - } - return result; -} +#include "sqrt.hpp" diff --git a/test-files/golden-tests/snippets/sqrt.hpp b/test-files/golden-tests/snippets/sqrt.hpp new file mode 100644 index 0000000000..b83304f815 --- /dev/null +++ b/test-files/golden-tests/snippets/sqrt.hpp @@ -0,0 +1,15 @@ +#include + +/** Computes the integer square root \f$\lfloor\sqrt{value}\rfloor\f$. + + This function calculates the square root of a given integral value + using bit manipulation, in $O(\log value)$ time. + + @throws std::invalid_argument if the input value is negative. + + @tparam T The type of the input value. Must be an integral type. + @param value The integral value to compute the square root of. + @return The square root of the input value. + */ +template +std::enable_if_t, T> sqrt(T value); diff --git a/test-files/golden-tests/snippets/terminate.adoc b/test-files/golden-tests/snippets/terminate.adoc index cd55e0221a..b286a60393 100644 --- a/test-files/golden-tests/snippets/terminate.adoc +++ b/test-files/golden-tests/snippets/terminate.adoc @@ -8,7 +8,7 @@ Exit the program. === Synopsis -Declared in `<terminate.cpp>` +Declared in `<terminate.hpp>` [source,cpp,subs="verbatim,replacements,macros,-callouts"] ---- diff --git a/test-files/golden-tests/snippets/terminate.cpp b/test-files/golden-tests/snippets/terminate.cpp index b3e291bc5d..ce821aef60 100644 --- a/test-files/golden-tests/snippets/terminate.cpp +++ b/test-files/golden-tests/snippets/terminate.cpp @@ -1,9 +1 @@ -/** Exit the program. - - The program will end immediately. - - @note This function does not return. -*/ -[[noreturn]] -void -terminate() noexcept; \ No newline at end of file +#include "terminate.hpp" diff --git a/test-files/golden-tests/snippets/terminate.hpp b/test-files/golden-tests/snippets/terminate.hpp new file mode 100644 index 0000000000..b3e291bc5d --- /dev/null +++ b/test-files/golden-tests/snippets/terminate.hpp @@ -0,0 +1,9 @@ +/** Exit the program. + + The program will end immediately. + + @note This function does not return. +*/ +[[noreturn]] +void +terminate() noexcept; \ No newline at end of file