11 * @author Dan Fuhry |
11 * @author Dan Fuhry |
12 * @package Text_Diff |
12 * @package Text_Diff |
13 */ |
13 */ |
14 class Text_Diff_Renderer_xhtml extends Text_Diff_Renderer { |
14 class Text_Diff_Renderer_xhtml extends Text_Diff_Renderer { |
15 |
15 |
16 /** |
16 /** |
17 * Number of leading context "lines" to preserve. |
17 * Number of leading context "lines" to preserve. |
18 */ |
18 */ |
19 var $_leading_context_lines = 5; |
19 var $_leading_context_lines = 5; |
20 |
20 |
21 /** |
21 /** |
22 * Number of trailing context "lines" to preserve. |
22 * Number of trailing context "lines" to preserve. |
23 */ |
23 */ |
24 var $_trailing_context_lines = 3; |
24 var $_trailing_context_lines = 3; |
25 |
25 |
26 /** |
26 /** |
27 * Prefix for inserted text. |
27 * Prefix for inserted text. |
28 */ |
28 */ |
29 var $_ins_prefix = "<!-- Start added text -->\n<tr><td style='width: 0px;'>+</td><td class=\"diff-added\" style='width: 100%;'>"; |
29 var $_ins_prefix = "<!-- Start added text -->\n<tr><td style='width: 0px;'>+</td><td class=\"diff-added\" style='width: 100%;'>"; |
30 |
30 |
31 /** |
31 /** |
32 * Suffix for inserted text. |
32 * Suffix for inserted text. |
33 */ |
33 */ |
34 var $_ins_suffix = "</td></tr>\n<!-- End added text -->\n\n"; |
34 var $_ins_suffix = "</td></tr>\n<!-- End added text -->\n\n"; |
35 |
35 |
36 /** |
36 /** |
37 * Prefix for deleted text. |
37 * Prefix for deleted text. |
38 */ |
38 */ |
39 var $_del_prefix = "<!-- Start deleted text -->\n<tr><td style='width: 0px;'>-</td><td class=\"diff-deleted\" style='width: 100%;'>"; |
39 var $_del_prefix = "<!-- Start deleted text -->\n<tr><td style='width: 0px;'>-</td><td class=\"diff-deleted\" style='width: 100%;'>"; |
40 |
40 |
41 /** |
41 /** |
42 * Suffix for deleted text. |
42 * Suffix for deleted text. |
43 */ |
43 */ |
44 var $_del_suffix = "</td></tr>\n<!-- End deleted text -->\n\n"; |
44 var $_del_suffix = "</td></tr>\n<!-- End deleted text -->\n\n"; |
45 |
45 |
46 /** |
46 /** |
47 * Header for each change block. |
47 * Header for each change block. |
48 */ |
48 */ |
49 var $_block_header = ''; |
49 var $_block_header = ''; |
50 |
50 |
51 /** |
51 /** |
52 * What are we currently splitting on? Used to recurse to show word-level |
52 * What are we currently splitting on? Used to recurse to show word-level |
53 * changes. |
53 * changes. |
54 */ |
54 */ |
55 var $_split_level = 'lines'; |
55 var $_split_level = 'lines'; |
56 |
56 |
57 function _blockHeader($xbeg, $xlen, $ybeg, $ylen) |
57 function _blockHeader($xbeg, $xlen, $ybeg, $ylen) |
58 { |
58 { |
59 return "<!-- Start block -->\n<tr><td colspan='2' class='diff-block'>Line $xbeg: {$this->_block_header}</td></tr>"; |
59 return "<!-- Start block -->\n<tr><td colspan='2' class='diff-block'>Line $xbeg: {$this->_block_header}</td></tr>"; |
60 } |
60 } |
61 |
61 |
62 function _startBlock($header) |
62 function _startBlock($header) |
63 { |
63 { |
64 return $header; |
64 return $header; |
65 } |
65 } |
66 |
66 |
67 function _lines($lines, $prefix = ' ', $encode = true) |
67 function _lines($lines, $prefix = ' ', $encode = true) |
68 { |
68 { |
69 if ($encode) { |
69 if ($encode) { |
70 array_walk($lines, array(&$this, '_encode')); |
70 array_walk($lines, array(&$this, '_encode')); |
71 } |
71 } |
72 |
72 |
73 if ($this->_split_level == 'words') { |
73 if ($this->_split_level == 'words') { |
74 return implode('', $lines); |
74 return implode('', $lines); |
75 } else { |
75 } else { |
76 return implode("<br />", $lines) . "\n"; |
76 return implode("<br />", $lines) . "\n"; |
77 } |
77 } |
78 } |
78 } |
79 |
79 |
80 function _added($lines) |
80 function _added($lines) |
81 { |
81 { |
82 array_walk($lines, array(&$this, '_encode')); |
82 array_walk($lines, array(&$this, '_encode')); |
83 $lines[0] = $this->_ins_prefix . $lines[0]; |
83 $lines[0] = $this->_ins_prefix . $lines[0]; |
84 $lines[count($lines) - 1] .= $this->_ins_suffix; |
84 $lines[count($lines) - 1] .= $this->_ins_suffix; |
85 return $this->_lines($lines, ' ', false); |
85 return $this->_lines($lines, ' ', false); |
86 } |
86 } |
87 |
87 |
88 function _deleted($lines, $words = false) |
88 function _deleted($lines, $words = false) |
89 { |
89 { |
90 array_walk($lines, array(&$this, '_encode')); |
90 array_walk($lines, array(&$this, '_encode')); |
91 $lines[0] = $this->_del_prefix . $lines[0]; |
91 $lines[0] = $this->_del_prefix . $lines[0]; |
92 $lines[count($lines) - 1] .= $this->_del_suffix; |
92 $lines[count($lines) - 1] .= $this->_del_suffix; |
93 return $this->_lines($lines, ' ', false); |
93 return $this->_lines($lines, ' ', false); |
94 } |
94 } |
95 |
95 |
96 function _context($lines) |
96 function _context($lines) |
97 { |
97 { |
98 return "<!-- Start context -->\n<tr><td></td><td class=\"diff-context\">".$this->_lines($lines).'</td></tr>'."\n<!-- End context -->\n\n"; |
98 return "<!-- Start context -->\n<tr><td></td><td class=\"diff-context\">".$this->_lines($lines).'</td></tr>'."\n<!-- End context -->\n\n"; |
99 } |
99 } |
100 |
100 |
101 function _changed($orig, $final) |
101 function _changed($orig, $final) |
102 { |
102 { |
103 /* If we've already split on words, don't try to do so again - just display. */ |
103 /* If we've already split on words, don't try to do so again - just display. */ |
104 if ($this->_split_level == 'words') { |
104 if ($this->_split_level == 'words') { |
105 $prefix = ''; |
105 $prefix = ''; |
106 while ($orig[0] !== false && $final[0] !== false && |
106 while ($orig[0] !== false && $final[0] !== false && |
107 substr($orig[0], 0, 1) == ' ' && |
107 substr($orig[0], 0, 1) == ' ' && |
108 substr($final[0], 0, 1) == ' ') { |
108 substr($final[0], 0, 1) == ' ') { |
109 $prefix .= substr($orig[0], 0, 1); |
109 $prefix .= substr($orig[0], 0, 1); |
110 $orig[0] = substr($orig[0], 1); |
110 $orig[0] = substr($orig[0], 1); |
111 $final[0] = substr($final[0], 1); |
111 $final[0] = substr($final[0], 1); |
112 } |
112 } |
113 $ret = $prefix . $this->_deleted($orig) . $this->_added($final) . "\n"; |
113 $ret = $prefix . $this->_deleted($orig) . $this->_added($final) . "\n"; |
114 //echo 'DEBUG:<pre>'.htmlspecialchars($ret).'</pre>'; |
114 //echo 'DEBUG:<pre>'.htmlspecialchars($ret).'</pre>'; |
115 return $ret; |
115 return $ret; |
116 } |
116 } |
117 |
117 |
118 $text1 = implode("\n", $orig); |
118 $text1 = implode("\n", $orig); |
119 $text2 = implode("\n", $final); |
119 $text2 = implode("\n", $final); |
120 |
120 |
121 /* Non-printing newline marker. */ |
121 /* Non-printing newline marker. */ |
122 $nl = "\0"; |
122 $nl = "\0"; |
123 |
123 |
124 /* We want to split on word boundaries, but we need to |
124 /* We want to split on word boundaries, but we need to |
125 * preserve whitespace as well. Therefore we split on words, |
125 * preserve whitespace as well. Therefore we split on words, |
126 * but include all blocks of whitespace in the wordlist. */ |
126 * but include all blocks of whitespace in the wordlist. */ |
127 $diff = &new Text_Diff($this->_splitOnWords($text1, $nl), |
127 $diff = &new Text_Diff($this->_splitOnWords($text1, $nl), |
128 $this->_splitOnWords($text2, $nl)); |
128 $this->_splitOnWords($text2, $nl)); |
129 |
129 |
130 /* Get the diff in inline format. */ |
130 /* Get the diff in inline format. */ |
131 $renderer = &new Text_Diff_Renderer_inline(array_merge($this->getParams(), |
131 $renderer = &new Text_Diff_Renderer_inline(array_merge($this->getParams(), |
132 array('split_level' => 'words'))); |
132 array('split_level' => 'words'))); |
133 |
133 |
134 /* Run the diff and get the output. */ |
134 /* Run the diff and get the output. */ |
135 $ret = str_replace($nl, "<br />", $renderer->render($diff)); |
135 $ret = str_replace($nl, "<br />", $renderer->render($diff)); |
136 //echo 'DEBUG:<pre>'.htmlspecialchars($ret).'</pre>'; |
136 //echo 'DEBUG:<pre>'.htmlspecialchars($ret).'</pre>'; |
137 return $ret . "\n"; |
137 return $ret . "\n"; |
138 } |
138 } |
139 |
139 |
140 function _splitOnWords($string, $newlineEscape = "<br />") |
140 function _splitOnWords($string, $newlineEscape = "<br />") |
141 { |
141 { |
142 $words = array(); |
142 $words = array(); |
143 $length = strlen($string); |
143 $length = strlen($string); |
144 $pos = 0; |
144 $pos = 0; |
145 |
145 |
146 while ($pos < $length) { |
146 while ($pos < $length) { |
147 // Eat a word with any preceding whitespace. |
147 // Eat a word with any preceding whitespace. |
148 $spaces = strspn(substr($string, $pos), " \n"); |
148 $spaces = strspn(substr($string, $pos), " \n"); |
149 $nextpos = strcspn(substr($string, $pos + $spaces), " \n"); |
149 $nextpos = strcspn(substr($string, $pos + $spaces), " \n"); |
150 $words[] = str_replace("\n", $newlineEscape, substr($string, $pos, $spaces + $nextpos)); |
150 $words[] = str_replace("\n", $newlineEscape, substr($string, $pos, $spaces + $nextpos)); |
151 $pos += $spaces + $nextpos; |
151 $pos += $spaces + $nextpos; |
152 } |
152 } |
153 |
153 |
154 return $words; |
154 return $words; |
155 } |
155 } |
156 |
156 |
157 function _encode(&$string) |
157 function _encode(&$string) |
158 { |
158 { |
159 $string = htmlspecialchars($string); |
159 $string = htmlspecialchars($string); |
160 } |
160 } |
161 |
161 |
162 /** |
162 /** |
163 * Renders a diff. |
163 * Renders a diff. |
164 * |
164 * |
165 * @param Text_Diff $diff A Text_Diff object. |
165 * @param Text_Diff $diff A Text_Diff object. |
166 * |
166 * |
167 * @return string The formatted output. |
167 * @return string The formatted output. |
168 */ |
168 */ |
169 |
169 |
170 function render($diff) |
170 function render($diff) |
171 { |
171 { |
172 $xi = $yi = 1; |
172 $xi = $yi = 1; |
173 $block = false; |
173 $block = false; |
174 $context = array(); |
174 $context = array(); |
175 |
175 |
176 $nlead = $this->_leading_context_lines; |
176 $nlead = $this->_leading_context_lines; |
177 $ntrail = $this->_trailing_context_lines; |
177 $ntrail = $this->_trailing_context_lines; |
178 |
178 |
179 $output = $this->_startDiff(); |
179 $output = $this->_startDiff(); |
180 |
180 |
181 $diffs = $diff->getDiff(); |
181 $diffs = $diff->getDiff(); |
182 foreach ($diffs as $i => $edit) { |
182 foreach ($diffs as $i => $edit) { |
183 if (is_a($edit, 'Text_Diff_Op_copy')) { |
183 if (is_a($edit, 'Text_Diff_Op_copy')) { |
184 if (is_array($block)) { |
184 if (is_array($block)) { |
185 $keep = $i == count($diffs) - 1 ? $ntrail : $nlead + $ntrail; |
185 $keep = $i == count($diffs) - 1 ? $ntrail : $nlead + $ntrail; |
186 if (count($edit->orig) <= $keep) { |
186 if (count($edit->orig) <= $keep) { |
187 $block[] = $edit; |
187 $block[] = $edit; |
188 } else { |
188 } else { |
189 if ($ntrail) { |
189 if ($ntrail) { |
190 $context = array_slice($edit->orig, 0, $ntrail); |
190 $context = array_slice($edit->orig, 0, $ntrail); |
191 $block[] = &new Text_Diff_Op_copy($context); |
191 $block[] = &new Text_Diff_Op_copy($context); |
192 } |
192 } |
193 $bk = $this->_block($x0, $ntrail + $xi - $x0, |
193 $bk = $this->_block($x0, $ntrail + $xi - $x0, |
194 $y0, $ntrail + $yi - $y0, |
194 $y0, $ntrail + $yi - $y0, |
195 $block); |
195 $block); |
196 $output .= $bk; |
196 $output .= $bk; |
197 $block = false; |
197 $block = false; |
198 } |
198 } |
199 } |
199 } |
200 $context = $edit->orig; |
200 $context = $edit->orig; |
201 } else { |
201 } else { |
202 if (!is_array($block)) { |
202 if (!is_array($block)) { |
203 $context = array_slice($context, count($context) - $nlead); |
203 $context = array_slice($context, count($context) - $nlead); |
204 $x0 = $xi - count($context); |
204 $x0 = $xi - count($context); |
205 $y0 = $yi - count($context); |
205 $y0 = $yi - count($context); |
206 $block = array(); |
206 $block = array(); |
207 if ($context) { |
207 if ($context) { |
208 $block[] = &new Text_Diff_Op_copy($context); |
208 $block[] = &new Text_Diff_Op_copy($context); |
209 } |
209 } |
210 } |
210 } |
211 $block[] = $edit; |
211 $block[] = $edit; |
212 } |
212 } |
213 |
213 |
214 if ($edit->orig) { |
214 if ($edit->orig) { |
215 $xi += count($edit->orig); |
215 $xi += count($edit->orig); |
216 } |
216 } |
217 if ($edit->final) { |
217 if ($edit->final) { |
218 $yi += count($edit->final); |
218 $yi += count($edit->final); |
219 } |
219 } |
220 } |
220 } |
221 |
221 |
222 if (is_array($block)) { |
222 if (is_array($block)) { |
223 $bk = $this->_block($x0, $xi - $x0, |
223 $bk = $this->_block($x0, $xi - $x0, |
224 $y0, $yi - $y0, |
224 $y0, $yi - $y0, |
225 $block); |
225 $block); |
226 $output .= $bk; |
226 $output .= $bk; |
227 } |
227 } |
228 |
228 |
229 $final = $output . $this->_endDiff(); |
229 $final = $output . $this->_endDiff(); |
230 if ($final == '') $final = '<tr><td class="diff-block">No differences.</td></tr>'; |
230 if ($final == '') $final = '<tr><td class="diff-block">No differences.</td></tr>'; |
231 //$final = preg_replace('#('.preg_quote($this->_ins_suffix).'|'.preg_quote($this->_del_suffix).')(.+?)('.preg_quote($this->_ins_prefix).'|'.preg_quote($this->_ins_suffix).')#', '\\1<tr><td></td><td class="diff-context>\\2</td></tr>\\3', $final); |
231 //$final = preg_replace('#('.preg_quote($this->_ins_suffix).'|'.preg_quote($this->_del_suffix).')(.+?)('.preg_quote($this->_ins_prefix).'|'.preg_quote($this->_ins_suffix).')#', '\\1<tr><td></td><td class="diff-context>\\2</td></tr>\\3', $final); |
232 return '<table class="diff">'.$final.'</table>'."\n\n"; |
232 return '<table class="diff">'.$final.'</table>'."\n\n"; |
233 } |
233 } |
234 |
234 |
235 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) |
235 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) |
236 { |
236 { |
237 $output = $this->_startBlock($this->_blockHeader($xbeg, $xlen, $ybeg, $ylen)); |
237 $output = $this->_startBlock($this->_blockHeader($xbeg, $xlen, $ybeg, $ylen)); |
238 |
238 |
239 foreach ($edits as $edit) { |
239 foreach ($edits as $edit) { |
240 switch (strtolower(get_class($edit))) { |
240 switch (strtolower(get_class($edit))) { |
241 case 'text_diff_op_copy': |
241 case 'text_diff_op_copy': |
242 $output .= $this->_context($edit->orig); |
242 $output .= $this->_context($edit->orig); |
243 break; |
243 break; |
244 |
244 |
245 case 'text_diff_op_add': |
245 case 'text_diff_op_add': |
246 $output .= $this->_added($edit->final); |
246 $output .= $this->_added($edit->final); |
247 break; |
247 break; |
248 |
248 |
249 case 'text_diff_op_delete': |
249 case 'text_diff_op_delete': |
250 $output .= $this->_deleted($edit->orig); |
250 $output .= $this->_deleted($edit->orig); |
251 break; |
251 break; |
252 |
252 |
253 case 'text_diff_op_change': |
253 case 'text_diff_op_change': |
254 $output .= $this->_changed($edit->orig, $edit->final); |
254 $output .= $this->_changed($edit->orig, $edit->final); |
255 break; |
255 break; |
256 } |
256 } |
257 } |
257 } |
258 |
258 |
259 return $output . $this->_endBlock(); |
259 return $output . $this->_endBlock(); |
260 } |
260 } |
261 |
261 |
262 } |
262 } |