BareGit

Implement full integration pipeline, Standard Library, and finalize HTML output

Author: MetroWind <chris.corsair@gmail.com>
Date: Sat Jan 10 12:47:36 2026 -0800
Commit: 996a74c3567d6f11cfd79b8ca871b624d8f6853a

Changes

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2f4e86f..924a0b1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -37,7 +37,7 @@ include_directories(include)
 # but for now we will just list them manually or add a library target.
 # Let's create a core library for the logic to share between main and tests.
 
-add_library(macrodown_lib STATIC src/lib_placeholder.cpp src/macro_engine.cpp src/parser.cpp src/block_parser.cpp)
+add_library(macrodown_lib STATIC src/lib_placeholder.cpp src/macro_engine.cpp src/parser.cpp src/block_parser.cpp src/converter.cpp src/standard_library.cpp)
 target_include_directories(macrodown_lib PUBLIC include)
 target_link_libraries(macrodown_lib PUBLIC uni-algo::uni-algo)
 
@@ -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)
+add_executable(macrodown_test tests/test_main.cpp tests/test_macro_engine.cpp tests/test_block_parser.cpp tests/test_integration.cpp)
 target_link_libraries(macrodown_test PRIVATE macrodown_lib GTest::gtest_main)
 target_include_directories(macrodown_test PRIVATE include)
 
diff --git a/include/converter.h b/include/converter.h
new file mode 100644
index 0000000..5ff3d48
--- /dev/null
+++ b/include/converter.h
@@ -0,0 +1,22 @@
+#ifndef MACRODOWN_CONVERTER_H
+#define MACRODOWN_CONVERTER_H
+
+#include <vector>
+#include <memory>
+#include "block.h"
+#include "nodes.h"
+
+namespace macrodown {
+
+class Converter {
+public:
+    // Converts a Block tree into a list of AST Nodes (Macros)
+    static std::vector<std::unique_ptr<Node>> convert(const Block* root);
+
+private:
+    static std::unique_ptr<Node> convert_block(const Block* block);
+};
+
+} // namespace macrodown
+
+#endif // MACRODOWN_CONVERTER_H
diff --git a/include/standard_library.h b/include/standard_library.h
new file mode 100644
index 0000000..134785f
--- /dev/null
+++ b/include/standard_library.h
@@ -0,0 +1,15 @@
+#ifndef MACRODOWN_STANDARD_LIBRARY_H
+#define MACRODOWN_STANDARD_LIBRARY_H
+
+#include "macro_engine.h"
+
+namespace macrodown {
+
+class StandardLibrary {
+public:
+    static void register_macros(Evaluator& evaluator);
+};
+
+} // namespace macrodown
+
+#endif // MACRODOWN_STANDARD_LIBRARY_H
diff --git a/src/converter.cpp b/src/converter.cpp
new file mode 100644
index 0000000..0500257
--- /dev/null
+++ b/src/converter.cpp
@@ -0,0 +1,73 @@
+#include "converter.h"
+#include "parser.h"
+#include <iostream>
+
+namespace macrodown {
+
+std::vector<std::unique_ptr<Node>> Converter::convert(const Block* root) {
+    std::vector<std::unique_ptr<Node>> nodes;
+    
+    if (root->type == BlockType::Document) {
+        for (const auto& child : root->children) {
+            auto node = convert_block(child.get());
+            if (node) {
+                nodes.push_back(std::move(node));
+            }
+        }
+    } else {
+        // Should not happen if root is Document, but handle single block
+        auto node = convert_block(root);
+        if (node) nodes.push_back(std::move(node));
+    }
+    
+    return nodes;
+}
+
+std::unique_ptr<Node> Converter::convert_block(const Block* block) {
+    if (!block) return nullptr;
+
+    std::string macro_name;
+    std::vector<std::unique_ptr<Node>> args;
+
+    switch (block->type) {
+        case BlockType::Paragraph:
+            macro_name = "p";
+            break;
+        case BlockType::Heading:
+            macro_name = "h" + std::to_string(block->level);
+            break;
+        case BlockType::Quote:
+            macro_name = "quote";
+            break;
+        default:
+            return nullptr; // Ignore unknown blocks for now
+    }
+
+    auto macro = std::make_unique<MacroNode>(macro_name);
+
+    // Handle Content
+    if (block->children.empty()) {
+        // Leaf block: Parse literal content
+        // Wrap result in GroupNode
+        auto group = std::make_unique<GroupNode>();
+        auto inline_nodes = Parser::parse(block->literal_content);
+        for (auto& n : inline_nodes) {
+            group->addChild(std::move(n));
+        }
+        macro->arguments.push_back(std::move(group));
+    } else {
+        // Container block: Recursively convert children
+        auto group = std::make_unique<GroupNode>();
+        for (const auto& child : block->children) {
+            auto child_node = convert_block(child.get());
+            if (child_node) {
+                group->addChild(std::move(child_node));
+            }
+        }
+        macro->arguments.push_back(std::move(group));
+    }
+
+    return macro;
+}
+
+} // namespace macrodown
diff --git a/src/main.cpp b/src/main.cpp
index 368f2b8..0b33586 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,6 +1,46 @@
 #include <iostream>
+#include <fstream>
+#include <sstream>
+#include "block_parser.h"
+#include "converter.h"
+#include "macro_engine.h"
+#include "standard_library.h"
+
+using namespace macrodown;
+
+int main(int argc, char** argv) {
+    std::string input;
+    
+    if (argc > 1) {
+        std::ifstream file(argv[1]);
+        if (!file) {
+            std::cerr << "Error: Could not open file " << argv[1] << std::endl;
+            return 1;
+        }
+        std::stringstream buffer;
+        buffer << file.rdbuf();
+        input = buffer.str();
+    } else {
+        // Read from stdin
+        std::stringstream buffer;
+        buffer << std::cin.rdbuf();
+        input = buffer.str();
+    }
+
+    // 1. Setup Environment
+    Evaluator evaluator;
+    StandardLibrary::register_macros(evaluator);
+
+    // 2. Parse Blocks
+    auto root = BlockParser::parse(input);
+
+    // 3. Convert to Macro AST
+    auto macro_nodes = Converter::convert(root.get());
+
+    // 4. Evaluate and Output
+    for (const auto& node : macro_nodes) {
+        std::cout << evaluator.evaluate(*node);
+    }
 
-int main() {
-    std::cout << "MacroDown: A C++ Macro-Markdown Processor" << std::endl;
     return 0;
 }
diff --git a/src/parser.cpp b/src/parser.cpp
index 5379537..2b33ea3 100644
--- a/src/parser.cpp
+++ b/src/parser.cpp
@@ -1,28 +1,35 @@
 #include "parser.h"
 #include <iostream>
-// We will use uni-algo later for full UTF-8, for now strict byte scanning for ASCII delimiters
-// is sufficient for this skeleton as per design doc 3.3
 
 namespace macrodown {
 
 std::vector<std::unique_ptr<Node>> Parser::parse(const std::string& input) {
     std::vector<std::unique_ptr<Node>> nodes;
     std::string current_text;
+
+    auto flush_text = [&]() {
+        if (!current_text.empty()) {
+            nodes.push_back(std::make_unique<TextNode>(current_text));
+            current_text.clear();
+        }
+    };
     
     size_t i = 0;
     while (i < input.length()) {
-        if (input[i] == '%') {
-            // Potential macro start
-            // If we have accumulated text, push it
-            if (!current_text.empty()) {
-                nodes.push_back(std::make_unique<TextNode>(current_text));
-                current_text.clear();
-            }
+        char c = input[i];
+        
+        // Escape handling
+        if (c == '\\' && i + 1 < input.length()) {
+            // Append the escaped character
+            current_text += input[i+1];
+            i += 2;
+            continue;
+        }
 
-            // Check if escaped "\%" (This logic should really be before checking %)
-            // But let's assume % is the trigger.
+        // Macro: %name{args}...
+        if (c == '%') {
+            flush_text();
             
-            // Parse Macro Name
             i++; // skip %
             size_t name_start = i;
             while (i < input.length() && (isalnum(input[i]) || input[i] == '_')) {
@@ -31,24 +38,18 @@ std::vector<std::unique_ptr<Node>> Parser::parse(const std::string& input) {
             std::string name = input.substr(name_start, i - name_start);
             
             if (name.empty()) {
-                // Just a standalone %, treat as text
                 current_text += "%";
                 continue;
             }
 
             auto macro = std::make_unique<MacroNode>(name);
 
-            // Parse Arguments: [opt] or {arg}
-            // We treat [...] and {...} as arguments.
+            // Parse Arguments
             while (i < input.length()) {
                 char open = input[i];
                 if (open == '{' || open == '[') {
                     char close = (open == '{') ? '}' : ']';
-                    i++; // skip open
-                    
-                    // Parse argument content recursively? 
-                    // For the "Parser::parse" logic, arguments are just text/nodes.
-                    // But we need to find the matching closing brace balancing nesting.
+                    i++; 
                     
                     std::string arg_content;
                     int balance = 1;
@@ -65,33 +66,121 @@ std::vector<std::unique_ptr<Node>> Parser::parse(const std::string& input) {
                         i++;
                     }
                     
-                    // Now parse the argument content recursively
                     auto group = std::make_unique<GroupNode>();
-                    // Recursively parse the content of the argument
                     std::vector<std::unique_ptr<Node>> sub_nodes = parse(arg_content);
                     for (auto& n : sub_nodes) {
                         group->addChild(std::move(n));
                     }
-                    
                     macro->arguments.push_back(std::move(group));
                 } else {
                     break; 
                 }
             }
-            
             nodes.push_back(std::move(macro));
+            continue;
+        }
+        
+        // Inline Code: `...`
+        if (c == '`') {
+            size_t start = i + 1;
+            size_t end = input.find('`', start);
+            if (end != std::string::npos) {
+                flush_text();
+                std::string content = input.substr(start, end - start);
+                auto macro = std::make_unique<MacroNode>("code");
+                auto group = std::make_unique<GroupNode>();
+                group->addChild(std::make_unique<TextNode>(content));
+                macro->arguments.push_back(std::move(group));
+                nodes.push_back(std::move(macro));
+                i = end + 1;
+                continue;
+            }
+        }
+        
+        // Link: [text](url)
+        if (c == '[') {
+            // Find matching ]
+            // Simplified: doesn't handle nested brackets well yet
+            size_t label_start = i + 1;
+            // We need a helper for balanced search, but for now simple find
+            size_t j = label_start;
+            int bracket_bal = 1;
+            while(j < input.length() && bracket_bal > 0) {
+                if (input[j] == '[') bracket_bal++;
+                else if (input[j] == ']') bracket_bal--;
+                if (bracket_bal > 0) j++;
+            }
             
-        } else {
-            current_text += input[i];
-            i++;
+            if (j < input.length() && bracket_bal == 0) {
+                size_t close_bracket = j;
+                // Check for (url)
+                if (close_bracket + 1 < input.length() && input[close_bracket + 1] == '(') {
+                    size_t url_start = close_bracket + 2;
+                    size_t url_end = input.find(')', url_start);
+                    if (url_end != std::string::npos) {
+                        flush_text();
+                        std::string label = input.substr(label_start, close_bracket - label_start);
+                        std::string url = input.substr(url_start, url_end - url_start);
+                        
+                        auto macro = std::make_unique<MacroNode>("link");
+                        
+                        // Arg 1: URL
+                        auto group1 = std::make_unique<GroupNode>();
+                        group1->addChild(std::make_unique<TextNode>(url));
+                        macro->arguments.push_back(std::move(group1));
+                        
+                        // Arg 2: Text (parsed)
+                        auto group2 = std::make_unique<GroupNode>();
+                        auto sub = parse(label);
+                        for(auto& n : sub) group2->addChild(std::move(n));
+                        macro->arguments.push_back(std::move(group2));
+                        
+                        nodes.push_back(std::move(macro));
+                        i = url_end + 1;
+                        continue;
+                    }
+                }
+            }
         }
+        
+        // Emphasis: * or **
+        // Simplified: Greedy scan for matching *
+        if (c == '*') {
+            bool strong = (i + 1 < input.length() && input[i+1] == '*');
+            size_t start_content = i + (strong ? 2 : 1);
+            
+            // Find delimiter
+            std::string delim = strong ? "**" : "*";
+            size_t end = input.find(delim, start_content);
+            
+            // Handle edge case: *foo **bar*** -> *foo *bar**
+            // Simplified: just find first match. 
+            // This is NOT correct CommonMark but sufficient for prototype.
+            
+            if (end != std::string::npos) {
+                flush_text();
+                std::string content = input.substr(start_content, end - start_content);
+                auto macro = std::make_unique<MacroNode>(strong ? "strong" : "em");
+                auto group = std::make_unique<GroupNode>();
+                auto sub = parse(content);
+                for(auto& n : sub) group->addChild(std::move(n));
+                macro->arguments.push_back(std::move(group));
+                
+                nodes.push_back(std::move(macro));
+                i = end + delim.length();
+                continue;
+            }
+        }
+
+        current_text += c;
+        i++;
     }
     
-    if (!current_text.empty()) {
-        nodes.push_back(std::make_unique<TextNode>(current_text));
-    }
-    
+    flush_text();
     return nodes;
 }
 
-} // namespace macrodown
+// Helper stub for compilation (not actually used since we inlined logic, but logic relied on start_pos_of_link_end which I removed)
+// I removed start_pos_of_link_end call in logic.
+
+} // namespace macrodown
\ No newline at end of file
diff --git a/src/standard_library.cpp b/src/standard_library.cpp
new file mode 100644
index 0000000..2fac350
--- /dev/null
+++ b/src/standard_library.cpp
@@ -0,0 +1,36 @@
+#include "standard_library.h"
+
+namespace macrodown {
+
+void StandardLibrary::register_macros(Evaluator& evaluator) {
+    // Blocks
+    evaluator.defineIntrinsic("p", [](const std::vector<std::string>& args) -> std::string {
+        if (args.empty()) return "";
+        std::string content = args[0];
+        // Check if content is empty or just whitespace
+        if (content.find_first_not_of(" \t\n\r") == std::string::npos) {
+            return "";
+        }
+        return "<p>" + content + "</p>\n";
+    });
+
+    evaluator.define("quote", {"content"}, "<blockquote>\n%content</blockquote>\n");
+    
+    // Headings
+    evaluator.define("h1", {"content"}, "<h1>%content</h1>\n");
+    evaluator.define("h2", {"content"}, "<h2>%content</h2>\n");
+    evaluator.define("h3", {"content"}, "<h3>%content</h3>\n");
+    evaluator.define("h4", {"content"}, "<h4>%content</h4>\n");
+    evaluator.define("h5", {"content"}, "<h5>%content</h5>\n");
+    evaluator.define("h6", {"content"}, "<h6>%content</h6>\n");
+    
+    // Inline
+    evaluator.define("em", {"content"}, "<em>%content</em>");
+    evaluator.define("strong", {"content"}, "<strong>%content</strong>");
+    evaluator.define("code", {"content"}, "<code>%content</code>");
+    evaluator.define("link", {"url", "text"}, "<a href=\"%url\">%text</a>");
+    evaluator.define("img", {"url", "alt"}, "<img src=\"%url\" alt=\"%alt\" />");
+}
+
+} // namespace macrodown
+
diff --git a/tests/test_integration.cpp b/tests/test_integration.cpp
new file mode 100644
index 0000000..a80dc20
--- /dev/null
+++ b/tests/test_integration.cpp
@@ -0,0 +1,96 @@
+#include <gtest/gtest.h>
+#include "block_parser.h"
+#include "converter.h"
+#include "macro_engine.h"
+#include <iostream>
+
+using namespace macrodown;
+
+class IntegrationTest : public ::testing::Test {
+protected:
+    Evaluator evaluator;
+
+    void SetUp() override {
+        // Define standard library macros for testing
+        evaluator.define("p", {"t"}, "<p>%t</p>");
+        evaluator.define("h1", {"t"}, "<h1>%t</h1>");
+        evaluator.define("em", {"t"}, "<em>%t</em>");
+        evaluator.define("strong", {"t"}, "<strong>%t</strong>");
+        evaluator.define("code", {"t"}, "<code>%t</code>");
+        evaluator.define("link", {"url", "text"}, "<a href=\"%url\">%text</a>");
+        evaluator.define("quote", {"t"}, "<blockquote>%t</blockquote>");
+    }
+
+    std::string process(const std::string& input) {
+        auto root = BlockParser::parse(input);
+        auto nodes = Converter::convert(root.get());
+        std::string result;
+        for (const auto& node : nodes) {
+            result += evaluator.evaluate(*node);
+        }
+        return result;
+    }
+};
+
+TEST_F(IntegrationTest, BasicParagraph) {
+    EXPECT_EQ(process("Hello World"), "<p>Hello World</p>");
+}
+
+TEST_F(IntegrationTest, Heading) {
+    EXPECT_EQ(process("# Title"), "<h1>Title</h1>");
+}
+
+TEST_F(IntegrationTest, Emphasis) {
+    EXPECT_EQ(process("Hello *World*"), "<p>Hello <em>World</em></p>");
+}
+
+TEST_F(IntegrationTest, Strong) {
+    EXPECT_EQ(process("**Bold**"), "<p><strong>Bold</strong></p>");
+}
+
+TEST_F(IntegrationTest, Link) {
+    EXPECT_EQ(process("[Click](http://example.com)"), "<p><a href=\"http://example.com\">Click</a></p>");
+}
+
+TEST_F(IntegrationTest, Code) {
+    EXPECT_EQ(process("Use `printf`"), "<p>Use <code>printf</code></p>");
+}
+
+TEST_F(IntegrationTest, MacroInMarkdown) {
+    // Define custom macro
+    evaluator.define("greet", {"name"}, "Hello, %name!");
+    
+    // Use it in Markdown
+    EXPECT_EQ(process("Say %greet{User}"), "<p>Say Hello, User!</p>");
+}
+
+TEST_F(IntegrationTest, Mixed) {
+    std::string input = "# Header\n\nParagraph with *em* and %code{macros}.";
+    std::string expected = "<h1>Header</h1><p>Paragraph with <em>em</em> and <code>macros</code>.</p>";
+    EXPECT_EQ(process(input), expected);
+}
+
+TEST_F(IntegrationTest, BlockQuote) {
+    std::string input = "> Hello\n> World";
+    // Quote contains Paragraph.
+    // Converter: Quote -> %quote{ content }.
+    // Content of Quote is Block (Paragraph).
+    // Paragraph -> %p{...}.
+    // So %quote{ %p{...} }.
+    // Evaluator: %quote expands to <blockquote>%t</blockquote>.
+    // %t is result of expanding arg.
+    // Arg is %p... -> <p>...</p>.
+    // Result: <blockquote><p>...</p></blockquote>.
+    
+    // Note: My Converter for Quote:
+    // macro "quote".
+    // Args: GroupNode of children.
+    // Children: P -> %p.
+    
+    // BUT: My generic "quote" definition takes "t".
+    // Does it capture all children?
+    // Converter produces: MacroNode("quote", args=[GroupNode(child1, child2...)])
+    // So it has 1 argument. Correct.
+    
+    EXPECT_EQ(process(input), "<blockquote><p>Hello\nWorld</p></blockquote>");
+}