<?php declare(strict_types=1);
/*
* This file is part of sebastian/diff.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Diff\Output;
/**
* Builds a diff string representation in unified diff format in chunks.
*/
final class UnifiedDiffOutputBuilder extends AbstractChunkOutputBuilder
{
/**
* @var string
*/
private $header;
/**
* @var bool
*/
private $addLineNumbers;
public function __construct(string $header = "--- Original\n+++ New\n", bool $addLineNumbers = false)
{
$this->header = $header;
$this->addLineNumbers = $addLineNumbers;
}
public function getDiff(array $diff): string
{
$buffer = \fopen('php://memory', 'r+b');
if ('' !== $this->header) {
\fwrite($buffer, $this->header);
if ("\n" !== \substr($this->header, -1, 1)) {
\fwrite($buffer, "\n");
}
}
$this->writeDiffChunked($buffer, $diff, $this->getCommonChunks($diff));
$diff = \stream_get_contents($buffer, -1, 0);
\fclose($buffer);
return $diff;
}
// `old` is an array with key => value pairs . Each pair represents a start and end index of `diff`
// of a list of elements all containing `same` (0) entries.
private function writeDiffChunked($output, array $diff, array $old)
{
$upperLimit = \count($diff);
$start = 0;
$fromStart = 0;
$toStart = 0;
if (\count($old)) { // no common parts, list all diff entries
\reset($old);
// iterate the diff, go from chunk to chunk skipping common chunk of lines between those
do {
$commonStart = \key($old);
$commonEnd = \current($old);
if ($commonStart !== $start) {
list($fromRange, $toRange) = $this->getChunkRange($diff, $start, $commonStart);
$this->writeChunk($output, $diff, $start, $commonStart, $fromStart, $fromRange, $toStart, $toRange);
$fromStart += $fromRange;
$toStart += $toRange;
}
$start = $commonEnd + 1;
$commonLength = $commonEnd - $commonStart + 1; // calculate number of non-change lines in the common part
$fromStart += $commonLength;
$toStart += $commonLength;
} while (false !== \next($old));
\end($old); // short cut for finding possible last `change entry`
$tmp = \key($old);
\reset($old);
if ($old[$tmp] === $upperLimit - 1) {
$upperLimit = $tmp;
}
}
if ($start < $upperLimit - 1) { // check for trailing (non) diff entries
do {
--$upperLimit;
} while (isset($diff[$upperLimit][1]) && $diff[$upperLimit][1] === 0);
++$upperLimit;
list($fromRange, $toRange) = $this->getChunkRange($diff, $start, $upperLimit);
$this->writeChunk($output, $diff, $start, $upperLimit, $fromStart, $fromRange, $toStart, $toRange);
}
}
private function writeChunk(
$output,
array $diff,
int $diffStartIndex,
int $diffEndIndex,
int $fromStart,
int $fromRange,
int $toStart,
int $toRange
) {
if ($this->addLineNumbers) {
\fwrite($output, '@@ -' . (1 + $fromStart));
if ($fromRange > 1) {
\fwrite($output, ',' . $fromRange);
}
\fwrite($output, ' +' . (1 + $toStart));
if ($toRange > 1) {
\fwrite($output, ',' . $toRange);
}
\fwrite($output, " @@\n");
} else {
\fwrite($output, "@@ @@\n");
}
for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) {
if ($diff[$i][1] === 1 /* ADDED */) {
\fwrite($output, '+' . $diff[$i][0]);
} elseif ($diff[$i][1] === 2 /* REMOVED */) {
\fwrite($output, '-' . $diff[$i][0]);
} else { /* Not changed (old) 0 or Warning 3 */
\fwrite($output, ' ' . $diff[$i][0]);
}
$lc = \substr($diff[$i][0], -1);
if ($lc !== "\n" && $lc !== "\r") {
\fwrite($output, "\n"); // \No newline at end of file
}
}
}
private function getChunkRange(array $diff, int $diffStartIndex, int $diffEndIndex): array
{
$toRange = 0;
$fromRange = 0;
for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) {
if ($diff[$i][1] === 1) { // added
++$toRange;
} elseif ($diff[$i][1] === 2) { // removed
++$fromRange;
} elseif ($diff[$i][1] === 0) { // same
++$fromRange;
++$toRange;
}
}
return [$fromRange, $toRange];
}
}