Author: Neil Nair
Date: 2026-07-03T10:51:15-07:00
New Revision: b6976d223eeb40091685e3b20e9c41f42f6bd6e0

URL: 
https://github.com/llvm/llvm-project/commit/b6976d223eeb40091685e3b20e9c41f42f6bd6e0
DIFF: 
https://github.com/llvm/llvm-project/commit/b6976d223eeb40091685e3b20e9c41f42f6bd6e0.diff

LOG: [clang-doc] Add Markdown AST node type definitions (#205609)

Markdown AST node hierarchy for clang-doc/support. Separate Block and
Inline node hierarchies using llvm::ilist_node and llvm::simple_ilist.
Virtual print() in base classes with LLVM_DUMP_METHOD dump() inherited
by subclasses. clangDocMarkdown is its own CMake target. Unit tests
exercise node construction, accessors, and children. Assisted-by: Claude

Added: 
    clang-tools-extra/clang-doc/support/Markdown.cpp
    clang-tools-extra/clang-doc/support/Markdown.h
    clang-tools-extra/unittests/clang-doc/MarkdownParserTest.cpp

Modified: 
    clang-tools-extra/clang-doc/support/CMakeLists.txt
    clang-tools-extra/unittests/clang-doc/CMakeLists.txt

Removed: 
    


################################################################################
diff  --git a/clang-tools-extra/clang-doc/support/CMakeLists.txt 
b/clang-tools-extra/clang-doc/support/CMakeLists.txt
index 8ac913ffbe998..2ea459b8a9404 100644
--- a/clang-tools-extra/clang-doc/support/CMakeLists.txt
+++ b/clang-tools-extra/clang-doc/support/CMakeLists.txt
@@ -5,6 +5,13 @@ set(LLVM_LINK_COMPONENTS
   )
 
 add_clang_library(clangDocSupport STATIC
+  PARTIAL_SOURCES_INTENDED
   File.cpp
   Utils.cpp
   )
+
+add_clang_library(clangDocMarkdown STATIC
+  PARTIAL_SOURCES_INTENDED
+  Markdown.cpp
+  )
+  
\ No newline at end of file

diff  --git a/clang-tools-extra/clang-doc/support/Markdown.cpp 
b/clang-tools-extra/clang-doc/support/Markdown.cpp
new file mode 100644
index 0000000000000..e7049822543c1
--- /dev/null
+++ b/clang-tools-extra/clang-doc/support/Markdown.cpp
@@ -0,0 +1,99 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Markdown.h"
+
+namespace clang::doc::markdown {
+
+// TODO: print() currently outputs nodes flat with no indentation. Add
+// S-expression style formatting (as used by the Swift AST printer) to make
+// dumped trees easier to read.
+
+#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+LLVM_DUMP_METHOD void InlineNode::dump() const { print(llvm::errs()); }
+#endif
+
+void TextNode::print(llvm::raw_ostream &OS) const {
+  OS << "TextNode: " << getText() << "\n";
+}
+
+void InlineCodeNode::print(llvm::raw_ostream &OS) const {
+  OS << "InlineCodeNode: " << getCode() << "\n";
+}
+
+void EmphasisNode::print(llvm::raw_ostream &OS) const {
+  OS << "EmphasisNode\n";
+  for (const auto &Child : children())
+    Child.print(OS);
+}
+
+void StrongNode::print(llvm::raw_ostream &OS) const {
+  OS << "StrongNode\n";
+  for (const auto &Child : children())
+    Child.print(OS);
+}
+
+#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+LLVM_DUMP_METHOD void BlockNode::dump() const { print(llvm::errs()); }
+#endif
+
+void ParagraphNode::print(llvm::raw_ostream &OS) const {
+  OS << "ParagraphNode\n";
+  for (const auto &Child : children())
+    Child.print(OS);
+}
+
+void HeadingNode::print(llvm::raw_ostream &OS) const {
+  OS << "HeadingNode: level=" << getLevel() << "\n";
+  for (const auto &Child : children())
+    Child.print(OS);
+}
+
+void FencedCodeNode::print(llvm::raw_ostream &OS) const {
+  OS << "FencedCodeNode: lang=" << getLang() << "\n" << getCode() << "\n";
+}
+
+void ListItemNode::print(llvm::raw_ostream &OS) const {
+  OS << "ListItemNode\n";
+  for (const auto &Child : children())
+    Child.print(OS);
+}
+
+#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+LLVM_DUMP_METHOD void ListItemNode::dump() const { print(llvm::errs()); }
+#endif
+
+void UnorderedListNode::print(llvm::raw_ostream &OS) const {
+  OS << "UnorderedListNode\n";
+  for (const auto &Item : items())
+    Item.print(OS);
+}
+
+void OrderedListNode::print(llvm::raw_ostream &OS) const {
+  OS << "OrderedListNode: start=" << getStart() << "\n";
+  for (const auto &Item : items())
+    Item.print(OS);
+}
+
+void BlockQuoteNode::print(llvm::raw_ostream &OS) const {
+  OS << "BlockQuoteNode\n";
+  for (const auto &Child : children())
+    Child.print(OS);
+}
+
+void ThematicBreakNode::print(llvm::raw_ostream &OS) const {
+  OS << "ThematicBreakNode\n";
+}
+
+void DocumentNode::print(llvm::raw_ostream &OS) const {
+  OS << "DocumentNode\n";
+  for (const auto &Child : children())
+    Child.print(OS);
+}
+
+} // namespace clang::doc::markdown

diff  --git a/clang-tools-extra/clang-doc/support/Markdown.h 
b/clang-tools-extra/clang-doc/support/Markdown.h
new file mode 100644
index 0000000000000..f92624bac7c86
--- /dev/null
+++ b/clang-tools-extra/clang-doc/support/Markdown.h
@@ -0,0 +1,315 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Defines the Markdown AST node hierarchy for the clang-doc Markdown parser.
+///
+/// Block nodes represent structural constructs (paragraphs, headings, lists,
+/// fenced code blocks, etc). Inline nodes represent span-level content (text,
+/// emphasis, inline code) that appears inside block nodes.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_DOC_SUPPORT_MARKDOWN_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_DOC_SUPPORT_MARKDOWN_H
+
+#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/simple_ilist.h"
+#include "llvm/Support/Casting.h"
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/raw_ostream.h"
+
+namespace clang::doc::markdown {
+
+enum class NodeKind {
+  // Inline nodes
+  NK_Text,
+  NK_InlineCode,
+  NK_Emphasis,
+  NK_Strong,
+  // Block nodes
+  NK_Paragraph,
+  NK_Heading,
+  NK_FencedCode,
+  NK_Table, // TODO: add TableNode
+  NK_UnorderedList,
+  NK_OrderedList,
+  NK_BlockQuote,
+  NK_ThematicBreak,
+  NK_Document,
+};
+
+/// Base class for all inline nodes. Inline nodes represent span-level content
+/// such as text, emphasis, and inline code.
+class InlineNode : public llvm::ilist_node<InlineNode> {
+public:
+  explicit InlineNode(NodeKind K) : Kind(K) {}
+  virtual ~InlineNode() = default;
+  NodeKind getKind() const { return Kind; }
+
+  /// Recursively prints the node and its children to OS.
+  virtual void print(llvm::raw_ostream &OS) const = 0;
+
+#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+  /// Prints to llvm::errs(). Only available in assert builds.
+  LLVM_DUMP_METHOD void dump() const;
+#endif
+
+private:
+  NodeKind Kind;
+};
+
+using InlineList = llvm::simple_ilist<InlineNode>;
+
+/// A plain text run.
+class TextNode : public InlineNode {
+public:
+  explicit TextNode(llvm::StringRef T)
+      : InlineNode(NodeKind::NK_Text), Text(T) {}
+  llvm::StringRef getText() const { return Text; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const InlineNode *N) {
+    return N->getKind() == NodeKind::NK_Text;
+  }
+
+private:
+  llvm::StringRef Text;
+};
+
+/// A backtick-delimited inline code span.
+class InlineCodeNode : public InlineNode {
+public:
+  explicit InlineCodeNode(llvm::StringRef C)
+      : InlineNode(NodeKind::NK_InlineCode), Code(C) {}
+  llvm::StringRef getCode() const { return Code; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const InlineNode *N) {
+    return N->getKind() == NodeKind::NK_InlineCode;
+  }
+
+private:
+  llvm::StringRef Code;
+};
+
+/// An emphasis span (* or _).
+class EmphasisNode : public InlineNode {
+public:
+  EmphasisNode() : InlineNode(NodeKind::NK_Emphasis) {}
+  void addChild(InlineNode &N) { Children.push_back(N); }
+  void removeChild(InlineNode &N) { Children.remove(N); }
+  InlineList &children() { return Children; }
+  const InlineList &children() const { return Children; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const InlineNode *N) {
+    return N->getKind() == NodeKind::NK_Emphasis;
+  }
+
+private:
+  InlineList Children;
+};
+
+/// A strong emphasis span (** or __).
+class StrongNode : public InlineNode {
+public:
+  StrongNode() : InlineNode(NodeKind::NK_Strong) {}
+  void addChild(InlineNode &N) { Children.push_back(N); }
+  void removeChild(InlineNode &N) { Children.remove(N); }
+  InlineList &children() { return Children; }
+  const InlineList &children() const { return Children; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const InlineNode *N) {
+    return N->getKind() == NodeKind::NK_Strong;
+  }
+
+private:
+  InlineList Children;
+};
+
+/// Base class for all block nodes. Block nodes represent structural constructs
+/// such as paragraphs, headings, and lists.
+class BlockNode : public llvm::ilist_node<BlockNode> {
+public:
+  explicit BlockNode(NodeKind K) : Kind(K) {}
+  virtual ~BlockNode() = default;
+  NodeKind getKind() const { return Kind; }
+
+  /// Recursively prints the node and its children to OS.
+  virtual void print(llvm::raw_ostream &OS) const = 0;
+
+#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+  /// Prints to llvm::errs(). Only available in assert builds.
+  LLVM_DUMP_METHOD void dump() const;
+#endif
+
+private:
+  NodeKind Kind;
+};
+
+using BlockList = llvm::simple_ilist<BlockNode>;
+
+/// A paragraph of inline content.
+class ParagraphNode : public BlockNode {
+public:
+  ParagraphNode() : BlockNode(NodeKind::NK_Paragraph) {}
+  void addChild(InlineNode &N) { Children.push_back(N); }
+  void removeChild(InlineNode &N) { Children.remove(N); }
+  InlineList &children() { return Children; }
+  const InlineList &children() const { return Children; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const BlockNode *N) {
+    return N->getKind() == NodeKind::NK_Paragraph;
+  }
+
+private:
+  InlineList Children;
+};
+
+/// An ATX heading (# through ######).
+class HeadingNode : public BlockNode {
+public:
+  explicit HeadingNode(unsigned L)
+      : BlockNode(NodeKind::NK_Heading), Level(L) {}
+  unsigned getLevel() const { return Level; }
+  void addChild(InlineNode &N) { Children.push_back(N); }
+  void removeChild(InlineNode &N) { Children.remove(N); }
+  InlineList &children() { return Children; }
+  const InlineList &children() const { return Children; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const BlockNode *N) {
+    return N->getKind() == NodeKind::NK_Heading;
+  }
+
+private:
+  unsigned Level;
+  InlineList Children;
+};
+
+/// A fenced code block (``` or ~~~). Lang holds the info string.
+class FencedCodeNode : public BlockNode {
+public:
+  FencedCodeNode(llvm::StringRef L, llvm::StringRef C)
+      : BlockNode(NodeKind::NK_FencedCode), Lang(L), Code(C) {}
+  llvm::StringRef getLang() const { return Lang; }
+  llvm::StringRef getCode() const { return Code; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const BlockNode *N) {
+    return N->getKind() == NodeKind::NK_FencedCode;
+  }
+
+private:
+  llvm::StringRef Lang;
+  llvm::StringRef Code;
+};
+
+/// A single item in an unordered or ordered list.
+/// ListItemNode is not a BlockNode -- it only lives inside list nodes.
+class ListItemNode : public llvm::ilist_node<ListItemNode> {
+public:
+  ListItemNode() = default;
+  void addChild(InlineNode &N) { Children.push_back(N); }
+  void removeChild(InlineNode &N) { Children.remove(N); }
+  InlineList &children() { return Children; }
+  const InlineList &children() const { return Children; }
+  void print(llvm::raw_ostream &OS) const;
+
+#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+  /// Prints to llvm::errs(). Only available in assert builds.
+  /// ListItemNode provides its own dump() since it does not inherit BlockNode.
+  LLVM_DUMP_METHOD void dump() const;
+#endif
+
+private:
+  InlineList Children;
+};
+
+using ItemList = llvm::simple_ilist<ListItemNode>;
+
+/// An unordered list (-, *, or + markers).
+class UnorderedListNode : public BlockNode {
+public:
+  UnorderedListNode() : BlockNode(NodeKind::NK_UnorderedList) {}
+  void addItem(ListItemNode &N) { Items.push_back(N); }
+  void removeItem(ListItemNode &N) { Items.remove(N); }
+  ItemList &items() { return Items; }
+  const ItemList &items() const { return Items; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const BlockNode *N) {
+    return N->getKind() == NodeKind::NK_UnorderedList;
+  }
+
+private:
+  ItemList Items;
+};
+
+/// An ordered list (1. 2. 3. markers). Start holds the first item number.
+class OrderedListNode : public BlockNode {
+public:
+  explicit OrderedListNode(unsigned S = 1)
+      : BlockNode(NodeKind::NK_OrderedList), Start(S) {}
+  unsigned getStart() const { return Start; }
+  void addItem(ListItemNode &N) { Items.push_back(N); }
+  void removeItem(ListItemNode &N) { Items.remove(N); }
+  ItemList &items() { return Items; }
+  const ItemList &items() const { return Items; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const BlockNode *N) {
+    return N->getKind() == NodeKind::NK_OrderedList;
+  }
+
+private:
+  unsigned Start;
+  ItemList Items;
+};
+
+/// A block quote (> marker).
+class BlockQuoteNode : public BlockNode {
+public:
+  BlockQuoteNode() : BlockNode(NodeKind::NK_BlockQuote) {}
+  void addChild(BlockNode &N) { Children.push_back(N); }
+  void removeChild(BlockNode &N) { Children.remove(N); }
+  BlockList &children() { return Children; }
+  const BlockList &children() const { return Children; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const BlockNode *N) {
+    return N->getKind() == NodeKind::NK_BlockQuote;
+  }
+
+private:
+  BlockList Children;
+};
+
+/// A thematic break (---, ***, or ___).
+class ThematicBreakNode : public BlockNode {
+public:
+  ThematicBreakNode() : BlockNode(NodeKind::NK_ThematicBreak) {}
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const BlockNode *N) {
+    return N->getKind() == NodeKind::NK_ThematicBreak;
+  }
+};
+
+/// The root document node. Contains all top-level block nodes.
+class DocumentNode : public BlockNode {
+public:
+  DocumentNode() : BlockNode(NodeKind::NK_Document) {}
+  void addChild(BlockNode &N) { Children.push_back(N); }
+  void removeChild(BlockNode &N) { Children.remove(N); }
+  BlockList &children() { return Children; }
+  const BlockList &children() const { return Children; }
+  void print(llvm::raw_ostream &OS) const override;
+  static bool classof(const BlockNode *N) {
+    return N->getKind() == NodeKind::NK_Document;
+  }
+
+private:
+  BlockList Children;
+};
+
+} // namespace clang::doc::markdown
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_DOC_SUPPORT_MARKDOWN_H

diff  --git a/clang-tools-extra/unittests/clang-doc/CMakeLists.txt 
b/clang-tools-extra/unittests/clang-doc/CMakeLists.txt
index 01b34ec9a791e..97a26f0e24d09 100644
--- a/clang-tools-extra/unittests/clang-doc/CMakeLists.txt
+++ b/clang-tools-extra/unittests/clang-doc/CMakeLists.txt
@@ -31,6 +31,7 @@ add_extra_unittest(ClangDocTests
   SerializeTest.cpp
   YAMLGeneratorTest.cpp
   JSONGeneratorTest.cpp
+  MarkdownParserTest.cpp
   )
 
 clang_target_link_libraries(ClangDocTests
@@ -49,5 +50,6 @@ clang_target_link_libraries(ClangDocTests
 target_link_libraries(ClangDocTests
   PRIVATE
   clangDoc
+  clangDocMarkdown
   LLVMTestingSupport
   )

diff  --git a/clang-tools-extra/unittests/clang-doc/MarkdownParserTest.cpp 
b/clang-tools-extra/unittests/clang-doc/MarkdownParserTest.cpp
new file mode 100644
index 0000000000000..01fbdbbba9bb9
--- /dev/null
+++ b/clang-tools-extra/unittests/clang-doc/MarkdownParserTest.cpp
@@ -0,0 +1,118 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "support/Markdown.h"
+#include "gtest/gtest.h"
+
+using namespace clang::doc::markdown;
+using namespace llvm;
+
+namespace {
+
+/// Returns the text of the first TextNode child in the given inline list.
+static llvm::StringRef firstChildText(const InlineList &L) {
+  return llvm::cast<TextNode>(L.front()).getText();
+}
+
+TEST(MarkdownNodeTest, TextNode) {
+  TextNode N("hello");
+  EXPECT_EQ(N.getKind(), NodeKind::NK_Text);
+  EXPECT_EQ(N.getText(), "hello");
+}
+
+TEST(MarkdownNodeTest, FencedCodeNode) {
+  FencedCodeNode N("cpp", "int x = 0;\nint y = 1;");
+  EXPECT_EQ(N.getKind(), NodeKind::NK_FencedCode);
+  EXPECT_EQ(N.getLang(), "cpp");
+  llvm::SmallVector<llvm::StringRef> Lines;
+  N.getCode().split(Lines, '\n');
+  EXPECT_EQ(Lines[0], "int x = 0;");
+  EXPECT_EQ(Lines[1], "int y = 1;");
+}
+
+TEST(MarkdownNodeTest, HeadingNode) {
+  HeadingNode N(2);
+  EXPECT_EQ(N.getKind(), NodeKind::NK_Heading);
+  EXPECT_EQ(N.getLevel(), 2u);
+}
+
+TEST(MarkdownNodeTest, ThematicBreakNode) {
+  ThematicBreakNode N;
+  EXPECT_EQ(N.getKind(), NodeKind::NK_ThematicBreak);
+}
+
+TEST(MarkdownNodeTest, InlineCodeNode) {
+  InlineCodeNode N("foo()");
+  EXPECT_EQ(N.getKind(), NodeKind::NK_InlineCode);
+  EXPECT_EQ(N.getCode(), "foo()");
+}
+
+TEST(MarkdownNodeTest, EmphasisNode) {
+  EmphasisNode N;
+  TextNode Child("emphasized");
+  N.addChild(Child);
+  EXPECT_EQ(N.getKind(), NodeKind::NK_Emphasis);
+  EXPECT_FALSE(N.children().empty());
+  EXPECT_EQ(firstChildText(N.children()), "emphasized");
+}
+
+TEST(MarkdownNodeTest, EmphasisRemoveChild) {
+  EmphasisNode N;
+  TextNode Child("temp");
+  N.addChild(Child);
+  EXPECT_FALSE(N.children().empty());
+  N.removeChild(Child);
+  EXPECT_TRUE(N.children().empty());
+}
+
+TEST(MarkdownNodeTest, UnorderedListNode) {
+  UnorderedListNode N;
+  EXPECT_EQ(N.getKind(), NodeKind::NK_UnorderedList);
+  EXPECT_TRUE(N.items().empty());
+}
+
+TEST(MarkdownNodeTest, UnorderedListRemoveItem) {
+  UnorderedListNode List;
+  ListItemNode Item;
+  List.addItem(Item);
+  EXPECT_FALSE(List.items().empty());
+  List.removeItem(Item);
+  EXPECT_TRUE(List.items().empty());
+}
+
+TEST(MarkdownNodeTest, ParagraphNode) {
+  ParagraphNode N;
+  EXPECT_EQ(N.getKind(), NodeKind::NK_Paragraph);
+  EXPECT_TRUE(N.children().empty());
+}
+
+TEST(MarkdownNodeTest, DocumentNode) {
+  DocumentNode N;
+  EXPECT_EQ(N.getKind(), NodeKind::NK_Document);
+  EXPECT_TRUE(N.children().empty());
+}
+
+TEST(MarkdownNodeTest, ParagraphWithChildren) {
+  ParagraphNode Para;
+  TextNode Child("hello");
+  Para.addChild(Child);
+  EXPECT_FALSE(Para.children().empty());
+  EXPECT_EQ(firstChildText(Para.children()), "hello");
+}
+
+TEST(MarkdownNodeTest, UnorderedListWithItems) {
+  UnorderedListNode List;
+  ListItemNode Item;
+  TextNode Child("item text");
+  Item.addChild(Child);
+  List.addItem(Item);
+  EXPECT_FALSE(List.items().empty());
+  EXPECT_EQ(firstChildText(List.items().front().children()), "item text");
+}
+
+} // namespace


        
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to