BareGit

Implement fenced code blocks and add corresponding tests

Author: MetroWind <chris.corsair@gmail.com>
Date: Fri Mar 13 21:20:11 2026 -0700
Commit: bc68afbf0e60f0358cf5f2a13f2c48e0fdda1210

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