编程

Laravel 中使用 Markdown:在 Blade 中嵌入 Markdown 内容

84 2024-12-16 01:20:00

Markdown 扩展

league/commonmark 包有一个扩展 API,允许你注册自定义块、行内元素及渲染器。本文中用于 Blade 扩展的语法如下:

@blade
    <!-- Put my custom Blade code here. -->
@endblade

这种类型的扩展是块扩展,因为语法跨越多行。league/commonmark 包在渲染 HTML 之前使用 AST(抽象语法树)来表示解析的 Markdown 代码,因此我们需要告诉包如何解析这个自定义的 Blade 块。

编写解析器

块通常跨越多行,并具有某开始和结束语法。在这个场景中,是 @blade@endblade。介于两者之间的所有内容都被解析为文字字符串,任何 Markdown 都会被忽略。

Markdown 解析器需要知道何时应该开始解析块。通过编写一个实现 BlockStartParserInterface 的类来实现的。

use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;

class BladeStartParser implements BlockStartParserInterface
{
    const REGEX = '/^@blade/';

    public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
    {
        if ($cursor->isIndented()) {
            return BlockStart::none();
        }

        if (! $cursor->match(self::REGEX)) {
            return BlockStart::none();
        }

        return BlockStart::of(new BladeParser)->at($cursor);
    }
}

这个类的唯一职责是确定块是否存在于 Markdown 文档中的当前位置。大多数块将使用正则表达式来实现这一点,就像 BladeStartParser 一样。

如果正则表达式不匹配,或者光标缩进(不在行首),那么找到该块的可能性为零,因此它会提前返回。

如果找到 @blade 构造,则告诉解析器使用 BladeParser 类开始解析块的其余部分。

Cursor::match() 方法将使用正则表达式匹配的任何文本,因此不需要手动移动光标。

BladeParser 类继承了 AbstractBlockContinueParser ,负责解析文本,直到到达块的末尾。

use App\CommonMark\Block\Blade;
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
use League\CommonMark\Parser\Block\BlockContinue;
use League\CommonMark\Util\ArrayCollection;

class BladeParser extends AbstractBlockContinueParser
{
    private Blade $block;

    private ArrayCollection $strings;

    public function __construct()
    {
        $this->block = new Blade();
        $this->strings = new ArrayCollection();
    }

    public function getBlock(): Blade
    {
        return $this->block;
    }

    public function addLine(string $line): void
    {
        $this->strings[] = $line;
    }

    public function closeBlock(): void
    {
        $this->block->setContent(
            ltrim(implode("\n", $this->strings->toArray()))
        );
    }

    public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
    {
        if ($cursor->match('/^@endblade/')) {
            return BlockContinue::finished();
        }

        return BlockContinue::at($cursor);
    }
}

这个类还需要完善。

Markdown 解析器需要知道正在解析哪个块,因此自定义 Blade 块被存储在属性中。还有一个属性存储字符串列表。

AbstractBlockContinueParser 实现了一个名为 isContainer() 的方法,该方法“告诉”解析器块是否可以包含其他 Markdown 元素(标题、blockquotes 引用块等)。默认实现返回 false,在这种情况下很好,因为 Blade 块不会被解析为 Markdown。

解析器将使用块内的每一行,并使用包含行文本的字符串调用 addLine() 方法,而不是将块的内容解析为 Markdown。它需要被收集并存储在某个地方,以便之后可以传递给 Blade 编译器。

tryContinue() 持续检查是否可以在行首找到 @endblade 标记,以指示块是否已完成。

如果找到了,那么解析器就会被告知该块不再被解析。否则,解析器将继续解析行。

解析器解析完 Blade 块后,将调用 closeBlock() 方法,并将收集到的所有文本行存储在自定义 Blade 块对象中。

namespace App\CommonMark\Block;

use League\CommonMark\Node\Block\AbstractBlock;

final class Blade extends AbstractBlock
{
    public function __construct(
        private string $content = ''
    ) {}

    public function getContent(): string
    {
        return $this->content;
    }

    public function setContent(string $content): void
    {
        $this->content = $content;
    }
}

Block 类大多是存储块信息的普通对象。他们不负责渲染。

渲染 Blade 模板

要实际渲染类,我们需要编写以恶自定义块渲染器。

use App\CommonMark\Block\Blade;
use Illuminate\Support\Facades\Blade as Engine;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;

class BladeRenderer implements NodeRendererInterface
{
    /**
     * @param Blade $node
     */
    public function render(Node $node, ChildNodeRendererInterface $childRenderer)
    {
        Blade::assertInstanceOf($node);

        return Engine::render($node->getContent());
    }
}

此类接收一个 Node,在本例中为之前的 Blade 块,并返回一个表示该块的 HTML 字符串。

Blade 引擎提供了一个 API,用于通过 Blade::render() 方法编译和渲染 Blade 字符串(别名为 Engine 以避免与块冲突)。
这就是 @blade@endblade 之间的代码如何编译并最终添加到最终的 HTML 输出中。

勾连起来

要告诉 league/commonmark 引擎有一个自定义块,我们需要向引擎注册所有自定义类。\

在我的博客中,我通过手动构建一个具有我需要的所有 CommonMark 功能的 Environment 对象,并注册一个具有自定义块和渲染器的自定义扩展来实现这一点。

$this->app->singleton(MarkdownConverter::class, static function (): MarkdownConverter {
    $environment = new Environment();

    $environment
        ->addExtension(new CommonMarkCoreExtension)
        ->addExtension(new GithubFlavoredMarkdownExtension)
        ->addExtension(new DescriptionListExtension)
        ->addExtension(new PhikiExtension(Theme::GithubDark))
        ->addExtension(new MyExtension);

    return new MarkdownConverter($environment);
});
use App\CommonMark\Block\Blade;
use App\CommonMark\Block\Parser\BladeStartParser;
use App\CommonMark\Block\Renderer\BladeRenderer;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\ExtensionInterface;

final class MyExtension implements ExtensionInterface
{
    public function register(EnvironmentBuilderInterface $environment): void
    {
        $environment
            ->addBlockStartParser(new BladeStartParser, 80)
            ->addRenderer(Blade::class, new BladeRenderer);
    }
}

这告诉引擎如何解析 @blade 块以及如何渲染自定义 Blade 块节点。

在你自己的应用中使用

在自己的应用中手动实现这一点可能有点太乏味了,可以通过 Composer 安装以下包:

composer require ryangjchandler/commonmark-blade-block

Github 源码:https://github.com/ryangjchandler/commonmark-blade-block