katex-render.js 3.3 KB

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