第 37 章 / 共 51 章
卷五 · 扩展、运维与实战

md4c:Markdown 渲染

两个符号管好一件事:CommonMark 解析与 HTML 渲染。小而正确的选择——事件流驱动自定义渲染,一步渲染管好文档站。

本章导读

站位:工具型扩展的代表——不像 sqlite/xllm 那样承载业务,而是补齐"受控内容"的一块拼图。前置:build.bat md4c 变体。

37.1 定位

md4c 是符合 CommonMark 的解析器:不生成 DOM,不分配字符串——解析即事件流。这种极简性正好匹配 XServer 的脚本场景:输入是不可信内容时,渲染成受控 HTML 比执行模板安全得多(ch19 的受控区块思想)。两个导入符号(md_parse / md_html)就是全部 API 面。

37.2 事件流模型

md_parse 按文档顺序回调:块的进出(DOC/P/LI/CODE/TABLE…)、span 的进出(EM/STRONG/CODE_SPAN/LINK/IMG…)与文本片段。回调结构必须完整填充——空指针无保护(这是与多数库不同的硬约束);每个回调返回非零即中止解析。自定义渲染器(摘要提取、目录生成、纯文本化)都建立在这层。

最小渲染器骨架
static int enter_block(MD_BLOCKTYPE t, void* d, void* u)
{
    return 0;  /* 继续 */
}
/* leave_block / enter_span / leave_span / text 同形 */
MD_PARSER tParser = { 0, NULL, enter_block, leave_block,
                      enter_span, leave_span, text_cb };
md_parse(sDoc, iLen, &tParser, &tCtx);

37.3 一步渲染

md_html 内建完整回调,把 Markdown 渲染成 HTML 片段经输出回调交给你——签名与 md_parse 相同的心智。flag 开启扩展方言:表格、删除线、任务列表、维基链接。HTML 逃逸默认开启(原文里的标签会被转义),这是把不可信输入变安全输出的关键默认。

37.4 与模板层组合

典型管线:请求带来的 Markdown → md_html 渲染进缓冲 → 作为变量注入 ch19 模板的受控区块。文档字段存 Markdown、渲染在请求期完成——编辑体验与安全性兼得。静态文档站则可以在 Init 期一次性渲染(ch41 的热点缓存纪律)。

37.5 契约

关注要点
内存解析零分配;md_html 的输出经回调交付,无库侧持有缓冲
回调结构必须完整填充;返回非零中止(错误传播通道)
字符默认 char;定义 _UNICODE 时 MD_CHAR 为 WCHAR(TCC 脚本默认窄字符)
探测#ifdef XS_USE_MD4C 或 xsExtensionEnabled("md4c")

常见错误

零值回调结构直接用
症状
md_parse 传 {0} 的解析器,进程访问违例。
原因
回调指针无空保护——库会直接调用。要么填完整结构,要么用自带回调的 md_html。
渲染结果直拼页面
症状
渲染出的 HTML 直接 echo 进自制页面,脚本注入复活。
原因
md_html 逃逸的是 Markdown 原文里的标签;你自己在输出后追加的内容仍需走模板的转义规则(ch19)。

编程练习

基础渲染对比

同一份带表格与代码块的 Markdown 分别用默认与 GFM flag 渲染,对比输出。

验收:表格仅在 GFM 下成表;原文中的 HTML 标签被转义。
进阶目录生成器

基于 md_parse 事件流提取 H1-H3 生成目录树(锚点跳转)。

验收:任意文档的目录层级正确;深嵌套列表不干扰。
挑战文档路由

GET /docs/:name 读 Markdown 文件 → 渲染 → 注入模板受控区块,Init 期缓存。

验收:文件更新经热重载生效;恶意样本文档不产生可执行注入。

小结

要点结论
定位CommonMark 解析 + HTML 渲染,2 符号全 API
事件流md_parse 零分配回调式,自定义渲染的地基
一步渲染md_html 内建回调 + flag 方言 + 默认逃逸
组合Markdown 进模板受控区块;静态内容 Init 期渲染
硬约束回调结构必须完整;返回非零即中止
xrt 延伸阅读