English | 简体中文
A TypeScript toolkit for parsing, normalizing, diffing, patching, serializing, and formatting XML document changes.
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.
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.
npm install xml-diff-kitMost examples below use the same pair of XML documents:
const oldXml = '<procedure><step id="s1">Remove the panel.</step></procedure>';
const newXml = '<procedure><step id="s1">Remove the access panel.</step><step id="s2">Inspect.</step></procedure>';Compare two XML documents and get structured diff operations.
import { diffXml } from 'xml-diff-kit';
const ops = diffXml(oldXml, newXml, {
keyAttrs: ['id'],
});
console.log(ops);Output:
[
{
op: 'replaceText',
path: '/procedure[0]/step[@id="s1"][0]/text()[0]',
oldValue: 'Remove the panel.',
newValue: 'Remove the access panel.',
changes: [{ op: 'insertText', offset: 11, text: 'access ' }],
segments: [
{ type: 'equal', text: 'Remove the ' },
{ type: 'insert', text: 'access ' },
{ type: 'equal', text: 'panel.' }
]
},
{
op: 'addNode',
path: '/procedure[0]/step[@id="s2"][1]',
value: {
type: 'element',
name: 'step',
namespaceURI: null,
attrs: { id: 's2' },
children: [{ type: 'text', text: 'Inspect.' }]
}
}
]Apply structured diff operations back to an XML string or a parsed XML node.
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.
import { patchXml, type XmlDiffOp } from 'xml-diff-kit';
const xml = '<root><a/></root>';
const ops: XmlDiffOp[] = [
{
op: 'addNode',
path: '/root[0]/b[1]',
value: {
type: 'element',
name: 'b',
namespaceURI: null,
attrs: {},
children: []
}
}
];
const patchedXml = patchXml(xml, ops);
console.log(patchedXml);Output:
<root><a/><b/></root>import { patchXml, type XmlDiffOp } from 'xml-diff-kit';
const xml = '<root status="draft"/>';
const ops: XmlDiffOp[] = [
{
op: 'updateAttr',
path: '/root[0]',
name: 'status',
oldValue: 'draft',
newValue: 'released'
}
];
const patchedXml = patchXml(xml, ops);
console.log(patchedXml);Output:
<root status="released"/>import { patchXml, type XmlDiffOp } from 'xml-diff-kit';
const xml = '<root>old text</root>';
const ops: XmlDiffOp[] = [
{
op: 'replaceText',
path: '/root[0]/text()[0]',
oldValue: 'old text',
newValue: 'new text',
changes: [
{
op: 'replaceTextRange',
offset: 0,
oldText: 'old',
newText: 'new'
}
],
segments: [
{ type: 'delete', text: 'old' },
{ type: 'insert', text: 'new' },
{ type: 'equal', text: ' text' }
]
}
];
const patchedXml = patchXml(xml, ops);
console.log(patchedXml);Output:
<root>new text</root>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.
Supported patch operations include adding, removing, replacing, and moving nodes; adding, updating, and removing attributes; and replacing text node values.
Format structured diff operations as summary objects or a Markdown report.
import { diffXml, formatDiff } from 'xml-diff-kit';
const ops = diffXml(oldXml, newXml, { keyAttrs: ['id'] });
const summary = formatDiff(ops);
console.log(summary);Output:
[
{
type: 'textChanged',
path: '/procedure[0]/step[@id="s1"][0]/text()[0]',
message: 'Changed text at /procedure[0]/step[@id="s1"][0]/text()[0]',
before: 'Remove the panel.',
after: 'Remove the access panel.'
},
{
type: 'nodeAdded',
path: '/procedure[0]/step[@id="s2"][1]',
message: 'Added node at /procedure[0]/step[@id="s2"][1]',
after: {
type: 'element',
name: 'step',
namespaceURI: null,
attrs: { id: 's2' },
children: [{ type: 'text', text: 'Inspect.' }]
}
}
]Markdown output:
const markdown = formatDiff(ops, { format: 'markdown' });
console.log(markdown);Output:
# XML Diff
Total changes: 2
## 1. Changed text
- Path: `/procedure[0]/step[@id="s1"][0]/text()[0]`
**Before**
```text
Remove the panel.
```
**After**
```text
Remove the access panel.
```
**Text segments**
- equal: `Remove the `
- insert: `access `
- equal: `panel.`
## 2. Added node
- Path: `/procedure[0]/step[@id="s2"][1]`
```xml
<step id="s2">Inspect.</step>
```Parse XML into the internal AST, then serialize it back to XML.
import { parseXml, serializeXml } from 'xml-diff-kit';
const doc = parseXml('<root><item id="1">Hello</item><item id="2">World</item></root>');
const xml = serializeXml(doc, { pretty: true });
console.log(xml);Output:
<root>
<item id="1">Hello</item>
<item id="2">World</item>
</root>Normalize an XML AST before diffing or custom processing.
import { normalizeXml, parseXml } from 'xml-diff-kit';
const doc = parseXml('<root b="2" a="1"> <item> value </item> </root>');
const normalized = normalizeXml(doc, {
ignoreWhitespaceText: true,
trimText: true,
sortAttributes: true,
});
console.log(normalized);Output:
{
type: 'element',
name: 'root',
namespaceURI: null,
attrs: { a: '1', b: '2' },
children: [
{
type: 'element',
name: 'item',
namespaceURI: null,
attrs: {},
children: [{ type: 'text', text: 'value' }]
}
]
}Diff two text values directly. This is the same text diff used inside replaceText operations.
import { diffText } from 'xml-diff-kit';
const textDiff = diffText('Remove the panel.', 'Remove the access panel.');
console.log(textDiff);Output:
{
changes: [{ op: 'insertText', offset: 11, text: 'access ' }],
segments: [
{ type: 'equal', text: 'Remove the ' },
{ type: 'insert', text: 'access ' },
{ type: 'equal', text: 'panel.' }
]
}Supported structured XML changes:
addNoderemoveNodereplaceNodemoveNodereplaceTextaddAttrupdateAttrremoveAttr
Text changes are represented as nested range operations inside replaceText.
interface XmlDiffOptions {
ignoreWhitespaceText?: boolean;
trimText?: boolean;
ignoreComments?: boolean;
sortAttributes?: boolean;
keyAttrs?: string[];
detectMoves?: boolean;
}keyAttrs lets the diff engine align sibling elements by stable identifiers, such as id, xml:id, or domain-specific keys.
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.
Diff operations use absolute paths from the XML root node:
/procedure[0]/step[@id="s1"][0]/text()[0]The numeric index is the executable addressing part used by patching. Key hints such as [@id="s1"] improve readability and keyed matching.
npm install
npm run lint
npm run typecheck
npm test
npm run coverage
npm run buildnpm install
npm run lint
npm run typecheck
npm test
npm run build
npm publish --access publicMIT