1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Caxy\HtmlDiff; |
4
|
|
|
|
5
|
|
|
class ListDiff extends HtmlDiff |
6
|
|
|
{ |
7
|
|
|
/** |
8
|
|
|
* This is the minimum percentage a list item can match its counterpart in order to be considered a match. |
9
|
|
|
* @var integer |
10
|
|
|
*/ |
11
|
|
|
protected static $listMatchThreshold = 35; |
12
|
|
|
|
13
|
|
|
/** @var array */ |
14
|
|
|
protected $listWords = array(); |
15
|
|
|
|
16
|
|
|
/** @var array */ |
17
|
|
|
protected $listTags = array(); |
18
|
|
|
|
19
|
|
|
/** @var array */ |
20
|
|
|
protected $listIsolatedDiffTags = array(); |
21
|
|
|
|
22
|
|
|
/** @var array */ |
23
|
|
|
protected $isolatedDiffTags = array ( |
24
|
|
|
'ol' => '[[REPLACE_ORDERED_LIST]]', |
25
|
|
|
'ul' => '[[REPLACE_UNORDERED_LIST]]', |
26
|
|
|
'dl' => '[[REPLACE_DEFINITION_LIST]]', |
27
|
|
|
); |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* List (li) placeholder. |
31
|
|
|
* @var string |
32
|
|
|
*/ |
33
|
|
|
protected static $listPlaceHolder = "[[REPLACE_LIST_ITEM]]"; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Holds the type of list this is ol, ul, dl. |
37
|
|
|
* @var string |
38
|
|
|
*/ |
39
|
|
|
protected $listType; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Used to hold what type of list the old list is. |
43
|
|
|
* @var string |
44
|
|
|
*/ |
45
|
|
|
protected $oldListType; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Used to hold what type of list the new list is. |
49
|
|
|
* @var string |
50
|
|
|
*/ |
51
|
|
|
protected $newListType; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Hold the old/new content of the content of the list. |
55
|
|
|
* @var array |
56
|
|
|
*/ |
57
|
|
|
protected $list; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Contains the old/new child lists content within this list. |
61
|
|
|
* @var array |
62
|
|
|
*/ |
63
|
|
|
protected $childLists; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Contains the old/new text strings that match |
67
|
|
|
* @var array |
68
|
|
|
*/ |
69
|
|
|
protected $textMatches; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Contains the indexed start positions of each list within word string. |
73
|
|
|
* @var array |
74
|
|
|
*/ |
75
|
|
|
protected $listsIndex; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Array that holds the index of all content outside of the array. Format is array(index => content). |
79
|
|
|
* @var array |
80
|
|
|
*/ |
81
|
|
|
protected $contentIndex = array(); |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Holds the order and data on each list/content block within this list. |
85
|
|
|
* @var array |
86
|
|
|
*/ |
87
|
|
|
protected $diffOrderIndex = array(); |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* This is the opening ol,ul,dl ist tag. |
91
|
|
|
* @var string |
92
|
|
|
*/ |
93
|
|
|
protected $oldParentTag; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* This is the opening ol,ul,dl ist tag. |
97
|
|
|
* @var string |
98
|
|
|
*/ |
99
|
|
|
protected $newParentTag; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* We're using the same functions as the parent in build() to get us to the point of |
103
|
|
|
* manipulating the data within this class. |
104
|
|
|
* |
105
|
|
|
* @return string |
106
|
|
|
*/ |
107
|
|
|
public function build() |
108
|
|
|
{ |
109
|
|
|
// Use the parent functions to get the data we need organized. |
110
|
|
|
$this->splitInputsToWords(); |
111
|
|
|
$this->replaceIsolatedDiffTags(); |
112
|
|
|
$this->indexNewWords(); |
113
|
|
|
// Now use the custom functions in this class to use the data and generate our diff. |
114
|
|
|
$this->diffListContent(); |
115
|
|
|
|
116
|
|
|
return $this->content; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Calls to the actual custom functions of this class, to diff list content. |
121
|
|
|
*/ |
122
|
|
|
protected function diffListContent() |
123
|
|
|
{ |
124
|
|
|
/* Format the list we're focusing on. |
125
|
|
|
* There will always be one list, though passed as an array with one item. |
126
|
|
|
* Format this to only have the list contents, outside of the array. |
127
|
|
|
*/ |
128
|
|
|
$this->formatThisListContent(); |
129
|
|
|
|
130
|
|
|
/* Build an index of content outside of list tags. |
131
|
|
|
*/ |
132
|
|
|
$this->indexContent(); |
133
|
|
|
|
134
|
|
|
/* In cases where we're dealing with nested lists, |
135
|
|
|
* make sure we use placeholders to replace the nested lists |
136
|
|
|
*/ |
137
|
|
|
$this->replaceListIsolatedDiffTags(); |
138
|
|
|
|
139
|
|
|
/* Build a list of matches we can reference when we diff the contents of the lists. |
140
|
|
|
* This is needed so that we each NEW list node is matched against the best possible OLD list node/ |
141
|
|
|
* It helps us determine whether the list was added, removed, or changed. |
142
|
|
|
*/ |
143
|
|
|
$this->matchAndCompareLists(); |
144
|
|
|
|
145
|
|
|
/* Go through the list of matches, content, and diff each. |
146
|
|
|
* Any nested lists would be sent to parent's diffList function, which creates a new listDiff class. |
147
|
|
|
*/ |
148
|
|
|
$this->diff(); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* This function is used to populate both contentIndex and diffOrderIndex arrays for use in the diff function. |
153
|
|
|
*/ |
154
|
|
|
protected function indexContent() |
155
|
|
|
{ |
156
|
|
|
$this->contentIndex = array(); |
|
|
|
|
157
|
|
|
$this->diffOrderIndex = array('new' => array(), 'old' => array()); |
158
|
|
|
foreach ($this->list as $type => $list) { |
159
|
|
|
|
160
|
|
|
$this->contentIndex[$type] = array(); |
161
|
|
|
$depth = 0; |
|
|
|
|
162
|
|
|
$parentList = 0; |
|
|
|
|
163
|
|
|
$position = 0; |
|
|
|
|
164
|
|
|
$newBlock = true; |
|
|
|
|
165
|
|
|
$listCount = 0; |
|
|
|
|
166
|
|
|
$contentCount = 0; |
|
|
|
|
167
|
|
|
foreach ($list as $key => $word) { |
168
|
|
|
if (!$parentList && $this->isOpeningListTag($word)) { |
169
|
|
|
$depth++; |
170
|
|
|
|
171
|
|
|
$this->diffOrderIndex[$type][] = array('type' => 'list', 'position' => $listCount, 'index' => $key); |
172
|
|
|
$listCount++; |
173
|
|
|
continue; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
if (!$parentList && $this->isClosingListTag($word)) { |
177
|
|
|
$depth--; |
178
|
|
|
|
179
|
|
|
if ($depth == 0) { |
180
|
|
|
$newBlock = true; |
181
|
|
|
} |
182
|
|
|
continue; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
if ($this->isOpeningIsolatedDiffTag($word)) { |
186
|
|
|
$parentList++; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
if ($this->isClosingIsolatedDiffTag($word)) { |
190
|
|
|
$parentList--; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
if ($depth == 0) { |
194
|
|
|
if ($newBlock && !array_key_exists($contentCount, $this->contentIndex[$type])) { |
195
|
|
|
$this->diffOrderIndex[$type][] = array('type' => 'content', 'position' => $contentCount, 'index' => $key); |
196
|
|
|
|
197
|
|
|
$position = $contentCount; |
|
|
|
|
198
|
|
|
$this->contentIndex[$type][$position] = ''; |
199
|
|
|
$contentCount++; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
$this->contentIndex[$type][$position] .= $word; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
$newBlock = false; |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/* |
211
|
|
|
* This function is used to remove the wrapped ul, ol, or dl characters from this list |
212
|
|
|
* and sets the listType as ul, ol, or dl, so that we can use it later. |
213
|
|
|
* $list is being set here as well, as an array with the old and new version of this list content. |
214
|
|
|
*/ |
215
|
|
|
protected function formatThisListContent() |
216
|
|
|
{ |
217
|
|
|
$formatArray = array( |
218
|
|
|
array('type' => 'old', 'array' => $this->oldIsolatedDiffTags), |
219
|
|
|
array('type' => 'new', 'array' => $this->newIsolatedDiffTags) |
220
|
|
|
); |
221
|
|
|
|
222
|
|
|
foreach ($formatArray as $item) { |
223
|
|
|
$values = array_values($item['array']); |
|
|
|
|
224
|
|
|
$this->list[$item['type']] = count($values) |
225
|
|
|
? $this->formatList($values[0], $item['type']) |
226
|
|
|
: array(); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
$this->listType = $this->newListType ?: $this->oldListType; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* |
234
|
|
|
* @param array $arrayData |
235
|
|
|
* @param string $index |
236
|
|
|
* @return array |
237
|
|
|
*/ |
238
|
|
|
protected function formatList(array $arrayData, $index = 'old') |
239
|
|
|
{ |
240
|
|
|
$openingTag = $this->getAndStripTag($arrayData[0]); |
241
|
|
|
$closingTag = $this->getAndStripTag($arrayData[count($arrayData) - 1]); |
242
|
|
|
|
243
|
|
|
if (array_key_exists($openingTag, $this->isolatedDiffTags) && |
244
|
|
|
array_key_exists($closingTag, $this->isolatedDiffTags) |
245
|
|
|
) { |
246
|
|
View Code Duplication |
if ($index == 'new' && $this->isOpeningTag($arrayData[0])) { |
|
|
|
|
247
|
|
|
$this->newParentTag = $arrayData[0]; |
248
|
|
|
$this->newListType = $this->getAndStripTag($arrayData[0]); |
|
|
|
|
249
|
|
|
} |
250
|
|
|
|
251
|
|
View Code Duplication |
if ($index == 'old' && $this->isOpeningTag($arrayData[0])) { |
|
|
|
|
252
|
|
|
$this->oldParentTag = $arrayData[0]; |
253
|
|
|
$this->oldListType = $this->getAndStripTag($arrayData[0]); |
|
|
|
|
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
array_shift($arrayData); |
257
|
|
|
array_pop($arrayData); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
return $arrayData; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* @param string $tag |
265
|
|
|
* @return string |
266
|
|
|
*/ |
267
|
|
|
protected function getAndStripTag($tag) |
268
|
|
|
{ |
269
|
|
|
$content = explode(' ', preg_replace("/[^A-Za-z0-9 ]/", '', $tag)); |
270
|
|
|
return $content[0]; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
protected function matchAndCompareLists() |
274
|
|
|
{ |
275
|
|
|
/** |
276
|
|
|
* Build the an array (childLists) to hold the contents of the list nodes within this list. |
277
|
|
|
* This only holds the content of each list node. |
278
|
|
|
*/ |
279
|
|
|
$this->buildChildLists(); |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* Index the list, starting positions, so that we can refer back to it later. |
283
|
|
|
* This is used to see where one list node starts and another ends. |
284
|
|
|
*/ |
285
|
|
|
$this->indexLists(); |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Compare the lists and build $textMatches array with the matches. |
289
|
|
|
* Each match is an array of "new" and "old" keys, with the id of the list it matches to. |
290
|
|
|
* Whenever there is no match (in cases where a new list item was added or removed), null is used instead of the id. |
291
|
|
|
*/ |
292
|
|
|
$this->compareChildLists(); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Creates matches for lists. |
297
|
|
|
*/ |
298
|
|
|
protected function compareChildLists() |
299
|
|
|
{ |
300
|
|
|
$this->createNewOldMatches($this->childLists, $this->textMatches, 'content'); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Abstracted function used to match items in an array. |
305
|
|
|
* This is used primarily for populating lists matches. |
306
|
|
|
* |
307
|
|
|
* @param array $listArray |
308
|
|
|
* @param array $resultArray |
309
|
|
|
* @param string|null $column |
310
|
|
|
*/ |
311
|
|
|
protected function createNewOldMatches(&$listArray, &$resultArray, $column = null) |
312
|
|
|
{ |
313
|
|
|
// Always compare the new against the old. |
314
|
|
|
// Compare each new string against each old string. |
315
|
|
|
$bestMatchPercentages = array(); |
316
|
|
|
|
317
|
|
|
foreach ($listArray['new'] as $thisKey => $thisList) { |
318
|
|
|
$bestMatchPercentages[$thisKey] = array(); |
319
|
|
|
foreach ($listArray['old'] as $thatKey => $thatList) { |
320
|
|
|
// Save the percent amount each new list content compares against the old list content. |
321
|
|
|
similar_text( |
322
|
|
|
$column ? $thisList[$column] : $thisList, |
323
|
|
|
$column ? $thatList[$column] : $thatList, |
324
|
|
|
$percentage |
325
|
|
|
); |
326
|
|
|
|
327
|
|
|
$bestMatchPercentages[$thisKey][] = $percentage; |
328
|
|
|
} |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
// Sort each array by value, highest percent to lowest percent. |
332
|
|
|
foreach ($bestMatchPercentages as &$thisMatch) { |
333
|
|
|
arsort($thisMatch); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
// Build matches. |
337
|
|
|
$matches = array(); |
|
|
|
|
338
|
|
|
$taken = array(); |
|
|
|
|
339
|
|
|
$takenItems = array(); |
|
|
|
|
340
|
|
|
$absoluteMatch = 100; |
341
|
|
|
foreach ($bestMatchPercentages as $item => $percentages) { |
342
|
|
|
$highestMatch = -1; |
|
|
|
|
343
|
|
|
$highestMatchKey = -1; |
344
|
|
|
$takeItemKey = -1; |
|
|
|
|
345
|
|
|
|
346
|
|
|
foreach ($percentages as $key => $percent) { |
347
|
|
|
// Check that the key for the percentage is not already taken and the new percentage is higher. |
348
|
|
|
if (!in_array($key, $taken) && $percent > $highestMatch) { |
349
|
|
|
// If an absolute match, choose this one. |
350
|
|
|
if ($percent == $absoluteMatch) { |
351
|
|
|
$highestMatch = $percent; |
|
|
|
|
352
|
|
|
$highestMatchKey = $key; |
353
|
|
|
$takenItemKey = $item; |
|
|
|
|
354
|
|
|
break; |
355
|
|
|
} else { |
356
|
|
|
// Get all the other matces for the same $key |
357
|
|
|
$columns = $this->getArrayColumn($bestMatchPercentages, $key); |
|
|
|
|
358
|
|
|
$thisBestMatches = array_filter( |
359
|
|
|
$columns, |
360
|
|
|
function ($v) use ($percent) { |
361
|
|
|
return $v > $percent; |
362
|
|
|
} |
363
|
|
|
); |
364
|
|
|
|
365
|
|
|
arsort($thisBestMatches); |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* If the list item does not meet the threshold, it will not be considered a match. |
369
|
|
|
*/ |
370
|
|
|
if ($percent >= self::$listMatchThreshold) { |
371
|
|
|
// If no greater amounts, use this one. |
372
|
|
|
if (!count($thisBestMatches)) { |
373
|
|
|
$highestMatch = $percent; |
|
|
|
|
374
|
|
|
$highestMatchKey = $key; |
375
|
|
|
$takenItemKey = $item; |
|
|
|
|
376
|
|
|
break; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
// Loop through, comparing only the items that have not already been added. |
380
|
|
|
foreach ($thisBestMatches as $k => $v) { |
381
|
|
|
if (in_array($k, $takenItems)) { |
382
|
|
|
$highestMatch = $percent; |
|
|
|
|
383
|
|
|
$highestMatchKey = $key; |
384
|
|
|
$takenItemKey = $item; |
|
|
|
|
385
|
|
|
break(2); |
386
|
|
|
} |
387
|
|
|
} |
388
|
|
|
} |
389
|
|
|
} |
390
|
|
|
} |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
$matches[] = array('new' => $item, 'old' => $highestMatchKey > -1 ? $highestMatchKey : null); |
394
|
|
|
if ($highestMatchKey > -1) { |
395
|
|
|
$taken[] = $highestMatchKey; |
|
|
|
|
396
|
|
|
$takenItems[] = $takenItemKey; |
|
|
|
|
397
|
|
|
} |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
|
401
|
|
|
|
402
|
|
|
/* Checking for removed items. Basically, if a list item from the old lists is removed |
403
|
|
|
* it will not be accounted for, and will disappear in the results altogether. |
404
|
|
|
* Loop through all the old lists, any that has not been added, will be added as: |
405
|
|
|
* array( new => null, old => oldItemId ) |
406
|
|
|
*/ |
407
|
|
|
$matchColumns = $this->getArrayColumn($matches, 'old'); |
408
|
|
|
foreach ($listArray['old'] as $thisKey => $thisList) { |
409
|
|
|
if (!in_array($thisKey, $matchColumns)) { |
410
|
|
|
$matches[] = array('new' => null, 'old' => $thisKey); |
411
|
|
|
} |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
// Save the matches. |
415
|
|
|
$resultArray = $matches; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* This fuction is exactly like array_column. This is added for PHP versions that do not support array_column. |
420
|
|
|
* @param array $targetArray |
421
|
|
|
* @param mixed $key |
422
|
|
|
* @return array |
423
|
|
|
*/ |
424
|
|
|
protected function getArrayColumn(array $targetArray, $key) |
425
|
|
|
{ |
426
|
|
|
$data = array(); |
427
|
|
|
foreach ($targetArray as $item) { |
428
|
|
|
if (array_key_exists($key, $item)) { |
429
|
|
|
$data[] = $item[$key]; |
430
|
|
|
} |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
return $data; |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* Build multidimensional array holding the contents of each list node, old and new. |
438
|
|
|
*/ |
439
|
|
|
protected function buildChildLists() |
440
|
|
|
{ |
441
|
|
|
$this->childLists['old'] = $this->getListsContent($this->list['old']); |
442
|
|
|
$this->childLists['new'] = $this->getListsContent($this->list['new']); |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Diff the actual contents of the lists against their matched counterpart. |
447
|
|
|
* Build the content of the class. |
448
|
|
|
*/ |
449
|
|
|
protected function diff() |
450
|
|
|
{ |
451
|
|
|
// Add the opening parent node from listType. So if ol, <ol>, etc. |
452
|
|
|
$this->content = $this->addListTypeWrapper(); |
453
|
|
|
|
454
|
|
|
$oldIndexCount = 0; |
|
|
|
|
455
|
|
|
$diffOrderNewKeys = array_keys($this->diffOrderIndex['new']); |
456
|
|
|
foreach ($this->diffOrderIndex['new'] as $key => $index) { |
457
|
|
|
|
458
|
|
|
if ($index['type'] == "list") { |
459
|
|
|
|
460
|
|
|
// Check to see if an old list was deleted. |
461
|
|
|
$oldMatch = $this->getArrayByColumnValue($this->textMatches, 'old', $index['position']); |
462
|
|
View Code Duplication |
if ($oldMatch && $oldMatch['new'] === null) { |
|
|
|
|
463
|
|
|
$newList = ''; |
|
|
|
|
464
|
|
|
$oldList = $this->getListByMatch($oldMatch, 'old'); |
|
|
|
|
465
|
|
|
$this->content .= $this->addListElementToContent($newList, $oldList, $oldMatch, $index, 'old'); |
|
|
|
|
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
$match = $this->getArrayByColumnValue($this->textMatches, 'new', $index['position']); |
|
|
|
|
469
|
|
|
$newList = $this->childLists['new'][$match['new']]; |
|
|
|
|
470
|
|
|
$oldList = $this->getListByMatch($match, 'old'); |
|
|
|
|
471
|
|
|
$this->content .= $this->addListElementToContent($newList, $oldList, $match, $index, 'new'); |
|
|
|
|
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
if ($index['type'] == 'content') { |
475
|
|
|
$this->content .= $this->addContentElementsToContent($oldIndexCount, $index['position']); |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
$oldIndexCount++; |
479
|
|
|
|
480
|
|
|
if ($key == $diffOrderNewKeys[count($diffOrderNewKeys) - 1]) { |
481
|
|
|
foreach ($this->diffOrderIndex['old'] as $oldKey => $oldIndex) { |
482
|
|
|
if ($oldKey > $key) { |
483
|
|
|
if ($oldIndex['type'] == 'list') { |
484
|
|
|
$oldMatch = $this->getArrayByColumnValue($this->textMatches, 'old', $oldIndex['position']); |
485
|
|
View Code Duplication |
if ($oldMatch && $oldMatch['new'] === null) { |
|
|
|
|
486
|
|
|
$newList = ''; |
|
|
|
|
487
|
|
|
$oldList = $this->getListByMatch($oldMatch, 'old'); |
|
|
|
|
488
|
|
|
$this->content .= $this->addListElementToContent($newList, $oldList, $oldMatch, $oldIndex, 'old'); |
|
|
|
|
489
|
|
|
} |
490
|
|
|
} else { |
491
|
|
|
$this->content .= $this->addContentElementsToContent($oldKey); |
492
|
|
|
} |
493
|
|
|
} |
494
|
|
|
} |
495
|
|
|
} |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
// Add the closing parent node from listType. So if ol, </ol>, etc. |
499
|
|
|
$this->content .= $this->addListTypeWrapper(false); |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
/** |
503
|
|
|
* |
504
|
|
|
* @param string $newList |
505
|
|
|
* @param string $oldList |
506
|
|
|
* @param array $match |
507
|
|
|
* @param array $index |
508
|
|
|
* @return string |
509
|
|
|
*/ |
510
|
|
|
protected function addListElementToContent($newList, $oldList, array $match, array $index, $type) |
511
|
|
|
{ |
512
|
|
|
$content = $this->list[$type][$index['index']]; |
|
|
|
|
513
|
|
|
$content .= $this->processPlaceholders( |
514
|
|
|
$this->diffElements( |
515
|
|
|
$this->convertListContentArrayToString($oldList), |
|
|
|
|
516
|
|
|
$this->convertListContentArrayToString($newList), |
|
|
|
|
517
|
|
|
false |
518
|
|
|
), |
519
|
|
|
$match |
520
|
|
|
); |
521
|
|
|
$content .= "</li>"; |
522
|
|
|
return $content; |
523
|
|
|
} |
524
|
|
|
|
525
|
|
|
/** |
526
|
|
|
* |
527
|
|
|
* @param integer $oldIndexCount |
528
|
|
|
* @param null|integer $newPosition |
529
|
|
|
* @return string |
530
|
|
|
*/ |
531
|
|
|
protected function addContentElementsToContent($oldIndexCount, $newPosition = null) |
532
|
|
|
{ |
533
|
|
|
$newContent = $newPosition && array_key_exists($newPosition, $this->contentIndex['new']) |
|
|
|
|
534
|
|
|
? $this->contentIndex['new'][$newPosition] |
535
|
|
|
: ''; |
536
|
|
|
|
537
|
|
|
$oldDiffOrderIndexMatch = array_key_exists($oldIndexCount, $this->diffOrderIndex['old']) |
538
|
|
|
? $this->diffOrderIndex['old'][$oldIndexCount] |
539
|
|
|
: ''; |
540
|
|
|
|
541
|
|
|
$oldContent = $oldDiffOrderIndexMatch && array_key_exists($oldDiffOrderIndexMatch['position'], $this->contentIndex['old']) |
542
|
|
|
? $this->contentIndex['old'][$oldDiffOrderIndexMatch['position']] |
543
|
|
|
: ''; |
544
|
|
|
|
545
|
|
|
$diffObject = new HtmlDiff($oldContent, $newContent); |
546
|
|
|
$content = $diffObject->build(); |
|
|
|
|
547
|
|
|
return $content; |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* |
552
|
|
|
* @param array $match |
553
|
|
|
* @param string $type |
554
|
|
|
* @return array|string |
555
|
|
|
*/ |
556
|
|
|
protected function getListByMatch(array $match, $type = 'new') |
557
|
|
|
{ |
558
|
|
|
return array_key_exists($match[$type], $this->childLists[$type]) |
559
|
|
|
? $this->childLists[$type][$match[$type]] |
560
|
|
|
: ''; |
561
|
|
|
} |
562
|
|
|
|
563
|
|
|
/** |
564
|
|
|
* This function replaces array_column function in PHP for older versions of php. |
565
|
|
|
* |
566
|
|
|
* @param array $parentArray |
567
|
|
|
* @param string $column |
568
|
|
|
* @param mixed $value |
569
|
|
|
* @param boolean $allMatches |
570
|
|
|
* @return array|boolean |
571
|
|
|
*/ |
572
|
|
|
protected function getArrayByColumnValue($parentArray, $column, $value, $allMatches = false) |
573
|
|
|
{ |
574
|
|
|
$returnArray = array(); |
575
|
|
|
foreach ($parentArray as $array) { |
576
|
|
|
if (array_key_exists($column, $array) && $array[$column] == $value) { |
577
|
|
|
if ($allMatches) { |
578
|
|
|
$returnArray[] = $array; |
579
|
|
|
} else { |
580
|
|
|
return $array; |
581
|
|
|
} |
582
|
|
|
} |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
return $allMatches ? $returnArray : false; |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
/** |
589
|
|
|
* Converts the list (li) content arrays to string. |
590
|
|
|
* |
591
|
|
|
* @param array $listContentArray |
592
|
|
|
* @return string |
593
|
|
|
*/ |
594
|
|
|
protected function convertListContentArrayToString($listContentArray) |
595
|
|
|
{ |
596
|
|
|
if (!is_array($listContentArray)) { |
597
|
|
|
return $listContentArray; |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
$content = array(); |
601
|
|
|
|
602
|
|
|
$words = explode(" ", $listContentArray['content']); |
|
|
|
|
603
|
|
|
$nestedListCount = 0; |
604
|
|
|
foreach ($words as $word) { |
605
|
|
|
$match = $word == self::$listPlaceHolder; |
606
|
|
|
|
607
|
|
|
$content[] = $match |
608
|
|
|
? "<li>" . $this->convertListContentArrayToString($listContentArray['kids'][$nestedListCount]) . "</li>" |
609
|
|
|
: $word; |
610
|
|
|
|
611
|
|
|
if ($match) { |
612
|
|
|
$nestedListCount++; |
613
|
|
|
} |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
return implode(" ", $content); |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
/** |
620
|
|
|
* Return the contents of each list node. |
621
|
|
|
* Process any placeholders for nested lists. |
622
|
|
|
* |
623
|
|
|
* @param string $text |
624
|
|
|
* @param array $matches |
625
|
|
|
* @return string |
626
|
|
|
*/ |
627
|
|
|
protected function processPlaceholders($text, array $matches) |
628
|
|
|
{ |
629
|
|
|
// Prepare return |
630
|
|
|
$returnText = array(); |
631
|
|
|
// Save the contents of all list nodes, new and old. |
632
|
|
|
$contentVault = array( |
633
|
|
|
'old' => $this->getListContent('old', $matches), |
634
|
|
|
'new' => $this->getListContent('new', $matches) |
635
|
|
|
); |
636
|
|
|
|
637
|
|
|
$count = 0; |
638
|
|
|
// Loop through the text checking for placeholders. If a nested list is found, create a new ListDiff object for it. |
639
|
|
|
foreach (explode(' ', $text) as $word) { |
640
|
|
|
$preContent = $this->checkWordForDiffTag($this->stripNewLine($word)); |
641
|
|
|
|
642
|
|
|
if (in_array( |
643
|
|
|
is_array($preContent) ? $preContent[1] : $preContent, |
644
|
|
|
$this->isolatedDiffTags |
645
|
|
|
) |
646
|
|
|
) { |
647
|
|
|
$oldText = array_key_exists($count, $contentVault['old']) ? implode('', $contentVault['old'][$count]) : ''; |
648
|
|
|
$newText = array_key_exists($count, $contentVault['new']) ? implode('', $contentVault['new'][$count]) : ''; |
649
|
|
|
$content = $this->diffList($oldText, $newText); |
650
|
|
|
$count++; |
651
|
|
|
} else { |
652
|
|
|
$content = $preContent; |
653
|
|
|
} |
654
|
|
|
|
655
|
|
|
$returnText[] = is_array($preContent) ? $preContent[0] . $content . $preContent[2] : $content; |
656
|
|
|
} |
657
|
|
|
// Return the result. |
658
|
|
|
return implode(' ', $returnText); |
659
|
|
|
} |
660
|
|
|
|
661
|
|
|
/** |
662
|
|
|
* Checks to see if a diff tag is in string. |
663
|
|
|
* |
664
|
|
|
* @param string $word |
665
|
|
|
* @return string |
|
|
|
|
666
|
|
|
*/ |
667
|
|
|
protected function checkWordForDiffTag($word) |
668
|
|
|
{ |
669
|
|
|
foreach ($this->isolatedDiffTags as $diffTag) { |
670
|
|
|
if (strpos($word, $diffTag) > -1) { |
671
|
|
|
$position = strpos($word, $diffTag); |
672
|
|
|
$length = strlen($diffTag); |
|
|
|
|
673
|
|
|
$result = array( |
|
|
|
|
674
|
|
|
substr($word, 0, $position), |
675
|
|
|
$diffTag, |
676
|
|
|
substr($word, ($position + $length)) |
677
|
|
|
); |
678
|
|
|
|
679
|
|
|
return $result; |
680
|
|
|
} |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
return $word; |
684
|
|
|
} |
685
|
|
|
|
686
|
|
|
/** |
687
|
|
|
* Used to remove new lines. |
688
|
|
|
* |
689
|
|
|
* @param string $text |
690
|
|
|
* @return string |
691
|
|
|
*/ |
692
|
|
|
protected function stripNewLine($text) |
693
|
|
|
{ |
694
|
|
|
return trim(preg_replace('/\s\s+/', ' ', $text)); |
695
|
|
|
} |
696
|
|
|
|
697
|
|
|
/** |
698
|
|
|
* Grab the list content using the listsIndex array. |
699
|
|
|
* |
700
|
|
|
* @param string $indexKey |
701
|
|
|
* @param array $matches |
702
|
|
|
* @return array |
703
|
|
|
*/ |
704
|
|
|
protected function getListContent($indexKey = 'new', array $matches) |
|
|
|
|
705
|
|
|
{ |
706
|
|
|
$bucket = array(); |
707
|
|
|
|
708
|
|
|
if (isset($matches[$indexKey]) && $matches[$indexKey] !== null) { |
709
|
|
|
$start = $this->listsIndex[$indexKey][$matches[$indexKey]]; |
710
|
|
|
$stop = $this->findEndForIndex($this->list[$indexKey], $start); |
|
|
|
|
711
|
|
|
|
712
|
|
|
for ($x = $start; $x <= $stop; $x++) { |
713
|
|
|
|
714
|
|
|
if (in_array($this->list[$indexKey][$x], $this->isolatedDiffTags)) { |
715
|
|
|
$bucket[] = $this->listIsolatedDiffTags[$indexKey][$x]; |
716
|
|
|
} |
717
|
|
|
} |
718
|
|
|
} |
719
|
|
|
|
720
|
|
|
return $bucket; |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
/** |
724
|
|
|
* Finds the end of list within its index. |
725
|
|
|
* |
726
|
|
|
* @param array $index |
727
|
|
|
* @param integer $start |
728
|
|
|
* @return integer |
|
|
|
|
729
|
|
|
*/ |
730
|
|
|
protected function findEndForIndex(array $index, $start) |
731
|
|
|
{ |
732
|
|
|
$array = array_splice($index, $start); |
733
|
|
|
$count = 0; |
734
|
|
|
foreach ($array as $key => $item) { |
735
|
|
|
if ($this->isOpeningListTag($item)) { |
736
|
|
|
$count++; |
737
|
|
|
} |
738
|
|
|
|
739
|
|
|
if ($this->isClosingListTag($item)) { |
740
|
|
|
$count--; |
741
|
|
|
if ($count === 0) { |
742
|
|
|
return $start + $key; |
743
|
|
|
} |
744
|
|
|
} |
745
|
|
|
} |
746
|
|
|
|
747
|
|
|
return $start + count($array); |
748
|
|
|
} |
749
|
|
|
|
750
|
|
|
/** |
751
|
|
|
* indexLists |
752
|
|
|
* |
753
|
|
|
* Index the list, starting positions, so that we can refer back to it later. |
754
|
|
|
* This is used to see where one list node starts and another ends. |
755
|
|
|
*/ |
756
|
|
|
protected function indexLists() |
757
|
|
|
{ |
758
|
|
|
$this->listsIndex = array(); |
759
|
|
|
$count = 0; |
|
|
|
|
760
|
|
|
foreach ($this->list as $type => $list) { |
761
|
|
|
$this->listsIndex[$type] = array(); |
762
|
|
|
|
763
|
|
|
foreach ($list as $key => $listItem) { |
764
|
|
|
if ($this->isOpeningListTag($listItem)) { |
765
|
|
|
$count++; |
766
|
|
|
if ($count === 1) { |
767
|
|
|
$this->listsIndex[$type][] = $key; |
768
|
|
|
} |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
if ($this->isClosingListTag($listItem)) { |
772
|
|
|
$count--; |
773
|
|
|
} |
774
|
|
|
} |
775
|
|
|
} |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
/** |
779
|
|
|
* Adds the opening or closing list html element, based on listType. |
780
|
|
|
* |
781
|
|
|
* @param boolean $opening |
782
|
|
|
* @return string |
783
|
|
|
*/ |
784
|
|
|
protected function addListTypeWrapper($opening = true) |
785
|
|
|
{ |
786
|
|
|
|
787
|
|
|
if ($opening) { |
788
|
|
|
return $this->newParentTag ?: $this->oldParentTag; |
789
|
|
|
} else { |
790
|
|
|
return "<" . (!$opening ? "/" : '') . $this->listType . ">"; |
791
|
|
|
} |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
/** |
795
|
|
|
* Replace nested list with placeholders. |
796
|
|
|
*/ |
797
|
|
|
public function replaceListIsolatedDiffTags() |
798
|
|
|
{ |
799
|
|
|
$this->listIsolatedDiffTags['old'] = $this->createIsolatedDiffTagPlaceholders($this->list['old']); |
800
|
|
|
$this->listIsolatedDiffTags['new'] = $this->createIsolatedDiffTagPlaceholders($this->list['new']); |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
/** |
804
|
|
|
* Grab the contents of a list node. |
805
|
|
|
* |
806
|
|
|
* @param array $contentArray |
807
|
|
|
* @param boolean $stripTags |
808
|
|
|
* @return array |
809
|
|
|
*/ |
810
|
|
|
protected function getListsContent(array $contentArray, $stripTags = true) |
|
|
|
|
811
|
|
|
{ |
812
|
|
|
$lematches = array(); |
|
|
|
|
813
|
|
|
$arrayDepth = 0; |
|
|
|
|
814
|
|
|
$nestedCount = array(); |
815
|
|
|
foreach ($contentArray as $index => $word) { |
816
|
|
|
|
817
|
|
|
if ($this->isOpeningListTag($word)) { |
818
|
|
|
$arrayDepth++; |
819
|
|
|
if (!array_key_exists($arrayDepth, $nestedCount)) { |
820
|
|
|
$nestedCount[$arrayDepth] = 1; |
821
|
|
|
} else { |
822
|
|
|
$nestedCount[$arrayDepth]++; |
823
|
|
|
} |
824
|
|
|
continue; |
825
|
|
|
} |
826
|
|
|
|
827
|
|
|
if ($this->isClosingListTag($word)) { |
828
|
|
|
$arrayDepth--; |
829
|
|
|
continue; |
830
|
|
|
} |
831
|
|
|
|
832
|
|
|
if ($arrayDepth > 0) { |
833
|
|
|
$this->addStringToArrayByDepth($word, $lematches, $arrayDepth, 1, $nestedCount); |
834
|
|
|
} |
835
|
|
|
} |
836
|
|
|
|
837
|
|
|
return $lematches; |
838
|
|
|
} |
839
|
|
|
|
840
|
|
|
/** |
841
|
|
|
* This function helps build the list content array of a list. |
842
|
|
|
* If a list has another list within it, the inner list is replaced with the list placeholder and the inner list |
843
|
|
|
* content becomes a child of the parent list. |
844
|
|
|
* This goes recursively down. |
845
|
|
|
* |
846
|
|
|
* @param string $word |
847
|
|
|
* @param array $array |
848
|
|
|
* @param integer $targetDepth |
849
|
|
|
* @param integer $thisDepth |
850
|
|
|
* @param array $nestedCount |
851
|
|
|
*/ |
852
|
|
|
protected function addStringToArrayByDepth($word, array &$array, $targetDepth, $thisDepth, array $nestedCount) |
853
|
|
|
{ |
854
|
|
|
// determine what depth we're at |
855
|
|
|
if ($targetDepth == $thisDepth) { |
856
|
|
|
// decide on what to do at this level |
857
|
|
|
|
858
|
|
|
if (array_key_exists('content', $array)) { |
859
|
|
|
$array['content'] .= $word; |
860
|
|
|
} else { |
861
|
|
|
// if we're on depth 1, add content |
862
|
|
|
if ($nestedCount[$targetDepth] > count($array)) { |
863
|
|
|
$array[] = array('content' => '', 'kids' => array()); |
864
|
|
|
} |
865
|
|
|
|
866
|
|
|
$array[count($array) - 1]['content'] .= $word; |
867
|
|
|
} |
868
|
|
|
|
869
|
|
|
} else { |
870
|
|
|
|
871
|
|
|
// create first kid if not exist |
872
|
|
|
$newArray = array('content' => '', 'kids' => array()); |
873
|
|
|
|
874
|
|
|
if (array_key_exists('kids', $array)) { |
875
|
|
|
if ($nestedCount[$targetDepth] > count($array['kids'])) { |
876
|
|
|
$array['kids'][] = $newArray; |
|
|
|
|
877
|
|
|
$array['content'] .= self::$listPlaceHolder; |
878
|
|
|
} |
879
|
|
|
|
880
|
|
|
// continue to the next depth |
881
|
|
|
$thisDepth++; |
882
|
|
|
|
883
|
|
|
// get last kid and send to next depth |
884
|
|
|
|
885
|
|
|
$this->addStringToArrayByDepth( |
886
|
|
|
$word, |
887
|
|
|
$array['kids'][count($array['kids']) - 1], |
888
|
|
|
$targetDepth, |
889
|
|
|
$thisDepth, |
890
|
|
|
$nestedCount |
891
|
|
|
); |
892
|
|
|
|
893
|
|
|
} else { |
894
|
|
|
|
895
|
|
|
if ($nestedCount[$targetDepth] > count($array[count($array) - 1]['kids'])) { |
896
|
|
|
$array[count($array) - 1]['kids'][] = $newArray; |
|
|
|
|
897
|
|
|
$array[count($array) - 1]['content'] .= self::$listPlaceHolder; |
898
|
|
|
} |
899
|
|
|
// continue to the next depth |
900
|
|
|
$thisDepth++; |
901
|
|
|
|
902
|
|
|
// get last kid and send to next depth |
903
|
|
|
|
904
|
|
|
$this->addStringToArrayByDepth( |
905
|
|
|
$word, |
906
|
|
|
$array[count($array) - 1]['kids'][count($array[count($array) - 1]['kids']) - 1], |
907
|
|
|
$targetDepth, |
908
|
|
|
$thisDepth, |
909
|
|
|
$nestedCount |
910
|
|
|
); |
911
|
|
|
} |
912
|
|
|
} |
913
|
|
|
} |
914
|
|
|
|
915
|
|
|
/** |
916
|
|
|
* Checks if text is opening list tag. |
917
|
|
|
* |
918
|
|
|
* @param string $item |
919
|
|
|
* @return boolean |
920
|
|
|
*/ |
921
|
|
|
protected function isOpeningListTag($item) |
922
|
|
|
{ |
923
|
|
|
if (preg_match("#<li[^>]*>\\s*#iU", $item)) { |
|
|
|
|
924
|
|
|
return true; |
925
|
|
|
} |
926
|
|
|
|
927
|
|
|
return false; |
928
|
|
|
} |
929
|
|
|
|
930
|
|
|
/** |
931
|
|
|
* Check if text is closing list tag. |
932
|
|
|
* |
933
|
|
|
* @param string $item |
934
|
|
|
* @return boolean |
935
|
|
|
*/ |
936
|
|
|
protected function isClosingListTag($item) |
937
|
|
|
{ |
938
|
|
|
if (preg_match("#</li[^>]*>\\s*#iU", $item)) { |
|
|
|
|
939
|
|
|
return true; |
940
|
|
|
} |
941
|
|
|
|
942
|
|
|
return false; |
943
|
|
|
} |
944
|
|
|
} |
945
|
|
|
|
This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.
To visualize
will produce issues in the first and second line, while this second example
will produce no issues.