sistemazione composer install
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<?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_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()
|
||||
->exclude('libs')
|
||||
->exclude('tests/Fixtures')
|
||||
->exclude('var')
|
||||
->exclude('vendor')
|
||||
->in(__DIR__)
|
||||
)
|
||||
;
|
||||
|
||||
return $config;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
|
||||
## VERSION 3 REMOVE GLOBAL FUNCTIONS
|
||||
|
||||
* Version **3.0** - Remove global functions
|
||||
* 2021-05-27 10:44 **3.0.0** initial release
|
||||
* fd33968 chore(ci): no need for hirak/prestissimo as of Composer 2
|
||||
* bbdd9d8 chore(ci): also test against PHP 8.0
|
||||
* 069f3b6 chore: add "liip/rmt" as a dev dep
|
||||
* 4cb784c chore: update php-cs-fixer config
|
||||
* 1de05ac docs: update license year
|
||||
* fcd87d8 chore: update deps
|
||||
* 9587412 feat: remove helper.php
|
||||
* 088ade8 chore(ci): migrate CI from Travis to GitHub Actions
|
||||
* b06f614 chore: update license year
|
||||
|
||||
## VERSION 2 NAMESPACE CHANGE
|
||||
|
||||
* Version **2.0** - namespace change
|
||||
* 2020-05-28 03:24 **2.0.2** allow PHP 8
|
||||
* 1ef5198 chore: update deps
|
||||
* 3c6ba82 chore: Composer PHP constrain >=7.1.3
|
||||
* 5035eba chore: update deps
|
||||
* c19946b docs: update readme
|
||||
* 2020-04-06 15:51 **2.0.1** fix demo.php
|
||||
* 245aa2f chore: allow PHPUnit ^9
|
||||
* f139892 chore: update deps
|
||||
* a09f00f fix: fix demo.php
|
||||
* 8384498 Update deps
|
||||
* 40e0d43 Update .travis.yml to PHP 7.4 stable
|
||||
* 2746bcf Add php_codesniffer
|
||||
* 6f96961 Update readme to use badges from shields.io
|
||||
* 2019-07-13 17:50 **2.0.0** initial release
|
||||
* d522a7d Add UPGRADING_v2.md
|
||||
* 33fc823 Rename/modify CHANGELOG -> CHANGELOG.md
|
||||
* 056f68a Renamed namespace and functions
|
||||
* e036a8d Update deps
|
||||
* 2a0b002 Update deps
|
||||
* f51db25 Add .gitattributes
|
||||
* 2e644d4 $ composer fix
|
||||
* d9df730 Update deps
|
||||
* 6556ba0 Update .travis.yml for 7.4snapshot
|
||||
* e6b57cd Add .editorconfig
|
||||
* 8b93884 Update deps
|
||||
* e7be444 Temporary fix for CI
|
||||
* 434fff4 year++
|
||||
* 2c4077f Add tests for PHP 7.3
|
||||
|
||||
## VERSION 1 INIT
|
||||
|
||||
* Version **1.0** - init
|
||||
* 2018-11-27 21:25 **1.0.8** nits
|
||||
* 61d365e Update codes
|
||||
* b6e69f0 Update README
|
||||
* 69d84bf Update deps
|
||||
* 2018-09-04 18:56 **1.0.7** php-cs-fixer
|
||||
* 7dd1322 php-cs-fixer: enable native_constant_invocation and native_function_invocation
|
||||
* 1a1d921 Update deps
|
||||
* 1af0ae3 Rename CHANGELOG.md -> CHANGELOG
|
||||
* 47776c4 Update deps
|
||||
* 2de4f26 Update .travis.yml
|
||||
* 2018-07-18 08:47 **1.0.6** Add composer.lock
|
||||
* 0281a77 Add composer.lock
|
||||
* 2018-07-04 15:33 **1.0.5** nits
|
||||
* fcecbfb Update .rmt.yml
|
||||
* 2018-07-04 15:06 **1.0.4** initial release
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2021 Jack Cherng <jfcherng@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# php-color-output
|
||||
|
||||
[](https://github.com/jfcherng/php-color-output/actions)
|
||||
[](https://packagist.org/packages/jfcherng/php-color-output)
|
||||
[](https://packagist.org/packages/jfcherng/php-color-output)
|
||||
[](https://github.com/jfcherng/php-color-output/blob/master/LICENSE)
|
||||
[](https://github.com/jfcherng/php-color-output/stargazers)
|
||||
[](https://www.paypal.me/jfcherng/5usd)
|
||||
|
||||

|
||||
|
||||
The above screenshot is the output of `demo.php`. See the [Example](#example) section.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```text
|
||||
composer require jfcherng/php-color-output
|
||||
```
|
||||
|
||||
|
||||
## Available Styles
|
||||
|
||||
| Background | Foreground | Compound | Special | Alias |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| b_black | f_black | f_dark_gray | blink | b (bold) |
|
||||
| b_blue | f_blue | f_light_blue | bold | blk (blink) |
|
||||
| b_cyan | f_brown | f_light_cyan | dim | h (hidden) |
|
||||
| b_green | f_cyan | f_light_green | hidden | rev (reverse) |
|
||||
| b_light_gray | f_green | f_light_purple | reset | rst (reset) |
|
||||
| b_magenta | f_light_gray | f_light_red | reverse | u (underline) |
|
||||
| b_red | f_normal | f_white | underline | - |
|
||||
| b_yellow | f_purple | f_yellow | - | - |
|
||||
| - | f_red | - | - | - |
|
||||
|
||||
|
||||
## Functions and Methods
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Make a string colorful.
|
||||
*
|
||||
* @param string $str the string
|
||||
* @param string|string[] $colors the colors
|
||||
* @param bool $reset reset color at the end of the string?
|
||||
*
|
||||
* @return string the colored string
|
||||
*/
|
||||
\Jfcherng\Utility\CliColor::color(string $str, $colors = [], bool $reset = true): string
|
||||
|
||||
/**
|
||||
* Remove all colors from a string.
|
||||
*
|
||||
* @param string $str the string
|
||||
*
|
||||
* @return string the string without colors
|
||||
*/
|
||||
\Jfcherng\Utility\CliColor::noColor(string $str): string
|
||||
```
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
include __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use \Jfcherng\Utility\CliColor;
|
||||
|
||||
// colors in a string using a comma as the delimiter
|
||||
echo CliColor::color('foo', 'f_light_cyan, b_yellow'); // "\033[1;36;43mfoo\033[0m"
|
||||
|
||||
echo PHP_EOL;
|
||||
|
||||
// colors in an array
|
||||
echo CliColor::color('foo', ['f_white', 'b_magenta']); // "\033[1;37;45mfoo\033[0m"
|
||||
|
||||
echo PHP_EOL;
|
||||
|
||||
// do not auto reset color at the end of string
|
||||
echo CliColor::color('foo', ['f_red', 'b_green', 'b', 'blk'], false); // "\033[31;42;1;5mfoo"
|
||||
|
||||
// manually add color reset
|
||||
echo CliColor::color('', 'reset'); // "\033[0m"
|
||||
|
||||
echo PHP_EOL;
|
||||
|
||||
// remove all color codes from a string
|
||||
echo CliColor::noColor("\033[31;42;5mfoo\033[0mbar"); // "foobar"
|
||||
|
||||
echo PHP_EOL;
|
||||
```
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
define('RMT_ROOT_DIR', __DIR__);
|
||||
require 'vendor\liip\rmt\command.php';
|
||||
@@ -0,0 +1,4 @@
|
||||
## Upgrading to v2
|
||||
|
||||
- The class name/namespace has been changed from `Jfcherng\Color\Colorful` to `Jfcherng\Utility\CliColor`.
|
||||
- The names of helper functions have been changed from `str_color/str_nocolor` to `str_cli_color/str_cli_nocolor`.
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "jfcherng/php-color-output",
|
||||
"description": "Make your PHP command-line application colorful.",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jack Cherng",
|
||||
"email": "jfcherng@gmail.com"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"command-line",
|
||||
"color",
|
||||
"ansi-colors",
|
||||
"str-color"
|
||||
],
|
||||
"minimum-stability": "beta",
|
||||
"prefer-stable": true,
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jfcherng\\Utility\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Jfcherng\\Utility\\Test\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.19",
|
||||
"liip/rmt": "^1.6",
|
||||
"phan/phan": "^2 || ^3 || ^4",
|
||||
"phpunit/phpunit": ">=7 <10",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.1.3"
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": [
|
||||
"phan --color",
|
||||
"phpcs --colors -n"
|
||||
],
|
||||
"fix": [
|
||||
"php-cs-fixer fix --verbose"
|
||||
],
|
||||
"test": [
|
||||
"phpunit --verbose"
|
||||
]
|
||||
}
|
||||
}
|
||||
+3864
File diff suppressed because it is too large
Load Diff
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
include __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Jfcherng\Utility\CliColor;
|
||||
|
||||
// colors in a string using a comma as the delimiter
|
||||
echo CliColor::color('foo', 'f_light_cyan, b_yellow'); // "\033[1;36;43mfoo\033[0m"
|
||||
|
||||
echo \PHP_EOL;
|
||||
|
||||
// colors in an array
|
||||
echo CliColor::color('foo', ['f_white', 'b_magenta']); // "\033[1;37;45mfoo\033[0m"
|
||||
|
||||
echo \PHP_EOL;
|
||||
|
||||
// do not auto reset color at the end of string
|
||||
echo CliColor::color('foo', ['f_red', 'b_green', 'b', 'blk'], false); // "\033[31;42;1;5mfoo"
|
||||
|
||||
// manually add color reset
|
||||
echo CliColor::color('', 'reset'); // "\033[0m"
|
||||
|
||||
echo \PHP_EOL;
|
||||
|
||||
// remove all color codes from a string
|
||||
echo CliColor::noColor("\033[31;42;5mfoo\033[0mbar"); // "foobar"
|
||||
|
||||
echo \PHP_EOL;
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jfcherng\Utility;
|
||||
|
||||
/**
|
||||
* Make your PHP command-line application colorful.
|
||||
*
|
||||
* @see https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
*
|
||||
* @author Jack Cherng <jfcherng@gmail.com>
|
||||
*/
|
||||
final class CliColor
|
||||
{
|
||||
const COLOR_BEGIN = "\033[";
|
||||
const COLOR_END = 'm';
|
||||
|
||||
const COLOR_BEGIN_REGEX = "\033\\[";
|
||||
const COLOR_END_REGEX = 'm';
|
||||
|
||||
/**
|
||||
* @var array the color map
|
||||
*/
|
||||
private static $colorMap = [
|
||||
// background
|
||||
'b_black' => '40',
|
||||
'b_blue' => '44',
|
||||
'b_cyan' => '46',
|
||||
'b_green' => '42',
|
||||
'b_light_gray' => '47',
|
||||
'b_magenta' => '45',
|
||||
'b_red' => '41',
|
||||
'b_yellow' => '43',
|
||||
|
||||
// foreground
|
||||
'f_black' => '30',
|
||||
'f_blue' => '34',
|
||||
'f_brown' => '33',
|
||||
'f_cyan' => '36',
|
||||
'f_green' => '32',
|
||||
'f_light_gray' => '37',
|
||||
'f_normal' => '39',
|
||||
'f_purple' => '35',
|
||||
'f_red' => '31',
|
||||
|
||||
// compound
|
||||
'f_dark_gray' => '1;30',
|
||||
'f_light_blue' => '1;34',
|
||||
'f_light_cyan' => '1;36',
|
||||
'f_light_green' => '1;32',
|
||||
'f_light_purple' => '1;35',
|
||||
'f_light_red' => '1;31',
|
||||
'f_white' => '1;37',
|
||||
'f_yellow' => '1;33',
|
||||
|
||||
// special
|
||||
'blink' => '5',
|
||||
'bold' => '1',
|
||||
'dim' => '2',
|
||||
'hidden' => '8',
|
||||
'reset' => '0',
|
||||
'reverse' => '7',
|
||||
'underline' => '4',
|
||||
|
||||
// alias
|
||||
'b' => 'bold',
|
||||
'blk' => 'blink',
|
||||
'h' => 'hidden',
|
||||
'rev' => 'reverse',
|
||||
'rst' => 'reset',
|
||||
'u' => 'underline',
|
||||
|
||||
// regex for color codes
|
||||
'regex_any' => '(?:[0-9]++;?)++',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the color map.
|
||||
*
|
||||
* @return array the color map
|
||||
*/
|
||||
public static function getColorMap(): array
|
||||
{
|
||||
return self::$colorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a string colorful.
|
||||
*
|
||||
* @param string $str the string
|
||||
* @param string|string[] $colors the colors
|
||||
* @param bool $reset reset color at the end of the string?
|
||||
*
|
||||
* @return string the colored string
|
||||
*/
|
||||
public static function color(string $str, $colors = [], bool $reset = true): string
|
||||
{
|
||||
// always convert $colors into an array
|
||||
if (\is_string($colors)) {
|
||||
$colors = \explode(',', $colors);
|
||||
}
|
||||
|
||||
$colored = self::getColorCode($colors) . $str;
|
||||
|
||||
if ($reset) {
|
||||
$colored .= self::getColorCode(['reset']);
|
||||
}
|
||||
|
||||
return self::simplifyColoredString($colored);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all colors from a string.
|
||||
*
|
||||
* @param string $str the string
|
||||
*
|
||||
* @return string the string without colors
|
||||
*/
|
||||
public static function noColor(string $str): string
|
||||
{
|
||||
return \preg_replace(
|
||||
'~' . self::getColorCode(['regex_any'], true) . '~uS',
|
||||
'',
|
||||
$str
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color code from given colors.
|
||||
*
|
||||
* @param array $colors the colors
|
||||
* @param bool $returnRegex return as an regex segment
|
||||
*
|
||||
* @return string the color code
|
||||
*/
|
||||
private static function getColorCode(array $colors, bool $returnRegex = false): string
|
||||
{
|
||||
$colors = self::sanitizeColors($colors);
|
||||
|
||||
if (empty($colors)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// convert color into color code
|
||||
$colorCodes = \array_map(
|
||||
function (string $color): string {
|
||||
// resolve color alias
|
||||
while (isset(self::$colorMap[$color])) {
|
||||
$color = self::$colorMap[$color];
|
||||
}
|
||||
|
||||
return $color;
|
||||
},
|
||||
$colors
|
||||
);
|
||||
|
||||
$closures = $returnRegex
|
||||
? [self::COLOR_BEGIN_REGEX, self::COLOR_END_REGEX]
|
||||
: [self::COLOR_BEGIN, self::COLOR_END];
|
||||
|
||||
return $closures[0] . \implode(';', $colorCodes) . $closures[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize colors.
|
||||
*
|
||||
* @param array $colors the colors
|
||||
*
|
||||
* @return array the sanitized colors
|
||||
*/
|
||||
private static function sanitizeColors(array $colors): array
|
||||
{
|
||||
return self::listUnique(\array_filter(
|
||||
\array_map('trim', $colors),
|
||||
function (string $color): bool {
|
||||
return isset(self::$colorMap[$color]);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplify the colored string.
|
||||
*
|
||||
* @param string $str the colored string
|
||||
*
|
||||
* @return string the simplified colored string
|
||||
*/
|
||||
private static function simplifyColoredString(string $str): string
|
||||
{
|
||||
// replace multiple consecutive resets with a single reset
|
||||
$str = \preg_replace(
|
||||
'~(' . self::getColorCode(['reset'], true) . '){2,}~uS',
|
||||
'$1',
|
||||
$str
|
||||
);
|
||||
|
||||
// remove colors for an emtpy string
|
||||
$str = \preg_replace(
|
||||
(
|
||||
'~' .
|
||||
'(' . self::getColorCode(['regex_any'], true) . ')' .
|
||||
'(' . self::getColorCode(['reset'], true) . ')' .
|
||||
'~uS'
|
||||
),
|
||||
'$2',
|
||||
$str
|
||||
);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* The fastest array_unique() implementation for a non-associative array AFAIK.
|
||||
*
|
||||
* @see https://stackoverflow.com/questions/8321620/array-unique-vs-array-flip
|
||||
*
|
||||
* @param array $array the array
|
||||
*/
|
||||
private static function listUnique(array $array): array
|
||||
{
|
||||
return \array_keys(\array_count_values($array));
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
$config = (new PhpCsFixer\Config())
|
||||
->setIndent(" ")
|
||||
->setLineEnding("\n")
|
||||
->setCacheFile(__DIR__ . '/.php-cs-fixer.cache')
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PHP71Migration' => true,
|
||||
'@PHP71Migration:risky' => true,
|
||||
'@PHP73Migration' => true,
|
||||
'@PHP74Migration' => true,
|
||||
'@PHP74Migration:risky' => true,
|
||||
'@PHP80Migration' => false,
|
||||
'@PHP80Migration:risky' => true,
|
||||
'@PHP81Migration' => 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_type_declaration' => true,
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'echo_tag_syntax' => ['format' => 'short'],
|
||||
'fully_qualified_strict_types' => true,
|
||||
'heredoc_indentation' => false, // disabling this makes test cases more readable
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'list_syntax' => ['syntax' => 'short'],
|
||||
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
|
||||
'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'],
|
||||
'native_constant_invocation' => ['scope' => 'all'],
|
||||
'native_function_invocation' => ['scope' => 'all', 'include' => ['@compiler_optimized']],
|
||||
'native_type_declaration_casing' => true,
|
||||
'no_alternative_syntax' => true,
|
||||
'no_null_property_initialization' => 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_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_order_by_value' => ['annotations' => ['covers']],
|
||||
'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,
|
||||
'static_lambda' => false,
|
||||
'strict_comparison' => false,
|
||||
'strict_param' => false,
|
||||
'string_implicit_backslashes' => false,
|
||||
'string_line_ending' => true,
|
||||
'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arrays', 'arguments']],
|
||||
'types_spaces' => ['space_multiple_catch' => 'single'],
|
||||
'yoda_style' => false,
|
||||
])
|
||||
->setFinder(
|
||||
PhpCsFixer\Finder::create()
|
||||
->notPath([
|
||||
'/branch-\\w+/', // git worktree
|
||||
'example/demo_web.php',
|
||||
])
|
||||
->exclude('libs')
|
||||
->exclude('tests/data')
|
||||
->exclude('tests/Fixtures')
|
||||
->exclude('var')
|
||||
->exclude('vendor')
|
||||
->in(__DIR__)
|
||||
)
|
||||
;
|
||||
|
||||
return $config;
|
||||
+43
@@ -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,
|
||||
])
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
## VERSION 2 NEW START
|
||||
|
||||
* Version **2.2** - char-level diff performance++
|
||||
* 2019-01-20 21:22 **2.2.0** initial release
|
||||
* b28ce64 Remove dependancy: jfcherng/php-levenshtein-distance
|
||||
|
||||
* Version **2.1** - init
|
||||
* 2019-01-20 11:57 **2.1.23** fix Json template
|
||||
* ced5f33 Fix the Json template
|
||||
* 7b21dc5 Update readme
|
||||
* 2019-01-20 09:01 **2.1.22** nits
|
||||
* 4dc695f Refactor template classes
|
||||
* 59824ed Code tidy
|
||||
* 7a5c1fb Code tidy
|
||||
* 100dd67 Update readme
|
||||
* 2019-01-19 09:17 **2.1.21** nits
|
||||
* 36f45d4 Code tidy
|
||||
* a3e70d1 Make the diff object in the renderer non-public
|
||||
* f13ad57 Code tidy
|
||||
* f430a8e nits
|
||||
* c4ddbb7 Add renderer option: spacesToNbsp
|
||||
* bbc88a1 htmlFixSpaces() may generate shorter output
|
||||
* 1d1ad4c Use more preservative reserved chars from Unicode reserved areas
|
||||
* 934706c Update deps
|
||||
* 2019-01-19 05:35 **2.1.20** small performance tweaks
|
||||
* 6516349 Small performance tweaks
|
||||
* d4b61a1 nits
|
||||
* 2019-01-18 08:52 **2.1.19** coding style fixes
|
||||
* e4284d6 php-cs-fixer
|
||||
* d91ec6f Update deps
|
||||
* ce3f817 nits
|
||||
* 76cd7f3 Fix phan error
|
||||
* 7fbd791 Revise badges in README
|
||||
* 887ee9f Add Codacy badge
|
||||
* 4c159f6 Some coding style tweaks
|
||||
* ee1c2a5 year++
|
||||
* eca66d7 Add tests for PHP 7.3
|
||||
* 2018-12-17 21:34 **2.1.18** Add Russian translation
|
||||
* efeee17 Update deps
|
||||
* 0f28b9d Fix tests
|
||||
* 767c432 Prefer using "rus" as the language code
|
||||
* 70a9275 Add new lang
|
||||
* 9c449ba nits
|
||||
* 08bc260 Update deps
|
||||
* 2018-11-20 19:31 **2.1.17** nits
|
||||
* 9297443 nits
|
||||
* 9251d8b Add FileNotFoundException
|
||||
* 7684942 Update deps
|
||||
* 04590f1 Update README
|
||||
* 0e11e19 Alias bg.php to bul.php
|
||||
* e186ff7 micro code tweak
|
||||
* 28a423f Fix some typos
|
||||
* 96c679c Update deps
|
||||
* 2018-09-16 14:34 **2.1.16** Bulgarian translation
|
||||
* e7fafe6 Bulgarian translation
|
||||
* 2018-09-04 18:56 **2.1.15** php-cs-fixer
|
||||
* 799cae1 php-cs-fixer: enable native_constant_invocation and native_function_invocation
|
||||
* 96ee1d2 Update deps
|
||||
* 83e3a54 Rename CHANGELOG.md -> CHANGELOG
|
||||
* 81364db Update deps
|
||||
* 605a963 Update .travis.yml
|
||||
* 2018-07-18 08:41 **2.1.14** Add composer.lock
|
||||
* 726b3a6 Add composer.lock
|
||||
* e36f649 Update README.md
|
||||
* 2018-07-04 15:26 **2.1.13** nits
|
||||
* f2ff363 Update .rmt.yml
|
||||
* 2018-07-04 14:59 **2.1.12** initial release
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## VERSION 3 SUN RISE
|
||||
|
||||
* Version **3.3** - JSON format language files
|
||||
* 2019-01-25 01:06 **3.3.0** initial release
|
||||
* 395b802 Use JSON format language files
|
||||
* c299e39 Remove debug codes
|
||||
|
||||
* Version **3.2** - add none-level diff
|
||||
* 2019-01-24 23:54 **3.2.6** nits
|
||||
* 7d9ca9c Code tidy
|
||||
* ce5273a Code tidy
|
||||
* 42bdf83 Remove a unnecesary "else"
|
||||
* 2019-01-24 19:44 **3.2.5** minify diff table block separator
|
||||
* 215f300 Change diff table block separator rendering
|
||||
* 08f3cf7 Remove extra empty lines when setting context to 0
|
||||
* 2019-01-24 15:32 **3.2.4** nits
|
||||
* af51b33 Add more punctuations as word separators
|
||||
* 9e649d8 Fix typos
|
||||
* 2019-01-24 14:48 **3.2.3** fix LineRendererFactory case-sensitive
|
||||
* 4825b96 Fix LineRendererFactory case-sensitive problem
|
||||
* 2019-01-24 14:21 **3.2.2** nits
|
||||
* 4b74e4f PreservedConstantInterface -> ReservedConstantInterface
|
||||
* 2019-01-24 14:18 **3.2.1** nits
|
||||
* d5206fb Refactor out AbstractHtml::renderChangedExtentByXXX()
|
||||
* 2019-01-24 12:49 **3.2.0** initial release
|
||||
* 9e6e37a Add none-level diff
|
||||
* 5e05555 Add line-level diff to example.php
|
||||
* cdc19df Unroll if-conditions for ReverseIterator::fromArray()
|
||||
|
||||
* Version **3.1** - word-level diff
|
||||
* 2019-01-22 15:32 **3.1.4** fix renderer options
|
||||
* d7dbacf Fix render options
|
||||
* 2019-01-22 13:55 **3.1.3** nist
|
||||
* 2d139a8 Code tidy
|
||||
* d54e4f1 Update deps
|
||||
* 9aeb234 Add ITERATOR_GET_KEY, ITERATOR_GET_BOTH for ReverseIterator
|
||||
* 11c7652 Code tidy
|
||||
* 2019-01-22 12:36 **3.1.2** nits
|
||||
* 1bef1a0 Diff's $a, $b are now required in \__construct()
|
||||
* 22426a5 Add Diff::setAB()
|
||||
* 2019-01-22 12:05 **3.1.1** fix ignoreCase/ignoreWhitespace
|
||||
* 300e4cf Fix ignoreCase/ignoreWhitespace for word/char-level diff
|
||||
* 9ea24dd Update readme
|
||||
* 2019-01-22 11:49 **3.1.0** initial release
|
||||
* f6fbce8 Suppress phan errors
|
||||
* 6850cb5 Add word-level diff
|
||||
* aea5ff8 Fix demo.php
|
||||
|
||||
* Version **3.0** - options changed
|
||||
* 2019-01-21 16:41 **3.0.0** initial release
|
||||
* d7b6079 Clear up options
|
||||
* c9a43a1 Fix diff getInstance()
|
||||
* 1b99649 Code tidy
|
||||
* f5cd010 Update demo.php
|
||||
@@ -0,0 +1,98 @@
|
||||
|
||||
## VERSION 4 AT NOON
|
||||
|
||||
* Version **4.2** - helper methods
|
||||
* 2019-03-03 06:36 **4.2.3** tiny fixes
|
||||
* c3d1512 Make Language::getTranslationsByLanguage() safer
|
||||
* df4488e Fix a typo
|
||||
* 8a41e9a Use native class name
|
||||
* fa48647 Fix a typo in readme
|
||||
* 2019-03-03 04:03 **4.2.2** final release for v4
|
||||
* 86e32c0 Adapt some non-breaking changes from the master branch
|
||||
* 35c1303 nits
|
||||
* 637e07e Update deps
|
||||
* 80c1861 Move CHANGELOG and UPGRADING
|
||||
* 2019-02-27 01:46 **4.2.1** fix typo
|
||||
* 37f7f75 Fix DiffHelp::getTemplatesInfo() typos
|
||||
* 3a3c276 Revise phpdoc
|
||||
* c221c25 Add (private) Diff::finalize() to help maintain cache vadility
|
||||
* 2019-02-26 14:49 **4.2.0** initial release
|
||||
* 43566f1 php-cs-fixer
|
||||
* be70f5e Add DiffHelper::getProjectDirectory()
|
||||
* 4937417 Add INFO['type'] for renderers
|
||||
* 3ef51f6 Rewrite DiffHelper::getTemplatesInfo() and DiffHelper::getStyleSheet()
|
||||
* 5e0f0da Add DiffHelper::getStyleSheet()
|
||||
* c7e5312 require-dev at least phpunit/phpunit 7.5
|
||||
* 01d52fd Add DiffHelper::calculateFiles()
|
||||
* 4601400 No need to clear cached value if $old/$new does not change
|
||||
* d690144 Code tidy
|
||||
|
||||
* Version **4.1** - Allow negative tabSize
|
||||
* 2019-02-22 02:51 **4.1.9** fix Json renderer with empty changes
|
||||
* d842546 Fix Json renderer with empty changes
|
||||
* 2019-02-22 02:38 **4.1.8** fix dep
|
||||
* c2fb459 squizlabs/php_codesniffer should be in require-dev
|
||||
* 2019-02-22 02:16 **4.1.7** fix them
|
||||
* 88ff257 Update example/old_file.txt and the new one.
|
||||
* 32537b9 Fix AbstractHtml::expandTabs() $tabSize undeclared
|
||||
* 37bd345 Fix problem caused by in OP_EQ, it still may be $old !== $new
|
||||
* a821017 AbstractHtml::expandTabs() add argument: $onlyLeadingTabs
|
||||
* bd00670 Code tidy
|
||||
* 2031306 Update readme
|
||||
* 0637909 Move upgrading guide from readme to other files
|
||||
* 5e932e6 Separate CHANGELOG by major version
|
||||
* 2019-02-21 19:50 **4.1.5** cs tweak
|
||||
* 1464d86 Update .rmt.yml
|
||||
* b3b0595 Code tidy
|
||||
* 7caae81 Update readme
|
||||
* 347935c Update deps
|
||||
* 6f8e9dd Uniform the term "$a, $b, from, to" into "old, new"
|
||||
* f7679ff Code tidy
|
||||
* 159d244 nits
|
||||
* 76675e4 Tiny regex performance increasement
|
||||
* 42eeff7 Fix 120 chars per line
|
||||
* 86f2325 Introduce squizlabs/php_codesniffer
|
||||
* 2019-02-20 18:49 **4.1.4** Fix Diff::getText()
|
||||
* 540daf5 Fix potential boundary error in Diff::getText()
|
||||
* 2b723e7 Revert "Inline and remove Diff::getText()"
|
||||
* 2019-02-20 16:45 **4.1.3** Fix Diff::getText()
|
||||
* a5171c8 Inline and remove Diff::getText()
|
||||
* be52d42 Fix Diff::getText() when $end is null
|
||||
* 3a6259a Code tidy
|
||||
* 1fd7935 Update option comments
|
||||
* 2019-02-20 15:04 **4.1.2** update deps
|
||||
* df25c88 Update jfcherng/php-sequence-matcher ^2.0
|
||||
* 0c7b425 Update readme
|
||||
* 2019-02-19 19:36 **4.1.1** fix some HTML templates
|
||||
* 0ed0e53 Fix HTML renderer should not emphasize inserted/deleted lines
|
||||
* 6e97b8a Update output example in readme
|
||||
* 80db0a0 Fix HTML special chars in JSON renderer should be escaped
|
||||
* 09a405e Code tidy
|
||||
* 6be0a0e Update deps
|
||||
* cf5bff2 AbstractHtml::expandTabs() has a default $tabSize = 4
|
||||
* 2019-02-16 18:36 **4.1.0** initial release
|
||||
* 53e6b17 Allow renderer option tabSize < 0
|
||||
* e60fb5c Remove useless codes
|
||||
* bf85310 Add more punctuations for word-level line renderer
|
||||
* 847b795 Update readme
|
||||
* ba77110 nits
|
||||
* b13149a Update deps
|
||||
|
||||
* Version **4.0** - Strip out the SequenceMatcher
|
||||
* 2019-02-08 20:17 **4.0.1** nits
|
||||
* 8f11cb1 Fix a typo
|
||||
* 54351a6 Move factory classes to Jfcherng\Diff\Factory\
|
||||
* 6d68583 Merge ReservedConstantInterface into RendererConstant
|
||||
* 2019-02-08 10:04 **4.0.0** initial release
|
||||
* fac4dae nits
|
||||
* b6fa08d Strip out SequenceMatcher as an external package
|
||||
* 2349bb2 Demo uses "font-family: monospace"
|
||||
* ed3f47c Temporary fix for CI
|
||||
* 538f270 Update tests
|
||||
* 11cf372 Fix typos
|
||||
* d56ad7e Use native exceptions
|
||||
* 67351d1 Update deps
|
||||
* 8b14dae Add "final" to classes non-inherited
|
||||
* ee6e5a3 Add UTF-8-ready demo
|
||||
* bf686a2 Prepare for being a new package
|
||||
* 04fb079 nits
|
||||
@@ -0,0 +1,90 @@
|
||||
|
||||
## VERSION 5 AFTERNOON
|
||||
|
||||
* Version **5.2** - clean up
|
||||
* 2019-05-15 00:17 **5.2.2** Final Release for v5
|
||||
* 2bd27d4 Update deps
|
||||
* fa189fa Update deps
|
||||
* 6b30019 Update deps
|
||||
* abc0d8a $ composer fix
|
||||
* 7cb5a75 Update deps
|
||||
* 70f66a2 Update .travis.yml for 7.4snapshot
|
||||
* e7e1839 Update deps
|
||||
* 1cee802 Update readme
|
||||
* 756970a Add .editorconfig
|
||||
* e6350bc Change screenshot size
|
||||
* 55748a5 Update deps
|
||||
* 03b0c55 Freeze documentation assets for v5
|
||||
* bd61843 Freeze documentation assets for v4
|
||||
* 2019-03-04 23:03 **5.2.1** tiny fixes
|
||||
* ed2bf46 Fix Diff::getOldNewComparison() should use finalize()
|
||||
* ecde729 Fix codacy's complaint
|
||||
* 5efa215 Revise the Language class (no BC break)
|
||||
* 3b369f8 Type-safe for Language::getTranslationsByLanguage()
|
||||
* d442360 Remove unneccesary phan error suppression
|
||||
* 8dffcb3 Fix readme badge
|
||||
* 2019-03-04 04:05 **5.2.0** initial release
|
||||
* ef500d0 Deprecate AbstractRenderer::getIdenticalResult()
|
||||
* 88648e3 nits
|
||||
* 0861774 $ php-cs-fix fix
|
||||
* bfbdd3d [php-cs-fixer] Add new rules: @PhpCsFixer, @PhpCsFixer:risky
|
||||
* dfeb1b8 Improve JSON decoding in Language::getTranslationsByLanguage()
|
||||
* 414eb29 Release of new version 4.2.3
|
||||
* 9d51c33 Make Language::getTranslationsByLanguage() safer
|
||||
* 8acb535 Fix a typo
|
||||
* 2b620b3 Use native class name
|
||||
* b76301a Fix a typo in readme
|
||||
* 8745678 Release of new version 4.2.2
|
||||
* 6117ab9 Adapt some non-breaking changes from the master branch
|
||||
* 8b06983 nits
|
||||
* 5659dc4 Update deps
|
||||
* cfb2652 Move CHANGELOG and UPGRADING
|
||||
* 28216ed Release of new version 4.2.1
|
||||
|
||||
* Version **5.1** - helper methods
|
||||
* 2019-02-27 01:47 **5.1.1** fix typo
|
||||
* 868c542 Fix DiffHelp::getTemplatesInfo() typos
|
||||
* 80cd8e7 Revise phpdoc
|
||||
* 0a202c8 Add (private) Diff::finalize() to help maintain cache vadility
|
||||
* 2019-02-26 14:57 **5.1.0** initial release
|
||||
* 00ae199 Fix .rmt.yml
|
||||
* 7a700fa Release of new version 4.2.0
|
||||
* 770636a php-cs-fixer
|
||||
* 0304537 Add INFO['type'] for renderers
|
||||
* a9bee51 Rewrite DiffHelper::getTemplatesInfo() and DiffHelper::getStyleSheet()
|
||||
* 9f9809e Add DiffHelper::getProjectDirectory()
|
||||
* 4d739c4 Add DiffHelper::getStyleSheet()
|
||||
* ec11495 require-dev at least phpunit/phpunit 7.5
|
||||
* 2553620 Add DiffHelper::calculateFiles()
|
||||
* a4f07c0 No need to clear cached value if $old/$new does not change
|
||||
* ff019a9 Code tidy
|
||||
* 02b30c8 Update deps
|
||||
* 742c096 Update UPGRADING_v5.md
|
||||
* 0ffd644 Code tidy
|
||||
* b619f91 Update readme
|
||||
|
||||
* Version **5.0** - AFTERNOON
|
||||
* 2019-02-22 02:55 **5.0.2** fix Json renderer with empty changes
|
||||
* 6ca5b97 Fix Json renderer with empty changes
|
||||
* e512f2d Update CHANGELOG_v4.md
|
||||
* 2019-02-22 02:35 **5.0.1** fix dep
|
||||
* 7757363 squizlabs/php_codesniffer should be in require-dev
|
||||
* 6ac0c99 Update .rmt.yml
|
||||
* 2019-02-22 02:24 **5.0.0** initial release
|
||||
* 9f85206 Update .rmt.yml
|
||||
* 064a3b3 Add renderer option: outputTagAsString
|
||||
* 23c59f8 Update jfcherng/php-sequence-matcher ^3.0
|
||||
* 1eb6bbb Update example/old_file.txt and the new one.
|
||||
* b9f2f7b Fix AbstractHtml::expandTabs() $tabSize undeclared
|
||||
* 469abb7 Fix problem caused by in OP_EQ, it still may be $old !== $new
|
||||
* 82ed9ca AbstractHtml::expandTabs() add argument: $onlyLeadingTabs
|
||||
* dd6d8dc Code tidy
|
||||
* 4750267 Move upgrading guide from readme to other files
|
||||
* 12b6024 Separate CHANGELOG by major version
|
||||
* ebbb509 Update readme
|
||||
* ef735c9 Code tidy
|
||||
* 4518927 Uniform more "from, to" into "old, new"
|
||||
* 079a5ec Update readme
|
||||
* dbcf0b8 Update readme
|
||||
* b84bef5 Remove deprecated APIs
|
||||
* 5330cd2 Uniform the term "base, changed" into "old, new"
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
|
||||
## VERSION 6 UNIFIED
|
||||
|
||||
* Version **6.16** - new differ option: fullContextIfIdentical
|
||||
* 2024-03-11 01:40 **6.16.2** remove debug msg
|
||||
* 8bafc7b fix: remove debug print
|
||||
* 2024-03-08 01:59 **6.16.1** fix Differ::getStatistics()
|
||||
* e6d485d fix: Differ::getStatistics() not working when no difference
|
||||
* 26f93d7 chore: update deps
|
||||
* 2024-03-05 16:44 **6.16.0** initial release
|
||||
* 809088e chore: $ composer fix
|
||||
* d07f241 chore: fix deprecated php-cs-fixer options
|
||||
* 00c7a38 chore: update deps
|
||||
* 5d094e4 feat: add new differ option: fullContextIfIdentical (#79)
|
||||
* bf2ddb8 chore: tidy codes
|
||||
* e0da986 style: $ composer fix
|
||||
* 5b70504 chore: ignore example/demo_web.php from php-cs-fixer
|
||||
* e1575a2 chore: update deps
|
||||
* 73069e7 chore: fix typos (#75)
|
||||
|
||||
* Version **6.15** - max sequence length
|
||||
* 2023-06-15 20:29 **6.15.3** add translation: cze
|
||||
* 2335d9e docs: update translation readme
|
||||
* d50beb0 chore: add traslation Czech (#74)
|
||||
* 2023-06-05 19:41 **6.15.2** add translation: nob
|
||||
* 9d55d2f chore: Norwegian translation (#73)
|
||||
* 2023-05-26 00:23 **6.15.1** update deps
|
||||
* b73d9e8 chore: update deps
|
||||
* 2023-05-21 16:04 **6.15.0** initial release
|
||||
* 5089d99 feat: add new differOption: lengthLimit
|
||||
* 9182995 chore: update deps
|
||||
|
||||
* Version **6.14** - add differ option: ignoreLineEnding
|
||||
* 2023-03-16 03:26 **6.14.2** (Combined) fix unmerge-able block
|
||||
* aeb1c3c fix: (Combined) unmerge-able block shouldn't be repeated
|
||||
* 341f943 Release of new version 6.14.1
|
||||
* 2023-03-11 15:24 **6.14.1** tidy codes
|
||||
* 71f0b0e refactor: move "ignoreLineEnding" implementation to sequence matcher
|
||||
* 77d4dbb test: tidy codes
|
||||
* 25d344b chore: update deps
|
||||
* df6db81 chore: fix typos
|
||||
* 2023-03-10 20:33 **6.14.0** initial release
|
||||
* 3b1e344 feat: add differ option: ignoreLineEnding
|
||||
* b194161 chore: move test files
|
||||
* 882ea1a chore: update deps
|
||||
|
||||
* Version **6.13** - override language
|
||||
* 2023-02-27 19:28 **6.13.1** another way to fix #34
|
||||
* 7b89fd7 fix: another way to fix \r shouldn't be visualized like a newline in Combined renderer
|
||||
* c069031 chore: add missing type hint
|
||||
* 2023-01-19 00:29 **6.13.0** initial release
|
||||
* bfb89b6 feat: provide a way to override an existing language
|
||||
* 775a043 chore: update deps
|
||||
* c1638b3 style: format readme with prettier
|
||||
* 5eb2dab docs: fix build status badge in readme
|
||||
* 33b5bf5 chore: update deps
|
||||
* 8063d63 style: reduce indentation for the demo codes
|
||||
* 815eb0f test: test against PHP 8.2
|
||||
|
||||
* Version **6.12** - new rendererOption: spaceToHtmlTag
|
||||
* 2022-09-26 22:33 **6.12.1** fix: side-by-side column too narrow
|
||||
* e12a477 style: make CI happy
|
||||
* 2022-09-25 21:58 **6.12.0** initial release
|
||||
* 97791ea feat: new rendererOption: spaceToHtmlTag (#60)
|
||||
* 47f9e74 revert: partially revert ef08158
|
||||
* df88798 fix: select the code only (#59)
|
||||
* ef08158 fix: wrong visual tabSize on browser
|
||||
* 397fb17 chore: fix typo/spelling (#57)
|
||||
* 7da0452 chore: update deps
|
||||
* e579d7e chore: update CI deps
|
||||
|
||||
* Version **6.11** - Bump PHP version to 7.4
|
||||
* 2022-06-26 13:42 **6.11.6** fix Combined::renderArray
|
||||
* 710be7f test: update tests
|
||||
* fea80b0 fix: Combined::renderArray isn't working properly
|
||||
* a8eabe3 chore: update deps
|
||||
* 2022-04-03 00:35 **6.11.5** Update upstream
|
||||
* 52551c6 chore: update deps
|
||||
* 4b08ef2 chore: change rules for php-cs-fixer 3.8
|
||||
* ba4dc50 chore: update deps
|
||||
* fc6613b chore: update deps
|
||||
* 2022-03-07 19:24 **6.11.4** add Italian translation
|
||||
* 6d4b53e feat: add Italian translation (#53)
|
||||
* f94c48f refactor: tidy codes
|
||||
* 2022-03-01 00:23 **6.11.3** PHP 7.4 refactoring
|
||||
* 27a92e4 refactor: utilize PHP 7.4 typed properties
|
||||
* 8ec5d02 refactor: utilize PHP 7.4 array spread operator
|
||||
* 7372b0a refactor: utilize PHP 7.4 arrow funciton
|
||||
* 74db0b4 refactor: utilize PHP 7.4 null coalescing assignment
|
||||
* 02c14b8 chore: update deps
|
||||
* fafa8a8 chore: remove "todo" from test data
|
||||
* 2022-02-21 13:54 **6.11.2** Fix PHP alpine
|
||||
* a589955 chore: update deps
|
||||
* 1b537d5 chore: license year +1
|
||||
* 2021-11-25 10:15 **6.11.0** initial release
|
||||
* 65610ae style: reformat with php-cs-fixer
|
||||
* 664ff0f chore: update php-cs-fixer rules for v3
|
||||
* 84abe9f chore: bump min PHP to 7.4.0 and update other deps
|
||||
* d55fd28 chore: remove "@todo" in readme
|
||||
* 8130948 docs: revise instructions for running demo
|
||||
|
||||
* Version **6.10** - feat: add methods to get diff statistics
|
||||
* 2021-09-02 12:03 **6.10.2** Bump deps
|
||||
* bf18634 chore: bump "jfcherng/php-sequence-matcher" 3.2.8
|
||||
* 2021-09-02 11:58 **6.10.1** Bump deps
|
||||
* 795555c chore: bump "jfcherng/php-sequence-matcher" 3.2.7
|
||||
* 814782b chore: add RMT into ignore-export
|
||||
* 98fbe77 chore: update demo js/css URLs
|
||||
* 809ea0f docs: update badegs
|
||||
* 17d912e style: fix some long lines
|
||||
* 7e9a001 style: add visibility to public consts
|
||||
* 2a28fe1 chore: update deps
|
||||
* 2021-03-19 17:20 **6.10.0** initial release
|
||||
* 56eee87 chore: add liip/rmt as a dev dependency
|
||||
* 4b170cd feat: add methods to get diff statistics
|
||||
* d95e0b8 chore: update deps
|
||||
* 4c6e2ab chore: update license year
|
||||
* f40467c chore: allow phan/phan ^4
|
||||
* bd322af fix: deprecated method in .php_cs
|
||||
* ab25d01 chore(ci): fix php-cs-fixer rule as of 2.17.0
|
||||
* c0d13c5 chore: update deps
|
||||
|
||||
* Version **6.9** - add option: showHeader
|
||||
* 2020-12-08 07:55 **6.9.0** initial release
|
||||
* 9d98db2 chore: exclude "tests/IgnoreWhitespaceTest.php" from formatting
|
||||
* 8f33ca2 style: php-cs-fix
|
||||
* 5e5f038 fix: undefined index 'showHeader'
|
||||
* 5f0dd2d feat: add option: showHeader
|
||||
* 863fbe1 chore(ci): real test against PHP 8.0
|
||||
* 7e830bf chore: update deps
|
||||
* 4576720 chore: update php-cs-fixer 2.17.0
|
||||
* 3fede5b chore(ci): test against PHP 8.0 as well
|
||||
* a342715 docs: fix typo
|
||||
* 6ec0dd7 docs: add description links for Unified/Context diff formats
|
||||
|
||||
* Version **6.8** - new renderer: JsonText
|
||||
* 2020-10-29 18:49 **6.8.0** initial release
|
||||
* 33339b8 chore: update deps
|
||||
* efa7217 feat: add renderer: JsonText
|
||||
* f7dac30 chore: update frontend Prism 1.22.0
|
||||
* 3494b40 docs: update permanent link
|
||||
* fceef80 docs: fix typos
|
||||
* 2cdcb6b chore: nits
|
||||
* ef66d12 chore: udpate deps
|
||||
* 8f765ec chore: Composer 2 no longer needs hirak/prestissimo
|
||||
* 14fe79b chore: update deps
|
||||
|
||||
* Version **6.7** - feat: support colorized CLI output for text renderers
|
||||
* 2020-08-23 18:52 **6.7.7** fix a regression about the last diff block
|
||||
* 2ce0540 refactor: migrate to jfcherng/php-sequence-matcher 3.2.5
|
||||
* 29115e2 chore: update deps
|
||||
* f7f6e4d style: run php-cs-fix
|
||||
* ba2fc9d chore: do not check/fix coding style for tests/data/
|
||||
* 8d4c888 fix: sytnax error for PHP 7.1
|
||||
* aa34ce3 test: add failing test (#36)
|
||||
* 2020-08-22 19:08 **6.7.6** fix "ignoreWhitespaces" not working
|
||||
* dd6a211 fix: "ignoreWhitespaces" doesn't produce the same result with GNU diff's
|
||||
* dd96f78 chore: update deps
|
||||
* 0fd52fb test: fixup: follow GNU's diff output
|
||||
* f8c08d0 Failing test for ignoreWhitespace
|
||||
* f0ed073 fix: use "static" to call non-static methods
|
||||
* 2020-08-10 23:39 **6.7.5** fix \r visualization in Combined renderer
|
||||
* 35a2502 fix: \r shouldn't be visualized like a newline in Combined renderer
|
||||
* cb2bed2 chore: update deps
|
||||
* 2020-07-06 11:10 **6.7.4** fix HTML escape for Combined renderer
|
||||
* 762238b chore: update deps
|
||||
* d32c7b6 fix: OP_EQ lines are not escaped in Combined renderer
|
||||
* ef6dcb4 test: make phan happy
|
||||
* dc2db03 chore: update readme badge
|
||||
* d143c26 chore(ci): migrate CI from Travis to GitHub Actions
|
||||
* 2020-05-28 03:22 **6.7.3** allow PHP 8
|
||||
* 3ab8a9e chore: update deps
|
||||
* 1b931eb chore: Composer PHP constrain >=7.1.3
|
||||
* 37036c2 chore: add some comments
|
||||
* 2020-04-27 01:49 **6.7.2** fix Combined internal HTML formatting
|
||||
* 8f8d919 chore: update deps
|
||||
* e172e4a fix: unexpected Combined output due to HTML formatting
|
||||
* 8011802 chore: rename example/demo.php -> example/demo_web.php
|
||||
* 444342d refactor: tidy codes
|
||||
* 2020-04-07 02:18 **6.7.1** refactor: colorization with jfcherng/php-color-output
|
||||
* 9c3da80 refactor: colorization with jfcherng/php-color-output
|
||||
* ea2d43d docs: adjust demo colors
|
||||
* 2020-04-06 20:29 **6.7.0** initial release
|
||||
* 40f0e74 test: fix test for CLI color output
|
||||
* ba9dd9e chore: update deps
|
||||
* 857f4b0 fix: make phan happy
|
||||
* 129d51e docs: add example/demo_cli.php
|
||||
* dbec4b8 feat: support colorized CLI output for text renderers
|
||||
* 007abaf chore: update deps
|
||||
|
||||
* Version **6.6** - Add new renderer option: wordGlues
|
||||
* 2020-03-31 23:53 **6.6.4** add Spanish translation
|
||||
* 842b4af feat: add Spanish translation (#26)
|
||||
* 2020-03-30 14:22 **6.6.3** Just a few refactoring
|
||||
* 29dca67 test: add tests for Word line renderer
|
||||
* 355952f refactor: Word line renderer uses PREG_SPLIT_NO_EMPTY
|
||||
* 68b530e refactor: tidy codes
|
||||
* 492f53c chore: add some comments
|
||||
* 2020-03-30 00:45 **6.6.2** Fix wordGlues
|
||||
* 7ddbb79 fix: "wordGlues" works wrongly under some circumstances (#25)
|
||||
* 16780d3 fix(CSS): better strip alignment effect
|
||||
* 2020-03-20 06:41 **6.6.1** Add new language: German (deu)
|
||||
* 00b9a03 German language file
|
||||
* 9f1ae6b chore: update deps
|
||||
* 92e8f25 docs: update readme badges to fit markdown style
|
||||
* 0615509 refactor: tidy codes
|
||||
* 2020-03-12 19:53 **6.6.0** initial release
|
||||
* 05a118a Update deps
|
||||
* 2b68ddc Update screenshots
|
||||
* d227113 Add new renderer option: wordGlues
|
||||
* d24c6ae Tidy codes
|
||||
|
||||
* Version **6.5** - New renderer: Combined
|
||||
* 2020-03-11 16:52 **6.5.7** Add Json renderer option: jsonEncodeFlags
|
||||
* 9a00a55 Add Json renderer option: jsonEncodeFlags
|
||||
* cb54261 Tidy codes
|
||||
* 2020-03-09 16:08 **6.5.6** Add Combined renderer option: mergeThreshold
|
||||
* 2f85968 Tidy codes (Inline/SideBySide/Combined)
|
||||
* e39d674 Tidy codes
|
||||
* d643094 Add Combined renderer option: mergeThreshold
|
||||
* 92af433 Add more test cases
|
||||
* 570fd51 Update deps
|
||||
* 2020-03-08 00:42 **6.5.5** Combined: do not merge blocks which change too much
|
||||
* f4f8b25 Tidy codes
|
||||
* 273d1b4 Combined: do not merge blocks which change too much
|
||||
* ac5e1b1 Fix CSS table text alignment for very long lines
|
||||
* 5046910 Tidy codes
|
||||
* 45c0b24 Do not trim trailing whitespaces for .txt files
|
||||
* 2020-03-07 18:02 **6.5.4** Various Context/Unified fixes
|
||||
* 4484193 Tidy codes
|
||||
* 0a91891 Add some textual renderer tests
|
||||
* 3fdad9c Fix Context/Unified output for EOL at EOF
|
||||
* 8ab502f Fix Unified line numbers when context is 0
|
||||
* 795628f Fix all-equal hunk content should be omitted in Context output
|
||||
* 0f2e39a Fix there are extra leading/trailing lines when context is set to 0
|
||||
* 973fecb Add some textual renderer tests
|
||||
* 232356f Fix DiffHelper::calculateFiles() for empty file
|
||||
* 899600c Tidy codes
|
||||
* a81078f Add some comments
|
||||
* 2020-03-06 01:29 **6.5.3** Improve Combined renderer output for newlines
|
||||
* 745199a Update deps
|
||||
* e531cdc Combined: better boundary newline chars visualization
|
||||
* bf09969 Tidy codes
|
||||
* 42366b1 Add "run-script" to custom composer commands
|
||||
* 2020-03-05 14:14 **6.5.2** Fix multiline replaced lines in the Combined renderer
|
||||
* 5c93db2 Stripe background should only work in SideBySide
|
||||
* ea45be3 Fix "\n" should be "<br>" in Combined renderer output
|
||||
* d65c878 Use Prism.js to highlight diff example demos
|
||||
* c4e7d7b Tidy codes
|
||||
* 2020-02-29 02:30 **6.5.1** Allow overriding SCSS variables
|
||||
* ec4583c Allow overriding SCSS variables
|
||||
* bb5203b Simplify codes for merging lines in Combined renderer
|
||||
* 6da9215 Update example readme
|
||||
* a0b906b Prefer the new if we can only show either old or new
|
||||
* 2020-02-28 22:56 **6.5.0** initial release
|
||||
* 98fe251 Update deps
|
||||
* 2de0300 Update readme
|
||||
* a6ff11a Finish the HTML Combined renderer
|
||||
* 2cf8e84 New Renderer (#22)
|
||||
* 2277a98 Improve type docs and variable naming
|
||||
* d212f41 Update docs
|
||||
* 7a45667 Tidy codes
|
||||
* e96e971 Rename some "tag" to "op"
|
||||
* 2dd547c Update deps
|
||||
|
||||
* Version **6.4** - Add renderer option: lineNumbers
|
||||
* 2020-02-25 04:22 **6.4.7** Fixed SideBySide stripe background
|
||||
* f395759 SideBySide stripes use "background-attachment: fixed;"
|
||||
* a93f7fb Update SideBySide screenshot
|
||||
* 2020-02-24 18:52 **6.4.6** Align stripes
|
||||
* bdc3036 Make SideBySide stripe background aligned across lines
|
||||
* 2020-02-24 14:47 **6.4.5** CSS tweaks
|
||||
* 36799b9 SideBySide shows a stripe for nonexistent lines
|
||||
* 6b7e36b Update readme
|
||||
* 8b89bbc Add a const for indicating showing all contexts
|
||||
* bd2c868 Make empty cells have height via CSS
|
||||
* 3ae6da1 Make coding thoughts more clear
|
||||
* 0394a74 Tidy Codes
|
||||
* 7093932 Update SideBySide screenshot
|
||||
* abb8955 Improve SideBySide CSS for nonexistent blocks
|
||||
* 8cb05e8 Add keywords to composer.json
|
||||
* f061297 Update example/README.md
|
||||
* f861a3f Add more cases to example text sources
|
||||
* 34786fa Update example/README.md
|
||||
* 2020-02-13 14:16 **6.4.4** Fix SideBySide wrong new line number
|
||||
* 7397cbe Tidy codes
|
||||
* 215209d Fix SideBySide wrong new line number
|
||||
* d91221d Fix documentation for outputTagAsString
|
||||
* a7c3222 Add example image for SideBySide without line numbers
|
||||
* 2020-02-10 05:40 **6.4.3** Better SideBySide output without line numbers
|
||||
* 067c715 Merge SideBySide insert/delete columns when lineNumbers is false
|
||||
* 2020-02-09 18:27 **6.4.2** Fix SidebySide fatal error
|
||||
* 4e74cc5 Fix SideBySide renderer fatal error
|
||||
* 2020-02-09 17:43 **6.4.1** Fix typo
|
||||
* f2afe14 Fix typo: Woker -> Worker
|
||||
* 2020-02-07 17:51 **6.4.0** initial release
|
||||
* 802a7cb Tidy codes
|
||||
* 52045f3 Update documents
|
||||
* 69f42a1 lineNumber option (#19)
|
||||
* 1140ec2 Fix phan errors
|
||||
* 10cc545 Fix documentation about rendering from JSON diff
|
||||
* 4072992 Update deps
|
||||
* 90a86e0 ++year
|
||||
|
||||
* Version **6.3** - Render HTML with JSON result
|
||||
* 2020-01-16 13:00 **6.3.2** Add French (fra) translation
|
||||
* 41d571f Add French language file
|
||||
* 2020-01-12 01:08 **6.3.1** Remove dep: nicmart/string-template
|
||||
* 82259d0 Update dev require
|
||||
* d83f3ac Update readme [skip ci]
|
||||
* b36aeec Revert "Use string template engine to render language translations"
|
||||
* 0a991e7 Update tests
|
||||
* 2020-01-05 16:21 **6.3.0** initial release
|
||||
* 91a6180 Update deps
|
||||
* c713a0a Update examples and README
|
||||
* 3c6d9a3 Variables renaming (example/demo.php)
|
||||
* ce71a93 Add tests for \Jfcherng\Diff\Renderer\AbstractRenderer::renderArray
|
||||
* f497539 Tidy codes
|
||||
* caec927 Require jfcherng/php-sequence-matcher ^3.2
|
||||
* 3111605 Let text renderers throw exception for renderArray()
|
||||
* 8235504 Ability to render HTML from JSON (#17)
|
||||
|
||||
* Version **6.2** - Add renderer option: resultForIdenticals
|
||||
* 2020-01-05 01:19 **6.2.1** Add Turkish translation
|
||||
* f86585a Update deps
|
||||
* 858625f Create tur.json
|
||||
* eae8262 Add some @return static
|
||||
* caaaba7 Add .phpstorm.meta.php
|
||||
* 1067679 Update .travis.yml to PHP 7.4 stable
|
||||
* 6a5cd7b $ composer fix
|
||||
* d69a153 Update deps
|
||||
* 3e1b965 Use string template engine to render language translations
|
||||
* 2019-10-12 05:33 **6.2.0** initial release
|
||||
* f7aba88 Optimize RendererFactory::resolveRenderer()
|
||||
* 288adb2 Fix typos
|
||||
* 601cf86 Throw an exception for invalid "resultForIdenticals"
|
||||
* 9a71712 Add renderer option: resultForIdenticals
|
||||
* 53ac441 nits: tests
|
||||
* d1a7479 Update deps
|
||||
* 31b4916 Remove unnecessary CSS
|
||||
* f05f4a3 Tidy files
|
||||
|
||||
* Version **6.1** - Resolve CSS conflict
|
||||
* 2019-10-11 06:34 **6.1.2** Add Portuguese translation
|
||||
* a07dea9 Rename pt_BR.json -> por.json
|
||||
* d89a7b3 Replace "switch" statements with callback function tables
|
||||
* 5b9a828 Create pt_BR.json
|
||||
* 896df76 Follow PSR-12
|
||||
* 6d3ae95 Move FUNDING.yml to .github/
|
||||
* cf9032a Add test for renderer custom language array
|
||||
* 2019-09-11 00:19 **6.1.1** Fix "language" cannot be an array
|
||||
* 98085b6 Fix renderer option "language" cannot be an array
|
||||
* 3518068 Update .gitattributes
|
||||
* 2019-09-10 17:24 **6.1.0** initial release
|
||||
* f9400d3 Add renderer option: wrapperClasses
|
||||
* f2f613c Update readme (nits)
|
||||
* fdead6f Update readme (flat-square style badges)
|
||||
|
||||
* Version **6.0** - Unified
|
||||
* 2019-09-10 14:30 **6.0.1** Fix jpn wording
|
||||
* 00b59ef Fix jpn wording
|
||||
* d04814e Sass: replace compass/css3 functions with dart-sass built-in
|
||||
* a3b05d9 Update readme to use badges from shields.io
|
||||
* 5dbdf21 Update readme: nits
|
||||
* db4439c Slightly improve code readability
|
||||
* 682fb39 nits
|
||||
* 2b13e8e Update deps
|
||||
* 0c9b79b Add .gitattributes
|
||||
* fa9f3a6 Create FUNDING.yml
|
||||
* f81b823 Move documentation assets to the current branch
|
||||
* 4c2c11d Release of new version 5.2.2
|
||||
* 2bd27d4 Update deps
|
||||
* fa189fa Update deps
|
||||
* 6b30019 Update deps
|
||||
* abc0d8a $ composer fix
|
||||
* 7cb5a75 Update deps
|
||||
* 70f66a2 Update .travis.yml for 7.4snapshot
|
||||
* e7e1839 Update deps
|
||||
* 1cee802 Update readme
|
||||
* bf14c89 Update deps
|
||||
* c52f66b Fix typo in UPGRADING_v6.md
|
||||
* 756970a Add .editorconfig
|
||||
* e6350bc Change screenshot size
|
||||
* 55748a5 Update deps
|
||||
* d8644db Update deps
|
||||
* 03b0c55 Freeze documentation assets for v5
|
||||
* bd61843 Freeze documentation assets for v4
|
||||
* 2019-03-20 11:31 **6.0.0** initial release
|
||||
* 46d8e1b Update .rmt.yml
|
||||
* f3f209c Fix tests
|
||||
* c625cb3 Revise UPGRADING_v6.md
|
||||
* 6807e65 Let Differ manages its own state, i.e., finalize()
|
||||
* ae309c5 Fix some grammar problems in UPGRADING_v6.md
|
||||
* 9fa89dd Add RendererInterface::getResultForIdenticals()
|
||||
* b781125 Release of new version 5.2.0
|
||||
* 57fa3cd nits
|
||||
* 926f19f $ php-cs-fixer fix
|
||||
* 89ec714 [php-cs-fixer] Add new rules: @PhpCsFixer, @PhpCsFixer:risky
|
||||
* 83830c7 Improve JSON decoding in Language::getTranslationsByLanguage()
|
||||
* 3b40e42 Release of new version 4.2.3
|
||||
* b39f29a Make Language::getTranslationsByLanguage() safer
|
||||
* 846d0ca Rename the term "template" to "renderer"
|
||||
* 8439977 Use native class name
|
||||
* 5330a8e Release of new version 4.2.2
|
||||
* 4979a53 nits
|
||||
* ffaefef Fix phan error
|
||||
* dc26604 Rename Differ::groupedCodes to Differ::groupedOpcodes
|
||||
* ba570f8 Make Differ work in a DI way in Renderer internally
|
||||
* 177146e Rename Diff to Differ
|
||||
* 0a30b27 Renderer::render() rather than Diff::render()
|
||||
* deb9ed3 Revise phpdoc
|
||||
* d8606b7 Release of new version 5.1.0
|
||||
Vendored
+31
@@ -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.
|
||||
Vendored
+633
@@ -0,0 +1,633 @@
|
||||
# php-diff
|
||||
|
||||
[](https://github.com/jfcherng/php-diff/actions)
|
||||
[](https://app.codacy.com/project/jfcherng/php-diff/dashboard)
|
||||
[](https://packagist.org/packages/jfcherng/php-diff)
|
||||
[](https://packagist.org/packages/jfcherng/php-diff)
|
||||
[](https://github.com/jfcherng/php-diff/blob/v6/LICENSE)
|
||||
[](https://github.com/jfcherng/php-diff/stargazers)
|
||||
[](https://www.paypal.me/jfcherng/5usd)
|
||||
|
||||
A comprehensive library for generating diff between two strings.
|
||||
|
||||
## Introduction
|
||||
|
||||
Generated diff can be rendered in all of the standard formats including:
|
||||
|
||||
**Text** renderers:
|
||||
|
||||
- Context
|
||||
- Json (plain text)
|
||||
- Unified
|
||||
|
||||
**HTML** renderers:
|
||||
|
||||
- Combined
|
||||
- Inline
|
||||
- Json (HTML)
|
||||
- Side by Side
|
||||
|
||||
Note that for HTML rendered results, you have to add CSS for a better visualization.
|
||||
You may modify one from `example/diff-table.css` or write your own from zero.
|
||||
|
||||
If you are okay with the default CSS, there is `\Jfcherng\Diff\DiffHelper::getStyleSheet()`
|
||||
which can be used to get the content of the `example/diff-table.css`.
|
||||
|
||||
## Requirements
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
This package is available on `Packagist` by the name of [jfcherng/php-diff](https://packagist.org/packages/jfcherng/php-diff).
|
||||
|
||||
```bash
|
||||
composer require jfcherng/php-diff
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
See files and readme in the [example/](https://github.com/jfcherng/php-diff/blob/v6/example) directory.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
include __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Jfcherng\Diff\Differ;
|
||||
use Jfcherng\Diff\DiffHelper;
|
||||
use Jfcherng\Diff\Factory\RendererFactory;
|
||||
use Jfcherng\Diff\Renderer\RendererConstant;
|
||||
|
||||
$oldFile = __DIR__ . '/example/old_file.txt';
|
||||
$newFile = __DIR__ . '/example/new_file.txt';
|
||||
|
||||
$old = 'This is the old string.';
|
||||
$new = 'And this is the new one.';
|
||||
|
||||
// renderer class name:
|
||||
// Text renderers: Context, JsonText, Unified
|
||||
// HTML renderers: Combined, Inline, JsonHtml, SideBySide
|
||||
$rendererName = 'Unified';
|
||||
|
||||
// the Diff class options
|
||||
$differOptions = [
|
||||
// show how many neighbor lines
|
||||
// Differ::CONTEXT_ALL can be used to show the whole file
|
||||
'context' => 3,
|
||||
// ignore case difference
|
||||
'ignoreCase' => false,
|
||||
// ignore line ending difference
|
||||
'ignoreLineEnding' => false,
|
||||
// ignore whitespace difference
|
||||
'ignoreWhitespace' => false,
|
||||
// if the input sequence is too long, it will just gives up (especially for char-level diff)
|
||||
'lengthLimit' => 2000,
|
||||
// if truthy, when inputs are identical, the whole inputs will be rendered in the output
|
||||
'fullContextIfIdentical' => false,
|
||||
];
|
||||
|
||||
// the renderer class options
|
||||
$rendererOptions = [
|
||||
// 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
|
||||
// check the "Custom Language" section in the readme for more advanced usage
|
||||
'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 available 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'],
|
||||
];
|
||||
|
||||
// one-line simply compare two files
|
||||
$result = DiffHelper::calculateFiles($oldFile, $newFile, $rendererName, $differOptions, $rendererOptions);
|
||||
// one-line simply compare two strings
|
||||
$result = DiffHelper::calculate($old, $new, $rendererName, $differOptions, $rendererOptions);
|
||||
// or even shorter if you are happy with default options
|
||||
$result = DiffHelper::calculate($old, $new, $rendererName);
|
||||
|
||||
// custom usage
|
||||
$differ = new Differ(explode("\n", $old), explode("\n", $new), $differOptions);
|
||||
$renderer = RendererFactory::make($rendererName, $rendererOptions); // or your own renderer object
|
||||
$result = $renderer->render($differ);
|
||||
|
||||
// use the JSON result to render in HTML
|
||||
$jsonResult = DiffHelper::calculate($old, $new, 'Json'); // may store the JSON result in your database
|
||||
$htmlRenderer = RendererFactory::make('Inline', $rendererOptions);
|
||||
$result = $htmlRenderer->renderArray(json_decode($jsonResult, true));
|
||||
```
|
||||
|
||||
## Rendered Results
|
||||
|
||||
### HTML Diff In-line Detailed Rendering
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>None-level</th>
|
||||
<th>Line-level (Default)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/inline-none-level-diff.png"></td>
|
||||
<td><img src="https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/inline-line-level-diff.png"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Word-level</th>
|
||||
<th>Char-level</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/inline-word-level-diff.png"></td>
|
||||
<td><img src="https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/inline-char-level-diff.png"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Renderer: Inline
|
||||
|
||||
```php
|
||||
<?php $rendererOptions = ['detailLevel' => 'line'];
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Renderer: Side By Side
|
||||
|
||||
```php
|
||||
<?php $rendererOptions = ['detailLevel' => 'line'];
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Renderer: Combined
|
||||
|
||||
```php
|
||||
<?php $rendererOptions = ['detailLevel' => 'word'];
|
||||
```
|
||||
|
||||
This renderer is suitable for articles and always has no line number information.
|
||||
|
||||

|
||||
|
||||
### Renderer: Unified
|
||||
|
||||
About the `Unified` diff format: https://en.wikipedia.org/wiki/Diff#Unified_format
|
||||
|
||||
```diff
|
||||
@@ -1,3 +1,4 @@
|
||||
-<p>Hello World!</p>
|
||||
+<div>Hello World!</div>
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
+Let's add a new line here.
|
||||
X
|
||||
@@ -7,6 +8,5 @@
|
||||
N
|
||||
-Do you know in Chinese, "金槍魚罐頭" means tuna can.
|
||||
+Do you know in Japanese, "魚の缶詰" means fish can.
|
||||
This is just a useless line.
|
||||
G
|
||||
-// remember to delete this line
|
||||
Say hello to my neighbors.
|
||||
```
|
||||
|
||||
### Renderer: Context
|
||||
|
||||
About the `Context` diff format: https://en.wikipedia.org/wiki/Diff#Context_format
|
||||
|
||||
<details><summary>Click to expand</summary>
|
||||
|
||||
```diff
|
||||
***************
|
||||
*** 1,3 ****
|
||||
! <p>Hello World!</p>
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
X
|
||||
--- 1,4 ----
|
||||
! <div>Hello World!</div>
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
+ Let's add a new line here.
|
||||
X
|
||||
***************
|
||||
*** 7,12 ****
|
||||
N
|
||||
! Do you know in Chinese, "金槍魚罐頭" means tuna can.
|
||||
This is just a useless line.
|
||||
G
|
||||
- // remember to delete this line
|
||||
Say hello to my neighbors.
|
||||
--- 8,12 ----
|
||||
N
|
||||
! Do you know in Japanese, "魚の缶詰" means fish can.
|
||||
This is just a useless line.
|
||||
G
|
||||
Say hello to my neighbors.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Renderer: Text JSON
|
||||
|
||||
This renderer has no detailed diff.
|
||||
|
||||
<details><summary>Click to expand</summary>
|
||||
|
||||
```json
|
||||
[
|
||||
[
|
||||
{
|
||||
"tag": "rep",
|
||||
"old": {
|
||||
"offset": 0,
|
||||
"lines": ["<p>Hello World! Good-looking.</p>"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 0,
|
||||
"lines": ["<div>Hello World! Bad-tempered.</div>"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 1,
|
||||
"lines": ["~~~~~~~~~~~~~~~~~~~"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 1,
|
||||
"lines": ["~~~~~~~~~~~~~~~~~~~"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "ins",
|
||||
"old": {
|
||||
"offset": 2,
|
||||
"lines": []
|
||||
},
|
||||
"new": {
|
||||
"offset": 2,
|
||||
"lines": ["Let's add a new line here."]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 2,
|
||||
"lines": ["X"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 3,
|
||||
"lines": ["X"]
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 6,
|
||||
"lines": ["N"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 7,
|
||||
"lines": ["N"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "rep",
|
||||
"old": {
|
||||
"offset": 7,
|
||||
"lines": ["Do you know in Chinese, \"金槍魚罐頭\" means tuna can."]
|
||||
},
|
||||
"new": {
|
||||
"offset": 8,
|
||||
"lines": ["Do you know in Japanese, \"魚の缶詰\" means fish can."]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 8,
|
||||
"lines": ["\t \tTab visualization test.", "G"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 9,
|
||||
"lines": ["\t \tTab visualization test.", "G"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "del",
|
||||
"old": {
|
||||
"offset": 10,
|
||||
"lines": ["// remember to delete this line"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 11,
|
||||
"lines": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 11,
|
||||
"lines": ["Say hello to my neighbors."]
|
||||
},
|
||||
"new": {
|
||||
"offset": 11,
|
||||
"lines": ["Say hello to my neighbors."]
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 14,
|
||||
"lines": ["B"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 14,
|
||||
"lines": ["B"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "rep",
|
||||
"old": {
|
||||
"offset": 15,
|
||||
"lines": ["Donec rutrum."]
|
||||
},
|
||||
"new": {
|
||||
"offset": 15,
|
||||
"lines": ["Donec rutrum test.", "There is a new inserted line."]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 16,
|
||||
"lines": ["C"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 17,
|
||||
"lines": ["C"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "rep",
|
||||
"old": {
|
||||
"offset": 17,
|
||||
"lines": ["Sed dictum lorem ipsum."]
|
||||
},
|
||||
"new": {
|
||||
"offset": 18,
|
||||
"lines": ["Sed dolor lorem ipsum hendrerit."]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 18,
|
||||
"lines": [""]
|
||||
},
|
||||
"new": {
|
||||
"offset": 19,
|
||||
"lines": [""]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Renderer: HTML JSON
|
||||
|
||||
For a `"tag": "rep" (8)` block, this renderer has HTML-style detailed diff.
|
||||
If you don't need those detailed diff, consider using the `JsonText` renderer.
|
||||
|
||||
<details><summary>Click to expand</summary>
|
||||
|
||||
```json
|
||||
[
|
||||
[
|
||||
{
|
||||
"tag": "rep",
|
||||
"old": {
|
||||
"offset": 0,
|
||||
"lines": ["<<del>p>Hello World! Good-looking.</p</del>>"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 0,
|
||||
"lines": ["<<ins>div>Hello World! Bad-tempered.</div</ins>>"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 1,
|
||||
"lines": ["~~~~~~~~~~~~~~~~~~~"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 1,
|
||||
"lines": ["~~~~~~~~~~~~~~~~~~~"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "ins",
|
||||
"old": {
|
||||
"offset": 2,
|
||||
"lines": [""]
|
||||
},
|
||||
"new": {
|
||||
"offset": 2,
|
||||
"lines": ["Let's add a new line here."]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 2,
|
||||
"lines": ["X"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 3,
|
||||
"lines": ["X"]
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 6,
|
||||
"lines": ["N"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 7,
|
||||
"lines": ["N"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "rep",
|
||||
"old": {
|
||||
"offset": 7,
|
||||
"lines": ["Do you know in <del>Chinese, \"金槍魚罐頭\" means tuna</del> can."]
|
||||
},
|
||||
"new": {
|
||||
"offset": 8,
|
||||
"lines": ["Do you know in <ins>Japanese, \"魚の缶詰\" means fish</ins> can."]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 8,
|
||||
"lines": ["\t \tTab visualization test.", "G"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 9,
|
||||
"lines": ["\t \tTab visualization test.", "G"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "del",
|
||||
"old": {
|
||||
"offset": 10,
|
||||
"lines": ["// remember to delete this line"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 11,
|
||||
"lines": [""]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 11,
|
||||
"lines": ["Say hello to my neighbors."]
|
||||
},
|
||||
"new": {
|
||||
"offset": 11,
|
||||
"lines": ["Say hello to my neighbors."]
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 14,
|
||||
"lines": ["B"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 14,
|
||||
"lines": ["B"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "rep",
|
||||
"old": {
|
||||
"offset": 15,
|
||||
"lines": ["Donec rutrum."]
|
||||
},
|
||||
"new": {
|
||||
"offset": 15,
|
||||
"lines": ["Donec rutrum test.", "There is a new inserted line."]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 16,
|
||||
"lines": ["C"]
|
||||
},
|
||||
"new": {
|
||||
"offset": 17,
|
||||
"lines": ["C"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "rep",
|
||||
"old": {
|
||||
"offset": 17,
|
||||
"lines": ["Sed d<del>ictum lorem ipsum</del>."]
|
||||
},
|
||||
"new": {
|
||||
"offset": 18,
|
||||
"lines": ["Sed d<ins>olor lorem ipsum hendrerit</ins>."]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "eq",
|
||||
"old": {
|
||||
"offset": 18,
|
||||
"lines": [""]
|
||||
},
|
||||
"new": {
|
||||
"offset": 19,
|
||||
"lines": [""]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Custom Language
|
||||
|
||||
### Override an Existing Language
|
||||
|
||||
If you just want to override some translations of an existing language...
|
||||
|
||||
```php
|
||||
$rendererOptions = [
|
||||
'language' => [
|
||||
// use English as the base language
|
||||
'eng',
|
||||
// your custom overrides
|
||||
[
|
||||
// use "Diff" as the new value of the "differences" key
|
||||
'differences' => 'Diff',
|
||||
],
|
||||
// maybe more overrides if you somehow need them...
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
## Acknowledgment
|
||||
|
||||
This package is built on the top of [chrisboulton/php-diff](https://github.com/chrisboulton/php-diff) initially.
|
||||
But the original repository looks like no longer maintained.
|
||||
Here have been quite lots of rewrites and new features since then, hence I re-started this as a new package for better visibility.
|
||||
@@ -0,0 +1,6 @@
|
||||
## Upgrading to v3
|
||||
|
||||
- `$diffOptions` removes: `charLevelDiff` and `separateBlock`.
|
||||
- `$templateOptions` adds: `detailLevel` (similar to `charLevelDiff`, read docs) and `separateBlock` (exact the same one in `$diffOptions`).
|
||||
- `Jfcherng\Diff\Diff`'s `$a` (`$old`), `$b` (`$new`) are required in `__construct()`. (You may pass two empty arrays if you do not want to do anything at that moment.)
|
||||
- The look of "skipped" block in HTML renderers (`SideBySide` and `Inline`) have been changed. (You may have to tweak your CSS.)
|
||||
@@ -0,0 +1,5 @@
|
||||
## Upgrading to v4
|
||||
|
||||
- `Jfcherng\Diff\Utility\SequenceMatcher` becomes [a new package](https://packagist.org/packages/jfcherng/php-sequence-matcher) by the namespace of `Jfcherng\Diff\SequenceMatcher`.
|
||||
- Factories under `Jfcherng\Diff\Utility\` are moved to `Jfcherng\Diff\Factory\`. For example, `Jfcherng\Diff\Utility\RendererFactory` is now `Jfcherng\Diff\Factory\RendererFactory`.
|
||||
- Non-abstract classes are no longer inheritable as they are added with `final` keywords. (This allows me to do more internal changes without causing possible BC breaks.)
|
||||
@@ -0,0 +1,24 @@
|
||||
## Upgrading to v5
|
||||
|
||||
- Names involving `a, b`, `from, to`, `base, changed` have been renamed to `old, new` for consistency.
|
||||
Here's some examples:
|
||||
|
||||
- `Diff::setAB()` becomes `Diff::setOldNew()`.
|
||||
- `Diff::setA()` becomes `Diff::setOld()`.
|
||||
- `Diff::setB()` becomes `Diff::setNew()`.
|
||||
- `Diff::getA()` becomes `Diff::getOld()`.
|
||||
- `Diff::getB()` becomes `Diff::getNew()`.
|
||||
- `base`, `changed` keys in the result of the `Json` renderer have become `old`, `new`.
|
||||
|
||||
- In the result of HTML renderers, classes of rows of line numbers has been changed.
|
||||
You may have to change your CSS if you have some customized things depend on these.
|
||||
|
||||
- `<th class="f-num">` (from-number) becomes `<th class="n-new">` (number-new).
|
||||
- `<th class="t-num">` (to-number) becomes `<th class="n-old">` (number-old).
|
||||
|
||||
- The `tag` (sometimes called `op`) in `Json` template is now in `int` form by default.
|
||||
To get previous behavior, set the renderer option `outputTagAsString` to `true`.
|
||||
|
||||
- The `tag` (sometimes called `op`) in `Diff::getGroupedOpcodes()`'s results are now in `int` form.
|
||||
The corresponding meaning could be found in
|
||||
[jfcherng/php-sequence-matcher](https://github.com/jfcherng/php-sequence-matcher/blob/3.0.0/src/SequenceMatcher.php#L16-L26).
|
||||
@@ -0,0 +1,47 @@
|
||||
## Upgrading to v6
|
||||
|
||||
|
||||
### For Simple Users
|
||||
|
||||
If you only use the `DiffHelper` and built-in `Renderer`s,
|
||||
there is no breaking change for you so you do not have to do anything.
|
||||
|
||||
|
||||
### Breaking Changes for Normal Users
|
||||
|
||||
- The `Diff` class has been renamed as `Differ`.
|
||||
It should be relatively easy to adapt to this by changing the class name.
|
||||
|
||||
- The term `template` has been renamed as `renderer`. Some examples are:
|
||||
|
||||
- Method `DiffHelper::getRenderersInfo()`
|
||||
- Method `DiffHelper::getAvailableRenderers()`
|
||||
- Constant `RendererConstant::RENDERER_TYPES`
|
||||
- Constant `AbstractRenderer::IS_TEXT_RENDERER`
|
||||
|
||||
- Now a `Renderer` has a `render()` method, but a `Differ` does not.
|
||||
(because it makes more sense saying a renderer would render something)
|
||||
If you use those classes by yourself, it should be written like below.
|
||||
|
||||
```php
|
||||
use Jfcherng\Diff\Differ;
|
||||
use Jfcherng\Diff\Factory\RendererFactory;
|
||||
|
||||
$differ = new Differ(explode("\n", $old), explode("\n", $new), $diffOptions);
|
||||
$renderer = RendererFactory::make($rendererName, $rendererOptions);
|
||||
$result = $renderer->render($differ); // <-- this line has been changed
|
||||
```
|
||||
|
||||
|
||||
### Breaking Changes for Customized Renderer Developers
|
||||
|
||||
- Remove the deprecated `AbstractRenderer::getIdenticalResult()` and
|
||||
add `RendererInterface::getResultForIdenticals()`. The returned value will be
|
||||
directly used before actually starting to calculate diff if we find that the
|
||||
two strings are the same. `AbstractRenderer::getResultForIdenticals()`
|
||||
returns an empty string by default.
|
||||
|
||||
- Now a `Renderer` should implement `protected function renderWorker(Differ $differ): string`
|
||||
rather than the previous `public function render(): string`. Note that
|
||||
`$this->diff` no longer works in `Renderer`s as it is now injected as a
|
||||
parameter to `Renderer::renderWorker()`.
|
||||
+69
@@ -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.4",
|
||||
"jfcherng/php-color-output": "^3",
|
||||
"jfcherng/php-mb-string": "^1.4.6 || ^2",
|
||||
"jfcherng/php-sequence-matcher": "^3.2.10 || ^4"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.51",
|
||||
"liip/rmt": "^1.6",
|
||||
"phan/phan": "^5",
|
||||
"phpunit/phpunit": "^9",
|
||||
"squizlabs/php_codesniffer": "^3.6"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.4.99"
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
+4655
File diff suppressed because it is too large
Load Diff
+21
@@ -0,0 +1,21 @@
|
||||
# Demo
|
||||
|
||||
Note that
|
||||
|
||||
- All `composer`-related commands are run in this project's root directory.
|
||||
That is php-diff directory rather than this `example` directory.
|
||||
- You can change differ/renderer options in `demo_base.php`.
|
||||
- Change contents of `old_file.txt` and `new_file.txt` to test different text.
|
||||
|
||||
To run demo, you have to first install dependencies via `composer install`.
|
||||
|
||||
## Web Environment
|
||||
|
||||
To run `demo_web.php` on your local machine, you can follow steps below.
|
||||
|
||||
1. Start PHP development server via `composer run-script server`.
|
||||
1. Visit `http://localhost:12388/demo_web.php` with a web browser.
|
||||
|
||||
## Cli Environment
|
||||
|
||||
Just run `php demo_cli.php`.
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Jfcherng\Diff\Differ;
|
||||
use Jfcherng\Diff\Renderer\RendererConstant;
|
||||
|
||||
// the two sample files for comparison
|
||||
$oldFile = __DIR__ . '/old_file.txt';
|
||||
$newFile = __DIR__ . '/new_file.txt';
|
||||
$oldString = file_get_contents($oldFile);
|
||||
$newString = file_get_contents($newFile);
|
||||
|
||||
// options for Diff class
|
||||
$diffOptions = [
|
||||
// show how many neighbor lines
|
||||
// Differ::CONTEXT_ALL can be used to show the whole file
|
||||
'context' => 1,
|
||||
// ignore case difference
|
||||
'ignoreCase' => false,
|
||||
// ignore line ending difference
|
||||
'ignoreLineEnding' => false,
|
||||
// ignore whitespace difference
|
||||
'ignoreWhitespace' => false,
|
||||
// if the input sequence is too long, it will just gives up (especially for char-level diff)
|
||||
'lengthLimit' => 2000,
|
||||
// if truthy, when inputs are identical, the whole inputs will be rendered in the output
|
||||
'fullContextIfIdentical' => false,
|
||||
];
|
||||
|
||||
// options for renderer class
|
||||
$rendererOptions = [
|
||||
// how detailed the rendered HTML 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
|
||||
// check the "Custom Language" section in the readme for more advanced usage
|
||||
'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,
|
||||
// convert spaces/tabs into HTML codes like `<span class="ch sp"> </span>`
|
||||
// and the frontend is responsible for rendering them with CSS.
|
||||
// when using this, "spacesToNbsp" should be false and "tabSize" is not respected.
|
||||
'spaceToHtmlTag' => false,
|
||||
// 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 available 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'],
|
||||
];
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/demo_base.php';
|
||||
|
||||
use Jfcherng\Diff\DiffHelper;
|
||||
use Jfcherng\Utility\CliColor;
|
||||
|
||||
$colorStyles = [
|
||||
'section' => ['f_black', 'b_cyan'],
|
||||
];
|
||||
|
||||
$manyNewlines = "\n\n\n\n";
|
||||
|
||||
echo CliColor::color("Unified Diff\n============", $colorStyles['section']) . "\n\n";
|
||||
|
||||
// generate a unified diff
|
||||
$unifiedResult = DiffHelper::calculate(
|
||||
$oldString,
|
||||
$newString,
|
||||
'Unified',
|
||||
$diffOptions,
|
||||
$rendererOptions,
|
||||
);
|
||||
|
||||
echo $unifiedResult . $manyNewlines;
|
||||
|
||||
echo CliColor::color("Context Diff\n============", $colorStyles['section']) . "\n\n";
|
||||
|
||||
// generate a context diff
|
||||
$contextResult = DiffHelper::calculate(
|
||||
$oldString,
|
||||
$newString,
|
||||
'Context',
|
||||
$diffOptions,
|
||||
$rendererOptions,
|
||||
);
|
||||
|
||||
echo $contextResult . $manyNewlines;
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
include __DIR__ . '/demo_base.php';
|
||||
|
||||
use Jfcherng\Diff\DiffHelper;
|
||||
use Jfcherng\Diff\Factory\RendererFactory;
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>jfcherng/php-diff - Examples</title>
|
||||
|
||||
<!-- Prism -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/line-numbers/prism-line-numbers.min.css" />
|
||||
|
||||
<style type="text/css">
|
||||
html {
|
||||
font-size: 13px;
|
||||
}
|
||||
.token.coord {
|
||||
color: #6cf;
|
||||
}
|
||||
.token.diff.bold {
|
||||
color: #fb0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
<?= DiffHelper::getStyleSheet(); ?>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>None-level Diff</h1>
|
||||
<?php
|
||||
|
||||
// demo the no-inline-detail diff
|
||||
$inlineResult = DiffHelper::calculate(
|
||||
$oldString,
|
||||
$newString,
|
||||
'Inline',
|
||||
$diffOptions,
|
||||
['detailLevel' => 'none'] + $rendererOptions,
|
||||
);
|
||||
|
||||
echo $inlineResult;
|
||||
|
||||
?>
|
||||
|
||||
<h1>Line-level Diff (Default)</h1>
|
||||
<?php
|
||||
|
||||
// demo the word-level diff
|
||||
$inlineResult = DiffHelper::calculate(
|
||||
$oldString,
|
||||
$newString,
|
||||
'Inline',
|
||||
$diffOptions,
|
||||
['detailLevel' => 'line'] + $rendererOptions,
|
||||
);
|
||||
|
||||
echo $inlineResult;
|
||||
|
||||
?>
|
||||
|
||||
<h1>Word-level Diff</h1>
|
||||
<?php
|
||||
|
||||
// demo the word-level diff
|
||||
$inlineResult = DiffHelper::calculate(
|
||||
$oldString,
|
||||
$newString,
|
||||
'Inline',
|
||||
$diffOptions,
|
||||
['detailLevel' => 'word'] + $rendererOptions,
|
||||
);
|
||||
|
||||
echo $inlineResult;
|
||||
|
||||
?>
|
||||
|
||||
<h1>Character-level Diff</h1>
|
||||
<?php
|
||||
|
||||
// demo the character-level diff
|
||||
$inlineResult = DiffHelper::calculate(
|
||||
$oldString,
|
||||
$newString,
|
||||
'Inline',
|
||||
$diffOptions,
|
||||
['detailLevel' => 'char'] + $rendererOptions,
|
||||
);
|
||||
|
||||
echo $inlineResult;
|
||||
|
||||
?>
|
||||
|
||||
<h1>Side by Side Diff</h1>
|
||||
<?php
|
||||
|
||||
// generate a side by side diff
|
||||
$sideBySideResult = DiffHelper::calculateFiles(
|
||||
$oldFile,
|
||||
$newFile,
|
||||
'SideBySide',
|
||||
$diffOptions,
|
||||
$rendererOptions,
|
||||
);
|
||||
|
||||
echo $sideBySideResult;
|
||||
|
||||
?>
|
||||
|
||||
<h1>Inline Diff</h1>
|
||||
<?php
|
||||
|
||||
// generate an inline diff
|
||||
$inlineResult = DiffHelper::calculateFiles(
|
||||
$oldFile,
|
||||
$newFile,
|
||||
'Inline',
|
||||
$diffOptions,
|
||||
$rendererOptions,
|
||||
);
|
||||
|
||||
echo $inlineResult;
|
||||
|
||||
?>
|
||||
|
||||
<h1>Combined Diff</h1>
|
||||
<?php
|
||||
|
||||
// generate a combined diff
|
||||
$sideBySideResult = DiffHelper::calculateFiles(
|
||||
$oldFile,
|
||||
$newFile,
|
||||
'Combined',
|
||||
$diffOptions,
|
||||
$rendererOptions,
|
||||
);
|
||||
|
||||
echo $sideBySideResult;
|
||||
|
||||
?>
|
||||
|
||||
<h1>Unified Diff</h1>
|
||||
<pre><code class="language-diff line-numbers"><?php
|
||||
|
||||
// generate a unified diff
|
||||
$unifiedResult = DiffHelper::calculateFiles(
|
||||
$oldFile,
|
||||
$newFile,
|
||||
'Unified',
|
||||
$diffOptions,
|
||||
$rendererOptions,
|
||||
);
|
||||
|
||||
echo htmlspecialchars($unifiedResult);
|
||||
|
||||
?></code></pre>
|
||||
|
||||
<h1>Context Diff</h1>
|
||||
<pre><code class="language-diff line-numbers"><?php
|
||||
|
||||
// generate a context diff
|
||||
$contextResult = DiffHelper::calculateFiles(
|
||||
$oldFile,
|
||||
$newFile,
|
||||
'Context',
|
||||
$diffOptions,
|
||||
$rendererOptions,
|
||||
);
|
||||
|
||||
echo htmlspecialchars($contextResult);
|
||||
|
||||
?></code></pre>
|
||||
|
||||
<?php
|
||||
|
||||
// change JSON output settings for better human reading
|
||||
$rendererOptions['outputTagAsString'] = true;
|
||||
$rendererOptions['jsonEncodeFlags'] |= \JSON_PRETTY_PRINT;
|
||||
|
||||
?>
|
||||
|
||||
<h1>Text JSON Diff</h1>
|
||||
<pre><code class="language-json line-numbers"><?php
|
||||
|
||||
// generate a plain text JSON diff
|
||||
$jsonResult = DiffHelper::calculateFiles(
|
||||
$oldFile,
|
||||
$newFile,
|
||||
'JsonText',
|
||||
$diffOptions,
|
||||
$rendererOptions,
|
||||
);
|
||||
|
||||
echo htmlspecialchars($jsonResult);
|
||||
|
||||
?></code></pre>
|
||||
|
||||
<h1>HTML JSON Diff</h1>
|
||||
<pre><code class="language-json line-numbers"><?php
|
||||
|
||||
// generate a HTML JSON diff
|
||||
$jsonResult = DiffHelper::calculateFiles(
|
||||
$oldFile,
|
||||
$newFile,
|
||||
'JsonHtml',
|
||||
$diffOptions,
|
||||
$rendererOptions,
|
||||
);
|
||||
|
||||
echo htmlspecialchars($jsonResult);
|
||||
|
||||
?></code></pre>
|
||||
|
||||
<h1>HTML Diff from the Result of JSON Diff</h1>
|
||||
<?php
|
||||
|
||||
$jsonArray = json_decode($jsonResult, true);
|
||||
|
||||
$htmlRenderer = RendererFactory::make('Inline', $rendererOptions);
|
||||
$inlineResult = $htmlRenderer->renderArray($jsonArray);
|
||||
|
||||
echo $inlineResult;
|
||||
|
||||
?>
|
||||
|
||||
<!-- Prism -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-diff.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-json.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/line-numbers/prism-line-numbers.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
.diff-wrapper.diff {
|
||||
--tab-size: 4;
|
||||
background: repeating-linear-gradient(-45deg, whitesmoke, whitesmoke 0.5em, #e8e8e8 0.5em, #e8e8e8 1em);
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border: 1px solid black;
|
||||
color: black;
|
||||
empty-cells: show;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
.diff-wrapper.diff th {
|
||||
font-weight: 700;
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.diff-wrapper.diff td {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.diff-wrapper.diff td,
|
||||
.diff-wrapper.diff th {
|
||||
border-collapse: separate;
|
||||
border: none;
|
||||
padding: 1px 2px;
|
||||
background: #fff;
|
||||
}
|
||||
.diff-wrapper.diff td:empty:after,
|
||||
.diff-wrapper.diff th:empty:after {
|
||||
content: " ";
|
||||
visibility: hidden;
|
||||
}
|
||||
.diff-wrapper.diff td a,
|
||||
.diff-wrapper.diff th a {
|
||||
color: #000;
|
||||
cursor: inherit;
|
||||
pointer-events: none;
|
||||
}
|
||||
.diff-wrapper.diff thead th {
|
||||
background: #a6a6a6;
|
||||
border-bottom: 1px solid black;
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
.diff-wrapper.diff tbody.skipped {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
.diff-wrapper.diff tbody.skipped td,
|
||||
.diff-wrapper.diff tbody.skipped th {
|
||||
display: none;
|
||||
}
|
||||
.diff-wrapper.diff tbody th {
|
||||
background: #cccccc;
|
||||
border-right: 1px solid black;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
width: 4em;
|
||||
}
|
||||
.diff-wrapper.diff tbody th.sign {
|
||||
background: #fff;
|
||||
border-right: none;
|
||||
padding: 1px 0;
|
||||
text-align: center;
|
||||
width: 1em;
|
||||
}
|
||||
.diff-wrapper.diff tbody th.sign.del {
|
||||
background: #fbe1e1;
|
||||
}
|
||||
.diff-wrapper.diff tbody th.sign.ins {
|
||||
background: #e1fbe1;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html {
|
||||
white-space: pre-wrap;
|
||||
tab-size: var(--tab-size);
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .ch {
|
||||
line-height: 1em;
|
||||
background-clip: border-box;
|
||||
background-repeat: repeat-x;
|
||||
background-position: left center;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .ch.sp {
|
||||
background-image: url('data:image/svg+xml,%3Csvg preserveAspectRatio="xMinYMid meet" viewBox="0 0 12 24" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M4.5 11C4.5 10.1716 5.17157 9.5 6 9.5C6.82843 9.5 7.5 10.1716 7.5 11C7.5 11.8284 6.82843 12.5 6 12.5C5.17157 12.5 4.5 11.8284 4.5 11Z" fill="rgba%2860, 60, 60, 50%25%29"/%3E%3C/svg%3E');
|
||||
background-size: 1ch 1.25em;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .ch.tab {
|
||||
background-image: url('data:image/svg+xml,%3Csvg preserveAspectRatio="xMinYMid meet" viewBox="0 0 12 24" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M9.5 10.44L6.62 8.12L7.32 7.26L12.04 11V11.44L7.28 14.9L6.62 13.9L9.48 11.78H0V10.44H9.5Z" fill="rgba%2860, 60, 60, 50%25%29"/%3E%3C/svg%3E');
|
||||
background-size: calc(var(--tab-size) * 1ch) 1.25em;
|
||||
background-position: 2px center;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .change.change-eq .old,
|
||||
.diff-wrapper.diff.diff-html .change.change-eq .new {
|
||||
background: #fff;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .change .old {
|
||||
background: #fbe1e1;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .change .new {
|
||||
background: #e1fbe1;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .change .rep {
|
||||
background: #fef6d9;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .change .old.none,
|
||||
.diff-wrapper.diff.diff-html .change .new.none,
|
||||
.diff-wrapper.diff.diff-html .change .rep.none {
|
||||
background: transparent;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .change ins,
|
||||
.diff-wrapper.diff.diff-html .change del {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .change ins {
|
||||
background: #94f094;
|
||||
}
|
||||
.diff-wrapper.diff.diff-html .change del {
|
||||
background: #f09494;
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
// You can compile this with https://www.sassmeister.com
|
||||
|
||||
$diff-bg-color: #fff !default;
|
||||
$diff-text-color: invert($diff-bg-color) !default;
|
||||
|
||||
$diff-bg-color-ins-base: #8e8 !default;
|
||||
$diff-bg-color-del-base: #e88 !default;
|
||||
$diff-bg-color-rep-base: #fbdb65 !default;
|
||||
|
||||
$diff-op-highlight-ratio: 90% !default;
|
||||
$diff-op-normal-ratio: 25% !default;
|
||||
|
||||
// emphasized colors for detailed inline difference
|
||||
$diff-bg-color-ins-highlight: mix($diff-bg-color-ins-base, $diff-bg-color, $diff-op-highlight-ratio) !default;
|
||||
$diff-bg-color-del-highlight: mix($diff-bg-color-del-base, $diff-bg-color, $diff-op-highlight-ratio) !default;
|
||||
|
||||
// colors for operation rows
|
||||
$diff-bg-color-ins: mix($diff-bg-color-ins-base, $diff-bg-color, $diff-op-normal-ratio) !default;
|
||||
$diff-bg-color-del: mix($diff-bg-color-del-base, $diff-bg-color, $diff-op-normal-ratio) !default;
|
||||
$diff-bg-color-rep: mix($diff-bg-color-rep-base, $diff-bg-color, $diff-op-normal-ratio) !default;
|
||||
|
||||
$diff-table-head-color: mix($diff-bg-color, $diff-text-color, 65%) !default;
|
||||
$diff-table-sidebar-color: mix($diff-bg-color, $diff-text-color, 80%) !default;
|
||||
$diff-border-color: $diff-text-color !default;
|
||||
|
||||
// color for the nonexistent block
|
||||
// for example, there are a deleted line that has no corresponding one
|
||||
$diff-bg-color-none-block: mix($diff-bg-color, $diff-table-sidebar-color, 80%) !default;
|
||||
$diff-bg-color-none-block-alternative: mix($diff-bg-color, $diff-table-sidebar-color, 55%) !default;
|
||||
|
||||
// symbol images
|
||||
$img-space: 'data:image/svg+xml,%3Csvg preserveAspectRatio="xMinYMid meet" viewBox="0 0 12 24" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M4.5 11C4.5 10.1716 5.17157 9.5 6 9.5C6.82843 9.5 7.5 10.1716 7.5 11C7.5 11.8284 6.82843 12.5 6 12.5C5.17157 12.5 4.5 11.8284 4.5 11Z" fill="rgba%2860, 60, 60, 50%25%29"/%3E%3C/svg%3E' !default;
|
||||
$img-tab: 'data:image/svg+xml,%3Csvg preserveAspectRatio="xMinYMid meet" viewBox="0 0 12 24" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M9.5 10.44L6.62 8.12L7.32 7.26L12.04 11V11.44L7.28 14.9L6.62 13.9L9.48 11.78H0V10.44H9.5Z" fill="rgba%2860, 60, 60, 50%25%29"/%3E%3C/svg%3E' !default;
|
||||
|
||||
.diff-wrapper.diff {
|
||||
--tab-size: 4;
|
||||
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
$diff-bg-color-none-block,
|
||||
$diff-bg-color-none-block 0.5em,
|
||||
$diff-bg-color-none-block-alternative 0.5em,
|
||||
$diff-bg-color-none-block-alternative 1em
|
||||
);
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border: 1px solid $diff-border-color;
|
||||
color: $diff-text-color;
|
||||
empty-cells: show;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
|
||||
th {
|
||||
font-weight: 700;
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-collapse: separate;
|
||||
border: none;
|
||||
padding: 1px 2px;
|
||||
background: $diff-bg-color;
|
||||
|
||||
// make empty cell has height
|
||||
&:empty:after {
|
||||
content: ' ';
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
cursor: inherit;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
thead th {
|
||||
background: $diff-table-head-color;
|
||||
border-bottom: 1px solid $diff-border-color;
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tbody {
|
||||
&.skipped {
|
||||
border-top: 1px solid $diff-border-color;
|
||||
|
||||
td,
|
||||
th {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
background: $diff-table-sidebar-color;
|
||||
border-right: 1px solid $diff-border-color;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
width: 4em;
|
||||
|
||||
&.sign {
|
||||
background: $diff-bg-color;
|
||||
border-right: none;
|
||||
padding: 1px 0;
|
||||
text-align: center;
|
||||
width: 1em;
|
||||
|
||||
&.del {
|
||||
background: $diff-bg-color-del;
|
||||
}
|
||||
|
||||
&.ins {
|
||||
background: $diff-bg-color-ins;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.diff-html {
|
||||
white-space: pre-wrap;
|
||||
tab-size: var(--tab-size);
|
||||
|
||||
.ch {
|
||||
line-height: 1em;
|
||||
background-clip: border-box;
|
||||
background-repeat: repeat-x;
|
||||
background-position: left center;
|
||||
|
||||
&.sp {
|
||||
background-image: url($img-space);
|
||||
background-size: 1ch 1.25em;
|
||||
}
|
||||
&.tab {
|
||||
background-image: url($img-tab);
|
||||
background-size: calc(var(--tab-size) * 1ch) 1.25em;
|
||||
background-position: 2px center;
|
||||
}
|
||||
}
|
||||
|
||||
.change {
|
||||
&.change-eq {
|
||||
.old,
|
||||
.new {
|
||||
background: $diff-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.old {
|
||||
background: $diff-bg-color-del;
|
||||
}
|
||||
|
||||
.new {
|
||||
background: $diff-bg-color-ins;
|
||||
}
|
||||
|
||||
.rep {
|
||||
background: $diff-bg-color-rep;
|
||||
}
|
||||
|
||||
.old,
|
||||
.new,
|
||||
.rep {
|
||||
&.none {
|
||||
background: transparent;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
ins,
|
||||
del {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ins {
|
||||
background: $diff-bg-color-ins-highlight;
|
||||
}
|
||||
|
||||
del {
|
||||
background: $diff-bg-color-del-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<div>Hello World! Bad-tempered.</div>
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
Let's add a new line here.
|
||||
X
|
||||
Y
|
||||
Just a context separator. 1
|
||||
M
|
||||
N
|
||||
Do you know in Japanese, "魚の缶詰" means fish can.
|
||||
Tab visualization test.
|
||||
G
|
||||
Say hello to my neighbors.
|
||||
A
|
||||
Just a context separator. 2
|
||||
B
|
||||
Donec rutrum test.
|
||||
There is a new inserted line.
|
||||
C
|
||||
Sed dolor lorem ipsum hendrerit.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<p>Hello World! Good-looking.</p>
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
X
|
||||
Y
|
||||
Just a context separator. 1
|
||||
M
|
||||
N
|
||||
Do you know in Chinese, "金槍魚罐頭" means tuna can.
|
||||
Tab visualization test.
|
||||
G
|
||||
// remember to delete this line
|
||||
Say hello to my neighbors.
|
||||
A
|
||||
Just a context separator. 2
|
||||
B
|
||||
Donec rutrum.
|
||||
C
|
||||
Sed dictum lorem ipsum.
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
<?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 ??= realpath(__DIR__ . '/..');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the information about available renderers.
|
||||
*/
|
||||
public static function getRenderersInfo(): array
|
||||
{
|
||||
static $info;
|
||||
|
||||
if (isset($info)) {
|
||||
return $info;
|
||||
}
|
||||
|
||||
$glob = implode(\DIRECTORY_SEPARATOR, [
|
||||
self::getProjectDirectory(),
|
||||
'src',
|
||||
'Renderer',
|
||||
'{' . implode(',', RendererConstant::RENDERER_TYPES) . '}',
|
||||
'*.php',
|
||||
]);
|
||||
|
||||
$fileNames = array_map(
|
||||
// get basename without file extension
|
||||
static fn (string $file): string => pathinfo($file, \PATHINFO_FILENAME),
|
||||
// paths of all Renderer files
|
||||
glob($glob, \GLOB_BRACE),
|
||||
);
|
||||
|
||||
$renderers = array_filter(
|
||||
$fileNames,
|
||||
// only normal class files are wanted
|
||||
static fn (string $fileName): bool => (
|
||||
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 = self::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 self::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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+522
@@ -0,0 +1,522 @@
|
||||
<?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 array $options = [];
|
||||
|
||||
/**
|
||||
* @var string[] the old sequence
|
||||
*/
|
||||
private array $old = [];
|
||||
|
||||
/**
|
||||
* @var string[] the new sequence
|
||||
*/
|
||||
private array $new = [];
|
||||
|
||||
/**
|
||||
* @var bool is any of cached properties dirty?
|
||||
*/
|
||||
private bool $isCacheDirty = true;
|
||||
|
||||
/**
|
||||
* @var SequenceMatcher the sequence matcher
|
||||
*/
|
||||
private SequenceMatcher $sequenceMatcher;
|
||||
|
||||
private int $oldSrcLength = 0;
|
||||
|
||||
private int $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 int $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 int $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 int $oldNewComparison = 0;
|
||||
|
||||
/**
|
||||
* @var int[][][] array containing the generated opcodes for the differences between the two items
|
||||
*/
|
||||
private array $groupedOpcodes = [];
|
||||
|
||||
/**
|
||||
* @var int[][][] array containing the generated opcodes for the differences between the two items (GNU version)
|
||||
*/
|
||||
private array $groupedOpcodesGnu = [];
|
||||
|
||||
/**
|
||||
* @var array associative array of the default options available for the Differ class and their default value
|
||||
*/
|
||||
private static array $defaultOptions = [
|
||||
// show how many neighbor lines
|
||||
// Differ::CONTEXT_ALL can be used to show the whole file
|
||||
'context' => 3,
|
||||
// ignore case difference
|
||||
'ignoreCase' => false,
|
||||
// ignore line ending difference
|
||||
'ignoreLineEnding' => false,
|
||||
// ignore whitespace difference
|
||||
'ignoreWhitespace' => false,
|
||||
// if the input sequence is too long, it will just gives up (especially for char-level diff)
|
||||
'lengthLimit' => 2000,
|
||||
// if truthy, when inputs are identical, the whole inputs will be rendered in the output
|
||||
'fullContextIfIdentical' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param string[] $old array containing the lines of the old string to compare
|
||||
* @param string[] $new array containing the lines of 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 + self::$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 ??= new self([], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if ($this->oldNewComparison === 0 && $this->options['fullContextIfIdentical']) {
|
||||
$opcodes = [
|
||||
[
|
||||
[SequenceMatcher::OP_EQ, 0, \count($old), 0, \count($new)],
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$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);
|
||||
|
||||
if ($this->oldNewComparison === 0 && $this->options['fullContextIfIdentical']) {
|
||||
$opcodes = [
|
||||
[
|
||||
[SequenceMatcher::OP_EQ, 0, \count($old), 0, \count($new)],
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$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 (self::CACHED_PROPERTIES as $property => $value) {
|
||||
$this->{$property} = $value;
|
||||
}
|
||||
|
||||
$this->isCacheDirty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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 array $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
|
||||
{
|
||||
return self::$singletons[$type] ??= self::make($type, ...$ctorArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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 array $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
|
||||
{
|
||||
return self::$singletons[$renderer] ??= self::make($renderer, ...$ctorArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?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.
|
||||
*
|
||||
* @todo use typed properties (BC breaking for public interface) in v7
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* If the input "changes" have `<ins>...</ins>` or `<del>...</del>`,
|
||||
* which means they have been processed, then `false`. Otherwise, `true`.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $changesAreRaw = true;
|
||||
|
||||
/**
|
||||
* @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
|
||||
// check the "Custom Language" section in the readme for more advanced usage
|
||||
'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,
|
||||
// convert spaces/tabs into HTML codes like `<span class="ch sp"> </span>`
|
||||
// and the frontend is responsible for rendering them with CSS.
|
||||
// when using this, "spacesToNbsp" should be false and "tabSize" is not respected.
|
||||
'spaceToHtmlTag' => false,
|
||||
// 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 available 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
final public function render(Differ $differ): string
|
||||
{
|
||||
$this->changesAreRaw = true;
|
||||
|
||||
// the "no difference" situation may happen frequently
|
||||
return $differ->getOldNewComparison() === 0 && !$differ->options['fullContextIfIdentical']
|
||||
? $this->getResultForIdenticals()
|
||||
: $this->renderWorker($differ);
|
||||
}
|
||||
|
||||
final public function renderArray(array $differArray): string
|
||||
{
|
||||
$this->changesAreRaw = false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
<?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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
protected function renderWorker(Differ $differ): string
|
||||
{
|
||||
$rendered = $this->redererChanges($this->getChanges($differ));
|
||||
|
||||
return $this->cleanUpDummyHtmlClosures($rendered);
|
||||
}
|
||||
|
||||
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 ??= new MbString();
|
||||
$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
|
||||
{
|
||||
if (!$this->options['spaceToHtmlTag']) {
|
||||
$string = $this->expandTabs($string, $this->options['tabSize']);
|
||||
}
|
||||
|
||||
$string = $this->htmlSafe($string);
|
||||
|
||||
if ($this->options['spacesToNbsp']) {
|
||||
$string = $this->htmlFixSpaces($string);
|
||||
}
|
||||
|
||||
if ($this->options['spaceToHtmlTag']) {
|
||||
$string = $this->htmlReplaceSpacesToHtmlTag($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
|
||||
static fn (array $matches): string => 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 str_replace(' ', ' ', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace spaces/tabs with HTML tags, which may be styled in frontend with CSS.
|
||||
*
|
||||
* @param string $string the string of spaces
|
||||
*
|
||||
* @return string the HTML representation of the string
|
||||
*/
|
||||
protected function htmlReplaceSpacesToHtmlTag(string $string): string
|
||||
{
|
||||
return strtr($string, [
|
||||
' ' => '<span class="ch sp"> </span>',
|
||||
"\t" => "<span class=\"ch tab\">\t</span>",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
<?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;
|
||||
|
||||
protected function redererChanges(array $changes): string
|
||||
{
|
||||
if (empty($changes)) {
|
||||
return $this->getResultForIdenticals();
|
||||
}
|
||||
|
||||
$wrapperClasses = [
|
||||
...$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
|
||||
{
|
||||
switch ($block['tag']) {
|
||||
case SequenceMatcher::OP_EQ:
|
||||
$content = $this->renderTableBlockEqual($block);
|
||||
break;
|
||||
case SequenceMatcher::OP_INS:
|
||||
$content = $this->renderTableBlockInsert($block);
|
||||
break;
|
||||
case SequenceMatcher::OP_DEL:
|
||||
$content = $this->renderTableBlockDelete($block);
|
||||
break;
|
||||
case SequenceMatcher::OP_REP:
|
||||
$content = $this->renderTableBlockReplace($block);
|
||||
break;
|
||||
default:
|
||||
$content = '';
|
||||
}
|
||||
|
||||
return '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' . $content . '</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);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$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 = [...$delParts, ...$insParts];
|
||||
usort(
|
||||
$mergedParts,
|
||||
// first sort by "offsetClean", "order" then by "type"
|
||||
static fn (array $a, array $b): int => (
|
||||
$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 information 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 information
|
||||
*/
|
||||
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 ??= new MbString();
|
||||
$mbNew ??= new MbString();
|
||||
$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",
|
||||
static 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",
|
||||
static 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
|
||||
{
|
||||
if (!$this->changesAreRaw) {
|
||||
return $lines;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// fixes https://github.com/jfcherng/php-diff/issues/34
|
||||
$line = str_replace("\r\n", "\n", $line);
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?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',
|
||||
];
|
||||
|
||||
protected function redererChanges(array $changes): string
|
||||
{
|
||||
if (empty($changes)) {
|
||||
return $this->getResultForIdenticals();
|
||||
}
|
||||
|
||||
$wrapperClasses = [
|
||||
...$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
|
||||
{
|
||||
switch ($block['tag']) {
|
||||
case SequenceMatcher::OP_EQ:
|
||||
$content = $this->renderTableBlockEqual($block);
|
||||
break;
|
||||
case SequenceMatcher::OP_INS:
|
||||
$content = $this->renderTableBlockInsert($block);
|
||||
break;
|
||||
case SequenceMatcher::OP_DEL:
|
||||
$content = $this->renderTableBlockDelete($block);
|
||||
break;
|
||||
case SequenceMatcher::OP_REP:
|
||||
$content = $this->renderTableBlockReplace($block);
|
||||
break;
|
||||
default:
|
||||
$content = '';
|
||||
}
|
||||
|
||||
return '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' . $content . '</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>'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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;
|
||||
|
||||
public function getResultForIdenticalsDefault(): string
|
||||
{
|
||||
return '[]';
|
||||
}
|
||||
|
||||
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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function formatStringFromLines(string $string): string
|
||||
{
|
||||
return $this->htmlSafe($string);
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
|
||||
|
||||
use Jfcherng\Diff\SequenceMatcher;
|
||||
|
||||
/**
|
||||
* Base renderer for rendering HTML-based line diffs.
|
||||
*
|
||||
* @todo use typed properties (BC breaking for public interface) in v7
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* @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];
|
||||
}
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
|
||||
|
||||
use Jfcherng\Utility\MbString;
|
||||
|
||||
final class None extends AbstractLineRenderer
|
||||
{
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* @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(
|
||||
static fn (string $glue): string => 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
<?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',
|
||||
];
|
||||
|
||||
protected function redererChanges(array $changes): string
|
||||
{
|
||||
if (empty($changes)) {
|
||||
return $this->getResultForIdenticals();
|
||||
}
|
||||
|
||||
$wrapperClasses = [
|
||||
...$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
|
||||
{
|
||||
switch ($block['tag']) {
|
||||
case SequenceMatcher::OP_EQ:
|
||||
$content = $this->renderTableBlockEqual($block);
|
||||
break;
|
||||
case SequenceMatcher::OP_INS:
|
||||
$content = $this->renderTableBlockInsert($block);
|
||||
break;
|
||||
case SequenceMatcher::OP_DEL:
|
||||
$content = $this->renderTableBlockDelete($block);
|
||||
break;
|
||||
case SequenceMatcher::OP_REP:
|
||||
$content = $this->renderTableBlockReplace($block);
|
||||
break;
|
||||
default:
|
||||
$content = '';
|
||||
}
|
||||
|
||||
return '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' . $content . '</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>';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function getResultForIdenticalsDefault(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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',
|
||||
];
|
||||
|
||||
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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?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',
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<?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 ??= $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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the array is associative.
|
||||
*
|
||||
* @param array $arr the array
|
||||
*
|
||||
* @return bool `true` if the array is associative, `false` otherwise
|
||||
*/
|
||||
public static function isAssociative($arr): bool
|
||||
{
|
||||
foreach ($arr as $key => $value) {
|
||||
if (\is_string($key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jfcherng\Diff\Utility;
|
||||
|
||||
final class Language
|
||||
{
|
||||
/**
|
||||
* @var string[] the translation dict
|
||||
*/
|
||||
private array $translations = [];
|
||||
|
||||
/**
|
||||
* @var string the language name
|
||||
*/
|
||||
private string $language = '_custom_';
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param array<int,string|string[]>|string|string[] $target the language ID or translations dict
|
||||
*/
|
||||
public function __construct($target = 'eng')
|
||||
{
|
||||
$this->load($target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the language.
|
||||
*
|
||||
* @return string the language
|
||||
*/
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the translations.
|
||||
*
|
||||
* @return array the translations
|
||||
*/
|
||||
public function getTranslations(): array
|
||||
{
|
||||
return $this->translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the target language.
|
||||
*
|
||||
* @param array<int,string|string[]>|string|string[] $target the language ID or translations dict
|
||||
*/
|
||||
public function load($target): void
|
||||
{
|
||||
$this->translations = $this->resolve($target);
|
||||
$this->language = \is_string($target) ? $target : '_custom_';
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the text.
|
||||
*
|
||||
* @param string $text the text
|
||||
*/
|
||||
public function translate(string $text): string
|
||||
{
|
||||
return $this->translations[$text] ?? "![{$text}]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[]
|
||||
*/
|
||||
private static function getTranslationsByLanguage(string $language): array
|
||||
{
|
||||
$filePath = __DIR__ . "/../languages/{$language}.json";
|
||||
$file = new \SplFileObject($filePath, 'r');
|
||||
$fileContent = $file->fread($file->getSize());
|
||||
|
||||
try {
|
||||
$decoded = json_decode($fileContent, true, 512, \JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new \Exception(sprintf('Fail to decode JSON file (%s): %s', realpath($filePath), (string) $e));
|
||||
}
|
||||
|
||||
return (array) $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the target language.
|
||||
*
|
||||
* @param array<int,string|string[]>|string|string[] $target the language ID or translations array
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return string[] the resolved translations
|
||||
*/
|
||||
private function resolve($target): array
|
||||
{
|
||||
if (\is_string($target)) {
|
||||
return self::getTranslationsByLanguage($target);
|
||||
}
|
||||
|
||||
if (\is_array($target)) {
|
||||
// $target is an associative array
|
||||
if (Arr::isAssociative($target)) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
// $target is a list of "key-value pairs or language ID"
|
||||
return array_reduce(
|
||||
$target,
|
||||
fn (array $carry, $translation): array => array_merge($carry, $this->resolve($translation)),
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('$target is not in valid form');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Стара версия",
|
||||
"new_version": "Нова версия",
|
||||
"differences": "Разлики"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "旧版本",
|
||||
"new_version": "新版本",
|
||||
"differences": "差异"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "舊版本",
|
||||
"new_version": "新版本",
|
||||
"differences": "差異"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Původní",
|
||||
"new_version": "Nové",
|
||||
"differences": "Rozdíly"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Alt",
|
||||
"new_version": "Neu",
|
||||
"differences": "Unterschiede"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Old",
|
||||
"new_version": "New",
|
||||
"differences": "Differences"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Avant",
|
||||
"new_version": "Après",
|
||||
"differences": "Différences"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Vecchio",
|
||||
"new_version": "Nuovo",
|
||||
"differences": "Differenze"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "古い",
|
||||
"new_version": "新しい",
|
||||
"differences": "差異"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Tidligere versjon",
|
||||
"new_version": "Ny versjon",
|
||||
"differences": "Differanse"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Original",
|
||||
"new_version": "Nova",
|
||||
"differences": "Diferenças"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Please use 3-char `ISO 639-2/B` naming convention: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Старая версия",
|
||||
"new_version": "Новая версия",
|
||||
"differences": "Различия"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Anterior",
|
||||
"new_version": "Nuevo",
|
||||
"differences": "Diferencias"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Eski",
|
||||
"new_version": "Yeni",
|
||||
"differences": "Değişiklikler"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"old_version": "Було",
|
||||
"new_version": "Стало",
|
||||
"differences": "Відмінності"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
$config = (new PhpCsFixer\Config())
|
||||
->setIndent(' ')
|
||||
->setLineEnding("\n")
|
||||
->setCacheFile(__DIR__ . '/.php-cs-fixer.cache')
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PHP71Migration' => true,
|
||||
'@PHP71Migration:risky' => true,
|
||||
'@PHP73Migration' => true,
|
||||
'@PHP74Migration' => true,
|
||||
'@PHP74Migration:risky' => true,
|
||||
'@PHP80Migration' => false,
|
||||
'@PHP80Migration:risky' => true,
|
||||
'@PHP81Migration' => 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'],
|
||||
'echo_tag_syntax' => ['format' => 'short'],
|
||||
'escape_implicit_backslashes' => false,
|
||||
'fully_qualified_strict_types' => true,
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'list_syntax' => ['syntax' => 'short'],
|
||||
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
|
||||
'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'],
|
||||
'native_constant_invocation' => ['scope' => 'all'],
|
||||
'native_function_invocation' => ['scope' => 'all', 'include' => ['@compiler_optimized']],
|
||||
'native_function_type_declaration_casing' => true,
|
||||
'no_alternative_syntax' => true,
|
||||
'no_null_property_initialization' => true,
|
||||
'no_superfluous_elseif' => true,
|
||||
'no_unneeded_control_parentheses' => true,
|
||||
'no_useless_concat_operator' => false,
|
||||
'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_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_order_by_value' => ['annotations' => ['covers']],
|
||||
'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,
|
||||
'static_lambda' => false,
|
||||
'strict_comparison' => false,
|
||||
'strict_param' => false,
|
||||
'string_line_ending' => true,
|
||||
'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arrays', 'arguments']],
|
||||
'types_spaces' => ['space_multiple_catch' => 'single'],
|
||||
'yoda_style' => false,
|
||||
])
|
||||
->setFinder(
|
||||
PhpCsFixer\Finder::create()
|
||||
->exclude('libs')
|
||||
->exclude('tests/Fixtures')
|
||||
->exclude('var')
|
||||
->exclude('vendor')
|
||||
->in(__DIR__)
|
||||
)
|
||||
;
|
||||
|
||||
return $config;
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
|
||||
VERSION 1 INIT
|
||||
===============
|
||||
|
||||
Version 1.4 - Add MbString::strToChars()
|
||||
28/05/2020 03:25 1.4.2 allow PHP 8
|
||||
7f8bdde chore: update deps
|
||||
738b66b chore: Composer PHP constrain >=7.1.3
|
||||
04/04/2020 01:10 1.4.1 nits
|
||||
c6b73d9 chore: update deps
|
||||
66fc4ef docs: use markdown grammar badges in readme
|
||||
1396862 chore: $ composer fix
|
||||
9a7a15e chore: update deps
|
||||
feffd7b Add PHP 8.0.0-dev benchmark result
|
||||
2108041 Suppress annoying PhanTypePossiblyInvalidDimOffset errors
|
||||
157c102 Remove unused codes
|
||||
42b320a Update deps
|
||||
b19e65f Update .travis.yml to PHP 7.4 stable
|
||||
21/09/2019 10:17 1.4.0 initial release
|
||||
7a81573 Add MbString::strToChars()
|
||||
|
||||
Version 1.3 - Add MbString::toArraySplit()
|
||||
21/09/2019 10:02 1.3.2 Faster MbString::toArray()
|
||||
75785d5 nits
|
||||
4fe3304 Faster MbString::toArray()
|
||||
d68c92d Update deps
|
||||
0191a10 Update readme to use badges from shields.io
|
||||
715f7b1 Revise docs
|
||||
a509909 Output "Nums of Runs" in the benchmark
|
||||
11/07/2019 09:16 1.3.1 nits
|
||||
e5ddaaf Add benchmark results
|
||||
464c021 Remove wrong demo.php
|
||||
649e809 Update deps
|
||||
24c449a Add .gitattributes
|
||||
37957e2 $ composer fix
|
||||
0bd808f Update deps
|
||||
8d873b7 Update .travis.yml for 7.4snapshot
|
||||
492dd4d Add .editorconfig
|
||||
e554752 Update deps
|
||||
64f5b45 nits
|
||||
48d0a13 Use native namespace
|
||||
68e31aa Update deps
|
||||
22/01/2019 13:47 1.3.0 initial release
|
||||
5bcdcc5 Add MbString::toArraySplit()
|
||||
21dff79 year++
|
||||
8865407 Add tests for PHP 7.3
|
||||
6b89bec Update README
|
||||
9f2cef0 Update deps
|
||||
|
||||
Version 1.2 - init
|
||||
04/09/2018 18:58 1.2.4 php-cs-fixer
|
||||
0020f0d php-cs-fixer: enable native_constant_invocation and native_function_invocation
|
||||
f49dd29 Update deps
|
||||
cb21cf1 Rename CHANGELOG.md -> CHANGELOG
|
||||
cf74396 Update deps
|
||||
4bb0848 Update .travis.yml
|
||||
18/07/2018 08:28 1.2.3 Update deps
|
||||
4dc8711 Add composer.lock
|
||||
73a7dd6 Mark ext-iconv as a requirement
|
||||
04/07/2018 15:28 1.2.2 nits
|
||||
8de1ef9 Update .rmt.yml
|
||||
04/07/2018 14:57 1.2.1 initial release
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2023 Jack Cherng <jfcherng@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# php-mb-string
|
||||
|
||||
[](https://github.com/jfcherng/php-mb-string/actions)
|
||||
[](https://packagist.org/packages/jfcherng/php-mb-string)
|
||||
[](https://packagist.org/packages/jfcherng/php-mb-string)
|
||||
[](https://github.com/jfcherng/php-mb-string/blob/master/LICENSE)
|
||||
[](https://github.com/jfcherng/php-mb-string/stargazers)
|
||||
[](https://www.paypal.me/jfcherng/5usd)
|
||||
|
||||
A high performance multibyte sting implementation for frequently reading/writing operations.
|
||||
|
||||
## Why I Write This Package?
|
||||
|
||||
Consider that you have a **LONG** multibyte string and
|
||||
you want to do lots of following operations on it.
|
||||
|
||||
- Random reading/writing such as `$char = $str[5];` or `$str[5] = '許';`.
|
||||
- Replacement such as `str_replace($search, $replace, $str);`.
|
||||
- Insertion such as `substr_replace($insert, $str, $position, 0);`.
|
||||
- Get substring such as `substr($str, $start, $length);`.
|
||||
|
||||
Because strings in PHP are not UTF-8, to do operations above safely,
|
||||
you have to either use `mb_*()` functions or calculate the index by yourself.
|
||||
Using `mb_*()` functions frequently can be a performance loss because it has
|
||||
to re-decode the source string basing on the given encoding every time when you call it.
|
||||
The longer the string is, the severer the problem becomes.
|
||||
|
||||
Instead, this class internally stores the string in its UTF-32 form,
|
||||
which is fixed-width (1 char always occupies 4 bytes) so we are able to
|
||||
perform speedy random accesses. With the power of random access, we could
|
||||
use `str_*()` functions to do the job internally.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
composer require jfcherng/php-mb-string
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
See [tests/MbStringTest.php](https://github.com/jfcherng/php-mb-string/blob/master/tests/MbStringTest.php).
|
||||
|
||||
## Benchmark
|
||||
|
||||
See [benchmark/\_results.txt](https://github.com/jfcherng/php-mb-string/blob/master/benchmark/_results.txt).
|
||||
|
||||
## What Are You Doing With This Package?
|
||||
|
||||
I develop this for a PHP diff package, [jfcherng/php-diff](https://github.com/jfcherng/php-diff).
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "jfcherng/php-mb-string",
|
||||
"description": "A high performance multibytes sting implementation for frequently reading/writing operations.",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jack Cherng",
|
||||
"email": "jfcherng@gmail.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "beta",
|
||||
"prefer-stable": true,
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jfcherng\\Utility\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Jfcherng\\Utility\\Test\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1.99"
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-iconv": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3",
|
||||
"phan/phan": "^5",
|
||||
"phpunit/phpunit": "^9 || ^10"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-iconv": "Either \"ext-iconv\" or \"ext-mbstring\" is requried.",
|
||||
"ext-mbstring": "Either \"ext-iconv\" or \"ext-mbstring\" is requried."
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": [
|
||||
"phan --color"
|
||||
],
|
||||
"fix": [
|
||||
"php-cs-fixer fix --verbose"
|
||||
],
|
||||
"test": [
|
||||
"phpunit"
|
||||
]
|
||||
}
|
||||
}
|
||||
+4267
File diff suppressed because it is too large
Load Diff
+400
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jfcherng\Utility;
|
||||
|
||||
/**
|
||||
* An internal UTF-32 multi-bytes string class.
|
||||
*
|
||||
* Because UTF-8 is varied-width, mb_*() is kinda O(n) when doing decoding.
|
||||
* Using iconv() to make it UTF-32 and work with str*() can be possibly faster.
|
||||
*
|
||||
* UTF-32 is a fix-width encoding (1 char = 4 bytes).
|
||||
* Note that the first 4 bytes in a UTF-32 string is the header (endian bytes).
|
||||
*
|
||||
* @author Jack Cherng <jfcherng@gmail.com>
|
||||
*/
|
||||
class MbString extends \ArrayObject implements \Stringable
|
||||
{
|
||||
public const MBSTRING_CONVMETHOD_ICONV = 1;
|
||||
public const MBSTRING_CONVMETHOD_MBSTRING = 2;
|
||||
|
||||
/**
|
||||
* The way to convert text encoding.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $convMethod;
|
||||
|
||||
/**
|
||||
* UTF-32 string without endian bytes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $str;
|
||||
|
||||
/**
|
||||
* The original encoding.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $encoding;
|
||||
|
||||
/**
|
||||
* The endian bytes for UTF-32.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $utf32Header;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param string $str the string
|
||||
* @param string $encoding the encoding
|
||||
*/
|
||||
public function __construct(string $str = '', string $encoding = 'UTF-8')
|
||||
{
|
||||
static::$convMethod ??= static::detectConvEncoding();
|
||||
static::$utf32Header ??= static::getUtf32Header();
|
||||
|
||||
$this->encoding = $encoding;
|
||||
$this->set($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
*
|
||||
* @return string string representation of the object
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The string setter.
|
||||
*
|
||||
* @param string $str the string
|
||||
*/
|
||||
public function set(string $str): self
|
||||
{
|
||||
$this->str = $this->inputConv($str);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAt(int $idx, string $char): self
|
||||
{
|
||||
$char = $this->inputConv($char);
|
||||
if (\strlen($char) > 4) {
|
||||
$char = substr($char, 0, 4);
|
||||
}
|
||||
|
||||
$spacesPrepend = $idx - $this->strlen();
|
||||
// set index (out of bound)
|
||||
if ($spacesPrepend > 0) {
|
||||
$this->str .= $this->inputConv(str_repeat(' ', $spacesPrepend)) . $char;
|
||||
}
|
||||
// set index (in bound)
|
||||
else {
|
||||
$this->str = substr_replace($this->str, $char, $idx << 2, 4);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The string getter.
|
||||
*/
|
||||
public function get(): string
|
||||
{
|
||||
return $this->outputConv($this->str);
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw string getter.
|
||||
*
|
||||
* @return string the UTF-32-encoded raw string
|
||||
*/
|
||||
public function getRaw(): string
|
||||
{
|
||||
return $this->str;
|
||||
}
|
||||
|
||||
public function getAt(int $idx): string
|
||||
{
|
||||
return $this->outputConv(substr($this->str, $idx << 2, 4));
|
||||
}
|
||||
|
||||
public function getAtRaw(int $idx): string
|
||||
{
|
||||
return substr($this->str, $idx << 2, 4);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return self::strToChars($this->get());
|
||||
}
|
||||
|
||||
public function toArraySplit(string $regex, int $limit = -1, $flags = 0): array
|
||||
{
|
||||
if ($this->str === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return preg_split($regex, $this->get(), $limit, $flags);
|
||||
}
|
||||
|
||||
public function toArrayRaw(): array
|
||||
{
|
||||
if ($this->str === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return str_split($this->str, 4);
|
||||
}
|
||||
|
||||
public static function strToChars(string $str): array
|
||||
{
|
||||
return preg_split('//uS', $str, -1, \PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
}
|
||||
|
||||
// /////////////////////////////////
|
||||
// string manipulation functions //
|
||||
// /////////////////////////////////
|
||||
|
||||
public function stripos(string $needle, int $offset = 0)
|
||||
{
|
||||
$needle = $this->inputConv($needle);
|
||||
$pos = stripos($this->str, $needle, $offset << 2);
|
||||
|
||||
return \is_bool($pos) ? $pos : $pos >> 2;
|
||||
}
|
||||
|
||||
public function strlen(): int
|
||||
{
|
||||
return \strlen($this->str) >> 2;
|
||||
}
|
||||
|
||||
public function strpos(string $needle, int $offset = 0)
|
||||
{
|
||||
$needle = $this->inputConv($needle);
|
||||
$pos = strpos($this->str, $needle, $offset << 2);
|
||||
|
||||
return \is_bool($pos) ? $pos : $pos >> 2;
|
||||
}
|
||||
|
||||
public function substr(int $start = 0, ?int $length = null): string
|
||||
{
|
||||
return $this->outputConv(
|
||||
isset($length)
|
||||
? substr($this->str, $start << 2, $length << 2)
|
||||
: substr($this->str, $start << 2),
|
||||
);
|
||||
}
|
||||
|
||||
public function substr_replace(string $replacement, int $start = 0, ?int $length = null): string
|
||||
{
|
||||
$replacement = $this->inputConv($replacement);
|
||||
|
||||
return $this->outputConv(
|
||||
isset($length)
|
||||
? substr_replace($this->str, $replacement, $start << 2, $length << 2)
|
||||
: substr_replace($this->str, $replacement, $start << 2),
|
||||
);
|
||||
}
|
||||
|
||||
public function strtolower(): string
|
||||
{
|
||||
return strtolower($this->get());
|
||||
}
|
||||
|
||||
public function strtoupper(): string
|
||||
{
|
||||
return strtoupper($this->get());
|
||||
}
|
||||
|
||||
// //////////////////////////////
|
||||
// non-manipulative functions //
|
||||
// //////////////////////////////
|
||||
|
||||
public function has(string $needle): bool
|
||||
{
|
||||
$needle = $this->inputConv($needle);
|
||||
|
||||
return str_contains($this->str, $needle);
|
||||
}
|
||||
|
||||
public function startsWith(string $needle): bool
|
||||
{
|
||||
$needle = $this->inputConv($needle);
|
||||
|
||||
return $needle === substr($this->str, 0, \strlen($needle));
|
||||
}
|
||||
|
||||
public function endsWith(string $needle): bool
|
||||
{
|
||||
$needle = $this->inputConv($needle);
|
||||
$length = \strlen($needle);
|
||||
|
||||
return $length === 0 ? true : $needle === substr($this->str, -$length);
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////
|
||||
// those functions will not return a value //
|
||||
// ///////////////////////////////////////////
|
||||
|
||||
public function str_insert_i(string $insert, int $position): self
|
||||
{
|
||||
$insert = $this->inputConv($insert);
|
||||
$this->str = substr_replace($this->str, $insert, $position << 2, 0);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function str_enclose_i(array $closures, int $start = 0, ?int $length = null): self
|
||||
{
|
||||
// ex: $closures = array('{', '}');
|
||||
foreach ($closures as &$closure) {
|
||||
$closure = $this->inputConv($closure);
|
||||
}
|
||||
unset($closure);
|
||||
|
||||
if (\count($closures) < 2) {
|
||||
$closures[0] = $closures[1] = reset($closures);
|
||||
}
|
||||
|
||||
if (isset($length)) {
|
||||
$replacement = $closures[0] . substr($this->str, $start << 2, $length << 2) . $closures[1];
|
||||
$this->str = substr_replace($this->str, $replacement, $start << 2, $length << 2);
|
||||
} else {
|
||||
$replacement = $closures[0] . substr($this->str, $start << 2) . $closures[1];
|
||||
$this->str = substr_replace($this->str, $replacement, $start << 2);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function str_replace_i(string $search, string $replace): self
|
||||
{
|
||||
$search = $this->inputConv($search);
|
||||
$replace = $this->inputConv($replace);
|
||||
$this->str = str_replace($search, $replace, $this->str);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function substr_replace_i(string $replacement, int $start = 0, ?int $length = null): self
|
||||
{
|
||||
$replacement = $this->inputConv($replacement);
|
||||
$this->str = (
|
||||
isset($length)
|
||||
? substr_replace($this->str, $replacement, $start << 2, $length << 2)
|
||||
: substr_replace($this->str, $replacement, $start << 2)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ///////////////
|
||||
// ArrayObject //
|
||||
// ///////////////
|
||||
|
||||
public function offsetSet(mixed $idx, mixed $char): void
|
||||
{
|
||||
$this->setAt($idx, $char);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $idx): string
|
||||
{
|
||||
return $this->getAt($idx);
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $idx): bool
|
||||
{
|
||||
return \is_int($idx) ? $this->strlen() > $idx : false;
|
||||
}
|
||||
|
||||
public function append(mixed $str): void
|
||||
{
|
||||
$this->str .= $this->inputConv($str);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return $this->strlen();
|
||||
}
|
||||
|
||||
// //////////////////
|
||||
// misc functions //
|
||||
// //////////////////
|
||||
|
||||
/**
|
||||
* Gets the utf 32 header.
|
||||
*
|
||||
* @return string the UTF-32 header or empty string
|
||||
*/
|
||||
protected static function getUtf32Header(): string
|
||||
{
|
||||
// just use any string to get the endian header, here we use "A"
|
||||
$tmp = self::convEncoding('A', 'UTF-8', 'UTF-32');
|
||||
// some distributions like "php alpine" docker image won't generate the header
|
||||
return $tmp && \strlen($tmp) > 4 ? substr($tmp, 0, 4) : '';
|
||||
}
|
||||
|
||||
protected static function detectConvEncoding(): int
|
||||
{
|
||||
if (\function_exists('iconv') && iconv('UTF-8', 'UTF-32', 'A') !== false) {
|
||||
return static::MBSTRING_CONVMETHOD_ICONV;
|
||||
}
|
||||
|
||||
if (\function_exists('mb_convert_encoding') && mb_convert_encoding('A', 'UTF-32', 'UTF-8') !== false) {
|
||||
return static::MBSTRING_CONVMETHOD_MBSTRING;
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Either "iconv" or "mbstring" extension is required.');
|
||||
}
|
||||
|
||||
protected static function convEncoding(string $str, string $from, string $to): string
|
||||
{
|
||||
if (static::$convMethod === static::MBSTRING_CONVMETHOD_ICONV) {
|
||||
return iconv($from, $to, $str);
|
||||
}
|
||||
|
||||
if (static::$convMethod === static::MBSTRING_CONVMETHOD_MBSTRING) {
|
||||
return mb_convert_encoding($str, $to, $from);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Unknown conversion method.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the output string to its original encoding.
|
||||
*
|
||||
* @param string $str The string
|
||||
*/
|
||||
protected function outputConv(string $str): string
|
||||
{
|
||||
if ($str === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return static::convEncoding(static::$utf32Header . $str, 'UTF-32', $this->encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the input string to UTF-32 without header.
|
||||
*
|
||||
* @param string $str The string
|
||||
*/
|
||||
protected function inputConv(string $str): string
|
||||
{
|
||||
if ($str === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return substr(static::convEncoding($str, $this->encoding, 'UTF-32'), \strlen(static::$utf32Header));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
$config = (new PhpCsFixer\Config())
|
||||
->setIndent(" ")
|
||||
->setLineEnding("\n")
|
||||
->setCacheFile(__DIR__ . '/.php-cs-fixer.cache')
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PHP71Migration' => true,
|
||||
'@PHP71Migration:risky' => true,
|
||||
'@PHP73Migration' => true,
|
||||
'@PHP74Migration' => true,
|
||||
'@PHP74Migration:risky' => true,
|
||||
'@PHP80Migration' => false,
|
||||
'@PHP80Migration:risky' => true,
|
||||
'@PHP81Migration' => 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'],
|
||||
'echo_tag_syntax' => ['format' => 'short'],
|
||||
'escape_implicit_backslashes' => false,
|
||||
'fully_qualified_strict_types' => true,
|
||||
'heredoc_indentation' => false, // disabling this makes test cases more readable
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'list_syntax' => ['syntax' => 'short'],
|
||||
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
|
||||
'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'],
|
||||
'native_constant_invocation' => ['scope' => 'all'],
|
||||
'native_function_invocation' => ['scope' => 'all', 'include' => ['@compiler_optimized']],
|
||||
'native_function_type_declaration_casing' => true,
|
||||
'no_alternative_syntax' => true,
|
||||
'no_null_property_initialization' => 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_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_order_by_value' => ['annotations' => ['covers']],
|
||||
'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,
|
||||
'static_lambda' => false,
|
||||
'strict_comparison' => false,
|
||||
'strict_param' => false,
|
||||
'string_line_ending' => true,
|
||||
'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arrays', 'arguments']],
|
||||
'types_spaces' => ['space_multiple_catch' => 'single'],
|
||||
'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;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2019-2023 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.
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# php-sequence-matcher
|
||||
|
||||
[](https://github.com/jfcherng/php-sequence-matcher/actions)
|
||||
[](https://app.codacy.com/project/jfcherng/php-sequence-matcher/dashboard)
|
||||
[](https://packagist.org/packages/jfcherng/php-sequence-matcher)
|
||||
[](https://packagist.org/packages/jfcherng/php-sequence-matcher)
|
||||
[](https://github.com/jfcherng/php-sequence-matcher/blob/master/LICENSE)
|
||||
[](https://github.com/jfcherng/php-sequence-matcher/stargazers)
|
||||
[](https://www.paypal.me/jfcherng/5usd)
|
||||
|
||||
A longest sequence matcher. The logic is primarily based on the Python [difflib](https://docs.python.org/3/library/difflib.html) package.
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||

|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
composer require jfcherng/php-sequence-matcher
|
||||
```
|
||||
|
||||
|
||||
## Acknowledgment
|
||||
|
||||
This package is stripped from [chrisboulton/php-diff](https://github.com/chrisboulton/php-diff) with some modifications.
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "jfcherng/php-sequence-matcher",
|
||||
"description": "A longest sequence matcher. The logic is primarily based on the Python difflib package.",
|
||||
"type": "library",
|
||||
"license": "BSD-3-Clause",
|
||||
"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": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3",
|
||||
"phan/phan": "^5",
|
||||
"phpunit/phpunit": "^9 || ^10",
|
||||
"squizlabs/php_codesniffer": "^3.7"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1.99"
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": [
|
||||
"phan --color",
|
||||
"phpcs --colors -n"
|
||||
],
|
||||
"fix": [
|
||||
"php-cs-fixer fix --verbose"
|
||||
],
|
||||
"test": [
|
||||
"phpunit --display-deprecations"
|
||||
]
|
||||
}
|
||||
}
|
||||
+4325
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,722 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jfcherng\Diff;
|
||||
|
||||
/**
|
||||
* A longest sequence matcher.
|
||||
*
|
||||
* The logic is primarily based on the Python difflib package.
|
||||
*
|
||||
* @see https://docs.python.org/3/library/difflib.html
|
||||
*/
|
||||
final class SequenceMatcher
|
||||
{
|
||||
/** @var int 0, opcode: no operation */
|
||||
public const OP_NOP = 0;
|
||||
|
||||
/** @var int 1, opcode: equal */
|
||||
public const OP_EQ = 1 << 0;
|
||||
|
||||
/** @var int 2, opcode: delete */
|
||||
public const OP_DEL = 1 << 1;
|
||||
|
||||
/** @var int 4, opcode: insert */
|
||||
public const OP_INS = 1 << 2;
|
||||
|
||||
/** @var int 8, opcode: replace */
|
||||
public const OP_REP = 1 << 3;
|
||||
|
||||
public const OP_INT_TO_STR_MAP = [
|
||||
self::OP_NOP => 'nop',
|
||||
self::OP_EQ => 'eq',
|
||||
self::OP_DEL => 'del',
|
||||
self::OP_INS => 'ins',
|
||||
self::OP_REP => 'rep',
|
||||
];
|
||||
|
||||
public const OP_STR_TO_INT_MAP = [
|
||||
'nop' => self::OP_NOP,
|
||||
'eq' => self::OP_EQ,
|
||||
'del' => self::OP_DEL,
|
||||
'ins' => self::OP_INS,
|
||||
'rep' => self::OP_REP,
|
||||
];
|
||||
|
||||
/**
|
||||
* The helper line which may be used to append to the source inputs to help
|
||||
* it easier to handle EOL at EOF problem. This line shouldn't be counted into diff.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const APPENDED_HELPER_LINE = "\u{fcf28}\u{fc232}";
|
||||
|
||||
/**
|
||||
* @var null|\Closure either a string or an array containing a callback function to determine if a line is "junk" or not
|
||||
*/
|
||||
private ?\Closure $junkCallback;
|
||||
|
||||
/**
|
||||
* @var array the first sequence to compare against
|
||||
*/
|
||||
private array $a = [];
|
||||
|
||||
/**
|
||||
* @var array the second sequence
|
||||
*/
|
||||
private array $b = [];
|
||||
|
||||
/**
|
||||
* @var array the first sequence to compare against (transformed)
|
||||
*/
|
||||
private array $at = [];
|
||||
|
||||
/**
|
||||
* @var array the second sequence (transformed)
|
||||
*/
|
||||
private array $bt = [];
|
||||
|
||||
/**
|
||||
* @var array array of characters that are considered junk from the second sequence. Characters are the array key.
|
||||
*/
|
||||
private array $junkDict = [];
|
||||
|
||||
/**
|
||||
* @var array array of indices that do not contain junk elements
|
||||
*/
|
||||
private array $b2j = [];
|
||||
|
||||
private array $options = [];
|
||||
|
||||
private static array $defaultOptions = [
|
||||
'ignoreCase' => false,
|
||||
'ignoreLineEnding' => false,
|
||||
'ignoreWhitespace' => false,
|
||||
'lengthLimit' => 2000,
|
||||
];
|
||||
|
||||
private array $matchingBlocks = [];
|
||||
|
||||
/**
|
||||
* @var array generated opcodes which manipulates seq1 to seq2
|
||||
*/
|
||||
private array $opcodes = [];
|
||||
|
||||
/**
|
||||
* The constructor. With the sequences being passed, they'll be set
|
||||
* for the sequence matcher and it will perform a basic cleanup &
|
||||
* calculate junk elements.
|
||||
*
|
||||
* @param string[] $a an array containing the lines to compare against
|
||||
* @param string[] $b an array containing the lines to compare
|
||||
* @param null|\Closure $junkCallback either an array or string that references a callback function (if there is one) to determine 'junk' characters
|
||||
* @param array $options the options
|
||||
*/
|
||||
public function __construct(array $a, array $b, ?\Closure $junkCallback = null, array $options = [])
|
||||
{
|
||||
$this->junkCallback = $junkCallback;
|
||||
$this->setOptions($options);
|
||||
$this->setSequences($a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the options.
|
||||
*
|
||||
* @param array $options The options
|
||||
*/
|
||||
public function setOptions(array $options): static
|
||||
{
|
||||
$needRerunChainB = $this->isAnyOptionChanged(
|
||||
$this->options,
|
||||
$options,
|
||||
['ignoreCase', 'ignoreLineEnding', 'ignoreWhitespace', 'lengthLimit'],
|
||||
);
|
||||
|
||||
$this->options = $options + self::$defaultOptions;
|
||||
|
||||
if ($needRerunChainB) {
|
||||
$this->chainB();
|
||||
}
|
||||
|
||||
$this->resetCachedResults();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the options.
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cached results.
|
||||
*/
|
||||
public function resetCachedResults(): static
|
||||
{
|
||||
$this->matchingBlocks = [];
|
||||
$this->opcodes = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the first and second sequences to use with the sequence matcher.
|
||||
*
|
||||
* This method is more effecient than "->setSeq1($old)->setSeq2($new)"
|
||||
* because it only run the routine once.
|
||||
*
|
||||
* @param string[] $a an array containing the lines to compare against
|
||||
* @param string[] $b an array containing the lines to compare
|
||||
*/
|
||||
public function setSequences(array $a, array $b): static
|
||||
{
|
||||
$need_routine = false;
|
||||
|
||||
if ($this->a !== $a) {
|
||||
$need_routine = true;
|
||||
$this->a = $a;
|
||||
}
|
||||
|
||||
if ($this->b !== $b) {
|
||||
$need_routine = true;
|
||||
$this->b = $b;
|
||||
}
|
||||
|
||||
if ($need_routine) {
|
||||
$this->chainB();
|
||||
$this->resetCachedResults();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the first sequence ($a) and reset any internal caches to indicate that
|
||||
* when calling the calculation methods, we need to recalculate them.
|
||||
*
|
||||
* @param string[] $a the sequence to set as the first sequence
|
||||
*/
|
||||
public function setSeq1(array $a): static
|
||||
{
|
||||
if ($this->a !== $a) {
|
||||
$this->a = $a;
|
||||
$this->chainB();
|
||||
$this->resetCachedResults();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the second sequence ($b) and reset any internal caches to indicate that
|
||||
* when calling the calculation methods, we need to recalculate them.
|
||||
*
|
||||
* @param string[] $b the sequence to set as the second sequence
|
||||
*/
|
||||
public function setSeq2(array $b): static
|
||||
{
|
||||
if ($this->b !== $b) {
|
||||
$this->b = $b;
|
||||
$this->chainB();
|
||||
$this->resetCachedResults();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the longest matching block in the two sequences, as defined by the
|
||||
* lower and upper constraints for each sequence. (for the first sequence,
|
||||
* $alo - $ahi and for the second sequence, $blo - $bhi).
|
||||
*
|
||||
* Essentially, of all of the maximal matching blocks, return the one that
|
||||
* startest earliest in $a, and all of those maximal matching blocks that
|
||||
* start earliest in $a, return the one that starts earliest in $b.
|
||||
*
|
||||
* If the junk callback is defined, do the above but with the restriction
|
||||
* that the junk element appears in the block. Extend it as far as possible
|
||||
* by matching only junk elements in both $a and $b.
|
||||
*
|
||||
* @param int $alo the lower constraint for the first sequence
|
||||
* @param int $ahi the upper constraint for the first sequence
|
||||
* @param int $blo the lower constraint for the second sequence
|
||||
* @param int $bhi the upper constraint for the second sequence
|
||||
*
|
||||
* @return int[] an array containing the longest match that includes the starting position in $a, start in $b and the length/size
|
||||
*/
|
||||
public function findLongestMatch(int $alo, int $ahi, int $blo, int $bhi): array
|
||||
{
|
||||
$bestI = $alo;
|
||||
$bestJ = $blo;
|
||||
$bestSize = 0;
|
||||
|
||||
$j2Len = [];
|
||||
|
||||
for ($i = $alo; $i < $ahi; ++$i) {
|
||||
if (null === ($element = $this->at[$i] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newJ2Len = [];
|
||||
$jDict = $this->b2j[$element] ?? [];
|
||||
|
||||
foreach ($jDict as $j) {
|
||||
if ($j < $blo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($j >= $bhi) {
|
||||
break;
|
||||
}
|
||||
|
||||
$k = ($j2Len[$j - 1] ?? 0) + 1;
|
||||
$newJ2Len[$j] = $k;
|
||||
|
||||
if ($k > $bestSize) {
|
||||
$bestI = $i - $k + 1;
|
||||
$bestJ = $j - $k + 1;
|
||||
$bestSize = $k;
|
||||
}
|
||||
}
|
||||
|
||||
$j2Len = $newJ2Len;
|
||||
}
|
||||
|
||||
while (
|
||||
$bestI > $alo
|
||||
&& $bestJ > $blo
|
||||
&& $this->at[$bestI - 1] === $this->bt[$bestJ - 1]
|
||||
&& !$this->isBJunk($this->bt[$bestJ - 1])
|
||||
) {
|
||||
--$bestI;
|
||||
--$bestJ;
|
||||
++$bestSize;
|
||||
}
|
||||
|
||||
while (
|
||||
$bestI + $bestSize < $ahi
|
||||
&& $bestJ + $bestSize < $bhi
|
||||
&& $this->at[$bestI + $bestSize] === $this->bt[$bestJ + $bestSize]
|
||||
&& !$this->isBJunk($this->bt[$bestJ + $bestSize])
|
||||
) {
|
||||
++$bestSize;
|
||||
}
|
||||
|
||||
while (
|
||||
$bestI > $alo
|
||||
&& $bestJ > $blo
|
||||
&& $this->at[$bestI - 1] === $this->bt[$bestJ - 1]
|
||||
&& $this->isBJunk($this->bt[$bestJ - 1])
|
||||
) {
|
||||
--$bestI;
|
||||
--$bestJ;
|
||||
++$bestSize;
|
||||
}
|
||||
|
||||
while (
|
||||
$bestI + $bestSize < $ahi
|
||||
&& $bestJ + $bestSize < $bhi
|
||||
&& $this->at[$bestI + $bestSize] === $this->bt[$bestJ + $bestSize]
|
||||
&& $this->isBJunk($this->bt[$bestJ + $bestSize])
|
||||
) {
|
||||
++$bestSize;
|
||||
}
|
||||
|
||||
return [$bestI, $bestJ, $bestSize];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a nested set of arrays for all of the matching sub-sequences
|
||||
* in the strings $a and $b.
|
||||
*
|
||||
* Each block contains the lower constraint of the block in $a, the lower
|
||||
* constraint of the block in $b and finally the number of lines that the
|
||||
* block continues for.
|
||||
*
|
||||
* @return int[][] a nested array of the matching blocks, as described by the function
|
||||
*/
|
||||
public function getMatchingBlocks(): array
|
||||
{
|
||||
if (!empty($this->matchingBlocks)) {
|
||||
return $this->matchingBlocks;
|
||||
}
|
||||
|
||||
$aCount = \count($this->a);
|
||||
$bCount = \count($this->b);
|
||||
|
||||
$queue = [
|
||||
[0, $aCount, 0, $bCount],
|
||||
];
|
||||
|
||||
$matchingBlocks = [];
|
||||
while (!empty($queue)) {
|
||||
[$alo, $ahi, $blo, $bhi] = array_pop($queue);
|
||||
[$i, $j, $k] = $x = $this->findLongestMatch($alo, $ahi, $blo, $bhi);
|
||||
|
||||
if ($k) {
|
||||
$matchingBlocks[] = $x;
|
||||
|
||||
if ($alo < $i && $blo < $j) {
|
||||
$queue[] = [$alo, $i, $blo, $j];
|
||||
}
|
||||
|
||||
if ($i + $k < $ahi && $j + $k < $bhi) {
|
||||
$queue[] = [$i + $k, $ahi, $j + $k, $bhi];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($matchingBlocks, function (array $a, array $b): int {
|
||||
$aCount = \count($a);
|
||||
$bCount = \count($b);
|
||||
$min = min($aCount, $bCount);
|
||||
|
||||
for ($i = 0; $i < $min; ++$i) {
|
||||
if ($a[$i] !== $b[$i]) {
|
||||
return $a[$i] <=> $b[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return $aCount <=> $bCount;
|
||||
});
|
||||
|
||||
$i1 = $j1 = $k1 = 0;
|
||||
$nonAdjacent = [];
|
||||
foreach ($matchingBlocks as [$i2, $j2, $k2]) {
|
||||
if ($i1 + $k1 === $i2 && $j1 + $k1 === $j2) {
|
||||
$k1 += $k2;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($k1) {
|
||||
$nonAdjacent[] = [$i1, $j1, $k1];
|
||||
}
|
||||
|
||||
$i1 = $i2;
|
||||
$j1 = $j2;
|
||||
$k1 = $k2;
|
||||
}
|
||||
|
||||
if ($k1) {
|
||||
$nonAdjacent[] = [$i1, $j1, $k1];
|
||||
}
|
||||
|
||||
$nonAdjacent[] = [$aCount, $bCount, 0];
|
||||
|
||||
$this->matchingBlocks = $nonAdjacent;
|
||||
|
||||
return $this->matchingBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all of the opcodes for the differences between the
|
||||
* two strings.
|
||||
*
|
||||
* The nested array returned contains an array describing the opcode
|
||||
* which includes:
|
||||
* 0 - The type of op (as described below) for the opcode.
|
||||
* 1 - The beginning line in the first sequence.
|
||||
* 2 - The end line in the first sequence.
|
||||
* 3 - The beginning line in the second sequence.
|
||||
* 4 - The end line in the second sequence.
|
||||
*
|
||||
* The different types of ops include:
|
||||
* replace - The string from $i1 to $i2 in $a should be replaced by
|
||||
* the string in $b from $j1 to $j2.
|
||||
* delete - The string in $a from $i1 to $j2 should be deleted.
|
||||
* insert - The string in $b from $j1 to $j2 should be inserted at
|
||||
* $i1 in $a.
|
||||
* equal - The two strings with the specified ranges are equal.
|
||||
*
|
||||
* @return int[][] array of the opcodes describing the differences between the strings
|
||||
*/
|
||||
public function getOpcodes(): array
|
||||
{
|
||||
if (!empty($this->opcodes)) {
|
||||
return $this->opcodes;
|
||||
}
|
||||
|
||||
$i = $j = 0;
|
||||
$this->opcodes = [];
|
||||
|
||||
foreach ($this->getMatchingBlocks() as [$ai, $bj, $size]) {
|
||||
if ($i < $ai && $j < $bj) {
|
||||
$op = self::OP_REP;
|
||||
} elseif ($i < $ai) {
|
||||
$op = self::OP_DEL;
|
||||
} elseif ($j < $bj) {
|
||||
$op = self::OP_INS;
|
||||
} else {
|
||||
$op = self::OP_NOP;
|
||||
}
|
||||
|
||||
if ($op) {
|
||||
$this->opcodes[] = [$op, $i, $ai, $j, $bj];
|
||||
}
|
||||
|
||||
$i = $ai + $size;
|
||||
$j = $bj + $size;
|
||||
|
||||
if ($size) {
|
||||
$this->opcodes[] = [self::OP_EQ, $ai, $i, $bj, $j];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->opcodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a series of nested arrays containing different groups of generated
|
||||
* opcodes for the differences between the strings with up to $context lines
|
||||
* of surrounding content.
|
||||
*
|
||||
* Essentially what happens here is any big equal blocks of strings are stripped
|
||||
* out, the smaller subsets of changes are then arranged in to their groups.
|
||||
* This means that the sequence matcher and diffs do not need to include the full
|
||||
* content of the different files but can still provide context as to where the
|
||||
* changes are.
|
||||
*
|
||||
* @param int $context the number of lines of context to provide around the groups
|
||||
*
|
||||
* @return int[][][] nested array of all of the grouped opcodes
|
||||
*/
|
||||
public function getGroupedOpcodes(int $context = 3): array
|
||||
{
|
||||
$opcodes = $this->getOpcodes();
|
||||
|
||||
if (empty($opcodes)) {
|
||||
$opcodes = [
|
||||
[self::OP_EQ, 0, 1, 0, 1],
|
||||
];
|
||||
}
|
||||
|
||||
if ($opcodes[0][0] === self::OP_EQ) {
|
||||
// fix the leading sequence which is out of context.
|
||||
$opcodes[0] = [
|
||||
$opcodes[0][0],
|
||||
max($opcodes[0][1], $opcodes[0][2] - $context),
|
||||
$opcodes[0][2],
|
||||
max($opcodes[0][3], $opcodes[0][4] - $context),
|
||||
$opcodes[0][4],
|
||||
];
|
||||
}
|
||||
|
||||
$lastItem = \count($opcodes) - 1;
|
||||
if ($opcodes[$lastItem][0] === self::OP_EQ) {
|
||||
[$op, $i1, $i2, $j1, $j2] = $opcodes[$lastItem];
|
||||
// fix the trailing sequence which is out of context.
|
||||
$opcodes[$lastItem] = [
|
||||
$op,
|
||||
$i1,
|
||||
min($i2, $i1 + $context),
|
||||
$j1,
|
||||
min($j2, $j1 + $context),
|
||||
];
|
||||
}
|
||||
|
||||
$maxRange = $context << 1;
|
||||
$groups = $group = [];
|
||||
foreach ($opcodes as [$op, $i1, $i2, $j1, $j2]) {
|
||||
if ($op === self::OP_EQ && $i2 - $i1 > $maxRange) {
|
||||
$group[] = [
|
||||
$op,
|
||||
$i1,
|
||||
min($i2, $i1 + $context),
|
||||
$j1,
|
||||
min($j2, $j1 + $context),
|
||||
];
|
||||
$groups[] = $group;
|
||||
$group = [];
|
||||
$i1 = max($i1, $i2 - $context);
|
||||
$j1 = max($j1, $j2 - $context);
|
||||
}
|
||||
|
||||
$group[] = [$op, $i1, $i2, $j1, $j2];
|
||||
}
|
||||
|
||||
if (
|
||||
!empty($group)
|
||||
&& (
|
||||
\count($group) !== 1
|
||||
|| $group[0][0] !== self::OP_EQ
|
||||
)
|
||||
) {
|
||||
$groups[] = $group;
|
||||
}
|
||||
|
||||
// there will be at least leading/trailing OP_EQ blocks
|
||||
// if we want really zero-context, we keep only non-equal blocks
|
||||
if ($context <= 0) {
|
||||
$groupsNew = [];
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$groupNew = [];
|
||||
|
||||
foreach ($group as $block) {
|
||||
if ($block[0] !== self::OP_EQ) {
|
||||
$groupNew[] = $block;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($groupNew)) {
|
||||
$groupsNew[] = $groupNew;
|
||||
}
|
||||
}
|
||||
|
||||
return $groupsNew;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an operation code from int into its string form.
|
||||
*
|
||||
* @param int $op the operation code
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return string the string representation of the operation code
|
||||
*/
|
||||
public static function opIntToStr(int $op): string
|
||||
{
|
||||
if (!isset(self::OP_INT_TO_STR_MAP[$op])) {
|
||||
throw new \InvalidArgumentException("Invalid OP: {$op}");
|
||||
}
|
||||
|
||||
return self::OP_INT_TO_STR_MAP[$op];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an operation code from string into its int form.
|
||||
*
|
||||
* @param string $op the operation code
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return int the int representation of the operation code
|
||||
*/
|
||||
public static function opStrToInt(string $op): int
|
||||
{
|
||||
if (!isset(self::OP_STR_TO_INT_MAP[$op])) {
|
||||
throw new \InvalidArgumentException("Invalid OP: {$op}");
|
||||
}
|
||||
|
||||
return self::OP_STR_TO_INT_MAP[$op];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if any option under test changed.
|
||||
*
|
||||
* @param array $old the old options
|
||||
* @param array $new the new options
|
||||
* @param array $keys the option keys under test
|
||||
*/
|
||||
private function isAnyOptionChanged(array $old, array $new, array $keys): bool
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (isset($new[$key]) && $new[$key] !== $old[$key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the processed line with the initialized options.
|
||||
*
|
||||
* @param string $line the line
|
||||
*
|
||||
* @return string the line after being processed
|
||||
*/
|
||||
private function processLineWithOptions(string $line): string
|
||||
{
|
||||
if ($this->options['ignoreWhitespace']) {
|
||||
static $whitespaces = [' ', "\t", "\r", "\n"];
|
||||
|
||||
$line = str_replace($whitespaces, '', $line);
|
||||
}
|
||||
|
||||
if ($this->options['ignoreCase']) {
|
||||
$line = strtolower($line);
|
||||
}
|
||||
|
||||
if ($this->options['ignoreLineEnding']) {
|
||||
$line = rtrim($line, "\r\n");
|
||||
}
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the internal arrays containing the list of junk and non-junk
|
||||
* characters for the second ($b) sequence.
|
||||
*/
|
||||
private function chainB(): static
|
||||
{
|
||||
$this->at = array_map([$this, 'processLineWithOptions'], $this->a);
|
||||
$this->bt = array_map([$this, 'processLineWithOptions'], $this->b);
|
||||
|
||||
$length = \count($this->bt);
|
||||
$this->b2j = [];
|
||||
$popularDict = [];
|
||||
|
||||
for ($i = 0; $i < $length; ++$i) {
|
||||
$char = $this->bt[$i];
|
||||
$this->b2j[$char] = $this->b2j[$char] ?? [];
|
||||
|
||||
if (
|
||||
$length >= $this->options['lengthLimit']
|
||||
&& \count($this->b2j[$char]) * 100 > $length
|
||||
&& $char !== self::APPENDED_HELPER_LINE
|
||||
) {
|
||||
$popularDict[$char] = 1;
|
||||
|
||||
unset($this->b2j[$char]);
|
||||
} else {
|
||||
$this->b2j[$char][] = $i;
|
||||
}
|
||||
}
|
||||
|
||||
// remove leftovers
|
||||
foreach (array_keys($popularDict) as $char) {
|
||||
unset($this->b2j[$char]);
|
||||
}
|
||||
|
||||
$this->junkDict = [];
|
||||
if (\is_callable($this->junkCallback)) {
|
||||
foreach (array_keys($popularDict) as $char) {
|
||||
if (($this->junkCallback)($char)) {
|
||||
$this->junkDict[$char] = 1;
|
||||
unset($popularDict[$char]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($this->b2j) as $char) {
|
||||
if (($this->junkCallback)($char)) {
|
||||
$this->junkDict[$char] = 1;
|
||||
unset($this->b2j[$char]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a particular character is in the junk dictionary
|
||||
* for the list of junk characters.
|
||||
*
|
||||
* @return bool $b True if the character is considered junk. False if not.
|
||||
*/
|
||||
private function isBJunk(string $b): bool
|
||||
{
|
||||
return isset($this->junkDict[$b]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user