Plugin to allow visitor contributions to WordPress posts, wiki style.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

280 lines
10 KiB

<?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);
}
}