setConfig($config);
}
return $diff;
}
/**
* {@inheritDoc}
*/
public function build()
{
$this->prepare();
if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) {
$this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText);
return $this->content;
}
$matchStrategy = new ListItemMatchStrategy($this->config->getMatchThreshold());
$this->lcsService = new LcsService($matchStrategy);
return $this->listByLines($this->oldText, $this->newText);
}
/**
* @param string $old
* @param string $new
*
* @return string
*/
protected function listByLines($old, $new)
{
/* @var $newDom \simple_html_dom */
$newDom = HtmlDomParser::str_get_html($new);
/* @var $oldDom \simple_html_dom */
$oldDom = HtmlDomParser::str_get_html($old);
$newListNode = $this->findListNode($newDom);
$oldListNode = $this->findListNode($oldDom);
$operations = $this->getListItemOperations($oldListNode, $newListNode);
return $this->processOperations($operations, $oldListNode, $newListNode);
}
/**
* @param \simple_html_dom|\simple_html_dom_node $dom
*
* @return \simple_html_dom_node[]|\simple_html_dom_node|null
*/
protected function findListNode($dom)
{
return $dom->find(implode(', ', static::$listTypes), 0);
}
/**
* @param \simple_html_dom_node $oldListNode
* @param \simple_html_dom_node $newListNode
*
* @return array|Operation[]
*/
protected function getListItemOperations($oldListNode, $newListNode)
{
// Prepare arrays of list item content to use in LCS algorithm
$oldListText = $this->getListTextArray($oldListNode);
$newListText = $this->getListTextArray($newListNode);
$lcsMatches = $this->lcsService->longestCommonSubsequence($oldListText, $newListText);
$oldLength = count($oldListText);
$newLength = count($newListText);
$operations = array();
$currentLineInOld = 0;
$currentLineInNew = 0;
$lcsMatches[$oldLength + 1] = $newLength + 1;
foreach ($lcsMatches as $matchInOld => $matchInNew) {
// No matching line in new list
if ($matchInNew === 0) {
continue;
}
$nextLineInOld = $currentLineInOld + 1;
$nextLineInNew = $currentLineInNew + 1;
if ($matchInNew > $nextLineInNew && $matchInOld > $nextLineInOld) {
// Change
$operations[] = new Operation(
Operation::CHANGED,
$nextLineInOld,
$matchInOld - 1,
$nextLineInNew,
$matchInNew - 1
);
} elseif ($matchInNew > $nextLineInNew && $matchInOld === $nextLineInOld) {
// Add items before this
$operations[] = new Operation(
Operation::ADDED,
$currentLineInOld,
$currentLineInOld,
$nextLineInNew,
$matchInNew - 1
);
} elseif ($matchInNew === $nextLineInNew && $matchInOld > $nextLineInOld) {
// Delete items before this
$operations[] = new Operation(
Operation::DELETED,
$nextLineInOld,
$matchInOld - 1,
$currentLineInNew,
$currentLineInNew
);
}
$currentLineInNew = $matchInNew;
$currentLineInOld = $matchInOld;
}
return $operations;
}
/**
* @param \simple_html_dom_node $listNode
*
* @return array
*/
protected function getListTextArray($listNode)
{
$output = array();
foreach ($listNode->children() as $listItem) {
$output[] = $this->getRelevantNodeText($listItem);
}
return $output;
}
/**
* @param \simple_html_dom_node $node
*
* @return string
*/
protected function getRelevantNodeText($node)
{
if (!$node->hasChildNodes()) {
return $node->innertext();
}
$output = '';
foreach ($node->nodes as $child) {
/* @var $child \simple_html_dom_node */
if (!$child->hasChildNodes()) {
$output .= $child->outertext();
} elseif (in_array($child->nodeName(), static::$listContentTags, true)) {
$output .= sprintf('<%1$s>%2$s%1$s>', $child->nodeName(), $this->getRelevantNodeText($child));
}
}
return $output;
}
/**
* @param \simple_html_dom_node $li
*
* @return string
*/
protected function deleteListItem($li)
{
$this->addClassToNode($li, self::CLASS_LIST_ITEM_DELETED);
$li->innertext = sprintf('%s', $li->innertext);
return $li->outertext;
}
/**
* @param \simple_html_dom_node $li
* @param bool $replacement
*
* @return string
*/
protected function addListItem($li, $replacement = false)
{
$this->addClassToNode($li, $replacement ? self::CLASS_LIST_ITEM_CHANGED : self::CLASS_LIST_ITEM_ADDED);
$li->innertext = sprintf('%s', $li->innertext);
return $li->outertext;
}
/**
* @param Operation[]|array $operations
* @param \simple_html_dom_node $oldListNode
* @param \simple_html_dom_node $newListNode
*
* @return string
*/
protected function processOperations($operations, $oldListNode, $newListNode)
{
$output = '';
$indexInOld = 0;
$indexInNew = 0;
$lastOperation = null;
foreach ($operations as $operation) {
$replaced = false;
while ($operation->startInOld > ($operation->action === Operation::ADDED ? $indexInOld : $indexInOld + 1)) {
$li = $oldListNode->children($indexInOld);
$matchingLi = null;
if ($operation->startInNew > ($operation->action === Operation::DELETED ? $indexInNew
: $indexInNew + 1)
) {
$matchingLi = $newListNode->children($indexInNew);
}
if (null !== $matchingLi) {
$htmlDiff = HtmlDiff::create($li->innertext, $matchingLi->innertext, $this->config);
$li->innertext = $htmlDiff->build();
$indexInNew++;
}
$class = self::CLASS_LIST_ITEM_NONE;
if ($lastOperation === Operation::DELETED && !$replaced) {
$class = self::CLASS_LIST_ITEM_CHANGED;
$replaced = true;
}
$li->setAttribute('class', trim($li->getAttribute('class').' '.$class));
$output .= $li->outertext;
$indexInOld++;
}
switch ($operation->action) {
case Operation::ADDED:
for ($i = $operation->startInNew; $i <= $operation->endInNew; $i++) {
$output .= $this->addListItem($newListNode->children($i - 1));
}
$indexInNew = $operation->endInNew;
break;
case Operation::DELETED:
for ($i = $operation->startInOld; $i <= $operation->endInOld; $i++) {
$output .= $this->deleteListItem($oldListNode->children($i - 1));
}
$indexInOld = $operation->endInOld;
break;
case Operation::CHANGED:
$changeDelta = 0;
for ($i = $operation->startInOld; $i <= $operation->endInOld; $i++) {
$output .= $this->deleteListItem($oldListNode->children($i - 1));
$changeDelta--;
}
for ($i = $operation->startInNew; $i <= $operation->endInNew; $i++) {
$output .= $this->addListItem($newListNode->children($i - 1), $changeDelta < 0);
$changeDelta++;
}
$indexInOld = $operation->endInOld;
$indexInNew = $operation->endInNew;
break;
}
$lastOperation = $operation->action;
}
$oldCount = count($oldListNode->children());
$newCount = count($newListNode->children());
while ($indexInOld < $oldCount) {
$li = $oldListNode->children($indexInOld);
$matchingLi = null;
if ($indexInNew < $newCount) {
$matchingLi = $newListNode->children($indexInNew);
}
if (null !== $matchingLi) {
$htmlDiff = HtmlDiff::create($li->innertext(), $matchingLi->innertext(), $this->config);
$li->innertext = $htmlDiff->build();
$indexInNew++;
}
$class = self::CLASS_LIST_ITEM_NONE;
if ($lastOperation === Operation::DELETED) {
$class = self::CLASS_LIST_ITEM_CHANGED;
}
$li->setAttribute('class', trim($li->getAttribute('class').' '.$class));
$output .= $li->outertext;
$indexInOld++;
}
$newListNode->innertext = $output;
$newListNode->setAttribute('class', trim($newListNode->getAttribute('class').' diff-list'));
return $newListNode->outertext;
}
/**
* @param \simple_html_dom_node $node
* @param string $class
*/
protected function addClassToNode($node, $class)
{
$node->setAttribute('class', trim(sprintf('%s %s', $node->getAttribute('class'), $class)));
}
}