MD4X 核心源码深度解析

在这里插入图片描述

一、整体架构流程图

渲染器

回调输出层

第二次扫描: 行内解析

第一次扫描: 块级解析

md_parse 入口

输入层

Markdown 文本
# Hello world

md_parse() 函数

md_split_lines()
将文本分割成行数组

逐行识别块类型

md_is_heading()
识别标题 #

md_is_fenced_code()
识别代码块 ```

md_is_list_item()
识别列表项

md_is_blockquote()
识别引用块 >

md_is_table()
识别表格 |

默认作为段落

md_parse_inline()
行内解析

md_scan_inline_mark()
识别行内标记

标记栈管理
MD_MARK[]

enter_block()

enter_span()

text()

leave_span()

leave_block()

HTML 渲染器

AST 渲染器

ANSI 渲染器

二、核心数据结构

2.1 解析器结构体(核心接口)

这是整个解析器的核心,用户需要填充这个结构体来定义自己的渲染逻辑。

// src/md4x.h 中的核心定义
typedef struct MD_PARSER {
    // ABI 版本,必须设为 0
    unsigned abi_version;
    
    // 方言选项,控制启用哪些扩展
    // 例如: MD_DIALECT_GITHUB = MD_FLAG_TABLES | MD_FLAG_STRIKETHROUGH | ...
    unsigned flags;
    
    // ========== 块级回调函数 ==========
    
    // 进入块时调用(例如遇到 <h1> 时调用)
    // type: 块类型(MD_BLOCK_H, MD_BLOCK_P 等)
    // detail: 块的详细信息(如标题级别)
    // userdata: 用户传递的数据
    int (*enter_block)(MD_BLOCKTYPE type, void* detail, void* userdata);
    
    // 离开块时调用
    int (*leave_block)(MD_BLOCKTYPE type, void* detail, void* userdata);
    
    // ========== 行内回调函数 ==========
    
    // 进入行内元素时调用(如遇到 ** 时调用)
    int (*enter_span)(MD_SPANTYPE type, void* detail, void* userdata);
    
    // 离开行内元素时调用
    int (*leave_span)(MD_SPANTYPE type, void* detail, void* userdata);
    
    // ========== 文本回调 ==========
    
    // 输出实际文本内容
    // type: 文本类型(MD_TEXT_NORMAL, MD_TEXT_CODE 等)
    // text: 文本内容(注意:是指针,不是副本!)
    // size: 文本长度
    int (*text)(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata);
    
    // 调试日志回调(可选)
    void (*debug_log)(const char* msg, void* userdata);
} MD_PARSER;

代码解释:

  • 这是一个典型的观察者模式实现,解析器像"推"数据一样把解析结果推送给渲染器
  • 所有回调都是函数指针,由用户提供实现
  • 注意 text 回调中的 MD_CHAR* text 是指向原始输入的指针,不是副本,这是零拷贝设计的关键

2.2 块级元素类型枚举

// src/md4x.h
typedef enum MD_BLOCKTYPE {
    MD_BLOCK_DOC = 0,        // 文档根,整个文档的入口
    MD_BLOCK_QUOTE,         // 引用块 > 
    MD_BLOCK_UL,            // 无序列表 -
    MD_BLOCK_OL,            // 有序列表 1.
    MD_BLOCK_LI,            // 列表项
    MD_BLOCK_H,             // 标题 # 
    MD_BLOCK_CODE,          // 代码块 ```或缩进
    MD_BLOCK_HTML,          // HTML 块
    MD_BLOCK_P,             // 段落(默认)
    MD_BLOCK_HR,            // 水平线 ---
    MD_BLOCK_TABLE,         // 表格
    MD_BLOCK_THEAD,         // 表头
    MD_BLOCK_TBODY,         // 表体
    MD_BLOCK_TR,            // 表格行
    MD_BLOCK_TH,            // 表头单元格
    MD_BLOCK_TD,            // 表格单元格
    // MD4X 扩展
    MD_BLOCK_FRONTMATTER,   // YAML 头部 ---
    MD_BLOCK_COMPONENT,     // MDC 组件 ::Card{}
    MD_BLOCK_ALERT,         // GitHub 警报 > [!NOTE]
} MD_BLOCKTYPE;

2.3 行内元素类型枚举

typedef enum MD_SPANTYPE {
    MD_SPAN_A = 0,              // 链接 [text](url)
    MD_SPAN_STRONG,              // 粗体 **text**
    MD_SPAN_EM,                  // 斜体 *text*
    MD_SPAN_CODE,                // 行内代码 `code`
    MD_SPAN_IMG,                 // 图片 ![alt](url)
    MD_SPAN_DEL,                 // 删除线 ~~text~~
    // MD4X 扩展
    MD_SPAN_LATEXMATH,          // LaTeX $...$
    MD_SPAN_LATEXMATH_DISPLAY, // LaTeX $$...$$
    MD_SPAN_WIKILINK,           // Wiki 链接 [[link]]
    MD_SPAN_U,                  // 下划线 __text__
    MD_SPAN_COMPONENT,          // MDC 行内组件
} MD_SPANTYPE;

2.4 标志位定义

// src/md4x.h - 控制解析行为的开关

// 基础标志
#define MD_FLAG_COLLAPSEWHITESPACE   0x0001  // 合并空白字符
#define MD_FLAG_NOHTMLBLOCKS         0x0020  // 禁用 HTML 块
#define MD_FLAG_NOHTMLSPANS          0x0040  // 禁用行内 HTML

// GFM 扩展
#define MD_FLAG_TABLES               0x0100  // 表格支持
#define MD_FLAG_STRIKETHROUGH       0x0200  // 删除线 ~~
#define MD_FLAG_TASKLISTS           0x1000  // 任务列表 - [x]

// MD4X 特有扩展
#define MD_FLAG_LATEXMATHSPANS      0x2000  // LaTeX 数学
#define MD_FLAG_WIKILINKS           0x4000  // Wiki 链接
#define MD_FLAG_UNDERLINE           0x8000  // 下划线
#define MD_FLAG_ATTRIBUTES         0x10000  // 行内属性 {.class}
#define MD_FLAG_COMPONENTS         0x20000 // MDC 组件
#define MD_FLAG_ALERTS             0x40000  // GitHub 警报

// 预定义方言组合
#define MD_DIALECT_COMMONMARK       0       // 纯 CommonMark
#define MD_DIALECT_GITHUB          (MD_FLAG_TABLES | \
                                   MD_FLAG_STRIKETHROUGH | \
                                   MD_FLAG_TASKLISTS)

三、解析器入口函数

3.1 主入口函数

这是用户调用的唯一入口函数,负责协调整个解析过程。

// src/md4x.c
int md_parse(const MD_CHAR* text, MD_SIZE size, 
             const MD_PARSER* parser, void* userdata) {
    
    // 1. 参数验证
    if (!text || !parser || parser->abi_version != 0) {
        return -1;  // 无效参数
    }
    
    // 2. 创建解析上下文(保存解析状态)
    MD_PARSER_CTX ctx;
    memset(&ctx, 0, sizeof(ctx));
    ctx.parser = parser;           // 保存用户提供的回调
    ctx.userdata = userdata;       // 保存用户数据
    ctx.text = text;               // 原始输入(指针,非拷贝)
    ctx.size = size;
    ctx.flags = parser->flags;     // 复制标志位
    
    // 3. 调用文档开始的 enter_block 回调
    //    这会触发渲染器输出 <body> 或类似标签
    if (ctx.parser->enter_block) {
        ctx.parser->enter_block(MD_BLOCK_DOC, NULL, ctx.userdata);
    }
    
    // 4. 执行第一次扫描:块级解析
    //    逐行扫描,识别标题、列表、代码块等
    md_parse_block(&ctx);
    
    // 5. 调用文档结束的 leave_block 回调
    if (ctx.parser->leave_block) {
        ctx.parser->leave_block(MD_BLOCK_DOC, NULL, ctx.userdata);
    }
    
    // 6. 清理资源并返回
    md_free(&ctx);
    return ctx.error_code;  // 0 表示成功
}

代码解释:

  • 第一步:参数验证 - 确保传入的解析器和文本有效
  • 第二步:创建上下文 - 创建一个上下文结构体来保存解析过程中的所有状态
  • 第三步:调用开始回调 - 通知渲染器文档开始,通常输出 <body> 或类似标签
  • 第四步:块级解析 - 这是核心,真正开始解析 Markdown
  • 第五步:调用结束回调 - 通知渲染器文档结束
  • 第六步:清理 - 释放内存

3.2 块级解析函数

// src/md4x.c - 块级解析主循环

static void md_parse_block(MD_PARSER_CTX* ctx) {
    MD_LINE* lines = NULL;
    MD_SIZE n_lines = 0;
    MD_SIZE i = 0;
    
    // 步骤1: 将输入文本分割成行数组
    //        注意:只是指针引用,不复制文本内容
    md_split_lines(ctx->text, ctx->size, &lines, &n_lines);
    
    // 步骤2: 逐行扫描,识别块类型
    while (i < n_lines) {
        MD_LINE* line = &lines[i];
        
        // 跳过空行
        if (line->size == 0) {
            i++;
            continue;
        }
        
        // 根据行特征识别块类型
        // 注意:检查顺序很重要!
        
        // 1. 检查是否为标题 (# Heading)
        if (md_is_heading(line, ctx->flags)) {
            md_parse_heading(ctx, line, &i, n_lines);
        }
        // 2. 检查是否为围栏代码块 (```js)
        else if (md_is_fenced_code(line, ctx->flags)) {
            md_parse_fenced_code(ctx, line, &i, n_lines);
        }
        // 3. 检查是否为引用块 (>)
        else if (md_is_blockquote(line)) {
            md_parse_blockquote(ctx, line, &i, n_lines);
        }
        // 4. 检查是否为列表项 (- item 或 1. item)
        else if (md_is_list_item(line, ctx->flags)) {
            md_parse_list(ctx, line, &i, n_lines);
        }
        // 5. 检查是否为表格
        else if (md_is_table(line, ctx->flags)) {
            md_parse_table(ctx, line, &i, n_lines);
        }
        // 6. 检查是否为水平线
        else if (md_is_hr(line)) {
            md_parse_hr(ctx);
            i++;
        }
        // 7. 检查是否为 MD4X frontmatter (---)
        else if (md_is_frontmatter(line)) {
            md_parse_frontmatter(ctx, line, &i, n_lines);
        }
        // 8. 检查是否为 MDC 组件块
        else if (md_is_mdc_block(line, ctx->flags)) {
            mdx_parse_mdc_block(ctx, line, &i, n_lines);
        }
        // 9. 其他情况作为段落处理
        else {
            md_parse_paragraph(ctx, line, &i, n_lines);
        }
    }
    
    // 步骤3: 释放行数组
    md_free(lines);
}

代码解释:

  • md_split_lines() - 将长文本分割成行数组,但只存储指针和长度,零拷贝设计
  • 识别顺序很重要 - 比如 # code 应该是标题而不是列表,所以先检查标题
  • &i 传递 - 使用指针传递索引,这样函数内可以修改外层的 i 值

3.3 标题解析函数详解

// src/md4x.c - 标题解析

// 标题识别:检查行首是否为 # 字符
static int md_is_heading(MD_LINE* line, unsigned flags) {
    // 检查是否可以识别 ATX 标题
    if (flags & MD_FLAG_PERMISSIVEAUTOLINKS)
        return 0;
    
    // 跳过围栏代码块标记 ```
    if (line->data[0] == '`')
        return 0;
    
    int level = 0;
    const MD_CHAR* p = line->data;
    
    // 统计连续的 # 字符数量(最多6个)
    while (*p == '#' && level < 6) {
        level++;
        p++;
    }
    
    // 有效的 ATX 标题:# 后面必须是空格或行结束
    // 例如: "# Hello" 有效,"##" 有效,"#Hello" 无效
    if (level > 0 && (p == line->data + line->size || *p == ' ')) {
        return level;  // 返回标题级别 1-6
    }
    
    return 0;  // 不是标题
}

// 标题解析:识别标题后调用此函数
static void md_parse_heading(MD_PARSER_CTX* ctx, MD_LINE* line, 
                              MD_SIZE* i, MD_SIZE n_lines) {
    
    // 1. 获取标题级别 (1-6)
    int level = md_is_heading(line, ctx->flags);
    
    // 2. 创建详细结构(传递给 enter_block 回调)
    MD_BLOCK_H_DETAIL detail = { 
        .level = (unsigned char)level 
    };
    
    // 3. 调用 enter_block 回调
    //    对于 # Hello,会调用 enter_block(MD_BLOCK_H, &detail)
    //    渲染器应该输出 <h1> 标签
    if (ctx.parser->enter_block) {
        ctx.parser->enter_block(MD_BLOCK_H, &detail, ctx.userdata);
    }
    
    // 4. 提取标题文本内容
    //    跳过 "### " 这部分,获取实际文本
    const MD_CHAR* text = line->data + level;
    MD_SIZE text_size = line->size - level;
    
    // 跳过 # 后的空格
    while (text_size > 0 && *text == ' ') {
        text++;
        text_size--;
    }
    
    // 5. 重要:对标题内容进行行内解析!
    //    标题内可以有 **加粗** *斜体* 等
    md_parse_inline(ctx, text, text_size);
    
    // 6. 调用 leave_block 回调
    //    渲染器应该输出 </h1> 标签
    if (ctx.parser->leave_block) {
        ctx.parser->leave_block(MD_BLOCK_H, &detail, ctx.userdata);
    }
    
    (*i)++;  // 移动到下一行
}

代码解释:

  • md_is_heading() - 核心逻辑:计算行首 # 数量,必须后面跟空格才有效
  • MD_BLOCK_H_DETAIL - 包含标题级别,渲染器用它来生成 <h1><h6> 标签
  • 行内解析是关键 - 标题内容如 **Hello** 需要再次解析,提取出加粗标记
  • 回调顺序 - enter_block → text → leave_block,渲染器按这个顺序生成 HTML

四、行内解析实现

4.1 行内解析流程图

输出回调

标记栈

行内解析器

输入

遇到 * 或 _

遇到 `

遇到 [

遇到 !

普通字符

块内容文本
Hello world!

md_parse_inline()

扫描下一个标记

处理强调/粗体
* _ ** __

处理代码
`

处理链接
[ ] ( )

处理图片
! [ ] ( )

处理其他字符

MD_MARK[]
栈结构

enter_span()

text()

leave_span()

4.2 行内解析核心代码

// src/md4x.c - 行内解析核心

// 行内标记结构 - 用于跟踪嵌套的标记
typedef struct MD_MARK {
    MD_SPANTYPE type;          // 标记类型 (MD_SPAN_EM, MD_SPAN_STRONG 等)
    const MD_CHAR* position;   // 标记在文本中的位置
    MD_CHAR marker;           // 标记字符 (*, _, `, [ 等)
    int marker_size;          // 标记长度
    int is_strong;            // 是否为强强调 (** 或 __)
} MD_MARK;

// 行内解析主函数
static void md_parse_inline(MD_PARSER_CTX* ctx, 
                           const MD_CHAR* text, MD_SIZE size) {
    
    // 标记栈:存储当前打开的标记
    MD_MARK* marks = NULL;
    MD_SIZE n_marks = 0;
    MD_SIZE marks_capacity = 0;
    
    const MD_CHAR* p = text;           // 当前扫描位置
    const MD_CHAR* end = text + size;   // 结束位置
    
    while (p < end) {
        
        // ========== 情况1: 转义字符 \ ==========
        if (*p == '\\' && p + 1 < end) {
            p++;  // 跳过 \
            // 输出转义后的字符(不作为标记处理)
            if (ctx.parser->text) {
                ctx.parser->text(MD_TEXT_NORMAL, p, 1, ctx.userdata);
            }
            p++;
            continue;
        }
        
        // ========== 情况2: 遇到行内标记字符 ==========
        // 检查是否为标记字符: * _ ` [ ! < ~ $
        if (md_is_marker_char(p)) {
            
            // 扫描并解析这个标记
            MD_MARK mark = {0};
            int consumed = md_scan_inline_mark(p, end - p, ctx->flags, &mark);
            
            if (consumed > 0) {
                // 在标记栈中查找匹配的结束标记
                int match_idx = md_find_matching_mark(marks, n_marks, &mark);
                
                if (match_idx >= 0) {
                    // ===== 找到匹配!这是结束标记 =====
                    // 例如: 遇到第二个 *,栈顶有开始的 *
                    
                    // 1. 输出标记之前的普通文本(如果有)
                    // (这个逻辑在循环中已处理)
                    
                    // 2. 调用 leave_span 关闭标记
                    if (ctx.parser->leave_span) {
                        ctx.parser->leave_span(marks[match_idx].type, 
                                             NULL, ctx.userdata);
                    }
                    
                    // 3. 如果是强强调 ** 或 __,需要关闭两个
                    if (mark.is_strong && match_idx > 0) {
                        if (ctx.parser->leave_span) {
                            ctx.parser->leave_span(marks[match_idx - 1].type,
                                                 NULL, ctx.userdata);
                        }
                        // 从栈中移除两个标记
                        n_marks = match_idx - 1;
                    } else {
                        // 从栈中移除一个标记
                        n_marks = match_idx;
                    }
                } else {
                    // ===== 没有匹配,这是开始标记 =====
                    // 例如: 遇到第一个 *,栈中没有匹配的 *
                    
                    // 1. 扩展栈容量(如需要)
                    if (n_marks >= marks_capacity) {
                        marks_capacity = marks_capacity > 0 ? marks_capacity * 2 : 16;
                        marks = md_realloc(marks, sizeof(MD_MARK) * marks_capacity);
                    }
                    
                    // 2. 标记入栈
                    marks[n_marks++] = mark;
                    
                    // 3. 调用 enter_span 打开标记
                    if (ctx.parser->enter_span) {
                        ctx.parser->enter_span(mark.type, NULL, ctx.userdata);
                    }
                }
                
                p += consumed;  // 移动到标记之后
                continue;
            }
        }
        
        // ========== 情况3: 普通字符 ==========
        // 找到下一个标记字符的位置
        const MD_CHAR* text_start = p;
        while (p < end && !md_is_marker_char(p) && !(*p == '\\')) {
            p++;
        }
        
        // 输出普通文本
        if (ctx.parser->text && p > text_start) {
            ctx.parser->text(MD_TEXT_NORMAL, text_start, p - text_start, 
                           ctx.userdata);
        }
    }
    
    // ========== 处理未关闭的标记 ==========
    // 例如: 输入 "**bold" 没有闭合的 **
    while (n_marks > 0) {
        n_marks--;
        if (ctx.parser->leave_span) {
            ctx.parser->leave_span(marks[n_marks].type, NULL, ctx.userdata);
        }
    }
    
    if (marks)
        md_free(marks);
}

代码解释:

  1. 标记栈 (marks) - 这是行内解析的核心数据结构,存储所有"打开"但未"关闭"的标记
  2. 两种情况 - 当遇到标记字符时:
    • 如果栈中已有匹配的未关闭标记 → 这是结束标记,调用 leave_span
      -如果没有匹配的 → 这是开始标记,入栈并调用 enter_span
  3. 强强调处理 - ** 需要作为两个标记处理,关闭时也要关闭两个
  4. 未关闭标记 - 解析结束时,栈中可能还有未关闭的标记,需要调用 leave_span 关闭它们

4.3 标记扫描代码

// src/md4x.c - 扫描行内标记

// 判断是否为标记字符
static int md_is_marker_char(const MD_CHAR* p) {
    switch (*p) {
        case '*':   // 强调/粗体
        case '_':   // 强调/粗体
        case '`':   // 行内代码
        case '[':   // 链接开始
        case '!':   // 图片开始
        case '<':   // HTML
        case '~':   // 删除线 ~~
        case '$':   // LaTeX
            return 1;
        default:
            return 0;
    }
}

// 扫描并解析行内标记
// 返回消费的字符数量
static int md_scan_inline_mark(const MD_CHAR* p, MD_SIZE remaining,
                               unsigned flags, MD_MARK* out_mark) {
    
    memset(out_mark, 0, sizeof(*out_mark));
    
    switch (*p) {
        // --- * 或 _ (强调/粗体) ---
        case '*':
        case '_': {
            // 检查是否为强强调 (** 或 __)
            int count = 0;
            while (count < 3 && p[count] == *p) {
                count++;
            }
            
            // 有效: 1个(*) 或 2个(**) 或 3个(***)
            if (count == 1 || count == 2) {
                out_mark->marker = *p;
                out_mark->marker_size = count;
                out_mark->is_strong = (count == 2);  // ** 是强强调
                out_mark->type = MD_SPAN_EM;         // 先作为 EM 处理
                if (out_mark->is_strong) {
                    out_mark->type = MD_SPAN_STRONG;  // 实际上是 STRONG
                }
                return count;
            }
            break;
        }
        
        // --- ` (行内代码) ---
        case '`': {
            // 统计连续的 ` 数量
            int count = 0;
            while (count < 4 && p[count] == '`') {
                count++;
            }
            
            out_mark->marker = '`';
            out_mark->marker_size = count;
            out_mark->type = MD_SPAN_CODE;
            return count;
        }
        
        // --- [ (链接) 或 ![ (图片) ---
        case '[':
        case '!': {
            // 特殊处理: 图片以 ![ 开头
            if (*p == '!') {
                if (p[1] == '[') {
                    out_mark->marker = '!';
                    out_mark->marker_size = 2;
                    out_mark->type = MD_SPAN_IMG;
                    return 2;
                }
            }
            
            // 普通链接 [
            out_mark->marker = '[';
            out_mark->marker_size = 1;
            out_mark->type = MD_SPAN_A;
            return 1;
        }
        
        // --- ~~ (删除线) ---
        case '~': {
            if (p[1] == '~') {
                out_mark->marker = '~';
                out_mark->marker_size = 2;
                out_mark->type = MD_SPAN_DEL;
                return 2;
            }
            break;
        }
        
        // --- $ (LaTeX) ---
        case '$': {
            // $ 是行内,$$ 是显示级
            if (p[1] == '$') {
                out_mark->marker = '$';
                out_mark->marker_size = 2;
                out_mark->type = MD_SPAN_LATEXMATH_DISPLAY;
                return 2;
            } else {
                out_mark->marker = '$';
                out_mark->marker_size = 1;
                out_mark->type = MD_SPAN_LATEXMATH;
                return 1;
            }
        }
    }
    
    return 0;  // 不是有效标记
}

// 在标记栈中查找匹配的结束标记
static int md_find_matching_mark(MD_MARK* marks, MD_SIZE n_marks,
                                 MD_MARK* new_mark) {
    
    // 从后往前查找(最近打开的优先匹配)
    for (int i = n_marks - 1; i >= 0; i--) {
        // 必须是相同类型的标记
        if (marks[i].type != new_mark->type)
            continue;
        
        // 标记字符必须相同 (* 匹配 *,不匹配 _)
        if (marks[i].marker != new_mark->marker)
            continue;
        
        // 强强调必须数量一致
        if (marks[i].is_strong != new_mark->is_strong)
            continue;
        
        // 找到匹配!
        return i;
    }
    
    return -1;  // 没有找到匹配
}

代码解释:

  1. md_is_marker_char() - 快速判断是否为标记字符,避免不必要的扫描

  2. md_scan_inline_mark() - 核心标记解析函数

    • 统计连续的相同字符数量(* → 1, ** → 2, *** → 3)
    • 设置标记类型(MD_SPAN_EM, MD_SPAN_STRONG, MD_SPAN_CODE 等)
    • 特殊处理:$ vs $$(行内 vs 显示级 LaTeX)
  3. md_find_matching_mark() - 从栈顶向下查找匹配

    • 必须类型相同
    • 必须标记字符相同(* 和 _ 不同)
    • 必须强强调状态一致(** 和 * 不匹配)

五、HTML 渲染器实现

5.1 渲染器结构

// src/renderers/md4x-html.h

typedef struct MD_HTML_RENDERER {
    // 输出函数:渲染器调用这个函数输出 HTML
    // userdata 会传递给这个函数
    void (*output)(const MD_CHAR* text, MD_SIZE size, void* userdata);
    void* output_userdata;
    
    // 渲染器选项
    unsigned flags;
    
    // 是否转义 HTML 特殊字符
    int escape;
    
    // 代码高亮器(可选)
    int (*highlighter)(const MD_CHAR* text, MD_SIZE size, 
                       const MD_BLOCK_CODE_DETAIL* detail, void* userdata);
    void* highlighter_userdata;
} MD_HTML_RENDERER;

5.2 块级回调实现

// src/renderers/md4x-html.c

// 进入块回调:遇到块开始时调用
static int md_html_enter_block(MD_BLOCKTYPE type, void* detail, 
                                void* userdata) {
    MD_HTML_RENDERER* r = (MD_HTML_RENDERER*)userdata;
    
    switch (type) {
        case MD_BLOCK_DOC:
            // 文档根,不输出任何标签
            break;
            
        case MD_BLOCK_P:
            // 段落:输出 <p>
            md_html_puts(r, "<p>");
            break;
            
        case MD_BLOCK_H: {
            // 标题:根据级别输出 <h1> - <h6>
            MD_BLOCK_H_DETAIL* d = (MD_BLOCK_H_DETAIL*)detail;
            char tag[4] = "h1";
            tag[1] = '0' + d->level;  // '1' + 0 = '1', '1' + 1 = '2' ...
            md_html_putc(r, '<');
            md_html_puts(r, tag);
            md_html_putc(r, '>');
            break;
        }
        
        case MD_BLOCK_CODE: {
            // 代码块:输出 <pre><code>
            MD_BLOCK_CODE_DETAIL* d = (MD_BLOCK_CODE_DETAIL*)detail;
            md_html_puts(r, "<pre><code");
            
            // 添加语言属性: <code class="language-js">
            if (d->lang_size > 0) {
                md_html_puts(r, " class=\"language-");
                md_html_write(r, d->lang, d->lang_size);
                md_html_putc(r, '"');
            }
            
            md_html_puts(r, ">");
            break;
        }
        
        case MD_BLOCK_UL:
            md_html_puts(r, "<ul>\n");
            break;
            
        case MD_BLOCK_OL:
            md_html_puts(r, "<ol>\n");
            break;
            
        case MD_BLOCK_LI:
            md_html_puts(r, "<li>");
            break;
            
        case MD_BLOCK_QUOTE:
            md_html_puts(r, "<blockquote>\n");
            break;
            
        case MD_BLOCK_TABLE: {
            md_html_puts(r, "<table>\n");
            break;
        }
        
        case MD_BLOCK_TR:
            md_html_puts(r, "<tr>");
            break;
            
        case MD_BLOCK_TH:
        case MD_BLOCK_TD: {
            // 表头/单元格
            const char* tag = (type == MD_BLOCK_TH) ? "th" : "td";
            md_html_putc(r, '<');
            md_html_puts(r, tag);
            
            // 处理对齐属性
            MD_BLOCK_TD_DETAIL* d = (MD_BLOCK_TD_DETAIL*)detail;
            if (d->align == MD_ALIGN_LEFT) {
                md_html_puts(r, " align=\"left\"");
            } else if (d->align == MD_ALIGN_CENTER) {
                md_html_puts(r, " align=\"center\"");
            } else if (d->align == MD_ALIGN_RIGHT) {
                md_html_puts(r, " align=\"right\"");
            }
            
            md_html_putc(r, '>');
            break;
        }
        
        case MD_BLOCK_HR:
            md_html_puts(r, "<hr />\n");
            break;
            
        default:
            break;
    }
    
    return 0;
}

// 离开块回调:遇到块结束时调用
static int md_html_leave_block(MD_BLOCKTYPE type, void* detail, 
                                void* userdata) {
    MD_HTML_RENDERER* r = (MD_HTML_RENDERER*)userdata;
    
    switch (type) {
        case MD_BLOCK_DOC:
            break;
            
        case MD_BLOCK_P:
            md_html_puts(r, "</p>\n");
            break;
            
        case MD_BLOCK_H: {
            MD_BLOCK_H_DETAIL* d = (MD_BLOCK_H_DETAIL*)detail;
            char tag[4] = "h1";
            tag[1] = '0' + d->level;
            md_html_putc(r, '<');
            md_html_putc(r, '/');
            md_html_puts(r, tag);
            md_html_puts(r, ">\n");
            break;
        }
        
        case MD_BLOCK_CODE:
            md_html_puts(r, "</code></pre>\n");
            break;
            
        case MD_BLOCK_UL:
            md_html_puts(r, "</ul>\n");
            break;
            
        case MD_BLOCK_OL:
            md_html_puts(r, "</ol>\n");
            break;
            
        case MD_BLOCK_LI:
            md_html_puts(r, "</li>\n");
            break;
            
        case MD_BLOCK_QUOTE:
            md_html_puts(r, "</blockquote>\n");
            break;
            
        case MD_BLOCK_TABLE:
            md_html_puts(r, "</table>\n");
            break;
            
        case MD_BLOCK_TR:
            md_html_puts(r, "</tr>\n");
            break;
            
        case MD_BLOCK_TH:
        case MD_BLOCK_TD: {
            const char* tag = (type == MD_BLOCK_TH) ? "th" : "td";
            md_html_putc(r, '<');
            md_html_putc(r, '/');
            md_html_puts(r, tag);
            md_html_putc(r, '>');
            break;
        }
        
        default:
            break;
    }
    
    return 0;
}

代码解释:

  1. enter_block - 输出开始标签,如 <h1>, <p>, <ul>
  2. leave_block - 输出结束标签,如 </h1>, </p>, </ul>
  3. detail 参数 - 包含块的详细信息,如标题的级别、代码块的语言等
  4. 注意格式 - 适当的换行和缩进使输出的 HTML 更可读

5.3 行内回调实现

// src/renderers/md4x-html.c

// 进入行内元素回调
static int md_html_enter_span(MD_SPANTYPE type, void* detail, 
                              void* userdata) {
    MD_HTML_RENDERER* r = (MD_HTML_RENDERER*)userdata;
    
    switch (type) {
        case MD_SPAN_STRONG:
            md_html_puts(r, "<strong>");
            break;
            
        case MD_SPAN_EM:
            md_html_puts(r, "<em>");
            break;
            
        case MD_SPAN_CODE:
            md_html_puts(r, "<code>");
            break;
            
        case MD_SPAN_A: {
            // 链接比较特殊,需要处理 href 和 title 属性
            MD_SPAN_A_DETAIL* d = (MD_SPAN_A_DETAIL*)detail;
            md_html_puts(r, "<a href=\"");
            md_html_write(r, d->href, d->href_size);
            md_html_putc(r, '"');
            
            if (d->title_size > 0) {
                md_html_puts(r, " title=\"");
                md_html_write(r, d->title, d->title_size);
                md_html_putc(r, '"');
            }
            
            md_html_putc(r, '>');
            break;
        }
        
        case MD_SPAN_IMG: {
            // 图片是自闭合的,输出完直接返回
            MD_SPAN_IMG_DETAIL* d = (MD_SPAN_IMG_DETAIL*)detail;
            md_html_puts(r, "<img src=\"");
            md_html_write(r, d->src, d->src_size);
            md_html_puts(r, "\" alt=\"");
            md_html_write(r, d->alt, d->alt_size);
            
            if (d->title_size > 0) {
                md_html_puts(r, "\" title=\"");
                md_html_write(r, d->title, d->title_size);
            }
            
            md_html_puts(r, "\" />");
            return 1;  // 返回 1 表示已处理,不需要 leave_span
        }
        
        case MD_SPAN_DEL:
            md_html_puts(r, "<del>");
            break;
            
        case MD_SPAN_U:
            md_html_puts(r, "<u>");
            break;
            
        case MD_SPAN_LATEXMATH:
            md_html_puts(r, "<span class=\"math\">");
            break;
            
        case MD_SPAN_LATEXMATH_DISPLAY:
            md_html_puts(r, "<div class=\"math-display\">");
            break;
            
        default:
            break;
    }
    
    return 0;  // 返回 0 表示需要调用 leave_span
}

// 离开行内元素回调
static int md_html_leave_span(MD_SPANTYPE type, void* detail, 
                              void* userdata) {
    MD_HTML_RENDERER* r = (MD_HTML_RENDERER*)userdata;
    
    switch (type) {
        case MD_SPAN_STRONG:
            md_html_puts(r, "</strong>");
            break;
            
        case MD_SPAN_EM:
            md_html_puts(r, "</em>");
            break;
            
        case MD_SPAN_CODE:
            md_html_puts(r, "</code>");
            break;
            
        case MD_SPAN_A:
            md_html_puts(r, "</a>");
            break;
            
        case MD_SPAN_DEL:
            md_html_puts(r, "</del>");
            break;
            
        case MD_SPAN_U:
            md_html_puts(r, "</u>");
            break;
            
        case MD_SPAN_LATEXMATH:
            md_html_puts(r, "</span>");
            break;
            
        case MD_SPAN_LATEXMATH_DISPLAY:
            md_html_puts(r, "</div>");
            break;
            
        default:
            break;
    }
    
    return 0;
}

5.4 文本回调和转义

// src/renderers/md4x-html.c

// 文本输出回调
static int md_html_text(MD_TEXTTYPE type, const MD_CHAR* text, 
                        MD_SIZE size, void* userdata) {
    MD_HTML_RENDERER* r = (MD_HTML_RENDERER*)userdata;
    
    switch (type) {
        case MD_TEXT_NORMAL:
        case MD_TEXT_SPACE:
            // 普通文本需要 HTML 转义
            // < → &lt;  > → &gt;  & → &amp;  " → &quot;
            if (r->escape) {
                md_html_escape(r, text, size);
            } else {
                md_html_write(r, text, size);
            }
            break;
            
        case MD_TEXT_CODE:
            // 代码文本通常不需要转义(代码块内容)
            md_html_write(r, text, size);
            break;
            
        case MD_TEXT_BR:
            // 硬换行
            md_html_puts(r, "<br />\n");
            break;
            
        case MD_TEXT_SOFTBR:
            // 软换行转为空格
            md_html_putc(r, ' ');
            break;
            
        case MD_TEXT_ENTITY:
            // HTML 实体直接输出
            md_html_write(r, text, size);
            break;
            
        case MD_TEXT_HTML:
            // 原始 HTML,根据设置决定是否转义
            if (r->escape) {
                md_html_escape(r, text, size);
            } else {
                md_html_write(r, text, size);
            }
            break;
            
        default:
            md_html_write(r, text, size);
            break;
    }
    
    return 0;
}

// HTML 转义实现
static void md_html_escape(MD_HTML_RENDERER* r, const MD_CHAR* text, 
                           MD_SIZE size) {
    const MD_CHAR* p = text;
    const MD_CHAR* end = text + size;
    
    while (p < end) {
        switch (*p) {
            case '&':
                md_html_puts(r, "&amp;");
                break;
            case '<':
                md_html_puts(r, "&lt;");
                break;
            case '>':
                md_html_puts(r, "&gt;");
                break;
            case '"':
                md_html_puts(r, "&quot;");
                break;
            case '\'':
                md_html_puts(r, "&#39;");
                break;
            default:
                md_html_putc(r, *p);
                break;
        }
        p++;
    }
}

六、完整使用示例

// 示例:如何手动使用 md4x 核心解析器

#include "md4x.h"
#include "md4x-html.h"
#include <stdio.h>
#include <string.h>

// 1. 定义输出函数
//    解析器通过这个函数输出结果
static void my_output(const MD_CHAR* text, MD_SIZE size, void* userdata) {
    FILE* f = (FILE*)userdata;
    fwrite(text, 1, size, f);
}

int main() {
    // 待解析的 Markdown
    const char* markdown = 
        "# Hello World\n\n"
        "This is **bold** and *italic* text.\n\n"
        "A [link](https://example.com \"Example Title\").\n\n"
        "```javascript\n"
        "const x = 1;\n"
        "console.log(x);\n"
        "```\n";
    
    // 2. 解析并输出 HTML
    int ret = md_html(
        markdown,                    // 输入 Markdown
        strlen(markdown),            // 长度
        my_output,                   // 输出函数
        stdout,                      // 输出目标(作为 userdata)
        MD_DIALECT_GITHUB,          // 使用 GitHub 方言
        0                            // 标志位
    );
    
    if (ret != 0) {
        printf("Error: %d\n", ret);
        return ret;
    }
    
    return 0;
}

输出结果:

<h1>Hello World</h1>
<p>This is <strong>bold</strong> and <em>italic</em> text.</p>
<p>A <a href="https://example.com" title="Example Title">link</a>.</p>
<pre><code class="language-javascript">
const x = 1;
console.log(x);
</code></pre>

七、MD4X 特有扩展

7.1 YAML Frontmatter

// MD4X 支持在 Markdown 开头使用 YAML frontmatter
// ---
// title: Hello
// author: John
// ---

// 解析时会被识别为 MD_BLOCK_FRONTMATTER 类型
typedef struct MD_BLOCK_FRONTMATTER_DETAIL {
    const MD_CHAR* data;     // YAML 内容
    MD_SIZE size;
    // 解析后的 JSON 对象
    void* metadata;          
} MD_BLOCK_FRONTMATTER_DETAIL;

7.2 MDC 组件

// MD4X 支持类似 Vue/React 的组件语法

// 块级组件
::Alert{type="warning"}
This is a warning message
::

// 行内组件
::Button{variant="primary"}Click me::

// 解析为 MD_BLOCK_COMPONENT 或 MD_SPAN_COMPONENT
typedef struct MD_BLOCK_COMPONENT_DETAIL {
    const MD_CHAR* name;       // "Alert"
    MD_SIZE name_size;
    const MD_CHAR* props;      // "type=\"warning\""
    MD_SIZE props_size;
} MD_BLOCK_COMPONENT_DETAIL;

7.3 其他扩展

// LaTeX 数学公式
// $E = mc^2$ → <span class="math">E = mc^2</span>
// $$\sum_{i=1}^n i$$ → <div class="math-display">...</div>

// Wiki 链接
// [[link]] → <a href="link">link</a>

// 下划线
// __underline__ → <u>underline</u>

// 行内属性
// [text]{.class #id} → <a class="class" id="id">text</a>

八、总结

MD4X/MD4C 的核心设计:

  1. SAX 推模型 - 通过回调函数实时推送解析事件,无需等待完整解析

  2. 双阶段解析 - 先块级后行内,分而治之

  3. 零拷贝 - 使用指针引用,避免不必要的数据复制

  4. 标记栈 - 用栈结构处理嵌套的行内元素

  5. 标志位控制 - 通过标志位组合控制启用哪些扩展

  6. 统一回调接口 - 一个接口支持多种渲染器

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐