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