nodejs: 能在线编辑 Markdown 文档的 Web 服务程序,更多扩展功能

承上一篇:nodejs: 能在线编辑 Markdown 文档的 Web 服务程序

如果需要更多 Markdown 扩展(如表格、数学公式)等功能,怎样编写?

已经采用了移除服务端 mermaid 依赖的方案,现在想要为这个 Markdown 编辑器扩展表格、数学公式等功能,继续完善代码,添加这些常用的 Markdown 扩展能力,同时保持代码的简洁和可维护性。

实现思路

  1. 表格支持marked 本身已内置 GitHub 风格的表格解析,只需确保启用相关配置
  2. 数学公式支持:集成 katexmathjax 来渲染 LaTeX 格式的数学公式
  3. 代码高亮:添加 highlight.js 增强代码块的语法高亮效果
  4. 其他扩展:支持任务列表、脚注等常用 Markdown 扩展

完整修改后的代码

1. 安装新增依赖
安装代码高亮和数学公式渲染相关依赖

npm install marked-highlight highlight.js

2. 编写 app2.js(服务端增强 Markdown 解析)
// Web Server 程序const express =require('express');const marked =require('marked');const markedHighlight =require('marked-highlight');const hljs =require('highlight.js');const cors =require('cors');const bodyParser =require('body-parser');const fs =require('fs-extra');const path =require('path');// 初始化 Express 应用const app =express();// 配置静态文件目录 app.use(express.static(path.join(__dirname,'public')));// 配置 Marked 解析器,添加高亮、表格、公式等支持// 配置代码高亮const highlightOptions ={langPrefix:'hljs language-',highlight(code, lang){if(lang && hljs.getLanguage(lang)){try{return hljs.highlight(code,{language: lang }).value;}catch(err){ console.error(`高亮 ${lang} 代码失败:`, err);}}// 自动检测语言return hljs.highlightAuto(code).value;}};// 创建带高亮的 rendererconst renderer =newmarked.Renderer();// 重写代码块渲染逻辑,识别 mermaid 代码块 renderer.code=(code, language)=>{if(language ==='mermaid'){// 为 Mermaid 代码块生成容器(仅保留代码,渲染交给前端)return`<div>${code}</div>`;}// 其他代码块使用高亮渲染return`<pre><codetoken interpolation">${language}">${ highlightOptions.highlight(code, language)}</code></pre>`;};// 配置 marked 核心选项(启用所有扩展) marked.setOptions({renderer: renderer,extensions:[// 启用表格扩展(GFM 已包含,但显式声明更清晰){name:'table',level:'block',start:(src)=> src.match(/^\|/)?.index },// 启用任务列表{name:'tasklist',level:'block',start:(src)=> src.match(/^\s*-\s\[\s?x?\]\s/)?.index }],gfm:true,// 启用 GitHub 风格 Markdownbreaks:true,// 支持换行符tables:true,// 启用表格tasklists:true,// 启用任务列表footnotes:true,// 启用脚注smartypants:true// 启用智能标点});// 中间件配置 app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended:true})); app.use(express.static(path.join(__dirname,'public')));// 设置模板引擎 app.set('view engine','ejs'); app.set('views', path.join(__dirname,'views'));// 确保保存文件的目录存在constDOCS_DIR= path.join(__dirname,'docs'); fs.ensureDirSync(DOCS_DIR);// 路由配置// 首页 - 编辑器界面 app.get('/',(req, res)=>{ res.render('editor',{title:'Markdown 在线编辑器 (支持表格/公式/Mermaid)'});});// 解析 Markdown 为 HTML (API) app.post('/api/parse',(req, res)=>{try{const{ markdown }= req.body;if(!markdown){return res.status(400).json({error:'Markdown 内容不能为空'});}// 解析 Markdown 为 HTMLconst html = marked.parse(markdown); res.json({ html });}catch(error){ res.status(500).json({error:'解析 Markdown 失败: '+ error.message });}});// 保存文档 (API) app.post('/api/save',(req, res)=>{try{const{ filename, content }= req.body;if(!filename ||!content){return res.status(400).json({error:'文件名和内容不能为空'});}// 拼接文件路径const filePath = path.join(DOCS_DIR,`${filename}.md`);// 写入文件 fs.writeFileSync(filePath, content,'utf8'); res.json({success:true,message:'文件保存成功', filePath });}catch(error){ res.status(500).json({error:'保存文件失败: '+ error.message });}});// 加载文档 (API) app.get('/api/load/:filename',(req, res)=>{try{const{ filename }= req.params;const filePath = path.join(DOCS_DIR,`${filename}.md`);// 检查文件是否存在if(!fs.existsSync(filePath)){return res.status(404).json({error:'文件不存在'});}// 读取文件内容const content = fs.readFileSync(filePath,'utf8'); res.json({success:true, content });}catch(error){ res.status(500).json({error:'加载文件失败: '+ error.message });}});// 获取文档列表 (API) app.get('/api/docs',(req, res)=>{try{// 读取目录下所有 .md 文件const files = fs.readdirSync(DOCS_DIR).filter(file=> path.extname(file)==='.md').map(file=>({name: path.basename(file,'.md'),path: file })); res.json({success:true,docs: files });}catch(error){ res.status(500).json({error:'获取文档列表失败: '+ error.message });}});// 启动服务器//const PORT = process.env.PORT || 8000;constPORT=8000; app.listen(PORT,()=>{ console.log(`服务器运行在: http://localhost:${PORT}`); console.log(`文档保存目录: ${DOCS_DIR}`);});
3. 编辑 views/editor.ejs(前端添加公式渲染和样式)
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title><%= title %></title><!-- 1. 引入 Mermaid --><scriptsrc="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script><!-- 2. 引入 highlight.js 核心脚本(关键修复) --><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script><!-- 3. 引入代码高亮样式 --><linkrel="stylesheet"href="https://cdn.jsdelivr.net/npm/highlight.js/styles/github-dark.min.css"><!-- 4. 引入 KaTeX 用于数学公式渲染 --><linkrel="stylesheet"href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"><scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js"></script><scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js"></script><style>*{box-sizing: border-box;margin: 0;padding: 0;}body{font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;display: flex;flex-direction: column;height: 100vh;}.header{background: #2c3e50;color: white;padding: 1rem;text-align: center;}.container{display: flex;flex: 1;overflow: hidden;}.editor-container, .preview-container{flex: 1;padding: 1rem;overflow: hidden;border: 1px solid #ddd;}textarea{width: 100%;height: 100%;padding: 1rem;font-family:'Consolas','Monaco', monospace;font-size: 14px;border: none;outline: none;resize: none;}.preview{width: 100%;height: 100%;overflow-y: auto;padding: 1rem;background: #f9f9f9;line-height: 1.6;}.controls{padding: 1rem;background: #f1f1f1;display: flex;gap: 1rem;}button{padding: 0.5rem 1rem;background: #3498db;color: white;border: none;border-radius: 4px;cursor: pointer;}button:hover{background: #2980b9;}.file-input{padding: 0.5rem;}/* 表格样式优化 */table{border-collapse: collapse;width: 100%;margin: 1rem 0;}th, td{border: 1px solid #ddd;padding: 8px 12px;text-align: left;}th{background-color: #f2f2f2;font-weight: bold;}tr:nth-child(even){background-color: #f9f9f9;}/* 任务列表样式 */.task-list-item{list-style-type: none;margin: 0.5rem 0;}/* 脚注样式 */.footnote-ref{vertical-align: super;font-size: 0.8em;}.footnotes{margin-top: 2rem;border-top: 1px solid #ddd;padding-top: 1rem;font-size: 0.9em;}/* 代码块样式优化 */pre{padding: 1rem;border-radius: 6px;margin: 1rem 0;overflow-x: auto;}code{font-family:'Consolas','Monaco', monospace;}/* 公式样式 */.katex{font-size: 1.1em !important;}</style></head><body><divclass="header"><h1>Markdown 在线编辑器 (支持表格/公式/Mermaid)</h1></div><divclass="controls"><inputtype="text"id="filename"placeholder="输入文件名(无需.md)"value="demo"><buttonid="saveBtn">保存文档</button><buttonid="loadBtn">加载文档</button><buttonid="refreshBtn">刷新预览</button></div><divclass="container"><divclass="editor-container"><textareaid="editor"placeholder="请输入 Markdown 内容..."></textarea></div><divclass="preview-container"><divid="preview"class="preview"></div></div></div><script>// 初始化 Mermaid mermaid.initialize({startOnLoad:false,theme:'default'});// 获取 DOM 元素const editor = document.getElementById('editor');const preview = document.getElementById('preview');const saveBtn = document.getElementById('saveBtn');const loadBtn = document.getElementById('loadBtn');const refreshBtn = document.getElementById('refreshBtn');const filenameInput = document.getElementById('filename');// 解析 Markdown 并更新预览asyncfunctionupdatePreview(){try{const markdown = editor.value;const response =awaitfetch('/api/parse',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ markdown })});const result =await response.json();if(result.error){ preview.innerHTML =`<div>解析错误: ${result.error}</div>`;return;}// 更新预览内容 preview.innerHTML = result.html;// 渲染 Mermaid 图表 mermaid.run({querySelector:'.mermaid',node: preview });// 渲染数学公式renderMathInElement(preview,{delimiters:[{left:'$$',right:'$$',display:true},// 块级公式{left:'$',right:'$',display:false},// 行内公式{left:'\\(',right:'\\)',display:false},// LaTeX 行内{left:'\\[',right:'\\]',display:true}// LaTeX 块级],throwOnError:false,strict:false});// 高亮所有代码块(兜底) document.querySelectorAll('pre code').forEach((block)=>{ hljs.highlightElement(block);});}catch(error){ preview.innerHTML =`<div>预览更新失败: ${error.message}</div>`;}}// 保存文档asyncfunctionsaveDocument(){const filename = filenameInput.value.trim();const content = editor.value;if(!filename){alert('请输入文件名');return;}try{const response =awaitfetch('/api/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ filename, content })});const result =await response.json();if(result.error){alert('保存失败: '+ result.error);}else{alert('保存成功!');}}catch(error){alert('保存失败: '+ error.message);}}// 加载文档asyncfunctionloadDocument(){const filename = filenameInput.value.trim();if(!filename){alert('请输入要加载的文件名');return;}try{const response =awaitfetch(`/api/load/${filename}`);const result =await response.json();if(result.error){alert('加载失败: '+ result.error);}else{ editor.value = result.content;updatePreview();alert('加载成功!');}}catch(error){alert('加载失败: '+ error.message);}}// 绑定事件 refreshBtn.addEventListener('click', updatePreview); saveBtn.addEventListener('click', saveDocument); loadBtn.addEventListener('click', loadDocument);// 初始加载预览 window.addEventListener('load', updatePreview);// 可选:实时预览(取消注释启用)// editor.addEventListener('input', debounce(updatePreview, 300));// 防抖函数(实时预览时避免频繁请求)functiondebounce(func, wait){let timeout;returnfunction(...args){clearTimeout(timeout); timeout =setTimeout(()=>func.apply(this, args), wait);};}</script></body></html>
4. 编写 docs/demo.md
# Markdown 编辑器 (增强版) ## 1. 表格示例 | 姓名 | 年龄 | 职业 | |------|------|------| | 张三 | 25 | 工程师 | | 李四 | 30 | 设计师 | | 王五 | 28 | 产品经理 | ## 2. 数学公式示例 ### 行内公式 欧拉公式:$e^{i\pi} + 1 = 0$ ### 块级公式 $$ \int_{0}^{\infty} e^{-x^2} dx = \frac{\sqrt{\pi}}{2} $$ 二次方程求根公式: $$ x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$ ## 3. 任务列表示例 - [x] 完成 Markdown 解析 - [x] 集成表格支持 - [ ] 优化公式渲染 - [ ] 测试兼容性 ## 4. 代码高亮示例 ```javascript // JavaScript 代码示例 function calculateArea(radius) { const PI = Math.PI; return PI * radius * radius; } console.log(calculateArea(5)); // 78.53981633974483 
# Python 代码示例deffibonacci(n): a, b =0,1for _ inrange(n):yield a a, b = b, a + b print(list(fibonacci(10)))

5. Mermaid 流程图示例

编辑

预览

开始

选择功能

Markdown 编辑

实时预览

支持表格/公式/代码高亮

保存文档

结束

## 6. 脚注示例 这是一个带脚注的文本[^1],还有另一个脚注[^2]。 [^1]: 第一个脚注的内容 [^2]: 第二个脚注的内容,支持多行 这是第二行内容 这是第三行(缩进显示) 

运行与使用说明

  1. 创建目录结构
 md-edit-app/ ├── app.js 和 app2.js // 服务器主程序 ├── views/ // 模板目录 │ └── edit.ejs 和 editor.ejs // 编辑器页面 ├── public/ // 静态资源目录 │ └── css 和 js // 静态子目录 └── docs/ // 文档保存目录 (自动创建) 

所有的 *.min.js , 可用 curl -O https://… 下载,都放在 public/js/
所有的 *.css , 可用 curl -O https://… 下载,都放在 public/css/

  1. 重启服务器:修改代码后,重启 Node.js 服务使配置生效
    node app2.js
  2. 功能测试
    • 表格:直接使用 GitHub 风格的表格语法即可渲染
    • 数学公式:使用 $公式$(行内)或 $$公式$$(块级)包裹 LaTeX 公式
    • 任务列表:使用 - [x](完成)或 - [ ](未完成)格式
    • 代码高亮:自动识别代码语言并高亮,支持上百种编程语言
    • 脚注:使用 [^标记] 定义脚注,文末添加 [^标记]: 内容

扩展更多功能的建议

  1. 图片上传:可以集成 multer 实现本地图片上传,或对接第三方图库
  2. 目录生成:解析 Markdown 标题,自动生成导航目录
  3. 导出功能:添加导出为 HTML/PDF 的功能(可使用 puppeteer
  4. 自定义样式:允许用户切换不同的预览主题(如浅色/深色模式)
  5. 自动保存:添加 localStorage 或定时保存功能,防止内容丢失

总结

  1. 核心扩展:通过启用 marked 的内置配置支持表格、任务列表、脚注;集成 katex 实现数学公式渲染;使用 highlight.js 增强代码高亮。
  2. 实现方式:服务端负责 Markdown 解析和扩展支持,前端负责公式、Mermaid 的渲染和样式美化。
  3. 使用要点:数学公式使用 LaTeX 语法,表格使用 GitHub 风格语法,所有扩展功能无需额外配置即可直接使用。

现在你的编辑器已经具备了完整的 Markdown 扩展能力,涵盖表格、数学公式、代码高亮、任务列表、脚注等常用功能,完全满足日常编辑需求。

Read more

B站PC端web自动开启字幕脚本(2026新版适配)

B站自动字幕用户脚本:快捷键开关 + 自动开启字幕(2026新版适配) 作者:Apixus 更新日期:2026年3月5日 项目地址:GitHub仓库 一、脚本介绍 你是否经常在B站看视频时反复手动开启字幕?是否希望切换视频时字幕能自动开启? 这个用户脚本就是为了解决这些问题而开发的。 B站自动字幕脚本 提供了以下功能: * 🎯 快捷键控制:按 C 键快速开启或关闭字幕 * 🔄 自动开启:切换分P、点击推荐视频时自动打开字幕 * 🆕  2026新版适配:专为B站最新版播放器优化 * ⚡ 性能优化:智能监听,告别卡顿轮询 * 🛡️ 防冲突:自动识别输入框,避免误触 二、适用页面 * 普通视频页:https://www.bilibili.com/video/* * 播放列表页:https://www.bilibili.com/list/* 支持普通视频页、番剧页、播放列表页等常见场景。 三、

STM32H743定时器触发ADC多通道采样与DMA传输实战

1. 项目背景与需求分析 最近在做一个电源采集板的项目,使用STM32H743芯片实现三路10kHz电流采集和三路差分电压采集。电流采集需要精确的定时采样,而电压采集则对频率没有严格要求。为了减少CPU占用,我选择了定时器触发ADC采样配合DMA传输的方案。这样CPU只需要在数据准备好后处理即可,大大提高了系统效率。 在实际项目中,高频数据采集对实时性要求很高。如果直接用CPU控制ADC采样,会占用大量资源,甚至可能导致数据丢失。而定时器触发ADC配合DMA传输,就像是在工厂里设置了一条自动化生产线:定时器负责发出"开始生产"的信号,ADC负责"生产"数据,DMA则负责"搬运"数据到指定仓库,整个过程不需要CPU参与。 这种方案特别适合需要高频采样的应用场景,比如电源监控、电机控制、音频处理等。STM32H743作为高性能MCU,其ADC和DMA功能非常强大,但配置起来也有些坑需要注意。接下来我就详细分享下我的实战经验。 2. 硬件平台与开发环境 我使用的是STM32H743ZI芯片,这是ST公司基于Cortex-M7内核的高性能微控制器,主频高达480MHz,内置3个16

【前端】前端面试题

【前端】前端面试题

前端面试题 闭包 1. 定义: 闭包(Closure) 是指一个函数能够访问并记住其外部作用域中的变量,即使外部函数已经执行完毕。闭包由两部分组成: * 一个函数(通常是内部函数)。 * 该函数被创建时所在的作用域(即外部函数的变量环境) functionouter(){let count =0;// 外部函数的变量functioninner(){ count++;// 内部函数访问外部变量 console.log(count);}return inner;}const counter =outer();counter();// 输出 1counter();// 输出 2 2. 闭包的核心原理 * 作用域链:函数在定义时,会记住自己的词法环境(即外部作用域)。当内部函数访问变量时,会沿着作用域链向上查找。 * 变量持久化:闭包使得外部函数的变量不会被垃圾回收,因为内部函数仍持有对它们的引用 3. 闭包的常见用途 3.1 私有变量封装 通过闭包隐藏内部变量,

深入理解前端防抖(Debounce)与节流(Throttle):原理、区别与实战示例

深入理解前端防抖(Debounce)与节流(Throttle):原理、区别与实战示例

深入理解前端防抖(Debounce)与节流(Throttle):原理、区别与实战示例 📌 引言 在前端开发中,我们经常需要处理高频事件(如输入框输入、滚动、窗口调整大小等)。如果不加限制,浏览器会频繁触发回调函数,导致性能问题,甚至页面卡顿。 防抖(Debounce) 和 节流(Throttle) 是两种优化方案,可以有效控制事件触发的频率,提高应用的性能和用户体验。 本篇文章将详细解析 防抖和节流的原理、适用场景及代码实现,帮助你更好地优化前端应用。 1. 什么是防抖(Debounce)? 📝 概念 防抖是一种在事件触发后延迟执行的技术,如果在延迟期间事件被再次触发,计时器会重置,重新计算延迟时间。 核心思想:短时间内多次触发,只执行最后一次。 📌 适用场景 * 搜索框输入(防止用户每次输入都发送请求) * 窗口调整大小(resize)(防止短时间内多次触发计算) * 表单输入验证(用户停止输入后再进行验证) ✅ 代码实现 functiondebounce(fn,