Completed
Push — master ( c7d60b...084747 )
by Josh
02:55
created

lib/Caxy/HtmlDiff/ListDiff.php (3 issues)

Upgrade to new PHP Analysis Engine

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
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;
0 ignored issues
show
$highestMatch is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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;
0 ignored issues
show
$highestMatch is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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;
0 ignored issues
show
$highestMatch is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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