如果需要更多 Markdown 扩展(如表格、数学公式)等功能,怎样编写?
已经采用了移除服务端 mermaid 依赖的方案,现在想要为这个 Markdown 编辑器扩展表格、数学公式等功能,继续完善代码,添加这些常用的 Markdown 扩展能力,同时保持代码的简洁和可维护性。
实现思路
- : 本身已内置 GitHub 风格的表格解析,只需确保启用相关配置
介绍如何扩展基于 Node.js 的在线 Markdown 编辑器。通过集成 marked-highlight 和 highlight.js 实现代码语法高亮,引入 katex 渲染 LaTeX 数学公式,并启用 marked 内置配置支持表格、任务列表及脚注。服务端使用 Express 提供解析与文件存储 API,前端利用 EJS 模板结合 KaTeX 和 Mermaid 完成实时渲染。最终构建了一个功能完善的 Markdown 在线编辑工具,满足日常文档编写需求。
已经采用了移除服务端 mermaid 依赖的方案,现在想要为这个 Markdown 编辑器扩展表格、数学公式等功能,继续完善代码,添加这些常用的 Markdown 扩展能力,同时保持代码的简洁和可维护性。
markedkatex 或 mathjax 来渲染 LaTeX 格式的数学公式highlight.js 增强代码块的语法高亮效果npm install marked-highlight highlight.js
// 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="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;
app.listen(PORT, () => {
console.log(`服务器运行在:http://localhost:${PORT}`);
console.log(`文档保存目录:${DOCS_DIR}`);
});
<!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/[email protected]/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js"></script>
<script src="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%; : ; : auto; : ; : ; : ; }
{ : ; : ; : flex; : ; }
{ : ; : ; : white; : none; : ; : pointer; }
{ : ; }
{ : ; }
{ : collapse; : ; : ; }
, { : solid ; : ; : left; }
{ : ; : bold; }
(even) { : ; }
{ : none; : ; }
{ : super; : ; }
{ : ; : solid ; : ; : ; }
pre { : ; : ; : ; : auto; }
{ : , , monospace; }
{ : ; }
</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 = response.();
(result.) {
preview. = ;
;
}
preview. = result.;
mermaid.({ : , : preview });
(preview, {
: [
{ : , : , : },
{ : , : , : },
{ : , : , : },
{ : , : , : }
],
: ,
:
});
.().( {
hljs.(block);
});
} (error) {
preview. = ;
}
}
() {
filename = filenameInput..();
content = editor.;
(!filename) {
();
;
}
{
response = (, {
: ,
: { : },
: .({ filename, content })
});
result = response.();
(result.) {
( + result.);
} {
();
}
} (error) {
( + error.);
}
}
() {
filename = filenameInput..();
(!filename) {
();
;
}
{
response = ();
result = response.();
(result.) {
( + result.);
} {
editor. = result.;
();
();
}
} (error) {
( + error.);
}
}
refreshBtn.(, updatePreview);
saveBtn.(, saveDocument);
loadBtn.(, loadDocument);
.(, updatePreview);
() {
timeout;
() {
(timeout);
timeout = ( func.(, args), wait);
};
}
</script>
</body>
</html>
# 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 代码示例
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
print(list(fibonacci(10)))
流程说明:开始 -> 选择功能 -> Markdown 编辑 -> 实时预览 -> 支持表格/公式/代码高亮 -> 保存文档 -> 结束
#### 运行与使用说明
1. **创建目录结构**:
md-edit-app/ ├── app.js 和 app2.js // 服务器主程序 ├── views/ // 模板目录 │ └── edit.ejs 和 editor.ejs // 编辑器页面 ├── public/ // 静态资源目录 │ └── css 和 js // 静态子目录 └── docs/ // 文档保存目录 (自动创建)
所有的 *.min.js 可用 curl 下载后放在 public/js/,所有的 *.css 放在 public/css/。
2. **重启服务器**:修改代码后,重启 Node.js 服务使配置生效
```bash
node app2.js
$公式$(行内)或 $$公式$$(块级)包裹 LaTeX 公式- [x](完成)或 - [ ](未完成)格式[^标记] 定义脚注,文末添加 [^标记]: 内容multer 实现本地图片上传,或对接第三方图库puppeteer)marked 的内置配置支持表格、任务列表、脚注;集成 katex 实现数学公式渲染;使用 highlight.js 增强代码高亮。现在你的编辑器已经具备了完整的 Markdown 扩展能力,涵盖表格、数学公式、代码高亮、任务列表、脚注等常用功能,完全满足日常编辑需求。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online