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>");
+}