| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- <?php
- namespace GorHill\FineDiff;
- /**
- * 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()?)
- */
- /**
- * 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(__NAMESPACE__ . '\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>';
- }
- }
- }
|