summaryrefslogtreecommitdiffstats
path: root/vendor/jfcherng/php-diff/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:38:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:38:42 +0000
commitc3ca98e1b35123f226c7f4c596b5dee78caa4223 (patch)
tree9b6eb109283da55e7d9064baa9fac795a40264cb /vendor/jfcherng/php-diff/src
parentInitial commit. (diff)
downloadicinga-php-thirdparty-upstream.tar.xz
icinga-php-thirdparty-upstream.zip
Adding upstream version 0.11.0.upstream/0.11.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/jfcherng/php-diff/src')
-rw-r--r--vendor/jfcherng/php-diff/src/DiffHelper.php184
-rw-r--r--vendor/jfcherng/php-diff/src/Differ.php502
-rw-r--r--vendor/jfcherng/php-diff/src/Exception/FileNotFoundException.php13
-rw-r--r--vendor/jfcherng/php-diff/src/Exception/UnsupportedFunctionException.php13
-rw-r--r--vendor/jfcherng/php-diff/src/Factory/LineRendererFactory.php59
-rw-r--r--vendor/jfcherng/php-diff/src/Factory/RendererFactory.php83
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/AbstractRenderer.php230
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/AbstractHtml.php365
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/Combined.php505
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/Inline.php259
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/Json.php14
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/JsonHtml.php68
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/AbstractLineRenderer.php105
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Char.php36
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Line.php81
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/LineRendererInterface.php20
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/None.php20
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Word.php110
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Html/SideBySide.php274
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/RendererConstant.php116
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/RendererInterface.php35
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Text/AbstractText.php145
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Text/Context.php163
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Text/JsonText.php81
-rw-r--r--vendor/jfcherng/php-diff/src/Renderer/Text/Unified.php147
-rw-r--r--vendor/jfcherng/php-diff/src/Utility/Arr.php47
-rw-r--r--vendor/jfcherng/php-diff/src/Utility/Language.php137
-rw-r--r--vendor/jfcherng/php-diff/src/Utility/ReverseIterator.php51
-rw-r--r--vendor/jfcherng/php-diff/src/Utility/Str.php30
-rw-r--r--vendor/jfcherng/php-diff/src/languages/bul.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/chs.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/cht.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/deu.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/eng.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/fra.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/ita.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/jpn.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/por.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/rus.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/spa.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/tur.json5
-rw-r--r--vendor/jfcherng/php-diff/src/languages/ukr.json5
42 files changed, 3958 insertions, 0 deletions
diff --git a/vendor/jfcherng/php-diff/src/DiffHelper.php b/vendor/jfcherng/php-diff/src/DiffHelper.php
new file mode 100644
index 0000000..7c7165f
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/DiffHelper.php
@@ -0,0 +1,184 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff;
+
+use Jfcherng\Diff\Factory\RendererFactory;
+use Jfcherng\Diff\Renderer\RendererConstant;
+
+final class DiffHelper
+{
+ /**
+ * The constructor.
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Get the absolute path of the project root directory.
+ */
+ public static function getProjectDirectory(): string
+ {
+ static $path;
+
+ return $path = $path ?? \realpath(__DIR__ . '/..');
+ }
+
+ /**
+ * Get the information about available renderers.
+ */
+ public static function getRenderersInfo(): array
+ {
+ static $info;
+
+ if (isset($info)) {
+ return $info;
+ }
+
+ $glob = \implode(\DIRECTORY_SEPARATOR, [
+ static::getProjectDirectory(),
+ 'src',
+ 'Renderer',
+ '{' . \implode(',', RendererConstant::RENDERER_TYPES) . '}',
+ '*.php',
+ ]);
+
+ $fileNames = \array_map(
+ // get basename without file extension
+ function (string $file): string {
+ return \pathinfo($file, \PATHINFO_FILENAME);
+ },
+ // paths of all Renderer files
+ \glob($glob, \GLOB_BRACE)
+ );
+
+ $renderers = \array_filter(
+ $fileNames,
+ // only normal class files are wanted
+ function (string $fileName): bool {
+ return
+ \substr($fileName, 0, 8) !== 'Abstract'
+ && \substr($fileName, -9) !== 'Interface'
+ && \substr($fileName, -5) !== 'Trait';
+ }
+ );
+
+ $info = [];
+ foreach ($renderers as $renderer) {
+ $info[$renderer] = RendererFactory::resolveRenderer($renderer)::INFO;
+ }
+
+ return $info;
+ }
+
+ /**
+ * Get the available renderers.
+ *
+ * @return string[] the available renderers
+ */
+ public static function getAvailableRenderers(): array
+ {
+ return \array_keys(self::getRenderersInfo());
+ }
+
+ /**
+ * Get the content of the CSS style sheet for HTML renderers.
+ *
+ * @throws \LogicException path is a directory
+ * @throws \RuntimeException path cannot be opened
+ */
+ public static function getStyleSheet(): string
+ {
+ static $fileContent;
+
+ if (isset($fileContent)) {
+ return $fileContent;
+ }
+
+ $filePath = static::getProjectDirectory() . '/example/diff-table.css';
+
+ $file = new \SplFileObject($filePath, 'r');
+
+ return $fileContent = $file->fread($file->getSize());
+ }
+
+ /**
+ * Gets the diff statistics such as inserted and deleted etc...
+ *
+ * @return array<string,float> the statistics
+ */
+ public static function getStatistics(): array
+ {
+ return Differ::getInstance()->getStatistics();
+ }
+
+ /**
+ * All-in-one static method to calculate the diff between two strings (or arrays of strings).
+ *
+ * @param string|string[] $old the old string (or array of lines)
+ * @param string|string[] $new the new string (or array of lines)
+ * @param string $renderer the renderer name
+ * @param array $differOptions the options for Differ object
+ * @param array $rendererOptions the options for renderer object
+ *
+ * @return string the rendered differences
+ */
+ public static function calculate(
+ $old,
+ $new,
+ string $renderer = 'Unified',
+ array $differOptions = [],
+ array $rendererOptions = []
+ ): string {
+ // always convert into array form
+ \is_string($old) && ($old = \explode("\n", $old));
+ \is_string($new) && ($new = \explode("\n", $new));
+
+ return RendererFactory::getInstance($renderer)
+ ->setOptions($rendererOptions)
+ ->render(
+ Differ::getInstance()
+ ->setOldNew($old, $new)
+ ->setOptions($differOptions)
+ );
+ }
+
+ /**
+ * All-in-one static method to calculate the diff between two files.
+ *
+ * @param string $old the path of the old file
+ * @param string $new the path of the new file
+ * @param string $renderer the renderer name
+ * @param array $differOptions the options for Differ object
+ * @param array $rendererOptions the options for renderer object
+ *
+ * @throws \LogicException path is a directory
+ * @throws \RuntimeException path cannot be opened
+ *
+ * @return string the rendered differences
+ */
+ public static function calculateFiles(
+ string $old,
+ string $new,
+ string $renderer = 'Unified',
+ array $differOptions = [],
+ array $rendererOptions = []
+ ): string {
+ // we want to leave the line-ending problem to static::calculate()
+ // so do not set SplFileObject::DROP_NEW_LINE flag
+ // otherwise, we will lose \r if the line-ending is \r\n
+ $oldFile = new \SplFileObject($old, 'r');
+ $newFile = new \SplFileObject($new, 'r');
+
+ return static::calculate(
+ // fread() requires the length > 0 hence we plus 1 for empty files
+ $oldFile->fread($oldFile->getSize() + 1),
+ $newFile->fread($newFile->getSize() + 1),
+ $renderer,
+ $differOptions,
+ $rendererOptions
+ );
+ }
+}
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;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Exception/FileNotFoundException.php b/vendor/jfcherng/php-diff/src/Exception/FileNotFoundException.php
new file mode 100644
index 0000000..9a9ed8f
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Exception/FileNotFoundException.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Exception;
+
+final class FileNotFoundException extends \Exception
+{
+ public function __construct(string $filepath = '', int $code = 0, \Throwable $previous = null)
+ {
+ parent::__construct("File not found: {$filepath}", $code, $previous);
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Exception/UnsupportedFunctionException.php b/vendor/jfcherng/php-diff/src/Exception/UnsupportedFunctionException.php
new file mode 100644
index 0000000..e2d4fed
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Exception/UnsupportedFunctionException.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Exception;
+
+final class UnsupportedFunctionException extends \Exception
+{
+ public function __construct(string $funcName = '', int $code = 0, \Throwable $previous = null)
+ {
+ parent::__construct("Unsupported function: {$funcName}", $code, $previous);
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Factory/LineRendererFactory.php b/vendor/jfcherng/php-diff/src/Factory/LineRendererFactory.php
new file mode 100644
index 0000000..de7438c
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Factory/LineRendererFactory.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Factory;
+
+use Jfcherng\Diff\Renderer\Html\LineRenderer\AbstractLineRenderer;
+use Jfcherng\Diff\Renderer\RendererConstant;
+
+final class LineRendererFactory
+{
+ /**
+ * Instances of line renderers.
+ *
+ * @var AbstractLineRenderer[]
+ */
+ private static $singletons = [];
+
+ /**
+ * The constructor.
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Get the singleton of a line renderer.
+ *
+ * @param string $type the type
+ * @param mixed ...$ctorArgs the constructor arguments
+ */
+ public static function getInstance(string $type, ...$ctorArgs): AbstractLineRenderer
+ {
+ if (!isset(self::$singletons[$type])) {
+ self::$singletons[$type] = self::make($type, ...$ctorArgs);
+ }
+
+ return self::$singletons[$type];
+ }
+
+ /**
+ * Make a new instance of a line renderer.
+ *
+ * @param string $type the type
+ * @param mixed ...$ctorArgs the constructor arguments
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function make(string $type, ...$ctorArgs): AbstractLineRenderer
+ {
+ $className = RendererConstant::RENDERER_NAMESPACE . '\\Html\\LineRenderer\\' . \ucfirst($type);
+
+ if (!\class_exists($className)) {
+ throw new \InvalidArgumentException("LineRenderer not found: {$type}");
+ }
+
+ return new $className(...$ctorArgs);
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Factory/RendererFactory.php b/vendor/jfcherng/php-diff/src/Factory/RendererFactory.php
new file mode 100644
index 0000000..7f1b1d1
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Factory/RendererFactory.php
@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Factory;
+
+use Jfcherng\Diff\Renderer\AbstractRenderer;
+use Jfcherng\Diff\Renderer\RendererConstant;
+
+final class RendererFactory
+{
+ /**
+ * Instances of renderers.
+ *
+ * @var AbstractRenderer[]
+ */
+ private static $singletons = [];
+
+ /**
+ * The constructor.
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Get the singleton of a renderer.
+ *
+ * @param string $renderer the renderer
+ * @param mixed ...$ctorArgs the constructor arguments
+ */
+ public static function getInstance(string $renderer, ...$ctorArgs): AbstractRenderer
+ {
+ if (!isset(self::$singletons[$renderer])) {
+ self::$singletons[$renderer] = self::make($renderer, ...$ctorArgs);
+ }
+
+ return self::$singletons[$renderer];
+ }
+
+ /**
+ * Make a new instance of a renderer.
+ *
+ * @param string $renderer the renderer
+ * @param mixed ...$ctorArgs the constructor arguments
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function make(string $renderer, ...$ctorArgs): AbstractRenderer
+ {
+ $className = self::resolveRenderer($renderer);
+
+ if (!isset($className)) {
+ throw new \InvalidArgumentException("Renderer not found: {$renderer}");
+ }
+
+ return new $className(...$ctorArgs);
+ }
+
+ /**
+ * Resolve the renderer name into a FQCN.
+ *
+ * @param string $renderer the renderer
+ */
+ public static function resolveRenderer(string $renderer): ?string
+ {
+ static $cache = [];
+
+ if (isset($cache[$renderer])) {
+ return $cache[$renderer];
+ }
+
+ foreach (RendererConstant::RENDERER_TYPES as $type) {
+ $className = RendererConstant::RENDERER_NAMESPACE . "\\{$type}\\{$renderer}";
+
+ if (\class_exists($className)) {
+ return $cache[$renderer] = $className;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/AbstractRenderer.php b/vendor/jfcherng/php-diff/src/Renderer/AbstractRenderer.php
new file mode 100644
index 0000000..77477c8
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/AbstractRenderer.php
@@ -0,0 +1,230 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer;
+
+use Jfcherng\Diff\Differ;
+use Jfcherng\Diff\SequenceMatcher;
+use Jfcherng\Diff\Utility\Language;
+
+/**
+ * Base class for diff renderers.
+ */
+abstract class AbstractRenderer implements RendererInterface
+{
+ /**
+ * @var array information about this renderer
+ */
+ public const INFO = [
+ 'desc' => 'default_desc',
+ 'type' => 'default_type',
+ ];
+
+ /**
+ * @var bool Is this renderer pure text?
+ */
+ public const IS_TEXT_RENDERER = true;
+
+ /**
+ * @var string[] array of the opcodes and their corresponding symbols
+ */
+ public const SYMBOL_MAP = [
+ SequenceMatcher::OP_DEL => '-',
+ SequenceMatcher::OP_EQ => ' ',
+ SequenceMatcher::OP_INS => '+',
+ SequenceMatcher::OP_REP => '!',
+ ];
+
+ /**
+ * @var Language the language translation object
+ */
+ protected $t;
+
+ /**
+ * @var array array of the default options that apply to this renderer
+ */
+ protected static $defaultOptions = [
+ // how detailed the rendered HTML in-line diff is? (none, line, word, char)
+ 'detailLevel' => 'line',
+ // renderer language: eng, cht, chs, jpn, ...
+ // or an array which has the same keys with a language file
+ 'language' => 'eng',
+ // show line numbers in HTML renderers
+ 'lineNumbers' => true,
+ // show a separator between different diff hunks in HTML renderers
+ 'separateBlock' => true,
+ // show the (table) header
+ 'showHeader' => true,
+ // the frontend HTML could use CSS "white-space: pre;" to visualize consecutive whitespaces
+ // but if you want to visualize them in the backend with "&nbsp;", you can set this to true
+ 'spacesToNbsp' => false,
+ // HTML renderer tab width (negative = do not convert into spaces)
+ 'tabSize' => 4,
+ // this option is currently only for the Combined renderer.
+ // it determines whether a replace-type block should be merged or not
+ // depending on the content changed ratio, which values between 0 and 1.
+ 'mergeThreshold' => 0.8,
+ // this option is currently only for the Unified and the Context renderers.
+ // RendererConstant::CLI_COLOR_AUTO = colorize the output if possible (default)
+ // RendererConstant::CLI_COLOR_ENABLE = force to colorize the output
+ // RendererConstant::CLI_COLOR_DISABLE = force not to colorize the output
+ 'cliColorization' => RendererConstant::CLI_COLOR_AUTO,
+ // this option is currently only for the Json renderer.
+ // internally, ops (tags) are all int type but this is not good for human reading.
+ // set this to "true" to convert them into string form before outputting.
+ 'outputTagAsString' => false,
+ // this option is currently only for the Json renderer.
+ // it controls how the output JSON is formatted.
+ // see availabe options on https://www.php.net/manual/en/function.json-encode.php
+ 'jsonEncodeFlags' => \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE,
+ // this option is currently effective when the "detailLevel" is "word"
+ // characters listed in this array can be used to make diff segments into a whole
+ // for example, making "<del>good</del>-<del>looking</del>" into "<del>good-looking</del>"
+ // this should bring better readability but set this to empty array if you do not want it
+ 'wordGlues' => ['-', ' '],
+ // change this value to a string as the returned diff if the two input strings are identical
+ 'resultForIdenticals' => null,
+ // extra HTML classes added to the DOM of the diff container
+ 'wrapperClasses' => ['diff-wrapper'],
+ ];
+
+ /**
+ * @var array array containing the user applied and merged default options for the renderer
+ */
+ protected $options = [];
+
+ /**
+ * The constructor. Instantiates the rendering engine and if options are passed,
+ * sets the options for the renderer.
+ *
+ * @param array $options optionally, an array of the options for the renderer
+ */
+ public function __construct(array $options = [])
+ {
+ $this->setOptions($options);
+ }
+
+ /**
+ * Set the options of the renderer to those supplied in the passed in array.
+ * Options are merged with the default to ensure that there aren't any missing
+ * options.
+ *
+ * @param array $options the options
+ *
+ * @return static
+ */
+ public function setOptions(array $options): self
+ {
+ $newOptions = $options + static::$defaultOptions;
+
+ $this->updateLanguage(
+ $this->options['language'] ?? '',
+ $newOptions['language']
+ );
+
+ $this->options = $newOptions;
+
+ return $this;
+ }
+
+ /**
+ * Get the options.
+ *
+ * @return array the options
+ */
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @final
+ *
+ * @todo mark this method with "final" in the next major release
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function getResultForIdenticals(): string
+ {
+ $custom = $this->options['resultForIdenticals'];
+
+ if (isset($custom) && !\is_string($custom)) {
+ throw new \InvalidArgumentException('renderer option `resultForIdenticals` must be null or string.');
+ }
+
+ return $custom ?? $this->getResultForIdenticalsDefault();
+ }
+
+ /**
+ * Get the renderer default result when the old and the new are the same.
+ */
+ abstract public function getResultForIdenticalsDefault(): string;
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function render(Differ $differ): string
+ {
+ // the "no difference" situation may happen frequently
+ return $differ->getOldNewComparison() === 0
+ ? $this->getResultForIdenticals()
+ : $this->renderWorker($differ);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ final public function renderArray(array $differArray): string
+ {
+ return $this->renderArrayWorker($differArray);
+ }
+
+ /**
+ * The real worker for self::render().
+ *
+ * @param Differ $differ the differ object
+ */
+ abstract protected function renderWorker(Differ $differ): string;
+
+ /**
+ * The real worker for self::renderArray().
+ *
+ * @param array[][] $differArray the differ array
+ */
+ abstract protected function renderArrayWorker(array $differArray): string;
+
+ /**
+ * Update the Language object.
+ *
+ * @param string|string[] $old the old language
+ * @param string|string[] $new the new language
+ *
+ * @return static
+ */
+ protected function updateLanguage($old, $new): self
+ {
+ if (!isset($this->t) || $old !== $new) {
+ $this->t = new Language($new);
+ }
+
+ return $this;
+ }
+
+ /**
+ * A shorthand to do translation.
+ *
+ * @param string $text The text
+ * @param bool $escapeHtml Escape the translated text for HTML?
+ *
+ * @return string the translated text
+ */
+ protected function _(string $text, bool $escapeHtml = true): string
+ {
+ $text = $this->t->translate($text);
+
+ return $escapeHtml ? \htmlspecialchars($text) : $text;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/AbstractHtml.php b/vendor/jfcherng/php-diff/src/Renderer/Html/AbstractHtml.php
new file mode 100644
index 0000000..49d8b4d
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/AbstractHtml.php
@@ -0,0 +1,365 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html;
+
+use Jfcherng\Diff\Differ;
+use Jfcherng\Diff\Factory\LineRendererFactory;
+use Jfcherng\Diff\Renderer\AbstractRenderer;
+use Jfcherng\Diff\Renderer\Html\LineRenderer\AbstractLineRenderer;
+use Jfcherng\Diff\Renderer\RendererConstant;
+use Jfcherng\Diff\SequenceMatcher;
+use Jfcherng\Utility\MbString;
+
+/**
+ * Base renderer for rendering HTML-based diffs.
+ */
+abstract class AbstractHtml extends AbstractRenderer
+{
+ /**
+ * @var bool is this renderer pure text?
+ */
+ public const IS_TEXT_RENDERER = false;
+
+ /**
+ * @var string[] array of the different opcodes and how they are mapped to HTML classes
+ *
+ * @todo rename to OP_CLASS_MAP in v7
+ */
+ public const TAG_CLASS_MAP = [
+ SequenceMatcher::OP_DEL => 'del',
+ SequenceMatcher::OP_EQ => 'eq',
+ SequenceMatcher::OP_INS => 'ins',
+ SequenceMatcher::OP_REP => 'rep',
+ ];
+
+ /**
+ * Auto format the content in "changes" to be suitable for HTML output.
+ *
+ * This may not be a wanted behavior for some (custom) renderers
+ * if they want to do this by themselves in a later stage.
+ *
+ * @var bool
+ */
+ public const AUTO_FORMAT_CHANGES = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResultForIdenticalsDefault(): string
+ {
+ return '';
+ }
+
+ /**
+ * Render and return an array structure suitable for generating HTML
+ * based differences. Generally called by subclasses that generate a
+ * HTML based diff and return an array of the changes to show in the diff.
+ *
+ * @param Differ $differ the differ object
+ *
+ * @return array[][] generated changes, suitable for presentation in HTML
+ */
+ public function getChanges(Differ $differ): array
+ {
+ $lineRenderer = LineRendererFactory::make(
+ $this->options['detailLevel'],
+ $differ->getOptions(),
+ $this->options
+ );
+
+ $old = $differ->getOld();
+ $new = $differ->getNew();
+
+ $changes = [];
+
+ foreach ($differ->getGroupedOpcodes() as $hunk) {
+ $change = [];
+
+ foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
+ $change[] = $this->getDefaultBlock($op, $i1, $j1);
+ $block = &$change[\count($change) - 1];
+
+ // if there are same amount of lines replaced
+ // we can render the inner detailed changes with corresponding lines
+ // @todo or use LineRenderer to do the job regardless different line counts?
+ if ($op === SequenceMatcher::OP_REP && $i2 - $i1 === $j2 - $j1) {
+ for ($k = $i2 - $i1 - 1; $k >= 0; --$k) {
+ $this->renderChangedExtent($lineRenderer, $old[$i1 + $k], $new[$j1 + $k]);
+ }
+ }
+
+ $block['old']['lines'] = \array_slice($old, $i1, $i2 - $i1);
+ $block['new']['lines'] = \array_slice($new, $j1, $j2 - $j1);
+ }
+ unset($block);
+
+ $changes[] = $change;
+ }
+
+ if (static::AUTO_FORMAT_CHANGES) {
+ $this->formatChanges($changes);
+ }
+
+ return $changes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function renderWorker(Differ $differ): string
+ {
+ $rendered = $this->redererChanges($this->getChanges($differ));
+
+ return $this->cleanUpDummyHtmlClosures($rendered);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function renderArrayWorker(array $differArray): string
+ {
+ $this->ensureChangesUseIntTag($differArray);
+
+ $rendered = $this->redererChanges($differArray);
+
+ return $this->cleanUpDummyHtmlClosures($rendered);
+ }
+
+ /**
+ * Render the array of changes.
+ *
+ * @param array[][] $changes the changes
+ *
+ * @todo rename typo to renderChanges() in v7
+ */
+ abstract protected function redererChanges(array $changes): string;
+
+ /**
+ * Renderer the changed extent.
+ *
+ * @param AbstractLineRenderer $lineRenderer the line renderer
+ * @param string $old the old line
+ * @param string $new the new line
+ */
+ protected function renderChangedExtent(AbstractLineRenderer $lineRenderer, string &$old, string &$new): void
+ {
+ static $mbOld, $mbNew;
+
+ $mbOld = $mbOld ?? new MbString();
+ $mbNew = $mbNew ?? new MbString();
+
+ $mbOld->set($old);
+ $mbNew->set($new);
+
+ $lineRenderer->render($mbOld, $mbNew);
+
+ $old = $mbOld->get();
+ $new = $mbNew->get();
+ }
+
+ /**
+ * Get the default block.
+ *
+ * @param int $op the operation
+ * @param int $i1 begin index of the diff of the old array
+ * @param int $j1 begin index of the diff of the new array
+ *
+ * @return array the default block
+ *
+ * @todo rename tag to op in v7
+ */
+ protected function getDefaultBlock(int $op, int $i1, int $j1): array
+ {
+ return [
+ 'tag' => $op,
+ 'old' => [
+ 'offset' => $i1,
+ 'lines' => [],
+ ],
+ 'new' => [
+ 'offset' => $j1,
+ 'lines' => [],
+ ],
+ ];
+ }
+
+ /**
+ * Make the content in "changes" suitable for HTML output.
+ *
+ * @param array[][] $changes the changes
+ */
+ final protected function formatChanges(array &$changes): void
+ {
+ foreach ($changes as &$hunk) {
+ foreach ($hunk as &$block) {
+ $block['old']['lines'] = $this->formatLines($block['old']['lines']);
+ $block['new']['lines'] = $this->formatLines($block['new']['lines']);
+
+ /** @phan-suppress-next-line PhanTypeInvalidLeftOperandOfBitwiseOp */
+ if ($block['tag'] & (SequenceMatcher::OP_REP | SequenceMatcher::OP_DEL)) {
+ $block['old']['lines'] = \str_replace(
+ RendererConstant::HTML_CLOSURES,
+ RendererConstant::HTML_CLOSURES_DEL,
+ $block['old']['lines']
+ );
+ }
+
+ /** @phan-suppress-next-line PhanTypeInvalidLeftOperandOfBitwiseOp */
+ if ($block['tag'] & (SequenceMatcher::OP_REP | SequenceMatcher::OP_INS)) {
+ $block['new']['lines'] = \str_replace(
+ RendererConstant::HTML_CLOSURES,
+ RendererConstant::HTML_CLOSURES_INS,
+ $block['new']['lines']
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Make a series of lines suitable for outputting in a HTML rendered diff.
+ *
+ * @param string[] $lines array of lines to format
+ *
+ * @return string[] array of the formatted lines
+ */
+ protected function formatLines(array $lines): array
+ {
+ /**
+ * To prevent from invoking the same function calls for several times,
+ * we can glue lines into a string and call functions for one time.
+ * After that, we split the string back into lines.
+ */
+ return \explode(
+ RendererConstant::IMPLODE_DELIMITER,
+ $this->formatStringFromLines(
+ \implode(
+ RendererConstant::IMPLODE_DELIMITER,
+ $lines
+ )
+ )
+ );
+ }
+
+ /**
+ * Make a string suitable for outputting in a HTML rendered diff.
+ *
+ * This my involve replacing tab characters with spaces, making the HTML safe
+ * for output, ensuring that double spaces are replaced with &nbsp; etc.
+ *
+ * @param string $string the string of imploded lines
+ *
+ * @return string the formatted string
+ */
+ protected function formatStringFromLines(string $string): string
+ {
+ $string = $this->expandTabs($string, $this->options['tabSize']);
+ $string = $this->htmlSafe($string);
+
+ if ($this->options['spacesToNbsp']) {
+ $string = $this->htmlFixSpaces($string);
+ }
+
+ return $string;
+ }
+
+ /**
+ * Replace tabs in a string with a number of spaces.
+ *
+ * @param string $string the input string which may contain tabs
+ * @param int $tabSize one tab = how many spaces, a negative does nothing
+ * @param bool $onlyLeadingTabs only expand leading tabs
+ *
+ * @return string the string with the tabs converted to spaces
+ */
+ protected function expandTabs(string $string, int $tabSize = 4, bool $onlyLeadingTabs = false): string
+ {
+ if ($tabSize < 0) {
+ return $string;
+ }
+
+ if ($onlyLeadingTabs) {
+ return \preg_replace_callback(
+ "/^[ \t]{1,}/mS", // tabs and spaces may be mixed
+ function (array $matches) use ($tabSize): string {
+ return \str_replace("\t", \str_repeat(' ', $tabSize), $matches[0]);
+ },
+ $string
+ );
+ }
+
+ return \str_replace("\t", \str_repeat(' ', $tabSize), $string);
+ }
+
+ /**
+ * Make a string containing HTML safe for output on a page.
+ *
+ * @param string $string the string
+ *
+ * @return string the string with the HTML characters replaced by entities
+ */
+ protected function htmlSafe(string $string): string
+ {
+ return \htmlspecialchars($string, \ENT_NOQUOTES, 'UTF-8');
+ }
+
+ /**
+ * Replace a string containing spaces with a HTML representation having "&nbsp;".
+ *
+ * @param string $string the string of spaces
+ *
+ * @return string the HTML representation of the string
+ */
+ protected function htmlFixSpaces(string $string): string
+ {
+ return \preg_replace_callback(
+ '/ {2,}/S', // only fix for more than 1 space
+ function (array $matches): string {
+ $count = \strlen($matches[0]);
+
+ return \str_repeat(' &nbsp;', $count >> 1) . ($count & 1 ? ' ' : '');
+ },
+ $string
+ );
+ }
+
+ /**
+ * Make sure the "changes" array uses int "tag".
+ *
+ * Internally, we would like always int form for better performance.
+ *
+ * @param array[][] $changes the changes
+ */
+ protected function ensureChangesUseIntTag(array &$changes): void
+ {
+ // check if the tag is already int type
+ if (\is_int($changes[0][0]['tag'] ?? null)) {
+ return;
+ }
+
+ foreach ($changes as &$hunks) {
+ foreach ($hunks as &$block) {
+ $block['tag'] = SequenceMatcher::opStrToInt($block['tag']);
+ }
+ }
+ }
+
+ /**
+ * Clean up empty HTML closures in the given string.
+ *
+ * @param string $string the string
+ */
+ protected function cleanUpDummyHtmlClosures(string $string): string
+ {
+ return \str_replace(
+ [
+ RendererConstant::HTML_CLOSURES_DEL[0] . RendererConstant::HTML_CLOSURES_DEL[1],
+ RendererConstant::HTML_CLOSURES_INS[0] . RendererConstant::HTML_CLOSURES_INS[1],
+ ],
+ '',
+ $string
+ );
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/Combined.php b/vendor/jfcherng/php-diff/src/Renderer/Html/Combined.php
new file mode 100644
index 0000000..4baab68
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/Combined.php
@@ -0,0 +1,505 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html;
+
+use Jfcherng\Diff\Factory\LineRendererFactory;
+use Jfcherng\Diff\Renderer\RendererConstant;
+use Jfcherng\Diff\SequenceMatcher;
+use Jfcherng\Diff\Utility\ReverseIterator;
+use Jfcherng\Utility\MbString;
+
+/**
+ * Combined HTML diff generator.
+ *
+ * Note that this renderer always has no line number.
+ */
+final class Combined extends AbstractHtml
+{
+ /**
+ * {@inheritdoc}
+ */
+ public const INFO = [
+ 'desc' => 'Combined',
+ 'type' => 'Html',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public const AUTO_FORMAT_CHANGES = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function redererChanges(array $changes): string
+ {
+ if (empty($changes)) {
+ return $this->getResultForIdenticals();
+ }
+
+ $wrapperClasses = \array_merge(
+ $this->options['wrapperClasses'],
+ ['diff', 'diff-html', 'diff-combined']
+ );
+
+ return
+ '<table class="' . \implode(' ', $wrapperClasses) . '">' .
+ $this->renderTableHeader() .
+ $this->renderTableHunks($changes) .
+ '</table>';
+ }
+
+ /**
+ * Renderer the table header.
+ */
+ protected function renderTableHeader(): string
+ {
+ if (!$this->options['showHeader']) {
+ return '';
+ }
+
+ return
+ '<thead>' .
+ '<tr>' .
+ '<th>' . $this->_('differences') . '</th>' .
+ '</tr>' .
+ '</thead>';
+ }
+
+ /**
+ * Renderer the table separate block.
+ */
+ protected function renderTableSeparateBlock(): string
+ {
+ return
+ '<tbody class="skipped">' .
+ '<tr>' .
+ '<td></td>' .
+ '</tr>' .
+ '</tbody>';
+ }
+
+ /**
+ * Renderer table hunks.
+ *
+ * @param array[][] $hunks each hunk has many blocks
+ */
+ protected function renderTableHunks(array $hunks): string
+ {
+ $ret = '';
+
+ foreach ($hunks as $i => $hunk) {
+ if ($i > 0 && $this->options['separateBlock']) {
+ $ret .= $this->renderTableSeparateBlock();
+ }
+
+ foreach ($hunk as $block) {
+ $ret .= $this->renderTableBlock($block);
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlock(array $block): string
+ {
+ static $callbacks = [
+ SequenceMatcher::OP_EQ => 'renderTableBlockEqual',
+ SequenceMatcher::OP_INS => 'renderTableBlockInsert',
+ SequenceMatcher::OP_DEL => 'renderTableBlockDelete',
+ SequenceMatcher::OP_REP => 'renderTableBlockReplace',
+ ];
+
+ return
+ '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' .
+ $this->{$callbacks[$block['tag']]}($block) .
+ '</tbody>';
+ }
+
+ /**
+ * Renderer the table block: equal.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockEqual(array $block): string
+ {
+ $block['new']['lines'] = $this->customFormatLines(
+ $block['new']['lines'],
+ SequenceMatcher::OP_EQ
+ );
+
+ $ret = '';
+
+ // 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
+ foreach ($block['new']['lines'] as $newLine) {
+ // we could only pick either the old or the new to show
+ // here we pick the new one to let the user know what it is now
+ $ret .= $this->renderTableRow('new', SequenceMatcher::OP_EQ, $newLine);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: insert.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockInsert(array $block): string
+ {
+ $block['new']['lines'] = $this->customFormatLines(
+ $block['new']['lines'],
+ SequenceMatcher::OP_INS
+ );
+
+ $ret = '';
+
+ foreach ($block['new']['lines'] as $newLine) {
+ $ret .= $this->renderTableRow('new', SequenceMatcher::OP_INS, $newLine);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: delete.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockDelete(array $block): string
+ {
+ $block['old']['lines'] = $this->customFormatLines(
+ $block['old']['lines'],
+ SequenceMatcher::OP_DEL
+ );
+
+ $ret = '';
+
+ foreach ($block['old']['lines'] as $oldLine) {
+ $ret .= $this->renderTableRow('old', SequenceMatcher::OP_DEL, $oldLine);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: replace.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockReplace(array $block): string
+ {
+ if ($this->options['detailLevel'] === 'none') {
+ return
+ $this->renderTableBlockDelete($block) .
+ $this->renderTableBlockInsert($block);
+ }
+
+ $ret = '';
+
+ $oldLines = $block['old']['lines'];
+ $newLines = $block['new']['lines'];
+
+ $oldLinesCount = \count($oldLines);
+ $newLinesCount = \count($newLines);
+
+ // if the line counts changes, we treat the old and the new as
+ // "a line with \n in it" and then do one-line-to-one-line diff
+ if ($oldLinesCount !== $newLinesCount) {
+ [$oldLines, $newLines] = $this->markReplaceBlockDiff($oldLines, $newLines);
+ $oldLinesCount = $newLinesCount = 1;
+ }
+
+ $oldLines = $this->customFormatLines($oldLines, SequenceMatcher::OP_DEL);
+ $newLines = $this->customFormatLines($newLines, SequenceMatcher::OP_INS);
+
+ // now $oldLines must has the same line counts with $newlines
+ for ($no = 0; $no < $newLinesCount; ++$no) {
+ $mergedLine = $this->mergeReplaceLines($oldLines[$no], $newLines[$no]);
+
+ // not merge-able, we fall back to separated form
+ if (!isset($mergedLine)) {
+ $ret .=
+ $this->renderTableBlockDelete($block) .
+ $this->renderTableBlockInsert($block);
+
+ continue;
+ }
+
+ $ret .= $this->renderTableRow('rep', SequenceMatcher::OP_REP, $mergedLine);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer a content row of the output table.
+ *
+ * @param string $tdClass the <td> class
+ * @param int $op the operation
+ * @param string $line the line
+ */
+ protected function renderTableRow(string $tdClass, int $op, string $line): string
+ {
+ return
+ '<tr data-type="' . self::SYMBOL_MAP[$op] . '">' .
+ '<td class="' . $tdClass . '">' . $line . '</td>' .
+ '</tr>';
+ }
+
+ /**
+ * Merge two "replace"-type lines into a single line.
+ *
+ * The implementation concept is that if we remove all closure parts from
+ * the old and the new, the rest of them (cleaned line) should be the same.
+ * And then, we add back those removed closure parts in a correct order.
+ *
+ * @param string $oldLine the old line
+ * @param string $newLine the new line
+ *
+ * @return null|string string if merge-able, null otherwise
+ */
+ protected function mergeReplaceLines(string $oldLine, string $newLine): ?string
+ {
+ $delParts = $this->analyzeClosureParts(
+ $oldLine,
+ RendererConstant::HTML_CLOSURES_DEL,
+ SequenceMatcher::OP_DEL
+ );
+ $insParts = $this->analyzeClosureParts(
+ $newLine,
+ RendererConstant::HTML_CLOSURES_INS,
+ SequenceMatcher::OP_INS
+ );
+
+ // get the cleaned line by a non-regex way (should be faster)
+ // i.e., the new line with all "<ins>...</ins>" parts removed
+ $mergedLine = $newLine;
+ foreach (ReverseIterator::fromArray($insParts) as $part) {
+ $mergedLine = \substr_replace(
+ $mergedLine,
+ '', // deletion
+ $part['offset'],
+ \strlen($part['content'])
+ );
+ }
+
+ // note that $mergedLine is actually a clean line at this point
+ if (!$this->isLinesMergeable($oldLine, $newLine, $mergedLine)) {
+ return null;
+ }
+
+ // before building the $mergedParts, we do some adjustments
+ $this->revisePartsForBoundaryNewlines($delParts, RendererConstant::HTML_CLOSURES_DEL);
+ $this->revisePartsForBoundaryNewlines($insParts, RendererConstant::HTML_CLOSURES_INS);
+
+ // create a sorted merged parts array
+ $mergedParts = \array_merge($delParts, $insParts);
+ \usort($mergedParts, function (array $a, array $b): int {
+ // first sort by "offsetClean", "order" then by "type"
+ return $a['offsetClean'] <=> $b['offsetClean']
+ ?: $a['order'] <=> $b['order']
+ ?: ($a['type'] === SequenceMatcher::OP_DEL ? -1 : 1);
+ });
+
+ // insert merged parts into the cleaned line
+ foreach (ReverseIterator::fromArray($mergedParts) as $part) {
+ $mergedLine = \substr_replace(
+ $mergedLine,
+ $part['content'],
+ $part['offsetClean'],
+ 0 // insertion
+ );
+ }
+
+ return \str_replace("\n", '<br>', $mergedLine);
+ }
+
+ /**
+ * Analyze and get the closure parts information of the line.
+ *
+ * Such as
+ * extract informations for "<ins>part 1</ins>" and "<ins>part 2</ins>"
+ * from "Hello <ins>part 1</ins>SOME OTHER TEXT<ins>part 2</ins> World"
+ *
+ * @param string $line the line
+ * @param string[] $closures the closures
+ * @param int $type the type
+ *
+ * @return array[] the closure informations
+ */
+ protected function analyzeClosureParts(string $line, array $closures, int $type): array
+ {
+ [$ld, $rd] = $closures;
+
+ $ldLength = \strlen($ld);
+ $rdLength = \strlen($rd);
+
+ $parts = [];
+ $partStart = $partEnd = 0;
+ $partLengthSum = 0;
+
+ // find the next left delimiter
+ while (false !== ($partStart = \strpos($line, $ld, $partEnd))) {
+ // find the corresponding right delimiter
+ if (false === ($partEnd = \strpos($line, $rd, $partStart + $ldLength))) {
+ break;
+ }
+
+ $partEnd += $rdLength;
+ $partLength = $partEnd - $partStart;
+
+ $parts[] = [
+ 'type' => $type,
+ // the sorting order used when both "offsetClean" are the same
+ 'order' => 0,
+ // the offset in the line
+ 'offset' => $partStart,
+ // the offset in the cleaned line (i.e., the line with closure parts removed)
+ 'offsetClean' => $partStart - $partLengthSum,
+ // the content of the part
+ 'content' => \substr($line, $partStart, $partLength),
+ ];
+
+ $partLengthSum += $partLength;
+ }
+
+ return $parts;
+ }
+
+ /**
+ * Mark differences between two "replace" blocks.
+ *
+ * Each of the returned block (lines) is always only one line.
+ *
+ * @param string[] $oldBlock The old block
+ * @param string[] $newBlock The new block
+ *
+ * @return string[][] the value of [[$oldLine], [$newLine]]
+ */
+ protected function markReplaceBlockDiff(array $oldBlock, array $newBlock): array
+ {
+ static $mbOld, $mbNew, $lineRenderer;
+
+ $mbOld = $mbOld ?? new MbString();
+ $mbNew = $mbNew ?? new MbString();
+ $lineRenderer = $lineRenderer ?? LineRendererFactory::make(
+ $this->options['detailLevel'],
+ [], /** @todo is it possible to get the differOptions here? */
+ $this->options
+ );
+
+ $mbOld->set(\implode("\n", $oldBlock));
+ $mbNew->set(\implode("\n", $newBlock));
+
+ $lineRenderer->render($mbOld, $mbNew);
+
+ return [
+ [$mbOld->get()], // one-line block for the old
+ [$mbNew->get()], // one-line block for the new
+ ];
+ }
+
+ /**
+ * Determine whether the "replace"-type lines are merge-able or not.
+ *
+ * @param string $oldLine the old line
+ * @param string $newLine the new line
+ * @param string $cleanLine the clean line
+ */
+ protected function isLinesMergeable(string $oldLine, string $newLine, string $cleanLine): bool
+ {
+ $oldLine = \str_replace(RendererConstant::HTML_CLOSURES_DEL, '', $oldLine);
+ $newLine = \str_replace(RendererConstant::HTML_CLOSURES_INS, '', $newLine);
+
+ $sumLength = \strlen($oldLine) + \strlen($newLine);
+
+ /** @var float the changed ratio, 0 <= value < 1 */
+ $changedRatio = ($sumLength - (\strlen($cleanLine) << 1)) / ($sumLength + 1);
+
+ return $changedRatio <= $this->options['mergeThreshold'];
+ }
+
+ /**
+ * Extract boundary newlines from parts into new parts.
+ *
+ * @param array[] $parts the parts
+ * @param string[] $closures the closures
+ *
+ * @see https://git.io/JvVXH
+ */
+ protected function revisePartsForBoundaryNewlines(array &$parts, array $closures): void
+ {
+ [$ld, $rd] = $closures;
+
+ $ldRegex = \preg_quote($ld, '/');
+ $rdRegex = \preg_quote($rd, '/');
+
+ for ($i = \count($parts) - 1; $i >= 0; --$i) {
+ $part = &$parts[$i];
+
+ // deal with leading newlines
+ $part['content'] = \preg_replace_callback(
+ "/(?P<closure>{$ldRegex})(?P<nl>[\r\n]++)/u",
+ function (array $matches) use (&$parts, $part, $ld, $rd): string {
+ // add a new part for the extracted newlines
+ $part['order'] = -1;
+ $part['content'] = "{$ld}{$matches['nl']}{$rd}";
+ $parts[] = $part;
+
+ return $matches['closure'];
+ },
+ $part['content']
+ );
+
+ // deal with trailing newlines
+ $part['content'] = \preg_replace_callback(
+ "/(?P<nl>[\r\n]++)(?P<closure>{$rdRegex})/u",
+ function (array $matches) use (&$parts, $part, $ld, $rd): string {
+ // add a new part for the extracted newlines
+ $part['order'] = 1;
+ $part['content'] = "{$ld}{$matches['nl']}{$rd}";
+ $parts[] = $part;
+
+ return $matches['closure'];
+ },
+ $part['content']
+ );
+ }
+ }
+
+ /**
+ * Make lines suitable for HTML output.
+ *
+ * @param string[] $lines the lines
+ * @param int $op the operation
+ */
+ protected function customFormatLines(array $lines, int $op): array
+ {
+ static $closureMap = [
+ SequenceMatcher::OP_DEL => RendererConstant::HTML_CLOSURES_DEL,
+ SequenceMatcher::OP_INS => RendererConstant::HTML_CLOSURES_INS,
+ ];
+
+ $lines = $this->formatLines($lines);
+
+ $htmlClosures = $closureMap[$op] ?? null;
+
+ foreach ($lines as &$line) {
+ if ($htmlClosures) {
+ $line = \str_replace(RendererConstant::HTML_CLOSURES, $htmlClosures, $line);
+ }
+ }
+
+ return $lines;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/Inline.php b/vendor/jfcherng/php-diff/src/Renderer/Html/Inline.php
new file mode 100644
index 0000000..d518b15
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/Inline.php
@@ -0,0 +1,259 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html;
+
+use Jfcherng\Diff\SequenceMatcher;
+
+/**
+ * Inline HTML diff generator.
+ */
+final class Inline extends AbstractHtml
+{
+ /**
+ * {@inheritdoc}
+ */
+ public const INFO = [
+ 'desc' => 'Inline',
+ 'type' => 'Html',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function redererChanges(array $changes): string
+ {
+ if (empty($changes)) {
+ return $this->getResultForIdenticals();
+ }
+
+ $wrapperClasses = \array_merge(
+ $this->options['wrapperClasses'],
+ ['diff', 'diff-html', 'diff-inline']
+ );
+
+ return
+ '<table class="' . \implode(' ', $wrapperClasses) . '">' .
+ $this->renderTableHeader() .
+ $this->renderTableHunks($changes) .
+ '</table>';
+ }
+
+ /**
+ * Renderer the table header.
+ */
+ protected function renderTableHeader(): string
+ {
+ if (!$this->options['showHeader']) {
+ return '';
+ }
+
+ $colspan = $this->options['lineNumbers'] ? '' : ' colspan="2"';
+
+ return
+ '<thead>' .
+ '<tr>' .
+ (
+ $this->options['lineNumbers']
+ ?
+ '<th>' . $this->_('old_version') . '</th>' .
+ '<th>' . $this->_('new_version') . '</th>' .
+ '<th></th>' // diff symbol column
+ :
+ ''
+ ) .
+ '<th' . $colspan . '>' . $this->_('differences') . '</th>' .
+ '</tr>' .
+ '</thead>';
+ }
+
+ /**
+ * Renderer the table separate block.
+ */
+ protected function renderTableSeparateBlock(): string
+ {
+ $colspan = $this->options['lineNumbers'] ? '4' : '2';
+
+ return
+ '<tbody class="skipped">' .
+ '<tr>' .
+ '<td colspan="' . $colspan . '"></td>' .
+ '</tr>' .
+ '</tbody>';
+ }
+
+ /**
+ * Renderer table hunks.
+ *
+ * @param array[][] $hunks each hunk has many blocks
+ */
+ protected function renderTableHunks(array $hunks): string
+ {
+ $ret = '';
+
+ foreach ($hunks as $i => $hunk) {
+ if ($i > 0 && $this->options['separateBlock']) {
+ $ret .= $this->renderTableSeparateBlock();
+ }
+
+ foreach ($hunk as $block) {
+ $ret .= $this->renderTableBlock($block);
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlock(array $block): string
+ {
+ static $callbacks = [
+ SequenceMatcher::OP_EQ => 'renderTableBlockEqual',
+ SequenceMatcher::OP_INS => 'renderTableBlockInsert',
+ SequenceMatcher::OP_DEL => 'renderTableBlockDelete',
+ SequenceMatcher::OP_REP => 'renderTableBlockReplace',
+ ];
+
+ return
+ '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' .
+ $this->{$callbacks[$block['tag']]}($block) .
+ '</tbody>';
+ }
+
+ /**
+ * Renderer the table block: equal.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockEqual(array $block): string
+ {
+ $ret = '';
+
+ // 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
+ foreach ($block['new']['lines'] as $no => $newLine) {
+ // we could only pick either the old or the new to show
+ // here we pick the new one to let the user know what it is now
+ $ret .= $this->renderTableRow(
+ 'new',
+ SequenceMatcher::OP_EQ,
+ $newLine,
+ $block['old']['offset'] + $no + 1,
+ $block['new']['offset'] + $no + 1
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: insert.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockInsert(array $block): string
+ {
+ $ret = '';
+
+ foreach ($block['new']['lines'] as $no => $newLine) {
+ $ret .= $this->renderTableRow(
+ 'new',
+ SequenceMatcher::OP_INS,
+ $newLine,
+ null,
+ $block['new']['offset'] + $no + 1
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: delete.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockDelete(array $block): string
+ {
+ $ret = '';
+
+ foreach ($block['old']['lines'] as $no => $oldLine) {
+ $ret .= $this->renderTableRow(
+ 'old',
+ SequenceMatcher::OP_DEL,
+ $oldLine,
+ $block['old']['offset'] + $no + 1,
+ null
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: replace.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockReplace(array $block): string
+ {
+ return
+ $this->renderTableBlockDelete($block) .
+ $this->renderTableBlockInsert($block);
+ }
+
+ /**
+ * Renderer a content row of the output table.
+ *
+ * @param string $tdClass the <td> class
+ * @param int $op the operation
+ * @param string $line the line
+ * @param null|int $oldLineNum the old line number
+ * @param null|int $newLineNum the new line number
+ */
+ protected function renderTableRow(
+ string $tdClass,
+ int $op,
+ string $line,
+ ?int $oldLineNum,
+ ?int $newLineNum
+ ): string {
+ return
+ '<tr data-type="' . self::SYMBOL_MAP[$op] . '">' .
+ (
+ $this->options['lineNumbers']
+ ? $this->renderLineNumberColumns($oldLineNum, $newLineNum)
+ : ''
+ ) .
+ '<th class="sign ' . self::TAG_CLASS_MAP[$op] . '">' . self::SYMBOL_MAP[$op] . '</th>' .
+ '<td class="' . $tdClass . '">' . $line . '</td>' .
+ '</tr>';
+ }
+
+ /**
+ * Renderer the line number columns.
+ *
+ * @param null|int $oldLineNum The old line number
+ * @param null|int $newLineNum The new line number
+ */
+ protected function renderLineNumberColumns(?int $oldLineNum, ?int $newLineNum): string
+ {
+ return
+ (
+ isset($oldLineNum)
+ ? '<th class="n-old">' . $oldLineNum . '</th>'
+ : '<th></th>'
+ ) .
+ (
+ isset($newLineNum)
+ ? '<th class="n-new">' . $newLineNum . '</th>'
+ : '<th></th>'
+ );
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/Json.php b/vendor/jfcherng/php-diff/src/Renderer/Html/Json.php
new file mode 100644
index 0000000..27f8f36
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/Json.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html;
+
+/**
+ * HTML Json diff generator.
+ *
+ * @deprecated 6.8.0 Use the "JsonHtml" renderer instead.
+ */
+final class Json extends JsonHtml
+{
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/JsonHtml.php b/vendor/jfcherng/php-diff/src/Renderer/Html/JsonHtml.php
new file mode 100644
index 0000000..a7981a1
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/JsonHtml.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html;
+
+use Jfcherng\Diff\SequenceMatcher;
+
+/**
+ * HTML Json diff generator.
+ */
+class JsonHtml extends AbstractHtml
+{
+ /**
+ * {@inheritdoc}
+ */
+ public const INFO = [
+ 'desc' => 'HTML Json',
+ 'type' => 'Html',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public const IS_TEXT_RENDERER = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResultForIdenticalsDefault(): string
+ {
+ return '[]';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function redererChanges(array $changes): string
+ {
+ if ($this->options['outputTagAsString']) {
+ $this->convertTagToString($changes);
+ }
+
+ return \json_encode($changes, $this->options['jsonEncodeFlags']);
+ }
+
+ /**
+ * Convert tags of changes to their string form for better readability.
+ *
+ * @param array[][] $changes the changes
+ */
+ protected function convertTagToString(array &$changes): void
+ {
+ foreach ($changes as &$hunks) {
+ foreach ($hunks as &$block) {
+ $block['tag'] = SequenceMatcher::opIntToStr($block['tag']);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function formatStringFromLines(string $string): string
+ {
+ return $this->htmlSafe($string);
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/AbstractLineRenderer.php b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/AbstractLineRenderer.php
new file mode 100644
index 0000000..5320994
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/AbstractLineRenderer.php
@@ -0,0 +1,105 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
+
+use Jfcherng\Diff\SequenceMatcher;
+
+/**
+ * Base renderer for rendering HTML-based line diffs.
+ */
+abstract class AbstractLineRenderer implements LineRendererInterface
+{
+ /**
+ * @var SequenceMatcher the sequence matcher
+ */
+ protected $sequenceMatcher;
+
+ /**
+ * @var array the differ options
+ */
+ protected $differOptions = [];
+
+ /**
+ * @var array the renderer options
+ */
+ protected $rendererOptions = [];
+
+ /**
+ * The constructor.
+ *
+ * @param array $differOptions the differ options
+ * @param array $rendererOptions the renderer options
+ */
+ public function __construct(array $differOptions, array $rendererOptions)
+ {
+ $this->sequenceMatcher = new SequenceMatcher([], []);
+
+ $this
+ ->setDifferOptions($differOptions)
+ ->setRendererOptions($rendererOptions);
+ }
+
+ /**
+ * Set the differ options.
+ *
+ * @param array $differOptions the differ options
+ *
+ * @return static
+ */
+ public function setDifferOptions(array $differOptions): self
+ {
+ $this->differOptions = $differOptions;
+ $this->sequenceMatcher->setOptions($differOptions);
+
+ return $this;
+ }
+
+ /**
+ * Set the renderer options.
+ *
+ * @param array $rendererOptions the renderer options
+ *
+ * @return static
+ */
+ public function setRendererOptions(array $rendererOptions): self
+ {
+ $this->rendererOptions = $rendererOptions;
+
+ return $this;
+ }
+
+ /**
+ * Gets the differ options.
+ *
+ * @return array the differ options
+ */
+ public function getDifferOptions(): array
+ {
+ return $this->differOptions;
+ }
+
+ /**
+ * Gets the renderer options.
+ *
+ * @return array the renderer options
+ */
+ public function getRendererOptions(): array
+ {
+ return $this->rendererOptions;
+ }
+
+ /**
+ * Get the changed extent segments.
+ *
+ * @param string[] $old the old array
+ * @param string[] $new the new array
+ *
+ * @return int[][] the changed extent segments
+ */
+ protected function getChangedExtentSegments(array $old, array $new): array
+ {
+ return $this->sequenceMatcher->setSequences($old, $new)->getOpcodes();
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Char.php b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Char.php
new file mode 100644
index 0000000..f66daba
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Char.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
+
+use Jfcherng\Diff\Renderer\RendererConstant;
+use Jfcherng\Diff\SequenceMatcher;
+use Jfcherng\Diff\Utility\ReverseIterator;
+use Jfcherng\Utility\MbString;
+
+final class Char extends AbstractLineRenderer
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @return static
+ */
+ public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface
+ {
+ $hunk = $this->getChangedExtentSegments($mbOld->toArray(), $mbNew->toArray());
+
+ // reversely iterate hunk
+ foreach (ReverseIterator::fromArray($hunk) as [$op, $i1, $i2, $j1, $j2]) {
+ if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_DEL)) {
+ $mbOld->str_enclose_i(RendererConstant::HTML_CLOSURES, $i1, $i2 - $i1);
+ }
+
+ if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_INS)) {
+ $mbNew->str_enclose_i(RendererConstant::HTML_CLOSURES, $j1, $j2 - $j1);
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Line.php b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Line.php
new file mode 100644
index 0000000..8e76c24
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Line.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
+
+use Jfcherng\Diff\Renderer\RendererConstant;
+use Jfcherng\Utility\MbString;
+
+final class Line extends AbstractLineRenderer
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @return static
+ */
+ public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface
+ {
+ [$start, $end] = $this->getChangedExtentRegion($mbOld, $mbNew);
+
+ // two strings are the same
+ if ($end === 0) {
+ return $this;
+ }
+
+ // two strings are different, we do rendering
+ $mbOld->str_enclose_i(
+ RendererConstant::HTML_CLOSURES,
+ $start,
+ $end + $mbOld->strlen() - $start + 1
+ );
+ $mbNew->str_enclose_i(
+ RendererConstant::HTML_CLOSURES,
+ $start,
+ $end + $mbNew->strlen() - $start + 1
+ );
+
+ return $this;
+ }
+
+ /**
+ * Given two strings, determine where the changes in the two strings begin,
+ * and where the changes in the two strings end.
+ *
+ * @param MbString $mbOld the old megabytes line
+ * @param MbString $mbNew the new megabytes line
+ *
+ * @return int[] Array containing the starting position (non-negative) and the ending position (negative)
+ * [0, 0] if two strings are the same
+ */
+ protected function getChangedExtentRegion(MbString $mbOld, MbString $mbNew): array
+ {
+ // two strings are the same
+ // most lines should be this cases, an early return could save many function calls
+ if ($mbOld->getRaw() === $mbNew->getRaw()) {
+ return [0, 0];
+ }
+
+ // calculate $start
+ $start = 0;
+ $startMax = \min($mbOld->strlen(), $mbNew->strlen());
+ while (
+ $start < $startMax // index out of range
+ && $mbOld->getAtRaw($start) === $mbNew->getAtRaw($start)
+ ) {
+ ++$start;
+ }
+
+ // calculate $end
+ $end = -1; // trick
+ $endMin = $startMax - $start;
+ while (
+ -$end <= $endMin // index out of range
+ && $mbOld->getAtRaw($end) === $mbNew->getAtRaw($end)
+ ) {
+ --$end;
+ }
+
+ return [$start, $end];
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/LineRendererInterface.php b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/LineRendererInterface.php
new file mode 100644
index 0000000..4437fcb
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/LineRendererInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
+
+use Jfcherng\Utility\MbString;
+
+interface LineRendererInterface
+{
+ /**
+ * Renderer the in-line changed extent.
+ *
+ * @param MbString $mbOld the old megabytes line
+ * @param MbString $mbNew the new megabytes line
+ *
+ * @return static
+ */
+ public function render(MbString $mbOld, MbString $mbNew): self;
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/None.php b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/None.php
new file mode 100644
index 0000000..b8abaa5
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/None.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
+
+use Jfcherng\Utility\MbString;
+
+final class None extends AbstractLineRenderer
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @return static
+ */
+ public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface
+ {
+ return $this;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Word.php b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Word.php
new file mode 100644
index 0000000..b9e83f8
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/LineRenderer/Word.php
@@ -0,0 +1,110 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
+
+use Jfcherng\Diff\Renderer\RendererConstant;
+use Jfcherng\Diff\SequenceMatcher;
+use Jfcherng\Diff\Utility\ReverseIterator;
+use Jfcherng\Diff\Utility\Str;
+use Jfcherng\Utility\MbString;
+
+final class Word extends AbstractLineRenderer
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @return static
+ */
+ public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface
+ {
+ static $splitRegex = '/([' . RendererConstant::PUNCTUATIONS_RANGE . '])/uS';
+ static $dummyHtmlClosure = RendererConstant::HTML_CLOSURES[0] . RendererConstant::HTML_CLOSURES[1];
+
+ $pregFlag = \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY;
+ $oldWords = $mbOld->toArraySplit($splitRegex, -1, $pregFlag);
+ $newWords = $mbNew->toArraySplit($splitRegex, -1, $pregFlag);
+
+ $hunk = $this->getChangedExtentSegments($oldWords, $newWords);
+
+ // reversely iterate hunk
+ foreach (ReverseIterator::fromArray($hunk) as [$op, $i1, $i2, $j1, $j2]) {
+ if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_DEL)) {
+ $oldWords[$i1] = RendererConstant::HTML_CLOSURES[0] . $oldWords[$i1];
+ $oldWords[$i2 - 1] .= RendererConstant::HTML_CLOSURES[1];
+
+ // insert dummy HTML closure to ensure there are always
+ // the same amounts of HTML closures in $oldWords and $newWords
+ // thus, this should make that "wordGlues" work correctly
+ // @see https://github.com/jfcherng/php-diff/pull/25
+ if ($op === SequenceMatcher::OP_DEL) {
+ \array_splice($newWords, $j1, 0, [$dummyHtmlClosure]);
+ }
+ }
+
+ if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_INS)) {
+ $newWords[$j1] = RendererConstant::HTML_CLOSURES[0] . $newWords[$j1];
+ $newWords[$j2 - 1] .= RendererConstant::HTML_CLOSURES[1];
+
+ if ($op === SequenceMatcher::OP_INS) {
+ \array_splice($oldWords, $i1, 0, [$dummyHtmlClosure]);
+ }
+ }
+ }
+
+ if (!empty($hunk) && !empty($this->rendererOptions['wordGlues'])) {
+ $regexGlues = \array_map(
+ function (string $glue): string {
+ return \preg_quote($glue, '/');
+ },
+ $this->rendererOptions['wordGlues']
+ );
+
+ $gluePattern = '/^(?:' . \implode('|', $regexGlues) . ')+$/uS';
+
+ $this->glueWordsResult($oldWords, $gluePattern);
+ $this->glueWordsResult($newWords, $gluePattern);
+ }
+
+ $mbOld->set(\implode('', $oldWords));
+ $mbNew->set(\implode('', $newWords));
+
+ return $this;
+ }
+
+ /**
+ * Beautify diff result by glueing words.
+ *
+ * What this function does is basically making
+ * ["<diff_begin>good<diff_end>", "-", "<diff_begin>looking<diff_end>"]
+ * into
+ * ["<diff_begin>good", "-", "looking<diff_end>"].
+ *
+ * @param array $words the words
+ * @param string $gluePattern the regex to determine a string is purely glue or not
+ */
+ protected function glueWordsResult(array &$words, string $gluePattern): void
+ {
+ /** @var int index of the word which has the trailing closure */
+ $endClosureIdx = -1;
+
+ foreach ($words as $idx => &$word) {
+ if ($word === '') {
+ continue;
+ }
+
+ if ($endClosureIdx < 0) {
+ if (Str::endsWith($word, RendererConstant::HTML_CLOSURES[1])) {
+ $endClosureIdx = $idx;
+ }
+ } elseif (Str::startsWith($word, RendererConstant::HTML_CLOSURES[0])) {
+ $words[$endClosureIdx] = \substr($words[$endClosureIdx], 0, -\strlen(RendererConstant::HTML_CLOSURES[1]));
+ $word = \substr($word, \strlen(RendererConstant::HTML_CLOSURES[0]));
+ $endClosureIdx = $idx;
+ } elseif (!\preg_match($gluePattern, $word)) {
+ $endClosureIdx = -1;
+ }
+ }
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Html/SideBySide.php b/vendor/jfcherng/php-diff/src/Renderer/Html/SideBySide.php
new file mode 100644
index 0000000..b5371a3
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Html/SideBySide.php
@@ -0,0 +1,274 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Html;
+
+use Jfcherng\Diff\SequenceMatcher;
+
+/**
+ * Side by Side HTML diff generator.
+ */
+final class SideBySide extends AbstractHtml
+{
+ /**
+ * {@inheritdoc}
+ */
+ public const INFO = [
+ 'desc' => 'Side by side',
+ 'type' => 'Html',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function redererChanges(array $changes): string
+ {
+ if (empty($changes)) {
+ return $this->getResultForIdenticals();
+ }
+
+ $wrapperClasses = \array_merge(
+ $this->options['wrapperClasses'],
+ ['diff', 'diff-html', 'diff-side-by-side']
+ );
+
+ return
+ '<table class="' . \implode(' ', $wrapperClasses) . '">' .
+ $this->renderTableHeader() .
+ $this->renderTableHunks($changes) .
+ '</table>';
+ }
+
+ /**
+ * Renderer the table header.
+ */
+ protected function renderTableHeader(): string
+ {
+ if (!$this->options['showHeader']) {
+ return '';
+ }
+
+ $colspan = $this->options['lineNumbers'] ? ' colspan="2"' : '';
+
+ return
+ '<thead>' .
+ '<tr>' .
+ '<th' . $colspan . '>' . $this->_('old_version') . '</th>' .
+ '<th' . $colspan . '>' . $this->_('new_version') . '</th>' .
+ '</tr>' .
+ '</thead>';
+ }
+
+ /**
+ * Renderer the table separate block.
+ */
+ protected function renderTableSeparateBlock(): string
+ {
+ $colspan = $this->options['lineNumbers'] ? '4' : '2';
+
+ return
+ '<tbody class="skipped">' .
+ '<tr>' .
+ '<td colspan="' . $colspan . '"></td>' .
+ '</tr>' .
+ '</tbody>';
+ }
+
+ /**
+ * Renderer table hunks.
+ *
+ * @param array[][] $hunks each hunk has many blocks
+ */
+ protected function renderTableHunks(array $hunks): string
+ {
+ $ret = '';
+
+ foreach ($hunks as $i => $hunk) {
+ if ($i > 0 && $this->options['separateBlock']) {
+ $ret .= $this->renderTableSeparateBlock();
+ }
+
+ foreach ($hunk as $block) {
+ $ret .= $this->renderTableBlock($block);
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlock(array $block): string
+ {
+ static $callbacks = [
+ SequenceMatcher::OP_EQ => 'renderTableBlockEqual',
+ SequenceMatcher::OP_INS => 'renderTableBlockInsert',
+ SequenceMatcher::OP_DEL => 'renderTableBlockDelete',
+ SequenceMatcher::OP_REP => 'renderTableBlockReplace',
+ ];
+
+ return
+ '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' .
+ $this->{$callbacks[$block['tag']]}($block) .
+ '</tbody>';
+ }
+
+ /**
+ * Renderer the table block: equal.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockEqual(array $block): string
+ {
+ $ret = '';
+
+ $rowCount = \count($block['new']['lines']);
+
+ for ($no = 0; $no < $rowCount; ++$no) {
+ $ret .= $this->renderTableRow(
+ $block['old']['lines'][$no],
+ $block['new']['lines'][$no],
+ $block['old']['offset'] + $no + 1,
+ $block['new']['offset'] + $no + 1
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: insert.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockInsert(array $block): string
+ {
+ $ret = '';
+
+ foreach ($block['new']['lines'] as $no => $newLine) {
+ $ret .= $this->renderTableRow(
+ null,
+ $newLine,
+ null,
+ $block['new']['offset'] + $no + 1
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: delete.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockDelete(array $block): string
+ {
+ $ret = '';
+
+ foreach ($block['old']['lines'] as $no => $oldLine) {
+ $ret .= $this->renderTableRow(
+ $oldLine,
+ null,
+ $block['old']['offset'] + $no + 1,
+ null
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer the table block: replace.
+ *
+ * @param array $block the block
+ */
+ protected function renderTableBlockReplace(array $block): string
+ {
+ $ret = '';
+
+ $lineCountMax = \max(\count($block['old']['lines']), \count($block['new']['lines']));
+
+ for ($no = 0; $no < $lineCountMax; ++$no) {
+ if (isset($block['old']['lines'][$no])) {
+ $oldLineNum = $block['old']['offset'] + $no + 1;
+ $oldLine = $block['old']['lines'][$no];
+ } else {
+ $oldLineNum = $oldLine = null;
+ }
+
+ if (isset($block['new']['lines'][$no])) {
+ $newLineNum = $block['new']['offset'] + $no + 1;
+ $newLine = $block['new']['lines'][$no];
+ } else {
+ $newLineNum = $newLine = null;
+ }
+
+ $ret .= $this->renderTableRow($oldLine, $newLine, $oldLineNum, $newLineNum);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renderer a content row of the output table.
+ *
+ * @param null|string $oldLine the old line
+ * @param null|string $newLine the new line
+ * @param null|int $oldLineNum the old line number
+ * @param null|int $newLineNum the new line number
+ */
+ protected function renderTableRow(
+ ?string $oldLine,
+ ?string $newLine,
+ ?int $oldLineNum,
+ ?int $newLineNum
+ ): string {
+ return
+ '<tr>' .
+ (
+ $this->options['lineNumbers']
+ ? $this->renderLineNumberColumn('old', $oldLineNum)
+ : ''
+ ) .
+ $this->renderLineContentColumn('old', $oldLine) .
+ (
+ $this->options['lineNumbers']
+ ? $this->renderLineNumberColumn('new', $newLineNum)
+ : ''
+ ) .
+ $this->renderLineContentColumn('new', $newLine) .
+ '</tr>';
+ }
+
+ /**
+ * Renderer the line number column.
+ *
+ * @param string $type the diff type
+ * @param null|int $lineNum the line number
+ */
+ protected function renderLineNumberColumn(string $type, ?int $lineNum): string
+ {
+ return isset($lineNum)
+ ? '<th class="n-' . $type . '">' . $lineNum . '</th>'
+ : '<th></th>';
+ }
+
+ /**
+ * Renderer the line content column.
+ *
+ * @param string $type the diff type
+ * @param null|string $content the line content
+ */
+ protected function renderLineContentColumn(string $type, ?string $content): string
+ {
+ return
+ '<td class="' . $type . (isset($content) ? '' : ' none') . '">' .
+ $content .
+ '</td>';
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/RendererConstant.php b/vendor/jfcherng/php-diff/src/Renderer/RendererConstant.php
new file mode 100644
index 0000000..a6d2481
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/RendererConstant.php
@@ -0,0 +1,116 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer;
+
+final class RendererConstant
+{
+ /**
+ * The base namespace of renderers.
+ *
+ * @var string
+ */
+ public const RENDERER_NAMESPACE = __NAMESPACE__;
+
+ /**
+ * Available renderer types.
+ *
+ * @var string[]
+ */
+ public const RENDERER_TYPES = ['Html', 'Text'];
+
+ /**
+ * Closures that are used to enclose different parts in string.
+ *
+ * Arbitrary chars from the 15-16th Unicode reserved areas
+ * and hopefully, they won't appear in source texts.
+ *
+ * @var string[]
+ */
+ public const HTML_CLOSURES = ["\u{fcffc}\u{ff2fb}", "\u{fff41}\u{fcffc}"];
+
+ /**
+ * Closures that are used to enclose deleted chars in output HTML.
+ *
+ * @var string[]
+ */
+ public const HTML_CLOSURES_DEL = ['<del>', '</del>'];
+
+ /**
+ * Closures that are used to enclose inserted chars in output HTML.
+ *
+ * @var string[]
+ */
+ public const HTML_CLOSURES_INS = ['<ins>', '</ins>'];
+
+ /**
+ * The delimiter to be used as the glue in string/array functions.
+ *
+ * Arbitrary chars from the 15-16th Unicode reserved areas
+ * and hopefully, it won't appear in source texts.
+ *
+ * @var string
+ */
+ public const IMPLODE_DELIMITER = "\u{ff2fa}\u{fcffc}\u{fff42}";
+
+ /**
+ * Regex range for punctuations.
+ *
+ * Presuming the regex delimiter is "/".
+ *
+ * @var string
+ */
+ public const PUNCTUATIONS_RANGE = (
+ // Latin-1 Supplement
+ // @see https://unicode-table.com/en/blocks/latin-1-supplement/
+ "\u{0080}-\u{00BB}" .
+ // Spacing Modifier Letters
+ // @see https://unicode-table.com/en/blocks/spacing-modifier-letters/
+ "\u{02B0}-\u{02FF}" .
+ // Combining Diacritical Marks
+ // @see https://unicode-table.com/en/blocks/combining-diacritical-marks/
+ "\u{0300}-\u{036F}" .
+ // Small Form Variants
+ // @see https://unicode-table.com/en/blocks/small-form-variants/
+ "\u{FE50}-\u{FE6F}" .
+ // General Punctuation
+ // @see https://unicode-table.com/en/blocks/general-punctuation/
+ "\u{2000}-\u{206F}" .
+ // Supplemental Punctuation
+ // @see https://unicode-table.com/en/blocks/supplemental-punctuation/
+ "\u{2E00}-\u{2E7F}" .
+ // CJK Symbols and Punctuation
+ // @see https://unicode-table.com/en/blocks/cjk-symbols-and-punctuation/
+ "\u{3000}-\u{303F}" .
+ // Ideographic Symbols and Punctuation
+ // @see https://unicode-table.com/en/blocks/ideographic-symbols-and-punctuation/
+ "\u{16FE0}-\u{16FFF}" .
+ // hmm... these seem to be no rule
+ " \t\r\n$,.:;!?'\"()\\[\\]{}%@<=>_+\\-*\\/~\\\\|" .
+ ' $,.:;!?’"()[]{}%@<=>_+-*/~\|' .
+ '「」『』〈〉《》【】()()‘’“”' .
+ '.‧・・•·¿'
+ );
+
+ /**
+ * Colorize the CLI output if possible.
+ *
+ * @var int
+ */
+ public const CLI_COLOR_AUTO = -1;
+
+ /**
+ * Force not to colorize the CLI output.
+ *
+ * @var int
+ */
+ public const CLI_COLOR_DISABLE = 0;
+
+ /**
+ * Force to colorize the CLI output if possible.
+ *
+ * @var int
+ */
+ public const CLI_COLOR_ENABLE = 1;
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/RendererInterface.php b/vendor/jfcherng/php-diff/src/Renderer/RendererInterface.php
new file mode 100644
index 0000000..97c109c
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/RendererInterface.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer;
+
+use Jfcherng\Diff\Differ;
+use Jfcherng\Diff\Exception\UnsupportedFunctionException;
+
+/**
+ * Renderer Interface.
+ */
+interface RendererInterface
+{
+ /**
+ * Get the renderer result when the old and the new are the same.
+ */
+ public function getResultForIdenticals(): string;
+
+ /**
+ * Render the differ and return the result.
+ *
+ * @param Differ $differ the Differ object to be rendered
+ */
+ public function render(Differ $differ): string;
+
+ /**
+ * Render the differ array and return the result.
+ *
+ * @param array[][] $differArray the Differ array to be rendered
+ *
+ * @throws UnsupportedFunctionException if the renderer does not support this method
+ */
+ public function renderArray(array $differArray): string;
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Text/AbstractText.php b/vendor/jfcherng/php-diff/src/Renderer/Text/AbstractText.php
new file mode 100644
index 0000000..b2f3f6c
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Text/AbstractText.php
@@ -0,0 +1,145 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Text;
+
+use Jfcherng\Diff\Exception\UnsupportedFunctionException;
+use Jfcherng\Diff\Renderer\AbstractRenderer;
+use Jfcherng\Diff\Renderer\RendererConstant;
+use Jfcherng\Utility\CliColor;
+
+/**
+ * Base renderer for rendering text-based diffs.
+ */
+abstract class AbstractText extends AbstractRenderer
+{
+ /**
+ * @var bool is this renderer pure text?
+ */
+ public const IS_TEXT_RENDERER = true;
+
+ /**
+ * @var string the diff output representing there is no EOL at EOF in the GNU diff tool
+ */
+ public const GNU_OUTPUT_NO_EOL_AT_EOF = '\ No newline at end of file';
+
+ /**
+ * @var bool controls whether cliColoredString() is enabled or not
+ */
+ protected $isCliColorEnabled = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOptions(array $options): AbstractRenderer
+ {
+ parent::setOptions($options);
+
+ // determine $this->isCliColorEnabled
+ if ($this->options['cliColorization'] === RendererConstant::CLI_COLOR_ENABLE) {
+ $this->isCliColorEnabled = true;
+ } elseif ($this->options['cliColorization'] === RendererConstant::CLI_COLOR_DISABLE) {
+ $this->isCliColorEnabled = false;
+ } else {
+ $this->isCliColorEnabled = \PHP_SAPI === 'cli' && $this->hasColorSupport(\STDOUT);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResultForIdenticalsDefault(): string
+ {
+ return '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function renderArrayWorker(array $differArray): string
+ {
+ throw new UnsupportedFunctionException(__METHOD__);
+
+ return ''; // make IDE not complain
+ }
+
+ /**
+ * Colorize the string for CLI output.
+ *
+ * @param string $str the string
+ * @param null|string $symbol the symbol
+ *
+ * @return string the (maybe) colorized string
+ */
+ protected function cliColoredString(string $str, ?string $symbol): string
+ {
+ static $symbolToStyles = [
+ '@' => ['f_purple', 'bold'], // header
+ '-' => ['f_red', 'bold'], // deleted
+ '+' => ['f_green', 'bold'], // inserted
+ '!' => ['f_yellow', 'bold'], // replaced
+ ];
+
+ $styles = $symbolToStyles[$symbol] ?? [];
+
+ if (!$this->isCliColorEnabled || empty($styles)) {
+ return $str;
+ }
+
+ return CliColor::color($str, $styles);
+ }
+
+ /**
+ * Returns true if the stream supports colorization.
+ *
+ * Colorization is disabled if not supported by the stream:
+ *
+ * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo
+ * terminals via named pipes, so we can only check the environment.
+ *
+ * Reference: Composer\XdebugHandler\Process::supportsColor
+ * https://github.com/composer/xdebug-handler
+ *
+ * @see https://github.com/symfony/console/blob/647c51ff073300a432a4a504e29323cf0d5e0571/Output/StreamOutput.php#L81-L124
+ *
+ * @param resource $stream
+ *
+ * @return bool true if the stream supports colorization, false otherwise
+ *
+ * @suppress PhanUndeclaredFunction
+ */
+ protected function hasColorSupport($stream): bool
+ {
+ // Follow https://no-color.org/
+ if (isset($_SERVER['NO_COLOR']) || false !== \getenv('NO_COLOR')) {
+ return false;
+ }
+
+ if ('Hyper' === \getenv('TERM_PROGRAM')) {
+ return true;
+ }
+
+ if (\DIRECTORY_SEPARATOR === '\\') {
+ return (\function_exists('sapi_windows_vt100_support')
+ && @\sapi_windows_vt100_support($stream))
+ || false !== \getenv('ANSICON')
+ || 'ON' === \getenv('ConEmuANSI')
+ || 'xterm' === \getenv('TERM');
+ }
+
+ if (\function_exists('stream_isatty')) {
+ return @\stream_isatty($stream);
+ }
+
+ if (\function_exists('posix_isatty')) {
+ return @posix_isatty($stream);
+ }
+
+ $stat = @\fstat($stream);
+ // Check if formatted mode is S_IFCHR
+ return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Text/Context.php b/vendor/jfcherng/php-diff/src/Renderer/Text/Context.php
new file mode 100644
index 0000000..ddc9b2a
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Text/Context.php
@@ -0,0 +1,163 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Text;
+
+use Jfcherng\Diff\Differ;
+use Jfcherng\Diff\SequenceMatcher;
+
+/**
+ * Context diff generator.
+ *
+ * @see https://en.wikipedia.org/wiki/Diff#Context_format
+ */
+final class Context extends AbstractText
+{
+ /**
+ * {@inheritdoc}
+ */
+ public const INFO = [
+ 'desc' => 'Context',
+ 'type' => 'Text',
+ ];
+
+ /**
+ * @var int the union of OPs that indicate there is a change
+ */
+ public const OP_BLOCK_CHANGED =
+ SequenceMatcher::OP_DEL |
+ SequenceMatcher::OP_INS |
+ SequenceMatcher::OP_REP;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function renderWorker(Differ $differ): string
+ {
+ $ret = '';
+
+ foreach ($differ->getGroupedOpcodesGnu() as $hunk) {
+ $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];
+
+ $ret .=
+ $this->cliColoredString("***************\n", '@') .
+ $this->renderHunkHeader('*', $i1, $i2) .
+ $this->renderHunkOld($differ, $hunk) .
+ $this->renderHunkHeader('-', $j1, $j2) .
+ $this->renderHunkNew($differ, $hunk);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Render the hunk header.
+ *
+ * @param string $symbol the symbol
+ * @param int $a1 the begin index
+ * @param int $a2 the end index
+ */
+ protected function renderHunkHeader(string $symbol, int $a1, int $a2): string
+ {
+ $a1x = $a1 + 1; // 1-based begin line number
+
+ return $this->cliColoredString(
+ "{$symbol}{$symbol}{$symbol} " .
+ ($a1x < $a2 ? "{$a1x},{$a2}" : $a2) .
+ " {$symbol}{$symbol}{$symbol}{$symbol}\n",
+ '@' // symbol
+ );
+ }
+
+ /**
+ * Render the old hunk.
+ *
+ * @param Differ $differ the differ object
+ * @param int[][] $hunk the hunk
+ */
+ protected function renderHunkOld(Differ $differ, array $hunk): string
+ {
+ $ret = '';
+ $hunkOps = 0;
+ $noEolAtEofIdx = $differ->getOldNoEolAtEofIdx();
+
+ foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
+ // OP_INS does not belongs to an old hunk
+ if ($op === SequenceMatcher::OP_INS) {
+ continue;
+ }
+
+ $hunkOps |= $op;
+
+ $ret .= $this->renderContext(
+ self::SYMBOL_MAP[$op],
+ $differ->getOld($i1, $i2),
+ $i2 === $noEolAtEofIdx
+ );
+ }
+
+ // if there is no content changed, the hunk context should be omitted
+ return $hunkOps & self::OP_BLOCK_CHANGED ? $ret : '';
+ }
+
+ /**
+ * Render the new hunk.
+ *
+ * @param Differ $differ the differ object
+ * @param int[][] $hunk the hunk
+ */
+ protected function renderHunkNew(Differ $differ, array $hunk): string
+ {
+ $ret = '';
+ $hunkOps = 0;
+ $noEolAtEofIdx = $differ->getNewNoEolAtEofIdx();
+
+ foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
+ // OP_DEL does not belongs to a new hunk
+ if ($op === SequenceMatcher::OP_DEL) {
+ continue;
+ }
+
+ $hunkOps |= $op;
+
+ $ret .= $this->renderContext(
+ self::SYMBOL_MAP[$op],
+ $differ->getNew($j1, $j2),
+ $j2 === $noEolAtEofIdx
+ );
+ }
+
+ // if there is no content changed, the hunk context should be omitted
+ return $hunkOps & self::OP_BLOCK_CHANGED ? $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;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Text/JsonText.php b/vendor/jfcherng/php-diff/src/Renderer/Text/JsonText.php
new file mode 100644
index 0000000..b99aa56
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Text/JsonText.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Renderer\Text;
+
+use Jfcherng\Diff\Differ;
+use Jfcherng\Diff\SequenceMatcher;
+
+/**
+ * Plain text Json diff generator.
+ */
+final class JsonText extends AbstractText
+{
+ /**
+ * {@inheritdoc}
+ */
+ public const INFO = [
+ 'desc' => 'Text JSON',
+ 'type' => 'Text',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function renderWorker(Differ $differ): string
+ {
+ $ret = [];
+
+ foreach ($differ->getGroupedOpcodes() as $hunk) {
+ $ret[] = $this->renderHunk($differ, $hunk);
+ }
+
+ if ($this->options['outputTagAsString']) {
+ $this->convertTagToString($ret);
+ }
+
+ return \json_encode($ret, $this->options['jsonEncodeFlags']);
+ }
+
+ /**
+ * Render the hunk.
+ *
+ * @param Differ $differ the differ object
+ * @param int[][] $hunk the hunk
+ */
+ protected function renderHunk(Differ $differ, array $hunk): array
+ {
+ $ret = [];
+
+ foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
+ $ret[] = [
+ 'tag' => $op,
+ 'old' => [
+ 'offset' => $i1,
+ 'lines' => $differ->getOld($i1, $i2),
+ ],
+ 'new' => [
+ 'offset' => $j1,
+ 'lines' => $differ->getNew($j1, $j2),
+ ],
+ ];
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Convert tags of changes to their string form for better readability.
+ *
+ * @param array[][] $changes the changes
+ */
+ protected function convertTagToString(array &$changes): void
+ {
+ foreach ($changes as &$hunks) {
+ foreach ($hunks as &$block) {
+ $block['tag'] = SequenceMatcher::opIntToStr($block['tag']);
+ }
+ }
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Renderer/Text/Unified.php b/vendor/jfcherng/php-diff/src/Renderer/Text/Unified.php
new file mode 100644
index 0000000..e211715
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Renderer/Text/Unified.php
@@ -0,0 +1,147 @@
+<?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;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Utility/Arr.php b/vendor/jfcherng/php-diff/src/Utility/Arr.php
new file mode 100644
index 0000000..9283e30
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Utility/Arr.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Utility;
+
+final class Arr
+{
+ /**
+ * Get a partial array slice with start/end indexes.
+ *
+ * @param array $array the array
+ * @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 array array of all of the lines between the specified range
+ */
+ public static function getPartialByIndex(array $array, int $start = 0, ?int $end = null): array
+ {
+ $count = \count($array);
+
+ // make $end set
+ $end = $end ?? $count;
+
+ // make $start non-negative
+ if ($start < 0) {
+ $start += $count;
+
+ if ($start < 0) {
+ $start = 0;
+ }
+ }
+
+ // make $end non-negative
+ if ($end < 0) {
+ $end += $count;
+
+ if ($end < 0) {
+ $end = 0;
+ }
+ }
+
+ // make the length non-negative
+ return \array_slice($array, $start, \max(0, $end - $start));
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Utility/Language.php b/vendor/jfcherng/php-diff/src/Utility/Language.php
new file mode 100644
index 0000000..a2e531b
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Utility/Language.php
@@ -0,0 +1,137 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Utility;
+
+final class Language
+{
+ /**
+ * @var string[] the translation dict
+ */
+ private $translations = [];
+
+ /**
+ * @var string the language name
+ */
+ private $language = '_custom_';
+
+ /**
+ * The constructor.
+ *
+ * @param string|string[] $target the language string or translations dict
+ */
+ public function __construct($target = 'eng')
+ {
+ $this->setLanguageOrTranslations($target);
+ }
+
+ /**
+ * Set up this class.
+ *
+ * @param string|string[] $target the language string or translations array
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setLanguageOrTranslations($target): self
+ {
+ if (\is_string($target)) {
+ $this->setUpWithLanguage($target);
+
+ return $this;
+ }
+
+ if (\is_array($target)) {
+ $this->setUpWithTranslations($target);
+
+ return $this;
+ }
+
+ throw new \InvalidArgumentException('$target must be the type of string|string[]');
+ }
+
+ /**
+ * Get the language.
+ *
+ * @return string the language
+ */
+ public function getLanguage(): string
+ {
+ return $this->language;
+ }
+
+ /**
+ * Get the translations.
+ *
+ * @return array the translations
+ */
+ public function getTranslations(): array
+ {
+ return $this->translations;
+ }
+
+ /**
+ * Get the translations from the language file.
+ *
+ * @param string $language the language
+ *
+ * @throws \Exception fail to decode the JSON file
+ * @throws \LogicException path is a directory
+ * @throws \RuntimeException path cannot be opened
+ *
+ * @return string[]
+ */
+ public static function getTranslationsByLanguage(string $language): array
+ {
+ $filePath = __DIR__ . "/../languages/{$language}.json";
+ $file = new \SplFileObject($filePath, 'r');
+ $fileContent = $file->fread($file->getSize());
+
+ /** @todo PHP ^7.3 JSON_THROW_ON_ERROR */
+ $decoded = \json_decode($fileContent, true);
+
+ if (\json_last_error() !== \JSON_ERROR_NONE) {
+ $msg = \sprintf('Fail to decode JSON file (code %d): %s', \json_last_error(), \realpath($filePath));
+ throw new \Exception($msg); // workaround single-line throw + 120-char limit
+ }
+
+ return (array) $decoded;
+ }
+
+ /**
+ * Translation the text.
+ *
+ * @param string $text the text
+ */
+ public function translate(string $text): string
+ {
+ return $this->translations[$text] ?? "![{$text}]";
+ }
+
+ /**
+ * Set up this class by language name.
+ *
+ * @param string $language the language name
+ */
+ private function setUpWithLanguage(string $language): self
+ {
+ return $this->setUpWithTranslations(
+ self::getTranslationsByLanguage($language),
+ $language
+ );
+ }
+
+ /**
+ * Set up this class by translations.
+ *
+ * @param string[] $translations the translations dict
+ * @param string $language the language name
+ */
+ private function setUpWithTranslations(array $translations, string $language = '_custom_'): self
+ {
+ $this->language = $language;
+ $this->translations = \array_map('strval', $translations);
+
+ return $this;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Utility/ReverseIterator.php b/vendor/jfcherng/php-diff/src/Utility/ReverseIterator.php
new file mode 100644
index 0000000..e684ae1
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Utility/ReverseIterator.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Utility;
+
+final class ReverseIterator
+{
+ public const ITERATOR_GET_VALUE = 0;
+ public const ITERATOR_GET_KEY = 1 << 0;
+ public const ITERATOR_GET_BOTH = 1 << 1;
+
+ /**
+ * The constructor.
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Iterate the array reversely.
+ *
+ * @param array $array the array
+ * @param int $flags the flags
+ */
+ public static function fromArray(array $array, int $flags = self::ITERATOR_GET_VALUE): \Generator
+ {
+ // iterate [key => value] pair
+ if ($flags & self::ITERATOR_GET_BOTH) {
+ for (\end($array); ($key = \key($array)) !== null; \prev($array)) {
+ yield $key => \current($array);
+ }
+
+ return;
+ }
+
+ // iterate only key
+ if ($flags & self::ITERATOR_GET_KEY) {
+ for (\end($array); ($key = \key($array)) !== null; \prev($array)) {
+ yield $key;
+ }
+
+ return;
+ }
+
+ // iterate only value
+ for (\end($array); \key($array) !== null; \prev($array)) {
+ yield \current($array);
+ }
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/Utility/Str.php b/vendor/jfcherng/php-diff/src/Utility/Str.php
new file mode 100644
index 0000000..c7478d4
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/Utility/Str.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Jfcherng\Diff\Utility;
+
+final class Str
+{
+ /**
+ * Determine if a given string starts with a given substring.
+ *
+ * @param string $haystack the haystack
+ * @param string $needle the needle
+ */
+ public static function startsWith(string $haystack, string $needle): bool
+ {
+ return \substr($haystack, 0, \strlen($needle)) === $needle;
+ }
+
+ /**
+ * Determine if a given string ends with a given substring.
+ *
+ * @param string $haystack the haystack
+ * @param string $needle the needle
+ */
+ public static function endsWith(string $haystack, string $needle): bool
+ {
+ return \substr($haystack, -\strlen($needle)) === $needle;
+ }
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/bul.json b/vendor/jfcherng/php-diff/src/languages/bul.json
new file mode 100644
index 0000000..33beaba
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/bul.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Стара версия",
+ "new_version": "Нова версия",
+ "differences": "Разлики"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/chs.json b/vendor/jfcherng/php-diff/src/languages/chs.json
new file mode 100644
index 0000000..23cafd6
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/chs.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "旧版本",
+ "new_version": "新版本",
+ "differences": "差异"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/cht.json b/vendor/jfcherng/php-diff/src/languages/cht.json
new file mode 100644
index 0000000..6d5bbc8
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/cht.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "舊版本",
+ "new_version": "新版本",
+ "differences": "差異"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/deu.json b/vendor/jfcherng/php-diff/src/languages/deu.json
new file mode 100644
index 0000000..6627331
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/deu.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Alt",
+ "new_version": "Neu",
+ "differences": "Unterschiede"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/eng.json b/vendor/jfcherng/php-diff/src/languages/eng.json
new file mode 100644
index 0000000..8c180fc
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/eng.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Old",
+ "new_version": "New",
+ "differences": "Differences"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/fra.json b/vendor/jfcherng/php-diff/src/languages/fra.json
new file mode 100644
index 0000000..a56192d
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/fra.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Avant",
+ "new_version": "Après",
+ "differences": "Différences"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/ita.json b/vendor/jfcherng/php-diff/src/languages/ita.json
new file mode 100644
index 0000000..0432bab
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/ita.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Vecchio",
+ "new_version": "Nuovo",
+ "differences": "Differenze"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/jpn.json b/vendor/jfcherng/php-diff/src/languages/jpn.json
new file mode 100644
index 0000000..72d46f6
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/jpn.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "古い",
+ "new_version": "新しい",
+ "differences": "差異"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/por.json b/vendor/jfcherng/php-diff/src/languages/por.json
new file mode 100644
index 0000000..9c48b5d
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/por.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Original",
+ "new_version": "Nova",
+ "differences": "Diferenças"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/rus.json b/vendor/jfcherng/php-diff/src/languages/rus.json
new file mode 100644
index 0000000..2d7fc8b
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/rus.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Старая версия",
+ "new_version": "Новая версия",
+ "differences": "Различия"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/spa.json b/vendor/jfcherng/php-diff/src/languages/spa.json
new file mode 100644
index 0000000..ac26acd
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/spa.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Anterior",
+ "new_version": "Nuevo",
+ "differences": "Diferencias"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/tur.json b/vendor/jfcherng/php-diff/src/languages/tur.json
new file mode 100644
index 0000000..a9ea14f
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/tur.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Eski",
+ "new_version": "Yeni",
+ "differences": "Değişiklikler"
+}
diff --git a/vendor/jfcherng/php-diff/src/languages/ukr.json b/vendor/jfcherng/php-diff/src/languages/ukr.json
new file mode 100644
index 0000000..43584d2
--- /dev/null
+++ b/vendor/jfcherng/php-diff/src/languages/ukr.json
@@ -0,0 +1,5 @@
+{
+ "old_version": "Було",
+ "new_version": "Стало",
+ "differences": "Відмінності"
+}