diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..fdade2f6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +phpcs.xml export-ignore diff --git a/.gitignore b/.gitignore index 9e1ec532..ddbe8d8f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,11 @@ ### Composer ### composer.phar +/codeCoverage /vendor/ +/composer.lock +/.phpunit.result.cache + # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file @@ -72,4 +76,3 @@ local.properties # End of https://www.gitignore.io/api/windows,eclipse,composer -/composer.lock diff --git a/LICENSE b/LICENSE index 88d00098..78654c0d 100644 --- a/LICENSE +++ b/LICENSE @@ -12,7 +12,7 @@ modification, are permitted provided that the following conditions are met: - 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 Chris Boulton nor the names of its contributors + - Neither the name of the Mario Brandt nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/README.md b/README.md index 67be32fa..349971fd 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,25 @@ # PHP Diff Class -[![SensioLabsInsight](https://insight.sensiolabs.com/projects/aa609edb-cdb1-45cf-ad51-afbdab48f6a1/mini.png)](https://insight.sensiolabs.com/projects/aa609edb-cdb1-45cf-ad51-afbdab48f6a1) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/db5f8d57b1234502aeb852afc87e0dfe)](https://www.codacy.com/app/leet31337/php-diff) - -[![Latest Version](https://img.shields.io/github/release/JBlond/php-diff.svg?style=flat-square&label=Release)](https://github.com/JBlond/php-diff/releases) [![Packagist Installs](https://badgen.net/packagist/dt/JBlond/php-diff)](https://packagist.org/packages/jblond/php-diff) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/db5f8d57b1234502aeb852afc87e0dfe)](https://app.codacy.com/gh/JBlond/php-diff/dashboard) +[![Latest Version](https://img.shields.io/github/release/JBlond/php-diff.svg?style=flat-square&label=Release)](https://github.com/JBlond/php-diff/releases) +[![Packagist Installs](https://badgen.net/packagist/dt/JBlond/php-diff)](https://packagist.org/packages/jblond/php-diff) ## Introduction -A comprehensive library for generating differences between two hashable objects (strings or arrays). -Generated differences can be rendered in all of the standard formats including: +A comprehensive library for generating differences between two hashable +objects (strings or arrays). Generated differences can be rendered in all the +standard formats including: + * Unified * Context -* Inline HTML * Side by Side HTML * Unified HTML +* Unified Commandline colored output +* JSON -The logic behind the core of the diff engine (ie, the sequence matcher) is primarily based on the Python difflib -package. The reason for doing so is primarily because of its high degree of accuracy. +The logic behind the core of the diff engine (ie, the sequence matcher) is +primarily based on the Python difflib package. The reason for doing so is +primarily because of its high degree of accuracy. ## Install @@ -23,81 +27,131 @@ package. The reason for doing so is primarily because of its high degree of accu composer require jblond/php-diff ``` +### Install for cli support + +For cli usage you need to install the suggested `jblond/php-cli` package. + +## Documentation + +See the [Wiki](https://github.com/JBlond/php-diff/wiki) for + +* [Getting started](https://github.com/JBlond/php-diff/wiki/1.-Getting-Started) +* [Parameters and Options](https://github.com/JBlond/php-diff/wiki/2.-Parameters-and-Options) +* [Custom Renderer](https://github.com/JBlond/php-diff/wiki/3.-Custom-Renderer) +* [Styling](https://github.com/JBlond/php-diff/wiki/4.-Styling) + +## Changelog + +[Changelog](changelog.md) + ## Example Use ```PHP true, 'ignoreCase' => true, 'context' => 2, + 'cliColor' => true, // for cli output + 'ignoreLines' => Diff::DIFF_IGNORE_LINE_BLANK, ]; // Initialize the diff class. -$diff = new Diff($a, $b /*, $options */); +$diff = new Diff($sampleA, $sampleB /*, $options */); // Choose Renderer. $renderer = new SideBySide([ - 'title1' => 'Custom title for OLD version', - 'title2' => 'Custom title for NEW version', + 'title1' => 'Custom title for sample A', + 'title2' => 'Custom title for sample B', ]); -// Show it. +// Show the output of the difference renderer. echo $diff->Render($renderer); + +// Alternative +// Show the differences or a message. +echo $diff->isIdentical() ? 'No differences found.' : '
' . htmlspecialchars($diff->render($renderer)) . '
' ; + ``` ### Example Output -A quick usage example can be found in the `example/` directory and under example.php. + +File `example.php` contains a quick demo and can be found in the `example/` +directory. Included is a light and a dark theme. #### HTML Side By Side Example -![HTML Side By Side Example](htmlSideBySide.png "HTML Side By Side Example") -#### HTML Inline Example -![HTML Inline Example](htmlInline.png "HTML Inline Example") + +![HTML Side By Side Example](assets/htmlSideBySide.png "HTML Side By Side Example") + +
More Example Pictures
+ #### HTML Unified Example -![HTML Unified Example](htmlUnified.png "HTML Unified Example") + +![HTML Unified Example](assets/htmlUnified.png "HTML Unified Example") + #### Text Unified Example -![Text Unified Example](textUnified.png "Text Unified Example") + +![Text Unified Example](assets/textUnified.png "Text Unified Example") + #### Text Context Example -![Text Context Example](textContext.png "Text Context Example") + +![Text Context Example](assets/textContext.png "Text Context Example") + +#### Text Unified Console Example + +![Text Unified Console Example](assets/textUnifiedCli.png "Text Unified Console Example") + +
+ +
HTML Side By Side Dark Theme Example
+ +![HTML Side By Side Dark Theme Example](assets/htmlSidebySideDarkTheme.png "HTML Side By Side Dark Theme Example") + +
## Requirements -* PHP 7.2 or greater +* PHP 7.3 or greater * PHP Multibyte String +* [jblond/php-cli](https://github.com/jblond/php-cli) (suggested) + +## Contribution, Issues and feature requests + +If you found a bug, or have an idea for new functionality, feel free to report +it on the issue tracker - just use search beforehand. +[Issue tracker](https://github.com/JBlond/php-diff/issues) + +You can also fork this repository and open a PR. ## Merge files using jQuery -Xiphe has build a jQuery plugin with that you can merge the compared files. -Have a look at [jQuery-Merge-for-php-diff](https://github.com/Xiphe/jQuery-Merge-for-php-diff). +Xiphe has build a jQuery plugin with that you can merge the compared files. Have +a look +at [jQuery-Merge-for-php-diff](https://github.com/DigiLive/jQuery-Merge-for-php-diff) +. ## Todo -* Ability to ignore blank line changes * 3 way diff support - + ## Contributors Contributors since I forked the repo. -* maxxer -* Creris -* jfcherng -* DigiLive +* [maxxer](https://github.com/maxxer) +* [Creris](https://github.com/Creris) +* [jfcherng](https://github.com/jfcherng) +* [DigiLive](https://github.com/DigiLive) ### License (BSD License) diff --git a/htmlSideBySide.png b/assets/htmlSideBySide.png similarity index 100% rename from htmlSideBySide.png rename to assets/htmlSideBySide.png diff --git a/assets/htmlSidebySideDarkTheme.png b/assets/htmlSidebySideDarkTheme.png new file mode 100644 index 00000000..2e5ede8a Binary files /dev/null and b/assets/htmlSidebySideDarkTheme.png differ diff --git a/assets/htmlUnified.png b/assets/htmlUnified.png new file mode 100644 index 00000000..a7c100f2 Binary files /dev/null and b/assets/htmlUnified.png differ diff --git a/textContext.png b/assets/textContext.png similarity index 100% rename from textContext.png rename to assets/textContext.png diff --git a/textUnified.png b/assets/textUnified.png similarity index 100% rename from textUnified.png rename to assets/textUnified.png diff --git a/assets/textUnifiedCli.png b/assets/textUnifiedCli.png new file mode 100644 index 00000000..88058081 Binary files /dev/null and b/assets/textUnifiedCli.png differ diff --git a/changelog.md b/changelog.md new file mode 100644 index 00000000..a9ef3e4d --- /dev/null +++ b/changelog.md @@ -0,0 +1,216 @@ +# Changelog + +## 2.4.0 (2021-08-23) + +* Add deprecation notice for missing method ([f494b3a](https://github.com/JBlond/php-diff/commit/f494b3a), [9403eba](https://github.com/JBlond/php-diff/commit/9403eba)) +* Add generator for ignored lines ([6b8662e](https://github.com/JBlond/php-diff/commit/6b8662e), [4dec4ad](https://github.com/JBlond/php-diff/commit/4dec4ad)) +* Add method `generateLinesIgnore` ([6ef61bc](https://github.com/JBlond/php-diff/commit/6ef61bc), [75f5ce0](https://github.com/JBlond/php-diff/commit/75f5ce0)) +* Bump library version und update Changelog ([e88dffb](https://github.com/JBlond/php-diff/commit/e88dffb)) +* Document methods ([9699b5b](https://github.com/JBlond/php-diff/commit/9699b5b), [7d973d3](https://github.com/JBlond/php-diff/commit/7d973d3)) +* Document option `ignoreLines` ([0849a1e](https://github.com/JBlond/php-diff/commit/0849a1e), [19634bb](https://github.com/JBlond/php-diff/commit/19634bb)) +* Document update grammar in the comments ([3f44195](https://github.com/JBlond/php-diff/commit/3f44195)) +* Fix PSR-12 code style ([0bf1a08](https://github.com/JBlond/php-diff/commit/0bf1a08)) +* Optimize Sequence renderer ([576830c](https://github.com/JBlond/php-diff/commit/576830c), [bb0eed4](https://github.com/JBlond/php-diff/commit/bb0eed4)) +* Optimize colorize is only used here ([02cf114](https://github.com/JBlond/php-diff/commit/02cf114)) +* Optimize constant usage ([d0cede3](https://github.com/JBlond/php-diff/commit/d0cede3), [3591515](https://github.com/JBlond/php-diff/commit/3591515)) +* Optimize stripping empty/blank lines ([ea6a2e4](https://github.com/JBlond/php-diff/commit/ea6a2e4), [a239f17](https://github.com/JBlond/php-diff/commit/a239f17)) + +## v2.3.3 (2021-08-23) + +* Bump library version und update Changelog ([f5ce6bc](https://github.com/JBlond/php-diff/commit/f5ce6bc)) +* Fix Autoload test classes only in development ([31b4222](https://github.com/JBlond/php-diff/commit/31b4222)) + +## v2.3.2 (2021-03-27) + +* Bump library version und update Changelog ([8a83b39](https://github.com/JBlond/php-diff/commit/8a83b39)) +* Fix [#90](https://github.com/JBlond/php-diff/issues/90) - Merged Diff shows result only partially ([acbfd7d](https://github.com/JBlond/php-diff/commit/acbfd7d)) +* Fix colors ([7eba340](https://github.com/JBlond/php-diff/commit/7eba340)) +* Fix repeating class assignment of line header ([fb32453](https://github.com/JBlond/php-diff/commit/fb32453)) +* Fix title attribute values ([533a6bf](https://github.com/JBlond/php-diff/commit/533a6bf)) + +## v2.3.1 (2021-02-03) + +* Add DigiLive/gitChangelog for change log generation ([0a6a84f](https://github.com/JBlond/php-diff/commit/0a6a84f)) +* Bump library version ([013f862](https://github.com/JBlond/php-diff/commit/013f862)) +* Cut HTML Unified Renderer ([1ba255f](https://github.com/JBlond/php-diff/commit/1ba255f)) +* Document PhpUnit Similarity Test ([7ec484c](https://github.com/JBlond/php-diff/commit/7ec484c)) +* Document Update Changelog ([28e1dc0](https://github.com/JBlond/php-diff/commit/28e1dc0), [c9881d3](https://github.com/JBlond/php-diff/commit/c9881d3)) +* Document disabled inspection ([909e195](https://github.com/JBlond/php-diff/commit/909e195)) +* Document generateLinesEqual() ([8a193c9](https://github.com/JBlond/php-diff/commit/8a193c9)) +* Document methods ([94c8bd5](https://github.com/JBlond/php-diff/commit/94c8bd5)) +* Fix [#83](https://github.com/JBlond/php-diff/issues/83) - Lines not properly marked ([6fcafe1](https://github.com/JBlond/php-diff/commit/6fcafe1)) +* Fix HTML Merged Renderer ([07da484](https://github.com/JBlond/php-diff/commit/07da484)) +* Fix constructor DocBlocks ([b13ff84](https://github.com/JBlond/php-diff/commit/b13ff84)) +* Fix html syntax error ([11ec623](https://github.com/JBlond/php-diff/commit/11ec623)) +* Fix namespace and unused code ([77a7b59](https://github.com/JBlond/php-diff/commit/77a7b59)) +* Fix probably undefined variable ([3954a2b](https://github.com/JBlond/php-diff/commit/3954a2b)) +* Fix property visibility and method docBlocks ([3bc0839](https://github.com/JBlond/php-diff/commit/3bc0839)) +* Fix property visibility and unused code ([34a032f](https://github.com/JBlond/php-diff/commit/34a032f)) +* Fix redundant and unused code. ([73f6776](https://github.com/JBlond/php-diff/commit/73f6776)) +* add phpunit config file ([7382ee8](https://github.com/JBlond/php-diff/commit/7382ee8)) + +## v2.3.0 (2020-11-19) + +* Add Html Merged renderer. ([d70eaf6](https://github.com/JBlond/php-diff/commit/d70eaf6)) +* Add PhpUnit test for html merged renderer ([4512c03](https://github.com/JBlond/php-diff/commit/4512c03)) +* Add calculation for similarity ratio. ([3e4bbe6](https://github.com/JBlond/php-diff/commit/3e4bbe6)) +* Add choosing marking levels to html example ([c27035a](https://github.com/JBlond/php-diff/commit/c27035a)) +* Add new marking levels for inline differences ([75358da](https://github.com/JBlond/php-diff/commit/75358da)) +* Document properties and constructor ([6c95ccd](https://github.com/JBlond/php-diff/commit/6c95ccd)) +* Fix: Html SideBySide renders equal lines of version 1 at both sides. ([ccfc465](https://github.com/JBlond/php-diff/commit/ccfc465)) +* Fix Merged::generateLinesReplace() ([cef85b5](https://github.com/JBlond/php-diff/commit/cef85b5)) +* Fix PSR-4 Auto loading Typo ([6e2ad47](https://github.com/JBlond/php-diff/commit/6e2ad47)) +* Fix PhpUnit test ([3ccaa10](https://github.com/JBlond/php-diff/commit/3ccaa10)) +* Fix Undefined offset notice ([b10fd38](https://github.com/JBlond/php-diff/commit/b10fd38)) +* Fixes [#64](https://github.com/JBlond/php-diff/issues/64) - maxLineMarkerWidth only calculated for input format plain. ([c5f6d72](https://github.com/JBlond/php-diff/commit/c5f6d72)) +* Fix generateBlockHeader docBlocks ([b5cfbd5](https://github.com/JBlond/php-diff/commit/b5cfbd5)) +* Fix visibility of removed lines ([ec0918b](https://github.com/JBlond/php-diff/commit/ec0918b)) +* add changelog ([4b7a56f](https://github.com/JBlond/php-diff/commit/4b7a56f)) +* add changelog to Readme file ([09aea70](https://github.com/JBlond/php-diff/commit/09aea70)) +* add date ([c64c0cc](https://github.com/JBlond/php-diff/commit/c64c0cc)) +* add declaration ([cff7db1](https://github.com/JBlond/php-diff/commit/cff7db1)) +* add wiki links to README.md ([ccd5a6d](https://github.com/JBlond/php-diff/commit/ccd5a6d)) + +## v2.2.1 (2020-08-06) + +* Fix [#58](https://github.com/JBlond/php-diff/issues/58) - Side by side diff shows empty diff ([0946d59](https://github.com/JBlond/php-diff/commit/0946d59), [369b146](https://github.com/JBlond/php-diff/commit/369b146), [02695d5](https://github.com/JBlond/php-diff/commit/02695d5)) +* add to dev for unit tests ([acd12cb](https://github.com/JBlond/php-diff/commit/acd12cb)) + +## v2.2.0 (2020-07-23) + +* Fix code quality. ([0ef6def](https://github.com/JBlond/php-diff/commit/0ef6def)) +* add line for readabiltity ([e28511b](https://github.com/JBlond/php-diff/commit/e28511b)) + +## v2.1.1 (2020-07-17) + +* Fix [#50](https://github.com/JBlond/php-diff/issues/50). ([47d6288](https://github.com/JBlond/php-diff/commit/47d6288)) +* add unit test for cli output ([0c75757](https://github.com/JBlond/php-diff/commit/0c75757)) + +## v2.1.0 (2020-07-13) + +* add plain output for cli ([d7bbe12](https://github.com/JBlond/php-diff/commit/d7bbe12)) + +## v2.0.0 (2020-07-09) + +* Add Cli color support ([4192d8b](https://github.com/JBlond/php-diff/commit/4192d8b)) +* Add dark theme example ([6f41894](https://github.com/JBlond/php-diff/commit/6f41894)) +* Add example picture ([ee37a28](https://github.com/JBlond/php-diff/commit/ee37a28)) +* add composer scripts descriptions. Update key words ([6bfd4f9](https://github.com/JBlond/php-diff/commit/6bfd4f9)) +* add missing tag from merge ([639f3cc](https://github.com/JBlond/php-diff/commit/639f3cc)) + +## v1.18 (2020-07-01) + +* add author ([e132cdb](https://github.com/JBlond/php-diff/commit/e132cdb)) +* add dark theme example ([b9d0ef6](https://github.com/JBlond/php-diff/commit/b9d0ef6)) + +## v1.17 (2020-06-08) + +* Fix issue [#32](https://github.com/JBlond/php-diff/issues/32). ([7ef67e6](https://github.com/JBlond/php-diff/commit/7ef67e6)) +* fix typo in phpdoc ([db259fc](https://github.com/JBlond/php-diff/commit/db259fc)) + +## v1.16 (2020-03-02) + +* Add composer package PHP Mess Detector v2.* ([3e527d1](https://github.com/JBlond/php-diff/commit/3e527d1)) +* Add contributor to author lists. ([4c2cbb7](https://github.com/JBlond/php-diff/commit/4c2cbb7), [c11b4ba](https://github.com/JBlond/php-diff/commit/c11b4ba)) +* Add trimEqual option. ([98d993e](https://github.com/JBlond/php-diff/commit/98d993e)) +* Add types of elements for renderer ([4d1b4a0](https://github.com/JBlond/php-diff/commit/4d1b4a0), [83b4104](https://github.com/JBlond/php-diff/commit/83b4104)) +* Fix PHPMD Violation. ([5d03eae](https://github.com/JBlond/php-diff/commit/5d03eae)) +* Fix expected value for HtmlRendererTest::testUnified() ([fbda2bd](https://github.com/JBlond/php-diff/commit/fbda2bd)) + +## v1.15 (2020-01-24) + +* fix notation ([795fe20](https://github.com/JBlond/php-diff/commit/795fe20)) + +## v1.14 (2019-12-03) + +* No changes. + +## v1.13 (2019-10-08) + +* No changes. + +## v1.12 (2019-03-18) + +* add more files to .gitignore ([27b21eb](https://github.com/JBlond/php-diff/commit/27b21eb)) +* add phpunit test to composer ([e8a3f71](https://github.com/JBlond/php-diff/commit/e8a3f71)) +* add tests ([6d165a6](https://github.com/JBlond/php-diff/commit/6d165a6)) + +## v1.11 (2019-02-22) + +* No changes. + +## v1.10 (2019-02-20) + +* fix codacy warnings of unused functions ([8037d99](https://github.com/JBlond/php-diff/commit/8037d99)) +* fix example ([9207f73](https://github.com/JBlond/php-diff/commit/9207f73)) + +## v1.9 (2019-02-19) + +* add comment like in the other file ([8b68a5d](https://github.com/JBlond/php-diff/commit/8b68a5d)) +* add stronger type hinting ([3a6ef42](https://github.com/JBlond/php-diff/commit/3a6ef42)) + +## v1.8 (2019-02-13) + +* fix test ([b4cfce1](https://github.com/JBlond/php-diff/commit/b4cfce1)) + +## v1.7 (2019-01-19) + +* add code sniffer PSR2 file ([6fa3c76](https://github.com/JBlond/php-diff/commit/6fa3c76)) +* adding curly brackets ([148e787](https://github.com/JBlond/php-diff/commit/148e787)) +* fix PSR1.Files.SideEffects.FoundWithSymbols ([fe21917](https://github.com/JBlond/php-diff/commit/fe21917)) +* fix tabs ([354bf5c](https://github.com/JBlond/php-diff/commit/354bf5c)) + +## v1.6 (2019-01-19) + +* Fix warning with PHP 7.2 when trying to count NULL ([fe69c4f](https://github.com/JBlond/php-diff/commit/fe69c4f)) +* add ci file ([add8165](https://github.com/JBlond/php-diff/commit/add8165)) +* add dock block ([2aafad1](https://github.com/JBlond/php-diff/commit/2aafad1)) +* add unit tests ([0db511d](https://github.com/JBlond/php-diff/commit/0db511d)) +* fix tests ([44a6ab0](https://github.com/JBlond/php-diff/commit/44a6ab0)) + +## v1.5 (2019-01-15) + +* No changes. + +## v1.4 (2019-01-14) + +* Add PSR4 autoloader ([bda1da9](https://github.com/JBlond/php-diff/commit/bda1da9)) +* add badge ([0fac082](https://github.com/JBlond/php-diff/commit/0fac082)) +* add keywords ([e64716d](https://github.com/JBlond/php-diff/commit/e64716d)) +* add name spacing ([1d15164](https://github.com/JBlond/php-diff/commit/1d15164)) + +## v1.3 (2019-01-11) + +* Fixed lengths of functions ([3591789](https://github.com/JBlond/php-diff/commit/3591789)) +* Fix some typos ([5ca2257](https://github.com/JBlond/php-diff/commit/5ca2257)) +* added missing doc block ([c6f3745](https://github.com/JBlond/php-diff/commit/c6f3745)) +* add lang to html ([1623626](https://github.com/JBlond/php-diff/commit/1623626)) +* add second image ([176b647](https://github.com/JBlond/php-diff/commit/176b647)) + +## v1.2 (2018-01-23) + +* added example code from https://github.com/JBlond/php-diff/issues/1 ([258b976](https://github.com/JBlond/php-diff/commit/258b976)) +* add image to README.md ([0432f78](https://github.com/JBlond/php-diff/commit/0432f78)) + +## v1.1 (2017-05-05) + +* Add ability not to expand tabs ([f5da126](https://github.com/JBlond/php-diff/commit/f5da126)) +* Added note about https://github.com/Xiphe/jQuery-Merge-for-php-diff ([2ebc51f](https://github.com/JBlond/php-diff/commit/2ebc51f)) +* Add in working ignoreWhitespace and ignoreCase options (self-describing), fix up an issue where a diff of two files exactly the same would show the last $context lines, general cleanup ([690419d](https://github.com/JBlond/php-diff/commit/690419d)) +* Add mbstring extension as package dependency ([a929467](https://github.com/JBlond/php-diff/commit/a929467)) +* Add missing docblock. Rename isLineDifferent to linesAreDifferent ([516c4be](https://github.com/JBlond/php-diff/commit/516c4be)) +* Fix ' ([c81931f](https://github.com/JBlond/php-diff/commit/c81931f)) +* Fix an issue with insertions being skipped. ([b13d23d](https://github.com/JBlond/php-diff/commit/b13d23d)) +* Fix links ([2c38d0e](https://github.com/JBlond/php-diff/commit/2c38d0e)) +* Fix tab expansion and deprecated preg_replace use on fixSpaces. ([f0aba03](https://github.com/JBlond/php-diff/commit/f0aba03)) +* Fix the ignoring of option context ([60de296](https://github.com/JBlond/php-diff/commit/60de296)) +* add composer file ([be8dc58](https://github.com/JBlond/php-diff/commit/be8dc58)) +* added ' ([09d0c4c](https://github.com/JBlond/php-diff/commit/09d0c4c)) +* added License ([3b5b338](https://github.com/JBlond/php-diff/commit/3b5b338)) +* added widget ([d1a5e18](https://github.com/JBlond/php-diff/commit/d1a5e18)) +* adding composer manifest to distribute as a library ([9083bd6](https://github.com/JBlond/php-diff/commit/9083bd6)) +* add missing doc blocks ([d3b9a63](https://github.com/JBlond/php-diff/commit/d3b9a63)) + +## v1.0 (2010-03-11) + +* No changes. diff --git a/composer.json b/composer.json index 818e62eb..59c9d4c4 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,12 @@ "license": "BSD-3-Clause", "keywords": [ "php", - "diff" + "diff", + "side-by-sidediff", + "unified", + "udiff", + "unidiff", + "unified diff" ], "authors": [ { @@ -22,26 +27,55 @@ } ], "require": { - "php": ">=7.2", - "ext-mbstring": "*" + "php": ">=7.3", + "ext-mbstring": "*", + "ext-pcre": "*", + "symfony/polyfill-mbstring": "^1" }, "require-dev": { - "phpunit/phpunit": "8.*", + "phpunit/phpunit": "^8 || ^9", "squizlabs/php_codesniffer": "*", - "phpmd/phpmd": "2.*" + "phpmd/phpmd": "2.*", + "jblond/php-cli": "^1.0", + "digilive/git-changelog": "^2" + }, + "suggest": { + "ext-json": "*", + "jblond/php-cli": "^1.0" }, "autoload": { "psr-4": { "jblond\\": "lib/jblond" } }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests" + } + }, "config": { "classmap-authoritative": true }, "scripts": { - "phpunit": "phpunit ./tests/", + "phpunit": "phpunit", "php_src": "phpcs --standard=phpcs.xml -s -p --colors ./lib/", "php_test": "phpcs --standard=phpcs.xml -s -p --colors ./tests/", - "phpmd": "phpmd ./ ansi cleancode,codesize,controversial,design,naming,unusedcode --exclude vendor" + "phpmd": "phpmd ./ ansi cleancode,codesize,controversial,design,naming,unusedcode --exclude vendor", + "changelog": "php generateChangelog.php", + "test": [ + "@php_src", + "@php_test", + "phpunit --colors=always --testdox" + ], + "coverage": "phpunit --colors=always --coverage-html codeCoverage" + }, + "scripts-descriptions": { + "phpunit": "Run PHPUnit tests", + "php_src": "Run code sniffer on lib directory", + "php_test": "Run code sniffer on tests directory", + "phpmd": "Run php mess detector", + "changelog": "generate changelog from commits", + "test": "Run code formatting test and phpunit", + "coverage": "Run phpunit code overage" } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..c4a37646 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +# For all options see +# https://dockerfile.readthedocs.io/en/latest/content/DockerImages/dockerfiles/php-nginx-dev.html +services: + php: + image: webdevops/php-nginx-dev:8.2 + container_name: diff + working_dir: /app + environment: + - WEB_DOCUMENT_ROOT=/app/example + - WEB_DOCUMENT_INDEX=example.php + - PHP_DISPLAY_ERRORS=1 + - PHP_MEMORY_LIMIT=2048M + - PHP_MAX_EXECUTION_TIME=-1 + - PHP_DATE_TIMEZONE=Europe/Berlin + - XDEBUG_IDE_KEY=PHPSTORM + ports: + - "88:80" + - "9000:9000" + volumes: + - ./:/app:rw,cached diff --git a/example/a.txt b/example/a.txt index eab1ced5..9dddb84c 100644 --- a/example/a.txt +++ b/example/a.txt @@ -8,7 +8,7 @@

This line is removed from version2.

This line is also removed from version2.

This line is the same for both versions.

-

This line has inline differences between both versions.

+

this line has inline differences between both versions.

This line is the same for both versions.

This line also has inline differences between both versions.

This line is the same for both versions.

diff --git a/example/cli.php b/example/cli.php new file mode 100644 index 00000000..6c456acd --- /dev/null +++ b/example/cli.php @@ -0,0 +1,75 @@ +'; + echo 'Please execute it from a cli interpreter.'; + throw new RuntimeException('Script for CLI use only!'); +} + + +// Include and instantiate autoloader. +require '../vendor/autoload.php'; + +// Include two sample files for comparison. +$sampleA = file_get_contents(__DIR__ . '/a.txt'); +$sampleB = file_get_contents(__DIR__ . '/b.txt'); + +$customOptions = [ + 'context' => 2, + 'trimEqual' => false, + 'ignoreWhitespace' => true, + 'ignoreCase' => true, +]; + +// Choose one of the initializations. +$diff = new Diff($sampleA, $sampleB); +//$diff = new Diff($a, $b, $customOptions); // Initialize the diff class with custom options. + +// Instantiate Cli wrapper +$cli = new Cli(); + +// Generate a unified diff. +$renderer = new UnifiedCli(); +echo "-= Unified Default =-\n\n"; +$cli->output($diff->render($renderer)); + +echo "\n\n-= Unified Colored =-\n\n"; + +$renderer = new UnifiedCli( +// Define renderer options. + [ + 'cliColor' => true, + ] +); + +$cli->output($diff->render($renderer)); + + +// Generate an inline diff. +$renderer = new InlineCli( +// Define renderer options. + [ + 'deleteMarkers' => ['-', '-'], + 'insertMarkers' => ['+', '+'], + 'equalityMarkers' => ['=', 'x'], + ] +); +echo "-= Inline Marked =-\n\n"; +$cli->output($diff->render($renderer)); + +echo "-= Inline Colored =-\n\n"; + +$coloredRenderer = new InlineCli( +// Define renderer options. + [ + 'cliColor' => true, + ] +); + +$cli->output($diff->render($coloredRenderer)); diff --git a/example/dark-theme.css b/example/dark-theme.css index 1c0408bb..7fa98403 100644 --- a/example/dark-theme.css +++ b/example/dark-theme.css @@ -1,13 +1,13 @@ body { background: #3A3B46; - color: #F8F8F2; + color: #F8F8F2; font-family: Arial, serif; font-size: 12px; } pre { - width: 100%; overflow: auto; + width: 100%; } a, a:visited { @@ -19,75 +19,77 @@ a, a:visited { */ .Differences { - width: 100%; border-collapse: collapse; border-spacing: 0; empty-cells: show; + width: 100%; } .Differences thead th { - text-align: left; - border-bottom: 1px solid #000000; background: #AAAAAA; + border-bottom: 1px solid #000000; color: #000000; padding: 4px; + text-align: left; } .Differences tbody th { - text-align: right; background: #AAAAAA; - color: #272822; - width: 4em; - padding: 1px 2px; border-right: 1px solid #000000; - vertical-align: top; + color: #272822; font-size: 13px; + padding: 1px 2px; + text-align: right; + vertical-align: top; + width: 4em; } .Differences td { - padding: 1px 2px; font-family: Consolas, monospace; font-size: 13px; + padding: 1px 2px; } .Differences .Skipped { background: #F7F7F7; + color: #000000; + display: block; +} + +.Differences ins, +.Differences del { + text-decoration: none; } /* * HTML Side by Side Diff */ .DifferencesSideBySide .ChangeInsert td.Left { - background: green; + background: #DDFFDD; } .DifferencesSideBySide .ChangeInsert td.Right { - background: green; + background: #008000; } .DifferencesSideBySide .ChangeDelete td.Left { background: #FF8888; - color: #272822; + color: #272822; } .DifferencesSideBySide .ChangeDelete td.Right { background: #FFAAAA; - color: #272822; + color: #272822; } .DifferencesSideBySide .ChangeReplace .Left { - background: #FFEE99; - color: #272822; + background: #FFDD88; + color: #272822; } .DifferencesSideBySide .ChangeReplace .Right { background: #FFDD88; - color: #272822; -} - -.Differences ins, -.Differences del { - text-decoration: none; + color: #272822; } .DifferencesSideBySide .ChangeReplace ins, @@ -95,92 +97,94 @@ a, a:visited { background: #EEBB00; } +.DifferencesSideBySide .ChangeIgnore .Left, +.DifferencesSideBySide .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesSideBySide .ChangeIgnore .Left.Ignore { + background: #4B4C57; +} + +.DifferencesSideBySide .ChangeIgnore .Right.Ignore { + background: #4B4C57; +} + /* - * HTML Inline Diff + * HTML Unified Diff */ -.DifferencesInline .ChangeReplace { +.DifferencesUnified .ChangeReplace { color: #272822; } -.DifferencesInline .ChangeReplace .Left, -.DifferencesInline .ChangeDelete .Left { +.DifferencesUnified .ChangeReplace .Left, +.DifferencesUnified .ChangeDelete .Left { background: #FFDDDD; - color: #272822; + color: #272822; } -.DifferencesInline .ChangeReplace .Right, -.DifferencesInline .ChangeInsert .Right { +.DifferencesUnified .ChangeReplace .Right, +.DifferencesUnified .ChangeInsert .Right { background: #DDFFDD; + color: #272822; } -.DifferencesInline .ChangeReplace ins { - background: green; -} - -.DifferencesInline .ChangeReplace del { - background: #EE9999; - color: #272822; -} - -/* - * HTML Unified Diff - */ - -/* Line removed in new */ -.DifferencesUnified .ChangeDelete .Left::before { - content: "- \00a0"; +.DifferencesUnified .ChangeReplace ins { + background: #008000; + color: #272822; } -.DifferencesUnified .ChangeDelete .Left { +.DifferencesUnified .ChangeReplace del { background: #EE9999; - color: #272822; + color: #272822; } -/* Line modified in old and new */ -.DifferencesUnified .ChangeReplace { - background: #FFEE99; - color: #272822; - display: table; +.DifferencesUnified .ChangeIgnore .Left, +.DifferencesUnified .ChangeIgnore .Right { + background: #FBF2BF; } -/* Line in old replaced by line in new */ -.DifferencesUnified .ChangeReplace .Left:first-child:before { - content: "\250C \00a0"; +.DifferencesUnified .ChangeIgnore .Left.Ignore { + background: #4B4C57; } -.DifferencesUnified .ChangeReplace .Left:before { - content: "\251C \00a0"; +.DifferencesUnified .ChangeIgnore .Right.Ignore { + background: #4B4C57; } -.DifferencesUnified .ChangeReplace .Left { - background: #FFEE99; +/* + * HTML Merged Diff + */ +.DifferencesMerged td.ChangeReplace { + background: #FFDD88; + color: #272822; } -/* Line in new replaced line in old */ -.DifferencesUnified .ChangeReplace .Right:last-of-type:before { - content: "\2514 \00a0"; +.DifferencesMerged .ChangeDelete { + background: #FFDDDD; + color: #272822; } -.DifferencesUnified .ChangeReplace .Right:before { - content: "\251C \00a0"; +.DifferencesMerged .ChangeInsert { + background: #DDFFDD; + color: #272822; } -.DifferencesUnified .ChangeReplace .Right { - background: #FFEE99; +.DifferencesMerged .ChangeReplace ins { + background: #008000; + color: #272822; } -/* Line inserted in new */ -.DifferencesUnified .ChangeInsert .Right:before { - content: "+ \00A0"; +.DifferencesMerged .ChangeReplace del { + background: #EE9999; + color: #272822; } -/* Character inserted in line of new */ -.DifferencesUnified .ChangeReplace ins { - background: #99EE99; +.DifferencesMerged th.ChangeDelete { + background-image: linear-gradient(-45deg, #AAAAAA 0%, #EE9999 100%); } -/* Character removed from line in old */ -.DifferencesUnified .ChangeReplace del { - background: #EE9999; +.DifferencesMerged th.ChangeReplace { + background-image: linear-gradient(-45deg, #CCCCCC 0%, #FFDD88 100%); } diff --git a/example/example.php b/example/example.php index 681f6869..8b8b6cd8 100644 --- a/example/example.php +++ b/example/example.php @@ -1,105 +1,123 @@ 2, 'trimEqual' => false, 'ignoreWhitespace' => true, 'ignoreCase' => true, + 'ignoreLines' => Diff::DIFF_IGNORE_LINE_EMPTY, ]; // Choose one of the initializations. -$diff = new Diff($a, $b); // Initialize the diff class with default options. -//$diff = new Diff($a, $b, $customOptions); // Initialize the diff class with custom options. -?> +$diff = new Diff($sampleA, $sampleB); // Initialize the diff class with default options. +//$diff = new Diff($sampleA, $sampleB, $diffOptions); // Initialize the diff class with custom options. + +// Options for rendering the diff. +$rendererOptions = [ + 'inlineMarking' => $_GET['inlineMarking'] ?? Diff\Renderer\MainRenderer::CHANGE_LEVEL_LINE, +] +?> + - - - PHP LibDiff - Examples - - + + +

PHP LibDiff - Examples

+ +
+ +
+ +
+ +

HTML Side by Side Diff

- var oldLink = document.getElementsByTagName("link").item(cssLinkIndex); - - var newLink = document.createElement("link"); - newLink.setAttribute("rel", "stylesheet"); - newLink.setAttribute("type", "text/css"); - newLink.setAttribute("href", cssFile); - - document.getElementsByTagName("head").item(0).replaceChild(newLink, oldLink); - } - - - -

PHP LibDiff - Examples

- -
- -

HTML Side by Side Diff

- - 'Custom title for version1', - 'title2' => 'Custom title for version2', - ]); - echo $diff->Render($renderer); - ?> - -

HTML Inline Diff

- - isIdentical() ? 'No differences found.' : $diff->Render($renderer); +?> - // Generate an inline diff. - // \jblond\Diff\Renderer\Html - $renderer = new Inline(); - echo $diff->render($renderer); - ?> +

HTML Unified Diff

+isIdentical() ? 'No differences found.' : $diff->Render($renderer); +?> -

HTML Unified Diff

- {$diff->render($renderer)}"; - ?> +

HTML Merged Diff

+isIdentical() ? 'No differences found.' : $diff->Render($renderer); +?> -

Text Unified Diff

- ' . htmlspecialchars($diff->render($renderer)) . ''; - ?> +

Text Unified Diff

+isIdentical() ? + 'No differences found.' : '
' . htmlspecialchars($diff->render($renderer)) . '
'; +?> -

Text Context Diff

- ' . htmlspecialchars($diff->render($renderer)) . ''; - ?> - +

Text Context Diff

+isIdentical() ? + 'No differences found.' : '
' . htmlspecialchars($diff->render($renderer)) . '
'; +?> + diff --git a/example/styles.css b/example/styles.css index afaa8586..23c68398 100644 --- a/example/styles.css +++ b/example/styles.css @@ -1,12 +1,13 @@ body { background: #FFFFFF; + color: #000000; font-family: Arial, serif; font-size: 12px; } pre { - width: 100%; overflow: auto; + width: 100%; } /* @@ -14,38 +15,40 @@ pre { */ .Differences { - width: 100%; border-collapse: collapse; border-spacing: 0; empty-cells: show; + width: 100%; } .Differences thead th { - text-align: left; - border-bottom: 1px solid #000000; background: #AAAAAA; + border-bottom: 1px solid #000000; color: #000000; padding: 4px; + text-align: left; } .Differences tbody th { - text-align: right; background: #CCCCCC; - width: 4em; - padding: 1px 2px; border-right: 1px solid #000000; - vertical-align: top; font-size: 13px; + padding: 1px 2px; + text-align: right; + vertical-align: top; + width: 4em; } .Differences td { - padding: 1px 2px; - font-family: Consolas, monospace; - font-size: 13px; + font-family: Consolas, monospace; + font-size: 13px; + padding: 1px 2px; + vertical-align: top; } .Differences .Skipped { background: #F7F7F7; + display: block; } /* @@ -75,6 +78,19 @@ pre { background: #FFDD88; } +.DifferencesSideBySide .ChangeIgnore .Left, +.DifferencesSideBySide .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesSideBySide .ChangeIgnore .Left.Ignore { + background: #F7F7F7; +} + +.DifferencesSideBySide .ChangeIgnore .Right.Ignore { + background: #F7F7F7; +} + .Differences ins, .Differences del { text-decoration: none; @@ -86,82 +102,66 @@ pre { } /* - * HTML Inline Diff + * HTML Unified Diff */ -.DifferencesInline .ChangeReplace .Left, -.DifferencesInline .ChangeDelete .Left { +.DifferencesUnified .ChangeReplace .Left, +.DifferencesUnified .ChangeDelete .Left { background: #FFDDDD; } -.DifferencesInline .ChangeReplace .Right, -.DifferencesInline .ChangeInsert .Right { +.DifferencesUnified .ChangeReplace .Right, +.DifferencesUnified .ChangeInsert .Right { background: #DDFFDD; } -.DifferencesInline .ChangeReplace ins { +.DifferencesUnified .ChangeReplace ins { background: #99EE99; } -.DifferencesInline .ChangeReplace del { +.DifferencesUnified .ChangeReplace del { background: #EE9999; } -/* - * HTML Unified Diff - */ - -/* Line removed in new */ -.DifferencesUnified .ChangeDelete .Left::before { - content: "- \00a0"; -} - -.DifferencesUnified .ChangeDelete .Left { - background: #CCCCCC; +.DifferencesUnified .ChangeIgnore .Left, +.DifferencesUnified .ChangeIgnore .Right { + background: #FBF2BF; } -/* Line modified in old and new */ -.DifferencesUnified .ChangeReplace { - display: table; - background: #FFEE99; -} - -/* Line in old replaced by line in new */ -.DifferencesUnified .ChangeReplace .Left:first-child:before { - content: "\250C \00a0"; +.DifferencesUnified .ChangeIgnore .Left.Ignore { + background: #F7F7F7; } -.DifferencesUnified .ChangeReplace .Left:before { - content: "\251C \00a0"; +.DifferencesUnified .ChangeIgnore .Right.Ignore { + background: #F7F7F7; } -.DifferencesUnified .ChangeReplace .Left { - background: #FFEE99; +/* + * HTML Merged Diff + */ +.DifferencesMerged td.ChangeReplace { + background: #FFDD88; } -/* Line in new replaced line in old */ -.DifferencesUnified .ChangeReplace .Right:last-of-type:before { - content: "\2514 \00a0"; +.DifferencesMerged .ChangeDelete { + background: #FFDDDD; } -.DifferencesUnified .ChangeReplace .Right:before { - content: "\251C \00a0"; +.DifferencesMerged .ChangeInsert { + background: #DDFFDD; } -.DifferencesUnified .ChangeReplace .Right { - background: #FFEE99; +.DifferencesMerged .ChangeReplace ins { + background: #99EE99; } -/* Line inserted in new */ -.DifferencesUnified .ChangeInsert .Right:before { - content: "+ \00A0"; +.DifferencesMerged .ChangeReplace del { + background: #EE9999; } -/* Character inserted in line of new */ -.DifferencesUnified .ChangeReplace ins { - background: #99EE99; +.DifferencesMerged th.ChangeDelete { + background-image: linear-gradient(-45deg, #CCCCCC 0%, #EE9999 100%); } -/* Character removed from line in old */ -.DifferencesUnified .ChangeReplace del { - background: #EE9999; +.DifferencesMerged th.ChangeReplace { + background-image: linear-gradient(-45deg, #CCCCCC 0%, #FFDD88 100%); } diff --git a/generateChangelog.php b/generateChangelog.php new file mode 100644 index 00000000..f834c5b7 --- /dev/null +++ b/generateChangelog.php @@ -0,0 +1,30 @@ + '2.4.0', + 'headTagDate' => '2021-08-23', + 'titleOrder' => 'ASC', +]; +$changelogLabels = ['Add', 'Cut', 'Fix', 'Bump', 'Document','Optimize']; + + +$changeLog = new MarkDown(); +$changeLog->setUrl('commit', 'https://github.com/JBlond/php-diff/commit/{commit}'); +$changeLog->setUrl('issue', 'https://github.com/JBlond/php-diff/issues/{issue}'); + +try { + $changeLog->setOptions($changelogOptions); +} catch (Exception $exception) { + echo $exception->getMessage(); +} +$changeLog->setLabels(...$changelogLabels); +try { + $changeLog->build(); +} catch (Exception $exception) { + echo $exception->getMessage(); +} +file_put_contents('changelog.md', $changeLog->get()); diff --git a/htmlInline.png b/htmlInline.png deleted file mode 100644 index ab141c32..00000000 Binary files a/htmlInline.png and /dev/null differ diff --git a/htmlUnified.png b/htmlUnified.png deleted file mode 100644 index bc63c8cd..00000000 Binary files a/htmlUnified.png and /dev/null differ diff --git a/lib/Autoloader.php b/lib/Autoloader.php deleted file mode 100644 index d2820528..00000000 --- a/lib/Autoloader.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @copyright (c) 2015 Mario Brandt - * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 - * @link https://github.com/JBlond/php-diff - */ -class Autoloader -{ - - /** - * Constructor. - * - * A function is registered as an __autoload() implementation to include and evaluate the class file when this class - * is called. - */ - public function __construct() - { - spl_autoload_register(function ($class) { - $class = str_replace('\\', '/', $class); // revert path for old PHP on Linux - $dir = str_replace('\\', '/', __DIR__); - if (file_exists($dir . '/' . $class . '.php')) { - /** @noinspection PhpIncludeInspection */ - require_once $dir . '/' . $class . '.php'; - } - }); - } -} diff --git a/lib/jblond/Diff.php b/lib/jblond/Diff.php index 4a77269f..adeccfd4 100644 --- a/lib/jblond/Diff.php +++ b/lib/jblond/Diff.php @@ -5,12 +5,9 @@ namespace jblond; use InvalidArgumentException; -use jblond\Diff\Renderer\Html\Inline; -use jblond\Diff\Renderer\Html\SideBySide; -use jblond\Diff\Renderer\Html\Unified as UnifiedHtml; -use jblond\Diff\Renderer\Text\Context; -use jblond\Diff\Renderer\Text\Unified; +use jblond\Diff\ConstantsInterface; use jblond\Diff\SequenceMatcher; +use jblond\Diff\Similarity; use OutOfRangeException; /** @@ -19,18 +16,18 @@ * A comprehensive library for comparing two strings and generating the differences between them in multiple formats. * (unified, side by side, inline, HTML, etc.) * - * PHP version 7.2 or greater + * PHP version 7.3 or greater * - * @package jblond - * @author Chris Boulton - * @author Mario Brandt - * @author Ferry Cools - * @copyright (c) 2009 Chris Boulton - * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 - * @link https://github.com/JBlond/php-diff + * @package jblond + * @author Chris Boulton + * @author Mario Brandt + * @author Ferry Cools + * @copyright (c) 2020 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff */ -class Diff +class Diff implements ConstantsInterface { /** * @var array The first version to compare. @@ -50,29 +47,41 @@ class Diff private $groupedCodes; /** - * @var array Associative array containing the default options available for the diff class and their default - * value. - * - context The amount of lines to include around blocks that differ. - * - trimEqual Strip blocks of equal lines from the start and end of the text. - * - ignoreWhitespace When true, tabs and spaces are ignored while comparing. - * The spacing of version1 is leading. - * - ignoreCase When true, character casing is ignored while comparing. - * The casing of version1 is leading. + * @var array Associative array containing the default options available for the diff class and their default value. + * + * - context The amount of lines to include around blocks that differ. + * - trimEqual Strip blocks of equal lines from the start and end of the text. + * - ignoreWhitespace True to ignore differences in tabs and spaces. + * - ignoreCase True to ignore differences in character casing. + * - ignoreLines 0: None. + * 1: Ignore empty lines. + * 2: Ignore blank lines. */ private $defaultOptions = [ 'context' => 3, 'trimEqual' => true, 'ignoreWhitespace' => false, 'ignoreCase' => false, + 'ignoreLines' => self::DIFF_IGNORE_LINE_NONE, ]; /** * @var array Associative array containing the options that will be applied for generating the diff. * The key-value pairs are set at the constructor of this class. + * * @see Diff::setOptions() */ private $options = []; + /** + * @var bool True when compared versions are identical, False otherwise. + */ + private $identical; + /** + * @var float Similarity ratio of the two sequences. + */ + private $similarity; + /** * The constructor. * @@ -80,14 +89,14 @@ class Diff * The values can be of type string or array. * If the type is string, it's split into array elements by line-end characters. * - * Options for comparison can be set by using the third parameter. The format of this value is expected to be a + * Options for comparison can be set by using the third parameter. The format of this value is expected to be an * associative array where each key-value pair represents an option and its value (E.g. ['context' => 3], ...). * When a keyName matches the name of a default option, that option's value will be overridden by the key's value. * Any other keyName (and it's value) can be added as an option, but will not be used if not implemented. * - * @param string|array $version1 Data to compare to. - * @param string|array $version2 Data to compare. - * @param array $options User defined option values. + * @param string|array $version1 Data to compare to. + * @param string|array $version2 Data to compare. + * @param array $options User defined option values. * * @see Diff::$defaultOptions * @@ -103,16 +112,16 @@ public function __construct($version1, $version2, array $options = []) } /** - * Get the type of a variable. + * Get the kind of variable. * * The return value depend on the type of variable: * 0 If the type is 'array' * 1 if the type is 'string' * - * @param mixed $var Variable to get type from. + * @param mixed $var Variable to get type from. * - * @return int Number indicating the type of the variable. 0 for array type and 1 for string type. - * @throws InvalidArgumentException When the type isn't 'array' or 'string'. + * @return int Number indicating the type of the variable. 0 for array type and 1 for string type. + * @throws InvalidArgumentException When the type isn't 'array' or 'string'. * */ public function getArgumentType($var): int @@ -130,7 +139,7 @@ public function getArgumentType($var): int /** * Set the options to be used by the sequence matcher, called by this class. * - * @param array $options User defined option names and values. + * @param array $options User defined option names and values. * * @see Diff::$defaultOptions * @@ -139,7 +148,7 @@ public function getArgumentType($var): int * When a keyName matches the name of a default option, that option's value will be overridden by the key's value. * Any other keyName (and it's value) will be added as an option, but will not be used if not implemented. */ - public function setOptions(array $options) + public function setOptions(array $options): void { $this->options = array_merge($this->defaultOptions, $options); } @@ -167,10 +176,9 @@ public function getVersion2(): array /** * Render a diff-view using a rendering class and get its results. * - * @param object|Context|Unified|UnifiedHtml|Inline|SideBySide $renderer An instance of the rendering object, - * used for generating the diff-view. + * @param object $renderer An instance of the rendering object, used for generating the diff-view. * - * @return mixed The generated diff-view. The type of the return value depends on the applied rendereder. + * @return mixed The generated diff-view. The type of the return value depends on the applied renderer. */ public function render(object $renderer) { @@ -189,16 +197,16 @@ public function render(object $renderer) * If the arguments for both parameters are omitted, the entire array will be returned. * If the argument for the second parameter is omitted, the element defined as start will be returned. * - * @param array $array The source array. - * @param int $start The first element of the range to get. - * @param int|null $end The last element of the range to get. - * If not supplied, only the element at start will be returned. + * @param array $array The source array. + * @param int $start The first element of the range to get. + * @param int|null $end The last element of the range to get. + * If not supplied, only the element at start will be returned. * - * @return array Array containing all of the elements of the specified range. + * @return array Array containing all the elements of the specified range. * @throws OutOfRangeException When the value of start or end are invalid to define a range. * */ - public function getArrayRange(array $array, int $start = 0, $end = null): array + public function getArrayRange(array $array, int $start = 0, ?int $end = null): array { if ($start < 0 || $end < 0 || $end < $start) { throw new OutOfRangeException('Start parameter must be lower than End parameter while both are positive!'); @@ -220,6 +228,20 @@ public function getArrayRange(array $array, int $start = 0, $end = null): array return array_slice($array, $start, $length); } + /** + * Get if the compared versions are identical or have differences. + * + * @return bool True when identical. + */ + public function isIdentical(): bool + { + if ($this->groupedCodes === null) { + $this->getGroupedOpCodes(); + } + + return $this->identical; + } + /** * Generate a list of the compiled and grouped op-codes for the differences between two strings. * @@ -239,7 +261,41 @@ public function getGroupedOpCodes(): array //Get and cache the grouped op-codes. $sequenceMatcher = new SequenceMatcher($this->version1, $this->version2, $this->options); $this->groupedCodes = $sequenceMatcher->getGroupedOpCodes(); + $opCodes = $sequenceMatcher->getOpCodes(); + $this->identical = count($opCodes) == 1 && $opCodes[0][0] == 'equal'; return $this->groupedCodes; } + + /** + * Get the similarity ratio of the two sequences. + * + * Once calculated, the results are cached in the diff class instance. + * + * @param int $method Calculation method. + * + * @return float Similarity ratio. + */ + public function getSimilarity(int $method = Similarity::CALC_DEFAULT): float + { + if ($this->similarity !== null) { + return $this->similarity; + } + + $similarity = new Similarity($this->version1, $this->version2, $this->options); + $this->similarity = $similarity->getSimilarity($method); + + return $this->similarity; + } + + /** + * Get diff statistics + * + * @return array + */ + public function getStatistics(): array + { + $similarity = new Similarity($this->version1, $this->version2, $this->options); + return $similarity->getDifference(); + } } diff --git a/lib/jblond/Diff/ConstantsInterface.php b/lib/jblond/Diff/ConstantsInterface.php new file mode 100644 index 00000000..c41e6055 --- /dev/null +++ b/lib/jblond/Diff/ConstantsInterface.php @@ -0,0 +1,33 @@ + + * @copyright (c) 2020 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.3.0 + * @link https://github.com/JBlond/php-diff + */ +interface ConstantsInterface +{ + /** + * Flag to disable ignore of successive empty/blank lines. + */ + public const DIFF_IGNORE_LINE_NONE = 0; + /** + * Flag to ignore empty lines. + */ + public const DIFF_IGNORE_LINE_EMPTY = 1; + /** + * Flag to ignore blank lines. (Lines which contain no or only non-printable characters.) + */ + public const DIFF_IGNORE_LINE_BLANK = 2; +} diff --git a/lib/jblond/Diff/Renderer/Html/HtmlArray.php b/lib/jblond/Diff/Renderer/Html/HtmlArray.php deleted file mode 100644 index 0cd0d53c..00000000 --- a/lib/jblond/Diff/Renderer/Html/HtmlArray.php +++ /dev/null @@ -1,353 +0,0 @@ - - * @author Mario Brandt - * @author Ferry Cools - * @copyright (c) 2009 Chris Boulton - * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 - * @link https://github.com/JBlond/php-diff - */ -class HtmlArray extends RendererAbstract -{ - /** - * @var array Associative array containing the default options available for this renderer and their default - * value. - * - tabSize The amount of spaces to replace a tab character with. - * - title_a Title of the "old" version of text. - * - title_b Title of the "new" version of text. - */ - protected $defaultOptions = [ - 'tabSize' => 4, - 'title1' => 'Version1', - 'title2' => 'Version2', - ]; - - /** - * @var string The last operation which was recorded in the array which contains the changes, used by the renderer. - * @see HtmlArray::appendChangesArray() - */ - private $lastTag; - - /** - * Generate a string representation of changes between the "old and "new" sequences. - * - * This method is called by the renderers which extends this class. - * - * @param array $changes Contains the op-codes about the differences between "old and "new". - * @param object|Inline|SideBySide|Unified $htmlRenderer Renderer which extends this class. - * - * @return string HTML representation of the differences. - */ - public function renderHtml(array $changes, object $htmlRenderer): string - { - if (empty($changes)) { - //No changes between "old" and "new" - return 'No differences found.'; - } - - $html = $htmlRenderer->generateTableHeader(); - - foreach ($changes as $i => $blocks) { - if ($i > 0) { - // If this is a separate block, we're condensing code to output …, - // indicating a significant portion of the code has been collapsed as it did not change. - $html .= $htmlRenderer->generateTableRowsSkipped(); - } - - foreach ($blocks as $change) { - $html .= ''; - switch ($change['tag']) { - // Equal changes should be shown on both sides of the diff - case 'equal': - $html .= $htmlRenderer->generateTableRowsEqual($change); - break; - // Added lines only on the right side - case 'insert': - $html .= $htmlRenderer->generateTableRowsInsert($change); - break; - // Show deleted lines only on the left side - case 'delete': - $html .= $htmlRenderer->generateTableRowsDelete($change); - break; - // Show modified lines on both sides - case 'replace': - $html .= $htmlRenderer->generateTableRowsReplace($change); - break; - } - - $html .= ''; - } - } - - $html .= ''; - - return $html; - } - - /** - * Render and return an array structure suitable for generating HTML based differences. - * - * Generally called by classes which extend this class and that generate a HTML based diff by returning an array of - * the changes to show in the diff. - * - * @return array An array of the generated changes, suitable for presentation in HTML. - */ - public function render() - { - // The old and New texts are copied so change markers can be added without modifying the original sequences. - $oldText = $this->diff->getVersion1(); - $newText = $this->diff->getVersion2(); - - $changes = []; - - foreach ($this->diff->getGroupedOpCodes() as $group) { - $blocks = []; - $this->lastTag = null; - - foreach ($group as $code) { - [$tag, $startOld, $endOld, $startNew, $endNew] = $code; - /** - * $code is an array describing a op-code which includes: - * 0 - The type of tag (as described below) for the op code. - * 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 tags include: - * replace - The string from $startOld to $endOld in $oldText should be replaced by - * the string in $newText from $startNew to $endNew. - * delete - The string in $oldText from $startOld to $endNew should be deleted. - * insert - The string in $newText from $startNew to $endNew should be inserted at $startOld in - * $oldText. - * equal - The two strings with the specified ranges are equal. - */ - - $blockSizeOld = $endOld - $startOld; - $blockSizeNew = $endNew - $startNew; - - if (($tag == 'replace') && ($blockSizeOld == $blockSizeNew)) { - // Inline differences between old and new block. - $this->markInlineChange($oldText, $newText, $startOld, $endOld, $startNew); - } - - $lastBlock = $this->appendChangesArray($blocks, $tag, $startOld, $startNew); - - // Extract the block from both the old and new text and format each line. - $oldBlock = $this->formatLines(array_slice($oldText, $startOld, $blockSizeOld)); - $newBlock = $this->formatLines(array_slice($newText, $startNew, $blockSizeNew)); - - if ($tag == 'equal') { - // Old block equals New block - $blocks[$lastBlock]['base']['lines'] += $oldBlock; - $blocks[$lastBlock]['changed']['lines'] += $newBlock; - continue; - } - - if ($tag == 'replace' || $tag == 'delete') { - // Inline differences or old block doesn't exist in the new text. - // Replace the markers, which where added above, by HTML delete tags. - $oldBlock = str_replace(["\0", "\1"], ['', ''], $oldBlock); - $blocks[$lastBlock]['base']['lines'] += $oldBlock; - } - - if ($tag == 'replace' || $tag == 'insert') { - // Inline differences or the new block doesn't exist in the old text. - // Replace the markers, which where added above, by HTML insert tags. - $newBlock = str_replace(["\0", "\1"], ['', ''], $newBlock); - $blocks[$lastBlock]['changed']['lines'] += $newBlock; - } - } - $changes[] = $blocks; - } - - return $changes; - } - - /** - * Add markers around inline changes between old and new text. - * - * Each line of the old and new text is evaluated. - * When a line of old differs from the same line of new, a marker is inserted into both lines, just before the first - * different character. A second marker is added just behind the last character which differs from each other. - * - * E.g. - *
-     *         1234567
-     * OLd => "abcdefg" Start marker inserted at position 3
-     * New => "ab123fg"   End marker inserted at position 6
-     * 
- * - * @param array $oldText Collection of lines of old text. - * @param array $newText Collection of lines of new text. - * @param int $startOld First line of the block in old to replace. - * @param int $endOld last line of the block in old to replace. - * @param int $startNew First line of the block in new to replace. - */ - private function markInlineChange(array &$oldText, array &$newText, $startOld, $endOld, $startNew) - { - for ($i = 0; $i < ($endOld - $startOld); ++$i) { - // Check each line in the block for differences. - $oldString = $oldText[$startOld + $i]; - $newString = $newText[$startNew + $i]; - - // Determine the start and end position of the line difference. - [$start, $end] = $this->getInlineChange($oldString, $newString); - if ($start != 0 || $end != 0) { - // Changes between the lines exist. - // Add markers around the changed character sequence in the old string. - $sequenceEnd = mb_strlen($oldString) + $end; - $oldString = - mb_substr($oldString, 0, $start) . "\0" . - mb_substr($oldString, $start, $sequenceEnd - $start) . "\1" . - mb_substr($oldString, $sequenceEnd); - - // Add markers around the changed character sequence in the new string. - $sequenceEnd = mb_strlen($newString) + $end; - $newString = - mb_substr($newString, 0, $start) . "\0" . - mb_substr($newString, $start, $sequenceEnd - $start) . "\1" . - mb_substr($newString, $sequenceEnd); - - // Overwrite the strings in the old and new text so the changed lines include the markers. - $oldText[$startOld + $i] = $oldString; - $newText[$startNew + $i] = $newString; - } - } - } - - /** - * Determine where changes between two strings begin and where they end. - * - * This returns a two elements array. - * The first element defines the first (starting at 0) character from the start of the old string which is - * different. - * The second element defines the last (starting at -0) character from the end of the old string which is different. - * - * - * @param string $oldString The first string to compare. - * @param string $newString The second string to compare. - * - * @return array Array containing the starting position (0 by default) and the ending position (-1 by default) - */ - private function getInlineChange(string $oldString, string $newString): array - { - $start = 0; - $limit = min(mb_strlen($oldString), mb_strlen($newString)); - - // Find the position of the first character which is different between old and new. - // Starts at the begin of the strings. - // Stops at the end of the shortest string. - while ($start < $limit && mb_substr($oldString, $start, 1) == mb_substr($newString, $start, 1)) { - ++$start; - } - - $end = -1; - $limit = $limit - $start; - - // Find the position of the last character which is different between old and new. - // Starts at the end of the shortest string. - // Stops just before the last different character. - while (-$end <= $limit && mb_substr($oldString, $end, 1) == mb_substr($newString, $end, 1)) { - --$end; - } - - return [ - $start, - $end + 1, - ]; - } - - /** - * Helper function that will fill the changes-array for the renderer with default values. - * Every time a operation changes (specified by $tag) , a new element will be appended to this array. - * - * The index of the last element of the array is always returned. - * - * @param array $blocks The array which keeps the changes for the HTML renderer. - * @param string $tag Kind of difference. - * @param integer $lineInOld Start of block in "old". - * @param integer $lineInNew Start of block in "new". - * - * @return int The index of the last element. - */ - private function appendChangesArray(array &$blocks, string $tag, int $lineInOld, int $lineInNew): int - { - if ($tag == $this->lastTag) { - return count($blocks) - 1; - } - - $blocks[] = [ - 'tag' => $tag, - 'base' => [ - 'offset' => $lineInOld, - 'lines' => [], - ], - 'changed' => [ - 'offset' => $lineInNew, - 'lines' => [], - ], - ]; - - $this->lastTag = $tag; - return count($blocks) - 1; - } - - /** - * Format a series of strings which are suitable for output in a HTML rendered diff. - * - * This involves replacing tab characters with spaces, making the HTML safe for output by ensuring that double - * spaces are replaced with   etc. - * - * @param array $strings Array of strings to format. - * - * @return array Array of formatted strings. - */ - protected function formatLines(array $strings): array - { - if ($this->options['tabSize'] !== false) { - // Replace tabs with spaces. - $strings = array_map( - function ($item) { - return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $item); - }, - $strings - ); - } - - // Convert special characters to HTML entities - $strings = array_map( - function ($item) { - return htmlspecialchars($item, ENT_NOQUOTES, 'UTF-8'); - }, - $strings - ); - - // Replace leading spaces of a line with HTML entities. - foreach ($strings as &$line) { - $line = preg_replace_callback( - '/(^[ \0\1]*)/', - function ($matches) { - return str_replace(' ', " ", $matches[0]); - }, - $line - ); - } - unset($line); - - return $strings; - } -} diff --git a/lib/jblond/Diff/Renderer/Html/Inline.php b/lib/jblond/Diff/Renderer/Html/Inline.php deleted file mode 100644 index 0b421622..00000000 --- a/lib/jblond/Diff/Renderer/Html/Inline.php +++ /dev/null @@ -1,197 +0,0 @@ - - * @author Mario Brandt - * @author Ferry Cools - * @copyright (c) 2009 Chris Boulton - * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 - * @link https://github.com/JBlond/php-diff - */ -class Inline extends HtmlArray -{ - /** - * Render a and return diff-view with changes between the two sequences displayed side by side. (under each other) - * - * @return string The generated inline diff-view. - */ - public function render(): string - { - $changes = parent::render(); - - return parent::renderHtml($changes, $this); - } - - - /** - * Generates a string representation of the opening of a predefined table and its header with titles from options. - * - * @return string HTML code representation of a table's header. - */ - public function generateTableHeader(): string - { - return << - - - {$this->options['title1']} - {$this->options['title2']} - Differences - - -HTML; - } - - /** - * Generates a string representation of table rows showing lines are skipped. - * - * @return string HTML code representation of a table's header. - */ - public function generateTableRowsSkipped(): string - { - return << - … - … - … - -HTML; - } - - /** - * Generates a string representation of table rows showing text with no difference. - * - * @param array $changes Contains the op-codes about the changes between two blocks. - * - * @return string HTML code representing table rows showing text with no difference. - */ - public function generateTableRowsEqual(array $changes): string - { - $html = ''; - - foreach ($changes['base']['lines'] as $lineNo => $line) { - $fromLine = $changes['base']['offset'] + $lineNo + 1; - $toLine = $changes['changed']['offset'] + $lineNo + 1; - - $html .= << - $fromLine - $toLine - $line - -HTML; - } - - return $html; - } - - /** - * Generates a string representation of table rows showing added text. - * - * @param array $changes Contains the op-codes about the changes between two blocks of text. - * - * @return string HTML code representing table rows showing with added text. - */ - public function generateTableRowsInsert(array $changes): string - { - $html = ''; - - foreach ($changes['changed']['lines'] as $lineNo => $line) { - $toLine = $changes['changed']['offset'] + $lineNo + 1; - - $html .= << -   - $toLine - - $line -   - - -HTML; - } - - return $html; - } - - /** - * Generates a string representation of table rows showing removed text. - * - * @param array $changes Contains the op-codes about the changes between two blocks of text. - * - * @return string HTML code representing table rows showing removed text. - */ - public function generateTableRowsDelete(array $changes): string - { - $html = ''; - - foreach ($changes['base']['lines'] as $lineNo => $line) { - $fromLine = $changes['base']['offset'] + $lineNo + 1; - - $html .= << - $fromLine -   - - $line -   - - -HTML; - } - - return $html; - } - - /** - * Generates a string representation of table rows showing partially modified text. - * - * @param array $changes Contains the op-codes about the changes between two blocks of text. - * - * @return string Html code representing table rows showing modified text. - */ - public function generateTableRowsReplace(array $changes): string - { - $html = ''; - - foreach ($changes['base']['lines'] as $lineNo => $line) { - $fromLine = $changes['base']['offset'] + $lineNo + 1; - - $html .= << - $fromLine -   - - $line - - -HTML; - } - - foreach ($changes['changed']['lines'] as $lineNo => $line) { - $toLine = $changes['changed']['offset'] + $lineNo + 1; - - $html .= << -   - $toLine - - $line - - -HTML; - } - - return $html; - } -} diff --git a/lib/jblond/Diff/Renderer/Html/Merged.php b/lib/jblond/Diff/Renderer/Html/Merged.php new file mode 100644 index 00000000..1ac26263 --- /dev/null +++ b/lib/jblond/Diff/Renderer/Html/Merged.php @@ -0,0 +1,296 @@ + + * @copyright (c) 2020 Ferry Cools + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ +class Merged extends MainRenderer implements SubRendererInterface +{ + /** + * @var array Associative array containing the default options available for this renderer and their default + * value. + * - format The Format of the texts. + * - insertMarkers Markers for inserted text. + * - deleteMarkers Markers for removed text. + * - title1 Title of the 1st version of text. + * - title2 Title of the 2nd version of text. + */ + private $subOptions = [ + 'format' => 'html', + 'insertMarkers' => ['', ''], + 'deleteMarkers' => ['', ''], + 'title1' => 'Version1', + 'title2' => 'Version2', + ]; + /** + * @var int Line offset to keep correct line number for merged diff. + */ + private $lineOffset = 0; + /** + * @var string last block of lines which where removed from version 2. + */ + private $lastDeleted; + + /** + * Merged constructor. + * + * @param array $options Custom defined options for the merged diff renderer. + * + * @see Merged::$subOptions + */ + public function __construct(array $options = []) + { + parent::__construct($this->subOptions); + $this->setOptions($options); + } + + /** + * @inheritDoc + */ + public function render() + { + $changes = parent::renderSequences(); + + return parent::renderOutput($changes, $this); + } + + /** + * @inheritDoc + * + * @return string Start of the diff view. + */ + public function generateDiffHeader(): string + { + return << + + + Merge of {$this->options['title1']} & {$this->options['title2']} + + +HTML; + } + + /** + * @inheritDoc + * + * @return string Start of the block. + */ + public function generateBlockHeader(array $changes): string + { + return $changes['tag'] != 'delete' ? '' : ''; + } + + /** + * @inheritDoc + * + * @return string HTML code representing table rows showing text which is 'Out Of Context' + */ + public function generateLinesOutOfContext($change): string + { + $marker = '…'; + $headerClass = ''; + + if ($this->lastDeleted !== null) { + $headerClass = 'ChangeDelete'; + } + + $this->lastDeleted = null; + + return << + $marker + … + +HTML; + } + + /** + * @inheritDoc + * + * @return string Text with no difference. + */ + public function generateLinesEqual(array $changes): string + { + $html = ''; + + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1 + $this->lineOffset; + $headerClass = ''; + + if (!$lineNo && $this->lastDeleted !== null) { + $headerClass = 'ChangeDelete'; + } + + $html .= << + $fromLine + $line + +HTML; + $this->lastDeleted = null; + } + + return $html; + } + + /** + * @inheritDoc + * + * @return string Added text. + */ + public function generateLinesInsert(array $changes): string + { + $html = ''; + + foreach ($changes['changed']['lines'] as $lineNo => $line) { + $this->lineOffset++; + $toLine = $changes['base']['offset'] + $this->lineOffset; + $headerClass = ''; + if (!$lineNo && $this->lastDeleted !== null) { + $headerClass = 'ChangeDelete'; + } + + $html .= << + $toLine + $line + +HTML; + $this->lastDeleted = null; + } + + + return $html; + } + + /** + * @inheritDoc + * + * @return string Removed text. + */ + public function generateLinesDelete(array $changes): string + { + $this->lineOffset -= count($changes['base']['lines']); + + $title = "Lines of {$this->options['title1']} deleted at {$this->options['title2']}:\n"; + + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + + $title .= <<lastDeleted = htmlentities($title); + + return ''; + } + + /** + * @inheritDoc + * + * @return string Modified text. + */ + public function generateLinesReplace(array $changes): string + { + $html = ''; + $baseLineCount = count($changes['base']['lines']); + $changedLineCount = count($changes['changed']['lines']); + + if (count($changes['base']['lines']) == $changedLineCount) { + // Lines of Version 1 are modified at version 2. + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1 + $this->lineOffset; + + // Capture line-parts which are added to the same line at version 2. + $addedParts = []; + preg_match_all('/\x0.*?\x1/', $changes['changed']['lines'][$lineNo], $addedParts, PREG_PATTERN_ORDER); + array_unshift($addedParts[0], ''); + + // Inline Replacement: + // Concatenate line-parts which are removed at version2 with line-parts which are added at version 2. + $line = preg_replace_callback( + '/\x0.*?\x1/', + function ($removedParts) use ($addedParts) { + $addedPart = str_replace(["\0", "\1"], $this->options['insertMarkers'], next($addedParts[0])); + $removedPart = str_replace(["\0", "\1"], $this->options['deleteMarkers'], $removedParts[0]); + + return "$removedPart$addedPart"; + }, + $line + ); + + $html .= << + $fromLine + $line + +HTML; + } + + return $html; + } + + // More or less lines at version 2. Block of version 1 is replaced by block of version 2. + $title = ''; + + foreach ($changes['changed']['lines'] as $lineNo => $line) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + + if (!$lineNo) { + $title = "Lines replaced at {$this->options['title1']}:\n"; + foreach ($changes['base']['lines'] as $baseLineNo => $baseLine) { + $title .= $changes['base']['offset'] + $baseLineNo + 1 . ": $baseLine\n"; + } + } + + $title = htmlentities($title); + $html .= << + $toLine + $line + +HTML; + } + + $this->lineOffset = $this->lineOffset + $changedLineCount - $baseLineCount; + + return $html; + } + + /** + * @inheritDoc + * + * @return string End of the block. + */ + public function generateBlockFooter(array $changes): string + { + return $changes['tag'] != 'delete' ? '' : ''; + } + + /** + * @inheritDoc + * + * @return string End of the diff view. + */ + public function generateDiffFooter(): string + { + return ''; + } +} diff --git a/lib/jblond/Diff/Renderer/Html/SideBySide.php b/lib/jblond/Diff/Renderer/Html/SideBySide.php index 128e8c98..67b6e2ef 100644 --- a/lib/jblond/Diff/Renderer/Html/SideBySide.php +++ b/lib/jblond/Diff/Renderer/Html/SideBySide.php @@ -4,40 +4,73 @@ namespace jblond\Diff\Renderer\Html; +use jblond\Diff\Renderer\MainRenderer; +use jblond\Diff\Renderer\SubRendererInterface; + /** * Side by Side HTML diff generator for PHP DiffLib. * - * PHP version 7.2 or greater + * PHP version 7.3 or greater * - * @package jblond\Diff\Renderer\Html - * @author Chris Boulton - * @author Mario Brandt - * @author Ferry Cools - * @copyright (c) 2009 Chris Boulton - * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 - * @link https://github.com/JBlond/php-diff + * @package jblond\Diff\Renderer\Html + * @author Chris Boulton + * @author Mario Brandt + * @author Ferry Cools + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff */ -class SideBySide extends HtmlArray +class SideBySide extends MainRenderer implements SubRendererInterface { /** - * Render a and return diff-view with changes between the two sequences displayed side by side. + * @var array Associative array containing the default options available for this renderer and their default + * value. + * - format The Format of the texts. + * - insertMarkers Markers for inserted text. + * - deleteMarkers Markers for removed text. + * - title1 Title of the 1st version of text. + * - title2 Title of the 2nd version of text. + */ + private $subOptions = [ + 'format' => 'html', + 'insertMarkers' => ['', ''], + 'deleteMarkers' => ['', ''], + 'title1' => 'Version1', + 'title2' => 'Version2', + ]; + + /** + * SideBySide constructor. + * + * @param array $options Custom defined options for the SideBySide diff renderer. + * + * @see SideBySide::$subOptions + */ + public function __construct(array $options = []) + { + parent::__construct($this->subOptions); + $this->setOptions($options); + } + + /** + * @inheritDoc * - * @return string The generated side by side diff-view. + * @return string|false The generated diff-view or false when there's no difference. */ - public function render(): string + public function render() { - $changes = parent::render(); + $changes = parent::renderSequences(); - return parent::renderHtml($changes, $this); + return parent::renderOutput($changes, $this); } /** - * Generates a string representation of the opening of a predefined table and its header with titles from options. + * @inheritDoc * * @return string HTML code representation of a table's header. */ - public function generateTableHeader(): string + public function generateDiffHeader(): string { return << @@ -51,11 +84,11 @@ public function generateTableHeader(): string } /** - * Generates a string representation of table rows showing lines are skipped. + * @inheritDoc * - * @return string HTML code representation of a table's header. + * @return string HTML code representing table rows showing text which is "Out Of Context" */ - public function generateTableRowsSkipped(): string + public function generateLinesOutOfContext($change): string { return << @@ -68,17 +101,15 @@ public function generateTableRowsSkipped(): string } /** - * Generates a string representation of table rows showing text with no difference. - * - * @param array $changes Contains the op-codes about the changes between two blocks. + * @inheritDoc * * @return string HTML code representing table rows showing text with no difference. */ - public function generateTableRowsEqual(array $changes): string + public function generateLinesEqual(array $changes): string { $html = ''; - foreach ($changes['base']['lines'] as $lineNo => $line) { + foreach ($changes['base']['lines'] as $lineNo => $baseLine) { $fromLine = $changes['base']['offset'] + $lineNo + 1; $toLine = $changes['changed']['offset'] + $lineNo + 1; @@ -86,11 +117,11 @@ public function generateTableRowsEqual(array $changes): string $fromLine - $line + $baseLine $toLine - $line + {$changes['changed']['lines'][$lineNo]} HTML; @@ -100,13 +131,11 @@ public function generateTableRowsEqual(array $changes): string } /** - * Generates a string representation of table rows showing added text. - * - * @param array $changes Contains the op-codes about the changes between two blocks of text. + * @inheritDoc * * @return string HTML code representing table rows showing with added text. */ - public function generateTableRowsInsert(array $changes): string + public function generateLinesInsert(array $changes): string { $html = ''; @@ -129,13 +158,11 @@ public function generateTableRowsInsert(array $changes): string } /** - * Generates a string representation of table rows showing removed text. - * - * @param array $changes Contains the op-codes about the changes between two blocks of text. + * @inheritDoc * * @return string HTML code representing table rows showing removed text. */ - public function generateTableRowsDelete(array $changes): string + public function generateLinesDelete(array $changes): string { $html = ''; @@ -158,13 +185,11 @@ public function generateTableRowsDelete(array $changes): string } /** - * Generates a string representation of table rows showing partially modified text. - * - * @param array $changes Contains the op-codes about the changes between two blocks of text. + * @inheritDoc * * @return string Html code representing table rows showing modified text. */ - public function generateTableRowsReplace(array $changes): string + public function generateLinesReplace(array $changes): string { $html = ''; @@ -172,13 +197,16 @@ public function generateTableRowsReplace(array $changes): string if (count($changes['base']['lines']) >= count($changes['changed']['lines'])) { foreach ($changes['base']['lines'] as $lineNo => $line) { $fromLine = $changes['base']['offset'] + $lineNo + 1; - $toLine = " "; - $changedLine = " "; + $toLine = ' '; + $changedLine = ' '; if (isset($changes['changed']['lines'][$lineNo])) { $toLine = $changes['changed']['offset'] + $lineNo + 1; $changedLine = $changes['changed']['lines'][$lineNo]; } + $line = str_replace(["\0", "\1"], $this->options['deleteMarkers'], $line); + $changedLine = str_replace(["\0", "\1"], $this->options['insertMarkers'], $changedLine); + $html .= << $fromLine @@ -198,13 +226,16 @@ public function generateTableRowsReplace(array $changes): string foreach ($changes['changed']['lines'] as $lineNo => $changedLine) { $toLine = $changes['changed']['offset'] + $lineNo + 1; - $fromLine = " "; - $line = " "; + $fromLine = ' '; + $line = ' '; if (isset($changes['base']['lines'][$lineNo])) { $fromLine = $changes['base']['offset'] + $lineNo + 1; $line = $changes['base']['lines'][$lineNo]; } + $line = str_replace(["\0", "\1"], $this->options['deleteMarkers'], $line); + $changedLine = str_replace(["\0", "\1"], $this->options['insertMarkers'], $changedLine); + $html .= << $fromLine @@ -221,4 +252,100 @@ public function generateTableRowsReplace(array $changes): string return $html; } + + /** + * @inheritDoc + * + * @return string Start of the block. + */ + public function generateBlockHeader(array $changes): string + { + return ''; + } + + /** + * @inheritDoc + * + * @return string End of the block. + */ + public function generateBlockFooter(array $changes): string + { + return ''; + } + + /** + * @inheritDoc + * + * @return string End of the diff view. + */ + public function generateDiffFooter(): string + { + return ''; + } + + /** + * @inheritDoc + * + * @return string Html code representing table rows showing ignored text. + */ + public function generateLinesIgnore(array $changes): string + { + $html = ''; + + // Is below comparison result ever false? + if (count($changes['base']['lines']) >= count($changes['changed']['lines'])) { + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $toLine = ' '; + $changedLine = ' '; + if (isset($changes['changed']['lines'][$lineNo])) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $changedLine = $changes['changed']['lines'][$lineNo]; + } + + $html .= << + $fromLine + + $line + + $toLine + + $changedLine + + +HTML; + } + + return $html; + } + + foreach ($changes['changed']['lines'] as $lineNo => $changedLine) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $fromLine = ' '; + $line = ' '; + if (isset($changes['base']['lines'][$lineNo])) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $line = $changes['base']['lines'][$lineNo]; + } + + $line = str_replace(["\0", "\1"], $this->options['deleteMarkers'], $line); + $changedLine = str_replace(["\0", "\1"], $this->options['insertMarkers'], $changedLine); + + $html .= << + $fromLine + + $line + + $toLine + + $changedLine + + +HTML; + } + + return $html; + } } diff --git a/lib/jblond/Diff/Renderer/Html/Unified.php b/lib/jblond/Diff/Renderer/Html/Unified.php index 6a60b11b..af61751d 100644 --- a/lib/jblond/Diff/Renderer/Html/Unified.php +++ b/lib/jblond/Diff/Renderer/Html/Unified.php @@ -4,166 +4,286 @@ namespace jblond\Diff\Renderer\Html; +use jblond\Diff\Renderer\MainRenderer; +use jblond\Diff\Renderer\SubRendererInterface; + /** * Unified HTML diff generator for PHP DiffLib. * - * PHP version 7.2 or greater + * PHP version 7.3 or greater * * @package jblond\Diff\Renderer\Html - * @author Mario Brandt + * @author Chris Boulton + * @author Mario Brandt * @author Ferry Cools * @copyright (c) 2009 Chris Boulton * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 + * @version 2.4.0 * @link https://github.com/JBlond/php-diff */ -class Unified extends HtmlArray +class Unified extends MainRenderer implements SubRendererInterface { /** - * Render and return a unified diff-view with changes between the two sequences displayed inline (under each other). + * @var array Associative array containing the default options available for this renderer and their default + * value. + * - format The Format of the texts. + * - insertMarkers Markers for inserted text. + * - deleteMarkers Markers for removed text. + * - title1 Title of the 1st version of text. + * - title2 Title of the 2nd version of text. + */ + private $subOptions = [ + 'format' => 'html', + 'insertMarkers' => ['', ''], + 'deleteMarkers' => ['', ''], + 'title1' => 'Version1', + 'title2' => 'Version2', + ]; + + /** + * Unified constructor. * - * @return string The generated inline diff-view. + * @param array $options Custom defined options for the unified diff renderer. + * + * @see Unified::$subOptions */ - public function render(): string + public function __construct(array $options = []) { - $changes = parent::render(); + parent::__construct($this->subOptions); + $this->setOptions($options); + } - return $this->renderHtml($changes); + /** + * @inheritDoc + * + * @return string|false The generated diff-view or false when there's no difference. + */ + public function render() + { + $changes = parent::renderSequences(); + + return parent::renderOutput($changes, $this); } /** - * Render the unified diff-view as html. + * @inheritDoc * - * Since this class extends the "HtmlArray" class which in turn extends "RendererAbstract" class, this method needs - * to match the signature of RendererAbstract::renderHTML(). However the second parameter isn't used and can be - * omitted. + * @return string HTML code representation of a table's header. + */ + public function generateDiffHeader(): string + { + return << + + + {$this->options['title1']} + {$this->options['title2']} + Differences + + +HTML; + } + + /** + * @inheritDoc * - * @param array $changes Contains the op-codes about the differences between "old and "new". - * @param null $object Unused. + * @return string HTML code representing table rows showing text which is 'Out Of Context' + */ + public function generateLinesOutOfContext($change): string + { + return << + … + … + … + +HTML; + } + + /** + * @inheritDoc * - * @return string HTML code containing the unified differences. + * @return string HTML code representing table rows showing text without differences. */ - public function renderHtml($changes, $object = null): string + public function generateLinesEqual(array $changes): string { - if (empty($changes)) { - //No changes between "old" and "new" - return 'No differences found.'; - } + $html = ''; - $html = ''; + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $toLine = $changes['changed']['offset'] + $lineNo + 1; - foreach ($changes as $i => $blocks) { - if ($i > 0) { - // If this is a separate block, we're condensing code to output …, - // indicating a significant portion of the code has been collapsed as it did not change. - $html .= <<… + $html .= << + $fromLine + $toLine + $line + HTML; - } - - foreach ($blocks as $change) { - $html .= ''; - switch ($change['tag']) { - case 'equal': - // Add unmodified lines. - $html .= $this->generateLinesEqual($change); - break; - case 'insert': - // Add Added lines. - $html .= $this->generateLinesInsert($change); - break; - case 'delete': - // Add deleted lines. - $html .= $this->generateLinesDelete($change); - break; - case 'replace': - // Add modified lines. - $html .= $this->generateLinesReplace($change); - break; - } - $html .= ''; - } } - $html .= ''; return $html; } - /** - * Generates a string representation of blocks of text with no difference. - * - * @param array $change Contains the op-codes about the changes between two blocks. + * @inheritDoc * - * @return string HTML code representing the blocks of text with no difference. + * @return string HTML code representing table rows showing with added text. */ - public function generateLinesEqual(array $change): string + public function generateLinesInsert(array $changes): string { $html = ''; - foreach ($change['base']['lines'] as $line) { - $html .= '' . $line . '
'; + foreach ($changes['changed']['lines'] as $lineNo => $line) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + + $html .= << +   + $toLine + + $line +   + + +HTML; } return $html; } /** - * Generates a string representation of a block of text, where new text was added. - * - * @param array $change Contains the op-codes about the changes between two blocks. + * @inheritDoc * - * @return string HTML code representing a block of added text. + * @return string HTML code representing table rows showing removed text. */ - public function generateLinesInsert(array $change): string + public function generateLinesDelete(array $changes): string { $html = ''; - foreach ($change['changed']['lines'] as $line) { - $html .= '' . $line . '
'; + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + + $html .= << + $fromLine +   + + $line +   + + +HTML; } return $html; } /** - * Generates a string representation of a block of text, where text was removed. + * @inheritDoc * - * @param array $change Contains the op-codes about the changes between two blocks. - * - * @return string HTML code representing a block of removed text. + * @return string Html code representing table rows showing modified text. */ - public function generateLinesDelete(array $change): string + public function generateLinesReplace(array $changes): string { $html = ''; - foreach ($change['base']['lines'] as $line) { - $html .= '' . $line . '
'; + + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $line = str_replace(["\0", "\1"], $this->options['deleteMarkers'], $line); + $html .= << + $fromLine +   + + $line + + +HTML; + } + + foreach ($changes['changed']['lines'] as $lineNo => $line) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $line = str_replace(["\0", "\1"], $this->options['insertMarkers'], $line); + $html .= << +   + $toLine + + $line + + +HTML; } return $html; } /** - * Generates a string representation of a block of text, where text was partially modified. + * @inheritDoc * - * @param array $change Contains the op-codes about the changes between two blocks. - * - * @return string HTML code representing a block of modified text. + * @return string Html code representing table rows showing ignored text. */ - public function generateLinesReplace(array $change): string + public function generateLinesIgnore(array $changes): string { $html = ''; - // Lines with characters removed. - foreach ($change['base']['lines'] as $line) { - $html .= '' . $line . '
'; + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $html .= << + $fromLine + + + $line + + +HTML; } - // Lines with characters added. - foreach ($change['changed']['lines'] as $line) { - $html .= '' . $line . '
'; + foreach ($changes['changed']['lines'] as $lineNo => $line) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $html .= << + + $toLine + + $line + + +HTML; } return $html; } + + /** + * @inheritDoc + * + * @return string Start of the block. + */ + public function generateBlockHeader(array $changes): string + { + return ''; + } + + /** + * @inheritDoc + * + * @return string End of the block. + */ + public function generateBlockFooter(array $changes): string + { + return ''; + } + + /** + * @inheritDoc + * + * @return string End of the diff view. + */ + public function generateDiffFooter(): string + { + return ''; + } } diff --git a/lib/jblond/Diff/Renderer/MainRenderer.php b/lib/jblond/Diff/Renderer/MainRenderer.php new file mode 100644 index 00000000..db0d4cd1 --- /dev/null +++ b/lib/jblond/Diff/Renderer/MainRenderer.php @@ -0,0 +1,477 @@ + + * @author Mario Brandt + * @author Ferry Cools + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ +class MainRenderer extends MainRendererAbstract +{ + /** + * @var int Character count of the line marker with the most characters. + */ + protected $maxLineMarkerWidth = 0; + /** + * @var string The last operation which was recorded in the array which contains the changes, used by the renderer. + * @see MainRenderer::appendChangesArray() + */ + private $lastTag; + + /** + * Generate a string representation of changes between version1 and version2. + * + * This method is called by the renderers which extends this class. + * + * @param array $changes Contains the op-codes about the differences between version1 and + * version2. + * @param SubRendererInterface $subRenderer Renderer which is child class of this class. + * + * @return string|false String representation of the differences or false when versions are identical. + */ + public function renderOutput(array $changes, SubRendererInterface $subRenderer) + { + if (!$changes) { + //No changes between version1 and version2 + return false; + } + + $output = $subRenderer->generateDiffHeader(); + + foreach ($changes as $blocks) { + $this->maxLineMarkerWidth = max( + strlen($this->options['insertMarkers'][0]), + strlen($this->options['deleteMarkers'][0]), + strlen($this->options['equalityMarkers'][0]), + strlen($this->options['equalityMarkers'][1]) + ); + + $deprecationTriggered = false; + foreach ($blocks as $change) { + if ( + $subRenderer instanceof MainRenderer && + !method_exists($subRenderer, 'generateLinesIgnore') && + $change['tag'] == 'ignore' + ) { + if (!$deprecationTriggered) { + trigger_error( + 'The use of a subRenderer without method generateLinesIgnore() is deprecated!', + E_USER_DEPRECATED + ); + $deprecationTriggered = true; + } + $change['tag'] = + (count($change['base']['lines']) > count($change['changed']['lines'])) ? 'delete' : 'insert'; + } + $output .= $subRenderer->generateBlockHeader($change); + switch ($change['tag']) { + case 'equal': + $output .= $subRenderer->generateLinesEqual($change); + break; + case 'insert': + $output .= $subRenderer->generateLinesInsert($change); + break; + case 'delete': + $output .= $subRenderer->generateLinesDelete($change); + break; + case 'replace': + $output .= $subRenderer->generateLinesReplace($change); + break; + case 'ignore': + // TODO: Keep backward compatible with renderers? + $output .= $subRenderer->generateLinesIgnore($change); + break; + case 'outOfContext': + // TODO: Keep backward compatible with renderers? + $output .= $subRenderer->generateLinesOutOfContext($change); + break; + } + + $output .= $subRenderer->generateBlockFooter($change); + } + } + + $output .= $subRenderer->generateDiffFooter(); + + return $output; + } + + /** + * Render the sequences where differences between them are marked. + * + * The marked sequences are returned as array which is suitable for rendering the final output. + * + * Generally called by classes which extend this class and that generate a diff by returning an array of the changes + * to show in the diff. + * + * @return array An array of marked sequences. + */ + protected function renderSequences(): array + { + // The old and New texts are copied so change markers can be added without modifying the original sequences. + $oldText = $this->diff->getVersion1(); + $newText = $this->diff->getVersion2(); + + $changes = []; + + foreach ($this->diff->getGroupedOpCodes() as $group) { + $blocks = []; + $this->lastTag = null; + + foreach ($group as $code) { + [$tag, $startOld, $endOld, $startNew, $endNew] = $code; + /** + * $code is an array describing an op-code which includes: + * 0 - The type of tag (as described below) for the op code. + * 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 tags include: + * replace - The string in $oldText from $startOld to $endOld, should be replaced by + * the string in $newText from $startNew to $endNew. + * delete - The string in $oldText from $startOld to $endNew should be deleted. + * insert - The string in $newText from $startNew to $endNew should be inserted at $startOld in + * $oldText. + * equal - The two strings with the specified ranges are equal. + * ignore - The string in $oldText from $startOld to $endOld and + * the string in $newText from $startNew to $endNew are different, but considered to be equal. + */ + + $blockSizeOld = $endOld - $startOld; + $blockSizeNew = $endNew - $startNew; + + if (($tag == 'replace') && ($blockSizeOld == $blockSizeNew)) { + // Inline differences between old and new block. + $this->markInlineChanges($oldText, $newText, $startOld, $endOld, $startNew); + } + + $lastBlock = $this->appendChangesArray($blocks, $tag, $startOld, $startNew); + + // Extract the block from both the old and new text and format each line. + $oldBlock = $this->formatLines(array_slice($oldText, $startOld, $blockSizeOld)); + $newBlock = $this->formatLines(array_slice($newText, $startNew, $blockSizeNew)); + + if ($tag != 'delete' && $tag != 'insert') { + // Old block "equals" New block or is replaced. + $blocks[$lastBlock]['base']['lines'] += $oldBlock; + $blocks[$lastBlock]['changed']['lines'] += $newBlock; + continue; + } + + if ($tag == 'delete') { + // Block of version1 doesn't exist in version2. + $blocks[$lastBlock]['base']['lines'] += $oldBlock; + continue; + } + + // Block of version2 doesn't exist in version1. + $blocks[$lastBlock]['changed']['lines'] += $newBlock; + } + + $changes[] = $blocks; + } + + return $changes; + } + + /** + * Surround inline changes with markers. + * + * @param array $oldText Collection of lines of old text. + * @param array $newText Collection of lines of new text. + * @param int $startOld First line of the block in old to replace. + * @param int $endOld last line of the block in old to replace. + * @param int $startNew First line of the block in new to replace. + */ + private function markInlineChanges( + array &$oldText, + array &$newText, + int $startOld, + int $endOld, + int $startNew + ): void { + if ($this->options['inlineMarking'] < self::CHANGE_LEVEL_LINE) { + $this->markInnerChange($oldText, $newText, $startOld, $endOld, $startNew); + + return; + } + + if ($this->options['inlineMarking'] == self::CHANGE_LEVEL_LINE) { + $this->markOuterChange($oldText, $newText, $startOld, $endOld, $startNew); + } + } + + /** + * Add markers around inline changes between old and new text. + * + * Each line of the old and new text is evaluated. + * When a line of old differs from the same line of new, a marker is inserted into both lines, just before the first + * different character/word. A second marker is added just before the following character/word which matches again. + * + * Setting parameter changeType to self::CHANGE_LEVEL_CHAR will mark differences at character level. + * Other values will mark differences at word level. + * + * E.g. Character level. + *
+     *         1234567890
+     * Old => "aa bbc cdd" Start marker inserted at position 4
+     * New => "aa 12c cdd" End marker inserted at position 6
+     * 
+ * E.g. Word level. + *
+     *         1234567890
+     * Old => "aa bbc cdd" Start marker inserted at position 4
+     * New => "aa 12c cdd" End marker inserted at position 7
+     * 
+ * + * @param array $oldText Collection of lines of old text. + * @param array $newText Collection of lines of new text. + * @param int $startOld First line of the block in old to replace. + * @param int $endOld last line of the block in old to replace. + * @param int $startNew First line of the block in new to replace. + */ + private function markInnerChange(array &$oldText, array &$newText, int $startOld, int $endOld, int $startNew): void + { + for ($iterator = 0; $iterator < ($endOld - $startOld); ++$iterator) { + // ChangeType 0: Character Level. + // ChangeType 1: Word Level. + $regex = $this->options['inlineMarking'] ? '/\w+|[^\w\s]|\s/u' : '/.?/u'; + + // Deconstruct the lines into arrays, including new empty element to the end in case a marker needs to be + // placed as last. + $oldLine = $this->sequenceToArray($regex, $oldText[$startOld + $iterator]); + $newLine = $this->sequenceToArray($regex, $newText[$startNew + $iterator]); + $oldLine[] = ''; + $newLine[] = ''; + + $sequenceMatcher = new SequenceMatcher($oldLine, $newLine); + $opCodes = $sequenceMatcher->getGroupedOpCodes(); + + foreach ($opCodes as $group) { + foreach ($group as [$tag, $changeStartOld, $changeEndOld, $changeStartNew, $changeEndNew]) { + if ($tag == 'equal') { + continue; + } + if ($tag == 'replace' || $tag == 'delete') { + $oldLine[$changeStartOld] = "\0" . $oldLine[$changeStartOld]; + $oldLine[$changeEndOld] = "\1" . $oldLine[$changeEndOld]; + } + if ($tag == 'replace' || $tag == 'insert') { + $newLine[$changeStartNew] = "\0" . $newLine[$changeStartNew]; + $newLine[$changeEndNew] = "\1" . $newLine[$changeEndNew]; + } + } + } + + // Reconstruct the lines and overwrite originals. + $oldText[$startOld + $iterator] = implode('', $oldLine); + $newText[$startNew + $iterator] = implode('', $newLine); + } + } + + /** + * Split a sequence of characters into an array. + * + * Each element of the returned array contains a full pattern match of the regex pattern. + * + * @param string $pattern Regex pattern to split by. + * @param string $sequence The sequence to split. + * + * @return array The split sequence. + */ + public function sequenceToArray(string $pattern, string $sequence): array + { + preg_match_all($pattern, $sequence, $matches); + + return $matches[0]; + } + + /** + * Add markers around inline changes between old and new text. + * + * Each line of the old and new text is evaluated. + * When a line of old differs from the same line of new, a marker is inserted into both lines, just before the first + * different character. A second marker is added just behind the last character which differs from each other. + * + * E.g. + *
+     *         1234567
+     * Old => "abcdefg" Start marker inserted at position 3
+     * New => "ab123fg"   End marker inserted at position 6
+     * 
+ * + * @param array $oldText Collection of lines of old text. + * @param array $newText Collection of lines of new text. + * @param int $startOld First line of the block in old to replace. + * @param int $endOld last line of the block in old to replace. + * @param int $startNew First line of the block in new to replace. + */ + private function markOuterChange(array &$oldText, array &$newText, int $startOld, int $endOld, int $startNew): void + { + for ($iterator = 0; $iterator < ($endOld - $startOld); ++$iterator) { + // Check each line in the block for differences. + $oldString = $oldText[$startOld + $iterator]; + $newString = $newText[$startNew + $iterator]; + + // Determine the start and end position of the line difference. + [$start, $end] = $this->getOuterChange($oldString, $newString); + // Changes between the lines exist. + // Add markers around the changed character sequence in the old string. + $sequenceEnd = mb_strlen($oldString) + $end; + $oldString = mb_substr($oldString, 0, $start) . "\0" . + mb_substr($oldString, $start, $sequenceEnd - $start) . "\1" . + mb_substr($oldString, $sequenceEnd); + + // Add markers around the changed character sequence in the new string. + $sequenceEnd = mb_strlen($newString) + $end; + $newString = mb_substr($newString, 0, $start) . "\0" . + mb_substr($newString, $start, $sequenceEnd - $start) . "\1" . + mb_substr($newString, $sequenceEnd); + + // Overwrite the strings in the old and new text so the changed lines include the markers. + $oldText[$startOld + $iterator] = $oldString; + $newText[$startNew + $iterator] = $newString; + } + } + + /** + * Determine where changes between two strings begin and where they end. + * + * This returns a two elements array. + * The first element defines the first (starting at 0) character from the start of the old string which is + * different. + * The second element defines the last (starting at -0) character from the end of the old string which is different. + * + * + * @param string $oldString The first string to compare. + * @param string $newString The second string to compare. + * + * @return array Array containing the starting position (0 by default) and the ending position (-1 by default) + */ + private function getOuterChange(string $oldString, string $newString): array + { + $start = 0; + $limit = min(mb_strlen($oldString), mb_strlen($newString)); + + // Find the position of the first character which is different between old and new. + // Starts at the beginning of the strings. + // Stops at the end of the shortest string. + while ($start < $limit && mb_substr($oldString, $start, 1) == mb_substr($newString, $start, 1)) { + ++$start; + } + + $end = -1; + $limit -= $start; + + // Find the position of the last character which is different between old and new. + // Starts at the end of the shortest string. + // Stops just before the last different character. + while (-$end <= $limit && mb_substr($oldString, $end, 1) == mb_substr($newString, $end, 1)) { + --$end; + } + + return [ + $start, + $end + 1, + ]; + } + + /** + * Helper function that will fill the changes-array for the renderer with default values. + * Every time an operation changes (specified by $tag), a new element will be appended to this array. + * + * The index of the last element of the array is always returned. + * + * @param array $blocks The array which keeps the changes for the HTML renderer. + * @param string $tag Kind of difference. + * @param int $lineInOld Start of block in "old". + * @param int $lineInNew Start of block in "new". + * + * @return int The index of the last element. + */ + private function appendChangesArray(array &$blocks, string $tag, int $lineInOld, int $lineInNew): int + { + if ($tag == $this->lastTag) { + return count($blocks) - 1; + } + + $blocks[] = [ + 'tag' => $tag, + 'base' => [ + 'offset' => $lineInOld, + 'lines' => [], + ], + 'changed' => [ + 'offset' => $lineInNew, + 'lines' => [], + ], + ]; + + $this->lastTag = $tag; + + return count($blocks) - 1; + } + + /** + * Format a series of strings which are suitable for output in an HTML rendered diff. + * + * This involves replacing tab characters with spaces, making the HTML safe for output by ensuring that double + * spaces are replaced with   etc. + * + * @param array $strings Array of strings to format. + * + * @return array Array of formatted strings. + */ + protected function formatLines(array $strings): array + { + if ($this->options['tabSize'] !== false) { + // Replace tabs with spaces. + $strings = array_map( + function ($line) { + return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $line); + }, + $strings + ); + } + + if (strtolower($this->options['format']) == 'html') { + // Convert special characters to HTML entities + $strings = array_map( + static function ($line) { + return htmlspecialchars($line, ENT_NOQUOTES); + }, + $strings + ); + + // Replace leading spaces of a line with HTML entities. + foreach ($strings as &$line) { + $line = preg_replace_callback( + '/(^[ \0\1]*)/', + static function ($matches) { + return str_replace(' ', ' ', $matches[0]); + }, + $line + ); + } + unset($line); + } + + return $strings; + } +} diff --git a/lib/jblond/Diff/Renderer/MainRendererAbstract.php b/lib/jblond/Diff/Renderer/MainRendererAbstract.php new file mode 100644 index 00000000..b45f089a --- /dev/null +++ b/lib/jblond/Diff/Renderer/MainRendererAbstract.php @@ -0,0 +1,102 @@ + + * @author Ferry Cools + * @copyright (c) 2020 Ferry Cools + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ +abstract class MainRendererAbstract +{ + /** + * Mark inline character differences. + */ + public const CHANGE_LEVEL_CHAR = 0; + /** + * Mark inline word differences. + */ + public const CHANGE_LEVEL_WORD = 1; + /** + * Mark line differences. + */ + public const CHANGE_LEVEL_LINE = 2; + /** + * @var Diff $diff Instance of the diff class that this renderer is generating the rendered diff for. + */ + public $diff; + + /** + * @var array Associative array containing the default options available for this renderer and their default + * value. + * - inlineMarking The level of how differences are marked. + * - self::CHANGE_LEVEL_NONE Don't Inline-Mark. + * - self::CHANGE_LEVEL_CHAR Inline-Mark each different character. + * - self::CHANGE_LEVEL_WORD Inline-Mark each different word. + * - self::CHANGE_LEVEL_LINE Inline-Mark from first to last line diff. + * - tabSize The amount of spaces to replace a tab character with. + * - format The format of the input texts. + * - cliColor Colorized output for cli. + * - deleteMarkers Markers for removed text. + * - insertMarkers Markers for inserted text. + * - equalityMarkers Markers for unchanged and changed lines. + * - insertColors Fore- and background color for inserted text. Only when cliColor = true. + * - deleteColors Fore- and background color for removed text. Only when cliColor = true. + */ + protected $mainOptions = [ + 'inlineMarking' => self::CHANGE_LEVEL_LINE, + 'tabSize' => 4, + 'format' => 'plain', + 'cliColor' => false, + 'deleteMarkers' => ['', ''], + 'insertMarkers' => ['', ''], + 'equalityMarkers' => ['', ''], + 'insertColors' => ['black', 'green'], + 'deleteColors' => ['black', 'red'], + ]; + + /** + * @var array Array containing a merge between the default options and user applied options for the renderer. + * @see MainRendererAbstract::$mainOptions + */ + 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 main renderer to the supplied options. + * + * Options are merged with the default to ensure that there aren't any missing options. + * When custom options are added to the default ones, they can be overwritten, but they can't be removed. + * + * @param array $options Array of options to set. + * + * @see MainRendererAbstract::$mainOptions + * + */ + public function setOptions(array $options): void + { + $this->options = array_merge($this->mainOptions, $this->options, $options); + } +} diff --git a/lib/jblond/Diff/Renderer/RendererAbstract.php b/lib/jblond/Diff/Renderer/RendererAbstract.php deleted file mode 100644 index f33ddc1c..00000000 --- a/lib/jblond/Diff/Renderer/RendererAbstract.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @author Mario Brandt - * @author Ferry Cools - * @copyright (c) 2009 Chris Boulton - * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 - * @link https://github.com/JBlond/php-diff - */ -abstract class RendererAbstract -{ - - /** - * @var Diff $diff Instance of the diff class that this renderer is generating the rendered diff for. - */ - public $diff; - - /** - * @var array Array of the default options that apply to this renderer. - */ - protected $defaultOptions = [ - 'title1' => 'Version1', - 'title2' => 'Version2', - ]; - - /** - * @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 Array of options to set. - */ - public function setOptions(array $options) - { - $this->options = array_merge($this->defaultOptions, $options); - } -} diff --git a/lib/jblond/Diff/Renderer/SubRendererInterface.php b/lib/jblond/Diff/Renderer/SubRendererInterface.php new file mode 100644 index 00000000..48d8069e --- /dev/null +++ b/lib/jblond/Diff/Renderer/SubRendererInterface.php @@ -0,0 +1,115 @@ + + * @copyright (c) 2020 Ferry Cools + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ +interface SubRendererInterface +{ + /** + * Render and return a diff-view with changes between two sequences. + * + * @return string|false The generated diff-view or false when there's no difference. + */ + public function render(); + + /** + * Generate a string representation of the start of a diff view. + * + * @return string Start of the diff view. + */ + public function generateDiffHeader(): string; + + /** + * Generate a string representation of the start of a block. + * + * @param array $changes Contains the op-codes about the changes between two blocks of text. + * + * @return string Start of the block. + */ + public function generateBlockHeader(array $changes): string; + + /** + * Generate a string representation of lines without differences between both versions. + * + * Note: Depending on the options, lines can be marked as being equal, while the contents actually differ. + * (E.g. ignoreWhitespace and ignoreCase) + * + * @param array $changes Contains the op-codes about the changes between two blocks of text. + * + * @return string Text with no difference. + */ + public function generateLinesEqual(array $changes): string; + + /** + * Generate a string representation of lines that are added to the 2nd version. + * + * @param array $changes Contains the op-codes about the changes between two blocks of text. + * + * @return string Added text. + */ + public function generateLinesInsert(array $changes): string; + + /** + * Generate a string representation of lines that are "Out Of Context" for the diff view. + * + * @return string Representation of 'Out Of Context' lines. + */ + public function generateLinesOutOfContext($change): string; + + /** + * Generate a string representation of lines with ignored differences between both versions. + * + * @param array $changes Contains the op-codes about the changes between two blocks of text. + * + * @return string Text with no difference. + * @todo: Uncomment once deprecation period is over. + */ + // public function generateLinesIgnore(array $changes): string; + + /** + * Generate a string representation of lines that are removed from the 2nd version. + * + * @param array $changes Contains the op-codes about the changes between two blocks of text. + * + * @return string Removed text. + */ + public function generateLinesDelete(array $changes): string; + + /** + * Generate a string representation of lines that are partially modified. + * + * @param array $changes Contains the op-codes about the changes between two blocks of text. + * + * @return string Modified text. + */ + public function generateLinesReplace(array $changes): string; + + /** + * Generate a string representation of the end of a block. + * + * @param array $changes Contains the op-codes about the changes between two blocks of text. + * + * @return string End of the block. + */ + public function generateBlockFooter(array $changes): string; + + /** + * Generate a string representation of the end of a diff view. + * + * @return string End of the diff view. + */ + public function generateDiffFooter(): string; +} diff --git a/lib/jblond/Diff/Renderer/Text/Context.php b/lib/jblond/Diff/Renderer/Text/Context.php index 91d2c98f..75d9b46a 100644 --- a/lib/jblond/Diff/Renderer/Text/Context.php +++ b/lib/jblond/Diff/Renderer/Text/Context.php @@ -4,23 +4,23 @@ namespace jblond\Diff\Renderer\Text; -use jblond\Diff\Renderer\RendererAbstract; +use jblond\Diff\Renderer\MainRendererAbstract; /** * Context diff generator for PHP DiffLib. * - * PHP version 7.2 or greater + * PHP version 7.3 or greater * - * @package jblond\Diff\Renderer\Text - * @author Chris Boulton - * @author Mario Brandt - * @author Ferry Cools - * @copyright (c) 2009 Chris Boulton - * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 - * @link https://github.com/JBlond/php-diff + * @package jblond\Diff\Renderer\Text + * @author Chris Boulton + * @author Mario Brandt + * @author Ferry Cools + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff */ -class Context extends RendererAbstract +class Context extends MainRendererAbstract { /** * @var array Array of the different op-code tags and how they map to the context diff-view equivalent. @@ -37,20 +37,24 @@ class Context extends RendererAbstract * * @link https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Context.html#Detailed-Context * - * @return string The generated context diff-view. + * @return string|false The generated diff-view or false when there's no difference. */ - public function render(): string + public function render() { - $diff = ''; + $diff = false; $opCodes = $this->diff->getGroupedOpCodes(); - foreach ($opCodes as $group) { + foreach ($opCodes as $key => $group) { + if ($key % 2) { + // Skip lines which are Out Of Context. + continue; + } $diff .= "***************\n"; - $lastItem = count($group) - 1; - $start1 = $group['0']['1']; - $end1 = $group[$lastItem]['2']; - $start2 = $group['0']['3']; - $end2 = $group[$lastItem]['4']; + $lastItem = array_key_last($group); + $start1 = $group[0][1]; + $end1 = $group[$lastItem][2]; + $start2 = $group[0][3]; + $end2 = $group[$lastItem][4]; // Line to line header for version 1. $diffStart = $end1 - $start1 >= 2 ? $start1 + 1 . ',' : ''; @@ -65,6 +69,7 @@ public function render(): string // Line differences between versions or lines of version 1 are removed from version 2. // Add all operations to diff-view of version 1, except for insert. $filteredGroups = $this->filterGroups($group, 'insert'); + $filteredGroups = $this->filterGroups($filteredGroups, 'ignore'); foreach ($filteredGroups as [$tag, $start1, $end1, $start2, $end2]) { $diff .= $this->tagMap[$tag] . ' ' . implode( @@ -81,6 +86,7 @@ public function render(): string // Line differences between versions or lines are inserted into version 2. // Add all operations to diff-view of version 2, except for delete. $filteredGroups = $this->filterGroups($group, 'delete'); + $filteredGroups = $this->filterGroups($filteredGroups, 'ignore'); foreach ($filteredGroups as [$tag, $start1, $end1, $start2, $end2]) { $diff .= $this->tagMap[$tag] . ' ' . implode( @@ -99,8 +105,8 @@ public function render(): string * * Given an array of groups, all groups which don't have the specified tag are returned. * - * @param array $groups A series of opCode groups. - * @param string $excludedTag Name of the opCode Tag to filter out. + * @param array $groups A series of opCode groups. + * @param string $excludedTag Name of the opCode Tag to filter out. * * @return array Filtered opCode Groups. */ @@ -108,7 +114,7 @@ private function filterGroups(array $groups, string $excludedTag): array { return array_filter( $groups, - function ($operation) use ($excludedTag) { + static function ($operation) use ($excludedTag) { return $operation[0] != $excludedTag; } ); diff --git a/lib/jblond/Diff/Renderer/Text/InlineCli.php b/lib/jblond/Diff/Renderer/Text/InlineCli.php new file mode 100644 index 00000000..74e0951d --- /dev/null +++ b/lib/jblond/Diff/Renderer/Text/InlineCli.php @@ -0,0 +1,254 @@ + + * @copyright (c) 2020 Ferry Cools + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ +class InlineCli extends MainRenderer implements SubRendererInterface +{ + /** + * @var array Associative array containing the default options available for this renderer and their default + * value. + */ + private $subOptions = []; + + /** + * InlineCli constructor. + * + * @param array $options Custom defined options for the InlineCli diff renderer. + * + * @see InlineCli::$subOptions + */ + public function __construct(array $options = []) + { + parent::__construct($this->subOptions); + $this->setOptions($options); + } + + /** + * @inheritDoc + * + * @return string|false The generated diff-view or false when there's no difference. + */ + public function render() + { + $changes = parent::renderSequences(); + + return parent::renderOutput($changes, $this); + } + + + /** + * @inheritDoc + * + * @return string Start of the diff view. + */ + public function generateDiffHeader(): string + { + return ''; + } + + /** + * @inheritDoc + * + * @return string Start of the diff view. + */ + public function generateBlockHeader(array $changes): string + { + return ''; + } + + /** + * @inheritDoc + * + * @return string HTML code representing table rows showing text which is 'Out Of Context' + */ + public function generateLinesOutOfContext($change): string + { + return "...\n"; + } + + /** + * @inheritDoc + * + * @return string Text with no difference. + */ + public function generateLinesEqual(array $changes): string + { + $returnValue = ''; + $padding = str_repeat( + ' ', + max($this->maxLineMarkerWidth - strlen($this->options['equalityMarkers'][0]), 0) + ); + + foreach ($changes['base']['lines'] as $line) { + $returnValue .= $this->options['equalityMarkers'][0] . $padding . '|' . $line . "\n"; + } + + return $returnValue; + } + + /** + * @inheritDoc + * + * @return string Added text. + */ + public function generateLinesInsert(array $changes): string + { + $colorize = new CliColors(); + $returnValue = ''; + $padding = str_repeat( + ' ', + max($this->maxLineMarkerWidth - strlen($this->options['insertMarkers'][0]), 0) + ); + + foreach ($changes['changed']['lines'] as $line) { + if ($this->options['cliColor']) { + [$fgColor, $bgColor] = $this->options['insertColors']; + $line = $colorize->getColoredString($line, $fgColor, $bgColor); + } + $returnValue .= $this->options['insertMarkers'][0] . $padding . '|' . $line . "\n"; + } + + return $returnValue; + } + + /** + * @inheritDoc + * + * @return string Removed text. + */ + public function generateLinesDelete(array $changes): string + { + $colorize = new CliColors(); + $returnValue = ''; + $padding = str_repeat( + ' ', + max($this->maxLineMarkerWidth - strlen($this->options['deleteMarkers'][0]), 0) + ); + + foreach ($changes['base']['lines'] as $line) { + if ($this->options['cliColor']) { + [$fgColor, $bgColor] = $this->options['deleteColors']; + $line = $colorize->getColoredString($line, $fgColor, $bgColor); + } + $returnValue .= $this->options['deleteMarkers'][0] . $padding . '|' . $line . "\n"; + } + + return $returnValue; + } + + /** + * @inheritDoc + * + * @return string Modified text. + */ + public function generateLinesReplace(array $changes): string + { + $returnValue = ''; + + $changes['base']['lines'] = $this->mergeChanges( + $changes['base']['lines'], + $changes['changed']['lines'], + $this->options['deleteColors'], + $this->options['insertColors'] + ); + + $returnValue .= implode("\n", $changes['base']['lines']) . "\n"; + + return $returnValue; + } + + /** + * Merge the changes between two lines together and mark these changes. + * + * @param array $baseLines Lines of version 1. + * @param array $changedLines Lines of version 2. + * @param array|null[] $deleteColors Fore- and background colors of part that is removed from the 2nd version. + * @param array|null[] $insertColors Fore- and background colors of part that is added to the 2nd version. + * + * Option $deleteColors and $insertColors only have affect when this class's cliColors option is set to true. + * + * @return array + */ + private function mergeChanges( + array $baseLines, + array $changedLines, + array $deleteColors = [null, null], + array $insertColors = [null, null] + ): array { + $padding = str_repeat( + ' ', + max($this->maxLineMarkerWidth - strlen($this->options['equalityMarkers'][1]), 0) + ); + + foreach ($baseLines as $lineKey => $line) { + $iterator = 0; + $baselineParts = preg_split('/\x00(.*?)\x01/', $line, -1, PREG_SPLIT_DELIM_CAPTURE); + $changedLineParts = preg_split('/\x00(.*?)\x01/', $changedLines[$lineKey], -1, PREG_SPLIT_DELIM_CAPTURE); + + foreach ($baselineParts as $partKey => &$part) { + if ($iterator++ % 2) { + // This part of the line has been changed. Surround it with user defined markers. + $basePart = $this->options['deleteMarkers'][0] . $part . $this->options['deleteMarkers'][1]; + $changedPart = ''; + if (isset($changedLineParts[$partKey])) { + $changedPart = + $this->options['insertMarkers'][0] . + $changedLineParts[$partKey] . + $this->options['insertMarkers'][1]; + } + + if ($this->options['cliColor']) { + $colorize = new CliColors(); + $basePart = $colorize->getColoredString($basePart, ...$deleteColors); + if (!empty($changedPart)) { + $changedPart = $colorize->getColoredString($changedPart, ...$insertColors); + } + } + $part = $basePart . $changedPart; + } + } + unset($part); + $baseLines[$lineKey] = $this->options['equalityMarkers'][1] . $padding . '|' . implode('', $baselineParts); + } + + return $baseLines; + } + + /** + * @inheritDoc + * + * @return string End of the block + */ + public function generateBlockFooter(array $changes): string + { + return ''; + } + + /** + * @inheritDoc + * + * @return string End of the diff view. + */ + public function generateDiffFooter(): string + { + return ''; + } +} diff --git a/lib/jblond/Diff/Renderer/Text/Json.php b/lib/jblond/Diff/Renderer/Text/Json.php new file mode 100644 index 00000000..bffce672 --- /dev/null +++ b/lib/jblond/Diff/Renderer/Text/Json.php @@ -0,0 +1,89 @@ + + * @copyright (c) 2023 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ + +/** + * Class Diff_Renderer_Text_Json + */ +class Json extends MainRendererAbstract +{ + /** + * @var array Associative array containing the default options available for this renderer and their default + * value. + */ + private $subOptions = [ + 'json' => JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR + ]; + + /** + * json constructor. + * + * @param array $options Custom defined options for the json diff renderer. + * + * @see Json::$subOptions + */ + public function __construct(array $options = []) + { + if (!extension_loaded('json')) { + throw new RuntimeException('json extension is not available'); + } + parent::__construct($this->subOptions); + $this->setOptions($options); + } + + /** + * @inheritDoc + * + * @return false|string + */ + public function render() + { + $return = []; + $opCodes = $this->diff->getGroupedOpCodes(); + + foreach ($opCodes as $group) { + $return[] = $this->toArray($group); + } + return json_encode($return, $this->options['json']); + } + + /** + * Convert the + * @param $group + * @return array + */ + protected function toArray($group): array + { + $return = []; + foreach ($group as [$tag, $iGroup1, $iGroup2, $jGroup1, $jGroup2]) { + $return[] = [ + 'tag' => $tag, + 'old' => [ + 'offset' => $iGroup1, + 'lines' => $this->diff->getArrayRange($this->diff->getVersion1(), $iGroup1, $iGroup2) + ], + 'new' => [ + 'offset' => $jGroup1, + 'lines' => $this->diff->getArrayRange($this->diff->getVersion2(), $jGroup1, $jGroup2) + ], + ]; + } + return $return; + } +} diff --git a/lib/jblond/Diff/Renderer/Text/Unified.php b/lib/jblond/Diff/Renderer/Text/Unified.php index e1a8c616..261760f4 100644 --- a/lib/jblond/Diff/Renderer/Text/Unified.php +++ b/lib/jblond/Diff/Renderer/Text/Unified.php @@ -4,55 +4,60 @@ namespace jblond\Diff\Renderer\Text; -use jblond\Diff\Renderer\RendererAbstract; +use jblond\Diff\Renderer\MainRendererAbstract; /** * Unified diff generator for PHP DiffLib. * - * PHP version 7.2 or greater + * PHP version 7.3 or greater * - * @package jblond\Diff\Renderer\Text - * @author Chris Boulton - * @author Mario Brandt - * @copyright (c) 2009 Chris Boulton - * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 - * @link https://github.com/JBlond/php-diff + * @package jblond\Diff\Renderer\Text + * @author Chris Boulton + * @author Mario Brandt + * @copyright (c) 2020 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff */ /** * Class Diff_Renderer_Text_Unified */ -class Unified extends RendererAbstract +class Unified extends MainRendererAbstract { /** * Render and return a unified diff. * - * @return string The unified diff. + * @return string|false The generated diff-view or false when there's no difference. */ - public function render(): string + public function render() { - $diff = ''; + $diff = false; $opCodes = $this->diff->getGroupedOpCodes(); - foreach ($opCodes as $group) { - $lastItem = count($group) - 1; - $i1 = $group['0']['1']; - $i2 = $group[$lastItem]['2']; - $j1 = $group['0']['3']; - $j2 = $group[$lastItem]['4']; + foreach ($opCodes as $key => $group) { + if ($key % 2) { + // Skip lines which are Out Of Context. + continue; + } + $lastItem = array_key_last($group); + $iGroup1 = $group[0][1]; + $iGroup2 = $group[$lastItem][2]; + $jGroup1 = $group[0][3]; + $jGroup2 = $group[$lastItem][4]; - if ($i1 == 0 && $i2 == 0) { - $i1 = -1; - $i2 = -1; + if ($iGroup1 == 0 && $iGroup2 == 0) { + $iGroup1 = -1; + $iGroup2 = -1; } - $diff .= '@@ -' . ($i1 + 1) . ',' . ($i2 - $i1) . ' +' . ($j1 + 1) . ',' . ($j2 - $j1) . " @@\n"; - foreach ($group as [$tag, $i1, $i2, $j1, $j2]) { + $diff .= '@@ -' . ($iGroup1 + 1) . ',' . ($iGroup2 - $iGroup1) . ' +' . ($jGroup1 + 1) + . ',' . ($jGroup2 - $jGroup1) . " @@\n"; + foreach ($group as [$tag, $iGroup1, $iGroup2, $jGroup1, $jGroup2]) { if ($tag == 'equal') { $diff .= ' ' . implode( "\n ", - $this->diff->getArrayRange($this->diff->getVersion1(), $i1, $i2) + $this->diff->getArrayRange($this->diff->getVersion1(), $iGroup1, $iGroup2) ) . "\n"; continue; } @@ -60,18 +65,19 @@ public function render(): string $diff .= '-' . implode( "\n-", - $this->diff->getArrayRange($this->diff->getVersion1(), $i1, $i2) + $this->diff->getArrayRange($this->diff->getVersion1(), $iGroup1, $iGroup2) ) . "\n"; } if ($tag == 'replace' || $tag == 'insert') { $diff .= '+' . implode( "\n+", - $this->diff->getArrayRange($this->diff->getVersion2(), $j1, $j2) + $this->diff->getArrayRange($this->diff->getVersion2(), $jGroup1, $jGroup2) ) . "\n"; } } } + return $diff; } } diff --git a/lib/jblond/Diff/Renderer/Text/UnifiedCli.php b/lib/jblond/Diff/Renderer/Text/UnifiedCli.php new file mode 100644 index 00000000..dd0285d1 --- /dev/null +++ b/lib/jblond/Diff/Renderer/Text/UnifiedCli.php @@ -0,0 +1,133 @@ + + * @copyright (c) 2020 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ +class UnifiedCli extends MainRendererAbstract +{ + /** + * @var CliColors + */ + private $colors; + + /** + * @var array Associative array containing the default options available for this renderer and their default + * value. + */ + private $subOptions = []; + + /** + * UnifiedCli constructor. + * + * @param array $options Custom defined options for the UnifiedCli diff renderer. + * + */ + public function __construct(array $options = []) + { + parent::__construct(); + $this->setOptions($this->subOptions); + $this->setOptions($options); + $this->colors = new CliColors(); + } + + /** + * Render and return a unified diff. + * + * @return string Direct Output to the console + * @throws InvalidArgumentException + */ + public function render(): string + { + return $this->output(); + } + + /** + * Render and return a unified colored diff. + * + * @return string + */ + private function output(): string + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpCodes(); + foreach ($opCodes as $key => $group) { + if ($key % 2) { + // Skip lines which are Out Of Context. + continue; + } + $lastItem = array_key_last($group); + $iGroup1 = $group[0][1]; + $iGroup2 = $group[$lastItem][2]; + $jGroup1 = $group[0][3]; + $jGroup2 = $group[$lastItem][4]; + + if ($iGroup1 == 0 && $iGroup2 == 0) { + $iGroup1 = -1; + $iGroup2 = -1; + } + + $diff .= $this->colorizeString( + '@@ -' . ($iGroup1 + 1) . ',' . ($iGroup2 - $iGroup1) . ' +' . ($jGroup1 + 1) + . ',' . ($jGroup2 - $jGroup1) . " @@\n", + 'purple' + ); + + foreach ($group as [$tag, $iGroup1, $iGroup2, $jGroup1, $jGroup2]) { + if ($tag == 'equal') { + $string = implode( + "\n ", + $this->diff->getArrayRange($this->diff->getVersion1(), $iGroup1, $iGroup2) + ); + $diff .= $this->colorizeString(' ' . $string . "\n", 'grey'); + continue; + } + if ($tag == 'replace' || $tag == 'delete') { + $string = implode( + "\n- ", + $this->diff->getArrayRange($this->diff->getVersion1(), $iGroup1, $iGroup2) + ); + $diff .= $this->colorizeString('-' . $string . "\n", 'light_red'); + } + if ($tag == 'replace' || $tag == 'insert') { + $string = implode( + "\n+", + $this->diff->getArrayRange($this->diff->getVersion2(), $jGroup1, $jGroup2) + ); + $diff .= $this->colorizeString('+' . $string . "\n", 'light_green'); + } + } + } + + return $diff; + } + + /** + * @param string $string + * @param string $color + * + * @return string + */ + private function colorizeString(string $string, string $color = ''): string + { + if ($this->options['cliColor']) { + return $this->colors->getColoredString($string, $color); + } + + return $string; + } +} diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index 2d7f0ce5..ded67ee1 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -9,95 +9,100 @@ /** * Sequence matcher for Diff * - * PHP version 7.2 or greater + * PHP version 7.3 or greater * - * @package jblond\Diff - * @author Chris Boulton - * @author Mario Brandt - * @author Ferry Cools - * @copyright (c) 2009 Chris Boulton - * @license New BSD License http://www.opensource.org/licenses/bsd-license.php - * @version 1.18 - * @link https://github.com/JBlond/php-diff + * @package jblond\Diff + * @author Chris Boulton + * @author Mario Brandt + * @author Ferry Cools + * @copyright (c) 2020 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff */ -class SequenceMatcher +class SequenceMatcher implements ConstantsInterface { - /** - * @var string|array Either a string or an array containing a callback function to determine - * if a line is "junk" or not. - */ - private $junkCallback; - /** * @var array The first sequence to compare against. */ - private $old; - + protected $old; /** * @var array The second sequence. */ - private $new; - + protected $new; + /** + * @var array Associative array containing the options that will be applied for generating the diff. + * The key-value pairs are set at the constructor of this class. + * + * @see SequenceMatcher::setOptions() + */ + protected $options = []; + /** + * @var string|array Either a string or an array containing a callback function to determine + * if a line is "junk" or not. + */ + private $junkCallback; /** * @var array Array of characters that are considered junk from the second sequence. Characters are the array key. */ private $junkDict = []; - /** * @var array Array of indices that do not contain junk elements. */ private $b2j = []; - - /** - * @var array - */ - private $options = []; - /** - * @var null|array + * @var array A list of all the op-codes for the differences between the compared strings. */ private $opCodes; /** - * @var null|array + * @var array A nested set of arrays for all the matching sub-sequences the compared strings. */ private $matchingBlocks; /** - * @var array + * @var array Associative array containing the default options available for the diff class and their default value. + * + * - context The amount of lines to include around blocks that differ. + * - trimEqual Strip blocks of equal lines from the start and end of the text. + * - ignoreWhitespace True to ignore differences in tabs and spaces. + * - ignoreCase True to ignore differences in character casing. + * - ignoreLines 0: None. + * 1: Ignore empty lines. + * 2: Ignore blank lines. */ private $defaultOptions = [ - 'context' => 3, - 'trimEqual' => true, + 'context' => 3, + 'trimEqual' => true, 'ignoreWhitespace' => false, - 'ignoreCase' => false, - 'ignoreNewLines' => false, + 'ignoreCase' => false, + 'ignoreLines' => self::DIFF_IGNORE_LINE_NONE, ]; /** * The constructor. With the sequences being passed, they'll be set for the - * sequence matcher and it will perform a basic cleanup & calculate junk + * sequence matcher, and it will perform a basic cleanup & calculate junk * elements. * - * @param string|array $old A string or array containing the lines to compare against. - * @param string|array $new A string or array containing the lines to compare. - * @param array $options - * @param string|array|null $junkCallback Either an array or string that references a callback function - * (if there is one) to determine 'junk' characters. + * @param string|array $old A string or array containing the lines to compare against. + * @param string|array $new A string or array containing the lines to compare. + * @param array $options + * @param string|array|null $junkCallback Either an array or string that references a callback function + * (if there is one) to determine 'junk' characters. */ public function __construct($old, $new, array $options = [], $junkCallback = null) { - $this->old = []; - $this->new = []; + $this->old = []; + $this->new = []; $this->junkCallback = $junkCallback; $this->setOptions($options); $this->setSequences($old, $new); } /** - * @param array $options + * @param array $options */ - public function setOptions(array $options) + public function setOptions(array $options): void { if (isset($options['context']) && $options['context'] < 0) { throw new InvalidArgumentException('The context option cannot be a negative value!'); @@ -106,55 +111,63 @@ public function setOptions(array $options) } /** - * Set the first and second sequences to use with the sequence matcher. + * Set the first and second sequence to use with the sequence matcher. + * + * @param string|array $version1 A string or array containing the lines to compare against. + * @param string|array $version2 A string or array containing the lines to compare. * - * @param string|array $partA A string or array containing the lines to compare against. - * @param string|array $partB A string or array containing the lines to compare. + * @return void */ - public function setSequences($partA, $partB) + public function setSequences($version1, $version2): void { - $this->setSeq1($partA); - $this->setSeq2($partB); + $this->setSeq1($version1); + $this->setSeq2($version2); } /** - * Set the first sequence ($partA) and reset any internal caches to indicate that - * when calling the calculation methods, we need to recalculate them. + * Set the first sequence. + * + * Also resets internal caches to indicate that, when calling the calculation methods, we need to recalculate them. + * + * @param string|array|void $version1 The sequence to set as the first sequence. * - * @param string|array $partA The sequence to set as the first sequence. + * @return void */ - public function setSeq1($partA) + public function setSeq1($version1): void { - if (!is_array($partA)) { - $partA = str_split($partA); + if (!is_array($version1)) { + $version1 = mb_str_split($version1); } - if ($partA == $this->old) { + if ($version1 == $this->old) { return; } - $this->old = $partA; + $this->old = $version1; $this->matchingBlocks = null; - $this->opCodes = null; + $this->opCodes = null; } /** - * Set the second sequence ($partB) and reset any internal caches to indicate that - * when calling the calculation methods, we need to recalculate them. + * Set the second sequence. * - * @param string|array $partB The sequence to set as the second sequence. + * Also resets internal caches to indicate that, when calling the calculation methods, we need to recalculate them. + * + * @param string|array $version2 The sequence to set as the second sequence. + * + * @return void */ - public function setSeq2($partB) + public function setSeq2($version2): void { - if (!is_array($partB)) { - $partB = str_split($partB); + if (!is_array($version2)) { + $version2 = mb_str_split($version2); } - if ($partB == $this->new) { + if ($version2 == $this->new) { return; } - $this->new = $partB; + $this->new = $version2; $this->matchingBlocks = null; - $this->opCodes = null; + $this->opCodes = null; $this->chainB(); } @@ -162,26 +175,26 @@ public function setSeq2($partB) * Generate the internal arrays containing the list of junk and non-junk * characters for the second ($b) sequence. */ - private function chainB() + private function chainB(): void { - $length = count($this->new); - $this->b2j = []; + $length = count($this->new); + $this->b2j = []; $popularDict = []; - for ($i = 0; $i < $length; ++$i) { - $char = $this->new[$i]; + foreach ($this->new as $i => $iValue) { + $char = $iValue; if (isset($this->b2j[$char])) { if ($length >= 200 && count($this->b2j[$char]) * 100 > $length) { $popularDict[$char] = 1; unset($this->b2j[$char]); - } else { - $this->b2j[$char][] = $i; + continue; } - } else { - $this->b2j[$char] = [ - $i - ]; + + $this->b2j[$char][] = $i; + continue; } + + $this->b2j[$char] = [$i]; } // Remove leftovers @@ -208,159 +221,197 @@ private function chainB() } /** - * Checks if a particular character is in the junk dictionary - * for the list of junk characters. + * Return a series of nested arrays containing different groups of generated op codes for the differences between + * the strings with up to $this->options['context'] lines of surrounding content. * - * @param string $bString - * @return bool $b True if the character is considered junk. False if not. - */ - private function isBJunk(string $bString): bool - { - if (isset($this->junkDict[$bString])) { - return true; - } - - return false; - } - - /** - * 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 - * starts 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. + * Any large equal block of strings is separated into smaller subsets which are "Within- or Out Of Context". * - * @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 array Array containing the longest match that includes the starting position in $a, - * start in $b and the length/size. + * @return array Nested array of all the grouped op codes. */ - public function findLongestMatch(int $alo, int $ahi, int $blo, int $bhi): array + public function getGroupedOpCodes(): array { - $old = $this->old; - $new = $this->new; + $opCodes = $this->getOpCodes(); + $opCodes = $opCodes ?: [['equal', 0, 1, 0, 1,],]; - $bestI = $alo; - $bestJ = $blo; - $bestSize = 0; + if ($this->options['trimEqual']) { + if ($opCodes[0][0] == 'equal') { + // Remove equal sequences at the start of the text, but keep the context lines. + $opCodes[0] = [ + $opCodes[0][0], + max($opCodes[0][1], $opCodes[0][2] - $this->options['context']), + $opCodes[0][2], + max($opCodes[0][3], $opCodes[0][4] - $this->options['context']), + $opCodes[0][4], + ]; + } - $j2Len = []; - $nothing = []; + $lastItem = array_key_last($opCodes); + if ($opCodes[$lastItem][0] == 'equal') { + // Remove equal sequences at the end of the text, but keep the context lines. + [$tag, $item1, $item2, $item3, $item4] = $opCodes[$lastItem]; + $opCodes[$lastItem] = [ + $tag, + $item1, + min($item2, $item1 + $this->options['context']), + $item3, + min($item4, $item3 + $this->options['context']), + ]; + } + } - for ($i = $alo; $i < $ahi; ++$i) { - $newJ2Len = []; - $jDict = $this->arrayGetDefault($this->b2j, $old[$i], $nothing); - foreach ($jDict as $j) { - if ($j < $blo) { - continue; - } elseif ($j >= $bhi) { - break; - } + $maxRange = $this->options['context'] * 2; + $groups = []; + $newGroup = []; - $k = $this->arrayGetDefault($j2Len, $j - 1, 0) + 1; - $newJ2Len[$j] = $k; - if ($k > $bestSize) { - $bestI = $i - $k + 1; - $bestJ = $j - $k + 1; - $bestSize = $k; - } - } + foreach ($opCodes as [$tag, $item1, $item2, $item3, $item4]) { + if ($tag == 'equal' && $item2 - $item1 > $maxRange) { + // Count of equal lines is greater than defined maximum context. + // Define lines before "Out of Context". + $newGroup[] = [ + $tag, + $item1, + min($item2, $item1 + $this->options['context']), + $item3, + min($item4, $item3 + $this->options['context']), + ]; - $j2Len = $newJ2Len; - } + $groups[] = $newGroup; - while ( - $bestI > $alo && - $bestJ > $blo && - !$this->isBJunk($new[$bestJ - 1]) && - !$this->linesAreDifferent($bestI - 1, $bestJ - 1) - ) { - --$bestI; - --$bestJ; - ++$bestSize; - } + // Define lines which are "Out Of Context". + $newGroup = []; + $newGroup[] = [ + 'outOfContext', + min($item2, $item1 + $this->options['context']), + max($item1, $item2 - $this->options['context']), + min($item4, $item3 + $this->options['context']), + max($item3, $item4 - $this->options['context']), + ]; + $groups[] = $newGroup; - while ( - $bestI + $bestSize < $ahi && - ($bestJ + $bestSize) < $bhi && - !$this->isBJunk($new[$bestJ + $bestSize]) && - !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize) - ) { - ++$bestSize; - } + // Define start of lines after "Out Of Context". + $newGroup = []; + $item1 = max($item1, $item2 - $this->options['context']); + $item3 = max($item3, $item4 - $this->options['context']); + } - while ( - $bestI > $alo && - $bestJ > $blo && - $this->isBJunk($new[$bestJ - 1]) && - !$this->linesAreDifferent($bestI - 1, $bestJ - 1) - ) { - --$bestI; - --$bestJ; - ++$bestSize; + // Define lines "Within Context". + $newGroup[] = [$tag, $item1, $item2, $item3, $item4,]; } - while ( - $bestI + $bestSize < $ahi && - $bestJ + $bestSize < $bhi && - $this->isBJunk($new[$bestJ + $bestSize]) && - !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize) + if ( + !$this->options['trimEqual'] || + (!empty($newGroup) && !(count($newGroup) == 1 && $newGroup[0][0] == 'equal')) ) { - ++$bestSize; + // Add the last sequences when !trimEqual || When there are no differences between both versions. + $groups[] = $newGroup; } - return [ - $bestI, - $bestJ, - $bestSize - ]; + return $groups; } /** - * Check if the two lines at the given indexes are different or not. + * Return a list of all the op codes for the differences between the + * two strings. * - * @param int $aIndex Line number to check against in a. - * @param int $bIndex Line number to check against in b. - * @return bool True if the lines are different and false if not. + * The nested array returned contains an array describing the op code which includes: + * 0 - The type of tag (as described below) for the op code. + * 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 tags 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 array Array of the opcodes describing the differences between the strings. */ - public function linesAreDifferent(int $aIndex, int $bIndex): bool + public function getOpCodes(): array { - $lineA = $this->old[$aIndex]; - $lineB = $this->new[$bIndex]; - - if ($this->options['ignoreWhitespace']) { - $replace = ["\t", ' ']; - $lineA = str_replace($replace, '', $lineA); - $lineB = str_replace($replace, '', $lineB); + if (!empty($this->opCodes)) { + //Return the cached results. + return $this->opCodes; } - if ($this->options['ignoreCase']) { - $lineA = strtolower($lineA); - $lineB = strtolower($lineB); - } + $iFrom = 0; + $jFrom = 0; + $this->opCodes = []; + + $blocks = $this->getMatchingBlocks(); + foreach ($blocks as [$blockA, $blockB, $size]) { + $tag = ''; + if ($iFrom < $blockA && $jFrom < $blockB) { + $tag = 'replace'; + } elseif ($iFrom < $blockA) { + $tag = 'delete'; + } elseif ($jFrom < $blockB) { + $tag = 'insert'; + } + + if ($this->options['ignoreLines']) { + $slice1 = array_slice($this->old, $iFrom, $blockA - $iFrom); + $slice2 = array_slice($this->new, $jFrom, $blockB - $jFrom); + + if ($this->options['ignoreLines'] == 2) { + array_walk( + $slice1, + static function (&$line) { + $line = trim($line); + } + ); + array_walk( + $slice2, + static function (&$line) { + $line = trim($line); + } + ); + } + + if ( + ($tag == 'delete' && implode('', $slice1) == '') || + ($tag == 'insert' && implode('', $slice2) == '') + ) { + $tag = 'ignore'; + } + } - if ($lineA != $lineB) { - return true; + if ($tag) { + $this->opCodes[] = [ + $tag, + $iFrom, + $blockA, + $jFrom, + $blockB, + ]; + } + + $iFrom = $blockA + $size; + $jFrom = $blockB + $size; + + if ($size) { + $this->opCodes[] = [ + 'equal', + $blockA, + $iFrom, + $blockB, + $jFrom, + ]; + } } - return false; + return $this->opCodes; } /** - * Return a nested set of arrays for all of the matching sub-sequences + * Return a nested set of arrays for all 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. + * 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 array Nested array of the matching blocks, as described by the function. */ @@ -378,285 +429,220 @@ public function getMatchingBlocks(): array 0, $aLength, 0, - $bLength - ] + $bLength, + ], ]; $matchingBlocks = []; while (!empty($queue)) { - [$alo, $ahi, $blo, $bhi] = array_pop($queue); - $longestMatch = $this->findLongestMatch($alo, $ahi, $blo, $bhi); + [$aLower, $aUpper, $bLower, $bUpper] = array_pop($queue); + $longestMatch = $this->findLongestMatch($aLower, $aUpper, $bLower, $bUpper); [$list1, $list2, $list3] = $longestMatch; if ($list3) { $matchingBlocks[] = $longestMatch; - if ($alo < $list1 && $blo < $list2) { + if ($aLower < $list1 && $bLower < $list2) { $queue[] = [ - $alo, + $aLower, $list1, - $blo, - $list2 + $bLower, + $list2, ]; } - if ($list1 + $list3 < $ahi && $list2 + $list3 < $bhi) { + if ($list1 + $list3 < $aUpper && $list2 + $list3 < $bUpper) { $queue[] = [ $list1 + $list3, - $ahi, + $aUpper, $list2 + $list3, - $bhi + $bUpper, ]; } } } - usort( - $matchingBlocks, - function ($aArray, $bArray) { - return $this->tupleSort($aArray, $bArray); - } - ); + sort($matchingBlocks); - $i1 = 0; - $j1 = 0; - $k1 = 0; + $iMatchingBlock = 0; + $jMatchingBlock = 0; + $kMatchingBlock = 0; $nonAdjacent = []; foreach ($matchingBlocks as [$list4, $list5, $list6]) { - if ($i1 + $k1 == $list4 && $j1 + $k1 == $list5) { - $k1 += $list6; - } else { - if ($k1) { - $nonAdjacent[] = [ - $i1, - $j1, - $k1 - ]; - } - - $i1 = $list4; - $j1 = $list5; - $k1 = $list6; + if ($iMatchingBlock + $kMatchingBlock == $list4 && $jMatchingBlock + $kMatchingBlock == $list5) { + $kMatchingBlock += $list6; + continue; } + if ($kMatchingBlock) { + $nonAdjacent[] = [ + $iMatchingBlock, + $jMatchingBlock, + $kMatchingBlock, + ]; + } + + $iMatchingBlock = $list4; + $jMatchingBlock = $list5; + $kMatchingBlock = $list6; } - if ($k1) { + + if ($kMatchingBlock) { $nonAdjacent[] = [ - $i1, - $j1, - $k1 + $iMatchingBlock, + $jMatchingBlock, + $kMatchingBlock, ]; } $nonAdjacent[] = [ $aLength, $bLength, - 0 + 0, ]; $this->matchingBlocks = $nonAdjacent; + return $this->matchingBlocks; } /** - * Return a list of all of the op codes for the differences between the - * two strings. + * 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) * - * The nested array returned contains an array describing the op code - * which includes: - * 0 - The type of tag (as described below) for the op code. - * 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. + * Essentially, of all the maximal matching blocks, return the one that + * starts earliest in $a, and all of those maximal matching blocks that + * start earliest in $a, return the one that starts earliest in $b. * - * The different types of tags 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. + * 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. * - * @return array Array of the opcodes describing the differences between the strings. + * @param int $aLower The lower constraint for the first sequence. + * @param int $aUpper The upper constraint for the first sequence. + * @param int $bLower The lower constraint for the second sequence. + * @param int $bUpper The upper constraint for the second sequence. + * + * @return array Array containing the longest match that includes the starting position in $a, + * start in $b and the length/size. */ - public function getOpCodes(): array + public function findLongestMatch(int $aLower, int $aUpper, int $bLower, int $bUpper): array { - if (!empty($this->opCodes)) { - return $this->opCodes; - } - - $i = 0; - $j = 0; - $this->opCodes = []; + $old = $this->old; + $new = $this->new; - $blocks = $this->getMatchingBlocks(); - foreach ($blocks as [$ai, $bj, $size]) { - $tag = ''; - if ($i < $ai && $j < $bj) { - $tag = 'replace'; - } elseif ($i < $ai) { - $tag = 'delete'; - } elseif ($j < $bj) { - $tag = 'insert'; - } + $bestI = $aLower; + $bestJ = $bLower; + $bestSize = 0; - if ($tag) { - $this->opCodes[] = [ - $tag, - $i, - $ai, - $j, - $bj - ]; - } + $j2Len = []; + $nothing = []; - $i = $ai + $size; - $j = $bj + $size; + for ($i = $aLower; $i < $aUpper; ++$i) { + $newJ2Len = []; + $jDict = $this->b2j[$old[$i]] ?? $nothing; + foreach ($jDict as $j) { + if ($j < $bLower) { + continue; + } elseif ($j >= $bUpper) { + break; + } - if ($size) { - $this->opCodes[] = [ - 'equal', - $ai, - $i, - $bj, - $j - ]; + $kRatio = ($j2Len[$j - 1] ?? 0) + 1; + $newJ2Len[$j] = $kRatio; + if ($kRatio > $bestSize) { + $bestI = $i - $kRatio + 1; + $bestJ = $j - $kRatio + 1; + $bestSize = $kRatio; + } } - } - return $this->opCodes; - } - /** - * Return a series of nested arrays containing different groups of generated - * op codes for the differences between the strings with up to $this->options['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. - * - * @return array Nested array of all of the grouped op codes. - */ - public function getGroupedOpCodes(): array - { - $opCodes = $this->getOpCodes(); - if (empty($opCodes)) { - $opCodes = [ - [ - 'equal', - 0, - 1, - 0, - 1 - ] - ]; + $j2Len = $newJ2Len; } - if ($this->options['trimEqual']) { - if ($opCodes['0']['0'] == 'equal') { - // Remove sequences at the start which are out of context. - $opCodes['0'] = [ - $opCodes['0']['0'], - max($opCodes['0']['1'], $opCodes['0']['2'] - $this->options['context']), - $opCodes['0']['2'], - max($opCodes['0']['3'], $opCodes['0']['4'] - $this->options['context']), - $opCodes['0']['4'] - ]; - } - - $lastItem = count($opCodes) - 1; - if ($opCodes[$lastItem]['0'] == 'equal') { - [$tag, $item1, $item2, $item3, $item4] = $opCodes[$lastItem]; - // Remove sequences at the end which are out of context. - $opCodes[$lastItem] = [ - $tag, - $item1, - min($item2, $item1 + $this->options['context']), - $item3, - min($item4, $item3 + $this->options['context']) - ]; - } + while ( + $bestI > $aLower && + $bestJ > $bLower && + !$this->isBJunk($new[$bestJ - 1]) && + !$this->linesAreDifferent($bestI - 1, $bestJ - 1) + ) { + --$bestI; + --$bestJ; + ++$bestSize; } - $maxRange = $this->options['context'] * 2; - $groups = []; - $group = []; - - foreach ($opCodes as [$tag, $item1, $item2, $item3, $item4]) { - if ($tag == 'equal' && $item2 - $item1 > $maxRange) { - $group[] = [ - $tag, - $item1, - min($item2, $item1 + $this->options['context']), - $item3, - min($item4, $item3 + $this->options['context']) - ]; - $groups[] = $group; - $group = []; - $item1 = max($item1, $item2 - $this->options['context']); - $item3 = max($item3, $item4 - $this->options['context']); - } + while ( + $bestI + $bestSize < $aUpper && + ($bestJ + $bestSize) < $bUpper && + !$this->isBJunk($new[$bestJ + $bestSize]) && + !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize) + ) { + ++$bestSize; + } - $group[] = [ - $tag, - $item1, - $item2, - $item3, - $item4 - ]; + while ( + $bestI > $aLower && + $bestJ > $bLower && + $this->isBJunk($new[$bestJ - 1]) && + !$this->linesAreDifferent($bestI - 1, $bestJ - 1) + ) { + --$bestI; + --$bestJ; + ++$bestSize; } - if ($this->options['trimEqual'] || (!empty($group) && !(count($group) == 1 && $group[0][0] == 'equal'))) { - //Do not add the last sequences. They're out of context. - $groups[] = $group; + while ( + $bestI + $bestSize < $aUpper && + $bestJ + $bestSize < $bUpper && + $this->isBJunk($new[$bestJ + $bestSize]) && + !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize) + ) { + ++$bestSize; } - return $groups; + return [ + $bestI, + $bestJ, + $bestSize, + ]; } /** - * Helper function that provides the ability to return the value for a key - * in an array of it exists, or if it doesn't then return a default value. - * Essentially cleaner than doing a series of if (isset()) {} else {} calls. + * Checks if a particular character is in the junk dictionary + * for the list of junk characters. + * + * @param string $bString * - * @param array $array The array to search. - * @param string|int $key The key to check that exists. - * @param mixed $default The value to return as the default value if the key doesn't exist. - * @return mixed The value from the array if the key exists or otherwise the default. + * @return bool True if the character is considered junk. False if not. */ - private function arrayGetDefault(array $array, $key, $default) + private function isBJunk(string $bString): bool { - if (isset($array[$key])) { - return $array[$key]; - } - return $default; + return isset($this->junkDict[$bString]); } /** - * Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks + * Check if the two lines at the given indexes are different or not. * - * @param array $aArray First array to compare. - * @param array $bArray Second array to compare. - * @return int -1, 0 or 1, as expected by the usort function. + * @param int $aIndex Number of line to check against in A. + * @param int $bIndex Number of line to check against in B. + * + * @return bool True if the lines are different and false if not. */ - private function tupleSort(array $aArray, array $bArray): int + public function linesAreDifferent(int $aIndex, int $bIndex): bool { - $max = max(count($aArray), count($bArray)); - for ($counter = 0; $counter < $max; ++$counter) { - if ($aArray[$counter] < $bArray[$counter]) { - return -1; - } elseif ($aArray[$counter] > $bArray[$counter]) { - return 1; - } - } + $lineA = $this->old[$aIndex]; + $lineB = $this->new[$bIndex]; - if (count($aArray) == count($bArray)) { - return 0; + if ($this->options['ignoreWhitespace']) { + $replace = ["\t", ' ']; + $lineA = str_replace($replace, '', $lineA); + $lineB = str_replace($replace, '', $lineB); } - if (count($aArray) < count($bArray)) { - return -1; + + if ($this->options['ignoreCase']) { + $lineA = strtolower($lineA); + $lineB = strtolower($lineB); } - return 1; + + return $lineA != $lineB; } } diff --git a/lib/jblond/Diff/Similarity.php b/lib/jblond/Diff/Similarity.php new file mode 100644 index 00000000..59ace0c8 --- /dev/null +++ b/lib/jblond/Diff/Similarity.php @@ -0,0 +1,260 @@ + + * @author Ferry Cools + * @copyright (c) 2020 Ferry Cools + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ +class Similarity extends SequenceMatcher +{ + /** + * Default method for calculation similarity ratio. + */ + public const CALC_DEFAULT = 0; + /** + * Fast method for calculation similarity ratio. + */ + public const CALC_FAST = 1; + /** + * Fastest method for calculation similarity ratio. + */ + public const CALC_FASTEST = 2; + /** + * @var array Count of each unique sequence at version 2. + */ + private $uniqueCount2; + /** + * @var array Contains the indexes of lines which are stripped from the sequences by Similarity::stripLines(). + * @see Similarity::stripLines() + */ + private $stripped = ['old' => [], 'new' => []]; + + + /** + * @inheritDoc + */ + public function setSeq2($version2): void + { + $this->uniqueCount2 = null; + parent::setSeq2($version2); + } + + /** + * Return a measure of similarity between the two sequences. + * + * This will be a float value between 0 and 1. + * + * Tree calculation methods are available: + * self::CALC_DEFAULT: Default method. + * self::CALC_FAST: Faster calculation. Less cpu load & less accurate than above. + * self::CALC_FASTEST: Fastest calculation. Less cpu load & less accurate than above. + * + * @param int $type Calculation method. + * + * @return float The calculated ratio. + * + */ + public function getSimilarity(int $type = self::CALC_DEFAULT): float + { + if ($this->options['ignoreLines']) { + // Backup original sequences and filter non-blank lines. + $this->stripLines(); + } + + switch ($type) { + case self::CALC_FAST: + $ratio = $this->getRatioFast(); + $this->restoreLines(); + break; + case self::CALC_FASTEST: + $ratio = $this->getRatioFastest(); + $this->restoreLines(); + break; + default: + $this->setSequences($this->old, $this->new); + $matches = array_reduce( + $this->getMatchingBlocks(), + function ($carry, $item) { + return $this->ratioReduce($carry, $item); + }, + 0 + ); + + $ratio = $this->calculateRatio($matches, count($this->old) + count($this->new)); + $this->restoreLines(); + $this->setSequences($this->old, $this->new); + } + + return $ratio; + } + + /** + * Strip empty or blank lines from the sequences to compare. + * + */ + private function stripLines(): void + { + foreach (['old', 'new'] as $version) { + // Remove empty lines. + $this->$version = array_filter( + $this->$version, + function ($line, $index) use ($version) { + $sanitizedLine = $line; + if ($this->options['ignoreLines'] == self::DIFF_IGNORE_LINE_BLANK) { + $sanitizedLine = trim($line); + } + + if ($sanitizedLine == '') { + // Store line to be able to restore later. + $this->stripped[$version][$index] = $line; + + return false; + } + + return true; + }, + ARRAY_FILTER_USE_BOTH + ); + + // Re-index sequence. + $this->$version = array_values($this->$version); + } + } + + /** + * Quickly return an upper bound ratio for the similarity of the strings. + * + * This is quicker to compute than self::CALC_DEFAULT. + * + * @return float The calculated ratio. + */ + private function getRatioFast(): float + { + if ($this->uniqueCount2 === null) { + // Build unless cached. + $this->uniqueCount2 = []; + foreach ($this->new as $iteratorValue) { + $char = $iteratorValue; + $this->uniqueCount2[$char] = ($this->uniqueCount2[$char] ?? 0) + 1; + } + } + + $avail = []; + $matches = 0; + foreach ($this->old as $iteratorValue) { + $char = $iteratorValue; + $numb = $avail[$char] ?? ($this->uniqueCount2[$char] ?? 0); + $avail[$char] = $numb - 1; + if ($numb > 0) { + ++$matches; + } + } + + return $this->calculateRatio($matches, count($this->old) + count($this->new)); + } + + /** + * Helper function for calculating the ratio to measure similarity for the strings. + * + * The ratio is defined as being 2 * (number of matches / total length) + * + * @param int $matches The number of matches in the two strings. + * @param int $length The length of the two sequences. + * + * @return float The calculated ratio. + */ + private function calculateRatio(int $matches, int $length = 0): float + { + $returnValue = 1; + if ($length) { + return 2 * ($matches / $length); + } + + return $returnValue; + } + + private function restoreLines(): void + { + foreach (['old', 'new'] as $version) { + foreach ($this->stripped[$version] as $index => $line) { + array_splice($this->$version, $index, 0, $line); + } + } + } + + /** + * Return an upper bound ratio really quickly for the similarity of the strings. + * + * This is quicker to compute than self::CALC_DEFAULT and self::CALC_FAST. + * + * @return float The calculated ratio. + */ + private function getRatioFastest(): float + { + $aLength = count($this->old); + $bLength = count($this->new); + + return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); + } + + /** + * Helper function to calculate the number of matches for Ratio(). + * + * @param int $sum The running total for the number of matches. + * @param array $triple Array containing the matching block triple to add to the running total. + * + * @return int The new running total for the number of matches. + */ + private function ratioReduce(int $sum, array $triple): int + { + return $sum + ($triple[count($triple) - 1]); + } + + /** + * Get diff statistics + * + * @return array + */ + public function getDifference(): array + { + $return = [ + 'inserted' => 0, + 'deleted' => 0, + 'replaced' => 0, + ]; + + foreach ($this->getGroupedOpCodes() as $chunk) { + foreach ($chunk as [$string, $one, $two, $three, $four]) { + switch ($string) { + case 'delete': + $return['deleted'] += $two - $one; + break; + case 'insert': + $return['inserted'] += $four - $three; + break; + case 'replace': + $return['replaced'] += $two - $one; + break; + } + } + } + + $return['equal'] = count($this->old) - $return['replaced'] - $return['deleted']; + + return $return; + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..7b889f61 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + ./lib + + + + + tests + + + diff --git a/tests/Diff/Renderer/Html/HtmlArrayTest.php b/tests/Diff/Renderer/Html/HtmlArrayTest.php deleted file mode 100644 index 5b7313b5..00000000 --- a/tests/Diff/Renderer/Html/HtmlArrayTest.php +++ /dev/null @@ -1,91 +0,0 @@ -diff = new Diff( - ['a'], - [] - ); - $result = $htmlRenderer->render(); - static::assertEquals([ - [ - [ - 'tag' => 'delete', - 'base' => [ - 'offset' => 0, - 'lines' => [ - 'a' - ] - ], - 'changed' => [ - 'offset' => 0, - 'lines' => [] - ] - ] - ] - ], $result); - } - - /** - * - */ - public function testRenderFixesSpaces() - { - $htmlRenderer = new HtmlArray(); - $htmlRenderer->diff = new Diff( - [' a'], - ['a'] - ); - $result = $htmlRenderer->render(); - static::assertEquals([ - [ - [ - 'tag' => 'replace', - 'base' => [ - 'offset' => 0, - 'lines' => [ - "    a", - ] - ], - 'changed' => [ - 'offset' => 0, - 'lines' => [ - 'a' - ] - ] - ] - ] - ], $result); - } -} diff --git a/tests/Diff/Renderer/Html/HtmlRenderersTest.php b/tests/Diff/Renderer/Html/HtmlRenderersTest.php index 938008c7..4df97271 100644 --- a/tests/Diff/Renderer/Html/HtmlRenderersTest.php +++ b/tests/Diff/Renderer/Html/HtmlRenderersTest.php @@ -5,7 +5,7 @@ namespace Tests\Diff\Renderer\Html; use jblond\Diff; -use jblond\Diff\Renderer\Html\Inline; +use jblond\Diff\Renderer\Html\Merged; use jblond\Diff\Renderer\Html\SideBySide; use jblond\Diff\Renderer\Html\Unified; use PHPUnit\Framework\TestCase; @@ -13,11 +13,17 @@ /** * Class HtmlRendererTest * - * PHPUnit tests to verify the output of the HTML renderers hasn't change by code changes. + * PHPUnit tests to verify that the output of the HTML renderers did not change due to code changes. * - * @package Tests\Diff\Renderer\Html + * @package Tests\Diff\Renderer\Html + * @author Mario Brandt + * @author Ferry Cools + * @copyright (c) 2020 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff */ -class HtmlRendererTest extends TestCase +class HtmlRenderersTest extends TestCase { /** * @var bool Store the renderer's output in a file, when set to true. @@ -27,9 +33,9 @@ class HtmlRendererTest extends TestCase /** * Constructor. * - * @param null $name - * @param array $data - * @param string $dataName + * @param null $name + * @param array $data + * @param string $dataName */ public function __construct($name = null, array $data = [], $dataName = '') { @@ -37,15 +43,20 @@ public function __construct($name = null, array $data = [], $dataName = '') parent::__construct($name, $data, $dataName); } - public function testSideBySide() + /** + * Test the output of the HTML Side by Side renderer. + * + * @covers \jblond\Diff\Renderer\Html\SideBySide + */ + public function testSideBySide(): void { $diff = new Diff( file_get_contents('tests/resources/a.txt'), file_get_contents('tests/resources/b.txt') ); - $renderer = new SideBySide(); - $result = $diff->render($renderer); + $renderer = new SideBySide(); + $result = $diff->render($renderer); if ($this->genOutputFiles) { file_put_contents('htmlSideBySide.txt', $result); } @@ -53,31 +64,41 @@ public function testSideBySide() $this->assertStringEqualsFile('tests/resources/htmlSideBySide.txt', $result); } - public function testInline() + /** + * Test the output of the HTML Unified renderer. + * + * @covers \jblond\Diff\Renderer\Html\Merged + */ + public function testMerged(): void { $diff = new Diff( file_get_contents('tests/resources/a.txt'), file_get_contents('tests/resources/b.txt') ); - $renderer = new Inline(); - $result = $diff->render($renderer); + $renderer = new Merged(); + $result = $diff->render($renderer); if ($this->genOutputFiles) { - file_put_contents('htmlInline.txt', $result); + file_put_contents('htmlMerged.txt', $result); } - $this->assertStringEqualsFile('tests/resources/htmlInline.txt', $result); + $this->assertStringEqualsFile('tests/resources/htmlMerged.txt', $result); } - public function testUnified() + /** + * Test the output of the HTML Unified renderer. + * + * @covers \jblond\Diff\Renderer\Html\Unified + */ + public function testUnified(): void { $diff = new Diff( file_get_contents('tests/resources/a.txt'), file_get_contents('tests/resources/b.txt') ); - $renderer = new Unified(); - $result = $diff->render($renderer); + $renderer = new Unified(); + $result = $diff->render($renderer); if ($this->genOutputFiles) { file_put_contents('htmlUnified.txt', $result); } diff --git a/tests/Diff/Renderer/MainRendererTest.php b/tests/Diff/Renderer/MainRendererTest.php new file mode 100644 index 00000000..e6f0ba15 --- /dev/null +++ b/tests/Diff/Renderer/MainRendererTest.php @@ -0,0 +1,172 @@ + + * @author Ferry Cools + * @copyright (c) 2009 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ + +/** + * Class MainRendererTest + * + * @package Tests\Diff\Renderer\Html + */ +class MainRendererTest extends TestCase +{ + /** + * @var string[] Defines the main renderer options. + */ + private $rendererOptions = [ + 'format' => 'html', + ]; + + /** + * Test if a sequence of version1 which is removed from version2 is caught by the MainRenderer. + */ + public function testRenderSimpleDelete(): void + { + $renderer = new MainRenderer(); + $renderer->diff = new Diff(['a'], []); + + /** @noinspection PhpUnhandledExceptionInspection */ + $this->assertEquals( + [ + [ + [ + 'tag' => 'delete', + 'base' => [ + 'offset' => 0, + 'lines' => ['a'], + ], + 'changed' => [ + 'offset' => 0, + 'lines' => [], + ], + ], + ], + ], + $this->invokeMethod($renderer, 'renderSequences') + ); + } + + /** + * Call protected/private method of a class. + * + * @param object $object Instantiated object that we will run method on. + * @param string $methodName Method name to call + * @param array $parameters Array of parameters to pass into method. + * + * @return mixed The return value of the invoked method. + * @throws ReflectionException If the class doesn't exist. + */ + public function invokeMethod(object $object, string $methodName, array $parameters = []) + { + $reflection = new ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } + + /** + * Test if leading spaces of a sequence are replaced with html entities. + */ + public function testRenderFixesSpaces(): void + { + $renderer = new MainRenderer($this->rendererOptions); + $renderer->diff = new Diff( + [' a'], + ['a'] + ); + + /** @noinspection PhpUnhandledExceptionInspection */ + $this->assertEquals( + [ + [ + [ + 'tag' => 'replace', + 'base' => [ + 'offset' => 0, + 'lines' => [ + "\0    \1a", + ], + ], + 'changed' => [ + 'offset' => 0, + 'lines' => ["\0\1a"], + ], + ], + ], + ], + $this->invokeMethod($renderer, 'renderSequences') + ); + } + + /** + * Test inline marking for changes at line level. + * + * Everything from the first difference to the last difference should be enclosed by the markers. + * + * @throws ReflectionException When invoking the method fails. + */ + public function testMarkOuterChange(): void + { + $renderer = new MainRenderer(); + $text1 = ['one two three four']; + $text2 = ['one tWo thrEe four']; + $this->invokeMethod($renderer, 'markOuterChange', [&$text1, &$text2, 0, 1, 0]); + $this->assertSame(["one t\0wo thre\1e four"], $text1); + $this->assertSame(["one t\0Wo thrE\1e four"], $text2); + } + + /** + * Test inline marking for changes at character and word level. + * + * At character level, everything from a different character to any subsequent different character should be + * enclosed by the markers. + * + * At word level, every word that is different should be enclosed by the markers. + * + * @throws ReflectionException When invoking the method fails. + */ + public function testMarkInnerChange(): void + { + $renderer = new MainRenderer(); + + // Character level. + $renderer->setOptions(['inlineMarking' => $renderer::CHANGE_LEVEL_CHAR]); + $text1 = ['one two three four']; + $text2 = ['one tWo thrEe fouR']; + $this->invokeMethod($renderer, 'markInnerChange', [&$text1, &$text2, 0, 1, 0]); + $this->assertSame(["one t\0w\1o thr\0e\1e fou\0r\1"], $text1); + $this->assertSame(["one t\0W\1o thr\0E\1e fou\0R\1"], $text2); + + // Word Level. + $renderer->setOptions(['inlineMarking' => $renderer::CHANGE_LEVEL_WORD]); + $text1 = ['one two three four']; + $text2 = ['one tWo thrEe fouR']; + $this->invokeMethod($renderer, 'markInnerChange', [&$text1, &$text2, 0, 1, 0]); + $this->assertSame(["one \0two\1 \0three\1 \0four\1"], $text1); + $this->assertSame(["one \0tWo\1 \0thrEe\1 \0fouR\1"], $text2); + } +} diff --git a/tests/Diff/Renderer/Text/TextRenderersTest.php b/tests/Diff/Renderer/Text/TextRenderersTest.php index a08a0851..1452e74f 100644 --- a/tests/Diff/Renderer/Text/TextRenderersTest.php +++ b/tests/Diff/Renderer/Text/TextRenderersTest.php @@ -10,25 +10,31 @@ use PHPUnit\Framework\TestCase; /** - * Class TextRendererTest + * Class TextRenderersTest * - * PHPUnit tests to verify the output of the text renderers hasn't change by code changes. + * PHPUnit tests to verify that the output of the text renderers did not change due to code changes. * - * @package Tests\Diff\Renderer\Text + * @package Tests\Diff\Renderer\Text + * @author Mario Brandt + * @author Ferry Cools + * @copyright (c) 2019 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff */ -class TextRendererTest extends TestCase +class TextRenderersTest extends TestCase { /** * @var bool Store the renderer's output in a file, when set to true. */ - private $genOutputFiles = false; + protected $genOutputFiles = false; /** - * Constructor. + * TextRenderersTest constructor. * - * @param null $name - * @param array $data - * @param string $dataName + * @param null $name + * @param array $data + * @param string $dataName */ public function __construct($name = null, array $data = [], $dataName = '') { @@ -36,15 +42,20 @@ public function __construct($name = null, array $data = [], $dataName = '') parent::__construct($name, $data, $dataName); } - public function testContext() + /** + * Test the output of the text-context renderer. + * + * @covers \jblond\Diff\Renderer\Text\Context + */ + public function testContext(): void { $diff = new Diff( file_get_contents('tests/resources/a.txt'), file_get_contents('tests/resources/b.txt') ); - $renderer = new Context(); - $result = $diff->render($renderer); + $renderer = new Context(); + $result = $diff->render($renderer); if ($this->genOutputFiles) { file_put_contents('textContext.txt', $result); } @@ -52,19 +63,90 @@ public function testContext() $this->assertStringEqualsFile('tests/resources/textContext.txt', $result); } - public function testUnified() + /** + * Test the output of the text-unified renderer. + * + * @covers \jblond\Diff\Renderer\Text\Unified + */ + public function testUnified(): void { $diff = new Diff( file_get_contents('tests/resources/a.txt'), file_get_contents('tests/resources/b.txt') ); - $renderer = new Unified(); - $result = $diff->render($renderer); + $renderer = new Unified(); + $result = $diff->render($renderer); if ($this->genOutputFiles) { file_put_contents('textUnified.txt', $result); } $this->assertStringEqualsFile('tests/resources/textUnified.txt', $result); } + + /** + * Test the output of the CLI text-context renderer. + * + * @covers \jblond\Diff\Renderer\Text\UnifiedCli + */ + public function testUnifiedCli(): void + { + $diff = new Diff( + file_get_contents('tests/resources/a.txt'), + file_get_contents('tests/resources/b.txt') + ); + + $renderer = new Diff\Renderer\Text\UnifiedCli(); + $result = $diff->render($renderer); + if ($this->genOutputFiles) { + file_put_contents('textUnifiedCli.txt', $result); + } + $this->assertStringEqualsFile('tests/resources/textUnifiedCli.txt', $result); + } + + /** + * Test the output of the CLI text-inline renderer. + * + * @covers \jblond\Diff\Renderer\Text\InlineCli + */ + public function testInlineCli(): void + { + $diff = new Diff( + file_get_contents('tests/resources/a.txt'), + file_get_contents('tests/resources/b.txt') + ); + + $renderer = new Diff\Renderer\Text\InlineCli( + [ + 'cliColor' => true, + 'deleteMarkers' => ['-', '-'], + 'insertMarkers' => ['+', '+'], + 'equalityMarkers' => ['=', 'x'], + ] + ); + $result = $diff->render($renderer); + if ($this->genOutputFiles) { + file_put_contents('textInlineCli.txt', $result); + } + $this->assertStringEqualsFile('tests/resources/textInlineCli.txt', $result); + } + + /** + * Test the output of the JSON text renderer + * + * @covers \jblond\Diff\Renderer\Text\Json + */ + public function testJson(): void + { + $diff = new Diff( + file_get_contents('tests/resources/a.txt'), + file_get_contents('tests/resources/b.txt') + ); + $renderer = new Diff\Renderer\Text\Json(); + $result = $diff->render($renderer); + if ($this->genOutputFiles) { + file_put_contents('results.json', $result); + } + $this->assertStringEqualsFile('tests/resources/textResult.json', $result); + } } diff --git a/tests/Diff/SequenceMatcherTest.php b/tests/Diff/SequenceMatcherTest.php index f3cb0c00..3a767489 100644 --- a/tests/Diff/SequenceMatcherTest.php +++ b/tests/Diff/SequenceMatcherTest.php @@ -1,58 +1,149 @@ + * @author Ferry Cools + * @copyright (c) 2009 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ class SequenceMatcherTest extends TestCase { - /** - * Constructor. - * - * @param null $name - * @param array $data - * @param string $dataName + * Test the opCodes of the differences between version1 and version2 with the default options. */ - public function __construct($name = null, array $data = [], $dataName = '') - { - parent::__construct($name, $data, $dataName); - } - - public function testGetGroupedOpCodes() + public function testGetGroupedOpCodesDefault(): void { // Test with default options. - $sequenceMatcher = new SequenceMatcher('54321ABXDE12345', '54321ABxDE12345'); + $sequenceMatcher = new SequenceMatcher( + '54321ABXDE12345', + '54321ABxDE12345' + ); + $this->assertEquals( - [[['equal', 4, 7, 4, 7], ['replace', 7, 8, 7, 8], ['equal', 8, 11, 8, 11]]], + [ + [ + ['equal', 4, 7, 4, 7], + ['replace', 7, 8, 7, 8], + ['equal', 8, 11, 8, 11], + ], + ], $sequenceMatcher->getGroupedOpCodes() ); + } + /** + * Test the opCodes of the differences between version1 and version2 with option trimEqual disabled. + */ + public function testGetGroupedOpCodesTrimEqualFalse(): void + { // Test with trimEqual disabled. - $sequenceMatcher = new SequenceMatcher('54321ABXDE12345', '54321ABxDE12345', ['trimEqual' => false]); + // First and last context lines of the sequences are included. + $sequenceMatcher = new SequenceMatcher( + '54321ABXDE12345', + '54321ABxDE12345', + ['trimEqual' => false] + ); + $this->assertEquals( - [[['equal', 0, 3, 0, 3]], [['equal', 4, 7, 4, 7], ['replace', 7, 8, 7, 8], ['equal', 8, 11, 8, 11]]], + [ + [['equal', 0, 3, 0, 3]], + [['outOfContext', 3, 4, 3, 4]], + [['equal', 4, 7, 4, 7], ['replace', 7, 8, 7, 8], ['equal', 8, 11, 8, 11]], + [['outOfContext', 11, 12, 11, 12]], + [['equal', 12, 15, 12, 15]], + ], $sequenceMatcher->getGroupedOpCodes() ); + } - // Test with ignoreWhitespace enabled. + /** + * Test the opCodes of the differences between version1 and version2 with option IgnoreWhitespace enabled. + */ + public function testGetGroupedOpCodesIgnoreWhitespaceTrue(): void + { + // Test with ignoreWhitespace enabled. Both sequences are considered to be the same. // Note: The sequenceMatcher evaluates the string character by character. Option ignoreWhitespace will ignore - // if the difference if the character is a tab in one sequence and a space in the other. + // if the difference is the character or is a tab in one sequence and a space in the other. $sequenceMatcher = new SequenceMatcher( "\t54321ABXDE12345 ", " 54321ABXDE12345\t", ['ignoreWhitespace' => true] ); + + $this->assertEquals([], $sequenceMatcher->getGroupedOpCodes()); + } + + /** + * Test the opCodes of the differences between version1 and version2 with option ignoreCase enabled. + */ + public function testGetGroupedOpCodesIgnoreCaseTrue(): void + { + // Test with ignoreCase enabled. Both sequences are considered to be the same. + $sequenceMatcher = new SequenceMatcher( + '54321ABXDE12345', + '54321ABxDE12345', + ['ignoreCase' => true] + ); + + $this->assertEquals([], $sequenceMatcher->getGroupedOpCodes()); + } + + /** + * Test the opCodes of the differences between version1 and version2 with option ignoreLines set to empty. + */ + public function testGetGroupedOpCodesIgnoreLinesEmpty(): void + { + // Test with ignoreCase enabled. Both sequences are considered to be the same. + $sequenceMatcher = new SequenceMatcher( + [0, 1, 2, 3], + [0, 1, '', 2, 3], + ['ignoreLines' => SequenceMatcher::DIFF_IGNORE_LINE_EMPTY] + ); + $this->assertEquals( - [[['equal', 14, 17, 14, 17]]], + [ + [ + ['equal', 0, 2, 0, 2], + ['ignore', 2, 2, 2, 3], + ['equal', 2, 4, 3, 5], + ], + ], $sequenceMatcher->getGroupedOpCodes() ); + } + + /** + * Test the opCodes of the differences between version1 and version2 with option ignoreLines set to blank. + */ + public function testGetGroupedOpCodesIgnoreLinesBlank(): void + { + // Test with ignoreCase enabled. Both sequences are considered to be the same. + $sequenceMatcher = new SequenceMatcher( + [0, 1, 2, 3], + [0, 1, "\t", 2, 3], + ['ignoreLines' => SequenceMatcher::DIFF_IGNORE_LINE_BLANK] + ); - // Test with ignoreCase enabled. - $sequenceMatcher = new SequenceMatcher('54321ABXDE12345', '54321ABxDE12345', ['ignoreCase' => true]); $this->assertEquals( - [[['equal', 12, 15, 12, 15]]], + [ + [ + ['equal', 0, 2, 0, 2], + ['ignore', 2, 2, 2, 3], + ['equal', 2, 4, 3, 5], + ], + ], $sequenceMatcher->getGroupedOpCodes() ); } diff --git a/tests/Diff/SimilarityTest.php b/tests/Diff/SimilarityTest.php new file mode 100644 index 00000000..abab54c7 --- /dev/null +++ b/tests/Diff/SimilarityTest.php @@ -0,0 +1,50 @@ + + * @copyright (c) 2020 Ferry Cools + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.4.0 + * @link https://github.com/JBlond/php-diff + */ +class SimilarityTest extends TestCase +{ + /** + * Test the similarity ratio between two sequences with different methods. + */ + public function testGetSimilarity(): void + { + $similarity = new Similarity(range(1, 10), range(1, 5)); + + $this->assertEquals(2 / 3, $similarity->getSimilarity(Similarity::CALC_DEFAULT)); + $this->assertEquals(2 / 3, $similarity->getSimilarity(Similarity::CALC_FAST)); + $this->assertEquals(2 / 3, $similarity->getSimilarity(Similarity::CALC_FASTEST)); + } + + /** + * Test the statistics function + */ + public function testGetDifference(): void + { + $similarity = new Similarity(range(0, 10), range(1, 23)); + $this->assertEquals( + [ + 'inserted' => 13, + 'deleted' => 1, + 'equal' => 10, + 'replaced' => 0, + ], + $similarity->getDifference() + ); + } +} diff --git a/tests/resources/a.txt b/tests/resources/a.txt index cdf834c1..066eedc0 100644 --- a/tests/resources/a.txt +++ b/tests/resources/a.txt @@ -7,7 +7,7 @@

This is demo content to show features of the php-diff package.

This line is removed from version2.

This line is the same for both versions.

-

This line has inline differences between both versions.

+

this line has inline differences between both versions.

This line is the same for both versions.

This line also has inline differences between both versions.

This line is the same for both versions.

diff --git a/tests/resources/htmlInline.txt b/tests/resources/htmlInline.txt deleted file mode 100644 index f28e973f..00000000 --- a/tests/resources/htmlInline.txt +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Version1Version2Differences
11<html>
22    <head>
33        <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
4  -         <title>Hello World!</title> -
 4 -         <title>Hello You!</title> -
55    </head>
66    <body>
77        <h1>This is demo content to show features of the php-diff package.</h1>
8  -         <h2>This line is removed from version2.</h2> -   -
98        <h2>This line is the same for both versions.</h2>
10  -         <h2>This line has inline differences between both versions.</h2> -
 9 -         <h2>This line has differences between both versions.</h2> -
1110        <h2>This line is the same for both versions.</h2>
12  -         <h2>This line also has inline differences between both versions.</h2> -
 11 -         <h2>This line also has InLine differences between both versions.</h2> -
1312        <h2>This line is the same for both versions.</h2>
 13 -         <h2>This line is added to version2.</h2> -   -
1414
1515        <p>
1616            It's also compatible with multibyte characters (like Chinese and emoji) as shown below:
17  -             另外我覺得那個評的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革” -
18  -             Do you know what "金槍魚罐頭" means in Chinese? -
19  -             🍏🍎🙂 -
 17 -             另外我覺得那個評的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革” -
 18 -             Do you know what "魚の缶詰" means in Chinese? -
 19 -             🍎🍏🙂 -
2020        </p>
2121
2222        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
2525        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
2626        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
2727        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
28  -         <h2>This line also has inline differences between both versions. It's the whitespace in front.</h2> -
 28 -         <h2>This line also has inline differences between both versions. It's the whitespace in front.</h2> -
2929        <h2>This line is the same for both versions.</h2>
30  -         <h2>This line also has inline differences between both versions.</h2> -
 30 -         <h2>This line also has inline differences between both versions!</h2> -
3131    </body>
3232</html>
3333
\ No newline at end of file diff --git a/tests/resources/htmlMerged.txt b/tests/resources/htmlMerged.txt new file mode 100644 index 00000000..ead95a7c --- /dev/null +++ b/tests/resources/htmlMerged.txt @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Merge of Version1 & Version2
1<html>
2    <head>
3        <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
4        <title>Hello WorldYou!</title>
5    </head>
6    <body>
7        <h1>This is demo content to show features of the php-diff package.</h1>
8        <h2>This line is the same for both versions.</h2>
9        <h2>this line has inlineThis line has differences between both versions.</h2>
10        <h2>This line is the same for both versions.</h2>
11        <h2>This line also has inlInLine differences between both versions.</h2>
12        <h2>This line is the same for both versions.</h2>
13        <h2>This line is added to version2.</h2>
14
15        <p>
16            It's also compatible with multibyte characters (like Chinese and emoji) as shown below:
17            另外我覺得那個評的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革”
18            Do you know what "金槍魚罐頭魚の缶詰" means in Chinese?
19            🍏🍎🍎🍏🙂
20        </p>
21
22        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
25        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
26        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
27        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
28                <h2>This line also has inline differences between both versions. It's the whitespace in front.</h2>
29        <h2>This line is the same for both versions.</h2>
30        <h2>This line also has inline differences between both versions.!</h2>
31    </body>
32</html>
33
\ No newline at end of file diff --git a/tests/resources/htmlSideBySide.txt b/tests/resources/htmlSideBySide.txt index fcbce226..9d114465 100644 --- a/tests/resources/htmlSideBySide.txt +++ b/tests/resources/htmlSideBySide.txt @@ -86,11 +86,11 @@ 10 -         <h2>This line has inline differences between both versions.</h2> +         <h2>this line has inline differences between both versions.</h2> 9 -         <h2>This line has differences between both versions.</h2> +         <h2>This line has differences between both versions.</h2> 11 @@ -207,12 +207,12 @@         <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p> - + … … … … - + 25         <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p> diff --git a/tests/resources/htmlUnified.txt b/tests/resources/htmlUnified.txt index 35b0c9e9..5eb2dc15 100644 --- a/tests/resources/htmlUnified.txt +++ b/tests/resources/htmlUnified.txt @@ -1 +1,210 @@ -<html>
    <head>
        <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
        <title>Hello World!</title>
        <title>Hello You!</title>
    </head>
    <body>
        <h1>This is demo content to show features of the php-diff package.</h1>
        <h2>This line is removed from version2.</h2>
        <h2>This line is the same for both versions.</h2>
        <h2>This line has inline differences between both versions.</h2>
        <h2>This line has differences between both versions.</h2>
        <h2>This line is the same for both versions.</h2>
        <h2>This line also has inline differences between both versions.</h2>
        <h2>This line also has InLine differences between both versions.</h2>
        <h2>This line is the same for both versions.</h2>
        <h2>This line is added to version2.</h2>

        <p>
            It's also compatible with multibyte characters (like Chinese and emoji) as shown below:
            另外我覺得那個評的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革”
            Do you know what "金槍魚罐頭" means in Chinese?
            🍏🍎🙂
            另外我覺得那個評的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革”
            Do you know what "魚の缶詰" means in Chinese?
            🍎🍏🙂
        </p>

        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
        <h2>This line also has inline differences between both versions. It's the whitespace in front.</h2>
        <h2>This line also has inline differences between both versions. It's the whitespace in front.</h2>
        <h2>This line is the same for both versions.</h2>
        <h2>This line also has inline differences between both versions.</h2>
        <h2>This line also has inline differences between both versions!</h2>
    </body>
</html>

\ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Version1Version2Differences
11<html>
22    <head>
33        <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
4  +         <title>Hello World!</title> +
 4 +         <title>Hello You!</title> +
55    </head>
66    <body>
77        <h1>This is demo content to show features of the php-diff package.</h1>
8  +         <h2>This line is removed from version2.</h2> +   +
98        <h2>This line is the same for both versions.</h2>
10  +         <h2>this line has inline differences between both versions.</h2> +
 9 +         <h2>This line has differences between both versions.</h2> +
1110        <h2>This line is the same for both versions.</h2>
12  +         <h2>This line also has inline differences between both versions.</h2> +
 11 +         <h2>This line also has InLine differences between both versions.</h2> +
1312        <h2>This line is the same for both versions.</h2>
 13 +         <h2>This line is added to version2.</h2> +   +
1414
1515        <p>
1616            It's also compatible with multibyte characters (like Chinese and emoji) as shown below:
17  +             另外我覺得那個評的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革” +
18  +             Do you know what "金槍魚罐頭" means in Chinese? +
19  +             🍏🍎🙂 +
 17 +             另外我覺得那個評的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革” +
 18 +             Do you know what "魚の缶詰" means in Chinese? +
 19 +             🍎🍏🙂 +
2020        </p>
2121
2222        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
2525        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
2626        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
2727        <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.</p>
28  +         <h2>This line also has inline differences between both versions. It's the whitespace in front.</h2> +
 28 +         <h2>This line also has inline differences between both versions. It's the whitespace in front.</h2> +
2929        <h2>This line is the same for both versions.</h2>
30  +         <h2>This line also has inline differences between both versions.</h2> +
 30 +         <h2>This line also has inline differences between both versions!</h2> +
3131    </body>
3232</html>
3333
\ No newline at end of file diff --git a/tests/resources/textContext.txt b/tests/resources/textContext.txt index 034c6c13..8d234956 100644 --- a/tests/resources/textContext.txt +++ b/tests/resources/textContext.txt @@ -9,7 +9,7 @@

This is demo content to show features of the php-diff package.

-

This line is removed from version2.

This line is the same for both versions.

-!

This line has inline differences between both versions.

+!

this line has inline differences between both versions.

This line is the same for both versions.

!

This line also has inline differences between both versions.

This line is the same for both versions.

diff --git a/tests/resources/textInlineCli.txt b/tests/resources/textInlineCli.txt new file mode 100644 index 00000000..b3b08400 --- /dev/null +++ b/tests/resources/textInlineCli.txt @@ -0,0 +1,33 @@ +=| +=| +=| +x| Hello -World-+You+! +=| +=| +=|

This is demo content to show features of the php-diff package.

+-|

This line is removed from version2.

 +=|

This line is the same for both versions.

+x|

-this line has inline-+This line has+ differences between both versions.

+=|

This line is the same for both versions.

+x|

This line also has -inl-+InL+ine differences between both versions.

+=|

This line is the same for both versions.

++|

This line is added to version2.

 +=| +=|

+=| It's also compatible with multibyte characters (like Chinese and emoji) as shown below: +x| 另外我覺得那個評-價-+鑑+的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革” +x| Do you know what "-金槍魚罐頭-+魚の缶詰+" means in Chinese? +x| -🍏🍎-+🍎🍏+🙂 +=|

+=| +=|

Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.

+... +=|

Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.

+=|

Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.

+=|

Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.

+x|- -+ +

This line also has inline differences between both versions. It's the whitespace in front.

+=|

This line is the same for both versions.

+x|

This line also has inline differences between both versions-.-+!+

+=| +=| +=| diff --git a/tests/resources/textResult.json b/tests/resources/textResult.json new file mode 100644 index 00000000..0f1c1b16 --- /dev/null +++ b/tests/resources/textResult.json @@ -0,0 +1,319 @@ +[ + [ + { + "tag": "equal", + "old": { + "offset": 0, + "lines": [ + "", + " ", + " " + ] + }, + "new": { + "offset": 0, + "lines": [ + "", + " ", + " " + ] + } + }, + { + "tag": "replace", + "old": { + "offset": 3, + "lines": [ + " Hello World!<\/title>" + ] + }, + "new": { + "offset": 3, + "lines": [ + " <title>Hello You!<\/title>" + ] + } + }, + { + "tag": "equal", + "old": { + "offset": 4, + "lines": [ + " <\/head>", + " <body>", + " <h1>This is demo content to show features of the php-diff package.<\/h1>" + ] + }, + "new": { + "offset": 4, + "lines": [ + " <\/head>", + " <body>", + " <h1>This is demo content to show features of the php-diff package.<\/h1>" + ] + } + }, + { + "tag": "delete", + "old": { + "offset": 7, + "lines": [ + " <h2>This line is removed from version2.<\/h2>" + ] + }, + "new": { + "offset": 7, + "lines": [] + } + }, + { + "tag": "equal", + "old": { + "offset": 8, + "lines": [ + " <h2>This line is the same for both versions.<\/h2>" + ] + }, + "new": { + "offset": 7, + "lines": [ + " <h2>This line is the same for both versions.<\/h2>" + ] + } + }, + { + "tag": "replace", + "old": { + "offset": 9, + "lines": [ + " <h2>this line has inline differences between both versions.<\/h2>" + ] + }, + "new": { + "offset": 8, + "lines": [ + " <h2>This line has differences between both versions.<\/h2>" + ] + } + }, + { + "tag": "equal", + "old": { + "offset": 10, + "lines": [ + " <h2>This line is the same for both versions.<\/h2>" + ] + }, + "new": { + "offset": 9, + "lines": [ + " <h2>This line is the same for both versions.<\/h2>" + ] + } + }, + { + "tag": "replace", + "old": { + "offset": 11, + "lines": [ + " <h2>This line also has inline differences between both versions.<\/h2>" + ] + }, + "new": { + "offset": 10, + "lines": [ + " <h2>This line also has InLine differences between both versions.<\/h2>" + ] + } + }, + { + "tag": "equal", + "old": { + "offset": 12, + "lines": [ + " <h2>This line is the same for both versions.<\/h2>" + ] + }, + "new": { + "offset": 11, + "lines": [ + " <h2>This line is the same for both versions.<\/h2>" + ] + } + }, + { + "tag": "insert", + "old": { + "offset": 13, + "lines": [] + }, + "new": { + "offset": 12, + "lines": [ + " <h2>This line is added to version2.<\/h2>" + ] + } + }, + { + "tag": "equal", + "old": { + "offset": 13, + "lines": [ + "", + " <p>", + " It's also compatible with multibyte characters (like Chinese and emoji) as shown below:" + ] + }, + "new": { + "offset": 13, + "lines": [ + "", + " <p>", + " It's also compatible with multibyte characters (like Chinese and emoji) as shown below:" + ] + } + }, + { + "tag": "replace", + "old": { + "offset": 16, + "lines": [ + " 另外我覺得那個評價的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革”", + " Do you know what \"金槍魚罐頭\" means in Chinese?", + " 🍏🍎🙂" + ] + }, + "new": { + "offset": 16, + "lines": [ + " 另外我覺得那個評鑑的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革”", + " Do you know what \"魚の缶詰\" means in Chinese?", + " 🍎🍏🙂" + ] + } + }, + { + "tag": "equal", + "old": { + "offset": 19, + "lines": [ + " <\/p>", + "", + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>" + ] + }, + "new": { + "offset": 19, + "lines": [ + " <\/p>", + "", + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>" + ] + } + } + ], + [ + { + "tag": "outOfContext", + "old": { + "offset": 22, + "lines": [ + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>", + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>" + ] + }, + "new": { + "offset": 22, + "lines": [ + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>", + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>" + ] + } + } + ], + [ + { + "tag": "equal", + "old": { + "offset": 24, + "lines": [ + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>", + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>", + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>" + ] + }, + "new": { + "offset": 24, + "lines": [ + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>", + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>", + " <p>Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.<\/p>" + ] + } + }, + { + "tag": "replace", + "old": { + "offset": 27, + "lines": [ + "\t\t<h2>This line also has inline differences between both versions. It's the whitespace in front.<\/h2>" + ] + }, + "new": { + "offset": 27, + "lines": [ + " <h2>This line also has inline differences between both versions. It's the whitespace in front.<\/h2>" + ] + } + }, + { + "tag": "equal", + "old": { + "offset": 28, + "lines": [ + " <h2>This line is the same for both versions.<\/h2>" + ] + }, + "new": { + "offset": 28, + "lines": [ + " <h2>This line is the same for both versions.<\/h2>" + ] + } + }, + { + "tag": "replace", + "old": { + "offset": 29, + "lines": [ + " <h2>This line also has inline differences between both versions.<\/h2>" + ] + }, + "new": { + "offset": 29, + "lines": [ + " <h2>This line also has inline differences between both versions!<\/h2>" + ] + } + }, + { + "tag": "equal", + "old": { + "offset": 30, + "lines": [ + " <\/body>", + "<\/html>", + "" + ] + }, + "new": { + "offset": 30, + "lines": [ + " <\/body>", + "<\/html>", + "" + ] + } + } + ] +] \ No newline at end of file diff --git a/tests/resources/textUnified.txt b/tests/resources/textUnified.txt index 1802b813..b5976f95 100644 --- a/tests/resources/textUnified.txt +++ b/tests/resources/textUnified.txt @@ -9,7 +9,7 @@ <h1>This is demo content to show features of the php-diff package.</h1> - <h2>This line is removed from version2.</h2> <h2>This line is the same for both versions.</h2> -- <h2>This line has inline differences between both versions.</h2> +- <h2>this line has inline differences between both versions.</h2> + <h2>This line has differences between both versions.</h2> <h2>This line is the same for both versions.</h2> - <h2>This line also has inline differences between both versions.</h2> diff --git a/tests/resources/textUnifiedCli.txt b/tests/resources/textUnifiedCli.txt new file mode 100644 index 00000000..a8842c05 --- /dev/null +++ b/tests/resources/textUnifiedCli.txt @@ -0,0 +1,42 @@ +@@ -1,22 +1,22 @@ + <html> + <head> + <meta http-equiv="Content-type" content="text/html; charset=utf-8"/> +- <title>Hello World! ++ Hello You! + + +

This is demo content to show features of the php-diff package.

+-

This line is removed from version2.

+

This line is the same for both versions.

+-

this line has inline differences between both versions.

++

This line has differences between both versions.

+

This line is the same for both versions.

+-

This line also has inline differences between both versions.

++

This line also has InLine differences between both versions.

+

This line is the same for both versions.

++

This line is added to version2.

+ +

+ It's also compatible with multibyte characters (like Chinese and emoji) as shown below: +- 另外我覺得那個評價的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革” +- Do you know what "金槍魚罐頭" means in Chinese? +- 🍏🍎🙂 ++ 另外我覺得那個評鑑的白色櫃子有點沒有必要欸。外觀我就不說了 ,怎麼連空間都那麼狹隘。不過倒是從這個地方看出所謂的“改革” ++ Do you know what "魚の缶詰" means in Chinese? ++ 🍎🍏🙂 +

+ +

Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.

+@@ -25,9 +25,9 @@ +

Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.

+

Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.

+

Just some lines to demonstrate the collapsing of a block of lines which are the same in both versions.

+-

This line also has inline differences between both versions. It's the whitespace in front.

++

This line also has inline differences between both versions. It's the whitespace in front.

+

This line is the same for both versions.

+-

This line also has inline differences between both versions.

++

This line also has inline differences between both versions!

+ + +