<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Text;
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\SequenceMatcher;
/**
* Unified diff generator.
*
* @see https://en.wikipedia.org/wiki/Diff#Unified_format
*/
final class Unified extends AbstractText
{
/**
* {@inheritdoc}
*/
public const INFO = [
'desc' => 'Unified',
'type' => 'Text',
];
/**
* {@inheritdoc}
*/
protected function renderWorker(Differ $differ): string
{
$ret = '';
foreach ($differ->getGroupedOpcodesGnu() as $hunk) {
$ret .= $this->renderHunkHeader($differ, $hunk);
$ret .= $this->renderHunkBlocks($differ, $hunk);
}
return $ret;
}
/**
* Render the hunk header.
*
* @param Differ $differ the differ
* @param int[][] $hunk the hunk
*/
protected function renderHunkHeader(Differ $differ, array $hunk): string
{
$lastBlockIdx = \count($hunk) - 1;
// note that these line number variables are 0-based
$i1 = $hunk[0][1];
$i2 = $hunk[$lastBlockIdx][2];
$j1 = $hunk[0][3];
$j2 = $hunk[$lastBlockIdx][4];
$oldLinesCount = $i2 - $i1;
$newLinesCount = $j2 - $j1;
return $this->cliColoredString(
'@@' .
' -' .
// the line number in GNU diff is 1-based, so we add 1
// a special case is when a hunk has only changed blocks,
// i.e., context is set to 0, we do not need the adding
($i1 === $i2 ? $i1 : $i1 + 1) .
// if the line counts is 1, it can (and mostly) be omitted
($oldLinesCount === 1 ? '' : ",{$oldLinesCount}") .
' +' .
($j1 === $j2 ? $j1 : $j1 + 1) .
($newLinesCount === 1 ? '' : ",{$newLinesCount}") .
" @@\n",
'@', // symbol
);
}
/**
* Render the hunk content.
*
* @param Differ $differ the differ
* @param int[][] $hunk the hunk
*/
protected function renderHunkBlocks(Differ $differ, array $hunk): string
{
$ret = '';
$oldNoEolAtEofIdx = $differ->getOldNoEolAtEofIdx();
$newNoEolAtEofIdx = $differ->getNewNoEolAtEofIdx();
foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
// note that although we are in a OP_EQ situation,
// the old and the new may not be exactly the same
// because of ignoreCase, ignoreWhitespace, etc
if ($op === SequenceMatcher::OP_EQ) {
// we could only pick either the old or the new to show
// note that the GNU diff will use the old one because it creates a patch
$ret .= $this->renderContext(
' ',
$differ->getOld($i1, $i2),
$i2 === $oldNoEolAtEofIdx,
);
continue;
}
if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_DEL)) {
$ret .= $this->renderContext(
'-',
$differ->getOld($i1, $i2),
$i2 === $oldNoEolAtEofIdx,
);
}
if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_INS)) {
$ret .= $this->renderContext(
'+',
$differ->getNew($j1, $j2),
$j2 === $newNoEolAtEofIdx,
);
}
}
return $ret;
}
/**
* Render the context array with the symbol.
*
* @param string $symbol the symbol
* @param string[] $context the context
* @param bool $noEolAtEof there is no EOL at EOF in this block
*/
protected function renderContext(string $symbol, array $context, bool $noEolAtEof = false): string
{
if (empty($context)) {
return '';
}
$ret = $symbol . implode("\n{$symbol}", $context) . "\n";
$ret = $this->cliColoredString($ret, $symbol);
if ($noEolAtEof) {
$ret .= self::GNU_OUTPUT_NO_EOL_AT_EOF . "\n";
}
return $ret;
}
}