Skip to content

Commit 75358da

Browse files
committed
Add new marking levels for inline differences
* In addition to the existing line level, this commit adds marking at character level, word level and no inline marking. * PhpUnit tests added for line, word and character-level marking.
1 parent 137274a commit 75358da

File tree

4 files changed

+218
-28
lines changed

4 files changed

+218
-28
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
},
4040
"autoload": {
4141
"psr-4": {
42-
"jblond\\": "lib/jblond"
42+
"jblond\\": "lib/jblond",
43+
"Tests\\": "tests"
4344
}
4445
},
4546
"config": {

lib/jblond/Diff/Renderer/MainRenderer.php

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace jblond\Diff\Renderer;
66

7+
use jblond\Diff\SequenceMatcher;
8+
79
/**
810
* Base renderer for rendering diffs for PHP DiffLib.
911
*
@@ -135,7 +137,7 @@ protected function renderSequences(): array
135137

136138
if (($tag == 'replace') && ($blockSizeOld == $blockSizeNew)) {
137139
// Inline differences between old and new block.
138-
$this->markInlineChange($oldText, $newText, $startOld, $endOld, $startNew);
140+
$this->markInlineChanges($oldText, $newText, $startOld, $endOld, $startNew);
139141
}
140142

141143
$lastBlock = $this->appendChangesArray($blocks, $tag, $startOld, $startNew);
@@ -167,6 +169,118 @@ protected function renderSequences(): array
167169
return $changes;
168170
}
169171

172+
/**
173+
* Surround inline changes with markers.
174+
*
175+
* @param array $oldText Collection of lines of old text.
176+
* @param array $newText Collection of lines of new text.
177+
* @param int $startOld First line of the block in old to replace.
178+
* @param int $endOld last line of the block in old to replace.
179+
* @param int $startNew First line of the block in new to replace.
180+
*/
181+
private function markInlineChanges(
182+
array &$oldText,
183+
array &$newText,
184+
int $startOld,
185+
int $endOld,
186+
int $startNew
187+
): void {
188+
if ($this->options['inlineMarking'] < self::CHANGE_LEVEL_LINE) {
189+
$this->markInnerChange($oldText, $newText, $startOld, $endOld, $startNew);
190+
191+
return;
192+
}
193+
194+
if ($this->options['inlineMarking'] == self::CHANGE_LEVEL_LINE) {
195+
$this->markOuterChange($oldText, $newText, $startOld, $endOld, $startNew);
196+
}
197+
}
198+
199+
/**
200+
* Add markers around inline changes between old and new text.
201+
*
202+
* Each line of the old and new text is evaluated.
203+
* When a line of old differs from the same line of new, a marker is inserted into both lines, just before the first
204+
* different character/word. A second marker is added just before the following character/word which matches again.
205+
*
206+
* Setting parameter changeType to self::CHANGE_LEVEL_CHAR will mark differences at character level.
207+
* Other values will mark differences at word level.
208+
*
209+
* E.g. Character level.
210+
* <pre>
211+
* 1234567890
212+
* Old => "aa bbc cdd" Start marker inserted at position 4
213+
* New => "aa 12c cdd" End marker inserted at position 6
214+
* </pre>
215+
* E.g. Word level.
216+
* <pre>
217+
* 1234567890
218+
* Old => "aa bbc cdd" Start marker inserted at position 4
219+
* New => "aa 12c cdd" End marker inserted at position 7
220+
* </pre>
221+
*
222+
* @param array $oldText Collection of lines of old text.
223+
* @param array $newText Collection of lines of new text.
224+
* @param int $startOld First line of the block in old to replace.
225+
* @param int $endOld last line of the block in old to replace.
226+
* @param int $startNew First line of the block in new to replace.
227+
*/
228+
private function markInnerChange(array &$oldText, array &$newText, int $startOld, int $endOld, int $startNew): void
229+
{
230+
for ($iterator = 0; $iterator < ($endOld - $startOld); ++$iterator) {
231+
// ChangeType 0: Character Level.
232+
// ChangeType 1: Word Level.
233+
$regex = $this->options['inlineMarking'] ? '/\w+|[^\w\s]|\s/u' : '/.?/u';
234+
235+
// Deconstruct the lines into arrays, including new empty element to the end in case a marker needs to be
236+
// placed as last.
237+
$oldLine = $this->sequenceToArray($regex, $oldText[$startOld + $iterator]);
238+
$newLine = $this->sequenceToArray($regex, $newText[$startNew + $iterator]);
239+
$oldLine[] = '';
240+
$newLine[] = '';
241+
242+
$sequenceMatcher = new SequenceMatcher($oldLine, $newLine);
243+
$opCodes = $sequenceMatcher->getGroupedOpCodes();
244+
245+
foreach ($opCodes as $group) {
246+
foreach ($group as [$tag, $changeStartOld, $changeEndOld, $changeStartNew, $changeEndNew]) {
247+
if ($tag == 'equal') {
248+
continue;
249+
}
250+
if ($tag == 'replace' || $tag == 'delete') {
251+
$oldLine[$changeStartOld] = "\0" . $oldLine[$changeStartOld];
252+
$oldLine[$changeEndOld] = "\1" . $oldLine[$changeEndOld];
253+
}
254+
if ($tag == 'replace' || $tag == 'insert') {
255+
$newLine[$changeStartNew] = "\0" . $newLine[$changeStartNew];
256+
$newLine[$changeEndNew] = "\1" . $newLine[$changeEndNew];
257+
}
258+
}
259+
}
260+
261+
// Reconstruct the lines and overwrite originals.
262+
$oldText[$startOld + $iterator] = implode('', $oldLine);
263+
$newText[$startNew + $iterator] = implode('', $newLine);
264+
}
265+
}
266+
267+
/**
268+
* Split a sequence of characters into an array.
269+
*
270+
* Each element of the returned array contains a full pattern match of the regex pattern.
271+
*
272+
* @param string $pattern Regex pattern to split by.
273+
* @param string $sequence The sequence to split.
274+
*
275+
* @return array The split sequence.
276+
*/
277+
public function sequenceToArray(string $pattern, string $sequence): array
278+
{
279+
preg_match_all($pattern, $sequence, $matches);
280+
281+
return $matches[0];
282+
}
283+
170284
/**
171285
* Add markers around inline changes between old and new text.
172286
*
@@ -187,15 +301,15 @@ protected function renderSequences(): array
187301
* @param int $endOld last line of the block in old to replace.
188302
* @param int $startNew First line of the block in new to replace.
189303
*/
190-
private function markInlineChange(array &$oldText, array &$newText, int $startOld, int $endOld, int $startNew)
304+
private function markOuterChange(array &$oldText, array &$newText, int $startOld, int $endOld, int $startNew): void
191305
{
192306
for ($iterator = 0; $iterator < ($endOld - $startOld); ++$iterator) {
193307
// Check each line in the block for differences.
194308
$oldString = $oldText[$startOld + $iterator];
195309
$newString = $newText[$startNew + $iterator];
196310

197311
// Determine the start and end position of the line difference.
198-
[$start, $end] = $this->getInlineChange($oldString, $newString);
312+
[$start, $end] = $this->getOuterChange($oldString, $newString);
199313
if ($start != 0 || $end != 0) {
200314
// Changes between the lines exist.
201315
// Add markers around the changed character sequence in the old string.
@@ -233,7 +347,7 @@ private function markInlineChange(array &$oldText, array &$newText, int $startOl
233347
*
234348
* @return array Array containing the starting position (0 by default) and the ending position (-1 by default)
235349
*/
236-
private function getInlineChange(string $oldString, string $newString): array
350+
private function getOuterChange(string $oldString, string $newString): array
237351
{
238352
$start = 0;
239353
$limit = min(mb_strlen($oldString), mb_strlen($newString));

lib/jblond/Diff/Renderer/MainRendererAbstract.php

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,32 @@
1111
*
1212
* PHP version 7.2 or greater
1313
*
14-
* @package jblond\Diff\Renderer
15-
* @author Mario Brandt <leet31337@web.de>
16-
* @author Ferry Cools <info@DigiLive.nl>
14+
* @package jblond\Diff\Renderer
15+
* @author Mario Brandt <leet31337@web.de>
16+
* @author Ferry Cools <info@DigiLive.nl>
1717
* @copyright (c) 2009 Chris Boulton
18-
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
19-
* @version 2.2.1
20-
* @link https://github.com/JBlond/php-diff
18+
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
19+
* @version 2.2.1
20+
* @link https://github.com/JBlond/php-diff
2121
*/
2222
abstract class MainRendererAbstract
2323
{
24-
24+
/**
25+
* Mark inline character differences.
26+
*/
27+
public const CHANGE_LEVEL_CHAR = 0;
28+
/**
29+
* Mark inline word differences.
30+
*/
31+
public const CHANGE_LEVEL_WORD = 1;
32+
/**
33+
* Mark line differences.
34+
*/
35+
public const CHANGE_LEVEL_LINE = 2;
36+
/**
37+
* Mark no inline differences.
38+
*/
39+
public const CHANGE_LEVEL_NONE = 4;
2540
/**
2641
* @var Diff $diff Instance of the diff class that this renderer is generating the rendered diff for.
2742
*/
@@ -30,6 +45,11 @@ abstract class MainRendererAbstract
3045
/**
3146
* @var array Associative array containing the default options available for this renderer and their default
3247
* value.
48+
* - inlineMarking The level of how differences are marked.
49+
* - self::CHANGE_LEVEL_NONE Don't Inline-Mark.
50+
* - self::CHANGE_LEVEL_CHAR Inline-Mark each different character.
51+
* - self::CHANGE_LEVEL_WORD Inline-Mark each different word.
52+
* - self::CHANGE_LEVEL_LINE Inline-Mark from first to last line diff.
3353
* - tabSize The amount of spaces to replace a tab character with.
3454
* - format The format of the input texts.
3555
* - cliColor Colorized output for cli.
@@ -40,6 +60,7 @@ abstract class MainRendererAbstract
4060
* - deleteColors Fore- and background color for removed text. Only when cliColor = true.
4161
*/
4262
protected $mainOptions = [
63+
'inlineMarking' => self::CHANGE_LEVEL_LINE,
4364
'tabSize' => 4,
4465
'format' => 'plain',
4566
'cliColor' => false,
@@ -60,7 +81,7 @@ abstract class MainRendererAbstract
6081
* The constructor. Instantiates the rendering engine and if options are passed,
6182
* sets the options for the renderer.
6283
*
63-
* @param array $options Optionally, an array of the options for the renderer.
84+
* @param array $options Optionally, an array of the options for the renderer.
6485
*/
6586
public function __construct(array $options = [])
6687
{
@@ -72,9 +93,11 @@ public function __construct(array $options = [])
7293
*
7394
* Options are merged with the default to ensure that there aren't any missing options.
7495
* When custom options are added to the default ones, they can be overwritten, but they can't be removed.
96+
*
97+
* @param array $options Array of options to set.
98+
*
7599
* @see MainRendererAbstract::$mainOptions
76100
*
77-
* @param array $options Array of options to set.
78101
*/
79102
public function setOptions(array $options)
80103
{

tests/Diff/Renderer/MainRendererTest.php

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
/** @noinspection PhpMethodNamingConventionInspection */
4+
35
declare(strict_types=1);
46

57
namespace Tests\Diff\Renderer;
@@ -8,23 +10,25 @@
810
use jblond\Diff\Renderer\MainRenderer;
911
use PHPUnit\Framework\TestCase;
1012
use ReflectionClass;
13+
use ReflectionException;
1114

1215
/**
1316
* PHPUnit Test for the main renderer of PHP DiffLib.
1417
*
1518
* PHP version 7.2 or greater
1619
*
17-
* @package Tests\Diff\Renderer
18-
* @author Mario Brandt <leet31337@web.de>
19-
* @author Ferry Cools <info@DigiLive.nl>
20+
* @package Tests\Diff\Renderer
21+
* @author Mario Brandt <leet31337@web.de>
22+
* @author Ferry Cools <info@DigiLive.nl>
2023
* @copyright (c) 2009 Mario Brandt
21-
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
22-
* @version 2.2.1
23-
* @link https://github.com/JBlond/php-diff
24+
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
25+
* @version 2.2.1
26+
* @link https://github.com/JBlond/php-diff
2427
*/
2528

2629
/**
2730
* Class MainRendererTest
31+
*
2832
* @package Tests\Diff\Renderer\Html
2933
*/
3034
class MainRendererTest extends TestCase
@@ -40,9 +44,9 @@ class MainRendererTest extends TestCase
4044
/**
4145
* MainRendererTest constructor.
4246
*
43-
* @param null $name
44-
* @param array $data
45-
* @param string $dataName
47+
* @param null $name
48+
* @param array $data
49+
* @param string $dataName
4650
*/
4751
public function __construct($name = null, array $data = [], $dataName = '')
4852
{
@@ -86,14 +90,14 @@ public function testRenderSimpleDelete()
8690
/**
8791
* Call protected/private method of a class.
8892
*
89-
* @param object &$object Instantiated object that we will run method on.
90-
* @param string $methodName Method name to call
91-
* @param array $parameters Array of parameters to pass into method.
93+
* @param object $object Instantiated object that we will run method on.
94+
* @param string $methodName Method name to call
95+
* @param array $parameters Array of parameters to pass into method.
9296
*
9397
* @return mixed Method return.
94-
* @throws \ReflectionException If the class doesn't exist.
98+
* @throws ReflectionException If the class doesn't exist.
9599
*/
96-
public function invokeMethod(&$object, $methodName, array $parameters = [])
100+
public function invokeMethod(object $object, string $methodName, array $parameters = [])
97101
{
98102
$reflection = new ReflectionClass(get_class($object));
99103
$method = $reflection->getMethod($methodName);
@@ -137,4 +141,52 @@ public function testRenderFixesSpaces()
137141
$result
138142
);
139143
}
144+
145+
/**
146+
* Test inline marking for changes at line level.
147+
*
148+
* Everything from the first difference to the last difference should be enclosed by the markers.
149+
*
150+
* @throws ReflectionException When invoking the method fails.
151+
*/
152+
public function testMarkOuterChange()
153+
{
154+
$renderer = new MainRenderer();
155+
$text1 = ['one two three four'];
156+
$text2 = ['one tWo thrEe four'];
157+
$this->invokeMethod($renderer, 'markOuterChange', [&$text1, &$text2, 0, 1, 0]);
158+
$this->assertSame(["one t\0wo thre\1e four"], $text1);
159+
$this->assertSame(["one t\0Wo thrE\1e four"], $text2);
160+
}
161+
162+
/**
163+
* Test inline marking for changes at character and word level.
164+
*
165+
* At character level, everything from a different character to any subsequent different character should be
166+
* enclosed by the markers.
167+
*
168+
* At word level, every word that is different should be enclosed by the markers.
169+
*
170+
* @throws ReflectionException When invoking the method fails.
171+
*/
172+
public function testMarkInnerChange()
173+
{
174+
$renderer = new MainRenderer();
175+
176+
// Character level.
177+
$renderer->setOptions(['inlineMarking' => $renderer::CHANGE_LEVEL_CHAR]);
178+
$text1 = ['one two three four'];
179+
$text2 = ['one tWo thrEe fouR'];
180+
$this->invokeMethod($renderer, 'markInnerChange', [&$text1, &$text2, 0, 1, 0]);
181+
$this->assertSame(["one t\0w\1o thr\0e\1e fou\0r\1"], $text1);
182+
$this->assertSame(["one t\0W\1o thr\0E\1e fou\0R\1"], $text2);
183+
184+
// Word Level.
185+
$renderer->setOptions(['inlineMarking' => $renderer::CHANGE_LEVEL_WORD]);
186+
$text1 = ['one two three four'];
187+
$text2 = ['one tWo thrEe fouR'];
188+
$this->invokeMethod($renderer, 'markInnerChange', [&$text1, &$text2, 0, 1, 0]);
189+
$this->assertSame(["one \0two\1 \0three\1 \0four\1"], $text1);
190+
$this->assertSame(["one \0tWo\1 \0thrEe\1 \0fouR\1"], $text2);
191+
}
140192
}

0 commit comments

Comments
 (0)