1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Caxy\HtmlDiff; |
4
|
|
|
|
5
|
|
|
use Caxy\HtmlDiff\ListDiff\DiffList; |
6
|
|
|
use Caxy\HtmlDiff\ListDiff\DiffListItem; |
7
|
|
|
|
8
|
|
|
class ListDiff extends AbstractDiff |
9
|
|
|
{ |
10
|
|
|
protected static $listTypes = array('ul', 'ol', 'dl'); |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* @param string $oldText |
14
|
|
|
* @param string $newText |
15
|
|
|
* @param HtmlDiffConfig|null $config |
16
|
|
|
* |
17
|
|
|
* @return ListDiff |
18
|
|
|
*/ |
19
|
7 |
View Code Duplication |
public static function create($oldText, $newText, HtmlDiffConfig $config = null) |
|
|
|
|
20
|
|
|
{ |
21
|
7 |
|
$diff = new self($oldText, $newText); |
22
|
|
|
|
23
|
7 |
|
if (null !== $config) { |
24
|
7 |
|
$diff->setConfig($config); |
25
|
7 |
|
} |
26
|
|
|
|
27
|
7 |
|
return $diff; |
28
|
|
|
} |
29
|
|
|
|
30
|
7 |
View Code Duplication |
public function build() |
|
|
|
|
31
|
|
|
{ |
32
|
7 |
|
if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) { |
33
|
|
|
$this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText); |
34
|
|
|
|
35
|
|
|
return $this->content; |
36
|
|
|
} |
37
|
|
|
|
38
|
7 |
|
$this->splitInputsToWords(); |
39
|
|
|
|
40
|
7 |
|
$this->content = $this->diffLists( |
41
|
7 |
|
$this->buildDiffList($this->oldWords), |
42
|
7 |
|
$this->buildDiffList($this->newWords) |
43
|
7 |
|
); |
44
|
|
|
|
45
|
7 |
|
if ($this->hasDiffCache()) { |
46
|
|
|
$this->getDiffCache()->save($this->oldText, $this->newText, $this->content); |
47
|
|
|
} |
48
|
|
|
|
49
|
7 |
|
return $this->content; |
50
|
|
|
} |
51
|
|
|
|
52
|
7 |
|
protected function diffLists(DiffList $oldList, DiffList $newList) |
53
|
|
|
{ |
54
|
7 |
|
$oldMatchData = array(); |
55
|
7 |
|
$newMatchData = array(); |
56
|
7 |
|
$oldListIndices = array(); |
57
|
7 |
|
$newListIndices = array(); |
58
|
7 |
|
$oldListItems = array(); |
59
|
7 |
|
$newListItems = array(); |
60
|
|
|
|
61
|
7 |
|
foreach ($oldList->getListItems() as $oldIndex => $oldListItem) { |
62
|
7 |
|
if ($oldListItem instanceof DiffListItem) { |
63
|
7 |
|
$oldListItems[$oldIndex] = $oldListItem; |
64
|
|
|
|
65
|
7 |
|
$oldListIndices[] = $oldIndex; |
66
|
7 |
|
$oldMatchData[$oldIndex] = array(); |
67
|
|
|
|
68
|
|
|
// Get match percentages |
69
|
7 |
|
foreach ($newList->getListItems() as $newIndex => $newListItem) { |
70
|
7 |
|
if ($newListItem instanceof DiffListItem) { |
71
|
7 |
|
if (!in_array($newListItem, $newListItems)) { |
72
|
7 |
|
$newListItems[$newIndex] = $newListItem; |
73
|
7 |
|
} |
74
|
7 |
|
if (!in_array($newIndex, $newListIndices)) { |
75
|
7 |
|
$newListIndices[] = $newIndex; |
76
|
7 |
|
} |
77
|
7 |
|
if (!array_key_exists($newIndex, $newMatchData)) { |
78
|
7 |
|
$newMatchData[$newIndex] = array(); |
79
|
7 |
|
} |
80
|
|
|
|
81
|
7 |
|
$oldText = implode('', $oldListItem->getText()); |
82
|
7 |
|
$newText = implode('', $newListItem->getText()); |
83
|
|
|
|
84
|
|
|
// similar_text |
85
|
7 |
|
$percentage = null; |
86
|
7 |
|
similar_text($oldText, $newText, $percentage); |
87
|
|
|
|
88
|
7 |
|
$oldMatchData[$oldIndex][$newIndex] = $percentage; |
89
|
7 |
|
$newMatchData[$newIndex][$oldIndex] = $percentage; |
90
|
7 |
|
} |
91
|
7 |
|
} |
92
|
7 |
|
} |
93
|
7 |
|
} |
94
|
|
|
|
95
|
7 |
|
$currentIndexInOld = 0; |
96
|
7 |
|
$currentIndexInNew = 0; |
97
|
7 |
|
$oldCount = count($oldListIndices); |
98
|
7 |
|
$newCount = count($newListIndices); |
99
|
7 |
|
$difference = max($oldCount, $newCount) - min($oldCount, $newCount); |
100
|
|
|
|
101
|
7 |
|
$diffOutput = ''; |
102
|
|
|
|
103
|
7 |
|
foreach ($newList->getListItems() as $newIndex => $newListItem) { |
104
|
7 |
|
if ($newListItem instanceof DiffListItem) { |
105
|
7 |
|
$operation = null; |
|
|
|
|
106
|
|
|
|
107
|
7 |
|
$oldListIndex = array_key_exists($currentIndexInOld, $oldListIndices) ? $oldListIndices[$currentIndexInOld] : null; |
|
|
|
|
108
|
7 |
|
$class = 'normal'; |
|
|
|
|
109
|
|
|
|
110
|
7 |
|
if (null !== $oldListIndex && array_key_exists($oldListIndex, $oldMatchData)) { |
111
|
|
|
// Check percentage matches of upcoming list items in old. |
112
|
7 |
|
$matchPercentage = $oldMatchData[$oldListIndex][$newIndex]; |
113
|
|
|
|
114
|
|
|
// does the old list item match better? |
115
|
7 |
|
$otherMatchBetter = false; |
116
|
7 |
|
foreach ($oldMatchData[$oldListIndex] as $index => $percentage) { |
117
|
7 |
|
if ($index > $newIndex && $percentage > $matchPercentage) { |
118
|
4 |
|
$otherMatchBetter = $index; |
119
|
4 |
|
} |
120
|
7 |
|
} |
121
|
|
|
|
122
|
7 |
|
if (false !== $otherMatchBetter && $newCount > $oldCount && $difference > 0) { |
123
|
2 |
|
$diffOutput .= sprintf('%s', $newListItem->getHtml('normal new', 'ins')); |
124
|
2 |
|
++$currentIndexInNew; |
125
|
2 |
|
--$difference; |
126
|
|
|
|
127
|
2 |
|
continue; |
128
|
|
|
} |
129
|
|
|
|
130
|
7 |
|
$nextOldListIndex = array_key_exists($currentIndexInOld + 1, $oldListIndices) ? $oldListIndices[$currentIndexInOld + 1] : null; |
|
|
|
|
131
|
|
|
|
132
|
7 |
|
$replacement = false; |
133
|
|
|
|
134
|
7 |
|
if ($nextOldListIndex !== null && $oldMatchData[$nextOldListIndex][$newIndex] > $matchPercentage && $oldMatchData[$nextOldListIndex][$newIndex] > $this->config->getMatchThreshold()) { |
|
|
|
|
135
|
|
|
// Following list item in old is better match, use that. |
136
|
1 |
|
$diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del')); |
137
|
|
|
|
138
|
1 |
|
++$currentIndexInOld; |
139
|
1 |
|
$oldListIndex = $nextOldListIndex; |
140
|
1 |
|
$matchPercentage = $oldMatchData[$oldListIndex]; |
141
|
1 |
|
$replacement = true; |
142
|
1 |
|
} |
143
|
|
|
|
144
|
7 |
|
if ($matchPercentage > $this->config->getMatchThreshold() || $currentIndexInNew === $currentIndexInOld) { |
|
|
|
|
145
|
|
|
// Diff the two lists. |
146
|
7 |
|
$htmlDiff = HtmlDiff::create( |
147
|
7 |
|
$oldListItems[$oldListIndex]->getInnerHtml(), |
148
|
7 |
|
$newListItem->getInnerHtml(), |
149
|
7 |
|
$this->config |
150
|
7 |
|
); |
151
|
7 |
|
$diffContent = $htmlDiff->build(); |
152
|
|
|
|
153
|
7 |
|
$diffOutput .= sprintf('%s%s%s', $newListItem->getStartTagWithDiffClass($replacement ? 'replacement' : 'normal'), $diffContent, $newListItem->getEndTag()); |
|
|
|
|
154
|
7 |
|
} else { |
155
|
|
|
$diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del')); |
156
|
|
|
$diffOutput .= sprintf('%s', $newListItem->getHtml('replacement', 'ins')); |
157
|
|
|
} |
158
|
7 |
|
++$currentIndexInOld; |
159
|
7 |
|
} else { |
160
|
|
|
$diffOutput .= sprintf('%s', $newListItem->getHtml('normal new', 'ins')); |
161
|
|
|
} |
162
|
|
|
|
163
|
7 |
|
++$currentIndexInNew; |
164
|
7 |
|
} |
165
|
7 |
|
} |
166
|
|
|
|
167
|
|
|
// Output any additional list items |
168
|
7 |
|
while (array_key_exists($currentIndexInOld, $oldListIndices)) { |
169
|
2 |
|
$oldListIndex = $oldListIndices[$currentIndexInOld]; |
170
|
2 |
|
$diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del')); |
171
|
2 |
|
++$currentIndexInOld; |
172
|
2 |
|
} |
173
|
|
|
|
174
|
7 |
|
return sprintf('%s%s%s', $newList->getStartTagWithDiffClass(), $diffOutput, $newList->getEndTag()); |
175
|
|
|
} |
176
|
|
|
|
177
|
7 |
|
protected function buildDiffList($words) |
178
|
|
|
{ |
179
|
7 |
|
$listType = null; |
180
|
7 |
|
$listStartTag = null; |
181
|
7 |
|
$listEndTag = null; |
182
|
7 |
|
$attributes = array(); |
183
|
7 |
|
$openLists = 0; |
184
|
7 |
|
$openListItems = 0; |
185
|
7 |
|
$list = array(); |
186
|
7 |
|
$currentListItem = null; |
187
|
7 |
|
$listItemType = null; |
188
|
7 |
|
$listItemStart = null; |
189
|
7 |
|
$listItemEnd = null; |
|
|
|
|
190
|
|
|
|
191
|
7 |
|
foreach ($words as $i => $word) { |
192
|
7 |
|
if ($this->isOpeningListTag($word, $listType)) { |
193
|
7 |
|
if ($openLists > 0) { |
194
|
2 |
|
if ($openListItems > 0) { |
195
|
2 |
|
$currentListItem[] = $word; |
196
|
2 |
|
} else { |
197
|
|
|
$list[] = $word; |
198
|
|
|
} |
199
|
2 |
|
} else { |
200
|
7 |
|
$listType = substr($word, 1, 2); |
201
|
7 |
|
$listStartTag = $word; |
202
|
|
|
} |
203
|
|
|
|
204
|
7 |
|
++$openLists; |
205
|
7 |
|
} elseif ($this->isClosingListTag($word, $listType)) { |
206
|
7 |
|
if ($openLists > 1) { |
207
|
2 |
|
if ($openListItems > 0) { |
208
|
2 |
|
$currentListItem[] = $word; |
209
|
2 |
|
} else { |
210
|
|
|
$list[] = $word; |
211
|
|
|
} |
212
|
2 |
|
} else { |
213
|
7 |
|
$listEndTag = $word; |
214
|
|
|
} |
215
|
|
|
|
216
|
7 |
|
--$openLists; |
217
|
7 |
|
} elseif ($this->isOpeningListItemTag($word, $listItemType)) { |
218
|
7 |
|
if ($openListItems === 0) { |
219
|
|
|
// New top-level list item |
220
|
7 |
|
$currentListItem = array(); |
221
|
7 |
|
$listItemType = substr($word, 1, 2); |
222
|
7 |
|
$listItemStart = $word; |
223
|
7 |
|
} else { |
224
|
4 |
|
$currentListItem[] = $word; |
225
|
|
|
} |
226
|
|
|
|
227
|
7 |
|
++$openListItems; |
228
|
7 |
|
} elseif ($this->isClosingListItemTag($word, $listItemType)) { |
229
|
7 |
|
if ($openListItems === 1) { |
230
|
7 |
|
$listItemEnd = $word; |
231
|
7 |
|
$listItem = new DiffListItem($currentListItem, array(), $listItemStart, $listItemEnd); |
232
|
7 |
|
$list[] = $listItem; |
233
|
7 |
|
$currentListItem = null; |
234
|
7 |
|
} else { |
235
|
4 |
|
$currentListItem[] = $word; |
236
|
|
|
} |
237
|
|
|
|
238
|
7 |
|
--$openListItems; |
239
|
7 |
|
} else { |
240
|
7 |
|
if ($openListItems > 0) { |
241
|
7 |
|
$currentListItem[] = $word; |
242
|
7 |
|
} else { |
243
|
7 |
|
$list[] = $word; |
244
|
|
|
} |
245
|
|
|
} |
246
|
7 |
|
} |
247
|
|
|
|
248
|
7 |
|
$diffList = new DiffList($listType, $listStartTag, $listEndTag, $list, $attributes); |
249
|
|
|
|
250
|
7 |
|
return $diffList; |
251
|
|
|
} |
252
|
|
|
|
253
|
7 |
|
protected function isOpeningListTag($word, $type = null) |
254
|
|
|
{ |
255
|
7 |
|
$filter = $type !== null ? array('<'.$type) : array('<ul', '<ol', '<dl'); |
256
|
|
|
|
257
|
7 |
|
return in_array(substr($word, 0, 3), $filter); |
258
|
|
|
} |
259
|
|
|
|
260
|
7 |
|
protected function isClosingListTag($word, $type = null) |
261
|
|
|
{ |
262
|
7 |
|
$filter = $type !== null ? array('</'.$type) : array('</ul', '</ol', '</dl'); |
263
|
|
|
|
264
|
7 |
|
return in_array(substr($word, 0, 4), $filter); |
265
|
|
|
} |
266
|
|
|
|
267
|
7 |
|
protected function isOpeningListItemTag($word, $type = null) |
268
|
|
|
{ |
269
|
7 |
|
$filter = $type !== null ? array('<'.$type) : array('<li', '<dd', '<dt'); |
270
|
|
|
|
271
|
7 |
|
return in_array(substr($word, 0, 3), $filter); |
272
|
|
|
} |
273
|
|
|
|
274
|
7 |
|
protected function isClosingListItemTag($word, $type = null) |
275
|
|
|
{ |
276
|
7 |
|
$filter = $type !== null ? array('</'.$type) : array('</li', '</dd', '</dt'); |
277
|
|
|
|
278
|
7 |
|
return in_array(substr($word, 0, 4), $filter); |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
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.