Skip to content

phil/text-diff

alpha

What changed between two texts, word by word, paragraph by paragraph, or block by block — shown in HTML or listed.

phil/text-diff compares two texts and gives the shortest set of deletions and insertions that turns the old one into the new one. It cuts texts into words, lines or paragraphs; it reads HTML without ever cutting a tag, so that the difference of two pages is itself a well-formed page; and it compares two documents in UJO (Universal JSON Output) — the block document format read by phil/ujo — block by block, by block identifier, then field by field inside each block that changed. Each difference comes out as HTML with <ins> and <del>, or as a structured list of changes with their offsets.

It is for any PHP application that keeps versions of what people write and must show what moved between two of them: the revision history of an article, a page or a wiki, the review of a proposed edit, the segments of a translation whose source changed, the audit of a document before it is published again. phil/content-bundle keeps the versions of its contents; this library is how an application shows their differences.

It is a plain PHP library, written from scratch, whose only requirements are ext-mbstring and phil/ujo. The comparison is Eugene W. Myers' O(ND) difference algorithm, run after the common start and end of both texts are set aside, and bounded: past a number of edits — a thousand by default — the rest is shown as replaced whole, which keeps time and memory in check on texts rewritten from end to end. The output is deterministic.

The design is small: a Tokenizer cuts a text into tokens that give it back byte for byte; Myers finds the script; a Diff holds its Operations and lists its Changes; HtmlFormatter writes the HTML. For documents, DocumentDiffer pairs the blocks of each level by identifier into BlockChanges with their FieldChanges, gathered in a DocumentDiff that DocumentFormatter renders through phil/ujo. TextDiff puts them together.

Status

PartState
Myers' algorithm with common start and end set aside and a bound of editsImplemented and tested
Words, lines and paragraphs; HTML with tags, comments and character references wholeImplemented and tested
Scripts written without spaces (Han, kana, Thai, Lao, Khmer, Myanmar) compared character by characterImplemented and tested
White space between two changes joined to them; deletions before insertions in each changeImplemented and tested
HTML of a difference: plain text escaped, HTML keeping the structure of the new textImplemented and tested
Change list: insertions, deletions and replacements with their offsets in code pointsImplemented and tested
UJO documents: blocks paired by identifier at every level, kept, changed, moved, added or removed; values compared by pathImplemented and tested
HTML of a document difference, each block rendered by phil/ujo and markedImplemented and tested
Lists of values other than blocks aligned by content rather than by positionNot started: they are compared by index

Documentation

DocumentWhat it covers
InstallationRequirements and installing.
Comparing textsTextDiff, the tokenizers, operations, changes and the bound of edits.
HTML outputHtmlFormatter: plain text and HTML, what is marked and what is kept, styling.
Comparing documentsUJO documents block by block: pairing, moves, field paths, nested blocks, HTML.
Versions of contentsShowing the differences between two versions kept by phil/content-bundle.
ExceptionsTextDiffException and its two exceptions.
Extension pointsThe ports, arguments and data an application chooses or replaces.
TestingThe library's tests and mutation bar, and how to test code that shows differences.
ReferenceEvery type of the library, with its role.

Quick start

composer require phil/text-diff
use Phil\TextDiff\TextDiff;

$differ = new TextDiff();

echo $differ->toHtml($differ->words('The quick brown fox', 'The slow brown dog'));
// The <del>quick</del><ins>slow</ins> brown <del>fox</del><ins>dog</ins>

echo $differ->toHtml($differ->html('<p>Hello <b>big</b> world</p>', '<p>Hello <i>small</i> world!</p>'));
// <p>Hello <del>big</del><i><ins>small</ins></i> world<ins>!</ins></p>

foreach ($differ->paragraphs($before, $after)->changes() as $change) {
    printf("%s at %d: %s -> %s\n", $change->kind->value, $change->oldOffset, $change->old, $change->new);
}
use Phil\Ujo\Ujo;

$ujo = new Ujo();
$diff = $differ->documents($ujo->decode($oldJson), $ujo->decode($newJson));

echo $differ->documentToHtml($diff);

Requirements

PHP 8.5 or later, ext-mbstring and phil/ujo.

Contributing

Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.

License

MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Parler Haut Interagir Librement project. See LICENSE.