From 426601c713d9da4d5a0818aeeafd1b31884c5161 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Fri, 30 Oct 2026 17:10:37 +0100 Subject: [PATCH] fix: inline formulas render inside HTML table cells Clang models a `\f$...\f$` formula as a verbatim block command, so the formula ends the paragraph it appears in and the text after it starts a new one. An HTML table with such a formula in a cell was therefore spread over several paragraphs, and the table parser, which looks for `` within one paragraph, warned that the tag was never closed and dropped the table. A Markdown formula (`$...$`) did not split the table, but it came out as plain text, because the traversal that parses inline Markdown never entered table cells. We now gather a paragraph together with the formulas and paragraph remainders that continue it, with no blank line between, before visiting it. The table parser then sees the whole table, and each formula becomes an inline node of its cell. This replaces merging the pieces after the fact, which ignored a blank line before a formula and appended a paragraph that starts with one to the paragraph before it. The traversal now descends into table cells, so their Markdown is parsed and their references are resolved. The AsciiDoc generator emits a formula verbatim, so a `|` in one, as in `\max(|x|, |y|)`, ended the cell. The table template now escapes it. Fixes #1370. --- .../adoc/partials/doc/block/table.adoc.hbs | 2 +- include/mrdocs/Metadata/DocComment.hpp | 29 +++ src/mrdocs/AST/ExtractDocComment.cpp | 161 ++++++++----- .../javadoc/html-table/html-table-math.adoc | 50 ++++ .../javadoc/html-table/html-table-math.cpp | 15 ++ .../javadoc/html-table/html-table-math.xml | 213 ++++++++++++++++++ .../javadoc/html-table/html-table-math.yml | 2 + 7 files changed, 414 insertions(+), 58 deletions(-) create mode 100644 tests/golden/fixtures/javadoc/html-table/html-table-math.adoc create mode 100644 tests/golden/fixtures/javadoc/html-table/html-table-math.cpp create mode 100644 tests/golden/fixtures/javadoc/html-table/html-table-math.xml create mode 100644 tests/golden/fixtures/javadoc/html-table/html-table-math.yml diff --git a/data/mrdocs/addons/generator/adoc/partials/doc/block/table.adoc.hbs b/data/mrdocs/addons/generator/adoc/partials/doc/block/table.adoc.hbs index 91b462b501e..c711a557e72 100644 --- a/data/mrdocs/addons/generator/adoc/partials/doc/block/table.adoc.hbs +++ b/data/mrdocs/addons/generator/adoc/partials/doc/block/table.adoc.hbs @@ -1,4 +1,4 @@ {{#if items.[0].is_header}}[%header] {{/if}}|=== -{{#each items}}{{#each cells}}| {{> doc/inline-container }} {{/each}} +{{#each items}}{{#each cells}}| {{#replace "|" "\|"}}{{> doc/inline-container }}{{/replace}} {{/each}} {{/each}}|=== diff --git a/include/mrdocs/Metadata/DocComment.hpp b/include/mrdocs/Metadata/DocComment.hpp index c768fce220e..80f2577dfde 100644 --- a/include/mrdocs/Metadata/DocComment.hpp +++ b/include/mrdocs/Metadata/DocComment.hpp @@ -244,6 +244,24 @@ traverseImpl(doc::ListItem& item, F&& func) } } +template +void +traverseImpl(doc::TableCell& cell, F&& func) +{ + if constexpr (!bottomUp && std::invocable) + { + func(cell); + } + for (Polymorphic& child : cell.children) + { + traverseImpl(child, func); + } + if constexpr (bottomUp && std::invocable) + { + func(cell); + } +} + // Traverse a derived node template requires(!std::same_as && !std::same_as) @@ -281,6 +299,17 @@ traverseImpl(NodeTy& N, F&& func) } } + if constexpr (std::same_as) + { + for (doc::TableRow& row: N.items) + { + for (doc::TableCell& cell: row.Cells) + { + traverseImpl(cell, func); + } + } + } + if constexpr (bottomUp && std::invocable) { func(N); diff --git a/src/mrdocs/AST/ExtractDocComment.cpp b/src/mrdocs/AST/ExtractDocComment.cpp index bb6c15609da..dc829a73e8b 100644 --- a/src/mrdocs/AST/ExtractDocComment.cpp +++ b/src/mrdocs/AST/ExtractDocComment.cpp @@ -303,12 +303,6 @@ class DocCommentVisitor // user's to fix, so their findings go to the debug log instead. bool warnings_{ true }; - // 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> @@ -516,10 +510,8 @@ class DocCommentVisitor // --- Visiting using Cursor (no member iterators) void - visitChildrenWithCursor(clang::comments::Comment const* C) + visitCursor(Cursor cur) { - MRDOCS_COMMENT_TRACE(C, ctx_); - Cursor cur(C); while (!cur.done()) { auto* n = cur.cur(); @@ -529,6 +521,95 @@ class DocCommentVisitor } } + void + visitChildrenWithCursor(clang::comments::Comment const* C) + { + MRDOCS_COMMENT_TRACE(C, ctx_); + visitCursor(Cursor(C)); + } + + // --- Paragraphs split by inline formulas + + bool + isInlineFormula(clang::comments::Comment const* c) const + { + using namespace clang::comments; + return c->getCommentKind() == CommentKind::VerbatimBlockComment + && static_cast(c)->getCommandName( + ctx_.getCommentCommandTraits()) == "f$"; + } + + unsigned + lineOf(clang::SourceLocation loc) const + { + return sm_.getPresumedLoc(loc).getLine(); + } + + // Whether `next`, the sibling after `prev`, continues the paragraph + // `prev` belongs to: it is a formula, or the text after one, and no blank + // line separates the two. + bool + continuesParagraph( + clang::comments::Comment const* prev, + clang::comments::Comment const* next) const + { + using namespace clang::comments; + bool const joinable + = next != nullptr + && (isInlineFormula(next) + || (isInlineFormula(prev) + && next->getCommentKind() + == CommentKind::ParagraphComment)); + return joinable + && lineOf(next->getBeginLoc()) <= lineOf(prev->getEndLoc()) + 1; + } + + static void + appendToParagraph(Cursor& paragraph, clang::comments::Comment const* c) + { + if (c->getCommentKind() + == clang::comments::CommentKind::ParagraphComment) + { + paragraph.children.append(c->child_begin(), c->child_end()); + } + else + { + paragraph.children.push_back(c); + } + } + + // Clang ends a paragraph at a `\f$...\f$` inline formula, which it models + // as a verbatim block, and puts the text after the formula in a new + // paragraph. Collect the nodes of the paragraph (or formula) at `cur.i` + // and of the siblings that continue it, leaving `cur.i` at the last of + // them, so that they are visited as one paragraph: the formulas stay + // inline, and an HTML table around them finds its end tag. + Cursor + joinFormulaSplitParagraph(Cursor& cur) const + { + Cursor paragraph; + appendToParagraph(paragraph, cur.cur()); + while (continuesParagraph(cur.cur(), cur.peek())) + { + cur.advance(); + appendToParagraph(paragraph, cur.cur()); + } + return paragraph; + } + + void + visitTopLevelParagraph(Cursor& cur) + { + Cursor joined = joinFormulaSplitParagraph(cur); + doc::ParagraphBlock paragraph; + BlockScope scope = enterScope(paragraph); + visitCursor(std::move(joined)); + if (!paragraph.empty()) + { + jd_.Document.emplace_back(std::move(paragraph)); + } + } + // What sits between an inline HTML start tag and its matching end tag. // When every node in between is text, `text` holds it all and the span // becomes one inline node. Otherwise (inline commands such as `@c`, @@ -1087,7 +1168,7 @@ class DocCommentVisitor return; case CommentKind::ParagraphComment: - visitParagraph(static_cast(C)); + visitParagraph(static_cast(C), cur); return; case CommentKind::BlockCommandComment: @@ -1103,7 +1184,14 @@ class DocCommentVisitor return; case CommentKind::VerbatimBlockComment: - visitVerbatimBlock(static_cast(C)); + if (!curInlines_ && isInlineFormula(C)) + { + visitTopLevelParagraph(cur); + } + else + { + visitVerbatimBlock(static_cast(C)); + } return; case CommentKind::VerbatimBlockLineComment: @@ -1571,34 +1659,16 @@ class DocCommentVisitor } void - visitParagraph(clang::comments::ParagraphComment const* C) + visitParagraph(clang::comments::ParagraphComment const* C, Cursor& cur) { MRDOCS_COMMENT_TRACE(C, ctx_); if (curInlines_) { 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); - if (!paragraph.empty()) + else { - jd_.Document.emplace_back(std::move(paragraph)); + visitTopLevelParagraph(cur); } } @@ -1931,31 +2001,8 @@ class DocCommentVisitor 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(); + // surrounding paragraph; visitTopLevelParagraph puts it back. + emplaceInline(true, std::move(payload)); return; } if (name == "f[") diff --git a/tests/golden/fixtures/javadoc/html-table/html-table-math.adoc b/tests/golden/fixtures/javadoc/html-table/html-table-math.adoc new file mode 100644 index 00000000000..a9053048d39 --- /dev/null +++ b/tests/golden/fixtures/javadoc/html-table/html-table-math.adoc @@ -0,0 +1,50 @@ += Reference +:mrdocs: + +[#index] +== Global index + +=== Functions + +[cols="1,4"] +|=== +| Name| Description +| link:#norm[`norm`] +| Compute the norm of a vector. +|=== + + +[#norm] +== norm + +Compute the norm of a vector. + +=== Synopsis + +Declared in `<html‐table‐math.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +double +norm( + double x, + double y, + int p); +---- + +=== Description + +[%header] +|=== +| Norm | Formula +| Euclidean | stem:[\sqrt{x^2 + y^2}] +| Maximum | stem:[\max(\|x\|, \|y\|)] +|=== +stem:[p] selects the norm. + +The result is stem:[\|v\|_p]. + +stem:[p = 2] is the default. + + +[.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/tests/golden/fixtures/javadoc/html-table/html-table-math.cpp b/tests/golden/fixtures/javadoc/html-table/html-table-math.cpp new file mode 100644 index 00000000000..f2fee409c95 --- /dev/null +++ b/tests/golden/fixtures/javadoc/html-table/html-table-math.cpp @@ -0,0 +1,15 @@ +/** Compute the norm of a vector. + + + + + +
NormFormula
Euclidean@f$ \sqrt{x^2 + y^2} @f$
Maximum$\max(|x|, |y|)$
+ + \f$p\f$ selects the norm. + + The result is \f$\|v\|_p\f$. + + \f$p = 2\f$ is the default. + */ +double norm(double x, double y, int p); diff --git a/tests/golden/fixtures/javadoc/html-table/html-table-math.xml b/tests/golden/fixtures/javadoc/html-table/html-table-math.xml new file mode 100644 index 00000000000..18301193ff8 --- /dev/null +++ b/tests/golden/fixtures/javadoc/html-table/html-table-math.xml @@ -0,0 +1,213 @@ + + + + index + namespace + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + regular + + FXSuomuVJ2mFDTFHnMYFugnvejt + + + + norm + norm + + + + html-table-math.cpp + html-table-math.cpp + 15 + 1 + + + + + function + FXSuomuVJ2mFDTFHnMYFugnvejt + regular + 4ZrjxJnU1LA5xSyrWMNuXTvSYKwt + + + + table + + + + + + + + text + Norm + + + + + + + text + Formula + + + + + + + + + + + text + Euclidean + + + + + + + math + \sqrt{x^2 + y^2} + + + + + + + + + + + text + Maximum + + + + + + + math + \max(|x|, |y|) + + + + + + +
+ + paragraph + + + math + p + + + text + selects the norm. + + + + + paragraph + + + text + The result is + + + math + \|v\|_p + + + text + . + + + + + paragraph + + + math + p = 2 + + + text + is the default. + + + +
+ + brief + + + text + Compute the norm of a vector. + + + +
+ + + named + + + identifier + double + + + double + + + + + + + named + + + identifier + double + + + double + + + x + + + + + named + + + identifier + double + + + double + + + y + + + + + named + + + identifier + int + + + int + + + p + + + normal +
+
diff --git a/tests/golden/fixtures/javadoc/html-table/html-table-math.yml b/tests/golden/fixtures/javadoc/html-table/html-table-math.yml new file mode 100644 index 00000000000..d5fd5e08fab --- /dev/null +++ b/tests/golden/fixtures/javadoc/html-table/html-table-math.yml @@ -0,0 +1,2 @@ +generator: [adoc, xml] +multipage: false