MarkdownOnline多人协作开发
一个功能完整、架构清晰的 Markdown 到 HTML 转换器,采用经典的编译器设计模式,通过词法分析、语法分析和渲染三个阶段,将 Markdown 文本高效转换为 HTML 代码。
📋 目录
🎯 项目简介
markdown2html-parser 是一个轻量级、高性能的 Markdown 解析器,采用 TypeScript 编写,支持将 Markdown 文本转换为标准的 HTML 代码。项目遵循编译器设计原则,通过词法分析(Lexer)、语法分析(Parser)和渲染(Render)三个阶段,实现了完整的 Markdown 解析流程。
设计理念
本项目采用经典的编译器前端设计模式:
Markdown 文本 → [词法分析] → Token 流 → [语法分析] → AST → [渲染] → HTML
这种设计使得代码结构清晰、易于维护和扩展,同时保证了解析的准确性和性能。
✨ 核心特性
1. 完整的 Markdown 语法支持
- ✅ 标题 :支持 H1-H6 六级标题(
#到######) - ✅ 文本样式 :加粗(
**bold**、__bold__)、斜体(*italic*、_italic_)、删除线(~~text~~) - ✅ 链接和图片:支持标准 Markdown 链接和图片语法
- ✅ 代码 :行内代码(
code)和代码块(` ```code````) - ✅ 列表 :有序列表(
1. item)和无序列表(*、+、-) - ✅ 引用 :块引用(
> quote) - ✅ 分隔线 :水平分隔线(
---) - ✅ 复杂组合:支持多种样式组合(如加粗链接、斜体加粗等)
2. 模块化架构
- 词法分析器(Lexer):将 Markdown 文本解析为 Token 流
- 语法分析器(Parser):将 Token 流转换为抽象语法树(AST)
- 渲染器(Render):将 AST 渲染为 HTML 代码
3. 类型安全
- 完整的 TypeScript 类型定义
- 严格的类型检查,确保代码质量
4. 多模块系统支持
- 支持 ESM(ES Module)导入
- 支持 CommonJS 导入
- 支持默认导入
🏗️ 架构设计
项目结构
markdown2html-parser/
├── src/ # 源代码目录
│ ├── md2html.ts # 主入口函数,协调整个解析流程
│ ├── lexer.ts # 词法分析器
│ ├── parser.ts # 语法分析器
│ ├── render.ts # HTML 渲染器
│ └── common/
│ └── html_regexp.ts # 正则表达式定义
├── dist/ # 编译输出目录
├── index.ts # 包入口文件,导出 API
├── package.json # 项目配置
└── tsconfig.json # TypeScript 配置
数据流
输入 Markdown 文本
↓
[Lexer] 词法分析
↓
Token[] 数组
↓
[Parser] 语法分析
↓
AST (抽象语法树)
↓
[Render] 渲染
↓
输出 HTML 字符串
🔍 核心代码解析
1. 主入口函数 (src/md2html.ts)
这是整个解析流程的协调者,将三个阶段串联起来:
5:9:src/md2html.ts
export function markdownToHtml(input: string): string {
const tokens = lexer(input); // 词法分析,生成 tokens
const ast = parser(tokens); // 语法分析,生成 AST
return render(ast); // 渲染 AST 为 HTML
}
设计亮点:
- 简洁的 API 设计,一行代码完成转换
- 清晰的职责分离,每个阶段独立处理
2. 词法分析器 (src/lexer.ts)
词法分析器负责将 Markdown 文本解析为 Token 流。核心逻辑包括:
Token 类型定义
24:58:src/lexer.ts
type Token = {
type:
| "text"
| "header"
| "bold"
| "italic"
| "strikethrough"
| "link"
| "image"
| "code"
| "code_block"
| "newline"
| "list_item"
| "horizontal_rule"
| "blockquote"
| "complex"
| "order_list"
| "unorder_list";
content?: string; // 适用于 header, bold, italic, strikethrough, code, code_block, list_item, blockquote
value?: string; // 适用于 text 类型
level?: number; // 适用于 header
href?: string; // 适用于 link
text?: string; // 适用于 link
src?: string; // 适用于 image
alt?: string; // 适用于 image
children?: String[]; // **允许所有 Token 存储子 Token**
start?: number; //有序列表开始的顺序,默认为1
listArr?: Array<{
value: string;
type: "order_list" | "unorder_list";
start: number;
isFirst: number;
}>;
isFirst?: number; //判断是否为第一项
};
核心解析函数
60:203:src/lexer.ts
function lexer(input: string): Token[] {
const tokens: Token[] = [];
console.log("原生代码");
console.log(input);
const lines = input.split("\n"); // 按行分割输入文本
lines.forEach((line, index) => {
const token = TypeCheck(line);
// 打印整个 token 数组的 JSON 字符串(已格式化)
// console.log("打印返回的数组");
console.log(JSON.stringify(token, null, 2));
// console.log("输出数组位置");
// console.log(index);
// 遍历 token 数组,获取每个 token 的键值对
const tokenType = token["type"] as string;
const tokenContent = (token["content"] as string) ?? "";
const tokenValue = (token["value"] as string) ?? "";
const tokenLevel = (token["level"] as number) ?? 0;
const tokenHref = (token["href"] as string) ?? "";
const tokenText = (token["text"] as string) ?? "";
const tokenSrc = (token["src"] as string) ?? "";
const tokenAlt = (token["alt"] as string) ?? "";
const tokenChildren = (token["children"] as string[]) ?? [];
const tokenStart = (token["start"] as number) ?? 1;
if (index !== 0) {
tokens.push({ type: "newline" });
}
// 处理不同类型的 Token
switch (tokenType) {
case "text":
console.log(token["value"]);
tokens.push({
type: "text",
value: tokenValue,
});
break;
case "header":
tokens.push({
type: "header",
content: tokenContent,
level: tokenLevel,
});
break;
case "bold":
tokens.push({
type: "bold",
content: tokenContent,
});
break;
case "italic":
tokens.push({
type: "italic",
content: tokenContent,
});
break;
case "strikethrough":
tokens.push({
type: "strikethrough",
content: tokenContent,
});
break;
case "link":
tokens.push({
type: "link",
href: tokenHref,
text: tokenText,
});
break;
case "image":
tokens.push({
type: "image",
src: tokenSrc,
alt: tokenAlt,
});
break;
case "code":
tokens.push({
type: "code",
content: tokenContent,
});
break;
case "code_block":
tokens.push({
type: "code_block",
content: tokenContent,
});
break;
case "horizontal_rule":
tokens.push({
type: "horizontal_rule",
});
break;
case "blockquote":
tokens.push({
type: "blockquote",
content: tokenContent,
});
break;
case "complex":
tokens.push({
type: "complex",
children: tokenChildren,
content: tokenContent,
});
break;
case "newline":
tokens.push({
type: "newline",
});
break;
case "order_list":
tokens.push({
type: "order_list",
value: tokenValue,
start: tokenStart,
});
break;
case "unorder_list":
tokens.push({
type: "unorder_list",
value: tokenValue,
});
break;
default:
console.warn(`未知的 Token 类型: ${tokenType}`);
break;
}
});
return tokens;
}
设计亮点:
- 按行处理,简化逻辑
- 支持复杂样式组合(通过
complex类型) - 完整的类型检查机制
类型检测函数
205:392:src/lexer.ts
function TypeCheck(input: string): Object {
const token: { [key: string]: any } = {};
const status: Array<string> = [];
// 内容保存
let content: string = "";
const headerReg = [h1Regex, h2Regex, h3Regex, h4Regex, h5Regex, h6Regex];
for (let i = 0; i < headerReg.length; i++) {
const header = input.match(headerReg[i]);
if (header) {
let temp = i + 1;
status.push("h" + temp);
input = replaceInput(headerReg[i], input);
}
}
// 处理加粗
const boldRegexes = [boldRegex, boldAltRegex];
const matchedBoldRegex = boldRegexes.find((regex) => input.match(regex));
const bold = matchedBoldRegex ? input.match(matchedBoldRegex) : null;
if (bold) {
status.push("bold");
input = replaceInput(matchedBoldRegex, input); // 确保替换用的是匹配到的正则
}
// 处理斜体
const italicRegexes = [italicRegex, italicAltRegex];
const matchedItalicRegex = italicRegexes.find((regex) => input.match(regex));
const italic = matchedItalicRegex ? input.match(matchedItalicRegex) : null;
if (italic) {
status.push("italic");
input = replaceInput(matchedItalicRegex, input);
}
// 匹配删除线
const strikethrough = input.match(strikethroughRegex);
if (strikethrough) {
status.push("strikethrough");
input = replaceInput(strikethroughRegex, input);
}
// 匹配图片
const image = input.match(imageRegex);
if (image) {
status.push("image");
input = imgAndLinkReplace(imageRegex, input);
}
// 匹配链接
const link = input.match(linkRegex);
if (link) {
status.push("link");
input = imgAndLinkReplace(linkRegex, input);
}
// 行内代码匹配
const code = input.match(inlineCodeRegex);
if (code) {
status.push("code");
input = replaceInput(inlineCodeRegex, input);
}
// 代码块匹配
const code_block = input.match(codeBlockRegex);
if (code_block) {
status.push("code_block");
input = replaceInput(codeBlockRegex, input);
}
// 换行符匹配
const lineBreak = input.match(lineBreakRegex);
if (lineBreak) {
console.log("匹配到换行符");
status.push("newline");
}
// 有序列表匹配
const orderLine = input.match(orderedListItemRegex);
let start: number = 1;
if (orderLine) {
status.push("order_list");
// console.log("匹配到了列表项");
let getStart = input.split(". "); //将数据分开
input = replaceInput(orderedListItemRegex, input);
start = +getStart[0];
}
const unorderedListRegexes = [
unorderedListItemRegex,
unorderedListItemAltRegex,
unorderedListItemAlt2Regex,
];
const matchedRegex = unorderedListRegexes.find((regex) => input.match(regex)); // 找到第一个成功匹配的正则
const unorder = matchedRegex ? input.match(matchedRegex) : null;
if (unorder) {
status.push("unorder_list");
input = replaceInput(matchedRegex, input); // 确保用匹配到的正则替换
console.log("匹配到无序列表");
console.log(input);
}
content += input;
// 封装token
if (status.length > 1) {
for (let s of status) {
if (s === "image") {
let ans = typeImgOrUrl(content);
token.type = "complex";
token.src = ans.url;
token.alt = ans.desc;
token.children = [...status];
} else if (s === "link") {
let ans = typeImgOrUrl(content);
token.type = "complex";
token.text = ans.desc;
token.href = ans.url;
token.children = [...status];
} else {
token.type = "complex";
token.content = content;
token.children = [...status];
}
}
} else if (status.length === 1) {
const style = status[0];
let headerLevel = typeHeader(style);
if (headerLevel) {
let str = style.split("");
token.type = "header";
token.level = +str[str.length - 1];
token.content = content;
} else {
// 不是标题则是其他简单样式
let ans = typeImgOrUrl(content);
switch (style) {
case "bold":
token.type = "bold";
token.content = content;
break;
case "italic":
token.type = "italic";
token.content = content;
break;
case "strikethrough":
token.type = "strikethrough";
token.content = content;
break;
case "image":
// console.log("style是图片");
token.type = "image";
token.src = ans.url;
token.alt = ans.desc;
break;
case "link":
// console.log("style是链接" + style);
token.type = "link";
token.text = ans.desc;
token.href = ans.url;
break;
case "order_list":
token.type = "order_list";
token.value = content;
token.start = start;
break;
case "unorder_list":
token.type = "unorder_list";
token.value = content;
break;
default:
break;
}
}
} else {
token.type = "text";
token.value = content;
console.log(token);
}
return token;
}
设计亮点:
- 使用状态数组记录匹配到的所有样式类型
- 支持多种样式的组合(如加粗链接、斜体加粗等)
- 通过正则表达式匹配,性能高效
3. 语法分析器 (src/parser.ts)
语法分析器将 Token 流转换为抽象语法树(AST)。
AST 节点类型定义
3:24:src/parser.ts
type ASTNode =
| { type: "document"; children: ASTNode[] }
| { type: "header"; level: number; content: string }
| { type: "bold"; content: string }
| { type: "italic"; content: string }
| { type: "strikethrough"; content: string }
| { type: "link"; href: string; text: string }
| { type: "image"; src: string; alt: string }
| { type: "code"; content: string }
| { type: "code_block"; content: string }
| { type: "blockquote"; children: ASTNode[] }
| { type: "horizontal_rule" }
| { type: "text"; value: string }
| { type: "complex"; content: string; children: String[] } // 修改 complex 类型
| { type: "newline" }
| {
type: "order_list";
content: String;
start: number;
nestedItems?: ASTNode[];
}
| { type: "unorder_list"; content: String };
核心解析函数
26:161:src/parser.ts
function parser(tokens: Token[]): ASTNode {
let current = 0;
function parse(): ASTNode {
const children: ASTNode[] = [];
while (current < tokens.length) {
const token = tokens[current];
switch (token.type) {
case "text":
// 处理 text 类型
children.push({ type: "text", value: token.value! });
current++;
break;
case "header":
// 处理 header 类型
children.push({
type: "header",
level: token.level!,
content: token.content!,
});
current++;
break;
case "bold":
// 处理 bold 类型
children.push({ type: "bold", content: token.content! });
current++;
break;
case "italic":
// 处理 italic 类型
children.push({ type: "italic", content: token.content! });
current++;
break;
case "strikethrough":
// 处理 strikethrough 类型
children.push({ type: "strikethrough", content: token.content! });
current++;
break;
case "link":
// 处理 link 类型
children.push({
type: "link",
href: token.href!,
text: token.text!,
});
current++;
break;
case "image":
// 处理 image 类型
children.push({
type: "image",
src: token.src!,
alt: token.alt!,
});
current++;
break;
case "code":
// 处理 inline code 类型
children.push({ type: "code", content: token.content! });
current++;
break;
case "code_block":
// 处理 code block 类型
children.push({ type: "code_block", content: token.content! });
current++;
break;
case "blockquote":
// 处理 blockquote 类型
children.push({
type: "blockquote",
children: [{ type: "text", value: token.content! }],
});
current++;
break;
case "newline":
// 处理 newline 类型
children.push({ type: "newline" });
current++;
break;
case "complex":
// 处理 complex 类型,多个样式组合
children.push({
type: "complex",
content: token.content!,
children: token.children!, // 保存组合的样式信息
});
current++;
break;
case "order_list":
children.push({
type: "order_list",
content: token.value,
start: token.start,
nestedItems: token.listArr
? token.listArr.map((item) => ({
type: "order_list",
content: item.value,
start: item.start,
}))
: [], // 如果 `listArr` 存在,则递归解析子项,否则为空数组
});
current++;
break;
case "unorder_list":
children.push({
type: "unorder_list",
content: token.value,
});
current++;
break;
default:
current++;
break;
}
}
// 返回包含所有内容的文档节点
return { type: "document", children };
}
return parse();
}
设计亮点:
- 使用递归下降解析器模式
- 统一的 AST 节点结构
- 支持嵌套结构(如列表项)
4. 渲染器 (src/render.ts)
渲染器将 AST 转换为 HTML 字符串。
核心渲染函数
6:60:src/render.ts
function render(ast: ASTNode): string {
switch (ast.type) {
case "document":
return ast.children.map((child) => render(child)).join("");
case "header":
return `<h${ast.level}>${ast.content}</h${ast.level}>`;
case "bold":
return `<strong>${ast.content}</strong>`;
case "italic":
return `<em>${ast.content}</em>`;
case "strikethrough":
return `<del>${ast.content}</del>`;
case "text":
return ast.value;
case "link":
return `<a href="${ast.href}" target="_blank" rel="nofollow">${ast.text}</a>`;
case "image":
return `<img src="${ast.src}" alt="${ast.alt}">`;
case "blockquote":
return `<blockquote>${ast.children
.map((child) => render(child))
.join("")}</blockquote>`;
case "horizontal_rule":
return `<hr>`;
case "code":
return `<code>${ast.content}</code>`;
case "code_block":
return `<pre><code>${ast.content}</code></pre>`;
case "newline":
// 确保处理换行符
return `<br>`;
case "order_list":
return `<ol start = ${ast.start ?? "1"}><li>${ast.content}</li></ol>`;
case "unorder_list":
return `<ul><li>${ast.content}</li></ul>`;
case "complex":
return renderComplex(ast.content, ast.children);
default:
return "";
}
}
复杂样式渲染
72:101:src/render.ts
function renderComplex(content: string, children: String[]): string {
if (children.length === 0) {
return content;
}
const style = children[0]; // 取出当前需要应用的样式
const remainingStyles = children.slice(1); // 剩下的样式继续递归
let ans = content.split(" ");
switch (style) {
case "bold":
return `<strong>${renderComplex(content, remainingStyles)}</strong>`;
case "italic":
return `<em>${renderComplex(content, remainingStyles)}</em>`;
case "strikethrough":
return `<del>${renderComplex(content, remainingStyles)}</del>`;
case "code":
return `<code>${renderComplex(content, remainingStyles)}</code>`;
case "link":
return `<a href="${
ans[1]
}" target="_blank" rel="nofollow">${renderComplex(
ans[0],
remainingStyles
)}</a>`;
case "image":
return `<img src="${ans[1]}" alt="${ans[0]}">`; // 图片不需要嵌套
default:
return renderComplex(content, remainingStyles); // 未知类型,继续递归
}
}
设计亮点:
- 递归渲染,支持嵌套结构
- 复杂样式通过递归方式处理,保证正确的 HTML 标签嵌套
- 链接自动添加
target="_blank"和rel="nofollow"属性
5. 正则表达式定义 (src/common/html_regexp.ts)
所有用于匹配 Markdown 语法的正则表达式都集中定义在这个文件中,便于维护和扩展。
1:84:src/common/html_regexp.ts
// 匹配一级标题(#)
// 例如: # 一级标题
export const h1Regex = /^#\s(.*)$/gm;
// 匹配二级标题(##)
// 例如: ## 二级标题
export const h2Regex = /^##\s(.*)$/gm;
// 匹配三级标题(###)
// 例如: ### 三级标题
export const h3Regex = /^###\s(.*)$/gm;
// 匹配四级标题(####)
// 例如: #### 四级标题
export const h4Regex = /^####\s(.*)$/gm;
// 匹配五级标题(#####)
// 例如: ##### 五级标题
export const h5Regex = /^#####\s(.*)$/gm;
// 匹配六级标题(######)
// 例如: ###### 六级标题
export const h6Regex = /^######\s(.*)$/gm;
// 匹配加粗文本(**bold** 或 __bold__)
// 例如: **加粗文本**
export const boldRegex = /\*\*(.*)\*\*/gm;
// 匹配加粗文本(__bold__)替代格式
// 例如: __加粗文本__
export const boldAltRegex = /__(.*)__/gm;
// 匹配斜体文本(*italic* 或 _italic_)
// 例如: *斜体文本* 或 _斜体文本_
export const italicRegex = /\*(.*)\*/gm;
// 匹配斜体文本(_italic_)替代格式
// 例如: _斜体文本_
export const italicAltRegex = /_(.*)_/gm;
// 匹配删除线文本(~~strikethrough~~)
// 例如: ~~删除线文本~~
export const strikethroughRegex = /~~(.*)~~/gm;
// 匹配链接([text](url))
// 例如: [点击这里](https://example.com)
export const linkRegex = /\[(.*?)\]\((.*?)\)/gm;
// 匹配图片()
// 例如: 
export const imageRegex = /!\[(.*?)\]\((.*?)\)/gm;
// 匹配行内代码(`code`)
// 例如: `console.log("Hello, world!");`
export const inlineCodeRegex = /`(.*?)`/gm;
// 匹配代码块(```code block```)
// 例如: ```
// function hello() {
// console.log("Hello");
// }
// ```
export const codeBlockRegex = /```([\s\S]*?)```/gm;
// 匹配换行符(用于处理换行)
// 例如: \n
export const lineBreakRegex = /\n/gm;
// 匹配无序列表项(星号 *)
// 例如: * 列表项
export const unorderedListItemRegex = /^\*\s(.*)$/gm;
// 匹配无序列表项(加号 +)
// 例如: + 列表项
export const unorderedListItemAltRegex = /^\+\s(.*)$/gm;
// 匹配无序列表项(减号 -)
// 例如: - 列表项
export const unorderedListItemAlt2Regex = /^-\s(.*)$/gm;
// 匹配有序列表项(数字加点)
// 例如: 1. 列表项
export const orderedListItemRegex = /^\d+\.\s(.*)$/gm;
设计亮点:
- 集中管理所有正则表达式,便于维护
- 每个正则都有详细的注释说明
- 支持多种 Markdown 语法变体(如
**bold**和__bold__)
📦 安装使用
安装
bash
npm install markdown2html-parser
基本使用
ESM 导入
javascript
import { convertMarkdownToHtml } from "markdown2html-parser";
const markdown = "# Hello World\n\nThis is **bold** text.";
const html = convertMarkdownToHtml(markdown);
console.log(html);
// 输出: <h1>Hello World</h1><br><br>This is <strong>bold</strong> text.
CommonJS 导入
javascript
const { convertMarkdownToHtml } = require("markdown2html-parser");
const markdown = "# Hello World\n\nThis is **bold** text.";
const html = convertMarkdownToHtml(markdown);
console.log(html);
默认导入
javascript
import convertMarkdownToHtml from "markdown2html-parser";
const html = convertMarkdownToHtml(markdown);
高级使用
如果需要单独使用词法分析器或语法分析器:
javascript
import { lexMarkdown, parseMarkdown, convertMarkdownToHtml } from "markdown2html-parser";
// 只进行词法分析
const tokens = lexMarkdown("# Hello World");
// 进行词法和语法分析
const tokens = lexMarkdown("# Hello World");
const ast = parseMarkdown(tokens);
// 完整转换
const html = convertMarkdownToHtml("# Hello World");
📚 API 文档
convertMarkdownToHtml(input: string): string
将 Markdown 文本转换为 HTML 字符串。
参数:
input(string): 要转换的 Markdown 文本
返回:
string: 转换后的 HTML 字符串
示例:
javascript
const html = convertMarkdownToHtml("# Title\n\n**Bold** text");
lexMarkdown(input: string): Token[]
将 Markdown 文本解析为 Token 数组(词法分析)。
参数:
input(string): 要解析的 Markdown 文本
返回:
Token[]: Token 数组
parseMarkdown(tokens: Token[]): ASTNode
将 Token 数组解析为 AST(语法分析)。
参数:
tokens(Token[]): Token 数组
返回:
ASTNode: 抽象语法树根节点
📝 支持语法
标题
markdown
# H1 标题
## H2 标题
### H3 标题
#### H4 标题
##### H5 标题
###### H6 标题
文本样式
markdown
**加粗文本** 或 __加粗文本__
*斜体文本* 或 _斜体文本_
~~删除线文本~~
链接和图片
markdown
[链接文本](https://example.com)

代码
markdown
行内代码:`const x = 1;`
代码块:
function hello() {
console.log("Hello");
}
列表
markdown
有序列表:
1. 第一项
2. 第二项
无序列表(三种方式):
* 项目一
+ 项目二
- 项目三
引用
markdown
> 这是一个引用块
分隔线
markdown
---
复杂组合
支持多种样式的组合,例如:
markdown
**[加粗链接](https://example.com)**
*斜体加粗文本*
🛠️ 技术栈
- TypeScript: 提供类型安全和更好的开发体验
- Node.js: 运行环境
- 正则表达式: 用于 Markdown 语法匹配
📄 许可证
本项目遵循 ISC 许可证。
👤 作者
Horizon
📅 版本历史
v0.0.5 (当前版本)
- 完整的 Markdown 语法支持
- 模块化架构设计
- ESM 和 CommonJS 双模块支持
v0.0.2
- 新增列表解析功能
- 优化导入方式,支持多种导入模式
注意:本项目仍在积极开发中,欢迎提交 Issue 和 Pull Request!