Changes
diff --git a/include/block.h b/include/block.h
index 12c249b..8fb9fd7 100644
--- a/include/block.h
+++ b/include/block.h
@@ -29,6 +29,11 @@ struct Block
std::string literal_content; // For leaf blocks
int level = 0; // For headings
bool open = true;
+
+ // For fenced code blocks
+ char fence_char = 0;
+ size_t fence_length = 0;
+ std::string info_string;
Block(BlockType t) : type(t) {}
};
diff --git a/src/block_parser.cpp b/src/block_parser.cpp
index 5af4746..f2c0916 100644
--- a/src/block_parser.cpp
+++ b/src/block_parser.cpp
@@ -122,6 +122,11 @@ bool BlockParser::matches(Block* block, const std::string& line, size_t& offset)
return true;
}
+ if(block->type == BlockType::FencedCode)
+ {
+ return true;
+ }
+
return false;
}
@@ -174,6 +179,76 @@ void BlockParser::process_line(const std::string& line)
Block* tip = open_blocks.back().block;
+ // Check if we are inside a FencedCode block
+ if(tip->type == BlockType::FencedCode)
+ {
+ size_t indent = count_indent(line, offset);
+ if(indent < 4)
+ {
+ size_t check_pos = offset + indent;
+ size_t fence_len = 0;
+ while(check_pos + fence_len < line.size() && line[check_pos + fence_len] == tip->fence_char) fence_len++;
+ if(fence_len >= tip->fence_length)
+ {
+ // Verify no trailing characters other than space
+ size_t trail = check_pos + fence_len;
+ while(trail < line.size() && line[trail] == ' ') trail++;
+ if(trail == line.size())
+ {
+ // Closing fence
+ close_unmatched_blocks(open_blocks.size() - 2);
+ return;
+ }
+ }
+ }
+ // Add line to content
+ if(!tip->literal_content.empty()) tip->literal_content += "\n";
+ tip->literal_content += line.substr(offset);
+ return;
+ }
+
+ // Check for FencedCode Opening
+ size_t indent = count_indent(line, offset);
+ if(indent < 4)
+ {
+ size_t check_pos = offset + indent;
+ if(check_pos < line.size() && (line[check_pos] == '`' || line[check_pos] == '~'))
+ {
+ char fence_char = line[check_pos];
+ size_t fence_len = 0;
+ while(check_pos + fence_len < line.size() && line[check_pos + fence_len] == fence_char) fence_len++;
+
+ if(fence_len >= 3)
+ {
+ size_t info_start = check_pos + fence_len;
+ while(info_start < line.size() && line[info_start] == ' ') info_start++;
+ std::string info_string = line.substr(info_start);
+
+ bool valid = true;
+ if(fence_char == '`' && info_string.find('`') != std::string::npos) valid = false;
+
+ if(valid)
+ {
+ if(tip->type == BlockType::Paragraph)
+ {
+ close_unmatched_blocks(open_blocks.size() - 2);
+ tip = open_blocks.back().block;
+ }
+
+ auto code_block = std::make_unique<Block>(BlockType::FencedCode);
+ code_block->fence_char = fence_char;
+ code_block->fence_length = fence_len;
+ code_block->info_string = info_string;
+
+ Block* ptr = code_block.get();
+ tip->children.push_back(std::move(code_block));
+ open_blocks.push_back({ptr});
+ return;
+ }
+ }
+ }
+ }
+
// If tip is a Paragraph, check for blank line (closes it)
if(tip->type == BlockType::Paragraph)
{
@@ -189,7 +264,7 @@ void BlockParser::process_line(const std::string& line)
}
// Check for ATX Heading
- size_t indent = count_indent(line, offset);
+ indent = count_indent(line, offset);
if(indent < 4)
{
size_t check_pos = offset + indent;
diff --git a/src/converter.cpp b/src/converter.cpp
index 85e4c2b..3c208be 100644
--- a/src/converter.cpp
+++ b/src/converter.cpp
@@ -54,6 +54,9 @@ std::unique_ptr<Node> Converter::convert_block(
case BlockType::Quote:
macro_name = "quote";
break;
+ case BlockType::FencedCode:
+ macro_name = "fenced_code";
+ break;
default:
return nullptr; // Ignore unknown blocks for now
}
@@ -61,8 +64,19 @@ std::unique_ptr<Node> Converter::convert_block(
Macro macro;
macro.name = macro_name;
- // Handle Content
- if(block->children.empty())
+ if(block->type == BlockType::FencedCode)
+ {
+ // Arg 1: Info string
+ Group info_group;
+ info_group.addChild(std::make_unique<Node>(Text{block->info_string}));
+ macro.arguments.push_back(std::make_unique<Node>(std::move(info_group)));
+
+ // Arg 2: Content (unparsed)
+ Group content_group;
+ content_group.addChild(std::make_unique<Node>(Text{block->literal_content}));
+ macro.arguments.push_back(std::make_unique<Node>(std::move(content_group)));
+ }
+ else if(block->children.empty())
{
// Leaf block: Parse literal content
Group group;
diff --git a/src/standard_library.cpp b/src/standard_library.cpp
index 92e84fb..1760af4 100644
--- a/src/standard_library.cpp
+++ b/src/standard_library.cpp
@@ -28,6 +28,18 @@ void StandardLibrary::registerMacros(Evaluator& evaluator)
evaluator.define("h5", {"content"}, "<h5>%content</h5>\n");
evaluator.define("h6", {"content"}, "<h6>%content</h6>\n");
+ evaluator.defineIntrinsic("fenced_code", [](const std::vector<std::string>& args) -> std::string
+ {
+ if(args.size() < 2) return "";
+ std::string info = args[0];
+ std::string content = args[1];
+ if(!info.empty())
+ {
+ return "<pre><code class=\"language-" + info + "\">" + content + "\n</code></pre>\n";
+ }
+ return "<pre><code>" + content + "\n</code></pre>\n";
+ });
+
// Inline
evaluator.define("em", {"content"}, "<em>%content</em>");
evaluator.define("strong", {"content"}, "<strong>%content</strong>");
diff --git a/tests/test_block_parser.cpp b/tests/test_block_parser.cpp
index 91cf324..75b9769 100644
--- a/tests/test_block_parser.cpp
+++ b/tests/test_block_parser.cpp
@@ -80,4 +80,34 @@ TEST(BlockParserTest, NestedQuote)
ASSERT_EQ(q2->children.size(), 1);
EXPECT_EQ(q2->children[0]->literal_content, "Level 2");
+}
+
+TEST(BlockParserTest, FencedCode)
+{
+ std::string input = "```cpp\nint main() {\n return 0;\n}\n```";
+ auto root = BlockParser::parse(input);
+
+ ASSERT_EQ(root->children.size(), 1);
+ auto* code = root->children[0].get();
+
+ EXPECT_EQ(code->type, BlockType::FencedCode);
+ EXPECT_EQ(code->fence_char, '`');
+ EXPECT_EQ(code->fence_length, 3);
+ EXPECT_EQ(code->info_string, "cpp");
+ EXPECT_EQ(code->literal_content, "int main() {\n return 0;\n}");
+}
+
+TEST(BlockParserTest, FencedCodeWithTildes)
+{
+ std::string input = "~~~ \ncode block\n~~~";
+ auto root = BlockParser::parse(input);
+
+ ASSERT_EQ(root->children.size(), 1);
+ auto* code = root->children[0].get();
+
+ EXPECT_EQ(code->type, BlockType::FencedCode);
+ EXPECT_EQ(code->fence_char, '~');
+ EXPECT_EQ(code->fence_length, 3);
+ EXPECT_EQ(code->info_string, "");
+ EXPECT_EQ(code->literal_content, "code block");
}
\ No newline at end of file
diff --git a/tests/test_macrodown.cpp b/tests/test_macrodown.cpp
index 59f091f..64d7c66 100644
--- a/tests/test_macrodown.cpp
+++ b/tests/test_macrodown.cpp
@@ -69,3 +69,19 @@ TEST(MacroDownTest, InlineDefinition)
std::string expected = "<p>This is <b>important</b>.</p>\n";
EXPECT_EQ(md.render(*md.parse(input)), expected);
}
+
+TEST(MacroDownTest, FencedCode)
+{
+ MacroDown md;
+ std::string input = "```cpp\nint main() {\n return 0;\n}\n```";
+ std::string expected = "<pre><code class=\"language-cpp\">int main() {\n return 0;\n}\n</code></pre>\n";
+ EXPECT_EQ(md.render(*md.parse(input)), expected);
+}
+
+TEST(MacroDownTest, FencedCodeNoLanguage)
+{
+ MacroDown md;
+ std::string input = "~~~\nplain text\n~~~";
+ std::string expected = "<pre><code>plain text\n</code></pre>\n";
+ EXPECT_EQ(md.render(*md.parse(input)), expected);
+}