HTML格式化

按标签层级重排缩进,同时把 pre、textarea、script、style、title 五个区块的正文逐字原样保留——这几处的空白是内容的一部分,动一个空格就算改了文档。

元素数14
最大嵌套深度4
空元素2
原样区块5
输出行数30
输出字节751

提示:输入框里按 Ctrl + Enter 开始格式化(单独按 Enter 只是换行);「空格数」输入框里按 Enter 会直接重排。结果区里 style、pre、textarea、script 四块的正文比周围少一层缩进,这是刻意的——见下文「为什么不给原样区块的正文加缩进」。

标签分类与缩进行为对照表(47 行,先显示前 12 行)

标签分类缩进行为说明
<area>空元素 void(规范 13 个)不缩进、不加结束标签图片热区,放在 map 里
<base>空元素 void(规范 13 个)不缩进、不加结束标签文档基准 URL 与默认 target
<br>空元素 void(规范 13 个)不缩进、不加结束标签换行
<col>空元素 void(规范 13 个)不缩进、不加结束标签表格列属性,放在 colgroup 里
<embed>空元素 void(规范 13 个)不缩进、不加结束标签内嵌外部内容
<hr>空元素 void(规范 13 个)不缩进、不加结束标签主题分隔线
<img>空元素 void(规范 13 个)不缩进、不加结束标签图片
<input>空元素 void(规范 13 个)不缩进、不加结束标签表单控件
<link>空元素 void(规范 13 个)不缩进、不加结束标签外部资源引用,常用来挂 CSS
<meta>空元素 void(规范 13 个)不缩进、不加结束标签文档元信息
<source>空元素 void(规范 13 个)不缩进、不加结束标签picture / video / audio 的候选源
<track>空元素 void(规范 13 个)不缩进、不加结束标签字幕与章节轨道
<wbr>空元素 void(规范 13 个)不缩进、不加结束标签可断行机会
<basefont>空元素 void(旧版遗留)不缩进、不加结束标签旧版设置基准字体;本工具仍按空元素处理
<bgsound>空元素 void(旧版遗留)不缩进、不加结束标签旧版播放背景音;本工具仍按空元素处理
<frame>空元素 void(旧版遗留)不缩进、不加结束标签旧版框架集里的一格;本工具仍按空元素处理
<keygen>空元素 void(旧版遗留)不缩进、不加结束标签旧版密钥对生成;本工具仍按空元素处理
<param>空元素 void(旧版遗留)不缩进、不加结束标签旧版 object 的参数;本工具仍按空元素处理
<script>正文原样区块开标签与结束标签之间不做任何增删正文整体原样输出,一个字符都不改
<style>正文原样区块开标签与结束标签之间不做任何增删正文整体原样输出,CSS 不做重排
<pre>正文原样区块开标签与结束标签之间不做任何增删正文整体原样输出,行首空格是渲染结果的一部分
<textarea>正文原样区块开标签与结束标签之间不做任何增删正文整体原样输出,改写缩进会改变提交内容
<title>正文原样区块开标签与结束标签之间不做任何增删正文整体原样输出
<a>行内元素与前后文本同行,不单独占行超链接
<abbr>行内元素与前后文本同行,不单独占行缩写
<b>行内元素与前后文本同行,不单独占行着重(旧)
<cite>行内元素与前后文本同行,不单独占行作品名
<code>行内元素与前后文本同行,不单独占行行内代码
<em>行内元素与前后文本同行,不单独占行强调
<i>行内元素与前后文本同行,不单独占行另一种口吻(旧)
<kbd>行内元素与前后文本同行,不单独占行键盘输入
<mark>行内元素与前后文本同行,不单独占行高亮
<q>行内元素与前后文本同行,不单独占行行内引用
<s>行内元素与前后文本同行,不单独占行删除线(旧)
<samp>行内元素与前后文本同行,不单独占行程序输出
<small>行内元素与前后文本同行,不单独占行小字旁注
<span>行内元素与前后文本同行,不单独占行通用行内容器
<strong>行内元素与前后文本同行,不单独占行重要
<sub>行内元素与前后文本同行,不单独占行下标
<sup>行内元素与前后文本同行,不单独占行上标
<time>行内元素与前后文本同行,不单独占行日期时间
<u>行内元素与前后文本同行,不单独占行非文本标注
<var>行内元素与前后文本同行,不单独占行变量名
<div>块级元素单独占行,内部层级 +1通用块容器
<p>块级元素单独占行,内部层级 +1段落
<li>块级元素单独占行,内部层级 +1列表项
<td>块级元素单独占行,内部层级 +1表格单元格

使用方法

  1. 把 HTML 片段或整页源码粘进上面的输入框。页面打开时输入框里已经放了一段 28 行的示例,结果区同时给出它的格式化结果,可以立刻对照着看。
  2. 设置「每级缩进的空格数」,允许 1~8 的整数,默认 2;勾选「用 Tab 缩进」后这个数字被忽略,每级改成一个制表符(ASCII 码 0x09)。在空格数输入框里直接按 Enter 就会立刻重排。
  3. 在源码输入框里按 Ctrl + Enter(macOS 用 Cmd + Enter)触发格式化。单独按 Enter 仍然是换行——这个框是用来写代码的,把回车抢去当快捷键会让多行代码根本没法手动调整。
  4. 看统计条:示例的六个数字是元素数 14、最大嵌套深度 4、空元素 2、原样区块 5、输出行数 30、输出字节 751。「最大嵌套深度」指整段里最深的那一层,示例是 body → div → p 这条链,第 4 层。
  5. 点「复制结果」带走格式化后的源码,或点「下载 .html」存盘。下面那张 47 行的标签对照表默认只显示前 12 行,点「加载更多」展开其余;也可以「导出 CSV」(带 BOM,Excel 打开不乱码)或「复制对照表」(制表符分隔,可直接粘进表格软件)。

这个 HTML格式化 能解决什么问题

一是把挤成一行的模板拆开。模板引擎吐出来的 HTML 常常整段压在一行里,标签套标签,出问题时光看结构都费劲。这类文本里没有任何有意义的空白,重排缩进是纯收益。

二是把「不能动的空白」保护起来。这正是在线 HTML格式化 最容易翻车的地方:拿到 <pre>、<textarea>、<script>、<style> 就当普通元素处理,给里面的每一行统一加缩进。可 <pre> 里的行首空格要显示给用户看,<textarea> 的内容会被原样提交给服务端,<script> 里可能藏着多行字符串——一旦被重新缩进,页面上就凭空多出空格,代码语义也可能被悄悄改掉。本工具把 <script>、<style>、<pre>、<textarea>、<title> 这五个区块的正文整体原样输出,开标签、正文、结束标签三者紧贴,中间连一个换行都不插。

三是看清单行的标签有没有漏闭合。压成一行的源码里少一个 </div> 根本看不出来;重排之后缩进层级会立刻显出异常,统计里的「最大嵌套深度」也会比预期深。工具还会在提示区报出未闭合标签的数量。

四是让 diff 变得可读。两个版本的模板如果都没格式化,代码评审时几乎所有行都被标成改动;先各自格式化再对比,改动的往往只有真正被改的那几行。

五是属性值里的尖括号不再把解析带偏。<a title="a > b"> 这种写法在粗心的实现里会被当成「标签在 > 处结束」,于是 b"> 变成游离文本。本工具按引号成对扫描标签,引号里的 > 不当分隔符用。

本页不做这些事:不压缩体积(去空白请用 HTML 压缩)、不校验标签是否符合规范(请用 HTML 校验)、不把富文本转成纯文本或 Markdown、不格式化 CSS 与 JavaScript 内部代码。它只做一件事:在不改变文档内容的前提下重排缩进。

原理和变量:词法切分、缩进栈与五个原样区块

① 调用的算法。本页没有引入任何第三方 beautifier,逻辑是一个两段式的手写词法器 + 缩进栈。第一段把源码切成四类节点:普通文本、注释与声明(<!-- -->、<!DOCTYPE>)、标签、原样区块;标签再分开始标签、结束标签与自闭合标签。第二段用一个整数 d 记录当前层级:遇到块级开始标签就 d++,遇到块级结束标签就 d--,每行输出前用 d 乘以一个缩进单元做前缀。

② 标签是怎么切出来的。扫描到 < 之后按字符往前走,同时维护一个「当前引号」状态:遇到 " 或 ' 进入引号态,只有再遇到同一个引号才退出;只有不在引号里的 > 才被当作标签结束。这就是 <a title="a > b"> 能被正确切成一个标签的原因。很多实现图省事用正则 /<[^>]*>/ 抓标签,碰到属性里带 > 必然切错,这是本页与它们最直接的差别。

③ 五个正文原样区块,凭什么是这五个。依据是 W3C HTML 5.1 与 WHATWG HTML Living Standard 第 13.1.2 节对元素的分类:script 与 style 是 raw text elements,解析器不把它们的内容当标签看;textarea 与 title 是 escapable raw text elements,内容里的字符引用会解码,但同样不产生子元素;pre 本身是普通元素,它保留空白靠的是标准渲染章节给的默认样式 white-space: pre。这五个的正文都属于「内容」而不是「排版」,所以工具一个字都不动。

④ 为什么不给原样区块的正文加缩进。因为缩进就是在正文前面插空白。给 <pre> 的每一行补上缩进空格,页面上显示的文字就真的会多出这些空格;给 <textarea> 补缩进,提交给服务端的表单值也跟着变了。所以工具输出的是「缩进前缀 + 开始标签 + 原样正文 + 结束标签」这一整段,只在整段的第一行前面加缩进,正文里的换行原封不动。示例里 <style>、<pre>、<textarea>、<script> 四块的正文看起来比周围浅一层,就是这个原因——不是漏缩进,是不能缩。

⑤ 空白只动两处。一是标签内部、引号之外的连续空白压成一个半角空格,所以示例里 <div class="card"   data-id="7"> 变成 <div class="card" data-id="7">,而引号内的空白原样保留;二是文本节点里的连续空白压成一个空格,行首行尾的空格去掉。注意 <style> 与 <script> 的正文属于原样区块,不参与这两条——所以示例里 .card   { color : #333 ; } 那三段空白一字未改。这两条合起来就是「只改排版、不改内容」的边界。

⑥ 空元素清单为什么是 13 个,工具里却装了 18 个。规范第 13.1.2 节列出的 void elements 是 area、base、br、col、embed、hr、img、input、link、meta、source、track、wbr 共 13 个,它们没有结束标签、不产生子节点,所以不增加层级。但 MDN 的 Void element 词条至今还把 param 一起列成 14 个,那是旧版 HTML5 的清单。本工具的处理是:13 个按规范算,另外把 basefont、bgsound、frame、keygen、param 这 5 个已废弃的空元素也按空元素处理,一共 18 个。理由是它们同样没有子节点,若按普通元素处理,会凭空多出一层缩进,而且每次都会报「未闭合」——那是纯粹的噪声。

⑦ 缩进栈对不上时会怎样。遇到没有配对的结束标签,d 到 0 就被夹住,不会变成负数。收尾时如果 d 还大于 0,说明有开始标签没闭合,工具照样输出结果,只在提示区报出数量——格式化器不该替你做「补标签」这种有副作用的决定。

⑧ 行内元素的处理。缩进单元只有两个来源:数字输入框(1~8 个空格)或勾选 Tab(0x09)。行内元素(a、span、strong、em、code 等 24 个,其中 4 个同时是空元素)不会换行,而是和前后文本拼在同一行里,这样 <p> 里的加粗和链接才不会各占一行。

完整示例(数值仅作演示)

示例条件与页面打开时完全一致:28 行源码、每级 2 个空格。实测结果:

同一段输入换缩进单元,行数不变、体积会变,这就是缩进宽度这个变量唯一的影响面:

缩进方式输出行数输出字节相对 2 空格
2 个空格(默认)30751基准
4 个空格30825+74 字节(+9.9%)
1 个 Tab(0x09,按 1 字节算)30714−37 字节(−4.9%)

最能说明「原样保留」的是 <pre> 那两行。输入里它们是「两个空格 + 第一行」「四个空格 + 第二行」,输出里依旧是这两个和四个空格,工具既没有把它们补齐到当前第 3 层(6 个空格),也没有把第二行相对第一行的两级缩进压平。同样地,<style> 里的 .card   { color : #333 ; } 保持三段空白原封不动——CSS 的内容不归本工具管,它只负责不破坏。另两处对照:

输入范围与标签对照表

下面把输入条件和结果放在一起,方便预判。表里的数字都来自上面那段 28 行示例。

输入 / 选项能否处理结果
空输入或只有空白拒绝报「输入框是空的」,结果区清空,不会给出空白的成功结果
超过 20 万字符拒绝报出实际字符数并说明上限,避免长文本把页面卡住
没有任何 < 标签的纯文本拒绝报「没有找到以 < 开头的标签」,提醒这页是给 HTML 用的
标签里的引号没闭合(title="a > b)拒绝报出出错的行第几列,指出从那里的 < 起找不到配对的 >
有开始标签没闭合(<div><p>a</p>)处理照常输出,提示区报「1 个开始标签未闭合」,最大深度按解析结果记
缩进空格数填 0 或 9拒绝报「只能填 1~8 的整数」,页面上限与常见编辑器一致
缩进空格数填 2.5 或空拒绝同上,小数与空值都不接受,不做四舍五入
勾选「用 Tab 缩进」处理忽略空格数,每级 1 个 0x09;示例总体积从 751 降到 714 字节
SVG / MathML 片段处理按同名元素的通用规则缩进;<circle /> 这类自闭合标签不增加层级

结果边界、失败路径与精度说明

常见问题

为什么 pre 和 textarea 里的内容看起来比周围浅一层?

因为没有给它们加缩进,这是刻意的。<pre> 的行首空格会真实渲染给用户看,给它补缩进等于改了显示内容;<textarea> 的正文会被当作表单值原样提交,补缩进等于改了数据。工具只在整段的第一行前面加缩进,正文里的换行和空格一律不动,所以看起来像少了一层。

我写 <div/> 会被当成空元素吗?

不会。HTML 解析器会直接忽略非空元素开始标签里那个斜杠,<div/> 等价于 <div>,后面的内容都是它的子节点。工具按同样的口径处理:非空元素带 /> 仍然增加一层缩进,而且因为等不到结束标签,会计入「未闭合」。这条规则对 <script/> 尤其危险——浏览器会把后面整段 HTML 都吃成脚本文本。

空元素到底是 13 个还是 14 个?

规范第 13.1.2 节列的是 13 个:area、base、br、col、embed、hr、img、input、link、meta、source、track、wbr。MDN 的 Void element 词条还多列了一个 param,那是旧版 HTML5 的清单,param 后来被废弃了。本页的 HTML格式化 按 13 个处理规范清单,另外把 basefont、bgsound、frame、keygen、param 这 5 个已废弃的空元素也一起当空元素,总共 18 个。

属性值里带 >,会被切断吗?

不会。切标签时工具维护了一个引号状态机,只有不在引号里的 > 才结束标签,所以 <a title="a > b"> 会被当成一个完整标签。真正会切错的是用 /<[^>]*>/ 这类正则抓标签的实现,它碰到属性里的 > 就断了。

为什么不顺手把 CSS 和 JavaScript 也格式化?

因为它们不是 HTML。CSS 的花括号与行内注释、JavaScript 的正则字面量与模板字符串,各自都要一套独立的状态机才能安全重排;用 HTML 的规则去套,很容易把 // 注释里的内容或正则里的空白改坏。本页只保证 HTML 层面的缩进正确,代码内部的排版请交给各自的格式化器。

代码里出现 </pre> 这种文字会不会提前结束区块?

会,而且这是 HTML 本身的规则,不是工具的缺陷。按解析规范,<pre> 的正文里出现 </pre> 就代表这个元素结束了,剩下的文字落到外面。要在页面上显示这段文字,必须写成 &lt;/pre&gt;。工具同样如此——它不猜你的意图,只按规范切。

格式化之后前端样式跑偏了,可能是什么原因?

绝大多数情况不是本工具改了内容,而是原文件本来就靠空白占位:比如把一段文字直接写在 <div> 里,用连续空格或换行做间距。这类写法依赖的是空白折叠之外的排版技巧,重排后自然就散了。正确做法是把间距交给 CSS 的 margin / padding,或者把需要原样显示的部分放进 <pre>。

能处理 XML、SVG 或 Vue / React 的组件文件吗?

能分段处理但不能当成专用格式化器。SVG 与 MathML 片段是按标签嵌套缩进的,<circle /> 这类自闭合标签不增加层级,这部分没问题。但 .vue 与 .jsx 里混着 JavaScript 表达式和插值语法,表达式里的比较运算符会被当成标签字符,所以整份组件源码不适合直接扔进来;只把其中的模板片段单独格式化是安全的。

相关工具