marked与MathJax集成:在Markdown中优雅渲染数学公式的完整指南
1. 项目概述为什么需要让 Markdown 支持数学公式作为一名长期在技术社区分享技术文章和笔记的开发者我经常遇到一个痛点如何在 Markdown 中优雅地书写和展示数学公式。无论是写算法解析、机器学习笔记还是分享一些物理引擎的推导过程数学公式都是不可或缺的。原生 Markdown 对此是无能为力的它只关心段落、列表、代码块这些基础排版。市面上常见的解决方案是直接使用 LaTeX但那套语法对于只想快速记录和分享的博主来说学习成本和排版复杂度都太高了。我们需要的是一个既能保留 Markdown 简洁书写体验又能无缝渲染复杂数学公式的方案。这就是marked加MathJax组合的价值所在。marked是一个极简、高效的 Markdown 解析器它能快速将你写的# 标题、**加粗**转换成标准的 HTML 标签。而MathJax是一个老牌且强大的数学公式渲染引擎它能够识别页面中的 LaTeX 或 MathML 语法并将其渲染成美观的数学符号。这个项目的核心就是让这两个“专家”协同工作先用marked处理掉所有常规的 Markdown 语法生成一个包含原始公式文本的 HTML 片段然后由MathJax上场在这个 HTML 片段中扫描并渲染所有被特殊标记比如$$...$$或\\(...\\)包裹的数学公式。最终我们得到的是一个完整的、支持数学公式渲染的 HTML 页面。这个过程听起来简单但其中涉及到解析器的扩展、渲染时机的控制、以及一些常见的“坑”比如公式中的下划线被误解析为强调标签。接下来我会详细拆解整个实现流程并分享我在多个项目中积累下来的实战经验。2. 核心工具选型与原理剖析2.1 为什么是 marked 和 MathJax在 JavaScript 生态里Markdown 解析器和数学公式渲染器都有不少选择。我最终锁定marked和MathJax这个组合是基于以下几个核心考量marked 的优势纯粹与高效marked的设计哲学就是“做一件事并做好”。它没有内置的语法高亮、表格扩展等杂七杂八的功能这反而让我们可以更干净地集成其他专业库如 MathJax。它的解析速度非常快对于即时预览等场景至关重要。可扩展性强marked提供了丰富的renderer和tokenizer选项允许我们深度定制解析过程。这正是我们保护数学公式语法不被错误解析的关键入口。社区稳定作为一个历史悠久且被广泛使用的库GitHub 本身早期就使用它其 API 稳定遇到问题容易找到解决方案。MathJax 的优势渲染质量顶尖MathJax渲染的公式无论是在屏幕显示还是打印输出上质量都无可挑剔。它支持多种输出格式如 CommonHTML, SVG并能根据上下文自动调整公式的字体和布局。对 LaTeX 语法支持最全面从基本的分数、根号到复杂的矩阵、多行公式对齐MathJax几乎支持所有常用的 LaTeX 数学宏包。这对于学术和技术写作来说是刚需。动态渲染能力MathJax可以监控 DOM 变化对新增的数学公式进行延迟渲染这完美契合了我们“先由 marked 生成 HTML再由 MathJax 处理公式”的流水线作业模式。对比其他方案例如使用remark生态链或KaTeXremark功能强大但更复杂适合构建复杂的文档处理管道对于“解析渲染”这个简单目标来说有些重。KaTeX速度极快但语法支持不如MathJax全面例如某些\begin{}...\end{}环境且渲染策略更静态。MathJax v3在速度上已有巨大提升兼顾了质量和性能。因此markedMathJax的组合在功能、灵活性、易用性上取得了最佳平衡。2.2 协同工作原理图解整个流程是一个清晰的“预处理 - 解析 - 后处理”管道[原始 Markdown 文本] | v (输入) [marked 解析器] |-- 1. 识别并“保护”数学公式区块通过自定义渲染器 |-- 2. 解析其余 Markdown 语法标题、列表、代码等 | v (输出) [包含“原始公式文本”的 HTML 片段] | v (注入DOM) [浏览器中的 DOM 树] | v (MathJax 启动) [MathJax 引擎] |-- 1. 扫描整个文档或指定容器 |-- 2. 查找 LaTeX 分隔符$$, \(\) |-- 3. 将公式文本转换为 MathML 或 SVG 等格式 | v (最终呈现) [支持精美数学公式的完整页面]这个流程中最关键的一步是“保护”。默认情况下marked会把_解析为em斜体把\\解析为转义。而 LaTeX 公式中大量使用_表示下标使用\\表示换行。如果让marked直接处理公式就会被破坏得面目全非。因此我们必须通过配置告诉marked“遇到看起来像数学公式的东西别动它原样输出即可。”3. 详细实现步骤与配置解析3.1 环境准备与基础安装首先我们需要创建一个项目环境并安装核心依赖。这里以在一个简单的 Node.js 环境或现代前端工程如 Vite、Webpack中集成为例。# 在你的项目目录下初始化并安装依赖 npm init -y npm install marked mathjax3注意这里安装的是mathjax3即 MathJax 的第 3 版。v3 版本相比 v2 在 API 设计、包大小和性能上都有革命性改进是当前项目的首选。其核心包mathjax体积很小具体渲染组件如tex输入、chtml输出会按需加载。安装完成后基本的项目结构可能如下your-project/ ├── index.html ├── main.js ├── package.json └── style.css3.2 配置 marked 以保护数学公式语法这是整个项目的核心技巧。我们需要自定义marked的渲染器renderer覆盖其中可能干扰数学公式语法的规则。// main.js import { marked } from marked; // 1. 获取默认的渲染器 const renderer new marked.Renderer(); // 2. 覆盖 codespan 渲染方法用于行内代码 code。 // 默认情况下marked 不会解析行内代码中的 Markdown这本来是个安全特性。 // 但我们也可以利用它不过这里更关键的是覆盖 em 和 strong。 // 实际上更直接的方法是配置 marked 的选项避免在公式内解析这些符号。 // 3. 关键配置设置 marked 的选项禁用对某些符号在“可能为公式”的上下文中的解析。 // 但 marked 本身不识别公式上下文。因此更通用的策略是先让 marked 正常解析 // 然后我们通过正则表达式“保护”公式区域。但这种方法容易出错。 // 社区公认的最佳实践是使用 marked 的扩展语法或自定义 lexer/parser。 // 一个简单有效的方法是在将文本交给 marked 前我们将公式部分替换为占位符。 // 以下是更可靠、更清晰的实现方案 /** * 自定义 marked 扩展防止公式内的特殊字符被转义 * 原理我们告诉 marked$...$ 和 $$...$$ 包裹的内容是“代码”不应进行 Markdown 解析。 */ renderer.codespan (code) { // 检查是否是行内数学公式单$包裹。这里我们假设用户使用 $...$ 和 $$...$$ // 注意这个判断在 renderer 层面做有点晚因为 token 已经生成。 // 更好的方式是在 marked.parse 时通过 walkTokens 钩子处理。 }; // 因此我们采用 walkTokens 钩子它在词法分析后、渲染前执行是处理 token 的理想位置。 marked.use({ walkTokens(token) { // 只处理 text 类型的 token if (token.type text) { // 这里是一个简化处理。更健壮的做法需要使用正则表达式匹配公式分隔符 // 并将匹配到的公式文本的 token 类型改为 math自定义 // 然后在 renderer 中为 ‘math’ 类型提供自定义渲染。 // 但这对初学者较复杂。 } } }); // 鉴于上述复杂性对于大多数实际应用我推荐以下“够用且稳定”的方案 // 方案A使用 marked 的 breaks: false 并配合 MathJax 的 processEscapes: true。 // 方案B使用一个第三方插件如 marked-math如果有维护。 // 方案C本文采用在将 Markdown 字符串传递给 marked 前进行简单的预处理。 /** * 预处理函数将数学公式区块用特殊的、唯一的占位符包裹起来。 * marked 解析后再将占位符替换回原始公式文本。 * 这样可以完美避免 marked 的解析干扰。 */ function preprocessMarkdown(markdown) { // 匹配行内公式 $...$ 和块级公式 $$...$$ const inlinePattern /\$(.*?[^\\])\$/g; // 简单匹配不处理嵌套$ const blockPattern /\$\$([\s\S]*?)\$\$/g; const inlinePlaceholders []; const blockPlaceholders []; // 第一步替换块级公式 let processed markdown.replace(blockPattern, (match, p1) { blockPlaceholders.push(p1); return \n\nBLOCK_MATH_${blockPlaceholders.length - 1}\n\n; }); // 第二步替换行内公式在替换块级公式后的文本上进行 processed processed.replace(inlinePattern, (match, p1) { inlinePlaceholders.push(p1); return INLINE_MATH_${inlinePlaceholders.length - 1}; }); return { processedText: processed, inlinePlaceholders, blockPlaceholders }; } /** * 后处理函数将 marked 输出的 HTML 中的占位符替换回 MathJax 可识别的分隔符。 */ function postprocessHtml(html, inlinePlaceholders, blockPlaceholders) { let finalHtml html; // 替换块级公式占位符 blockPlaceholders.forEach((formula, index) { const placeholder BLOCK_MATH_${index}; finalHtml finalHtml.replace(placeholder, $$${formula}$$); }); // 替换行内公式占位符 inlinePlaceholders.forEach((formula, index) { const placeholder INLINE_MATH_${index}; finalHtml finalHtml.replace(placeholder, $${formula}$); }); return finalHtml; } // 使用示例 const originalMarkdown # 数学示例 这是一个行内公式$E mc^2$。 这是一个块级公式 $$ \\int_{-\\infty}^{\\infty} e^{-x^2} dx \\sqrt{\\pi} $$ 公式结束。; const { processedText, inlinePlaceholders, blockPlaceholders } preprocessMarkdown(originalMarkdown); const rawHtml marked.parse(processedText); const finalHtml postprocessHtml(rawHtml, inlinePlaceholders, blockPlaceholders); console.log(finalHtml); // 输出h1数学示例/h1\np这是一个行内公式$E mc^2$。\n这是一个块级公式/p\np$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx \\sqrt{\\pi}\n$$\n公式结束。/p实操心得预处理/后处理的方法看似“笨”但极其有效和稳定。它完全隔离了marked和数学公式语法避免了任何复杂的解析规则冲突。占位符...要确保在原文中不会出现。在实际项目中我常用更复杂的 UUID 或!-- MATH_PLACEHOLDER_{id} --这样的 HTML 注释作为占位符安全性更高。3.3 集成并配置 MathJax 进行渲染得到包含正确公式分隔符的 HTML 后下一步就是让 MathJax 来渲染它们。MathJax v3 的配置比 v2 简洁得多。首先在index.html中引入 MathJax 并配置一个容器!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleMarkdown with MathJax/title !-- 引入 MathJax 核心库 -- script window.MathJax { tex: { inlineMath: [[$, $], [\\(, \\)]], // 行内公式分隔符 displayMath: [[$$, $$], [\\[, \\]]], // 块级公式分隔符 processEscapes: true, // 允许使用 \\ 来转义公式中的特殊字符 processEnvironments: true // 处理 \begin{}...\end{} 环境 }, options: { skipHtmlTags: [script, noscript, style, textarea, pre, code], // 跳过这些标签内的内容 ignoreHtmlClass: ignore-mathjax // 忽略带有此 class 的元素 }, startup: { pageReady: () { console.log(MathJax is ready.); // 初始渲染可以由后续的 JS 代码触发 return MathJax.startup.defaultPageReady(); } } }; /script script srchttps://cdn.jsdelivr.net/npm/mathjax3/es5/tex-mml-chtml.js idMathJax-script async/script link relstylesheet hrefstyle.css /head body div idcontent !-- 这里将由 JavaScript 动态插入生成的 HTML -- p初始内容... 公式如 $a^2 b^2 c^2$ 将在此处渲染。/p /div script typemodule src./main.js/script /body /html然后在main.js中我们将最终生成的 HTML 插入到页面并通知 MathJax 进行渲染// ... 之前的 marked 预处理和解析代码 ... // 假设 finalHtml 是经过 postprocessHtml 处理后的字符串 document.getElementById(content).innerHTML finalHtml; // 通知 MathJax 重新渲染页面中的数学公式 if (window.MathJax) { // MathJax v3 的 API MathJax.typesetPromise?.then(() { console.log(MathJax 公式渲染完成。); }).catch((err) { console.error(MathJax 渲染出错:, err); }); // 或者使用更通用的方法 // MathJax.typeset MathJax.typeset(); }3.4 样式优化与排版调整默认渲染的公式可能和你的网站样式不搭。MathJax 渲染的公式本身会带有一些内联样式并且包裹在mjx-container等标签中。我们可以通过 CSS 进行微调。/* style.css */ #content { font-family: Helvetica Neue, Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; } /* 调整块级公式的间距 */ mjx-container[jaxCHTML][displaytrue] { display: block; text-align: center; margin: 1em 0 !important; /* 添加上下边距 */ overflow-x: auto; /* 长公式可以横向滚动 */ } /* 调整行内公式的垂直对齐方式使其与文字中线对齐 */ mjx-container { vertical-align: middle; } /* 代码块和公式共存时的样式 */ pre, code { background-color: #f5f5f5; border-radius: 3px; } pre { padding: 1em; overflow: auto; } /* 确保代码块内的公式不被渲染 */ pre .MathJax, code .MathJax { display: none !important; }注意事项!important的使用要谨慎。因为 MathJax 会注入大量内联样式有时为了覆盖它们不得不使用。最好先检查 MathJax 生成的样式特性值。另外overflow-x: auto对于很长的公式如大型矩阵非常有用可以防止撑破布局。4. 高级技巧与性能优化4.1 处理代码块内的美元符号一个常见的冲突是在 Markdown 的代码块 或中可能会包含用于表示变量的美元符号如$PATH,echo $HOME。我们不希望这些美元符号被 MathJax 误认为是公式分隔符。我们的预处理函数已经通过“先替换公式”的方式部分解决了这个问题因为代码块内的文本在marked解析后会被放在code或pre标签内。MathJax 的配置skipHtmlTags: [pre, code]会告诉它跳过这些标签内的内容从而避免误渲染。但是为了万无一失可以在预处理阶段加强识别排除掉在代码块上下文中的$。这需要更复杂的解析可以利用marked的lexer先进行词法分析只对非代码类型的 token 进行公式占位符替换。这对于有复杂混合内容的文档是必要的优化。4.2 实现实时预览如编辑器在很多场景下如博客后台编辑器我们需要实现 Markdown 的实时预览。这意味着每当用户输入都需要执行“解析 - 插入DOM - 渲染公式”这个流程。直接这样做性能会很差尤其是当文档很长时。优化策略如下防抖Debounce监听输入事件但设置一个延迟如 500ms只有在用户停止输入一段时间后才触发预览更新。let previewTimeout; editorElement.addEventListener(input, () { clearTimeout(previewTimeout); previewTimeout setTimeout(updatePreview, 500); });增量更新这是一个更高级的优化。我们可以只更新预览区域中发生变化的部分。但这需要比较复杂的 DOM Diff 逻辑。一个折中方案是如果内容变化不大可以只对 MathJax 执行MathJax.typesetClear()清理再MathJax.typesetPromise()重新渲染而不是替换整个innerHTML。使用 Web Worker将marked.parse这个可能耗时的计算任务放到 Web Worker 中避免阻塞主线程导致输入卡顿。4.3 服务端渲染SSR与静态站点生成SSG如果你在使用 Next.js, Nuxt.js 或 Gatsby 等框架可能需要在构建时或服务端将 Markdown 转换为最终的 HTML。这意味着 MathJax 也需要在 Node.js 环境中运行。MathJax v3 提供了mathjax-full这个 npm 包可以在 Node.js 中无头运行。基本思路是在服务端使用marked或remark解析 Markdown。使用mathjax-full的 API 将 HTML 中的公式转换为 SVG 或 CommonHTML 字符串。将最终生成的、包含已渲染公式静态标签如svg的 HTML 发送给客户端。这样做的好处是客户端无需加载和运行 MathJax 核心库提升页面加载速度和首屏渲染性能。缺点是生成的 HTML 体积会变大且公式无法再动态重排如字体大小变化后。一个简单的服务端渲染示例// server-render.js (Node.js 环境) import { marked } from marked; import { mathjax } from mathjax-full/js/mathjax.js; import { TeX } from mathjax-full/js/input/tex.js; import { CHTML } from mathjax-full/js/output/chtml.js; import { AllPackages } from mathjax-full/js/input/tex/AllPackages.js; import { liteAdaptor } from mathjax-full/js/adaptors/liteAdaptor.js; // 1. 使用之前的预处理/后处理函数处理 Markdown const { processedText, inlinePlaceholders, blockPlaceholders } preprocessMarkdown(markdownContent); const rawHtml marked.parse(processedText); const htmlWithFormulas postprocessHtml(rawHtml, inlinePlaceholders, blockPlaceholders); // 2. 配置并初始化 MathJax const adaptor liteAdaptor(); const tex new TeX({ packages: AllPackages }); const chtml new CHTML({ fontURL: https://cdn.jsdelivr.net/npm/mathjax3/es5/output/chtml/fonts/woff-v2 }); // 注意字体URL const doc mathjax.document(, { InputJax: tex, OutputJax: chtml }); // 3. 将 HTML 字符串转换为 MathJax 文档并渲染公式 const node adaptor.parse(htmlWithFormulas, { fragment: true }); const processed doc.convert(node, { display: true, em: 16, ex: 8 }); // 设置字号和 ex 值 // 4. 获取处理后的 HTML 字符串 const finalHtml adaptor.innerHTML(processed); console.log(finalHtml); // 这就是可以发送给客户端的、包含渲染后公式的静态 HTML踩坑记录服务端渲染 MathJax 时字体路径(fontURL) 是关键。必须确保客户端能访问到这个路径下的字体文件.woff2否则公式显示为乱码或方框。通常直接使用 CDN 地址是最简单的。另外计算em和ex单位时要和客户端的 CSS 基准字体大小匹配。5. 常见问题排查与解决方案实录在实际集成过程中你几乎一定会遇到下面这些问题。这里是我总结的“排坑指南”。5.1 公式没有渲染显示为原始 LaTeX 代码可能原因及解决方案现象可能原因解决方案公式文本$$...$$原样显示1. MathJax 库未加载或加载失败。2. MathJax 配置错误分隔符不匹配。3. 包含公式的 DOM 元素在 MathJax 启动后才被插入。1. 检查浏览器控制台有无网络错误或 JS 错误。确保MathJax-script标签的src正确且网络可达。2. 核对window.MathJax.tex.inlineMath和displayMath的配置是否与你使用的分隔符一致你用的是$$还是\[。3. 确保在公式 HTML 插入到 DOM之后再调用MathJax.typesetPromise()或相关渲染 API。只有部分公式渲染1. 公式语法有误MathJax 解析失败。2. 公式被包裹在skipHtmlTags配置指定的标签内如pre。3. 预处理阶段公式文本被意外破坏如转义符处理不当。1. 检查未渲染的公式语法常见错误如花括号不匹配、未转义的反斜杠\。在浏览器控制台查看 MathJax 的警告信息。2. 如果你希望代码块内的公式也被渲染需要调整skipHtmlTags配置但这通常不是好主意。3. 在预处理和后处理函数中添加console.log对比输入和输出的公式文本是否一致。确保反斜杠\被正确保留在 JS 字符串中需要写为\\。5.2 公式样式错乱或布局问题可能原因及解决方案现象可能原因解决方案行内公式垂直对齐不对MathJax 生成的容器元素与周围文字基线未对齐。为mjx-container元素添加 CSSvertical-align: middle;。有时可能需要微调margin或padding。块级公式溢出容器公式过长如很长的积分式或矩阵超过父容器宽度。为块级公式的容器[displaytrue]添加 CSSoverflow-x: auto;并确保其display属性为block。这样会产生横向滚动条。公式字体大小与正文不协调MathJax 默认字体大小与你的网站 CSS 基准字体不匹配。在 MathJax 配置的startup部分或输出配置中调整em和ex的像素值。或者在 CSS 中覆盖.MJX-TEX等相关类的字体大小属性注意样式优先级。5.3 性能问题页面加载慢或输入卡顿可能原因及解决方案现象可能原因解决方案页面首次加载白屏时间长MathJax 核心库较大网络下载和初始化耗时。1.使用 CDN 并利用浏览器缓存。2. 对于静态站点考虑服务端渲染SSR将公式预先渲染为静态 SVG/HTML客户端无需加载 MathJax。3. 如果公式不多可以评估使用更轻量的KaTeX。在编辑器中输入时预览卡顿每次输入都触发全量 Markdown 解析和公式渲染。1.实施防抖Debounce如 300-500ms 延迟。2. 将 Markdown 解析marked.parse放入Web Worker。3. 对于极长的文档探索增量渲染只更新可视区域或变化的部分。5.4 反斜杠\被错误转义或丢失这是最隐蔽也最常见的问题。在 JavaScript 字符串中反斜杠是转义字符。LaTeX 命令如\frac、\sqrt在字符串中需要写成\\frac、\\sqrt。问题场景从数据库或 API 获取的 Markdown 文本其中的\可能已经是正确格式。但在 JS 代码中硬编码的示例文本必须使用双反斜杠。预处理过程中如果使用字符串替换或正则表达式操作不当可能会丢失或增加反斜杠。调试技巧 在预处理和后处理的关键节点用console.log(JSON.stringify(text))输出文本。JSON.stringify会将字符串中的反斜杠显示为\\便于你精确查看其数量。确保最终交给 MathJax 的字符串中LaTeX 命令是单反斜杠格式例如在 DOM 的textContent里看到的是\sqrt。我个人在项目中会建立一个专门的sanitizeLatex函数用于统一处理从各种来源来的文本确保反斜杠的规范性。6. 完整示例与代码整合最后我将提供一个整合了所有最佳实践的、可直接运行的简化示例。这个示例使用 ES 模块并包含了防抖的实时预览功能。项目结构demo/ ├── index.html ├── main.js ├── markdown-processor.js └── style.cssindex.html:!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleMarkdown MathJax 实时预览/title script window.MathJax { tex: { inlineMath: [[$, $]], displayMath: [[$$, $$]], processEscapes: true, }, options: { skipHtmlTags: [script, noscript, style, textarea, pre, code], } }; /script script srchttps://cdn.jsdelivr.net/npm/mathjax3/es5/tex-mml-chtml.js idMathJax-script async/script link relstylesheet hrefstyle.css /head body div classcontainer div classeditor-pane h2编辑区 (Markdown)/h2 textarea ideditor placeholder输入 Markdown支持数学公式...# 示例 这是一个**行内公式** $\\vec{F} m\\vec{a}$。 这是一个**块级公式** $$ \\begin{aligned} \\nabla \\times \\vec{E} -\\frac{\\partial \\vec{B}}{\\partial t} \\\\ \\nabla \\times \\vec{H} \\vec{J} \\frac{\\partial \\vec{D}}{\\partial t} \\end{aligned} $$ 代码块 内的美元符号不会被渲染 echo $PATH。/textarea /div div classpreview-pane h2预览区 (HTML)/h2 div idpreview/div /div /div script typemodule src./main.js/script /body /htmlmarkdown-processor.js:import { marked } from marked; // 配置 marked例如设置高亮如果需要 // marked.setOptions({ highlight: ... }); /** * 核心预处理函数保护数学公式 */ function protectMath(markdown) { const blockPlaceholders []; const inlinePlaceholders []; // 先处理块级公式避免与行内公式混淆 let step1 markdown.replace(/\$\$([\s\S]*?)\$\$/g, (match, formula) { blockPlaceholders.push(formula); return \n\nMATH_BLOCK_${blockPlaceholders.length - 1}\n\n; }); // 再处理行内公式 let step2 step1.replace(/(^|[^\\])\$([^$\n]?)\$/g, (match, prefix, formula) { // 注意这个正则不能完美处理所有边界情况例如公式内的$。 // 对于生产环境建议使用更严谨的解析器或 lexer。 inlinePlaceholders.push(formula); return ${prefix}MATH_INLINE_${inlinePlaceholders.length - 1}; }); return { processed: step2, blockPlaceholders, inlinePlaceholders }; } /** * 核心后处理函数恢复数学公式 */ function restoreMath(html, blockPlaceholders, inlinePlaceholders) { let result html; blockPlaceholders.forEach((formula, idx) { result result.replace(MATH_BLOCK_${idx}, $$${formula}$$); }); inlinePlaceholders.forEach((formula, idx) { result result.replace(MATH_INLINE_${idx}, $${formula}$); }); return result; } /** * 主函数将 Markdown 转换为等待 MathJax 渲染的 HTML */ export function markdownToHtml(markdown) { const { processed, blockPlaceholders, inlinePlaceholders } protectMath(markdown); const rawHtml marked.parse(processed); const finalHtml restoreMath(rawHtml, blockPlaceholders, inlinePlaceholders); return finalHtml; }main.js:import { markdownToHtml } from ./markdown-processor.js; const editor document.getElementById(editor); const preview document.getElementById(preview); let renderTimeout null; function updatePreview() { const markdownText editor.value; const html markdownToHtml(markdownText); preview.innerHTML html; // 使用 MathJax v3 的 API 重新渲染公式 if (window.MathJax MathJax.typesetPromise) { MathJax.typesetPromise([preview]).catch(err { console.error(MathJax typeset error:, err); }); } } // 初始渲染 updatePreview(); // 防抖输入监听 editor.addEventListener(input, () { clearTimeout(renderTimeout); renderTimeout setTimeout(updatePreview, 400); // 400ms 防抖 }); // 可选处理 MathJax 加载完成前的公式 if (window.MathJax) { MathJax.startup.promise.then(() { console.log(MathJax 初始化完成重新渲染。); updatePreview(); }); }style.css:body { margin: 0; font-family: sans-serif; } .container { display: flex; height: 100vh; } .editor-pane, .preview-pane { flex: 1; padding: 20px; box-sizing: border-box; overflow: auto; } #editor { width: 100%; height: 90%; font-family: Monaco, Consolas, monospace; font-size: 14px; line-height: 1.5; border: 1px solid #ccc; border-radius: 4px; padding: 10px; resize: none; } #preview { border: 1px solid #eee; border-radius: 4px; padding: 20px; min-height: 200px; background-color: #fafafa; } /* MathJax 公式样式微调 */ mjx-container[jaxCHTML][displaytrue] { margin: 1em 0; overflow-x: auto; }将这个示例保存到本地并用一个现代浏览器打开index.html你就得到了一个功能完整的、支持数学公式的 Markdown 实时预览器。你可以在此基础上根据实际需求添加更多功能比如代码高亮集成highlight.js、导出 HTML、主题切换等。整个项目从原理到实现的脉络就是这样。最关键的是理解“保护-解析-恢复”这个核心流程它有效地解耦了 Markdown 解析和数学公式渲染这两个独立的任务。掌握了这个方法你不仅可以处理数学公式还可以将此模式扩展到其他需要保护特定语法的场景中。