xml-diff-kit
v0.2.1
Published
A TypeScript toolkit for parsing, normalizing, diffing, patching, and formatting XML and HTML document changes.
Maintainers
Readme
xml-diff-kit
English | 简体中文
一个 TypeScript XML / HTML 差异工具包,用于解析、标准化、比较、补丁应用、序列化和格式化文档结构变更。
xml-diff-kit 面向需要结构化、机器可读文档差异数据的场景,而不是面向可视化文本对比。它适合结构化编辑器、审阅流程、变更追踪、补丁应用、XML 文档比较、HTML 结构配对,以及浏览器端工具。
它可以在 Node.js 和现代浏览器中使用。包同时提供 ESM 和 CJS 产物,公共 API 不依赖 Node.js 专属运行时能力。
安装
npm install xml-diff-kit使用方式
下面大部分示例共用同一组 XML:
const oldXml = '<procedure><step id="s1">Remove the panel.</step></procedure>';
const newXml = '<procedure><step id="s1">Remove the access panel.</step><step id="s2">Inspect.</step></procedure>';diffXml
比较两个 XML 文档,输出结构化 diff operations。
import { diffXml } from 'xml-diff-kit';
const ops = diffXml(oldXml, newXml, {
keyAttrs: ['id'],
});
console.log(ops);输出:
[
{
op: 'replaceText',
path: '/procedure[0]/step[@id="s1"][0]/text()[0]',
oldValue: 'Remove the panel.',
newValue: 'Remove the access panel.',
changes: [{ op: 'insertText', offset: 11, text: 'access ' }],
segments: [
{ type: 'equal', text: 'Remove the ' },
{ type: 'insert', text: 'access ' },
{ type: 'equal', text: 'panel.' }
]
},
{
op: 'addNode',
path: '/procedure[0]/step[@id="s2"][1]',
value: {
type: 'element',
name: 'step',
namespaceURI: null,
attrs: { id: 's2' },
children: [{ type: 'text', text: 'Inspect.' }]
}
}
]patchXml
把结构化 diff operations 应用回 XML 字符串或已解析的 XML 节点。
当你已经有一组 XmlDiffOp[],并希望把它们应用到 XML 文档上时,可以使用 patchXml。当输入是 XML 字符串时,patchXml 返回字符串。当输入是 XmlNode 时,它返回补丁后的 XmlNode。
新增节点
import { patchXml, type XmlDiffOp } from 'xml-diff-kit';
const xml = '<root><a/></root>';
const ops: XmlDiffOp[] = [
{
op: 'addNode',
path: '/root[0]/b[1]',
value: {
type: 'element',
name: 'b',
namespaceURI: null,
attrs: {},
children: []
}
}
];
const patchedXml = patchXml(xml, ops);
console.log(patchedXml);输出:
<root><a/><b/></root>更新属性
import { patchXml, type XmlDiffOp } from 'xml-diff-kit';
const xml = '<root status="draft"/>';
const ops: XmlDiffOp[] = [
{
op: 'updateAttr',
path: '/root[0]',
name: 'status',
oldValue: 'draft',
newValue: 'released'
}
];
const patchedXml = patchXml(xml, ops);
console.log(patchedXml);输出:
<root status="released"/>替换文本
import { patchXml, type XmlDiffOp } from 'xml-diff-kit';
const xml = '<root>old text</root>';
const ops: XmlDiffOp[] = [
{
op: 'replaceText',
path: '/root[0]/text()[0]',
oldValue: 'old text',
newValue: 'new text',
changes: [
{
op: 'replaceTextRange',
offset: 0,
oldText: 'old',
newText: 'new'
}
],
segments: [
{ type: 'delete', text: 'old' },
{ type: 'insert', text: 'new' },
{ type: 'equal', text: ' text' }
]
}
];
const patchedXml = patchXml(xml, ops);
console.log(patchedXml);输出:
<root>new text</root>patchXml 根据路径应用变更。路径中的数字索引是实际用于定位节点的部分。类似 [@id="s1"] 的 key 提示用于提升可读性,并帮助 diffXml 对齐节点,但 patch 时仍然依赖数字索引。
支持的 patch 操作包括新增、删除、替换、移动节点;新增、更新、删除属性;以及替换文本节点内容。
formatDiff
把结构化 diff operations 格式化为摘要对象,或者 Markdown 报告。
import { diffXml, formatDiff } from 'xml-diff-kit';
const ops = diffXml(oldXml, newXml, { keyAttrs: ['id'] });
const summary = formatDiff(ops);
console.log(summary);输出:
[
{
type: 'textChanged',
path: '/procedure[0]/step[@id="s1"][0]/text()[0]',
message: 'Changed text at /procedure[0]/step[@id="s1"][0]/text()[0]',
before: 'Remove the panel.',
after: 'Remove the access panel.'
},
{
type: 'nodeAdded',
path: '/procedure[0]/step[@id="s2"][1]',
message: 'Added node at /procedure[0]/step[@id="s2"][1]',
after: {
type: 'element',
name: 'step',
namespaceURI: null,
attrs: { id: 's2' },
children: [{ type: 'text', text: 'Inspect.' }]
}
}
]Markdown 输出:
const markdown = formatDiff(ops, { format: 'markdown' });
console.log(markdown);输出:
# XML Diff
Total changes: 2
## 1. Changed text
- Path: `/procedure[0]/step[@id="s1"][0]/text()[0]`
**Before**
```text
Remove the panel.
```
**After**
```text
Remove the access panel.
```
**Text segments**
- equal: `Remove the `
- insert: `access `
- equal: `panel.`
## 2. Added node
- Path: `/procedure[0]/step[@id="s2"][1]`
```xml
<step id="s2">Inspect.</step>
```parseXml 和 serializeXml
把 XML 解析为内部 AST,再序列化回 XML 字符串。
import { parseXml, serializeXml } from 'xml-diff-kit';
const doc = parseXml('<root><item id="1">Hello</item><item id="2">World</item></root>');
const xml = serializeXml(doc, { pretty: true });
console.log(xml);输出:
<root>
<item id="1">Hello</item>
<item id="2">World</item>
</root>normalizeXml
在 diff 或自定义处理前,标准化 XML AST。
import { normalizeXml, parseXml } from 'xml-diff-kit';
const doc = parseXml('<root b="2" a="1"> <item> value </item> </root>');
const normalized = normalizeXml(doc, {
ignoreWhitespaceText: true,
trimText: true,
sortAttributes: true,
});
console.log(normalized);输出:
{
type: 'element',
name: 'root',
namespaceURI: null,
attrs: { a: '1', b: '2' },
children: [
{
type: 'element',
name: 'item',
namespaceURI: null,
attrs: {},
children: [{ type: 'text', text: 'value' }]
}
]
}HTML 支持
HTML 支持是显式入口,不会和 XML 解析混在一起。parseXml 仍然只处理 XML;遇到 HTML 输入时,请使用基于 parse5 的 HTML API。
diffHtml
比较两个 HTML fragment 或 document,按浏览器兼容的 text/html 语义输出结构化 diff。
import { diffHtml } from 'xml-diff-kit';
const ops = diffHtml(
'<div><p>Hello</p></div>',
'<div><p>Hello world</p><span>New</span></div>',
);
console.log(ops);diffHtml 复用和 diffXml 相同的 XmlDiffOp[] 模型,包括 addNode、removeNode、replaceNode、moveNode、replaceText、addAttr、updateAttr、removeAttr。
parseHtml
把 HTML 解析为共享内部 AST。
import { parseHtml } from 'xml-diff-kit';
const fragment = parseHtml('<div><img src="hero.png"><p>Hello</p></div>');
const documentNode = parseHtml('<title>Hi</title><p>Hello</p>', {
sourceType: 'document',
});为了在不扩展公开 AST union 的前提下表达浏览器风格的 HTML 树,HTML 会使用合成容器根节点:
- Fragment 使用
#document-fragment - 完整 document 使用
#document
因此 HTML diff path 会从这些合成根开始,例如 /#document-fragment[0]/div[0]/p[1]。
normalizeHtml
在 diff 或自定义处理前,对 HTML AST 做稳定化标准化。
import { normalizeHtml, parseHtml } from 'xml-diff-kit';
const normalized = normalizeHtml(parseHtml('<input DISABLED="disabled" b="2" a="1">'));
console.log(normalized);HTML 标准化行为:
- HTML 标签名统一为小写
- HTML 属性名统一为小写
- 默认按属性名排序,保证稳定输出
- boolean attribute 统一归一为
"" ignoreWhitespaceText和trimText依然可用pre、code、textarea、script、style内部保留空白parse5自动补全出的html、head、body、tbody等结构会被当作稳定结构来 diff
patchHtml 与 serializeHtml
把结构化操作应用回 HTML,并把共享 AST 序列化为规范化 HTML 字符串。
import { diffHtml, patchHtml } from 'xml-diff-kit';
const oldHtml = '<div><p>Hello</p></div>';
const newHtml = '<div><p>Hello world</p><span>New</span></div>';
const ops = diffHtml(oldHtml, newHtml);
const patched = patchHtml(oldHtml, ops);
console.log(patched);HTML 与 XML 的区别
parseXml仍然是严格 XML 入口parseHtml使用parse5,遵循浏览器式 HTML tree construction- HTML fragment / document 使用合成根;XML 仍以真实文档根元素为根
- HTML diff 基于解析后的 DOM 树,而不是原始源码 token
- XML 序列化继续使用 XML 语义;HTML 序列化使用 parse5 的 HTML serializer
已知限制
- HTML 支持是结构化层能力,不保留原始源码格式、optional end tag 写法,也不保留节点是否由 parser 自动补全这一词法信息。
- 共享 AST 目前不表示 HTML doctype 节点,因此
parseHtml和serializeHtmlround-trip 时不会保留<!DOCTYPE html>。 serializeHtml输出规范化 HTML,例如 boolean attribute 会被写成disabled=""。formatDiff可以直接格式化 HTML 产生的 diff op,但 markdown 标题仍然是XML Diff,因为它面向的是共享 diff operation 模型。
diffText
直接比较两个文本值。replaceText 操作内部使用的就是同一套文本 diff。
import { diffText } from 'xml-diff-kit';
const textDiff = diffText('Remove the panel.', 'Remove the access panel.');
console.log(textDiff);输出:
{
changes: [{ op: 'insertText', offset: 11, text: 'access ' }],
segments: [
{ type: 'equal', text: 'Remove the ' },
{ type: 'insert', text: 'access ' },
{ type: 'equal', text: 'panel.' }
]
}Diff 操作类型
支持的结构化 XML 变更:
addNoderemoveNodereplaceNodemoveNodereplaceTextaddAttrupdateAttrremoveAttr
文本变更会作为 replaceText 内部的 range 操作表达。
选项
interface XmlDiffOptions {
ignoreWhitespaceText?: boolean;
trimText?: boolean;
ignoreComments?: boolean;
sortAttributes?: boolean;
keyAttrs?: string[];
detectMoves?: boolean;
}interface HtmlDiffOptions extends HtmlNormalizeOptions {
sourceType?: 'document' | 'fragment';
}
interface HtmlNormalizeOptions extends XmlDiffOptions {
preserveWhitespaceElements?: string[];
normalizeBooleanAttributes?: boolean;
}keyAttrs 用于让 diff 引擎通过稳定标识对齐兄弟元素,例如 id、xml:id 或业务自定义 key。
detectMoves 是可选能力。启用后,带 key 的兄弟节点重排会被报告为 moveNode。默认关闭,以保持 patch 行为保守。
路径说明
Diff operations 使用从 XML 根节点开始的绝对路径:
/procedure[0]/step[@id="s1"][0]/text()[0]数字索引是 patchXml 实际用于定位节点的部分。类似 [@id="s1"] 的 key 提示用于提升可读性,并辅助 keyed matching。
开发
npm install
npm run lint
npm run typecheck
npm test
npm run coverage
npm run build发布
npm install
npm run lint
npm run typecheck
npm test
npm run build
npm publish --access publicLicense
MIT
