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

lib/Caxy/HtmlDiff/ListDiff.php (27 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]);
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
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]);
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
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...
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
352
                        $highestMatchKey = $key;
353
                        $takenItemKey = $item;
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
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...
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
374
                                $highestMatchKey = $key;
375
                                $takenItemKey = $item;
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
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...
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
383
                                    $highestMatchKey = $key;
384
                                    $takenItemKey = $item;
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
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) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
463
                    $newList = '';
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
464
                    $oldList = $this->getListByMatch($oldMatch, 'old');
0 ignored issues
show
It seems like $oldMatch defined by $this->getArrayByColumnV...d', $index['position']) on line 461 can also be of type boolean; however, Caxy\HtmlDiff\ListDiff::getListByMatch() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
465
                    $this->content .= $this->addListElementToContent($newList, $oldList, $oldMatch, $index, 'old');
0 ignored issues
show
It seems like $oldList defined by $this->getListByMatch($oldMatch, 'old') on line 464 can also be of type array; however, Caxy\HtmlDiff\ListDiff::addListElementToContent() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
It seems like $oldMatch defined by $this->getArrayByColumnV...d', $index['position']) on line 461 can also be of type boolean; however, Caxy\HtmlDiff\ListDiff::addListElementToContent() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
466
                }
467
                
468
                $match = $this->getArrayByColumnValue($this->textMatches, 'new', $index['position']);
469
                $newList = $this->childLists['new'][$match['new']];
470
                $oldList = $this->getListByMatch($match, 'old');
0 ignored issues
show
It seems like $match defined by $this->getArrayByColumnV...w', $index['position']) on line 468 can also be of type boolean; however, Caxy\HtmlDiff\ListDiff::getListByMatch() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
471
                $this->content .= $this->addListElementToContent($newList, $oldList, $match, $index, 'new');
0 ignored issues
show
It seems like $oldList defined by $this->getListByMatch($match, 'old') on line 470 can also be of type array; however, Caxy\HtmlDiff\ListDiff::addListElementToContent() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
It seems like $match defined by $this->getArrayByColumnV...w', $index['position']) on line 468 can also be of type boolean; however, Caxy\HtmlDiff\ListDiff::addListElementToContent() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
486
                                $newList = '';
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
487
                                $oldList = $this->getListByMatch($oldMatch, 'old');
0 ignored issues
show
It seems like $oldMatch defined by $this->getArrayByColumnV... $oldIndex['position']) on line 484 can also be of type boolean; however, Caxy\HtmlDiff\ListDiff::getListByMatch() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
488
                                $this->content .= $this->addListElementToContent($newList, $oldList, $oldMatch, $oldIndex, 'old');
0 ignored issues
show
It seems like $oldList defined by $this->getListByMatch($oldMatch, 'old') on line 487 can also be of type array; however, Caxy\HtmlDiff\ListDiff::addListElementToContent() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
It seems like $oldMatch defined by $this->getArrayByColumnV... $oldIndex['position']) on line 484 can also be of type boolean; however, Caxy\HtmlDiff\ListDiff::addListElementToContent() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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']];
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

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

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

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