@@ -37,6 +37,18 @@ class ListDiff extends HtmlDiff
3737 * @var string
3838 */
3939 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 ;
4052
4153 /**
4254 * Hold the old/new content of the content of the list.
@@ -61,6 +73,18 @@ class ListDiff extends HtmlDiff
6173 * @var array
6274 */
6375 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 ();
6488
6589 /**
6690 * We're using the same functions as the parent in build() to get us to the point of
@@ -90,20 +114,86 @@ protected function diffListContent()
90114 * Format this to only have the list contents, outside of the array.
91115 */
92116 $ this ->formatThisListContent ();
117+
118+ /* Build an index of content outside of list tags.
119+ */
120+ $ this ->indexContent ();
121+
93122 /* In cases where we're dealing with nested lists,
94123 * make sure we use placeholders to replace the nested lists
95124 */
96125 $ this ->replaceListIsolatedDiffTags ();
126+
97127 /* Build a list of matches we can reference when we diff the contents of the lists.
98128 * This is needed so that we each NEW list node is matched against the best possible OLD list node/
99129 * It helps us determine whether the list was added, removed, or changed.
100130 */
101131 $ this ->matchAndCompareLists ();
102- /* Go through the list of matches, and diff the contents of each.
132+
133+ /* Go through the list of matches, content, and diff each.
103134 * Any nested lists would be sent to parent's diffList function, which creates a new listDiff class.
104135 */
105136 $ this ->diff ();
106137 }
138+
139+ /**
140+ * This function is used to populate both contentIndex and diffOrderIndex arrays for use in the diff function.
141+ */
142+ protected function indexContent ()
143+ {
144+ $ this ->contentIndex = array ();
145+ $ this ->diffOrderIndex = array ('new ' => array (), 'old ' => array ());
146+ foreach ($ this ->list as $ type => $ list ) {
147+
148+ $ this ->contentIndex [$ type ] = array ();
149+ $ depth = 0 ;
150+ $ parentList = 0 ;
151+ $ position = 0 ;
152+ $ newBlock = true ;
153+ $ listCount = 0 ;
154+ $ contentCount = 0 ;
155+ foreach ($ list as $ key => $ word ) {
156+ if (!$ parentList && $ this ->isOpeningListTag ($ word )) {
157+ $ depth ++;
158+
159+ $ this ->diffOrderIndex [$ type ][] = array ('type ' => 'list ' , 'position ' => $ listCount , 'index ' => $ key );
160+ $ listCount ++;
161+ continue ;
162+ }
163+
164+ if (!$ parentList && $ this ->isClosingListTag ($ word )) {
165+ $ depth --;
166+
167+ if ($ depth == 0 ) {
168+ $ newBlock = true ;
169+ }
170+ continue ;
171+ }
172+
173+ if ($ this ->isOpeningIsolatedDiffTag ($ word )) {
174+ $ parentList ++;
175+ }
176+
177+ if ($ this ->isClosingIsolatedDiffTag ($ word )) {
178+ $ parentList --;
179+ }
180+
181+ if ($ depth == 0 ) {
182+ if ($ newBlock && !array_key_exists ($ contentCount , $ this ->contentIndex [$ type ])) {
183+ $ this ->diffOrderIndex [$ type ][] = array ('type ' => 'content ' , 'position ' => $ contentCount , 'index ' => $ key );
184+
185+ $ position = $ contentCount ;
186+ $ this ->contentIndex [$ type ][$ position ] = '' ;
187+ $ contentCount ++;
188+ }
189+
190+ $ this ->contentIndex [$ type ][$ position ] .= $ word ;
191+ }
192+
193+ $ newBlock = false ;
194+ }
195+ }
196+ }
107197
108198 /*
109199 * This function is used to remove the wrapped ul, ol, or dl characters from this list
@@ -123,6 +213,8 @@ protected function formatThisListContent()
123213 ? $ this ->formatList ($ values [0 ], $ item ['type ' ])
124214 : array ();
125215 }
216+
217+ $ this ->listType = $ this ->newListType ?: $ this ->oldListType ;
126218 }
127219
128220 /**
@@ -139,8 +231,12 @@ protected function formatList(array $arrayData, $index = 'old')
139231 if (array_key_exists ($ openingTag , $ this ->isolatedDiffTags ) &&
140232 array_key_exists ($ closingTag , $ this ->isolatedDiffTags )
141233 ) {
142- if ($ index == 'old ' ) {
143- $ this ->listType = $ this ->getAndStripTag ($ arrayData [0 ]);
234+ if ($ index == 'new ' && $ this ->isOpeningTag ($ arrayData [0 ])) {
235+ $ this ->newListType = $ this ->getAndStripTag ($ arrayData [0 ]);
236+ }
237+
238+ if ($ index == 'old ' && $ this ->isOpeningTag ($ arrayData [0 ])) {
239+ $ this ->oldListType = $ this ->getAndStripTag ($ arrayData [0 ]);
144240 }
145241
146242 array_shift ($ arrayData );
@@ -181,27 +277,48 @@ protected function matchAndCompareLists()
181277 */
182278 $ this ->compareChildLists ();
183279 }
184-
280+
281+ /**
282+ * Creates matches for lists.
283+ */
185284 protected function compareChildLists ()
285+ {
286+ $ this ->createNewOldMatches ($ this ->childLists , $ this ->textMatches , 'content ' );
287+ }
288+
289+ /**
290+ * Abstracted function used to match items in an array.
291+ * This is used primarily for populating lists matches.
292+ *
293+ * @param array $listArray
294+ * @param array $resultArray
295+ * @param string|null $column
296+ */
297+ protected function createNewOldMatches (&$ listArray , &$ resultArray , $ column = null )
186298 {
187299 // Always compare the new against the old.
188300 // Compare each new string against each old string.
189301 $ bestMatchPercentages = array ();
190-
191- foreach ($ this -> childLists ['new ' ] as $ thisKey => $ thisList ) {
302+
303+ foreach ($ listArray ['new ' ] as $ thisKey => $ thisList ) {
192304 $ bestMatchPercentages [$ thisKey ] = array ();
193- foreach ($ this -> childLists ['old ' ] as $ thatKey => $ thatList ) {
305+ foreach ($ listArray ['old ' ] as $ thatKey => $ thatList ) {
194306 // Save the percent amount each new list content compares against the old list content.
195- similar_text ($ thisList ['content ' ], $ thatList ['content ' ], $ percentage );
307+ similar_text (
308+ $ column ? $ thisList [$ column ] : $ thisList ,
309+ $ column ? $ thatList [$ column ] : $ thatList ,
310+ $ percentage
311+ );
312+
196313 $ bestMatchPercentages [$ thisKey ][] = $ percentage ;
197314 }
198315 }
199-
316+
200317 // Sort each array by value, highest percent to lowest percent.
201318 foreach ($ bestMatchPercentages as &$ thisMatch ) {
202319 arsort ($ thisMatch );
203320 }
204-
321+
205322 // Build matches.
206323 $ matches = array ();
207324 $ taken = array ();
@@ -258,28 +375,30 @@ function ($v) use ($percent) {
258375 }
259376 }
260377 }
261-
378+
262379 $ matches [] = array ('new ' => $ item , 'old ' => $ highestMatchKey > -1 ? $ highestMatchKey : null );
263380 if ($ highestMatchKey > -1 ) {
264381 $ taken [] = $ highestMatchKey ;
265382 $ takenItems [] = $ takenItemKey ;
266383 }
267384 }
385+
386+
268387
269388 /* Checking for removed items. Basically, if a list item from the old lists is removed
270389 * it will not be accounted for, and will disappear in the results altogether.
271390 * Loop through all the old lists, any that has not been added, will be added as:
272391 * array( new => null, old => oldItemId )
273392 */
274393 $ matchColumns = $ this ->getArrayColumn ($ matches , 'old ' );
275- foreach ($ this -> childLists ['old ' ] as $ thisKey => $ thisList ) {
394+ foreach ($ listArray ['old ' ] as $ thisKey => $ thisList ) {
276395 if (!in_array ($ thisKey , $ matchColumns )) {
277396 $ matches [] = array ('new ' => null , 'old ' => $ thisKey );
278397 }
279398 }
280-
399+
281400 // Save the matches.
282- $ this -> textMatches = $ matches ;
401+ $ resultArray = $ matches ;
283402 }
284403
285404 /**
@@ -314,32 +433,80 @@ protected function buildChildLists()
314433 * Build the content of the class.
315434 */
316435 protected function diff ()
317- {
436+ {
318437 // Add the opening parent node from listType. So if ol, <ol>, etc.
319438 $ this ->content = $ this ->addListTypeWrapper ();
320- foreach ($ this ->textMatches as $ key => $ matches ) {
321-
322- $ oldText = $ matches ['old ' ] !== null ? $ this ->childLists ['old ' ][$ matches ['old ' ]] : '' ;
323- $ newText = $ matches ['new ' ] !== null ? $ this ->childLists ['new ' ][$ matches ['new ' ]] : '' ;
324-
325- // Add the opened and closed the list
326- $ this ->content .= "<li> " ;
327- // Process any placeholders, if they exist.
328- // Placeholders would be nested lists (a nested ol, ul, dl for example).
329- $ this ->content .= $ this ->processPlaceholders (
330- $ this ->diffElements (
331- $ this ->convertListContentArrayToString ($ oldText ),
332- $ this ->convertListContentArrayToString ($ newText ),
333- false
334- ),
335- $ matches
336- );
337- $ this ->content .= "</li> " ;
439+
440+ $ oldIndexCount = 0 ;
441+ foreach ($ this ->diffOrderIndex ['new ' ] as $ key => $ index ) {
442+
443+ if ($ index ['type ' ] == "list " ) {
444+ $ match = $ this ->getArrayByColumnValue ($ this ->textMatches , 'new ' , $ index ['position ' ]);
445+ $ newList = $ this ->childLists ['new ' ][$ match ['new ' ]];
446+ $ oldList = array_key_exists ($ match ['old ' ], $ this ->childLists ['old ' ])
447+ ? $ this ->childLists ['old ' ][$ match ['old ' ]]
448+ : '' ;
449+
450+ $ content = "<li> " ;
451+ $ content .= $ this ->processPlaceholders (
452+ $ this ->diffElements (
453+ $ this ->convertListContentArrayToString ($ oldList ),
454+ $ this ->convertListContentArrayToString ($ newList ),
455+ false
456+ ),
457+ $ match
458+ );
459+ $ content .= "</li> " ;
460+ $ this ->content .= $ content ;
461+ }
462+
463+ if ($ index ['type ' ] == 'content ' ) {
464+ $ newContent = $ this ->contentIndex ['new ' ][$ index ['position ' ]];
465+
466+ $ oldDiffOrderIndexMatch = array_key_exists ($ oldIndexCount , $ this ->diffOrderIndex ['old ' ])
467+ ? $ this ->diffOrderIndex ['old ' ][$ oldIndexCount ]
468+ : '' ;
469+
470+ $ oldContent = $ oldDiffOrderIndexMatch && array_key_exists ($ oldDiffOrderIndexMatch ['position ' ], $ this ->contentIndex ['old ' ])
471+ ? $ this ->contentIndex ['old ' ][$ oldDiffOrderIndexMatch ['position ' ]]
472+ : '' ;
473+
474+ $ diffObject = new HtmlDiff ($ oldContent , $ newContent );
475+ $ content = $ diffObject ->build ();
476+ $ this ->content .= $ content ;
477+ }
478+
479+ $ oldIndexCount ++;
338480 }
339481
340482 // Add the closing parent node from listType. So if ol, </ol>, etc.
341483 $ this ->content .= $ this ->addListTypeWrapper (false );
342484 }
485+
486+ /**
487+ * This function replaces array_column function in PHP for older versions of php.
488+ *
489+ * @param array $parentArray
490+ * @param string $column
491+ * @param mixed $value
492+ * @param boolean $allMatches
493+ * @return array|boolean
494+ */
495+ protected function getArrayByColumnValue ($ parentArray , $ column , $ value , $ allMatches = false )
496+ {
497+ $ returnArray = array ();
498+ foreach ($ parentArray as $ array ) {
499+ if (array_key_exists ($ column , $ array ) && $ array [$ column ] == $ value ) {
500+ if ($ allMatches ) {
501+ $ returnArray [] = $ array ;
502+ } else {
503+ return $ array ;
504+ }
505+ }
506+ }
507+
508+ return $ allMatches ? $ returnArray : false ;
509+ }
343510
344511 /**
345512 * Converts the list (li) content arrays to string.
@@ -564,7 +731,7 @@ protected function getListsContent(array $contentArray, $stripTags = true)
564731 $ arrayDepth = 0 ;
565732 $ nestedCount = array ();
566733 foreach ($ contentArray as $ index => $ word ) {
567-
734+
568735 if ($ this ->isOpeningListTag ($ word )) {
569736 $ arrayDepth ++;
570737 if (!array_key_exists ($ arrayDepth , $ nestedCount )) {
0 commit comments