119 lines
3.9 KiB
PHP
119 lines
3.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
|
||
namespace App\Blog\Services;
|
||
|
||
use App\Models\Post;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use League\CommonMark\Environment\Environment;
|
||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
|
||
use League\CommonMark\MarkdownConverter;
|
||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||
|
||
class PostContentRenderer
|
||
{
|
||
private MarkdownConverter $converter;
|
||
|
||
public function __construct()
|
||
{
|
||
$environment = new Environment([
|
||
'html_input' => 'allow',
|
||
'allow_unsafe_links' => true,
|
||
'max_nesting_level' => 100,
|
||
]);
|
||
$environment->addExtension(new CommonMarkCoreExtension);
|
||
$environment->addExtension(new GithubFlavoredMarkdownExtension);
|
||
|
||
$this->converter = new MarkdownConverter($environment);
|
||
}
|
||
|
||
/**
|
||
* 将文章内容按格式渲染为 HTML。
|
||
* markdown -> HTML;html 原样返回。两种格式都会解析附件令牌。
|
||
*/
|
||
public function render(Post $post): string
|
||
{
|
||
$content = $post->content_format === 'markdown'
|
||
? $this->toHtml($post->content)
|
||
: $post->content;
|
||
|
||
$html = $this->renderShortcodes($content, $post);
|
||
|
||
// 插件钩子:付费内容过滤、AI 处理等
|
||
return app(\App\Blog\Support\PluginManager::class)->applyFilters('post.rendered', $html, $post);
|
||
}
|
||
|
||
public function toHtml(string $markdown): string
|
||
{
|
||
return $this->converter->convert($markdown)->getContent();
|
||
}
|
||
|
||
/**
|
||
* 解析附件引用。
|
||
*
|
||
* 兼容两种写法:
|
||
* - 老 sablog HTML 内容: [attach=xx] / [img=xx]
|
||
* - markdown 转换导入: {{attach:xx}} / {{img:xx}}([] 是 markdown 保留字符,转换时改为令牌)
|
||
*
|
||
* xx 是 sablog attachmentid;找不到时回退到按出现顺序的第 N 个附件。
|
||
*/
|
||
public function renderShortcodes(string $html, Post $post): string
|
||
{
|
||
$pattern = '/\[(attach|img)=(\d+)\]|\{\{(attach|img):(\d+)\}\}/';
|
||
|
||
if (! preg_match_all($pattern, $html, $matches, PREG_SET_ORDER)) {
|
||
return $html;
|
||
}
|
||
|
||
// 短代码引用的是 sablog 全局附件 id,可能属于其他文章,需要全局查
|
||
$byLegacyId = Cache::remember('attach.legacy_ids', now()->addHour(), function () {
|
||
return Media::query()
|
||
->where('collection_name', 'attachments')
|
||
->get()
|
||
->keyBy(fn ($m) => (int) $m->getCustomProperty('legacy_attachmentid'));
|
||
});
|
||
|
||
// 当前文章媒体用于索引回退
|
||
$ordered = $post->getMedia('attachments')->values();
|
||
|
||
foreach ($matches as $match) {
|
||
$full = $match[0];
|
||
$type = $match[1] !== '' ? $match[1] : $match[3];
|
||
$id = (int) ($match[2] !== '' ? $match[2] : $match[4]);
|
||
|
||
$media = $byLegacyId->get($id);
|
||
|
||
if (! $media) {
|
||
// 兼容按索引引用的旧写法:第 $id 个附件(从 1 开始)
|
||
$media = $ordered->get($id - 1);
|
||
}
|
||
|
||
if (! $media) {
|
||
// 找不到媒体时移除残留短代码,避免把 [attach=999] 当正文显示
|
||
$html = str_replace($full, '', $html);
|
||
continue;
|
||
}
|
||
|
||
$replacement = $type === 'img'
|
||
? sprintf('<img src="%s" alt="%s" loading="lazy" />', $media->getUrl(), e($media->name))
|
||
: sprintf('<a href="%s">%s</a>', $media->getUrl(), e($media->file_name));
|
||
|
||
$html = str_replace($full, $replacement, $html);
|
||
}
|
||
|
||
return $html;
|
||
}
|
||
|
||
/**
|
||
* 把老 sablog HTML 里的 [attach=xx]/[img=xx] 换成 markdown 安全令牌,
|
||
* 供 HTML -> markdown 转换前调用。
|
||
*/
|
||
public function toMarkdownSafeTokens(string $html): string
|
||
{
|
||
return preg_replace('/\[(attach|img)=(\d+)\]/', '{{$1:$2}}', $html);
|
||
}
|
||
}
|