diff options
Diffstat (limited to 'vendor/jfcherng/php-diff/src/Differ.php')
-rw-r--r-- | vendor/jfcherng/php-diff/src/Differ.php | 502 |
1 files changed, 502 insertions, 0 deletions
diff --git a/vendor/jfcherng/php-diff/src/Differ.php b/vendor/jfcherng/php-diff/src/Differ.php new file mode 100644 index 0000000..bcb29ab --- /dev/null +++ b/vendor/jfcherng/php-diff/src/Differ.php @@ -0,0 +1,502 @@ +<?php + +declare(strict_types=1); + +namespace Jfcherng\Diff; + +use Jfcherng\Diff\Utility\Arr; + +/** + * A comprehensive library for generating differences between two strings + * in multiple formats (unified, side by side HTML etc). + * + * @author Jack Cherng <jfcherng@gmail.com> + * @author Chris Boulton <chris.boulton@interspire.com> + * + * @see http://github.com/chrisboulton/php-diff + */ +final class Differ +{ + /** + * @var int a safe number for indicating showing all contexts + */ + public const CONTEXT_ALL = \PHP_INT_MAX >> 3; + + /** + * @var string used to indicate a line has no EOL + * + * Arbitrary chars from the 15-16th Unicode reserved areas + * and hopefully, they won't appear in source texts + */ + public const LINE_NO_EOL = "\u{fcf28}\u{fc231}"; + + /** + * @var array cached properties and their default values + */ + private const CACHED_PROPERTIES = [ + 'groupedOpcodes' => [], + 'groupedOpcodesGnu' => [], + 'oldNoEolAtEofIdx' => -1, + 'newNoEolAtEofIdx' => -1, + 'oldNewComparison' => 0, + ]; + + /** + * @var array array of the options that have been applied for generating the diff + */ + public $options = []; + + /** + * @var string[] the old sequence + */ + private $old = []; + + /** + * @var string[] the new sequence + */ + private $new = []; + + /** + * @var bool is any of cached properties dirty? + */ + private $isCacheDirty = true; + + /** + * @var SequenceMatcher the sequence matcher + */ + private $sequenceMatcher; + + /** + * @var int + */ + private $oldSrcLength = 0; + + /** + * @var int + */ + private $newSrcLength = 0; + + /** + * @var int the end index for the old if the old has no EOL at EOF + * -1 means the old has an EOL at EOF + */ + private $oldNoEolAtEofIdx = -1; + + /** + * @var int the end index for the new if the new has no EOL at EOF + * -1 means the new has an EOL at EOF + */ + private $newNoEolAtEofIdx = -1; + + /** + * @var int the result of comparing the old and the new with the spaceship operator + * -1 means old < new, 0 means old == new, 1 means old > new + */ + private $oldNewComparison = 0; + + /** + * @var int[][][] array containing the generated opcodes for the differences between the two items + */ + private $groupedOpcodes = []; + + /** + * @var int[][][] array containing the generated opcodes for the differences between the two items (GNU version) + */ + private $groupedOpcodesGnu = []; + + /** + * @var array associative array of the default options available for the Differ class and their default value + */ + private static $defaultOptions = [ + // show how many neighbor lines + // Differ::CONTEXT_ALL can be used to show the whole file + 'context' => 3, + // ignore case difference + 'ignoreWhitespace' => false, + // ignore whitespace difference + 'ignoreCase' => false, + ]; + + /** + * The constructor. + * + * @param string[] $old array containing the lines of the old string to compare + * @param string[] $new array containing the lines for the new string to compare + * @param array $options the options + */ + public function __construct(array $old, array $new, array $options = []) + { + $this->sequenceMatcher = new SequenceMatcher([], []); + + $this->setOldNew($old, $new)->setOptions($options); + } + + /** + * Set old and new. + * + * @param string[] $old the old + * @param string[] $new the new + */ + public function setOldNew(array $old, array $new): self + { + return $this->setOld($old)->setNew($new); + } + + /** + * Set old. + * + * @param string[] $old the old + */ + public function setOld(array $old): self + { + if ($this->old !== $old) { + $this->old = $old; + $this->isCacheDirty = true; + } + + return $this; + } + + /** + * Set new. + * + * @param string[] $new the new + */ + public function setNew(array $new): self + { + if ($this->new !== $new) { + $this->new = $new; + $this->isCacheDirty = true; + } + + return $this; + } + + /** + * Set the options. + * + * @param array $options the options + */ + public function setOptions(array $options): self + { + $mergedOptions = $options + static::$defaultOptions; + + if ($this->options !== $mergedOptions) { + $this->options = $mergedOptions; + $this->isCacheDirty = true; + } + + return $this; + } + + /** + * Get a range of lines from $start to $end from the old. + * + * @param int $start the starting index (negative = count from backward) + * @param null|int $end the ending index (negative = count from backward) + * if is null, it returns a slice from $start to the end + * + * @return string[] array of all of the lines between the specified range + */ + public function getOld(int $start = 0, ?int $end = null): array + { + return Arr::getPartialByIndex($this->old, $start, $end); + } + + /** + * Get a range of lines from $start to $end from the new. + * + * @param int $start the starting index (negative = count from backward) + * @param null|int $end the ending index (negative = count from backward) + * if is null, it returns a slice from $start to the end + * + * @return string[] array of all of the lines between the specified range + */ + public function getNew(int $start = 0, ?int $end = null): array + { + return Arr::getPartialByIndex($this->new, $start, $end); + } + + /** + * Get the options. + * + * @return array the options + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * Get the old no EOL at EOF index. + * + * @return int the old no EOL at EOF index + */ + public function getOldNoEolAtEofIdx(): int + { + return $this->finalize()->oldNoEolAtEofIdx; + } + + /** + * Get the new no EOL at EOF index. + * + * @return int the new no EOL at EOF index + */ + public function getNewNoEolAtEofIdx(): int + { + return $this->finalize()->newNoEolAtEofIdx; + } + + /** + * Compare the old and the new with the spaceship operator. + */ + public function getOldNewComparison(): int + { + return $this->finalize()->oldNewComparison; + } + + /** + * Get the singleton. + */ + public static function getInstance(): self + { + static $singleton; + + return $singleton = $singleton ?? new static([], []); + } + + /** + * Gets the diff statistics such as inserted and deleted etc... + * + * @return array<string,float> the statistics + */ + public function getStatistics(): array + { + $ret = [ + 'inserted' => 0, + 'deleted' => 0, + 'unmodified' => 0, + 'changedRatio' => 0.0, + ]; + + foreach ($this->getGroupedOpcodes() as $hunk) { + foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) { + if ($op & (SequenceMatcher::OP_INS | SequenceMatcher::OP_REP)) { + $ret['inserted'] += $j2 - $j1; + } + if ($op & (SequenceMatcher::OP_DEL | SequenceMatcher::OP_REP)) { + $ret['deleted'] += $i2 - $i1; + } + } + } + + $ret['unmodified'] = $this->oldSrcLength - $ret['deleted']; + $ret['changedRatio'] = 1 - ($ret['unmodified'] / $this->oldSrcLength); + + return $ret; + } + + /** + * Generate a list of the compiled and grouped opcodes for the differences between the + * two strings. Generally called by the renderer, this class instantiates the sequence + * matcher and performs the actual diff generation and return an array of the opcodes + * for it. Once generated, the results are cached in the Differ class instance. + * + * @return int[][][] array of the grouped opcodes for the generated diff + */ + public function getGroupedOpcodes(): array + { + $this->finalize(); + + if (!empty($this->groupedOpcodes)) { + return $this->groupedOpcodes; + } + + $old = $this->old; + $new = $this->new; + $this->getGroupedOpcodesPre($old, $new); + + $opcodes = $this->sequenceMatcher + ->setSequences($old, $new) + ->getGroupedOpcodes($this->options['context']); + + $this->getGroupedOpcodesPost($opcodes); + + return $this->groupedOpcodes = $opcodes; + } + + /** + * A EOL-at-EOF-sensitive version of getGroupedOpcodes(). + * + * @return int[][][] array of the grouped opcodes for the generated diff (GNU version) + */ + public function getGroupedOpcodesGnu(): array + { + $this->finalize(); + + if (!empty($this->groupedOpcodesGnu)) { + return $this->groupedOpcodesGnu; + } + + $old = $this->old; + $new = $this->new; + $this->getGroupedOpcodesGnuPre($old, $new); + + $opcodes = $this->sequenceMatcher + ->setSequences($old, $new) + ->getGroupedOpcodes($this->options['context']); + + $this->getGroupedOpcodesGnuPost($opcodes); + + return $this->groupedOpcodesGnu = $opcodes; + } + + /** + * Triggered before getGroupedOpcodes(). May modify the $old and $new. + * + * @param string[] $old the old + * @param string[] $new the new + */ + private function getGroupedOpcodesPre(array &$old, array &$new): void + { + // append these lines to make sure the last block of the diff result is OP_EQ + static $eolAtEofHelperLines = [ + SequenceMatcher::APPENDED_HELPER_LINE, + SequenceMatcher::APPENDED_HELPER_LINE, + SequenceMatcher::APPENDED_HELPER_LINE, + SequenceMatcher::APPENDED_HELPER_LINE, + ]; + + $this->oldSrcLength = \count($old); + \array_push($old, ...$eolAtEofHelperLines); + + $this->newSrcLength = \count($new); + \array_push($new, ...$eolAtEofHelperLines); + } + + /** + * Triggered after getGroupedOpcodes(). May modify the $opcodes. + * + * @param int[][][] $opcodes the opcodes + */ + private function getGroupedOpcodesPost(array &$opcodes): void + { + // remove those extra lines cause by adding extra SequenceMatcher::APPENDED_HELPER_LINE lines + foreach ($opcodes as $hunkIdx => &$hunk) { + foreach ($hunk as $blockIdx => &$block) { + // range overflow + if ($block[1] > $this->oldSrcLength) { + $block[1] = $this->oldSrcLength; + } + if ($block[2] > $this->oldSrcLength) { + $block[2] = $this->oldSrcLength; + } + if ($block[3] > $this->newSrcLength) { + $block[3] = $this->newSrcLength; + } + if ($block[4] > $this->newSrcLength) { + $block[4] = $this->newSrcLength; + } + + // useless extra block? + /** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */ + if ($block[1] === $block[2] && $block[3] === $block[4]) { + unset($hunk[$blockIdx]); + } + } + + if (empty($hunk)) { + unset($opcodes[$hunkIdx]); + } + } + } + + /** + * Triggered before getGroupedOpcodesGnu(). May modify the $old and $new. + * + * @param string[] $old the old + * @param string[] $new the new + */ + private function getGroupedOpcodesGnuPre(array &$old, array &$new): void + { + /** + * Make the lines to be prepared for GNU-style diff. + * + * This method checks whether $lines has no EOL at EOF and append a special + * indicator to the last line. + * + * @param string[] $lines the lines created by simply explode("\n", $string) + */ + $createGnuCompatibleLines = static function (array $lines): array { + // note that the $lines should not be empty at this point + // they have at least one element "" in the array because explode("\n", "") === [""] + $lastLineIdx = \count($lines) - 1; + $lastLine = &$lines[$lastLineIdx]; + + if ($lastLine === '') { + // remove the last plain "" line since we don't need it anymore + // use array_slice() to also reset the array index + $lines = \array_slice($lines, 0, -1); + } else { + // this means the original source has no EOL at EOF + // we append a special indicator to that line so it no longer matches + $lastLine .= self::LINE_NO_EOL; + } + + return $lines; + }; + + $old = $createGnuCompatibleLines($old); + $new = $createGnuCompatibleLines($new); + + $this->getGroupedOpcodesPre($old, $new); + } + + /** + * Triggered after getGroupedOpcodesGnu(). May modify the $opcodes. + * + * @param int[][][] $opcodes the opcodes + */ + private function getGroupedOpcodesGnuPost(array &$opcodes): void + { + $this->getGroupedOpcodesPost($opcodes); + } + + /** + * Claim this class has settled down and we could calculate cached + * properties by current properties. + * + * This method must be called before accessing cached properties to + * make suer that you will not get a outdated cached value. + * + * @internal + */ + private function finalize(): self + { + if ($this->isCacheDirty) { + $this->resetCachedResults(); + + $this->oldNoEolAtEofIdx = $this->getOld(-1) === [''] ? -1 : \count($this->old); + $this->newNoEolAtEofIdx = $this->getNew(-1) === [''] ? -1 : \count($this->new); + $this->oldNewComparison = $this->old <=> $this->new; + + $this->sequenceMatcher->setOptions($this->options); + } + + return $this; + } + + /** + * Reset cached results. + */ + private function resetCachedResults(): self + { + foreach (static::CACHED_PROPERTIES as $property => $value) { + $this->{$property} = $value; + } + + $this->isCacheDirty = false; + + return $this; + } +} |