scriptPath = base_path('scripts/katex-render.mjs');
}
/**
* 渲染 HTML 中的所有 LaTeX 公式
*
* @param string $html 包含 LaTeX 公式的 HTML
* @return string 渲染后的 HTML
*/
public function renderHtml(string $html): string
{
// 检查是否包含需要渲染的公式
if (!$this->containsLatex($html)) {
Log::debug('KatexRenderer: HTML 不包含 LaTeX 公式,跳过渲染');
return $html;
}
// 在渲染前修复公式中的实体与 cases 换行问题
$html = $this->sanitizeLatexInHtml($html);
// 尝试从缓存获取
$cacheKey = $this->getCacheKey($html);
if ($this->cacheEnabled && $cached = cache()->get($cacheKey)) {
Log::debug('KatexRenderer: 从缓存获取渲染结果');
return $cached;
}
// 调用 Node.js 脚本渲染
$rendered = $this->callNodeScript($html);
if (strpos($rendered, 'katex-error') !== false) {
Log::warning('KatexRenderer: 发现未解析公式(katex-error)', [
'sample' => $this->extractKatexErrorSnippet($rendered),
]);
}
// 缓存结果
if ($this->cacheEnabled && $rendered !== $html) {
cache()->put($cacheKey, $rendered, self::CACHE_TTL);
}
return $rendered;
}
/**
* 检查 HTML 是否包含 LaTeX 公式
*/
private function containsLatex(string $html): bool
{
// 检查常见的 LaTeX 定界符
return preg_match('/\$[^$]+\$|\$\$[\s\S]+?\$\$|\\\\\([\s\S]+?\\\\\)|\\\\\[[\s\S]+?\\\\\]/', $html) === 1;
}
/**
* 调用 Node.js KaTeX 渲染脚本
*/
private function callNodeScript(string $html): string
{
// 检查脚本是否存在
if (!file_exists($this->scriptPath)) {
Log::warning('KatexRenderer: 渲染脚本不存在', ['path' => $this->scriptPath]);
return $html;
}
try {
// 创建进程
$process = new Process(['node', $this->scriptPath]);
$process->setInput($html);
$process->setTimeout(30); // 30秒超时
// 执行
$process->run();
// 检查是否成功
if (!$process->isSuccessful()) {
Log::warning('KatexRenderer: Node.js 脚本执行失败', [
'exit_code' => $process->getExitCode(),
'error' => $process->getErrorOutput(),
]);
return $html;
}
$output = $process->getOutput();
// 验证输出
if (empty($output)) {
Log::warning('KatexRenderer: Node.js 脚本输出为空');
return $html;
}
Log::info('KatexRenderer: LaTeX 公式渲染成功', [
'input_length' => strlen($html),
'output_length' => strlen($output),
]);
return $output;
} catch (\Exception $e) {
Log::error('KatexRenderer: 渲染异常', [
'error' => $e->getMessage(),
]);
return $html;
}
}
/**
* 生成缓存键
*/
private function getCacheKey(string $html): string
{
return self::CACHE_PREFIX . md5($html);
}
/**
* 禁用缓存(用于调试)
*/
public function disableCache(): self
{
$this->cacheEnabled = false;
return $this;
}
/**
* 启用缓存
*/
public function enableCache(): self
{
$this->cacheEnabled = true;
return $this;
}
/**
* 清除所有 KaTeX 渲染缓存
*/
public function clearCache(): void
{
// 注意:这个方法需要 Redis 或支持通配符删除的缓存驱动
Log::info('KatexRenderer: 缓存清除请求(需要手动清理或使用 Redis)');
}
private function sanitizeLatexInHtml(string $html): string
{
$sanitize = function (string $tex): string {
$decoded = html_entity_decode($tex, ENT_QUOTES, 'UTF-8');
while ($decoded !== $tex) {
$tex = $decoded;
$decoded = html_entity_decode($tex, ENT_QUOTES, 'UTF-8');
}
// 清理公式内部的换行与
,避免 \frac{M}\n{N} 破坏解析
$tex = preg_replace('/
/i', '', $tex);
$tex = preg_replace('/\\r\\n|\\r|\\n/', '', $tex);
// 处理 KaTeX 不支持的操作符命令
$tex = preg_replace('/\\\\Arg\\b/', '\\\\operatorname{Arg}', $tex);
// 修复漏空格的 \quad/\qquad(如 \quadz、\quadx)
$tex = preg_replace('/\\\\q(u)?ad(?=[A-Za-z0-9])/', '\\\\q$1ad ', $tex);
return $this->fixCasesLineBreaks($tex);
};
// $$...$$
$html = preg_replace_callback('/\$\$([\s\S]*?)\$\$/', function ($m) use ($sanitize) {
return '$$' . $sanitize($m[1]) . '$$';
}, $html);
// $...$ (avoid $$)
$html = preg_replace_callback('/(?]|\\s))/', '\\\\\\\\', $content);
return '\\begin{cases}' . $content . '\\end{cases}';
}, $tex);
}
private function extractKatexErrorSnippet(string $html): array
{
if (!preg_match('/]*>(.*?)<\/span>/is', $html, $match)) {
return [];
}
$text = trim(strip_tags($match[1]));
$text = preg_replace('/\s+/', ' ', $text);
return [
'text' => mb_substr($text, 0, 200),
];
}
}