katex-render.mjs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. #!/usr/bin/env node
  2. /**
  3. * KaTeX 服务端渲染脚本
  4. * 用法: echo "HTML内容" | node katex-render.js
  5. * 或: node katex-render.js < input.html > output.html
  6. *
  7. * 将 HTML 中的 LaTeX 公式渲染为 KaTeX HTML
  8. */
  9. import { createRequire } from 'module';
  10. const require = createRequire(import.meta.url);
  11. // 尝试多个路径加载 KaTeX
  12. let katex;
  13. const possiblePaths = [
  14. '/usr/local/lib/node_modules/katex', // npm -g 全局安装路径
  15. '/usr/lib/node_modules/katex', // Alpine 系统路径
  16. 'katex', // 本地 node_modules
  17. ];
  18. let loadError = null;
  19. for (const modulePath of possiblePaths) {
  20. try {
  21. katex = require(modulePath);
  22. break;
  23. } catch (e) {
  24. loadError = e;
  25. }
  26. }
  27. if (!katex) {
  28. console.error('Error: KaTeX module not found.');
  29. console.error('Tried paths:', possiblePaths.join(', '));
  30. if (loadError) console.error('Last error:', loadError.message);
  31. process.exit(1);
  32. }
  33. // 读取标准输入
  34. let input = '';
  35. process.stdin.setEncoding('utf8');
  36. process.stdin.on('readable', () => {
  37. let chunk;
  38. while ((chunk = process.stdin.read()) !== null) {
  39. input += chunk;
  40. }
  41. });
  42. process.stdin.on('end', () => {
  43. try {
  44. const output = renderMathInHtml(input);
  45. process.stdout.write(output);
  46. } catch (error) {
  47. console.error('KaTeX render error:', error.message);
  48. process.exit(1);
  49. }
  50. });
  51. /**
  52. * 渲染 HTML 中的所有数学公式
  53. */
  54. function renderMathInHtml(html) {
  55. // 定界符配置(按优先级排序)
  56. const delimiters = [
  57. { left: '$$', right: '$$', display: true },
  58. { left: '\\[', right: '\\]', display: true },
  59. { left: '\\(', right: '\\)', display: false },
  60. { left: '$', right: '$', display: false },
  61. ];
  62. let result = html;
  63. // 按顺序处理每种定界符
  64. for (const delimiter of delimiters) {
  65. result = processDelimiter(result, delimiter.left, delimiter.right, delimiter.display);
  66. }
  67. return result;
  68. }
  69. /**
  70. * 处理特定定界符的公式
  71. */
  72. function processDelimiter(html, left, right, displayMode) {
  73. // 转义正则特殊字符
  74. const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  75. const leftEscaped = escapeRegex(left);
  76. const rightEscaped = escapeRegex(right);
  77. // 构建正则表达式
  78. // 对于 $ ... $,需要确保不匹配 $$ ... $$
  79. let pattern;
  80. if (left === '$' && right === '$') {
  81. // 单个 $ 不能紧跟另一个 $
  82. pattern = new RegExp(`(?<!\\$)\\$(?!\\$)([^$]+?)(?<!\\$)\\$(?!\\$)`, 'g');
  83. } else {
  84. pattern = new RegExp(`${leftEscaped}([\\s\\S]*?)${rightEscaped}`, 'g');
  85. }
  86. return html.replace(pattern, (match, latex) => {
  87. try {
  88. // 清理 LaTeX 内容
  89. let cleanLatex = latex.trim();
  90. // 跳过空内容
  91. if (!cleanLatex) {
  92. return match;
  93. }
  94. // 渲染 KaTeX
  95. const rendered = katex.renderToString(cleanLatex, {
  96. displayMode: displayMode,
  97. throwOnError: false,
  98. strict: false,
  99. trust: true,
  100. output: 'html', // 使用 HTML 输出(比 mathml 兼容性更好)
  101. });
  102. return rendered;
  103. } catch (error) {
  104. // 渲染失败时保留原始内容
  105. console.error(`KaTeX error for "${latex.substring(0, 50)}...":`, error.message);
  106. return match;
  107. }
  108. });
  109. }