不用 Typora 的 html 导出功能,手搓纯 HTML5 转换器
原创 夏群林 2025.12.23
一、缘起
我日常工作使用 Typora, 一款很好的 Markdown 编辑器。建网站,写博文,用 Typora 打底稿。然后导出成 html 格式文件,所见即所得,一个静态网站就成了!
不过,Typora 自带的 HTML 导出功能存在核心缺陷:夹带 Typora 编辑器 UI 冗余代码、HTML 语义不合规、依赖非标准化样式体系、导出文件体积大、可维护性差。基于此,我们手搓纯HTML5转换器,核心目标:
- 剔除 UI 冗余,仅保留 Markdown 内容渲染逻辑;
- 纯 ES6+ 原生实现,无第三方库依赖;
- 符合HTML5语义标准;
- 还原 Typora 纯 Markdown 内容的渲染风格。
二、纯 ES6+ 实现,无第三方库
2.1 核心优势
- 轻量化:产物仅包含核心业务逻辑,无冗余依赖代码,体积较 Typora 导出缩减 80% 以上;
- 可控性:全流程掌控 Markdown 语法解析、HTML 重构、样式生成;
- 无依赖风险:避免第三方库版本迭代、兼容性问题,转换器长期稳定可用。
2.2 ES6+ 核心特性应用
| ES6+特性 | 应用场景 | 价值 |
|---|---|---|
| 模块化(ES Module) | 拆分解析、工具、入口模块 | 解耦代码,便于维护 |
| 模板字符串 | HTML/CSS生成 | 替代字符串拼接,提升可读性 |
| 正则表达式增强 | Markdown解析、合规修复 | 精准匹配语法,解决反向引用冲突 |
三、整体架构设计
3.1 目录结构
md2html/
├── index.html // 交互界面(MD输入、HTML预览/导出)
├── js/
│ ├── app.js // 入口(DOM操作、转换/下载逻辑)
│ ├── utils.js // 工具函数(格式化、提示)
│ └── marked.es6.js // MD核心解析(合规修复核心)
└── css/
├── style.css // 转换器界面样式
├── concise.css // 简洁版MD渲染样式
└── typora.css // Typora纯内容渲染样式
3.2 核心流程
flowchart TD A[MD输入] --> B[marked.es6.js解析(合规修复)] B --> C[utils.js格式化] C --> D[app.js生成HTML/下载] D --> E[预览/导出纯HTML5文件]
四、核心技术点
4.1 基于正则的 Markdown 语法解析
放弃第三方库,通过精准的正则表达式匹配Markdown核心语法,是实现"纯原生"的基础。
核心思路为:按"块级语法(标题、列表、表格)→ 行内语法(加粗、链接、代码)"的顺序解析,确保语法嵌套的正确性。
示例:表格语法解析正则
javascript
// 解析表格
const parseTables = (md, options) => {
if (!md.includes('|') || md.includes('<table>')) return md;
const tableRegex = /^(\|.*\|)\n(\|[-:| ]*\|)\n((?:\|.*\|\n?)+)/gm;
return md.replace(tableRegex, (match, headerLine, separatorLine, bodyLines) => {
const alignments = separatorLine.split('|')
.filter(cell => cell.trim() !== '')
.map(cell => {
const trimCell = cell.trim();
return trimCell.startsWith(':') && trimCell.endsWith(':') ? 'center' :
trimCell.startsWith(':') ? 'left' :
trimCell.endsWith(':') ? 'right' : 'left';
});
const headerCells = headerLine.split('|').filter(cell => cell.trim() !== '');
let headerHtml = '<thead><tr>';
headerCells.forEach((cell, index) => {
const align = alignments[index] || 'left';
const inlineContent = parseInlineOnly(cell, options);
headerHtml += `<th class="text-${align}">${inlineContent}</th>`;
});
headerHtml += '</tr></thead>';
const bodyRows = bodyLines.split('\n').filter(row => row.trim() !== '');
let bodyHtml = '<tbody>';
bodyRows.forEach(row => {
const cells = row.split('|').filter(cell => cell.trim() !== '');
bodyHtml += '<tr>';
cells.forEach((cell, index) => {
const align = alignments[index] || 'left';
const inlineContent = parseInlineOnly(cell, options);
bodyHtml += `<td class="text-${align}">${inlineContent}</td>`;
});
bodyHtml += '</tr>';
});
bodyHtml += '</tbody>';
return `<table>${headerHtml}${bodyHtml}</table>`;
});
};
4.2 HTML语义合规化重构
例如,Typora 导出的<ul>/<ol>被<p>包裹,换行生成空<p>标签,违反HTML语义规范。
核心逻辑:解析列表时仅生成<li>子元素,段落解析排除列表标签,避免误判。
javascript
// marked.es6.js 核心修复代码
const parseLists = (md) => {
// 生成纯<li>,移除多余换行
let listItems = md.replace(/^( {0,3})(-|\*|\+)\s+(.*?$)/gm, (_, indent, marker, text) => {
const inlineContent = parseInlineOnly(text).replace(/\n+/g, ' ');
return `${indent}<li>${inlineContent}</li>`;
});
// 包裹列表容器,清理换行干扰
return listItems.replace(/((?: {0,3}<li>[\s\S]*?<\/li>\s*)+)/gm, (_, items) => {
const cleanItems = items.replace(/\n+/g, '').replace(/>\s+</g, '><');
return /[-*+]/.test(_) ? `<ul>${cleanItems}</ul>` : `<ol>${cleanItems}</ol>`;
});
};
// 段落解析排除列表标签,避免空<p>
const parseParagraphs = (md) => {
const paraRegex = /^(?!<(ul|ol|li)>)(?!<\/(ul|ol|li)>)([^<\n]+?)(?=\n{2,}|$)/gm;
return md.replace(paraRegex, (_, __, ___, content) => {
const trimmed = content?.trim().replace(/\n+/g, ' ') || '';
return trimmed ? `<p>${trimmed}</p>` : '';
});
};
4.3 仅保留 Typora 内容渲染核心样式
Typora 导出样式包含编辑器 UI 规则,冗余且非标准化,予以剥离。基于Typora原生的Markdown渲染风格,构建模块化的CSS体系:
css
/* typora.css 核心代码 */
:root {
--typora-text-color: #333;
--typora-heading-color: #2c3e50;
--typora-code-bg: #f8f8f8;
--typora-table-border: #ddd;
}
/* 暗黑模式适配 */
@media (prefers-color-scheme: dark) {
:root {
--typora-text-color: #e0e0e0;
--typora-code-bg: #2d2d2d;
--typora-table-border: #444;
}
}
/* 列表合规兜底 */
ul>li, ol>li { margin: 0.4em 0; }
ul>br, ol>br, ul>p, ol>p { display: none; }
/* 表格对齐类 */
.text-left { text-align: left !important; }
.text-center { text-align: center !important; }
.text-right { text-align: right !important; }
4.4 原生 ES6+ 模块化整合
javascript
// app.js 核心逻辑
import { parseMarkdown } from './marked.es6.js';
import { formatHtml, showAlert } from './utils.js';
// 转换逻辑
dom.convertBtn.addEventListener('click', () => {
const mdContent = dom.mdInput.value.trim();
if (!mdContent) return showAlert('请输入Markdown内容');
// 核心流程:解析(合规)→ 格式化 → 预览/导出
const htmlFragment = parseMarkdown(mdContent); // 生成即合规
const finalHtml = formatHtml(`<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><link rel="stylesheet" href="./css/typora.css"></head>
<body>${htmlFragment}</body></html>`);
dom.preview.innerHTML = htmlFragment;
dom.htmlCode.value = finalHtml;
dom.downloadBtn.disabled = false;
});
五、使用说明
- 按指定目录结构存放文件,确保
js/和css/子文件夹路径正确; - 用支持ES6模块的浏览器(Chrome/Firefox/Edge)打开
index.html; - 输入Markdown内容,勾选"保留 Typora 原格式",点击"转换为 HTML5";
- 预览区查看效果,点击"下载 HTML 文件"获取符合 HTML5 规范的文件。
六、总结
- 核心价值:精准解决 Typora 导出的编辑器UI样式冗余痛点,生成纯内容的符合 HTML5 规范的文件;
- 技术亮点:纯 ES6+ 原生实现;
- 优势:轻量化、可控性强、跨环境兼容,可直接部署使用。
本方案源代码开源,按照 MIT 协议许可。地址: xiaql/md2html5: A typora to pure html5 converter