sugarcraft / sugar-diff
Unified-diff engine — LCS line diff, context hunks, GNU diff -u writer and scanner. Extracted from sugar-crush.
dev-master
2026-10-08 17:36 UTC
Requires
- php: ^8.3
- ext-mbstring: *
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-08 21:50:01 UTC
README
Unified-diff engine — LCS line diffing, context hunks, a GNU diff -u writer, and a line-number scanner. Extracted from sugar-crush (Tools\Concerns\BuildsUnifiedDiff + the Tui\DiffGutter region model) so every SugarCraft surface that shows or produces patches shares one faithful implementation. First-party: this is the ecosystem's own engine.
Installation
composer require sugarcraft/sugar-diff
Role
Diff::compute()— prefix/suffix-trimmed LCS over lines (same algorithmic guarantees as the sugar-crush trait:>=tie-break deletes first,maxLcsCellsbudget falls back to a whole-middle replace block), producing numberedLinerows grouped intoHunks.Writer\UnifiedFormat— rendersdiff -ushaped text:--- / +++headers,@@ -a,b +c,d @@hunk headers, optional\ No newline at end of filemarkers and/dev/nullempty-side labels.Reader\UnifiedScan— walks any rendered unified diff and tells a viewer which old/new line each row points at (the gutter model the sugar-crush TUI consumes), without owning any view code.
Quickstart
use SugarCraft\Diff\Diff; use SugarCraft\Diff\DiffOptions; $diff = Diff::compute("Hello World\n", "Hello PHP\n"); $diff->isEmpty(); // false $diff->addedLines(); // 1 $diff->removedLines(); // 1 echo $diff->unified('file.txt'); // --- a/file.txt // +++ b/file.txt // @@ -1,1 +1,1 @@ // -Hello World // +Hello PHP // Options are immutable + fluent; defaults reproduce the sugar-crush output byte-for-byte. $wide = Diff::compute($before, $after, DiffOptions::new() ->withContextLines(10) ->withIgnoreWhitespace() ->withNoNewlineMarker() ->withDevNullOnEmptySide()); // Row model for renderers: foreach ($diff->lines as $line) { // $line->kind (LineKind enum), $line->text, $line->oldNumber, $line->newNumber } // Number the rows of an existing patch (e.g. for gutter display): use SugarCraft\Diff\Reader\UnifiedScan; foreach (UnifiedScan::lines($patchText) as $row) { // $row->old, $row->new, $row->fileHeader }
API surface
| Class | Purpose |
|---|---|
Diff |
compute() entry point; lines, hunks, options, beforeCount/afterCount, beforeHasFinalNewline/afterHasFinalNewline; isEmpty(), addedLines(), removedLines(), unified(), hunkText() |
DiffOptions |
withContextLines() (default 3), withMaxLcsCells() (default 250 000), withIgnoreWhitespace(), withIgnoreCase(), withNoNewlineMarker(), withDevNullOnEmptySide() |
Hunk / Line / LineKind |
The immutable region model — one @@ block, one numbered row, eq/del/ins |
Writer\UnifiedFormat |
write() / hunksOnly() statics behind Diff::unified() / Diff::hunkText() |
Reader\UnifiedScan |
lines() / fileHeaders() row-number walk of any unified diff |
Non-visual engine: no ANSI, no rendering, no demos — view layers (gutters, coloring) consume the model and stay downstream.
Fidelity notes
- Default options reproduce
sugar-crush'sBuildsUnifiedDiffoutput byte-for-byte; the\ No newlinemarkers and/dev/nulllabels are opt-in GNU affordances the trait never emitted. - Header printing is GNU-faithful (oracle-verified vs
/usr/bin/diff -U0): a zero-length side anchors at the line the hunk sits after (@@ -1,0 +2 @@mid-file,@@ -0,0 +1 @@at the head) and keeps its,0, while a single-line range drops its count (-2, not-2,1). - Hunk ranges always carry an explicit count: a single line renders
@@ -4,1 +4,1 @@, where both GNUdiff -uand git elide the,1as@@ -4 +4 @@. - File headers are git-style
--- a/<path>/+++ b/<path>with no timestamps; GNU emits bare paths tab-separated from a date column. - Like git, a line's newline termination is invisible to the diff itself:
"a\n"vs"a"compare equal;"a\n"vs"a\n\n"shows one added empty row.
License
MIT