BareGit

Implement forEach node iteration and add unit tests

Author: MetroWind <chris.corsair@gmail.com>
Date: Sat Jan 17 17:07:14 2026 -0800
Commit: d7f2c95bdb91c77c2125f592272416d598ed777c

Changes

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 05f3fe2..60f1395 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -47,7 +47,7 @@ target_link_libraries(macrodown PRIVATE macrodown_lib)
 # Testing
 enable_testing()
 
-add_executable(macrodown_test tests/test_main.cpp tests/test_macro_engine.cpp tests/test_block_parser.cpp tests/test_integration.cpp)
+add_executable(macrodown_test tests/test_main.cpp tests/test_macro_engine.cpp tests/test_block_parser.cpp tests/test_integration.cpp tests/test_nodes.cpp)
 target_link_libraries(macrodown_test PRIVATE macrodown_lib GTest::gtest_main)
 target_include_directories(macrodown_test PRIVATE include)
 
diff --git a/design.md b/design.md
index aa7d541..5a7f723 100644
--- a/design.md
+++ b/design.md
@@ -36,29 +36,37 @@ We strictly follow the CommonMark "Appendix A" strategy:
 ## 3. Data Structures
 
 ### 3.1 AST Nodes
-The final tree consists of these node types:
+The final tree consists of a unified `Node` type using `std::variant` to hold different data types:
 
 ```cpp
-enum class NodeType { Text, Macro };
+struct Text {
+    std::string content;
+};
 
-struct Node {
-    virtual ~Node() = default;
-    virtual NodeType type() const = 0;
+struct Macro {
+    std::string name;
+    std::vector<std::unique_ptr<Node>> arguments;
+    bool is_special = false;
 };
 
-struct TextNode : public Node {
-    std::string content;
-    NodeType type() const override { return NodeType::Text; }
+struct Group {
+    std::vector<std::unique_ptr<Node>> children;
 };
 
-struct MacroNode : public Node {
-    std::string name;
-    std::vector<std::unique_ptr<Node>> arguments; // Arguments are themselves ASTs
-    bool is_special = false; // For intrinsics like %def
-    NodeType type() const override { return NodeType::Macro; }
+struct Node {
+    using Data = std::variant<Text, Macro, Group>;
+    Data data;
+
+    // Call function on each node in the tree (pre-order traversal).
+    // The callback function takes const Node& as an argument.
+    template<typename Callback>
+    void forEach(Callback f) const;
 };
 ```
 
+The `forEach` method provides a way to iterate over all nodes in the tree, including children of `Group` nodes and arguments of `Macro` nodes.
+
+
 ### 3.2 Block Structure (Intermediate)
 During Phase 1, we use a structure mirroring CommonMark blocks to maintain state (open/closed blocks, list types).
 
@@ -116,13 +124,14 @@ struct Block {
 ### 4.3 Evaluator (`Evaluator`)
 *   **Environment**: A map `std::map<std::string, MacroDefinition>`.
 *   **Mechanism**:
-    *   Traverses the `MacroNode` tree.
-    *   If it's a `TextNode`, append to output.
-    *   If it's a `MacroNode`:
+    *   Traverses the `Node` tree.
+    *   If it's a `Text` node, append to output.
+    *   If it's a `Macro` node:
         *   Look up definition.
         *   Bind arguments.
         *   Parse the definition body (if it's a user macro) or execute C++ callback (if intrinsic).
         *   Recursively evaluate the result.
+    *   If it's a `Group` node, recursively evaluate all children.
 
 ## 5. The Standard Library
 The system will boot with a "Prelude" of defined macros to support Markdown features.
diff --git a/include/nodes.h b/include/nodes.h
index f15444b..c3dbede 100644
--- a/include/nodes.h
+++ b/include/nodes.h
@@ -38,6 +38,30 @@ struct Node
     Node(Text t) : data(std::move(t)) {}
     Node(Macro m) : data(std::move(m)) {}
     Node(Group g) : data(std::move(g)) {}
+
+    // Call function on each node in the tree. Callback function takes
+    // const Node& as argument.
+    template<typename Callback>
+    void forEach(Callback f) const
+    {
+        f(*this);
+        if(std::holds_alternative<Group>(data))
+        {
+            for(const std::unique_ptr<Node>& child:
+                    std::get<Group>(data).children)
+            {
+                child->forEach(f);
+            }
+        }
+        else if(std::holds_alternative<Macro>(data))
+        {
+            for(const std::unique_ptr<Node>& arg:
+                    std::get<Macro>(data).arguments)
+            {
+                arg->forEach(f);
+            }
+        }
+    }
 };
 
 // Inline implementation to avoid circular dependency issues in headers
@@ -48,4 +72,4 @@ inline void Group::addChild(std::unique_ptr<Node> node)
 
 } // namespace macrodown
 
-#endif // MACRODOWN_NODES_H
\ No newline at end of file
+#endif // MACRODOWN_NODES_H
diff --git a/prompts.md b/prd.md
similarity index 67%
rename from prompts.md
rename to prd.md
index a3a2583..977d71a 100644
--- a/prompts.md
+++ b/prd.md
@@ -50,24 +50,12 @@ Naming style:
 
 Unicode should be handled properly.
 
-Use Cmake as the build system. You can see an exmaple cmake file at
+Use Cmake as the build system. Exmaple cmake file:
 https://github.com/MetroWind/planck-blog/blob/master/CMakeLists.txt.
 
-----
+The syntax tree should have a function to iterate over all nodes.
 
-Success Rate:               96.7%
-User Agreement:             100.0% (60 reviewed)
-Code Changes:               +1244 -136
-
-Performance
-Wall Time:                  38m 35s
-Agent Active:               32m 50s
-  » API Time:               10m 31s (32.1%)
-  » Tool Time:              22m 18s (67.9%)
-
-Model Usage                 Reqs   Input Tokens   Cache Reads  Output Tokens
-────────────────────────────────────────────────────────────────────────────
-gemini-2.5-flash-lite         17         28,986             0          9,364
-gemini-3-flash-preview        14        175,114       348,017          1,187
-gemini-3-pro-preview          55        268,873     3,224,960         37,756
-gemini-2.5-flash               4         17,697             0          2,957
+The library should expose an interface that allows the user to render
+a document with 2 steps: The first step will expose the syntax tree
+(the root node), and the second step will render the syntax tree into
+HTML.
diff --git a/tests/test_nodes.cpp b/tests/test_nodes.cpp
new file mode 100644
index 0000000..7745f9f
--- /dev/null
+++ b/tests/test_nodes.cpp
@@ -0,0 +1,39 @@
+#include <gtest/gtest.h>
+#include "nodes.h"
+#include <vector>
+#include <string>
+
+using namespace macrodown;
+
+TEST(NodeTest, ForEachIteration)
+{
+    // Create a tree:
+    // Group
+    //   Text("a")
+    //   Macro("m")
+    //     Group
+    //       Text("b")
+
+    Group root;
+    root.addChild(std::make_unique<Node>(Text{"a"}));
+    
+    Macro m;
+    m.name = "m";
+    Group arg_group;
+    arg_group.addChild(std::make_unique<Node>(Text{"b"}));
+    m.arguments.push_back(std::make_unique<Node>(std::move(arg_group)));
+    
+    root.addChild(std::make_unique<Node>(std::move(m)));
+    
+    Node root_node(std::move(root));
+    
+    std::vector<std::string> visited_types;
+    root_node.forEach([&](const Node& n) {
+        if (std::holds_alternative<Text>(n.data)) visited_types.push_back("Text");
+        else if (std::holds_alternative<Macro>(n.data)) visited_types.push_back("Macro");
+        else if (std::holds_alternative<Group>(n.data)) visited_types.push_back("Group");
+    });
+    
+    std::vector<std::string> expected = {"Group", "Text", "Macro", "Group", "Text"};
+    EXPECT_EQ(visited_types, expected);
+}