#!/usr/bin/env node /** * KaTeX 服务端渲染脚本 * 用法: echo "HTML内容" | node katex-render.js * 或: node katex-render.js < input.html > output.html * * 将 HTML 中的 LaTeX 公式渲染为 KaTeX HTML */ import { createRequire } from 'module'; const require = createRequire(import.meta.url); // 尝试多个路径加载 KaTeX let katex; const possiblePaths = [ '/usr/local/lib/node_modules/katex', // npm -g 全局安装路径 '/usr/lib/node_modules/katex', // Alpine 系统路径 'katex', // 本地 node_modules ]; let loadError = null; for (const modulePath of possiblePaths) { try { katex = require(modulePath); break; } catch (e) { loadError = e; } } if (!katex) { console.error('Error: KaTeX module not found.'); console.error('Tried paths:', possiblePaths.join(', ')); if (loadError) console.error('Last error:', loadError.message); process.exit(1); } // 读取标准输入 let input = ''; process.stdin.setEncoding('utf8'); process.stdin.on('readable', () => { let chunk; while ((chunk = process.stdin.read()) !== null) { input += chunk; } }); process.stdin.on('end', () => { try { const output = renderMathInHtml(input); process.stdout.write(output); } catch (error) { console.error('KaTeX render error:', error.message); process.exit(1); } }); /** * 解码 HTML 实体 */ function decodeHtmlEntities(text) { return text .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/ /g, ' '); } /** * 编码 HTML 实体(用于安全输出) */ function encodeHtmlEntities(text) { return text .replace(/&/g, '&') .replace(//g, '>'); } /** * 渲染 HTML 中的所有数学公式 */ function renderMathInHtml(html) { // 定界符配置(按优先级排序) const delimiters = [ { left: '$$', right: '$$', display: true }, { left: '\\[', right: '\\]', display: true }, { left: '\\(', right: '\\)', display: false }, { left: '$', right: '$', display: false }, ]; let result = html; // 按顺序处理每种定界符 for (const delimiter of delimiters) { result = processDelimiter(result, delimiter.left, delimiter.right, delimiter.display); } return result; } /** * 处理特定定界符的公式 */ function processDelimiter(html, left, right, displayMode) { // 转义正则特殊字符 const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const leftEscaped = escapeRegex(left); const rightEscaped = escapeRegex(right); // 构建正则表达式 // 【关键修复】排除包含 HTML 标签的内容(不匹配 < 或 >) let pattern; if (left === '$' && right === '$') { // 单个 $...$:不匹配 $$,不匹配包含 < > 的内容 pattern = new RegExp(`(?]+?)(? 的内容 pattern = new RegExp(`\\$\\$([^<>]*?)\\$\\$`, 'g'); } else { // \(...\) 和 \[...\]:不匹配包含 < > 的内容 pattern = new RegExp(`${leftEscaped}([^<>]*?)${rightEscaped}`, 'g'); } return html.replace(pattern, (match, latex) => { try { // 清理 LaTeX 内容 - 先解码 HTML 实体 let cleanLatex = decodeHtmlEntities(latex.trim()); // 跳过空内容 if (!cleanLatex) { return match; } // 【安全检查】如果内容看起来不像 LaTeX,跳过 // 跳过只有普通文本的内容(没有任何 LaTeX 特征) if (!/[\\^_{}]/.test(cleanLatex) && !/[a-zA-Z]{2,}/.test(cleanLatex)) { // 可能只是普通数字或单字母,检查是否有意义 if (/^[\d\s\.\,\-\+]+$/.test(cleanLatex)) { return match; // 纯数字,不渲染 } } // 渲染 KaTeX const rendered = katex.renderToString(cleanLatex, { displayMode: displayMode, throwOnError: false, strict: false, trust: true, output: 'html', }); return rendered; } catch (error) { // 渲染失败时保留原始内容 // console.error(`KaTeX error for "${latex.substring(0, 50)}...":`, error.message); return match; } }); }