sistemazione composer install

This commit is contained in:
mariano
2026-05-22 14:13:33 +02:00
parent ad42773519
commit ed29fbc62d
252 changed files with 52727 additions and 13 deletions
+91
View File
@@ -0,0 +1,91 @@
<?php
$config = (new PhpCsFixer\Config())
->setIndent(" ")
->setLineEnding("\n")
->setCacheFile(__DIR__ . '/.php-cs-fixer.cache')
->setRiskyAllowed(true)
->setRules([
'@PHP71Migration' => true,
'@PHP71Migration:risky' => true,
'@PHP73Migration' => true,
'@PHP74Migration' => true,
'@PHP74Migration:risky' => true,
'@PHP80Migration' => false,
'@PHP80Migration:risky' => true,
'@PHP81Migration' => false,
'@PhpCsFixer' => true,
'@PhpCsFixer:risky' => true,
'@PSR12' => true,
'@Symfony' => true,
'@Symfony:risky' => true,
'align_multiline_comment' => true,
'array_indentation' => true,
'array_syntax' => ['syntax' => 'short'],
'combine_consecutive_issets' => true,
'combine_consecutive_unsets' => true,
'combine_nested_dirname' => true,
'comment_to_phpdoc' => true,
'compact_nullable_type_declaration' => true,
'concat_space' => ['spacing' => 'one'],
'echo_tag_syntax' => ['format' => 'short'],
'fully_qualified_strict_types' => true,
'heredoc_indentation' => false, // disabling this makes test cases more readable
'linebreak_after_opening_tag' => true,
'list_syntax' => ['syntax' => 'short'],
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'],
'native_constant_invocation' => ['scope' => 'all'],
'native_function_invocation' => ['scope' => 'all', 'include' => ['@compiler_optimized']],
'native_type_declaration_casing' => true,
'no_alternative_syntax' => true,
'no_null_property_initialization' => true,
'no_superfluous_elseif' => true,
'no_trailing_whitespace_in_string' => false, // test cases have trailing spaces
'no_unneeded_control_parentheses' => true,
'no_useless_else' => true,
'no_useless_return' => true,
'not_operator_with_space' => false,
'not_operator_with_successor_space' => false,
'ordered_class_elements' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha', 'imports_order' => ['class', 'const', 'function']],
'ordered_interfaces' => true,
'php_unit_set_up_tear_down_visibility' => true,
'php_unit_strict' => true,
'php_unit_test_class_requires_covers' => true,
'phpdoc_add_missing_param_annotation' => true,
'phpdoc_order' => true,
'phpdoc_order_by_value' => ['annotations' => ['covers']],
'phpdoc_to_comment' => false,
'phpdoc_types_order' => true,
'pow_to_exponentiation' => true,
'random_api_migration' => true,
'return_assignment' => false,
'simple_to_complex_string_variable' => true,
'single_line_comment_style' => true,
'single_trait_insert_per_statement' => true,
'static_lambda' => false,
'strict_comparison' => false,
'strict_param' => false,
'string_implicit_backslashes' => false,
'string_line_ending' => true,
'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arrays', 'arguments']],
'types_spaces' => ['space_multiple_catch' => 'single'],
'yoda_style' => false,
])
->setFinder(
PhpCsFixer\Finder::create()
->notPath([
'/branch-\\w+/', // git worktree
'example/demo_web.php',
])
->exclude('libs')
->exclude('tests/data')
->exclude('tests/Fixtures')
->exclude('var')
->exclude('vendor')
->in(__DIR__)
)
;
return $config;
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace PHPSTORM_META;
override(
\Jfcherng\Diff\Factory\LineRendererFactory::getInstance(0),
map(['' => 'Jfcherng\Diff\Renderer\Html\LineRenderer\@'])
);
override(
\Jfcherng\Diff\Factory\LineRendererFactory::make(0),
map(['' => 'Jfcherng\Diff\Renderer\Html\LineRenderer\@'])
);
override(
\Jfcherng\Diff\Factory\RendererFactory::getInstance(0),
map([
// html
'Combined' => \Jfcherng\Diff\Renderer\Html\Combined::class,
'Inline' => \Jfcherng\Diff\Renderer\Html\Inline::class,
'Json' => \Jfcherng\Diff\Renderer\Html\Json::class,
'JsonHtml' => \Jfcherng\Diff\Renderer\Html\JsonHtml::class,
'SideBySide' => \Jfcherng\Diff\Renderer\Html\SideBySide::class,
// text
'Context' => \Jfcherng\Diff\Renderer\Text\Context::class,
'JsonText' => \Jfcherng\Diff\Renderer\Text\JsonText::class,
'Unified' => \Jfcherng\Diff\Renderer\Text\Unified::class,
])
);
override(
\Jfcherng\Diff\Factory\RendererFactory::make(0),
map([
// html
'Combined' => \Jfcherng\Diff\Renderer\Html\Combined::class,
'Inline' => \Jfcherng\Diff\Renderer\Html\Inline::class,
'Json' => \Jfcherng\Diff\Renderer\Html\Json::class,
'JsonHtml' => \Jfcherng\Diff\Renderer\Html\JsonHtml::class,
'SideBySide' => \Jfcherng\Diff\Renderer\Html\SideBySide::class,
// text
'Context' => \Jfcherng\Diff\Renderer\Text\Context::class,
'JsonText' => \Jfcherng\Diff\Renderer\Text\JsonText::class,
'Unified' => \Jfcherng\Diff\Renderer\Text\Unified::class,
])
);
+68
View File
@@ -0,0 +1,68 @@
## VERSION 2 NEW START
* Version **2.2** - char-level diff performance++
* 2019-01-20 21:22 **2.2.0** initial release
* b28ce64 Remove dependancy: jfcherng/php-levenshtein-distance
* Version **2.1** - init
* 2019-01-20 11:57 **2.1.23** fix Json template
* ced5f33 Fix the Json template
* 7b21dc5 Update readme
* 2019-01-20 09:01 **2.1.22** nits
* 4dc695f Refactor template classes
* 59824ed Code tidy
* 7a5c1fb Code tidy
* 100dd67 Update readme
* 2019-01-19 09:17 **2.1.21** nits
* 36f45d4 Code tidy
* a3e70d1 Make the diff object in the renderer non-public
* f13ad57 Code tidy
* f430a8e nits
* c4ddbb7 Add renderer option: spacesToNbsp
* bbc88a1 htmlFixSpaces() may generate shorter output
* 1d1ad4c Use more preservative reserved chars from Unicode reserved areas
* 934706c Update deps
* 2019-01-19 05:35 **2.1.20** small performance tweaks
* 6516349 Small performance tweaks
* d4b61a1 nits
* 2019-01-18 08:52 **2.1.19** coding style fixes
* e4284d6 php-cs-fixer
* d91ec6f Update deps
* ce3f817 nits
* 76cd7f3 Fix phan error
* 7fbd791 Revise badges in README
* 887ee9f Add Codacy badge
* 4c159f6 Some coding style tweaks
* ee1c2a5 year++
* eca66d7 Add tests for PHP 7.3
* 2018-12-17 21:34 **2.1.18** Add Russian translation
* efeee17 Update deps
* 0f28b9d Fix tests
* 767c432 Prefer using "rus" as the language code
* 70a9275 Add new lang
* 9c449ba nits
* 08bc260 Update deps
* 2018-11-20 19:31 **2.1.17** nits
* 9297443 nits
* 9251d8b Add FileNotFoundException
* 7684942 Update deps
* 04590f1 Update README
* 0e11e19 Alias bg.php to bul.php
* e186ff7 micro code tweak
* 28a423f Fix some typos
* 96c679c Update deps
* 2018-09-16 14:34 **2.1.16** Bulgarian translation
* e7fafe6 Bulgarian translation
* 2018-09-04 18:56 **2.1.15** php-cs-fixer
* 799cae1 php-cs-fixer: enable native_constant_invocation and native_function_invocation
* 96ee1d2 Update deps
* 83e3a54 Rename CHANGELOG.md -> CHANGELOG
* 81364db Update deps
* 605a963 Update .travis.yml
* 2018-07-18 08:41 **2.1.14** Add composer.lock
* 726b3a6 Add composer.lock
* e36f649 Update README.md
* 2018-07-04 15:26 **2.1.13** nits
* f2ff363 Update .rmt.yml
* 2018-07-04 14:59 **2.1.12** initial release
+55
View File
@@ -0,0 +1,55 @@
## VERSION 3 SUN RISE
* Version **3.3** - JSON format language files
* 2019-01-25 01:06 **3.3.0** initial release
* 395b802 Use JSON format language files
* c299e39 Remove debug codes
* Version **3.2** - add none-level diff
* 2019-01-24 23:54 **3.2.6** nits
* 7d9ca9c Code tidy
* ce5273a Code tidy
* 42bdf83 Remove a unnecesary "else"
* 2019-01-24 19:44 **3.2.5** minify diff table block separator
* 215f300 Change diff table block separator rendering
* 08f3cf7 Remove extra empty lines when setting context to 0
* 2019-01-24 15:32 **3.2.4** nits
* af51b33 Add more punctuations as word separators
* 9e649d8 Fix typos
* 2019-01-24 14:48 **3.2.3** fix LineRendererFactory case-sensitive
* 4825b96 Fix LineRendererFactory case-sensitive problem
* 2019-01-24 14:21 **3.2.2** nits
* 4b74e4f PreservedConstantInterface -> ReservedConstantInterface
* 2019-01-24 14:18 **3.2.1** nits
* d5206fb Refactor out AbstractHtml::renderChangedExtentByXXX()
* 2019-01-24 12:49 **3.2.0** initial release
* 9e6e37a Add none-level diff
* 5e05555 Add line-level diff to example.php
* cdc19df Unroll if-conditions for ReverseIterator::fromArray()
* Version **3.1** - word-level diff
* 2019-01-22 15:32 **3.1.4** fix renderer options
* d7dbacf Fix render options
* 2019-01-22 13:55 **3.1.3** nist
* 2d139a8 Code tidy
* d54e4f1 Update deps
* 9aeb234 Add ITERATOR_GET_KEY, ITERATOR_GET_BOTH for ReverseIterator
* 11c7652 Code tidy
* 2019-01-22 12:36 **3.1.2** nits
* 1bef1a0 Diff's $a, $b are now required in \__construct()
* 22426a5 Add Diff::setAB()
* 2019-01-22 12:05 **3.1.1** fix ignoreCase/ignoreWhitespace
* 300e4cf Fix ignoreCase/ignoreWhitespace for word/char-level diff
* 9ea24dd Update readme
* 2019-01-22 11:49 **3.1.0** initial release
* f6fbce8 Suppress phan errors
* 6850cb5 Add word-level diff
* aea5ff8 Fix demo.php
* Version **3.0** - options changed
* 2019-01-21 16:41 **3.0.0** initial release
* d7b6079 Clear up options
* c9a43a1 Fix diff getInstance()
* 1b99649 Code tidy
* f5cd010 Update demo.php
+98
View File
@@ -0,0 +1,98 @@
## VERSION 4 AT NOON
* Version **4.2** - helper methods
* 2019-03-03 06:36 **4.2.3** tiny fixes
* c3d1512 Make Language::getTranslationsByLanguage() safer
* df4488e Fix a typo
* 8a41e9a Use native class name
* fa48647 Fix a typo in readme
* 2019-03-03 04:03 **4.2.2** final release for v4
* 86e32c0 Adapt some non-breaking changes from the master branch
* 35c1303 nits
* 637e07e Update deps
* 80c1861 Move CHANGELOG and UPGRADING
* 2019-02-27 01:46 **4.2.1** fix typo
* 37f7f75 Fix DiffHelp::getTemplatesInfo() typos
* 3a3c276 Revise phpdoc
* c221c25 Add (private) Diff::finalize() to help maintain cache vadility
* 2019-02-26 14:49 **4.2.0** initial release
* 43566f1 php-cs-fixer
* be70f5e Add DiffHelper::getProjectDirectory()
* 4937417 Add INFO['type'] for renderers
* 3ef51f6 Rewrite DiffHelper::getTemplatesInfo() and DiffHelper::getStyleSheet()
* 5e0f0da Add DiffHelper::getStyleSheet()
* c7e5312 require-dev at least phpunit/phpunit 7.5
* 01d52fd Add DiffHelper::calculateFiles()
* 4601400 No need to clear cached value if $old/$new does not change
* d690144 Code tidy
* Version **4.1** - Allow negative tabSize
* 2019-02-22 02:51 **4.1.9** fix Json renderer with empty changes
* d842546 Fix Json renderer with empty changes
* 2019-02-22 02:38 **4.1.8** fix dep
* c2fb459 squizlabs/php_codesniffer should be in require-dev
* 2019-02-22 02:16 **4.1.7** fix them
* 88ff257 Update example/old_file.txt and the new one.
* 32537b9 Fix AbstractHtml::expandTabs() $tabSize undeclared
* 37bd345 Fix problem caused by in OP_EQ, it still may be $old !== $new
* a821017 AbstractHtml::expandTabs() add argument: $onlyLeadingTabs
* bd00670 Code tidy
* 2031306 Update readme
* 0637909 Move upgrading guide from readme to other files
* 5e932e6 Separate CHANGELOG by major version
* 2019-02-21 19:50 **4.1.5** cs tweak
* 1464d86 Update .rmt.yml
* b3b0595 Code tidy
* 7caae81 Update readme
* 347935c Update deps
* 6f8e9dd Uniform the term "$a, $b, from, to" into "old, new"
* f7679ff Code tidy
* 159d244 nits
* 76675e4 Tiny regex performance increasement
* 42eeff7 Fix 120 chars per line
* 86f2325 Introduce squizlabs/php_codesniffer
* 2019-02-20 18:49 **4.1.4** Fix Diff::getText()
* 540daf5 Fix potential boundary error in Diff::getText()
* 2b723e7 Revert "Inline and remove Diff::getText()"
* 2019-02-20 16:45 **4.1.3** Fix Diff::getText()
* a5171c8 Inline and remove Diff::getText()
* be52d42 Fix Diff::getText() when $end is null
* 3a6259a Code tidy
* 1fd7935 Update option comments
* 2019-02-20 15:04 **4.1.2** update deps
* df25c88 Update jfcherng/php-sequence-matcher ^2.0
* 0c7b425 Update readme
* 2019-02-19 19:36 **4.1.1** fix some HTML templates
* 0ed0e53 Fix HTML renderer should not emphasize inserted/deleted lines
* 6e97b8a Update output example in readme
* 80db0a0 Fix HTML special chars in JSON renderer should be escaped
* 09a405e Code tidy
* 6be0a0e Update deps
* cf5bff2 AbstractHtml::expandTabs() has a default $tabSize = 4
* 2019-02-16 18:36 **4.1.0** initial release
* 53e6b17 Allow renderer option tabSize < 0
* e60fb5c Remove useless codes
* bf85310 Add more punctuations for word-level line renderer
* 847b795 Update readme
* ba77110 nits
* b13149a Update deps
* Version **4.0** - Strip out the SequenceMatcher
* 2019-02-08 20:17 **4.0.1** nits
* 8f11cb1 Fix a typo
* 54351a6 Move factory classes to Jfcherng\Diff\Factory\
* 6d68583 Merge ReservedConstantInterface into RendererConstant
* 2019-02-08 10:04 **4.0.0** initial release
* fac4dae nits
* b6fa08d Strip out SequenceMatcher as an external package
* 2349bb2 Demo uses "font-family: monospace"
* ed3f47c Temporary fix for CI
* 538f270 Update tests
* 11cf372 Fix typos
* d56ad7e Use native exceptions
* 67351d1 Update deps
* 8b14dae Add "final" to classes non-inherited
* ee6e5a3 Add UTF-8-ready demo
* bf686a2 Prepare for being a new package
* 04fb079 nits
+90
View File
@@ -0,0 +1,90 @@
## VERSION 5 AFTERNOON
* Version **5.2** - clean up
* 2019-05-15 00:17 **5.2.2** Final Release for v5
* 2bd27d4 Update deps
* fa189fa Update deps
* 6b30019 Update deps
* abc0d8a $ composer fix
* 7cb5a75 Update deps
* 70f66a2 Update .travis.yml for 7.4snapshot
* e7e1839 Update deps
* 1cee802 Update readme
* 756970a Add .editorconfig
* e6350bc Change screenshot size
* 55748a5 Update deps
* 03b0c55 Freeze documentation assets for v5
* bd61843 Freeze documentation assets for v4
* 2019-03-04 23:03 **5.2.1** tiny fixes
* ed2bf46 Fix Diff::getOldNewComparison() should use finalize()
* ecde729 Fix codacy's complaint
* 5efa215 Revise the Language class (no BC break)
* 3b369f8 Type-safe for Language::getTranslationsByLanguage()
* d442360 Remove unneccesary phan error suppression
* 8dffcb3 Fix readme badge
* 2019-03-04 04:05 **5.2.0** initial release
* ef500d0 Deprecate AbstractRenderer::getIdenticalResult()
* 88648e3 nits
* 0861774 $ php-cs-fix fix
* bfbdd3d [php-cs-fixer] Add new rules: @PhpCsFixer, @PhpCsFixer:risky
* dfeb1b8 Improve JSON decoding in Language::getTranslationsByLanguage()
* 414eb29 Release of new version 4.2.3
* 9d51c33 Make Language::getTranslationsByLanguage() safer
* 8acb535 Fix a typo
* 2b620b3 Use native class name
* b76301a Fix a typo in readme
* 8745678 Release of new version 4.2.2
* 6117ab9 Adapt some non-breaking changes from the master branch
* 8b06983 nits
* 5659dc4 Update deps
* cfb2652 Move CHANGELOG and UPGRADING
* 28216ed Release of new version 4.2.1
* Version **5.1** - helper methods
* 2019-02-27 01:47 **5.1.1** fix typo
* 868c542 Fix DiffHelp::getTemplatesInfo() typos
* 80cd8e7 Revise phpdoc
* 0a202c8 Add (private) Diff::finalize() to help maintain cache vadility
* 2019-02-26 14:57 **5.1.0** initial release
* 00ae199 Fix .rmt.yml
* 7a700fa Release of new version 4.2.0
* 770636a php-cs-fixer
* 0304537 Add INFO['type'] for renderers
* a9bee51 Rewrite DiffHelper::getTemplatesInfo() and DiffHelper::getStyleSheet()
* 9f9809e Add DiffHelper::getProjectDirectory()
* 4d739c4 Add DiffHelper::getStyleSheet()
* ec11495 require-dev at least phpunit/phpunit 7.5
* 2553620 Add DiffHelper::calculateFiles()
* a4f07c0 No need to clear cached value if $old/$new does not change
* ff019a9 Code tidy
* 02b30c8 Update deps
* 742c096 Update UPGRADING_v5.md
* 0ffd644 Code tidy
* b619f91 Update readme
* Version **5.0** - AFTERNOON
* 2019-02-22 02:55 **5.0.2** fix Json renderer with empty changes
* 6ca5b97 Fix Json renderer with empty changes
* e512f2d Update CHANGELOG_v4.md
* 2019-02-22 02:35 **5.0.1** fix dep
* 7757363 squizlabs/php_codesniffer should be in require-dev
* 6ac0c99 Update .rmt.yml
* 2019-02-22 02:24 **5.0.0** initial release
* 9f85206 Update .rmt.yml
* 064a3b3 Add renderer option: outputTagAsString
* 23c59f8 Update jfcherng/php-sequence-matcher ^3.0
* 1eb6bbb Update example/old_file.txt and the new one.
* b9f2f7b Fix AbstractHtml::expandTabs() $tabSize undeclared
* 469abb7 Fix problem caused by in OP_EQ, it still may be $old !== $new
* 82ed9ca AbstractHtml::expandTabs() add argument: $onlyLeadingTabs
* dd6d8dc Code tidy
* 4750267 Move upgrading guide from readme to other files
* 12b6024 Separate CHANGELOG by major version
* ebbb509 Update readme
* ef735c9 Code tidy
* 4518927 Uniform more "from, to" into "old, new"
* 079a5ec Update readme
* dbcf0b8 Update readme
* b84bef5 Remove deprecated APIs
* 5330cd2 Uniform the term "base, changed" into "old, new"
+412
View File
@@ -0,0 +1,412 @@
## VERSION 6 UNIFIED
* Version **6.16** - new differ option: fullContextIfIdentical
* 2024-03-11 01:40 **6.16.2** remove debug msg
* 8bafc7b fix: remove debug print
* 2024-03-08 01:59 **6.16.1** fix Differ::getStatistics()
* e6d485d fix: Differ::getStatistics() not working when no difference
* 26f93d7 chore: update deps
* 2024-03-05 16:44 **6.16.0** initial release
* 809088e chore: $ composer fix
* d07f241 chore: fix deprecated php-cs-fixer options
* 00c7a38 chore: update deps
* 5d094e4 feat: add new differ option: fullContextIfIdentical (#79)
* bf2ddb8 chore: tidy codes
* e0da986 style: $ composer fix
* 5b70504 chore: ignore example/demo_web.php from php-cs-fixer
* e1575a2 chore: update deps
* 73069e7 chore: fix typos (#75)
* Version **6.15** - max sequence length
* 2023-06-15 20:29 **6.15.3** add translation: cze
* 2335d9e docs: update translation readme
* d50beb0 chore: add traslation Czech (#74)
* 2023-06-05 19:41 **6.15.2** add translation: nob
* 9d55d2f chore: Norwegian translation (#73)
* 2023-05-26 00:23 **6.15.1** update deps
* b73d9e8 chore: update deps
* 2023-05-21 16:04 **6.15.0** initial release
* 5089d99 feat: add new differOption: lengthLimit
* 9182995 chore: update deps
* Version **6.14** - add differ option: ignoreLineEnding
* 2023-03-16 03:26 **6.14.2** (Combined) fix unmerge-able block
* aeb1c3c fix: (Combined) unmerge-able block shouldn't be repeated
* 341f943 Release of new version 6.14.1
* 2023-03-11 15:24 **6.14.1** tidy codes
* 71f0b0e refactor: move "ignoreLineEnding" implementation to sequence matcher
* 77d4dbb test: tidy codes
* 25d344b chore: update deps
* df6db81 chore: fix typos
* 2023-03-10 20:33 **6.14.0** initial release
* 3b1e344 feat: add differ option: ignoreLineEnding
* b194161 chore: move test files
* 882ea1a chore: update deps
* Version **6.13** - override language
* 2023-02-27 19:28 **6.13.1** another way to fix #34
* 7b89fd7 fix: another way to fix \r shouldn't be visualized like a newline in Combined renderer
* c069031 chore: add missing type hint
* 2023-01-19 00:29 **6.13.0** initial release
* bfb89b6 feat: provide a way to override an existing language
* 775a043 chore: update deps
* c1638b3 style: format readme with prettier
* 5eb2dab docs: fix build status badge in readme
* 33b5bf5 chore: update deps
* 8063d63 style: reduce indentation for the demo codes
* 815eb0f test: test against PHP 8.2
* Version **6.12** - new rendererOption: spaceToHtmlTag
* 2022-09-26 22:33 **6.12.1** fix: side-by-side column too narrow
* e12a477 style: make CI happy
* 2022-09-25 21:58 **6.12.0** initial release
* 97791ea feat: new rendererOption: spaceToHtmlTag (#60)
* 47f9e74 revert: partially revert ef08158
* df88798 fix: select the code only (#59)
* ef08158 fix: wrong visual tabSize on browser
* 397fb17 chore: fix typo/spelling (#57)
* 7da0452 chore: update deps
* e579d7e chore: update CI deps
* Version **6.11** - Bump PHP version to 7.4
* 2022-06-26 13:42 **6.11.6** fix Combined::renderArray
* 710be7f test: update tests
* fea80b0 fix: Combined::renderArray isn't working properly
* a8eabe3 chore: update deps
* 2022-04-03 00:35 **6.11.5** Update upstream
* 52551c6 chore: update deps
* 4b08ef2 chore: change rules for php-cs-fixer 3.8
* ba4dc50 chore: update deps
* fc6613b chore: update deps
* 2022-03-07 19:24 **6.11.4** add Italian translation
* 6d4b53e feat: add Italian translation (#53)
* f94c48f refactor: tidy codes
* 2022-03-01 00:23 **6.11.3** PHP 7.4 refactoring
* 27a92e4 refactor: utilize PHP 7.4 typed properties
* 8ec5d02 refactor: utilize PHP 7.4 array spread operator
* 7372b0a refactor: utilize PHP 7.4 arrow funciton
* 74db0b4 refactor: utilize PHP 7.4 null coalescing assignment
* 02c14b8 chore: update deps
* fafa8a8 chore: remove "todo" from test data
* 2022-02-21 13:54 **6.11.2** Fix PHP alpine
* a589955 chore: update deps
* 1b537d5 chore: license year +1
* 2021-11-25 10:15 **6.11.0** initial release
* 65610ae style: reformat with php-cs-fixer
* 664ff0f chore: update php-cs-fixer rules for v3
* 84abe9f chore: bump min PHP to 7.4.0 and update other deps
* d55fd28 chore: remove "@todo" in readme
* 8130948 docs: revise instructions for running demo
* Version **6.10** - feat: add methods to get diff statistics
* 2021-09-02 12:03 **6.10.2** Bump deps
* bf18634 chore: bump "jfcherng/php-sequence-matcher" 3.2.8
* 2021-09-02 11:58 **6.10.1** Bump deps
* 795555c chore: bump "jfcherng/php-sequence-matcher" 3.2.7
* 814782b chore: add RMT into ignore-export
* 98fbe77 chore: update demo js/css URLs
* 809ea0f docs: update badegs
* 17d912e style: fix some long lines
* 7e9a001 style: add visibility to public consts
* 2a28fe1 chore: update deps
* 2021-03-19 17:20 **6.10.0** initial release
* 56eee87 chore: add liip/rmt as a dev dependency
* 4b170cd feat: add methods to get diff statistics
* d95e0b8 chore: update deps
* 4c6e2ab chore: update license year
* f40467c chore: allow phan/phan ^4
* bd322af fix: deprecated method in .php_cs
* ab25d01 chore(ci): fix php-cs-fixer rule as of 2.17.0
* c0d13c5 chore: update deps
* Version **6.9** - add option: showHeader
* 2020-12-08 07:55 **6.9.0** initial release
* 9d98db2 chore: exclude "tests/IgnoreWhitespaceTest.php" from formatting
* 8f33ca2 style: php-cs-fix
* 5e5f038 fix: undefined index 'showHeader'
* 5f0dd2d feat: add option: showHeader
* 863fbe1 chore(ci): real test against PHP 8.0
* 7e830bf chore: update deps
* 4576720 chore: update php-cs-fixer 2.17.0
* 3fede5b chore(ci): test against PHP 8.0 as well
* a342715 docs: fix typo
* 6ec0dd7 docs: add description links for Unified/Context diff formats
* Version **6.8** - new renderer: JsonText
* 2020-10-29 18:49 **6.8.0** initial release
* 33339b8 chore: update deps
* efa7217 feat: add renderer: JsonText
* f7dac30 chore: update frontend Prism 1.22.0
* 3494b40 docs: update permanent link
* fceef80 docs: fix typos
* 2cdcb6b chore: nits
* ef66d12 chore: udpate deps
* 8f765ec chore: Composer 2 no longer needs hirak/prestissimo
* 14fe79b chore: update deps
* Version **6.7** - feat: support colorized CLI output for text renderers
* 2020-08-23 18:52 **6.7.7** fix a regression about the last diff block
* 2ce0540 refactor: migrate to jfcherng/php-sequence-matcher 3.2.5
* 29115e2 chore: update deps
* f7f6e4d style: run php-cs-fix
* ba2fc9d chore: do not check/fix coding style for tests/data/
* 8d4c888 fix: sytnax error for PHP 7.1
* aa34ce3 test: add failing test (#36)
* 2020-08-22 19:08 **6.7.6** fix "ignoreWhitespaces" not working
* dd6a211 fix: "ignoreWhitespaces" doesn't produce the same result with GNU diff's
* dd96f78 chore: update deps
* 0fd52fb test: fixup: follow GNU's diff output
* f8c08d0 Failing test for ignoreWhitespace
* f0ed073 fix: use "static" to call non-static methods
* 2020-08-10 23:39 **6.7.5** fix \r visualization in Combined renderer
* 35a2502 fix: \r shouldn't be visualized like a newline in Combined renderer
* cb2bed2 chore: update deps
* 2020-07-06 11:10 **6.7.4** fix HTML escape for Combined renderer
* 762238b chore: update deps
* d32c7b6 fix: OP_EQ lines are not escaped in Combined renderer
* ef6dcb4 test: make phan happy
* dc2db03 chore: update readme badge
* d143c26 chore(ci): migrate CI from Travis to GitHub Actions
* 2020-05-28 03:22 **6.7.3** allow PHP 8
* 3ab8a9e chore: update deps
* 1b931eb chore: Composer PHP constrain >=7.1.3
* 37036c2 chore: add some comments
* 2020-04-27 01:49 **6.7.2** fix Combined internal HTML formatting
* 8f8d919 chore: update deps
* e172e4a fix: unexpected Combined output due to HTML formatting
* 8011802 chore: rename example/demo.php -> example/demo_web.php
* 444342d refactor: tidy codes
* 2020-04-07 02:18 **6.7.1** refactor: colorization with jfcherng/php-color-output
* 9c3da80 refactor: colorization with jfcherng/php-color-output
* ea2d43d docs: adjust demo colors
* 2020-04-06 20:29 **6.7.0** initial release
* 40f0e74 test: fix test for CLI color output
* ba9dd9e chore: update deps
* 857f4b0 fix: make phan happy
* 129d51e docs: add example/demo_cli.php
* dbec4b8 feat: support colorized CLI output for text renderers
* 007abaf chore: update deps
* Version **6.6** - Add new renderer option: wordGlues
* 2020-03-31 23:53 **6.6.4** add Spanish translation
* 842b4af feat: add Spanish translation (#26)
* 2020-03-30 14:22 **6.6.3** Just a few refactoring
* 29dca67 test: add tests for Word line renderer
* 355952f refactor: Word line renderer uses PREG_SPLIT_NO_EMPTY
* 68b530e refactor: tidy codes
* 492f53c chore: add some comments
* 2020-03-30 00:45 **6.6.2** Fix wordGlues
* 7ddbb79 fix: "wordGlues" works wrongly under some circumstances (#25)
* 16780d3 fix(CSS): better strip alignment effect
* 2020-03-20 06:41 **6.6.1** Add new language: German (deu)
* 00b9a03 German language file
* 9f1ae6b chore: update deps
* 92e8f25 docs: update readme badges to fit markdown style
* 0615509 refactor: tidy codes
* 2020-03-12 19:53 **6.6.0** initial release
* 05a118a Update deps
* 2b68ddc Update screenshots
* d227113 Add new renderer option: wordGlues
* d24c6ae Tidy codes
* Version **6.5** - New renderer: Combined
* 2020-03-11 16:52 **6.5.7** Add Json renderer option: jsonEncodeFlags
* 9a00a55 Add Json renderer option: jsonEncodeFlags
* cb54261 Tidy codes
* 2020-03-09 16:08 **6.5.6** Add Combined renderer option: mergeThreshold
* 2f85968 Tidy codes (Inline/SideBySide/Combined)
* e39d674 Tidy codes
* d643094 Add Combined renderer option: mergeThreshold
* 92af433 Add more test cases
* 570fd51 Update deps
* 2020-03-08 00:42 **6.5.5** Combined: do not merge blocks which change too much
* f4f8b25 Tidy codes
* 273d1b4 Combined: do not merge blocks which change too much
* ac5e1b1 Fix CSS table text alignment for very long lines
* 5046910 Tidy codes
* 45c0b24 Do not trim trailing whitespaces for .txt files
* 2020-03-07 18:02 **6.5.4** Various Context/Unified fixes
* 4484193 Tidy codes
* 0a91891 Add some textual renderer tests
* 3fdad9c Fix Context/Unified output for EOL at EOF
* 8ab502f Fix Unified line numbers when context is 0
* 795628f Fix all-equal hunk content should be omitted in Context output
* 0f2e39a Fix there are extra leading/trailing lines when context is set to 0
* 973fecb Add some textual renderer tests
* 232356f Fix DiffHelper::calculateFiles() for empty file
* 899600c Tidy codes
* a81078f Add some comments
* 2020-03-06 01:29 **6.5.3** Improve Combined renderer output for newlines
* 745199a Update deps
* e531cdc Combined: better boundary newline chars visualization
* bf09969 Tidy codes
* 42366b1 Add "run-script" to custom composer commands
* 2020-03-05 14:14 **6.5.2** Fix multiline replaced lines in the Combined renderer
* 5c93db2 Stripe background should only work in SideBySide
* ea45be3 Fix "\n" should be "<br>" in Combined renderer output
* d65c878 Use Prism.js to highlight diff example demos
* c4e7d7b Tidy codes
* 2020-02-29 02:30 **6.5.1** Allow overriding SCSS variables
* ec4583c Allow overriding SCSS variables
* bb5203b Simplify codes for merging lines in Combined renderer
* 6da9215 Update example readme
* a0b906b Prefer the new if we can only show either old or new
* 2020-02-28 22:56 **6.5.0** initial release
* 98fe251 Update deps
* 2de0300 Update readme
* a6ff11a Finish the HTML Combined renderer
* 2cf8e84 New Renderer (#22)
* 2277a98 Improve type docs and variable naming
* d212f41 Update docs
* 7a45667 Tidy codes
* e96e971 Rename some "tag" to "op"
* 2dd547c Update deps
* Version **6.4** - Add renderer option: lineNumbers
* 2020-02-25 04:22 **6.4.7** Fixed SideBySide stripe background
* f395759 SideBySide stripes use "background-attachment: fixed;"
* a93f7fb Update SideBySide screenshot
* 2020-02-24 18:52 **6.4.6** Align stripes
* bdc3036 Make SideBySide stripe background aligned across lines
* 2020-02-24 14:47 **6.4.5** CSS tweaks
* 36799b9 SideBySide shows a stripe for nonexistent lines
* 6b7e36b Update readme
* 8b89bbc Add a const for indicating showing all contexts
* bd2c868 Make empty cells have height via CSS
* 3ae6da1 Make coding thoughts more clear
* 0394a74 Tidy Codes
* 7093932 Update SideBySide screenshot
* abb8955 Improve SideBySide CSS for nonexistent blocks
* 8cb05e8 Add keywords to composer.json
* f061297 Update example/README.md
* f861a3f Add more cases to example text sources
* 34786fa Update example/README.md
* 2020-02-13 14:16 **6.4.4** Fix SideBySide wrong new line number
* 7397cbe Tidy codes
* 215209d Fix SideBySide wrong new line number
* d91221d Fix documentation for outputTagAsString
* a7c3222 Add example image for SideBySide without line numbers
* 2020-02-10 05:40 **6.4.3** Better SideBySide output without line numbers
* 067c715 Merge SideBySide insert/delete columns when lineNumbers is false
* 2020-02-09 18:27 **6.4.2** Fix SidebySide fatal error
* 4e74cc5 Fix SideBySide renderer fatal error
* 2020-02-09 17:43 **6.4.1** Fix typo
* f2afe14 Fix typo: Woker -> Worker
* 2020-02-07 17:51 **6.4.0** initial release
* 802a7cb Tidy codes
* 52045f3 Update documents
* 69f42a1 lineNumber option (#19)
* 1140ec2 Fix phan errors
* 10cc545 Fix documentation about rendering from JSON diff
* 4072992 Update deps
* 90a86e0 ++year
* Version **6.3** - Render HTML with JSON result
* 2020-01-16 13:00 **6.3.2** Add French (fra) translation
* 41d571f Add French language file
* 2020-01-12 01:08 **6.3.1** Remove dep: nicmart/string-template
* 82259d0 Update dev require
* d83f3ac Update readme [skip ci]
* b36aeec Revert "Use string template engine to render language translations"
* 0a991e7 Update tests
* 2020-01-05 16:21 **6.3.0** initial release
* 91a6180 Update deps
* c713a0a Update examples and README
* 3c6d9a3 Variables renaming (example/demo.php)
* ce71a93 Add tests for \Jfcherng\Diff\Renderer\AbstractRenderer::renderArray
* f497539 Tidy codes
* caec927 Require jfcherng/php-sequence-matcher ^3.2
* 3111605 Let text renderers throw exception for renderArray()
* 8235504 Ability to render HTML from JSON (#17)
* Version **6.2** - Add renderer option: resultForIdenticals
* 2020-01-05 01:19 **6.2.1** Add Turkish translation
* f86585a Update deps
* 858625f Create tur.json
* eae8262 Add some @return static
* caaaba7 Add .phpstorm.meta.php
* 1067679 Update .travis.yml to PHP 7.4 stable
* 6a5cd7b $ composer fix
* d69a153 Update deps
* 3e1b965 Use string template engine to render language translations
* 2019-10-12 05:33 **6.2.0** initial release
* f7aba88 Optimize RendererFactory::resolveRenderer()
* 288adb2 Fix typos
* 601cf86 Throw an exception for invalid "resultForIdenticals"
* 9a71712 Add renderer option: resultForIdenticals
* 53ac441 nits: tests
* d1a7479 Update deps
* 31b4916 Remove unnecessary CSS
* f05f4a3 Tidy files
* Version **6.1** - Resolve CSS conflict
* 2019-10-11 06:34 **6.1.2** Add Portuguese translation
* a07dea9 Rename pt_BR.json -> por.json
* d89a7b3 Replace "switch" statements with callback function tables
* 5b9a828 Create pt_BR.json
* 896df76 Follow PSR-12
* 6d3ae95 Move FUNDING.yml to .github/
* cf9032a Add test for renderer custom language array
* 2019-09-11 00:19 **6.1.1** Fix "language" cannot be an array
* 98085b6 Fix renderer option "language" cannot be an array
* 3518068 Update .gitattributes
* 2019-09-10 17:24 **6.1.0** initial release
* f9400d3 Add renderer option: wrapperClasses
* f2f613c Update readme (nits)
* fdead6f Update readme (flat-square style badges)
* Version **6.0** - Unified
* 2019-09-10 14:30 **6.0.1** Fix jpn wording
* 00b59ef Fix jpn wording
* d04814e Sass: replace compass/css3 functions with dart-sass built-in
* a3b05d9 Update readme to use badges from shields.io
* 5dbdf21 Update readme: nits
* db4439c Slightly improve code readability
* 682fb39 nits
* 2b13e8e Update deps
* 0c9b79b Add .gitattributes
* fa9f3a6 Create FUNDING.yml
* f81b823 Move documentation assets to the current branch
* 4c2c11d Release of new version 5.2.2
* 2bd27d4 Update deps
* fa189fa Update deps
* 6b30019 Update deps
* abc0d8a $ composer fix
* 7cb5a75 Update deps
* 70f66a2 Update .travis.yml for 7.4snapshot
* e7e1839 Update deps
* 1cee802 Update readme
* bf14c89 Update deps
* c52f66b Fix typo in UPGRADING_v6.md
* 756970a Add .editorconfig
* e6350bc Change screenshot size
* 55748a5 Update deps
* d8644db Update deps
* 03b0c55 Freeze documentation assets for v5
* bd61843 Freeze documentation assets for v4
* 2019-03-20 11:31 **6.0.0** initial release
* 46d8e1b Update .rmt.yml
* f3f209c Fix tests
* c625cb3 Revise UPGRADING_v6.md
* 6807e65 Let Differ manages its own state, i.e., finalize()
* ae309c5 Fix some grammar problems in UPGRADING_v6.md
* 9fa89dd Add RendererInterface::getResultForIdenticals()
* b781125 Release of new version 5.2.0
* 57fa3cd nits
* 926f19f $ php-cs-fixer fix
* 89ec714 [php-cs-fixer] Add new rules: @PhpCsFixer, @PhpCsFixer:risky
* 83830c7 Improve JSON decoding in Language::getTranslationsByLanguage()
* 3b40e42 Release of new version 4.2.3
* b39f29a Make Language::getTranslationsByLanguage() safer
* 846d0ca Rename the term "template" to "renderer"
* 8439977 Use native class name
* 5330a8e Release of new version 4.2.2
* 4979a53 nits
* ffaefef Fix phan error
* dc26604 Rename Differ::groupedCodes to Differ::groupedOpcodes
* ba570f8 Make Differ work in a DI way in Renderer internally
* 177146e Rename Diff to Differ
* 0a30b27 Renderer::render() rather than Diff::render()
* deb9ed3 Revise phpdoc
* d8606b7 Release of new version 5.1.0
+31
View File
@@ -0,0 +1,31 @@
BSD 3-Clause License
Copyright (c) 2018-2022 Jack Cherng <jfcherng@gmail.com>
Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+633
View File
@@ -0,0 +1,633 @@
# php-diff
[![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/jfcherng/php-diff/php.yml?branch=v6&style=flat-square)](https://github.com/jfcherng/php-diff/actions)
[![Codacy grade](https://img.shields.io/codacy/grade/5b7ab5ed613d48b99f12cd334f6489ff/v6?style=flat-square)](https://app.codacy.com/project/jfcherng/php-diff/dashboard)
[![Packagist](https://img.shields.io/packagist/dt/jfcherng/php-diff?style=flat-square)](https://packagist.org/packages/jfcherng/php-diff)
[![Packagist Version](https://img.shields.io/packagist/v/jfcherng/php-diff?style=flat-square)](https://packagist.org/packages/jfcherng/php-diff)
[![Project license](https://img.shields.io/github/license/jfcherng/php-diff?style=flat-square)](https://github.com/jfcherng/php-diff/blob/v6/LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/jfcherng/php-diff?style=flat-square&logo=github)](https://github.com/jfcherng/php-diff/stargazers)
[![Donate to this project using Paypal](https://img.shields.io/badge/paypal-donate-blue.svg?style=flat-square&logo=paypal)](https://www.paypal.me/jfcherng/5usd)
A comprehensive library for generating diff between two strings.
## Introduction
Generated diff can be rendered in all of the standard formats including:
**Text** renderers:
- Context
- Json (plain text)
- Unified
**HTML** renderers:
- Combined
- Inline
- Json (HTML)
- Side by Side
Note that for HTML rendered results, you have to add CSS for a better visualization.
You may modify one from `example/diff-table.css` or write your own from zero.
If you are okay with the default CSS, there is `\Jfcherng\Diff\DiffHelper::getStyleSheet()`
which can be used to get the content of the `example/diff-table.css`.
## Requirements
![php](https://img.shields.io/badge/php-%E2%89%A57.4.0-blue?style=flat-square)
![ext-iconv](https://img.shields.io/badge/ext-iconv-brightgreen?style=flat-square)
## Installation
This package is available on `Packagist` by the name of [jfcherng/php-diff](https://packagist.org/packages/jfcherng/php-diff).
```bash
composer require jfcherng/php-diff
```
## Example
See files and readme in the [example/](https://github.com/jfcherng/php-diff/blob/v6/example) directory.
```php
<?php
include __DIR__ . '/vendor/autoload.php';
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\DiffHelper;
use Jfcherng\Diff\Factory\RendererFactory;
use Jfcherng\Diff\Renderer\RendererConstant;
$oldFile = __DIR__ . '/example/old_file.txt';
$newFile = __DIR__ . '/example/new_file.txt';
$old = 'This is the old string.';
$new = 'And this is the new one.';
// renderer class name:
// Text renderers: Context, JsonText, Unified
// HTML renderers: Combined, Inline, JsonHtml, SideBySide
$rendererName = 'Unified';
// the Diff class options
$differOptions = [
// show how many neighbor lines
// Differ::CONTEXT_ALL can be used to show the whole file
'context' => 3,
// ignore case difference
'ignoreCase' => false,
// ignore line ending difference
'ignoreLineEnding' => false,
// ignore whitespace difference
'ignoreWhitespace' => false,
// if the input sequence is too long, it will just gives up (especially for char-level diff)
'lengthLimit' => 2000,
// if truthy, when inputs are identical, the whole inputs will be rendered in the output
'fullContextIfIdentical' => false,
];
// the renderer class options
$rendererOptions = [
// how detailed the rendered HTML in-line diff is? (none, line, word, char)
'detailLevel' => 'line',
// renderer language: eng, cht, chs, jpn, ...
// or an array which has the same keys with a language file
// check the "Custom Language" section in the readme for more advanced usage
'language' => 'eng',
// show line numbers in HTML renderers
'lineNumbers' => true,
// show a separator between different diff hunks in HTML renderers
'separateBlock' => true,
// show the (table) header
'showHeader' => true,
// the frontend HTML could use CSS "white-space: pre;" to visualize consecutive whitespaces
// but if you want to visualize them in the backend with "&nbsp;", you can set this to true
'spacesToNbsp' => false,
// HTML renderer tab width (negative = do not convert into spaces)
'tabSize' => 4,
// this option is currently only for the Combined renderer.
// it determines whether a replace-type block should be merged or not
// depending on the content changed ratio, which values between 0 and 1.
'mergeThreshold' => 0.8,
// this option is currently only for the Unified and the Context renderers.
// RendererConstant::CLI_COLOR_AUTO = colorize the output if possible (default)
// RendererConstant::CLI_COLOR_ENABLE = force to colorize the output
// RendererConstant::CLI_COLOR_DISABLE = force not to colorize the output
'cliColorization' => RendererConstant::CLI_COLOR_AUTO,
// this option is currently only for the Json renderer.
// internally, ops (tags) are all int type but this is not good for human reading.
// set this to "true" to convert them into string form before outputting.
'outputTagAsString' => false,
// this option is currently only for the Json renderer.
// it controls how the output JSON is formatted.
// see available options on https://www.php.net/manual/en/function.json-encode.php
'jsonEncodeFlags' => \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE,
// this option is currently effective when the "detailLevel" is "word"
// characters listed in this array can be used to make diff segments into a whole
// for example, making "<del>good</del>-<del>looking</del>" into "<del>good-looking</del>"
// this should bring better readability but set this to empty array if you do not want it
'wordGlues' => [' ', '-'],
// change this value to a string as the returned diff if the two input strings are identical
'resultForIdenticals' => null,
// extra HTML classes added to the DOM of the diff container
'wrapperClasses' => ['diff-wrapper'],
];
// one-line simply compare two files
$result = DiffHelper::calculateFiles($oldFile, $newFile, $rendererName, $differOptions, $rendererOptions);
// one-line simply compare two strings
$result = DiffHelper::calculate($old, $new, $rendererName, $differOptions, $rendererOptions);
// or even shorter if you are happy with default options
$result = DiffHelper::calculate($old, $new, $rendererName);
// custom usage
$differ = new Differ(explode("\n", $old), explode("\n", $new), $differOptions);
$renderer = RendererFactory::make($rendererName, $rendererOptions); // or your own renderer object
$result = $renderer->render($differ);
// use the JSON result to render in HTML
$jsonResult = DiffHelper::calculate($old, $new, 'Json'); // may store the JSON result in your database
$htmlRenderer = RendererFactory::make('Inline', $rendererOptions);
$result = $htmlRenderer->renderArray(json_decode($jsonResult, true));
```
## Rendered Results
### HTML Diff In-line Detailed Rendering
<table>
<tr>
<th>None-level</th>
<th>Line-level (Default)</th>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/inline-none-level-diff.png"></td>
<td><img src="https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/inline-line-level-diff.png"></td>
</tr>
<tr>
<th>Word-level</th>
<th>Char-level</th>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/inline-word-level-diff.png"></td>
<td><img src="https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/inline-char-level-diff.png"></td>
</tr>
</table>
### Renderer: Inline
```php
<?php $rendererOptions = ['detailLevel' => 'line'];
```
![Inline](https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/inline-renderer.png)
### Renderer: Side By Side
```php
<?php $rendererOptions = ['detailLevel' => 'line'];
```
![Side By Side](https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/side-by-side-renderer.png)
### Renderer: Combined
```php
<?php $rendererOptions = ['detailLevel' => 'word'];
```
This renderer is suitable for articles and always has no line number information.
![Combined](https://raw.githubusercontent.com/jfcherng/php-diff/v6/example/images/combined-renderer-word-level.png)
### Renderer: Unified
About the `Unified` diff format: https://en.wikipedia.org/wiki/Diff#Unified_format
```diff
@@ -1,3 +1,4 @@
-<p>Hello World!</p>
+<div>Hello World!</div>
~~~~~~~~~~~~~~~~~~~
+Let's add a new line here.
X
@@ -7,6 +8,5 @@
N
-Do you know in Chinese, "金槍魚罐頭" means tuna can.
+Do you know in Japanese, "魚の缶詰" means fish can.
This is just a useless line.
G
-// remember to delete this line
Say hello to my neighbors.
```
### Renderer: Context
About the `Context` diff format: https://en.wikipedia.org/wiki/Diff#Context_format
<details><summary>Click to expand</summary>
```diff
***************
*** 1,3 ****
! <p>Hello World!</p>
~~~~~~~~~~~~~~~~~~~
X
--- 1,4 ----
! <div>Hello World!</div>
~~~~~~~~~~~~~~~~~~~
+ Let's add a new line here.
X
***************
*** 7,12 ****
N
! Do you know in Chinese, "金槍魚罐頭" means tuna can.
This is just a useless line.
G
- // remember to delete this line
Say hello to my neighbors.
--- 8,12 ----
N
! Do you know in Japanese, "魚の缶詰" means fish can.
This is just a useless line.
G
Say hello to my neighbors.
```
</details>
### Renderer: Text JSON
This renderer has no detailed diff.
<details><summary>Click to expand</summary>
```json
[
[
{
"tag": "rep",
"old": {
"offset": 0,
"lines": ["<p>Hello World! Good-looking.</p>"]
},
"new": {
"offset": 0,
"lines": ["<div>Hello World! Bad-tempered.</div>"]
}
},
{
"tag": "eq",
"old": {
"offset": 1,
"lines": ["~~~~~~~~~~~~~~~~~~~"]
},
"new": {
"offset": 1,
"lines": ["~~~~~~~~~~~~~~~~~~~"]
}
},
{
"tag": "ins",
"old": {
"offset": 2,
"lines": []
},
"new": {
"offset": 2,
"lines": ["Let's add a new line here."]
}
},
{
"tag": "eq",
"old": {
"offset": 2,
"lines": ["X"]
},
"new": {
"offset": 3,
"lines": ["X"]
}
}
],
[
{
"tag": "eq",
"old": {
"offset": 6,
"lines": ["N"]
},
"new": {
"offset": 7,
"lines": ["N"]
}
},
{
"tag": "rep",
"old": {
"offset": 7,
"lines": ["Do you know in Chinese, \"金槍魚罐頭\" means tuna can."]
},
"new": {
"offset": 8,
"lines": ["Do you know in Japanese, \"魚の缶詰\" means fish can."]
}
},
{
"tag": "eq",
"old": {
"offset": 8,
"lines": ["\t \tTab visualization test.", "G"]
},
"new": {
"offset": 9,
"lines": ["\t \tTab visualization test.", "G"]
}
},
{
"tag": "del",
"old": {
"offset": 10,
"lines": ["// remember to delete this line"]
},
"new": {
"offset": 11,
"lines": []
}
},
{
"tag": "eq",
"old": {
"offset": 11,
"lines": ["Say hello to my neighbors."]
},
"new": {
"offset": 11,
"lines": ["Say hello to my neighbors."]
}
}
],
[
{
"tag": "eq",
"old": {
"offset": 14,
"lines": ["B"]
},
"new": {
"offset": 14,
"lines": ["B"]
}
},
{
"tag": "rep",
"old": {
"offset": 15,
"lines": ["Donec rutrum."]
},
"new": {
"offset": 15,
"lines": ["Donec rutrum test.", "There is a new inserted line."]
}
},
{
"tag": "eq",
"old": {
"offset": 16,
"lines": ["C"]
},
"new": {
"offset": 17,
"lines": ["C"]
}
},
{
"tag": "rep",
"old": {
"offset": 17,
"lines": ["Sed dictum lorem ipsum."]
},
"new": {
"offset": 18,
"lines": ["Sed dolor lorem ipsum hendrerit."]
}
},
{
"tag": "eq",
"old": {
"offset": 18,
"lines": [""]
},
"new": {
"offset": 19,
"lines": [""]
}
}
]
]
```
</details>
### Renderer: HTML JSON
For a `"tag": "rep" (8)` block, this renderer has HTML-style detailed diff.
If you don't need those detailed diff, consider using the `JsonText` renderer.
<details><summary>Click to expand</summary>
```json
[
[
{
"tag": "rep",
"old": {
"offset": 0,
"lines": ["&lt;<del>p&gt;Hello World! Good-looking.&lt;/p</del>&gt;"]
},
"new": {
"offset": 0,
"lines": ["&lt;<ins>div&gt;Hello World! Bad-tempered.&lt;/div</ins>&gt;"]
}
},
{
"tag": "eq",
"old": {
"offset": 1,
"lines": ["~~~~~~~~~~~~~~~~~~~"]
},
"new": {
"offset": 1,
"lines": ["~~~~~~~~~~~~~~~~~~~"]
}
},
{
"tag": "ins",
"old": {
"offset": 2,
"lines": [""]
},
"new": {
"offset": 2,
"lines": ["Let's add a new line here."]
}
},
{
"tag": "eq",
"old": {
"offset": 2,
"lines": ["X"]
},
"new": {
"offset": 3,
"lines": ["X"]
}
}
],
[
{
"tag": "eq",
"old": {
"offset": 6,
"lines": ["N"]
},
"new": {
"offset": 7,
"lines": ["N"]
}
},
{
"tag": "rep",
"old": {
"offset": 7,
"lines": ["Do you know in <del>Chinese, \"金槍魚罐頭\" means tuna</del> can."]
},
"new": {
"offset": 8,
"lines": ["Do you know in <ins>Japanese, \"魚の缶詰\" means fish</ins> can."]
}
},
{
"tag": "eq",
"old": {
"offset": 8,
"lines": ["\t \tTab visualization test.", "G"]
},
"new": {
"offset": 9,
"lines": ["\t \tTab visualization test.", "G"]
}
},
{
"tag": "del",
"old": {
"offset": 10,
"lines": ["// remember to delete this line"]
},
"new": {
"offset": 11,
"lines": [""]
}
},
{
"tag": "eq",
"old": {
"offset": 11,
"lines": ["Say hello to my neighbors."]
},
"new": {
"offset": 11,
"lines": ["Say hello to my neighbors."]
}
}
],
[
{
"tag": "eq",
"old": {
"offset": 14,
"lines": ["B"]
},
"new": {
"offset": 14,
"lines": ["B"]
}
},
{
"tag": "rep",
"old": {
"offset": 15,
"lines": ["Donec rutrum."]
},
"new": {
"offset": 15,
"lines": ["Donec rutrum test.", "There is a new inserted line."]
}
},
{
"tag": "eq",
"old": {
"offset": 16,
"lines": ["C"]
},
"new": {
"offset": 17,
"lines": ["C"]
}
},
{
"tag": "rep",
"old": {
"offset": 17,
"lines": ["Sed d<del>ictum lorem ipsum</del>."]
},
"new": {
"offset": 18,
"lines": ["Sed d<ins>olor lorem ipsum hendrerit</ins>."]
}
},
{
"tag": "eq",
"old": {
"offset": 18,
"lines": [""]
},
"new": {
"offset": 19,
"lines": [""]
}
}
]
]
```
</details>
## Custom Language
### Override an Existing Language
If you just want to override some translations of an existing language...
```php
$rendererOptions = [
'language' => [
// use English as the base language
'eng',
// your custom overrides
[
// use "Diff" as the new value of the "differences" key
'differences' => 'Diff',
],
// maybe more overrides if you somehow need them...
],
]
```
## Acknowledgment
This package is built on the top of [chrisboulton/php-diff](https://github.com/chrisboulton/php-diff) initially.
But the original repository looks like no longer maintained.
Here have been quite lots of rewrites and new features since then, hence I re-started this as a new package for better visibility.
+6
View File
@@ -0,0 +1,6 @@
## Upgrading to v3
- `$diffOptions` removes: `charLevelDiff` and `separateBlock`.
- `$templateOptions` adds: `detailLevel` (similar to `charLevelDiff`, read docs) and `separateBlock` (exact the same one in `$diffOptions`).
- `Jfcherng\Diff\Diff`'s `$a` (`$old`), `$b` (`$new`) are required in `__construct()`. (You may pass two empty arrays if you do not want to do anything at that moment.)
- The look of "skipped" block in HTML renderers (`SideBySide` and `Inline`) have been changed. (You may have to tweak your CSS.)
+5
View File
@@ -0,0 +1,5 @@
## Upgrading to v4
- `Jfcherng\Diff\Utility\SequenceMatcher` becomes [a new package](https://packagist.org/packages/jfcherng/php-sequence-matcher) by the namespace of `Jfcherng\Diff\SequenceMatcher`.
- Factories under `Jfcherng\Diff\Utility\` are moved to `Jfcherng\Diff\Factory\`. For example, `Jfcherng\Diff\Utility\RendererFactory` is now `Jfcherng\Diff\Factory\RendererFactory`.
- Non-abstract classes are no longer inheritable as they are added with `final` keywords. (This allows me to do more internal changes without causing possible BC breaks.)
+24
View File
@@ -0,0 +1,24 @@
## Upgrading to v5
- Names involving `a, b`, `from, to`, `base, changed` have been renamed to `old, new` for consistency.
Here's some examples:
- `Diff::setAB()` becomes `Diff::setOldNew()`.
- `Diff::setA()` becomes `Diff::setOld()`.
- `Diff::setB()` becomes `Diff::setNew()`.
- `Diff::getA()` becomes `Diff::getOld()`.
- `Diff::getB()` becomes `Diff::getNew()`.
- `base`, `changed` keys in the result of the `Json` renderer have become `old`, `new`.
- In the result of HTML renderers, classes of rows of line numbers has been changed.
You may have to change your CSS if you have some customized things depend on these.
- `<th class="f-num">` (from-number) becomes `<th class="n-new">` (number-new).
- `<th class="t-num">` (to-number) becomes `<th class="n-old">` (number-old).
- The `tag` (sometimes called `op`) in `Json` template is now in `int` form by default.
To get previous behavior, set the renderer option `outputTagAsString` to `true`.
- The `tag` (sometimes called `op`) in `Diff::getGroupedOpcodes()`'s results are now in `int` form.
The corresponding meaning could be found in
[jfcherng/php-sequence-matcher](https://github.com/jfcherng/php-sequence-matcher/blob/3.0.0/src/SequenceMatcher.php#L16-L26).
+47
View File
@@ -0,0 +1,47 @@
## Upgrading to v6
### For Simple Users
If you only use the `DiffHelper` and built-in `Renderer`s,
there is no breaking change for you so you do not have to do anything.
### Breaking Changes for Normal Users
- The `Diff` class has been renamed as `Differ`.
It should be relatively easy to adapt to this by changing the class name.
- The term `template` has been renamed as `renderer`. Some examples are:
- Method `DiffHelper::getRenderersInfo()`
- Method `DiffHelper::getAvailableRenderers()`
- Constant `RendererConstant::RENDERER_TYPES`
- Constant `AbstractRenderer::IS_TEXT_RENDERER`
- Now a `Renderer` has a `render()` method, but a `Differ` does not.
(because it makes more sense saying a renderer would render something)
If you use those classes by yourself, it should be written like below.
```php
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\Factory\RendererFactory;
$differ = new Differ(explode("\n", $old), explode("\n", $new), $diffOptions);
$renderer = RendererFactory::make($rendererName, $rendererOptions);
$result = $renderer->render($differ); // <-- this line has been changed
```
### Breaking Changes for Customized Renderer Developers
- Remove the deprecated `AbstractRenderer::getIdenticalResult()` and
add `RendererInterface::getResultForIdenticals()`. The returned value will be
directly used before actually starting to calculate diff if we find that the
two strings are the same. `AbstractRenderer::getResultForIdenticals()`
returns an empty string by default.
- Now a `Renderer` should implement `protected function renderWorker(Differ $differ): string`
rather than the previous `public function render(): string`. Note that
`$this->diff` no longer works in `Renderer`s as it is now injected as a
parameter to `Renderer::renderWorker()`.
+69
View File
@@ -0,0 +1,69 @@
{
"name": "jfcherng/php-diff",
"description": "A comprehensive library for generating differences between two strings in multiple formats (unified, side by side HTML etc).",
"type": "library",
"license": "BSD-3-Clause",
"keywords": [
"diff",
"udiff",
"unidiff",
"unified diff"
],
"minimum-stability": "beta",
"prefer-stable": true,
"authors": [
{
"name": "Jack Cherng",
"email": "jfcherng@gmail.com"
},
{
"name": "Chris Boulton",
"email": "chris.boulton@interspire.com"
}
],
"autoload": {
"psr-4": {
"Jfcherng\\Diff\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Jfcherng\\Diff\\Test\\": "tests/"
}
},
"require": {
"php": ">=7.4",
"jfcherng/php-color-output": "^3",
"jfcherng/php-mb-string": "^1.4.6 || ^2",
"jfcherng/php-sequence-matcher": "^3.2.10 || ^4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.51",
"liip/rmt": "^1.6",
"phan/phan": "^5",
"phpunit/phpunit": "^9",
"squizlabs/php_codesniffer": "^3.6"
},
"config": {
"platform": {
"php": "7.4.99"
},
"sort-packages": true
},
"scripts": {
"analyze": [
"phan --color",
"phpcs --colors -n"
],
"fix": [
"php-cs-fixer fix --verbose"
],
"server": [
"Composer\\Config::disableProcessTimeout",
"@php -S localhost:12388 -t example/"
],
"test": [
"phpunit --verbose"
]
}
}
+4655
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
# Demo
Note that
- All `composer`-related commands are run in this project's root directory.
That is php-diff directory rather than this `example` directory.
- You can change differ/renderer options in `demo_base.php`.
- Change contents of `old_file.txt` and `new_file.txt` to test different text.
To run demo, you have to first install dependencies via `composer install`.
## Web Environment
To run `demo_web.php` on your local machine, you can follow steps below.
1. Start PHP development server via `composer run-script server`.
1. Visit `http://localhost:12388/demo_web.php` with a web browser.
## Cli Environment
Just run `php demo_cli.php`.
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
include __DIR__ . '/../vendor/autoload.php';
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\Renderer\RendererConstant;
// the two sample files for comparison
$oldFile = __DIR__ . '/old_file.txt';
$newFile = __DIR__ . '/new_file.txt';
$oldString = file_get_contents($oldFile);
$newString = file_get_contents($newFile);
// options for Diff class
$diffOptions = [
// show how many neighbor lines
// Differ::CONTEXT_ALL can be used to show the whole file
'context' => 1,
// ignore case difference
'ignoreCase' => false,
// ignore line ending difference
'ignoreLineEnding' => false,
// ignore whitespace difference
'ignoreWhitespace' => false,
// if the input sequence is too long, it will just gives up (especially for char-level diff)
'lengthLimit' => 2000,
// if truthy, when inputs are identical, the whole inputs will be rendered in the output
'fullContextIfIdentical' => false,
];
// options for renderer class
$rendererOptions = [
// how detailed the rendered HTML is? (none, line, word, char)
'detailLevel' => 'line',
// renderer language: eng, cht, chs, jpn, ...
// or an array which has the same keys with a language file
// check the "Custom Language" section in the readme for more advanced usage
'language' => 'eng',
// show line numbers in HTML renderers
'lineNumbers' => true,
// show a separator between different diff hunks in HTML renderers
'separateBlock' => true,
// show the (table) header
'showHeader' => true,
// convert spaces/tabs into HTML codes like `<span class="ch sp"> </span>`
// and the frontend is responsible for rendering them with CSS.
// when using this, "spacesToNbsp" should be false and "tabSize" is not respected.
'spaceToHtmlTag' => false,
// the frontend HTML could use CSS "white-space: pre;" to visualize consecutive whitespaces
// but if you want to visualize them in the backend with "&nbsp;", you can set this to true
'spacesToNbsp' => false,
// HTML renderer tab width (negative = do not convert into spaces)
'tabSize' => 4,
// this option is currently only for the Combined renderer.
// it determines whether a replace-type block should be merged or not
// depending on the content changed ratio, which values between 0 and 1.
'mergeThreshold' => 0.8,
// this option is currently only for the Unified and the Context renderers.
// RendererConstant::CLI_COLOR_AUTO = colorize the output if possible (default)
// RendererConstant::CLI_COLOR_ENABLE = force to colorize the output
// RendererConstant::CLI_COLOR_DISABLE = force not to colorize the output
'cliColorization' => RendererConstant::CLI_COLOR_AUTO,
// this option is currently only for the Json renderer.
// internally, ops (tags) are all int type but this is not good for human reading.
// set this to "true" to convert them into string form before outputting.
'outputTagAsString' => false,
// this option is currently only for the Json renderer.
// it controls how the output JSON is formatted.
// see available options on https://www.php.net/manual/en/function.json-encode.php
'jsonEncodeFlags' => \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE,
// this option is currently effective when the "detailLevel" is "word"
// characters listed in this array can be used to make diff segments into a whole
// for example, making "<del>good</del>-<del>looking</del>" into "<del>good-looking</del>"
// this should bring better readability but set this to empty array if you do not want it
'wordGlues' => [' ', '-'],
// change this value to a string as the returned diff if the two input strings are identical
'resultForIdenticals' => null,
// extra HTML classes added to the DOM of the diff container
'wrapperClasses' => ['diff-wrapper'],
];
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
include __DIR__ . '/demo_base.php';
use Jfcherng\Diff\DiffHelper;
use Jfcherng\Utility\CliColor;
$colorStyles = [
'section' => ['f_black', 'b_cyan'],
];
$manyNewlines = "\n\n\n\n";
echo CliColor::color("Unified Diff\n============", $colorStyles['section']) . "\n\n";
// generate a unified diff
$unifiedResult = DiffHelper::calculate(
$oldString,
$newString,
'Unified',
$diffOptions,
$rendererOptions,
);
echo $unifiedResult . $manyNewlines;
echo CliColor::color("Context Diff\n============", $colorStyles['section']) . "\n\n";
// generate a context diff
$contextResult = DiffHelper::calculate(
$oldString,
$newString,
'Context',
$diffOptions,
$rendererOptions,
);
echo $contextResult . $manyNewlines;
+242
View File
@@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
include __DIR__ . '/demo_base.php';
use Jfcherng\Diff\DiffHelper;
use Jfcherng\Diff\Factory\RendererFactory;
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>jfcherng/php-diff - Examples</title>
<!-- Prism -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/line-numbers/prism-line-numbers.min.css" />
<style type="text/css">
html {
font-size: 13px;
}
.token.coord {
color: #6cf;
}
.token.diff.bold {
color: #fb0;
font-weight: normal;
}
<?= DiffHelper::getStyleSheet(); ?>
</style>
</head>
<body>
<h1>None-level Diff</h1>
<?php
// demo the no-inline-detail diff
$inlineResult = DiffHelper::calculate(
$oldString,
$newString,
'Inline',
$diffOptions,
['detailLevel' => 'none'] + $rendererOptions,
);
echo $inlineResult;
?>
<h1>Line-level Diff (Default)</h1>
<?php
// demo the word-level diff
$inlineResult = DiffHelper::calculate(
$oldString,
$newString,
'Inline',
$diffOptions,
['detailLevel' => 'line'] + $rendererOptions,
);
echo $inlineResult;
?>
<h1>Word-level Diff</h1>
<?php
// demo the word-level diff
$inlineResult = DiffHelper::calculate(
$oldString,
$newString,
'Inline',
$diffOptions,
['detailLevel' => 'word'] + $rendererOptions,
);
echo $inlineResult;
?>
<h1>Character-level Diff</h1>
<?php
// demo the character-level diff
$inlineResult = DiffHelper::calculate(
$oldString,
$newString,
'Inline',
$diffOptions,
['detailLevel' => 'char'] + $rendererOptions,
);
echo $inlineResult;
?>
<h1>Side by Side Diff</h1>
<?php
// generate a side by side diff
$sideBySideResult = DiffHelper::calculateFiles(
$oldFile,
$newFile,
'SideBySide',
$diffOptions,
$rendererOptions,
);
echo $sideBySideResult;
?>
<h1>Inline Diff</h1>
<?php
// generate an inline diff
$inlineResult = DiffHelper::calculateFiles(
$oldFile,
$newFile,
'Inline',
$diffOptions,
$rendererOptions,
);
echo $inlineResult;
?>
<h1>Combined Diff</h1>
<?php
// generate a combined diff
$sideBySideResult = DiffHelper::calculateFiles(
$oldFile,
$newFile,
'Combined',
$diffOptions,
$rendererOptions,
);
echo $sideBySideResult;
?>
<h1>Unified Diff</h1>
<pre><code class="language-diff line-numbers"><?php
// generate a unified diff
$unifiedResult = DiffHelper::calculateFiles(
$oldFile,
$newFile,
'Unified',
$diffOptions,
$rendererOptions,
);
echo htmlspecialchars($unifiedResult);
?></code></pre>
<h1>Context Diff</h1>
<pre><code class="language-diff line-numbers"><?php
// generate a context diff
$contextResult = DiffHelper::calculateFiles(
$oldFile,
$newFile,
'Context',
$diffOptions,
$rendererOptions,
);
echo htmlspecialchars($contextResult);
?></code></pre>
<?php
// change JSON output settings for better human reading
$rendererOptions['outputTagAsString'] = true;
$rendererOptions['jsonEncodeFlags'] |= \JSON_PRETTY_PRINT;
?>
<h1>Text JSON Diff</h1>
<pre><code class="language-json line-numbers"><?php
// generate a plain text JSON diff
$jsonResult = DiffHelper::calculateFiles(
$oldFile,
$newFile,
'JsonText',
$diffOptions,
$rendererOptions,
);
echo htmlspecialchars($jsonResult);
?></code></pre>
<h1>HTML JSON Diff</h1>
<pre><code class="language-json line-numbers"><?php
// generate a HTML JSON diff
$jsonResult = DiffHelper::calculateFiles(
$oldFile,
$newFile,
'JsonHtml',
$diffOptions,
$rendererOptions,
);
echo htmlspecialchars($jsonResult);
?></code></pre>
<h1>HTML Diff from the Result of JSON Diff</h1>
<?php
$jsonArray = json_decode($jsonResult, true);
$htmlRenderer = RendererFactory::make('Inline', $rendererOptions);
$inlineResult = $htmlRenderer->renderArray($jsonArray);
echo $inlineResult;
?>
<!-- Prism -->
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-diff.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-json.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/line-numbers/prism-line-numbers.min.js"></script>
</body>
</html>
+122
View File
@@ -0,0 +1,122 @@
.diff-wrapper.diff {
--tab-size: 4;
background: repeating-linear-gradient(-45deg, whitesmoke, whitesmoke 0.5em, #e8e8e8 0.5em, #e8e8e8 1em);
border-collapse: collapse;
border-spacing: 0;
border: 1px solid black;
color: black;
empty-cells: show;
font-family: monospace;
font-size: 13px;
width: 100%;
word-break: break-all;
}
.diff-wrapper.diff th {
font-weight: 700;
cursor: default;
-webkit-user-select: none;
user-select: none;
}
.diff-wrapper.diff td {
vertical-align: baseline;
}
.diff-wrapper.diff td,
.diff-wrapper.diff th {
border-collapse: separate;
border: none;
padding: 1px 2px;
background: #fff;
}
.diff-wrapper.diff td:empty:after,
.diff-wrapper.diff th:empty:after {
content: " ";
visibility: hidden;
}
.diff-wrapper.diff td a,
.diff-wrapper.diff th a {
color: #000;
cursor: inherit;
pointer-events: none;
}
.diff-wrapper.diff thead th {
background: #a6a6a6;
border-bottom: 1px solid black;
padding: 4px;
text-align: left;
}
.diff-wrapper.diff tbody.skipped {
border-top: 1px solid black;
}
.diff-wrapper.diff tbody.skipped td,
.diff-wrapper.diff tbody.skipped th {
display: none;
}
.diff-wrapper.diff tbody th {
background: #cccccc;
border-right: 1px solid black;
text-align: right;
vertical-align: top;
width: 4em;
}
.diff-wrapper.diff tbody th.sign {
background: #fff;
border-right: none;
padding: 1px 0;
text-align: center;
width: 1em;
}
.diff-wrapper.diff tbody th.sign.del {
background: #fbe1e1;
}
.diff-wrapper.diff tbody th.sign.ins {
background: #e1fbe1;
}
.diff-wrapper.diff.diff-html {
white-space: pre-wrap;
tab-size: var(--tab-size);
}
.diff-wrapper.diff.diff-html .ch {
line-height: 1em;
background-clip: border-box;
background-repeat: repeat-x;
background-position: left center;
}
.diff-wrapper.diff.diff-html .ch.sp {
background-image: url('data:image/svg+xml,%3Csvg preserveAspectRatio="xMinYMid meet" viewBox="0 0 12 24" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M4.5 11C4.5 10.1716 5.17157 9.5 6 9.5C6.82843 9.5 7.5 10.1716 7.5 11C7.5 11.8284 6.82843 12.5 6 12.5C5.17157 12.5 4.5 11.8284 4.5 11Z" fill="rgba%2860, 60, 60, 50%25%29"/%3E%3C/svg%3E');
background-size: 1ch 1.25em;
}
.diff-wrapper.diff.diff-html .ch.tab {
background-image: url('data:image/svg+xml,%3Csvg preserveAspectRatio="xMinYMid meet" viewBox="0 0 12 24" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M9.5 10.44L6.62 8.12L7.32 7.26L12.04 11V11.44L7.28 14.9L6.62 13.9L9.48 11.78H0V10.44H9.5Z" fill="rgba%2860, 60, 60, 50%25%29"/%3E%3C/svg%3E');
background-size: calc(var(--tab-size) * 1ch) 1.25em;
background-position: 2px center;
}
.diff-wrapper.diff.diff-html .change.change-eq .old,
.diff-wrapper.diff.diff-html .change.change-eq .new {
background: #fff;
}
.diff-wrapper.diff.diff-html .change .old {
background: #fbe1e1;
}
.diff-wrapper.diff.diff-html .change .new {
background: #e1fbe1;
}
.diff-wrapper.diff.diff-html .change .rep {
background: #fef6d9;
}
.diff-wrapper.diff.diff-html .change .old.none,
.diff-wrapper.diff.diff-html .change .new.none,
.diff-wrapper.diff.diff-html .change .rep.none {
background: transparent;
cursor: not-allowed;
}
.diff-wrapper.diff.diff-html .change ins,
.diff-wrapper.diff.diff-html .change del {
font-weight: bold;
text-decoration: none;
}
.diff-wrapper.diff.diff-html .change ins {
background: #94f094;
}
.diff-wrapper.diff.diff-html .change del {
background: #f09494;
}
+193
View File
@@ -0,0 +1,193 @@
// You can compile this with https://www.sassmeister.com
$diff-bg-color: #fff !default;
$diff-text-color: invert($diff-bg-color) !default;
$diff-bg-color-ins-base: #8e8 !default;
$diff-bg-color-del-base: #e88 !default;
$diff-bg-color-rep-base: #fbdb65 !default;
$diff-op-highlight-ratio: 90% !default;
$diff-op-normal-ratio: 25% !default;
// emphasized colors for detailed inline difference
$diff-bg-color-ins-highlight: mix($diff-bg-color-ins-base, $diff-bg-color, $diff-op-highlight-ratio) !default;
$diff-bg-color-del-highlight: mix($diff-bg-color-del-base, $diff-bg-color, $diff-op-highlight-ratio) !default;
// colors for operation rows
$diff-bg-color-ins: mix($diff-bg-color-ins-base, $diff-bg-color, $diff-op-normal-ratio) !default;
$diff-bg-color-del: mix($diff-bg-color-del-base, $diff-bg-color, $diff-op-normal-ratio) !default;
$diff-bg-color-rep: mix($diff-bg-color-rep-base, $diff-bg-color, $diff-op-normal-ratio) !default;
$diff-table-head-color: mix($diff-bg-color, $diff-text-color, 65%) !default;
$diff-table-sidebar-color: mix($diff-bg-color, $diff-text-color, 80%) !default;
$diff-border-color: $diff-text-color !default;
// color for the nonexistent block
// for example, there are a deleted line that has no corresponding one
$diff-bg-color-none-block: mix($diff-bg-color, $diff-table-sidebar-color, 80%) !default;
$diff-bg-color-none-block-alternative: mix($diff-bg-color, $diff-table-sidebar-color, 55%) !default;
// symbol images
$img-space: 'data:image/svg+xml,%3Csvg preserveAspectRatio="xMinYMid meet" viewBox="0 0 12 24" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M4.5 11C4.5 10.1716 5.17157 9.5 6 9.5C6.82843 9.5 7.5 10.1716 7.5 11C7.5 11.8284 6.82843 12.5 6 12.5C5.17157 12.5 4.5 11.8284 4.5 11Z" fill="rgba%2860, 60, 60, 50%25%29"/%3E%3C/svg%3E' !default;
$img-tab: 'data:image/svg+xml,%3Csvg preserveAspectRatio="xMinYMid meet" viewBox="0 0 12 24" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M9.5 10.44L6.62 8.12L7.32 7.26L12.04 11V11.44L7.28 14.9L6.62 13.9L9.48 11.78H0V10.44H9.5Z" fill="rgba%2860, 60, 60, 50%25%29"/%3E%3C/svg%3E' !default;
.diff-wrapper.diff {
--tab-size: 4;
background: repeating-linear-gradient(
-45deg,
$diff-bg-color-none-block,
$diff-bg-color-none-block 0.5em,
$diff-bg-color-none-block-alternative 0.5em,
$diff-bg-color-none-block-alternative 1em
);
border-collapse: collapse;
border-spacing: 0;
border: 1px solid $diff-border-color;
color: $diff-text-color;
empty-cells: show;
font-family: monospace;
font-size: 13px;
width: 100%;
word-break: break-all;
th {
font-weight: 700;
cursor: default;
-webkit-user-select: none;
user-select: none;
}
td {
vertical-align: baseline;
}
td,
th {
border-collapse: separate;
border: none;
padding: 1px 2px;
background: $diff-bg-color;
// make empty cell has height
&:empty:after {
content: ' ';
visibility: hidden;
}
a {
color: #000;
cursor: inherit;
pointer-events: none;
}
}
thead th {
background: $diff-table-head-color;
border-bottom: 1px solid $diff-border-color;
padding: 4px;
text-align: left;
}
tbody {
&.skipped {
border-top: 1px solid $diff-border-color;
td,
th {
display: none;
}
}
th {
background: $diff-table-sidebar-color;
border-right: 1px solid $diff-border-color;
text-align: right;
vertical-align: top;
width: 4em;
&.sign {
background: $diff-bg-color;
border-right: none;
padding: 1px 0;
text-align: center;
width: 1em;
&.del {
background: $diff-bg-color-del;
}
&.ins {
background: $diff-bg-color-ins;
}
}
}
}
&.diff-html {
white-space: pre-wrap;
tab-size: var(--tab-size);
.ch {
line-height: 1em;
background-clip: border-box;
background-repeat: repeat-x;
background-position: left center;
&.sp {
background-image: url($img-space);
background-size: 1ch 1.25em;
}
&.tab {
background-image: url($img-tab);
background-size: calc(var(--tab-size) * 1ch) 1.25em;
background-position: 2px center;
}
}
.change {
&.change-eq {
.old,
.new {
background: $diff-bg-color;
}
}
.old {
background: $diff-bg-color-del;
}
.new {
background: $diff-bg-color-ins;
}
.rep {
background: $diff-bg-color-rep;
}
.old,
.new,
.rep {
&.none {
background: transparent;
cursor: not-allowed;
}
}
ins,
del {
font-weight: bold;
text-decoration: none;
}
ins {
background: $diff-bg-color-ins-highlight;
}
del {
background: $diff-bg-color-del-highlight;
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<div>Hello World! Bad-tempered.</div>
~~~~~~~~~~~~~~~~~~~
Let's add a new line here.
X
Y
Just a context separator. 1
M
N
Do you know in Japanese, "魚の缶詰" means fish can.
Tab visualization test.
G
Say hello to my neighbors.
A
Just a context separator. 2
B
Donec rutrum test.
There is a new inserted line.
C
Sed dolor lorem ipsum hendrerit.
+18
View File
@@ -0,0 +1,18 @@
<p>Hello World! Good-looking.</p>
~~~~~~~~~~~~~~~~~~~
X
Y
Just a context separator. 1
M
N
Do you know in Chinese, "金槍魚罐頭" means tuna can.
Tab visualization test.
G
// remember to delete this line
Say hello to my neighbors.
A
Just a context separator. 2
B
Donec rutrum.
C
Sed dictum lorem ipsum.
+182
View File
@@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff;
use Jfcherng\Diff\Factory\RendererFactory;
use Jfcherng\Diff\Renderer\RendererConstant;
final class DiffHelper
{
/**
* The constructor.
*/
private function __construct()
{
}
/**
* Get the absolute path of the project root directory.
*/
public static function getProjectDirectory(): string
{
static $path;
return $path ??= realpath(__DIR__ . '/..');
}
/**
* Get the information about available renderers.
*/
public static function getRenderersInfo(): array
{
static $info;
if (isset($info)) {
return $info;
}
$glob = implode(\DIRECTORY_SEPARATOR, [
self::getProjectDirectory(),
'src',
'Renderer',
'{' . implode(',', RendererConstant::RENDERER_TYPES) . '}',
'*.php',
]);
$fileNames = array_map(
// get basename without file extension
static fn (string $file): string => pathinfo($file, \PATHINFO_FILENAME),
// paths of all Renderer files
glob($glob, \GLOB_BRACE),
);
$renderers = array_filter(
$fileNames,
// only normal class files are wanted
static fn (string $fileName): bool => (
substr($fileName, 0, 8) !== 'Abstract'
&& substr($fileName, -9) !== 'Interface'
&& substr($fileName, -5) !== 'Trait'
),
);
$info = [];
foreach ($renderers as $renderer) {
$info[$renderer] = RendererFactory::resolveRenderer($renderer)::INFO;
}
return $info;
}
/**
* Get the available renderers.
*
* @return string[] the available renderers
*/
public static function getAvailableRenderers(): array
{
return array_keys(self::getRenderersInfo());
}
/**
* Get the content of the CSS style sheet for HTML renderers.
*
* @throws \LogicException path is a directory
* @throws \RuntimeException path cannot be opened
*/
public static function getStyleSheet(): string
{
static $fileContent;
if (isset($fileContent)) {
return $fileContent;
}
$filePath = self::getProjectDirectory() . '/example/diff-table.css';
$file = new \SplFileObject($filePath, 'r');
return $fileContent = $file->fread($file->getSize());
}
/**
* Gets the diff statistics such as inserted and deleted etc...
*
* @return array<string,float> the statistics
*/
public static function getStatistics(): array
{
return Differ::getInstance()->getStatistics();
}
/**
* All-in-one static method to calculate the diff between two strings (or arrays of strings).
*
* @param string|string[] $old the old string (or array of lines)
* @param string|string[] $new the new string (or array of lines)
* @param string $renderer the renderer name
* @param array $differOptions the options for Differ object
* @param array $rendererOptions the options for renderer object
*
* @return string the rendered differences
*/
public static function calculate(
$old,
$new,
string $renderer = 'Unified',
array $differOptions = [],
array $rendererOptions = []
): string {
// always convert into array form
\is_string($old) && ($old = explode("\n", $old));
\is_string($new) && ($new = explode("\n", $new));
return RendererFactory::getInstance($renderer)
->setOptions($rendererOptions)
->render(
Differ::getInstance()
->setOldNew($old, $new)
->setOptions($differOptions),
)
;
}
/**
* All-in-one static method to calculate the diff between two files.
*
* @param string $old the path of the old file
* @param string $new the path of the new file
* @param string $renderer the renderer name
* @param array $differOptions the options for Differ object
* @param array $rendererOptions the options for renderer object
*
* @throws \LogicException path is a directory
* @throws \RuntimeException path cannot be opened
*
* @return string the rendered differences
*/
public static function calculateFiles(
string $old,
string $new,
string $renderer = 'Unified',
array $differOptions = [],
array $rendererOptions = []
): string {
// we want to leave the line-ending problem to static::calculate()
// so do not set SplFileObject::DROP_NEW_LINE flag
// otherwise, we will lose \r if the line-ending is \r\n
$oldFile = new \SplFileObject($old, 'r');
$newFile = new \SplFileObject($new, 'r');
return self::calculate(
// fread() requires the length > 0 hence we plus 1 for empty files
$oldFile->fread($oldFile->getSize() + 1),
$newFile->fread($newFile->getSize() + 1),
$renderer,
$differOptions,
$rendererOptions,
);
}
}
+522
View File
@@ -0,0 +1,522 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff;
use Jfcherng\Diff\Utility\Arr;
/**
* A comprehensive library for generating differences between two strings
* in multiple formats (unified, side by side HTML etc).
*
* @author Jack Cherng <jfcherng@gmail.com>
* @author Chris Boulton <chris.boulton@interspire.com>
*
* @see http://github.com/chrisboulton/php-diff
*/
final class Differ
{
/**
* @var int a safe number for indicating showing all contexts
*/
public const CONTEXT_ALL = \PHP_INT_MAX >> 3;
/**
* @var string used to indicate a line has no EOL
*
* Arbitrary chars from the 15-16th Unicode reserved areas
* and hopefully, they won't appear in source texts
*/
public const LINE_NO_EOL = "\u{fcf28}\u{fc231}";
/**
* @var array cached properties and their default values
*/
private const CACHED_PROPERTIES = [
'groupedOpcodes' => [],
'groupedOpcodesGnu' => [],
'oldNoEolAtEofIdx' => -1,
'newNoEolAtEofIdx' => -1,
'oldNewComparison' => 0,
];
/**
* @var array array of the options that have been applied for generating the diff
*/
public array $options = [];
/**
* @var string[] the old sequence
*/
private array $old = [];
/**
* @var string[] the new sequence
*/
private array $new = [];
/**
* @var bool is any of cached properties dirty?
*/
private bool $isCacheDirty = true;
/**
* @var SequenceMatcher the sequence matcher
*/
private SequenceMatcher $sequenceMatcher;
private int $oldSrcLength = 0;
private int $newSrcLength = 0;
/**
* @var int the end index for the old if the old has no EOL at EOF
* `-1` means the old has an EOL at EOF
*/
private int $oldNoEolAtEofIdx = -1;
/**
* @var int the end index for the new if the new has no EOL at EOF
* `-1` means the new has an EOL at EOF
*/
private int $newNoEolAtEofIdx = -1;
/**
* @var int the result of comparing the old and the new with the spaceship operator
* `-1` means `old < new`, `0` means `old == new`, `1` means `old > new`
*/
private int $oldNewComparison = 0;
/**
* @var int[][][] array containing the generated opcodes for the differences between the two items
*/
private array $groupedOpcodes = [];
/**
* @var int[][][] array containing the generated opcodes for the differences between the two items (GNU version)
*/
private array $groupedOpcodesGnu = [];
/**
* @var array associative array of the default options available for the Differ class and their default value
*/
private static array $defaultOptions = [
// show how many neighbor lines
// Differ::CONTEXT_ALL can be used to show the whole file
'context' => 3,
// ignore case difference
'ignoreCase' => false,
// ignore line ending difference
'ignoreLineEnding' => false,
// ignore whitespace difference
'ignoreWhitespace' => false,
// if the input sequence is too long, it will just gives up (especially for char-level diff)
'lengthLimit' => 2000,
// if truthy, when inputs are identical, the whole inputs will be rendered in the output
'fullContextIfIdentical' => false,
];
/**
* The constructor.
*
* @param string[] $old array containing the lines of the old string to compare
* @param string[] $new array containing the lines of the new string to compare
* @param array $options the options
*/
public function __construct(array $old, array $new, array $options = [])
{
$this->sequenceMatcher = new SequenceMatcher([], []);
$this->setOldNew($old, $new)->setOptions($options);
}
/**
* Set old and new.
*
* @param string[] $old the old
* @param string[] $new the new
*/
public function setOldNew(array $old, array $new): self
{
return $this->setOld($old)->setNew($new);
}
/**
* Set old.
*
* @param string[] $old the old
*/
public function setOld(array $old): self
{
if ($this->old !== $old) {
$this->old = $old;
$this->isCacheDirty = true;
}
return $this;
}
/**
* Set new.
*
* @param string[] $new the new
*/
public function setNew(array $new): self
{
if ($this->new !== $new) {
$this->new = $new;
$this->isCacheDirty = true;
}
return $this;
}
/**
* Set the options.
*
* @param array $options the options
*/
public function setOptions(array $options): self
{
$mergedOptions = $options + self::$defaultOptions;
if ($this->options !== $mergedOptions) {
$this->options = $mergedOptions;
$this->isCacheDirty = true;
}
return $this;
}
/**
* Get a range of lines from $start to $end from the old.
*
* @param int $start the starting index (negative = count from backward)
* @param null|int $end the ending index (negative = count from backward)
* if is null, it returns a slice from $start to the end
*
* @return string[] array of all of the lines between the specified range
*/
public function getOld(int $start = 0, ?int $end = null): array
{
return Arr::getPartialByIndex($this->old, $start, $end);
}
/**
* Get a range of lines from $start to $end from the new.
*
* @param int $start the starting index (negative = count from backward)
* @param null|int $end the ending index (negative = count from backward)
* if is null, it returns a slice from $start to the end
*
* @return string[] array of all of the lines between the specified range
*/
public function getNew(int $start = 0, ?int $end = null): array
{
return Arr::getPartialByIndex($this->new, $start, $end);
}
/**
* Get the options.
*
* @return array the options
*/
public function getOptions(): array
{
return $this->options;
}
/**
* Get the old no EOL at EOF index.
*
* @return int the old no EOL at EOF index
*/
public function getOldNoEolAtEofIdx(): int
{
return $this->finalize()->oldNoEolAtEofIdx;
}
/**
* Get the new no EOL at EOF index.
*
* @return int the new no EOL at EOF index
*/
public function getNewNoEolAtEofIdx(): int
{
return $this->finalize()->newNoEolAtEofIdx;
}
/**
* Compare the old and the new with the spaceship operator.
*/
public function getOldNewComparison(): int
{
return $this->finalize()->oldNewComparison;
}
/**
* Get the singleton.
*/
public static function getInstance(): self
{
static $singleton;
return $singleton ??= new self([], []);
}
/**
* Gets the diff statistics such as inserted and deleted etc...
*
* @return array<string,float> the statistics
*/
public function getStatistics(): array
{
$ret = [
'inserted' => 0,
'deleted' => 0,
'unmodified' => 0,
'changedRatio' => 0.0,
];
foreach ($this->getGroupedOpcodes() as $hunk) {
foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
if ($op & (SequenceMatcher::OP_INS | SequenceMatcher::OP_REP)) {
$ret['inserted'] += $j2 - $j1;
}
if ($op & (SequenceMatcher::OP_DEL | SequenceMatcher::OP_REP)) {
$ret['deleted'] += $i2 - $i1;
}
}
}
$ret['unmodified'] = $this->oldSrcLength - $ret['deleted'];
$ret['changedRatio'] = 1 - ($ret['unmodified'] / $this->oldSrcLength);
return $ret;
}
/**
* Generate a list of the compiled and grouped opcodes for the differences between the
* two strings. Generally called by the renderer, this class instantiates the sequence
* matcher and performs the actual diff generation and return an array of the opcodes
* for it. Once generated, the results are cached in the Differ class instance.
*
* @return int[][][] array of the grouped opcodes for the generated diff
*/
public function getGroupedOpcodes(): array
{
$this->finalize();
if (!empty($this->groupedOpcodes)) {
return $this->groupedOpcodes;
}
$old = $this->old;
$new = $this->new;
$this->getGroupedOpcodesPre($old, $new);
if ($this->oldNewComparison === 0 && $this->options['fullContextIfIdentical']) {
$opcodes = [
[
[SequenceMatcher::OP_EQ, 0, \count($old), 0, \count($new)],
],
];
} else {
$opcodes = $this->sequenceMatcher
->setSequences($old, $new)
->getGroupedOpcodes($this->options['context'])
;
}
$this->getGroupedOpcodesPost($opcodes);
return $this->groupedOpcodes = $opcodes;
}
/**
* A EOL-at-EOF-sensitive version of getGroupedOpcodes().
*
* @return int[][][] array of the grouped opcodes for the generated diff (GNU version)
*/
public function getGroupedOpcodesGnu(): array
{
$this->finalize();
if (!empty($this->groupedOpcodesGnu)) {
return $this->groupedOpcodesGnu;
}
$old = $this->old;
$new = $this->new;
$this->getGroupedOpcodesGnuPre($old, $new);
if ($this->oldNewComparison === 0 && $this->options['fullContextIfIdentical']) {
$opcodes = [
[
[SequenceMatcher::OP_EQ, 0, \count($old), 0, \count($new)],
],
];
} else {
$opcodes = $this->sequenceMatcher
->setSequences($old, $new)
->getGroupedOpcodes($this->options['context'])
;
}
$this->getGroupedOpcodesGnuPost($opcodes);
return $this->groupedOpcodesGnu = $opcodes;
}
/**
* Triggered before getGroupedOpcodes(). May modify the $old and $new.
*
* @param string[] $old the old
* @param string[] $new the new
*/
private function getGroupedOpcodesPre(array &$old, array &$new): void
{
// append these lines to make sure the last block of the diff result is OP_EQ
static $eolAtEofHelperLines = [
SequenceMatcher::APPENDED_HELPER_LINE,
SequenceMatcher::APPENDED_HELPER_LINE,
SequenceMatcher::APPENDED_HELPER_LINE,
SequenceMatcher::APPENDED_HELPER_LINE,
];
$this->oldSrcLength = \count($old);
array_push($old, ...$eolAtEofHelperLines);
$this->newSrcLength = \count($new);
array_push($new, ...$eolAtEofHelperLines);
}
/**
* Triggered after getGroupedOpcodes(). May modify the $opcodes.
*
* @param int[][][] $opcodes the opcodes
*/
private function getGroupedOpcodesPost(array &$opcodes): void
{
// remove those extra lines cause by adding extra SequenceMatcher::APPENDED_HELPER_LINE lines
foreach ($opcodes as $hunkIdx => &$hunk) {
foreach ($hunk as $blockIdx => &$block) {
// range overflow
if ($block[1] > $this->oldSrcLength) {
$block[1] = $this->oldSrcLength;
}
if ($block[2] > $this->oldSrcLength) {
$block[2] = $this->oldSrcLength;
}
if ($block[3] > $this->newSrcLength) {
$block[3] = $this->newSrcLength;
}
if ($block[4] > $this->newSrcLength) {
$block[4] = $this->newSrcLength;
}
// useless extra block?
/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
if ($block[1] === $block[2] && $block[3] === $block[4]) {
unset($hunk[$blockIdx]);
}
}
if (empty($hunk)) {
unset($opcodes[$hunkIdx]);
}
}
}
/**
* Triggered before getGroupedOpcodesGnu(). May modify the $old and $new.
*
* @param string[] $old the old
* @param string[] $new the new
*/
private function getGroupedOpcodesGnuPre(array &$old, array &$new): void
{
/**
* Make the lines to be prepared for GNU-style diff.
*
* This method checks whether $lines has no EOL at EOF and append a special
* indicator to the last line.
*
* @param string[] $lines the lines created by simply explode("\n", $string)
*/
$createGnuCompatibleLines = static function (array $lines): array {
// note that the $lines should not be empty at this point
// they have at least one element "" in the array because explode("\n", "") === [""]
$lastLineIdx = \count($lines) - 1;
$lastLine = &$lines[$lastLineIdx];
if ($lastLine === '') {
// remove the last plain "" line since we don't need it anymore
// use array_slice() to also reset the array index
$lines = \array_slice($lines, 0, -1);
} else {
// this means the original source has no EOL at EOF
// we append a special indicator to that line so it no longer matches
$lastLine .= self::LINE_NO_EOL;
}
return $lines;
};
$old = $createGnuCompatibleLines($old);
$new = $createGnuCompatibleLines($new);
$this->getGroupedOpcodesPre($old, $new);
}
/**
* Triggered after getGroupedOpcodesGnu(). May modify the $opcodes.
*
* @param int[][][] $opcodes the opcodes
*/
private function getGroupedOpcodesGnuPost(array &$opcodes): void
{
$this->getGroupedOpcodesPost($opcodes);
}
/**
* Claim this class has settled down and we could calculate cached
* properties by current properties.
*
* This method must be called before accessing cached properties to
* make suer that you will not get a outdated cached value.
*
* @internal
*/
private function finalize(): self
{
if ($this->isCacheDirty) {
$this->resetCachedResults();
$this->oldNoEolAtEofIdx = $this->getOld(-1) === [''] ? -1 : \count($this->old);
$this->newNoEolAtEofIdx = $this->getNew(-1) === [''] ? -1 : \count($this->new);
$this->oldNewComparison = $this->old <=> $this->new;
$this->sequenceMatcher->setOptions($this->options);
}
return $this;
}
/**
* Reset cached results.
*/
private function resetCachedResults(): self
{
foreach (self::CACHED_PROPERTIES as $property => $value) {
$this->{$property} = $value;
}
$this->isCacheDirty = false;
return $this;
}
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Exception;
final class FileNotFoundException extends \Exception
{
public function __construct(string $filepath = '', int $code = 0, ?\Throwable $previous = null)
{
parent::__construct("File not found: {$filepath}", $code, $previous);
}
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Exception;
final class UnsupportedFunctionException extends \Exception
{
public function __construct(string $funcName = '', int $code = 0, ?\Throwable $previous = null)
{
parent::__construct("Unsupported function: {$funcName}", $code, $previous);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Factory;
use Jfcherng\Diff\Renderer\Html\LineRenderer\AbstractLineRenderer;
use Jfcherng\Diff\Renderer\RendererConstant;
final class LineRendererFactory
{
/**
* Instances of line renderers.
*
* @var AbstractLineRenderer[]
*/
private static array $singletons = [];
/**
* The constructor.
*/
private function __construct()
{
}
/**
* Get the singleton of a line renderer.
*
* @param string $type the type
* @param mixed ...$ctorArgs the constructor arguments
*/
public static function getInstance(string $type, ...$ctorArgs): AbstractLineRenderer
{
return self::$singletons[$type] ??= self::make($type, ...$ctorArgs);
}
/**
* Make a new instance of a line renderer.
*
* @param string $type the type
* @param mixed ...$ctorArgs the constructor arguments
*
* @throws \InvalidArgumentException
*/
public static function make(string $type, ...$ctorArgs): AbstractLineRenderer
{
$className = RendererConstant::RENDERER_NAMESPACE . '\\Html\\LineRenderer\\' . ucfirst($type);
if (!class_exists($className)) {
throw new \InvalidArgumentException("LineRenderer not found: {$type}");
}
return new $className(...$ctorArgs);
}
}
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Factory;
use Jfcherng\Diff\Renderer\AbstractRenderer;
use Jfcherng\Diff\Renderer\RendererConstant;
final class RendererFactory
{
/**
* Instances of renderers.
*
* @var AbstractRenderer[]
*/
private static array $singletons = [];
/**
* The constructor.
*/
private function __construct()
{
}
/**
* Get the singleton of a renderer.
*
* @param string $renderer the renderer
* @param mixed ...$ctorArgs the constructor arguments
*/
public static function getInstance(string $renderer, ...$ctorArgs): AbstractRenderer
{
return self::$singletons[$renderer] ??= self::make($renderer, ...$ctorArgs);
}
/**
* Make a new instance of a renderer.
*
* @param string $renderer the renderer
* @param mixed ...$ctorArgs the constructor arguments
*
* @throws \InvalidArgumentException
*/
public static function make(string $renderer, ...$ctorArgs): AbstractRenderer
{
$className = self::resolveRenderer($renderer);
if (!isset($className)) {
throw new \InvalidArgumentException("Renderer not found: {$renderer}");
}
return new $className(...$ctorArgs);
}
/**
* Resolve the renderer name into a FQCN.
*
* @param string $renderer the renderer
*/
public static function resolveRenderer(string $renderer): ?string
{
static $cache = [];
if (isset($cache[$renderer])) {
return $cache[$renderer];
}
foreach (RendererConstant::RENDERER_TYPES as $type) {
$className = RendererConstant::RENDERER_NAMESPACE . "\\{$type}\\{$renderer}";
if (class_exists($className)) {
return $cache[$renderer] = $className;
}
}
return null;
}
}
@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer;
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\SequenceMatcher;
use Jfcherng\Diff\Utility\Language;
/**
* Base class for diff renderers.
*
* @todo use typed properties (BC breaking for public interface) in v7
*/
abstract class AbstractRenderer implements RendererInterface
{
/**
* @var array information about this renderer
*/
public const INFO = [
'desc' => 'default_desc',
'type' => 'default_type',
];
/**
* @var bool Is this renderer pure text?
*/
public const IS_TEXT_RENDERER = true;
/**
* @var string[] array of the opcodes and their corresponding symbols
*/
public const SYMBOL_MAP = [
SequenceMatcher::OP_DEL => '-',
SequenceMatcher::OP_EQ => ' ',
SequenceMatcher::OP_INS => '+',
SequenceMatcher::OP_REP => '!',
];
/**
* @var Language the language translation object
*/
protected $t;
/**
* If the input "changes" have `<ins>...</ins>` or `<del>...</del>`,
* which means they have been processed, then `false`. Otherwise, `true`.
*
* @var bool
*/
protected $changesAreRaw = true;
/**
* @var array array of the default options that apply to this renderer
*/
protected static $defaultOptions = [
// how detailed the rendered HTML in-line diff is? (none, line, word, char)
'detailLevel' => 'line',
// renderer language: eng, cht, chs, jpn, ...
// or an array which has the same keys with a language file
// check the "Custom Language" section in the readme for more advanced usage
'language' => 'eng',
// show line numbers in HTML renderers
'lineNumbers' => true,
// show a separator between different diff hunks in HTML renderers
'separateBlock' => true,
// show the (table) header
'showHeader' => true,
// convert spaces/tabs into HTML codes like `<span class="ch sp"> </span>`
// and the frontend is responsible for rendering them with CSS.
// when using this, "spacesToNbsp" should be false and "tabSize" is not respected.
'spaceToHtmlTag' => false,
// the frontend HTML could use CSS "white-space: pre;" to visualize consecutive whitespaces
// but if you want to visualize them in the backend with "&nbsp;", you can set this to true
'spacesToNbsp' => false,
// HTML renderer tab width (negative = do not convert into spaces)
'tabSize' => 4,
// this option is currently only for the Combined renderer.
// it determines whether a replace-type block should be merged or not
// depending on the content changed ratio, which values between 0 and 1.
'mergeThreshold' => 0.8,
// this option is currently only for the Unified and the Context renderers.
// RendererConstant::CLI_COLOR_AUTO = colorize the output if possible (default)
// RendererConstant::CLI_COLOR_ENABLE = force to colorize the output
// RendererConstant::CLI_COLOR_DISABLE = force not to colorize the output
'cliColorization' => RendererConstant::CLI_COLOR_AUTO,
// this option is currently only for the Json renderer.
// internally, ops (tags) are all int type but this is not good for human reading.
// set this to "true" to convert them into string form before outputting.
'outputTagAsString' => false,
// this option is currently only for the Json renderer.
// it controls how the output JSON is formatted.
// see available options on https://www.php.net/manual/en/function.json-encode.php
'jsonEncodeFlags' => \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE,
// this option is currently effective when the "detailLevel" is "word"
// characters listed in this array can be used to make diff segments into a whole
// for example, making "<del>good</del>-<del>looking</del>" into "<del>good-looking</del>"
// this should bring better readability but set this to empty array if you do not want it
'wordGlues' => ['-', ' '],
// change this value to a string as the returned diff if the two input strings are identical
'resultForIdenticals' => null,
// extra HTML classes added to the DOM of the diff container
'wrapperClasses' => ['diff-wrapper'],
];
/**
* @var array array containing the user applied and merged default options for the renderer
*/
protected $options = [];
/**
* The constructor. Instantiates the rendering engine and if options are passed,
* sets the options for the renderer.
*
* @param array $options optionally, an array of the options for the renderer
*/
public function __construct(array $options = [])
{
$this->setOptions($options);
}
/**
* Set the options of the renderer to those supplied in the passed in array.
* Options are merged with the default to ensure that there aren't any missing
* options.
*
* @param array $options the options
*
* @return static
*/
public function setOptions(array $options): self
{
$newOptions = $options + static::$defaultOptions;
$this->updateLanguage(
$this->options['language'] ?? '',
$newOptions['language'],
);
$this->options = $newOptions;
return $this;
}
/**
* Get the options.
*
* @return array the options
*/
public function getOptions(): array
{
return $this->options;
}
/**
* @final
*
* @todo mark this method with "final" in the next major release
*
* @throws \InvalidArgumentException
*/
public function getResultForIdenticals(): string
{
$custom = $this->options['resultForIdenticals'];
if (isset($custom) && !\is_string($custom)) {
throw new \InvalidArgumentException('renderer option `resultForIdenticals` must be null or string.');
}
return $custom ?? $this->getResultForIdenticalsDefault();
}
/**
* Get the renderer default result when the old and the new are the same.
*/
abstract public function getResultForIdenticalsDefault(): string;
final public function render(Differ $differ): string
{
$this->changesAreRaw = true;
// the "no difference" situation may happen frequently
return $differ->getOldNewComparison() === 0 && !$differ->options['fullContextIfIdentical']
? $this->getResultForIdenticals()
: $this->renderWorker($differ);
}
final public function renderArray(array $differArray): string
{
$this->changesAreRaw = false;
return $this->renderArrayWorker($differArray);
}
/**
* The real worker for self::render().
*
* @param Differ $differ the differ object
*/
abstract protected function renderWorker(Differ $differ): string;
/**
* The real worker for self::renderArray().
*
* @param array[][] $differArray the differ array
*/
abstract protected function renderArrayWorker(array $differArray): string;
/**
* Update the Language object.
*
* @param string|string[] $old the old language
* @param string|string[] $new the new language
*
* @return static
*/
protected function updateLanguage($old, $new): self
{
if (!isset($this->t) || $old !== $new) {
$this->t = new Language($new);
}
return $this;
}
/**
* A shorthand to do translation.
*
* @param string $text The text
* @param bool $escapeHtml Escape the translated text for HTML?
*
* @return string the translated text
*/
protected function _(string $text, bool $escapeHtml = true): string
{
$text = $this->t->translate($text);
return $escapeHtml ? htmlspecialchars($text) : $text;
}
}
@@ -0,0 +1,368 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html;
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\Factory\LineRendererFactory;
use Jfcherng\Diff\Renderer\AbstractRenderer;
use Jfcherng\Diff\Renderer\Html\LineRenderer\AbstractLineRenderer;
use Jfcherng\Diff\Renderer\RendererConstant;
use Jfcherng\Diff\SequenceMatcher;
use Jfcherng\Utility\MbString;
/**
* Base renderer for rendering HTML-based diffs.
*/
abstract class AbstractHtml extends AbstractRenderer
{
/**
* @var bool is this renderer pure text?
*/
public const IS_TEXT_RENDERER = false;
/**
* @var string[] array of the different opcodes and how they are mapped to HTML classes
*
* @todo rename to OP_CLASS_MAP in v7
*/
public const TAG_CLASS_MAP = [
SequenceMatcher::OP_DEL => 'del',
SequenceMatcher::OP_EQ => 'eq',
SequenceMatcher::OP_INS => 'ins',
SequenceMatcher::OP_REP => 'rep',
];
/**
* Auto format the content in "changes" to be suitable for HTML output.
*
* This may not be a wanted behavior for some (custom) renderers
* if they want to do this by themselves in a later stage.
*
* @var bool
*/
public const AUTO_FORMAT_CHANGES = true;
public function getResultForIdenticalsDefault(): string
{
return '';
}
/**
* Render and return an array structure suitable for generating HTML
* based differences. Generally called by subclasses that generate a
* HTML based diff and return an array of the changes to show in the diff.
*
* @param Differ $differ the differ object
*
* @return array[][] generated changes, suitable for presentation in HTML
*/
public function getChanges(Differ $differ): array
{
$lineRenderer = LineRendererFactory::make(
$this->options['detailLevel'],
$differ->getOptions(),
$this->options,
);
$old = $differ->getOld();
$new = $differ->getNew();
$changes = [];
foreach ($differ->getGroupedOpcodes() as $hunk) {
$change = [];
foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
$change[] = $this->getDefaultBlock($op, $i1, $j1);
$block = &$change[\count($change) - 1];
// if there are same amount of lines replaced
// we can render the inner detailed changes with corresponding lines
// @todo or use LineRenderer to do the job regardless different line counts?
if ($op === SequenceMatcher::OP_REP && $i2 - $i1 === $j2 - $j1) {
for ($k = $i2 - $i1 - 1; $k >= 0; --$k) {
$this->renderChangedExtent($lineRenderer, $old[$i1 + $k], $new[$j1 + $k]);
}
}
$block['old']['lines'] = \array_slice($old, $i1, $i2 - $i1);
$block['new']['lines'] = \array_slice($new, $j1, $j2 - $j1);
}
unset($block);
$changes[] = $change;
}
if (static::AUTO_FORMAT_CHANGES) {
$this->formatChanges($changes);
}
return $changes;
}
protected function renderWorker(Differ $differ): string
{
$rendered = $this->redererChanges($this->getChanges($differ));
return $this->cleanUpDummyHtmlClosures($rendered);
}
protected function renderArrayWorker(array $differArray): string
{
$this->ensureChangesUseIntTag($differArray);
$rendered = $this->redererChanges($differArray);
return $this->cleanUpDummyHtmlClosures($rendered);
}
/**
* Render the array of changes.
*
* @param array[][] $changes the changes
*
* @todo rename typo to renderChanges() in v7
*/
abstract protected function redererChanges(array $changes): string;
/**
* Renderer the changed extent.
*
* @param AbstractLineRenderer $lineRenderer the line renderer
* @param string $old the old line
* @param string $new the new line
*/
protected function renderChangedExtent(AbstractLineRenderer $lineRenderer, string &$old, string &$new): void
{
static $mbOld, $mbNew;
$mbOld ??= new MbString();
$mbNew ??= new MbString();
$mbOld->set($old);
$mbNew->set($new);
$lineRenderer->render($mbOld, $mbNew);
$old = $mbOld->get();
$new = $mbNew->get();
}
/**
* Get the default block.
*
* @param int $op the operation
* @param int $i1 begin index of the diff of the old array
* @param int $j1 begin index of the diff of the new array
*
* @return array the default block
*
* @todo rename tag to op in v7
*/
protected function getDefaultBlock(int $op, int $i1, int $j1): array
{
return [
'tag' => $op,
'old' => [
'offset' => $i1,
'lines' => [],
],
'new' => [
'offset' => $j1,
'lines' => [],
],
];
}
/**
* Make the content in "changes" suitable for HTML output.
*
* @param array[][] $changes the changes
*/
final protected function formatChanges(array &$changes): void
{
foreach ($changes as &$hunk) {
foreach ($hunk as &$block) {
$block['old']['lines'] = $this->formatLines($block['old']['lines']);
$block['new']['lines'] = $this->formatLines($block['new']['lines']);
/** @phan-suppress-next-line PhanTypeInvalidLeftOperandOfBitwiseOp */
if ($block['tag'] & (SequenceMatcher::OP_REP | SequenceMatcher::OP_DEL)) {
$block['old']['lines'] = str_replace(
RendererConstant::HTML_CLOSURES,
RendererConstant::HTML_CLOSURES_DEL,
$block['old']['lines'],
);
}
/** @phan-suppress-next-line PhanTypeInvalidLeftOperandOfBitwiseOp */
if ($block['tag'] & (SequenceMatcher::OP_REP | SequenceMatcher::OP_INS)) {
$block['new']['lines'] = str_replace(
RendererConstant::HTML_CLOSURES,
RendererConstant::HTML_CLOSURES_INS,
$block['new']['lines'],
);
}
}
}
}
/**
* Make a series of lines suitable for outputting in a HTML rendered diff.
*
* @param string[] $lines array of lines to format
*
* @return string[] array of the formatted lines
*/
protected function formatLines(array $lines): array
{
/**
* To prevent from invoking the same function calls for several times,
* we can glue lines into a string and call functions for one time.
* After that, we split the string back into lines.
*/
return explode(
RendererConstant::IMPLODE_DELIMITER,
$this->formatStringFromLines(
implode(
RendererConstant::IMPLODE_DELIMITER,
$lines,
),
),
);
}
/**
* Make a string suitable for outputting in a HTML rendered diff.
*
* This my involve replacing tab characters with spaces, making the HTML safe
* for output, ensuring that double spaces are replaced with &nbsp; etc.
*
* @param string $string the string of imploded lines
*
* @return string the formatted string
*/
protected function formatStringFromLines(string $string): string
{
if (!$this->options['spaceToHtmlTag']) {
$string = $this->expandTabs($string, $this->options['tabSize']);
}
$string = $this->htmlSafe($string);
if ($this->options['spacesToNbsp']) {
$string = $this->htmlFixSpaces($string);
}
if ($this->options['spaceToHtmlTag']) {
$string = $this->htmlReplaceSpacesToHtmlTag($string);
}
return $string;
}
/**
* Replace tabs in a string with a number of spaces.
*
* @param string $string the input string which may contain tabs
* @param int $tabSize one tab = how many spaces, a negative does nothing
* @param bool $onlyLeadingTabs only expand leading tabs
*
* @return string the string with the tabs converted to spaces
*/
protected function expandTabs(string $string, int $tabSize = 4, bool $onlyLeadingTabs = false): string
{
if ($tabSize < 0) {
return $string;
}
if ($onlyLeadingTabs) {
return preg_replace_callback(
"/^[ \t]{1,}/mS", // tabs and spaces may be mixed
static fn (array $matches): string => str_replace("\t", str_repeat(' ', $tabSize), $matches[0]),
$string,
);
}
return str_replace("\t", str_repeat(' ', $tabSize), $string);
}
/**
* Make a string containing HTML safe for output on a page.
*
* @param string $string the string
*
* @return string the string with the HTML characters replaced by entities
*/
protected function htmlSafe(string $string): string
{
return htmlspecialchars($string, \ENT_NOQUOTES, 'UTF-8');
}
/**
* Replace a string containing spaces with a HTML representation having "&nbsp;".
*
* @param string $string the string of spaces
*
* @return string the HTML representation of the string
*/
protected function htmlFixSpaces(string $string): string
{
return str_replace(' ', '&nbsp;', $string);
}
/**
* Replace spaces/tabs with HTML tags, which may be styled in frontend with CSS.
*
* @param string $string the string of spaces
*
* @return string the HTML representation of the string
*/
protected function htmlReplaceSpacesToHtmlTag(string $string): string
{
return strtr($string, [
' ' => '<span class="ch sp"> </span>',
"\t" => "<span class=\"ch tab\">\t</span>",
]);
}
/**
* Make sure the "changes" array uses int "tag".
*
* Internally, we would like always int form for better performance.
*
* @param array[][] $changes the changes
*/
protected function ensureChangesUseIntTag(array &$changes): void
{
// check if the tag is already int type
if (\is_int($changes[0][0]['tag'] ?? null)) {
return;
}
foreach ($changes as &$hunks) {
foreach ($hunks as &$block) {
$block['tag'] = SequenceMatcher::opStrToInt($block['tag']);
}
}
}
/**
* Clean up empty HTML closures in the given string.
*
* @param string $string the string
*/
protected function cleanUpDummyHtmlClosures(string $string): string
{
return str_replace(
[
RendererConstant::HTML_CLOSURES_DEL[0] . RendererConstant::HTML_CLOSURES_DEL[1],
RendererConstant::HTML_CLOSURES_INS[0] . RendererConstant::HTML_CLOSURES_INS[1],
],
'',
$string,
);
}
}
+518
View File
@@ -0,0 +1,518 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html;
use Jfcherng\Diff\Factory\LineRendererFactory;
use Jfcherng\Diff\Renderer\RendererConstant;
use Jfcherng\Diff\SequenceMatcher;
use Jfcherng\Diff\Utility\ReverseIterator;
use Jfcherng\Utility\MbString;
/**
* Combined HTML diff generator.
*
* Note that this renderer always has no line number.
*/
final class Combined extends AbstractHtml
{
/**
* {@inheritdoc}
*/
public const INFO = [
'desc' => 'Combined',
'type' => 'Html',
];
/**
* {@inheritdoc}
*/
public const AUTO_FORMAT_CHANGES = false;
protected function redererChanges(array $changes): string
{
if (empty($changes)) {
return $this->getResultForIdenticals();
}
$wrapperClasses = [
...$this->options['wrapperClasses'],
'diff', 'diff-html', 'diff-combined',
];
return
'<table class="' . implode(' ', $wrapperClasses) . '">' .
$this->renderTableHeader() .
$this->renderTableHunks($changes) .
'</table>';
}
/**
* Renderer the table header.
*/
protected function renderTableHeader(): string
{
if (!$this->options['showHeader']) {
return '';
}
return
'<thead>' .
'<tr>' .
'<th>' . $this->_('differences') . '</th>' .
'</tr>' .
'</thead>';
}
/**
* Renderer the table separate block.
*/
protected function renderTableSeparateBlock(): string
{
return
'<tbody class="skipped">' .
'<tr>' .
'<td></td>' .
'</tr>' .
'</tbody>';
}
/**
* Renderer table hunks.
*
* @param array[][] $hunks each hunk has many blocks
*/
protected function renderTableHunks(array $hunks): string
{
$ret = '';
foreach ($hunks as $i => $hunk) {
if ($i > 0 && $this->options['separateBlock']) {
$ret .= $this->renderTableSeparateBlock();
}
foreach ($hunk as $block) {
$ret .= $this->renderTableBlock($block);
}
}
return $ret;
}
/**
* Renderer the table block.
*
* @param array $block the block
*/
protected function renderTableBlock(array $block): string
{
switch ($block['tag']) {
case SequenceMatcher::OP_EQ:
$content = $this->renderTableBlockEqual($block);
break;
case SequenceMatcher::OP_INS:
$content = $this->renderTableBlockInsert($block);
break;
case SequenceMatcher::OP_DEL:
$content = $this->renderTableBlockDelete($block);
break;
case SequenceMatcher::OP_REP:
$content = $this->renderTableBlockReplace($block);
break;
default:
$content = '';
}
return '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' . $content . '</tbody>';
}
/**
* Renderer the table block: equal.
*
* @param array $block the block
*/
protected function renderTableBlockEqual(array $block): string
{
$block['new']['lines'] = $this->customFormatLines(
$block['new']['lines'],
SequenceMatcher::OP_EQ,
);
$ret = '';
// note that although we are in a OP_EQ situation,
// the old and the new may not be exactly the same
// because of ignoreCase, ignoreWhitespace, etc
foreach ($block['new']['lines'] as $newLine) {
// we could only pick either the old or the new to show
// here we pick the new one to let the user know what it is now
$ret .= $this->renderTableRow('new', SequenceMatcher::OP_EQ, $newLine);
}
return $ret;
}
/**
* Renderer the table block: insert.
*
* @param array $block the block
*/
protected function renderTableBlockInsert(array $block): string
{
$block['new']['lines'] = $this->customFormatLines(
$block['new']['lines'],
SequenceMatcher::OP_INS,
);
$ret = '';
foreach ($block['new']['lines'] as $newLine) {
$ret .= $this->renderTableRow('new', SequenceMatcher::OP_INS, $newLine);
}
return $ret;
}
/**
* Renderer the table block: delete.
*
* @param array $block the block
*/
protected function renderTableBlockDelete(array $block): string
{
$block['old']['lines'] = $this->customFormatLines(
$block['old']['lines'],
SequenceMatcher::OP_DEL,
);
$ret = '';
foreach ($block['old']['lines'] as $oldLine) {
$ret .= $this->renderTableRow('old', SequenceMatcher::OP_DEL, $oldLine);
}
return $ret;
}
/**
* Renderer the table block: replace.
*
* @param array $block the block
*/
protected function renderTableBlockReplace(array $block): string
{
if ($this->options['detailLevel'] === 'none') {
return
$this->renderTableBlockDelete($block) .
$this->renderTableBlockInsert($block);
}
$ret = '';
$oldLines = $block['old']['lines'];
$newLines = $block['new']['lines'];
$oldLinesCount = \count($oldLines);
$newLinesCount = \count($newLines);
// if the line counts changes, we treat the old and the new as
// "a line with \n in it" and then do one-line-to-one-line diff
if ($oldLinesCount !== $newLinesCount) {
[$oldLines, $newLines] = $this->markReplaceBlockDiff($oldLines, $newLines);
$oldLinesCount = $newLinesCount = 1;
}
$oldLines = $this->customFormatLines($oldLines, SequenceMatcher::OP_DEL);
$newLines = $this->customFormatLines($newLines, SequenceMatcher::OP_INS);
// now $oldLines must has the same line counts with $newlines
for ($no = 0; $no < $newLinesCount; ++$no) {
$mergedLine = $this->mergeReplaceLines($oldLines[$no], $newLines[$no]);
// not merge-able, we fall back to separated form
if (!isset($mergedLine)) {
$ret .=
$this->renderTableBlockDelete($block) .
$this->renderTableBlockInsert($block);
break;
}
$ret .= $this->renderTableRow('rep', SequenceMatcher::OP_REP, $mergedLine);
}
return $ret;
}
/**
* Renderer a content row of the output table.
*
* @param string $tdClass the <td> class
* @param int $op the operation
* @param string $line the line
*/
protected function renderTableRow(string $tdClass, int $op, string $line): string
{
return
'<tr data-type="' . self::SYMBOL_MAP[$op] . '">' .
'<td class="' . $tdClass . '">' . $line . '</td>' .
'</tr>';
}
/**
* Merge two "replace"-type lines into a single line.
*
* The implementation concept is that if we remove all closure parts from
* the old and the new, the rest of them (cleaned line) should be the same.
* And then, we add back those removed closure parts in a correct order.
*
* @param string $oldLine the old line
* @param string $newLine the new line
*
* @return null|string string if merge-able, null otherwise
*/
protected function mergeReplaceLines(string $oldLine, string $newLine): ?string
{
$delParts = $this->analyzeClosureParts(
$oldLine,
RendererConstant::HTML_CLOSURES_DEL,
SequenceMatcher::OP_DEL,
);
$insParts = $this->analyzeClosureParts(
$newLine,
RendererConstant::HTML_CLOSURES_INS,
SequenceMatcher::OP_INS,
);
// get the cleaned line by a non-regex way (should be faster)
// i.e., the new line with all "<ins>...</ins>" parts removed
$mergedLine = $newLine;
foreach (ReverseIterator::fromArray($insParts) as $part) {
$mergedLine = substr_replace(
$mergedLine,
'', // deletion
$part['offset'],
\strlen($part['content']),
);
}
// note that $mergedLine is actually a clean line at this point
if (!$this->isLinesMergeable($oldLine, $newLine, $mergedLine)) {
return null;
}
// before building the $mergedParts, we do some adjustments
$this->revisePartsForBoundaryNewlines($delParts, RendererConstant::HTML_CLOSURES_DEL);
$this->revisePartsForBoundaryNewlines($insParts, RendererConstant::HTML_CLOSURES_INS);
// create a sorted merged parts array
$mergedParts = [...$delParts, ...$insParts];
usort(
$mergedParts,
// first sort by "offsetClean", "order" then by "type"
static fn (array $a, array $b): int => (
$a['offsetClean'] <=> $b['offsetClean']
?: $a['order'] <=> $b['order']
?: ($a['type'] === SequenceMatcher::OP_DEL ? -1 : 1)
),
);
// insert merged parts into the cleaned line
foreach (ReverseIterator::fromArray($mergedParts) as $part) {
$mergedLine = substr_replace(
$mergedLine,
$part['content'],
$part['offsetClean'],
0, // insertion
);
}
return str_replace("\n", '<br>', $mergedLine);
}
/**
* Analyze and get the closure parts information of the line.
*
* Such as
* extract information for "<ins>part 1</ins>" and "<ins>part 2</ins>"
* from "Hello <ins>part 1</ins>SOME OTHER TEXT<ins>part 2</ins> World"
*
* @param string $line the line
* @param string[] $closures the closures
* @param int $type the type
*
* @return array[] the closure information
*/
protected function analyzeClosureParts(string $line, array $closures, int $type): array
{
[$ld, $rd] = $closures;
$ldLength = \strlen($ld);
$rdLength = \strlen($rd);
$parts = [];
$partStart = $partEnd = 0;
$partLengthSum = 0;
// find the next left delimiter
while (false !== ($partStart = strpos($line, $ld, $partEnd))) {
// find the corresponding right delimiter
if (false === ($partEnd = strpos($line, $rd, $partStart + $ldLength))) {
break;
}
$partEnd += $rdLength;
$partLength = $partEnd - $partStart;
$parts[] = [
'type' => $type,
// the sorting order used when both "offsetClean" are the same
'order' => 0,
// the offset in the line
'offset' => $partStart,
// the offset in the cleaned line (i.e., the line with closure parts removed)
'offsetClean' => $partStart - $partLengthSum,
// the content of the part
'content' => substr($line, $partStart, $partLength),
];
$partLengthSum += $partLength;
}
return $parts;
}
/**
* Mark differences between two "replace" blocks.
*
* Each of the returned block (lines) is always only one line.
*
* @param string[] $oldBlock The old block
* @param string[] $newBlock The new block
*
* @return string[][] the value of [[$oldLine], [$newLine]]
*/
protected function markReplaceBlockDiff(array $oldBlock, array $newBlock): array
{
static $mbOld, $mbNew, $lineRenderer;
$mbOld ??= new MbString();
$mbNew ??= new MbString();
$lineRenderer ??= LineRendererFactory::make(
$this->options['detailLevel'],
[], /** @todo is it possible to get the differOptions here? */
$this->options,
);
$mbOld->set(implode("\n", $oldBlock));
$mbNew->set(implode("\n", $newBlock));
$lineRenderer->render($mbOld, $mbNew);
return [
[$mbOld->get()], // one-line block for the old
[$mbNew->get()], // one-line block for the new
];
}
/**
* Determine whether the "replace"-type lines are merge-able or not.
*
* @param string $oldLine the old line
* @param string $newLine the new line
* @param string $cleanLine the clean line
*/
protected function isLinesMergeable(string $oldLine, string $newLine, string $cleanLine): bool
{
$oldLine = str_replace(RendererConstant::HTML_CLOSURES_DEL, '', $oldLine);
$newLine = str_replace(RendererConstant::HTML_CLOSURES_INS, '', $newLine);
$sumLength = \strlen($oldLine) + \strlen($newLine);
/** @var float the changed ratio, 0 <= value < 1 */
$changedRatio = ($sumLength - (\strlen($cleanLine) << 1)) / ($sumLength + 1);
return $changedRatio <= $this->options['mergeThreshold'];
}
/**
* Extract boundary newlines from parts into new parts.
*
* @param array[] $parts the parts
* @param string[] $closures the closures
*
* @see https://git.io/JvVXH
*/
protected function revisePartsForBoundaryNewlines(array &$parts, array $closures): void
{
[$ld, $rd] = $closures;
$ldRegex = preg_quote($ld, '/');
$rdRegex = preg_quote($rd, '/');
for ($i = \count($parts) - 1; $i >= 0; --$i) {
$part = &$parts[$i];
// deal with leading newlines
$part['content'] = preg_replace_callback(
"/(?P<closure>{$ldRegex})(?P<nl>[\r\n]++)/u",
static function (array $matches) use (&$parts, $part, $ld, $rd): string {
// add a new part for the extracted newlines
$part['order'] = -1;
$part['content'] = "{$ld}{$matches['nl']}{$rd}";
$parts[] = $part;
return $matches['closure'];
},
$part['content'],
);
// deal with trailing newlines
$part['content'] = preg_replace_callback(
"/(?P<nl>[\r\n]++)(?P<closure>{$rdRegex})/u",
static function (array $matches) use (&$parts, $part, $ld, $rd): string {
// add a new part for the extracted newlines
$part['order'] = 1;
$part['content'] = "{$ld}{$matches['nl']}{$rd}";
$parts[] = $part;
return $matches['closure'];
},
$part['content'],
);
}
}
/**
* Make lines suitable for HTML output.
*
* @param string[] $lines the lines
* @param int $op the operation
*/
protected function customFormatLines(array $lines, int $op): array
{
if (!$this->changesAreRaw) {
return $lines;
}
static $closureMap = [
SequenceMatcher::OP_DEL => RendererConstant::HTML_CLOSURES_DEL,
SequenceMatcher::OP_INS => RendererConstant::HTML_CLOSURES_INS,
];
$lines = $this->formatLines($lines);
$htmlClosures = $closureMap[$op] ?? null;
foreach ($lines as &$line) {
if ($htmlClosures) {
$line = str_replace(RendererConstant::HTML_CLOSURES, $htmlClosures, $line);
}
// fixes https://github.com/jfcherng/php-diff/issues/34
$line = str_replace("\r\n", "\n", $line);
}
return $lines;
}
}
+263
View File
@@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html;
use Jfcherng\Diff\SequenceMatcher;
/**
* Inline HTML diff generator.
*/
final class Inline extends AbstractHtml
{
/**
* {@inheritdoc}
*/
public const INFO = [
'desc' => 'Inline',
'type' => 'Html',
];
protected function redererChanges(array $changes): string
{
if (empty($changes)) {
return $this->getResultForIdenticals();
}
$wrapperClasses = [
...$this->options['wrapperClasses'],
'diff', 'diff-html', 'diff-inline',
];
return
'<table class="' . implode(' ', $wrapperClasses) . '">' .
$this->renderTableHeader() .
$this->renderTableHunks($changes) .
'</table>';
}
/**
* Renderer the table header.
*/
protected function renderTableHeader(): string
{
if (!$this->options['showHeader']) {
return '';
}
$colspan = $this->options['lineNumbers'] ? '' : ' colspan="2"';
return
'<thead>' .
'<tr>' .
(
$this->options['lineNumbers']
?
'<th>' . $this->_('old_version') . '</th>' .
'<th>' . $this->_('new_version') . '</th>' .
'<th></th>' // diff symbol column
:
''
) .
'<th' . $colspan . '>' . $this->_('differences') . '</th>' .
'</tr>' .
'</thead>';
}
/**
* Renderer the table separate block.
*/
protected function renderTableSeparateBlock(): string
{
$colspan = $this->options['lineNumbers'] ? '4' : '2';
return
'<tbody class="skipped">' .
'<tr>' .
'<td colspan="' . $colspan . '"></td>' .
'</tr>' .
'</tbody>';
}
/**
* Renderer table hunks.
*
* @param array[][] $hunks each hunk has many blocks
*/
protected function renderTableHunks(array $hunks): string
{
$ret = '';
foreach ($hunks as $i => $hunk) {
if ($i > 0 && $this->options['separateBlock']) {
$ret .= $this->renderTableSeparateBlock();
}
foreach ($hunk as $block) {
$ret .= $this->renderTableBlock($block);
}
}
return $ret;
}
/**
* Renderer the table block.
*
* @param array $block the block
*/
protected function renderTableBlock(array $block): string
{
switch ($block['tag']) {
case SequenceMatcher::OP_EQ:
$content = $this->renderTableBlockEqual($block);
break;
case SequenceMatcher::OP_INS:
$content = $this->renderTableBlockInsert($block);
break;
case SequenceMatcher::OP_DEL:
$content = $this->renderTableBlockDelete($block);
break;
case SequenceMatcher::OP_REP:
$content = $this->renderTableBlockReplace($block);
break;
default:
$content = '';
}
return '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' . $content . '</tbody>';
}
/**
* Renderer the table block: equal.
*
* @param array $block the block
*/
protected function renderTableBlockEqual(array $block): string
{
$ret = '';
// note that although we are in a OP_EQ situation,
// the old and the new may not be exactly the same
// because of ignoreCase, ignoreWhitespace, etc
foreach ($block['new']['lines'] as $no => $newLine) {
// we could only pick either the old or the new to show
// here we pick the new one to let the user know what it is now
$ret .= $this->renderTableRow(
'new',
SequenceMatcher::OP_EQ,
$newLine,
$block['old']['offset'] + $no + 1,
$block['new']['offset'] + $no + 1,
);
}
return $ret;
}
/**
* Renderer the table block: insert.
*
* @param array $block the block
*/
protected function renderTableBlockInsert(array $block): string
{
$ret = '';
foreach ($block['new']['lines'] as $no => $newLine) {
$ret .= $this->renderTableRow(
'new',
SequenceMatcher::OP_INS,
$newLine,
null,
$block['new']['offset'] + $no + 1,
);
}
return $ret;
}
/**
* Renderer the table block: delete.
*
* @param array $block the block
*/
protected function renderTableBlockDelete(array $block): string
{
$ret = '';
foreach ($block['old']['lines'] as $no => $oldLine) {
$ret .= $this->renderTableRow(
'old',
SequenceMatcher::OP_DEL,
$oldLine,
$block['old']['offset'] + $no + 1,
null,
);
}
return $ret;
}
/**
* Renderer the table block: replace.
*
* @param array $block the block
*/
protected function renderTableBlockReplace(array $block): string
{
return
$this->renderTableBlockDelete($block) .
$this->renderTableBlockInsert($block);
}
/**
* Renderer a content row of the output table.
*
* @param string $tdClass the <td> class
* @param int $op the operation
* @param string $line the line
* @param null|int $oldLineNum the old line number
* @param null|int $newLineNum the new line number
*/
protected function renderTableRow(
string $tdClass,
int $op,
string $line,
?int $oldLineNum,
?int $newLineNum
): string {
return
'<tr data-type="' . self::SYMBOL_MAP[$op] . '">' .
(
$this->options['lineNumbers']
? $this->renderLineNumberColumns($oldLineNum, $newLineNum)
: ''
) .
'<th class="sign ' . self::TAG_CLASS_MAP[$op] . '">' . self::SYMBOL_MAP[$op] . '</th>' .
'<td class="' . $tdClass . '">' . $line . '</td>' .
'</tr>';
}
/**
* Renderer the line number columns.
*
* @param null|int $oldLineNum The old line number
* @param null|int $newLineNum The new line number
*/
protected function renderLineNumberColumns(?int $oldLineNum, ?int $newLineNum): string
{
return
(
isset($oldLineNum)
? '<th class="n-old">' . $oldLineNum . '</th>'
: '<th></th>'
) .
(
isset($newLineNum)
? '<th class="n-new">' . $newLineNum . '</th>'
: '<th></th>'
);
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html;
/**
* HTML Json diff generator.
*
* @deprecated 6.8.0 Use the "JsonHtml" renderer instead.
*/
final class Json extends JsonHtml
{
}
+59
View File
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html;
use Jfcherng\Diff\SequenceMatcher;
/**
* HTML Json diff generator.
*/
class JsonHtml extends AbstractHtml
{
/**
* {@inheritdoc}
*/
public const INFO = [
'desc' => 'HTML Json',
'type' => 'Html',
];
/**
* {@inheritdoc}
*/
public const IS_TEXT_RENDERER = true;
public function getResultForIdenticalsDefault(): string
{
return '[]';
}
protected function redererChanges(array $changes): string
{
if ($this->options['outputTagAsString']) {
$this->convertTagToString($changes);
}
return json_encode($changes, $this->options['jsonEncodeFlags']);
}
/**
* Convert tags of changes to their string form for better readability.
*
* @param array[][] $changes the changes
*/
protected function convertTagToString(array &$changes): void
{
foreach ($changes as &$hunks) {
foreach ($hunks as &$block) {
$block['tag'] = SequenceMatcher::opIntToStr($block['tag']);
}
}
}
protected function formatStringFromLines(string $string): string
{
return $this->htmlSafe($string);
}
}
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
use Jfcherng\Diff\SequenceMatcher;
/**
* Base renderer for rendering HTML-based line diffs.
*
* @todo use typed properties (BC breaking for public interface) in v7
*/
abstract class AbstractLineRenderer implements LineRendererInterface
{
/**
* @var SequenceMatcher the sequence matcher
*/
protected $sequenceMatcher;
/**
* @var array the differ options
*/
protected $differOptions = [];
/**
* @var array the renderer options
*/
protected $rendererOptions = [];
/**
* The constructor.
*
* @param array $differOptions the differ options
* @param array $rendererOptions the renderer options
*/
public function __construct(array $differOptions, array $rendererOptions)
{
$this->sequenceMatcher = new SequenceMatcher([], []);
$this
->setDifferOptions($differOptions)
->setRendererOptions($rendererOptions)
;
}
/**
* Set the differ options.
*
* @param array $differOptions the differ options
*
* @return static
*/
public function setDifferOptions(array $differOptions): self
{
$this->differOptions = $differOptions;
$this->sequenceMatcher->setOptions($differOptions);
return $this;
}
/**
* Set the renderer options.
*
* @param array $rendererOptions the renderer options
*
* @return static
*/
public function setRendererOptions(array $rendererOptions): self
{
$this->rendererOptions = $rendererOptions;
return $this;
}
/**
* Gets the differ options.
*
* @return array the differ options
*/
public function getDifferOptions(): array
{
return $this->differOptions;
}
/**
* Gets the renderer options.
*
* @return array the renderer options
*/
public function getRendererOptions(): array
{
return $this->rendererOptions;
}
/**
* Get the changed extent segments.
*
* @param string[] $old the old array
* @param string[] $new the new array
*
* @return int[][] the changed extent segments
*/
protected function getChangedExtentSegments(array $old, array $new): array
{
return $this->sequenceMatcher->setSequences($old, $new)->getOpcodes();
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
use Jfcherng\Diff\Renderer\RendererConstant;
use Jfcherng\Diff\SequenceMatcher;
use Jfcherng\Diff\Utility\ReverseIterator;
use Jfcherng\Utility\MbString;
final class Char extends AbstractLineRenderer
{
/**
* @return static
*/
public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface
{
$hunk = $this->getChangedExtentSegments($mbOld->toArray(), $mbNew->toArray());
// reversely iterate hunk
foreach (ReverseIterator::fromArray($hunk) as [$op, $i1, $i2, $j1, $j2]) {
if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_DEL)) {
$mbOld->str_enclose_i(RendererConstant::HTML_CLOSURES, $i1, $i2 - $i1);
}
if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_INS)) {
$mbNew->str_enclose_i(RendererConstant::HTML_CLOSURES, $j1, $j2 - $j1);
}
}
return $this;
}
}
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
use Jfcherng\Diff\Renderer\RendererConstant;
use Jfcherng\Utility\MbString;
final class Line extends AbstractLineRenderer
{
/**
* @return static
*/
public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface
{
[$start, $end] = $this->getChangedExtentRegion($mbOld, $mbNew);
// two strings are the same
if ($end === 0) {
return $this;
}
// two strings are different, we do rendering
$mbOld->str_enclose_i(
RendererConstant::HTML_CLOSURES,
$start,
$end + $mbOld->strlen() - $start + 1,
);
$mbNew->str_enclose_i(
RendererConstant::HTML_CLOSURES,
$start,
$end + $mbNew->strlen() - $start + 1,
);
return $this;
}
/**
* Given two strings, determine where the changes in the two strings begin,
* and where the changes in the two strings end.
*
* @param MbString $mbOld the old megabytes line
* @param MbString $mbNew the new megabytes line
*
* @return int[] Array containing the starting position (non-negative) and the ending position (negative)
* [0, 0] if two strings are the same
*/
protected function getChangedExtentRegion(MbString $mbOld, MbString $mbNew): array
{
// two strings are the same
// most lines should be this cases, an early return could save many function calls
if ($mbOld->getRaw() === $mbNew->getRaw()) {
return [0, 0];
}
// calculate $start
$start = 0;
$startMax = min($mbOld->strlen(), $mbNew->strlen());
while (
$start < $startMax // index out of range
&& $mbOld->getAtRaw($start) === $mbNew->getAtRaw($start)
) {
++$start;
}
// calculate $end
$end = -1; // trick
$endMin = $startMax - $start;
while (
-$end <= $endMin // index out of range
&& $mbOld->getAtRaw($end) === $mbNew->getAtRaw($end)
) {
--$end;
}
return [$start, $end];
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
use Jfcherng\Utility\MbString;
interface LineRendererInterface
{
/**
* Renderer the in-line changed extent.
*
* @param MbString $mbOld the old megabytes line
* @param MbString $mbNew the new megabytes line
*
* @return static
*/
public function render(MbString $mbOld, MbString $mbNew): self;
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
use Jfcherng\Utility\MbString;
final class None extends AbstractLineRenderer
{
/**
* @return static
*/
public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface
{
return $this;
}
}
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html\LineRenderer;
use Jfcherng\Diff\Renderer\RendererConstant;
use Jfcherng\Diff\SequenceMatcher;
use Jfcherng\Diff\Utility\ReverseIterator;
use Jfcherng\Diff\Utility\Str;
use Jfcherng\Utility\MbString;
final class Word extends AbstractLineRenderer
{
/**
* @return static
*/
public function render(MbString $mbOld, MbString $mbNew): LineRendererInterface
{
static $splitRegex = '/([' . RendererConstant::PUNCTUATIONS_RANGE . '])/uS';
static $dummyHtmlClosure = RendererConstant::HTML_CLOSURES[0] . RendererConstant::HTML_CLOSURES[1];
$pregFlag = \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY;
$oldWords = $mbOld->toArraySplit($splitRegex, -1, $pregFlag);
$newWords = $mbNew->toArraySplit($splitRegex, -1, $pregFlag);
$hunk = $this->getChangedExtentSegments($oldWords, $newWords);
// reversely iterate hunk
foreach (ReverseIterator::fromArray($hunk) as [$op, $i1, $i2, $j1, $j2]) {
if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_DEL)) {
$oldWords[$i1] = RendererConstant::HTML_CLOSURES[0] . $oldWords[$i1];
$oldWords[$i2 - 1] .= RendererConstant::HTML_CLOSURES[1];
// insert dummy HTML closure to ensure there are always
// the same amounts of HTML closures in $oldWords and $newWords
// thus, this should make that "wordGlues" work correctly
// @see https://github.com/jfcherng/php-diff/pull/25
if ($op === SequenceMatcher::OP_DEL) {
array_splice($newWords, $j1, 0, [$dummyHtmlClosure]);
}
}
if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_INS)) {
$newWords[$j1] = RendererConstant::HTML_CLOSURES[0] . $newWords[$j1];
$newWords[$j2 - 1] .= RendererConstant::HTML_CLOSURES[1];
if ($op === SequenceMatcher::OP_INS) {
array_splice($oldWords, $i1, 0, [$dummyHtmlClosure]);
}
}
}
if (!empty($hunk) && !empty($this->rendererOptions['wordGlues'])) {
$regexGlues = array_map(
static fn (string $glue): string => preg_quote($glue, '/'),
$this->rendererOptions['wordGlues'],
);
$gluePattern = '/^(?:' . implode('|', $regexGlues) . ')+$/uS';
$this->glueWordsResult($oldWords, $gluePattern);
$this->glueWordsResult($newWords, $gluePattern);
}
$mbOld->set(implode('', $oldWords));
$mbNew->set(implode('', $newWords));
return $this;
}
/**
* Beautify diff result by glueing words.
*
* What this function does is basically making
* ["<diff_begin>good<diff_end>", "-", "<diff_begin>looking<diff_end>"]
* into
* ["<diff_begin>good", "-", "looking<diff_end>"].
*
* @param array $words the words
* @param string $gluePattern the regex to determine a string is purely glue or not
*/
protected function glueWordsResult(array &$words, string $gluePattern): void
{
/** @var int index of the word which has the trailing closure */
$endClosureIdx = -1;
foreach ($words as $idx => &$word) {
if ($word === '') {
continue;
}
if ($endClosureIdx < 0) {
if (Str::endsWith($word, RendererConstant::HTML_CLOSURES[1])) {
$endClosureIdx = $idx;
}
} elseif (Str::startsWith($word, RendererConstant::HTML_CLOSURES[0])) {
$words[$endClosureIdx] = substr($words[$endClosureIdx], 0, -\strlen(RendererConstant::HTML_CLOSURES[1]));
$word = substr($word, \strlen(RendererConstant::HTML_CLOSURES[0]));
$endClosureIdx = $idx;
} elseif (!preg_match($gluePattern, $word)) {
$endClosureIdx = -1;
}
}
}
}
@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Html;
use Jfcherng\Diff\SequenceMatcher;
/**
* Side by Side HTML diff generator.
*/
final class SideBySide extends AbstractHtml
{
/**
* {@inheritdoc}
*/
public const INFO = [
'desc' => 'Side by side',
'type' => 'Html',
];
protected function redererChanges(array $changes): string
{
if (empty($changes)) {
return $this->getResultForIdenticals();
}
$wrapperClasses = [
...$this->options['wrapperClasses'],
'diff', 'diff-html', 'diff-side-by-side',
];
return
'<table class="' . implode(' ', $wrapperClasses) . '">' .
$this->renderTableHeader() .
$this->renderTableHunks($changes) .
'</table>';
}
/**
* Renderer the table header.
*/
protected function renderTableHeader(): string
{
if (!$this->options['showHeader']) {
return '';
}
$colspan = $this->options['lineNumbers'] ? ' colspan="2"' : '';
return
'<thead>' .
'<tr>' .
'<th' . $colspan . '>' . $this->_('old_version') . '</th>' .
'<th' . $colspan . '>' . $this->_('new_version') . '</th>' .
'</tr>' .
'</thead>';
}
/**
* Renderer the table separate block.
*/
protected function renderTableSeparateBlock(): string
{
$colspan = $this->options['lineNumbers'] ? '4' : '2';
return
'<tbody class="skipped">' .
'<tr>' .
'<td colspan="' . $colspan . '"></td>' .
'</tr>' .
'</tbody>';
}
/**
* Renderer table hunks.
*
* @param array[][] $hunks each hunk has many blocks
*/
protected function renderTableHunks(array $hunks): string
{
$ret = '';
foreach ($hunks as $i => $hunk) {
if ($i > 0 && $this->options['separateBlock']) {
$ret .= $this->renderTableSeparateBlock();
}
foreach ($hunk as $block) {
$ret .= $this->renderTableBlock($block);
}
}
return $ret;
}
/**
* Renderer the table block.
*
* @param array $block the block
*/
protected function renderTableBlock(array $block): string
{
switch ($block['tag']) {
case SequenceMatcher::OP_EQ:
$content = $this->renderTableBlockEqual($block);
break;
case SequenceMatcher::OP_INS:
$content = $this->renderTableBlockInsert($block);
break;
case SequenceMatcher::OP_DEL:
$content = $this->renderTableBlockDelete($block);
break;
case SequenceMatcher::OP_REP:
$content = $this->renderTableBlockReplace($block);
break;
default:
$content = '';
}
return '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' . $content . '</tbody>';
}
/**
* Renderer the table block: equal.
*
* @param array $block the block
*/
protected function renderTableBlockEqual(array $block): string
{
$ret = '';
$rowCount = \count($block['new']['lines']);
for ($no = 0; $no < $rowCount; ++$no) {
$ret .= $this->renderTableRow(
$block['old']['lines'][$no],
$block['new']['lines'][$no],
$block['old']['offset'] + $no + 1,
$block['new']['offset'] + $no + 1,
);
}
return $ret;
}
/**
* Renderer the table block: insert.
*
* @param array $block the block
*/
protected function renderTableBlockInsert(array $block): string
{
$ret = '';
foreach ($block['new']['lines'] as $no => $newLine) {
$ret .= $this->renderTableRow(
null,
$newLine,
null,
$block['new']['offset'] + $no + 1,
);
}
return $ret;
}
/**
* Renderer the table block: delete.
*
* @param array $block the block
*/
protected function renderTableBlockDelete(array $block): string
{
$ret = '';
foreach ($block['old']['lines'] as $no => $oldLine) {
$ret .= $this->renderTableRow(
$oldLine,
null,
$block['old']['offset'] + $no + 1,
null,
);
}
return $ret;
}
/**
* Renderer the table block: replace.
*
* @param array $block the block
*/
protected function renderTableBlockReplace(array $block): string
{
$ret = '';
$lineCountMax = max(\count($block['old']['lines']), \count($block['new']['lines']));
for ($no = 0; $no < $lineCountMax; ++$no) {
if (isset($block['old']['lines'][$no])) {
$oldLineNum = $block['old']['offset'] + $no + 1;
$oldLine = $block['old']['lines'][$no];
} else {
$oldLineNum = $oldLine = null;
}
if (isset($block['new']['lines'][$no])) {
$newLineNum = $block['new']['offset'] + $no + 1;
$newLine = $block['new']['lines'][$no];
} else {
$newLineNum = $newLine = null;
}
$ret .= $this->renderTableRow($oldLine, $newLine, $oldLineNum, $newLineNum);
}
return $ret;
}
/**
* Renderer a content row of the output table.
*
* @param null|string $oldLine the old line
* @param null|string $newLine the new line
* @param null|int $oldLineNum the old line number
* @param null|int $newLineNum the new line number
*/
protected function renderTableRow(
?string $oldLine,
?string $newLine,
?int $oldLineNum,
?int $newLineNum
): string {
return
'<tr>' .
(
$this->options['lineNumbers']
? $this->renderLineNumberColumn('old', $oldLineNum)
: ''
) .
$this->renderLineContentColumn('old', $oldLine) .
(
$this->options['lineNumbers']
? $this->renderLineNumberColumn('new', $newLineNum)
: ''
) .
$this->renderLineContentColumn('new', $newLine) .
'</tr>';
}
/**
* Renderer the line number column.
*
* @param string $type the diff type
* @param null|int $lineNum the line number
*/
protected function renderLineNumberColumn(string $type, ?int $lineNum): string
{
return isset($lineNum)
? '<th class="n-' . $type . '">' . $lineNum . '</th>'
: '<th></th>';
}
/**
* Renderer the line content column.
*
* @param string $type the diff type
* @param null|string $content the line content
*/
protected function renderLineContentColumn(string $type, ?string $content): string
{
return
'<td class="' . $type . (isset($content) ? '' : ' none') . '">' .
$content .
'</td>';
}
}
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer;
final class RendererConstant
{
/**
* The base namespace of renderers.
*
* @var string
*/
public const RENDERER_NAMESPACE = __NAMESPACE__;
/**
* Available renderer types.
*
* @var string[]
*/
public const RENDERER_TYPES = ['Html', 'Text'];
/**
* Closures that are used to enclose different parts in string.
*
* Arbitrary chars from the 15-16th Unicode reserved areas
* and hopefully, they won't appear in source texts.
*
* @var string[]
*/
public const HTML_CLOSURES = ["\u{fcffc}\u{ff2fb}", "\u{fff41}\u{fcffc}"];
/**
* Closures that are used to enclose deleted chars in output HTML.
*
* @var string[]
*/
public const HTML_CLOSURES_DEL = ['<del>', '</del>'];
/**
* Closures that are used to enclose inserted chars in output HTML.
*
* @var string[]
*/
public const HTML_CLOSURES_INS = ['<ins>', '</ins>'];
/**
* The delimiter to be used as the glue in string/array functions.
*
* Arbitrary chars from the 15-16th Unicode reserved areas
* and hopefully, it won't appear in source texts.
*
* @var string
*/
public const IMPLODE_DELIMITER = "\u{ff2fa}\u{fcffc}\u{fff42}";
/**
* Regex range for punctuations.
*
* Presuming the regex delimiter is "/".
*
* @var string
*/
public const PUNCTUATIONS_RANGE = (
// Latin-1 Supplement
// @see https://unicode-table.com/en/blocks/latin-1-supplement/
"\u{0080}-\u{00BB}" .
// Spacing Modifier Letters
// @see https://unicode-table.com/en/blocks/spacing-modifier-letters/
"\u{02B0}-\u{02FF}" .
// Combining Diacritical Marks
// @see https://unicode-table.com/en/blocks/combining-diacritical-marks/
"\u{0300}-\u{036F}" .
// Small Form Variants
// @see https://unicode-table.com/en/blocks/small-form-variants/
"\u{FE50}-\u{FE6F}" .
// General Punctuation
// @see https://unicode-table.com/en/blocks/general-punctuation/
"\u{2000}-\u{206F}" .
// Supplemental Punctuation
// @see https://unicode-table.com/en/blocks/supplemental-punctuation/
"\u{2E00}-\u{2E7F}" .
// CJK Symbols and Punctuation
// @see https://unicode-table.com/en/blocks/cjk-symbols-and-punctuation/
"\u{3000}-\u{303F}" .
// Ideographic Symbols and Punctuation
// @see https://unicode-table.com/en/blocks/ideographic-symbols-and-punctuation/
"\u{16FE0}-\u{16FFF}" .
// hmm... these seem to be no rule
" \t\r\n$,.:;!?'\"()\\[\\]{}%@<=>_+\\-*\\/~\\\\|" .
' $,.:;!?’"()[]{}%@<=>_+-*/~\|' .
'「」『』〈〉《》【】()()‘’“”' .
'.‧・・•·¿'
);
/**
* Colorize the CLI output if possible.
*
* @var int
*/
public const CLI_COLOR_AUTO = -1;
/**
* Force not to colorize the CLI output.
*
* @var int
*/
public const CLI_COLOR_DISABLE = 0;
/**
* Force to colorize the CLI output if possible.
*
* @var int
*/
public const CLI_COLOR_ENABLE = 1;
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer;
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\Exception\UnsupportedFunctionException;
/**
* Renderer Interface.
*/
interface RendererInterface
{
/**
* Get the renderer result when the old and the new are the same.
*/
public function getResultForIdenticals(): string;
/**
* Render the differ and return the result.
*
* @param Differ $differ the Differ object to be rendered
*/
public function render(Differ $differ): string;
/**
* Render the differ array and return the result.
*
* @param array[][] $differArray the Differ array to be rendered
*
* @throws UnsupportedFunctionException if the renderer does not support this method
*/
public function renderArray(array $differArray): string;
}
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Text;
use Jfcherng\Diff\Exception\UnsupportedFunctionException;
use Jfcherng\Diff\Renderer\AbstractRenderer;
use Jfcherng\Diff\Renderer\RendererConstant;
use Jfcherng\Utility\CliColor;
/**
* Base renderer for rendering text-based diffs.
*/
abstract class AbstractText extends AbstractRenderer
{
/**
* @var bool is this renderer pure text?
*/
public const IS_TEXT_RENDERER = true;
/**
* @var string the diff output representing there is no EOL at EOF in the GNU diff tool
*/
public const GNU_OUTPUT_NO_EOL_AT_EOF = '\ No newline at end of file';
/**
* @var bool controls whether cliColoredString() is enabled or not
*/
protected $isCliColorEnabled = false;
public function setOptions(array $options): AbstractRenderer
{
parent::setOptions($options);
// determine $this->isCliColorEnabled
if ($this->options['cliColorization'] === RendererConstant::CLI_COLOR_ENABLE) {
$this->isCliColorEnabled = true;
} elseif ($this->options['cliColorization'] === RendererConstant::CLI_COLOR_DISABLE) {
$this->isCliColorEnabled = false;
} else {
$this->isCliColorEnabled = \PHP_SAPI === 'cli' && $this->hasColorSupport(\STDOUT);
}
return $this;
}
public function getResultForIdenticalsDefault(): string
{
return '';
}
protected function renderArrayWorker(array $differArray): string
{
throw new UnsupportedFunctionException(__METHOD__);
return ''; // make IDE not complain
}
/**
* Colorize the string for CLI output.
*
* @param string $str the string
* @param null|string $symbol the symbol
*
* @return string the (maybe) colorized string
*/
protected function cliColoredString(string $str, ?string $symbol): string
{
static $symbolToStyles = [
'@' => ['f_purple', 'bold'], // header
'-' => ['f_red', 'bold'], // deleted
'+' => ['f_green', 'bold'], // inserted
'!' => ['f_yellow', 'bold'], // replaced
];
$styles = $symbolToStyles[$symbol] ?? [];
if (!$this->isCliColorEnabled || empty($styles)) {
return $str;
}
return CliColor::color($str, $styles);
}
/**
* Returns true if the stream supports colorization.
*
* Colorization is disabled if not supported by the stream:
*
* This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo
* terminals via named pipes, so we can only check the environment.
*
* Reference: Composer\XdebugHandler\Process::supportsColor
* https://github.com/composer/xdebug-handler
*
* @see https://github.com/symfony/console/blob/647c51ff073300a432a4a504e29323cf0d5e0571/Output/StreamOutput.php#L81-L124
*
* @param resource $stream
*
* @return bool true if the stream supports colorization, false otherwise
*
* @suppress PhanUndeclaredFunction
*/
protected function hasColorSupport($stream): bool
{
// Follow https://no-color.org/
if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) {
return false;
}
if ('Hyper' === getenv('TERM_PROGRAM')) {
return true;
}
if (\DIRECTORY_SEPARATOR === '\\') {
return (\function_exists('sapi_windows_vt100_support')
&& @sapi_windows_vt100_support($stream))
|| false !== getenv('ANSICON')
|| 'ON' === getenv('ConEmuANSI')
|| 'xterm' === getenv('TERM');
}
if (\function_exists('stream_isatty')) {
return @stream_isatty($stream);
}
if (\function_exists('posix_isatty')) {
return @posix_isatty($stream);
}
$stat = @fstat($stream);
// Check if formatted mode is S_IFCHR
return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Text;
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\SequenceMatcher;
/**
* Context diff generator.
*
* @see https://en.wikipedia.org/wiki/Diff#Context_format
*/
final class Context extends AbstractText
{
/**
* {@inheritdoc}
*/
public const INFO = [
'desc' => 'Context',
'type' => 'Text',
];
/**
* @var int the union of OPs that indicate there is a change
*/
public const OP_BLOCK_CHANGED =
SequenceMatcher::OP_DEL |
SequenceMatcher::OP_INS |
SequenceMatcher::OP_REP;
protected function renderWorker(Differ $differ): string
{
$ret = '';
foreach ($differ->getGroupedOpcodesGnu() as $hunk) {
$lastBlockIdx = \count($hunk) - 1;
// note that these line number variables are 0-based
$i1 = $hunk[0][1];
$i2 = $hunk[$lastBlockIdx][2];
$j1 = $hunk[0][3];
$j2 = $hunk[$lastBlockIdx][4];
$ret .=
$this->cliColoredString("***************\n", '@') .
$this->renderHunkHeader('*', $i1, $i2) .
$this->renderHunkOld($differ, $hunk) .
$this->renderHunkHeader('-', $j1, $j2) .
$this->renderHunkNew($differ, $hunk);
}
return $ret;
}
/**
* Render the hunk header.
*
* @param string $symbol the symbol
* @param int $a1 the begin index
* @param int $a2 the end index
*/
protected function renderHunkHeader(string $symbol, int $a1, int $a2): string
{
$a1x = $a1 + 1; // 1-based begin line number
return $this->cliColoredString(
"{$symbol}{$symbol}{$symbol} " .
($a1x < $a2 ? "{$a1x},{$a2}" : $a2) .
" {$symbol}{$symbol}{$symbol}{$symbol}\n",
'@', // symbol
);
}
/**
* Render the old hunk.
*
* @param Differ $differ the differ object
* @param int[][] $hunk the hunk
*/
protected function renderHunkOld(Differ $differ, array $hunk): string
{
$ret = '';
$hunkOps = 0;
$noEolAtEofIdx = $differ->getOldNoEolAtEofIdx();
foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
// OP_INS does not belongs to an old hunk
if ($op === SequenceMatcher::OP_INS) {
continue;
}
$hunkOps |= $op;
$ret .= $this->renderContext(
self::SYMBOL_MAP[$op],
$differ->getOld($i1, $i2),
$i2 === $noEolAtEofIdx,
);
}
// if there is no content changed, the hunk context should be omitted
return $hunkOps & self::OP_BLOCK_CHANGED ? $ret : '';
}
/**
* Render the new hunk.
*
* @param Differ $differ the differ object
* @param int[][] $hunk the hunk
*/
protected function renderHunkNew(Differ $differ, array $hunk): string
{
$ret = '';
$hunkOps = 0;
$noEolAtEofIdx = $differ->getNewNoEolAtEofIdx();
foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
// OP_DEL does not belongs to a new hunk
if ($op === SequenceMatcher::OP_DEL) {
continue;
}
$hunkOps |= $op;
$ret .= $this->renderContext(
self::SYMBOL_MAP[$op],
$differ->getNew($j1, $j2),
$j2 === $noEolAtEofIdx,
);
}
// if there is no content changed, the hunk context should be omitted
return $hunkOps & self::OP_BLOCK_CHANGED ? $ret : '';
}
/**
* Render the context array with the symbol.
*
* @param string $symbol the symbol
* @param string[] $context the context
* @param bool $noEolAtEof there is no EOL at EOF in this block
*/
protected function renderContext(string $symbol, array $context, bool $noEolAtEof = false): string
{
if (empty($context)) {
return '';
}
$ret = "{$symbol} " . implode("\n{$symbol} ", $context) . "\n";
$ret = $this->cliColoredString($ret, $symbol);
if ($noEolAtEof) {
$ret .= self::GNU_OUTPUT_NO_EOL_AT_EOF . "\n";
}
return $ret;
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Text;
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\SequenceMatcher;
/**
* Plain text Json diff generator.
*/
final class JsonText extends AbstractText
{
/**
* {@inheritdoc}
*/
public const INFO = [
'desc' => 'Text JSON',
'type' => 'Text',
];
protected function renderWorker(Differ $differ): string
{
$ret = [];
foreach ($differ->getGroupedOpcodes() as $hunk) {
$ret[] = $this->renderHunk($differ, $hunk);
}
if ($this->options['outputTagAsString']) {
$this->convertTagToString($ret);
}
return json_encode($ret, $this->options['jsonEncodeFlags']);
}
/**
* Render the hunk.
*
* @param Differ $differ the differ object
* @param int[][] $hunk the hunk
*/
protected function renderHunk(Differ $differ, array $hunk): array
{
$ret = [];
foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
$ret[] = [
'tag' => $op,
'old' => [
'offset' => $i1,
'lines' => $differ->getOld($i1, $i2),
],
'new' => [
'offset' => $j1,
'lines' => $differ->getNew($j1, $j2),
],
];
}
return $ret;
}
/**
* Convert tags of changes to their string form for better readability.
*
* @param array[][] $changes the changes
*/
protected function convertTagToString(array &$changes): void
{
foreach ($changes as &$hunks) {
foreach ($hunks as &$block) {
$block['tag'] = SequenceMatcher::opIntToStr($block['tag']);
}
}
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Renderer\Text;
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\SequenceMatcher;
/**
* Unified diff generator.
*
* @see https://en.wikipedia.org/wiki/Diff#Unified_format
*/
final class Unified extends AbstractText
{
/**
* {@inheritdoc}
*/
public const INFO = [
'desc' => 'Unified',
'type' => 'Text',
];
protected function renderWorker(Differ $differ): string
{
$ret = '';
foreach ($differ->getGroupedOpcodesGnu() as $hunk) {
$ret .= $this->renderHunkHeader($differ, $hunk);
$ret .= $this->renderHunkBlocks($differ, $hunk);
}
return $ret;
}
/**
* Render the hunk header.
*
* @param Differ $differ the differ
* @param int[][] $hunk the hunk
*/
protected function renderHunkHeader(Differ $differ, array $hunk): string
{
$lastBlockIdx = \count($hunk) - 1;
// note that these line number variables are 0-based
$i1 = $hunk[0][1];
$i2 = $hunk[$lastBlockIdx][2];
$j1 = $hunk[0][3];
$j2 = $hunk[$lastBlockIdx][4];
$oldLinesCount = $i2 - $i1;
$newLinesCount = $j2 - $j1;
return $this->cliColoredString(
'@@' .
' -' .
// the line number in GNU diff is 1-based, so we add 1
// a special case is when a hunk has only changed blocks,
// i.e., context is set to 0, we do not need the adding
($i1 === $i2 ? $i1 : $i1 + 1) .
// if the line counts is 1, it can (and mostly) be omitted
($oldLinesCount === 1 ? '' : ",{$oldLinesCount}") .
' +' .
($j1 === $j2 ? $j1 : $j1 + 1) .
($newLinesCount === 1 ? '' : ",{$newLinesCount}") .
" @@\n",
'@', // symbol
);
}
/**
* Render the hunk content.
*
* @param Differ $differ the differ
* @param int[][] $hunk the hunk
*/
protected function renderHunkBlocks(Differ $differ, array $hunk): string
{
$ret = '';
$oldNoEolAtEofIdx = $differ->getOldNoEolAtEofIdx();
$newNoEolAtEofIdx = $differ->getNewNoEolAtEofIdx();
foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
// note that although we are in a OP_EQ situation,
// the old and the new may not be exactly the same
// because of ignoreCase, ignoreWhitespace, etc
if ($op === SequenceMatcher::OP_EQ) {
// we could only pick either the old or the new to show
// note that the GNU diff will use the old one because it creates a patch
$ret .= $this->renderContext(
' ',
$differ->getOld($i1, $i2),
$i2 === $oldNoEolAtEofIdx,
);
continue;
}
if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_DEL)) {
$ret .= $this->renderContext(
'-',
$differ->getOld($i1, $i2),
$i2 === $oldNoEolAtEofIdx,
);
}
if ($op & (SequenceMatcher::OP_REP | SequenceMatcher::OP_INS)) {
$ret .= $this->renderContext(
'+',
$differ->getNew($j1, $j2),
$j2 === $newNoEolAtEofIdx,
);
}
}
return $ret;
}
/**
* Render the context array with the symbol.
*
* @param string $symbol the symbol
* @param string[] $context the context
* @param bool $noEolAtEof there is no EOL at EOF in this block
*/
protected function renderContext(string $symbol, array $context, bool $noEolAtEof = false): string
{
if (empty($context)) {
return '';
}
$ret = $symbol . implode("\n{$symbol}", $context) . "\n";
$ret = $this->cliColoredString($ret, $symbol);
if ($noEolAtEof) {
$ret .= self::GNU_OUTPUT_NO_EOL_AT_EOF . "\n";
}
return $ret;
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Utility;
final class Arr
{
/**
* Get a partial array slice with start/end indexes.
*
* @param array $array the array
* @param int $start the starting index (negative = count from backward)
* @param null|int $end the ending index (negative = count from backward)
* if is null, it returns a slice from $start to the end
*
* @return array array of all of the lines between the specified range
*/
public static function getPartialByIndex(array $array, int $start = 0, ?int $end = null): array
{
$count = \count($array);
// make $end set
$end ??= $count;
// make $start non-negative
if ($start < 0) {
$start += $count;
if ($start < 0) {
$start = 0;
}
}
// make $end non-negative
if ($end < 0) {
$end += $count;
if ($end < 0) {
$end = 0;
}
}
// make the length non-negative
return \array_slice($array, $start, max(0, $end - $start));
}
/**
* Determines whether the array is associative.
*
* @param array $arr the array
*
* @return bool `true` if the array is associative, `false` otherwise
*/
public static function isAssociative($arr): bool
{
foreach ($arr as $key => $value) {
if (\is_string($key)) {
return true;
}
}
return false;
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Utility;
final class Language
{
/**
* @var string[] the translation dict
*/
private array $translations = [];
/**
* @var string the language name
*/
private string $language = '_custom_';
/**
* The constructor.
*
* @param array<int,string|string[]>|string|string[] $target the language ID or translations dict
*/
public function __construct($target = 'eng')
{
$this->load($target);
}
/**
* Gets the language.
*
* @return string the language
*/
public function getLanguage(): string
{
return $this->language;
}
/**
* Gets the translations.
*
* @return array the translations
*/
public function getTranslations(): array
{
return $this->translations;
}
/**
* Loads the target language.
*
* @param array<int,string|string[]>|string|string[] $target the language ID or translations dict
*/
public function load($target): void
{
$this->translations = $this->resolve($target);
$this->language = \is_string($target) ? $target : '_custom_';
}
/**
* Translates the text.
*
* @param string $text the text
*/
public function translate(string $text): string
{
return $this->translations[$text] ?? "![{$text}]";
}
/**
* Get the translations from the language file.
*
* @param string $language the language
*
* @throws \Exception fail to decode the JSON file
* @throws \LogicException path is a directory
* @throws \RuntimeException path cannot be opened
*
* @return string[]
*/
private static function getTranslationsByLanguage(string $language): array
{
$filePath = __DIR__ . "/../languages/{$language}.json";
$file = new \SplFileObject($filePath, 'r');
$fileContent = $file->fread($file->getSize());
try {
$decoded = json_decode($fileContent, true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \Exception(sprintf('Fail to decode JSON file (%s): %s', realpath($filePath), (string) $e));
}
return (array) $decoded;
}
/**
* Resolves the target language.
*
* @param array<int,string|string[]>|string|string[] $target the language ID or translations array
*
* @throws \InvalidArgumentException
*
* @return string[] the resolved translations
*/
private function resolve($target): array
{
if (\is_string($target)) {
return self::getTranslationsByLanguage($target);
}
if (\is_array($target)) {
// $target is an associative array
if (Arr::isAssociative($target)) {
return $target;
}
// $target is a list of "key-value pairs or language ID"
return array_reduce(
$target,
fn (array $carry, $translation): array => array_merge($carry, $this->resolve($translation)),
[],
);
}
throw new \InvalidArgumentException('$target is not in valid form');
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Utility;
final class ReverseIterator
{
public const ITERATOR_GET_VALUE = 0;
public const ITERATOR_GET_KEY = 1 << 0;
public const ITERATOR_GET_BOTH = 1 << 1;
/**
* The constructor.
*/
private function __construct()
{
}
/**
* Iterate the array reversely.
*
* @param array $array the array
* @param int $flags the flags
*/
public static function fromArray(array $array, int $flags = self::ITERATOR_GET_VALUE): \Generator
{
// iterate [key => value] pair
if ($flags & self::ITERATOR_GET_BOTH) {
for (end($array); ($key = key($array)) !== null; prev($array)) {
yield $key => current($array);
}
return;
}
// iterate only key
if ($flags & self::ITERATOR_GET_KEY) {
for (end($array); ($key = key($array)) !== null; prev($array)) {
yield $key;
}
return;
}
// iterate only value
for (end($array); key($array) !== null; prev($array)) {
yield current($array);
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jfcherng\Diff\Utility;
final class Str
{
/**
* Determine if a given string starts with a given substring.
*
* @param string $haystack the haystack
* @param string $needle the needle
*/
public static function startsWith(string $haystack, string $needle): bool
{
return substr($haystack, 0, \strlen($needle)) === $needle;
}
/**
* Determine if a given string ends with a given substring.
*
* @param string $haystack the haystack
* @param string $needle the needle
*/
public static function endsWith(string $haystack, string $needle): bool
{
return substr($haystack, -\strlen($needle)) === $needle;
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Стара версия",
"new_version": "Нова версия",
"differences": "Разлики"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "旧版本",
"new_version": "新版本",
"differences": "差异"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "舊版本",
"new_version": "新版本",
"differences": "差異"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Původní",
"new_version": "Nové",
"differences": "Rozdíly"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Alt",
"new_version": "Neu",
"differences": "Unterschiede"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Old",
"new_version": "New",
"differences": "Differences"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Avant",
"new_version": "Après",
"differences": "Différences"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Vecchio",
"new_version": "Nuovo",
"differences": "Differenze"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "古い",
"new_version": "新しい",
"differences": "差異"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Tidligere versjon",
"new_version": "Ny versjon",
"differences": "Differanse"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Original",
"new_version": "Nova",
"differences": "Diferenças"
}
+1
View File
@@ -0,0 +1 @@
Please use 3-char `ISO 639-2/B` naming convention: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Старая версия",
"new_version": "Новая версия",
"differences": "Различия"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Anterior",
"new_version": "Nuevo",
"differences": "Diferencias"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Eski",
"new_version": "Yeni",
"differences": "Değişiklikler"
}
+5
View File
@@ -0,0 +1,5 @@
{
"old_version": "Було",
"new_version": "Стало",
"differences": "Відмінності"
}