AI生产式页面markdown渲染:MD4X 核心源码深度解析
·
MD4X 核心源码深度解析

一、整体架构流程图
二、核心数据结构
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, // 图片 
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 行内解析流程图
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);
}
代码解释:
- 标记栈 (marks) - 这是行内解析的核心数据结构,存储所有"打开"但未"关闭"的标记
- 两种情况 - 当遇到标记字符时:
- 如果栈中已有匹配的未关闭标记 → 这是结束标记,调用 leave_span
-如果没有匹配的 → 这是开始标记,入栈并调用 enter_span
- 如果栈中已有匹配的未关闭标记 → 这是结束标记,调用 leave_span
- 强强调处理 -
**需要作为两个标记处理,关闭时也要关闭两个 - 未关闭标记 - 解析结束时,栈中可能还有未关闭的标记,需要调用 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; // 没有找到匹配
}
代码解释:
-
md_is_marker_char() - 快速判断是否为标记字符,避免不必要的扫描
-
md_scan_inline_mark() - 核心标记解析函数
- 统计连续的相同字符数量(
*→ 1,**→ 2,***→ 3) - 设置标记类型(MD_SPAN_EM, MD_SPAN_STRONG, MD_SPAN_CODE 等)
- 特殊处理:$ vs $$(行内 vs 显示级 LaTeX)
- 统计连续的相同字符数量(
-
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;
}
代码解释:
- enter_block - 输出开始标签,如
<h1>,<p>,<ul> - leave_block - 输出结束标签,如
</h1>,</p>,</ul> - detail 参数 - 包含块的详细信息,如标题的级别、代码块的语言等
- 注意格式 - 适当的换行和缩进使输出的 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 转义
// < → < > → > & → & " → "
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, "&");
break;
case '<':
md_html_puts(r, "<");
break;
case '>':
md_html_puts(r, ">");
break;
case '"':
md_html_puts(r, """);
break;
case '\'':
md_html_puts(r, "'");
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 的核心设计:
-
SAX 推模型 - 通过回调函数实时推送解析事件,无需等待完整解析
-
双阶段解析 - 先块级后行内,分而治之
-
零拷贝 - 使用指针引用,避免不必要的数据复制
-
标记栈 - 用栈结构处理嵌套的行内元素
-
标志位控制 - 通过标志位组合控制启用哪些扩展
-
统一回调接口 - 一个接口支持多种渲染器
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)