xref: /web-bugs/src/Horde/Text/Diff/Renderer/Inline.php (revision e3c4b0ac)
1<?php
2/**
3 * "Inline" diff renderer.
4 *
5 * This class renders diffs in the Wiki-style "inline" format.
6 *
7 * Copyright 2004-2017 Horde LLC (http://www.horde.org/)
8 *
9 * See the enclosed file COPYING for license information (LGPL). If you did
10 * not receive this file, see http://www.horde.org/licenses/lgpl21.
11 *
12 * @author  Ciprian Popovici
13 * @package Text_Diff
14 */
15class Horde_Text_Diff_Renderer_Inline extends Horde_Text_Diff_Renderer
16{
17    /**
18     * Number of leading context "lines" to preserve.
19     *
20     * @var integer
21     */
22    protected $_leading_context_lines = 10000;
23
24    /**
25     * Number of trailing context "lines" to preserve.
26     *
27     * @var integer
28     */
29    protected $_trailing_context_lines = 10000;
30
31    /**
32     * Prefix for inserted text.
33     *
34     * @var string
35     */
36    protected $_ins_prefix = '<ins>';
37
38    /**
39     * Suffix for inserted text.
40     *
41     * @var string
42     */
43    protected $_ins_suffix = '</ins>';
44
45    /**
46     * Prefix for deleted text.
47     *
48     * @var string
49     */
50    protected $_del_prefix = '<del>';
51
52    /**
53     * Suffix for deleted text.
54     *
55     * @var string
56     */
57    protected $_del_suffix = '</del>';
58
59    /**
60     * Header for each change block.
61     *
62     * @var string
63     */
64    protected $_block_header = '';
65
66    /**
67     * Whether to split down to character-level.
68     *
69     * @var boolean
70     */
71    protected $_split_characters = false;
72
73    /**
74     * What are we currently splitting on? Used to recurse to show word-level
75     * or character-level changes.
76     *
77     * @var string
78     */
79    protected $_split_level = 'lines';
80
81    protected function _blockHeader($xbeg, $xlen, $ybeg, $ylen)
82    {
83        return $this->_block_header;
84    }
85
86    protected function _startBlock($header)
87    {
88        return $header;
89    }
90
91    protected function _lines($lines, $prefix = ' ', $encode = true)
92    {
93        if ($encode) {
94            array_walk($lines, array(&$this, '_encode'));
95        }
96
97        if ($this->_split_level == 'lines') {
98            return implode("\n", $lines) . "\n";
99        } else {
100            return implode('', $lines);
101        }
102    }
103
104    protected function _added($lines)
105    {
106        array_walk($lines, array(&$this, '_encode'));
107        $lines[0] = $this->_ins_prefix . $lines[0];
108        $lines[count($lines) - 1] .= $this->_ins_suffix;
109        return $this->_lines($lines, ' ', false);
110    }
111
112    protected function _deleted($lines, $words = false)
113    {
114        array_walk($lines, array(&$this, '_encode'));
115        $lines[0] = $this->_del_prefix . $lines[0];
116        $lines[count($lines) - 1] .= $this->_del_suffix;
117        return $this->_lines($lines, ' ', false);
118    }
119
120    protected function _changed($orig, $final)
121    {
122        /* If we've already split on characters, just display. */
123        if ($this->_split_level == 'characters') {
124            return $this->_deleted($orig)
125                . $this->_added($final);
126        }
127
128        /* If we've already split on words, just display. */
129        if ($this->_split_level == 'words') {
130            $prefix = '';
131            while ($orig[0] !== false && $final[0] !== false &&
132                   substr($orig[0], 0, 1) == ' ' &&
133                   substr($final[0], 0, 1) == ' ') {
134                $prefix .= substr($orig[0], 0, 1);
135                $orig[0] = substr($orig[0], 1);
136                $final[0] = substr($final[0], 1);
137            }
138            return $prefix . $this->_deleted($orig) . $this->_added($final);
139        }
140
141        $text1 = implode("\n", $orig);
142        $text2 = implode("\n", $final);
143
144        /* Non-printing newline marker. */
145        $nl = "\0";
146
147        if ($this->_split_characters) {
148            $diff = new Horde_Text_Diff('native',
149                                  array(preg_split('//u', str_replace("\n", $nl, $text1)),
150                                        preg_split('//u', str_replace("\n", $nl, $text2))));
151        } else {
152            /* We want to split on word boundaries, but we need to preserve
153             * whitespace as well. Therefore we split on words, but include
154             * all blocks of whitespace in the wordlist. */
155            $diff = new Horde_Text_Diff('native',
156                                  array($this->_splitOnWords($text1, $nl),
157                                        $this->_splitOnWords($text2, $nl)));
158        }
159
160        /* Get the diff in inline format. */
161        $renderer = new Horde_Text_Diff_Renderer_inline
162            (array_merge($this->getParams(),
163                         array('split_level' => $this->_split_characters ? 'characters' : 'words')));
164
165        /* Run the diff and get the output. */
166        return str_replace($nl, "\n", $renderer->render($diff)) . "\n";
167    }
168
169    protected function _splitOnWords($string, $newlineEscape = "\n")
170    {
171        // Ignore \0; otherwise the while loop will never finish.
172        $string = str_replace("\0", '', $string);
173
174        $words = array();
175        $length = strlen($string);
176        $pos = 0;
177
178        while ($pos < $length) {
179            // Eat a word with any preceding whitespace.
180            $spaces = strspn(substr($string, $pos), " \n");
181            $nextpos = strcspn(substr($string, $pos + $spaces), " \n");
182            $words[] = str_replace("\n", $newlineEscape, substr($string, $pos, $spaces + $nextpos));
183            $pos += $spaces + $nextpos;
184        }
185
186        return $words;
187    }
188
189    protected function _encode(&$string)
190    {
191        $string = htmlspecialchars($string);
192    }
193}
194