KatexRenderer.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\Log;
  4. use Symfony\Component\Process\Process;
  5. use Symfony\Component\Process\Exception\ProcessFailedException;
  6. /**
  7. * KaTeX 服务端渲染服务
  8. *
  9. * 使用 Node.js 的 KaTeX 库在服务端预渲染 LaTeX 公式
  10. * 避免依赖 Chrome headless 执行 JavaScript
  11. */
  12. class KatexRenderer
  13. {
  14. /**
  15. * Node.js 脚本路径
  16. */
  17. private string $scriptPath;
  18. /**
  19. * 是否启用缓存
  20. */
  21. private bool $cacheEnabled = true;
  22. /**
  23. * 缓存前缀
  24. */
  25. private const CACHE_PREFIX = 'katex_rendered_';
  26. /**
  27. * 缓存时间(秒)
  28. */
  29. private const CACHE_TTL = 86400; // 24小时
  30. public function __construct()
  31. {
  32. $this->scriptPath = base_path('scripts/katex-render.js');
  33. }
  34. /**
  35. * 渲染 HTML 中的所有 LaTeX 公式
  36. *
  37. * @param string $html 包含 LaTeX 公式的 HTML
  38. * @return string 渲染后的 HTML
  39. */
  40. public function renderHtml(string $html): string
  41. {
  42. // 检查是否包含需要渲染的公式
  43. if (!$this->containsLatex($html)) {
  44. Log::debug('KatexRenderer: HTML 不包含 LaTeX 公式,跳过渲染');
  45. return $html;
  46. }
  47. // 尝试从缓存获取
  48. $cacheKey = $this->getCacheKey($html);
  49. if ($this->cacheEnabled && $cached = cache()->get($cacheKey)) {
  50. Log::debug('KatexRenderer: 从缓存获取渲染结果');
  51. return $cached;
  52. }
  53. // 调用 Node.js 脚本渲染
  54. $rendered = $this->callNodeScript($html);
  55. // 缓存结果
  56. if ($this->cacheEnabled && $rendered !== $html) {
  57. cache()->put($cacheKey, $rendered, self::CACHE_TTL);
  58. }
  59. return $rendered;
  60. }
  61. /**
  62. * 检查 HTML 是否包含 LaTeX 公式
  63. */
  64. private function containsLatex(string $html): bool
  65. {
  66. // 检查常见的 LaTeX 定界符
  67. return preg_match('/\$[^$]+\$|\$\$[\s\S]+?\$\$|\\\\\([\s\S]+?\\\\\)|\\\\\[[\s\S]+?\\\\\]/', $html) === 1;
  68. }
  69. /**
  70. * 调用 Node.js KaTeX 渲染脚本
  71. */
  72. private function callNodeScript(string $html): string
  73. {
  74. // 检查脚本是否存在
  75. if (!file_exists($this->scriptPath)) {
  76. Log::warning('KatexRenderer: 渲染脚本不存在', ['path' => $this->scriptPath]);
  77. return $html;
  78. }
  79. try {
  80. // 创建进程
  81. $process = new Process(['node', $this->scriptPath]);
  82. $process->setInput($html);
  83. $process->setTimeout(30); // 30秒超时
  84. // 执行
  85. $process->run();
  86. // 检查是否成功
  87. if (!$process->isSuccessful()) {
  88. Log::warning('KatexRenderer: Node.js 脚本执行失败', [
  89. 'exit_code' => $process->getExitCode(),
  90. 'error' => $process->getErrorOutput(),
  91. ]);
  92. return $html;
  93. }
  94. $output = $process->getOutput();
  95. // 验证输出
  96. if (empty($output)) {
  97. Log::warning('KatexRenderer: Node.js 脚本输出为空');
  98. return $html;
  99. }
  100. Log::info('KatexRenderer: LaTeX 公式渲染成功', [
  101. 'input_length' => strlen($html),
  102. 'output_length' => strlen($output),
  103. ]);
  104. return $output;
  105. } catch (\Exception $e) {
  106. Log::error('KatexRenderer: 渲染异常', [
  107. 'error' => $e->getMessage(),
  108. ]);
  109. return $html;
  110. }
  111. }
  112. /**
  113. * 生成缓存键
  114. */
  115. private function getCacheKey(string $html): string
  116. {
  117. return self::CACHE_PREFIX . md5($html);
  118. }
  119. /**
  120. * 禁用缓存(用于调试)
  121. */
  122. public function disableCache(): self
  123. {
  124. $this->cacheEnabled = false;
  125. return $this;
  126. }
  127. /**
  128. * 启用缓存
  129. */
  130. public function enableCache(): self
  131. {
  132. $this->cacheEnabled = true;
  133. return $this;
  134. }
  135. /**
  136. * 清除所有 KaTeX 渲染缓存
  137. */
  138. public function clearCache(): void
  139. {
  140. // 注意:这个方法需要 Redis 或支持通配符删除的缓存驱动
  141. Log::info('KatexRenderer: 缓存清除请求(需要手动清理或使用 Redis)');
  142. }
  143. }