Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
75 changes: 73 additions & 2 deletions docs/comparison.md → docs/diffing.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
<?php

require __DIR__.'/vendor/autoload.php';

use Alto\Code\Diff\Diff;
use Alto\Code\Diff\Engine\LcsDiffEngine;

$result = Diff::build()
->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
<?php

require __DIR__.'/vendor/autoload.php';

use Alto\Code\Diff\Diff;
use Alto\Code\Diff\Engine\DiffEngineInterface;
use Alto\Code\Diff\Engine\MyersDiffEngine;
use Alto\Code\Diff\Model\DiffResult;
use Alto\Code\Diff\Options\Options;

final class AuditedEngine implements DiffEngineInterface
{
public function diff(string $old, string $new, Options $options): DiffResult
{
error_log(sprintf('Comparing %d and %d bytes', strlen($old), strlen($new)));

return (new MyersDiffEngine())->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.
64 changes: 0 additions & 64 deletions docs/engines.md

This file was deleted.

34 changes: 34 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
@@ -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.
55 changes: 50 additions & 5 deletions docs/patches.md → docs/formats.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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
<?php

require __DIR__.'/vendor/autoload.php';

use Alto\Code\Diff\Diff;
use Alto\Code\Diff\Model\DiffBundle;
use Alto\Code\Diff\Model\DiffFile;
use Alto\Code\Diff\Patch\PatchApplier;
use Alto\Code\Diff\Patch\UnifiedEmitter;
use Alto\Code\Diff\Patch\UnifiedParser;

$files = ['a.txt' => "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"}
```
7 changes: 6 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
31 changes: 18 additions & 13 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 9 additions & 1 deletion docs/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Loading