杂记 › 技术笔记 › 「允许 HTML」打开后,为什么代码块里的 <div> 还是被转义

「允许 HTML」打开后,为什么代码块里的 <div> 还是被转义

一、满屏的字面量是从哪一步冒出来的

整理一篇内部文档时,我在 Markdown 里嵌了一段现成的 HTML:一个 <div class="callout"> 包住三行提醒,想靠它自带的边框把「这段配置别乱改」标出来。转完以后边框没有,标签倒整整齐齐排在地面上,尖括号全变成了 < 与 >。

卡住的是「允许 HTML」这个默认关着的开关:关掉时正文里的 <b> 一律被转义成字面量,开启后行内与块级标签才原样透传。同一份源码 x <b>y</b> z,关着产出 28 字符,开着产出 20 字符,差的就是被转义撑出来的那几个。

开关拨上去之后新的困惑跟着来了:那段示例代码块里的 <div> 仍是字面量,像什么都没发生过。这一篇把透传的边界逐条讲清,顺带说说表格、任务列表和标题层级上容易归错因的几处。

关闭时 28 字符 开启时 20 字符 同一条源码 x <b>y</b> z 差的 8 个字符全在尖括号上 图 1:切换「允许 HTML」前后的输出长度

二、透传有三条硬边界,差一条就退回转义

第一条是实体要成串。源码里的 &copy;、&amp;、&#65;、&#x41; 在开关关闭时被整体二次转义,屏幕上留下 &copy; 这种前面多套一层的字样;开启后具名、十进制、十六进制三种成形实体一起放行。裸的与号与引号不在放行范围,开着也照样被写成 &amp; 和 "。

第二条是标签形状。透传要求小于号后面紧跟字母或感叹号,并以大于号收尾。夹在段落中间的 a <!-- hidden --> b 开着也被转义,行内那一路不接受注释;独占一行的 <!-- c --> 反倒原样落地,块级分支的字符类里带着感叹号。没收尾的 <b c 同样按普通文本处理。

第三条是属性体的长度。属性段正好 400 字符时 <b x="…"> 整句透传;再加一个字符到 401,开标签被转义、闭标签 </b> 仍透传,一句 HTML 被劈成半转义半标签。

能碰上的上限就这两处:属性体 400 字符、转换提示 20 条(第 21 条起静默丢弃)。字数与体积没有门槛,输入区不做长度判断,也没有超限回退。

400 字符 整句透传 401 字符 一句被劈成两半 左段:开标签 <b x="…"> 被转义 右段:闭标签 </b> 仍原样透传 图 2:属性体长度卡在 400 与 401 之间

三、代码块这一路只经过转义,开关碰不到它

围栏代码块和 4 空格缩进代码块都直接进转义那一步,不经过处理原始 HTML 的分支。于是 ```html 围栏里的 <div class="x">y</div>,开关开着与关着拿到逐字节相同的输出;缩进代码里贴一段脚本,两种设置下同样写成转义字样。

行内代码是同一口径:一对反引号包住的 <b>,开关两侧都输出 <code><b></code>。甚至开着「允许 HTML」之后,反引号里的成形实体会被再多转一层,落点比关着时更绕。

这么一想就顺了:开关回答的是「我写的这段是不是真 HTML」,代码块天生在说「我在展示别人的 HTML」。我是把同一段源码在 Markdown 转 HTML 的两种设置下各跑一遍、再逐字符比输出才确认的。

四、开启之后,反而变差的两件事

第一件是尖括号自动链接。独占一行的 <https://example.org>,关着时正常出锚点;开着时块级分支先命中,输出原样还是 <https://example.org> 这一段文本,链接没了。同一行前面垫几个字才回到行内那一路,锚点重新出现。

第二件是它压根不是过滤器。没有标签白名单这回事:开着时 <script>alert(1)</script> 原样进结果,<span onclick="alert(1)"> 的事件属性不会被洗掉,<iframe> 也照抄,而预览那一侧走的是 innerHTML,这些标签是真会生效的。所以它的定位是「我知道自己在写什么」,不是「我粘的内容可以随便信」。

五、表格、任务列表和标题:静默的地方比提示多

表格的入场券是一行分隔行,且那一行里同时有短横与竖线。差一点,落点就完全不同:

输入实测输出
表头、分隔行、一行数据206 字符,thead 与 tbody 齐全
同一张表关掉 GFM整张表退回一个 <p>,不出 table 标签
把分隔行删掉同样退回 <p>,且不给任何提示
数据行比表头多一格只出 2 个 <td>,多余内容没了,提示写「表格第 1 行单元格比表头多」
:---、:---:、---: 三列只有居中与右对齐拿到 style,左对齐那列不带 style
| a | 下面紧接一行 ---被认成下划线式标题,整张表变成 <h2 id="a">
  • 提示里的「第 1 行」是数据行序号,不是源文件的行号,照着它去数原文容易错位。
  • 重新点一次转换是把结果整体覆盖,不会把两轮的提示或正文叠起来。
  • 任务列表 - [x] 与 - [ ] 出 <input type="checkbox" checked disabled>,每条 <li> 带 class="md-task",大写 X 同样算勾选;关掉 GFM 后方括号标记按原文留在正文里,没有 class 也没有 input。

标题上有两处容易归错因:七个井号起头不会被认成标题,级别最高只到 h6,目录的嵌套因此严格跟着标题级别走、跳级会插进空的一层 <ul>。标题 id 走自建 slug——ASCII 标点整枚丢弃、中文保留、空格转连字符、同名依次追加 -1、-2,这套并不属于 CommonMark 或 GFM 的规定动作。

而 id 的写入条件是「标题锚点」与「生成目录」有一个为真:只关掉锚点、目录还开着,正文里照样出现 <h1 id="a">;两个都关才拿到不带 id 的标签。

六、三个绕不过去的小问题

Q:代码块里的 HTML 有办法透传出来吗?

没有办法。围栏与缩进两条分支只经过转义,压根不看「允许 HTML」。想让它真的变成结构,就写进正文并开启开关;只想展示代码本身,留在代码块里,字面量正是你要的结果。

Q:开着「允许 HTML」,粘来的脚本会被挡住吗?

挡不住,它不是安全过滤,也没有标签白名单。带脚本的标签、带事件属性的 span、iframe 都原样进结果,预览那一侧还会真的生效。要发到公网的稿子,我先把开关关着跑一遍,把满地字面量当一份待审清单。

Q:统计行的「字符」和「HTML」为什么对不上?

两栏用的是不同的尺:「字符」按输入源码的 UTF-16 码元数计,「HTML」按输出文本的 UTF-8 字节计,混着中文时差距会被拉大——一个汉字算一个码元,却不止一个字节。

透传只覆盖正文里成形且没超长的标签,代码块、不成串的实体、写到一半的尖括号都在覆盖之外。先弄清自己那段文本走的是哪一路分支,比记住开关的名字有用得多。

📎 相关文章

后缀决定 MIME:同一份 70 字节 PNG 头改出 18 个后缀、13 种类型和 5 个未知 2026-10-23 转 JPG 时透明被强制填底色:91 个质量档位在 PNG 面前不参与编码 2026-10-23 「全部清除」后还剩 553 字节,「只去掉位置信息」连缩略图都没动 2026-10-23 序号位数只是下限、重名时 (2) 插在后缀之前:批量重命名 2000 个文件的实测口径 2026-10-23 证书指纹实测:1159 字节 DER 与 3 种摘要、4 种写法 2026-10-23