Skip to content

vndmea/xml-diff-kit

Repository files navigation

xml-diff-kit

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.

Install

npm install xml-diff-kit

Usage

Most 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>';

diffXml

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.' }]
    }
  }
]

patchXml

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.

Add a node

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>

Update an attribute

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

Replace text

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.

formatDiff

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

parseXml and serializeXml

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>

normalizeXml

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' }]
    }
  ]
}

diffText

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.' }
  ]
}

Diff operations

Supported structured XML changes:

  • addNode
  • removeNode
  • replaceNode
  • moveNode
  • replaceText
  • addAttr
  • updateAttr
  • removeAttr

Text changes are represented as nested range operations inside replaceText.

Options

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.

Paths

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.

Development

npm install
npm run lint
npm run typecheck
npm test
npm run coverage
npm run build

Release

npm install
npm run lint
npm run typecheck
npm test
npm run build
npm publish --access public

License

MIT

About

A TypeScript toolkit for parsing, normalizing, diffing, patching, and formatting XML document changes.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors