1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Caxy\HtmlDiff; |
4
|
|
|
|
5
|
|
|
use Caxy\HtmlDiff\Table\TableDiff; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Class HtmlDiff |
9
|
|
|
* @package Caxy\HtmlDiff |
10
|
|
|
*/ |
11
|
|
|
class HtmlDiff extends AbstractDiff |
12
|
|
|
{ |
13
|
|
|
/** |
14
|
|
|
* @var array |
15
|
|
|
*/ |
16
|
|
|
protected $wordIndices; |
17
|
|
|
/** |
18
|
|
|
* @var array |
19
|
|
|
*/ |
20
|
|
|
protected $oldTables; |
21
|
|
|
/** |
22
|
|
|
* @var array |
23
|
|
|
*/ |
24
|
|
|
protected $newTables; |
25
|
|
|
/** |
26
|
|
|
* @var array |
27
|
|
|
*/ |
28
|
|
|
protected $newIsolatedDiffTags; |
29
|
|
|
/** |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
protected $oldIsolatedDiffTags; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @param string $oldText |
36
|
|
|
* @param string $newText |
37
|
|
|
* @param HtmlDiffConfig|null $config |
38
|
|
|
* |
39
|
|
|
* @return self |
40
|
|
|
*/ |
41
|
7 |
View Code Duplication |
public static function create($oldText, $newText, HtmlDiffConfig $config = null) |
|
|
|
|
42
|
|
|
{ |
43
|
7 |
|
$diff = new self($oldText, $newText); |
44
|
|
|
|
45
|
7 |
|
if (null !== $config) { |
46
|
7 |
|
$diff->setConfig($config); |
47
|
7 |
|
} |
48
|
|
|
|
49
|
7 |
|
return $diff; |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @param $bool |
54
|
|
|
* |
55
|
|
|
* @return $this |
56
|
|
|
* |
57
|
|
|
* @deprecated since 0.1.0 |
58
|
|
|
*/ |
59
|
|
|
public function setUseTableDiffing($bool) |
60
|
|
|
{ |
61
|
|
|
$this->config->setUseTableDiffing($bool); |
62
|
|
|
|
63
|
|
|
return $this; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @param boolean $boolean |
68
|
|
|
* @return HtmlDiff |
69
|
|
|
* |
70
|
|
|
* @deprecated since 0.1.0 |
71
|
|
|
*/ |
72
|
|
|
public function setInsertSpaceInReplace($boolean) |
73
|
|
|
{ |
74
|
|
|
$this->config->setInsertSpaceInReplace($boolean); |
75
|
|
|
|
76
|
|
|
return $this; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @return boolean |
81
|
|
|
* |
82
|
|
|
* @deprecated since 0.1.0 |
83
|
|
|
*/ |
84
|
|
|
public function getInsertSpaceInReplace() |
85
|
|
|
{ |
86
|
|
|
return $this->config->isInsertSpaceInReplace(); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* @return string |
91
|
|
|
*/ |
92
|
11 |
|
public function build() |
93
|
|
|
{ |
94
|
11 |
|
if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) { |
95
|
|
|
$this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText); |
96
|
|
|
|
97
|
|
|
return $this->content; |
98
|
|
|
} |
99
|
|
|
|
100
|
11 |
|
$this->splitInputsToWords(); |
101
|
11 |
|
$this->replaceIsolatedDiffTags(); |
102
|
11 |
|
$this->indexNewWords(); |
103
|
|
|
|
104
|
11 |
|
$operations = $this->operations(); |
105
|
11 |
|
foreach ($operations as $item) { |
106
|
11 |
|
$this->performOperation( $item ); |
107
|
11 |
|
} |
108
|
|
|
|
109
|
11 |
|
if ($this->hasDiffCache()) { |
110
|
|
|
$this->getDiffCache()->save($this->oldText, $this->newText, $this->content); |
111
|
|
|
} |
112
|
|
|
|
113
|
11 |
|
return $this->content; |
114
|
|
|
} |
115
|
|
|
|
116
|
11 |
|
protected function indexNewWords() |
117
|
|
|
{ |
118
|
11 |
|
$this->wordIndices = array(); |
119
|
11 |
|
foreach ($this->newWords as $i => $word) { |
120
|
11 |
|
if ( $this->isTag( $word ) ) { |
121
|
8 |
|
$word = $this->stripTagAttributes( $word ); |
122
|
8 |
|
} |
123
|
11 |
|
if ( isset( $this->wordIndices[ $word ] ) ) { |
124
|
11 |
|
$this->wordIndices[ $word ][] = $i; |
125
|
11 |
|
} else { |
126
|
11 |
|
$this->wordIndices[ $word ] = array( $i ); |
127
|
|
|
} |
128
|
11 |
|
} |
129
|
11 |
|
} |
130
|
|
|
|
131
|
11 |
|
protected function replaceIsolatedDiffTags() |
132
|
|
|
{ |
133
|
11 |
|
$this->oldIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->oldWords); |
134
|
11 |
|
$this->newIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->newWords); |
135
|
11 |
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* @param array $words |
139
|
|
|
* |
140
|
|
|
* @return array |
141
|
|
|
*/ |
142
|
11 |
|
protected function createIsolatedDiffTagPlaceholders(&$words) |
143
|
|
|
{ |
144
|
11 |
|
$openIsolatedDiffTags = 0; |
145
|
11 |
|
$isolatedDiffTagIndicies = array(); |
146
|
11 |
|
$isolatedDiffTagStart = 0; |
147
|
11 |
|
$currentIsolatedDiffTag = null; |
148
|
11 |
|
foreach ($words as $index => $word) { |
149
|
11 |
|
$openIsolatedDiffTag = $this->isOpeningIsolatedDiffTag($word, $currentIsolatedDiffTag); |
150
|
11 |
|
if ($openIsolatedDiffTag) { |
151
|
11 |
|
if ($openIsolatedDiffTags === 0) { |
152
|
11 |
|
$isolatedDiffTagStart = $index; |
153
|
11 |
|
} |
154
|
11 |
|
$openIsolatedDiffTags++; |
155
|
11 |
|
$currentIsolatedDiffTag = $openIsolatedDiffTag; |
156
|
11 |
|
} elseif ($openIsolatedDiffTags > 0 && $this->isClosingIsolatedDiffTag($word, $currentIsolatedDiffTag)) { |
157
|
10 |
|
$openIsolatedDiffTags--; |
158
|
10 |
|
if ($openIsolatedDiffTags == 0) { |
159
|
10 |
|
$isolatedDiffTagIndicies[] = array ('start' => $isolatedDiffTagStart, 'length' => $index - $isolatedDiffTagStart + 1, 'tagType' => $currentIsolatedDiffTag); |
|
|
|
|
160
|
10 |
|
$currentIsolatedDiffTag = null; |
161
|
10 |
|
} |
162
|
10 |
|
} |
163
|
11 |
|
} |
164
|
11 |
|
$isolatedDiffTagScript = array(); |
165
|
11 |
|
$offset = 0; |
166
|
11 |
|
foreach ($isolatedDiffTagIndicies as $isolatedDiffTagIndex) { |
167
|
10 |
|
$start = $isolatedDiffTagIndex['start'] - $offset; |
168
|
10 |
|
$placeholderString = $this->config->getIsolatedDiffTagPlaceholder($isolatedDiffTagIndex['tagType']); |
169
|
10 |
|
$isolatedDiffTagScript[$start] = array_splice($words, $start, $isolatedDiffTagIndex['length'], $placeholderString); |
|
|
|
|
170
|
10 |
|
$offset += $isolatedDiffTagIndex['length'] - 1; |
171
|
11 |
|
} |
172
|
|
|
|
173
|
11 |
|
return $isolatedDiffTagScript; |
174
|
|
|
|
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* @param string $item |
179
|
|
|
* @param null|string $currentIsolatedDiffTag |
180
|
|
|
* |
181
|
|
|
* @return false|string |
|
|
|
|
182
|
|
|
*/ |
183
|
11 |
View Code Duplication |
protected function isOpeningIsolatedDiffTag($item, $currentIsolatedDiffTag = null) |
|
|
|
|
184
|
|
|
{ |
185
|
|
|
$tagsToMatch = $currentIsolatedDiffTag !== null |
186
|
11 |
|
? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag)) |
187
|
11 |
|
: $this->config->getIsolatedDiffTags(); |
188
|
11 |
|
foreach ($tagsToMatch as $key => $value) { |
189
|
11 |
|
if (preg_match("#<".$key."[^>]*>\\s*#iU", $item)) { |
190
|
11 |
|
return $key; |
191
|
|
|
} |
192
|
11 |
|
} |
193
|
|
|
|
194
|
11 |
|
return false; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* @param string $item |
199
|
|
|
* @param null|string $currentIsolatedDiffTag |
200
|
|
|
* |
201
|
|
|
* @return false|string |
|
|
|
|
202
|
|
|
*/ |
203
|
11 |
View Code Duplication |
protected function isClosingIsolatedDiffTag($item, $currentIsolatedDiffTag = null) |
|
|
|
|
204
|
|
|
{ |
205
|
|
|
$tagsToMatch = $currentIsolatedDiffTag !== null |
206
|
11 |
|
? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag)) |
207
|
11 |
|
: $this->config->getIsolatedDiffTags(); |
208
|
11 |
|
foreach ($tagsToMatch as $key => $value) { |
209
|
11 |
|
if (preg_match("#</".$key."[^>]*>\\s*#iU", $item)) { |
210
|
10 |
|
return $key; |
211
|
|
|
} |
212
|
11 |
|
} |
213
|
|
|
|
214
|
11 |
|
return false; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* @param Operation $operation |
219
|
|
|
*/ |
220
|
11 |
|
protected function performOperation($operation) |
221
|
|
|
{ |
222
|
11 |
|
switch ($operation->action) { |
223
|
11 |
|
case 'equal' : |
|
|
|
|
224
|
11 |
|
$this->processEqualOperation( $operation ); |
225
|
11 |
|
break; |
226
|
9 |
|
case 'delete' : |
|
|
|
|
227
|
5 |
|
$this->processDeleteOperation( $operation, "diffdel" ); |
228
|
5 |
|
break; |
229
|
9 |
|
case 'insert' : |
|
|
|
|
230
|
8 |
|
$this->processInsertOperation( $operation, "diffins"); |
231
|
8 |
|
break; |
232
|
7 |
|
case 'replace': |
233
|
7 |
|
$this->processReplaceOperation( $operation ); |
234
|
7 |
|
break; |
235
|
|
|
default: |
236
|
|
|
break; |
237
|
11 |
|
} |
238
|
11 |
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* @param Operation $operation |
242
|
|
|
*/ |
243
|
7 |
|
protected function processReplaceOperation($operation) |
244
|
|
|
{ |
245
|
7 |
|
$this->processDeleteOperation( $operation, "diffmod" ); |
246
|
7 |
|
$this->processInsertOperation( $operation, "diffmod" ); |
247
|
7 |
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* @param Operation $operation |
251
|
|
|
* @param string $cssClass |
252
|
|
|
*/ |
253
|
9 |
View Code Duplication |
protected function processInsertOperation($operation, $cssClass) |
|
|
|
|
254
|
|
|
{ |
255
|
9 |
|
$text = array(); |
256
|
9 |
|
foreach ($this->newWords as $pos => $s) { |
257
|
9 |
|
if ($pos >= $operation->startInNew && $pos < $operation->endInNew) { |
258
|
9 |
|
if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) { |
259
|
4 |
|
foreach ($this->newIsolatedDiffTags[$pos] as $word) { |
260
|
4 |
|
$text[] = $word; |
261
|
4 |
|
} |
262
|
4 |
|
} else { |
263
|
9 |
|
$text[] = $s; |
264
|
|
|
} |
265
|
9 |
|
} |
266
|
9 |
|
} |
267
|
9 |
|
$this->insertTag( "ins", $cssClass, $text ); |
268
|
9 |
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* @param Operation $operation |
272
|
|
|
* @param string $cssClass |
273
|
|
|
*/ |
274
|
9 |
View Code Duplication |
protected function processDeleteOperation($operation, $cssClass) |
|
|
|
|
275
|
|
|
{ |
276
|
9 |
|
$text = array(); |
277
|
9 |
|
foreach ($this->oldWords as $pos => $s) { |
278
|
9 |
|
if ($pos >= $operation->startInOld && $pos < $operation->endInOld) { |
279
|
9 |
|
if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->oldIsolatedDiffTags[$pos])) { |
280
|
6 |
|
foreach ($this->oldIsolatedDiffTags[$pos] as $word) { |
281
|
6 |
|
$text[] = $word; |
282
|
6 |
|
} |
283
|
6 |
|
} else { |
284
|
8 |
|
$text[] = $s; |
285
|
|
|
} |
286
|
9 |
|
} |
287
|
9 |
|
} |
288
|
9 |
|
$this->insertTag( "del", $cssClass, $text ); |
289
|
9 |
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* @param Operation $operation |
293
|
|
|
* @param int $pos |
294
|
|
|
* @param string $placeholder |
295
|
|
|
* @param bool $stripWrappingTags |
296
|
|
|
* |
297
|
|
|
* @return string |
|
|
|
|
298
|
|
|
*/ |
299
|
7 |
|
protected function diffIsolatedPlaceholder($operation, $pos, $placeholder, $stripWrappingTags = true) |
300
|
|
|
{ |
301
|
7 |
|
$oldText = implode("", $this->findIsolatedDiffTagsInOld($operation, $pos)); |
302
|
7 |
|
$newText = implode("", $this->newIsolatedDiffTags[$pos]); |
303
|
|
|
|
304
|
7 |
|
if ($this->isListPlaceholder($placeholder)) { |
305
|
4 |
|
return $this->diffList($oldText, $newText); |
306
|
5 |
|
} elseif ($this->config->isUseTableDiffing() && $this->isTablePlaceholder($placeholder)) { |
307
|
|
|
return $this->diffTables($oldText, $newText); |
308
|
5 |
|
} elseif ($this->isLinkPlaceholder($placeholder)) { |
309
|
1 |
|
return $this->diffLinks($oldText, $newText); |
310
|
|
|
} |
311
|
|
|
|
312
|
4 |
|
return $this->diffElements($oldText, $newText, $stripWrappingTags); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* @param string $oldText |
317
|
|
|
* @param string $newText |
318
|
|
|
* @param bool $stripWrappingTags |
319
|
|
|
* |
320
|
|
|
* @return string |
321
|
|
|
*/ |
322
|
5 |
|
protected function diffElements($oldText, $newText, $stripWrappingTags = true) |
323
|
|
|
{ |
324
|
5 |
|
$wrapStart = ''; |
325
|
5 |
|
$wrapEnd = ''; |
326
|
|
|
|
327
|
5 |
|
if ($stripWrappingTags) { |
328
|
5 |
|
$pattern = '/(^<[^>]+>)|(<\/[^>]+>$)/i'; |
329
|
5 |
|
$matches = array(); |
330
|
|
|
|
331
|
5 |
|
if (preg_match_all($pattern, $newText, $matches)) { |
332
|
5 |
|
$wrapStart = isset($matches[0][0]) ? $matches[0][0] : ''; |
333
|
5 |
|
$wrapEnd = isset($matches[0][1]) ? $matches[0][1] : ''; |
334
|
5 |
|
} |
335
|
5 |
|
$oldText = preg_replace($pattern, '', $oldText); |
336
|
5 |
|
$newText = preg_replace($pattern, '', $newText); |
337
|
5 |
|
} |
338
|
|
|
|
339
|
5 |
|
$diff = HtmlDiff::create($oldText, $newText, $this->config); |
340
|
|
|
|
341
|
5 |
|
return $wrapStart . $diff->build() . $wrapEnd; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* @param string $oldText |
346
|
|
|
* @param string $newText |
347
|
|
|
* |
348
|
|
|
* @return string |
|
|
|
|
349
|
|
|
*/ |
350
|
4 |
|
protected function diffList($oldText, $newText) |
351
|
|
|
{ |
352
|
4 |
|
$diff = ListDiffNew::create($oldText, $newText, $this->config); |
353
|
|
|
|
354
|
4 |
|
return $diff->build(); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* @param string $oldText |
359
|
|
|
* @param string $newText |
360
|
|
|
* |
361
|
|
|
* @return string |
362
|
|
|
*/ |
363
|
|
|
protected function diffTables($oldText, $newText) |
364
|
|
|
{ |
365
|
|
|
$diff = TableDiff::create($oldText, $newText, $this->config); |
366
|
|
|
|
367
|
|
|
return $diff->build(); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* @param string $oldText |
372
|
|
|
* @param string $newText |
373
|
|
|
* |
374
|
|
|
* @return string |
375
|
|
|
*/ |
376
|
1 |
|
protected function diffLinks($oldText, $newText) |
377
|
|
|
{ |
378
|
1 |
|
$oldHref = $this->getAttributeFromTag($oldText, 'href'); |
379
|
1 |
|
$newHref = $this->getAttributeFromTag($newText, 'href'); |
380
|
|
|
|
381
|
1 |
|
if ($oldHref != $newHref) { |
382
|
1 |
|
return sprintf( |
383
|
1 |
|
'%s%s', |
384
|
1 |
|
$this->wrapText($oldText, 'del', 'diffmod diff-href'), |
385
|
1 |
|
$this->wrapText($newText, 'ins', 'diffmod diff-href') |
386
|
1 |
|
); |
387
|
|
|
} |
388
|
|
|
|
389
|
1 |
|
return $this->diffElements($oldText, $newText); |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* @param Operation $operation |
394
|
|
|
*/ |
395
|
11 |
View Code Duplication |
protected function processEqualOperation($operation) |
|
|
|
|
396
|
|
|
{ |
397
|
11 |
|
$result = array(); |
398
|
11 |
|
foreach ($this->newWords as $pos => $s) { |
399
|
|
|
|
400
|
11 |
|
if ($pos >= $operation->startInNew && $pos < $operation->endInNew) { |
401
|
11 |
|
if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) { |
402
|
|
|
|
403
|
7 |
|
$result[] = $this->diffIsolatedPlaceholder($operation, $pos, $s); |
404
|
7 |
|
} else { |
405
|
11 |
|
$result[] = $s; |
406
|
|
|
} |
407
|
11 |
|
} |
408
|
11 |
|
} |
409
|
11 |
|
$this->content .= implode( "", $result ); |
410
|
11 |
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* @param string $text |
414
|
|
|
* @param string $attribute |
415
|
|
|
* |
416
|
|
|
* @return null|string |
417
|
|
|
*/ |
418
|
1 |
|
protected function getAttributeFromTag($text, $attribute) |
419
|
|
|
{ |
420
|
1 |
|
$matches = array(); |
421
|
1 |
|
if (preg_match(sprintf('/<a\s+[^>]*%s=([\'"])(.*)\1[^>]*>/i', $attribute), $text, $matches)) { |
422
|
1 |
|
return $matches[2]; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
return null; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* @param string $text |
430
|
|
|
* |
431
|
|
|
* @return bool |
432
|
|
|
*/ |
433
|
7 |
|
protected function isListPlaceholder($text) |
434
|
|
|
{ |
435
|
7 |
|
return $this->isPlaceholderType($text, array('ol', 'dl', 'ul')); |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** |
439
|
|
|
* @param string $text |
440
|
|
|
* |
441
|
|
|
* @return bool |
442
|
|
|
*/ |
443
|
5 |
|
public function isLinkPlaceholder($text) |
444
|
|
|
{ |
445
|
5 |
|
return $this->isPlaceholderType($text, 'a'); |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
/** |
449
|
|
|
* @param string $text |
450
|
|
|
* @param array|string $types |
451
|
|
|
* @param bool $strict |
452
|
|
|
* |
453
|
|
|
* @return bool |
454
|
|
|
*/ |
455
|
7 |
|
protected function isPlaceholderType($text, $types, $strict = true) |
456
|
|
|
{ |
457
|
7 |
|
if (!is_array($types)) { |
458
|
5 |
|
$types = array($types); |
459
|
5 |
|
} |
460
|
|
|
|
461
|
7 |
|
$criteria = array(); |
462
|
7 |
|
foreach ($types as $type) { |
463
|
7 |
|
if ($this->config->isIsolatedDiffTag($type)) { |
464
|
7 |
|
$criteria[] = $this->config->getIsolatedDiffTagPlaceholder($type); |
465
|
7 |
|
} else { |
466
|
|
|
$criteria[] = $type; |
467
|
|
|
} |
468
|
7 |
|
} |
469
|
|
|
|
470
|
7 |
|
return in_array($text, $criteria, $strict); |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* @param string $text |
475
|
|
|
* |
476
|
|
|
* @return bool |
477
|
|
|
*/ |
478
|
5 |
|
protected function isTablePlaceholder($text) |
479
|
|
|
{ |
480
|
5 |
|
return $this->isPlaceholderType($text, 'table'); |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
/** |
484
|
|
|
* @param Operation $operation |
485
|
|
|
* @param int $posInNew |
486
|
|
|
* |
487
|
|
|
* @return array |
488
|
|
|
*/ |
489
|
7 |
|
protected function findIsolatedDiffTagsInOld($operation, $posInNew) |
490
|
|
|
{ |
491
|
7 |
|
$offset = $posInNew - $operation->startInNew; |
492
|
|
|
|
493
|
7 |
|
return $this->oldIsolatedDiffTags[$operation->startInOld + $offset]; |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
/** |
497
|
|
|
* @param string $tag |
498
|
|
|
* @param string $cssClass |
499
|
|
|
* @param array $words |
500
|
|
|
*/ |
501
|
9 |
|
protected function insertTag($tag, $cssClass, &$words) |
502
|
|
|
{ |
503
|
9 |
|
while (true) { |
504
|
9 |
|
if ( count( $words ) == 0 ) { |
505
|
9 |
|
break; |
506
|
|
|
} |
507
|
|
|
|
508
|
9 |
|
$nonTags = $this->extractConsecutiveWords( $words, 'noTag' ); |
509
|
|
|
|
510
|
9 |
|
$specialCaseTagInjection = ''; |
511
|
9 |
|
$specialCaseTagInjectionIsBefore = false; |
512
|
|
|
|
513
|
9 |
|
if ( count( $nonTags ) != 0 ) { |
514
|
9 |
|
$text = $this->wrapText( implode( "", $nonTags ), $tag, $cssClass ); |
515
|
9 |
|
$this->content .= $text; |
516
|
9 |
|
} else { |
517
|
6 |
|
$firstOrDefault = false; |
518
|
6 |
|
foreach ($this->config->getSpecialCaseOpeningTags() as $x) { |
519
|
|
|
if ( preg_match( $x, $words[ 0 ] ) ) { |
520
|
|
|
$firstOrDefault = $x; |
521
|
|
|
break; |
522
|
|
|
} |
523
|
6 |
|
} |
524
|
6 |
|
if ($firstOrDefault) { |
525
|
|
|
$specialCaseTagInjection = '<ins class="mod">'; |
526
|
|
|
if ($tag == "del") { |
527
|
|
|
unset( $words[ 0 ] ); |
528
|
|
|
} |
529
|
6 |
|
} elseif ( array_search( $words[ 0 ], $this->config->getSpecialCaseClosingTags()) !== false ) { |
530
|
|
|
$specialCaseTagInjection = "</ins>"; |
531
|
|
|
$specialCaseTagInjectionIsBefore = true; |
532
|
|
|
if ($tag == "del") { |
533
|
|
|
unset( $words[ 0 ] ); |
534
|
|
|
} |
535
|
|
|
} |
536
|
|
|
} |
537
|
9 |
|
if ( count( $words ) == 0 && count( $specialCaseTagInjection ) == 0 ) { |
538
|
|
|
break; |
539
|
|
|
} |
540
|
9 |
|
if ($specialCaseTagInjectionIsBefore) { |
541
|
|
|
$this->content .= $specialCaseTagInjection . implode( "", $this->extractConsecutiveWords( $words, 'tag' ) ); |
|
|
|
|
542
|
|
|
} else { |
543
|
9 |
|
$workTag = $this->extractConsecutiveWords( $words, 'tag' ); |
544
|
9 |
|
if ( isset( $workTag[ 0 ] ) && $this->isOpeningTag( $workTag[ 0 ] ) && !$this->isClosingTag( $workTag[ 0 ] ) ) { |
|
|
|
|
545
|
8 |
|
if ( strpos( $workTag[ 0 ], 'class=' ) ) { |
546
|
4 |
|
$workTag[ 0 ] = str_replace( 'class="', 'class="diffmod ', $workTag[ 0 ] ); |
547
|
4 |
|
$workTag[ 0 ] = str_replace( "class='", 'class="diffmod ', $workTag[ 0 ] ); |
548
|
4 |
|
} else { |
549
|
8 |
|
$workTag[ 0 ] = str_replace( ">", ' class="diffmod">', $workTag[ 0 ] ); |
550
|
|
|
} |
551
|
8 |
|
} |
552
|
9 |
|
$this->content .= implode( "", $workTag ) . $specialCaseTagInjection; |
553
|
|
|
} |
554
|
9 |
|
} |
555
|
9 |
|
} |
556
|
|
|
|
557
|
|
|
/** |
558
|
|
|
* @param string $word |
559
|
|
|
* @param string $condition |
560
|
|
|
* |
561
|
|
|
* @return bool |
562
|
|
|
*/ |
563
|
9 |
|
protected function checkCondition($word, $condition) |
564
|
|
|
{ |
565
|
9 |
|
return $condition == 'tag' ? $this->isTag( $word ) : !$this->isTag( $word ); |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
/** |
569
|
|
|
* @param string $text |
570
|
|
|
* @param string $tagName |
571
|
|
|
* @param string $cssClass |
572
|
|
|
* |
573
|
|
|
* @return string |
574
|
|
|
*/ |
575
|
10 |
|
protected function wrapText($text, $tagName, $cssClass) |
576
|
|
|
{ |
577
|
10 |
|
return sprintf( '<%1$s class="%2$s">%3$s</%1$s>', $tagName, $cssClass, $text ); |
578
|
|
|
} |
579
|
|
|
|
580
|
|
|
/** |
581
|
|
|
* @param array $words |
582
|
|
|
* @param string $condition |
583
|
|
|
* |
584
|
|
|
* @return array |
585
|
|
|
*/ |
586
|
9 |
|
protected function extractConsecutiveWords(&$words, $condition) |
587
|
|
|
{ |
588
|
9 |
|
$indexOfFirstTag = null; |
589
|
9 |
|
$words = array_values($words); |
590
|
9 |
|
foreach ($words as $i => $word) { |
591
|
9 |
|
if ( !$this->checkCondition( $word, $condition ) ) { |
592
|
8 |
|
$indexOfFirstTag = $i; |
593
|
8 |
|
break; |
594
|
|
|
} |
595
|
9 |
|
} |
596
|
9 |
|
if ($indexOfFirstTag !== null) { |
597
|
8 |
|
$items = array(); |
598
|
8 |
View Code Duplication |
foreach ($words as $pos => $s) { |
|
|
|
|
599
|
8 |
|
if ($pos >= 0 && $pos < $indexOfFirstTag) { |
600
|
8 |
|
$items[] = $s; |
601
|
8 |
|
} |
602
|
8 |
|
} |
603
|
8 |
|
if ($indexOfFirstTag > 0) { |
604
|
8 |
|
array_splice( $words, 0, $indexOfFirstTag ); |
605
|
8 |
|
} |
606
|
|
|
|
607
|
8 |
|
return $items; |
608
|
|
|
} else { |
609
|
9 |
|
$items = array(); |
610
|
9 |
View Code Duplication |
foreach ($words as $pos => $s) { |
|
|
|
|
611
|
9 |
|
if ( $pos >= 0 && $pos <= count( $words ) ) { |
612
|
9 |
|
$items[] = $s; |
613
|
9 |
|
} |
614
|
9 |
|
} |
615
|
9 |
|
array_splice( $words, 0, count( $words ) ); |
616
|
|
|
|
617
|
9 |
|
return $items; |
618
|
|
|
} |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
/** |
622
|
|
|
* @param string $item |
623
|
|
|
* |
624
|
|
|
* @return bool |
625
|
|
|
*/ |
626
|
11 |
|
protected function isTag($item) |
627
|
|
|
{ |
628
|
11 |
|
return $this->isOpeningTag( $item ) || $this->isClosingTag( $item ); |
629
|
|
|
} |
630
|
|
|
|
631
|
|
|
/** |
632
|
|
|
* @param string $item |
633
|
|
|
* |
634
|
|
|
* @return bool |
|
|
|
|
635
|
|
|
*/ |
636
|
11 |
|
protected function isOpeningTag($item) |
637
|
|
|
{ |
638
|
11 |
|
return preg_match( "#<[^>]+>\\s*#iU", $item ); |
639
|
|
|
} |
640
|
|
|
|
641
|
|
|
/** |
642
|
|
|
* @param string $item |
643
|
|
|
* |
644
|
|
|
* @return bool |
|
|
|
|
645
|
|
|
*/ |
646
|
11 |
|
protected function isClosingTag($item) |
647
|
|
|
{ |
648
|
11 |
|
return preg_match( "#</[^>]+>\\s*#iU", $item ); |
649
|
|
|
} |
650
|
|
|
|
651
|
|
|
/** |
652
|
|
|
* @return Operation[] |
653
|
|
|
*/ |
654
|
11 |
|
protected function operations() |
655
|
|
|
{ |
656
|
11 |
|
$positionInOld = 0; |
657
|
11 |
|
$positionInNew = 0; |
658
|
11 |
|
$operations = array(); |
659
|
11 |
|
$matches = $this->matchingBlocks(); |
660
|
11 |
|
$matches[] = new Match( count( $this->oldWords ), count( $this->newWords ), 0 ); |
661
|
11 |
|
foreach ($matches as $i => $match) { |
662
|
11 |
|
$matchStartsAtCurrentPositionInOld = ( $positionInOld == $match->startInOld ); |
663
|
11 |
|
$matchStartsAtCurrentPositionInNew = ( $positionInNew == $match->startInNew ); |
664
|
11 |
|
$action = 'none'; |
|
|
|
|
665
|
|
|
|
666
|
11 |
|
if ($matchStartsAtCurrentPositionInOld == false && $matchStartsAtCurrentPositionInNew == false) { |
|
|
|
|
667
|
7 |
|
$action = 'replace'; |
668
|
11 |
|
} elseif ($matchStartsAtCurrentPositionInOld == true && $matchStartsAtCurrentPositionInNew == false) { |
|
|
|
|
669
|
8 |
|
$action = 'insert'; |
670
|
11 |
|
} elseif ($matchStartsAtCurrentPositionInOld == false && $matchStartsAtCurrentPositionInNew == true) { |
|
|
|
|
671
|
5 |
|
$action = 'delete'; |
672
|
5 |
|
} else { // This occurs if the first few words are the same in both versions |
673
|
11 |
|
$action = 'none'; |
674
|
|
|
} |
675
|
11 |
|
if ($action != 'none') { |
676
|
9 |
|
$operations[] = new Operation( $action, $positionInOld, $match->startInOld, $positionInNew, $match->startInNew ); |
|
|
|
|
677
|
9 |
|
} |
678
|
11 |
|
if ( count( $match ) != 0 ) { |
679
|
11 |
|
$operations[] = new Operation( 'equal', $match->startInOld, $match->endInOld(), $match->startInNew, $match->endInNew() ); |
|
|
|
|
680
|
11 |
|
} |
681
|
11 |
|
$positionInOld = $match->endInOld(); |
682
|
11 |
|
$positionInNew = $match->endInNew(); |
683
|
11 |
|
} |
684
|
|
|
|
685
|
11 |
|
return $operations; |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
/** |
689
|
|
|
* @return Match[] |
690
|
|
|
*/ |
691
|
11 |
|
protected function matchingBlocks() |
692
|
|
|
{ |
693
|
11 |
|
$matchingBlocks = array(); |
694
|
11 |
|
$this->findMatchingBlocks( 0, count( $this->oldWords ), 0, count( $this->newWords ), $matchingBlocks ); |
695
|
|
|
|
696
|
11 |
|
return $matchingBlocks; |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
/** |
700
|
|
|
* @param int $startInOld |
701
|
|
|
* @param int $endInOld |
702
|
|
|
* @param int $startInNew |
703
|
|
|
* @param int $endInNew |
704
|
|
|
* @param array $matchingBlocks |
705
|
|
|
*/ |
706
|
11 |
|
protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks) |
707
|
|
|
{ |
708
|
11 |
|
$match = $this->findMatch( $startInOld, $endInOld, $startInNew, $endInNew ); |
709
|
11 |
|
if ($match !== null) { |
710
|
11 |
|
if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) { |
711
|
8 |
|
$this->findMatchingBlocks( $startInOld, $match->startInOld, $startInNew, $match->startInNew, $matchingBlocks ); |
|
|
|
|
712
|
8 |
|
} |
713
|
11 |
|
$matchingBlocks[] = $match; |
714
|
11 |
|
if ( $match->endInOld() < $endInOld && $match->endInNew() < $endInNew ) { |
715
|
9 |
|
$this->findMatchingBlocks( $match->endInOld(), $endInOld, $match->endInNew(), $endInNew, $matchingBlocks ); |
|
|
|
|
716
|
9 |
|
} |
717
|
11 |
|
} |
718
|
11 |
|
} |
719
|
|
|
|
720
|
|
|
/** |
721
|
|
|
* @param string $word |
722
|
|
|
* |
723
|
|
|
* @return string |
724
|
|
|
*/ |
725
|
8 |
|
protected function stripTagAttributes($word) |
726
|
|
|
{ |
727
|
8 |
|
$word = explode( ' ', trim( $word, '<>' ) ); |
728
|
|
|
|
729
|
8 |
|
return '<' . $word[ 0 ] . '>'; |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
/** |
733
|
|
|
* @param int $startInOld |
734
|
|
|
* @param int $endInOld |
735
|
|
|
* @param int $startInNew |
736
|
|
|
* @param int $endInNew |
737
|
|
|
* |
738
|
|
|
* @return Match|null |
739
|
|
|
*/ |
740
|
11 |
|
protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew) |
741
|
|
|
{ |
742
|
11 |
|
$bestMatchInOld = $startInOld; |
743
|
11 |
|
$bestMatchInNew = $startInNew; |
744
|
11 |
|
$bestMatchSize = 0; |
745
|
11 |
|
$matchLengthAt = array(); |
746
|
11 |
|
for ($indexInOld = $startInOld; $indexInOld < $endInOld; $indexInOld++) { |
747
|
11 |
|
$newMatchLengthAt = array(); |
748
|
11 |
|
$index = $this->oldWords[ $indexInOld ]; |
749
|
11 |
|
if ( $this->isTag( $index ) ) { |
750
|
6 |
|
$index = $this->stripTagAttributes( $index ); |
751
|
6 |
|
} |
752
|
11 |
|
if ( !isset( $this->wordIndices[ $index ] ) ) { |
753
|
9 |
|
$matchLengthAt = $newMatchLengthAt; |
754
|
9 |
|
continue; |
755
|
|
|
} |
756
|
11 |
|
foreach ($this->wordIndices[ $index ] as $indexInNew) { |
757
|
11 |
|
if ($indexInNew < $startInNew) { |
758
|
9 |
|
continue; |
759
|
|
|
} |
760
|
11 |
|
if ($indexInNew >= $endInNew) { |
761
|
8 |
|
break; |
762
|
|
|
} |
763
|
11 |
|
$newMatchLength = ( isset( $matchLengthAt[ $indexInNew - 1 ] ) ? $matchLengthAt[ $indexInNew - 1 ] : 0 ) + 1; |
|
|
|
|
764
|
11 |
|
$newMatchLengthAt[ $indexInNew ] = $newMatchLength; |
765
|
11 |
|
if ($newMatchLength > $bestMatchSize || |
766
|
|
|
( |
767
|
11 |
|
$this->isGroupDiffs() && |
|
|
|
|
768
|
11 |
|
$bestMatchSize > 0 && |
769
|
11 |
|
preg_match( |
770
|
11 |
|
'/^\s+$/', |
771
|
11 |
|
implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize)) |
772
|
11 |
|
) |
773
|
11 |
|
) |
774
|
11 |
|
) { |
775
|
11 |
|
$bestMatchInOld = $indexInOld - $newMatchLength + 1; |
776
|
11 |
|
$bestMatchInNew = $indexInNew - $newMatchLength + 1; |
777
|
11 |
|
$bestMatchSize = $newMatchLength; |
778
|
11 |
|
} |
779
|
11 |
|
} |
780
|
11 |
|
$matchLengthAt = $newMatchLengthAt; |
781
|
11 |
|
} |
782
|
|
|
|
783
|
|
|
// Skip match if none found or match consists only of whitespace |
784
|
11 |
|
if ($bestMatchSize != 0 && |
785
|
|
|
( |
786
|
11 |
|
!$this->isGroupDiffs() || |
|
|
|
|
787
|
11 |
|
!preg_match('/^\s+$/', implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize))) |
788
|
11 |
|
) |
789
|
11 |
|
) { |
790
|
11 |
|
return new Match($bestMatchInOld, $bestMatchInNew, $bestMatchSize); |
791
|
|
|
} |
792
|
|
|
|
793
|
7 |
|
return null; |
794
|
|
|
} |
795
|
|
|
} |
796
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.