不能直接用正则解析 markdown,因其嵌套结构和跨行元素导致回溯失控;需按行状态驱动词法分析,先分类块级类型(如标题、代码块、引用块),行内格式延迟至渲染时处理,并用 lexerstate 管理上下文状态。

为什么不能直接用正则解析 Markdown
正则表达式在遇到嵌套结构(比如 **加粗里有*斜体*)或跨行元素(如代码块、引用块)时会迅速失控。很多初学者试图用一两个 std::regex 匹配所有 ## 标题 或 `inline code`,结果要么漏匹配,要么过度回溯导致卡死。Markdown 本质是上下文相关语言,词法分析必须按行推进、状态驱动。
从最简 Lexer 开始:只识别行首模式
真正能落地的起点不是“解析整个文档”,而是把输入按行切分后,对每行做类型标注——这是后续语法树构建的基础。关键不是识别内容细节,而是快速判定这一行属于什么块级类别:
-
HeaderLine:以 1–6 个#开头,后面跟空格和文字 -
CodeBlockStart:以```开头(可带语言标识) -
QuoteLine:以>+ 空格开头(注意要跳过前面可能的空白) -
ParagraphLine:其他非空行,默认为段落内容
示例判断逻辑(C++20):
std::string_view line = trim(line_raw); // 自定义 trim 去首尾空格
if (line.starts_with("```")) {
return TokenType::CodeBlockStart;
} else if (line.starts_with("# ")) {
return TokenType::Header1;
} else if (line.starts_with("## ")) {
return TokenType::Header2;
} else if (line.starts_with("> ")) {
return TokenType::QuoteLine;
} else if (!line.empty()) {
return TokenType::ParagraphLine;
}
行内标记怎么处理:延迟到渲染阶段再扫描
像 *斜体*、[链接](url) 这类行内格式,硬塞进词法分析器只会让状态机爆炸。正确做法是:Lexer 只输出「段落文本原始字符串」,把解析委托给独立的 parseInline() 函数,在需要渲染时才调用。这样既解耦,又避免提前处理未闭合标记(如段末孤零零的 *)。
常见坑:
- 不区分
_下划线_和*星号*的语义差异(实际标准中二者等价,但实现时容易漏判) - 忽略转义:用户写
\*不强调\*应原样显示,需先扫描反斜杠再匹配标记 - 链接语法中的括号嵌套:
[text](/path?x=1&y=2)—— 此处不能简单找第一个)
状态管理比算法更重要:用 struct 封装当前上下文
纯函数式逐行处理会丢失关键信息。比如进入 ```cpp 后,接下来所有行都应视为代码内容,直到再次遇到 ```;引用块也需记录嵌套层级。推荐用一个 LexerState struct 持有:
-
current_block_type(BlockType::Code, BlockType::Quote, BlockType::None) -
code_fence_lang(仅当在代码块中时有效) -
quote_depth(用于支持多层引用>> 内容)
每次读入新行,先根据 current_block_type 决定是否跳过行首模式匹配——例如在代码块中,## 标题 就不该被识别为标题。
这一步没做对,后续所有嵌套解析都会错位。很多人卡在“为什么引用块里的列表不生效”,其实是状态没切换过去。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











