281 Zeilen
10 KiB
PHP
281 Zeilen
10 KiB
PHP
<?php
|
|
|
|
namespace Caxy\HtmlDiff;
|
|
|
|
use Caxy\HtmlDiff\ListDiff\DiffList;
|
|
use Caxy\HtmlDiff\ListDiff\DiffListItem;
|
|
|
|
class ListDiffNew extends AbstractDiff
|
|
{
|
|
protected static $listTypes = array('ul', 'ol', 'dl');
|
|
|
|
/**
|
|
* @param string $oldText
|
|
* @param string $newText
|
|
* @param HtmlDiffConfig|null $config
|
|
*
|
|
* @return self
|
|
*/
|
|
public static function create($oldText, $newText, HtmlDiffConfig $config = null)
|
|
{
|
|
$diff = new self($oldText, $newText);
|
|
|
|
if (null !== $config) {
|
|
$diff->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('<ul', '<ol', '<dl');
|
|
return in_array(substr($word, 0, 3), $filter);
|
|
}
|
|
|
|
protected function isClosingListTag($word, $type = null)
|
|
{
|
|
$filter = $type !== null ? array('</' . $type) : array('</ul', '</ol', '</dl');
|
|
|
|
return in_array(substr($word, 0, 4), $filter);
|
|
}
|
|
|
|
protected function isOpeningListItemTag($word, $type = null)
|
|
{
|
|
$filter = $type !== null ? array('<' . $type) : array('<li', '<dd', '<dt');
|
|
|
|
return in_array(substr($word, 0, 3), $filter);
|
|
}
|
|
|
|
protected function isClosingListItemTag($word, $type = null)
|
|
{
|
|
$filter = $type !== null ? array('</' . $type) : array('</li', '</dd', '</dt');
|
|
|
|
return in_array(substr($word, 0, 4), $filter);
|
|
}
|
|
}
|