ConfigDiff: byebye PHP-FineDiff, hello php-diff
Gives far better performance for larger strings
This commit is contained in:
parent
aa94b98f55
commit
f18e1b6a5f
|
@ -2,7 +2,11 @@
|
|||
|
||||
namespace Icinga\Module\Director;
|
||||
|
||||
use FineDiff;
|
||||
use Diff;
|
||||
use Diff_Renderer_Html_Inline;
|
||||
use Diff_Renderer_Html_SideBySide;
|
||||
use Diff_Renderer_Text_Context;
|
||||
use Diff_Renderer_Text_Unified;
|
||||
use Icinga\Application\Benchmark;
|
||||
|
||||
class ConfigDiff
|
||||
|
@ -16,24 +20,49 @@ class ConfigDiff
|
|||
|
||||
protected function __construct($a, $b)
|
||||
{
|
||||
$this->a = $a;
|
||||
$this->b = $b;
|
||||
require_once dirname(__DIR__) . '/vendor/PHP-FineDiff/finediff.php';
|
||||
require_once dirname(__DIR__) . '/vendor/php-diff/lib/Diff.php';
|
||||
|
||||
// Trying character granularity first...
|
||||
$granularity = FineDiff::$characterGranularity;
|
||||
$this->diff = new FineDiff($a, $b, $granularity);
|
||||
if (count($this->diff->getOps()) > 4) {
|
||||
// ...fall back to word granularity if too many differences
|
||||
// (available granularities: character, word, sentence, paragraph
|
||||
$granularity = FineDiff::$wordGranularity;
|
||||
$this->diff = new FineDiff($a, $b, $granularity);
|
||||
}
|
||||
$this->a = explode("\n", (string) $a);
|
||||
$this->b = explode("\n", (string) $b);
|
||||
|
||||
$options = array(
|
||||
// 'ignoreWhitespace' => true,
|
||||
// 'ignoreCase' => true,
|
||||
);
|
||||
$this->diff = new Diff($this->a, $this->b, $options);
|
||||
}
|
||||
|
||||
public function renderHtml()
|
||||
{
|
||||
return $this->diff->renderDiffToHTML();
|
||||
return $this->renderHtmlSideBySide();
|
||||
}
|
||||
|
||||
public function renderHtmlSideBySide()
|
||||
{
|
||||
require_once dirname(__DIR__) . '/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php';
|
||||
$renderer = new Diff_Renderer_Html_SideBySide;
|
||||
return $this->diff->Render($renderer);
|
||||
}
|
||||
|
||||
public function renderHtmlInline()
|
||||
{
|
||||
require_once dirname(__DIR__) . '/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php';
|
||||
$renderer = new Diff_Renderer_Html_Inline;
|
||||
return $this->diff->Render($renderer);
|
||||
}
|
||||
|
||||
public function renderTextContext()
|
||||
{
|
||||
require_once dirname(__DIR__) . '/vendor/php-diff/lib/Diff/Renderer/Text/Context.php';
|
||||
$renderer = new Diff_Renderer_Text_Context;
|
||||
return $this->diff->Render($renderer);
|
||||
}
|
||||
|
||||
public function renderTextUnified()
|
||||
{
|
||||
require_once dirname(__DIR__) . '/vendor/php-diff/lib/Diff/Renderer/Text/Context.php';
|
||||
$renderer = new Diff_Renderer_Text_Context;
|
||||
return $this->diff->Render($renderer);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
|
|
|
@ -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;
|
||||
ob_start();
|
||||
foreach ( $this->edits as $edit ) {
|
||||
$n = $edit->getFromLen();
|
||||
if ( $edit instanceof FineDiffCopyOp ) {
|
||||
FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
|
||||
}
|
||||
else if ( $edit instanceof FineDiffDeleteOp ) {
|
||||
FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
|
||||
}
|
||||
else if ( $edit instanceof FineDiffInsertOp ) {
|
||||
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());
|
||||
}
|
||||
$in_offset += $n;
|
||||
}
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* 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) {
|
||||
ob_start();
|
||||
FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Generic opcodes parser, user must supply callback for handling
|
||||
* single opcode
|
||||
*/
|
||||
public static function renderFromOpcodes($from, $opcodes, $callback) {
|
||||
if ( !is_callable($callback) ) {
|
||||
return;
|
||||
}
|
||||
$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
|
||||
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, '');
|
||||
$from_offset += $n;
|
||||
}
|
||||
else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
|
||||
call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
|
||||
$opcodes_offset += 1 + $n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' ) {
|
||||
echo substr($from, $from_offset, $from_len);
|
||||
}
|
||||
}
|
||||
|
||||
private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
|
||||
if ( $opcode === 'c' ) {
|
||||
echo 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>';
|
||||
}
|
||||
else /* if ( $opcode === 'i' ) */ {
|
||||
echo '<ins>', htmlentities(substr($from, $from_offset, $from_len)), '</ins>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
PHP Diff Class
|
||||
--------------
|
||||
|
||||
Introduction
|
||||
------------
|
||||
A comprehensive library for generating differences between
|
||||
two hashable objects (strings or arrays). Generated differences can be
|
||||
rendered in all of the standard formats including:
|
||||
* Unified
|
||||
* Context
|
||||
* Inline HTML
|
||||
* Side by Side HTML
|
||||
|
||||
The logic behind the core of the diff engine (ie, the sequence matcher)
|
||||
is primarily based on the Python difflib package. The reason for doing
|
||||
so is primarily because of its high degree of accuracy.
|
||||
|
||||
Example Use
|
||||
-----------
|
||||
A quick usage example can be found in the example/ directory and under
|
||||
example.php.
|
||||
|
||||
More complete documentation will be available shortly.
|
||||
|
||||
Merge files using jQuery
|
||||
------------------------
|
||||
Xiphe has build a jQuery plugin with that you can merge the compared
|
||||
files. Have a look at [jQuery-Merge-for-php-diff](https://github.com/Xiphe/jQuery-Merge-for-php-diff).
|
||||
|
||||
Todo
|
||||
----
|
||||
* Ability to ignore blank line changes
|
||||
* 3 way diff support
|
||||
* Performance optimizations
|
||||
|
||||
License (BSD License)
|
||||
---------------------
|
||||
Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
- Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
- Neither the name of the Chris Boulton nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
```
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
git clone https://github.com/chrisboulton/php-diff.git
|
||||
# Last used commit:
|
||||
cd php-diff
|
||||
git checkout f4db229d7ae8dffa0a4f90e1adbec9bf22c93d99
|
||||
rm -rf .git
|
||||
rm -rf .gitignore
|
||||
rm -rf composer.json
|
||||
rm -rf example
|
||||
cd ..
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
/**
|
||||
* Diff
|
||||
*
|
||||
* A comprehensive library for generating differences between two strings
|
||||
* in multiple formats (unified, side by side HTML etc)
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* - Neither the name of the Chris Boulton nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @package Diff
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2009 Chris Boulton
|
||||
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
|
||||
* @version 1.1
|
||||
* @link http://github.com/chrisboulton/php-diff
|
||||
*/
|
||||
|
||||
class Diff
|
||||
{
|
||||
/**
|
||||
* @var array The "old" sequence to use as the basis for the comparison.
|
||||
*/
|
||||
private $a = null;
|
||||
|
||||
/**
|
||||
* @var array The "new" sequence to generate the changes for.
|
||||
*/
|
||||
private $b = null;
|
||||
|
||||
/**
|
||||
* @var array Array containing the generated opcodes for the differences between the two items.
|
||||
*/
|
||||
private $groupedCodes = null;
|
||||
|
||||
/**
|
||||
* @var array Associative array of the default options available for the diff class and their default value.
|
||||
*/
|
||||
private $defaultOptions = array(
|
||||
'context' => 3,
|
||||
'ignoreNewLines' => false,
|
||||
'ignoreWhitespace' => false,
|
||||
'ignoreCase' => false
|
||||
);
|
||||
|
||||
/**
|
||||
* @var array Array of the options that have been applied for generating the diff.
|
||||
*/
|
||||
private $options = array();
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param array $a Array containing the lines of the first string to compare.
|
||||
* @param array $b Array containing the lines for the second string to compare.
|
||||
*/
|
||||
public function __construct($a, $b, $options=array())
|
||||
{
|
||||
$this->a = $a;
|
||||
$this->b = $b;
|
||||
|
||||
if (is_array($options))
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
else
|
||||
$this->options = $this->defaultOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a diff using the supplied rendering class and return it.
|
||||
*
|
||||
* @param object $renderer An instance of the rendering object to use for generating the diff.
|
||||
* @return mixed The generated diff. Exact return value depends on the rendered.
|
||||
*/
|
||||
public function render(Diff_Renderer_Abstract $renderer)
|
||||
{
|
||||
$renderer->diff = $this;
|
||||
return $renderer->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a range of lines from $start to $end from the first comparison string
|
||||
* and return them as an array. If no values are supplied, the entire string
|
||||
* is returned. It's also possible to specify just one line to return only
|
||||
* that line.
|
||||
*
|
||||
* @param int $start The starting number.
|
||||
* @param int $end The ending number. If not supplied, only the item in $start will be returned.
|
||||
* @return array Array of all of the lines between the specified range.
|
||||
*/
|
||||
public function getA($start=0, $end=null)
|
||||
{
|
||||
if($start == 0 && $end === null) {
|
||||
return $this->a;
|
||||
}
|
||||
|
||||
if($end === null) {
|
||||
$length = 1;
|
||||
}
|
||||
else {
|
||||
$length = $end - $start;
|
||||
}
|
||||
|
||||
return array_slice($this->a, $start, $length);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a range of lines from $start to $end from the second comparison string
|
||||
* and return them as an array. If no values are supplied, the entire string
|
||||
* is returned. It's also possible to specify just one line to return only
|
||||
* that line.
|
||||
*
|
||||
* @param int $start The starting number.
|
||||
* @param int $end The ending number. If not supplied, only the item in $start will be returned.
|
||||
* @return array Array of all of the lines between the specified range.
|
||||
*/
|
||||
public function getB($start=0, $end=null)
|
||||
{
|
||||
if($start == 0 && $end === null) {
|
||||
return $this->b;
|
||||
}
|
||||
|
||||
if($end === null) {
|
||||
$length = 1;
|
||||
}
|
||||
else {
|
||||
$length = $end - $start;
|
||||
}
|
||||
|
||||
return array_slice($this->b, $start, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a list of the compiled and grouped opcodes for the differences between the
|
||||
* two strings. Generally called by the renderer, this class instantiates the sequence
|
||||
* matcher and performs the actual diff generation and return an array of the opcodes
|
||||
* for it. Once generated, the results are cached in the diff class instance.
|
||||
*
|
||||
* @return array Array of the grouped opcodes for the generated diff.
|
||||
*/
|
||||
public function getGroupedOpcodes()
|
||||
{
|
||||
if(!is_null($this->groupedCodes)) {
|
||||
return $this->groupedCodes;
|
||||
}
|
||||
|
||||
require_once dirname(__FILE__).'/Diff/SequenceMatcher.php';
|
||||
$sequenceMatcher = new Diff_SequenceMatcher($this->a, $this->b, null, $this->options);
|
||||
$this->groupedCodes = $sequenceMatcher->getGroupedOpcodes($this->options['context']);
|
||||
return $this->groupedCodes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
/**
|
||||
* Abstract class for diff renderers in PHP DiffLib.
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* - Neither the name of the Chris Boulton nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @package DiffLib
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2009 Chris Boulton
|
||||
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
|
||||
* @version 1.1
|
||||
* @link http://github.com/chrisboulton/php-diff
|
||||
*/
|
||||
|
||||
abstract class Diff_Renderer_Abstract
|
||||
{
|
||||
/**
|
||||
* @var object Instance of the diff class that this renderer is generating the rendered diff for.
|
||||
*/
|
||||
public $diff;
|
||||
|
||||
/**
|
||||
* @var array Array of the default options that apply to this renderer.
|
||||
*/
|
||||
protected $defaultOptions = array();
|
||||
|
||||
/**
|
||||
* @var array Array containing the user applied and merged default options for the renderer.
|
||||
*/
|
||||
protected $options = array();
|
||||
|
||||
/**
|
||||
* The constructor. Instantiates the rendering engine and if options are passed,
|
||||
* sets the options for the renderer.
|
||||
*
|
||||
* @param array $options Optionally, an array of the options for the renderer.
|
||||
*/
|
||||
public function __construct(array $options = array())
|
||||
{
|
||||
$this->setOptions($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the options of the renderer to those supplied in the passed in array.
|
||||
* Options are merged with the default to ensure that there aren't any missing
|
||||
* options.
|
||||
*
|
||||
* @param array $options Array of options to set.
|
||||
*/
|
||||
public function setOptions(array $options)
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
<?php
|
||||
/**
|
||||
* Base renderer for rendering HTML based diffs for PHP DiffLib.
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* - Neither the name of the Chris Boulton nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @package DiffLib
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2009 Chris Boulton
|
||||
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
|
||||
* @version 1.1
|
||||
* @link http://github.com/chrisboulton/php-diff
|
||||
*/
|
||||
|
||||
require_once dirname(__FILE__).'/../Abstract.php';
|
||||
|
||||
class Diff_Renderer_Html_Array extends Diff_Renderer_Abstract
|
||||
{
|
||||
/**
|
||||
* @var array Array of the default options that apply to this renderer.
|
||||
*/
|
||||
protected $defaultOptions = array(
|
||||
'tabSize' => 4
|
||||
);
|
||||
|
||||
/**
|
||||
* Render and return an array structure suitable for generating HTML
|
||||
* based differences. Generally called by subclasses that generate a
|
||||
* HTML based diff and return an array of the changes to show in the diff.
|
||||
*
|
||||
* @return array An array of the generated chances, suitable for presentation in HTML.
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
// As we'll be modifying a & b to include our change markers,
|
||||
// we need to get the contents and store them here. That way
|
||||
// we're not going to destroy the original data
|
||||
$a = $this->diff->getA();
|
||||
$b = $this->diff->getB();
|
||||
|
||||
$changes = array();
|
||||
$opCodes = $this->diff->getGroupedOpcodes();
|
||||
foreach($opCodes as $group) {
|
||||
$blocks = array();
|
||||
$lastTag = null;
|
||||
$lastBlock = 0;
|
||||
foreach($group as $code) {
|
||||
list($tag, $i1, $i2, $j1, $j2) = $code;
|
||||
|
||||
if($tag == 'replace' && $i2 - $i1 == $j2 - $j1) {
|
||||
for($i = 0; $i < ($i2 - $i1); ++$i) {
|
||||
$fromLine = $a[$i1 + $i];
|
||||
$toLine = $b[$j1 + $i];
|
||||
|
||||
list($start, $end) = $this->getChangeExtent($fromLine, $toLine);
|
||||
if($start != 0 || $end != 0) {
|
||||
$last = $end + strlen($fromLine);
|
||||
$fromLine = substr_replace($fromLine, "\0", $start, 0);
|
||||
$fromLine = substr_replace($fromLine, "\1", $last + 1, 0);
|
||||
$last = $end + strlen($toLine);
|
||||
$toLine = substr_replace($toLine, "\0", $start, 0);
|
||||
$toLine = substr_replace($toLine, "\1", $last + 1, 0);
|
||||
$a[$i1 + $i] = $fromLine;
|
||||
$b[$j1 + $i] = $toLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($tag != $lastTag) {
|
||||
$blocks[] = array(
|
||||
'tag' => $tag,
|
||||
'base' => array(
|
||||
'offset' => $i1,
|
||||
'lines' => array()
|
||||
),
|
||||
'changed' => array(
|
||||
'offset' => $j1,
|
||||
'lines' => array()
|
||||
)
|
||||
);
|
||||
$lastBlock = count($blocks)-1;
|
||||
}
|
||||
|
||||
$lastTag = $tag;
|
||||
|
||||
if($tag == 'equal') {
|
||||
$lines = array_slice($a, $i1, ($i2 - $i1));
|
||||
$blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines);
|
||||
$lines = array_slice($b, $j1, ($j2 - $j1));
|
||||
$blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines);
|
||||
}
|
||||
else {
|
||||
if($tag == 'replace' || $tag == 'delete') {
|
||||
$lines = array_slice($a, $i1, ($i2 - $i1));
|
||||
$lines = $this->formatLines($lines);
|
||||
$lines = str_replace(array("\0", "\1"), array('<del>', '</del>'), $lines);
|
||||
$blocks[$lastBlock]['base']['lines'] += $lines;
|
||||
}
|
||||
|
||||
if($tag == 'replace' || $tag == 'insert') {
|
||||
$lines = array_slice($b, $j1, ($j2 - $j1));
|
||||
$lines = $this->formatLines($lines);
|
||||
$lines = str_replace(array("\0", "\1"), array('<ins>', '</ins>'), $lines);
|
||||
$blocks[$lastBlock]['changed']['lines'] += $lines;
|
||||
}
|
||||
}
|
||||
}
|
||||
$changes[] = $blocks;
|
||||
}
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two strings, determine where the changes in the two strings
|
||||
* begin, and where the changes in the two strings end.
|
||||
*
|
||||
* @param string $fromLine The first string.
|
||||
* @param string $toLine The second string.
|
||||
* @return array Array containing the starting position (0 by default) and the ending position (-1 by default)
|
||||
*/
|
||||
private function getChangeExtent($fromLine, $toLine)
|
||||
{
|
||||
$start = 0;
|
||||
$limit = min(strlen($fromLine), strlen($toLine));
|
||||
while($start < $limit && $fromLine{$start} == $toLine{$start}) {
|
||||
++$start;
|
||||
}
|
||||
$end = -1;
|
||||
$limit = $limit - $start;
|
||||
while(-$end <= $limit && substr($fromLine, $end, 1) == substr($toLine, $end, 1)) {
|
||||
--$end;
|
||||
}
|
||||
return array(
|
||||
$start,
|
||||
$end + 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a series of lines suitable for output in a HTML rendered diff.
|
||||
* This involves replacing tab characters with spaces, making the HTML safe
|
||||
* for output, ensuring that double spaces are replaced with etc.
|
||||
*
|
||||
* @param array $lines Array of lines to format.
|
||||
* @return array Array of the formatted lines.
|
||||
*/
|
||||
protected function formatLines($lines)
|
||||
{
|
||||
$lines = array_map(array($this, 'ExpandTabs'), $lines);
|
||||
$lines = array_map(array($this, 'HtmlSafe'), $lines);
|
||||
foreach($lines as &$line) {
|
||||
$line = preg_replace('# ( +)|^ #e', "\$this->fixSpaces('\\1')", $line);
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a string containing spaces with a HTML representation using .
|
||||
*
|
||||
* @param string $spaces The string of spaces.
|
||||
* @return string The HTML representation of the string.
|
||||
*/
|
||||
function fixSpaces($spaces='')
|
||||
{
|
||||
$count = strlen($spaces);
|
||||
if($count == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$div = floor($count / 2);
|
||||
$mod = $count % 2;
|
||||
return str_repeat(' ', $div).str_repeat(' ', $mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace tabs in a single line with a number of spaces as defined by the tabSize option.
|
||||
*
|
||||
* @param string $line The containing tabs to convert.
|
||||
* @return string The line with the tabs converted to spaces.
|
||||
*/
|
||||
private function expandTabs($line)
|
||||
{
|
||||
return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a string containing HTML safe for output on a page.
|
||||
*
|
||||
* @param string $string The string.
|
||||
* @return string The string with the HTML characters replaced by entities.
|
||||
*/
|
||||
private function htmlSafe($string)
|
||||
{
|
||||
return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
/**
|
||||
* Inline HTML diff generator for PHP DiffLib.
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* - Neither the name of the Chris Boulton nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @package DiffLib
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2009 Chris Boulton
|
||||
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
|
||||
* @version 1.1
|
||||
* @link http://github.com/chrisboulton/php-diff
|
||||
*/
|
||||
|
||||
require_once dirname(__FILE__).'/Array.php';
|
||||
|
||||
class Diff_Renderer_Html_Inline extends Diff_Renderer_Html_Array
|
||||
{
|
||||
/**
|
||||
* Render a and return diff with changes between the two sequences
|
||||
* displayed inline (under each other)
|
||||
*
|
||||
* @return string The generated inline diff.
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$changes = parent::render();
|
||||
$html = '';
|
||||
if(empty($changes)) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$html .= '<table class="Differences DifferencesInline">';
|
||||
$html .= '<thead>';
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>Old</th>';
|
||||
$html .= '<th>New</th>';
|
||||
$html .= '<th>Differences</th>';
|
||||
$html .= '</tr>';
|
||||
$html .= '</thead>';
|
||||
foreach($changes as $i => $blocks) {
|
||||
// If this is a separate block, we're condensing code so output ...,
|
||||
// indicating a significant portion of the code has been collapsed as
|
||||
// it is the same
|
||||
if($i > 0) {
|
||||
$html .= '<tbody class="Skipped">';
|
||||
$html .= '<th>…</th>';
|
||||
$html .= '<th>…</th>';
|
||||
$html .= '<td> </td>';
|
||||
$html .= '</tbody>';
|
||||
}
|
||||
|
||||
foreach($blocks as $change) {
|
||||
$html .= '<tbody class="Change'.ucfirst($change['tag']).'">';
|
||||
// Equal changes should be shown on both sides of the diff
|
||||
if($change['tag'] == 'equal') {
|
||||
foreach($change['base']['lines'] as $no => $line) {
|
||||
$fromLine = $change['base']['offset'] + $no + 1;
|
||||
$toLine = $change['changed']['offset'] + $no + 1;
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>'.$fromLine.'</th>';
|
||||
$html .= '<th>'.$toLine.'</th>';
|
||||
$html .= '<td class="Left">'.$line.'</td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
}
|
||||
// Added lines only on the right side
|
||||
else if($change['tag'] == 'insert') {
|
||||
foreach($change['changed']['lines'] as $no => $line) {
|
||||
$toLine = $change['changed']['offset'] + $no + 1;
|
||||
$html .= '<tr>';
|
||||
$html .= '<th> </th>';
|
||||
$html .= '<th>'.$toLine.'</th>';
|
||||
$html .= '<td class="Right"><ins>'.$line.'</ins> </td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
}
|
||||
// Show deleted lines only on the left side
|
||||
else if($change['tag'] == 'delete') {
|
||||
foreach($change['base']['lines'] as $no => $line) {
|
||||
$fromLine = $change['base']['offset'] + $no + 1;
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>'.$fromLine.'</th>';
|
||||
$html .= '<th> </th>';
|
||||
$html .= '<td class="Left"><del>'.$line.'</del> </td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
}
|
||||
// Show modified lines on both sides
|
||||
else if($change['tag'] == 'replace') {
|
||||
foreach($change['base']['lines'] as $no => $line) {
|
||||
$fromLine = $change['base']['offset'] + $no + 1;
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>'.$fromLine.'</th>';
|
||||
$html .= '<th> </th>';
|
||||
$html .= '<td class="Left"><span>'.$line.'</span></td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
|
||||
foreach($change['changed']['lines'] as $no => $line) {
|
||||
$toLine = $change['changed']['offset'] + $no + 1;
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>'.$toLine.'</th>';
|
||||
$html .= '<th> </th>';
|
||||
$html .= '<td class="Right"><span>'.$line.'</span></td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
}
|
||||
$html .= '</tbody>';
|
||||
}
|
||||
}
|
||||
$html .= '</table>';
|
||||
return $html;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
/**
|
||||
* Side by Side HTML diff generator for PHP DiffLib.
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* - Neither the name of the Chris Boulton nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @package DiffLib
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2009 Chris Boulton
|
||||
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
|
||||
* @version 1.1
|
||||
* @link http://github.com/chrisboulton/php-diff
|
||||
*/
|
||||
|
||||
require_once dirname(__FILE__).'/Array.php';
|
||||
|
||||
class Diff_Renderer_Html_SideBySide extends Diff_Renderer_Html_Array
|
||||
{
|
||||
/**
|
||||
* Render a and return diff with changes between the two sequences
|
||||
* displayed side by side.
|
||||
*
|
||||
* @return string The generated side by side diff.
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$changes = parent::render();
|
||||
|
||||
$html = '';
|
||||
if(empty($changes)) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$html .= '<table class="Differences DifferencesSideBySide">';
|
||||
$html .= '<thead>';
|
||||
$html .= '<tr>';
|
||||
$html .= '<th colspan="2">Old Version</th>';
|
||||
$html .= '<th colspan="2">New Version</th>';
|
||||
$html .= '</tr>';
|
||||
$html .= '</thead>';
|
||||
foreach($changes as $i => $blocks) {
|
||||
if($i > 0) {
|
||||
$html .= '<tbody class="Skipped">';
|
||||
$html .= '<th>…</th><td> </td>';
|
||||
$html .= '<th>…</th><td> </td>';
|
||||
$html .= '</tbody>';
|
||||
}
|
||||
|
||||
foreach($blocks as $change) {
|
||||
$html .= '<tbody class="Change'.ucfirst($change['tag']).'">';
|
||||
// Equal changes should be shown on both sides of the diff
|
||||
if($change['tag'] == 'equal') {
|
||||
foreach($change['base']['lines'] as $no => $line) {
|
||||
$fromLine = $change['base']['offset'] + $no + 1;
|
||||
$toLine = $change['changed']['offset'] + $no + 1;
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>'.$fromLine.'</th>';
|
||||
$html .= '<td class="Left"><span>'.$line.'</span> </span></td>';
|
||||
$html .= '<th>'.$toLine.'</th>';
|
||||
$html .= '<td class="Right"><span>'.$line.'</span> </span></td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
}
|
||||
// Added lines only on the right side
|
||||
else if($change['tag'] == 'insert') {
|
||||
foreach($change['changed']['lines'] as $no => $line) {
|
||||
$toLine = $change['changed']['offset'] + $no + 1;
|
||||
$html .= '<tr>';
|
||||
$html .= '<th> </th>';
|
||||
$html .= '<td class="Left"> </td>';
|
||||
$html .= '<th>'.$toLine.'</th>';
|
||||
$html .= '<td class="Right"><ins>'.$line.'</ins> </td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
}
|
||||
// Show deleted lines only on the left side
|
||||
else if($change['tag'] == 'delete') {
|
||||
foreach($change['base']['lines'] as $no => $line) {
|
||||
$fromLine = $change['base']['offset'] + $no + 1;
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>'.$fromLine.'</th>';
|
||||
$html .= '<td class="Left"><del>'.$line.'</del> </td>';
|
||||
$html .= '<th> </th>';
|
||||
$html .= '<td class="Right"> </td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
}
|
||||
// Show modified lines on both sides
|
||||
else if($change['tag'] == 'replace') {
|
||||
if(count($change['base']['lines']) >= count($change['changed']['lines'])) {
|
||||
foreach($change['base']['lines'] as $no => $line) {
|
||||
$fromLine = $change['base']['offset'] + $no + 1;
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>'.$fromLine.'</th>';
|
||||
$html .= '<td class="Left"><span>'.$line.'</span> </td>';
|
||||
if(!isset($change['changed']['lines'][$no])) {
|
||||
$toLine = ' ';
|
||||
$changedLine = ' ';
|
||||
}
|
||||
else {
|
||||
$toLine = $change['base']['offset'] + $no + 1;
|
||||
$changedLine = '<span>'.$change['changed']['lines'][$no].'</span>';
|
||||
}
|
||||
$html .= '<th>'.$toLine.'</th>';
|
||||
$html .= '<td class="Right">'.$changedLine.'</td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach($change['changed']['lines'] as $no => $changedLine) {
|
||||
if(!isset($change['base']['lines'][$no])) {
|
||||
$fromLine = ' ';
|
||||
$line = ' ';
|
||||
}
|
||||
else {
|
||||
$fromLine = $change['base']['offset'] + $no + 1;
|
||||
$line = '<span>'.$change['base']['lines'][$no].'</span>';
|
||||
}
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>'.$fromLine.'</th>';
|
||||
$html .= '<td class="Left"><span>'.$line.'</span> </td>';
|
||||
$toLine = $change['changed']['offset'] + $no + 1;
|
||||
$html .= '<th>'.$toLine.'</th>';
|
||||
$html .= '<td class="Right">'.$changedLine.'</td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
}
|
||||
}
|
||||
$html .= '</tbody>';
|
||||
}
|
||||
}
|
||||
$html .= '</table>';
|
||||
return $html;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
/**
|
||||
* Context diff generator for PHP DiffLib.
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* - Neither the name of the Chris Boulton nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @package DiffLib
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2009 Chris Boulton
|
||||
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
|
||||
* @version 1.1
|
||||
* @link http://github.com/chrisboulton/php-diff
|
||||
*/
|
||||
|
||||
require_once dirname(__FILE__).'/../Abstract.php';
|
||||
|
||||
class Diff_Renderer_Text_Context extends Diff_Renderer_Abstract
|
||||
{
|
||||
/**
|
||||
* @var array Array of the different opcode tags and how they map to the context diff equivalent.
|
||||
*/
|
||||
private $tagMap = array(
|
||||
'insert' => '+',
|
||||
'delete' => '-',
|
||||
'replace' => '!',
|
||||
'equal' => ' '
|
||||
);
|
||||
|
||||
/**
|
||||
* Render and return a context formatted (old school!) diff file.
|
||||
*
|
||||
* @return string The generated context diff.
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$diff = '';
|
||||
$opCodes = $this->diff->getGroupedOpcodes();
|
||||
foreach($opCodes as $group) {
|
||||
$diff .= "***************\n";
|
||||
$lastItem = count($group)-1;
|
||||
$i1 = $group[0][1];
|
||||
$i2 = $group[$lastItem][2];
|
||||
$j1 = $group[0][3];
|
||||
$j2 = $group[$lastItem][4];
|
||||
|
||||
if($i2 - $i1 >= 2) {
|
||||
$diff .= '*** '.($group[0][1] + 1).','.$i2." ****\n";
|
||||
}
|
||||
else {
|
||||
$diff .= '*** '.$i2." ****\n";
|
||||
}
|
||||
|
||||
if($j2 - $j1 >= 2) {
|
||||
$separator = '--- '.($j1 + 1).','.$j2." ----\n";
|
||||
}
|
||||
else {
|
||||
$separator = '--- '.$j2." ----\n";
|
||||
}
|
||||
|
||||
$hasVisible = false;
|
||||
foreach($group as $code) {
|
||||
if($code[0] == 'replace' || $code[0] == 'delete') {
|
||||
$hasVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if($hasVisible) {
|
||||
foreach($group as $code) {
|
||||
list($tag, $i1, $i2, $j1, $j2) = $code;
|
||||
if($tag == 'insert') {
|
||||
continue;
|
||||
}
|
||||
$diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetA($i1, $i2))."\n";
|
||||
}
|
||||
}
|
||||
|
||||
$hasVisible = false;
|
||||
foreach($group as $code) {
|
||||
if($code[0] == 'replace' || $code[0] == 'insert') {
|
||||
$hasVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$diff .= $separator;
|
||||
|
||||
if($hasVisible) {
|
||||
foreach($group as $code) {
|
||||
list($tag, $i1, $i2, $j1, $j2) = $code;
|
||||
if($tag == 'delete') {
|
||||
continue;
|
||||
}
|
||||
$diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetB($j1, $j2))."\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return $diff;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
/**
|
||||
* Unified diff generator for PHP DiffLib.
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* - Neither the name of the Chris Boulton nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @package DiffLib
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2009 Chris Boulton
|
||||
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
|
||||
* @version 1.1
|
||||
* @link http://github.com/chrisboulton/php-diff
|
||||
*/
|
||||
|
||||
require_once dirname(__FILE__).'/../Abstract.php';
|
||||
|
||||
class Diff_Renderer_Text_Unified extends Diff_Renderer_Abstract
|
||||
{
|
||||
/**
|
||||
* Render and return a unified diff.
|
||||
*
|
||||
* @return string The unified diff.
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$diff = '';
|
||||
$opCodes = $this->diff->getGroupedOpcodes();
|
||||
foreach($opCodes as $group) {
|
||||
$lastItem = count($group)-1;
|
||||
$i1 = $group[0][1];
|
||||
$i2 = $group[$lastItem][2];
|
||||
$j1 = $group[0][3];
|
||||
$j2 = $group[$lastItem][4];
|
||||
|
||||
if($i1 == 0 && $i2 == 0) {
|
||||
$i1 = -1;
|
||||
$i2 = -1;
|
||||
}
|
||||
|
||||
$diff .= '@@ -'.($i1 + 1).','.($i2 - $i1).' +'.($j1 + 1).','.($j2 - $j1)." @@\n";
|
||||
foreach($group as $code) {
|
||||
list($tag, $i1, $i2, $j1, $j2) = $code;
|
||||
if($tag == 'equal') {
|
||||
$diff .= ' '.implode("\n ", $this->diff->GetA($i1, $i2))."\n";
|
||||
}
|
||||
else {
|
||||
if($tag == 'replace' || $tag == 'delete') {
|
||||
$diff .= '-'.implode("\n-", $this->diff->GetA($i1, $i2))."\n";
|
||||
}
|
||||
|
||||
if($tag == 'replace' || $tag == 'insert') {
|
||||
$diff .= '+'.implode("\n+", $this->diff->GetB($j1, $j2))."\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $diff;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,742 @@
|
|||
<?php
|
||||
/**
|
||||
* Sequence matcher for Diff
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* - Neither the name of the Chris Boulton nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @package Diff
|
||||
* @author Chris Boulton <chris.boulton@interspire.com>
|
||||
* @copyright (c) 2009 Chris Boulton
|
||||
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
|
||||
* @version 1.1
|
||||
* @link http://github.com/chrisboulton/php-diff
|
||||
*/
|
||||
|
||||
class Diff_SequenceMatcher
|
||||
{
|
||||
/**
|
||||
* @var string|array Either a string or an array containing a callback function to determine if a line is "junk" or not.
|
||||
*/
|
||||
private $junkCallback = null;
|
||||
|
||||
/**
|
||||
* @var array The first sequence to compare against.
|
||||
*/
|
||||
private $a = null;
|
||||
|
||||
/**
|
||||
* @var array The second sequence.
|
||||
*/
|
||||
private $b = null;
|
||||
|
||||
/**
|
||||
* @var array Array of characters that are considered junk from the second sequence. Characters are the array key.
|
||||
*/
|
||||
private $junkDict = array();
|
||||
|
||||
/**
|
||||
* @var array Array of indices that do not contain junk elements.
|
||||
*/
|
||||
private $b2j = array();
|
||||
|
||||
private $options = array();
|
||||
|
||||
private $defaultOptions = array(
|
||||
'ignoreNewLines' => false,
|
||||
'ignoreWhitespace' => false,
|
||||
'ignoreCase' => false
|
||||
);
|
||||
|
||||
/**
|
||||
* The constructor. With the sequences being passed, they'll be set for the
|
||||
* sequence matcher and it will perform a basic cleanup & calculate junk
|
||||
* elements.
|
||||
*
|
||||
* @param string|array $a A string or array containing the lines to compare against.
|
||||
* @param string|array $b A string or array containing the lines to compare.
|
||||
* @param string|array $junkCallback Either an array or string that references a callback function (if there is one) to determine 'junk' characters.
|
||||
*/
|
||||
public function __construct($a, $b, $junkCallback=null, $options)
|
||||
{
|
||||
$this->a = null;
|
||||
$this->b = null;
|
||||
$this->junkCallback = $junkCallback;
|
||||
$this->setOptions($options);
|
||||
$this->setSequences($a, $b);
|
||||
}
|
||||
|
||||
public function setOptions($options)
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the first and second sequences to use with the sequence matcher.
|
||||
*
|
||||
* @param string|array $a A string or array containing the lines to compare against.
|
||||
* @param string|array $b A string or array containing the lines to compare.
|
||||
*/
|
||||
public function setSequences($a, $b)
|
||||
{
|
||||
$this->setSeq1($a);
|
||||
$this->setSeq2($b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the first sequence ($a) and reset any internal caches to indicate that
|
||||
* when calling the calculation methods, we need to recalculate them.
|
||||
*
|
||||
* @param string|array $a The sequence to set as the first sequence.
|
||||
*/
|
||||
public function setSeq1($a)
|
||||
{
|
||||
if(!is_array($a)) {
|
||||
$a = str_split($a);
|
||||
}
|
||||
if($a == $this->a) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->a= $a;
|
||||
$this->matchingBlocks = null;
|
||||
$this->opCodes = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the second sequence ($b) and reset any internal caches to indicate that
|
||||
* when calling the calculation methods, we need to recalculate them.
|
||||
*
|
||||
* @param string|array $b The sequence to set as the second sequence.
|
||||
*/
|
||||
public function setSeq2($b)
|
||||
{
|
||||
if(!is_array($b)) {
|
||||
$b = str_split($b);
|
||||
}
|
||||
if($b == $this->b) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->b = $b;
|
||||
$this->matchingBlocks = null;
|
||||
$this->opCodes = null;
|
||||
$this->fullBCount = null;
|
||||
$this->chainB();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the internal arrays containing the list of junk and non-junk
|
||||
* characters for the second ($b) sequence.
|
||||
*/
|
||||
private function chainB()
|
||||
{
|
||||
$length = count ($this->b);
|
||||
$this->b2j = array();
|
||||
$popularDict = array();
|
||||
|
||||
for($i = 0; $i < $length; ++$i) {
|
||||
$char = $this->b[$i];
|
||||
if(isset($this->b2j[$char])) {
|
||||
if($length >= 200 && count($this->b2j[$char]) * 100 > $length) {
|
||||
$popularDict[$char] = 1;
|
||||
unset($this->b2j[$char]);
|
||||
}
|
||||
else {
|
||||
$this->b2j[$char][] = $i;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->b2j[$char] = array(
|
||||
$i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leftovers
|
||||
foreach(array_keys($popularDict) as $char) {
|
||||
unset($this->b2j[$char]);
|
||||
}
|
||||
|
||||
$this->junkDict = array();
|
||||
if(is_callable($this->junkCallback)) {
|
||||
foreach(array_keys($popularDict) as $char) {
|
||||
if(call_user_func($this->junkCallback, $char)) {
|
||||
$this->junkDict[$char] = 1;
|
||||
unset($popularDict[$char]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach(array_keys($this->b2j) as $char) {
|
||||
if(call_user_func($this->junkCallback, $char)) {
|
||||
$this->junkDict[$char] = 1;
|
||||
unset($this->b2j[$char]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a particular character is in the junk dictionary
|
||||
* for the list of junk characters.
|
||||
*
|
||||
* @return boolean $b True if the character is considered junk. False if not.
|
||||
*/
|
||||
private function isBJunk($b)
|
||||
{
|
||||
if(isset($this->juncDict[$b])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the longest matching block in the two sequences, as defined by the
|
||||
* lower and upper constraints for each sequence. (for the first sequence,
|
||||
* $alo - $ahi and for the second sequence, $blo - $bhi)
|
||||
*
|
||||
* Essentially, of all of the maximal matching blocks, return the one that
|
||||
* startest earliest in $a, and all of those maximal matching blocks that
|
||||
* start earliest in $a, return the one that starts earliest in $b.
|
||||
*
|
||||
* If the junk callback is defined, do the above but with the restriction
|
||||
* that the junk element appears in the block. Extend it as far as possible
|
||||
* by matching only junk elements in both $a and $b.
|
||||
*
|
||||
* @param int $alo The lower constraint for the first sequence.
|
||||
* @param int $ahi The upper constraint for the first sequence.
|
||||
* @param int $blo The lower constraint for the second sequence.
|
||||
* @param int $bhi The upper constraint for the second sequence.
|
||||
* @return array Array containing the longest match that includes the starting position in $a, start in $b and the length/size.
|
||||
*/
|
||||
public function findLongestMatch($alo, $ahi, $blo, $bhi)
|
||||
{
|
||||
$a = $this->a;
|
||||
$b = $this->b;
|
||||
|
||||
$bestI = $alo;
|
||||
$bestJ = $blo;
|
||||
$bestSize = 0;
|
||||
|
||||
$j2Len = array();
|
||||
$nothing = array();
|
||||
|
||||
for($i = $alo; $i < $ahi; ++$i) {
|
||||
$newJ2Len = array();
|
||||
$jDict = $this->arrayGetDefault($this->b2j, $a[$i], $nothing);
|
||||
foreach($jDict as $jKey => $j) {
|
||||
if($j < $blo) {
|
||||
continue;
|
||||
}
|
||||
else if($j >= $bhi) {
|
||||
break;
|
||||
}
|
||||
|
||||
$k = $this->arrayGetDefault($j2Len, $j -1, 0) + 1;
|
||||
$newJ2Len[$j] = $k;
|
||||
if($k > $bestSize) {
|
||||
$bestI = $i - $k + 1;
|
||||
$bestJ = $j - $k + 1;
|
||||
$bestSize = $k;
|
||||
}
|
||||
}
|
||||
|
||||
$j2Len = $newJ2Len;
|
||||
}
|
||||
|
||||
while($bestI > $alo && $bestJ > $blo && !$this->isBJunk($b[$bestJ - 1]) &&
|
||||
!$this->linesAreDifferent($bestI - 1, $bestJ - 1)) {
|
||||
--$bestI;
|
||||
--$bestJ;
|
||||
++$bestSize;
|
||||
}
|
||||
|
||||
while($bestI + $bestSize < $ahi && ($bestJ + $bestSize) < $bhi &&
|
||||
!$this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) {
|
||||
++$bestSize;
|
||||
}
|
||||
|
||||
while($bestI > $alo && $bestJ > $blo && $this->isBJunk($b[$bestJ - 1]) &&
|
||||
!$this->isLineDifferent($bestI - 1, $bestJ - 1)) {
|
||||
--$bestI;
|
||||
--$bestJ;
|
||||
++$bestSize;
|
||||
}
|
||||
|
||||
while($bestI + $bestSize < $ahi && $bestJ + $bestSize < $bhi &&
|
||||
$this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) {
|
||||
++$bestSize;
|
||||
}
|
||||
|
||||
return array(
|
||||
$bestI,
|
||||
$bestJ,
|
||||
$bestSize
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the two lines at the given indexes are different or not.
|
||||
*
|
||||
* @param int $aIndex Line number to check against in a.
|
||||
* @param int $bIndex Line number to check against in b.
|
||||
* @return boolean True if the lines are different and false if not.
|
||||
*/
|
||||
public function linesAreDifferent($aIndex, $bIndex)
|
||||
{
|
||||
$lineA = $this->a[$aIndex];
|
||||
$lineB = $this->b[$bIndex];
|
||||
|
||||
if($this->options['ignoreWhitespace']) {
|
||||
$replace = array("\t", ' ');
|
||||
$lineA = str_replace($replace, '', $lineA);
|
||||
$lineB = str_replace($replace, '', $lineB);
|
||||
}
|
||||
|
||||
if($this->options['ignoreCase']) {
|
||||
$lineA = strtolower($lineA);
|
||||
$lineB = strtolower($lineB);
|
||||
}
|
||||
|
||||
if($lineA != $lineB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a nested set of arrays for all of the matching sub-sequences
|
||||
* in the strings $a and $b.
|
||||
*
|
||||
* Each block contains the lower constraint of the block in $a, the lower
|
||||
* constraint of the block in $b and finally the number of lines that the
|
||||
* block continues for.
|
||||
*
|
||||
* @return array Nested array of the matching blocks, as described by the function.
|
||||
*/
|
||||
public function getMatchingBlocks()
|
||||
{
|
||||
if(!empty($this->matchingBlocks)) {
|
||||
return $this->matchingBlocks;
|
||||
}
|
||||
|
||||
$aLength = count($this->a);
|
||||
$bLength = count($this->b);
|
||||
|
||||
$queue = array(
|
||||
array(
|
||||
0,
|
||||
$aLength,
|
||||
0,
|
||||
$bLength
|
||||
)
|
||||
);
|
||||
|
||||
$matchingBlocks = array();
|
||||
while(!empty($queue)) {
|
||||
list($alo, $ahi, $blo, $bhi) = array_pop($queue);
|
||||
$x = $this->findLongestMatch($alo, $ahi, $blo, $bhi);
|
||||
list($i, $j, $k) = $x;
|
||||
if($k) {
|
||||
$matchingBlocks[] = $x;
|
||||
if($alo < $i && $blo < $j) {
|
||||
$queue[] = array(
|
||||
$alo,
|
||||
$i,
|
||||
$blo,
|
||||
$j
|
||||
);
|
||||
}
|
||||
|
||||
if($i + $k < $ahi && $j + $k < $bhi) {
|
||||
$queue[] = array(
|
||||
$i + $k,
|
||||
$ahi,
|
||||
$j + $k,
|
||||
$bhi
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($matchingBlocks, array($this, 'tupleSort'));
|
||||
|
||||
$i1 = 0;
|
||||
$j1 = 0;
|
||||
$k1 = 0;
|
||||
$nonAdjacent = array();
|
||||
foreach($matchingBlocks as $block) {
|
||||
list($i2, $j2, $k2) = $block;
|
||||
if($i1 + $k1 == $i2 && $j1 + $k1 == $j2) {
|
||||
$k1 += $k2;
|
||||
}
|
||||
else {
|
||||
if($k1) {
|
||||
$nonAdjacent[] = array(
|
||||
$i1,
|
||||
$j1,
|
||||
$k1
|
||||
);
|
||||
}
|
||||
|
||||
$i1 = $i2;
|
||||
$j1 = $j2;
|
||||
$k1 = $k2;
|
||||
}
|
||||
}
|
||||
|
||||
if($k1) {
|
||||
$nonAdjacent[] = array(
|
||||
$i1,
|
||||
$j1,
|
||||
$k1
|
||||
);
|
||||
}
|
||||
|
||||
$nonAdjacent[] = array(
|
||||
$aLength,
|
||||
$bLength,
|
||||
0
|
||||
);
|
||||
|
||||
$this->matchingBlocks = $nonAdjacent;
|
||||
return $this->matchingBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all of the opcodes for the differences between the
|
||||
* two strings.
|
||||
*
|
||||
* The nested array returned contains an array describing the opcode
|
||||
* which includes:
|
||||
* 0 - The type of tag (as described below) for the opcode.
|
||||
* 1 - The beginning line in the first sequence.
|
||||
* 2 - The end line in the first sequence.
|
||||
* 3 - The beginning line in the second sequence.
|
||||
* 4 - The end line in the second sequence.
|
||||
*
|
||||
* The different types of tags include:
|
||||
* replace - The string from $i1 to $i2 in $a should be replaced by
|
||||
* the string in $b from $j1 to $j2.
|
||||
* delete - The string in $a from $i1 to $j2 should be deleted.
|
||||
* insert - The string in $b from $j1 to $j2 should be inserted at
|
||||
* $i1 in $a.
|
||||
* equal - The two strings with the specified ranges are equal.
|
||||
*
|
||||
* @return array Array of the opcodes describing the differences between the strings.
|
||||
*/
|
||||
public function getOpCodes()
|
||||
{
|
||||
if(!empty($this->opCodes)) {
|
||||
return $this->opCodes;
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
$j = 0;
|
||||
$this->opCodes = array();
|
||||
|
||||
$blocks = $this->getMatchingBlocks();
|
||||
foreach($blocks as $block) {
|
||||
list($ai, $bj, $size) = $block;
|
||||
$tag = '';
|
||||
if($i < $ai && $j < $bj) {
|
||||
$tag = 'replace';
|
||||
}
|
||||
else if($i < $ai) {
|
||||
$tag = 'delete';
|
||||
}
|
||||
else if($j < $bj) {
|
||||
$tag = 'insert';
|
||||
}
|
||||
|
||||
if($tag) {
|
||||
$this->opCodes[] = array(
|
||||
$tag,
|
||||
$i,
|
||||
$ai,
|
||||
$j,
|
||||
$bj
|
||||
);
|
||||
}
|
||||
|
||||
$i = $ai + $size;
|
||||
$j = $bj + $size;
|
||||
|
||||
if($size) {
|
||||
$this->opCodes[] = array(
|
||||
'equal',
|
||||
$ai,
|
||||
$i,
|
||||
$bj,
|
||||
$j
|
||||
);
|
||||
}
|
||||
}
|
||||
return $this->opCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a series of nested arrays containing different groups of generated
|
||||
* opcodes for the differences between the strings with up to $context lines
|
||||
* of surrounding content.
|
||||
*
|
||||
* Essentially what happens here is any big equal blocks of strings are stripped
|
||||
* out, the smaller subsets of changes are then arranged in to their groups.
|
||||
* This means that the sequence matcher and diffs do not need to include the full
|
||||
* content of the different files but can still provide context as to where the
|
||||
* changes are.
|
||||
*
|
||||
* @param int $context The number of lines of context to provide around the groups.
|
||||
* @return array Nested array of all of the grouped opcodes.
|
||||
*/
|
||||
public function getGroupedOpcodes($context=3)
|
||||
{
|
||||
$opCodes = $this->getOpCodes();
|
||||
if(empty($opCodes)) {
|
||||
$opCodes = array(
|
||||
array(
|
||||
'equal',
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if($opCodes[0][0] == 'equal') {
|
||||
$opCodes[0] = array(
|
||||
$opCodes[0][0],
|
||||
max($opCodes[0][1], $opCodes[0][2] - $context),
|
||||
$opCodes[0][2],
|
||||
max($opCodes[0][3], $opCodes[0][4] - $context),
|
||||
$opCodes[0][4]
|
||||
);
|
||||
}
|
||||
|
||||
$lastItem = count($opCodes) - 1;
|
||||
if($opCodes[$lastItem][0] == 'equal') {
|
||||
list($tag, $i1, $i2, $j1, $j2) = $opCodes[$lastItem];
|
||||
$opCodes[$lastItem] = array(
|
||||
$tag,
|
||||
$i1,
|
||||
min($i2, $i1 + $context),
|
||||
$j1,
|
||||
min($j2, $j1 + $context)
|
||||
);
|
||||
}
|
||||
|
||||
$maxRange = $context * 2;
|
||||
$groups = array();
|
||||
$group = array();
|
||||
foreach($opCodes as $code) {
|
||||
list($tag, $i1, $i2, $j1, $j2) = $code;
|
||||
if($tag == 'equal' && $i2 - $i1 > $maxRange) {
|
||||
$group[] = array(
|
||||
$tag,
|
||||
$i1,
|
||||
min($i2, $i1 + $context),
|
||||
$j1,
|
||||
min($j2, $j1 + $context)
|
||||
);
|
||||
$groups[] = $group;
|
||||
$group = array();
|
||||
$i1 = max($i1, $i2 - $context);
|
||||
$j1 = max($j1, $j2 - $context);
|
||||
}
|
||||
$group[] = array(
|
||||
$tag,
|
||||
$i1,
|
||||
$i2,
|
||||
$j1,
|
||||
$j2
|
||||
);
|
||||
}
|
||||
|
||||
if(!empty($group) && !(count($group) == 1 && $group[0][0] == 'equal')) {
|
||||
$groups[] = $group;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a measure of the similarity between the two sequences.
|
||||
* This will be a float value between 0 and 1.
|
||||
*
|
||||
* Out of all of the ratio calculation functions, this is the most
|
||||
* expensive to call if getMatchingBlocks or getOpCodes is yet to be
|
||||
* called. The other calculation methods (quickRatio and realquickRatio)
|
||||
* can be used to perform quicker calculations but may be less accurate.
|
||||
*
|
||||
* The ratio is calculated as (2 * number of matches) / total number of
|
||||
* elements in both sequences.
|
||||
*
|
||||
* @return float The calculated ratio.
|
||||
*/
|
||||
public function Ratio()
|
||||
{
|
||||
$matches = array_reduce($this->getMatchingBlocks(), array($this, 'ratioReduce'), 0);
|
||||
return $this->calculateRatio($matches, count ($this->a) + count ($this->b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to calculate the number of matches for Ratio().
|
||||
*
|
||||
* @param int $sum The running total for the number of matches.
|
||||
* @param array $triple Array containing the matching block triple to add to the running total.
|
||||
* @return int The new running total for the number of matches.
|
||||
*/
|
||||
private function ratioReduce($sum, $triple)
|
||||
{
|
||||
return $sum + ($triple[count($triple) - 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quickly return an upper bound ratio for the similarity of the strings.
|
||||
* This is quicker to compute than Ratio().
|
||||
*
|
||||
* @return float The calculated ratio.
|
||||
*/
|
||||
private function quickRatio()
|
||||
{
|
||||
if($this->fullBCount === null) {
|
||||
$this->fullBCount = array();
|
||||
$bLength = count ($b);
|
||||
for($i = 0; $i < $bLength; ++$i) {
|
||||
$char = $this->b[$i];
|
||||
$this->fullBCount[$char] = $this->arrayGetDefault($this->fullBCount, $char, 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
$avail = array();
|
||||
$matches = 0;
|
||||
$aLength = count ($this->a);
|
||||
for($i = 0; $i < $aLength; ++$i) {
|
||||
$char = $this->a[$i];
|
||||
if(isset($avail[$char])) {
|
||||
$numb = $avail[$char];
|
||||
}
|
||||
else {
|
||||
$numb = $this->arrayGetDefault($this->fullBCount, $char, 0);
|
||||
}
|
||||
$avail[$char] = $numb - 1;
|
||||
if($numb > 0) {
|
||||
++$matches;
|
||||
}
|
||||
}
|
||||
|
||||
$this->calculateRatio($matches, count ($this->a) + count ($this->b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an upper bound ratio really quickly for the similarity of the strings.
|
||||
* This is quicker to compute than Ratio() and quickRatio().
|
||||
*
|
||||
* @return float The calculated ratio.
|
||||
*/
|
||||
private function realquickRatio()
|
||||
{
|
||||
$aLength = count ($this->a);
|
||||
$bLength = count ($this->b);
|
||||
|
||||
return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for calculating the ratio to measure similarity for the strings.
|
||||
* The ratio is defined as being 2 * (number of matches / total length)
|
||||
*
|
||||
* @param int $matches The number of matches in the two strings.
|
||||
* @param int $length The length of the two strings.
|
||||
* @return float The calculated ratio.
|
||||
*/
|
||||
private function calculateRatio($matches, $length=0)
|
||||
{
|
||||
if($length) {
|
||||
return 2 * ($matches / $length);
|
||||
}
|
||||
else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that provides the ability to return the value for a key
|
||||
* in an array of it exists, or if it doesn't then return a default value.
|
||||
* Essentially cleaner than doing a series of if(isset()) {} else {} calls.
|
||||
*
|
||||
* @param array $array The array to search.
|
||||
* @param string $key The key to check that exists.
|
||||
* @param mixed $default The value to return as the default value if the key doesn't exist.
|
||||
* @return mixed The value from the array if the key exists or otherwise the default.
|
||||
*/
|
||||
private function arrayGetDefault($array, $key, $default)
|
||||
{
|
||||
if(isset($array[$key])) {
|
||||
return $array[$key];
|
||||
}
|
||||
else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks
|
||||
*
|
||||
* @param array $a First array to compare.
|
||||
* @param array $b Second array to compare.
|
||||
* @return int -1, 0 or 1, as expected by the usort function.
|
||||
*/
|
||||
private function tupleSort($a, $b)
|
||||
{
|
||||
$max = max(count($a), count($b));
|
||||
for($i = 0; $i < $max; ++$i) {
|
||||
if($a[$i] < $b[$i]) {
|
||||
return -1;
|
||||
}
|
||||
else if($a[$i] > $b[$i]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if(count($a) == $count($b)) {
|
||||
return 0;
|
||||
}
|
||||
else if(count($a) < count($b)) {
|
||||
return -1;
|
||||
}
|
||||
else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,10 @@ a {
|
|||
font-weight: normal;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: none;
|
||||
}
|
||||
|
||||
table.avp th {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
@ -427,3 +431,103 @@ div.diff {
|
|||
.tree a.abstract { background-image: url('../img/director/tree.png'); }
|
||||
.tree a.object { background-image: url('../img/director/leaf.gif'); }
|
||||
|
||||
/** php-diff **/
|
||||
|
||||
.Differences {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
empty-cells: show;
|
||||
}
|
||||
|
||||
.Differences thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.Differences thead th {
|
||||
text-align: left;
|
||||
padding-left: 4 / 14 * 16em;
|
||||
}
|
||||
.Differences tbody th {
|
||||
text-align: right;
|
||||
width: 4em;
|
||||
padding: 1px 2px;
|
||||
border-right: 1px solid @gray-light;
|
||||
background: @gray-lightest;
|
||||
font-weight: normal;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.Differences tbody td {
|
||||
width: 50%;
|
||||
.preformatted();
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@color-diff-ins: #bfb;
|
||||
@color-diff-del: #faa;
|
||||
@color-diff-changed-old: #fdd;
|
||||
@color-diff-changed-new: #efe;
|
||||
|
||||
.DifferencesSideBySide {
|
||||
ins, del {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ChangeInsert {
|
||||
td.Left {
|
||||
background: @gray-lighter;
|
||||
}
|
||||
td.Right {
|
||||
background: @color-diff-ins;
|
||||
}
|
||||
}
|
||||
|
||||
.ChangeDelete {
|
||||
td.Left {
|
||||
background: @color-diff-del;
|
||||
}
|
||||
td.Right {
|
||||
background: @gray-ligher;
|
||||
}
|
||||
}
|
||||
|
||||
.ChangeReplace {
|
||||
td.Left {
|
||||
background: @color-diff-changed-old;
|
||||
del {
|
||||
background: @color-diff-del;
|
||||
}
|
||||
}
|
||||
|
||||
td.Right {
|
||||
background: @color-diff-changed-new;
|
||||
ins {
|
||||
background: @color-diff-ins;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.Differences .Skipped {
|
||||
background: @gray-lightest;
|
||||
}
|
||||
|
||||
.DifferencesInline .ChangeReplace .Left,
|
||||
.DifferencesInline .ChangeDelete .Left {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
.DifferencesInline .ChangeReplace .Right,
|
||||
.DifferencesInline .ChangeInsert .Right {
|
||||
background: #dfd;
|
||||
}
|
||||
|
||||
.DifferencesInline .ChangeReplace ins {
|
||||
background: #9e9;
|
||||
}
|
||||
|
||||
.DifferencesInline .ChangeReplace del {
|
||||
background: #e99;
|
||||
}
|
||||
/** END of php-diff **/
|
||||
|
|
Loading…
Reference in New Issue