长洪 發表於 2025-12-23 21:38:00

不用 Typora 的 html 导出功能,手搓纯 HTML5 转换器

<h1 id="不用-typora-的-html-导出功能手搓纯-html5-转换器">不用 Typora 的 html 导出功能,手搓纯 HTML5 转换器</h1>
<p>原创 夏群林 2025.12.23</p>
<h2 id="一缘起">一、缘起</h2>
<p>我日常工作使用 Typora, 一款很好的 Markdown 编辑器。建网站,写博文,用 Typora 打底稿。然后导出成 html 格式文件,所见即所得,一个静态网站就成了!</p>
<p>不过,Typora 自带的 HTML 导出功能存在核心缺陷:夹带 Typora编辑器 UI 冗余代码、HTML 语义不合规、依赖非标准化样式体系、导出文件体积大、可维护性差。基于此,我们手搓纯HTML5转换器,核心目标:</p>
<ol>
<li>剔除 UI 冗余,仅保留 Markdown 内容渲染逻辑;</li>
<li>纯 ES6+ 原生实现,无第三方库依赖;</li>
<li>符合HTML5语义标准;</li>
<li>还原 Typora 纯 Markdown 内容的渲染风格。</li>
</ol>
<h2 id="二纯-es6-实现无第三方库">二、纯 ES6+ 实现,无第三方库</h2>
<h3 id="21-核心优势">2.1 核心优势</h3>
<ul>
<li>轻量化:产物仅包含核心业务逻辑,无冗余依赖代码,体积较 Typora 导出缩减 80% 以上;</li>
<li>可控性:全流程掌控 Markdown 语法解析、HTML 重构、样式生成;</li>
<li>无依赖风险:避免第三方库版本迭代、兼容性问题,转换器长期稳定可用。</li>
</ul>
<h3 id="22-es6-核心特性应用">2.2 ES6+ 核心特性应用</h3>
<table>
<thead>
<tr>
<th>ES6+特性</th>
<th>应用场景</th>
<th>价值</th>
</tr>
</thead>
<tbody>
<tr>
<td>模块化(ES Module)</td>
<td>拆分解析、工具、入口模块</td>
<td>解耦代码,便于维护</td>
</tr>
<tr>
<td>模板字符串</td>
<td>HTML/CSS生成</td>
<td>替代字符串拼接,提升可读性</td>
</tr>
<tr>
<td>正则表达式增强</td>
<td>Markdown解析、合规修复</td>
<td>精准匹配语法,解决反向引用冲突</td>
</tr>
</tbody>
</table>
<h2 id="三整体架构设计">三、整体架构设计</h2>
<h3 id="31-目录结构">3.1 目录结构</h3>
<pre><code>md2html/
├── index.html      // 交互界面(MD输入、HTML预览/导出)
├── js/
│   ├── app.js      // 入口(DOM操作、转换/下载逻辑)
│   ├── utils.js      // 工具函数(格式化、提示)
│   └── marked.es6.js // MD核心解析(合规修复核心)
└── css/
    ├── style.css   // 转换器界面样式
    ├── concise.css   // 简洁版MD渲染样式
    └── typora.css    // Typora纯内容渲染样式
</code></pre>
<h3 id="32-核心流程">3.2 核心流程</h3>
<div class="mermaid">flowchart TD
    A --&gt; B
    B --&gt; C
    C --&gt; D
    D --&gt; E[预览/导出纯HTML5文件]
</div><h2 id="四核心技术点">四、核心技术点</h2>
<h3 id="41-基于正则的-markdown-语法解析">4.1 基于正则的 Markdown 语法解析</h3>
<p>放弃第三方库,通过精准的正则表达式匹配Markdown核心语法,是实现“纯原生”的基础。</p>
<p>核心思路为:按“块级语法(标题、列表、表格)→ 行内语法(加粗、链接、代码)”的顺序解析,确保语法嵌套的正确性。</p>
<p>示例:表格语法解析正则</p>
<pre><code class="language-javascript">// 解析表格
const parseTables = (md, options) =&gt; {
    if (!md.includes('|') || md.includes('&lt;table&gt;')) return md;

    const tableRegex = /^(\|.*\|)\n(\|[-:| ]*\|)\n((?:\|.*\|\n?)+)/gm;
    return md.replace(tableRegex, (match, headerLine, separatorLine, bodyLines) =&gt; {
      const alignments = separatorLine.split('|')
            .filter(cell =&gt; cell.trim() !== '')
            .map(cell =&gt; {
                const trimCell = cell.trim();
                return trimCell.startsWith(':') &amp;&amp; trimCell.endsWith(':') ? 'center' :
                     trimCell.startsWith(':') ? 'left' :
                     trimCell.endsWith(':') ? 'right' : 'left';
            });

      const headerCells = headerLine.split('|').filter(cell =&gt; cell.trim() !== '');
      let headerHtml = '&lt;thead&gt;&lt;tr&gt;';
      headerCells.forEach((cell, index) =&gt; {
            const align = alignments || 'left';
            const inlineContent = parseInlineOnly(cell, options);
            headerHtml += `&lt;th class="text-${align}"&gt;${inlineContent}&lt;/th&gt;`;
      });
      headerHtml += '&lt;/tr&gt;&lt;/thead&gt;';

      const bodyRows = bodyLines.split('\n').filter(row =&gt; row.trim() !== '');
      let bodyHtml = '&lt;tbody&gt;';
      bodyRows.forEach(row =&gt; {
            const cells = row.split('|').filter(cell =&gt; cell.trim() !== '');
            bodyHtml += '&lt;tr&gt;';
            cells.forEach((cell, index) =&gt; {
                const align = alignments || 'left';
                const inlineContent = parseInlineOnly(cell, options);
                bodyHtml += `&lt;td class="text-${align}"&gt;${inlineContent}&lt;/td&gt;`;
            });
            bodyHtml += '&lt;/tr&gt;';
      });
      bodyHtml += '&lt;/tbody&gt;';

      return `&lt;table&gt;${headerHtml}${bodyHtml}&lt;/table&gt;`;
    });
};
</code></pre>
<h3 id="42-html语义合规化重构">4.2 HTML语义合规化重构</h3>
<p>例如,Typora 导出的<code>&lt;ul&gt;/&lt;ol&gt;</code>被<code>&lt;p&gt;</code>包裹,换行生成空<code>&lt;p&gt;</code>标签,违反HTML语义规范。</p>
<p>核心逻辑:解析列表时仅生成<code>&lt;li&gt;</code>子元素,段落解析排除列表标签,避免误判。</p>
<pre><code class="language-javascript">// marked.es6.js 核心修复代码
const parseLists = (md) =&gt; {
    // 生成纯&lt;li&gt;,移除多余换行
    let listItems = md.replace(/^( {0,3})(-|\*|\+)\s+(.*?$)/gm, (_, indent, marker, text) =&gt; {
      const inlineContent = parseInlineOnly(text).replace(/\n+/g, ' ');
      return `${indent}&lt;li&gt;${inlineContent}&lt;/li&gt;`;
    });
    // 包裹列表容器,清理换行干扰
    return listItems.replace(/((?: {0,3}&lt;li&gt;[\s\S]*?&lt;\/li&gt;\s*)+)/gm, (_, items) =&gt; {
      const cleanItems = items.replace(/\n+/g, '').replace(/&gt;\s+&lt;/g, '&gt;&lt;');
      return /[-*+]/.test(_) ? `&lt;ul&gt;${cleanItems}&lt;/ul&gt;` : `&lt;ol&gt;${cleanItems}&lt;/ol&gt;`;
    });
};

// 段落解析排除列表标签,避免空&lt;p&gt;
const parseParagraphs = (md) =&gt; {
    const paraRegex = /^(?!&lt;(ul|ol|li)&gt;)(?!&lt;\/(ul|ol|li)&gt;)([^&lt;\n]+?)(?=\n{2,}|$)/gm;
    return md.replace(paraRegex, (_, __, ___, content) =&gt; {
      const trimmed = content?.trim().replace(/\n+/g, ' ') || '';
      return trimmed ? `&lt;p&gt;${trimmed}&lt;/p&gt;` : '';
    });
};
</code></pre>
<h3 id="43-仅保留-typora--内容渲染核心样式">4.3 仅保留 Typora内容渲染核心样式</h3>
<p>Typora 导出样式包含编辑器 UI 规则,冗余且非标准化,予以剥离。基于Typora原生的Markdown渲染风格,构建模块化的CSS体系:</p>
<pre><code class="language-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&gt;li, ol&gt;li { margin: 0.4em 0; }
ul&gt;br, ol&gt;br, ul&gt;p, ol&gt;p { display: none; }
/* 表格对齐类 */
.text-left { text-align: left !important; }
.text-center { text-align: center !important; }
.text-right { text-align: right !important; }
</code></pre>
<h3 id="44-原生-es6-模块化整合">4.4 原生 ES6+ 模块化整合</h3>
<pre><code class="language-javascript">// app.js 核心逻辑
import { parseMarkdown } from './marked.es6.js';
import { formatHtml, showAlert } from './utils.js';

// 转换逻辑
dom.convertBtn.addEventListener('click', () =&gt; {
    const mdContent = dom.mdInput.value.trim();
    if (!mdContent) return showAlert('请输入Markdown内容');
   
    // 核心流程:解析(合规)→ 格式化 → 预览/导出
    const htmlFragment = parseMarkdown(mdContent); // 生成即合规
    const finalHtml = formatHtml(`&lt;!DOCTYPE html&gt;
&lt;html lang="zh-CN"&gt;
&lt;head&gt;&lt;meta charset="UTF-8"&gt;&lt;link rel="stylesheet" href="./css/typora.css"&gt;&lt;/head&gt;
&lt;body&gt;${htmlFragment}&lt;/body&gt;&lt;/html&gt;`);
   
    dom.preview.innerHTML = htmlFragment;
    dom.htmlCode.value = finalHtml;
    dom.downloadBtn.disabled = false;
});
</code></pre>
<h2 id="五使用说明">五、使用说明</h2>
<ol>
<li>按指定目录结构存放文件,确保<code>js/</code>和<code>css/</code>子文件夹路径正确;</li>
<li>用支持ES6模块的浏览器(Chrome/Firefox/Edge)打开<code>index.html</code>;</li>
<li>输入Markdown内容,勾选“保留 Typora 原格式”,点击“转换为 HTML5”;</li>
<li>预览区查看效果,点击“下载 HTML 文件”获取符合 HTML5 规范的文件。</li>
</ol>
<h2 id="六总结">六、总结</h2>
<ol>
<li>核心价值:精准解决 Typora 导出的编辑器UI样式冗余痛点,生成纯内容的符合 HTML5 规范的文件;</li>
<li>技术亮点:纯 ES6+ 原生实现;</li>
<li>优势:轻量化、可控性强、跨环境兼容,可直接部署使用。</li>
</ol>
<p>本方案源代码开源,按照 MIT 协议许可。地址: xiaql/md2html5: A typora to pure html5 converter</p><br><br>
来源:https://www.cnblogs.com/zhally/p/19389248
頁: [1]
查看完整版本: 不用 Typora 的 html 导出功能,手搓纯 HTML5 转换器