, and is used/adapted with his permission. * * Copyright 2004 Geoffrey T. Dairiki * Copyright 2004-2017 Horde LLC (http://www.horde.org/) * * See the enclosed file COPYING for license information (LGPL). If you did * not receive this file, see http://www.horde.org/licenses/lgpl21. * * @package Text_Diff * @author Geoffrey T. Dairiki */ class Horde_Text_Diff { /** * Array of changes. * * @var array */ protected $_edits; /** * Computes diffs between sequences of strings. * * @param string $engine Name of the diffing engine to use. 'auto' * will automatically select the best. * @param array $params Parameters to pass to the diffing engine. * Normally an array of two arrays, each * containing the lines from a file. */ public function __construct($engine, $params) { if ($engine == 'auto') { $engine = extension_loaded('xdiff') ? 'Xdiff' : 'Native'; } else { $engine = Horde_String::ucfirst(basename($engine)); } $class = 'Horde_Text_Diff_Engine_' . $engine; $diff_engine = new $class(); $this->_edits = call_user_func_array(array($diff_engine, 'diff'), $params); } /** * Returns the array of differences. */ public function getDiff() { return $this->_edits; } /** * returns the number of new (added) lines in a given diff. * * @return integer The number of new lines */ public function countAddedLines() { $count = 0; foreach ($this->_edits as $edit) { if ($edit instanceof Horde_Text_Diff_Op_Add || $edit instanceof Horde_Text_Diff_Op_Change) { $count += $edit->nfinal(); } } return $count; } /** * Returns the number of deleted (removed) lines in a given diff. * * @return integer The number of deleted lines */ public function countDeletedLines() { $count = 0; foreach ($this->_edits as $edit) { if ($edit instanceof Horde_Text_Diff_Op_Delete || $edit instanceof Horde_Text_Diff_Op_Change) { $count += $edit->norig(); } } return $count; } /** * Computes a reversed diff. * * Example: * * $diff = new Horde_Text_Diff($lines1, $lines2); * $rev = $diff->reverse(); * * * @return Horde_Text_Diff A Diff object representing the inverse of the * original diff. Note that we purposely don't return a * reference here, since this essentially is a clone() * method. */ public function reverse() { if (version_compare(zend_version(), '2', '>')) { $rev = clone($this); } else { $rev = $this; } $rev->_edits = array(); foreach ($this->_edits as $edit) { $rev->_edits[] = $edit->reverse(); } return $rev; } /** * Checks for an empty diff. * * @return boolean True if two sequences were identical. */ public function isEmpty() { foreach ($this->_edits as $edit) { if (!($edit instanceof Horde_Text_Diff_Op_Copy)) { return false; } } return true; } /** * Computes the length of the Longest Common Subsequence (LCS). * * This is mostly for diagnostic purposes. * * @return integer The length of the LCS. */ public function lcs() { $lcs = 0; foreach ($this->_edits as $edit) { if ($edit instanceof Horde_Text_Diff_Op_Copy) { $lcs += count($edit->orig); } } return $lcs; } /** * Gets the original set of lines. * * This reconstructs the $from_lines parameter passed to the constructor. * * @return array The original sequence of strings. */ public function getOriginal() { $lines = array(); foreach ($this->_edits as $edit) { if ($edit->orig) { array_splice($lines, count($lines), 0, $edit->orig); } } return $lines; } /** * Gets the final set of lines. * * This reconstructs the $to_lines parameter passed to the constructor. * * @return array The sequence of strings. */ public function getFinal() { $lines = array(); foreach ($this->_edits as $edit) { if ($edit->final) { array_splice($lines, count($lines), 0, $edit->final); } } return $lines; } /** * Removes trailing newlines from a line of text. This is meant to be used * with array_walk(). * * @param string $line The line to trim. * @param integer $key The index of the line in the array. Not used. */ public static function trimNewlines(&$line, $key) { $line = str_replace(array("\n", "\r"), '', $line); } /** * Checks a diff for validity. * * This is here only for debugging purposes. */ protected function _check($from_lines, $to_lines) { if (serialize($from_lines) != serialize($this->getOriginal())) { trigger_error("Reconstructed original doesn't match", E_USER_ERROR); } if (serialize($to_lines) != serialize($this->getFinal())) { trigger_error("Reconstructed final doesn't match", E_USER_ERROR); } $rev = $this->reverse(); if (serialize($to_lines) != serialize($rev->getOriginal())) { trigger_error("Reversed original doesn't match", E_USER_ERROR); } if (serialize($from_lines) != serialize($rev->getFinal())) { trigger_error("Reversed final doesn't match", E_USER_ERROR); } $prevtype = null; foreach ($this->_edits as $edit) { if ($prevtype == get_class($edit)) { trigger_error("Edit sequence is non-optimal", E_USER_ERROR); } $prevtype = get_class($edit); } return true; } }