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/dist/index.js ADDED
@@ -0,0 +1,736 @@
1
+ // src/utils.ts
2
+ function isElementNode(node) {
3
+ return node.type === "element";
4
+ }
5
+ function cloneXmlNode(node) {
6
+ return structuredClone(node);
7
+ }
8
+ function sortRecord(record) {
9
+ return Object.fromEntries(
10
+ Object.entries(record).sort(([left], [right]) => left.localeCompare(right))
11
+ );
12
+ }
13
+ function escapeXmlText(value) {
14
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
15
+ }
16
+ function escapeXmlAttr(value) {
17
+ return escapeXmlText(value).replaceAll('"', "&quot;");
18
+ }
19
+ function getNodeKey(node, keyAttrs = []) {
20
+ if (!isElementNode(node)) return void 0;
21
+ for (const keyAttr of keyAttrs) {
22
+ const value = node.attrs[keyAttr];
23
+ if (value !== void 0) {
24
+ return `${node.name}:${keyAttr}=${value}`;
25
+ }
26
+ }
27
+ return void 0;
28
+ }
29
+ function formatPathSegment(node, index, keyAttrs = []) {
30
+ if (node.type === "text") return `text()[${index}]`;
31
+ if (node.type === "comment") return `comment()[${index}]`;
32
+ const keyAttr = keyAttrs.find((attr) => node.attrs[attr] !== void 0);
33
+ const keyValue = keyAttr ? node.attrs[keyAttr] : void 0;
34
+ const keyPart = keyAttr && keyValue !== void 0 ? `[@${keyAttr}="${keyValue}"]` : "";
35
+ return `${node.name}${keyPart}[${index}]`;
36
+ }
37
+
38
+ // src/normalize.ts
39
+ function normalizeXml(node, options = {}) {
40
+ const cloned = cloneXmlNode(node);
41
+ const normalized = normalizeNode(cloned, options);
42
+ if (!normalized) {
43
+ throw new Error("Cannot normalize an empty XML document.");
44
+ }
45
+ return normalized;
46
+ }
47
+ function normalizeNode(node, options) {
48
+ if (node.type === "text") {
49
+ const text = options.trimText ? node.text.trim() : node.text;
50
+ if (options.ignoreWhitespaceText && text.trim() === "") {
51
+ return null;
52
+ }
53
+ return {
54
+ type: "text",
55
+ text
56
+ };
57
+ }
58
+ if (node.type === "comment") {
59
+ if (options.ignoreComments) return null;
60
+ return node;
61
+ }
62
+ const children = node.children.map((child) => normalizeNode(child, options)).filter((child) => Boolean(child));
63
+ return {
64
+ ...node,
65
+ attrs: options.sortAttributes ? sortRecord(node.attrs) : { ...node.attrs },
66
+ children
67
+ };
68
+ }
69
+
70
+ // src/parse.ts
71
+ import { DOMParser } from "@xmldom/xmldom";
72
+ function parseXml(xml) {
73
+ const doc = new DOMParser({
74
+ errorHandler: {
75
+ warning: (message) => {
76
+ throw new Error(`Invalid XML: ${message}`);
77
+ },
78
+ error: (message) => {
79
+ throw new Error(`Invalid XML: ${message}`);
80
+ },
81
+ fatalError: (message) => {
82
+ throw new Error(`Invalid XML: ${message}`);
83
+ }
84
+ }
85
+ }).parseFromString(xml, "application/xml");
86
+ if (!doc.documentElement) {
87
+ throw new Error("Invalid XML: missing document element.");
88
+ }
89
+ return fromDomNode(doc.documentElement);
90
+ }
91
+ function fromDomNode(node) {
92
+ if (node.nodeType === 1) {
93
+ const element = node;
94
+ const attrs = {};
95
+ for (let index = 0; index < element.attributes.length; index += 1) {
96
+ const attr = element.attributes.item(index);
97
+ if (attr) attrs[attr.name] = attr.value;
98
+ }
99
+ const children = [];
100
+ for (let index = 0; index < element.childNodes.length; index += 1) {
101
+ const child = element.childNodes.item(index);
102
+ if (child.nodeType === 1 || child.nodeType === 3 || child.nodeType === 8) {
103
+ children.push(fromDomNode(child));
104
+ }
105
+ }
106
+ return {
107
+ type: "element",
108
+ name: element.nodeName,
109
+ namespaceURI: element.namespaceURI ?? null,
110
+ attrs,
111
+ children
112
+ };
113
+ }
114
+ if (node.nodeType === 3) {
115
+ return {
116
+ type: "text",
117
+ text: node.nodeValue ?? ""
118
+ };
119
+ }
120
+ if (node.nodeType === 8) {
121
+ return {
122
+ type: "comment",
123
+ text: node.nodeValue ?? ""
124
+ };
125
+ }
126
+ throw new Error(`Unsupported XML node type: ${node.nodeType}`);
127
+ }
128
+
129
+ // src/text-diff.ts
130
+ function diffText(oldValue, newValue) {
131
+ if (oldValue === newValue) {
132
+ return {
133
+ changes: [],
134
+ segments: [{ type: "equal", text: oldValue }]
135
+ };
136
+ }
137
+ const oldChars = Array.from(oldValue);
138
+ const newChars = Array.from(newValue);
139
+ const segments = buildDiffSegments(oldChars, newChars);
140
+ return {
141
+ changes: segmentsToChanges(segments),
142
+ segments
143
+ };
144
+ }
145
+ function buildDiffSegments(oldChars, newChars) {
146
+ const rows = oldChars.length + 1;
147
+ const cols = newChars.length + 1;
148
+ const dp = Array.from({ length: rows }, () => Array.from({ length: cols }, () => 0));
149
+ for (let oldIndex2 = oldChars.length - 1; oldIndex2 >= 0; oldIndex2 -= 1) {
150
+ for (let newIndex2 = newChars.length - 1; newIndex2 >= 0; newIndex2 -= 1) {
151
+ if (oldChars[oldIndex2] === newChars[newIndex2]) {
152
+ dp[oldIndex2][newIndex2] = dp[oldIndex2 + 1][newIndex2 + 1] + 1;
153
+ } else {
154
+ dp[oldIndex2][newIndex2] = Math.max(dp[oldIndex2 + 1][newIndex2], dp[oldIndex2][newIndex2 + 1]);
155
+ }
156
+ }
157
+ }
158
+ const segments = [];
159
+ let oldIndex = 0;
160
+ let newIndex = 0;
161
+ while (oldIndex < oldChars.length && newIndex < newChars.length) {
162
+ if (oldChars[oldIndex] === newChars[newIndex]) {
163
+ pushSegment(segments, { type: "equal", text: oldChars[oldIndex] });
164
+ oldIndex += 1;
165
+ newIndex += 1;
166
+ } else if (dp[oldIndex + 1][newIndex] >= dp[oldIndex][newIndex + 1]) {
167
+ pushSegment(segments, { type: "delete", text: oldChars[oldIndex] });
168
+ oldIndex += 1;
169
+ } else {
170
+ pushSegment(segments, { type: "insert", text: newChars[newIndex] });
171
+ newIndex += 1;
172
+ }
173
+ }
174
+ while (oldIndex < oldChars.length) {
175
+ pushSegment(segments, { type: "delete", text: oldChars[oldIndex] });
176
+ oldIndex += 1;
177
+ }
178
+ while (newIndex < newChars.length) {
179
+ pushSegment(segments, { type: "insert", text: newChars[newIndex] });
180
+ newIndex += 1;
181
+ }
182
+ return segments;
183
+ }
184
+ function segmentsToChanges(segments) {
185
+ const changes = [];
186
+ let offset = 0;
187
+ let index = 0;
188
+ while (index < segments.length) {
189
+ const segment = segments[index];
190
+ if (segment.type === "equal") {
191
+ offset += Array.from(segment.text).length;
192
+ index += 1;
193
+ continue;
194
+ }
195
+ if (segment.type === "delete") {
196
+ const next = segments[index + 1];
197
+ if (next?.type === "insert") {
198
+ changes.push({
199
+ op: "replaceTextRange",
200
+ offset,
201
+ oldText: segment.text,
202
+ newText: next.text
203
+ });
204
+ offset += Array.from(segment.text).length;
205
+ index += 2;
206
+ continue;
207
+ }
208
+ changes.push({
209
+ op: "deleteText",
210
+ offset,
211
+ oldText: segment.text
212
+ });
213
+ offset += Array.from(segment.text).length;
214
+ index += 1;
215
+ continue;
216
+ }
217
+ changes.push({
218
+ op: "insertText",
219
+ offset,
220
+ text: segment.text
221
+ });
222
+ index += 1;
223
+ }
224
+ return changes;
225
+ }
226
+ function pushSegment(segments, next) {
227
+ const previous = segments.at(-1);
228
+ if (previous?.type === next.type) {
229
+ previous.text += next.text;
230
+ return;
231
+ }
232
+ segments.push({ ...next });
233
+ }
234
+
235
+ // src/diff.ts
236
+ function diffXml(oldInput, newInput, options = {}) {
237
+ const oldNode = normalizeXml(typeof oldInput === "string" ? parseXml(oldInput) : oldInput, options);
238
+ const newNode = normalizeXml(typeof newInput === "string" ? parseXml(newInput) : newInput, options);
239
+ const ops = [];
240
+ diffNode(oldNode, newNode, `/${formatPathSegment(oldNode, 0, options.keyAttrs)}`, options, ops);
241
+ return ops;
242
+ }
243
+ function diffNode(oldNode, newNode, path, options, ops) {
244
+ if (oldNode.type !== newNode.type) {
245
+ ops.push({ op: "replaceNode", path, oldValue: oldNode, newValue: newNode });
246
+ return;
247
+ }
248
+ if (oldNode.type === "text" && newNode.type === "text") {
249
+ if (oldNode.text !== newNode.text) {
250
+ const textDiff = diffText(oldNode.text, newNode.text);
251
+ ops.push({
252
+ op: "replaceText",
253
+ path,
254
+ oldValue: oldNode.text,
255
+ newValue: newNode.text,
256
+ changes: textDiff.changes,
257
+ segments: textDiff.segments
258
+ });
259
+ }
260
+ return;
261
+ }
262
+ if (oldNode.type === "comment" && newNode.type === "comment") {
263
+ if (oldNode.text !== newNode.text) {
264
+ ops.push({ op: "replaceNode", path, oldValue: oldNode, newValue: newNode });
265
+ }
266
+ return;
267
+ }
268
+ if (oldNode.type !== "element" || newNode.type !== "element") return;
269
+ if (oldNode.name !== newNode.name || oldNode.namespaceURI !== newNode.namespaceURI) {
270
+ ops.push({ op: "replaceNode", path, oldValue: oldNode, newValue: newNode });
271
+ return;
272
+ }
273
+ diffAttributes(oldNode.attrs, newNode.attrs, path, ops);
274
+ diffChildren(oldNode.children, newNode.children, path, options, ops);
275
+ }
276
+ function diffAttributes(oldAttrs, newAttrs, path, ops) {
277
+ for (const [name, oldValue] of Object.entries(oldAttrs)) {
278
+ const newValue = newAttrs[name];
279
+ if (newValue === void 0) {
280
+ ops.push({ op: "removeAttr", path, name, oldValue });
281
+ } else if (newValue !== oldValue) {
282
+ ops.push({ op: "updateAttr", path, name, oldValue, newValue });
283
+ }
284
+ }
285
+ for (const [name, value] of Object.entries(newAttrs)) {
286
+ if (oldAttrs[name] === void 0) {
287
+ ops.push({ op: "addAttr", path, name, value });
288
+ }
289
+ }
290
+ }
291
+ function diffChildren(oldChildren, newChildren, parentPath, options, ops) {
292
+ const keyAttrs = options.keyAttrs ?? [];
293
+ if (keyAttrs.length > 0) {
294
+ const oldKeyed = /* @__PURE__ */ new Map();
295
+ const newKeyed = /* @__PURE__ */ new Map();
296
+ oldChildren.forEach((node, index) => {
297
+ const key = getNodeKey(node, keyAttrs);
298
+ if (key) oldKeyed.set(key, { node, index });
299
+ });
300
+ newChildren.forEach((node, index) => {
301
+ const key = getNodeKey(node, keyAttrs);
302
+ if (key) newKeyed.set(key, { node, index });
303
+ });
304
+ if (oldKeyed.size > 0 || newKeyed.size > 0) {
305
+ for (const [key, { node, index }] of oldKeyed) {
306
+ const next = newKeyed.get(key);
307
+ const childPath = `${parentPath}/${formatPathSegment(node, index, keyAttrs)}`;
308
+ if (!next) {
309
+ ops.push({ op: "removeNode", path: childPath, oldValue: node });
310
+ } else {
311
+ const newPath = `${parentPath}/${formatPathSegment(next.node, next.index, keyAttrs)}`;
312
+ if (options.detectMoves === true && index !== next.index) {
313
+ ops.push({
314
+ op: "moveNode",
315
+ path: childPath,
316
+ fromPath: childPath,
317
+ toPath: newPath,
318
+ value: next.node
319
+ });
320
+ }
321
+ diffNode(node, next.node, childPath, options, ops);
322
+ }
323
+ }
324
+ for (const [, { node, index }] of newKeyed) {
325
+ const key = getNodeKey(node, keyAttrs);
326
+ if (key && !oldKeyed.has(key)) {
327
+ const childPath = `${parentPath}/${formatPathSegment(node, index, keyAttrs)}`;
328
+ ops.push({ op: "addNode", path: childPath, value: node });
329
+ }
330
+ }
331
+ const oldUnkeyed = oldChildren.map((node, index) => ({ node, index })).filter(({ node }) => !getNodeKey(node, keyAttrs));
332
+ const newUnkeyed = newChildren.map((node, index) => ({ node, index })).filter(({ node }) => !getNodeKey(node, keyAttrs));
333
+ diffChildrenByIndex(oldUnkeyed, newUnkeyed, parentPath, options, ops);
334
+ return;
335
+ }
336
+ }
337
+ diffChildrenByIndex(
338
+ oldChildren.map((node, index) => ({ node, index })),
339
+ newChildren.map((node, index) => ({ node, index })),
340
+ parentPath,
341
+ options,
342
+ ops
343
+ );
344
+ }
345
+ function diffChildrenByIndex(oldChildren, newChildren, parentPath, options, ops) {
346
+ const max = Math.max(oldChildren.length, newChildren.length);
347
+ for (let index = 0; index < max; index += 1) {
348
+ const oldItem = oldChildren[index];
349
+ const newItem = newChildren[index];
350
+ if (!oldItem && newItem) {
351
+ const childPath = `${parentPath}/${formatPathSegment(newItem.node, newItem.index, options.keyAttrs)}`;
352
+ ops.push({ op: "addNode", path: childPath, value: newItem.node });
353
+ continue;
354
+ }
355
+ if (oldItem && !newItem) {
356
+ const childPath = `${parentPath}/${formatPathSegment(oldItem.node, oldItem.index, options.keyAttrs)}`;
357
+ ops.push({ op: "removeNode", path: childPath, oldValue: oldItem.node });
358
+ continue;
359
+ }
360
+ if (oldItem && newItem) {
361
+ const childPath = `${parentPath}/${formatPathSegment(oldItem.node, oldItem.index, options.keyAttrs)}`;
362
+ diffNode(oldItem.node, newItem.node, childPath, options, ops);
363
+ }
364
+ }
365
+ }
366
+
367
+ // src/serialize.ts
368
+ function serializeXml(node, options = {}) {
369
+ return serializeNode(node, options, 0);
370
+ }
371
+ function serializeNode(node, options, level) {
372
+ if (node.type === "text") {
373
+ return escapeXmlText(node.text);
374
+ }
375
+ if (node.type === "comment") {
376
+ return `<!--${node.text}-->`;
377
+ }
378
+ const attrs = Object.entries(node.attrs).map(([name, value]) => ` ${name}="${escapeXmlAttr(value)}"`).join("");
379
+ if (node.children.length === 0) {
380
+ return `${indent(options, level)}<${node.name}${attrs}/>`;
381
+ }
382
+ if (!options.pretty || node.children.every((child) => child.type === "text")) {
383
+ const children2 = node.children.map((child) => serializeNode(child, options, level + 1)).join("");
384
+ return `${indent(options, level)}<${node.name}${attrs}>${children2}</${node.name}>`;
385
+ }
386
+ const children = node.children.map((child) => serializeNode(child, options, level + 1)).join("\n");
387
+ return `${indent(options, level)}<${node.name}${attrs}>
388
+ ${children}
389
+ ${indent(
390
+ options,
391
+ level
392
+ )}</${node.name}>`;
393
+ }
394
+ function indent(options, level) {
395
+ if (!options.pretty) return "";
396
+ return (options.indent ?? " ").repeat(level);
397
+ }
398
+
399
+ // src/format.ts
400
+ function formatDiff(ops, options = {}) {
401
+ if (options.format === "markdown") {
402
+ return toMarkdown(ops);
403
+ }
404
+ return ops.map(toSummaryItem);
405
+ }
406
+ function toMarkdown(ops) {
407
+ if (ops.length === 0) {
408
+ return "# XML Diff\n\nNo XML differences.";
409
+ }
410
+ return [
411
+ "# XML Diff",
412
+ "",
413
+ `Total changes: ${ops.length}`,
414
+ "",
415
+ ...ops.flatMap((op, index) => formatMarkdownOp(op, index + 1))
416
+ ].join("\n");
417
+ }
418
+ function formatMarkdownOp(op, index) {
419
+ switch (op.op) {
420
+ case "addNode":
421
+ return [
422
+ `## ${index}. Added node`,
423
+ "",
424
+ `- Path: \`${op.path}\``,
425
+ "",
426
+ codeBlock("xml", serializeForMarkdown(op.value)),
427
+ ""
428
+ ];
429
+ case "removeNode":
430
+ return [
431
+ `## ${index}. Removed node`,
432
+ "",
433
+ `- Path: \`${op.path}\``,
434
+ "",
435
+ codeBlock("xml", serializeForMarkdown(op.oldValue)),
436
+ ""
437
+ ];
438
+ case "replaceNode":
439
+ return [
440
+ `## ${index}. Replaced node`,
441
+ "",
442
+ `- Path: \`${op.path}\``,
443
+ "",
444
+ "**Before**",
445
+ "",
446
+ codeBlock("xml", serializeForMarkdown(op.oldValue)),
447
+ "",
448
+ "**After**",
449
+ "",
450
+ codeBlock("xml", serializeForMarkdown(op.newValue)),
451
+ ""
452
+ ];
453
+ case "moveNode":
454
+ return [
455
+ `## ${index}. Moved node`,
456
+ "",
457
+ `- From: \`${op.fromPath}\``,
458
+ `- To: \`${op.toPath}\``,
459
+ "",
460
+ codeBlock("xml", serializeForMarkdown(op.value)),
461
+ ""
462
+ ];
463
+ case "replaceText":
464
+ return [
465
+ `## ${index}. Changed text`,
466
+ "",
467
+ `- Path: \`${op.path}\``,
468
+ "",
469
+ "**Before**",
470
+ "",
471
+ codeBlock("text", op.oldValue),
472
+ "",
473
+ "**After**",
474
+ "",
475
+ codeBlock("text", op.newValue),
476
+ "",
477
+ "**Text segments**",
478
+ "",
479
+ ...op.segments.map((segment) => `- ${segment.type}: \`${escapeInlineCode(segment.text)}\``),
480
+ ""
481
+ ];
482
+ case "addAttr":
483
+ return [
484
+ `## ${index}. Added attribute`,
485
+ "",
486
+ `- Path: \`${op.path}\``,
487
+ `- Name: \`${op.name}\``,
488
+ `- Value: \`${escapeInlineCode(op.value)}\``,
489
+ ""
490
+ ];
491
+ case "updateAttr":
492
+ return [
493
+ `## ${index}. Updated attribute`,
494
+ "",
495
+ `- Path: \`${op.path}\``,
496
+ `- Name: \`${op.name}\``,
497
+ `- Before: \`${escapeInlineCode(op.oldValue)}\``,
498
+ `- After: \`${escapeInlineCode(op.newValue)}\``,
499
+ ""
500
+ ];
501
+ case "removeAttr":
502
+ return [
503
+ `## ${index}. Removed attribute`,
504
+ "",
505
+ `- Path: \`${op.path}\``,
506
+ `- Name: \`${op.name}\``,
507
+ `- Old value: \`${escapeInlineCode(op.oldValue)}\``,
508
+ ""
509
+ ];
510
+ }
511
+ }
512
+ function toSummaryItem(op) {
513
+ switch (op.op) {
514
+ case "addNode":
515
+ return {
516
+ type: "nodeAdded",
517
+ path: op.path,
518
+ message: `Added node at ${op.path}`,
519
+ after: op.value
520
+ };
521
+ case "removeNode":
522
+ return {
523
+ type: "nodeRemoved",
524
+ path: op.path,
525
+ message: `Removed node at ${op.path}`,
526
+ before: op.oldValue
527
+ };
528
+ case "replaceNode":
529
+ return {
530
+ type: "nodeReplaced",
531
+ path: op.path,
532
+ message: `Replaced node at ${op.path}`,
533
+ before: op.oldValue,
534
+ after: op.newValue
535
+ };
536
+ case "moveNode":
537
+ return {
538
+ type: "nodeMoved",
539
+ path: op.path,
540
+ message: `Moved node from ${op.fromPath} to ${op.toPath}`,
541
+ before: op.fromPath,
542
+ after: op.toPath
543
+ };
544
+ case "replaceText":
545
+ return {
546
+ type: "textChanged",
547
+ path: op.path,
548
+ message: `Changed text at ${op.path}`,
549
+ before: op.oldValue,
550
+ after: op.newValue
551
+ };
552
+ case "addAttr":
553
+ return {
554
+ type: "attrAdded",
555
+ path: op.path,
556
+ message: `Added attribute ${op.name} at ${op.path}`,
557
+ after: op.value
558
+ };
559
+ case "updateAttr":
560
+ return {
561
+ type: "attrUpdated",
562
+ path: op.path,
563
+ message: `Updated attribute ${op.name} at ${op.path}`,
564
+ before: op.oldValue,
565
+ after: op.newValue
566
+ };
567
+ case "removeAttr":
568
+ return {
569
+ type: "attrRemoved",
570
+ path: op.path,
571
+ message: `Removed attribute ${op.name} at ${op.path}`,
572
+ before: op.oldValue
573
+ };
574
+ }
575
+ }
576
+ function serializeForMarkdown(node) {
577
+ if (node.type === "text" || node.type === "comment") {
578
+ return serializeXml(node);
579
+ }
580
+ return serializeXml(node, {
581
+ pretty: true
582
+ });
583
+ }
584
+ function codeBlock(language, value) {
585
+ return `\`\`\`${language}
586
+ ${escapeCodeFence(value)}
587
+ \`\`\``;
588
+ }
589
+ function escapeCodeFence(value) {
590
+ return value.replaceAll("```", "`\u200B``");
591
+ }
592
+ function escapeInlineCode(value) {
593
+ return value.replaceAll("`", "\\`");
594
+ }
595
+
596
+ // src/patch.ts
597
+ function patchXml(input, ops, options = {}) {
598
+ const root = cloneXmlNode(typeof input === "string" ? parseXml(input) : input);
599
+ for (const op of ops) {
600
+ applyOp(root, op);
601
+ }
602
+ const normalized = normalizeXml(root, { sortAttributes: true });
603
+ if (typeof input === "string") {
604
+ return serializeXml(normalized, options);
605
+ }
606
+ return normalized;
607
+ }
608
+ function applyOp(root, op) {
609
+ if (op.op === "replaceNode" && isRootPath(op.path)) {
610
+ Object.assign(root, cloneXmlNode(op.newValue));
611
+ return;
612
+ }
613
+ if (op.op === "addNode") {
614
+ const parentPath = getParentPath(op.path);
615
+ const parent = getTarget(root, parentPath).node;
616
+ assertElement(parent, parentPath);
617
+ const insertIndex = getLastIndex(op.path);
618
+ parent.children.splice(insertIndex, 0, cloneXmlNode(op.value));
619
+ return;
620
+ }
621
+ if (op.op === "moveNode") {
622
+ moveNode(root, op.fromPath, op.toPath);
623
+ return;
624
+ }
625
+ const target = getTarget(root, op.path);
626
+ switch (op.op) {
627
+ case "addAttr":
628
+ assertElement(target.node, op.path);
629
+ target.node.attrs[op.name] = op.value;
630
+ break;
631
+ case "updateAttr":
632
+ assertElement(target.node, op.path);
633
+ target.node.attrs[op.name] = op.newValue;
634
+ break;
635
+ case "removeAttr":
636
+ assertElement(target.node, op.path);
637
+ delete target.node.attrs[op.name];
638
+ break;
639
+ case "replaceText":
640
+ if (target.node.type !== "text") {
641
+ throw new Error(`Cannot replace text at non-text path: ${op.path}`);
642
+ }
643
+ target.node.text = op.newValue;
644
+ break;
645
+ case "replaceNode":
646
+ if (!target.parent) throw new Error("Cannot replace root node without root replace path.");
647
+ target.parent.children[target.index] = cloneXmlNode(op.newValue);
648
+ break;
649
+ case "removeNode":
650
+ if (!target.parent) throw new Error("Cannot remove root node.");
651
+ target.parent.children.splice(target.index, 1);
652
+ break;
653
+ }
654
+ }
655
+ function moveNode(root, fromPath, toPath) {
656
+ const source = getTarget(root, fromPath);
657
+ if (!source.parent) {
658
+ throw new Error("Cannot move root node.");
659
+ }
660
+ const [removed] = source.parent.children.splice(source.index, 1);
661
+ if (!removed) {
662
+ throw new Error(`Path not found: ${fromPath}`);
663
+ }
664
+ const parentPath = getParentPath(toPath);
665
+ const targetParent = getTarget(root, parentPath).node;
666
+ assertElement(targetParent, parentPath);
667
+ const targetIndex = getLastIndex(toPath);
668
+ targetParent.children.splice(targetIndex, 0, removed);
669
+ }
670
+ function getTarget(root, path) {
671
+ const indexes = getPathIndexes(path);
672
+ if (indexes.length === 0) {
673
+ return {
674
+ node: root,
675
+ index: 0
676
+ };
677
+ }
678
+ let current = root;
679
+ let parent;
680
+ let currentIndex = 0;
681
+ for (const index of indexes.slice(1)) {
682
+ assertElement(current, path);
683
+ parent = current;
684
+ currentIndex = index;
685
+ const child = current.children[index];
686
+ if (!child) {
687
+ throw new Error(`Path not found: ${path}`);
688
+ }
689
+ current = child;
690
+ }
691
+ const result = {
692
+ node: current,
693
+ index: currentIndex
694
+ };
695
+ if (parent) {
696
+ result.parent = parent;
697
+ }
698
+ return result;
699
+ }
700
+ function getPathIndexes(path) {
701
+ const matches = path.match(/\[(\d+)\]/g) ?? [];
702
+ return matches.map((match) => Number.parseInt(match.slice(1, -1), 10));
703
+ }
704
+ function getLastIndex(path) {
705
+ const indexes = getPathIndexes(path);
706
+ const index = indexes.at(-1);
707
+ if (index === void 0) {
708
+ throw new Error(`Path has no numeric index: ${path}`);
709
+ }
710
+ return index;
711
+ }
712
+ function getParentPath(path) {
713
+ const parts = path.split("/").filter(Boolean);
714
+ if (parts.length <= 1) {
715
+ return path;
716
+ }
717
+ return `/${parts.slice(0, -1).join("/")}`;
718
+ }
719
+ function isRootPath(path) {
720
+ return path.split("/").filter(Boolean).length === 1;
721
+ }
722
+ function assertElement(node, path) {
723
+ if (node.type !== "element") {
724
+ throw new Error(`Expected element node at path: ${path}`);
725
+ }
726
+ }
727
+ export {
728
+ diffText,
729
+ diffXml,
730
+ formatDiff,
731
+ normalizeXml,
732
+ parseXml,
733
+ patchXml,
734
+ serializeXml
735
+ };
736
+ //# sourceMappingURL=index.js.map