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);
    }
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);
    }
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 ) ; }
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.
Perform a character - level diff .