CommonMark解析Markdown语法的原理和主要代码分析
- 作者: 刘杰
- 来源: 技术那些事
- 阅读:80
- 发布: 2025-08-07 18:48
- 最后更新: 2025-08-07 18:48
CommonMark 的原理
CommonMark 的解析过程,本质是将无序的文本字符串转化为结构化的抽象语法树(AST),核心围绕 “识别结构” 和 “建立层级” 展开。
首先从文本的 “块级结构” 入手。解析器逐行扫描文本,根据行首特征判断块类型 —— 比如以#
开头的标题、以>
开头的引用、缩进 4 个空格的代码块,或是连续换行形成的段落。这些块并非孤立存在,比如引用内部可能嵌套列表,列表项又可能包含段落,解析器会通过 “容器块” 机制记录这种嵌套关系,像引用块会作为容器,收纳内部的其他块。
块级结构确定后,再处理块内部的 “内联元素”。比如段落中的*斜体*
、**粗体**
、[链接](url)
等,解析器会对块内文本做二次扫描,识别这些内联标记,将其拆分为文本节点、强调节点、链接节点等,并嵌入对应的块级节点中。内联解析需处理标记的嵌套和转义(比如\*
会被识别为普通星号),避免歧义。
整个过程依赖 “状态机” 驱动。解析器始终维护当前的解析状态(比如是否处于列表中、是否在引用内),每读取一行或一个字符,就根据状态和语法规则切换状态,同时生成对应的 AST 节点。最终,AST 完整反映了文本的结构逻辑,为后续渲染(如转为 HTML)提供了清晰的依据。
这种解析方式严格遵循 CommonMark 规范,确保不同实现对同一份文本的解析结果一致,解决了传统 Markdown 语法模糊导致的兼容性问题。
类图和主要解析器说明
以下是 CommonMark 主要代码结构类图:
classDiagram
direction BT
class BlockContinueParserInterface {
<<interface>>
+ AbstractBlock getBlock() // 返回被当前解析器解析的容器块
+ bool isContainer() // 是否当前被解析的是一个容器块
+ bool canContain(AbstractBlock) // 是否包含给定的子块
+ BlockContinue tryContinue(Cursor, BlockContinueParserInterface) // 解析当前块
+ void addLine(string) // 给当前块增加文本行
+ void closeBlock() // 关闭结束当前块
}
class BlockStartParserInterface {
<<interface>>
+ BlockStart tryStart(Cursor, MarkdownParserStateInterface) // 当前位置是否应该处理当前块
}
class AbstractBlockContinueParser {
}
class BlockContinue
class BlockStart {
- BlockContinueParserInterface[] blockParsers
}
class MarkdownParser {
+ Document parser(string)
}
class Environment {
- BlockStartParserInterface[] blockStartParsers
}
class Document
class Node {
<<abstract>>
}
class AbstractBlock
Environment --o BlockStartParserInterface
Document --|> AbstractBlock
AbstractBlockContinueParser ..|> BlockContinueParserInterface
BlockStart --o BlockContinueParserInterface
BlockStartParserInterface ..> BlockStart
BlockContinueParserInterface ..> BlockContinue
MarkdownParser --> BlockContinueParserInterface
MarkdownParser ..> Document
AbstractBlock --|> Node
Node --o Node
解析器分为以下几种:
-
BlockStartParserInterface
用来识别开始的一串标识是否当前是 Markdown 的某个块
-
BlockContinueParserInterface
用来解析当前的某个Markdown块的内容
-
BlockContinueParserWithInlinesInterface
用来解析当前 Markdown 块中的 inlines 元素
-
BlockStart(BlockStartParserInterface 类型解析器的匹配结果)
表示开始解析一个块的结果数据对象,返回此对象,表示开始进入了某个块,此对象主要存储了 BlockContinueParserInterface 解析器,和当前的指针(位置)状态信息。
-
BlockContinue(BlockContinueParserInterface 类型解析器的匹配结果)
表示继续解析一个块的结果数据,返回此对象,表示可以继续解析此块
解析主流程
MarkdownParser->parse()
通过逐行对 Markdown 的源码进行解析。每行解析的时候,会先匹配预先在 Environment->addBlockStartParser()
添加的块开始标识符解析器。按照优先级从高到低,一旦匹配到一个块开始解析器(如:HeadingStartParser),则会将包含在块开始解析器中的块解析器(BlockContinueParser)取出来,将其对应的 Block(BlockContinueParser->getBlock()
)添加到其父解析器的 Block中。然后递归的解析,直到此行中没有匹配的解析器,此行语法解析完毕,会进行下一行的语法解析。
当然其中还有一些细节,需要通过分析源码才能看明白。但是主要的流程就是如此。
最后解析完毕后,会得到一个 Document 结构的对象。此对象就是通过markdown语法生成的语法树对象。
php
class CommonMarkCoreExtension {
public function configureSchema(ConfigurationBuilderInterface $builder): void
{
$builder->addSchema('commonmark', Expect::structure([
'use_asterisk' => Expect::bool(true),
'use_underscore' => Expect::bool(true),
'enable_strong' => Expect::bool(true),
'enable_em' => Expect::bool(true),
'unordered_list_markers' => Expect::listOf('string')
->min(1)
->default(['*', '+', '-'])
->mergeDefaults(false)
])
);
}
public function register(EnvironmentBuilderInterface $environment): void
{
$environment
// 添加块开始解析器,在 MarkdownParser 进行逐行解析的时候,每行都会逐个匹配
->addBlockStartParser(new Parser\Block\BlockQuoteStartParser(), 70)
->addBlockStartParser(new Parser\Block\HeadingStartParser(), 60)
->addBlockStartParser(new Parser\Block\FencedCodeStartParser(), 50)
->addBlockStartParser(new Parser\Block\HtmlBlockStartParser(), 40)
->addBlockStartParser(new Parser\Block\ThematicBreakStartParser(), 20)
->addBlockStartParser(new Parser\Block\ListBlockStartParser(), 10)
->addBlockStartParser(new Parser\Block\IndentedCodeStartParser(), -100)
->addInlineParser(new CoreParser\Inline\NewlineParser(), 200)
->addInlineParser(new Parser\Inline\BacktickParser(), 150)
->addInlineParser(new Parser\Inline\EscapableParser(), 80)
->addInlineParser(new Parser\Inline\EntityParser(), 70)
->addInlineParser(new Parser\Inline\AutolinkParser(), 50)
->addInlineParser(new Parser\Inline\HtmlInlineParser(), 40)
->addInlineParser(new Parser\Inline\CloseBracketParser(), 30)
->addInlineParser(new Parser\Inline\OpenBracketParser(), 20)
->addInlineParser(new Parser\Inline\BangParser(), 10)
// 以下省略大部分对 Block 和关联的渲染器的代码
->addRenderer(
Node\Block\BlockQuote::class,
new Renderer\Block\BlockQuoteRenderer(),
0
)->.....
if ($environment->getConfiguration()->get('commonmark/use_asterisk')) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*'));
}
if ($environment->getConfiguration()->get('commonmark/use_underscore')) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_'));
}
}
}