setConfig($config); } return $diff; } public function build() { if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) { $this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText); return $this->content; } $this->splitInputsToWords(); $this->content = $this->diffLists( $this->buildDiffList($this->oldWords), $this->buildDiffList($this->newWords) ); if ($this->hasDiffCache()) { $this->getDiffCache()->save($this->oldText, $this->newText, $this->content); } return $this->content; } protected function diffLists(DiffList $oldList, DiffList $newList) { $oldMatchData = array(); $newMatchData = array(); $oldListIndices = array(); $newListIndices = array(); $oldListItems = array(); $newListItems = array(); foreach ($oldList->getListItems() as $oldIndex => $oldListItem) { if ($oldListItem instanceof DiffListItem) { $oldListItems[$oldIndex] = $oldListItem; $oldListIndices[] = $oldIndex; $oldMatchData[$oldIndex] = array(); // Get match percentages foreach ($newList->getListItems() as $newIndex => $newListItem) { if ($newListItem instanceof DiffListItem) { if (!in_array($newListItem, $newListItems)) { $newListItems[$newIndex] = $newListItem; } if (!in_array($newIndex, $newListIndices)) { $newListIndices[] = $newIndex; } if (!array_key_exists($newIndex, $newMatchData)) { $newMatchData[$newIndex] = array(); } $oldText = implode('', $oldListItem->getText()); $newText = implode('', $newListItem->getText()); // similar_text $percentage = null; similar_text($oldText, $newText, $percentage); $oldMatchData[$oldIndex][$newIndex] = $percentage; $newMatchData[$newIndex][$oldIndex] = $percentage; } } } } $currentIndexInOld = 0; $currentIndexInNew = 0; $oldCount = count($oldListIndices); $newCount = count($newListIndices); $difference = max($oldCount, $newCount) - min($oldCount, $newCount); $diffOutput = ''; foreach ($newList->getListItems() as $newIndex => $newListItem) { if ($newListItem instanceof DiffListItem) { $operation = null; $oldListIndex = array_key_exists($currentIndexInOld, $oldListIndices) ? $oldListIndices[$currentIndexInOld] : null; $class = 'normal'; if (null !== $oldListIndex && array_key_exists($oldListIndex, $oldMatchData)) { // Check percentage matches of upcoming list items in old. $matchPercentage = $oldMatchData[$oldListIndex][$newIndex]; // does the old list item match better? $otherMatchBetter = false; foreach ($oldMatchData[$oldListIndex] as $index => $percentage) { if ($index > $newIndex && $percentage > $matchPercentage) { $otherMatchBetter = $index; } } if (false !== $otherMatchBetter && $newCount > $oldCount && $difference > 0) { $diffOutput .= sprintf('%s', $newListItem->getHtml('normal new', 'ins')); $currentIndexInNew++; $difference--; continue; } $nextOldListIndex = array_key_exists($currentIndexInOld + 1, $oldListIndices) ? $oldListIndices[$currentIndexInOld + 1] : null; $replacement = false; if ($nextOldListIndex !== null && $oldMatchData[$nextOldListIndex][$newIndex] > $matchPercentage && $oldMatchData[$nextOldListIndex][$newIndex] > $this->config->getMatchThreshold()) { // Following list item in old is better match, use that. $diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del')); $currentIndexInOld++; $oldListIndex = $nextOldListIndex; $matchPercentage = $oldMatchData[$oldListIndex]; $replacement = true; } if ($matchPercentage > $this->config->getMatchThreshold() || $currentIndexInNew === $currentIndexInOld) { // Diff the two lists. $htmlDiff = HtmlDiff::create( $oldListItems[$oldListIndex]->getInnerHtml(), $newListItem->getInnerHtml(), $this->config ); $diffContent = $htmlDiff->build(); $diffOutput .= sprintf('%s%s%s', $newListItem->getStartTagWithDiffClass($replacement ? 'replacement' : 'normal'), $diffContent, $newListItem->getEndTag()); } else { $diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del')); $diffOutput .= sprintf('%s', $newListItem->getHtml('replacement', 'ins')); } $currentIndexInOld++; } else { $diffOutput .= sprintf('%s', $newListItem->getHtml('normal new', 'ins')); } $currentIndexInNew++; } } // Output any additional list items while (array_key_exists($currentIndexInOld, $oldListIndices)) { $oldListIndex = $oldListIndices[$currentIndexInOld]; $diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del')); $currentIndexInOld++; } return sprintf('%s%s%s', $newList->getStartTagWithDiffClass(), $diffOutput, $newList->getEndTag()); } protected function buildDiffList($words) { $listType = null; $listStartTag = null; $listEndTag = null; $attributes = array(); $openLists = 0; $openListItems = 0; $list = array(); $currentListItem = null; $listItemType = null; $listItemStart = null; $listItemEnd = null; foreach ($words as $i => $word) { if ($this->isOpeningListTag($word, $listType)) { if ($openLists > 0) { if ($openListItems > 0) { $currentListItem[] = $word; } else { $list[] = $word; } } else { $listType = substr($word, 1, 2); $listStartTag = $word; } $openLists++; } elseif ($this->isClosingListTag($word, $listType)) { if ($openLists > 1) { if ($openListItems > 0) { $currentListItem[] = $word; } else { $list[] = $word; } } else { $listEndTag = $word; } $openLists--; } elseif ($this->isOpeningListItemTag($word, $listItemType)) { if ($openListItems === 0) { // New top-level list item $currentListItem = array(); $listItemType = substr($word, 1, 2); $listItemStart = $word; } else { $currentListItem[] = $word; } $openListItems++; } elseif ($this->isClosingListItemTag($word, $listItemType)) { if ($openListItems === 1) { $listItemEnd = $word; $listItem = new DiffListItem($currentListItem, array(), $listItemStart, $listItemEnd); $list[] = $listItem; $currentListItem = null; } else { $currentListItem[] = $word; } $openListItems--; } else { if ($openListItems > 0) { $currentListItem[] = $word; } else { $list[] = $word; } } } $diffList = new DiffList($listType, $listStartTag, $listEndTag, $list, $attributes); return $diffList; } protected function isOpeningListTag($word, $type = null) { $filter = $type !== null ? array('<' . $type) : array('