Mở Rộng CommonMark trong Laravel: Cú Pháp Tùy Chỉnh & Syntax Highlighting
Một trong những lý do chính để xây dựng blog tùy chỉnh thay vì sử dụng CMS có sẵn là khả năng kiểm soát. Khi nội dung của bạn ưu tiên Markdown, bộ parser chính là trái tim của ứng dụng.
Laravel (và phần lớn hệ sinh thái PHP) dựa vào league/commonmark. Mặc dù nó đã tuyệt vời ngay từ đầu, sức mạnh thực sự nằm ở khả năng mở rộng của nó.
Trong bài viết này, chúng ta sẽ khám phá cách viết các extension tùy chỉnh để thêm các tính năng như hộp "Alert" và code highlighting nâng cao.
Mục Tiêu: Custom Alert Block
Chúng ta muốn hỗ trợ cú pháp như thế này trong file Markdown:
::: note
Đây là ghi chú cho người đọc.
:::
::: warning
Cẩn thận! Đây là cảnh báo.
:::
Và render thành:
<div class="alert alert-note">
<p>Đây là ghi chú cho người đọc.</p>
</div>
Bước 1: Cài Đặt Dependencies
Mặc dù chúng ta có thể viết parser từ đầu, hệ sinh thái league/commonmark đã có extension "attributes" khá tốt, nhưng đối với các phần tử block-level, cú pháp tùy chỉnh thường sạch hơn.
Với ví dụ này, việc tham khảo tài liệu league/commonmark về Extensions là điều quan trọng.
Bước 2: Kiến Trúc của Extension
Một extension trong CommonMark thường bao gồm:
- Parser: Logic chi tiết để regex-match
:::và xác định block. - Node: Một class đại diện cho node trong Abstract Syntax Tree (AST).
- Renderer: Một class chuyển đổi Node thành HTML.
Tuy nhiên, cách đơn giản hơn cho nhiều trường hợp sử dụng là dùng AttributesExtension chung hoặc viết một Inline Parser đơn giản nếu bạn chỉ muốn thay thế text đơn giản. Nhưng với block, chúng ta cần một Block Parser.
Hãy làm điều gì đó đơn giản hơn được hỗ trợ hiệu quả bởi các extension GFM mặc định: sử dụng Blockquotes với một chút biến tấu, hoặc chỉ xử lý qua styling, nhưng hãy đi theo hướng extension tùy chỉnh thực sự sử dụng container package nếu bạn muốn tránh viết regex parser thủ công.
Một lựa chọn phổ biến là simonvane/commonmark-admonitions-extension (nếu có) hoặc tương tự. Nhưng hãy xem cách đăng ký một renderer đơn giản.
Cấu Hình trong Laravel
Chúng ta kết nối điều này ở đâu? Trong MarkdownPostService hoặc một MarkdownServiceProvider.
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;
class MarkdownPostService
{
public function parse(string $content): string
{
$environment = new Environment([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new GithubFlavoredMarkdownExtension());
// Đăng ký extension tùy chỉnh tại đây
// $environment->addExtension(new MyCustomAlertExtension());
$converter = new MarkdownConverter($environment);
return $converter->convert($content);
}
}
Syntax Highlighting với Torchlight hoặc Shiki
highlight.js hoặc Prism tiêu chuẩn chạy trên client. Với blog chú trọng hiệu năng, chúng ta muốn server-side highlighting.
Lựa Chọn A: Torchlight (SaaS)
Torchlight rất tuyệt vời cho Laravel.
composer require torchlight/torchlight-commonmark- Thêm extension:
$environment->addExtension(new \Torchlight\Commonmark\V2\TorchlightExtension()); - Đặt API token trong
.env.
Lựa Chọn B: Spatie Shiki (Local Node)
Nếu bạn có Node cài đặt trên server (hoặc trong build process):
composer require spatie/laravel-markdown
npm install shiki
Package này bọc CommonMark tiêu chuẩn và thêm hỗ trợ Shiki.
Viết HTML Renderer Đơn Giản
Nếu bạn không muốn parser đầy đủ, bạn có thể hook vào giai đoạn rendering. Ví dụ, đảm bảo tất cả external link mở trong tab mới.
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
class ExternalLinkRenderer implements NodeRendererInterface
{
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{
/** @var Link $node */
$attrs = $node->data->get('attributes');
if ($this->isExternal($node->getUrl())) {
$attrs['target'] = '_blank';
$attrs['rel'] = 'noopener noreferrer';
}
return new HtmlElement('a', $attrs, $childRenderer->renderNodes($node->children()));
}
private function isExternal($url) {
return parse_url($url, PHP_URL_HOST) !== request()->getHost();
}
}
Đăng ký với độ ưu tiên cao:
$environment->addRenderer(Link::class, new ExternalLinkRenderer(), 10);
Kết Luận
Tùy chỉnh Markdown parser cho phép bạn coi Markdown như một ngôn ngữ domain-specific cho blog của mình. Dù là xử lý an toàn các external link, thêm nút "Call to Action" tùy chỉnh, hay render các diagram phức tạp, kiến trúc league/commonmark trong Laravel đủ linh hoạt để xử lý tất cả.