Markdown 转 HTML

全球通用工具

粘贴一份文档,得到它的 HTML;或者粘贴一个页面,把 Markdown 拿回来。两个方向都在这个标签页里运行:转换按固定规则处理文本,所以没有任何上传,页面背后也没有能接收草稿的接口,即便有人尝试发送。真正值得读的是答案旁边打印出来的东西——文档大纲、每个链接及其协议、Markdown 承载不了的元素、这个页面没有实现的语法,以及往返结果,也就是把文档写回去再与你粘贴的内容比较。

页面支持双向转换,只显示当前方向用得到的字段:反向转换时还要问标题,就是一个没人会填完的表单。

粘贴文档、README、发布说明或草稿。示例里包含了这个页面有看法的每一样东西,所以加载时下面的表格不会是空的。

GFM 是人们实际书写的方言,因为 GitHub、GitLab 和大多数编辑器都加了同样四样东西。选 CommonMark 会把这四项全部关掉,`| a | b |` 和 `~~x~~` 就保持它们本来的文本状态。

默认转义,因为粘贴进来的文档是不可信输入:`<div>` 会变成 `&lt;div&gt;`,读起来就是这五个字符。原样放行是 CommonMark 规定应当做的,也是处理你自己标记时该选的设置。

凡是链接可以被点击的地方,`javascript:`、`vbscript:`、`data:` 和 `file:` 默认都被拒绝。图片仍然可以携带 `data:image/...`,因为图片源不是导航目标。

id 让 `#what-changed` 这样的链接能指向某个标题。文字相同的第二个标题会追加 `-1`,因为两个元素共用一个 id 是无效 HTML,第二个链接会落到第一个上。

CommonMark 把单个换行读成空格,真正的换行需要两个空格或一个反斜杠;GFM 把单个换行读成换行。两种读法只差一个开关,示例用的是 CommonMark 的读法。

片段是内容字段需要的。整份文档可以直接在浏览器里打开:一个 doctype、取自第一个标题的 title,以及七行样式表,而不是一套设计。

整份文档的 `<title>`。留空时由第一个标题命名——一份以 h1 开头的文档,本来就该这样。

留空则使用“document.html”“document-report.txt”和“document-audit.csv”。扩展名跟随方向:`.html` 或 `.md`。

边输入边更新。所有计算都在你的浏览器里完成,数据不会离开本机。

Markdown 对应的 HTML

2.1 KB

43 行Markdown 产出 31 行HTML,包含 4 个标题、3 个链接和 1 张图片;文字通过了往返检查。

输入
195 词 43 行,1.4 KB
输出
31 行 2.1 KB 的 HTML
标题
4 最深的是 h4
链接
3 全部保留
发现
7 没有需要修复的
读回
文字相同 由本页读回的 Markdown
往返
19 行 写回后与原文比较

可以复制的 HTML

<h1 id="markdown-to-html-and-back">Markdown to HTML, and back</h1>
<p>Markdown is a small language, and this page is honest about which part of it it speaks. The text you type is converted in the tab you typed it in - <strong>nothing</strong> is uploaded - and the same function produced the HTML below.</p>
<h2 id="what-this-page-does">What this page does</h2>
<ul>
  <li>Reads a pipe table with alignment, and a task list</li>
  <li><input type="checkbox" checked disabled> Converts fenced code, with the language after the fence</li>
  <li><input type="checkbox" disabled> Implements footnotes, which it does not</li>
</ul>
<table>
  <thead>
    <tr><th style="text-align: left">Setting</th><th style="text-align: right">Default</th><th style="text-align: center">What it changes</th></tr>
  </thead>
  <tbody>
    <tr><td style="text-align: left">tables</td><td style="text-align: right">on</td><td style="text-align: center">GFM only</td></tr>
    <tr><td style="text-align: left">line breaks</td><td style="text-align: right">off</td><td style="text-align: center"></td></tr>
  </tbody>
</table>
<h4 id="a-sub-subsection-which-skips-a-level">A sub-subsection, which skips a level</h4>
<blockquote>
<p>A quotation, with <strong>emphasis</strong> inside it, and a second paragraph that shows how a quote is
written back out.</p>
<p>Like that.</p>
</blockquote>
<pre><code class="language-js">const heading = document.querySelector('h1');
heading.textContent = 'Markdown';</code></pre>
<pre><code>A fence with no language, which no highlighter can color.</code></pre>
<hr>
<h2 id="setext-headings-work-as-well">Setext headings work as well</h2>
<p>A footnote marker[^1], a phrase someone wanted ==highlighted==, and an ordinary link: <a href="https://example.com/reset">reset your password</a>.</p>
<p>Raw HTML is escaped by default, so &lt;span class=&quot;badge&quot;&gt;Beta&lt;/span&gt; reads as the characters it is.</p>
<p>The guide is <a href="https://example.com/guide" title="The guide">over here</a>, the bare address <a href="https://example.com/bare">https://example.com/bare</a> is recognized, and an image with no alt text looks like this: <img src="/images/diagram.png" alt=""></p>

下面的第一个下载就是这段文本,逐字节相同。把它复制进文件即可打开,或者直接粘进接受 HTML 的页面。

大纲(4 个标题)

层级标题行
h1Markdown to HTML, and back1
h2What this page does5
h4A sub-subsection, which skips a level16
h2Setext headings work as well34
层级只表示“比上一级深一层”,所以跳级值得注意:它读起来像缺了一节,而不是字号变小。这里最深的标题是 h4,最长的标题有 37 个字符。HTML 里的每个标题都会得到一个 `id`,由标题文字生成;文字相同的第二个标题会追加 `-1`——两个元素共用一个 id 是无效 HTML,第二个链接会落到第一个上。

引用或列表项内部的标题属于那个条目,而不是这份大纲:把引用当成章节列进目录,会把文档说错。

链接(3)

文字目标协议行处理结果
reset your passwordhttps://example.com/resethttps37保留
over herehttps://example.com/guidehttps41保留,来自引用定义
https://example.com/barehttps://example.com/barehttps41保留
这份文档里没有任何链接使用可执行代码的协议,所以拒绝开关没有可做的事。
`relative` 表示地址完全没有协议——`/about`、`page.html`、`#section`——页面上大多数链接都是这样写的。
凡是链接可以被点击的地方,`data:` 都会被拒绝;在图片上只允许 `data:image/...`:图片源不是导航目标,而在本该放图片的位置塞进一整份 HTML 文档,正是页面执行别人脚本的方式。

写成引用定义的链接会单独标出:定义行永远不会变成内容,而文档里任何地方都可以指向它。

图片(1)

替代文本来源行处理结果
(无)/images/diagram.png41保留
没有替代文本的图片会渲染成 `alt=""`,屏幕阅读器读到的是一片空白——索引页面图片的搜索引擎也一样。
页面本身走 `https://` 时,以 `http://` 加载的图片会被当作混合内容拦截,根本不会显示。

这个页面不抓取任何内容:图片只是文本里的一个 URL,这里没有任何东西会去读它指向的图片。

代码、表格与任务列表

类型位置内容大小
代码第 23 行js2 行
代码第 28 行无语言1 line
表格第 11 行left right center3 列,2 行
任务列表——完成 1,未完成 1
没有语言的围栏代码块只会得到裸的 `<pre><code>`,没有 `class`——语法高亮器正是靠它来上色,所以围栏后面什么都不写,就是一个没人能上色的代码块。
管道表格必须保持同一种形状,所以单元格数量不同的行会被补齐或裁掉,审计会说明这一点:内容悄悄消失就发生在这里。
任务列表项在 HTML 里会变成禁用的复选框,这正是 GFM 的产物——它也能被读回成任务,因为那个复选框是 HTML 里唯一能说“这原本是任务”的地方。

目录、标题和段落不在这里列出:上面的提纲已经覆盖了标题,而段落就是输出框里的内容。

转换时注意到的问题(7 条)

区域行层级说明
表格14值得一看这一行有 2 个单元格,表头行有 3 个。管道表格必须保持同一种形状,所以这一行会被补齐或裁掉——单元格悄悄消失就发生在这里。
结构16值得一看标题层级跳级了:上一个标题是 h2,这一行是 h4。层级只表示“比上一级深一层”,所以这个缺口意味着缺了一节——对屏幕阅读器、目录和搜索引擎都是如此。
代码28仅供参考这个代码块的围栏后面什么都没写,所以 HTML 是裸的 `<pre><code>`,没有 `class`。放在那里的语言标记,正是语法高亮器要读的东西。
语法37值得一看`[^1]` 是多种 Markdown 方言与静态站点生成器里的脚注语法。这里不实现脚注,所以标记按它本来的文本打印,而不是悄悄变成一个没人能读的注释。
语法37值得一看`==highlighted==` 是某些方言使用的高亮标记。GFM 和 CommonMark 都没有它,所以按文本打印,而不是变成 `<mark>`。
原始 HTML39值得一看这一行里的内联 HTML `<span class="badge">` 被转义了,而不是交给浏览器。粘贴进来的文档通常就是这样带着 `<b>` 出现,而原样放行正是页面渲染出没人读过的标记的方式。
图片41值得一看这一行的图片没有替代文本。它会渲染成 `<img src="…" alt="">`,屏幕阅读器读到的是一片空白。
表格里的文字就是发现本身的原话:这张表由报告和 CSV 携带的同一批句子生成,所以页面、报告与下载内容不会对“注意到了什么”产生分歧。
所有发现都打印在这里,行号就是上面输入框里的行号。
往返类的发现说的是这个页面,而不是你的文档:它意味着本页写出的 Markdown 读回来时文字不同,那会是本页的 bug。

这个引擎没有实现的语法,会按它最终会变成的文本报告出来,而不是悄悄打印得好像真有什么含义。

你的 Markdown 与本页会写回的 Markdown 对比

--- the first text
+++ the second text
@@ -11,7 +11,7 @@
 | Setting | Default | What it changes |
 | :--- | ---: | :---: |
 | tables | on | GFM only |
-| line breaks | off |
+| line breaks | off |  |
 
 #### A sub-subsection, which skips a level
 
@@ -31,13 +31,10 @@
 
 ---
 
-Setext headings work as well
+## Setext headings work as well
-----------------------------
+
-
+A footnote marker\[^1\], a phrase someone wanted ==highlighted==, and an ordinary link: [reset your password](https://example.com/reset).
-A footnote marker[^1], a phrase someone wanted ==highlighted==, and an ordinary link: [reset your password](https://example.com/reset).
+
-
+Raw HTML is escaped by default, so \<span class="badge">Beta\</span> reads as the characters it is.
-Raw HTML is escaped by default, so <span class="badge">Beta</span> reads as the characters it is.
+
-
+The guide is [over here](https://example.com/guide "The guide"), the bare address [https://example.com/bare](https://example.com/bare) is recognized, and an image with no alt text looks like this: ![](/images/diagram.png)
-The guide is [over here][the-guide], the bare address <https://example.com/bare> is recognized, and an image with no alt text looks like this: ![](/images/diagram.png)
-
-[the-guide]: https://example.com/guide "The guide"

原文消失了 11 行,新增了 8 行。经过另一种语言的往返不是恒等变换,差异就在这里:强调换了拼法,列表写成紧凑或松散,表格被补齐以维持形状。不能变的是文字,上面那条读回检查测的就是这一点。

生效的规则

决定作用
方向Markdown 转成 HTML
方言GFM:管道表格、任务列表、删除线和裸链接都会读取并写出
原始 HTML转义成文本,因为粘贴进来的文档是不可信输入
危险协议凡是链接可以被点击的地方,`javascript:`、`vbscript:`、`data:` 和 `file:` 都会被拒绝,并打印所在行
实体实体引用原样保留:`&copy;` 仍是 `&copy;`,浏览器画出 ©,这正是 CommonMark 规定的含义
标题 id为每个标题添加,由标题文字生成,重复时追加 `-1`
单个换行在 HTML 源码里保持为换行,浏览器把它画成空格
输出只输出片段,可以直接粘进现有页面
表格对齐写成 `style="text-align: …"`,这是现行标准要求的写法,而不是已过时的 `align` 属性
缩进代码四个空格在这里不会开启代码块,这是对 CommonMark 的一处偏离:请用围栏
列表续行续行必须缩进到条目正文的位置,这是另一处偏离:没有惰性续行
未实现脚注、定义列表、数学公式、`==highlight==`、警告块引用、容器、属性块和 emoji 短代码都会被识别并按文本报告,而不是转换
其中两条是对 CommonMark 的偏离——缩进代码与惰性续行——它们和引擎完全不实现的语法一样,被打印出来而不是藏起来。方言、原始 HTML 设置和协议设置是页面在下方解释里专门讨论的三项;其余都是每种只有一个合理答案的实现选择。

肯列出自己决定的转换器,才是行为可预测的转换器;在这里这比速度更重要:产出的 HTML 最终会出现在别人阅读的页面上。

这个引擎实现的语法

  • 标题:`#` 到 `######`,以及下划线形式的 `===` / `---`
  • 段落由空行分隔,硬换行来自行尾两个空格或一个反斜杠
  • 块:`>` 引用,`-` 与 `1.` 列表(可嵌套、紧凑或松散,支持 `start`),`---` 分隔线,反引号或波浪线围栏的代码
  • 行内:强调、加粗、删除线、代码段、链接(行内与引用式)、图片、`<https://…>` 自动链接
  • 仅 GFM:带对齐的管道表格、任务列表项、`~~删除线~~`、直接写在文本里的链接

这些数字没有说明的事

  • 这个引擎实现的是一个有名字的子集,而不是整份规范,下面的规则表会把它列出来——包括两处对 CommonMark 的偏离:四个空格不开启代码块,列表续行必须缩进。
  • 读回检查针对的是这个页面,而不是你的文档:它写出的 Markdown 由自己的解析器读回来,再比较文字。结构可能在途中换一种拼法;文字变了才是这里的 bug,发现里会指出位置。
  • 这个方向不会丢东西,因为 Markdown 是更小的语言:它能表达的一切在 HTML 里都有地方可去。反方向回来时,有些元素没有 Markdown 对应物,无法保留内容的表格会把它们逐个点名。
  • 原始 HTML 默认被转义,这是决定而不是限制:粘贴进转换器的文档是不可信输入,把它的标记直接交给浏览器,正是转换器变成注入最后一环的方式。
  • 答案是源码,不是预览。页面把产出的 HTML 打印出来,而不是塞回自己体内,因为绘制别人的标记,正是上面那些转义要避免的事。把结果放进文件就能看到它被绘制。
转换在这个标签页里完成,用的就是你加载页面时已经产出 HTML 的那个函数,所以什么都没有被发送——这一点在这里比在多数页面上更重要,因为人们粘进转换器的,往往是别人的页面或还没发布的草稿。

它不是 CommonMark 实现,也从不这样宣称。它只实现一个有名字的子集,会把它列在下面,并点明两处刻意的偏离。它认得出但没有实现的语法——脚注标记、`$math$`、`==highlight==`、GitHub 警告块——会被当作最终会变成的文本*报告*出来,而不是悄悄打印得好像真有什么含义。可信的转换器和不可信的转换器,区别就在这里:后者会在文章中间留下一个 `[^1]` 而什么都不说。

答案是源码,不是预览。页面把它产出的 HTML 打印出来,而不是塞回自己体内,因为绘制别人的标记,正是转义要避免的事。把结果放进文件就能看到它被绘制——如果你选了整份文档,它会按原样打开。

关于这个工具

如果你把文档粘进一个转换器、把页面粘进另一个,就会有两套心智模型,却只有一份事实来源。这一页把两个方向放在同一处完成:Markdown 转 HTML,以及 HTML 转回 Markdown,并在答案旁边打印大纲、每个链接及其协议、无法保留的内容,还有往返结果。

没有任何上传,因为页面背后没有能接收草稿的接口:两个转换都按固定规则处理文本,都在你正在阅读的标签页里运行。这一点对往返尤其重要,因为它会改写你自己的文档并与原文比较——看见转换器改了什么,不该以交出一份私人草稿为代价。

它不是 CommonMark 实现,也从不假装是:它只实现一个有名字的子集,把它列在输入框下面,并点明两处刻意的偏离。它认得出但没有实现的语法,会按最终会变成的文本报告,所以脚注标记或数学片段不会以别的样子混进文章里。

它是怎么工作的

一份文档如何在这两种格式之间往返

两个方向读的是同一份结构——标题、段落、引用、列表、代码与表格,再把它写成另一种语言。这就是一份文档能在 Markdown 与 HTML 之间来回转换而不丢字的原因:第二次转换承接的是第一次读懂的内容,而不是重新猜一遍原文。

页面上的往返结果,就是把文档写回去再与你粘贴的内容比较。它报出的每一处差异,都是两种语言之间的差异——强调符号换了一种写法、列表的空行排布不同——而不是转换本身出错。

过滤了什么,为什么过滤

别人给的 Markdown 是不可信输入,而转换器正是不可信输入变成标记的地方。所以文本里的每个字符都会被转义——`<`、`>`、`&` 和 `"` 变成实体,唯一放过的是本来就已经是实体引用的那些 &。原始 HTML 块默认同样被转义:`<div class="note">` 变成 `&lt;div class="note"&gt;`,读者看到的就是这些字符本身。页面提供了放行的开关,因为 CommonMark 规定应当放行,也因为你自己的标记不是别人的标记。

链接有自己的规则,而且是实践中最要紧的一条。链接里的 `javascript:` 地址,会让页面执行随文档一起送来的代码;`data:text/html,...` 则是在本该放链接的位置塞进一整份文档。凡是链接可以被点击的地方,这两类默认都被拒绝,每次拒绝都会连同所在行号打印出来。不被拒绝的是图片里的 `data:image/...`:图片源不是导航目标,把像素内联进来是常规做法,区分两者的只有协议。检查会按浏览器读取地址的方式来判断——先解码、再去掉空白——所以 `&#106;avascript:` 与 `java&#9;script:` 同样会被拦下。

读回检查:往返结果告诉了你什么

页面写出的 Markdown 会被读回来,再比较两份文档的文本。如果有词变了,报告会指出来并给出位置。结构允许在途中变化——`_emphasis_` 会写成 `*emphasis*`,列表回来时空行的排布可能不同——因为那是同一份文档的另一种拼法,报告会说明它遇到的是哪一种。

比较之前会折叠连续空白,所以段落重新折行不算差异,词变了才算。这是一个很窄的承诺,页面上写明了;每个答案旁边的往返结果就是它的证据。

Markdown 装不下的东西,以及这个页面不会做的事

Markdown 是更小的语言,所以进入它的方向一定会丢东西:`<div>` 没有对应物,会被脱壳;`<span>` 消失而其中的文字留下;`<sup>` 因为没有上标语法而退化成普通文本;`<script>` 则被直接丢弃,而不是转换。这些都不留给访问者事后发现——无法保留内容的表格会写出元素名、出现次数以及最终的处理方式。

另一个方向不会丢东西,因为 Markdown 能表达的内容在 HTML 里都有地方可去。这个方向有的是它拒绝解释的语法清单:脚注、定义列表、行间与行内数学公式、`==highlight==`、警告块引用、容器指令、属性块和 emoji 短代码都会被识别,并按它们最终会变成的文本报告出来。页面还印出两处对 CommonMark 的偏离:四个空格在这里不会开启代码块,请用围栏;列表的续行必须缩进到条目正文的位置,因此没有惰性续行。

示例

示例使用页面自带的默认输入,只改设置;下面的结果与你在页面上看到的完全一致。

一份简短的 README,带列表和表格

转换方向: Markdown 转成 HTML · Markdown 文本: # Widget - Built with **care** - [x] Tested | Flag | Default | | --- | ---: | | `--fast` | off | · 用哪种 Markdown: GFM:表格、任务列表、删除线、裸链接

Markdown 对应的 HTML

349 字节

8 行Markdown 产出 13 行HTML,包含 1 个标题、0 个链接和 0 张图片;文字通过了往返检查。

输入: 10 词 · 输出: 13 行 · 标题: 1 · 链接: 0

上面的主数字是产出的 HTML 大小;统计数据来自页面自己的计数;页面使用时,大纲、链接和审计会打印在答案旁边。

把 CMS 页面转回 Markdown

转换方向: HTML 转成 Markdown · HTML 文本: <h2>Notes</h2> <p>A <strong>paragraph</strong> with a <a href="/guide">link</a> and a <sup>marker</sup>.</p> <div><p>In a div, which Markdown has no container for.</p></div>

HTML 对应的 Markdown

110 字节

3 行HTML 产出 5 行Markdown,包含 1 个标题、1 个链接和 0 张图片;文字通过了往返检查。

输入: 19 词 · 输出: 5 行 · 标题: 1 · 链接: 1

这个方向上 Markdown 是更小的语言,所以无法保留内容的表格会列出 `<sup>` 和 `<div>`——而它们里面的文字依然保留了下来。

能执行代码的链接,被拒绝并报告

转换方向: Markdown 转成 HTML · Markdown 文本: Reset your password [here](javascript:alert(1)) and keep the ![icon](data:image/gif;base64,R0lGOD). · 能执行代码的链接: 拒绝,保留文字

Markdown 对应的 HTML

97 字节

1 行Markdown 产出 1 行HTML,包含 0 个标题、1 个链接和 1 张图片;文字通过了往返检查。

输入: 8 词 · 输出: 1 行 · 标题: 0 · 链接: 1

链接的文字留下,目标被丢掉:上面的开关决定这一点,审计会打印它所在的行号。图片是另一种情况,允许把像素内联进来。

常见问题

这是 CommonMark 实现吗?

不是,页面会说明它实际实现了哪个子集,而不是暗示其余部分也有。两个方向在重叠之处都遵循 CommonMark 规则——决定 `snake_case` 与 `2*3*4` 含义的侧翼规则,围栏优先于标题、下划线标题优先于分隔线的顺序——有两处行为是刻意不同的:四个空格不开启代码块,列表续行必须缩进到条目正文的位置。两条都印在规则表里,所以输出没有意外。

为什么没有渲染预览?

因为预览意味着在这个页面里绘制别人的标记,而那正是转义要消除的风险。这一页给出的答案是源码:它产出的 HTML,可以直接复制或下载。把它放进文件,或者选择整份文档,浏览器会在自己的标签页里绘制它——那里出了错,是你文件里的错,而不是这个页面的错。

像 `javascript:alert(1)` 这样的链接会怎样?

默认会被拒绝:链接的文字保留,地址被丢掉,因为删掉整句话比要求的改动更大。拒绝会带着行号记入审计,表单上的开关可以为你自己写的文档关掉这条规则。检查针对解码并去掉空白后的地址,所以 `&#106;avascript:` 同样被拒绝——浏览器把它读作 `javascript:`,这一页也必须这样读。

为什么原始 HTML 被转义而不是原样放行?

因为粘贴进转换器的文档通常是别人的,把它的标记原样放行,正是转换器变成注入最后一环的方式。转义之后,`<div onclick="...">` 变成读者看得见、浏览器执行不了的文本。CommonMark 说渲染器应当放行原始 HTML,开关正是为 CommonMark 设想的场景准备的:你自己的标记,你自己的文档。

我的文档会被上传吗?

不会。转换按同一条固定规则处理文本,所以在输入它的标签页里运行;页面背后没有可以接收文档的接口,即便有人尝试发送也没有落点。这一点在这里比在多数页面上更重要:人们粘进转换器的,往往是还没发布的草稿,或者竞争对手的页面。

往返结果在告诉我什么?

它把文档按原来的形式写回去,再和你粘贴的内容做 diff。经过另一种语言的往返不是恒等变换,所以有意思的是差异出现在哪里:强调从 `_x_` 改成 `*x*`,列表由紧凑变成松散,表格被补齐以维持形状。不能变的是文字,旁边那条读回检查测的就是这一点——如果有词变了,页面会说出来并指出位置。

为什么 `_emphasis_` 回来变成了 `*emphasis*`?

因为它们是同一份文档的两种拼法,而页面写回时对每样东西只保留一种拼法。另一种做法是记住访问者用的是哪一种,那意味着把原文一路带着,往返检查就变成了自己测自己,而不是在测两种语言。

页面没有实现的语法会怎样?

会被识别并报告。脚注标记、`$math$`、`==highlight==`、GitHub 警告块、定义列表、容器指令、属性块和 emoji 短代码,都会以它们最终会变成的文本出现在审计里,并附上所在行。可信的转换器和不可信的转换器,区别就在这里:后者会在文章中间留下一个 `[^1]`,什么都不说。

资料来源与统计口径

  • 转换遵循 GitHub Flavored Markdown(GFM)与 CommonMark;方言开关控制表格、任务列表与删除线。
  • 链接与原始 HTML 都会被过滤:`javascript:` 等可执行协议一律拒绝,图片才允许内联。
  • 安全与隐私:转换在浏览器里完成,数据绝不上传。

最近复核于 2026-09-18.

本页在您的浏览器本地运行,请保留原文件备份;不涉及财务建议。 阅读完整免责声明。