Files
laralog/app/Blog/Services/PostContentRenderer.php
T
ak f1b9d1e1c3 refactor: 外链中转拆为内置插件 neatstudio.link-tracker
- 插件承载全部逻辑:redirects 迁移(hasTable 兼容旧库)、Redirect 模型、UrlRedirector、/go 路由+控制器、Filament 外链中转资源
- 核心只留 hook 接入点:
  - tracked_url() 助手走 link.redirect 过滤器(评论正文/作者网址/友链模板统一调用)
  - 正文外链改写移到插件的 post.rendered 过滤器(优先级 20,晚于会员付费过滤)
- 停用插件:链接恢复直连、内容不受影响;后台「管理 → 外链中转」查看点击量/启停
- 测试更新为插件上下文(含无过滤器降级断言);97 通过
2026-08-13 02:05:32 +08:00

159 lines
5.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 -> HTMLhtml 原样返回。两种格式都会解析附件令牌。
*/
public function render(Post $post): string
{
$content = $post->content_format === 'markdown'
? $this->toHtml($post->content)
: $post->content;
$html = $this->renderShortcodes($content, $post);
// 插件钩子:付费内容过滤、AI 处理等
$html = app(\App\Blog\Support\PluginManager::class)->applyFilters('post.rendered', $html, $post);
// 兜底:任何情况下不把裸 [paid] 标签输出到页面(如会员插件被停用时)
return preg_replace('/\[(\/?paid)\]/i', '', $html);
}
/**
* 渲染并提取目录(TOC)。
*
* @return array{html: string, toc: array<int, array{id: string, text: string, level: int}>}
*/
public function renderWithToc(Post $post): array
{
$html = $this->render($post);
$toc = [];
$index = 0;
$html = preg_replace_callback(
'/<h([2-6])([^>]*)>(.*?)<\/h\1>/is',
function (array $m) use (&$toc, &$index): string {
$level = (int) $m[1];
$attrs = $m[2];
$text = trim(strip_tags($m[3]));
$id = 'toc-'.(++$index);
$toc[] = ['id' => $id, 'text' => $text, 'level' => $level];
// 已存在 id 则保留原 id,否则注入锚点
if (preg_match('/id="([^"]+)"/', $attrs, $idMatch)) {
$toc[array_key_last($toc)]['id'] = $idMatch[1];
return $m[0];
}
return '<h'.$level.$attrs.' id="'.$id.'">'.$m[3].'</h'.$level.'>';
},
$html
);
return ['html' => $html, 'toc' => $toc];
}
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);
}
}