xref: /web-bugs/src/Horde/Text/Diff.php (revision e3c4b0ac)
1<?php
2/**
3 * General API for generating and formatting diffs - the differences between
4 * two sequences of strings.
5 *
6 * The original PHP version of this code was written by Geoffrey T. Dairiki
7 * <dairiki@dairiki.org>, and is used/adapted with his permission.
8 *
9 * Copyright 2004 Geoffrey T. Dairiki <dairiki@dairiki.org>
10 * Copyright 2004-2017 Horde LLC (http://www.horde.org/)
11 *
12 * See the enclosed file COPYING for license information (LGPL). If you did
13 * not receive this file, see http://www.horde.org/licenses/lgpl21.
14 *
15 * @package Text_Diff
16 * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
17 */
18class Horde_Text_Diff
19{
20    /**
21     * Array of changes.
22     *
23     * @var array
24     */
25    protected $_edits;
26
27    /**
28     * Computes diffs between sequences of strings.
29     *
30     * @param string $engine     Name of the diffing engine to use.  'auto'
31     *                           will automatically select the best.
32     * @param array $params      Parameters to pass to the diffing engine.
33     *                           Normally an array of two arrays, each
34     *                           containing the lines from a file.
35     */
36    public function __construct($engine, $params)
37    {
38        if ($engine == 'auto') {
39            $engine = extension_loaded('xdiff') ? 'Xdiff' : 'Native';
40        } else {
41            $engine = Horde_String::ucfirst(basename($engine));
42        }
43
44        $class = 'Horde_Text_Diff_Engine_' . $engine;
45        $diff_engine = new $class();
46
47        $this->_edits = call_user_func_array(array($diff_engine, 'diff'), $params);
48    }
49
50    /**
51     * Returns the array of differences.
52     */
53    public function getDiff()
54    {
55        return $this->_edits;
56    }
57
58    /**
59     * returns the number of new (added) lines in a given diff.
60     *
61     * @return integer The number of new lines
62     */
63    public function countAddedLines()
64    {
65        $count = 0;
66        foreach ($this->_edits as $edit) {
67            if ($edit instanceof Horde_Text_Diff_Op_Add ||
68                $edit instanceof Horde_Text_Diff_Op_Change) {
69                $count += $edit->nfinal();
70            }
71        }
72        return $count;
73    }
74
75    /**
76     * Returns the number of deleted (removed) lines in a given diff.
77     *
78     * @return integer The number of deleted lines
79     */
80    public function countDeletedLines()
81    {
82        $count = 0;
83        foreach ($this->_edits as $edit) {
84            if ($edit instanceof Horde_Text_Diff_Op_Delete ||
85                $edit instanceof Horde_Text_Diff_Op_Change) {
86                $count += $edit->norig();
87            }
88        }
89        return $count;
90    }
91
92    /**
93     * Computes a reversed diff.
94     *
95     * Example:
96     * <code>
97     * $diff = new Horde_Text_Diff($lines1, $lines2);
98     * $rev = $diff->reverse();
99     * </code>
100     *
101     * @return Horde_Text_Diff  A Diff object representing the inverse of the
102     *                    original diff.  Note that we purposely don't return a
103     *                    reference here, since this essentially is a clone()
104     *                    method.
105     */
106    public function reverse()
107    {
108        if (version_compare(zend_version(), '2', '>')) {
109            $rev = clone($this);
110        } else {
111            $rev = $this;
112        }
113        $rev->_edits = array();
114        foreach ($this->_edits as $edit) {
115            $rev->_edits[] = $edit->reverse();
116        }
117        return $rev;
118    }
119
120    /**
121     * Checks for an empty diff.
122     *
123     * @return boolean  True if two sequences were identical.
124     */
125    public function isEmpty()
126    {
127        foreach ($this->_edits as $edit) {
128            if (!($edit instanceof Horde_Text_Diff_Op_Copy)) {
129                return false;
130            }
131        }
132        return true;
133    }
134
135    /**
136     * Computes the length of the Longest Common Subsequence (LCS).
137     *
138     * This is mostly for diagnostic purposes.
139     *
140     * @return integer  The length of the LCS.
141     */
142    public function lcs()
143    {
144        $lcs = 0;
145        foreach ($this->_edits as $edit) {
146            if ($edit instanceof Horde_Text_Diff_Op_Copy) {
147                $lcs += count($edit->orig);
148            }
149        }
150        return $lcs;
151    }
152
153    /**
154     * Gets the original set of lines.
155     *
156     * This reconstructs the $from_lines parameter passed to the constructor.
157     *
158     * @return array  The original sequence of strings.
159     */
160    public function getOriginal()
161    {
162        $lines = array();
163        foreach ($this->_edits as $edit) {
164            if ($edit->orig) {
165                array_splice($lines, count($lines), 0, $edit->orig);
166            }
167        }
168        return $lines;
169    }
170
171    /**
172     * Gets the final set of lines.
173     *
174     * This reconstructs the $to_lines parameter passed to the constructor.
175     *
176     * @return array  The sequence of strings.
177     */
178    public function getFinal()
179    {
180        $lines = array();
181        foreach ($this->_edits as $edit) {
182            if ($edit->final) {
183                array_splice($lines, count($lines), 0, $edit->final);
184            }
185        }
186        return $lines;
187    }
188
189    /**
190     * Removes trailing newlines from a line of text. This is meant to be used
191     * with array_walk().
192     *
193     * @param string $line  The line to trim.
194     * @param integer $key  The index of the line in the array. Not used.
195     */
196    public static function trimNewlines(&$line, $key)
197    {
198        $line = str_replace(array("\n", "\r"), '', $line);
199    }
200
201    /**
202     * Checks a diff for validity.
203     *
204     * This is here only for debugging purposes.
205     */
206    protected function _check($from_lines, $to_lines)
207    {
208        if (serialize($from_lines) != serialize($this->getOriginal())) {
209            trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
210        }
211        if (serialize($to_lines) != serialize($this->getFinal())) {
212            trigger_error("Reconstructed final doesn't match", E_USER_ERROR);
213        }
214
215        $rev = $this->reverse();
216        if (serialize($to_lines) != serialize($rev->getOriginal())) {
217            trigger_error("Reversed original doesn't match", E_USER_ERROR);
218        }
219        if (serialize($from_lines) != serialize($rev->getFinal())) {
220            trigger_error("Reversed final doesn't match", E_USER_ERROR);
221        }
222
223        $prevtype = null;
224        foreach ($this->_edits as $edit) {
225            if ($prevtype == get_class($edit)) {
226                trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
227            }
228            $prevtype = get_class($edit);
229        }
230
231        return true;
232    }
233}
234