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