These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Caxy\HtmlDiff; |
||
4 | |||
5 | use Caxy\HtmlDiff\Table\TableDiff; |
||
6 | |||
7 | /** |
||
8 | * Class HtmlDiff. |
||
9 | */ |
||
10 | class HtmlDiff extends AbstractDiff |
||
11 | { |
||
12 | /** |
||
13 | * @var array |
||
14 | */ |
||
15 | protected $wordIndices; |
||
16 | /** |
||
17 | * @var array |
||
18 | */ |
||
19 | protected $oldTables; |
||
20 | /** |
||
21 | * @var array |
||
22 | */ |
||
23 | protected $newTables; |
||
24 | /** |
||
25 | * @var array |
||
26 | */ |
||
27 | protected $newIsolatedDiffTags; |
||
28 | /** |
||
29 | * @var array |
||
30 | */ |
||
31 | protected $oldIsolatedDiffTags; |
||
32 | |||
33 | /** |
||
34 | * @param string $oldText |
||
35 | * @param string $newText |
||
36 | * @param HtmlDiffConfig|null $config |
||
37 | * |
||
38 | * @return self |
||
39 | */ |
||
40 | 15 | View Code Duplication | public static function create($oldText, $newText, HtmlDiffConfig $config = null) |
0 ignored issues
–
show
|
|||
41 | { |
||
42 | 15 | $diff = new self($oldText, $newText); |
|
43 | |||
44 | 15 | if (null !== $config) { |
|
45 | 15 | $diff->setConfig($config); |
|
46 | } |
||
47 | |||
48 | 15 | return $diff; |
|
49 | } |
||
50 | |||
51 | /** |
||
52 | * @param $bool |
||
53 | * |
||
54 | * @return $this |
||
55 | * |
||
56 | * @deprecated since 0.1.0 |
||
57 | */ |
||
58 | public function setUseTableDiffing($bool) |
||
59 | { |
||
60 | $this->config->setUseTableDiffing($bool); |
||
61 | |||
62 | return $this; |
||
63 | } |
||
64 | |||
65 | /** |
||
66 | * @param bool $boolean |
||
67 | * |
||
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 bool |
||
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 | 18 | public function build() |
|
93 | { |
||
94 | 18 | $this->prepare(); |
|
95 | |||
96 | 18 | View Code Duplication | if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) { |
0 ignored issues
–
show
This code seems to be duplicated across your project.
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.
Loading history...
|
|||
97 | $this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText); |
||
98 | |||
99 | return $this->content; |
||
100 | } |
||
101 | |||
102 | // Pre-processing Optimizations |
||
103 | |||
104 | // 1. Equality |
||
105 | 18 | if ($this->oldText == $this->newText) { |
|
106 | 11 | return $this->newText; |
|
107 | } |
||
108 | |||
109 | 18 | $this->splitInputsToWords(); |
|
110 | 18 | $this->replaceIsolatedDiffTags(); |
|
111 | 18 | $this->indexNewWords(); |
|
112 | |||
113 | 18 | $operations = $this->operations(); |
|
114 | |||
115 | 18 | foreach ($operations as $item) { |
|
116 | 18 | $this->performOperation($item); |
|
117 | } |
||
118 | |||
119 | 18 | if ($this->hasDiffCache()) { |
|
120 | $this->getDiffCache()->save($this->oldText, $this->newText, $this->content); |
||
121 | } |
||
122 | |||
123 | 18 | return $this->content; |
|
124 | } |
||
125 | |||
126 | 18 | protected function indexNewWords() |
|
127 | { |
||
128 | 18 | $this->wordIndices = array(); |
|
129 | 18 | foreach ($this->newWords as $i => $word) { |
|
130 | 18 | if ($this->isTag($word)) { |
|
131 | 10 | $word = $this->stripTagAttributes($word); |
|
132 | } |
||
133 | 18 | if (isset($this->wordIndices[ $word ])) { |
|
134 | 13 | $this->wordIndices[ $word ][] = $i; |
|
135 | } else { |
||
136 | 18 | $this->wordIndices[ $word ] = array($i); |
|
137 | } |
||
138 | } |
||
139 | 18 | } |
|
140 | |||
141 | 18 | protected function replaceIsolatedDiffTags() |
|
142 | { |
||
143 | 18 | $this->oldIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->oldWords); |
|
144 | 18 | $this->newIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->newWords); |
|
145 | 18 | } |
|
146 | |||
147 | /** |
||
148 | * @param array $words |
||
149 | * |
||
150 | * @return array |
||
151 | */ |
||
152 | 18 | protected function createIsolatedDiffTagPlaceholders(&$words) |
|
153 | { |
||
154 | 18 | $openIsolatedDiffTags = 0; |
|
155 | 18 | $isolatedDiffTagIndices = array(); |
|
156 | 18 | $isolatedDiffTagStart = 0; |
|
157 | 18 | $currentIsolatedDiffTag = null; |
|
158 | 18 | foreach ($words as $index => $word) { |
|
159 | 18 | $openIsolatedDiffTag = $this->isOpeningIsolatedDiffTag($word, $currentIsolatedDiffTag); |
|
160 | 18 | if ($openIsolatedDiffTag) { |
|
161 | 16 | if ($this->isSelfClosingTag($word) || $this->stringUtil->stripos($word, '<img') !== false) { |
|
162 | View Code Duplication | if ($openIsolatedDiffTags === 0) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
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.
Loading history...
|
|||
163 | $isolatedDiffTagIndices[] = array( |
||
164 | 'start' => $index, |
||
165 | 'length' => 1, |
||
166 | 'tagType' => $openIsolatedDiffTag, |
||
167 | ); |
||
168 | $currentIsolatedDiffTag = null; |
||
169 | } |
||
170 | } else { |
||
171 | 16 | if ($openIsolatedDiffTags === 0) { |
|
172 | 16 | $isolatedDiffTagStart = $index; |
|
173 | } |
||
174 | 16 | ++$openIsolatedDiffTags; |
|
175 | 16 | $currentIsolatedDiffTag = $openIsolatedDiffTag; |
|
176 | } |
||
177 | 18 | } elseif ($openIsolatedDiffTags > 0 && $this->isClosingIsolatedDiffTag($word, $currentIsolatedDiffTag)) { |
|
178 | 16 | --$openIsolatedDiffTags; |
|
179 | 16 | View Code Duplication | if ($openIsolatedDiffTags == 0) { |
0 ignored issues
–
show
This code seems to be duplicated across your project.
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.
Loading history...
|
|||
180 | 16 | $isolatedDiffTagIndices[] = array('start' => $isolatedDiffTagStart, 'length' => $index - $isolatedDiffTagStart + 1, 'tagType' => $currentIsolatedDiffTag); |
|
181 | 16 | $currentIsolatedDiffTag = null; |
|
182 | } |
||
183 | } |
||
184 | } |
||
185 | 18 | $isolatedDiffTagScript = array(); |
|
186 | 18 | $offset = 0; |
|
187 | 18 | foreach ($isolatedDiffTagIndices as $isolatedDiffTagIndex) { |
|
188 | 16 | $start = $isolatedDiffTagIndex['start'] - $offset; |
|
189 | 16 | $placeholderString = $this->config->getIsolatedDiffTagPlaceholder($isolatedDiffTagIndex['tagType']); |
|
190 | 16 | $isolatedDiffTagScript[$start] = array_splice($words, $start, $isolatedDiffTagIndex['length'], $placeholderString); |
|
191 | 16 | $offset += $isolatedDiffTagIndex['length'] - 1; |
|
192 | } |
||
193 | |||
194 | 18 | return $isolatedDiffTagScript; |
|
195 | } |
||
196 | |||
197 | /** |
||
198 | * @param string $item |
||
199 | * @param null|string $currentIsolatedDiffTag |
||
200 | * |
||
201 | * @return false|string |
||
202 | */ |
||
203 | 18 | View Code Duplication | protected function isOpeningIsolatedDiffTag($item, $currentIsolatedDiffTag = null) |
0 ignored issues
–
show
This method seems to be duplicated in your project.
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.
Loading history...
|
|||
204 | { |
||
205 | 18 | $tagsToMatch = $currentIsolatedDiffTag !== null |
|
206 | 16 | ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag)) |
|
207 | 18 | : $this->config->getIsolatedDiffTags(); |
|
208 | 18 | $pattern = '#<%s(\s+[^>]*)?>#iUu'; |
|
209 | 18 | foreach ($tagsToMatch as $key => $value) { |
|
210 | 18 | if (preg_match(sprintf($pattern, $key), $item)) { |
|
211 | 16 | return $key; |
|
212 | } |
||
213 | } |
||
214 | |||
215 | 18 | return false; |
|
216 | } |
||
217 | |||
218 | 16 | protected function isSelfClosingTag($text) |
|
219 | { |
||
220 | 16 | return (bool) preg_match('/<[^>]+\/\s*>/u', $text); |
|
221 | } |
||
222 | |||
223 | /** |
||
224 | * @param string $item |
||
225 | * @param null|string $currentIsolatedDiffTag |
||
226 | * |
||
227 | * @return false|string |
||
228 | */ |
||
229 | 16 | View Code Duplication | protected function isClosingIsolatedDiffTag($item, $currentIsolatedDiffTag = null) |
0 ignored issues
–
show
This method seems to be duplicated in your project.
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.
Loading history...
|
|||
230 | { |
||
231 | 16 | $tagsToMatch = $currentIsolatedDiffTag !== null |
|
232 | 16 | ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag)) |
|
233 | 16 | : $this->config->getIsolatedDiffTags(); |
|
234 | 16 | $pattern = '#</%s(\s+[^>]*)?>#iUu'; |
|
235 | 16 | foreach ($tagsToMatch as $key => $value) { |
|
236 | 16 | if (preg_match(sprintf($pattern, $key), $item)) { |
|
237 | 16 | return $key; |
|
238 | } |
||
239 | } |
||
240 | |||
241 | 16 | return false; |
|
242 | } |
||
243 | |||
244 | /** |
||
245 | * @param Operation $operation |
||
246 | */ |
||
247 | 18 | protected function performOperation($operation) |
|
248 | { |
||
249 | 18 | switch ($operation->action) { |
|
250 | 18 | case 'equal' : |
|
251 | 18 | $this->processEqualOperation($operation); |
|
252 | 18 | break; |
|
253 | 15 | case 'delete' : |
|
254 | 4 | $this->processDeleteOperation($operation, 'diffdel'); |
|
255 | 4 | break; |
|
256 | 15 | case 'insert' : |
|
257 | 10 | $this->processInsertOperation($operation, 'diffins'); |
|
258 | 10 | break; |
|
259 | 11 | case 'replace': |
|
260 | 11 | $this->processReplaceOperation($operation); |
|
261 | 11 | break; |
|
262 | default: |
||
263 | break; |
||
264 | } |
||
265 | 18 | } |
|
266 | |||
267 | /** |
||
268 | * @param Operation $operation |
||
269 | */ |
||
270 | 11 | protected function processReplaceOperation($operation) |
|
271 | { |
||
272 | 11 | $this->processDeleteOperation($operation, 'diffmod'); |
|
273 | 11 | $this->processInsertOperation($operation, 'diffmod'); |
|
274 | 11 | } |
|
275 | |||
276 | /** |
||
277 | * @param Operation $operation |
||
278 | * @param string $cssClass |
||
279 | */ |
||
280 | 15 | View Code Duplication | protected function processInsertOperation($operation, $cssClass) |
0 ignored issues
–
show
This method seems to be duplicated in your project.
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.
Loading history...
|
|||
281 | { |
||
282 | 15 | $text = array(); |
|
283 | 15 | foreach ($this->newWords as $pos => $s) { |
|
284 | 15 | if ($pos >= $operation->startInNew && $pos < $operation->endInNew) { |
|
285 | 15 | if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) { |
|
286 | 4 | foreach ($this->newIsolatedDiffTags[$pos] as $word) { |
|
287 | 4 | $text[] = $word; |
|
288 | } |
||
289 | } else { |
||
290 | 15 | $text[] = $s; |
|
291 | } |
||
292 | } |
||
293 | } |
||
294 | |||
295 | 15 | $this->insertTag('ins', $cssClass, $text); |
|
296 | 15 | } |
|
297 | |||
298 | /** |
||
299 | * @param Operation $operation |
||
300 | * @param string $cssClass |
||
301 | */ |
||
302 | 13 | View Code Duplication | protected function processDeleteOperation($operation, $cssClass) |
0 ignored issues
–
show
This method seems to be duplicated in your project.
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.
Loading history...
|
|||
303 | { |
||
304 | 13 | $text = array(); |
|
305 | 13 | foreach ($this->oldWords as $pos => $s) { |
|
306 | 13 | if ($pos >= $operation->startInOld && $pos < $operation->endInOld) { |
|
307 | 13 | if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->oldIsolatedDiffTags[$pos])) { |
|
308 | 6 | foreach ($this->oldIsolatedDiffTags[$pos] as $word) { |
|
309 | 6 | $text[] = $word; |
|
310 | } |
||
311 | } else { |
||
312 | 13 | $text[] = $s; |
|
313 | } |
||
314 | } |
||
315 | } |
||
316 | 13 | $this->insertTag('del', $cssClass, $text); |
|
317 | 13 | } |
|
318 | |||
319 | /** |
||
320 | * @param Operation $operation |
||
321 | * @param int $pos |
||
322 | * @param string $placeholder |
||
323 | * @param bool $stripWrappingTags |
||
324 | * |
||
325 | * @return string |
||
326 | */ |
||
327 | 15 | protected function diffIsolatedPlaceholder($operation, $pos, $placeholder, $stripWrappingTags = true) |
|
328 | { |
||
329 | 15 | $oldText = implode('', $this->findIsolatedDiffTagsInOld($operation, $pos)); |
|
330 | 15 | $newText = implode('', $this->newIsolatedDiffTags[$pos]); |
|
331 | |||
332 | 15 | if ($this->isListPlaceholder($placeholder)) { |
|
333 | 8 | return $this->diffList($oldText, $newText); |
|
334 | 11 | } elseif ($this->config->isUseTableDiffing() && $this->isTablePlaceholder($placeholder)) { |
|
335 | 1 | return $this->diffTables($oldText, $newText); |
|
336 | 10 | } elseif ($this->isLinkPlaceholder($placeholder)) { |
|
337 | 1 | return $this->diffElementsByAttribute($oldText, $newText, 'href', 'a'); |
|
338 | 9 | } elseif ($this->isImagePlaceholder($placeholder)) { |
|
339 | return $this->diffElementsByAttribute($oldText, $newText, 'src', 'img'); |
||
340 | } |
||
341 | |||
342 | 9 | return $this->diffElements($oldText, $newText, $stripWrappingTags); |
|
343 | } |
||
344 | |||
345 | /** |
||
346 | * @param string $oldText |
||
347 | * @param string $newText |
||
348 | * @param bool $stripWrappingTags |
||
349 | * |
||
350 | * @return string |
||
351 | */ |
||
352 | 10 | protected function diffElements($oldText, $newText, $stripWrappingTags = true) |
|
353 | { |
||
354 | 10 | $wrapStart = ''; |
|
355 | 10 | $wrapEnd = ''; |
|
356 | |||
357 | 10 | if ($stripWrappingTags) { |
|
358 | 10 | $pattern = '/(^<[^>]+>)|(<\/[^>]+>$)/iu'; |
|
359 | 10 | $matches = array(); |
|
360 | |||
361 | 10 | if (preg_match_all($pattern, $newText, $matches)) { |
|
362 | 10 | $wrapStart = isset($matches[0][0]) ? $matches[0][0] : ''; |
|
363 | 10 | $wrapEnd = isset($matches[0][1]) ? $matches[0][1] : ''; |
|
364 | } |
||
365 | 10 | $oldText = preg_replace($pattern, '', $oldText); |
|
366 | 10 | $newText = preg_replace($pattern, '', $newText); |
|
367 | } |
||
368 | |||
369 | 10 | $diff = self::create($oldText, $newText, $this->config); |
|
370 | |||
371 | 10 | return $wrapStart.$diff->build().$wrapEnd; |
|
372 | } |
||
373 | |||
374 | /** |
||
375 | * @param string $oldText |
||
376 | * @param string $newText |
||
377 | * |
||
378 | * @return string |
||
379 | */ |
||
380 | 8 | protected function diffList($oldText, $newText) |
|
381 | { |
||
382 | 8 | $diff = ListDiffLines::create($oldText, $newText, $this->config); |
|
383 | |||
384 | 8 | return $diff->build(); |
|
385 | } |
||
386 | |||
387 | /** |
||
388 | * @param string $oldText |
||
389 | * @param string $newText |
||
390 | * |
||
391 | * @return string |
||
392 | */ |
||
393 | 1 | protected function diffTables($oldText, $newText) |
|
394 | { |
||
395 | 1 | $diff = TableDiff::create($oldText, $newText, $this->config); |
|
396 | |||
397 | 1 | return $diff->build(); |
|
398 | } |
||
399 | |||
400 | 1 | protected function diffElementsByAttribute($oldText, $newText, $attribute, $element) |
|
401 | { |
||
402 | 1 | $oldAttribute = $this->getAttributeFromTag($oldText, $attribute); |
|
403 | 1 | $newAttribute = $this->getAttributeFromTag($newText, $attribute); |
|
404 | |||
405 | 1 | if ($oldAttribute !== $newAttribute) { |
|
406 | 1 | $diffClass = sprintf('diffmod diff%s diff%s', $element, $attribute); |
|
407 | |||
408 | 1 | return sprintf( |
|
409 | 1 | '%s%s', |
|
410 | 1 | $this->wrapText($oldText, 'del', $diffClass), |
|
411 | 1 | $this->wrapText($newText, 'ins', $diffClass) |
|
412 | ); |
||
413 | } |
||
414 | |||
415 | 1 | return $this->diffElements($oldText, $newText); |
|
416 | } |
||
417 | |||
418 | /** |
||
419 | * @param Operation $operation |
||
420 | */ |
||
421 | 18 | View Code Duplication | protected function processEqualOperation($operation) |
0 ignored issues
–
show
This method seems to be duplicated in your project.
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.
Loading history...
|
|||
422 | { |
||
423 | 18 | $result = array(); |
|
424 | 18 | foreach ($this->newWords as $pos => $s) { |
|
425 | 18 | if ($pos >= $operation->startInNew && $pos < $operation->endInNew) { |
|
426 | 18 | if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) { |
|
427 | 15 | $result[] = $this->diffIsolatedPlaceholder($operation, $pos, $s); |
|
428 | } else { |
||
429 | 14 | $result[] = $s; |
|
430 | } |
||
431 | } |
||
432 | } |
||
433 | 18 | $this->content .= implode('', $result); |
|
434 | 18 | } |
|
435 | |||
436 | /** |
||
437 | * @param string $text |
||
438 | * @param string $attribute |
||
439 | * |
||
440 | * @return null|string |
||
441 | */ |
||
442 | 1 | protected function getAttributeFromTag($text, $attribute) |
|
443 | { |
||
444 | 1 | $matches = array(); |
|
445 | 1 | if (preg_match(sprintf('/<[^>]*\b%s\s*=\s*([\'"])(.*)\1[^>]*>/iu', $attribute), $text, $matches)) { |
|
446 | 1 | return htmlspecialchars_decode($matches[2]); |
|
447 | } |
||
448 | |||
449 | return; |
||
450 | } |
||
451 | |||
452 | /** |
||
453 | * @param string $text |
||
454 | * |
||
455 | * @return bool |
||
456 | */ |
||
457 | 15 | protected function isListPlaceholder($text) |
|
458 | { |
||
459 | 15 | return $this->isPlaceholderType($text, array('ol', 'dl', 'ul')); |
|
460 | } |
||
461 | |||
462 | /** |
||
463 | * @param string $text |
||
464 | * |
||
465 | * @return bool |
||
466 | */ |
||
467 | 10 | public function isLinkPlaceholder($text) |
|
468 | { |
||
469 | 10 | return $this->isPlaceholderType($text, 'a'); |
|
470 | } |
||
471 | |||
472 | /** |
||
473 | * @param string $text |
||
474 | * |
||
475 | * @return bool |
||
476 | */ |
||
477 | 9 | public function isImagePlaceholder($text) |
|
478 | { |
||
479 | 9 | return $this->isPlaceholderType($text, 'img'); |
|
480 | } |
||
481 | |||
482 | /** |
||
483 | * @param string $text |
||
484 | * @param array|string $types |
||
485 | * @param bool $strict |
||
486 | * |
||
487 | * @return bool |
||
488 | */ |
||
489 | 15 | protected function isPlaceholderType($text, $types, $strict = true) |
|
490 | { |
||
491 | 15 | if (!is_array($types)) { |
|
492 | 11 | $types = array($types); |
|
493 | } |
||
494 | |||
495 | 15 | $criteria = array(); |
|
496 | 15 | foreach ($types as $type) { |
|
497 | 15 | if ($this->config->isIsolatedDiffTag($type)) { |
|
498 | 15 | $criteria[] = $this->config->getIsolatedDiffTagPlaceholder($type); |
|
499 | } else { |
||
500 | $criteria[] = $type; |
||
501 | } |
||
502 | } |
||
503 | |||
504 | 15 | return in_array($text, $criteria, $strict); |
|
505 | } |
||
506 | |||
507 | /** |
||
508 | * @param string $text |
||
509 | * |
||
510 | * @return bool |
||
511 | */ |
||
512 | 11 | protected function isTablePlaceholder($text) |
|
513 | { |
||
514 | 11 | return $this->isPlaceholderType($text, 'table'); |
|
515 | } |
||
516 | |||
517 | /** |
||
518 | * @param Operation $operation |
||
519 | * @param int $posInNew |
||
520 | * |
||
521 | * @return array |
||
522 | */ |
||
523 | 15 | protected function findIsolatedDiffTagsInOld($operation, $posInNew) |
|
524 | { |
||
525 | 15 | $offset = $posInNew - $operation->startInNew; |
|
526 | |||
527 | 15 | return $this->oldIsolatedDiffTags[$operation->startInOld + $offset]; |
|
528 | } |
||
529 | |||
530 | /** |
||
531 | * @param string $tag |
||
532 | * @param string $cssClass |
||
533 | * @param array $words |
||
534 | */ |
||
535 | 15 | protected function insertTag($tag, $cssClass, &$words) |
|
536 | { |
||
537 | 15 | while (true) { |
|
538 | 15 | if (count($words) === 0) { |
|
539 | 9 | break; |
|
540 | } |
||
541 | |||
542 | 15 | $nonTags = $this->extractConsecutiveWords($words, 'noTag'); |
|
543 | |||
544 | 15 | $specialCaseTagInjection = ''; |
|
545 | 15 | $specialCaseTagInjectionIsBefore = false; |
|
546 | |||
547 | 15 | if (count($nonTags) !== 0) { |
|
548 | 15 | $this->content .= $this->wrapText(implode('', $nonTags), $tag, $cssClass); |
|
549 | } else { |
||
550 | 7 | $firstOrDefault = false; |
|
551 | 7 | foreach ($this->config->getSpecialCaseOpeningTags() as $x) { |
|
552 | if (preg_match($x, $words[ 0 ])) { |
||
553 | $firstOrDefault = $x; |
||
554 | break; |
||
555 | } |
||
556 | } |
||
557 | 7 | if ($firstOrDefault) { |
|
558 | $specialCaseTagInjection = '<ins class="mod">'; |
||
559 | if ($tag === 'del') { |
||
560 | unset($words[ 0 ]); |
||
561 | } |
||
562 | 7 | } elseif (array_search($words[ 0 ], $this->config->getSpecialCaseClosingTags()) !== false) { |
|
563 | $specialCaseTagInjection = '</ins>'; |
||
564 | $specialCaseTagInjectionIsBefore = true; |
||
565 | if ($tag === 'del') { |
||
566 | unset($words[ 0 ]); |
||
567 | } |
||
568 | } |
||
569 | } |
||
570 | 15 | if (count($words) == 0 && $this->stringUtil->strlen($specialCaseTagInjection) == 0) { |
|
571 | 14 | break; |
|
572 | } |
||
573 | 9 | if ($specialCaseTagInjectionIsBefore) { |
|
574 | $this->content .= $specialCaseTagInjection . implode('', $this->extractConsecutiveWords($words, 'tag')); |
||
575 | } else { |
||
576 | 9 | $workTag = $this->extractConsecutiveWords($words, 'tag'); |
|
577 | |||
578 | if ( |
||
579 | 9 | isset($workTag[0]) === true && |
|
580 | 9 | $this->isOpeningTag($workTag[0]) === true && |
|
581 | 9 | $this->isClosingTag($workTag[0]) === false |
|
582 | ) { |
||
583 | 9 | if ($this->stringUtil->strpos($workTag[0], 'class=')) { |
|
584 | 2 | $workTag[0] = str_replace('class="', 'class="diffmod ', $workTag[0]); |
|
585 | } else { |
||
586 | 9 | $isSelfClosing = $this->stringUtil->strpos($workTag[0], '/>') !== false; |
|
587 | |||
588 | 9 | if ($isSelfClosing === true) { |
|
589 | 5 | $workTag[0] = str_replace('/>', ' class="diffmod" />', $workTag[0]); |
|
590 | } else { |
||
591 | 8 | $workTag[0] = str_replace('>', ' class="diffmod">', $workTag[0]); |
|
592 | } |
||
593 | } |
||
594 | } |
||
595 | |||
596 | 9 | $appendContent = implode('', $workTag) . $specialCaseTagInjection; |
|
597 | |||
598 | 9 | if (isset($workTag[0]) === true && $this->stringUtil->stripos($workTag[0], '<img') !== false) { |
|
599 | $appendContent = $this->wrapText($appendContent, $tag, $cssClass); |
||
600 | } |
||
601 | |||
602 | 9 | $this->content .= $appendContent; |
|
603 | } |
||
604 | } |
||
605 | 15 | } |
|
606 | |||
607 | /** |
||
608 | * @param string $word |
||
609 | * @param string $condition |
||
610 | * |
||
611 | * @return bool |
||
612 | */ |
||
613 | 15 | protected function checkCondition($word, $condition) |
|
614 | { |
||
615 | 15 | return $condition == 'tag' ? $this->isTag($word) : !$this->isTag($word); |
|
616 | } |
||
617 | |||
618 | 16 | protected function wrapText(string $text, string $tagName, string $cssClass) : string |
|
619 | { |
||
620 | 16 | if (trim($text) === '') { |
|
621 | 7 | return ''; |
|
622 | } |
||
623 | |||
624 | 16 | return sprintf('<%1$s class="%2$s">%3$s</%1$s>', $tagName, $cssClass, $text); |
|
625 | } |
||
626 | |||
627 | /** |
||
628 | * @param array $words |
||
629 | * @param string $condition |
||
630 | * |
||
631 | * @return array |
||
632 | */ |
||
633 | 15 | protected function extractConsecutiveWords(&$words, $condition) |
|
634 | { |
||
635 | 15 | $indexOfFirstTag = null; |
|
636 | 15 | $words = array_values($words); |
|
637 | 15 | foreach ($words as $i => $word) { |
|
638 | 15 | if (!$this->checkCondition($word, $condition)) { |
|
639 | 9 | $indexOfFirstTag = $i; |
|
640 | 9 | break; |
|
641 | } |
||
642 | } |
||
643 | 15 | if ($indexOfFirstTag !== null) { |
|
644 | 9 | $items = array(); |
|
645 | 9 | View Code Duplication | foreach ($words as $pos => $s) { |
0 ignored issues
–
show
This code seems to be duplicated across your project.
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.
Loading history...
|
|||
646 | 9 | if ($pos >= 0 && $pos < $indexOfFirstTag) { |
|
647 | 9 | $items[] = $s; |
|
648 | } |
||
649 | } |
||
650 | 9 | if ($indexOfFirstTag > 0) { |
|
651 | 9 | array_splice($words, 0, $indexOfFirstTag); |
|
652 | } |
||
653 | |||
654 | 9 | return $items; |
|
655 | } else { |
||
656 | 15 | $items = array(); |
|
657 | 15 | View Code Duplication | foreach ($words as $pos => $s) { |
0 ignored issues
–
show
This code seems to be duplicated across your project.
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.
Loading history...
|
|||
658 | 15 | if ($pos >= 0 && $pos <= count($words)) { |
|
659 | 15 | $items[] = $s; |
|
660 | } |
||
661 | } |
||
662 | 15 | array_splice($words, 0, count($words)); |
|
663 | |||
664 | 15 | return $items; |
|
665 | } |
||
666 | } |
||
667 | |||
668 | /** |
||
669 | * @param string $item |
||
670 | * |
||
671 | * @return bool |
||
672 | */ |
||
673 | 18 | protected function isTag($item) |
|
674 | { |
||
675 | 18 | return $this->isOpeningTag($item) || $this->isClosingTag($item); |
|
676 | } |
||
677 | |||
678 | 18 | protected function isOpeningTag($item) : bool |
|
679 | { |
||
680 | 18 | return preg_match('#<[^>]+>\\s*#iUu', $item) === 1; |
|
681 | } |
||
682 | |||
683 | 18 | protected function isClosingTag($item) : bool |
|
684 | { |
||
685 | 18 | return preg_match('#</[^>]+>\\s*#iUu', $item) === 1; |
|
686 | } |
||
687 | |||
688 | /** |
||
689 | * @return Operation[] |
||
690 | */ |
||
691 | 18 | protected function operations() |
|
692 | { |
||
693 | 18 | $positionInOld = 0; |
|
694 | 18 | $positionInNew = 0; |
|
695 | 18 | $operations = array(); |
|
696 | |||
697 | 18 | $matches = $this->matchingBlocks(); |
|
698 | 18 | $matches[] = new MatchingBlock(count($this->oldWords), count($this->newWords), 0); |
|
699 | |||
700 | 18 | foreach ($matches as $match) { |
|
701 | 18 | $matchStartsAtCurrentPositionInOld = ($positionInOld === $match->startInOld); |
|
702 | 18 | $matchStartsAtCurrentPositionInNew = ($positionInNew === $match->startInNew); |
|
703 | |||
704 | 18 | if ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === false) { |
|
705 | 11 | $action = 'replace'; |
|
706 | 18 | } elseif ($matchStartsAtCurrentPositionInOld === true && $matchStartsAtCurrentPositionInNew === false) { |
|
707 | 10 | $action = 'insert'; |
|
708 | 18 | } elseif ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === true) { |
|
709 | 4 | $action = 'delete'; |
|
710 | } else { // This occurs if the first few words are the same in both versions |
||
711 | 18 | $action = 'none'; |
|
712 | } |
||
713 | |||
714 | 18 | if ($action !== 'none') { |
|
715 | 15 | $operations[] = new Operation($action, $positionInOld, $match->startInOld, $positionInNew, $match->startInNew); |
|
716 | } |
||
717 | |||
718 | 18 | if (count($match) !== 0) { |
|
719 | 18 | $operations[] = new Operation('equal', $match->startInOld, $match->endInOld(), $match->startInNew, $match->endInNew()); |
|
720 | } |
||
721 | |||
722 | 18 | $positionInOld = $match->endInOld(); |
|
723 | 18 | $positionInNew = $match->endInNew(); |
|
724 | } |
||
725 | |||
726 | 18 | return $operations; |
|
727 | } |
||
728 | |||
729 | /** |
||
730 | * @return MatchingBlock[] |
||
731 | */ |
||
732 | 18 | protected function matchingBlocks() |
|
733 | { |
||
734 | 18 | $matchingBlocks = array(); |
|
735 | 18 | $this->findMatchingBlocks(0, count($this->oldWords), 0, count($this->newWords), $matchingBlocks); |
|
736 | |||
737 | 18 | return $matchingBlocks; |
|
738 | } |
||
739 | |||
740 | /** |
||
741 | * @param int $startInOld |
||
742 | * @param int $endInOld |
||
743 | * @param int $startInNew |
||
744 | * @param int $endInNew |
||
745 | * @param array $matchingBlocks |
||
746 | */ |
||
747 | 18 | protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks) |
|
748 | { |
||
749 | 18 | $match = $this->findMatch($startInOld, $endInOld, $startInNew, $endInNew); |
|
750 | |||
751 | 18 | if ($match !== null) { |
|
752 | 18 | if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) { |
|
753 | 9 | $this->findMatchingBlocks($startInOld, $match->startInOld, $startInNew, $match->startInNew, $matchingBlocks); |
|
754 | } |
||
755 | |||
756 | 18 | $matchingBlocks[] = $match; |
|
757 | |||
758 | 18 | if ($match->endInOld() < $endInOld && $match->endInNew() < $endInNew) { |
|
759 | 11 | $this->findMatchingBlocks($match->endInOld(), $endInOld, $match->endInNew(), $endInNew, $matchingBlocks); |
|
760 | } |
||
761 | } |
||
762 | 18 | } |
|
763 | |||
764 | /** |
||
765 | * @param string $word |
||
766 | * |
||
767 | * @return string |
||
768 | */ |
||
769 | 10 | protected function stripTagAttributes($word) |
|
770 | { |
||
771 | 10 | $space = $this->stringUtil->strpos($word, ' ', 1); |
|
772 | |||
773 | 10 | if ($space) { |
|
774 | 7 | return '<' . $this->stringUtil->substr($word, 1, $space) . '>'; |
|
775 | } |
||
776 | |||
777 | 7 | return trim($word, '<>'); |
|
778 | } |
||
779 | |||
780 | /** |
||
781 | * @param int $startInOld |
||
782 | * @param int $endInOld |
||
783 | * @param int $startInNew |
||
784 | * @param int $endInNew |
||
785 | * |
||
786 | * @return MatchingBlock|null |
||
787 | */ |
||
788 | 18 | protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew) |
|
789 | { |
||
790 | 18 | $groupDiffs = $this->isGroupDiffs(); |
|
791 | 18 | $bestMatchInOld = $startInOld; |
|
792 | 18 | $bestMatchInNew = $startInNew; |
|
793 | 18 | $bestMatchSize = 0; |
|
794 | 18 | $matchLengthAt = array(); |
|
795 | |||
796 | 18 | for ($indexInOld = $startInOld; $indexInOld < $endInOld; ++$indexInOld) { |
|
797 | 18 | $newMatchLengthAt = array(); |
|
798 | 18 | $index = $this->oldWords[ $indexInOld ]; |
|
799 | 18 | if ($this->isTag($index)) { |
|
800 | 7 | $index = $this->stripTagAttributes($index); |
|
801 | } |
||
802 | 18 | if (!isset($this->wordIndices[ $index ])) { |
|
803 | 13 | $matchLengthAt = $newMatchLengthAt; |
|
804 | 13 | continue; |
|
805 | } |
||
806 | 18 | foreach ($this->wordIndices[ $index ] as $indexInNew) { |
|
807 | 18 | if ($indexInNew < $startInNew) { |
|
808 | 8 | continue; |
|
809 | } |
||
810 | 18 | if ($indexInNew >= $endInNew) { |
|
811 | 9 | break; |
|
812 | } |
||
813 | |||
814 | 18 | $newMatchLength = (isset($matchLengthAt[ $indexInNew - 1 ]) ? $matchLengthAt[ $indexInNew - 1 ] : 0) + 1; |
|
815 | 18 | $newMatchLengthAt[ $indexInNew ] = $newMatchLength; |
|
816 | |||
817 | 18 | if ($newMatchLength > $bestMatchSize || |
|
818 | ( |
||
819 | 13 | $groupDiffs && |
|
820 | 13 | $bestMatchSize > 0 && |
|
821 | 18 | $this->isOnlyWhitespace($this->array_slice_cached($this->oldWords, $bestMatchInOld, $bestMatchSize)) |
|
822 | ) |
||
823 | ) { |
||
824 | 18 | $bestMatchInOld = $indexInOld - $newMatchLength + 1; |
|
825 | 18 | $bestMatchInNew = $indexInNew - $newMatchLength + 1; |
|
826 | 18 | $bestMatchSize = $newMatchLength; |
|
827 | } |
||
828 | } |
||
829 | 18 | $matchLengthAt = $newMatchLengthAt; |
|
830 | } |
||
831 | |||
832 | // Skip match if none found or match consists only of whitespace |
||
833 | 18 | if ($bestMatchSize !== 0 && |
|
834 | ( |
||
835 | 18 | !$groupDiffs || |
|
836 | 18 | !$this->isOnlyWhitespace($this->array_slice_cached($this->oldWords, $bestMatchInOld, $bestMatchSize)) |
|
837 | ) |
||
838 | ) { |
||
839 | 18 | return new MatchingBlock($bestMatchInOld, $bestMatchInNew, $bestMatchSize); |
|
840 | } |
||
841 | |||
842 | 11 | return null; |
|
843 | } |
||
844 | |||
845 | /** |
||
846 | * @param string $str |
||
847 | * |
||
848 | * @return bool |
||
849 | */ |
||
850 | 18 | protected function isOnlyWhitespace($str) |
|
851 | { |
||
852 | // Slightly faster then using preg_match |
||
853 | 18 | return $str !== '' && trim($str) === ''; |
|
854 | } |
||
855 | |||
856 | /** |
||
857 | * Special array_slice function that caches its last request. |
||
858 | * |
||
859 | * The diff algorithm seems to request the same information many times in a row. |
||
860 | * by returning the previous answer the algorithm preforms way faster. |
||
861 | * |
||
862 | * The result is a string instead of an array, this way we safe on the amount of |
||
863 | * memory intensive implode() calls. |
||
864 | * |
||
865 | * @param array &$array |
||
866 | * @param integer $offset |
||
867 | * @param integer|null $length |
||
868 | * |
||
869 | * @return string |
||
0 ignored issues
–
show
|
|||
870 | */ |
||
871 | 18 | protected function array_slice_cached(&$array, $offset, $length = null) |
|
0 ignored issues
–
show
|
|||
872 | { |
||
873 | 18 | static $lastOffset = null; |
|
874 | 18 | static $lastLength = null; |
|
875 | 18 | static $cache = null; |
|
876 | |||
877 | // PHP has no support for by-reference comparing. |
||
878 | // to prevent false positive hits, reset the cache when the oldWords or newWords is changed. |
||
879 | 18 | if ($this->resetCache === true) { |
|
880 | 18 | $cache = null; |
|
881 | |||
882 | 18 | $this->resetCache = false; |
|
883 | } |
||
884 | |||
885 | if ( |
||
886 | 18 | $cache !== null && |
|
887 | 18 | $lastLength === $length && |
|
888 | 18 | $lastOffset === $offset |
|
889 | ) { // Hit |
||
890 | 13 | return $cache; |
|
891 | } // Miss |
||
892 | |||
893 | 18 | $lastOffset = $offset; |
|
894 | 18 | $lastLength = $length; |
|
895 | |||
896 | 18 | $cache = implode('', array_slice($array, $offset, $length)); |
|
897 | |||
898 | 18 | return $cache; |
|
899 | } |
||
900 | } |
||
901 |
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.