| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- #!/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, "'")
- .replace(/ /g, ' ');
- }
- /**
- * 编码 HTML 实体(用于安全输出)
- */
- function encodeHtmlEntities(text) {
- return text
- .replace(/&/g, '&')
- .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(`(?<!\\$)\\$(?!\\$)([^$<>]+?)(?<!\\$)\\$(?!\\$)`, 'g');
- } else if (left === '$$' && right === '$$') {
- // $$...$$:不匹配包含 < > 的内容
- 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;
- }
- });
- }
|