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