taufik-nurrohman / markdown
明显,这是一个Markdown解析器。
Requires
- php: >=7.1
This package is auto-updated.
Last update: 2024-09-16 16:28:08 UTC
README
与 CommonMark 0.31.2 规范保持 90% 的兼容性。
动机
我赞赏 Parsedown 项目,因为它简单快捷。它仅使用一个类文件将Markdown语法转换为HTML。然而,鉴于Parsedown项目随着时间的推移而活动减少,我假设它现在处于“功能完善”的状态。它仍然有一些需要修复的错误,并且随着 PHP 8.1 的最新发布,其中的一些PHP语法已经过时。
实际上,有一个 Parsedown 2.0 版本的草案,但它不再是一个单一的类文件。它被拆分为组件。我认为,目标是使添加功能变得容易,而不会破坏核心中的内容。对其他人来说,这可能非常有用,但我认为它与 CommonMark 提供的功能相似。因此,如果我想更新,可能更优的选择是直接切换到CommonMark。
我对这些事情不感兴趣。作为一个需要将Markdown语法转换为HTML的功能的人,这种灵活性对我来说完全没有必要。我只想将Markdown语法转换为HTML一次,然后继续。这可以通过 Parsedown 1.8 版本 实现,但它似乎已经不再得到积极维护。
这个项目的目标是将来在我的 Mecha 的Markdown扩展 中使用它。之前,我想要将这个转换器直接开发到扩展中,但我的朋友建议我将这个项目独立出来,因为它可能对其他开发者也具有潜在价值,而不仅仅是 Mecha CMS 开发者。
用法
此转换器可以使用 Composer 安装,但它不需要其他依赖项,只需使用Composer的自动包含文件功能。对于那些不使用Composer的人,应该能够直接将 from.php
和 to.php
文件包含到您的应用程序中而不会出现问题。
使用Composer
从命令行界面,导航到您的项目文件夹,然后运行以下命令
composer require taufik-nurrohman/markdown
在您的应用程序中引入生成的自动加载文件
<?php use function x\markdown\from as from_markdown; use function x\markdown\to as to_markdown; require 'vendor/autoload.php'; echo from_markdown('# asdf {#asdf}'); // Returns `'<h1 id="asdf">asdf</h1>'`
使用文件
在您的应用程序中引入 from.php
和 to.php
文件
<?php use function x\markdown\from as from_markdown; use function x\markdown\to as to_markdown; require 'from.php'; require 'to.php'; echo from_markdown('# asdf {#asdf}'); // Returns `'<h1 id="asdf">asdf</h1>'`
to.php
文件是可选的,用于将HTML转换为Markdown。如果您只想将Markdown转换为HTML,则不需要包含此文件。此功能是实验性的,并提供作为辅助功能,因为除了 json_decode()
函数外,还有 json_encode()
函数。Markdown结果可能无法满足每个人,但可以进行进一步讨论。
选项
/** * Convert Markdown string to HTML string. * * @param null|string $value Your Markdown string. * @param bool $block If this option is set to `false`, Markdown block syntax will be ignored. * @return null|string */ from(?string $value, bool $block = true): ?string;
/** * Convert HTML string to Markdown string. * * @param null|string $value Your HTML string. * @param bool $block If this option is set to `false`, HTML block syntax will be stripped out. * @return null|string */ to(?string $value, bool $block = true): ?string;
方言
随着时间的推移,Mecha的历史逐渐塑造了我的Markdown写作风格。Mecha使用的Markdown扩展最初是使用构建的,该扩展基于Michel Fortin的Markdown转换器(我认为这是基于PHP的Markdown转换器的第一个端口,最初是用Perl编写的,由John Gruber编写)。直到Mecha版本1.2.3的发布,我决定切换到Parsedown,因为当时它相当流行。它还可以使转换过程更快。Emanuil Rusev通过读取第一个字符来检测块类型的方法,在我看来,非常聪明且高效。
属性
我的Markdown转换器支持更广泛的属性语法,包括.class
和#id
属性语法的混合,以及key=value
属性语法的混合。
行内属性总是优先于原生语法属性和预定义属性。
强调
CommonMark的强调(和强强调)规范儿乎让我发疯!🤯
实现这一级别的严格性将会使项目更加缓慢地向稳定发布迈进。我实际上非常了解解析策略,但将其转化为最小化的PHP代码对我来说感觉非常困难。为了加快项目的完成速度,我决定降低强调(和强强调)规范的严格性。
它们将不完全遵循CommonMark的强调(和强强调)规范,但我保证HTML结果仍然有意义,尤其是对于那些从未阅读过规范的人。
规则1:只有当子强调的一边或两边以空白或标点符号开始和/或结束时,同类型的强调才能嵌套。
这将创建嵌套强调
这不会
规则2:对于强调类型不同的情况,规则1不适用。
规则3:对于强调标记不同的情况,规则1不适用。
规则4:为了使它成为一个有效的强调标记,打开分隔符后不能跟空白,关闭分隔符前不能有空白。
规则5:强调标记不能为空。
链接
相对链接和具有服务器主机名的绝对链接将被视为内部链接,否则将被视为外部链接,并自动获得rel="nofollow"
和target="_blank"
属性。
注释
注释遵循Markdown Extra的注释语法,但HTML输出略有不同,以匹配Mecha的常见命名风格。多行注释不需要像Markdown Extra要求的缩进四个空格。一个空格或制表符就足够继续注释。
软换行
软换行在非关键部分(如段落和列表项)中会被折叠成空格。
代码块
我试图避免不同Markdown方言之间的冲突,并尽量支持你使用的任何方言。例如,由于我最初使用Markdown Extra,我习惯于在代码块语法中使用点前缀添加信息字符串。这是Parsedown(或者更确切地说,Parsedown不关心给定信息字符串的模式,而是简单地将其添加到language-
前缀之后,因为CommonMark也没有为处理代码块语法中的信息字符串提供特别的规则)所不支持的行为。
下面是如何将代码块结果与每个Markdown转换器进行比较的
Markdown Extra
Parsedown Extra
我的
HTML 块
CommonMark不关心DOM,因此也不关心HTML元素是否平衡。与原始Markdown语法规范不同,该规范不允许在HTML块内转换Markdown语法,而CommonMark规范不限制这种情况。它关注的是看起来像HTML块标签的行周围的空白行,如第4.6节所述,类型6。
HTML块的开始和/或结束之后出现的任何文本都视为原始文本,不会被处理为Markdown语法。结束原始HTML块状态需要空白行。
类型1、2、3、4和5的例外。只需换行符就足以结束原始HTML块状态。
下面的示例将生成可预测的HTML代码,但这并不是因为此转换器关心现有HTML标签的平衡
当你将空白行添加到HTML块中的任何位置时,你将理解其中的原因
Markdown Extra在HTML上具有markdown
属性,允许你在HTML块中将Markdown语法转换为HTML。在此转换器中,此功能将不起作用。目前,我没有计划添加此功能,以尽可能避免DOM解析任务。这也确保我不会使用PHP dom
。
然而,如果你添加空白行,它就像功能一样工作(尽管markdown
属性仍然存在,但它不会影响浏览器窗口中的渲染)。如果你习惯于在HTML块标签的开头和结尾之后添加空白行,你应该没问题。
除非开标签和闭标签独立占一行,否则打开内联HTML元素不会触发原始HTML块状态。这在第4.6节中解释
由于CommonMark不关心HTML结构,下面的示例也将符合规范,即使它们导致HTML损坏。然而,这些情况很少会故意手动编写,因此这种情况很少发生
图片块
Markdown是在HTML5时代之前开始的。当<figure>
元素被引入时,人们开始将其用作显示带有标题的图片的功能。大多数Markdown转换器会将单独在一行上的图像语法转换为包含在段落元素中的图像元素。我的转换器会将其包裹在figure元素中。因为现在看来,figure元素在这种情况下更受欢迎。
在其下方出现的段落如果前面有少于4个空格,将被视为图片标题。
请注意,此模式也应适用于普通Markdown文件。因此,当其他Markdown转换器解析时,它将优雅地降级。
列表块
列表块遵循CommonMark规范,有一个例外:如果下一个有序列表项使用的数字小于上一个有序列表项的数字,则将创建一个新的列表块。这与原始规范不同,原始规范不关心数字的绝对值。
表格块
表格块遵循Markdown Extra的表格块语法。然而,有一些额外的功能和规则
- 实际列数遵循表头分隔符中的列数。如果表头和/或表数据中的列数超过了实际列数,将丢弃多余的列。如果表头和/或表数据中的列数少于实际列数,系统将自动在右侧添加几个空列。
- 表格列中的字面量竖线字符必须转义。例外情况是出现在代码跨度内和原始HTML标签的属性值中的那些。
- 支持无标题表格,但可能与其他Markdown转换器不兼容。尽可能少地使用此功能,除非您没有计划在未来切换到其他Markdown转换器。
- 支持表格标题,可以使用与图像块标题语法相同的语法创建。
跨站脚本攻击(XSS)
此转换器旨在仅根据CommonMark规范将Markdown语法转换为HTML。它不关心您的用户输入。我无意在未来添加任何特殊的安全功能,对此表示歉意。如果您想在评论条目中使用此转换器,属性语法功能可能是一个安全风险
应该已经有许多专门处理XSS的PHP应用程序,因此考虑在将其发布到网络上之前对生成的HTML标记进行后处理
测试
将此存储库克隆到支持PHP的Web服务器的根目录中,然后您可以使用浏览器打开test/from.php
和test/to.php
文件,以查看各种情况下此转换器的结果和性能。
调整
由于各种原因,并不支持所有的Markdown方言。以下的一些修改方法可以实现添加您可能在其他Markdown转换器中找到的功能。
您的Markdown内容表示为变量$value
。如果在调用函数from_markdown()
之前修改了内容,则表示您在转换之前修改了Markdown内容。如果在调用函数from_markdown()
之后修改了内容,则表示您修改了Markdown转换的结果。
全局可重用函数
要使from_markdown()
和to_markdown()
函数可全局重用,请使用此方法
<?php require 'from.php'; require 'to.php'; // Or, if you are using Composer… // require 'vendor/autoload.php'; function from_markdown(...$v) { return x\markdown\from(...$v); } function to_markdown(...$v) { return x\markdown\to(...$v); }
从XHTML到HTML5
此转换器会转义无效的HTML元素,并注意您在Markdown属性语法中放入的HTML特殊字符,因此可以从Markdown转换的结果中直接将' />'
替换为'>'
$value = from_markdown($value); $value = strtr($value, [' />' => '>']); echo $value;
删除线
此方法允许您添加删除线语法,正如您在GFM规范中已经注意到的
$value = from_markdown($value); $value = preg_replace('/((?<![~])[~]{1,2}(?![~]))([^~]+)\1/', '<del>$2</del>', $value); echo $value;
任务列表
我反对任务列表功能,因为它鼓励滥用表单输入元素的不良做法。虽然在表现层上它正确地显示了复选框界面,但我仍然认为输入元素理想情况下应该用于表单元素内部。有几个Unicode符号更适合,并且从Markdown源代码中更容易阅读,例如☐和☒,这意味着这个功能实际上可以使用现有的列表功能来实现。
- ☒ asdf - ☐ asdf - ☐ asdf
如果你需要它,或者不想更新你的Markdown文件中的现有任务列表语法,这里有一个技巧
$value = from_markdown($value); $value = strtr($value, [ '<li><p>[ ] ' => '<li><p>☐ ', '<li><p>[x] ' => '<li><p>☒ ', '<li>[ ] ' => '<li>☐ ', '<li>[x] ' => '<li>☒ ' ]); echo $value;
预定义缩写、注释和参考
通过在Markdown内容的末尾插入缩写、注释和参考,就好像你有预定义的缩写、注释和参考功能。这应该放在Markdown内容的末尾,因为根据链接参考定义规范,首先声明的参考始终具有优先级。
$abbreviations = [ 'CSS' => 'Cascading Style Sheet', 'HTML' => 'Hyper Text Markup Language', 'JS' => 'JavaScript' ]; $references = [ 'mecha-cms' => ['https://github.com/mecha-cms', 'Mecha CMS', []], 'taufik-nurrohman' => ['https://github.com/taufik-nurrohman', 'Taufik Nurrohman', []], ]; $suffix = ""; if (!empty($abbreviations)) { foreach ($abbreviations as $k => $v) { $k = strtr($k, [ '[' => '\[', ']' => '\]' ]); $v = trim(preg_replace('/\s+/', ' ', $v)); $suffix .= "\n*[" . $k . ']: ' . $v; } } if (!empty($references)) { foreach ($references as $k => $v) { [$link, $title, $attributes] = $v; $k = strtr($k, [ '[' => '\[', ']' => '\]' ]); if ("" === $link || false !== strpos($link, ' ')) { $link = '<' . $link . '>'; } $reference = '[' . $k . ']: ' . $link; if (!empty($title)) { $reference .= " '" . strtr($title, ["'" => "\\'"]) . "'"; } if (!empty($attributes)) { foreach ($attributes as $kk => &$vv) { // `{.asdf}` if ('class' === $kk) { $vv = '.' . trim(preg_replace('/\s+/', '.', $vv)); continue; } // `{#asdf}` if ('id' === $kk) { $vv = '#' . $vv; continue; } // `{asdf}` if (true === $vv) { $vv = $kk; continue; } // `{asdf=""}` if ("" === $vv) { $vv = $kk . '=""'; continue; } // `{asdf='asdf'}` $vv = $kk . "='" . strtr($vv, ["'" => "\\'"]) . "'"; } unset($vv); sort($attributes); $attributes = trim(strtr(implode(' ', $attributes), [ ' #' => '#', ' .' => '.' ])); $reference .= ' {' . $attributes . '}'; } $suffix .= "\n" . $reference; } } $value = from_markdown($value . "\n" . $suffix); echo $value;
预定义标题的ID
如果未设置,为2级到6级的标题添加自动id
属性,然后在其前面添加一个指向它的锚点元素
$value = from_markdown($value); if ($value && false !== strpos($value, '</h')) { $value = preg_replace_callback('/<(h[2-6])(\s(?>"[^"]*"|\'[^\']*\'|[^>])*)?>([\s\S]+?)<\/\1>/', static function ($m) { if (!empty($m[2]) && false !== strpos($m[2], 'id=') && preg_match('/\bid=("[^"]+"|\'[^\']+\'|[^\/>\s]+)/', $m[2], $n)) { if ('"' === $n[1][0] && '"' === substr($n[1], -1)) { $id = substr($n[1], 1, -1); } else if ("'" === $n[1][0] && "'" === substr($n[1], -1)) { $id = substr($n[1], 1, -1); } else { $id = $n[1]; } $m[3] = '<a href="#' . htmlspecialchars($id) . '" style="text-decoration: none;">⚓</a> ' . $m[3]; return '<' . $m[1] . $m[2] . '>' . $m[3] . '</' . $m[1] . '>'; } $id = trim(preg_replace('/[^a-z\x{4e00}-\x{9fa5}\d]+/u', '-', strtolower($m[3])), '-'); $m[3] = '<a href="#' . htmlspecialchars($id) . '" style="text-decoration: none;">⚓</a> ' . $m[3]; return '<' . $m[1] . ($m[2] ?? "") . ' id="' . htmlspecialchars($id) . '">' . $m[3] . '</' . $m[1] . '>'; }, $value); } echo $value;
想法:嵌入语法
CommonMark自动链接规范并不限制特定类型的URL协议。它只指定了模式,因此我们可以利用自动链接语法将其渲染为一种“嵌入”语法,然后将其转换为HTML元素块。
我相信这个想法以前从未被做过,这就是为什么我想第一个提到它。但我不打算直接将其集成到我的转换器中,以保持其精简。我只是想给你一些建议。
请注意,这些调整非常简单,因为它们将直接转换“嵌入”语法,而不考虑块类型。你可能需要使用这个过滤器仅在某些块类型中替换“嵌入”语法,例如,忽略带有“嵌入”语法的fenced代码块语法。
YouTube视频嵌入
通过视频ID显示YouTube视频的嵌入语法。
<youtube:dQw4w9WgXcQ>
$value = preg_replace('/^[ ]{0,3}<youtube:([^>]+)>\s*$/m', '<iframe src="https://www.youtube.com/embed/$1"></iframe>', $value); $value = from_markdown($value); echo $value;
GitHub Gist嵌入
通过gist ID显示GitHub gist的嵌入语法。
<gist:9c96049ca6c66e30e50793f5aef4818b>
$value = preg_replace('/^[ ]{0,3}<gist:([^>]+)>\s*$/m', '<script src="https://gist.github.com/taufik-nurrohman/$1.js"></script>', $value); $value = from_markdown($value); echo $value;
表单嵌入
通过服务器端生成的具有引用ID 18a4596d42c
和 title
参数来自定义HTML表单标题的HTML表单的嵌入语法。
<form:18a4596d42c?title=Form+Title>
$value = preg_replace_callback('/^[ ]{0,3}<form:([^#>?]+)([?][^#>]*)?([#][^>]*)?>\s*$/m', static function ($m) { $path = $m[1]; $value = ""; parse_str(substr($m[2] ?? "", 1), $state); $value .= '<form action="/form/' . $path . '" method="post">'; if (!empty($state['title'])) { $value .= '<h1>' . $state['title'] . '</h1>'; } // … etc. // Be careful not to include blank line(s), or the raw HTML block state will end before the HTML form is complete! $value .= '</form>'; return $value; }, $value); $value = from_markdown($value); echo $value;
想法:注释块
有几个人讨论了这个功能,我认为我非常喜欢这个答案。语法与原生Markdown语法兼容,这在Markdown源代码中直接查看时看起来很棒,即使它在渲染到HTML时仍然可以接受。
------------------------------ **NOTE:** asdf asdf asdf ------------------------------
------------------------------ **NOTE:** asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf ------------------------------
大多数Markdown转换器都会将其渲染为上面的HTML,尽管其语义不完整,但其表现层仍然可以被视为注释块。
<hr /><p><strong>NOTE:</strong> asdf asdf asdf</p><hr />
<hr /><p><strong>NOTE:</strong></p><p>asdf asdf asdf asdf asdf asdf asdf asdf</p><p>asdf asdf asdf asdf</p><hr />
使用正则表达式,你可以改善其语义。
$value = from_markdown($value); $value = preg_replace_callback('/<hr\s*\/?>(<p><strong>NOTE:<\/strong>[\s\S]*?<\/p>)<hr\s*\/?>/', static function ($m) { return '<div role="note">' . $m[1] . '</div>'; }, $value); echo $value;
许可
此库受MIT许可许可。如果您从本库中获得经济利益,请考虑捐赠💰。
链接
- 秋天图片示例由@blmiers2提供
- 表情符号图片示例由@emoticons4u(网络存档)提供