1<?php
2/**
3 * A class to render Diffs in different formats.
4 *
5 * This class renders the diff in classic diff format. It is intended that
6 * this class be customized via inheritance, to obtain fancier outputs.
7 *
8 * $Horde: framework/Text_Diff/Diff/Renderer.php,v 1.5.10.12 2009/07/24 13:26:40 jan Exp $
9 *
10 * Copyright 2004-2009 The Horde Project (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://opensource.org/licenses/lgpl-license.php.
14 *
15 * @package Text_Diff
16 */
17class Text_Diff_Renderer {
18
19    /**
20     * Number of leading context "lines" to preserve.
21     *
22     * This should be left at zero for this class, but subclasses may want to
23     * set this to other values.
24     */
25    var $_leading_context_lines = 0;
26
27    /**
28     * Number of trailing context "lines" to preserve.
29     *
30     * This should be left at zero for this class, but subclasses may want to
31     * set this to other values.
32     */
33    var $_trailing_context_lines = 0;
34
35    /**
36     * Constructor.
37     */
38    function __construct($params = array())
39    {
40        foreach ($params as $param => $value) {
41            $v = '_' . $param;
42            if (isset($this->$v)) {
43                $this->$v = $value;
44            }
45        }
46    }
47
48    /**
49     * Get any renderer parameters.
50     *
51     * @return array  All parameters of this renderer object.
52     */
53    function getParams()
54    {
55        $params = array();
56        foreach (get_object_vars($this) as $k => $v) {
57            if ($k[0] == '_') {
58                $params[substr($k, 1)] = $v;
59            }
60        }
61
62        return $params;
63    }
64
65    /**
66     * Renders a diff.
67     *
68     * @param Text_Diff $diff  A Text_Diff object.
69     *
70     * @return string  The formatted output.
71     */
72    function render($diff)
73    {
74        $xi = $yi = 1;
75        $block = false;
76        $context = array();
77
78        $nlead = $this->_leading_context_lines;
79        $ntrail = $this->_trailing_context_lines;
80
81        $output = $this->_startDiff();
82
83        $diffs = $diff->getDiff();
84        foreach ($diffs as $i => $edit) {
85            /* If these are unchanged (copied) lines, and we want to keep
86             * leading or trailing context lines, extract them from the copy
87             * block. */
88            if (is_a($edit, 'Text_Diff_Op_copy')) {
89                /* Do we have any diff blocks yet? */
90                if (is_array($block)) {
91                    /* How many lines to keep as context from the copy
92                     * block. */
93                    $keep = $i == count($diffs) - 1 ? $ntrail : $nlead + $ntrail;
94                    if (count($edit->orig) <= $keep) {
95                        /* We have less lines in the block than we want for
96                         * context => keep the whole block. */
97                        $block[] = $edit;
98                    } else {
99                        if ($ntrail) {
100                            /* Create a new block with as many lines as we need
101                             * for the trailing context. */
102                            $context = array_slice($edit->orig, 0, $ntrail);
103                            $block[] = new Text_Diff_Op_copy($context);
104                        }
105                        /* @todo */
106                        $output .= $this->_block($x0, $ntrail + $xi - $x0,
107                                                 $y0, $ntrail + $yi - $y0,
108                                                 $block);
109                        $block = false;
110                    }
111                }
112                /* Keep the copy block as the context for the next block. */
113                $context = $edit->orig;
114            } else {
115                /* Don't we have any diff blocks yet? */
116                if (!is_array($block)) {
117                    /* Extract context lines from the preceding copy block. */
118                    $context = array_slice($context, count($context) - $nlead);
119                    $x0 = $xi - count($context);
120                    $y0 = $yi - count($context);
121                    $block = array();
122                    if ($context) {
123                        $block[] = new Text_Diff_Op_copy($context);
124                    }
125                }
126                $block[] = $edit;
127            }
128
129            if ($edit->orig) {
130                $xi += count($edit->orig);
131            }
132            if ($edit->final) {
133                $yi += count($edit->final);
134            }
135        }
136
137        if (is_array($block)) {
138            $output .= $this->_block($x0, $xi - $x0,
139                                     $y0, $yi - $y0,
140                                     $block);
141        }
142
143        return $output . $this->_endDiff();
144    }
145
146    function _block($xbeg, $xlen, $ybeg, $ylen, &$edits)
147    {
148        $output = $this->_startBlock($this->_blockHeader($xbeg, $xlen, $ybeg, $ylen));
149
150        foreach ($edits as $edit) {
151            switch (strtolower(get_class($edit))) {
152            case 'text_diff_op_copy':
153                $output .= $this->_context($edit->orig);
154                break;
155
156            case 'text_diff_op_add':
157                $output .= $this->_added($edit->final);
158                break;
159
160            case 'text_diff_op_delete':
161                $output .= $this->_deleted($edit->orig);
162                break;
163
164            case 'text_diff_op_change':
165                $output .= $this->_changed($edit->orig, $edit->final);
166                break;
167            }
168        }
169
170        return $output . $this->_endBlock();
171    }
172
173    function _startDiff()
174    {
175        return '';
176    }
177
178    function _endDiff()
179    {
180        return '';
181    }
182
183    function _blockHeader($xbeg, $xlen, $ybeg, $ylen)
184    {
185        if ($xlen > 1) {
186            $xbeg .= ',' . ($xbeg + $xlen - 1);
187        }
188        if ($ylen > 1) {
189            $ybeg .= ',' . ($ybeg + $ylen - 1);
190        }
191
192        // this matches the GNU Diff behaviour
193        if ($xlen && !$ylen) {
194            $ybeg--;
195        } elseif (!$xlen) {
196            $xbeg--;
197        }
198
199        return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
200    }
201
202    function _startBlock($header)
203    {
204        return $header . "\n";
205    }
206
207    function _endBlock()
208    {
209        return '';
210    }
211
212    function _lines($lines, $prefix = ' ')
213    {
214        return $prefix . implode("\n$prefix", $lines) . "\n";
215    }
216
217    function _context($lines)
218    {
219        return $this->_lines($lines, '  ');
220    }
221
222    function _added($lines)
223    {
224        return $this->_lines($lines, '> ');
225    }
226
227    function _deleted($lines)
228    {
229        return $this->_lines($lines, '< ');
230    }
231
232    function _changed($orig, $final)
233    {
234        return $this->_deleted($orig) . "---\n" . $this->_added($final);
235    }
236
237}
238