From 6b3aa350c9b8021c1524f70d60f3ffc214d03eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sat, 19 Sep 2026 23:15:21 +0200 Subject: [PATCH] docs: update structure --- README.md | 8 +++- docs/{comparison.md => diffing.md} | 75 +++++++++++++++++++++++++++++- docs/engines.md | 64 ------------------------- docs/errors.md | 34 ++++++++++++++ docs/{patches.md => formats.md} | 55 ++++++++++++++++++++-- docs/getting-started.md | 7 ++- docs/index.md | 31 ++++++------ docs/rendering.md | 10 +++- 8 files changed, 197 insertions(+), 87 deletions(-) rename docs/{comparison.md => diffing.md} (51%) delete mode 100644 docs/engines.md create mode 100644 docs/errors.md rename docs/{patches.md => formats.md} (61%) diff --git a/README.md b/README.md index d6674af..b1f0100 100644 --- a/README.md +++ b/README.md @@ -348,7 +348,13 @@ $file->headers['index']; // 'index abcdef..123456 100644' ## Documentation -The complete documentation is available at [altophp.com/code-diff](https://altophp.com/code-diff). +- [Installation](docs/installation.md): install the package and verify its requirements. +- [Getting started](docs/getting-started.md): compare two strings and render a unified diff. +- [Diffing](docs/diffing.md): configure comparisons, inspect results, and select an engine. +- [Rendering](docs/rendering.md): produce unified text, HTML, JSON, or ANSI output. +- [Formats](docs/formats.md): emit, parse, and apply unified patches. +- [Errors](docs/errors.md): recover from rejected inputs and patches. +- [Complete documentation](docs/index.md): review the package scope and every guide. ## Testing diff --git a/docs/comparison.md b/docs/diffing.md similarity index 51% rename from docs/comparison.md rename to docs/diffing.md index 56ce334..571baa6 100644 --- a/docs/comparison.md +++ b/docs/diffing.md @@ -1,4 +1,4 @@ -# Comparison +# Diffing `Diff::compare(string $old, string $new): DiffResult` compares two text strings line by line. The default Myers engine returns only changed regions and three surrounding context lines. @@ -35,7 +35,10 @@ echo count($result->hunks())." changed region(s)\n"; | `maxBytes(int $bytes)` | `5_000_000` | Limits each input string. The value must be positive. | | `withEngine(DiffEngineInterface $engine)` | Myers | Selects another comparison engine. | -`compare()` throws `SizeLimitException` when either input exceeds the configured limit and `BinaryInputException` when an input appears to contain binary data. Binary detection checks null bytes and excessive control characters near the beginning of the input. +`compare()` throws `SizeLimitException` when either input exceeds the configured +limit and `BinaryInputException` when an input appears to contain binary data. +Binary detection checks null bytes and excessive control characters near the +beginning of the input. See [Errors](errors.md) before retrying a rejected input. ## Inspect the result @@ -79,3 +82,71 @@ The result model contains: - `WordSpan`: an `add`, `del`, or `eq` token produced by word-level comparison. Use [Rendering](rendering.md) when you need formatted output. + +## Engines + +The default `MyersDiffEngine` computes a minimal edit script in `O(ND)` time and +is suitable for general use. `LcsDiffEngine` uses the classic longest common +subsequence algorithm. Its `O(MN)` time and memory cost makes it appropriate +only for small, controlled inputs. + +```php +withEngine(new LcsDiffEngine()) + ->compare("A\nB\n", "A\nC\n"); + +echo count($result->hunks())." changed region(s)\n"; +``` + +Both built-in engines enforce the configured size limit, reject binary input, +honor whitespace and context options, and can compute word-level spans. + +### Custom engine + +An engine implements +`DiffEngineInterface::diff(string $old, string $new, Options $options): DiffResult`. + +Pass the implementation to `Diff::withEngine()`. + +```php +diff($old, $new, $options); + } +} + +$result = Diff::build() + ->withEngine(new AuditedEngine()) + ->compare("old\n", "new\n"); + +echo count($result->hunks())." changed region(s)\n"; +``` + +### Word tokenizer + +`MyersDiffEngine` and `LcsDiffEngine` accept a `TokenizerInterface` in their +constructors. Its `tokenize(string $input): array` method returns a list of +tokens. Pass `null` to disable word spans even when `withWordDiff()` is enabled. +The built-in `WordTokenizer` keeps whitespace as tokens so renderers can +reconstruct the original line exactly. diff --git a/docs/engines.md b/docs/engines.md deleted file mode 100644 index c1034ed..0000000 --- a/docs/engines.md +++ /dev/null @@ -1,64 +0,0 @@ -# Engines - -The default `MyersDiffEngine` computes a minimal edit script in `O(ND)` time and is suitable for general use. - -`LcsDiffEngine` uses the classic longest common subsequence algorithm. Its `O(MN)` time and memory cost makes it appropriate only for small, controlled inputs. - -```php -withEngine(new LcsDiffEngine()) - ->compare("A\nB\n", "A\nC\n"); - -echo count($result->hunks())." changed region(s)\n"; -``` - -Both built-in engines enforce the configured size limit, reject binary input, honor whitespace and context options, and can compute word-level spans. - -## Custom engine - -An engine implements one method: - -`DiffEngineInterface::diff(string $old, string $new, Options $opts): DiffResult` - -Pass the implementation to `Diff::withEngine()`. - -```php -diff($old, $new, $opts); - } -} - -$result = Diff::build() - ->withEngine(new AuditedEngine()) - ->compare("old\n", "new\n"); - -echo count($result->hunks())." changed region(s)\n"; -``` - -## Custom word tokenizer - -`MyersDiffEngine` and `LcsDiffEngine` accept a `TokenizerInterface` in their constructors. Its `tokenize(string $input): array` method must return a list of tokens. Pass `null` to disable word spans even when `withWordDiff()` is enabled. - -The built-in `WordTokenizer` keeps whitespace as tokens, allowing renderers to reconstruct the original line exactly. diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..c7258bb --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,34 @@ +# Errors + +All package exceptions implement `CodeDiffExceptionInterface`. Catch a precise +exception when the application has a recovery path, or use the shared interface +at a request, job, or command boundary. + +| Exception | Cause | Recovery | +| --- | --- | --- | +| `SizeLimitException` | Comparison or patch input exceeds `maxBytes` | Compare smaller meaningful units, or deliberately raise the limit after reviewing memory and execution time | +| `BinaryInputException` | Text input contains binary indicators, or a patch contains binary file markers | Decode the content to an appropriate textual representation, or use a binary comparison tool | +| `ParseException` | A unified patch has an invalid header, hunk count, edit line, or newline marker | Obtain the complete patch and preserve its headers and hunk counts rather than guessing missing lines | +| `PatchApplyException` | A patch contains several files for `apply()`, a path is missing, or a hunk does not match | Use `applyBundle()` for several files, verify the source revision and path map, then inspect `hunkIndex` | + +## Comparison limits + +`Diff::compare()` validates both strings before an engine computes changes. +Binary detection checks null bytes and excessive control characters near the +beginning of the input. Retrying the same bytes with a larger size limit does +not make binary content valid text. + +`contextLines()` rejects negative values and `Diff::maxBytes()` rejects +non-positive values. These configuration errors raise `InvalidArgumentException` +and should be fixed before processing user input. + +## Patch recovery + +`PatchApplyException::hunkIndex` identifies the zero-based hunk that failed. +Fuzz searches nearby line positions; it does not ignore changed content and is +not conflict resolution. If a patch targets another revision, regenerate it +against the intended base or let the application present a conflict workflow. + +For multi-file patches, every required old path must exist in the input +`path => content` map. Keep the returned map separate until the application has +decided how to persist it. Alto Code Diff never writes files or invokes Git. diff --git a/docs/patches.md b/docs/formats.md similarity index 61% rename from docs/patches.md rename to docs/formats.md index 02eb0c2..36ee8ec 100644 --- a/docs/patches.md +++ b/docs/formats.md @@ -1,6 +1,9 @@ -# Patches +# Formats -Alto Code Diff understands standard unified diffs and common Git headers. It operates on strings and associative arrays; your application remains responsible for file-system access. +Alto Code Diff exchanges changes as standard unified diffs and understands +common Git headers. A `DiffResult` represents one comparison; a `DiffBundle` +groups path-labelled results for multi-file patches. The package operates on +strings and associative arrays, while the application owns file-system access. ## Emit a patch @@ -78,7 +81,9 @@ $file = $bundle->files()[0]; printf("%s -> %s\n", $file->oldPath, $file->newPath); ``` -The parser recognizes file modes, creation, deletion, rename, copy, similarity, and index headers. It preserves no-trailing-newline markers. It throws `ParseException` for malformed hunks and `BinaryInputException` for binary patch markers. +The parser recognizes file modes, creation, deletion, rename, copy, similarity, +and index headers. It preserves no-trailing-newline markers. See +[Errors](errors.md) for malformed hunks and binary patch markers. ## Apply a single-file patch @@ -105,10 +110,50 @@ $updated = (new PatchApplier())->apply($original, $patch); echo $updated; ``` -The constructor accepts `fuzz` and `maxBytes`, both defaulting to `0` and `5_000_000`. Fuzz searches that many lines before and after a hunk's expected position. `PatchApplyException` exposes the failed zero-based `hunkIndex`; `SizeLimitException` reports oversized source content. +The constructor accepts `fuzz` and `maxBytes`, both defaulting to `0` and +`5_000_000`. Fuzz searches that many lines before and after a hunk's expected +position. It does not resolve conflicts or accept different source text. ## Apply a bundle Use `applyBundle(array $files, DiffBundle $bundle): array` for multiple files. The input and result use `path => content` maps. -The method handles modifications, renames, creations from `/dev/null`, and deletions to `/dev/null`. It throws `PatchApplyException` when a required source path is missing or a hunk cannot be matched. The library returns updated content but never writes it to disk. +The method handles modifications, renames, creations from `/dev/null`, and +deletions to `/dev/null`. The library returns updated content but never writes +it to disk. Missing paths and unmatched hunks are covered in [Errors](errors.md). + +## Round trip + +This example emits, parses, and applies a patch while keeping both files in +memory. The path keys are data; the package does not open them. + +```php + "old\n", 'b.txt' => "keep\n"]; +$change = new DiffFile( + 'a.txt', + 'a.txt', + Diff::build()->compare($files['a.txt'], "new\n"), +); +$patch = (new UnifiedEmitter())->emit(new DiffBundle([$change])); +$bundle = (new UnifiedParser())->parse($patch); +$updated = (new PatchApplier())->applyBundle($files, $bundle); + +echo json_encode($updated, JSON_THROW_ON_ERROR), "\n"; +``` + +The output is: + +```text +{"a.txt":"new\n","b.txt":"keep\n"} +``` diff --git a/docs/getting-started.md b/docs/getting-started.md index b973c13..3a5e228 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -29,6 +29,11 @@ The output is a standard unified diff: +line two changed ``` +The returned string has no final line break. Append `PHP_EOL` when the output +destination requires one. Source newline-at-EOF metadata remains available in +the diff result. + `Diff::build()` creates an immutable builder. Each configuration method returns a new instance, so a configured builder can be reused safely. -Next, configure the [comparison](comparison.md), select another [renderer](rendering.md), or work with [patches](patches.md). +Next, configure [diffing](diffing.md), select another [renderer](rendering.md), +exchange unified [formats](formats.md), or handle [errors](errors.md). diff --git a/docs/index.md b/docs/index.md index 4ffb4d0..a7a6aa1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,20 +1,25 @@ # Alto Code Diff -Alto Code Diff compares text, renders structured differences, and parses, emits, or applies unified patches. It works with strings: reading and writing files remains the responsibility of your application. +Alto Code Diff compares text, renders structured differences, and parses, +emits, or applies unified patches. It works with strings and in-memory path +maps, so applications retain control of file and process access. -## Introduction +```php +use Alto\Code\Diff\Diff; +use Alto\Code\Diff\Renderer\UnifiedRenderer; -- [Installation](installation.md) installs the package and lists its requirements. -- [Getting started](getting-started.md) compares two strings and renders the result. +$result = Diff::build()->compare("old\n", "new\n"); +$output = (new UnifiedRenderer('old.txt', 'new.txt'))->render($result); +``` -## Diffing +## Documentation -- [Comparison](comparison.md) covers options, word-level changes, limits, and the result model. -- [Rendering](rendering.md) produces unified text, HTML, JSON, or ANSI output. -- [Engines](engines.md) explains the built-in algorithms and extension points. +- [Installation](installation.md): install the package and verify its requirements. +- [Getting started](getting-started.md): compare two strings and render a unified diff. +- [Diffing](diffing.md): configure comparisons, inspect results, and select an engine. +- [Rendering](rendering.md): produce unified text, HTML, JSON, or ANSI output. +- [Formats](formats.md): emit, parse, and apply single-file or multi-file patches. +- [Errors](errors.md): recover from rejected inputs and patches. -## Patches - -- [Patches](patches.md) parses, emits, and applies single-file or multi-file unified patches. - -The package rejects binary input and does not read files, execute Git, or resolve patch conflicts automatically. +The package rejects binary input. It does not read or write files, invoke Git, +or resolve patch conflicts automatically. diff --git a/docs/rendering.md b/docs/rendering.md index a79aac8..4dea773 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -2,6 +2,13 @@ Every renderer implements `RendererInterface::render(DiffResult|DiffBundle): string`. An empty result produces an empty string, except that `UnifiedRenderer` can still emit explicitly configured labels. +| Destination | Renderer | Integration | +| --- | --- | --- | +| Patch or review text | `UnifiedRenderer` | Preserve newline markers and labels | +| Web page | `HtmlRenderer` | Add application CSS; source text is escaped | +| Data or API | `JsonRenderer` | Consume structured edits and metadata | +| Terminal | `AnsiSideBySideRenderer` | Select a width suitable for the terminal | + ## Unified diff Use `UnifiedRenderer` for familiar line-oriented output. Its optional constructor arguments label the old and new versions. @@ -89,4 +96,5 @@ echo (new AnsiSideBySideRenderer( ))->render($result); ``` -All four renderers also accept a multi-file `DiffBundle`; see [Patches](patches.md) for its structure. +All four renderers also accept a multi-file `DiffBundle`; see +[Formats](formats.md) for its structure.