Make libkolab::html_diff() compatible with PHP8
- use external packages, remove libkolab/vendor - Warning: These might not work on PHP<7.3 - added some tests for the feature
This commit is contained in:
parent
a3bad978e2
commit
158b6c972d
8 changed files with 11 additions and 1500 deletions
|
@ -4,7 +4,8 @@
|
|||
"license": "AGPL-3.0+",
|
||||
"require": {
|
||||
"php": ">=7.2.0",
|
||||
"caxy/php-htmldiff": "~0.1.7",
|
||||
"caxy/php-htmldiff": "~0.1.15",
|
||||
"lolli42/finediff": "~1.0.3",
|
||||
"pear/pear-core-minimal": "~1.10.1",
|
||||
"pear/net_socket": "~1.2.1",
|
||||
"pear/auth_sasl": "~1.1.0",
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"roundcube/plugin-installer": ">=0.1.3",
|
||||
"kolab/libcalendaring": ">=3.4.0",
|
||||
"pear/http_request2": "~2.5.0",
|
||||
"caxy/php-htmldiff": "~0.1.7"
|
||||
"caxy/php-htmldiff": "~0.1.15",
|
||||
"lolli42/finediff": "~1.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -311,8 +311,8 @@ class libkolab extends rcube_plugin
|
|||
{
|
||||
// auto-detect text/html format
|
||||
if ($is_html === null) {
|
||||
$from_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $from, $m) && strpos($from, '</' . $m[1] . '>') > 0);
|
||||
$to_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $to, $m) && strpos($to, '</' . $m[1] . '>') > 0);
|
||||
$from_html = preg_match('/<(html|body)(\s+[a-z]|>)/', $from, $m) && strpos($from, '</' . $m[1] . '>') > 0;
|
||||
$to_html = preg_match('/<(html|body)(\s+[a-z]|>)/', $to, $m) && strpos($to, '</' . $m[1] . '>') > 0;
|
||||
$is_html = $from_html || $to_html;
|
||||
|
||||
// ensure both parts are of the same format
|
||||
|
@ -328,13 +328,10 @@ class libkolab extends rcube_plugin
|
|||
|
||||
// compute diff from HTML
|
||||
if ($is_html) {
|
||||
include_once __dir__ . '/vendor/Caxy/HtmlDiff/Match.php';
|
||||
include_once __dir__ . '/vendor/Caxy/HtmlDiff/Operation.php';
|
||||
include_once __dir__ . '/vendor/Caxy/HtmlDiff/HtmlDiff.php';
|
||||
|
||||
// replace data: urls with a transparent image to avoid memory problems
|
||||
$from = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $from);
|
||||
$to = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $to);
|
||||
$src = 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7';
|
||||
$from = preg_replace('/src="data:image[^"]+/', $src, $from);
|
||||
$to = preg_replace('/src="data:image[^"]+/', $src, $to);
|
||||
|
||||
$diff = new Caxy\HtmlDiff\HtmlDiff($from, $to);
|
||||
$diffhtml = $diff->build();
|
||||
|
@ -342,10 +339,8 @@ class libkolab extends rcube_plugin
|
|||
// remove empty inserts (from tables)
|
||||
return preg_replace('!<ins class="diff\w+">\s*</ins>!Uims', '', $diffhtml);
|
||||
} else {
|
||||
include_once __dir__ . '/vendor/finediff.php';
|
||||
|
||||
$diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
|
||||
return $diff->renderDiffToHTML();
|
||||
$diff = new cogpowered\FineDiff\Diff(new cogpowered\FineDiff\Granularity\Word());
|
||||
return $diff->render($from, $to);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
629
plugins/libkolab/vendor/Caxy/HtmlDiff/HtmlDiff.php
vendored
629
plugins/libkolab/vendor/Caxy/HtmlDiff/HtmlDiff.php
vendored
|
@ -1,629 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Caxy\HtmlDiff;
|
||||
|
||||
class HtmlDiff
|
||||
{
|
||||
public static $defaultSpecialCaseTags = array('strong', 'b', 'i', 'big', 'small', 'u', 'sub', 'sup', 'strike', 's', 'p');
|
||||
public static $defaultSpecialCaseChars = array('.', ',', '(', ')', '\'');
|
||||
public static $defaultGroupDiffs = true;
|
||||
|
||||
protected $content;
|
||||
protected $oldText;
|
||||
protected $newText;
|
||||
protected $oldWords = array();
|
||||
protected $newWords = array();
|
||||
protected $wordIndices;
|
||||
protected $encoding;
|
||||
protected $specialCaseOpeningTags = array();
|
||||
protected $specialCaseClosingTags = array();
|
||||
protected $specialCaseTags;
|
||||
protected $specialCaseChars;
|
||||
protected $groupDiffs;
|
||||
protected $insertSpaceInReplace = false;
|
||||
|
||||
public function __construct($oldText, $newText, $encoding = 'UTF-8', $specialCaseTags = null, $groupDiffs = null)
|
||||
{
|
||||
if ($specialCaseTags === null) {
|
||||
$specialCaseTags = static::$defaultSpecialCaseTags;
|
||||
}
|
||||
|
||||
if ($groupDiffs === null) {
|
||||
$groupDiffs = static::$defaultGroupDiffs;
|
||||
}
|
||||
|
||||
$this->oldText = $this->purifyHtml(trim($oldText));
|
||||
$this->newText = $this->purifyHtml(trim($newText));
|
||||
$this->encoding = $encoding;
|
||||
$this->content = '';
|
||||
$this->groupDiffs = $groupDiffs;
|
||||
$this->setSpecialCaseTags($specialCaseTags);
|
||||
$this->setSpecialCaseChars(static::$defaultSpecialCaseChars);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param boolean $boolean
|
||||
* @return HtmlDiff
|
||||
*/
|
||||
public function setInsertSpaceInReplace($boolean)
|
||||
{
|
||||
$this->insertSpaceInReplace = $boolean;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function getInsertSpaceInReplace()
|
||||
{
|
||||
return $this->insertSpaceInReplace;
|
||||
}
|
||||
|
||||
public function setSpecialCaseChars(array $chars)
|
||||
{
|
||||
$this->specialCaseChars = $chars;
|
||||
}
|
||||
|
||||
public function getSpecialCaseChars()
|
||||
{
|
||||
return $this->specialCaseChars;
|
||||
}
|
||||
|
||||
public function addSpecialCaseChar($char)
|
||||
{
|
||||
if (!in_array($char, $this->specialCaseChars)) {
|
||||
$this->specialCaseChars[] = $char;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeSpecialCaseChar($char)
|
||||
{
|
||||
$key = array_search($char, $this->specialCaseChars);
|
||||
if ($key !== false) {
|
||||
unset($this->specialCaseChars[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
public function setSpecialCaseTags(array $tags = array())
|
||||
{
|
||||
$this->specialCaseTags = $tags;
|
||||
|
||||
foreach ($this->specialCaseTags as $tag) {
|
||||
$this->addSpecialCaseTag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
public function addSpecialCaseTag($tag)
|
||||
{
|
||||
if (!in_array($tag, $this->specialCaseTags)) {
|
||||
$this->specialCaseTags[] = $tag;
|
||||
}
|
||||
|
||||
$opening = $this->getOpeningTag($tag);
|
||||
$closing = $this->getClosingTag($tag);
|
||||
|
||||
if (!in_array($opening, $this->specialCaseOpeningTags)) {
|
||||
$this->specialCaseOpeningTags[] = $opening;
|
||||
}
|
||||
if (!in_array($closing, $this->specialCaseClosingTags)) {
|
||||
$this->specialCaseClosingTags[] = $closing;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeSpecialCaseTag($tag)
|
||||
{
|
||||
if (($key = array_search($tag, $this->specialCaseTags)) !== false) {
|
||||
unset($this->specialCaseTags[$key]);
|
||||
|
||||
$opening = $this->getOpeningTag($tag);
|
||||
$closing = $this->getClosingTag($tag);
|
||||
|
||||
if (($key = array_search($opening, $this->specialCaseOpeningTags)) !== false) {
|
||||
unset($this->specialCaseOpeningTags[$key]);
|
||||
}
|
||||
if (($key = array_search($closing, $this->specialCaseClosingTags)) !== false) {
|
||||
unset($this->specialCaseClosingTags[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getSpecialCaseTags()
|
||||
{
|
||||
return $this->specialCaseTags;
|
||||
}
|
||||
|
||||
public function getOldHtml()
|
||||
{
|
||||
return $this->oldText;
|
||||
}
|
||||
|
||||
public function getNewHtml()
|
||||
{
|
||||
return $this->newText;
|
||||
}
|
||||
|
||||
public function getDifference()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setGroupDiffs($boolean)
|
||||
{
|
||||
$this->groupDiffs = $boolean;
|
||||
}
|
||||
|
||||
public function isGroupDiffs()
|
||||
{
|
||||
return $this->groupDiffs;
|
||||
}
|
||||
|
||||
protected function getOpeningTag($tag)
|
||||
{
|
||||
return "/<".$tag."[^>]*/i";
|
||||
}
|
||||
|
||||
protected function getClosingTag($tag)
|
||||
{
|
||||
return "</".$tag.">";
|
||||
}
|
||||
|
||||
protected function getStringBetween($str, $start, $end)
|
||||
{
|
||||
$expStr = explode( $start, $str, 2 );
|
||||
if ( count( $expStr ) > 1 ) {
|
||||
$expStr = explode( $end, $expStr[ 1 ] );
|
||||
if ( count( $expStr ) > 1 ) {
|
||||
array_pop( $expStr );
|
||||
|
||||
return implode( $end, $expStr );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function purifyHtml($html, $tags = null)
|
||||
{
|
||||
if ( class_exists( 'Tidy' ) && false ) {
|
||||
$config = array( 'output-xhtml' => true, 'indent' => false );
|
||||
$tidy = new tidy;
|
||||
$tidy->parseString( $html, $config, 'utf8' );
|
||||
$html = (string) $tidy;
|
||||
|
||||
return $this->getStringBetween( $html, '<body>' );
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
$this->splitInputsToWords();
|
||||
$this->indexNewWords();
|
||||
$operations = $this->operations();
|
||||
foreach ($operations as $item) {
|
||||
$this->performOperation( $item );
|
||||
}
|
||||
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
protected function indexNewWords()
|
||||
{
|
||||
$this->wordIndices = array();
|
||||
foreach ($this->newWords as $i => $word) {
|
||||
if ( $this->isTag( $word ) ) {
|
||||
$word = $this->stripTagAttributes( $word );
|
||||
}
|
||||
if ( isset( $this->wordIndices[ $word ] ) ) {
|
||||
$this->wordIndices[ $word ][] = $i;
|
||||
} else {
|
||||
$this->wordIndices[ $word ] = array( $i );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function splitInputsToWords()
|
||||
{
|
||||
$this->oldWords = $this->convertHtmlToListOfWords( $this->explode( $this->oldText ) );
|
||||
$this->newWords = $this->convertHtmlToListOfWords( $this->explode( $this->newText ) );
|
||||
}
|
||||
|
||||
protected function isPartOfWord($text)
|
||||
{
|
||||
return ctype_alnum(str_replace($this->specialCaseChars, '', $text));
|
||||
}
|
||||
|
||||
protected function convertHtmlToListOfWords($characterString)
|
||||
{
|
||||
$mode = 'character';
|
||||
$current_word = '';
|
||||
$words = array();
|
||||
foreach ($characterString as $i => $character) {
|
||||
switch ($mode) {
|
||||
case 'character':
|
||||
if ( $this->isStartOfTag( $character ) ) {
|
||||
if ($current_word != '') {
|
||||
$words[] = $current_word;
|
||||
}
|
||||
$current_word = "<";
|
||||
$mode = 'tag';
|
||||
} elseif ( preg_match( "[^\s]", $character ) > 0 ) {
|
||||
if ($current_word != '') {
|
||||
$words[] = $current_word;
|
||||
}
|
||||
$current_word = $character;
|
||||
$mode = 'whitespace';
|
||||
} else {
|
||||
if (
|
||||
(ctype_alnum($character) && (strlen($current_word) == 0 || $this->isPartOfWord($current_word))) ||
|
||||
(in_array($character, $this->specialCaseChars) && isset($characterString[$i+1]) && $this->isPartOfWord($characterString[$i+1]))
|
||||
) {
|
||||
$current_word .= $character;
|
||||
} else {
|
||||
$words[] = $current_word;
|
||||
$current_word = $character;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'tag' :
|
||||
if ( $this->isEndOfTag( $character ) ) {
|
||||
$current_word .= ">";
|
||||
$words[] = $current_word;
|
||||
$current_word = "";
|
||||
|
||||
if ( !preg_match('[^\s]', $character ) ) {
|
||||
$mode = 'whitespace';
|
||||
} else {
|
||||
$mode = 'character';
|
||||
}
|
||||
} else {
|
||||
$current_word .= $character;
|
||||
}
|
||||
break;
|
||||
case 'whitespace':
|
||||
if ( $this->isStartOfTag( $character ) ) {
|
||||
if ($current_word != '') {
|
||||
$words[] = $current_word;
|
||||
}
|
||||
$current_word = "<";
|
||||
$mode = 'tag';
|
||||
} elseif ( preg_match( "[^\s]", $character ) ) {
|
||||
$current_word .= $character;
|
||||
} else {
|
||||
if ($current_word != '') {
|
||||
$words[] = $current_word;
|
||||
}
|
||||
$current_word = $character;
|
||||
$mode = 'character';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($current_word != '') {
|
||||
$words[] = $current_word;
|
||||
}
|
||||
|
||||
return $words;
|
||||
}
|
||||
|
||||
protected function isStartOfTag($val)
|
||||
{
|
||||
return $val == "<";
|
||||
}
|
||||
|
||||
protected function isEndOfTag($val)
|
||||
{
|
||||
return $val == ">";
|
||||
}
|
||||
|
||||
protected function isWhiteSpace($value)
|
||||
{
|
||||
return !preg_match( '[^\s]', $value );
|
||||
}
|
||||
|
||||
protected function explode($value)
|
||||
{
|
||||
// as suggested by @onassar
|
||||
return preg_split( '//u', $value );
|
||||
}
|
||||
|
||||
protected function performOperation($operation)
|
||||
{
|
||||
switch ($operation->action) {
|
||||
case 'equal' :
|
||||
$this->processEqualOperation( $operation );
|
||||
break;
|
||||
case 'delete' :
|
||||
$this->processDeleteOperation( $operation, "diffdel" );
|
||||
break;
|
||||
case 'insert' :
|
||||
$this->processInsertOperation( $operation, "diffins");
|
||||
break;
|
||||
case 'replace':
|
||||
$this->processReplaceOperation( $operation );
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function processReplaceOperation($operation)
|
||||
{
|
||||
$processDelete = strlen($this->oldText) > 0;
|
||||
$processInsert = strlen($this->newText) > 0;
|
||||
|
||||
if ($processDelete) {
|
||||
$this->processDeleteOperation( $operation, "diffmod" );
|
||||
}
|
||||
|
||||
if ($this->insertSpaceInReplace && $processDelete && $processInsert) {
|
||||
$this->content .= ' ';
|
||||
}
|
||||
|
||||
if ($processInsert) {
|
||||
$this->processInsertOperation( $operation, "diffmod" );
|
||||
}
|
||||
}
|
||||
|
||||
protected function processInsertOperation($operation, $cssClass)
|
||||
{
|
||||
$text = array();
|
||||
foreach ($this->newWords as $pos => $s) {
|
||||
if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
|
||||
$text[] = $s;
|
||||
}
|
||||
}
|
||||
$this->insertTag( "ins", $cssClass, $text );
|
||||
}
|
||||
|
||||
protected function processDeleteOperation($operation, $cssClass)
|
||||
{
|
||||
$text = array();
|
||||
foreach ($this->oldWords as $pos => $s) {
|
||||
if ($pos >= $operation->startInOld && $pos < $operation->endInOld) {
|
||||
$text[] = $s;
|
||||
}
|
||||
}
|
||||
$this->insertTag( "del", $cssClass, $text );
|
||||
}
|
||||
|
||||
protected function processEqualOperation($operation)
|
||||
{
|
||||
$result = array();
|
||||
foreach ($this->newWords as $pos => $s) {
|
||||
if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
|
||||
$result[] = $s;
|
||||
}
|
||||
}
|
||||
$this->content .= implode( "", $result );
|
||||
}
|
||||
|
||||
protected function insertTag($tag, $cssClass, &$words)
|
||||
{
|
||||
while (true) {
|
||||
if ( count( $words ) == 0 ) {
|
||||
break;
|
||||
}
|
||||
|
||||
$nonTags = $this->extractConsecutiveWords( $words, 'noTag' );
|
||||
|
||||
$specialCaseTagInjection = '';
|
||||
$specialCaseTagInjectionIsBefore = false;
|
||||
|
||||
if ( count( $nonTags ) != 0 ) {
|
||||
$text = $this->wrapText( implode( "", $nonTags ), $tag, $cssClass );
|
||||
$this->content .= $text;
|
||||
} else {
|
||||
$firstOrDefault = false;
|
||||
foreach ($this->specialCaseOpeningTags as $x) {
|
||||
if ( preg_match( $x, $words[ 0 ] ) ) {
|
||||
$firstOrDefault = $x;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($firstOrDefault) {
|
||||
$specialCaseTagInjection = '<ins class="mod">';
|
||||
if ($tag == "del") {
|
||||
unset( $words[ 0 ] );
|
||||
}
|
||||
} elseif ( array_search( $words[ 0 ], $this->specialCaseClosingTags ) !== false ) {
|
||||
$specialCaseTagInjection = "</ins>";
|
||||
$specialCaseTagInjectionIsBefore = true;
|
||||
if ($tag == "del") {
|
||||
unset( $words[ 0 ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( count( $words ) == 0 && count( $specialCaseTagInjection ) == 0 ) {
|
||||
break;
|
||||
}
|
||||
if ($specialCaseTagInjectionIsBefore) {
|
||||
$this->content .= $specialCaseTagInjection . implode( "", $this->extractConsecutiveWords( $words, 'tag' ) );
|
||||
} else {
|
||||
$workTag = $this->extractConsecutiveWords( $words, 'tag' );
|
||||
if ( isset( $workTag[ 0 ] ) && $this->isOpeningTag( $workTag[ 0 ] ) && !$this->isClosingTag( $workTag[ 0 ] ) ) {
|
||||
if ( strpos( $workTag[ 0 ], 'class=' ) ) {
|
||||
$workTag[ 0 ] = str_replace( 'class="', 'class="diffmod ', $workTag[ 0 ] );
|
||||
$workTag[ 0 ] = str_replace( "class='", 'class="diffmod ', $workTag[ 0 ] );
|
||||
} else {
|
||||
$workTag[ 0 ] = str_replace( ">", ' class="diffmod">', $workTag[ 0 ] );
|
||||
}
|
||||
}
|
||||
$this->content .= implode( "", $workTag ) . $specialCaseTagInjection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkCondition($word, $condition)
|
||||
{
|
||||
return $condition == 'tag' ? $this->isTag( $word ) : !$this->isTag( $word );
|
||||
}
|
||||
|
||||
protected function wrapText($text, $tagName, $cssClass)
|
||||
{
|
||||
return sprintf( '<%1$s class="%2$s">%3$s</%1$s>', $tagName, $cssClass, $text );
|
||||
}
|
||||
|
||||
protected function extractConsecutiveWords(&$words, $condition)
|
||||
{
|
||||
$indexOfFirstTag = null;
|
||||
foreach ($words as $i => $word) {
|
||||
if ( !$this->checkCondition( $word, $condition ) ) {
|
||||
$indexOfFirstTag = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($indexOfFirstTag !== null) {
|
||||
$items = array();
|
||||
foreach ($words as $pos => $s) {
|
||||
if ($pos >= 0 && $pos < $indexOfFirstTag) {
|
||||
$items[] = $s;
|
||||
}
|
||||
}
|
||||
if ($indexOfFirstTag > 0) {
|
||||
array_splice( $words, 0, $indexOfFirstTag );
|
||||
}
|
||||
|
||||
return $items;
|
||||
} else {
|
||||
$items = array();
|
||||
foreach ($words as $pos => $s) {
|
||||
if ( $pos >= 0 && $pos <= count( $words ) ) {
|
||||
$items[] = $s;
|
||||
}
|
||||
}
|
||||
array_splice( $words, 0, count( $words ) );
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
|
||||
protected function isTag($item)
|
||||
{
|
||||
return $this->isOpeningTag( $item ) || $this->isClosingTag( $item );
|
||||
}
|
||||
|
||||
protected function isOpeningTag($item)
|
||||
{
|
||||
return preg_match( "#<[^>]+>\\s*#iU", $item );
|
||||
}
|
||||
|
||||
protected function isClosingTag($item)
|
||||
{
|
||||
return preg_match( "#</[^>]+>\\s*#iU", $item );
|
||||
}
|
||||
|
||||
protected function operations()
|
||||
{
|
||||
$positionInOld = 0;
|
||||
$positionInNew = 0;
|
||||
$operations = array();
|
||||
$matches = $this->matchingBlocks();
|
||||
$matches[] = new Match( count( $this->oldWords ), count( $this->newWords ), 0 );
|
||||
foreach ($matches as $i => $match) {
|
||||
$matchStartsAtCurrentPositionInOld = ( $positionInOld == $match->startInOld );
|
||||
$matchStartsAtCurrentPositionInNew = ( $positionInNew == $match->startInNew );
|
||||
$action = 'none';
|
||||
|
||||
if ($matchStartsAtCurrentPositionInOld == false && $matchStartsAtCurrentPositionInNew == false) {
|
||||
$action = 'replace';
|
||||
} elseif ($matchStartsAtCurrentPositionInOld == true && $matchStartsAtCurrentPositionInNew == false) {
|
||||
$action = 'insert';
|
||||
} elseif ($matchStartsAtCurrentPositionInOld == false && $matchStartsAtCurrentPositionInNew == true) {
|
||||
$action = 'delete';
|
||||
} else { // This occurs if the first few words are the same in both versions
|
||||
$action = 'none';
|
||||
}
|
||||
if ($action != 'none') {
|
||||
$operations[] = new Operation( $action, $positionInOld, $match->startInOld, $positionInNew, $match->startInNew );
|
||||
}
|
||||
if ( count( $match ) != 0 ) {
|
||||
$operations[] = new Operation( 'equal', $match->startInOld, $match->endInOld(), $match->startInNew, $match->endInNew() );
|
||||
}
|
||||
$positionInOld = $match->endInOld();
|
||||
$positionInNew = $match->endInNew();
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
|
||||
protected function matchingBlocks()
|
||||
{
|
||||
$matchingBlocks = array();
|
||||
$this->findMatchingBlocks( 0, count( $this->oldWords ), 0, count( $this->newWords ), $matchingBlocks );
|
||||
|
||||
return $matchingBlocks;
|
||||
}
|
||||
|
||||
protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
|
||||
{
|
||||
$match = $this->findMatch( $startInOld, $endInOld, $startInNew, $endInNew );
|
||||
if ($match !== null) {
|
||||
if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) {
|
||||
$this->findMatchingBlocks( $startInOld, $match->startInOld, $startInNew, $match->startInNew, $matchingBlocks );
|
||||
}
|
||||
$matchingBlocks[] = $match;
|
||||
if ( $match->endInOld() < $endInOld && $match->endInNew() < $endInNew ) {
|
||||
$this->findMatchingBlocks( $match->endInOld(), $endInOld, $match->endInNew(), $endInNew, $matchingBlocks );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function stripTagAttributes($word)
|
||||
{
|
||||
$word = explode( ' ', trim( $word, '<>' ) );
|
||||
|
||||
return '<' . $word[ 0 ] . '>';
|
||||
}
|
||||
|
||||
protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
|
||||
{
|
||||
$bestMatchInOld = $startInOld;
|
||||
$bestMatchInNew = $startInNew;
|
||||
$bestMatchSize = 0;
|
||||
$matchLengthAt = array();
|
||||
for ($indexInOld = $startInOld; $indexInOld < $endInOld; $indexInOld++) {
|
||||
$newMatchLengthAt = array();
|
||||
$index = $this->oldWords[ $indexInOld ];
|
||||
if ( $this->isTag( $index ) ) {
|
||||
$index = $this->stripTagAttributes( $index );
|
||||
}
|
||||
if ( !isset( $this->wordIndices[ $index ] ) ) {
|
||||
$matchLengthAt = $newMatchLengthAt;
|
||||
continue;
|
||||
}
|
||||
foreach ($this->wordIndices[ $index ] as $indexInNew) {
|
||||
if ($indexInNew < $startInNew) {
|
||||
continue;
|
||||
}
|
||||
if ($indexInNew >= $endInNew) {
|
||||
break;
|
||||
}
|
||||
$newMatchLength = ( isset( $matchLengthAt[ $indexInNew - 1 ] ) ? $matchLengthAt[ $indexInNew - 1 ] : 0 ) + 1;
|
||||
$newMatchLengthAt[ $indexInNew ] = $newMatchLength;
|
||||
if ($newMatchLength > $bestMatchSize) {
|
||||
$bestMatchInOld = $indexInOld - $newMatchLength + 1;
|
||||
$bestMatchInNew = $indexInNew - $newMatchLength + 1;
|
||||
$bestMatchSize = $newMatchLength;
|
||||
}
|
||||
}
|
||||
$matchLengthAt = $newMatchLengthAt;
|
||||
}
|
||||
|
||||
// Skip match if none found or match consists only of whitespace
|
||||
if ($bestMatchSize != 0 &&
|
||||
(
|
||||
!$this->isGroupDiffs() ||
|
||||
!preg_match('/^\s+$/', implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize)))
|
||||
)
|
||||
) {
|
||||
return new Match($bestMatchInOld, $bestMatchInNew, $bestMatchSize);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
27
plugins/libkolab/vendor/Caxy/HtmlDiff/Match.php
vendored
27
plugins/libkolab/vendor/Caxy/HtmlDiff/Match.php
vendored
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Caxy\HtmlDiff;
|
||||
|
||||
class Match
|
||||
{
|
||||
public $startInOld;
|
||||
public $startInNew;
|
||||
public $size;
|
||||
|
||||
public function __construct($startInOld, $startInNew, $size)
|
||||
{
|
||||
$this->startInOld = $startInOld;
|
||||
$this->startInNew = $startInNew;
|
||||
$this->size = $size;
|
||||
}
|
||||
|
||||
public function endInOld()
|
||||
{
|
||||
return $this->startInOld + $this->size;
|
||||
}
|
||||
|
||||
public function endInNew()
|
||||
{
|
||||
return $this->startInNew + $this->size;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Caxy\HtmlDiff;
|
||||
|
||||
class Operation
|
||||
{
|
||||
public $action;
|
||||
public $startInOld;
|
||||
public $endInOld;
|
||||
public $startInNew;
|
||||
public $endInNew;
|
||||
|
||||
public function __construct($action, $startInOld, $endInOld, $startInNew, $endInNew)
|
||||
{
|
||||
$this->action = $action;
|
||||
$this->startInOld = $startInOld;
|
||||
$this->endInOld = $endInOld;
|
||||
$this->startInNew = $startInNew;
|
||||
$this->endInNew = $endInNew;
|
||||
}
|
||||
}
|
688
plugins/libkolab/vendor/finediff.php
vendored
688
plugins/libkolab/vendor/finediff.php
vendored
|
@ -1,688 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* FINE granularity DIFF
|
||||
*
|
||||
* Computes a set of instructions to convert the content of
|
||||
* one string into another.
|
||||
*
|
||||
* Copyright (c) 2011 Raymond Hill (http://raymondhill.net/blog/?p=441)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @copyright Copyright 2011 (c) Raymond Hill (http://raymondhill.net/blog/?p=441)
|
||||
* @link http://www.raymondhill.net/finediff/
|
||||
* @version 0.6
|
||||
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Usage (simplest):
|
||||
*
|
||||
* include 'finediff.php';
|
||||
*
|
||||
* // for the stock stack, granularity values are:
|
||||
* // FineDiff::$paragraphGranularity = paragraph/line level
|
||||
* // FineDiff::$sentenceGranularity = sentence level
|
||||
* // FineDiff::$wordGranularity = word level
|
||||
* // FineDiff::$characterGranularity = character level [default]
|
||||
*
|
||||
* $opcodes = FineDiff::getDiffOpcodes($from_text, $to_text [, $granularityStack = null] );
|
||||
* // store opcodes for later use...
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* // restore $to_text from $from_text + $opcodes
|
||||
* include 'finediff.php';
|
||||
* $to_text = FineDiff::renderToTextFromOpcodes($from_text, $opcodes);
|
||||
*
|
||||
* ...
|
||||
*/
|
||||
|
||||
/**
|
||||
* Persisted opcodes (string) are a sequence of atomic opcode.
|
||||
* A single opcode can be one of the following:
|
||||
* c | c{n} | d | d{n} | i:{c} | i{length}:{s}
|
||||
* 'c' = copy one character from source
|
||||
* 'c{n}' = copy n characters from source
|
||||
* 'd' = skip one character from source
|
||||
* 'd{n}' = skip n characters from source
|
||||
* 'i:{c} = insert character 'c'
|
||||
* 'i{n}:{s}' = insert string s, which is of length n
|
||||
*
|
||||
* Do not exist as of now, under consideration:
|
||||
* 'm{n}:{o} = move n characters from source o characters ahead.
|
||||
* It would be essentially a shortcut for a delete->copy->insert
|
||||
* command (swap) for when the inserted segment is exactly the same
|
||||
* as the deleted one, and with only a copy operation in between.
|
||||
* TODO: How often this case occurs? Is it worth it? Can only
|
||||
* be done as a postprocessing method (->optimize()?)
|
||||
*/
|
||||
abstract class FineDiffOp {
|
||||
abstract public function getFromLen();
|
||||
abstract public function getToLen();
|
||||
abstract public function getOpcode();
|
||||
}
|
||||
|
||||
class FineDiffDeleteOp extends FineDiffOp {
|
||||
public function __construct($len) {
|
||||
$this->fromLen = $len;
|
||||
}
|
||||
public function getFromLen() {
|
||||
return $this->fromLen;
|
||||
}
|
||||
public function getToLen() {
|
||||
return 0;
|
||||
}
|
||||
public function getOpcode() {
|
||||
if ( $this->fromLen === 1 ) {
|
||||
return 'd';
|
||||
}
|
||||
return "d{$this->fromLen}";
|
||||
}
|
||||
}
|
||||
|
||||
class FineDiffInsertOp extends FineDiffOp {
|
||||
public function __construct($text) {
|
||||
$this->text = $text;
|
||||
}
|
||||
public function getFromLen() {
|
||||
return 0;
|
||||
}
|
||||
public function getToLen() {
|
||||
return strlen($this->text);
|
||||
}
|
||||
public function getText() {
|
||||
return $this->text;
|
||||
}
|
||||
public function getOpcode() {
|
||||
$to_len = strlen($this->text);
|
||||
if ( $to_len === 1 ) {
|
||||
return "i:{$this->text}";
|
||||
}
|
||||
return "i{$to_len}:{$this->text}";
|
||||
}
|
||||
}
|
||||
|
||||
class FineDiffReplaceOp extends FineDiffOp {
|
||||
public function __construct($fromLen, $text) {
|
||||
$this->fromLen = $fromLen;
|
||||
$this->text = $text;
|
||||
}
|
||||
public function getFromLen() {
|
||||
return $this->fromLen;
|
||||
}
|
||||
public function getToLen() {
|
||||
return strlen($this->text);
|
||||
}
|
||||
public function getText() {
|
||||
return $this->text;
|
||||
}
|
||||
public function getOpcode() {
|
||||
if ( $this->fromLen === 1 ) {
|
||||
$del_opcode = 'd';
|
||||
}
|
||||
else {
|
||||
$del_opcode = "d{$this->fromLen}";
|
||||
}
|
||||
$to_len = strlen($this->text);
|
||||
if ( $to_len === 1 ) {
|
||||
return "{$del_opcode}i:{$this->text}";
|
||||
}
|
||||
return "{$del_opcode}i{$to_len}:{$this->text}";
|
||||
}
|
||||
}
|
||||
|
||||
class FineDiffCopyOp extends FineDiffOp {
|
||||
public function __construct($len) {
|
||||
$this->len = $len;
|
||||
}
|
||||
public function getFromLen() {
|
||||
return $this->len;
|
||||
}
|
||||
public function getToLen() {
|
||||
return $this->len;
|
||||
}
|
||||
public function getOpcode() {
|
||||
if ( $this->len === 1 ) {
|
||||
return 'c';
|
||||
}
|
||||
return "c{$this->len}";
|
||||
}
|
||||
public function increase($size) {
|
||||
return $this->len += $size;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FineDiff ops
|
||||
*
|
||||
* Collection of ops
|
||||
*/
|
||||
class FineDiffOps {
|
||||
public function appendOpcode($opcode, $from, $from_offset, $from_len) {
|
||||
if ( $opcode === 'c' ) {
|
||||
$edits[] = new FineDiffCopyOp($from_len);
|
||||
}
|
||||
else if ( $opcode === 'd' ) {
|
||||
$edits[] = new FineDiffDeleteOp($from_len);
|
||||
}
|
||||
else /* if ( $opcode === 'i' ) */ {
|
||||
$edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len));
|
||||
}
|
||||
}
|
||||
public $edits = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* FineDiff class
|
||||
*
|
||||
* TODO: Document
|
||||
*
|
||||
*/
|
||||
class FineDiff {
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
*
|
||||
* Public section
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* ...
|
||||
* The $granularityStack allows FineDiff to be configurable so that
|
||||
* a particular stack tailored to the specific content of a document can
|
||||
* be passed.
|
||||
*/
|
||||
public function __construct($from_text = '', $to_text = '', $granularityStack = null) {
|
||||
// setup stack for generic text documents by default
|
||||
$this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity;
|
||||
$this->edits = array();
|
||||
$this->from_text = $from_text;
|
||||
$this->doDiff($from_text, $to_text);
|
||||
}
|
||||
|
||||
public function getOps() {
|
||||
return $this->edits;
|
||||
}
|
||||
|
||||
public function getOpcodes() {
|
||||
$opcodes = array();
|
||||
foreach ( $this->edits as $edit ) {
|
||||
$opcodes[] = $edit->getOpcode();
|
||||
}
|
||||
return implode('', $opcodes);
|
||||
}
|
||||
|
||||
public function renderDiffToHTML() {
|
||||
$in_offset = 0;
|
||||
$html = '';
|
||||
foreach ( $this->edits as $edit ) {
|
||||
$n = $edit->getFromLen();
|
||||
if ( $edit instanceof FineDiffCopyOp ) {
|
||||
$html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
|
||||
}
|
||||
else if ( $edit instanceof FineDiffDeleteOp ) {
|
||||
$html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
|
||||
}
|
||||
else if ( $edit instanceof FineDiffInsertOp ) {
|
||||
$html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
|
||||
}
|
||||
else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
|
||||
$html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
|
||||
$html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
|
||||
}
|
||||
$in_offset += $n;
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Return an opcodes string describing the diff between a "From" and a
|
||||
* "To" string
|
||||
*/
|
||||
public static function getDiffOpcodes($from, $to, $granularities = null) {
|
||||
$diff = new FineDiff($from, $to, $granularities);
|
||||
return $diff->getOpcodes();
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Return an iterable collection of diff ops from an opcodes string
|
||||
*/
|
||||
public static function getDiffOpsFromOpcodes($opcodes) {
|
||||
$diffops = new FineDiffOps();
|
||||
FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode'));
|
||||
return $diffops->edits;
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Re-create the "To" string from the "From" string and an "Opcodes" string
|
||||
*/
|
||||
public static function renderToTextFromOpcodes($from, $opcodes) {
|
||||
return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Render the diff to an HTML string
|
||||
*/
|
||||
public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
|
||||
return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Generic opcodes parser, user must supply callback for handling
|
||||
* single opcode
|
||||
*/
|
||||
public static function renderFromOpcodes($from, $opcodes, $callback) {
|
||||
if ( !is_callable($callback) ) {
|
||||
return '';
|
||||
}
|
||||
$out = '';
|
||||
$opcodes_len = strlen($opcodes);
|
||||
$from_offset = $opcodes_offset = 0;
|
||||
while ( $opcodes_offset < $opcodes_len ) {
|
||||
$opcode = substr($opcodes, $opcodes_offset, 1);
|
||||
$opcodes_offset++;
|
||||
$n = intval(substr($opcodes, $opcodes_offset));
|
||||
if ( $n ) {
|
||||
$opcodes_offset += strlen(strval($n));
|
||||
}
|
||||
else {
|
||||
$n = 1;
|
||||
}
|
||||
if ( $opcode === 'c' ) { // copy n characters from source
|
||||
$out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
|
||||
$from_offset += $n;
|
||||
}
|
||||
else if ( $opcode === 'd' ) { // delete n characters from source
|
||||
$out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
|
||||
$from_offset += $n;
|
||||
}
|
||||
else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
|
||||
$out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
|
||||
$opcodes_offset += 1 + $n;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stock granularity stacks and delimiters
|
||||
*/
|
||||
|
||||
const paragraphDelimiters = "\n\r";
|
||||
public static $paragraphGranularity = array(
|
||||
FineDiff::paragraphDelimiters
|
||||
);
|
||||
const sentenceDelimiters = ".\n\r";
|
||||
public static $sentenceGranularity = array(
|
||||
FineDiff::paragraphDelimiters,
|
||||
FineDiff::sentenceDelimiters
|
||||
);
|
||||
const wordDelimiters = " \t.\n\r";
|
||||
public static $wordGranularity = array(
|
||||
FineDiff::paragraphDelimiters,
|
||||
FineDiff::sentenceDelimiters,
|
||||
FineDiff::wordDelimiters
|
||||
);
|
||||
const characterDelimiters = "";
|
||||
public static $characterGranularity = array(
|
||||
FineDiff::paragraphDelimiters,
|
||||
FineDiff::sentenceDelimiters,
|
||||
FineDiff::wordDelimiters,
|
||||
FineDiff::characterDelimiters
|
||||
);
|
||||
|
||||
public static $textStack = array(
|
||||
".",
|
||||
" \t.\n\r",
|
||||
""
|
||||
);
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
*
|
||||
* Private section
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Entry point to compute the diff.
|
||||
*/
|
||||
private function doDiff($from_text, $to_text) {
|
||||
$this->last_edit = false;
|
||||
$this->stackpointer = 0;
|
||||
$this->from_text = $from_text;
|
||||
$this->from_offset = 0;
|
||||
// can't diff without at least one granularity specifier
|
||||
if ( empty($this->granularityStack) ) {
|
||||
return;
|
||||
}
|
||||
$this->_processGranularity($from_text, $to_text);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the recursive function which is responsible for
|
||||
* handling/increasing granularity.
|
||||
*
|
||||
* Incrementally increasing the granularity is key to compute the
|
||||
* overall diff in a very efficient way.
|
||||
*/
|
||||
private function _processGranularity($from_segment, $to_segment) {
|
||||
$delimiters = $this->granularityStack[$this->stackpointer++];
|
||||
$has_next_stage = $this->stackpointer < count($this->granularityStack);
|
||||
foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) {
|
||||
// increase granularity
|
||||
if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) {
|
||||
$this->_processGranularity(
|
||||
substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()),
|
||||
$fragment_edit->getText()
|
||||
);
|
||||
}
|
||||
// fuse copy ops whenever possible
|
||||
else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) {
|
||||
$this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen());
|
||||
$this->from_offset += $fragment_edit->getFromLen();
|
||||
}
|
||||
else {
|
||||
/* $fragment_edit instanceof FineDiffCopyOp */
|
||||
/* $fragment_edit instanceof FineDiffDeleteOp */
|
||||
/* $fragment_edit instanceof FineDiffInsertOp */
|
||||
$this->edits[] = $this->last_edit = $fragment_edit;
|
||||
$this->from_offset += $fragment_edit->getFromLen();
|
||||
}
|
||||
}
|
||||
$this->stackpointer--;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the core algorithm which actually perform the diff itself,
|
||||
* fragmenting the strings as per specified delimiters.
|
||||
*
|
||||
* This function is naturally recursive, however for performance purpose
|
||||
* a local job queue is used instead of outright recursivity.
|
||||
*/
|
||||
private static function doFragmentDiff($from_text, $to_text, $delimiters) {
|
||||
// Empty delimiter means character-level diffing.
|
||||
// In such case, use code path optimized for character-level
|
||||
// diffing.
|
||||
if ( empty($delimiters) ) {
|
||||
return FineDiff::doCharDiff($from_text, $to_text);
|
||||
}
|
||||
|
||||
$result = array();
|
||||
|
||||
// fragment-level diffing
|
||||
$from_text_len = strlen($from_text);
|
||||
$to_text_len = strlen($to_text);
|
||||
$from_fragments = FineDiff::extractFragments($from_text, $delimiters);
|
||||
$to_fragments = FineDiff::extractFragments($to_text, $delimiters);
|
||||
|
||||
$jobs = array(array(0, $from_text_len, 0, $to_text_len));
|
||||
|
||||
$cached_array_keys = array();
|
||||
|
||||
while ( $job = array_pop($jobs) ) {
|
||||
|
||||
// get the segments which must be diff'ed
|
||||
list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
|
||||
|
||||
// catch easy cases first
|
||||
$from_segment_length = $from_segment_end - $from_segment_start;
|
||||
$to_segment_length = $to_segment_end - $to_segment_start;
|
||||
if ( !$from_segment_length || !$to_segment_length ) {
|
||||
if ( $from_segment_length ) {
|
||||
$result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length);
|
||||
}
|
||||
else if ( $to_segment_length ) {
|
||||
$result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// find longest copy operation for the current segments
|
||||
$best_copy_length = 0;
|
||||
|
||||
$from_base_fragment_index = $from_segment_start;
|
||||
|
||||
$cached_array_keys_for_current_segment = array();
|
||||
|
||||
while ( $from_base_fragment_index < $from_segment_end ) {
|
||||
$from_base_fragment = $from_fragments[$from_base_fragment_index];
|
||||
$from_base_fragment_length = strlen($from_base_fragment);
|
||||
// performance boost: cache array keys
|
||||
if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) {
|
||||
if ( !isset($cached_array_keys[$from_base_fragment]) ) {
|
||||
$to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true);
|
||||
}
|
||||
else {
|
||||
$to_all_fragment_indices = $cached_array_keys[$from_base_fragment];
|
||||
}
|
||||
// get only indices which falls within current segment
|
||||
if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) {
|
||||
$to_fragment_indices = array();
|
||||
foreach ( $to_all_fragment_indices as $to_fragment_index ) {
|
||||
if ( $to_fragment_index < $to_segment_start ) { continue; }
|
||||
if ( $to_fragment_index >= $to_segment_end ) { break; }
|
||||
$to_fragment_indices[] = $to_fragment_index;
|
||||
}
|
||||
$cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices;
|
||||
}
|
||||
else {
|
||||
$to_fragment_indices = $to_all_fragment_indices;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment];
|
||||
}
|
||||
// iterate through collected indices
|
||||
foreach ( $to_fragment_indices as $to_base_fragment_index ) {
|
||||
$fragment_index_offset = $from_base_fragment_length;
|
||||
// iterate until no more match
|
||||
for (;;) {
|
||||
$fragment_from_index = $from_base_fragment_index + $fragment_index_offset;
|
||||
if ( $fragment_from_index >= $from_segment_end ) {
|
||||
break;
|
||||
}
|
||||
$fragment_to_index = $to_base_fragment_index + $fragment_index_offset;
|
||||
if ( $fragment_to_index >= $to_segment_end ) {
|
||||
break;
|
||||
}
|
||||
if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) {
|
||||
break;
|
||||
}
|
||||
$fragment_length = strlen($from_fragments[$fragment_from_index]);
|
||||
$fragment_index_offset += $fragment_length;
|
||||
}
|
||||
if ( $fragment_index_offset > $best_copy_length ) {
|
||||
$best_copy_length = $fragment_index_offset;
|
||||
$best_from_start = $from_base_fragment_index;
|
||||
$best_to_start = $to_base_fragment_index;
|
||||
}
|
||||
}
|
||||
$from_base_fragment_index += strlen($from_base_fragment);
|
||||
// If match is larger than half segment size, no point trying to find better
|
||||
// TODO: Really?
|
||||
if ( $best_copy_length >= $from_segment_length / 2) {
|
||||
break;
|
||||
}
|
||||
// no point to keep looking if what is left is less than
|
||||
// current best match
|
||||
if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $best_copy_length ) {
|
||||
$jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start);
|
||||
$result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length);
|
||||
$jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end);
|
||||
}
|
||||
else {
|
||||
$result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length));
|
||||
}
|
||||
}
|
||||
|
||||
ksort($result, SORT_NUMERIC);
|
||||
return array_values($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a character-level diff.
|
||||
*
|
||||
* The algorithm is quite similar to doFragmentDiff(), except that
|
||||
* the code path is optimized for character-level diff -- strpos() is
|
||||
* used to find out the longest common subequence of characters.
|
||||
*
|
||||
* We try to find a match using the longest possible subsequence, which
|
||||
* is at most the length of the shortest of the two strings, then incrementally
|
||||
* reduce the size until a match is found.
|
||||
*
|
||||
* I still need to study more the performance of this function. It
|
||||
* appears that for long strings, the generic doFragmentDiff() is more
|
||||
* performant. For word-sized strings, doCharDiff() is somewhat more
|
||||
* performant.
|
||||
*/
|
||||
private static function doCharDiff($from_text, $to_text) {
|
||||
$result = array();
|
||||
$jobs = array(array(0, strlen($from_text), 0, strlen($to_text)));
|
||||
while ( $job = array_pop($jobs) ) {
|
||||
// get the segments which must be diff'ed
|
||||
list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
|
||||
$from_segment_len = $from_segment_end - $from_segment_start;
|
||||
$to_segment_len = $to_segment_end - $to_segment_start;
|
||||
|
||||
// catch easy cases first
|
||||
if ( !$from_segment_len || !$to_segment_len ) {
|
||||
if ( $from_segment_len ) {
|
||||
$result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len);
|
||||
}
|
||||
else if ( $to_segment_len ) {
|
||||
$result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ( $from_segment_len >= $to_segment_len ) {
|
||||
$copy_len = $to_segment_len;
|
||||
while ( $copy_len ) {
|
||||
$to_copy_start = $to_segment_start;
|
||||
$to_copy_start_max = $to_segment_end - $copy_len;
|
||||
while ( $to_copy_start <= $to_copy_start_max ) {
|
||||
$from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len));
|
||||
if ( $from_copy_start !== false ) {
|
||||
$from_copy_start += $from_segment_start;
|
||||
break 2;
|
||||
}
|
||||
$to_copy_start++;
|
||||
}
|
||||
$copy_len--;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$copy_len = $from_segment_len;
|
||||
while ( $copy_len ) {
|
||||
$from_copy_start = $from_segment_start;
|
||||
$from_copy_start_max = $from_segment_end - $copy_len;
|
||||
while ( $from_copy_start <= $from_copy_start_max ) {
|
||||
$to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len));
|
||||
if ( $to_copy_start !== false ) {
|
||||
$to_copy_start += $to_segment_start;
|
||||
break 2;
|
||||
}
|
||||
$from_copy_start++;
|
||||
}
|
||||
$copy_len--;
|
||||
}
|
||||
}
|
||||
// match found
|
||||
if ( $copy_len ) {
|
||||
$jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start);
|
||||
$result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len);
|
||||
$jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end);
|
||||
}
|
||||
// no match, so delete all, insert all
|
||||
else {
|
||||
$result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len));
|
||||
}
|
||||
}
|
||||
ksort($result, SORT_NUMERIC);
|
||||
return array_values($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently fragment the text into an array according to
|
||||
* specified delimiters.
|
||||
* No delimiters means fragment into single character.
|
||||
* The array indices are the offset of the fragments into
|
||||
* the input string.
|
||||
* A sentinel empty fragment is always added at the end.
|
||||
* Careful: No check is performed as to the validity of the
|
||||
* delimiters.
|
||||
*/
|
||||
private static function extractFragments($text, $delimiters) {
|
||||
// special case: split into characters
|
||||
if ( empty($delimiters) ) {
|
||||
$chars = str_split($text, 1);
|
||||
$chars[strlen($text)] = '';
|
||||
return $chars;
|
||||
}
|
||||
$fragments = array();
|
||||
$start = $end = 0;
|
||||
for (;;) {
|
||||
$end += strcspn($text, $delimiters, $end);
|
||||
$end += strspn($text, $delimiters, $end);
|
||||
if ( $end === $start ) {
|
||||
break;
|
||||
}
|
||||
$fragments[$start] = substr($text, $start, $end - $start);
|
||||
$start = $end;
|
||||
}
|
||||
$fragments[$start] = '';
|
||||
return $fragments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stock opcode renderers
|
||||
*/
|
||||
private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
|
||||
if ( $opcode === 'c' || $opcode === 'i' ) {
|
||||
return substr($from, $from_offset, $from_len);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
|
||||
if ( $opcode === 'c' ) {
|
||||
return htmlentities(substr($from, $from_offset, $from_len));
|
||||
}
|
||||
else if ( $opcode === 'd' ) {
|
||||
$deletion = substr($from, $from_offset, $from_len);
|
||||
if ( strcspn($deletion, " \n\r") === 0 ) {
|
||||
$deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
|
||||
}
|
||||
return '<del>' . htmlentities($deletion) . '</del>';
|
||||
}
|
||||
else /* if ( $opcode === 'i' ) */ {
|
||||
return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
121
plugins/libkolab/vendor/finediff_modifications.diff
vendored
121
plugins/libkolab/vendor/finediff_modifications.diff
vendored
|
@ -1,121 +0,0 @@
|
|||
--- finediff.php.orig 2014-07-29 14:24:10.000000000 +0200
|
||||
+++ finediff.php 2014-07-29 14:30:38.000000000 +0200
|
||||
@@ -234,25 +234,25 @@
|
||||
|
||||
public function renderDiffToHTML() {
|
||||
$in_offset = 0;
|
||||
- ob_start();
|
||||
+ $html = '';
|
||||
foreach ( $this->edits as $edit ) {
|
||||
$n = $edit->getFromLen();
|
||||
if ( $edit instanceof FineDiffCopyOp ) {
|
||||
- FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
|
||||
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
|
||||
}
|
||||
else if ( $edit instanceof FineDiffDeleteOp ) {
|
||||
- FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
|
||||
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
|
||||
}
|
||||
else if ( $edit instanceof FineDiffInsertOp ) {
|
||||
- FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
|
||||
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
|
||||
}
|
||||
else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
|
||||
- FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
|
||||
- FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
|
||||
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
|
||||
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
|
||||
}
|
||||
$in_offset += $n;
|
||||
}
|
||||
- return ob_get_clean();
|
||||
+ return $html;
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
@@ -277,18 +277,14 @@
|
||||
* Re-create the "To" string from the "From" string and an "Opcodes" string
|
||||
*/
|
||||
public static function renderToTextFromOpcodes($from, $opcodes) {
|
||||
- ob_start();
|
||||
- FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
|
||||
- return ob_get_clean();
|
||||
+ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Render the diff to an HTML string
|
||||
*/
|
||||
public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
|
||||
- ob_start();
|
||||
- FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
|
||||
- return ob_get_clean();
|
||||
+ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
@@ -297,8 +293,9 @@
|
||||
*/
|
||||
public static function renderFromOpcodes($from, $opcodes, $callback) {
|
||||
if ( !is_callable($callback) ) {
|
||||
- return;
|
||||
+ return '';
|
||||
}
|
||||
+ $out = '';
|
||||
$opcodes_len = strlen($opcodes);
|
||||
$from_offset = $opcodes_offset = 0;
|
||||
while ( $opcodes_offset < $opcodes_len ) {
|
||||
@@ -312,18 +309,19 @@
|
||||
$n = 1;
|
||||
}
|
||||
if ( $opcode === 'c' ) { // copy n characters from source
|
||||
- call_user_func($callback, 'c', $from, $from_offset, $n, '');
|
||||
+ $out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
|
||||
$from_offset += $n;
|
||||
}
|
||||
else if ( $opcode === 'd' ) { // delete n characters from source
|
||||
- call_user_func($callback, 'd', $from, $from_offset, $n, '');
|
||||
+ $out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
|
||||
$from_offset += $n;
|
||||
}
|
||||
else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
|
||||
- call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
|
||||
+ $out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
|
||||
$opcodes_offset += 1 + $n;
|
||||
}
|
||||
}
|
||||
+ return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -665,24 +663,26 @@
|
||||
*/
|
||||
private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
|
||||
if ( $opcode === 'c' || $opcode === 'i' ) {
|
||||
- echo substr($from, $from_offset, $from_len);
|
||||
+ return substr($from, $from_offset, $from_len);
|
||||
}
|
||||
+ return '';
|
||||
}
|
||||
|
||||
private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
|
||||
if ( $opcode === 'c' ) {
|
||||
- echo htmlentities(substr($from, $from_offset, $from_len));
|
||||
+ return htmlentities(substr($from, $from_offset, $from_len));
|
||||
}
|
||||
else if ( $opcode === 'd' ) {
|
||||
$deletion = substr($from, $from_offset, $from_len);
|
||||
if ( strcspn($deletion, " \n\r") === 0 ) {
|
||||
$deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
|
||||
}
|
||||
- echo '<del>', htmlentities($deletion), '</del>';
|
||||
+ return '<del>' . htmlentities($deletion) . '</del>';
|
||||
}
|
||||
else /* if ( $opcode === 'i' ) */ {
|
||||
- echo '<ins>', htmlentities(substr($from, $from_offset, $from_len)), '</ins>';
|
||||
+ return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
|
||||
}
|
||||
+ return '';
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue