XML Formatter 自闭合标签缩进:Token、标签类型与缩进栈
自闭合标签 <item/> 不会打开新的子元素层级;普通开始标签 <item> 则需要等待对应结束标签。formatter 若只按 < 和 > 换行,容易让自闭合元素多缩进一层或让后续兄弟节点错位。本文聚焦 token 化、标签类型和缩进栈,不宣称覆盖全部 XML 语法。
一、问题概述:自闭合标签没有待匹配的结束层
<root><item/><item><name>A</name></item></root> 中第一个 item 在同一个位置结束,第二个 item 才会压入缩进栈。若算法把两者都当成开始标签,<name> 和后续 </root> 的缩进都会偏移。
二、最小复现:三种标签类型改变深度
formatter 至少要区分开始标签、结束标签和自闭合标签;注释与处理指令通常作为不可拆分 token。下面的标签只用于演示缩进深度。
const tokens = ["<root>", "<item/>", "<item>", "<name>A</name>", "</item>", "</root>"];
console.log(tokens);
// <item/> changes no depth; <item> pushes; </item> pops.三、根因:换行决策依赖标签状态而不是字符数量
开始/结束标签、注释、CDATA、处理指令和文本节点的边界不同。缩进栈需要保存尚未关闭的元素名称,并在结束标签到来时先弹栈再输出;遇到不支持的 token 时应停止猜测,而不是继续生成看似整齐的 XML。
四、推荐方案:先 token 化,再按栈输出缩进
将输入分成标签、注释、处理指令和文本 token,识别标签名称与是否 / > 结尾。输出前处理结束标签,输出后处理开始标签;自闭合和叶子 token 不改变栈。这个算法只负责格式布局,XML 合法性仍需 parser 验证。
五、完整代码:受限标签流的缩进算法
以下代码使用简单 token 化和元素栈,适合说明自闭合缩进;它不完整处理属性值中的 >、CDATA、DOCTYPE、命名空间或所有 XML 转义,因此不能替代 XML parser。
function formatSimpleXml(source: string): string {
const tokens = source.match(/<!--[\s\S]*?-->|<[^>]+>|[^<]+/g) ?? [];
const stack: string[] = [];
const lines: string[] = [];
for (const token of tokens.map((value) => value.trim()).filter(Boolean)) {
if (token.startsWith("</")) {
stack.pop();
lines.push(" ".repeat(stack.length) + token);
continue;
}
if (token.startsWith("<!--") || token.startsWith("<?") || token.endsWith("/>") || !token.startsWith("<")) {
lines.push(" ".repeat(stack.length) + token);
continue;
}
lines.push(" ".repeat(stack.length) + token);
const name = token.match(/^<([A-Za-z_][A-Za-z0-9_.:-]*)/)?.[1];
if (name) stack.push(name);
}
if (stack.length !== 0) throw new Error("unclosed element in restricted formatter");
return lines.join("\n");
}
console.log(formatSimpleXml("<root><item/><item><name>A</name></item></root>"));六、常见错误方案
看到每个 <...> 就增加深度会误处理自闭合标签;先输出开始标签再遇到结束标签才弹栈会让结束标签多缩进;用正则匹配完整标签却不处理属性引号中的 >,可能提前截断 token。
七、边界条件:文本、注释与不完整输入
内联文本是否与元素同一行需要单独规则;注释和处理指令不能被拆成普通标签;CDATA 与 DOCTYPE 可能包含看似标签的字符。输入未闭合、标签不匹配或包含未知构造时,受限 formatter 应报错或原样保留。
八、如何验证缩进算法
测试根元素、连续自闭合标签、嵌套开始/结束标签、注释、处理指令、文本节点和属性中的 >。断言栈深度、输出顺序、自闭合标签不增加层级,并将格式化结果交给 XML parser 做语法验证。
九、FAQ
问:自闭合标签和空元素开始/结束标签完全等价吗?答:在 XML 数据模型中常可表示相同空元素,但 formatter 仍必须保留输入是否自闭合的布局选择。
问:缩进栈能验证 XML 合法性吗?答:只能检查受限标签流的层级,不能覆盖完整 XML 语法和命名空间规则。
问:为什么不直接用正则完成全部格式化?答:属性、CDATA、DOCTYPE 和方言扩展会包含标签样字符,必须先有可靠 token 边界。
十、总结
自闭合缩进算法的关键是标签分类和栈顺序:结束标签先弹栈,普通开始标签入栈,自闭合标签不改变深度。受限 token 化适合布局说明,但完整 XML 合法性、CDATA、DOCTYPE 和属性边界仍需正式 parser。
来源与延伸阅读
技术审校所依据的规范与权威参考资料。
相关文章
XML Formatter:HTML 实体、字符引用与二次转义
区分 XML 实体、数字字符引用、HTML 实体和 CDATA,定位 XML formatter 在解析与序列化之间产生二次转义的环节。
踩坑避坑XML Formatter:空白文本节点污染与 mixed content
区分纯字符串格式化和重新解析 DOM,说明缩进空白何时会成为文本节点,以及 mixed content 为什么不能随意插入换行。
实现原理XML 转 JSON:浏览器 DOMParser 的解析、错误检测与非流式边界
说明浏览器 DOMParser 的 XML 解析流程、parsererror 检测和 DOM 遍历转 JSON 方法,不编造性能数据,并明确 DOMParser 不是流式解析器。
继续阅读
可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。
打开关联工具