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

Flutter for OpenHarmony: Flutter 三方库 ntp 精准同步鸿蒙设备系统时间(分布式协同授时利器)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 分布式开发、金融交易或具有严格时效性的业务(如:秒杀倒计时、双因素认证 OTP)时,开发者不能完全信任设备本地的系统时间。用户可能为了某种目的手动篡改时间,或者由于网络同步问题导致时间存在偏差。 ntp 软件包提供了一种直接与互联网授时中心(NTP 服务器)通信的能力。它能绕过本地系统时钟,获取绝对精准的 UTC 时间,并计算出本地时间与真实时间的“偏移量(Offset)”。 一、核心授时原理 ntp 通过测量往返网络延迟来消除误差。 发送 NTP 请求 (UDP) 返回高精度时间戳 鸿蒙 App 全球授时中枢 (pool.ntp.org) 计算网络往返耗时 (RTT) 得出绝对时间偏移量 生成鸿蒙业务专用准时 二、

By Ne0inhk
Spring Boot 数据导入导出与报表生成

Spring Boot 数据导入导出与报表生成

Spring Boot 数据导入导出与报表生成 24.1 学习目标与重点提示 学习目标:掌握Spring Boot数据导入导出与报表生成的核心概念与使用方法,包括数据导入导出的定义与特点、Spring Boot与数据导入导出的集成、Spring Boot与数据导入导出的配置、Spring Boot与报表生成的基本方法、Spring Boot的实际应用场景,学会在实际开发中处理数据导入导出与报表生成问题。 重点:数据导入导出的定义与特点、Spring Boot与数据导入导出的集成、Spring Boot与数据导入导出的配置、Spring Boot与报表生成的基本方法、Spring Boot的实际应用场景。 24.2 数据导入导出概述 数据导入导出是Java开发中的重要组件。 24.2.1 数据导入导出的定义 定义:数据导入导出是指将数据从一个系统导入到另一个系统,或从一个系统导出到另一个系统的过程。 作用: * 实现数据的迁移。 * 实现数据的备份。 * 实现数据的共享。 常见的数据导入导出格式: * CSV:Comma-Separated Values,逗号分

By Ne0inhk
Django REST framework企业级API架构实战

Django REST framework企业级API架构实战

目录 摘要 1. 🎯 开篇:从踩坑到架构 2. 🏗️ 核心原理深度解析 2.1 DRF架构设计哲学 2.2 视图集:CRUD的终极抽象 2.3 序列化器:不只是数据转换 3. 🔧 实战:完整API实现 3.1 用户管理API 3.2 分页、过滤、排序 3.3 节流与限流 4. 🔥 高级实战:企业级API 4.1 缓存优化策略 4.2 性能监控中间件 4.3 API版本管理 5. 🚀 性能优化指南 5.1 数据库优化 5.

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 redux_thunk 解决鸿蒙应用状态管理中的复杂异步副作用(异步架构神器)

Flutter for OpenHarmony: Flutter 三方库 redux_thunk 解决鸿蒙应用状态管理中的复杂异步副作用(异步架构神器)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在 OpenHarmony 应用架构设计中,状态管理(State Management)是业务的核心。如果你选择了经典的 Redux 模式,你会发现它天生是“同步”的:Action 发出,Reducer 改变 State。但在真实项目中,我们需要处理网络请求、数据库读写、文件 IO 等延时操作。如何在纯净的 Redux 链条中插入这些破坏性的“副作用”? redux_thunk 提供了一个简单而精妙的方案。它通过扩展 Redux 的中间件机制,允许你 Dispatch(派发)一个 函数 而不仅仅是对象。这为鸿蒙应用处理复杂的业务流提供了极大灵活性。 一、异步 Action

By Ne0inhk