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