jolicode/jolimarkdown

Markdown 文件样式统一修复工具。

v1.0.2 2024-08-01 11:52 UTC

This package is auto-updated.

Last update: 2024-09-01 12:04:39 UTC


README

用法

use JoliMarkdown\MarkdownFixer;

$markdown = <<<MARKDOWN
    # A sample Markdown document

    Some paragraph here with an image <img src="/image.png" alt="description" /> inside.
MARKDOWN;

$markdownFixer = new MarkdownFixer();
$fixedMarkdown = $markdownFixer->fix($markdown);

上面的代码将返回输入字符串的 "markdown化" 版本

# A sample Markdown document

Some paragraph here with an image ![description](/image.png) inside.

如果您正在使用 Symfony,您可能需要阅读相关捆绑包的文档

安装

composer require jolicode/jolimarkdown

配置

有几个配置选项可以作为League CommonMark环境配置选项使用,以自定义 Markdown 修复器的行为

use JoliMarkdown\MarkdownFixer;
use League\CommonMark\Environment\Environment;

$markdown = <<<MARKDOWN
    - some
    - list
MARKDOWN;

$markdownFixer = new MarkdownFixer(new Environment([
    'joli_markdown' => [
        'unordered_list_marker' => '*',
    ],
]));
$fixedMarkdown = $markdownFixer->fix($markdown);

// outputs:
// * some
// * list
  • internal_domains:一个包含网站内域名的数组。每当找到图像或链接 URL 时,该 URL 位于所列域名之一下,URL 将被转换为相对 URL。默认为 []
  • prefer_asterisk_over_underscore:一个布尔值,表示是否更喜欢 * 而不是 _ 用于强调。默认为 true
  • unordered_list_marker:用于无序列表标记的字符串。默认为 -

测试

测试位于 tests 目录下,并使用 PHPUnit 编写

castor qa:install
castor qa:phpunit

上下文

Markdown 是一种简单的文本语法,用于编写结构化文档。自 2004 年创建以来,这种语法旨在提供一种替代、更快、更简单的 HTML 文档编写方式,用于 Web 发布。在接下来的几年中,Markdown 语法迭代性地发展,没有正式、完美的标准化规范。已经出现了各种变体,但没有一种成为事实标准。

然而,最稳健的替代品之一是 CommonMark,这是一种于 2014 年正式指定的 Markdown 变体,自那时起一直在发展。

Markdown / Commonmark 经常在开发世界中使用(以 Markdown README 文件形式的文档、许多发布平台的采用)并且也经常用于 Web 发布。例如,当 JoliCode 网站 在 2012 年创建时,选择了这种语法,并且至今仍在用于结构化各种内容体(博客文章、客户参考、技术、团队表等)。

然而,在过去的 12 年里,我们转换 Markdown 内容到 HTML 的方式发生了变化:先写几篇纯 HTML 文章,然后使用 客户端 JavaScript Markdown 预处理器(在 Web 浏览器中),最后,在过去的几年里,迁移到 league/commonmark 库,该库允许您在 PHP 服务器端将 Markdown 转换为 HTML。选择这个库是因为它特别完整、维护良好、可扩展和稳健。

在开发 league/commonmark 的过程中,添加了扩展机制,以支持不同的 Markdown "扩展",即支持不属于 CommonMark 标准但为作者提供语法灵活性的语法元素。例如,表格扩展 使得在 Markdown 中以更轻、更易读的语法编写表格成为可能,这在 "标准" CommonMark 中是不可能的。

Markdown的一个基本特性是它与HTML的兼容性:在Markdown中,将HTML标签插入文本是完全可以的,这些标签将直接传递到最终的HTML文档中。例如,你可以写

# A Markdown document

<p>An HTML paragraph.</p>

A paragraph in Markdown.

这样的文档在HTML中渲染如下

<h1>A Markdown document</h1>
<p>A paragraph in HTML.</p>
<p>A paragraph in Markdown.</p>

因此,CommonMark的扩展机制非常有趣,因为它允许添加扩展的语法元素,这些元素可以由扩展解释以生成丰富、复杂的HTML输出,而无需最终用户(即编辑器)编写HTML。这种扩展的概念在CommonMark中有规定(CommonMark规范本身是使用CommonMark编写的,并且使用了扩展以生成Markdown语法和相应HTML输出的并排渲染,例如在标签页部分所看到的)。

在JoliCode网站上,我们利用了league/commonmark的灵活性,多年来不断丰富HTML渲染,以便我们可以编写更丰富、更具有表现力和更视觉化的Markdown文档。例如,我们添加了扩展以编写脚注、HTML表格、删除线文本,为外部链接添加HTML属性,自动为<img>标签添加属性等。

尽管如此,在过去的12年里,我们经常在Markdown文章中编写HTML代码以满足某些需求

  • 为HTML元素添加CSS类,以便能够以不同的方式对其进行样式化(例如,在页面上居中图像);
  • 插入带有CSS类的代码,以使用语法高亮库;
  • 创建HTML结构以使两个图像并排显示;
  • 等等。

有时添加HTML代码是因为文章的作者对Markdown的一些晦涩之处感到不适,并选择最直接的方法来发布其内容。当时使用HTML可能是合适的,但随着HTML提供的可能性发生变化,其局限性也随之变化:而对于用Markdown编写的元素,我们现在可以更新负责HTML渲染的程序以支持新的HTML功能,但我们不能这样做对于直接用HTML编写的元素,它们将永远停留在作者选择的形式上。

例如,我们希望能够提供比几年前使用的格式(如webp,它更小且质量更好)的现代、高性能图像。对于这些图像,我们还想摆脱使用<img>标签,并利用<picture><source>标签以及如srcset之类的属性。对于使用Markdown语法插入到文章中的图像,我们可以升级HTML渲染程序以支持这些新格式和标签。但对于在HTML中插入的图像,我们无法做到这一点,因此必须手动替换它们——或者保持原样,这会给用户带来不便,因为必须接受相关的文章使用过时、效率较低的技术,这会影响速度和网站用户提供的舒适度。

因此,我们正在寻找一种方法来纠正现有的Markdown文章,尽可能地将它们包含的HTML元素替换为等效的Markdown元素,而不会扭曲最终的HTML渲染。

一个扩展,已经存在于league/commonmark中几年了,可以专门帮助我们完成这项任务:这是属性扩展,它允许您向Markdown元素添加HTML属性。例如,您可以写

{.block-class}
![An image](/path/to/image.jpg)

![Another image](/path/to/image.jpg){.image-class}

这将按以下方式在HTML中渲染

<p class="block-class"><img src="/path/to/image.jpg" alt="Une image"></p>
<p><img src="/path/to/image.jpg" alt="Another image" class="image-class"></p>

借助这个扩展,我们希望编写一个程序,该程序可以对网站上每篇Markdown文章执行以下操作:

  • 分析文章的Markdown内容;
  • 识别可以替换为等效Markdown元素的HTML元素;
  • 将这些HTML元素替换为Markdown元素,并添加必要的HTML属性,以确保最终HTML渲染与原始文章相同。

此仓库提出了一种工具来实现这一目标,采用以下总体方法

  • 从现有的字符串(文章的初始Markdown内容)开始,使用league/commonmark Markdown解析器生成一个抽象语法树(AST)。该解析器经过特别配置,启用了很少的扩展,尽可能接近标准CommonMark语法,并生成一个仅包含(几乎)CommonMark基本语法元素的AST;
  • league/commonmark 解析器返回一个Document,这是一个Nodes(AST的一个元素)的层次结构。每个节点都是类型化的(例如,类型为ParagraphNode代表一个段落,类型为ImageNode代表一个图像等),HTML代码部分以HtmlBlockHtmlInline节点的形式解析;
  • 然后,通过一组修正类对这个文档进行修正。例如
    • 如果一个类型为FencedCode的节点有一个CSS类属性language-php,则删除此类,并将节点的Info属性更新为php值;
    • 如果一个类型为Image的节点有一个绝对URL,而图像是由JoliCode站点提供的,则此URL将被替换为相对URL。可以使用许多测试来检查特殊情况和不同情况。
    • HTML链接也是如此;
    • 对于HTMLBlockHTMLInline节点,采取特殊处理
      • 将HTML内容加载到DOM树中
      • 然后递归遍历此树,尝试重新构成等效的纯Markdown节点。例如,如果我们找到DOM中的<p>元素,我们将尝试将其替换为类型为Paragraph的Markdown节点。每次,都将HTML属性转换为league/commonmark "Attributes"扩展中提出的属性;
      • 作为最后的手段,如果在递归的某个级别上我们无法重新构成Markdown节点
        • 我们使用league/html-to-markdown库尝试将HTML转换为Markdown。这一步是必要的,因为我们需要将不支持我们实现的修正类的HTML元素转换为Markdown(例如,HTML表格:我们不提供用于DOM元素<table>的"Fixer")。
        • 将结果字符串返回为新的HTMLBlockHTMLInline节点,具体取决于顶级节点的类型;
  • 最后,作为最后一步,将修正后的新Document“渲染”为字符串,这是原始文章的修正Markdown内容。为此,编写了一组Renderer类,这些类受到了wnx/commonmark-markdown-renderer库的强烈启发。

许可证

此库受MIT许可证的约束。请参阅LICENSE文件。