xml-diff-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/README.zh-CN.md +416 -0
- package/dist/index.cjs +769 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +267 -0
- package/dist/index.d.ts +267 -0
- package/dist/index.js +736 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# xml-diff-kit
|
|
2
|
+
|
|
3
|
+
[English](https://github.com/vndmea/xml-diff-kit/blob/master/README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
一个 TypeScript XML 差异工具包,用于解析、标准化、比较、补丁应用、序列化和格式化 XML 文档变更。
|
|
6
|
+
|
|
7
|
+
`xml-diff-kit` 面向需要**结构化、机器可读 XML 差异数据**的场景,而不是面向可视化文本对比。它适合结构化编辑器、审阅流程、变更追踪、补丁应用、XML 文档比较,以及浏览器端 XML 工具。
|
|
8
|
+
|
|
9
|
+
它可以在 Node.js 和现代浏览器中使用。包同时提供 ESM 和 CJS 产物,公共 API 不依赖 Node.js 专属运行时能力。
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install xml-diff-kit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 使用方式
|
|
18
|
+
|
|
19
|
+
下面大部分示例共用同一组 XML:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const oldXml = '<procedure><step id="s1">Remove the panel.</step></procedure>';
|
|
23
|
+
const newXml = '<procedure><step id="s1">Remove the access panel.</step><step id="s2">Inspect.</step></procedure>';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### `diffXml`
|
|
27
|
+
|
|
28
|
+
比较两个 XML 文档,输出结构化 diff operations。
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { diffXml } from 'xml-diff-kit';
|
|
32
|
+
|
|
33
|
+
const ops = diffXml(oldXml, newXml, {
|
|
34
|
+
keyAttrs: ['id'],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log(ops);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
输出:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
op: 'replaceText',
|
|
46
|
+
path: '/procedure[0]/step[@id="s1"][0]/text()[0]',
|
|
47
|
+
oldValue: 'Remove the panel.',
|
|
48
|
+
newValue: 'Remove the access panel.',
|
|
49
|
+
changes: [{ op: 'insertText', offset: 11, text: 'access ' }],
|
|
50
|
+
segments: [
|
|
51
|
+
{ type: 'equal', text: 'Remove the ' },
|
|
52
|
+
{ type: 'insert', text: 'access ' },
|
|
53
|
+
{ type: 'equal', text: 'panel.' }
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
op: 'addNode',
|
|
58
|
+
path: '/procedure[0]/step[@id="s2"][1]',
|
|
59
|
+
value: {
|
|
60
|
+
type: 'element',
|
|
61
|
+
name: 'step',
|
|
62
|
+
namespaceURI: null,
|
|
63
|
+
attrs: { id: 's2' },
|
|
64
|
+
children: [{ type: 'text', text: 'Inspect.' }]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `patchXml`
|
|
71
|
+
|
|
72
|
+
把结构化 diff operations 应用回 XML 字符串或已解析的 XML 节点。
|
|
73
|
+
|
|
74
|
+
当你已经有一组 `XmlDiffOp[]`,并希望把它们应用到 XML 文档上时,可以使用 `patchXml`。当输入是 XML 字符串时,`patchXml` 返回字符串。当输入是 `XmlNode` 时,它返回补丁后的 `XmlNode`。
|
|
75
|
+
|
|
76
|
+
#### 新增节点
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { patchXml, type XmlDiffOp } from 'xml-diff-kit';
|
|
80
|
+
|
|
81
|
+
const xml = '<root><a/></root>';
|
|
82
|
+
|
|
83
|
+
const ops: XmlDiffOp[] = [
|
|
84
|
+
{
|
|
85
|
+
op: 'addNode',
|
|
86
|
+
path: '/root[0]/b[1]',
|
|
87
|
+
value: {
|
|
88
|
+
type: 'element',
|
|
89
|
+
name: 'b',
|
|
90
|
+
namespaceURI: null,
|
|
91
|
+
attrs: {},
|
|
92
|
+
children: []
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const patchedXml = patchXml(xml, ops);
|
|
98
|
+
|
|
99
|
+
console.log(patchedXml);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
输出:
|
|
103
|
+
|
|
104
|
+
```xml
|
|
105
|
+
<root><a/><b/></root>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### 更新属性
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { patchXml, type XmlDiffOp } from 'xml-diff-kit';
|
|
112
|
+
|
|
113
|
+
const xml = '<root status="draft"/>';
|
|
114
|
+
|
|
115
|
+
const ops: XmlDiffOp[] = [
|
|
116
|
+
{
|
|
117
|
+
op: 'updateAttr',
|
|
118
|
+
path: '/root[0]',
|
|
119
|
+
name: 'status',
|
|
120
|
+
oldValue: 'draft',
|
|
121
|
+
newValue: 'released'
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const patchedXml = patchXml(xml, ops);
|
|
126
|
+
|
|
127
|
+
console.log(patchedXml);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
输出:
|
|
131
|
+
|
|
132
|
+
```xml
|
|
133
|
+
<root status="released"/>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### 替换文本
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { patchXml, type XmlDiffOp } from 'xml-diff-kit';
|
|
140
|
+
|
|
141
|
+
const xml = '<root>old text</root>';
|
|
142
|
+
|
|
143
|
+
const ops: XmlDiffOp[] = [
|
|
144
|
+
{
|
|
145
|
+
op: 'replaceText',
|
|
146
|
+
path: '/root[0]/text()[0]',
|
|
147
|
+
oldValue: 'old text',
|
|
148
|
+
newValue: 'new text',
|
|
149
|
+
changes: [
|
|
150
|
+
{
|
|
151
|
+
op: 'replaceTextRange',
|
|
152
|
+
offset: 0,
|
|
153
|
+
oldText: 'old',
|
|
154
|
+
newText: 'new'
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
segments: [
|
|
158
|
+
{ type: 'delete', text: 'old' },
|
|
159
|
+
{ type: 'insert', text: 'new' },
|
|
160
|
+
{ type: 'equal', text: ' text' }
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const patchedXml = patchXml(xml, ops);
|
|
166
|
+
|
|
167
|
+
console.log(patchedXml);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
输出:
|
|
171
|
+
|
|
172
|
+
```xml
|
|
173
|
+
<root>new text</root>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`patchXml` 根据路径应用变更。路径中的数字索引是实际用于定位节点的部分。类似 `[@id="s1"]` 的 key 提示用于提升可读性,并帮助 `diffXml` 对齐节点,但 patch 时仍然依赖数字索引。
|
|
177
|
+
|
|
178
|
+
支持的 patch 操作包括新增、删除、替换、移动节点;新增、更新、删除属性;以及替换文本节点内容。
|
|
179
|
+
|
|
180
|
+
### `formatDiff`
|
|
181
|
+
|
|
182
|
+
把结构化 diff operations 格式化为摘要对象,或者 Markdown 报告。
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { diffXml, formatDiff } from 'xml-diff-kit';
|
|
186
|
+
|
|
187
|
+
const ops = diffXml(oldXml, newXml, { keyAttrs: ['id'] });
|
|
188
|
+
const summary = formatDiff(ops);
|
|
189
|
+
|
|
190
|
+
console.log(summary);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
输出:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
[
|
|
197
|
+
{
|
|
198
|
+
type: 'textChanged',
|
|
199
|
+
path: '/procedure[0]/step[@id="s1"][0]/text()[0]',
|
|
200
|
+
message: 'Changed text at /procedure[0]/step[@id="s1"][0]/text()[0]',
|
|
201
|
+
before: 'Remove the panel.',
|
|
202
|
+
after: 'Remove the access panel.'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
type: 'nodeAdded',
|
|
206
|
+
path: '/procedure[0]/step[@id="s2"][1]',
|
|
207
|
+
message: 'Added node at /procedure[0]/step[@id="s2"][1]',
|
|
208
|
+
after: {
|
|
209
|
+
type: 'element',
|
|
210
|
+
name: 'step',
|
|
211
|
+
namespaceURI: null,
|
|
212
|
+
attrs: { id: 's2' },
|
|
213
|
+
children: [{ type: 'text', text: 'Inspect.' }]
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Markdown 输出:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const markdown = formatDiff(ops, { format: 'markdown' });
|
|
223
|
+
|
|
224
|
+
console.log(markdown);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
输出:
|
|
228
|
+
|
|
229
|
+
````md
|
|
230
|
+
# XML Diff
|
|
231
|
+
|
|
232
|
+
Total changes: 2
|
|
233
|
+
|
|
234
|
+
## 1. Changed text
|
|
235
|
+
|
|
236
|
+
- Path: `/procedure[0]/step[@id="s1"][0]/text()[0]`
|
|
237
|
+
|
|
238
|
+
**Before**
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
Remove the panel.
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**After**
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
Remove the access panel.
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Text segments**
|
|
251
|
+
|
|
252
|
+
- equal: `Remove the `
|
|
253
|
+
- insert: `access `
|
|
254
|
+
- equal: `panel.`
|
|
255
|
+
|
|
256
|
+
## 2. Added node
|
|
257
|
+
|
|
258
|
+
- Path: `/procedure[0]/step[@id="s2"][1]`
|
|
259
|
+
|
|
260
|
+
```xml
|
|
261
|
+
<step id="s2">Inspect.</step>
|
|
262
|
+
```
|
|
263
|
+
````
|
|
264
|
+
|
|
265
|
+
### `parseXml` 和 `serializeXml`
|
|
266
|
+
|
|
267
|
+
把 XML 解析为内部 AST,再序列化回 XML 字符串。
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { parseXml, serializeXml } from 'xml-diff-kit';
|
|
271
|
+
|
|
272
|
+
const doc = parseXml('<root><item id="1">Hello</item><item id="2">World</item></root>');
|
|
273
|
+
const xml = serializeXml(doc, { pretty: true });
|
|
274
|
+
|
|
275
|
+
console.log(xml);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
输出:
|
|
279
|
+
|
|
280
|
+
```xml
|
|
281
|
+
<root>
|
|
282
|
+
<item id="1">Hello</item>
|
|
283
|
+
<item id="2">World</item>
|
|
284
|
+
</root>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `normalizeXml`
|
|
288
|
+
|
|
289
|
+
在 diff 或自定义处理前,标准化 XML AST。
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { normalizeXml, parseXml } from 'xml-diff-kit';
|
|
293
|
+
|
|
294
|
+
const doc = parseXml('<root b="2" a="1"> <item> value </item> </root>');
|
|
295
|
+
|
|
296
|
+
const normalized = normalizeXml(doc, {
|
|
297
|
+
ignoreWhitespaceText: true,
|
|
298
|
+
trimText: true,
|
|
299
|
+
sortAttributes: true,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
console.log(normalized);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
输出:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
{
|
|
309
|
+
type: 'element',
|
|
310
|
+
name: 'root',
|
|
311
|
+
namespaceURI: null,
|
|
312
|
+
attrs: { a: '1', b: '2' },
|
|
313
|
+
children: [
|
|
314
|
+
{
|
|
315
|
+
type: 'element',
|
|
316
|
+
name: 'item',
|
|
317
|
+
namespaceURI: null,
|
|
318
|
+
attrs: {},
|
|
319
|
+
children: [{ type: 'text', text: 'value' }]
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### `diffText`
|
|
326
|
+
|
|
327
|
+
直接比较两个文本值。`replaceText` 操作内部使用的就是同一套文本 diff。
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { diffText } from 'xml-diff-kit';
|
|
331
|
+
|
|
332
|
+
const textDiff = diffText('Remove the panel.', 'Remove the access panel.');
|
|
333
|
+
|
|
334
|
+
console.log(textDiff);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
输出:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
{
|
|
341
|
+
changes: [{ op: 'insertText', offset: 11, text: 'access ' }],
|
|
342
|
+
segments: [
|
|
343
|
+
{ type: 'equal', text: 'Remove the ' },
|
|
344
|
+
{ type: 'insert', text: 'access ' },
|
|
345
|
+
{ type: 'equal', text: 'panel.' }
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Diff 操作类型
|
|
351
|
+
|
|
352
|
+
支持的结构化 XML 变更:
|
|
353
|
+
|
|
354
|
+
- `addNode`
|
|
355
|
+
- `removeNode`
|
|
356
|
+
- `replaceNode`
|
|
357
|
+
- `moveNode`
|
|
358
|
+
- `replaceText`
|
|
359
|
+
- `addAttr`
|
|
360
|
+
- `updateAttr`
|
|
361
|
+
- `removeAttr`
|
|
362
|
+
|
|
363
|
+
文本变更会作为 `replaceText` 内部的 range 操作表达。
|
|
364
|
+
|
|
365
|
+
## 选项
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
interface XmlDiffOptions {
|
|
369
|
+
ignoreWhitespaceText?: boolean;
|
|
370
|
+
trimText?: boolean;
|
|
371
|
+
ignoreComments?: boolean;
|
|
372
|
+
sortAttributes?: boolean;
|
|
373
|
+
keyAttrs?: string[];
|
|
374
|
+
detectMoves?: boolean;
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`keyAttrs` 用于让 diff 引擎通过稳定标识对齐兄弟元素,例如 `id`、`xml:id` 或业务自定义 key。
|
|
379
|
+
|
|
380
|
+
`detectMoves` 是可选能力。启用后,带 key 的兄弟节点重排会被报告为 `moveNode`。默认关闭,以保持 patch 行为保守。
|
|
381
|
+
|
|
382
|
+
## 路径说明
|
|
383
|
+
|
|
384
|
+
Diff operations 使用从 XML 根节点开始的绝对路径:
|
|
385
|
+
|
|
386
|
+
```txt
|
|
387
|
+
/procedure[0]/step[@id="s1"][0]/text()[0]
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
数字索引是 `patchXml` 实际用于定位节点的部分。类似 `[@id="s1"]` 的 key 提示用于提升可读性,并辅助 keyed matching。
|
|
391
|
+
|
|
392
|
+
## 开发
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
npm install
|
|
396
|
+
npm run lint
|
|
397
|
+
npm run typecheck
|
|
398
|
+
npm test
|
|
399
|
+
npm run coverage
|
|
400
|
+
npm run build
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## 发布
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
npm install
|
|
407
|
+
npm run lint
|
|
408
|
+
npm run typecheck
|
|
409
|
+
npm test
|
|
410
|
+
npm run build
|
|
411
|
+
npm publish --access public
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## License
|
|
415
|
+
|
|
416
|
+
MIT
|