Most people think comparing two Word documents is like comparing two text files. Find the differences, highlight them, done.
It's not. Not even close.
I've spent years working on this problem, and I built DocumentDelta to solve it. Here's what makes DOCX comparison so brutally hard — and what I learned building a comparison engine from scratch.
A DOCX File Is Not Text
A .docx file is a ZIP archive containing XML. A single paragraph with one bold word might look like this under the hood:
<w:p>
<w:r><w:t>This is </w:t></w:r>
<w:r><w:rPr><w:b/></w:rPr><w:t>important</w:t></w:r>
<w:r><w:t> text.</w:t></w:r>
</w:p>
Three separate "runs" for one sentence. Now imagine diffing two documents where the same text is split into runs differently. Text-level comparison breaks immediately.
The Five Hardest Problems
1. Field Codes Are Invisible Landmines
A cross-reference like "see Section 3.1" isn't just text. In the XML, it's a multi-element structure: a fldChar begin marker, an instrText containing something like REF _Ref488222269 \h, a separator, the visible result text, and an end marker.
If your diff algorithm matches individual runs, it shatters field groups. The result? "Error! Reference source not found" scattered throughout the document — even in paragraphs that didn't change. I've seen tools destroy 75% of cross-references in unchanged content because they treated field codes as regular text.
2. Everything Shifts When You Insert a Section
Insert a new clause in the middle of a contract. Now every subsequent section number changes. Bookmark IDs get renumbered. Page references update. TOC entry prefixes shift from "12.1" to "13.1".
A naive comparison algorithm sees every subsequent paragraph as "changed" because the numbering is different — even though the actual content is identical. You need to strip dynamic content during matching, then diff the actual changes separately.
3. You Can't Delete a Table Column the Normal Way
In Open XML, you can wrap text in w:del to mark it as deleted. Simple enough. But try wrapping a table cell (w:tc) in w:del — Word rejects the file.
Column deletions require specialized revision elements: w:tblGridChange on the grid definition, w:cellDel markers in the cell properties, and content marked as deleted inside the cell. Most comparison tools either corrupt the file or duplicate rows when columns change.
4. Content Controls Can't Be Children of Revisions
Content controls (w:sdt) — used for structured fields, TOC wrappers, and form elements — cannot be placed inside w:ins or w:del tags. The OOXML spec forbids it.
So how do you track changes inside a content control? You have to preserve the SDT wrapper intact and mark changes inside w:sdtContent. Get this wrong with picture content controls and Word crashes — they use w14: namespace attributes that must be preserved exactly.
5. The Interleaved Phrase Problem
"Balancing Amount" changes to "Earn-out Consideration". A character-level diff algorithm sees the space character as a common substring and produces:
BalancingEarn-out AmountConsideration
This is technically correct but completely unreadable. The fix is non-obvious: tokenize at word boundaries with trailing whitespace attached, so the diff produces clean output:
Balancing Amount Earn-out Consideration
This kind of cleanup is the difference between a comparison tool that's technically right and one that's actually useful.
What Clean Output Requires
Getting readable tracked changes isn't just about finding differences. The output needs multiple cleanup passes: merging fragmented whitespace-adjacent edits, absorbing trailing whitespace into strikethrough ranges, collapsing tiny unchanged segments sandwiched between deletions and insertions, and generating proper revision markup elements with exactly the right XML wrapper structure.
I've had bugs where Word showed zero tracked changes because of a single missing XML wrapper element. Valid-looking XML that Word "repairs" by silently deleting bookmarks. Documents that open fine but have 18 more bookmark starts than ends.
The Numbers
DocumentDelta compares a ~230KB legal Share Purchase Agreement — a real enterprise contract — in about 473 milliseconds. The output is a complete, valid DOCX with native Word tracked changes that you can open and accept/reject normally.
It handles tables, text boxes, headers, footers, footnotes, endnotes, comments, hyperlinks, images, bookmarks, field codes, content controls, and pre-existing tracked changes.
279 tests. All passing.
Try It
If you work with DOCX files at scale — in legal, compliance, publishing, or enterprise document automation — give it a try:
There's also a REST API for integration into your workflows. Contact me for details.
Find me on LinkedIn or check out DocumentDelta.com.
No comments:
Post a Comment