Laravel 中使用 Markdown:在 Blade 中嵌入 Markdown 内容
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