CommonMark解析Markdown语法的原理和主要代码分析

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('_'));
        }
    }
}