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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vndmea
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# xml-diff-kit
|
|
2
|
+
|
|
3
|
+
English | [简体中文](https://github.com/vndmea/xml-diff-kit/blob/master/README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
A TypeScript toolkit for parsing, normalizing, diffing, patching, serializing, and formatting XML document changes.
|
|
6
|
+
|
|
7
|
+
`xml-diff-kit` is designed for applications that need structured, machine-readable XML differences rather than visual line-based diffs. It is suitable for structured editors, review workflows, change tracking, patch application, XML document comparison, and browser-based XML tooling.
|
|
8
|
+
|
|
9
|
+
It works in both Node.js and modern browsers. The package exports ESM and CJS builds, and the public API does not require Node.js-only runtime APIs.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install xml-diff-kit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Most examples below use the same pair of XML documents:
|
|
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
|
+
Compare two XML documents and get structured 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
|
+
Output:
|
|
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
|
+
Apply structured diff operations back to an XML string or a parsed XML node.
|
|
73
|
+
|
|
74
|
+
`patchXml` is useful when you already have an `XmlDiffOp[]` and want to apply it to an XML document. When the input is an XML string, `patchXml` returns a string. When the input is an `XmlNode`, it returns a patched `XmlNode`.
|
|
75
|
+
|
|
76
|
+
#### Add a node
|
|
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
|
+
Output:
|
|
103
|
+
|
|
104
|
+
```xml
|
|
105
|
+
<root><a/><b/></root>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Update an attribute
|
|
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
|
+
Output:
|
|
131
|
+
|
|
132
|
+
```xml
|
|
133
|
+
<root status="released"/>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Replace text
|
|
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
|
+
Output:
|
|
171
|
+
|
|
172
|
+
```xml
|
|
173
|
+
<root>new text</root>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`patchXml` applies operations by path. The numeric indexes in paths are the executable addressing part used to locate nodes. Key hints such as `[@id="s1"]` make paths easier to read and help `diffXml` align nodes, but patching still relies on the numeric indexes.
|
|
177
|
+
|
|
178
|
+
Supported patch operations include adding, removing, replacing, and moving nodes; adding, updating, and removing attributes; and replacing text node values.
|
|
179
|
+
|
|
180
|
+
### `formatDiff`
|
|
181
|
+
|
|
182
|
+
Format structured diff operations as summary objects or a Markdown report.
|
|
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
|
+
Output:
|
|
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 output:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const markdown = formatDiff(ops, { format: 'markdown' });
|
|
223
|
+
|
|
224
|
+
console.log(markdown);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Output:
|
|
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` and `serializeXml`
|
|
266
|
+
|
|
267
|
+
Parse XML into the internal AST, then serialize it back to 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
|
+
Output:
|
|
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
|
+
Normalize an XML AST before diffing or custom processing.
|
|
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
|
+
Output:
|
|
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
|
+
Diff two text values directly. This is the same text diff used inside `replaceText` operations.
|
|
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
|
+
Output:
|
|
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 operations
|
|
351
|
+
|
|
352
|
+
Supported structured XML changes:
|
|
353
|
+
|
|
354
|
+
- `addNode`
|
|
355
|
+
- `removeNode`
|
|
356
|
+
- `replaceNode`
|
|
357
|
+
- `moveNode`
|
|
358
|
+
- `replaceText`
|
|
359
|
+
- `addAttr`
|
|
360
|
+
- `updateAttr`
|
|
361
|
+
- `removeAttr`
|
|
362
|
+
|
|
363
|
+
Text changes are represented as nested range operations inside `replaceText`.
|
|
364
|
+
|
|
365
|
+
## Options
|
|
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` lets the diff engine align sibling elements by stable identifiers, such as `id`, `xml:id`, or domain-specific keys.
|
|
379
|
+
|
|
380
|
+
`detectMoves` is opt-in. When enabled, keyed sibling reorder changes are reported as `moveNode` operations. It is disabled by default to keep patching behavior conservative.
|
|
381
|
+
|
|
382
|
+
## Paths
|
|
383
|
+
|
|
384
|
+
Diff operations use absolute paths from the XML root node:
|
|
385
|
+
|
|
386
|
+
```txt
|
|
387
|
+
/procedure[0]/step[@id="s1"][0]/text()[0]
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
The numeric index is the executable addressing part used by patching. Key hints such as `[@id="s1"]` improve readability and keyed matching.
|
|
391
|
+
|
|
392
|
+
## Development
|
|
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
|
+
## Release
|
|
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
|