cebe / markdown
一个超快、高度可扩展的PHP Markdown 解析器
Requires
- php: >=5.4.0
- lib-pcre: *
Requires (Dev)
- cebe/indent: *
- facebook/xhprof: *@dev
- phpunit/phpunit: 4.1.*
README
这是什么?
一组代表不同Markdown风格的PHP类,以及一个将Markdown文件转换为HTML文件的命令行工具。
实现重点是保持 快速(见 基准测试)和 可扩展。将Markdown转换为HTML就像调用一个方法(见 用法)一样简单,即使在不平凡的边缘情况下也能提供稳定的实现,并给出大多数预期的结果。
通过在类中添加新方法来扩展Markdown语言,以将Markdown文本转换为HTML的预期输出,这就像添加一个新方法一样简单。这不需要处理复杂且易出错的正则表达式。还可以通过Markdown文本的内部表示(抽象语法树)来挂钩Markdown结构并添加元素或读取元信息(见 扩展语言)。
目前支持以下Markdown风格
- 传统Markdown,根据http://daringfireball.net/projects/markdown/syntax(试试)。
- Github风格的Markdown,根据https://help.github.com/articles/github-flavored-markdown(试试)。
- Markdown Extra,根据http://michelf.ca/projects/php-markdown/extra/(目前不完全支持,WIP,见#25,试试)
- 任何您喜欢的混合Markdown风格,因为它具有高度可扩展的结构(见以下文档)。
未来的计划是支持
- Smarty Pants http://daringfireball.net/projects/smartypants/
- ...(欢迎建议更多功能!)
谁在使用它?
- 它为API文档和 definitive guide提供动力,该文档针对Yii Framework 2.0。
安装
需要PHP 5.4或更高版本才能使用。它还可以在Facebook的hhvm上运行。
该库使用PHPDoc注释来确定应解析的Markdown元素。因此,如果您正在使用PHP opcache
,请确保它不删除注释。
建议通过运行composer来安装。
composer require cebe/markdown "~1.2.0"
您也可以手动将以下内容添加到您的composer.json
文件中的require
部分。
"cebe/markdown": "~1.2.0"
之后运行composer update cebe/markdown
。
注意:如果您已配置PHP使用opcache,则需要启用opcache.save_comments选项,因为行内元素解析依赖于PHPdoc注释来查找声明的元素。
使用方法
在您的PHP项目中
要解析Markdown,您只需要两行代码。第一行是选择以下Markdown方言之一
- 传统Markdown:
$parser = new \cebe\markdown\Markdown();
- Github Flavored Markdown:
$parser = new \cebe\markdown\GithubMarkdown();
- Markdown Extra:
$parser = new \cebe\markdown\MarkdownExtra();
下一步是调用parse()
方法来使用完整Markdown语言解析文本,或调用parseParagraph()
方法来仅解析行内元素。
以下是一些示例
// traditional markdown and parse full text $parser = new \cebe\markdown\Markdown(); echo $parser->parse($markdown); // use github markdown $parser = new \cebe\markdown\GithubMarkdown(); echo $parser->parse($markdown); // use markdown extra $parser = new \cebe\markdown\MarkdownExtra(); echo $parser->parse($markdown); // parse only inline elements (useful for one-line descriptions) $parser = new \cebe\markdown\GithubMarkdown(); echo $parser->parseParagraph($markdown);
您可以选择在解析器对象上设置以下选项之一
对于所有Markdown方言
$parser->html5 = true
以启用HTML5输出而不是HTML4。$parser->keepListStartNumber = true
以启用保留Markdown中指定的有序列表的数字。默认行为是始终从1开始,并增加1,而不管Markdown中的数字是多少。
对于GithubMarkdown
$parser->enableNewlines = true
将所有换行符转换为<br/>
标签。默认情况下,只有带有两个前导空格的换行符才转换为<br/>
标签。
建议使用UTF-8编码输入字符串。其他编码可能也可以工作,但目前尚未测试。
命令行脚本
您可以使用它来渲染此readme文件
bin/markdown README.md > README.html
使用github flavored markdown
bin/markdown --flavor=gfm README.md > README.html
或使用unix管道将原始markdown描述转换为html
curl http://daringfireball.net/projects/markdown/syntax.text | bin/markdown > md.html
以下是运行bin/markdown --help
时将看到的完整帮助输出
PHP Markdown to HTML converter
------------------------------
by Carsten Brandt <[email protected]>
Usage:
bin/markdown [--flavor=<flavor>] [--full] [file.md]
--flavor specifies the markdown flavor to use. If omitted the original markdown by John Gruber [1] will be used.
Available flavors:
gfm - Github flavored markdown [2]
extra - Markdown Extra [3]
--full ouput a full HTML page with head and body. If not given, only the parsed markdown will be output.
--help shows this usage information.
If no file is specified input will be read from STDIN.
Examples:
Render a file with original markdown:
bin/markdown README.md > README.html
Render a file using gihtub flavored markdown:
bin/markdown --flavor=gfm README.md > README.html
Convert the original markdown description to html using STDIN:
curl http://daringfireball.net/projects/markdown/syntax.text | bin/markdown > md.html
[1] http://daringfireball.net/projects/markdown/syntax
[2] https://help.github.com/articles/github-flavored-markdown
[3] http://michelf.ca/projects/php-markdown/extra/
安全考虑
设计上,markdown 允许在markdown文本中包含HTML。这也意味着它可能包含JavaScript和CSS样式。这使得输出非常灵活,不受markdown语法的限制,但如果将用户输入作为markdown解析,则存在安全风险(见XSS)。
在这种情况下,您应该使用如HTML Purifier之类的工具处理markdown转换的结果,这些工具会过滤掉用户不允许添加的所有元素。
markdown允许的元素列表可以是
[ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'pre', 'code', 'blockquote', 'table', 'tr', 'td', 'th', 'thead', 'tbody', 'strong', 'em', 'b', 'i', 'u', 's', 'span', 'a', 'p', 'br', 'nobr', 'ul', 'ol', 'li', 'img', ],
允许的属性列表将是
['th.align', 'td.align', 'ol.start', 'code.class']
上述配置是一般建议,可能需要根据您的需求进行调整。
扩展
以下是一些对该库的扩展
- Bogardo/markdown-codepen - 短代码,用于在Markdown中嵌入来自http://codepen.io/的codepen。
- cebe/markdown-latex - 将Markdown转换为LaTeX和PDF
- softark/creole - Creole标记解析器
- hyn/frontmatter - 前置元数据支持(JSON, TOML, YAML)
- ... 添加你的项目!
扩展语言
Markdown由两种类型的语言元素组成,我将它们称为块级元素和行内元素,类似于HTML中的<div>
和<span>
。块级元素通常跨越多行,并由空行分隔。最基本的块级元素是段落(<p>
)。行内元素是添加在块级元素内部(即文本内部)的元素。
此Markdown解析器允许您通过更改现有元素的行为以及添加新的块级和行内元素来扩展Markdown语言。您可以通过扩展解析器类并添加/覆盖类方法和属性来完成此操作。对于不同的元素类型,有不同方式可以扩展它们,您将在以下部分中看到。
添加块级元素
Markdown按行解析以识别每行非空行作为块级元素类型之一。要识别块级元素的开始,它将调用所有以identify
开头的受保护类方法。如果标识函数已识别其负责的块级元素,则返回true;如果没有,则返回false。在以下示例中,我们将实现围栏代码块的支持,这是GitHub flavored markdown的一部分。
<?php class MyMarkdown extends \cebe\markdown\Markdown { protected function identifyFencedCode($line, $lines, $current) { // if a line starts with at least 3 backticks it is identified as a fenced code block if (strncmp($line, '```', 3) === 0) { return true; } return false; } // ... }
在上面的示例中,$line
是一个包含当前行内容的字符串,等于$lines[$current]
。您可以使用$lines
和$current
来检查除当前行之外的其它行。在大多数情况下,您可以忽略这些参数。
块级元素的解析分为两个步骤
-
消耗属于它的所有行。在大多数情况下,这是从识别的行开始迭代到出现空行为止的行。此步骤由名为
consume{blockName}()
的方法实现,其中{blockName}
与上面标识函数使用的名称相同。消耗方法还接受行数组和当前行的行号。它将返回两个参数:表示Markdown文档的抽象语法树中的块级元素的数组,以及要解析的下一个行号。在抽象语法树数组中,第一个元素是指元素的名称,所有其他数组元素可以由您自由定义。在我们的示例中,我们将实现如下protected function consumeFencedCode($lines, $current) { // create block array $block = [ 'fencedCode', 'content' => [], ]; $line = rtrim($lines[$current]); // detect language and fence length (can be more than 3 backticks) $fence = substr($line, 0, $pos = strrpos($line, '`') + 1); $language = substr($line, $pos); if (!empty($language)) { $block['language'] = $language; } // consume all lines until ``` for($i = $current + 1, $count = count($lines); $i < $count; $i++) { if (rtrim($line = $lines[$i]) !== $fence) { $block['content'][] = $line; } else { // stop consuming when code block is over break; } } return [$block, $i]; }
-
渲染元素。在消耗所有块级元素之后,它们将使用
render{elementName}()
-方法进行渲染,其中elementName
是指抽象语法树中元素的名称protected function renderFencedCode($block) { $class = isset($block['language']) ? ' class="language-' . $block['language'] . '"' : ''; return "<pre><code$class>" . htmlspecialchars(implode("\n", $block['content']) . "\n", ENT_NOQUOTES, 'UTF-8') . '</code></pre>'; }
您还可以在此处添加代码高亮。一般来说,也可以将输出渲染为HTML之外的语言,例如LaTeX。
添加行内元素
添加行内元素与块级元素不同,因为它们使用文本中的标记进行解析。行内元素由标记识别,该标记标记行内元素的开始(例如,[
将标记可能的链接开始,`
将标记行内代码)。
内联元素的解析方法也受到保护,并由前缀 parse
标识。此外,在PHPDoc中还需要一个 @marker
注解来注册一个或多个标记的解析函数。当在文本中找到标记时,将调用该方法。它以从标记位置开始的部分文本作为参数。解析方法将返回一个数组,包含抽象语法树中的元素和它已从输入的Markdown中解析出的文本偏移量。在下一个标记之前,此偏移量之前的所有文本都将从Markdown中删除。
例如,我们将添加对 github flavored markdown中的删除线 功能的支持
<?php class MyMarkdown extends \cebe\markdown\Markdown { /** * @marker ~~ */ protected function parseStrike($markdown) { // check whether the marker really represents a strikethrough (i.e. there is a closing ~~) if (preg_match('/^~~(.+?)~~/', $markdown, $matches)) { return [ // return the parsed tag as an element of the abstract syntax tree and call `parseInline()` to allow // other inline markdown elements inside this tag ['strike', $this->parseInline($matches[1])], // return the offset of the parsed text strlen($matches[0]) ]; } // in case we did not find a closing ~~ we just return the marker and skip 2 characters return [['text', '~~'], 2]; } // rendering is the same as for block elements, we turn the abstract syntax array into a string. protected function renderStrike($element) { return '<del>' . $this->renderAbsy($element[1]) . '</del>'; } }
自定义Markdown方言的编写
此Markdown库由特性组成,因此通过添加和/或删除单个特性特性,非常容易创建自己的Markdown方言。
设计Markdown方言的步骤包括四个
- 选择基类
- 选择语言特性特性
- 定义可转义的字符
- 可选:添加自定义渲染行为
选择基类
如果您想从一个方言扩展并仅添加功能,可以使用现有的类(Markdown
、GithubMarkdown
或 MarkdownExtra
)作为方言的基类。
如果您想定义Markdown语言的一个子集,即删除一些功能,您必须从 Parser
扩展您的类。
选择语言特性特性
以下显示了传统Markdown的特性选择。
class MyMarkdown extends Parser { // include block element parsing using traits use block\CodeTrait; use block\HeadlineTrait; use block\HtmlTrait { parseInlineHtml as private; } use block\ListTrait { // Check Ul List before headline identifyUl as protected identifyBUl; consumeUl as protected consumeBUl; } use block\QuoteTrait; use block\RuleTrait { // Check Hr before checking lists identifyHr as protected identifyAHr; consumeHr as protected consumeAHr; } // include inline element parsing using traits use inline\CodeTrait; use inline\EmphStrongTrait; use inline\LinkTrait; /** * @var boolean whether to format markup according to HTML5 spec. * Defaults to `false` which means that markup is formatted as HTML4. */ public $html5 = false; protected function prepare() { // reset references $this->references = []; } // ... }
通常,只需添加具有 use
的特性就足够了,但在某些情况下,可能需要一些微调以获得预期的解析结果。元素是按照其识别函数的字母顺序检测的。这意味着如果以 -
开头的行可以是列表或水平线,则必须通过重命名识别函数来设置优先级。这就是为什么将 identifyHr
重命名为 identifyAHr
和将 identifyBUl
重命名为 identifyBUl
的原因。消耗函数始终必须与识别函数具有相同的名称,因此这也必须重命名。
在解析 <
字符时也存在冲突。这可能是包含在 <
和 >
中的链接/电子邮件或内联HTML标签。为了在添加 LinkTrait
时解决此冲突,我们需要隐藏 HtmlTrait
的 parseInlineHtml
方法。
如果您使用任何使用 $html5
属性来调整其输出的特性,您还需要定义此属性。
如果您使用链接特性,实现如上所示的 prepare()
可能很有用,以在解析之前重置引用,以确保您得到一个可重用的对象。
定义可转义的字符
根据您选择的语言特性,可以使用不同的字符集来转义 \
。以下为传统Markdown的可转义字符集,您可以将其直接复制到您的类中。
/** * @var array these are "escapeable" characters. When using one of these prefixed with a * backslash, the character will be outputted without the backslash and is not interpreted * as markdown. */ protected $escapeCharacters = [ '\\', // backslash '`', // backtick '*', // asterisk '_', // underscore '{', '}', // curly braces '[', ']', // square brackets '(', ')', // parentheses '#', // hash mark '+', // plus sign '-', // minus sign (hyphen) '.', // dot '!', // exclamation mark '<', '>', ];
添加自定义渲染行为
您还可以通过重写某些方法来调整渲染行为。您可以参考 Markdown
和 GithubMarkdown
类中的 consumeParagraph()
方法以获得一些灵感,这些方法为哪些元素可以中断段落定义了不同的规则。
致谢
我想感谢 @erusev 为创建 Parsedown 做出贡献,它极大地影响了这项工作,并提供了基于行解析方法的思路。
常见问题解答
为什么还需要另一个Markdown解析器?
在审查PHP的Markdown解析器以选择一个用于与Yii框架2.0捆绑使用的过程中,我发现大多数实现都是使用正则表达式来替换模式,而不是进行真正的解析。因此,扩展它们以包含新的语言元素非常困难,因为你必须想出一个复杂的正则表达式,它匹配你的新增内容但不会与其他元素混淆。这种新增非常常见,如你在GitHub上所见,GitHub支持在评论中引用问题、用户和提交。一个真正的解析器应使用上下文感知方法遍历文本,并按找到的顺序解析标记。唯一一个我发现使用这种方法的实现是Parsedown,这表明这种实现比正则表达式方法快得多。Parsedown然而是一个专注于速度的实现,在一个类中实现了自己的版本(主要是GitHub风格的Markdown),并且在撰写本文时并不容易扩展。
鉴于上述情况,我决定开始自己的实现,使用Parsedown的解析方法,并使其可扩展,为每种Markdown风格创建一个类,这样它们就可以像Markdown语言那样相互扩展。这允许您在Markdown语言风格之间进行选择,同时也提供了一种方法来组合您自己的风格,从所有风格中挑选出最好的。我选择这种方法,因为它比使用回调向解析器注入功能更易于实现和更直观。
我在哪里报告错误或渲染问题?
只需在GitHub上创建一个问题,发布您的Markdown代码,并描述问题。您还可以附加渲染后的HTML结果的屏幕截图来描述您的问题。
我如何为这个库做出贡献?
请查看CONTRIBUTING.md文件以获取更多信息。
我是否可以自由使用这个库?
这个库是开源的,并使用MIT许可证。这意味着只要您提到我的名字并包含许可证文件,您可以随意使用它。请查看许可证以获取详细信息。