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 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