From 7d973d3b18ef9b558fd784b9447251ee73c91c23 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 08:00:07 +0100 Subject: [PATCH 01/24] Document methods * Add missing documentation. * Fixes and reformatting. --- lib/jblond/Diff.php | 3 +-- lib/jblond/Diff/Renderer/MainRenderer.php | 4 ++-- lib/jblond/Diff/SequenceMatcher.php | 28 ++++++++--------------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/lib/jblond/Diff.php b/lib/jblond/Diff.php index 5caf06e..fa3e7fc 100644 --- a/lib/jblond/Diff.php +++ b/lib/jblond/Diff.php @@ -46,8 +46,7 @@ class Diff private $groupedCodes; /** - * @var array Associative array containing the default options available - * for the diff class and their default value. + * @var array Associative array containing the default options available for the diff class and their default value. * * - context The amount of lines to include around blocks that differ. * - trimEqual Strip blocks of equal lines from the start and end of the text. diff --git a/lib/jblond/Diff/Renderer/MainRenderer.php b/lib/jblond/Diff/Renderer/MainRenderer.php index f0b579a..720c460 100644 --- a/lib/jblond/Diff/Renderer/MainRenderer.php +++ b/lib/jblond/Diff/Renderer/MainRenderer.php @@ -124,7 +124,7 @@ protected function renderSequences(): array * 4 - The end line in the second sequence. * * The different types of tags include: - * replace - The string from $startOld to $endOld in $oldText should be replaced by + * replace - The string in $oldText from $startOld to $endOld, should be replaced by * the string in $newText from $startNew to $endNew. * delete - The string in $oldText from $startOld to $endNew should be deleted. * insert - The string in $newText from $startNew to $endNew should be inserted at $startOld in @@ -291,7 +291,7 @@ public function sequenceToArray(string $pattern, string $sequence): array * E.g. *
      *         1234567
-     * OLd => "abcdefg" Start marker inserted at position 3
+     * Old => "abcdefg" Start marker inserted at position 3
      * New => "ab123fg"   End marker inserted at position 6
      * 
* diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index e7a92b6..e335217 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -46,17 +46,12 @@ class SequenceMatcher private $b2j = []; /** - * @var array - */ - private $options = []; - - /** - * @var null|array + * @var array A list of all of the op-codes for the differences between the compared strings. */ private $opCodes; /** - * @var null|array + * @var array A nested set of arrays for all of the matching sub-sequences the compared strings. */ private $matchingBlocks; @@ -72,9 +67,8 @@ class SequenceMatcher ]; /** - * The constructor. With the sequences being passed, they'll be set for the - * sequence matcher and it will perform a basic cleanup & calculate junk - * elements. + * The constructor. With the sequences being passed, they'll be set for the sequence matcher and it will perform a + * basic cleanup & calculate junk elements. * * @param string|array $old A string or array containing the lines to compare against. * @param string|array $new A string or array containing the lines to compare. @@ -295,11 +289,9 @@ public function getGroupedOpCodes(): array } /** - * Return a list of all of the op codes for the differences between the - * two strings. + * Return a list of all of the op codes for the differences between the two strings. * - * The nested array returned contains an array describing the op code - * which includes: + * The nested array returned contains an array describing the op code which includes: * 0 - The type of tag (as described below) for the op code. * 1 - The beginning line in the first sequence. * 2 - The end line in the first sequence. @@ -366,12 +358,10 @@ public function getOpCodes(): array } /** - * Return a nested set of arrays for all of the matching sub-sequences - * in the strings $a and $b. + * Return a nested set of arrays for all of the matching sub-sequences in compared strings $a and $b. * - * Each block contains the lower constraint of the block in $a, the lower - * constraint of the block in $b and finally the number of lines that the - * block continues for. + * Each block contains the lower constraint of the block in $a, the lower constraint of the block in $b and finally + * the number of lines that the block continues for. * * @return array Nested array of the matching blocks, as described by the function. */ From bb0eed4a8af3c68d91daae48b2289797841f7ff9 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 08:02:29 +0100 Subject: [PATCH 02/24] Optimize Sequence renderer * Reduction of cyclomatic complexity. --- lib/jblond/Diff/Renderer/MainRenderer.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/jblond/Diff/Renderer/MainRenderer.php b/lib/jblond/Diff/Renderer/MainRenderer.php index 720c460..73616ea 100644 --- a/lib/jblond/Diff/Renderer/MainRenderer.php +++ b/lib/jblond/Diff/Renderer/MainRenderer.php @@ -146,23 +146,23 @@ protected function renderSequences(): array $oldBlock = $this->formatLines(array_slice($oldText, $startOld, $blockSizeOld)); $newBlock = $this->formatLines(array_slice($newText, $startNew, $blockSizeNew)); - if ($tag == 'equal') { - // Old block equals New block + if ($tag != 'delete' && $tag != 'insert') { + // Old block "equals" New block or is replaced. $blocks[$lastBlock]['base']['lines'] += $oldBlock; $blocks[$lastBlock]['changed']['lines'] += $newBlock; continue; } - if ($tag == 'replace' || $tag == 'delete') { - // Inline differences or old block doesn't exist in the new text. + if ($tag == 'delete') { + // Block of version1 doesn't exist in version2. $blocks[$lastBlock]['base']['lines'] += $oldBlock; + continue; } - if ($tag == 'replace' || $tag == 'insert') { - // Inline differences or the new block doesn't exist in the old text. - $blocks[$lastBlock]['changed']['lines'] += $newBlock; - } + // Block of version2 doesn't exist in version1. + $blocks[$lastBlock]['changed']['lines'] += $newBlock; } + $changes[] = $blocks; } From 87d49c90d9a59a8385fdd1631a7061c613d5ed26 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 08:27:13 +0100 Subject: [PATCH 03/24] Start option to ignore blank lines * Add option `ignoreLines` to control the ability to ignore blank or empty lines. * Add styles of ignored lines to example css. * Add generator for ignored lines to HTML SideBySide renderer. * Add `options` property to the Sequence Mather. * Refactor option `ignoreNewLines` of the Sequence Matcher. * Add flags to define the level of ignoring blank lines. * Add tag `ignored` to the opCodes. * Add method stripLines to Similarity. * Add default option `ignoreLines` to Diff. * Add tests for testing option `ignoreLines` of the Sequence Matcher. * Reformat code. --- example/dark-theme.css | 13 ++++ example/example.php | 1 + example/styles.css | 31 ++++++--- lib/jblond/Diff.php | 26 ++++++-- lib/jblond/Diff/Renderer/Html/SideBySide.php | 66 ++++++++++++++++++++ lib/jblond/Diff/Renderer/MainRenderer.php | 6 ++ lib/jblond/Diff/SequenceMatcher.php | 55 ++++++++++++++-- lib/jblond/Diff/Similarity.php | 31 +++++++++ tests/Diff/SequenceMatcherTest.php | 50 ++++++++++++++- 9 files changed, 259 insertions(+), 20 deletions(-) diff --git a/example/dark-theme.css b/example/dark-theme.css index 77669d2..fec7395 100644 --- a/example/dark-theme.css +++ b/example/dark-theme.css @@ -97,6 +97,19 @@ a, a:visited { background: #EEBB00; } +.DifferencesSideBySide .ChangeIgnore .Left, +.DifferencesSideBySide .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesSideBySide .ChangeIgnore .Left.Ignore { + background: #4B4C57; +} + +.DifferencesSideBySide .ChangeIgnore .Right.Ignore { + background: #4B4C57; +} + /* * HTML Unified Diff */ diff --git a/example/example.php b/example/example.php index fe2cb0c..e4d7ed0 100644 --- a/example/example.php +++ b/example/example.php @@ -20,6 +20,7 @@ 'trimEqual' => false, 'ignoreWhitespace' => true, 'ignoreCase' => true, + 'ignoreLines' => Diff::DIFF_IGNORE_LINE_EMPTY, ]; // Choose one of the initializations. diff --git a/example/styles.css b/example/styles.css index bbf2ba7..57af672 100644 --- a/example/styles.css +++ b/example/styles.css @@ -6,8 +6,8 @@ body { } pre { - width: 100%; overflow: auto; + width: 100%; } /* @@ -15,34 +15,34 @@ pre { */ .Differences { - width: 100%; border-collapse: collapse; border-spacing: 0; empty-cells: show; + width: 100%; } .Differences thead th { - text-align: left; - border-bottom: 1px solid #000000; background: #AAAAAA; + border-bottom: 1px solid #000000; color: #000000; padding: 4px; + text-align: left; } .Differences tbody th { - text-align: right; background: #CCCCCC; - width: 4em; - padding: 1px 2px; border-right: 1px solid #000000; - vertical-align: top; font-size: 13px; + padding: 1px 2px; + text-align: right; + vertical-align: top; + width: 4em; } .Differences td { - padding: 1px 2px; font-family: Consolas, monospace; font-size: 13px; + padding: 1px 2px; } .Differences .Skipped { @@ -77,6 +77,19 @@ pre { background: #FFDD88; } +.DifferencesSideBySide .ChangeIgnore .Left, +.DifferencesSideBySide .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesSideBySide .ChangeIgnore .Left.Ignore { + background: #F7F7F7; +} + +.DifferencesSideBySide .ChangeIgnore .Right.Ignore { + background: #F7F7F7; +} + .Differences ins, .Differences del { text-decoration: none; diff --git a/lib/jblond/Diff.php b/lib/jblond/Diff.php index fa3e7fc..02b437b 100644 --- a/lib/jblond/Diff.php +++ b/lib/jblond/Diff.php @@ -28,6 +28,18 @@ */ class Diff { + /** + * Flag to disable ignore of successive empty/blank lines. + */ + public const DIFF_IGNORE_LINE_NONE = 0; + /** + * Flag to ignore successive empty lines. + */ + public const DIFF_IGNORE_LINE_EMPTY = 1; + /** + * Flag to ignore successive blank lines. (Lines which contain no or only non printable characters.) + */ + public const DIFF_IGNORE_LINE_BLANK = 2; /** * @var array The first version to compare. * Each element contains a line of this string. @@ -48,18 +60,20 @@ class Diff /** * @var array Associative array containing the default options available for the diff class and their default value. * - * - context The amount of lines to include around blocks that differ. - * - trimEqual Strip blocks of equal lines from the start and end of the text. - * - ignoreWhitespace When true, tabs and spaces are ignored while comparing. - * The spacing of version1 is leading. - * - ignoreCase When true, character casing is ignored while comparing. - * The casing of version1 is leading. + * - context The amount of lines to include around blocks that differ. + * - trimEqual Strip blocks of equal lines from the start and end of the text. + * - ignoreWhitespace True to ignore differences in tabs and spaces. + * - ignoreCase True to ignore differences in character casing. + * - ignoreLines 0: None. + * 1: Ignore empty lines. + * 2: Ignore blank lines. */ private $defaultOptions = [ 'context' => 3, 'trimEqual' => true, 'ignoreWhitespace' => false, 'ignoreCase' => false, + 'ignoreLines' => self::DIFF_IGNORE_LINE_NONE, ]; /** diff --git a/lib/jblond/Diff/Renderer/Html/SideBySide.php b/lib/jblond/Diff/Renderer/Html/SideBySide.php index cedf2d2..8abcdac 100644 --- a/lib/jblond/Diff/Renderer/Html/SideBySide.php +++ b/lib/jblond/Diff/Renderer/Html/SideBySide.php @@ -282,4 +282,70 @@ public function generateDiffFooter(): string { return ''; } + + /** + * @inheritDoc + * + * @return string Html code representing table rows showing ignored text. + */ + public function generateLinesIgnore(array $changes): string + { + $html = ''; + + // Is below comparison result ever false? + if (count($changes['base']['lines']) >= count($changes['changed']['lines'])) { + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $toLine = ' '; + $changedLine = ' '; + if (isset($changes['changed']['lines'][$lineNo])) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $changedLine = $changes['changed']['lines'][$lineNo]; + } + + $html .= << + $fromLine + + $line + + $toLine + + $changedLine + + +HTML; + } + + return $html; + } + + foreach ($changes['changed']['lines'] as $lineNo => $changedLine) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $fromLine = ' '; + $line = ' '; + if (isset($changes['base']['lines'][$lineNo])) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $line = $changes['base']['lines'][$lineNo]; + } + + $line = str_replace(["\0", "\1"], $this->options['deleteMarkers'], $line); + $changedLine = str_replace(["\0", "\1"], $this->options['insertMarkers'], $changedLine); + + $html .= << + $fromLine + + $line + + $toLine + + $changedLine + + +HTML; + } + + return $html; + } } diff --git a/lib/jblond/Diff/Renderer/MainRenderer.php b/lib/jblond/Diff/Renderer/MainRenderer.php index 73616ea..cc1e2e4 100644 --- a/lib/jblond/Diff/Renderer/MainRenderer.php +++ b/lib/jblond/Diff/Renderer/MainRenderer.php @@ -80,6 +80,10 @@ public function renderOutput(array $changes, object $subRenderer) case 'replace': $output .= $subRenderer->generateLinesReplace($change); break; + case 'ignore': + // TODO: Keep backward compatible with renderers? + $output .= $subRenderer->generateLinesIgnore($change); + break; } $output .= $subRenderer->generateBlockFooter($change); @@ -130,6 +134,8 @@ protected function renderSequences(): array * insert - The string in $newText from $startNew to $endNew should be inserted at $startOld in * $oldText. * equal - The two strings with the specified ranges are equal. + * ignore - The string in $oldText from $startOld to $endOld and + * the string in $newText from $startNew to $endNew are different, but considered to be equal. */ $blockSizeOld = $endOld - $startOld; diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index e335217..60c55b2 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -22,6 +22,18 @@ */ class SequenceMatcher { + /** + * Flag to disable ignore of successive empty/blank lines. + */ + public const DIFF_IGNORE_LINE_NONE = 0; + /** + * Flag to ignore empty lines. + */ + public const DIFF_IGNORE_LINE_EMPTY = 1; + /** + * Flag to ignore blank lines. (Lines which contain no or only non printable characters.) + */ + public const DIFF_IGNORE_LINE_BLANK = 2; /** * @var array The first sequence to compare against. */ @@ -30,6 +42,13 @@ class SequenceMatcher * @var array The second sequence. */ protected $new; + /** + * @var array Associative array containing the options that will be applied for generating the diff. + * The key-value pairs are set at the constructor of this class. + * + * @see SequenceMatcher::setOptions() + */ + protected $options = []; /** * @var string|array Either a string or an array containing a callback function to determine * if a line is "junk" or not. @@ -39,12 +58,10 @@ class SequenceMatcher * @var array Array of characters that are considered junk from the second sequence. Characters are the array key. */ private $junkDict = []; - /** * @var array Array of indices that do not contain junk elements. */ private $b2j = []; - /** * @var array A list of all of the op-codes for the differences between the compared strings. */ @@ -56,14 +73,22 @@ class SequenceMatcher private $matchingBlocks; /** - * @var array + * @var array Associative array containing the default options available for the diff class and their default value. + * + * - context The amount of lines to include around blocks that differ. + * - trimEqual Strip blocks of equal lines from the start and end of the text. + * - ignoreWhitespace True to ignore differences in tabs and spaces. + * - ignoreCase True to ignore differences in character casing. + * - ignoreLines 0: None. + * 1: Ignore empty lines. + * 2: Ignore blank lines. */ private $defaultOptions = [ 'context' => 3, 'trimEqual' => true, 'ignoreWhitespace' => false, 'ignoreCase' => false, - 'ignoreNewLines' => false, + 'ignoreLines' => self::DIFF_IGNORE_LINE_NONE, ]; /** @@ -330,6 +355,28 @@ public function getOpCodes(): array $tag = 'insert'; } + if ($this->options['ignoreLines']) { + $part1 = array_slice($this->old, $i, $ai - $i); + $part2 = array_slice($this->new, $j, $bj - $j); + + if ($this->options['ignoreLines'] == 2) { + array_walk($part1, function (&$line) { + $line = trim($line); + }); + array_walk($part2, function (&$line) { + $line = trim($line); + }); + unset($line); + } + + if ( + ($tag == 'delete' && implode('', $part1) == '') || + ($tag == 'insert' && implode('', $part2) == '') + ) { + $tag = 'ignore'; + } + } + if ($tag) { $this->opCodes[] = [ $tag, diff --git a/lib/jblond/Diff/Similarity.php b/lib/jblond/Diff/Similarity.php index acf640d..12285fe 100644 --- a/lib/jblond/Diff/Similarity.php +++ b/lib/jblond/Diff/Similarity.php @@ -71,6 +71,9 @@ public function getSimilarity(int $type = self::CALC_DEFAULT): float case self::CALC_FASTEST: return $this->getRatioFastest(); default: + if ($this->options['ignoreLines']) { + $this->stripLines(); + } $matches = array_reduce( $this->getMatchingBlocks(), function ($carry, $item) { @@ -80,6 +83,7 @@ function ($carry, $item) { ); return $this->calculateRatio($matches, count($this->old) + count($this->new)); + // TODO: Restore original (un-stripped) versions? } } @@ -151,6 +155,33 @@ private function getRatioFastest(): float return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); } + /** + * Strip empty or blank lines from the sequences to compare. + * + */ + private function stripLines(): void + { + foreach (['old', 'new'] as $version) { + if ($this->options['ignoreLines'] == self::DIFF_IGNORE_LINE_BLANK) { + array_walk( + $this->$version, + function (&$line) { + $line = trim($line); + } + ); + unset($line); + } + + $this->$version = array_filter( + $this->$version, + function ($line) { + return $line != ''; + } + ); + } + + $this->setSequences(array_values($this->old), array_values($this->new)); + } /** * Helper function to calculate the number of matches for Ratio(). diff --git a/tests/Diff/SequenceMatcherTest.php b/tests/Diff/SequenceMatcherTest.php index cbd2c93..aab2251 100644 --- a/tests/Diff/SequenceMatcherTest.php +++ b/tests/Diff/SequenceMatcherTest.php @@ -84,7 +84,7 @@ public function testGetGroupedOpCodesIgnoreWhitespaceTrue() } /** - *T est the opCodes of the differences between version1 and version2 with option ignoreCase enabled. + * Test the opCodes of the differences between version1 and version2 with option ignoreCase enabled. */ public function testGetGroupedOpCodesIgnoreCaseTrue() { @@ -97,4 +97,52 @@ public function testGetGroupedOpCodesIgnoreCaseTrue() $this->assertEquals([], $sequenceMatcher->getGroupedOpCodes()); } + + /** + * Test the opCodes of the differences between version1 and version2 with option ignoreLines set to empty. + */ + public function testGetGroupedOpCodesIgnoreLinesEmpty() + { + // Test with ignoreCase enabled. Both sequences are considered to be the same. + $sequenceMatcher = new SequenceMatcher( + [0, 1, 2, 3], + [0, 1, '', 2, 3], + ['ignoreLines' => SequenceMatcher::DIFF_IGNORE_LINE_EMPTY] + ); + + $this->assertEquals( + [ + [ + ['equal', 0, 2, 0, 2], + ['ignore', 2, 2, 2, 3], + ['equal', 2, 4, 3, 5], + ], + ], + $sequenceMatcher->getGroupedOpCodes() + ); + } + + /** + * Test the opCodes of the differences between version1 and version2 with option ignoreLines set to blank. + */ + public function testGetGroupedOpCodesIgnoreLinesBlank() + { + // Test with ignoreCase enabled. Both sequences are considered to be the same. + $sequenceMatcher = new SequenceMatcher( + [0, 1, 2, 3], + [0, 1, "\t", 2, 3], + ['ignoreLines' => SequenceMatcher::DIFF_IGNORE_LINE_BLANK] + ); + + $this->assertEquals( + [ + [ + ['equal', 0, 2, 0, 2], + ['ignore', 2, 2, 2, 3], + ['equal', 2, 4, 3, 5], + ], + ], + $sequenceMatcher->getGroupedOpCodes() + ); + } } From 2925723bc82951a677975476740e644a4d14c203 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 15 Dec 2020 15:08:37 +0100 Subject: [PATCH 04/24] Update Key words --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0ce866a..43742be 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,10 @@ "php", "diff", "side-by-sidediff", - "unified" + "unified", + "udiff", + "unidiff", + "unified diff" ], "authors": [ { From caef106269c42948c23f2eadfe80ac06dd7d3ec2 Mon Sep 17 00:00:00 2001 From: JBlond Date: Tue, 15 Dec 2020 15:18:48 +0100 Subject: [PATCH 05/24] exclude not needed files for zip / composer export --- .gitattributes | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7460cf4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +assets/ export-ignore +tests/ export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +phpcs.xml export-ignore From dabb4676a6b7553f7e9b7b36b79e0e01a6c2658a Mon Sep 17 00:00:00 2001 From: JBlond Date: Wed, 16 Dec 2020 10:02:05 +0100 Subject: [PATCH 06/24] keep assets for readme file --- .gitattributes | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 7460cf4..fdade2f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,4 @@ -assets/ export-ignore -tests/ export-ignore +tests export-ignore .editorconfig export-ignore .gitattributes export-ignore phpcs.xml export-ignore From 3591515d1362080fa7b8de5614929de67eb4b747 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 12:01:22 +0100 Subject: [PATCH 07/24] Optimize constant usage Instead of defining constants with the same name and values at different namespaces, they're now defined at an interface which can be shared among classes by implementing this interface. --- lib/jblond/Diff.php | 15 ++---------- lib/jblond/Diff/ConstantsInterface.php | 33 ++++++++++++++++++++++++++ lib/jblond/Diff/SequenceMatcher.php | 32 ++++++++++--------------- 3 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 lib/jblond/Diff/ConstantsInterface.php diff --git a/lib/jblond/Diff.php b/lib/jblond/Diff.php index 02b437b..048e1db 100644 --- a/lib/jblond/Diff.php +++ b/lib/jblond/Diff.php @@ -5,6 +5,7 @@ namespace jblond; use InvalidArgumentException; +use jblond\Diff\ConstantsInterface; use jblond\Diff\SequenceMatcher; use jblond\Diff\Similarity; use OutOfRangeException; @@ -26,20 +27,8 @@ * @version 2.3.0 * @link https://github.com/JBlond/php-diff */ -class Diff +class Diff implements ConstantsInterface { - /** - * Flag to disable ignore of successive empty/blank lines. - */ - public const DIFF_IGNORE_LINE_NONE = 0; - /** - * Flag to ignore successive empty lines. - */ - public const DIFF_IGNORE_LINE_EMPTY = 1; - /** - * Flag to ignore successive blank lines. (Lines which contain no or only non printable characters.) - */ - public const DIFF_IGNORE_LINE_BLANK = 2; /** * @var array The first version to compare. * Each element contains a line of this string. diff --git a/lib/jblond/Diff/ConstantsInterface.php b/lib/jblond/Diff/ConstantsInterface.php new file mode 100644 index 0000000..121e7cb --- /dev/null +++ b/lib/jblond/Diff/ConstantsInterface.php @@ -0,0 +1,33 @@ + + * @copyright (c) 2020 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.3.0 + * @link https://github.com/JBlond/php-diff + */ +interface ConstantsInterface +{ + /** + * Flag to disable ignore of successive empty/blank lines. + */ + public const DIFF_IGNORE_LINE_NONE = 0; + /** + * Flag to ignore empty lines. + */ + public const DIFF_IGNORE_LINE_EMPTY = 1; + /** + * Flag to ignore blank lines. (Lines which contain no or only non printable characters.) + */ + public const DIFF_IGNORE_LINE_BLANK = 2; + +} diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index 60c55b2..ef97f17 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -20,20 +20,8 @@ * @version 2.3.0 * @link https://github.com/JBlond/php-diff */ -class SequenceMatcher +class SequenceMatcher implements ConstantsInterface { - /** - * Flag to disable ignore of successive empty/blank lines. - */ - public const DIFF_IGNORE_LINE_NONE = 0; - /** - * Flag to ignore empty lines. - */ - public const DIFF_IGNORE_LINE_EMPTY = 1; - /** - * Flag to ignore blank lines. (Lines which contain no or only non printable characters.) - */ - public const DIFF_IGNORE_LINE_BLANK = 2; /** * @var array The first sequence to compare against. */ @@ -360,12 +348,18 @@ public function getOpCodes(): array $part2 = array_slice($this->new, $j, $bj - $j); if ($this->options['ignoreLines'] == 2) { - array_walk($part1, function (&$line) { - $line = trim($line); - }); - array_walk($part2, function (&$line) { - $line = trim($line); - }); + array_walk( + $part1, + function (&$line) { + $line = trim($line); + } + ); + array_walk( + $part2, + function (&$line) { + $line = trim($line); + } + ); unset($line); } From 19634bbaf9882ec014c8e6aa1c469356df7b0749 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 12:08:25 +0100 Subject: [PATCH 08/24] Document option `ignoreLines` --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30e5a38..5920ae9 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ $options = [ 'ignoreWhitespace' => true, 'ignoreCase' => true, 'context' => 2, - 'cliColor' => true // for cli output + 'cliColor' => true, // for cli output + 'ignoreLines' => Diff::DIFF_IGNORE_LINE_BLANK, ]; // Initialize the diff class. @@ -140,7 +141,6 @@ at [jQuery-Merge-for-php-diff](https://github.com/Xiphe/jQuery-Merge-for-php-dif ## Todo -* Ability to ignore blank line changes * 3 way diff support ## Contributors From 9403eba4aacad481f762bcd029b298a8fcdb3d7a Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 17 Dec 2020 07:28:05 +0100 Subject: [PATCH 09/24] Add deprecation notice for missing method If a used sub renderer, which extends the main renderer, a deprecation notice will be thrown when the sub renderer is missing method `generateLinesIgnore`. --- lib/jblond/Diff/Renderer/MainRenderer.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/jblond/Diff/Renderer/MainRenderer.php b/lib/jblond/Diff/Renderer/MainRenderer.php index cc1e2e4..3a26186 100644 --- a/lib/jblond/Diff/Renderer/MainRenderer.php +++ b/lib/jblond/Diff/Renderer/MainRenderer.php @@ -65,7 +65,23 @@ public function renderOutput(array $changes, object $subRenderer) strlen($this->options['equalityMarkers'][1]) ); + $deprecationTriggered = false; foreach ($blocks as $change) { + if ( + $subRenderer instanceof MainRenderer && + !method_exists($subRenderer, 'generateLinesIgnore') && + $change['tag'] == 'ignore' + ) { + if (!$deprecationTriggered) { + trigger_error( + 'The use of a subRenderer without method generateLinesIgnore() is deprecated!', + E_USER_DEPRECATED + ); + $deprecationTriggered = true; + } + $change['tag'] = + (count($change['base']['lines']) > count($change['changed']['lines'])) ? 'delete' : 'insert'; + } $output .= $subRenderer->generateBlockHeader($change); switch ($change['tag']) { case 'equal': From 75f5ce0e14039b8f2d9c477c99f28c203ff1394a Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 17 Dec 2020 07:38:24 +0100 Subject: [PATCH 10/24] Add method `generateLinesIgnore` Currently the line is commented out, because the method is not required until the deprecation period for missing this method is over. --- .../Diff/Renderer/SubRendererInterface.php | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/jblond/Diff/Renderer/SubRendererInterface.php b/lib/jblond/Diff/Renderer/SubRendererInterface.php index c3a9f74..d4d95f0 100644 --- a/lib/jblond/Diff/Renderer/SubRendererInterface.php +++ b/lib/jblond/Diff/Renderer/SubRendererInterface.php @@ -41,13 +41,6 @@ public function generateDiffHeader(): string; */ public function generateBlockHeader(array $changes): string; - /** - * Generate a string representation of lines that are skipped in the diff view. - * - * @return string Representation of skipped lines. - */ - public function generateSkippedLines(): string; - /** * Generate a string representation of lines without differences between both versions. * @@ -69,6 +62,23 @@ public function generateLinesEqual(array $changes): string; */ public function generateLinesInsert(array $changes): string; + /** + * Generate a string representation of lines that are skipped in the diff view. + * + * @return string Representation of skipped lines. + */ + public function generateSkippedLines(): string; + + /** + * Generate a string representation of lines with ignored differences between both versions. + * + * @param array $changes Contains the op-codes about the changes between two blocks of text. + * + * @return string Text with no difference. + * @todo: Uncomment once deprecation period is over. + */ + // public function generateLinesIgnore(array $changes): string; + /** * Generate a string representation of lines that are removed from the 2nd version. * From a239f179492ddda3d8531333ee915d85341e3072 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 17 Dec 2020 07:46:45 +0100 Subject: [PATCH 11/24] Optimize stripping empty/blank lines For calculating the similarity ratio when empty/blank lines are ignored, these lines have to be stripped from the sequences beforehand. The stripped lines are restored after calculation so the class can also be used as sequenceMatcher. --- lib/jblond/Diff/Similarity.php | 100 +++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/lib/jblond/Diff/Similarity.php b/lib/jblond/Diff/Similarity.php index 12285fe..b4e2e23 100644 --- a/lib/jblond/Diff/Similarity.php +++ b/lib/jblond/Diff/Similarity.php @@ -37,6 +37,11 @@ class Similarity extends SequenceMatcher * @var array Count of each unique sequence at version 2. */ private $uniqueCount2; + /** + * @var array Contains the indexes of lines which are stripped from the sequences by Similarity::stripLines(). + * @see Similarity::stripLines() + */ + private $stripped = ['old' => [], 'new' => []]; /** @@ -65,15 +70,22 @@ public function setSeq2($version2) */ public function getSimilarity(int $type = self::CALC_DEFAULT): float { + if ($this->options['ignoreLines']) { + // Backup original sequences and filter non blank lines. + $this->stripLines(); + } + switch ($type) { case self::CALC_FAST: - return $this->getRatioFast(); + $ratio = $this->getRatioFast(); + $this->restoreLines(); + break; case self::CALC_FASTEST: - return $this->getRatioFastest(); + $ratio = $this->getRatioFastest(); + $this->restoreLines(); + break; default: - if ($this->options['ignoreLines']) { - $this->stripLines(); - } + $this->setSequences($this->old, $this->new); $matches = array_reduce( $this->getMatchingBlocks(), function ($carry, $item) { @@ -82,8 +94,44 @@ function ($carry, $item) { 0 ); - return $this->calculateRatio($matches, count($this->old) + count($this->new)); - // TODO: Restore original (un-stripped) versions? + $ratio = $this->calculateRatio($matches, count($this->old) + count($this->new)); + $this->restoreLines(); + $this->setSequences($this->old, $this->new); + } + + return $ratio; + } + + /** + * Strip empty or blank lines from the sequences to compare. + * + */ + private function stripLines(): void + { + foreach (['old', 'new'] as $version) { + // Remove empty lines. + $this->$version = array_filter( + $this->$version, + function ($line, $index) use ($version) { + $sanitizedLine = $line; + if ($this->options['ignoreLines'] == self::DIFF_IGNORE_LINE_BLANK) { + $sanitizedLine = trim($line); + } + + if ($sanitizedLine == '') { + // Store line to be able to restore later. + $this->stripped[$version][$index] = $line; + + return false; + } + + return true; + }, + ARRAY_FILTER_USE_BOTH + ); + + // Re-index sequence. + $this->$version = array_values($this->$version); } } @@ -97,6 +145,7 @@ function ($carry, $item) { private function getRatioFast(): float { if ($this->uniqueCount2 === null) { + // Build unless cached. $this->uniqueCount2 = []; $bLength = count($this->new); for ($iterator = 0; $iterator < $bLength; ++$iterator) { @@ -140,6 +189,15 @@ private function calculateRatio(int $matches, int $length = 0): float return $returnValue; } + private function restoreLines() + { + foreach (['old', 'new'] as $version) { + foreach ($this->stripped[$version] as $index => $line) { + array_splice($this->$version, $index, 0, $line); + } + } + } + /** * Return an upper bound ratio really quickly for the similarity of the strings. * @@ -155,34 +213,6 @@ private function getRatioFastest(): float return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); } - /** - * Strip empty or blank lines from the sequences to compare. - * - */ - private function stripLines(): void - { - foreach (['old', 'new'] as $version) { - if ($this->options['ignoreLines'] == self::DIFF_IGNORE_LINE_BLANK) { - array_walk( - $this->$version, - function (&$line) { - $line = trim($line); - } - ); - unset($line); - } - - $this->$version = array_filter( - $this->$version, - function ($line) { - return $line != ''; - } - ); - } - - $this->setSequences(array_values($this->old), array_values($this->new)); - } - /** * Helper function to calculate the number of matches for Ratio(). * From 251ccf062a8822d7cc774e2e4b09c3b7dd6dfc53 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 17 Dec 2020 07:47:19 +0100 Subject: [PATCH 12/24] Refactor variables --- lib/jblond/Diff/SequenceMatcher.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index ef97f17..df042ac 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -344,18 +344,18 @@ public function getOpCodes(): array } if ($this->options['ignoreLines']) { - $part1 = array_slice($this->old, $i, $ai - $i); - $part2 = array_slice($this->new, $j, $bj - $j); + $slice1 = array_slice($this->old, $i, $ai - $i); + $slice2 = array_slice($this->new, $j, $bj - $j); if ($this->options['ignoreLines'] == 2) { array_walk( - $part1, + $slice1, function (&$line) { $line = trim($line); } ); array_walk( - $part2, + $slice2, function (&$line) { $line = trim($line); } @@ -364,8 +364,8 @@ function (&$line) { } if ( - ($tag == 'delete' && implode('', $part1) == '') || - ($tag == 'insert' && implode('', $part2) == '') + ($tag == 'delete' && implode('', $slice1) == '') || + ($tag == 'insert' && implode('', $slice2) == '') ) { $tag = 'ignore'; } From 4dec4adcec6d810b6ef919500e770af740bd5bc8 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Fri, 18 Dec 2020 15:06:45 +0100 Subject: [PATCH 13/24] Add generator for ignored lines --- example/dark-theme.css | 17 +++++ example/styles.css | 17 +++++ lib/jblond/Diff/Renderer/Html/Merged.php | 82 ++++++++++++++--------- lib/jblond/Diff/Renderer/Html/Unified.php | 38 +++++++++++ lib/jblond/Diff/Renderer/Text/Context.php | 2 + 5 files changed, 125 insertions(+), 31 deletions(-) diff --git a/example/dark-theme.css b/example/dark-theme.css index fec7395..cd151ac 100644 --- a/example/dark-theme.css +++ b/example/dark-theme.css @@ -138,6 +138,19 @@ a, a:visited { color: #272822; } +.DifferencesUnified .ChangeIgnore .Left, +.DifferencesUnified .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesUnified .ChangeIgnore .Left.Ignore { + background: #4B4C57; +} + +.DifferencesUnified .ChangeIgnore .Right.Ignore { + background: #4B4C57; +} + /* * HTML Merged Diff */ @@ -166,3 +179,7 @@ a, a:visited { .DifferencesMerged th.ChangeDelete { background-image: linear-gradient(-45deg, #AAAAAA 0%, #EE9999 100%); } + +.DifferencesMerged th.ChangeIgnore { + background-image: linear-gradient(-45deg, #CCCCCC 0%, #4B4C57 100%); +} diff --git a/example/styles.css b/example/styles.css index 57af672..a67d04a 100644 --- a/example/styles.css +++ b/example/styles.css @@ -121,6 +121,19 @@ pre { background: #EE9999; } +.DifferencesUnified .ChangeIgnore .Left, +.DifferencesUnified .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesUnified .ChangeIgnore .Left.Ignore { + background: #F7F7F7; +} + +.DifferencesUnified .ChangeIgnore .Right.Ignore { + background: #F7F7F7; +} + /* * HTML Merged Diff */ @@ -145,3 +158,7 @@ pre { .DifferencesMerged th.ChangeDelete { background-image: linear-gradient(-45deg, #CCCCCC 0%, #EE9999 100%); } + +.DifferencesMerged th.ChangeIgnore { + background-image: linear-gradient(-45deg, #CCCCCC 0%, #F7F7F7 100%); +} diff --git a/lib/jblond/Diff/Renderer/Html/Merged.php b/lib/jblond/Diff/Renderer/Html/Merged.php index b55889b..dfd89bf 100644 --- a/lib/jblond/Diff/Renderer/Html/Merged.php +++ b/lib/jblond/Diff/Renderer/Html/Merged.php @@ -43,6 +43,10 @@ class Merged extends MainRenderer implements SubRendererInterface * @var string last block of lines which where removed from version 2. */ private $lastDeleted; + /** + * @var string + */ + private $headerClass = ''; /** * Merged constructor. @@ -101,21 +105,17 @@ public function generateBlockHeader(array $changes): string */ public function generateSkippedLines(): string { - $marker = '…'; - $headerClass = ''; - - if ($this->lastDeleted !== null) { - $headerClass = 'ChangeDelete'; - } - - $this->lastDeleted = null; - - return << - $marker + … … HTML; + + $this->headerClass = ''; + $this->lastDeleted = null; + + return $html; } /** @@ -125,22 +125,20 @@ public function generateSkippedLines(): string */ public function generateLinesEqual(array $changes): string { - $html = ''; - $headerClass = ''; + $html = ''; foreach ($changes['base']['lines'] as $lineNo => $line) { $fromLine = $changes['base']['offset'] + $lineNo + 1 + $this->lineOffset; - if (!$lineNo && $this->lastDeleted !== null) { - $headerClass = 'ChangeDelete'; - } - $html .= << - $fromLine + $fromLine $line HTML; + $this->lastDeleted = null; + $this->headerClass = ''; } return $html; @@ -153,22 +151,20 @@ public function generateLinesEqual(array $changes): string */ public function generateLinesInsert(array $changes): string { - $html = ''; - $headerClass = ''; + $html = ''; - foreach ($changes['changed']['lines'] as $lineNo => $line) { + foreach ($changes['changed']['lines'] as $line) { $this->lineOffset++; $toLine = $changes['base']['offset'] + $this->lineOffset; - if (!$lineNo && $this->lastDeleted !== null) { - $headerClass = 'ChangeDelete'; - } $html .= << - $toLine + $toLine $line HTML; + + $this->headerClass = ''; $this->lastDeleted = null; } @@ -197,6 +193,7 @@ public function generateLinesDelete(array $changes): string } $this->lastDeleted = $title; + $this->headerClass = 'ChangeDelete'; return ''; } @@ -208,14 +205,10 @@ public function generateLinesDelete(array $changes): string */ public function generateLinesReplace(array $changes): string { - $html = ''; - $headerClass = ''; + $html = ''; foreach ($changes['base']['lines'] as $lineNo => $line) { $fromLine = $changes['base']['offset'] + $lineNo + 1 + $this->lineOffset; - if (!$lineNo && $this->lastDeleted !== null) { - $headerClass = 'ChangeDelete'; - } // Capture added parts. $addedParts = []; @@ -236,10 +229,11 @@ function ($removedParts) use ($addedParts) { $html .= << - $fromLine + $fromLine $line HTML; + $this->headerClass = ''; $this->lastDeleted = null; } @@ -265,4 +259,30 @@ public function generateDiffFooter(): string { return ''; } + + /** + * @inheritDoc + * + * @return string Modified text. + */ + public function generateLinesIgnore(array $changes): string + { + $baseLineCount = count($changes['base']['lines']); + $changedLineCount = count($changes['changed']['lines']); + + $this->lineOffset -= $baseLineCount; + + $title = "Lines ignored at {$this->options['title2']}: "; + $title .= $changes['changed']['offset'] + 1 . '-' . ($changes['changed']['offset'] + $changedLineCount); + + if ($baseLineCount > $changedLineCount) { + $title = "Lines ignored at {$this->options['title1']}: "; + $title .= $changes['base']['offset'] + 1 . '-' . ($changes['base']['offset'] + $baseLineCount); + } + + $this->lastDeleted = $title; + $this->headerClass = 'ChangeIgnore'; + + return ''; + } } diff --git a/lib/jblond/Diff/Renderer/Html/Unified.php b/lib/jblond/Diff/Renderer/Html/Unified.php index 1569e83..537681d 100644 --- a/lib/jblond/Diff/Renderer/Html/Unified.php +++ b/lib/jblond/Diff/Renderer/Html/Unified.php @@ -219,6 +219,44 @@ public function generateLinesReplace(array $changes): string return $html; } + /** + * @inheritDoc + * + * @return string Html code representing table rows showing modified text. + */ + public function generateLinesIgnore(array $changes): string + { + $html = ''; + + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $html .= << + $fromLine + + + $line + + +HTML; + } + + foreach ($changes['changed']['lines'] as $lineNo => $line) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $html .= << + + $toLine + + $line + + +HTML; + } + + return $html; + } + /** * @inheritDoc * diff --git a/lib/jblond/Diff/Renderer/Text/Context.php b/lib/jblond/Diff/Renderer/Text/Context.php index 7b50dc3..e8b8228 100644 --- a/lib/jblond/Diff/Renderer/Text/Context.php +++ b/lib/jblond/Diff/Renderer/Text/Context.php @@ -65,6 +65,7 @@ public function render() // Line differences between versions or lines of version 1 are removed from version 2. // Add all operations to diff-view of version 1, except for insert. $filteredGroups = $this->filterGroups($group, 'insert'); + $filteredGroups = $this->filterGroups($filteredGroups, 'ignore'); foreach ($filteredGroups as [$tag, $start1, $end1, $start2, $end2]) { $diff .= $this->tagMap[$tag] . ' ' . implode( @@ -81,6 +82,7 @@ public function render() // Line differences between versions or lines are inserted into version 2. // Add all operations to diff-view of version 2, except for delete. $filteredGroups = $this->filterGroups($group, 'delete'); + $filteredGroups = $this->filterGroups($filteredGroups, 'ignore'); foreach ($filteredGroups as [$tag, $start1, $end1, $start2, $end2]) { $diff .= $this->tagMap[$tag] . ' ' . implode( From 9699b5bf6d05638e73d3a57d9bef78fc45f0cdae Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 08:00:07 +0100 Subject: [PATCH 14/24] Document methods * Add missing documentation. * Fixes and reformatting. --- lib/jblond/Diff.php | 3 +-- lib/jblond/Diff/Renderer/MainRenderer.php | 4 ++-- lib/jblond/Diff/SequenceMatcher.php | 28 ++++++++--------------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/lib/jblond/Diff.php b/lib/jblond/Diff.php index fce45e2..69125dd 100644 --- a/lib/jblond/Diff.php +++ b/lib/jblond/Diff.php @@ -46,8 +46,7 @@ class Diff private $groupedCodes; /** - * @var array Associative array containing the default options available - * for the diff class and their default value. + * @var array Associative array containing the default options available for the diff class and their default value. * * - context The amount of lines to include around blocks that differ. * - trimEqual Strip blocks of equal lines from the start and end of the text. diff --git a/lib/jblond/Diff/Renderer/MainRenderer.php b/lib/jblond/Diff/Renderer/MainRenderer.php index 7399e0c..64f1aca 100644 --- a/lib/jblond/Diff/Renderer/MainRenderer.php +++ b/lib/jblond/Diff/Renderer/MainRenderer.php @@ -124,7 +124,7 @@ protected function renderSequences(): array * 4 - The end line in the second sequence. * * The different types of tags include: - * replace - The string from $startOld to $endOld in $oldText should be replaced by + * replace - The string in $oldText from $startOld to $endOld, should be replaced by * the string in $newText from $startNew to $endNew. * delete - The string in $oldText from $startOld to $endNew should be deleted. * insert - The string in $newText from $startNew to $endNew should be inserted at $startOld in @@ -291,7 +291,7 @@ public function sequenceToArray(string $pattern, string $sequence): array * E.g. *
      *         1234567
-     * OLd => "abcdefg" Start marker inserted at position 3
+     * Old => "abcdefg" Start marker inserted at position 3
      * New => "ab123fg"   End marker inserted at position 6
      * 
* diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index 1e5fc92..a9e40f6 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -46,17 +46,12 @@ class SequenceMatcher private $b2j = []; /** - * @var array - */ - private $options = []; - - /** - * @var null|array + * @var array A list of all of the op-codes for the differences between the compared strings. */ private $opCodes; /** - * @var null|array + * @var array A nested set of arrays for all of the matching sub-sequences the compared strings. */ private $matchingBlocks; @@ -72,9 +67,8 @@ class SequenceMatcher ]; /** - * The constructor. With the sequences being passed, they'll be set for the - * sequence matcher and it will perform a basic cleanup & calculate junk - * elements. + * The constructor. With the sequences being passed, they'll be set for the sequence matcher and it will perform a + * basic cleanup & calculate junk elements. * * @param string|array $old A string or array containing the lines to compare against. * @param string|array $new A string or array containing the lines to compare. @@ -295,11 +289,9 @@ public function getGroupedOpCodes(): array } /** - * Return a list of all of the op codes for the differences between the - * two strings. + * Return a list of all of the op codes for the differences between the two strings. * - * The nested array returned contains an array describing the op code - * which includes: + * The nested array returned contains an array describing the op code which includes: * 0 - The type of tag (as described below) for the op code. * 1 - The beginning line in the first sequence. * 2 - The end line in the first sequence. @@ -366,12 +358,10 @@ public function getOpCodes(): array } /** - * Return a nested set of arrays for all of the matching sub-sequences - * in the strings $a and $b. + * Return a nested set of arrays for all of the matching sub-sequences in compared strings $a and $b. * - * Each block contains the lower constraint of the block in $a, the lower - * constraint of the block in $b and finally the number of lines that the - * block continues for. + * Each block contains the lower constraint of the block in $a, the lower constraint of the block in $b and finally + * the number of lines that the block continues for. * * @return array Nested array of the matching blocks, as described by the function. */ From 576830c45c0fd86365bffa45231feaa83bac0fd5 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 08:02:29 +0100 Subject: [PATCH 15/24] Optimize Sequence renderer * Reduction of cyclomatic complexity. --- lib/jblond/Diff/Renderer/MainRenderer.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/jblond/Diff/Renderer/MainRenderer.php b/lib/jblond/Diff/Renderer/MainRenderer.php index 64f1aca..f429154 100644 --- a/lib/jblond/Diff/Renderer/MainRenderer.php +++ b/lib/jblond/Diff/Renderer/MainRenderer.php @@ -146,23 +146,23 @@ protected function renderSequences(): array $oldBlock = $this->formatLines(array_slice($oldText, $startOld, $blockSizeOld)); $newBlock = $this->formatLines(array_slice($newText, $startNew, $blockSizeNew)); - if ($tag == 'equal') { - // Old block equals New block + if ($tag != 'delete' && $tag != 'insert') { + // Old block "equals" New block or is replaced. $blocks[$lastBlock]['base']['lines'] += $oldBlock; $blocks[$lastBlock]['changed']['lines'] += $newBlock; continue; } - if ($tag == 'replace' || $tag == 'delete') { - // Inline differences or old block doesn't exist in the new text. + if ($tag == 'delete') { + // Block of version1 doesn't exist in version2. $blocks[$lastBlock]['base']['lines'] += $oldBlock; + continue; } - if ($tag == 'replace' || $tag == 'insert') { - // Inline differences or the new block doesn't exist in the old text. - $blocks[$lastBlock]['changed']['lines'] += $newBlock; - } + // Block of version2 doesn't exist in version1. + $blocks[$lastBlock]['changed']['lines'] += $newBlock; } + $changes[] = $blocks; } From 7b2ab7977148fc6a372751112acbf0c3e2291b7b Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 08:27:13 +0100 Subject: [PATCH 16/24] Start option to ignore blank lines * Add option `ignoreLines` to control the ability to ignore blank or empty lines. * Add styles of ignored lines to example css. * Add generator for ignored lines to HTML SideBySide renderer. * Add `options` property to the Sequence Mather. * Refactor option `ignoreNewLines` of the Sequence Matcher. * Add flags to define the level of ignoring blank lines. * Add tag `ignored` to the opCodes. * Add method stripLines to Similarity. * Add default option `ignoreLines` to Diff. * Add tests for testing option `ignoreLines` of the Sequence Matcher. * Reformat code. --- example/dark-theme.css | 13 ++++ example/example.php | 1 + example/styles.css | 31 ++++++--- lib/jblond/Diff.php | 26 ++++++-- lib/jblond/Diff/Renderer/Html/SideBySide.php | 66 ++++++++++++++++++++ lib/jblond/Diff/Renderer/MainRenderer.php | 6 ++ lib/jblond/Diff/SequenceMatcher.php | 55 ++++++++++++++-- lib/jblond/Diff/Similarity.php | 31 +++++++++ tests/Diff/SequenceMatcherTest.php | 50 ++++++++++++++- 9 files changed, 259 insertions(+), 20 deletions(-) diff --git a/example/dark-theme.css b/example/dark-theme.css index 77669d2..fec7395 100644 --- a/example/dark-theme.css +++ b/example/dark-theme.css @@ -97,6 +97,19 @@ a, a:visited { background: #EEBB00; } +.DifferencesSideBySide .ChangeIgnore .Left, +.DifferencesSideBySide .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesSideBySide .ChangeIgnore .Left.Ignore { + background: #4B4C57; +} + +.DifferencesSideBySide .ChangeIgnore .Right.Ignore { + background: #4B4C57; +} + /* * HTML Unified Diff */ diff --git a/example/example.php b/example/example.php index fe2cb0c..e4d7ed0 100644 --- a/example/example.php +++ b/example/example.php @@ -20,6 +20,7 @@ 'trimEqual' => false, 'ignoreWhitespace' => true, 'ignoreCase' => true, + 'ignoreLines' => Diff::DIFF_IGNORE_LINE_EMPTY, ]; // Choose one of the initializations. diff --git a/example/styles.css b/example/styles.css index bbf2ba7..57af672 100644 --- a/example/styles.css +++ b/example/styles.css @@ -6,8 +6,8 @@ body { } pre { - width: 100%; overflow: auto; + width: 100%; } /* @@ -15,34 +15,34 @@ pre { */ .Differences { - width: 100%; border-collapse: collapse; border-spacing: 0; empty-cells: show; + width: 100%; } .Differences thead th { - text-align: left; - border-bottom: 1px solid #000000; background: #AAAAAA; + border-bottom: 1px solid #000000; color: #000000; padding: 4px; + text-align: left; } .Differences tbody th { - text-align: right; background: #CCCCCC; - width: 4em; - padding: 1px 2px; border-right: 1px solid #000000; - vertical-align: top; font-size: 13px; + padding: 1px 2px; + text-align: right; + vertical-align: top; + width: 4em; } .Differences td { - padding: 1px 2px; font-family: Consolas, monospace; font-size: 13px; + padding: 1px 2px; } .Differences .Skipped { @@ -77,6 +77,19 @@ pre { background: #FFDD88; } +.DifferencesSideBySide .ChangeIgnore .Left, +.DifferencesSideBySide .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesSideBySide .ChangeIgnore .Left.Ignore { + background: #F7F7F7; +} + +.DifferencesSideBySide .ChangeIgnore .Right.Ignore { + background: #F7F7F7; +} + .Differences ins, .Differences del { text-decoration: none; diff --git a/lib/jblond/Diff.php b/lib/jblond/Diff.php index 69125dd..c7b0368 100644 --- a/lib/jblond/Diff.php +++ b/lib/jblond/Diff.php @@ -28,6 +28,18 @@ */ class Diff { + /** + * Flag to disable ignore of successive empty/blank lines. + */ + public const DIFF_IGNORE_LINE_NONE = 0; + /** + * Flag to ignore successive empty lines. + */ + public const DIFF_IGNORE_LINE_EMPTY = 1; + /** + * Flag to ignore successive blank lines. (Lines which contain no or only non printable characters.) + */ + public const DIFF_IGNORE_LINE_BLANK = 2; /** * @var array The first version to compare. * Each element contains a line of this string. @@ -48,18 +60,20 @@ class Diff /** * @var array Associative array containing the default options available for the diff class and their default value. * - * - context The amount of lines to include around blocks that differ. - * - trimEqual Strip blocks of equal lines from the start and end of the text. - * - ignoreWhitespace When true, tabs and spaces are ignored while comparing. - * The spacing of version1 is leading. - * - ignoreCase When true, character casing is ignored while comparing. - * The casing of version1 is leading. + * - context The amount of lines to include around blocks that differ. + * - trimEqual Strip blocks of equal lines from the start and end of the text. + * - ignoreWhitespace True to ignore differences in tabs and spaces. + * - ignoreCase True to ignore differences in character casing. + * - ignoreLines 0: None. + * 1: Ignore empty lines. + * 2: Ignore blank lines. */ private $defaultOptions = [ 'context' => 3, 'trimEqual' => true, 'ignoreWhitespace' => false, 'ignoreCase' => false, + 'ignoreLines' => self::DIFF_IGNORE_LINE_NONE, ]; /** diff --git a/lib/jblond/Diff/Renderer/Html/SideBySide.php b/lib/jblond/Diff/Renderer/Html/SideBySide.php index 545d4ed..e8213f1 100644 --- a/lib/jblond/Diff/Renderer/Html/SideBySide.php +++ b/lib/jblond/Diff/Renderer/Html/SideBySide.php @@ -282,4 +282,70 @@ public function generateDiffFooter(): string { return ''; } + + /** + * @inheritDoc + * + * @return string Html code representing table rows showing ignored text. + */ + public function generateLinesIgnore(array $changes): string + { + $html = ''; + + // Is below comparison result ever false? + if (count($changes['base']['lines']) >= count($changes['changed']['lines'])) { + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $toLine = ' '; + $changedLine = ' '; + if (isset($changes['changed']['lines'][$lineNo])) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $changedLine = $changes['changed']['lines'][$lineNo]; + } + + $html .= << + $fromLine + + $line + + $toLine + + $changedLine + + +HTML; + } + + return $html; + } + + foreach ($changes['changed']['lines'] as $lineNo => $changedLine) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $fromLine = ' '; + $line = ' '; + if (isset($changes['base']['lines'][$lineNo])) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $line = $changes['base']['lines'][$lineNo]; + } + + $line = str_replace(["\0", "\1"], $this->options['deleteMarkers'], $line); + $changedLine = str_replace(["\0", "\1"], $this->options['insertMarkers'], $changedLine); + + $html .= << + $fromLine + + $line + + $toLine + + $changedLine + + +HTML; + } + + return $html; + } } diff --git a/lib/jblond/Diff/Renderer/MainRenderer.php b/lib/jblond/Diff/Renderer/MainRenderer.php index f429154..b24459f 100644 --- a/lib/jblond/Diff/Renderer/MainRenderer.php +++ b/lib/jblond/Diff/Renderer/MainRenderer.php @@ -80,6 +80,10 @@ public function renderOutput(array $changes, object $subRenderer) case 'replace': $output .= $subRenderer->generateLinesReplace($change); break; + case 'ignore': + // TODO: Keep backward compatible with renderers? + $output .= $subRenderer->generateLinesIgnore($change); + break; } $output .= $subRenderer->generateBlockFooter($change); @@ -130,6 +134,8 @@ protected function renderSequences(): array * insert - The string in $newText from $startNew to $endNew should be inserted at $startOld in * $oldText. * equal - The two strings with the specified ranges are equal. + * ignore - The string in $oldText from $startOld to $endOld and + * the string in $newText from $startNew to $endNew are different, but considered to be equal. */ $blockSizeOld = $endOld - $startOld; diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index a9e40f6..595f1b4 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -22,6 +22,18 @@ */ class SequenceMatcher { + /** + * Flag to disable ignore of successive empty/blank lines. + */ + public const DIFF_IGNORE_LINE_NONE = 0; + /** + * Flag to ignore empty lines. + */ + public const DIFF_IGNORE_LINE_EMPTY = 1; + /** + * Flag to ignore blank lines. (Lines which contain no or only non printable characters.) + */ + public const DIFF_IGNORE_LINE_BLANK = 2; /** * @var array The first sequence to compare against. */ @@ -30,6 +42,13 @@ class SequenceMatcher * @var array The second sequence. */ protected $new; + /** + * @var array Associative array containing the options that will be applied for generating the diff. + * The key-value pairs are set at the constructor of this class. + * + * @see SequenceMatcher::setOptions() + */ + protected $options = []; /** * @var string|array Either a string or an array containing a callback function to determine * if a line is "junk" or not. @@ -39,12 +58,10 @@ class SequenceMatcher * @var array Array of characters that are considered junk from the second sequence. Characters are the array key. */ private $junkDict = []; - /** * @var array Array of indices that do not contain junk elements. */ private $b2j = []; - /** * @var array A list of all of the op-codes for the differences between the compared strings. */ @@ -56,14 +73,22 @@ class SequenceMatcher private $matchingBlocks; /** - * @var array + * @var array Associative array containing the default options available for the diff class and their default value. + * + * - context The amount of lines to include around blocks that differ. + * - trimEqual Strip blocks of equal lines from the start and end of the text. + * - ignoreWhitespace True to ignore differences in tabs and spaces. + * - ignoreCase True to ignore differences in character casing. + * - ignoreLines 0: None. + * 1: Ignore empty lines. + * 2: Ignore blank lines. */ private $defaultOptions = [ 'context' => 3, 'trimEqual' => true, 'ignoreWhitespace' => false, 'ignoreCase' => false, - 'ignoreNewLines' => false, + 'ignoreLines' => self::DIFF_IGNORE_LINE_NONE, ]; /** @@ -330,6 +355,28 @@ public function getOpCodes(): array $tag = 'insert'; } + if ($this->options['ignoreLines']) { + $part1 = array_slice($this->old, $i, $ai - $i); + $part2 = array_slice($this->new, $j, $bj - $j); + + if ($this->options['ignoreLines'] == 2) { + array_walk($part1, function (&$line) { + $line = trim($line); + }); + array_walk($part2, function (&$line) { + $line = trim($line); + }); + unset($line); + } + + if ( + ($tag == 'delete' && implode('', $part1) == '') || + ($tag == 'insert' && implode('', $part2) == '') + ) { + $tag = 'ignore'; + } + } + if ($tag) { $this->opCodes[] = [ $tag, diff --git a/lib/jblond/Diff/Similarity.php b/lib/jblond/Diff/Similarity.php index 0d2ec81..a13c240 100644 --- a/lib/jblond/Diff/Similarity.php +++ b/lib/jblond/Diff/Similarity.php @@ -71,6 +71,9 @@ public function getSimilarity(int $type = self::CALC_DEFAULT): float case self::CALC_FASTEST: return $this->getRatioFastest(); default: + if ($this->options['ignoreLines']) { + $this->stripLines(); + } $matches = array_reduce( $this->getMatchingBlocks(), function ($carry, $item) { @@ -80,6 +83,7 @@ function ($carry, $item) { ); return $this->calculateRatio($matches, count($this->old) + count($this->new)); + // TODO: Restore original (un-stripped) versions? } } @@ -151,6 +155,33 @@ private function getRatioFastest(): float return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); } + /** + * Strip empty or blank lines from the sequences to compare. + * + */ + private function stripLines(): void + { + foreach (['old', 'new'] as $version) { + if ($this->options['ignoreLines'] == self::DIFF_IGNORE_LINE_BLANK) { + array_walk( + $this->$version, + function (&$line) { + $line = trim($line); + } + ); + unset($line); + } + + $this->$version = array_filter( + $this->$version, + function ($line) { + return $line != ''; + } + ); + } + + $this->setSequences(array_values($this->old), array_values($this->new)); + } /** * Helper function to calculate the number of matches for Ratio(). diff --git a/tests/Diff/SequenceMatcherTest.php b/tests/Diff/SequenceMatcherTest.php index 0d85127..939de5d 100644 --- a/tests/Diff/SequenceMatcherTest.php +++ b/tests/Diff/SequenceMatcherTest.php @@ -84,7 +84,7 @@ public function testGetGroupedOpCodesIgnoreWhitespaceTrue() } /** - *T est the opCodes of the differences between version1 and version2 with option ignoreCase enabled. + * Test the opCodes of the differences between version1 and version2 with option ignoreCase enabled. */ public function testGetGroupedOpCodesIgnoreCaseTrue() { @@ -97,4 +97,52 @@ public function testGetGroupedOpCodesIgnoreCaseTrue() $this->assertEquals([], $sequenceMatcher->getGroupedOpCodes()); } + + /** + * Test the opCodes of the differences between version1 and version2 with option ignoreLines set to empty. + */ + public function testGetGroupedOpCodesIgnoreLinesEmpty() + { + // Test with ignoreCase enabled. Both sequences are considered to be the same. + $sequenceMatcher = new SequenceMatcher( + [0, 1, 2, 3], + [0, 1, '', 2, 3], + ['ignoreLines' => SequenceMatcher::DIFF_IGNORE_LINE_EMPTY] + ); + + $this->assertEquals( + [ + [ + ['equal', 0, 2, 0, 2], + ['ignore', 2, 2, 2, 3], + ['equal', 2, 4, 3, 5], + ], + ], + $sequenceMatcher->getGroupedOpCodes() + ); + } + + /** + * Test the opCodes of the differences between version1 and version2 with option ignoreLines set to blank. + */ + public function testGetGroupedOpCodesIgnoreLinesBlank() + { + // Test with ignoreCase enabled. Both sequences are considered to be the same. + $sequenceMatcher = new SequenceMatcher( + [0, 1, 2, 3], + [0, 1, "\t", 2, 3], + ['ignoreLines' => SequenceMatcher::DIFF_IGNORE_LINE_BLANK] + ); + + $this->assertEquals( + [ + [ + ['equal', 0, 2, 0, 2], + ['ignore', 2, 2, 2, 3], + ['equal', 2, 4, 3, 5], + ], + ], + $sequenceMatcher->getGroupedOpCodes() + ); + } } From d0cede314d53674b162c8224fd5f3e5b2d491c34 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 12:01:22 +0100 Subject: [PATCH 17/24] Optimize constant usage Instead of defining constants with the same name and values at different namespaces, they're now defined at an interface which can be shared among classes by implementing this interface. --- lib/jblond/Diff.php | 15 ++---------- lib/jblond/Diff/ConstantsInterface.php | 33 ++++++++++++++++++++++++++ lib/jblond/Diff/SequenceMatcher.php | 32 ++++++++++--------------- 3 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 lib/jblond/Diff/ConstantsInterface.php diff --git a/lib/jblond/Diff.php b/lib/jblond/Diff.php index c7b0368..f166ace 100644 --- a/lib/jblond/Diff.php +++ b/lib/jblond/Diff.php @@ -5,6 +5,7 @@ namespace jblond; use InvalidArgumentException; +use jblond\Diff\ConstantsInterface; use jblond\Diff\SequenceMatcher; use jblond\Diff\Similarity; use OutOfRangeException; @@ -26,20 +27,8 @@ * @version 2.3.0 * @link https://github.com/JBlond/php-diff */ -class Diff +class Diff implements ConstantsInterface { - /** - * Flag to disable ignore of successive empty/blank lines. - */ - public const DIFF_IGNORE_LINE_NONE = 0; - /** - * Flag to ignore successive empty lines. - */ - public const DIFF_IGNORE_LINE_EMPTY = 1; - /** - * Flag to ignore successive blank lines. (Lines which contain no or only non printable characters.) - */ - public const DIFF_IGNORE_LINE_BLANK = 2; /** * @var array The first version to compare. * Each element contains a line of this string. diff --git a/lib/jblond/Diff/ConstantsInterface.php b/lib/jblond/Diff/ConstantsInterface.php new file mode 100644 index 0000000..121e7cb --- /dev/null +++ b/lib/jblond/Diff/ConstantsInterface.php @@ -0,0 +1,33 @@ + + * @copyright (c) 2020 Mario Brandt + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 2.3.0 + * @link https://github.com/JBlond/php-diff + */ +interface ConstantsInterface +{ + /** + * Flag to disable ignore of successive empty/blank lines. + */ + public const DIFF_IGNORE_LINE_NONE = 0; + /** + * Flag to ignore empty lines. + */ + public const DIFF_IGNORE_LINE_EMPTY = 1; + /** + * Flag to ignore blank lines. (Lines which contain no or only non printable characters.) + */ + public const DIFF_IGNORE_LINE_BLANK = 2; + +} diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index 595f1b4..19ea8fa 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -20,20 +20,8 @@ * @version 2.3.0 * @link https://github.com/JBlond/php-diff */ -class SequenceMatcher +class SequenceMatcher implements ConstantsInterface { - /** - * Flag to disable ignore of successive empty/blank lines. - */ - public const DIFF_IGNORE_LINE_NONE = 0; - /** - * Flag to ignore empty lines. - */ - public const DIFF_IGNORE_LINE_EMPTY = 1; - /** - * Flag to ignore blank lines. (Lines which contain no or only non printable characters.) - */ - public const DIFF_IGNORE_LINE_BLANK = 2; /** * @var array The first sequence to compare against. */ @@ -360,12 +348,18 @@ public function getOpCodes(): array $part2 = array_slice($this->new, $j, $bj - $j); if ($this->options['ignoreLines'] == 2) { - array_walk($part1, function (&$line) { - $line = trim($line); - }); - array_walk($part2, function (&$line) { - $line = trim($line); - }); + array_walk( + $part1, + function (&$line) { + $line = trim($line); + } + ); + array_walk( + $part2, + function (&$line) { + $line = trim($line); + } + ); unset($line); } From 0849a1e4c9ebebf017d49f078e796bdfd0d67a29 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 10 Dec 2020 12:08:25 +0100 Subject: [PATCH 18/24] Document option `ignoreLines` --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5cbc6f8..3ad115e 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ $options = [ 'ignoreWhitespace' => true, 'ignoreCase' => true, 'context' => 2, - 'cliColor' => true // for cli output + 'cliColor' => true, // for cli output + 'ignoreLines' => Diff::DIFF_IGNORE_LINE_BLANK, ]; // Initialize the diff class. @@ -140,7 +141,6 @@ at [jQuery-Merge-for-php-diff](https://github.com/Xiphe/jQuery-Merge-for-php-dif ## Todo -* Ability to ignore blank line changes * 3 way diff support ## Contributors From f494b3abd15c55a5050498d95c1222c522191407 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 17 Dec 2020 07:28:05 +0100 Subject: [PATCH 19/24] Add deprecation notice for missing method If a used sub renderer, which extends the main renderer, a deprecation notice will be thrown when the sub renderer is missing method `generateLinesIgnore`. --- lib/jblond/Diff/Renderer/MainRenderer.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/jblond/Diff/Renderer/MainRenderer.php b/lib/jblond/Diff/Renderer/MainRenderer.php index b24459f..05e71cf 100644 --- a/lib/jblond/Diff/Renderer/MainRenderer.php +++ b/lib/jblond/Diff/Renderer/MainRenderer.php @@ -65,7 +65,23 @@ public function renderOutput(array $changes, object $subRenderer) strlen($this->options['equalityMarkers'][1]) ); + $deprecationTriggered = false; foreach ($blocks as $change) { + if ( + $subRenderer instanceof MainRenderer && + !method_exists($subRenderer, 'generateLinesIgnore') && + $change['tag'] == 'ignore' + ) { + if (!$deprecationTriggered) { + trigger_error( + 'The use of a subRenderer without method generateLinesIgnore() is deprecated!', + E_USER_DEPRECATED + ); + $deprecationTriggered = true; + } + $change['tag'] = + (count($change['base']['lines']) > count($change['changed']['lines'])) ? 'delete' : 'insert'; + } $output .= $subRenderer->generateBlockHeader($change); switch ($change['tag']) { case 'equal': From 6ef61bc7518d9728a9bf405e9928eb105cbe913d Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 17 Dec 2020 07:38:24 +0100 Subject: [PATCH 20/24] Add method `generateLinesIgnore` Currently the line is commented out, because the method is not required until the deprecation period for missing this method is over. --- .../Diff/Renderer/SubRendererInterface.php | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/jblond/Diff/Renderer/SubRendererInterface.php b/lib/jblond/Diff/Renderer/SubRendererInterface.php index a3ea1bb..e468843 100644 --- a/lib/jblond/Diff/Renderer/SubRendererInterface.php +++ b/lib/jblond/Diff/Renderer/SubRendererInterface.php @@ -41,13 +41,6 @@ public function generateDiffHeader(): string; */ public function generateBlockHeader(array $changes): string; - /** - * Generate a string representation of lines that are skipped in the diff view. - * - * @return string Representation of skipped lines. - */ - public function generateSkippedLines(): string; - /** * Generate a string representation of lines without differences between both versions. * @@ -69,6 +62,23 @@ public function generateLinesEqual(array $changes): string; */ public function generateLinesInsert(array $changes): string; + /** + * Generate a string representation of lines that are skipped in the diff view. + * + * @return string Representation of skipped lines. + */ + public function generateSkippedLines(): string; + + /** + * Generate a string representation of lines with ignored differences between both versions. + * + * @param array $changes Contains the op-codes about the changes between two blocks of text. + * + * @return string Text with no difference. + * @todo: Uncomment once deprecation period is over. + */ + // public function generateLinesIgnore(array $changes): string; + /** * Generate a string representation of lines that are removed from the 2nd version. * From ea6a2e4554ab0ab24271ad3770b53e1b2e0080b7 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 17 Dec 2020 07:46:45 +0100 Subject: [PATCH 21/24] Optimize stripping empty/blank lines For calculating the similarity ratio when empty/blank lines are ignored, these lines have to be stripped from the sequences beforehand. The stripped lines are restored after calculation so the class can also be used as sequenceMatcher. --- lib/jblond/Diff/Similarity.php | 100 +++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/lib/jblond/Diff/Similarity.php b/lib/jblond/Diff/Similarity.php index a13c240..eef9b44 100644 --- a/lib/jblond/Diff/Similarity.php +++ b/lib/jblond/Diff/Similarity.php @@ -37,6 +37,11 @@ class Similarity extends SequenceMatcher * @var array Count of each unique sequence at version 2. */ private $uniqueCount2; + /** + * @var array Contains the indexes of lines which are stripped from the sequences by Similarity::stripLines(). + * @see Similarity::stripLines() + */ + private $stripped = ['old' => [], 'new' => []]; /** @@ -65,15 +70,22 @@ public function setSeq2($version2) */ public function getSimilarity(int $type = self::CALC_DEFAULT): float { + if ($this->options['ignoreLines']) { + // Backup original sequences and filter non blank lines. + $this->stripLines(); + } + switch ($type) { case self::CALC_FAST: - return $this->getRatioFast(); + $ratio = $this->getRatioFast(); + $this->restoreLines(); + break; case self::CALC_FASTEST: - return $this->getRatioFastest(); + $ratio = $this->getRatioFastest(); + $this->restoreLines(); + break; default: - if ($this->options['ignoreLines']) { - $this->stripLines(); - } + $this->setSequences($this->old, $this->new); $matches = array_reduce( $this->getMatchingBlocks(), function ($carry, $item) { @@ -82,8 +94,44 @@ function ($carry, $item) { 0 ); - return $this->calculateRatio($matches, count($this->old) + count($this->new)); - // TODO: Restore original (un-stripped) versions? + $ratio = $this->calculateRatio($matches, count($this->old) + count($this->new)); + $this->restoreLines(); + $this->setSequences($this->old, $this->new); + } + + return $ratio; + } + + /** + * Strip empty or blank lines from the sequences to compare. + * + */ + private function stripLines(): void + { + foreach (['old', 'new'] as $version) { + // Remove empty lines. + $this->$version = array_filter( + $this->$version, + function ($line, $index) use ($version) { + $sanitizedLine = $line; + if ($this->options['ignoreLines'] == self::DIFF_IGNORE_LINE_BLANK) { + $sanitizedLine = trim($line); + } + + if ($sanitizedLine == '') { + // Store line to be able to restore later. + $this->stripped[$version][$index] = $line; + + return false; + } + + return true; + }, + ARRAY_FILTER_USE_BOTH + ); + + // Re-index sequence. + $this->$version = array_values($this->$version); } } @@ -97,6 +145,7 @@ function ($carry, $item) { private function getRatioFast(): float { if ($this->uniqueCount2 === null) { + // Build unless cached. $this->uniqueCount2 = []; $bLength = count($this->new); for ($iterator = 0; $iterator < $bLength; ++$iterator) { @@ -140,6 +189,15 @@ private function calculateRatio(int $matches, int $length = 0): float return $returnValue; } + private function restoreLines() + { + foreach (['old', 'new'] as $version) { + foreach ($this->stripped[$version] as $index => $line) { + array_splice($this->$version, $index, 0, $line); + } + } + } + /** * Return an upper bound ratio really quickly for the similarity of the strings. * @@ -155,34 +213,6 @@ private function getRatioFastest(): float return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); } - /** - * Strip empty or blank lines from the sequences to compare. - * - */ - private function stripLines(): void - { - foreach (['old', 'new'] as $version) { - if ($this->options['ignoreLines'] == self::DIFF_IGNORE_LINE_BLANK) { - array_walk( - $this->$version, - function (&$line) { - $line = trim($line); - } - ); - unset($line); - } - - $this->$version = array_filter( - $this->$version, - function ($line) { - return $line != ''; - } - ); - } - - $this->setSequences(array_values($this->old), array_values($this->new)); - } - /** * Helper function to calculate the number of matches for Ratio(). * From 9d46847558b44d2239e53772b36d7d0795713659 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Thu, 17 Dec 2020 07:47:19 +0100 Subject: [PATCH 22/24] Refactor variables --- lib/jblond/Diff/SequenceMatcher.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/jblond/Diff/SequenceMatcher.php b/lib/jblond/Diff/SequenceMatcher.php index 19ea8fa..545fed9 100644 --- a/lib/jblond/Diff/SequenceMatcher.php +++ b/lib/jblond/Diff/SequenceMatcher.php @@ -344,18 +344,18 @@ public function getOpCodes(): array } if ($this->options['ignoreLines']) { - $part1 = array_slice($this->old, $i, $ai - $i); - $part2 = array_slice($this->new, $j, $bj - $j); + $slice1 = array_slice($this->old, $i, $ai - $i); + $slice2 = array_slice($this->new, $j, $bj - $j); if ($this->options['ignoreLines'] == 2) { array_walk( - $part1, + $slice1, function (&$line) { $line = trim($line); } ); array_walk( - $part2, + $slice2, function (&$line) { $line = trim($line); } @@ -364,8 +364,8 @@ function (&$line) { } if ( - ($tag == 'delete' && implode('', $part1) == '') || - ($tag == 'insert' && implode('', $part2) == '') + ($tag == 'delete' && implode('', $slice1) == '') || + ($tag == 'insert' && implode('', $slice2) == '') ) { $tag = 'ignore'; } From 6b8662e35f0967b4fffe7fa721d9166e2cd85cb1 Mon Sep 17 00:00:00 2001 From: DigiLive Date: Fri, 18 Dec 2020 15:06:45 +0100 Subject: [PATCH 23/24] Add generator for ignored lines --- example/dark-theme.css | 17 +++++ example/styles.css | 17 +++++ lib/jblond/Diff/Renderer/Html/Merged.php | 82 ++++++++++++++--------- lib/jblond/Diff/Renderer/Html/Unified.php | 38 +++++++++++ lib/jblond/Diff/Renderer/Text/Context.php | 2 + 5 files changed, 125 insertions(+), 31 deletions(-) diff --git a/example/dark-theme.css b/example/dark-theme.css index fec7395..cd151ac 100644 --- a/example/dark-theme.css +++ b/example/dark-theme.css @@ -138,6 +138,19 @@ a, a:visited { color: #272822; } +.DifferencesUnified .ChangeIgnore .Left, +.DifferencesUnified .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesUnified .ChangeIgnore .Left.Ignore { + background: #4B4C57; +} + +.DifferencesUnified .ChangeIgnore .Right.Ignore { + background: #4B4C57; +} + /* * HTML Merged Diff */ @@ -166,3 +179,7 @@ a, a:visited { .DifferencesMerged th.ChangeDelete { background-image: linear-gradient(-45deg, #AAAAAA 0%, #EE9999 100%); } + +.DifferencesMerged th.ChangeIgnore { + background-image: linear-gradient(-45deg, #CCCCCC 0%, #4B4C57 100%); +} diff --git a/example/styles.css b/example/styles.css index 57af672..a67d04a 100644 --- a/example/styles.css +++ b/example/styles.css @@ -121,6 +121,19 @@ pre { background: #EE9999; } +.DifferencesUnified .ChangeIgnore .Left, +.DifferencesUnified .ChangeIgnore .Right { + background: #FBF2BF; +} + +.DifferencesUnified .ChangeIgnore .Left.Ignore { + background: #F7F7F7; +} + +.DifferencesUnified .ChangeIgnore .Right.Ignore { + background: #F7F7F7; +} + /* * HTML Merged Diff */ @@ -145,3 +158,7 @@ pre { .DifferencesMerged th.ChangeDelete { background-image: linear-gradient(-45deg, #CCCCCC 0%, #EE9999 100%); } + +.DifferencesMerged th.ChangeIgnore { + background-image: linear-gradient(-45deg, #CCCCCC 0%, #F7F7F7 100%); +} diff --git a/lib/jblond/Diff/Renderer/Html/Merged.php b/lib/jblond/Diff/Renderer/Html/Merged.php index ab75577..7492d9f 100644 --- a/lib/jblond/Diff/Renderer/Html/Merged.php +++ b/lib/jblond/Diff/Renderer/Html/Merged.php @@ -43,6 +43,10 @@ class Merged extends MainRenderer implements SubRendererInterface * @var string last block of lines which where removed from version 2. */ private $lastDeleted; + /** + * @var string + */ + private $headerClass = ''; /** * Merged constructor. @@ -101,21 +105,17 @@ public function generateBlockHeader(array $changes): string */ public function generateSkippedLines(): string { - $marker = '…'; - $headerClass = ''; - - if ($this->lastDeleted !== null) { - $headerClass = 'ChangeDelete'; - } - - $this->lastDeleted = null; - - return << - $marker + … … HTML; + + $this->headerClass = ''; + $this->lastDeleted = null; + + return $html; } /** @@ -125,22 +125,20 @@ public function generateSkippedLines(): string */ public function generateLinesEqual(array $changes): string { - $html = ''; - $headerClass = ''; + $html = ''; foreach ($changes['base']['lines'] as $lineNo => $line) { $fromLine = $changes['base']['offset'] + $lineNo + 1 + $this->lineOffset; - if (!$lineNo && $this->lastDeleted !== null) { - $headerClass = 'ChangeDelete'; - } - $html .= << - $fromLine + $fromLine $line HTML; + $this->lastDeleted = null; + $this->headerClass = ''; } return $html; @@ -153,22 +151,20 @@ public function generateLinesEqual(array $changes): string */ public function generateLinesInsert(array $changes): string { - $html = ''; - $headerClass = ''; + $html = ''; - foreach ($changes['changed']['lines'] as $lineNo => $line) { + foreach ($changes['changed']['lines'] as $line) { $this->lineOffset++; $toLine = $changes['base']['offset'] + $this->lineOffset; - if (!$lineNo && $this->lastDeleted !== null) { - $headerClass = 'ChangeDelete'; - } $html .= << - $toLine + $toLine $line HTML; + + $this->headerClass = ''; $this->lastDeleted = null; } @@ -197,6 +193,7 @@ public function generateLinesDelete(array $changes): string } $this->lastDeleted = $title; + $this->headerClass = 'ChangeDelete'; return ''; } @@ -208,14 +205,10 @@ public function generateLinesDelete(array $changes): string */ public function generateLinesReplace(array $changes): string { - $html = ''; - $headerClass = ''; + $html = ''; foreach ($changes['base']['lines'] as $lineNo => $line) { $fromLine = $changes['base']['offset'] + $lineNo + 1 + $this->lineOffset; - if (!$lineNo && $this->lastDeleted !== null) { - $headerClass = 'ChangeDelete'; - } // Capture added parts. $addedParts = []; @@ -236,10 +229,11 @@ function ($removedParts) use ($addedParts) { $html .= << - $fromLine + $fromLine $line HTML; + $this->headerClass = ''; $this->lastDeleted = null; } @@ -265,4 +259,30 @@ public function generateDiffFooter(): string { return ''; } + + /** + * @inheritDoc + * + * @return string Modified text. + */ + public function generateLinesIgnore(array $changes): string + { + $baseLineCount = count($changes['base']['lines']); + $changedLineCount = count($changes['changed']['lines']); + + $this->lineOffset -= $baseLineCount; + + $title = "Lines ignored at {$this->options['title2']}: "; + $title .= $changes['changed']['offset'] + 1 . '-' . ($changes['changed']['offset'] + $changedLineCount); + + if ($baseLineCount > $changedLineCount) { + $title = "Lines ignored at {$this->options['title1']}: "; + $title .= $changes['base']['offset'] + 1 . '-' . ($changes['base']['offset'] + $baseLineCount); + } + + $this->lastDeleted = $title; + $this->headerClass = 'ChangeIgnore'; + + return ''; + } } diff --git a/lib/jblond/Diff/Renderer/Html/Unified.php b/lib/jblond/Diff/Renderer/Html/Unified.php index b69f6cf..63dff3c 100644 --- a/lib/jblond/Diff/Renderer/Html/Unified.php +++ b/lib/jblond/Diff/Renderer/Html/Unified.php @@ -219,6 +219,44 @@ public function generateLinesReplace(array $changes): string return $html; } + /** + * @inheritDoc + * + * @return string Html code representing table rows showing modified text. + */ + public function generateLinesIgnore(array $changes): string + { + $html = ''; + + foreach ($changes['base']['lines'] as $lineNo => $line) { + $fromLine = $changes['base']['offset'] + $lineNo + 1; + $html .= << + $fromLine + + + $line + + +HTML; + } + + foreach ($changes['changed']['lines'] as $lineNo => $line) { + $toLine = $changes['changed']['offset'] + $lineNo + 1; + $html .= << + + $toLine + + $line + + +HTML; + } + + return $html; + } + /** * @inheritDoc * diff --git a/lib/jblond/Diff/Renderer/Text/Context.php b/lib/jblond/Diff/Renderer/Text/Context.php index 6fae3bb..ab4e311 100644 --- a/lib/jblond/Diff/Renderer/Text/Context.php +++ b/lib/jblond/Diff/Renderer/Text/Context.php @@ -65,6 +65,7 @@ public function render() // Line differences between versions or lines of version 1 are removed from version 2. // Add all operations to diff-view of version 1, except for insert. $filteredGroups = $this->filterGroups($group, 'insert'); + $filteredGroups = $this->filterGroups($filteredGroups, 'ignore'); foreach ($filteredGroups as [$tag, $start1, $end1, $start2, $end2]) { $diff .= $this->tagMap[$tag] . ' ' . implode( @@ -81,6 +82,7 @@ public function render() // Line differences between versions or lines are inserted into version 2. // Add all operations to diff-view of version 2, except for delete. $filteredGroups = $this->filterGroups($group, 'delete'); + $filteredGroups = $this->filterGroups($filteredGroups, 'ignore'); foreach ($filteredGroups as [$tag, $start1, $end1, $start2, $end2]) { $diff .= $this->tagMap[$tag] . ' ' . implode( From 0bf1a08ceb06cbf099c4dd07c52c4bf6265fb31f Mon Sep 17 00:00:00 2001 From: JBlond Date: Tue, 26 Oct 2021 19:56:23 +0200 Subject: [PATCH 24/24] Fix PSR-12 code style --- lib/jblond/Diff/ConstantsInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jblond/Diff/ConstantsInterface.php b/lib/jblond/Diff/ConstantsInterface.php index 121e7cb..4166a60 100644 --- a/lib/jblond/Diff/ConstantsInterface.php +++ b/lib/jblond/Diff/ConstantsInterface.php @@ -1,6 +1,7 @@