Skip to content

Commit eb5ae61

Browse files
authored
Merge pull request #74 from JBlond/marking-levels
Add new marking levels for inline differences
2 parents 137274a + b10fd38 commit eb5ae61

File tree

15 files changed

+292
-86
lines changed

15 files changed

+292
-86
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": {

example/a.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<h2>This line is removed from version2.</h2>
99
<h2>This line is also removed from version2.</h2>
1010
<h2>This line is the same for both versions.</h2>
11-
<h2>This line has inline differences between both versions.</h2>
11+
<h2>this line has inline differences between both versions.</h2>
1212
<h2>This line is the same for both versions.</h2>
1313
<h2>This line also has inline differences between both versions.</h2>
1414
<h2>This line is the same for both versions.</h2>

example/example.php

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
$sampleB = file_get_contents(dirname(__FILE__) . '/b.txt');
1616

1717
// Options for generating the diff.
18-
$customOptions = [
18+
$diffOptions = [
1919
'context' => 2,
2020
'trimEqual' => false,
2121
'ignoreWhitespace' => true,
@@ -24,8 +24,14 @@
2424

2525
// Choose one of the initializations.
2626
$diff = new Diff($sampleA, $sampleB); // Initialize the diff class with default options.
27-
//$diff = new Diff($sampleA, $sampleB, $customOptions); // Initialize the diff class with custom options.
28-
?><!DOCTYPE html>
27+
//$diff = new Diff($sampleA, $sampleB, $diffOptions); // Initialize the diff class with custom options.
28+
29+
// Options for rendering the diff.
30+
$rendererOptions = [
31+
'inlineMarking' => $_GET['inlineMarking'] ?? Diff\Renderer\MainRenderer::CHANGE_LEVEL_LINE,
32+
]
33+
?>
34+
<!DOCTYPE html>
2935
<html lang="en">
3036
<head>
3137
<meta charset="utf-8"/>
@@ -46,50 +52,58 @@ function changeCSS(cssFile, cssLinkIndex) {
4652
</script>
4753
</head>
4854
<body>
49-
<h1>PHP LibDiff - Examples</h1>
50-
<aside>
51-
<h2>Change Theme</h2>
52-
<a href="#" onclick="changeCSS('styles.css', 0);">Light Theme</a>
53-
<a href="#" onclick="changeCSS('dark-theme.css', 0);">Dark Theme</a>
54-
</aside>
55-
<hr>
55+
<h1>PHP LibDiff - Examples</h1>
56+
<aside>
57+
<h2>Change Theme</h2>
58+
<a href="#" onclick="changeCSS('styles.css', 0);">Light Theme</a>
59+
<a href="#" onclick="changeCSS('dark-theme.css', 0);">Dark Theme</a>
60+
</aside>
61+
<hr>
62+
<aside>
63+
<h2>Inline Marking</h2>
64+
<a href="example.php?inlineMarking=2">Line</a>
65+
<a href="example.php?inlineMarking=1">Word</a>
66+
<a href="example.php?inlineMarking=0">Character</a>
67+
<a href="example.php?inlineMarking=4">None</a>
68+
</aside>
69+
<hr>
5670

57-
<h2>HTML Side by Side Diff</h2>
71+
<h2>HTML Side by Side Diff</h2>
5872

59-
<?php
60-
// Generate a side by side diff.
61-
$renderer = new SideBySide();
62-
echo $diff->isIdentical() ? 'No differences found.' : $diff->Render($renderer);
63-
?>
73+
<?php
74+
// Generate a side by side diff.
75+
$renderer = new SideBySide($rendererOptions);
76+
echo $diff->isIdentical() ? 'No differences found.' : $diff->Render($renderer);
77+
?>
6478

65-
<h2>HTML Inline Diff</h2>
66-
<?php
67-
// Generate an inline diff.
68-
$renderer = new Inline();
69-
echo $diff->isIdentical() ? 'No differences found.' : $diff->Render($renderer);
70-
?>
79+
<h2>HTML Inline Diff</h2>
80+
<?php
81+
// Generate an inline diff.
82+
$renderer = new Inline($rendererOptions);
83+
echo $diff->isIdentical() ? 'No differences found.' : $diff->Render($renderer);
84+
?>
7185

72-
<h2>HTML Unified Diff</h2>
73-
<?php
74-
// Generate a unified diff.
75-
$renderer = new HtmlUnified();
76-
echo $diff->isIdentical() ? 'No differences found.' : '<pre>' . $diff->Render($renderer) . '</pre>';
77-
?>
86+
<h2>HTML Unified Diff</h2>
87+
<?php
88+
// Generate a unified diff.
89+
$renderer = new HtmlUnified($rendererOptions);
90+
echo $diff->isIdentical() ? 'No differences found.' : '<pre>' . $diff->Render($renderer) . '</pre>';
91+
?>
7892

79-
<h2>Text Unified Diff</h2>
80-
<?php
81-
// Generate a unified diff.
82-
$renderer = new Unified();
83-
echo $diff->isIdentical() ?
84-
'No differences found.' : '<pre>' . htmlspecialchars($diff->render($renderer)) . '</pre>';
85-
?>
93+
<h2>Text Unified Diff</h2>
94+
<?php
95+
// Generate a unified diff.
96+
$renderer = new Unified();
97+
echo $diff->isIdentical() ?
98+
'No differences found.' : '<pre>' . htmlspecialchars($diff->render($renderer)) . '</pre>';
99+
?>
86100

87-
<h2>Text Context Diff</h2>
88-
<?php
89-
// Generate a context diff.
90-
$renderer = new Context();
91-
echo $diff->isIdentical() ?
92-
'No differences found.' : '<pre>' . htmlspecialchars($diff->render($renderer)) . '</pre>';
93-
?>
101+
<h2>Text Context Diff</h2>
102+
<?php
103+
// Generate a context diff.
104+
$renderer = new Context();
105+
echo $diff->isIdentical() ?
106+
'No differences found.' : '<pre>' . htmlspecialchars($diff->render($renderer)) . '</pre>';
107+
?>
94108
</body>
95109
</html>

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 <[email protected]>
16-
* @author Ferry Cools <[email protected]>
14+
* @package jblond\Diff\Renderer
15+
* @author Mario Brandt <[email protected]>
16+
* @author Ferry Cools <[email protected]>
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
{

0 commit comments

Comments
 (0)