2 commits - plugins/libkolab plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Tue Mar 31 14:55:05 CEST 2015


 plugins/libkolab/composer.json                      |    3 
 plugins/libkolab/js/audittrail.js                   |   16 
 plugins/libkolab/libkolab.php                       |   45 +
 plugins/libkolab/vendor/Caxy/HtmlDiff/HtmlDiff.php  |  629 ++++++++++++++++++++
 plugins/libkolab/vendor/Caxy/HtmlDiff/Match.php     |   27 
 plugins/libkolab/vendor/Caxy/HtmlDiff/Operation.php |   21 
 plugins/tasklist/tasklist.js                        |   16 
 7 files changed, 733 insertions(+), 24 deletions(-)

New commits:
commit dfa8e1e4deb8b778593a71765797a769e4a7a264
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Mar 31 14:55:02 2015 +0200

    Support computing diffs from HTML documents (#4904)

diff --git a/plugins/libkolab/composer.json b/plugins/libkolab/composer.json
index 02971fc..bf6cb6c 100644
--- a/plugins/libkolab/composer.json
+++ b/plugins/libkolab/composer.json
@@ -25,6 +25,7 @@
     ],
     "require": {
         "php": ">=5.3.0",
-        "roundcube/plugin-installer": ">=0.1.3"
+        "roundcube/plugin-installer": ">=0.1.3",
+        "caxy/php-htmldiff": "dev-master"
     }
 }
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index 4aa1ce5..4abb288 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -9,7 +9,7 @@
  * @version @package_version@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2015, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -150,12 +150,47 @@ class libkolab extends rcube_plugin
     /**
      * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
      */
-    public static function html_diff($from, $to)
+    public static function html_diff($from, $to, $is_html = null)
     {
-      include_once __dir__ . '/vendor/finediff.php';
+        // 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);
+            $is_html   = $from_html || $to_html;
+
+            // ensure both parts are of the same format
+            if ($is_html && !$from_html) {
+                $converter = new rcube_text2html($from, false, array('wrap' => true));
+                $from = $converter->get_html();
+            }
+            if ($is_html && !$to_html) {
+                $converter = new rcube_text2html($to, false, array('wrap' => true));
+                $to = $converter->get_html();
+            }
+        }
+
+        // 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);
 
-      $diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
-      return $diff->renderDiffToHTML();
+            $diff = new Caxy\HtmlDiff\HtmlDiff($from, $to);
+            $diffhtml = $diff->build();
+
+            // 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 --git a/plugins/libkolab/vendor/Caxy/HtmlDiff/HtmlDiff.php b/plugins/libkolab/vendor/Caxy/HtmlDiff/HtmlDiff.php
new file mode 100644
index 0000000..11cc31b
--- /dev/null
+++ b/plugins/libkolab/vendor/Caxy/HtmlDiff/HtmlDiff.php
@@ -0,0 +1,629 @@
+<?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;
+    }
+}
diff --git a/plugins/libkolab/vendor/Caxy/HtmlDiff/Match.php b/plugins/libkolab/vendor/Caxy/HtmlDiff/Match.php
new file mode 100644
index 0000000..c76cfba
--- /dev/null
+++ b/plugins/libkolab/vendor/Caxy/HtmlDiff/Match.php
@@ -0,0 +1,27 @@
+<?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;
+    }
+}
diff --git a/plugins/libkolab/vendor/Caxy/HtmlDiff/Operation.php b/plugins/libkolab/vendor/Caxy/HtmlDiff/Operation.php
new file mode 100644
index 0000000..b2276a7
--- /dev/null
+++ b/plugins/libkolab/vendor/Caxy/HtmlDiff/Operation.php
@@ -0,0 +1,21 @@
+<?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;
+    }
+}


commit c65039cf3e8f00c688d7b3fdc3dc89776ff9dc0f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Mar 31 14:53:02 2015 +0200

    Simplify UI dialog usage

diff --git a/plugins/libkolab/js/audittrail.js b/plugins/libkolab/js/audittrail.js
index 56e0df2..42cdbf0 100644
--- a/plugins/libkolab/js/audittrail.js
+++ b/plugins/libkolab/js/audittrail.js
@@ -43,11 +43,6 @@ libkolab_audittrail.object_history_dialog = function(p)
     if ($dialog.is(':ui-dialog'))
         $dialog.dialog('close');
 
-    var buttons = {};
-    buttons[rcmail.gettext('close')] = function() {
-        $dialog.dialog('close');
-    };
-
     // hide and reset changelog table
     $dialog.find('div.notfound-message').remove();
     $dialog.find('.changelog-table').show().children('tbody')
@@ -61,14 +56,17 @@ libkolab_audittrail.object_history_dialog = function(p)
         title: p.title,
         open: function() {
             $dialog.attr('aria-hidden', 'false');
-            setTimeout(function(){
-                $dialog.parent().find('.ui-dialog-buttonpane .ui-button').first().focus();
-            }, 5);
         },
         close: function() {
             $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
         },
-        buttons: buttons,
+        buttons: [
+            {
+                text: rcmail.gettext('close'),
+                click: function() { $dialog.dialog('close'); },
+                autofocus: true
+            }
+        ],
         minWidth: 450,
         width: 650,
         height: 350,
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index d31a26d..42f6707 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -2227,11 +2227,6 @@ function rcube_tasklist_ui(settings)
               row.show().data('set', true);
         });
 
-        var buttons = {};
-        buttons[rcmail.gettext('close')] = function() {
-            $dialog.dialog('close');
-        };
-
         // open jquery UI dialog
         $dialog.dialog({
             modal: false,
@@ -2240,14 +2235,17 @@ function rcube_tasklist_ui(settings)
             title: rcmail.gettext('objectdiff','tasklist').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + rec.title,
             open: function() {
                 $dialog.attr('aria-hidden', 'false');
-                setTimeout(function(){
-                    $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
-                }, 5);
             },
             close: function() {
                 $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
             },
-            buttons: buttons,
+            buttons: [
+                {
+                    text: rcmail.gettext('close'),
+                    click: function() { $dialog.dialog('close'); },
+                    autofocus: true
+                }
+            ],
             minWidth: 320,
             width: 450
         }).show();




More information about the commits mailing list