1<?php
2/**
3 * Parses unified or context diffs output from eg. the diff utility.
4 *
5 * Example:
6 * <code>
7 * $patch = file_get_contents('example.patch');
8 * $diff = new Text_Diff('string', array($patch));
9 * $renderer = new Text_Diff_Renderer_inline();
10 * echo $renderer->render($diff);
11 * </code>
12 *
13 * $Horde: framework/Text_Diff/Diff/Engine/string.php,v 1.5.2.7 2009/07/24 13:04:43 jan Exp $
14 *
15 * Copyright 2005 �rjan Persson <o@42mm.org>
16 * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
17 *
18 * See the enclosed file COPYING for license information (LGPL). If you did
19 * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
20 *
21 * @author  �rjan Persson <o@42mm.org>
22 * @package Text_Diff
23 * @since   0.2.0
24 */
25class Text_Diff_Engine_string {
26
27    /**
28     * Parses a unified or context diff.
29     *
30     * First param contains the whole diff and the second can be used to force
31     * a specific diff type. If the second parameter is 'autodetect', the
32     * diff will be examined to find out which type of diff this is.
33     *
34     * @param string $diff  The diff content.
35     * @param string $mode  The diff mode of the content in $diff. One of
36     *                      'context', 'unified', or 'autodetect'.
37     *
38     * @return array  List of all diff operations.
39     */
40    function diff($diff, $mode = 'autodetect')
41    {
42        // Detect line breaks.
43        $lnbr = "\n";
44        if (strpos($diff, "\r\n") !== false) {
45            $lnbr = "\r\n";
46        } elseif (strpos($diff, "\r") !== false) {
47            $lnbr = "\r";
48        }
49
50        // Make sure we have a line break at the EOF.
51        if (substr($diff, -strlen($lnbr)) != $lnbr) {
52            $diff .= $lnbr;
53        }
54
55        if ($mode != 'autodetect' && $mode != 'context' && $mode != 'unified') {
56            return PEAR::raiseError('Type of diff is unsupported');
57        }
58
59        if ($mode == 'autodetect') {
60            $context = strpos($diff, '***');
61            $unified = strpos($diff, '---');
62            if ($context === $unified) {
63                return PEAR::raiseError('Type of diff could not be detected');
64            } elseif ($context === false || $unified === false) {
65                $mode = $context !== false ? 'context' : 'unified';
66            } else {
67                $mode = $context < $unified ? 'context' : 'unified';
68            }
69        }
70
71        // Split by new line and remove the diff header, if there is one.
72        $diff = explode($lnbr, $diff);
73        if (($mode == 'context' && strpos($diff[0], '***') === 0) ||
74            ($mode == 'unified' && strpos($diff[0], '---') === 0)) {
75            array_shift($diff);
76            array_shift($diff);
77        }
78
79        if ($mode == 'context') {
80            return $this->parseContextDiff($diff);
81        } else {
82            return $this->parseUnifiedDiff($diff);
83        }
84    }
85
86    /**
87     * Parses an array containing the unified diff.
88     *
89     * @param array $diff  Array of lines.
90     *
91     * @return array  List of all diff operations.
92     */
93    function parseUnifiedDiff($diff)
94    {
95        $edits = array();
96        $end = count($diff) - 1;
97        for ($i = 0; $i < $end;) {
98            $diff1 = array();
99            switch (substr($diff[$i], 0, 1)) {
100            case ' ':
101                do {
102                    $diff1[] = substr($diff[$i], 1);
103                } while (++$i < $end && substr($diff[$i], 0, 1) == ' ');
104                $edits[] = new Text_Diff_Op_copy($diff1);
105                break;
106
107            case '+':
108                // get all new lines
109                do {
110                    $diff1[] = substr($diff[$i], 1);
111                } while (++$i < $end && substr($diff[$i], 0, 1) == '+');
112                $edits[] = new Text_Diff_Op_add($diff1);
113                break;
114
115            case '-':
116                // get changed or removed lines
117                $diff2 = array();
118                do {
119                    $diff1[] = substr($diff[$i], 1);
120                } while (++$i < $end && substr($diff[$i], 0, 1) == '-');
121
122                while ($i < $end && substr($diff[$i], 0, 1) == '+') {
123                    $diff2[] = substr($diff[$i++], 1);
124                }
125                if (count($diff2) == 0) {
126                    $edits[] = new Text_Diff_Op_delete($diff1);
127                } else {
128                    $edits[] = new Text_Diff_Op_change($diff1, $diff2);
129                }
130                break;
131
132            default:
133                $i++;
134                break;
135            }
136        }
137
138        return $edits;
139    }
140
141    /**
142     * Parses an array containing the context diff.
143     *
144     * @param array $diff  Array of lines.
145     *
146     * @return array  List of all diff operations.
147     */
148    function parseContextDiff(&$diff)
149    {
150        $edits = array();
151        $i = $max_i = $j = $max_j = 0;
152        $end = count($diff) - 1;
153        while ($i < $end && $j < $end) {
154            while ($i >= $max_i && $j >= $max_j) {
155                // Find the boundaries of the diff output of the two files
156                for ($i = $j;
157                     $i < $end && substr($diff[$i], 0, 3) == '***';
158                     $i++);
159                for ($max_i = $i;
160                     $max_i < $end && substr($diff[$max_i], 0, 3) != '---';
161                     $max_i++);
162                for ($j = $max_i;
163                     $j < $end && substr($diff[$j], 0, 3) == '---';
164                     $j++);
165                for ($max_j = $j;
166                     $max_j < $end && substr($diff[$max_j], 0, 3) != '***';
167                     $max_j++);
168            }
169
170            // find what hasn't been changed
171            $array = array();
172            while ($i < $max_i &&
173                   $j < $max_j &&
174                   strcmp($diff[$i], $diff[$j]) == 0) {
175                $array[] = substr($diff[$i], 2);
176                $i++;
177                $j++;
178            }
179
180            while ($i < $max_i && ($max_j-$j) <= 1) {
181                if ($diff[$i] != '' && substr($diff[$i], 0, 1) != ' ') {
182                    break;
183                }
184                $array[] = substr($diff[$i++], 2);
185            }
186
187            while ($j < $max_j && ($max_i-$i) <= 1) {
188                if ($diff[$j] != '' && substr($diff[$j], 0, 1) != ' ') {
189                    break;
190                }
191                $array[] = substr($diff[$j++], 2);
192            }
193            if (count($array) > 0) {
194                $edits[] = new Text_Diff_Op_copy($array);
195            }
196
197            if ($i < $max_i) {
198                $diff1 = array();
199                switch (substr($diff[$i], 0, 1)) {
200                case '!':
201                    $diff2 = array();
202                    do {
203                        $diff1[] = substr($diff[$i], 2);
204                        if ($j < $max_j && substr($diff[$j], 0, 1) == '!') {
205                            $diff2[] = substr($diff[$j++], 2);
206                        }
207                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '!');
208                    $edits[] = new Text_Diff_Op_change($diff1, $diff2);
209                    break;
210
211                case '+':
212                    do {
213                        $diff1[] = substr($diff[$i], 2);
214                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '+');
215                    $edits[] = new Text_Diff_Op_add($diff1);
216                    break;
217
218                case '-':
219                    do {
220                        $diff1[] = substr($diff[$i], 2);
221                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '-');
222                    $edits[] = new Text_Diff_Op_delete($diff1);
223                    break;
224                }
225            }
226
227            if ($j < $max_j) {
228                $diff2 = array();
229                switch (substr($diff[$j], 0, 1)) {
230                case '+':
231                    do {
232                        $diff2[] = substr($diff[$j++], 2);
233                    } while ($j < $max_j && substr($diff[$j], 0, 1) == '+');
234                    $edits[] = new Text_Diff_Op_add($diff2);
235                    break;
236
237                case '-':
238                    do {
239                        $diff2[] = substr($diff[$j++], 2);
240                    } while ($j < $max_j && substr($diff[$j], 0, 1) == '-');
241                    $edits[] = new Text_Diff_Op_delete($diff2);
242                    break;
243                }
244            }
245        }
246
247        return $edits;
248    }
249
250}
251