/** * @group performance */ public function testParagraphPerformance() { $fixturesPath = __DIR__ . '/../../../../fixtures/Performance/'; $expected = file_get_contents($fixturesPath . 'paragraphs_expected.html'); $diff = new HtmlDiff(file_get_contents($fixturesPath . 'paragraphs.html'), file_get_contents($fixturesPath . 'paragraphs_changed.html'), 'UTF-8', array()); $output = $diff->build(); $this->assertSame($this->stripExtraWhitespaceAndNewLines($output), $this->stripExtraWhitespaceAndNewLines($expected)); }
public function testHtmlDiffConfigStatic() { $oldText = '<b>text</b>'; $newText = '<b>t3xt</b>'; $config = new HtmlDiffConfig(); $config->setPurifierCacheLocation('/tmp'); $diff = HtmlDiff::create($oldText, $newText, $config); $diff->setHTMLPurifierConfig($this->config); $diff->build(); }
public static function diff($old, $new) { $htmlDiff = new HtmlDiff($old, $new); return $htmlDiff->build(); }
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; } $replacement = false; // is there a better old list item match coming up? if ($oldCount > $newCount) { while ($difference > 0 && $this->hasBetterMatch($newMatchData[$newIndex], $oldListIndex)) { $diffOutput .= sprintf('%s', $oldListItems[$oldListIndex]->getHtml('removed', 'del')); ++$currentIndexInOld; --$difference; $oldListIndex = array_key_exists($currentIndexInOld, $oldListIndices) ? $oldListIndices[$currentIndexInOld] : null; $matchPercentage = $oldMatchData[$oldListIndex][$newIndex]; $replacement = true; } } $nextOldListIndex = array_key_exists($currentIndexInOld + 1, $oldListIndices) ? $oldListIndices[$currentIndexInOld + 1] : null; 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][$newIndex]; $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()); }
/** * @dataProvider diffContentProvider * * @param $oldText * @param $newText * @param $expected */ public function testHtmlDiff($oldText, $newText, $expected) { $diff = new HtmlDiff(trim($oldText), trim($newText), 'UTF-8', array()); $output = $diff->build(); static::assertEquals($this->stripExtraWhitespaceAndNewLines($expected), $this->stripExtraWhitespaceAndNewLines($output)); }
/** * @param TableCell|null $oldCell * @param TableCell|null $newCell * @param bool $usingExtraRow * * @return \DOMElement */ protected function diffCells($oldCell, $newCell, $usingExtraRow = false) { $diffCell = $this->getNewCellNode($oldCell, $newCell); $oldContent = $oldCell ? $this->getInnerHtml($oldCell->getDomNode()) : ''; $newContent = $newCell ? $this->getInnerHtml($newCell->getDomNode()) : ''; $htmlDiff = HtmlDiff::create(mb_convert_encoding($oldContent, 'UTF-8', 'HTML-ENTITIES'), mb_convert_encoding($newContent, 'UTF-8', 'HTML-ENTITIES'), $this->config); $diff = $htmlDiff->build(); $this->setInnerHtml($diffCell, $diff); if (null === $newCell) { $diffCell->setAttribute('class', trim($diffCell->getAttribute('class') . ' del')); } if (null === $oldCell) { $diffCell->setAttribute('class', trim($diffCell->getAttribute('class') . ' ins')); } if ($usingExtraRow) { $diffCell->setAttribute('class', trim($diffCell->getAttribute('class') . ' extra-row')); } return $diffCell; }
/** * @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; }
if (!is_string($value)) { $value = var_export($value, true); } if (!array_key_exists($key, $debugOutput)) { $debugOutput[$key] = array(); } $debugOutput[$key][] = $value; } $input = file_get_contents('php://input'); if ($input) { header('Content-Type: application/json'); $data = json_decode($input, true); $oldText = $data['oldText']; $newText = $data['newText']; $useTableDiffing = isset($data['tableDiffing']) ? $data['tableDiffing'] : true; $diff = new HtmlDiff($oldText, $newText, 'UTF-8', array()); if (array_key_exists('matchThreshold', $data)) { $diff->setMatchThreshold($data['matchThreshold']); } $diff->setUseTableDiffing($useTableDiffing); $diffOutput = $diff->build(); $diffOutput = iconv('UTF-8', 'UTF-8//IGNORE', $diffOutput); $jsonOutput = json_encode(array('diff' => $diffOutput, 'debug' => $debugOutput)); if (false === $jsonOutput) { throw new \Exception('Failed to encode JSON: ' . json_last_error_msg()); } echo $jsonOutput; } else { header('Content-Type: text/html'); echo file_get_contents('demo.html'); }
<?php use Caxy\HtmlDiff\HtmlDiff; require __DIR__ . '/../lib/Caxy/HtmlDiff/HtmlDiff.php'; require __DIR__ . '/../lib/Caxy/HtmlDiff/Match.php'; require __DIR__ . '/../lib/Caxy/HtmlDiff/Operation.php'; $input = file_get_contents('php://input'); if ($input) { $data = json_decode($input, true); $diff = new HtmlDiff($data['oldText'], $data['newText']); $diff->build(); header('Content-Type: application/json'); echo json_encode(array('diff' => $diff->getDifference())); } else { header('Content-Type: text/html'); echo file_get_contents('demo.html'); }