渲染Markdown中的Blade组件- Aaron Francis

渲染Markdown中的Blade组件- Aaron Francis

你正在阅读的这个网站是一个基本的Laravel应用程序,只有几个附加包。所有的文章(包括这篇文章!)都是用Markdown编写的,然后使用CommonMark PHP库 (opens new window)Laravel-Markdown (opens new window) Laravel shim进行渲染。

我喜欢Markdown的写作体验和简单性,但有时我需要跳出Markdown并使用一些更加花哨的东西,比如这个嵌入的推文:

Aaron Francis

@aarondfrancis

完成项目的90%最好的部分是你已经完成了一半!

这个嵌入的推文是一个blade组件 (opens new window),我可以在.blade文件中像这样使用:

<x-tweet url='https://twitter.com/aarondfrancis/status/1705211030882684946'>    The best part about being 以下是90%完成的一个项目几乎已经完成一半!

在我理想的世界里,我可以像这样将其放入.md文件中,一切都能正常工作!

这样:

一些内容在这里<x-tweet url='https://twitter.com/aarondfrancis/status/1705211030882684946'>完成一个项目的90%最好的部分是你几乎已经完成了一半!</x-tweet>这里还有一些内容

但是,现实世界并不理想,这样做行不通。这样做行不通是因为markdown处理流程不包括任何Blade渲染。但是只要付出足够的努力,一切问题都可以解决!我尝试过几种方法,每种方法都有其优缺点。最终我选择了两种同样可行的方式,选择哪种方式是个人偏好的问题。

它们是:

* 解析除代码块之外的所有内容
* 仅解析代码块

## 解析除代码块之外的所有内容

第一种方法是:

1. 从markdown文档中删除所有代码块(下面会详细介绍)
2. 将markdown渲染为HTML
3. 渲染 当Commonmark解析Markdown文档时,它将其转换成一个充满节点的[抽象语法树](https://commonmark.thephpleague.com/2.4/customization/abstract-syntax-tree/)。在将它们转换成HTML之前,我们可以遍历这些节点并进行一些修改。在我们的情况下,我们将从文档中删除所有的代码节点。

我们必须从文档中删除代码节点,因为这些节点无法安全地通过Blade解析器运行。

例如,假设您有一个代码块,其中包含以下代码:

@php(DB::table('users')->delete())


您肯定不希望这段代码被执行!

或者更糟糕的是:

@dd(file_get_contents(base_path('.env'))


这些是最糟糕的情况,但即使是相对正常的内容也可能引起问题。如果您甚至包含了一个带有Blade指令的内联块,比如`@endif`或`@include`,Blade也会出现问题。 寻找代码块

我们不再在HTML中寻找代码块,因为这可能很棘手,而是在AST阶段将代码块提取出来,这非常简单。

首先,我们将创建一个名为`BladeParsingExtension`的Commonmark扩展。我们将连接到两个事件:

*   文档解析完成:在将Markdown转换为AST后。
*   文档渲染完成:在将AST转换为HTML后。

class BladeParsingExtension implements ExtensionInterface{ public function register(EnvironmentBuilderInterface $environment): void { $environment->addEventListener( DocumentParsedEvent::class, [$this, 'onDocumentParsed'], -10 ); $environment->addEventListener( DocumentRenderedEvent::class, [$this, 'onDocumentRendered'], 10 ); } public function onDocumentParsed(DocumentParsedEvent $event) { // Walk through every node in the document foreach ($e 移除代码块

当我们找到一个代码节点时,我们会将其从AST中提取出来,留下一个占位符。占位符只是一个我们稍后查找的随机字符串。

class BladeParsingExtension implements ExtensionInterface{    protected array $rendered = [];    protected Environment $environment;    public function register(EnvironmentBuilderInterface $environment): void ...    {        $environment->addEventListener(            DocumentParsedEvent::class, [$this, 'on 删除第一级标题后,将以下Markdown翻译成中文:

```php
DocumentParsed'], -10        );        $environment->addEventListener(            DocumentRenderedEvent::class, [$this, 'onDocumentRendered'], 10        );        $this->environment = $environment;    }      public function onDocumentParsed(DocumentParsedEvent $event)    {        foreach ($event->getDocument()->iterator() as $node) {            if (!$this->isCodeNode($node)) {                continue;            }            // 创建一个唯一的随机ID             $id = Str::uuid()->toString();            // 创建一个只包含占位符的新HTML块            $replacement = new HtmlBlock(HtmlBlock::TYPE_6_BLOCK_ELEMENT);            $replacement->setLiteral("[[replace:$id]]");            // 用占位符替换代码节点            $node->replaceWith($replacement);            // 创建一个与主要渲染器完全相同的渲染器            $renderer = new HtmlRenderer($this->environment)            // 渲染代码节点并将其存储起来。            $this->rendere
``` d[$id] = $renderer->renderNodes([$node]);        }     }    public function onDocumentRendered(DocumentRenderedEvent $event) ...    {        // @TODO    }    protected function isCodeNode($node) ...    {        return $node instanceof FencedCode            || $node instanceof IndentedCode            || $node instanceof Code;    }} 以下是Markdown的翻译版本,同时删除了一级标题:

```php
s, [$this, 'onDocumentParsed'], -10);
$environment->addEventListener(DocumentRenderedEvent::class, [$this, 'onDocumentRendered'], 10);
$this->environment = $environment;
}

public function onDocumentParsed(DocumentParsedEvent $event) ...
{
    foreach ($event->getDocument()->iterator() as $node) {
        if (!$this->isCodeNode($node)) {
            continue;
        }
        // 创建一个唯一的随机ID
        $id = Str::uuid()->toString();
        // 创建一个只包含占位符的新HTML块
        $replacement = new HtmlBlock(HtmlBlock::TYPE_6_BLOCK_ELEMENT);
        $replacement->setLiteral("[[replace:$id]]");
        // 用占位符替换代码节点
        $node->replaceWith($replacement);
        // 创建一个与主要渲染器相同的渲染器
        $renderer = new HtmlRenderer($this->environment)
        // 渲染代码节点并储存起来。

请注意,此处的翻译是根据Markdown内容进行的,因此不包含Markdown标记的翻译。 $this->rendered[$id] = $renderer->renderNodes([$node]); } } public function onDocumentRendered(DocumentRenderedEvent $event) { $search = []; $replace = []; // 收集所有占位符和它们的真实内容 foreach ($this->rendered as $id => $content) { $search[] = "[[replace:$id]]"; $replace[] = $content; } // Commonmark 生成的 HTML $content = $event->getOutput()->getContent(); // 首先渲染没有代码块的输出。 $content = Blade::render($content); // 然后将代码块添加回去。 $content = Str::replace($search, $replace, $content); // 用我们新的、经过 Blade 处理的输出替换整个响应。 $event->replaceOutput( new RenderedContent($event->getOutput()->getDocument(), $content) ); } protected function isCodeNode($node) ... { return $node instanceof FencedCode || 将以下Markdown翻译成中文并删除一级标题:

$node instanceof IndentedCode || $node instanceof Code;}}

启用扩展 #

要启用扩展,我们需要将其添加到markdown.php文件的扩展数组中。与此同时,我们将html_input设置为allow,以便HTML标签不会被转义。

return [    // 其他配置...    'extensions' => [        // 添加扩展        BladeParsingExtension::class,    ],    // 保留HTML标签    'html_input' => HtmlFilter::ALLOW,]

仅解析代码块 #

第二个选项是使用"magic"代码块。这种技术有一些好处,但我不确定它是否更好。

"magic"代码块在Markdown中的写法如下:

```blade +parse<x-tweet url='https://twitter.com/aarondfrancis/status/1705211030882684946'>    The best part about being 90% done with a project is that you're almost halfway finished!</x-tweet>```

它看起来像是一个普通的代码块:三个反引号后面是语言标识符 blade。但是在标识符后面是一个魔术注释+parse

这个+parse注释将告诉我们这不是一个需要突出显示的代码块,而是一个需要运行的代码块。

这种方法的好处是,在代码块内工作时,您可以在编辑器中获得完整的语法突出显示。这真的很棒。

第二个好处是对解析和不解析的内容有精细的控制。只有您标记为+parse的代码块将被处理。我不知道这是好事还是烦人的事。

无论如何,我们继续进行。

创建一个渲染扩展 #

与之前一样,我们首先创建一个扩展。这次我们将把它命名为CodeRendererExtension。我们只需为代码块注册一些渲染器,而不是监听事件。

class CodeRendererExtension implements ExtensionInterface, NodeRendererInterface{    public function register(EnvironmentBuilderInterface $environment): void    {        $environment->addRe 我们给我们的渲染器一个相对较高的优先级(100),以便在任何语法高亮器之前运行。

在`render`函数内部,我们可以访问代码节点并做任何我们喜欢的操作。在我们的情况下,我们将检查是否存在魔术词,如果找到,我们将通过Blade运行内容。

```php
class CodeRendererExtension implements ExtensionInterface, NodeRendererInterface{    public function register(EnvironmentBuilderInterface $environment): void ...    {        $environment->addRenderer(FencedCode::class, $this, 100);        $environment->addRenderer(IndentedCode::class, $this, 100);    }     public function render(Node $node, ChildNodeRendererInterface $childRenderer)    {        /** @var FencedCode|IndentedCode $node  */        // @TODO something???    }}
``` $info = $node->getInfoWords();        // 寻找我们的魔术词        if (in_array('+parse', $info)) {            // 通过 Blade 运行内容            return Blade::render($node->getLiteral());        }     }}

正如您所看到的,这是一种在后端上更简单的方法,但作者体验不够灵活。

我更喜欢第一种方法,其中 Blade 解析所有内容,除了代码块。

### 启用扩展

启用扩展与之前相同。我们将其添加到 `markdown.php` 配置文件中。这次,您可以将 `html_input` 设置为任何您想要的值,因为我们不依赖于 Markdown 中的 HTML 标记。而是将它们包装在代码块中。

return [ // 其他配置... 'extensions' => [ // 添加扩展 CodeRendererExtension::class, ],]


## 安全考虑

在用户提供的内容上运行 Blade 是非常危险的,您绝对不能这样做。如果您在用户提供的内容上运行 Blade,那么您应该非常小心。 作为一个YouTube视频

你想在YouTube上看到这个视频吗?请在[Twitter](https://twitter.com/@aarondfrancis)上告诉我,我会把它加入到我的队列中!

作为一个包

在我从这篇文章的人们那里得到一些反馈后,我会在接下来的几天内将其制作成一个可安装的包。