BareGit

Implement the rest of markups. Now it should have all CommonMark markups.

Author: MetroWind <chris.corsair@gmail.com>
Date: Sat Mar 21 22:05:28 2026 -0700
Commit: 9bcd984fe0406d5708d848b698c656d556fbceae

Changes

diff --git a/.clang-format b/.clang-format
index 28faefc..0a0b535 100644
--- a/.clang-format
+++ b/.clang-format
@@ -7,7 +7,7 @@ ColumnLimit: 80
 BreakBeforeBraces: Allman
 AllowShortFunctionsOnASingleLine: Empty
 AllowShortBlocksOnASingleLine: Empty
-AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortIfStatementsOnASingleLine: true
 IndentCaseLabels: false
 SpaceBeforeParens: Never
 PointerAlignment: Left
diff --git a/design.md b/designs/design-0-initial.md
similarity index 100%
rename from design.md
rename to designs/design-0-initial.md
diff --git a/designs/design-1-commonmark.md b/designs/design-1-commonmark.md
new file mode 100644
index 0000000..88313c5
--- /dev/null
+++ b/designs/design-1-commonmark.md
@@ -0,0 +1,123 @@
+# CommonMark Implementation Design
+
+## 1. Overview
+This design document details the technical strategy for implementing the remaining core CommonMark features in the MacroDown library. Based on the CommonMark specification and the requirements listed in the CommonMark Help page, the currently missing features are **Lists (Ordered and Unordered)**, **Thematic Breaks (Horizontal Rules)**, and **Indented Code Blocks**. 
+
+This document provides step-by-step guidance on how to extend the existing parsing pipeline (`BlockParser`, `Converter`, and `StandardLibrary`) to fully support these constructs. The focus is strictly on extending the two-phase parsing strategy (Block Phase followed by Inline Phase/Macro Conversion) while adhering to the extreme detail requirements for inexperienced implementers.
+
+## 2. Lists (Unordered and Ordered)
+
+Lists in CommonMark are container blocks that contain List Item blocks. A list is formed when consecutive list items of the same type (unordered vs ordered) are parsed.
+
+### 2.1 Extending the `Block` Data Structure
+To represent the specific properties of a list (whether it's ordered or unordered, and the starting number for ordered lists), we need to add fields to the `Block` struct located in `include/block.h`.
+
+**Changes to `struct Block`:**
+```cpp
+    // Add to include/block.h inside struct Block:
+    char list_char = 0;      // Marker character: '-', '+', '*', '.' or ')'
+    int list_start = 1;      // Starting number for ordered lists (default 1)
+    bool is_ordered = false; // True if it's an ordered list (1., 2.), false for unordered (*, -, +)
+    size_t indent = 0;       // Indentation level required for child content
+```
+*Explanation*: 
+- `list_char`: Records the exact character used to start the list. For unordered lists, it is the bullet (`*`, `-`, `+`). For ordered lists, it is the punctuation mark after the number (`.` or `)`).
+- `list_start`: CommonMark allows ordered lists to start at an arbitrary number (e.g., `2. Item`). We need to store this so the HTML `<ol start="2">` can be rendered.
+- `indent`: List items define an indentation context for their children. If a list item starts with `-   ` (dash and 3 spaces), any child blocks (like nested paragraphs) must be indented by 4 spaces.
+
+### 2.2 Extending `BlockParser::matches`
+The `matches` method is responsible for determining if an open block in our stack can accept the current line.
+
+**Logic for `BlockType::List` and `BlockType::ListItem`:**
+- A `List` block always matches as long as it has at least one open `ListItem` child.
+- A `ListItem` block matches if the current line's indentation is strictly greater than or equal to the `ListItem`'s base `indent`. If it matches, we "consume" that indentation by incrementing the `offset`. If the line is completely blank, it also matches an open `ListItem` (as list items can contain blank lines).
+
+### 2.3 Extending `BlockParser::process_line`
+When a line doesn't match existing blocks, we check if it opens a new block. We need to add logic to detect List Items.
+
+**Step-by-step algorithm to detect a List Item:**
+1. Determine the current line's indentation. If it's 4 or more spaces, it might be an indented code block, not a new list item (unless we are already inside a list).
+2. Look for an unordered list marker: a single `*`, `-`, or `+` followed by a space (or the end of the line).
+3. Look for an ordered list marker: a sequence of 1 to 9 digits, followed by `.` or `)`, followed by a space (or the end of the line).
+4. **Calculations**:
+    - Let `marker_length` be the length of the matched marker (e.g., 2 for `- `, 3 for `1. `).
+    - Determine the number of spaces following the marker. Let this be `spaces`. If `spaces` > 4, CommonMark says it should be treated as 1 space.
+    - The new block's `indent` equals the line's starting indentation + `marker_length` + `spaces`.
+5. **List vs ListItem Creation**:
+    - If the current open block (the "tip") is NOT a `List`, OR it is a `List` but the marker type (`list_char` or `is_ordered`) differs from the tip, we must create a new `List` container block first.
+    - Append the new `List` to the current tip, then open it.
+    - Then, create a `ListItem` block, set its `indent`, `list_char`, and `is_ordered` properties, append it to the `List`, and open it.
+
+### 2.4 Converter and Standard Library
+The `Converter` maps AST blocks to Macro AST nodes.
+1. **Converter (`src/converter.cpp`)**:
+    - In `convert_block`, when `block->type == BlockType::List`:
+      - Determine the macro name: `"ol"` if `block->is_ordered`, otherwise `"ul"`.
+      - Convert all child `ListItem` blocks and pack them into a `Group` node as the single argument to the macro.
+    - When `block->type == BlockType::ListItem`:
+      - The macro name is `"li"`.
+      - Convert its children and pack them into the argument.
+2. **Standard Library (`src/standard_library.cpp`)**:
+    - Define `%ul{content}`: `<ul
+>%content</ul>
+`
+    - Define `%ol{content}`: `<ol
+>%content</ol>
+` *(Note: To support `start`, you might need to change `Converter` to pass `start` as a first argument to `ol`, but for simplicity, a basic `%ol` is sufficient for standard unordered lists, or we can define intrinsic `%ol` that takes start number if we want 100% compliance).*
+    - Define `%li{content}`: `<li>
+%content</li>
+`
+
+## 3. Thematic Breaks (Horizontal Rules)
+
+A thematic break consists of three or more matching characters (`-`, `_`, or `*`) with optional spaces between them.
+
+### 3.1 BlockParser Logic
+In `BlockParser::process_line`, before checking for paragraphs or headings, we check for a thematic break.
+1. Check the line's initial indentation. If it's >= 4 spaces, it cannot be a thematic break.
+2. Scan the line starting from the non-space character.
+3. If the character is `-`, `_`, or `*`, count how many times it appears.
+4. Allow spaces between the characters. If any other character is encountered, it is NOT a thematic break.
+5. If the count of the marker character is >= 3, and the rest of the line contains only spaces, it IS a thematic break.
+6. **Action**: Close the current paragraph (if open). Create a new `Block` of type `BlockType::ThematicBreak`, add it to the parent, and immediately close it (set `open = false` since it cannot contain children or multiline content).
+
+### 3.2 Converter and Standard Library
+1. **Converter**: 
+   - When `block->type == BlockType::ThematicBreak`: Macro name is `"hr"`. It takes no arguments.
+2. **Standard Library**:
+   - Define `%hr{}`: `<hr />
+`
+
+## 4. Indented Code Blocks
+
+Indented code blocks are triggered by lines indented by 4 or more spaces.
+
+### 4.1 BlockParser Logic
+In `BlockParser::process_line`:
+1. Check if the line has an indentation of 4 or more spaces.
+2. Ensure the current tip is NOT a paragraph. (An indented line cannot interrupt a paragraph in CommonMark).
+3. If not in a paragraph, create a new `BlockType::IndentedCode` block.
+4. The literal content of this block is the line's content *after* stripping exactly 4 spaces.
+5. In `matches`: 
+   - An `IndentedCode` block matches if the line has >= 4 spaces. We consume exactly 4 spaces and add the rest to `literal_content` (with a newline).
+   - It also matches blank lines (which are added as empty lines to the code block), provided a non-blank indented line follows eventually. (For simplicity, CommonMark allows trailing blank lines to be stripped later).
+
+### 4.2 Converter and Standard Library
+1. **Converter**: 
+   - When `block->type == BlockType::IndentedCode`: Map it to the `"fenced_code"` macro or a dedicated `"code_block"` macro. Since `fenced_code` already exists and takes two arguments (info string and content), we can reuse it by passing an empty string `""` as the first argument, and `literal_content` as the second argument.
+2. **Standard Library**: 
+   - The intrinsic `fenced_code` macro already handles an empty info string correctly by emitting `<pre><code>%content
+</code></pre>`.
+
+## 5. Summary of Implementation Steps
+
+To implement these features successfully, an engineer should follow this exact sequence:
+1. Modify `include/block.h` to add `list_char`, `is_ordered`, and `indent` to `struct Block`.
+2. Update `src/block_parser.cpp` -> `matches()` to correctly handle `List` and `ListItem` continuation.
+3. Update `src/block_parser.cpp` -> `process_line()` to parse:
+   - Thematic Breaks (`---`, `***`, `___`).
+   - Indented Code Blocks (4+ spaces).
+   - List markers for Unordered and Ordered lists.
+4. Update `src/converter.cpp` -> `convert_block()` to support `BlockType::List`, `BlockType::ListItem`, `BlockType::ThematicBreak`, and `BlockType::IndentedCode`. Map them to their respective macros (`ul`, `ol`, `li`, `hr`, and `fenced_code`).
+5. Update `src/standard_library.cpp` -> `registerMacros()` to add definitions for `ul`, `ol`, `li`, and `hr`.
+6. Add comprehensive unit tests in `tests/test_block_parser.cpp` to verify parsing accuracy against standard CommonMark list, hr, and indented code snippets.
\ No newline at end of file
diff --git a/include/block.h b/include/block.h
index 8fb9fd7..95e4927 100644
--- a/include/block.h
+++ b/include/block.h
@@ -1,9 +1,9 @@
 #ifndef MACRODOWN_BLOCK_H
 #define MACRODOWN_BLOCK_H
 
+#include <memory>
 #include <string>
 #include <vector>
-#include <memory>
 
 namespace macrodown
 {
@@ -26,10 +26,17 @@ struct Block
 {
     BlockType type;
     std::vector<std::unique_ptr<Block>> children; // For container blocks
-    std::string literal_content; // For leaf blocks
-    int level = 0; // For headings
+    std::string literal_content;                  // For leaf blocks
+    int level = 0;                                // For headings
     bool open = true;
-    
+
+    // For lists
+    char list_char = 0;      // Marker character: '-', '+', '*', '.' or ')'
+    int list_start = 1;      // Starting number for ordered lists (default 1)
+    bool is_ordered = false; // True if it's an ordered list (1., 2.), false for
+                             // unordered (*, -, +)
+    size_t indent = 0;       // Indentation level required for child content
+
     // For fenced code blocks
     char fence_char = 0;
     size_t fence_length = 0;
diff --git a/include/block_parser.h b/include/block_parser.h
index 779c2f1..1c0cae6 100644
--- a/include/block_parser.h
+++ b/include/block_parser.h
@@ -1,9 +1,10 @@
 #ifndef MACRODOWN_BLOCK_PARSER_H
 #define MACRODOWN_BLOCK_PARSER_H
 
+#include <memory>
 #include <string>
 #include <vector>
-#include <memory>
+
 #include "block.h"
 
 namespace macrodown
@@ -29,7 +30,7 @@ private:
     void process_line(const std::string& line);
     void close_unmatched_blocks(size_t last_matched_index);
     void add_text_to_current(const std::string& text);
-    
+
     // Checkers
     bool is_container(BlockType type);
     bool matches(Block* block, const std::string& line, size_t& offset);
diff --git a/include/converter.h b/include/converter.h
index b0282c2..b6cb093 100644
--- a/include/converter.h
+++ b/include/converter.h
@@ -1,11 +1,12 @@
 #ifndef MACRODOWN_CONVERTER_H
 #define MACRODOWN_CONVERTER_H
 
-#include <vector>
 #include <memory>
+#include <vector>
+
 #include "block.h"
-#include "nodes.h"
 #include "markups.h"
+#include "nodes.h"
 
 namespace macrodown
 {
@@ -14,16 +15,16 @@ 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,
-        const std::vector<PrefixMarkup>& prefix_markups = {},
-        const std::vector<DelimitedMarkup>& delimited_markups = {});
+    static std::vector<std::unique_ptr<Node>>
+    convert(const Block* root,
+            const std::vector<PrefixMarkup>& prefix_markups = {},
+            const std::vector<DelimitedMarkup>& delimited_markups = {});
 
 private:
-    static std::unique_ptr<Node> convert_block(
-        const Block* block,
-        const std::vector<PrefixMarkup>& prefix_markups,
-        const std::vector<DelimitedMarkup>& delimited_markups);
+    static std::unique_ptr<Node>
+    convert_block(const Block* block,
+                  const std::vector<PrefixMarkup>& prefix_markups,
+                  const std::vector<DelimitedMarkup>& delimited_markups);
 };
 
 } // namespace macrodown
diff --git a/include/macro_engine.h b/include/macro_engine.h
index 399175f..d84e9f5 100644
--- a/include/macro_engine.h
+++ b/include/macro_engine.h
@@ -1,35 +1,42 @@
 #ifndef MACRODOWN_MACRO_ENGINE_H
 #define MACRODOWN_MACRO_ENGINE_H
 
-#include <string>
-#include <vector>
+#include <functional>
 #include <map>
 #include <memory>
-#include <functional>
+#include <string>
+#include <vector>
+
 #include "nodes.h"
 
 namespace macrodown
 {
 
-using MacroCallback = std::function<std::string(const std::vector<std::string>&)>;
+using MacroCallback =
+    std::function<std::string(const std::vector<std::string>&)>;
 
 struct MacroDefinition
 {
     std::string name;
     std::vector<std::string> arg_names;
-    std::string body; // Raw body for user-defined macros
+    std::string body;       // Raw body for user-defined macros
     MacroCallback callback; // For intrinsic macros
     bool is_intrinsic = false;
 
     MacroDefinition() = default;
-    
+
     // For user-defined macros
     MacroDefinition(std::string n, std::vector<std::string> args, std::string b)
-        : name(std::move(n)), arg_names(std::move(args)), body(std::move(b)), is_intrinsic(false) {}
+        : name(std::move(n)), arg_names(std::move(args)), body(std::move(b)),
+          is_intrinsic(false)
+    {
+    }
 
     // For intrinsic macros
     MacroDefinition(std::string n, MacroCallback cb)
-        : name(std::move(n)), callback(std::move(cb)), is_intrinsic(true) {}
+        : name(std::move(n)), callback(std::move(cb)), is_intrinsic(true)
+    {
+    }
 };
 
 class Evaluator
@@ -37,7 +44,8 @@ class Evaluator
 public:
     Evaluator();
 
-    void define(const std::string& name, const std::vector<std::string>& args, const std::string& body);
+    void define(const std::string& name, const std::vector<std::string>& args,
+                const std::string& body);
     void defineIntrinsic(const std::string& name, MacroCallback callback);
 
     std::string evaluate(const Node& node);
diff --git a/include/macrodown.h b/include/macrodown.h
index 4899328..3a27bc9 100644
--- a/include/macrodown.h
+++ b/include/macrodown.h
@@ -1,12 +1,13 @@
 #ifndef MACRODOWN_H
 #define MACRODOWN_H
 
-#include <string>
 #include <memory>
+#include <string>
 #include <vector>
-#include "nodes.h"
+
 #include "macro_engine.h"
 #include "markups.h"
+#include "nodes.h"
 
 namespace macrodown
 {
@@ -33,7 +34,10 @@ public:
     /**
      * Access the evaluator to define custom macros.
      */
-    Evaluator& evaluator() { return evaluator_; }
+    Evaluator& evaluator()
+    {
+        return evaluator_;
+    }
 
     /**
      * Define a prefix markup.
diff --git a/include/nodes.h b/include/nodes.h
index c3dbede..7b61962 100644
--- a/include/nodes.h
+++ b/include/nodes.h
@@ -1,10 +1,10 @@
 #ifndef MACRODOWN_NODES_H
 #define MACRODOWN_NODES_H
 
-#include <string>
-#include <vector>
 #include <memory>
+#include <string>
 #include <variant>
+#include <vector>
 
 namespace macrodown
 {
@@ -41,22 +41,21 @@ struct Node
 
     // Call function on each node in the tree. Callback function takes
     // const Node& as argument.
-    template<typename Callback>
-    void forEach(Callback f) const
+    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)
+            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)
+            for(const std::unique_ptr<Node>& arg :
+                std::get<Macro>(data).arguments)
             {
                 arg->forEach(f);
             }
diff --git a/include/parser.h b/include/parser.h
index 91028b0..c4ba895 100644
--- a/include/parser.h
+++ b/include/parser.h
@@ -1,11 +1,12 @@
 #ifndef MACRODOWN_PARSER_H
 #define MACRODOWN_PARSER_H
 
+#include <memory>
 #include <string>
 #include <vector>
-#include <memory>
-#include "nodes.h"
+
 #include "markups.h"
+#include "nodes.h"
 
 namespace macrodown
 {
@@ -15,10 +16,10 @@ class Parser
 public:
     // Parses a string into a list of nodes (Text and Macros)
     // This handles the Inline Parsing phase (Phase 2 of the design)
-    static std::vector<std::unique_ptr<Node>> parse(
-        const std::string& input,
-        const std::vector<PrefixMarkup>& prefix_markups = {},
-        const std::vector<DelimitedMarkup>& delimited_markups = {});
+    static std::vector<std::unique_ptr<Node>>
+    parse(const std::string& input,
+          const std::vector<PrefixMarkup>& prefix_markups = {},
+          const std::vector<DelimitedMarkup>& delimited_markups = {});
 };
 
 } // namespace macrodown
diff --git a/src/block_parser.cpp b/src/block_parser.cpp
index 3a13c33..a83df52 100644
--- a/src/block_parser.cpp
+++ b/src/block_parser.cpp
@@ -94,6 +94,26 @@ bool BlockParser::matches(Block* block, const std::string& line, size_t& offset)
         return false;
     }
 
+    if(block->type == BlockType::List)
+    {
+        return true;
+    }
+
+    if(block->type == BlockType::ListItem)
+    {
+        if(is_blank(line))
+        {
+            return true;
+        }
+        size_t indent = count_indent(line, offset);
+        if(indent >= block->indent)
+        {
+            offset += block->indent;
+            return true;
+        }
+        return false;
+    }
+
     if(block->type == BlockType::Paragraph)
     {
         if(is_blank(line))
@@ -111,12 +131,38 @@ bool BlockParser::matches(Block* block, const std::string& line, size_t& offset)
                 return false;
             }
 
-            // Check for ATX Heading
+            // Check for ThematicBreak
             size_t check_pos = offset + indent;
+            if(check_pos < line.size())
+            {
+                char c = line[check_pos];
+                if(c == '-' || c == '_' || c == '*')
+                {
+                    int count = 0;
+                    size_t p = check_pos;
+                    while(p < line.size())
+                    {
+                        if(line[p] == c)
+                        {
+                            count++;
+                        }
+                        else if(line[p] != ' ')
+                        {
+                            break;
+                        }
+                        p++;
+                    }
+                    if(count >= 3 && p == line.size())
+                    {
+                        return false;
+                    }
+                }
+            }
+
+            // Check for ATX Heading
+            check_pos = offset + indent;
             if(check_pos < line.size() && line[check_pos] == '#')
             {
-                // Confirm it's a heading (sequence of # followed by space or
-                // end)
                 size_t hash_count = 0;
                 while(check_pos + hash_count < line.size() &&
                       line[check_pos + hash_count] == '#' && hash_count < 6)
@@ -130,8 +176,6 @@ bool BlockParser::matches(Block* block, const std::string& line, size_t& offset)
                 }
             }
         }
-
-        // It's a continuation
         return true;
     }
 
@@ -140,6 +184,21 @@ bool BlockParser::matches(Block* block, const std::string& line, size_t& offset)
         return true;
     }
 
+    if(block->type == BlockType::IndentedCode)
+    {
+        size_t indent = count_indent(line, offset);
+        if(indent >= 4)
+        {
+            offset += 4;
+            return true;
+        }
+        else if(is_blank(line))
+        {
+            return true;
+        }
+        return false;
+    }
+
     return false;
 }
 
@@ -166,12 +225,11 @@ void BlockParser::process_line(const std::string& line)
     close_unmatched_blocks(matches_count);
 
     // 3. Open new blocks
-    // Scan rest of line (at offset)
-
-    // Check for BlockQuote
     while(true)
     {
         size_t indent = count_indent(line, offset);
+
+        // Check for BlockQuote
         if(indent < 4 && offset + indent < line.size() &&
            line[offset + indent] == '>')
         {
@@ -185,18 +243,136 @@ void BlockParser::process_line(const std::string& line)
             Block* ptr = new_block.get();
             open_blocks.back().block->children.push_back(std::move(new_block));
             open_blocks.push_back({ptr});
+            continue;
         }
-        else
+
+        // Check for List Items
+        bool is_list_item = false;
+        char list_char = 0;
+        bool is_ordered = false;
+        int list_start = 1;
+        size_t marker_length = 0;
+
+        if(indent < 4)
         {
-            break;
+            size_t p = offset + indent;
+            if(p < line.size())
+            {
+                char c = line[p];
+                if(c == '-' || c == '+' || c == '*')
+                {
+                    if(p + 1 == line.size() || line[p + 1] == ' ')
+                    {
+                        // Avoid thematic break collision (like ---)
+                        int count = 0;
+                        size_t tb_p = p;
+                        while(tb_p < line.size())
+                        {
+                            if(line[tb_p] == c)
+                            {
+                                count++;
+                            }
+                            else if(line[tb_p] != ' ')
+                            {
+                                break;
+                            }
+                            tb_p++;
+                        }
+                        if(count < 3 || tb_p != line.size())
+                        {
+                            is_list_item = true;
+                            list_char = c;
+                            is_ordered = false;
+                            marker_length = 1;
+                        }
+                    }
+                }
+                else if(std::isdigit(c))
+                {
+                    size_t start_p = p;
+                    int num = 0;
+                    int digits = 0;
+                    while(p < line.size() && std::isdigit(line[p]) &&
+                          digits < 9)
+                    {
+                        num = num * 10 + (line[p] - '0');
+                        p++;
+                        digits++;
+                    }
+                    if(digits > 0 && p < line.size() &&
+                       (line[p] == '.' || line[p] == ')'))
+                    {
+                        char delim = line[p];
+                        if(p + 1 == line.size() || line[p + 1] == ' ')
+                        {
+                            is_list_item = true;
+                            list_char = delim;
+                            is_ordered = true;
+                            list_start = num;
+                            marker_length = (p + 1) - start_p;
+                        }
+                    }
+                }
+            }
         }
-    }
 
-    // 4. Handle Leaf Blocks (Heading, ThematicBreak) or continuation
+        if(is_list_item)
+        {
+            size_t p = offset + indent + marker_length;
+            size_t spaces = 0;
+            while(p + spaces < line.size() && line[p + spaces] == ' ')
+            {
+                spaces++;
+            }
+            size_t item_indent = indent + marker_length + spaces;
+            if(spaces > 4)
+            {
+                item_indent = indent + marker_length + 1;
+                offset = offset + indent + marker_length + 1;
+            }
+            else if(spaces == 0 && p < line.size())
+            {
+                item_indent = indent + marker_length; // blank line
+                offset = p;
+            }
+            else
+            {
+                offset = p + spaces;
+            }
+
+            Block* tip = open_blocks.back().block;
+            if(tip->type == BlockType::Paragraph)
+            {
+                close_unmatched_blocks(open_blocks.size() - 2);
+                tip = open_blocks.back().block;
+            }
+
+            if(tip->type != BlockType::List || tip->list_char != list_char ||
+               tip->is_ordered != is_ordered)
+            {
+                auto list_block = std::make_unique<Block>(BlockType::List);
+                list_block->list_char = list_char;
+                list_block->is_ordered = is_ordered;
+                list_block->list_start = list_start;
+                Block* l_ptr = list_block.get();
+                tip->children.push_back(std::move(list_block));
+                open_blocks.push_back({l_ptr});
+                tip = l_ptr;
+            }
+
+            auto li_block = std::make_unique<Block>(BlockType::ListItem);
+            li_block->indent = item_indent;
+            Block* li_ptr = li_block.get();
+            tip->children.push_back(std::move(li_block));
+            open_blocks.push_back({li_ptr});
+            continue;
+        }
+
+        break;
+    }
 
     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);
@@ -211,7 +387,6 @@ void BlockParser::process_line(const std::string& line)
             }
             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] == ' ')
                 {
@@ -219,13 +394,11 @@ void BlockParser::process_line(const std::string& line)
                 }
                 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";
@@ -234,8 +407,59 @@ void BlockParser::process_line(const std::string& line)
         return;
     }
 
-    // Check for FencedCode Opening
+    if(tip->type == BlockType::IndentedCode)
+    {
+        if(!tip->literal_content.empty())
+        {
+            tip->literal_content += "\n";
+        }
+        tip->literal_content += line.substr(offset);
+        return;
+    }
+
+    // 4. Handle Leaf Blocks
     size_t indent = count_indent(line, offset);
+
+    // Check for Thematic Break
+    if(indent < 4)
+    {
+        size_t check_pos = offset + indent;
+        if(check_pos < line.size())
+        {
+            char c = line[check_pos];
+            if(c == '-' || c == '_' || c == '*')
+            {
+                int count = 0;
+                size_t p = check_pos;
+                while(p < line.size())
+                {
+                    if(line[p] == c)
+                    {
+                        count++;
+                    }
+                    else if(line[p] != ' ')
+                    {
+                        break;
+                    }
+                    p++;
+                }
+                if(count >= 3 && p == line.size())
+                {
+                    if(tip->type == BlockType::Paragraph)
+                    {
+                        close_unmatched_blocks(open_blocks.size() - 2);
+                        tip = open_blocks.back().block;
+                    }
+                    auto hr = std::make_unique<Block>(BlockType::ThematicBreak);
+                    hr->open = false;
+                    tip->children.push_back(std::move(hr));
+                    return;
+                }
+            }
+        }
+    }
+
+    // Check for FencedCode Opening
     if(indent < 4)
     {
         size_t check_pos = offset + indent;
@@ -258,7 +482,6 @@ void BlockParser::process_line(const std::string& line)
                     info_start++;
                 }
                 std::string info_string = line.substr(info_start);
-
                 bool valid = true;
                 if(fence_char == '`' &&
                    info_string.find('`') != std::string::npos)
@@ -289,22 +512,21 @@ void BlockParser::process_line(const std::string& line)
         }
     }
 
-    // If tip is a Paragraph, check for blank line (closes it)
-    if(tip->type == BlockType::Paragraph)
+    // Check for IndentedCode Opening
+    if(indent >= 4)
     {
-        if(is_blank(line))
+        if(tip->type != BlockType::Paragraph && !is_blank(line))
         {
-            close_unmatched_blocks(open_blocks.size() - 2); // Close paragraph
+            auto code_block = std::make_unique<Block>(BlockType::IndentedCode);
+            code_block->literal_content = line.substr(offset + 4);
+            Block* ptr = code_block.get();
+            tip->children.push_back(std::move(code_block));
+            open_blocks.push_back({ptr});
             return;
         }
-        // Else, it's a continuation
-        // (Unless it's interrupted by a Heading/Quote etc. - Simplified: we
-        // assume it continues) Strictly, we should check if the line *starts* a
-        // new block (like Header) If it does, we close the paragraph.
     }
 
     // Check for ATX Heading
-    indent = count_indent(line, offset);
     if(indent < 4)
     {
         size_t check_pos = offset + indent;
@@ -318,8 +540,6 @@ void BlockParser::process_line(const std::string& line)
         if(hash_count > 0 && (check_pos + hash_count == line.size() ||
                               line[check_pos + hash_count] == ' '))
         {
-            // Found Heading
-            // If we were in a paragraph, close it
             if(tip->type == BlockType::Paragraph)
             {
                 close_unmatched_blocks(open_blocks.size() - 2);
@@ -328,46 +548,48 @@ void BlockParser::process_line(const std::string& line)
 
             auto heading = std::make_unique<Block>(BlockType::Heading);
             heading->level = hash_count;
-            // Content is the rest of the line (trimmed)
             size_t content_start = check_pos + hash_count;
             while(content_start < line.size() && line[content_start] == ' ')
             {
                 content_start++;
             }
             heading->literal_content = line.substr(content_start);
-            // Remove trailing hashes? CommonMark says yes. Optional for now.
-            heading->open = false; // Headings are single line
+            heading->open = false;
 
             tip->children.push_back(std::move(heading));
             return;
         }
     }
 
+    // Paragraph continuation check
+    if(tip->type == BlockType::Paragraph)
+    {
+        if(is_blank(line))
+        {
+            close_unmatched_blocks(open_blocks.size() - 2); // Close paragraph
+            return;
+        }
+    }
+
     // 5. Finalize: Text or Paragraph
     if(is_blank(line))
     {
-        return; // Ignore blank lines if not ending a paragraph
+        return;
     }
 
     if(tip->type == BlockType::Document || tip->type == BlockType::Quote ||
        tip->type == BlockType::List || tip->type == BlockType::ListItem)
     {
-        // Create new Paragraph
         auto p = std::make_unique<Block>(BlockType::Paragraph);
         Block* p_ptr = p.get();
         tip->children.push_back(std::move(p));
         open_blocks.push_back({p_ptr});
 
-        // Add text
-        // Note: indentation in paragraph text is preserved but leading spaces
-        // of the first line? CommonMark: stripped.
         size_t content_start = offset + count_indent(line, offset);
         p_ptr->literal_content = line.substr(content_start);
     }
     else if(tip->type == BlockType::Paragraph)
     {
-        // Continuation
-        // Remove leading spaces up to indent? simplified: just add space + text
         size_t content_start = offset + count_indent(line, offset);
         tip->literal_content += "\n" + line.substr(content_start);
     }
diff --git a/src/converter.cpp b/src/converter.cpp
index 0cf55aa..80bf327 100644
--- a/src/converter.cpp
+++ b/src/converter.cpp
@@ -66,6 +66,18 @@ Converter::convert_block(const Block* block,
     case BlockType::FencedCode:
         macro_name = "fenced_code";
         break;
+    case BlockType::List:
+        macro_name = block->is_ordered ? "ol" : "ul";
+        break;
+    case BlockType::ListItem:
+        macro_name = "li";
+        break;
+    case BlockType::ThematicBreak:
+        macro_name = "hr";
+        break;
+    case BlockType::IndentedCode:
+        macro_name = "fenced_code";
+        break;
     default:
         return nullptr; // Ignore unknown blocks for now
     }
@@ -73,11 +85,14 @@ Converter::convert_block(const Block* block,
     Macro macro;
     macro.name = macro_name;
 
-    if(block->type == BlockType::FencedCode)
+    if(block->type == BlockType::FencedCode ||
+       block->type == BlockType::IndentedCode)
     {
         // Arg 1: Info string
         Group info_group;
-        info_group.addChild(std::make_unique<Node>(Text{block->info_string}));
+        std::string info =
+            (block->type == BlockType::FencedCode) ? block->info_string : "";
+        info_group.addChild(std::make_unique<Node>(Text{info}));
         macro.arguments.push_back(
             std::make_unique<Node>(std::move(info_group)));
 
@@ -88,6 +103,10 @@ Converter::convert_block(const Block* block,
         macro.arguments.push_back(
             std::make_unique<Node>(std::move(content_group)));
     }
+    else if(block->type == BlockType::ThematicBreak)
+    {
+        // No arguments needed
+    }
     else if(block->children.empty())
     {
         // Leaf block: Parse literal content
diff --git a/src/standard_library.cpp b/src/standard_library.cpp
index 37f9ef9..e043306 100644
--- a/src/standard_library.cpp
+++ b/src/standard_library.cpp
@@ -34,6 +34,11 @@ void StandardLibrary::registerMacros(Evaluator& evaluator)
     evaluator.define("h5", {"content"}, "<h5>%content</h5>\n");
     evaluator.define("h6", {"content"}, "<h6>%content</h6>\n");
 
+    evaluator.define("hr", {}, "<hr />\n");
+    evaluator.define("ul", {"content"}, "<ul>\n%content</ul>\n");
+    evaluator.define("ol", {"content"}, "<ol>\n%content</ol>\n");
+    evaluator.define("li", {"content"}, "<li>\n%content</li>\n");
+
     evaluator.defineIntrinsic(
         "fenced_code",
         [](const std::vector<std::string>& args) -> std::string
diff --git a/tests/test_block_parser.cpp b/tests/test_block_parser.cpp
index 75b9769..22a3acb 100644
--- a/tests/test_block_parser.cpp
+++ b/tests/test_block_parser.cpp
@@ -1,4 +1,5 @@
 #include <gtest/gtest.h>
+
 #include "block_parser.h"
 
 using namespace macrodown;
@@ -7,10 +8,10 @@ TEST(BlockParserTest, SimpleParagraph)
 {
     std::string input = "Hello\nWorld";
     auto root = BlockParser::parse(input);
-    
+
     ASSERT_EQ(root->type, BlockType::Document);
     ASSERT_EQ(root->children.size(), 1);
-    
+
     auto* p = root->children[0].get();
     EXPECT_EQ(p->type, BlockType::Paragraph);
     EXPECT_EQ(p->literal_content, "Hello\nWorld");
@@ -20,7 +21,7 @@ TEST(BlockParserTest, MultipleParagraphs)
 {
     std::string input = "Para 1\n\nPara 2";
     auto root = BlockParser::parse(input);
-    
+
     ASSERT_EQ(root->children.size(), 2);
     EXPECT_EQ(root->children[0]->type, BlockType::Paragraph);
     EXPECT_EQ(root->children[1]->type, BlockType::Paragraph);
@@ -30,14 +31,14 @@ TEST(BlockParserTest, Headers)
 {
     std::string input = "# H1\n## H2";
     auto root = BlockParser::parse(input);
-    
+
     ASSERT_EQ(root->children.size(), 2);
-    
+
     auto* h1 = root->children[0].get();
     EXPECT_EQ(h1->type, BlockType::Heading);
     EXPECT_EQ(h1->level, 1);
     EXPECT_EQ(h1->literal_content, "H1");
-    
+
     auto* h2 = root->children[1].get();
     EXPECT_EQ(h2->type, BlockType::Heading);
     EXPECT_EQ(h2->level, 2);
@@ -48,11 +49,11 @@ TEST(BlockParserTest, BlockQuote)
 {
     std::string input = "> Hello\n> World";
     auto root = BlockParser::parse(input);
-    
+
     ASSERT_EQ(root->children.size(), 1);
     auto* quote = root->children[0].get();
     EXPECT_EQ(quote->type, BlockType::Quote);
-    
+
     ASSERT_EQ(quote->children.size(), 1);
     auto* p = quote->children[0].get();
     EXPECT_EQ(p->type, BlockType::Paragraph);
@@ -63,21 +64,22 @@ TEST(BlockParserTest, NestedQuote)
 {
     std::string input = "> Level 1\n>> Level 2";
     auto root = BlockParser::parse(input);
-    
+
     ASSERT_EQ(root->children.size(), 1);
     auto* q1 = root->children[0].get();
     EXPECT_EQ(q1->type, BlockType::Quote);
-    
-    // Structure: Quote -> [Paragraph("Level 1"), Quote -> [Paragraph("Level 2")]]
-    
+
+    // Structure: Quote -> [Paragraph("Level 1"), Quote -> [Paragraph("Level
+    // 2")]]
+
     ASSERT_EQ(q1->children.size(), 2); // P("Level 1") + Quote
-    
+
     EXPECT_EQ(q1->children[0]->type, BlockType::Paragraph);
     EXPECT_EQ(q1->children[0]->literal_content, "Level 1");
-    
+
     EXPECT_EQ(q1->children[1]->type, BlockType::Quote);
     auto* q2 = q1->children[1].get();
-    
+
     ASSERT_EQ(q2->children.size(), 1);
     EXPECT_EQ(q2->children[0]->literal_content, "Level 2");
 }
@@ -86,10 +88,10 @@ 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);
@@ -101,13 +103,110 @@ 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
+}
+
+TEST(BlockParserTest, ThematicBreak)
+{
+    std::string input = "---\n***\n___";
+    auto root = BlockParser::parse(input);
+
+    ASSERT_EQ(root->children.size(), 3);
+    EXPECT_EQ(root->children[0]->type, BlockType::ThematicBreak);
+    EXPECT_EQ(root->children[1]->type, BlockType::ThematicBreak);
+    EXPECT_EQ(root->children[2]->type, BlockType::ThematicBreak);
+}
+
+TEST(BlockParserTest, IndentedCode)
+{
+    std::string input = "    int x = 0;\n    x++;";
+    auto root = BlockParser::parse(input);
+
+    ASSERT_EQ(root->children.size(), 1);
+    EXPECT_EQ(root->children[0]->type, BlockType::IndentedCode);
+    EXPECT_EQ(root->children[0]->literal_content, "int x = 0;\nx++;");
+}
+
+TEST(BlockParserTest, UnorderedList)
+{
+    std::string input = "- Item 1\n- Item 2";
+    auto root = BlockParser::parse(input);
+
+    ASSERT_EQ(root->children.size(), 1);
+    auto* list = root->children[0].get();
+    EXPECT_EQ(list->type, BlockType::List);
+    EXPECT_FALSE(list->is_ordered);
+    EXPECT_EQ(list->list_char, '-');
+
+    ASSERT_EQ(list->children.size(), 2);
+    EXPECT_EQ(list->children[0]->type, BlockType::ListItem);
+    EXPECT_EQ(list->children[1]->type, BlockType::ListItem);
+}
+
+TEST(BlockParserTest, UnorderedListPlus)
+{
+    std::string input = "+ Item 1\n+ Item 2";
+    auto root = BlockParser::parse(input);
+
+    ASSERT_EQ(root->children.size(), 1);
+    auto* list = root->children[0].get();
+    EXPECT_EQ(list->type, BlockType::List);
+    EXPECT_FALSE(list->is_ordered);
+    EXPECT_EQ(list->list_char, '+');
+
+    ASSERT_EQ(list->children.size(), 2);
+}
+
+TEST(BlockParserTest, UnorderedListAsterisk)
+{
+    std::string input = "* Item 1\n* Item 2";
+    auto root = BlockParser::parse(input);
+
+    ASSERT_EQ(root->children.size(), 1);
+    auto* list = root->children[0].get();
+    EXPECT_EQ(list->type, BlockType::List);
+    EXPECT_FALSE(list->is_ordered);
+    EXPECT_EQ(list->list_char, '*');
+
+    ASSERT_EQ(list->children.size(), 2);
+}
+
+TEST(BlockParserTest, OrderedList)
+{
+    std::string input = "1. First\n2. Second";
+    auto root = BlockParser::parse(input);
+
+    ASSERT_EQ(root->children.size(), 1);
+    auto* list = root->children[0].get();
+    EXPECT_EQ(list->type, BlockType::List);
+    EXPECT_TRUE(list->is_ordered);
+    EXPECT_EQ(list->list_char, '.');
+    EXPECT_EQ(list->list_start, 1);
+
+    ASSERT_EQ(list->children.size(), 2);
+    EXPECT_EQ(list->children[0]->type, BlockType::ListItem);
+    EXPECT_EQ(list->children[1]->type, BlockType::ListItem);
+}
+
+TEST(BlockParserTest, OrderedListParen)
+{
+    std::string input = "1) First\n2) Second";
+    auto root = BlockParser::parse(input);
+
+    ASSERT_EQ(root->children.size(), 1);
+    auto* list = root->children[0].get();
+    EXPECT_EQ(list->type, BlockType::List);
+    EXPECT_TRUE(list->is_ordered);
+    EXPECT_EQ(list->list_char, ')');
+    EXPECT_EQ(list->list_start, 1);
+
+    ASSERT_EQ(list->children.size(), 2);
+}
diff --git a/tests/test_custom_markup.cpp b/tests/test_custom_markup.cpp
index bcd7719..09be91b 100644
--- a/tests/test_custom_markup.cpp
+++ b/tests/test_custom_markup.cpp
@@ -1,4 +1,5 @@
 #include <gtest/gtest.h>
+
 #include "macrodown.h"
 
 using namespace macrodown;
@@ -9,13 +10,15 @@ TEST(CustomMarkupTest, PrefixMarkup)
     // Define # as prefix markup for 'tag'
     md.definePrefixMarkup({"#", "tag", ""});
     // We also need to define what the 'tag' macro does in HTML
-    md.evaluator().define("tag", {"content"}, "<span class=\"tag\">#%content</span>");
+    md.evaluator().define("tag", {"content"},
+                          "<span class=\"tag\">#%content</span>");
 
     std::string input = "This is a #test markup.";
     auto root = md.parse(input);
     std::string html = md.render(*root);
 
-    EXPECT_EQ(html, "<p>This is a <span class=\"tag\">#test</span> markup.</p>\n");
+    EXPECT_EQ(html,
+              "<p>This is a <span class=\"tag\">#test</span> markup.</p>\n");
 }
 
 TEST(CustomMarkupTest, PrefixMarkupPunctuation)
@@ -51,11 +54,13 @@ TEST(CustomMarkupTest, DelimitedMarkupInvalid)
     md.evaluator().define("highlight", {"content"}, "<mark>%content</mark>");
 
     // Whitespace or punctuation (except _ and -) makes it invalid
-    std::string input = "This is :not valid: because of space. And :this.too: because of dot.";
+    std::string input =
+        "This is :not valid: because of space. And :this.too: because of dot.";
     std::string html = md.render(*md.parse(input));
 
     // Should remain as plain text
-    EXPECT_EQ(html, "<p>This is :not valid: because of space. And :this.too: because of dot.</p>\n");
+    EXPECT_EQ(html, "<p>This is :not valid: because of space. And :this.too: "
+                    "because of dot.</p>\n");
 }
 
 TEST(CustomMarkupTest, DelimitedMarkupUnderscoreDash)
@@ -100,13 +105,16 @@ TEST(CustomMarkupTest, PrefixMarkupAtPrefix)
 {
     MacroDown md;
     md.definePrefixMarkup({"@", "mention", ""});
-    md.evaluator().define("mention", {"content"}, "<a href=\" /u/%content\">@%content</a>");
+    md.evaluator().define("mention", {"content"},
+                          "<a href=\" /u/%content\">@%content</a>");
 
     // Dash, @ and . allowed inside.
     std::string input = "Hello @user-name! Email: @user@domain.com";
     std::string html = md.render(*md.parse(input));
 
-    EXPECT_EQ(html, "<p>Hello <a href=\" /u/user-name\">@user-name</a>! Email: <a href=\" /u/user@domain.com\">@user@domain.com</a></p>\n");
+    EXPECT_EQ(html,
+              "<p>Hello <a href=\" /u/user-name\">@user-name</a>! Email: <a "
+              "href=\" /u/user@domain.com\">@user@domain.com</a></p>\n");
 }
 
 TEST(CustomMarkupTest, PrefixMarkupTrailingDot)
diff --git a/tests/test_macro_engine.cpp b/tests/test_macro_engine.cpp
index ce886f0..87ab4fc 100644
--- a/tests/test_macro_engine.cpp
+++ b/tests/test_macro_engine.cpp
@@ -1,8 +1,10 @@
+#include <variant>
+
 #include <gtest/gtest.h>
+
 #include "macro_engine.h"
-#include "parser.h"
 #include "nodes.h"
-#include <variant>
+#include "parser.h"
 
 using namespace macrodown;
 
@@ -17,7 +19,7 @@ TEST_F(MacroEngineTest, ParseText)
 {
     auto nodes = Parser::parse("Hello World");
     ASSERT_EQ(nodes.size(), 1);
-    
+
     ASSERT_TRUE(std::holds_alternative<Text>(nodes[0]->data));
     EXPECT_EQ(std::get<Text>(nodes[0]->data).content, "Hello World");
 }
@@ -27,17 +29,17 @@ TEST_F(MacroEngineTest, ParseMacro)
 {
     auto nodes = Parser::parse("%m{arg}");
     ASSERT_EQ(nodes.size(), 1);
-    
+
     ASSERT_TRUE(std::holds_alternative<Macro>(nodes[0]->data));
     const auto& macro = std::get<Macro>(nodes[0]->data);
     EXPECT_EQ(macro.name, "m");
     ASSERT_EQ(macro.arguments.size(), 1);
-    
+
     // Argument should be a Group
     ASSERT_TRUE(std::holds_alternative<Group>(macro.arguments[0]->data));
     const auto& group = std::get<Group>(macro.arguments[0]->data);
     ASSERT_EQ(group.children.size(), 1);
-    
+
     ASSERT_TRUE(std::holds_alternative<Text>(group.children[0]->data));
     EXPECT_EQ(std::get<Text>(group.children[0]->data).content, "arg");
 }
@@ -49,13 +51,13 @@ TEST_F(MacroEngineTest, DefAndExpand)
     // We use the Parser to create the definition call
     std::string input = "%def[hello]{name}{Hello %name!}";
     auto nodes = Parser::parse(input);
-    
+
     // Evaluate the definition
     for(const auto& node : nodes)
     {
         evaluator.evaluate(*node);
     }
-    
+
     // Now call it: %hello{World}
     auto call_nodes = Parser::parse("%hello{World}");
     std::string result;
@@ -63,7 +65,7 @@ TEST_F(MacroEngineTest, DefAndExpand)
     {
         result += evaluator.evaluate(*node);
     }
-    
+
     EXPECT_EQ(result, "Hello World!");
 }
 
@@ -73,24 +75,25 @@ TEST_F(MacroEngineTest, NestedMacros)
     // %def[b]{t}{<b>%t</b>}
     // %def[p]{t}{<p>%t</p>}
     // Call: %p{Hello %b{World}} -> <p>Hello <b>World</b></p>
-    
-    std::vector<std::string> defs = {
-        "%def[b]{t}{<b>%t</b>}",
-        "%def[p]{t}{<p>%t</p>}"
-    };
-    
+
+    std::vector<std::string> defs = {"%def[b]{t}{<b>%t</b>}",
+                                     "%def[p]{t}{<p>%t</p>}"};
+
     for(const auto& def : defs)
     {
         auto nodes = Parser::parse(def);
-        for(const auto& node : nodes) evaluator.evaluate(*node);
+        for(const auto& node : nodes)
+        {
+            evaluator.evaluate(*node);
+        }
     }
-    
+
     auto nodes = Parser::parse("%p{Hello %b{World}}");
     std::string result;
     for(const auto& node : nodes)
     {
         result += evaluator.evaluate(*node);
     }
-    
+
     EXPECT_EQ(result, "<p>Hello <b>World</b></p>");
 }
diff --git a/tests/test_macrodown.cpp b/tests/test_macrodown.cpp
index 9e16cd3..11e646b 100644
--- a/tests/test_macrodown.cpp
+++ b/tests/test_macrodown.cpp
@@ -1,4 +1,5 @@
 #include <gtest/gtest.h>
+
 #include "macrodown.h"
 
 using namespace macrodown;
@@ -35,13 +36,16 @@ TEST(MacroDownTest, MarkdownElements)
     EXPECT_EQ(md.render(*md.parse("*em*")), "<p><em>em</em></p>\n");
 
     // Strong
-    EXPECT_EQ(md.render(*md.parse("**bold**")), "<p><strong>bold</strong></p>\n");
+    EXPECT_EQ(md.render(*md.parse("**bold**")),
+              "<p><strong>bold</strong></p>\n");
 
     // Link
-    EXPECT_EQ(md.render(*md.parse("[Link](url)")), "<p><a href=\"url\">Link</a></p>\n");
+    EXPECT_EQ(md.render(*md.parse("[Link](url)")),
+              "<p><a href=\"url\">Link</a></p>\n");
 
     // Image
-    EXPECT_EQ(md.render(*md.parse("![Alt Text](img.jpg)")), "<p><img src=\"img.jpg\" alt=\"Alt Text\" /></p>\n");
+    EXPECT_EQ(md.render(*md.parse("![Alt Text](img.jpg)")),
+              "<p><img src=\"img.jpg\" alt=\"Alt Text\" /></p>\n");
 
     // Code
     EXPECT_EQ(md.render(*md.parse("`code`")), "<p><code>code</code></p>\n");
@@ -59,7 +63,8 @@ TEST(MacroDownTest, MixedContent)
 {
     MacroDown md;
     std::string input = "# Header\n\nParagraph with *em* and %code{macros}.";
-    std::string expected = "<h1>Header</h1>\n<p>Paragraph with <em>em</em> and <code>macros</code>.</p>\n";
+    std::string expected = "<h1>Header</h1>\n<p>Paragraph with <em>em</em> and "
+                           "<code>macros</code>.</p>\n";
     EXPECT_EQ(md.render(*md.parse(input)), expected);
 }
 
@@ -67,8 +72,8 @@ TEST(MacroDownTest, InlineDefinition)
 {
     MacroDown md;
     std::string input = "%def[bold]{t}{<b>%t</b>}\n\nThis is %bold{important}.";
-    // The first paragraph only contains the definition, which evaluates to empty string.
-    // The second paragraph uses the newly defined macro.
+    // The first paragraph only contains the definition, which evaluates to
+    // empty string. The second paragraph uses the newly defined macro.
     std::string expected = "<p>This is <b>important</b>.</p>\n";
     EXPECT_EQ(md.render(*md.parse(input)), expected);
 }
@@ -77,7 +82,8 @@ 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";
+    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);
 }
 
diff --git a/tests/test_main.cpp b/tests/test_main.cpp
index 2387483..01afcf7 100644
--- a/tests/test_main.cpp
+++ b/tests/test_main.cpp
@@ -2,11 +2,11 @@
 
 TEST(SetupTest, BasicAssertions)
 {
-  EXPECT_EQ(1 + 1, 2);
+    EXPECT_EQ(1 + 1, 2);
 }
 
-int main(int argc, char **argv)
+int main(int argc, char** argv)
 {
-  ::testing::InitGoogleTest(&argc, argv);
-  return RUN_ALL_TESTS();
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
 }
\ No newline at end of file
diff --git a/tests/test_nodes.cpp b/tests/test_nodes.cpp
index 7745f9f..a16f540 100644
--- a/tests/test_nodes.cpp
+++ b/tests/test_nodes.cpp
@@ -1,7 +1,9 @@
+#include <string>
+#include <vector>
+
 #include <gtest/gtest.h>
+
 #include "nodes.h"
-#include <vector>
-#include <string>
 
 using namespace macrodown;
 
@@ -16,24 +18,36 @@ TEST(NodeTest, ForEachIteration)
 
     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"};
+    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);
 }