diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:38:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:38:42 +0000 |
commit | c3ca98e1b35123f226c7f4c596b5dee78caa4223 (patch) | |
tree | 9b6eb109283da55e7d9064baa9fac795a40264cb /vendor/jfcherng/php-diff | |
parent | Initial commit. (diff) | |
download | icinga-php-thirdparty-c3ca98e1b35123f226c7f4c596b5dee78caa4223.tar.xz icinga-php-thirdparty-c3ca98e1b35123f226c7f4c596b5dee78caa4223.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')
46 files changed, 4179 insertions, 0 deletions
diff --git a/vendor/jfcherng/php-diff/.php-cs-fixer.dist.php b/vendor/jfcherng/php-diff/.php-cs-fixer.dist.php new file mode 100644 index 0000000..1878a60 --- /dev/null +++ b/vendor/jfcherng/php-diff/.php-cs-fixer.dist.php @@ -0,0 +1,78 @@ +<?php + +$config = (new PhpCsFixer\Config()) + ->setIndent(" ") + ->setLineEnding("\n") + ->setCacheFile(__DIR__ . '/.php-cs-fixer.cache') + ->setRiskyAllowed(true) + ->setRules([ + '@PHP71Migration' => true, + '@PHP73Migration' => false, + '@PhpCsFixer' => true, + '@PhpCsFixer:risky' => true, + '@PSR12' => true, + '@Symfony' => true, + '@Symfony:risky' => true, + 'align_multiline_comment' => true, + 'array_indentation' => true, + 'array_syntax' => ['syntax' => 'short'], + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'combine_nested_dirname' => true, + 'comment_to_phpdoc' => true, + 'compact_nullable_typehint' => true, + 'concat_space' => ['spacing' => 'one'], + 'escape_implicit_backslashes' => false, + 'fully_qualified_strict_types' => true, + 'linebreak_after_opening_tag' => true, + 'list_syntax' => ['syntax' => 'short'], + 'method_argument_space' => ['ensure_fully_multiline' => true], + 'native_constant_invocation' => true, + 'native_function_invocation' => true, + 'native_function_type_declaration_casing' => true, + 'no_alternative_syntax' => true, + 'no_multiline_whitespace_before_semicolons' => true, + 'no_null_property_initialization' => true, + 'no_short_echo_tag' => true, + 'no_superfluous_elseif' => true, + 'no_trailing_whitespace_in_string' => false, // test cases have trailing spaces + 'no_unneeded_control_parentheses' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'not_operator_with_space' => false, + 'not_operator_with_successor_space' => false, + 'ordered_class_elements' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha', 'imports_order' => ['class', 'const', 'function']], + 'ordered_interfaces' => true, + 'php_unit_ordered_covers' => true, + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_strict' => true, + 'php_unit_test_class_requires_covers' => true, + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_order' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_types_order' => true, + 'pow_to_exponentiation' => true, + 'random_api_migration' => true, + 'return_assignment' => false, + 'simple_to_complex_string_variable' => true, + 'single_line_comment_style' => true, + 'single_trait_insert_per_statement' => true, + 'strict_comparison' => false, + 'strict_param' => false, + 'string_line_ending' => true, + 'yoda_style' => false, + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->notPath('/branch-\\w+/') // git worktree + ->exclude('libs') + ->exclude('tests/data') + ->exclude('tests/Fixtures') + ->exclude('var') + ->exclude('vendor') + ->in(__DIR__) + ) +; + +return $config; diff --git a/vendor/jfcherng/php-diff/.phpstorm.meta.php b/vendor/jfcherng/php-diff/.phpstorm.meta.php new file mode 100644 index 0000000..a8f8c52 --- /dev/null +++ b/vendor/jfcherng/php-diff/.phpstorm.meta.php @@ -0,0 +1,43 @@ +<?php + +namespace PHPSTORM_META; + +override( + \Jfcherng\Diff\Factory\LineRendererFactory::getInstance(0), + map(['' => 'Jfcherng\Diff\Renderer\Html\LineRenderer\@']) +); +override( + \Jfcherng\Diff\Factory\LineRendererFactory::make(0), + map(['' => 'Jfcherng\Diff\Renderer\Html\LineRenderer\@']) +); + +override( + \Jfcherng\Diff\Factory\RendererFactory::getInstance(0), + map([ + // html + 'Combined' => \Jfcherng\Diff\Renderer\Html\Combined::class, + 'Inline' => \Jfcherng\Diff\Renderer\Html\Inline::class, + 'Json' => \Jfcherng\Diff\Renderer\Html\Json::class, + 'JsonHtml' => \Jfcherng\Diff\Renderer\Html\JsonHtml::class, + 'SideBySide' => \Jfcherng\Diff\Renderer\Html\SideBySide::class, + // text + 'Context' => \Jfcherng\Diff\Renderer\Text\Context::class, + 'JsonText' => \Jfcherng\Diff\Renderer\Text\JsonText::class, + 'Unified' => \Jfcherng\Diff\Renderer\Text\Unified::class, + ]) +); +override( + \Jfcherng\Diff\Factory\RendererFactory::make(0), + map([ + // html + 'Combined' => \Jfcherng\Diff\Renderer\Html\Combined::class, + 'Inline' => \Jfcherng\Diff\Renderer\Html\Inline::class, + 'Json' => \Jfcherng\Diff\Renderer\Html\Json::class, + 'JsonHtml' => \Jfcherng\Diff\Renderer\Html\JsonHtml::class, + 'SideBySide' => \Jfcherng\Diff\Renderer\Html\SideBySide::class, + // text + 'Context' => \Jfcherng\Diff\Renderer\Text\Context::class, + 'JsonText' => \Jfcherng\Diff\Renderer\Text\JsonText::class, + 'Unified' => \Jfcherng\Diff\Renderer\Text\Unified::class, + ]) +); diff --git a/vendor/jfcherng/php-diff/LICENSE b/vendor/jfcherng/php-diff/LICENSE new file mode 100644 index 0000000..52d05aa --- /dev/null +++ b/vendor/jfcherng/php-diff/LICENSE @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2018-2022 Jack Cherng <jfcherng@gmail.com> +Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/jfcherng/php-diff/composer.json b/vendor/jfcherng/php-diff/composer.json new file mode 100644 index 0000000..66533f3 --- /dev/null +++ b/vendor/jfcherng/php-diff/composer.json @@ -0,0 +1,69 @@ +{ + "name": "jfcherng/php-diff", + "description": "A comprehensive library for generating differences between two strings in multiple formats (unified, side by side HTML etc).", + "type": "library", + "license": "BSD-3-Clause", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "minimum-stability": "beta", + "prefer-stable": true, + "authors": [ + { + "name": "Jack Cherng", + "email": "jfcherng@gmail.com" + }, + { + "name": "Chris Boulton", + "email": "chris.boulton@interspire.com" + } + ], + "autoload": { + "psr-4": { + "Jfcherng\\Diff\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Jfcherng\\Diff\\Test\\": "tests/" + } + }, + "require": { + "php": ">=7.1.3", + "jfcherng/php-color-output": "^2.0", + "jfcherng/php-mb-string": "^1.4.6", + "jfcherng/php-sequence-matcher": "^3.2.8" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "liip/rmt": "^1.6", + "phan/phan": "^2.5 || ^3 || ^4", + "phpunit/phpunit": ">=7 <10", + "squizlabs/php_codesniffer": "^3.6" + }, + "config": { + "platform": { + "php": "7.1.3" + }, + "sort-packages": true + }, + "scripts": { + "analyze": [ + "phan --color", + "phpcs --colors -n" + ], + "fix": [ + "php-cs-fixer fix --verbose" + ], + "server": [ + "Composer\\Config::disableProcessTimeout", + "@php -S localhost:12388 -t example/" + ], + "test": [ + "phpunit --verbose" + ] + } +} 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 " ", 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 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 " ". + * + * @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(' ', $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": "Відмінності" +} |