11
11
*
12
12
* PHP version 7.2 or greater
13
13
*
14
- * @package jblond\Diff\Renderer\Html
15
- * @author Chris Boulton <[email protected] >
14
+ * @package jblond\Diff\Renderer\Html
15
+ * @author Chris Boulton <[email protected] >
16
16
* @copyright (c) 2009 Chris Boulton
17
- * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
18
- * @version 1.15
19
- * @link https://github.com/JBlond/php-diff
17
+ * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
18
+ * @version 1.15
19
+ * @link https://github.com/JBlond/php-diff
20
20
*/
21
21
class HtmlArray extends RendererAbstract
22
22
{
@@ -33,13 +33,19 @@ class HtmlArray extends RendererAbstract
33
33
'title_b ' => 'New Version ' ,
34
34
];
35
35
36
+ /**
37
+ * @var string The last operation which was recorded in the array which contains the changes, used by the renderer.
38
+ * @see HtmlArray::appendChangesArray()
39
+ */
40
+ private $ lastTag ;
41
+
36
42
/**
37
43
* Generate a string representation of changes between the "old and "new" sequences.
38
44
*
39
45
* This method is called by the renderers which extends this class.
40
46
*
41
- * @param array $changes Contains the op-codes about the differences between "old and "new".
42
- * @param SideBySide|Inline $htmlRenderer Renderer which extends this class.
47
+ * @param array $changes Contains the op-codes about the differences between "old and "new".
48
+ * @param object $htmlRenderer Renderer which extends this class.
43
49
*
44
50
* @return string HTML representation of the differences.
45
51
*/
@@ -101,70 +107,68 @@ public function renderHtml(array $changes, object $htmlRenderer): string
101
107
*/
102
108
public function render ()
103
109
{
104
- // " old" & "new" are copied so change markers can be added without modifying the original sequences.
105
- $ old = $ this ->diff ->getOld ();
106
- $ new = $ this ->diff ->getNew ();
110
+ // The old and New texts are copied so change markers can be added without modifying the original sequences.
111
+ $ oldText = $ this ->diff ->getOld ();
112
+ $ newText = $ this ->diff ->getNew ();
107
113
108
114
$ changes = [];
109
- $ opCodes = $ this ->diff ->getGroupedOpcodes ();
110
115
111
- foreach ($ opCodes as $ group ) {
112
- $ blocks = [];
113
- $ lastTag = null ;
114
- $ lastBlock = 0 ;
116
+ foreach ($ this -> diff -> getGroupedOpcodes () as $ group ) {
117
+ $ blocks = [];
118
+ $ this -> lastTag = null ;
119
+
115
120
foreach ($ group as $ code ) {
116
- list ($ tag , $ i1 , $ i2 , $ j1 , $ j2 ) = $ code ;
117
-
118
- if ($ tag == 'replace ' && $ i2 - $ i1 == $ j2 - $ j1 ) {
119
- for ($ i = 0 ; $ i < ($ i2 - $ i1 ); ++$ i ) {
120
- $ fromLine = $ old [$ i1 + $ i ];
121
- $ toLine = $ new [$ j1 + $ i ];
122
-
123
- list ($ start , $ end ) = $ this ->getChangeExtent ($ fromLine , $ toLine );
124
- if ($ start != 0 || $ end != 0 ) {
125
- $ realEnd = mb_strlen ($ fromLine ) + $ end ;
126
- $ fromLine = mb_substr ($ fromLine , 0 , $ start ) . "\0" .
127
- mb_substr ($ fromLine , $ start , $ realEnd - $ start ) . "\1" .
128
- mb_substr ($ fromLine , $ realEnd );
129
-
130
- $ realEnd = mb_strlen ($ toLine ) + $ end ;
131
- $ toLine = mb_substr ($ toLine , 0 , $ start ) . "\0" .
132
- mb_substr ($ toLine , $ start , $ realEnd - $ start ) . "\1" .
133
- mb_substr ($ toLine , $ realEnd );
134
-
135
- $ old [$ i1 + $ i ] = $ fromLine ;
136
- $ new [$ j1 + $ i ] = $ toLine ;
137
- }
138
- }
121
+ list ($ tag , $ startOld , $ endOld , $ startNew , $ endNew ) = $ code ;
122
+ /**
123
+ * $code is an array describing a op-code which includes:
124
+ * 0 - The type of tag (as described below) for the op code.
125
+ * 1 - The beginning line in the first sequence.
126
+ * 2 - The end line in the first sequence.
127
+ * 3 - The beginning line in the second sequence.
128
+ * 4 - The end line in the second sequence.
129
+ *
130
+ * The different types of tags include:
131
+ * replace - The string from $startOld to $endOld in $oldText should be replaced by
132
+ * the string in $newText from $startNew to $endNew.
133
+ * delete - The string in $oldText from $startOld to $endNew should be deleted.
134
+ * insert - The string in $newText from $startNew to $endNew should be inserted at $startOld in
135
+ * $oldText.
136
+ * equal - The two strings with the specified ranges are equal.
137
+ */
138
+
139
+ $ blockSizeOld = $ endOld - $ startOld ;
140
+ $ blockSizeNew = $ endNew - $ startNew ;
141
+
142
+ if (($ tag == 'replace ' ) && ($ blockSizeOld == $ blockSizeNew )) {
143
+ // Inline differences between old and new block.
144
+ $ this ->markInlineChange ($ oldText , $ newText , $ startOld , $ endOld , $ startNew );
139
145
}
140
146
141
- if ($ tag != $ lastTag ) {
142
- $ blocks [] = $ this ->getDefaultArray ($ tag , $ i1 , $ j1 );
143
- $ lastBlock = count ($ blocks ) - 1 ;
144
- }
147
+ $ lastBlock = $ this ->appendChangesArray ($ blocks , $ tag , $ startOld , $ startNew );
145
148
146
- $ lastTag = $ tag ;
149
+ // Extract the block from both the old and new text and format each line.
150
+ $ oldBlock = $ this ->formatLines (array_slice ($ oldText , $ startOld , $ blockSizeOld ));
151
+ $ newBlock = $ this ->formatLines (array_slice ($ newText , $ startNew , $ blockSizeNew ));
147
152
148
153
if ($ tag == 'equal ' ) {
149
- $ lines = array_slice ($ old , $ i1 , ($ i2 - $ i1 ));
150
- $ blocks [$ lastBlock ]['base ' ]['lines ' ] += $ this ->formatLines ($ lines );
151
-
152
- $ lines = array_slice ($ new , $ j1 , ($ j2 - $ j1 ));
153
- $ blocks [$ lastBlock ]['changed ' ]['lines ' ] += $ this ->formatLines ($ lines );
154
- } else {
155
- if ($ tag == 'replace ' || $ tag == 'delete ' ) {
156
- $ lines = array_slice ($ old , $ i1 , ($ i2 - $ i1 ));
157
- $ lines = $ this ->formatLines ($ lines );
158
- $ lines = str_replace (array ("\0" , "\1" ), array ('<del> ' , '</del> ' ), $ lines );
159
- $ blocks [$ lastBlock ]['base ' ]['lines ' ] += $ lines ;
160
- }
161
-
162
- if ($ tag == 'replace ' || $ tag == 'insert ' ) {
163
- $ lines = array_slice ($ new , $ j1 , ($ j2 - $ j1 ));
164
- $ lines = $ this ->formatLines ($ lines );
165
- $ lines = str_replace (array ("\0" , "\1" ), array ('<ins> ' , '</ins> ' ), $ lines );
166
- $ blocks [$ lastBlock ]['changed ' ]['lines ' ] += $ lines ;
167
- }
154
+ // Old block equals New block
155
+ $ blocks [$ lastBlock ]['base ' ]['lines ' ] += $ oldBlock ;
156
+ $ blocks [$ lastBlock ]['changed ' ]['lines ' ] += $ newBlock ;
157
+ continue ;
158
+ }
159
+
160
+ if ($ tag == 'replace ' || $ tag == 'delete ' ) {
161
+ // Inline differences or old block doesn't exist in the new text.
162
+ // Replace the markers, which where added above, by HTML delete tags.
163
+ $ oldBlock = str_replace (["\0" , "\1" ], ['<del> ' , '</del> ' ], $ oldBlock );
164
+ $ blocks [$ lastBlock ]['base ' ]['lines ' ] += $ oldBlock ;
165
+ }
166
+
167
+ if ($ tag == 'replace ' || $ tag == 'insert ' ) {
168
+ // Inline differences or the new block doesn't exist in the old text.
169
+ // Replace the markers, which where added above, by HTML insert tags.
170
+ $ newBlock = str_replace (["\0" , "\1" ], ['<ins> ' , '</ins> ' ], $ newBlock );
171
+ $ blocks [$ lastBlock ]['changed ' ]['lines ' ] += $ newBlock ;
168
172
}
169
173
}
170
174
$ changes [] = $ blocks ;
@@ -174,32 +178,89 @@ public function render()
174
178
}
175
179
176
180
/**
177
- * Determine where changes in two strings begin and where they end .
181
+ * Add markers around inline changes between old and new text .
178
182
*
179
- * This returns an array.
180
- * The first value defines the first (starting at 0) character from start of the old string which is different.
181
- * The second value defines the last character from end of the old string which is different.
183
+ * Each line of the old and new text is evaluated.
184
+ * When a line of old differs from the same line of new, a marker is inserted into both lines, just before the first
185
+ * different character. A second marker is added just behind the last character which differs from each other.
186
+ *
187
+ * E.g.
188
+ * <pre>
189
+ * 1234567
190
+ * OLd => "abcdefg" Start marker inserted at position 3
191
+ * New => "ab123fg" End marker inserted at position 6
192
+ * </pre>
193
+ *
194
+ * @param array $oldText Collection of lines of old text.
195
+ * @param array $newText Collection of lines of new text.
196
+ * @param int $startOld First line of the block in old to replace.
197
+ * @param int $endOld last line of the block in old to replace.
198
+ * @param int $startNew First line of the block in new to replace.
199
+ */
200
+ private function markInlineChange (array &$ oldText , array &$ newText , $ startOld , $ endOld , $ startNew )
201
+ {
202
+ for ($ i = 0 ; $ i < ($ endOld - $ startOld ); ++$ i ) {
203
+ // Check each line in the block for differences.
204
+ $ oldString = $ oldText [$ startOld + $ i ];
205
+ $ newString = $ newText [$ startNew + $ i ];
206
+
207
+ // Determine the start and end position of the line difference.
208
+ list ($ start , $ end ) = $ this ->getInlineChange ($ oldString , $ newString );
209
+ if ($ start != 0 || $ end != 0 ) {
210
+ // Changes between the lines exist.
211
+ // Add markers around the changed character sequence in the old string.
212
+ $ sequenceEnd = mb_strlen ($ oldString ) + $ end ;
213
+ $ oldString =
214
+ mb_substr ($ oldString , 0 , $ start ) . "\0" .
215
+ mb_substr ($ oldString , $ start , $ sequenceEnd - $ start ) . "\1" .
216
+ mb_substr ($ oldString , $ sequenceEnd );
217
+
218
+ // Add markers around the changed character sequence in the new string.
219
+ $ sequenceEnd = mb_strlen ($ newString ) + $ end ;
220
+ $ newString =
221
+ mb_substr ($ newString , 0 , $ start ) . "\0" .
222
+ mb_substr ($ newString , $ start , $ sequenceEnd - $ start ) . "\1" .
223
+ mb_substr ($ newString , $ sequenceEnd );
224
+
225
+ // Overwrite the strings in the old and new text so the changed lines include the markers.
226
+ $ oldText [$ startOld + $ i ] = $ oldString ;
227
+ $ newText [$ startNew + $ i ] = $ newString ;
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Determine where changes between two strings begin and where they end.
234
+ *
235
+ * This returns a two elements array.
236
+ * The first element defines the first (starting at 0) character from the start of the old string which is
237
+ * different.
238
+ * The second element defines the last (starting at -0) character from the end of the old string which is different.
182
239
*
183
240
*
184
241
* @param string $oldString The first string to compare.
185
242
* @param string $newString The second string to compare.
186
243
*
187
244
* @return array Array containing the starting position (0 by default) and the ending position (-1 by default)
188
245
*/
189
- private function getChangeExtent (string $ oldString , string $ newString ): array
246
+ private function getInlineChange (string $ oldString , string $ newString ): array
190
247
{
191
248
$ start = 0 ;
192
249
$ limit = min (mb_strlen ($ oldString ), mb_strlen ($ newString ));
193
250
194
- // Find first difference.
251
+ // Find the position of the first character which is different between old and new.
252
+ // Starts at the begin of the strings.
253
+ // Stops at the end of the shortest string.
195
254
while ($ start < $ limit && mb_substr ($ oldString , $ start , 1 ) == mb_substr ($ newString , $ start , 1 )) {
196
255
++$ start ;
197
256
}
198
257
199
- $ end = -1 ;
200
- $ limit = $ limit - $ start ;
258
+ $ end = -1 ;
259
+ $ limit = $ limit - $ start ;
201
260
202
- // Find last difference.
261
+ // Find the position of the last character which is different between old and new.
262
+ // Starts at the end of the shortest string.
263
+ // Stops just before the last different character.
203
264
while (-$ end <= $ limit && mb_substr ($ oldString , $ end , 1 ) == mb_substr ($ newString , $ end , 1 )) {
204
265
--$ end ;
205
266
}
@@ -210,6 +271,41 @@ private function getChangeExtent(string $oldString, string $newString): array
210
271
];
211
272
}
212
273
274
+ /**
275
+ * Helper function that will fill the changes-array for the renderer with default values.
276
+ * Every time a operation changes (specified by $tag) , a new element will be appended to this array.
277
+ *
278
+ * The index of the last element of the array is always returned.
279
+ *
280
+ * @param array $blocks The array which keeps the changes for the HTML renderer.
281
+ * @param string $tag Kind of difference.
282
+ * @param integer $lineInOld Start of block in "old".
283
+ * @param integer $lineInNew Start of block in "new".
284
+ *
285
+ * @return int The index of the last element.
286
+ */
287
+ private function appendChangesArray (array &$ blocks , string $ tag , int $ lineInOld , int $ lineInNew ): int
288
+ {
289
+ if ($ tag == $ this ->lastTag ) {
290
+ return count ($ blocks ) - 1 ;
291
+ }
292
+
293
+ $ blocks [] = [
294
+ 'tag ' => $ tag ,
295
+ 'base ' => [
296
+ 'offset ' => $ lineInOld ,
297
+ 'lines ' => []
298
+ ],
299
+ 'changed ' => [
300
+ 'offset ' => $ lineInNew ,
301
+ 'lines ' => []
302
+ ]
303
+ ];
304
+
305
+ $ this ->lastTag = $ tag ;
306
+ return count ($ blocks ) - 1 ;
307
+ }
308
+
213
309
/**
214
310
* Format a series of strings which are suitable for output in a HTML rendered diff.
215
311
*
@@ -226,7 +322,7 @@ protected function formatLines(array $strings): array
226
322
// Replace tabs with spaces.
227
323
$ strings = array_map (
228
324
function ($ item ) {
229
- return $ this ->expandTabs ( $ item );
325
+ return str_replace ( "\t" , str_repeat ( ' ' , $ this ->options [ ' tabSize ' ]), $ item );
230
326
},
231
327
$ strings
232
328
);
@@ -235,12 +331,12 @@ function ($item) {
235
331
// Convert special characters to HTML entities
236
332
$ strings = array_map (
237
333
function ($ item ) {
238
- return $ this -> htmlSafe ($ item );
334
+ return htmlspecialchars ($ item, ENT_NOQUOTES , ' UTF-8 ' );
239
335
},
240
336
$ strings
241
337
);
242
338
243
- // Replace leading spaces of a line with HTML enities .
339
+ // Replace leading spaces of a line with HTML entities .
244
340
foreach ($ strings as &$ line ) {
245
341
$ line = preg_replace_callback (
246
342
'/(^[ \0\1]*)/ ' ,
@@ -254,52 +350,4 @@ function ($matches) {
254
350
255
351
return $ strings ;
256
352
}
257
-
258
- /**
259
- * Replace tabs in a string with an amount of spaces as defined by the tabSize option of this class.
260
- *
261
- * @param string $line The string which contains tabs to convert.
262
- *
263
- * @return string The line with the tabs converted to spaces.
264
- */
265
- private function expandTabs (string $ line ): string
266
- {
267
- return str_replace ("\t" , str_repeat (' ' , $ this ->options ['tabSize ' ]), $ line );
268
- }
269
-
270
- /**
271
- * Make a string HTML safe for output on a page.
272
- *
273
- * @param string $string The string to make safe.
274
- *
275
- * @return string The string with the HTML characters replaced by entities.
276
- */
277
- private function htmlSafe (string $ string ): string
278
- {
279
- return htmlspecialchars ($ string , ENT_NOQUOTES , 'UTF-8 ' );
280
- }
281
-
282
- /**
283
- * Helper function that provides an array for the renderer with default values for the changes to render.
284
- *
285
- * @param string $tag Kind of difference.
286
- * @param integer $lineInOld Start of block in "old".
287
- * @param integer $lineInNew Start of block in "new".
288
- *
289
- * @return array
290
- */
291
- private function getDefaultArray (string $ tag , int $ lineInOld , int $ lineInNew ): array
292
- {
293
- return [
294
- 'tag ' => $ tag ,
295
- 'base ' => [
296
- 'offset ' => $ lineInOld ,
297
- 'lines ' => []
298
- ],
299
- 'changed ' => [
300
- 'offset ' => $ lineInNew ,
301
- 'lines ' => []
302
- ]
303
- ];
304
- }
305
353
}
0 commit comments