xenocrat / chyrp-markdown
一个快速且可扩展的 Markdown 解析器,用 PHP 编写。
Requires
- php: >=8.0.0
- ext-ctype: *
- ext-pcre: *
- ext-reflection: *
- ext-spl: *
- lib-pcre: *
README
这是一组用于将 Markdown 转换为 HTML 的 PHP 类,注重速度和简单性。解析器可以通过向 Markdown 风格添加新的特质,或者通过定义一个基于基础解析器类的全新风格来扩展,以识别新元素。
当前支持以下 Markdown 风格
要求
- 需要 PHP 8.0+。
- UTF-8 是唯一支持的文本编码。
局限性
由于它注重速度,解析器在某些方面受到限制,导致其不完全符合 CommonMark 和 GFM 规范。目前,它能够通过 77% 的 CommonMark 测试用例和 76% 的 GFM 测试用例。
解析器的最明显限制包括
- 它不支持混合使用制表符和空格进行缩进;
- 它不识别跨越多行的 setext 标题;
- 它不允许在块引用或列表中使用“懒惰”的续行符。
使用方法
第一步是选择 Markdown 风格并实例化解析器
- CommonMark
$parser = new \xenocrat\markdown\Markdown();
- GitHub 风格 Markdown
$parser = new \xenocrat\markdown\GithubMarkdown();
- GitLab 风格 Markdown
$parser = new \xenocrat\markdown\GitlabMarkdown();
- Chyrp 风格 Markdown
$parser = new \xenocrat\markdown\ChyrpMarkdown();
下一步是调用解析器方法
- 使用
parse()
解析使用完整 Markdown 语言的文本; - 使用
parseParagraph()
仅解析文本中的内联元素。
下面是一些示例
// CommonMark; parse full text $parser = new \xenocrat\markdown\Markdown(); echo $parser->parse($markdown); // GFM $parser = new \xenocrat\markdown\GithubMarkdown(); echo $parser->parse($markdown); // GLM $parser = new \xenocrat\markdown\GitlabMarkdown(); echo $parser->parse($markdown); // CFM $parser = new \xenocrat\markdown\ChyrpMarkdown(); echo $parser->parse($markdown); // CommonMark; parse only inline elements (useful for one-line descriptions) $parser = new \xenocrat\markdown\Markdown(); echo $parser->parseParagraph($markdown);
在解析之前,您可以在解析器对象上设置以下选项之一
$parser->html5 = true
以启用 HTML5 输出而不是 HTML4。$parser->convertTabsToSpaces = true
在解析之前将所有制表符转换为 4 个空格。$parser->setContextId(string)
设置渲染上下文的标识字符串。$parser->maximumNestingLevel = int
设置要解析的嵌套元素的最大级别。$parser->maximumNestingLevelThrow = true
如果超出最大嵌套级别则抛出异常。$parser->keepListStartNumber = false
忽略有序列表的起始数字。$parser->keepReversedList = true
启用降序数字的有序列表。$parser->headlineAnchors = true
在渲染标题时添加 GitHub 风格的锚点。
对于 GitHub 风格 Markdown
$parser->enableNewlines = true
将文本中的所有换行符转换为<br/>
标签。$parser->renderCheckboxInputs = true
将任务项渲染为输入而不是表情符号。$parser->disallowedRawHTML = false
禁用 GFM 规范的第 6.11 节。
对于 GitLab 风格 Markdown
$parser->enableNewlines = true
将文本中的所有换行符转换为<br/>
标签。$parser->renderCheckboxInputs = true
将任务项渲染为输入而不是表情符号。$parser->renderFrontMatter = false
禁用将前导内容块渲染为代码。$parser->renderOrderedToc = true
将目录渲染为有序列表。
安全性考虑
由于设计上的原因,Markdown 允许在 Markdown 文本中包含 HTML,这意味着输入可能包含 JavaScript 和 CSS 样式。这使 Markdown 在创建不受 Markdown 语法限制的输出方面非常灵活,但如果您解析不受信任的输入,则存在安全风险(请参阅 XSS)。
GitHub风格的Markdown规范扩展了CommonMark,包括一个不允许的原始HTML扩展(第6.11节:Disallowed Raw HTML),它定义了在输出中过滤并作为文本渲染的原始HTML的子集。此解析器实现了GFM规范的第6.11节。
如果您正在解析用户输入或其他不受信任的输入,强烈建议您使用像HTML Purifier这样的工具处理生成的HTML,以过滤掉您选择不允许的所有元素。
扩展语言
Markdown由两种类型的语言元素组成,我们可以称它们为块元素和内联元素,类似于HTML中的<div>
和<span>
。块元素通常跨越多行,并由空白行分隔。最基本的块元素是段落(<p>
)。内联元素是指添加到块元素内部(即文本内部)的元素。
此Markdown解析器允许您通过更改现有元素的行为以及添加新的块元素和内联元素来扩展Markdown语言。您通过扩展解析器类,并添加/覆盖类方法和属性来完成此操作。对于不同的元素类型,有不同的扩展方式,您将在以下各节中看到。
添加块元素
Markdown按行解析以识别每行非空行作为块元素类型之一。为了将一行识别为块元素的开始,它调用所有以identify
开头命名的受保护类方法。如果识别方法已识别其负责的块元素,则返回true;如果该行不符合其要求,则返回false。
解析块元素分为三个步骤
-
识别负责解析块的函数,通过调用所有检测到的以
identify{blockName}()
命名的函数,直到其中一个返回true。 -
消费属于块的所有行,通过从识别的行开始迭代到发生结束条件。此步骤由名为
consume{blockName}()
的函数实现,其中{blockName}
与上述识别方法中使用的名称相同。消费方法还接受行数组和当前行的编号。它将返回两个参数:一个表示Markdown文档抽象语法树的块元素的数组,以及要解析的下一个行号。在抽象语法数组中,第一个元素指的是元素的名称,所有其他数组元素都可以由您自由定义。 -
渲染元素。在所有块都被消费之后,每个块都使用名为
render{elementName}()
的函数进行渲染,其中elementName
指的是抽象语法树中元素的名称。
添加内联元素
由于内联元素是使用文本中的字符串标记进行解析的,因此添加内联元素的方式与块元素不同。内联元素通过一个或多个字符的标记来识别,该标记表示内联元素的可能开始(例如,[
标记表示链接的可能开始,而`
标记表示可能的内联代码)。
解析内联元素分为两个步骤
-
解析内联元素的函数受保护,名称以
parse
开头。此外,还需要一个后缀为Markers
的匹配方法来为一个或多个标记注册解析方法。例如,parseEscape()
和parseEscapeMarkers()
。当在文本中找到其已注册的任何标记时,将调用解析方法。解析方法接受从标记位置开始的文本作为参数。解析方法将返回一个数组,包含要添加到抽象语法树中的元素和从输入中解析的文本的偏移量。从此偏移量之前的所有文本都将从Markdown中删除,然后再继续搜索下一个标记。 -
渲染元素。每个元素都使用方法
render{elementName}()
进行渲染,其中elementName
指的是抽象语法树中元素的名称。
创建自己的Markdown风格
此Markdown解析器由特性组成,因此通过添加和/或删除单个特性特性,创建自己的Markdown风格非常容易。
设计自己的Markdown风格包括四个步骤
- 选择一个要扩展的基类;
- 选择语言特性特性;
- 定义可转义字符;
- 可选地添加自定义渲染行为。
选择基类
如果您想扩展一个风格并添加功能,可以使用现有的类之一作为您的基类。如果您想定义Markdown语言的子集,即删除一些功能,您必须从Parser
扩展您的类。
选择语言特性特性
通常,仅添加带有use
的特性就足够了。在解析过程中,特性添加的块标识符按字母顺序排序并调用。如果您创建了一个用于解析必须早期识别的块类型的特性,这可能会成为问题。您可以通过在Markdown风格中定义属性blockPriorities
并提供预定义的调用顺序来破坏字母顺序排序/调用策略。任何在运行时检测到但未列在预定义调用顺序中的方法将在所有预定义方法之后按字母顺序调用。
如果您使用HeadlineTrait、LinkTrait或FootnoteTrait,实现prepare()
以在解析前重置变量可能很有用,以确保您获得可重用的解析器对象。
定义可转义字符
根据您选择实现的语言特性,必须定义不同的一组字符作为可转义的,使用反斜杠(\
)。解析器最初只定义反斜杠(\\
)为可转义的。
添加自定义渲染行为
您可以选择通过覆盖一些方法来调整渲染行为。有关不同规则的定义,这些规则定义了哪些元素可以中断一个段落,请参阅风格的consumeParagraph()
方法以获得灵感。
致谢
Carsten Brandt想感谢@erusev创建了Parsedown,这极大地影响了这项工作,并提供了基于行解析方法的想法。
作者
以下人员创建了此软件
- cebe/markdown: Carsten Brandt
- xenocrat/chyrp-markdown: Daniel Pimley
许可证
此软件是开源的,并使用MIT许可证授权。有关详细信息,请参阅LICENSE。