承上一篇:nodejs: 能在线编辑 Markdown 文档的 Web 服务程序
如果需要更多 Markdown 扩展(如表格、数学公式)等功能,怎样编写?
已经采用了移除服务端 mermaid 依赖的方案,现在想要为这个 Markdown 编辑器扩展表格、数学公式等功能,继续完善代码,添加这些常用的 Markdown 扩展能力,同时保持代码的简洁和可维护性。
实现思路
- 表格支持 :
marked本身已内置 GitHub 风格的表格解析,只需确保启用相关配置 - 数学公式支持 :集成
katex或mathjax来渲染 LaTeX 格式的数学公式 - 代码高亮 :添加
highlight.js增强代码块的语法高亮效果 - 其他扩展:支持任务列表、脚注等常用 Markdown 扩展
完整修改后的代码
1. 安装新增依赖
安装代码高亮和数学公式渲染相关依赖
npm install marked-highlight highlight.js
2. 编写 app2.js(服务端增强 Markdown 解析)
javascript
// 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;
}
};
// 创建带高亮的 renderer
const renderer = new marked.Renderer();
// 重写代码块渲染逻辑,识别 mermaid 代码块
renderer.code = (code, language) => {
if (language === 'mermaid') {
// 为 Mermaid 代码块生成容器(仅保留代码,渲染交给前端)
return `<div class="mermaid">${code}</div>`;
}
// 其他代码块使用高亮渲染
return `<pre><code class="hljs language-${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 风格 Markdown
breaks: 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'));
// 确保保存文件的目录存在
const DOCS_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 为 HTML
const 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;
const PORT = 8000;
app.listen(PORT, () => {
console.log(`服务器运行在: http://localhost:${PORT}`);
console.log(`文档保存目录: ${DOCS_DIR}`);
});
3. 编辑 views/editor.ejs(前端添加公式渲染和样式)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<!-- 1. 引入 Mermaid -->
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<!-- 2. 引入 highlight.js 核心脚本(关键修复) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- 3. 引入代码高亮样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js/styles/github-dark.min.css">
<!-- 4. 引入 KaTeX 用于数学公式渲染 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/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>
<div class="header">
<h1>Markdown 在线编辑器 (支持表格/公式/Mermaid)</h1>
</div>
<div class="controls">
<input type="text" id="filename" placeholder="输入文件名(无需.md)" value="demo">
<button id="saveBtn">保存文档</button>
<button id="loadBtn">加载文档</button>
<button id="refreshBtn">刷新预览</button>
</div>
<div class="container">
<div class="editor-container">
<textarea id="editor" placeholder="请输入 Markdown 内容...">
</textarea>
</div>
<div class="preview-container">
<div id="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 并更新预览
async function updatePreview() {
try {
const markdown = editor.value;
const response = await fetch('/api/parse', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ markdown })
});
const result = await response.json();
if (result.error) {
preview.innerHTML = `<div style="color: red;">解析错误: ${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 style="color: red;">预览更新失败: ${error.message}</div>`;
}
}
// 保存文档
async function saveDocument() {
const filename = filenameInput.value.trim();
const content = editor.value;
if (!filename) {
alert('请输入文件名');
return;
}
try {
const response = await fetch('/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);
}
}
// 加载文档
async function loadDocument() {
const filename = filenameInput.value.trim();
if (!filename) {
alert('请输入要加载的文件名');
return;
}
try {
const response = await fetch(`/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));
// 防抖函数(实时预览时避免频繁请求)
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
</script>
</body>
</html>
4. 编写 docs/demo.md
markdown
# 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
# Python 代码示例
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
print(list(fibonacci(10)))
5. Mermaid 流程图示例
编辑
预览
开始
选择功能
Markdown 编辑
实时预览
支持表格/公式/代码高亮
保存文档
结束
## 6. 脚注示例
这是一个带脚注的文本[^1],还有另一个脚注[^2]。
[^1]: 第一个脚注的内容
[^2]: 第二个脚注的内容,支持多行
这是第二行内容
这是第三行(缩进显示)
运行与使用说明
-
创建目录结构:
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/
-
重启服务器 :修改代码后,重启 Node.js 服务使配置生效
node app2.js
-
功能测试:
- 表格:直接使用 GitHub 风格的表格语法即可渲染
- 数学公式:使用
$公式$(行内)或$$公式$$(块级)包裹 LaTeX 公式 - 任务列表:使用
- [x](完成)或- [ ](未完成)格式 - 代码高亮:自动识别代码语言并高亮,支持上百种编程语言
- 脚注:使用
[^标记]定义脚注,文末添加[^标记]: 内容
扩展更多功能的建议
- 图片上传 :可以集成
multer实现本地图片上传,或对接第三方图库 - 目录生成:解析 Markdown 标题,自动生成导航目录
- 导出功能 :添加导出为 HTML/PDF 的功能(可使用
puppeteer) - 自定义样式:允许用户切换不同的预览主题(如浅色/深色模式)
- 自动保存:添加 localStorage 或定时保存功能,防止内容丢失
总结
- 核心扩展 :通过启用
marked的内置配置支持表格、任务列表、脚注;集成katex实现数学公式渲染;使用highlight.js增强代码高亮。 - 实现方式:服务端负责 Markdown 解析和扩展支持,前端负责公式、Mermaid 的渲染和样式美化。
- 使用要点:数学公式使用 LaTeX 语法,表格使用 GitHub 风格语法,所有扩展功能无需额外配置即可直接使用。
现在你的编辑器已经具备了完整的 Markdown 扩展能力,涵盖表格、数学公式、代码高亮、任务列表、脚注等常用功能,完全满足日常编辑需求。