1<?php
2if (! class_exists('syntax_plugin_diff')) {
3	if (! defined('DOKU_PLUGIN')) {
4		if (! defined('DOKU_INC')) {
5			define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/');
6		} // if
7		define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
8	} // if
9	// Include parent class:
10	require_once(DOKU_PLUGIN . 'syntax.php');
11
12/**
13 * <tt>syntax_plugin_diff.php </tt>- A PHP4 class that implements a
14 * plugin for highlighting <tt>diff</tt> output in <tt>DokuWiki</tt>
15 * pages.
16 *
17 * <p>
18 * The purpose of this plugin is to provide a facility for inserting
19 * a <tt>diff</tt> file into a Wiki page. While this could be done by
20 * using the <tt>code</tt> tag this plugin additionally provides some
21 * visual feedback (so-called "syntax highlighting") by emphasizing
22 * added/deleted lines using CSS rules.
23 * </p>
24 * <p>
25 * Three types of <tt>diff</tt> output formats are supported:
26 * </p>
27 * <dl>
28 * <dt><tt>unified</tt></dt>
29 * <dd>The output of the <tt>diff</tt> program with the <tt>-u</tt>
30 * commandline format option.</dd>
31 * <dt><tt>context</tt></dt>
32 * <dd>The output of the <tt>diff</tt> program with the <tt>-c</tt>
33 * commandline format option.</dd>
34 * <dt><tt>context</tt></dt>
35 * <dd>The output of the <tt>diff</tt> program with the <tt>-n</tt>
36 * commandline format option.</dd>
37 * <dt><tt>simple</tt></dt>
38 * <dd>The output of the <tt>diff</tt> program without any commandline
39 * format option.</dd>
40 * </dl><pre>
41 *	Copyright (C) 2005, 2010  M.Watermann, D-10247 Berlin, FRG
42 *			All rights reserved
43 *		EMail : &lt;support@mwat.de&gt;
44 * </pre>
45 * <div class="disclaimer">
46 * This program is free software; you can redistribute it and/or modify
47 * it under the terms of the GNU General Public License as published by
48 * the Free Software Foundation; either
49 * <a href="http://www.gnu.org/licenses/gpl.html">version 3</a> of the
50 * License, or (at your option) any later version.<br>
51 * This software is distributed in the hope that it will be useful, but
52 * WITHOUT ANY WARRANTY; without even the implied warranty of
53 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
54 * General Public License for more details.
55 * </div>
56 * @author <a href="mailto:support@mwat.de">Matthias Watermann</a>
57 * @version <tt>$Id: syntax_plugin_diff.php,v 1.11 2010/02/22 10:49:59 matthias Exp $</tt>
58 * @since created 14-Aug-2005
59 */
60class syntax_plugin_diff extends DokuWiki_Syntax_Plugin {
61
62	/**
63	 * @privatesection
64	 */
65	//@{
66
67	/**
68	 * Prepare the markup to render the DIFF text.
69	 *
70	 * @param $aText String The DIFF text to markup.
71	 * @param $aFormat String The DIFF format used ('u', 'c', 'n|r', 's').
72	 * @param $aDoc String Reference to the current renderer's
73	 * <tt>doc</tt> property.
74	 * @return Boolean <tt>TRUE</tt>.
75	 * @private
76	 * @see render()
77	 */
78	function _addDiff(&$aText, &$aFormat, &$aDoc) {
79		// Since we're inside a PRE block we need the leading LFs:
80		$ADD = "\n" . '<span class="diff-addedline">';
81		$DEL = "\n" . '<span class="diff-deletedline">';
82		$HEAD = "\n" . '<span class="diff-blockheader">';
83		$CLOSE = '</span>';
84		// Common headers for all formats;
85		// the RegEx needs at least ')#' appended!
86		$DiffHead = '#\n((?:diff\s[^\n]*)|(?:Index:\s[^\n]*)|(?:={60,})'
87			. '|(?:RCS file:\s[^\n]*)|(?:retrieving revision [0-9][^\n]*)';
88		switch ($aFormat) {
89			case 'u':	// unified output
90				$aDoc .= preg_replace(
91					array($DiffHead . '|(?:@@[^\n]*))#',
92						'|\n(\+[^\n]*)|',
93						'|\n(\-[^\n]*)|'),
94					array($HEAD . '\1' . $CLOSE,
95						$ADD . '\1' . $CLOSE,
96						$DEL . '\1' . $CLOSE),
97					$aText);
98				return TRUE;
99			case 'c':	// context output
100				$sections = preg_split('|(\n\*{5,})|',
101					preg_replace($DiffHead . ')#',
102						$HEAD . '\1' . $CLOSE,
103						$aText),
104					-1, PREG_SPLIT_DELIM_CAPTURE);
105				$sections[0] = preg_replace(
106					array('|\n(\-{3}[^\n]*)|',
107						'|\n(\*{3}[^\n]*)|'),
108					array($ADD . '\1' . $CLOSE,
109						$DEL . '\1' . $CLOSE),
110					$sections[0]);
111				$c = count($sections);
112				for ($i = 1; $c > $i; ++$i) {
113					$hits = array();
114					if (preg_match('|^\n(\*{5,})|',
115						$sections[$i], $hits)) {
116						unset($hits[0]);
117						$sections[$i] = $HEAD . $hits[1] . $CLOSE;
118					} else if (preg_match('|^\n(\x2A{3}\s[^\n]*)(.*)|s',
119						$sections[$i], $hits)) {
120						unset($hits[0]);	// free mem
121						$parts = preg_split('|\n(\-{3}\s[^\n]*)|',
122							$hits[2], -1, PREG_SPLIT_DELIM_CAPTURE);
123						// $parts[0] == OLD code
124						$parts[0] = preg_replace('|\n([!\-][^\n]*)|',
125							$DEL . '\1' . $CLOSE, $parts[0]);
126						// $parts[1] == head of NEW code
127						$parts[1] = $ADD . $parts[1] . $CLOSE;
128						// $parts[2] == NEW code
129						$parts[2] = preg_replace(
130							array('|\n([!\x2B][^\n]*)|',
131								'|\n(\x2A{3}[^\n]*)|'),
132							array($ADD . '\1' . $CLOSE,
133								$DEL . '\1' . $CLOSE),
134							$parts[2]);
135						if (isset($parts[3])) {
136							// TRUE when handling multi-file patches
137							$parts[3] = preg_replace('|^(\x2D{3}[^\n]*)|',
138								$ADD . '\1' . $CLOSE, $parts[3]);
139						} // if
140						$sections[$i] = $DEL . $hits[1] . $CLOSE
141							. implode('', $parts);
142					} // if
143					// ELSE: leave $sections[$i] as is
144				} // for
145				$aDoc .= implode('', $sections);
146				return TRUE;
147			case 'n':	// RCS output
148				// Only added lines are there so we highlight just the
149				// diff indicators while leaving the text alone.
150				$aDoc .= preg_replace(
151					array($DiffHead . ')#',
152						'|\n(d[0-9]+\s+[0-9]+)|',
153						'|\n(a[0-9]+\s+[0-9]+)|'),
154					array($HEAD . '\1' . $CLOSE,
155						$DEL . '\1' . $CLOSE,
156						$ADD . '\1' . $CLOSE),
157					$aText);
158				return TRUE;
159			case 's':	// simple output
160				$aDoc .= preg_replace(
161					array($DiffHead . '|((?:[0-9a-z]+(?:,[0-9a-z]+)*)(?:[^\n]*)))#',
162						'|\n(\x26#60;[^\n]*)|',
163						'|\n(\x26#62;[^\n]*)|'),
164					array($HEAD . '\1' . $CLOSE,
165						$DEL . '\1' . $CLOSE,
166						$ADD . '\1' . $CLOSE),
167					$aText);
168				return TRUE;
169			default:	// unknown diff format
170				$aDoc .= $aText;	// just append any unrecognized text
171				return TRUE;
172		} // switch
173	} // _addDiff()
174
175	//@}
176	/**
177	 * @publicsection
178	 */
179	//@{
180
181	/**
182	 * Tell the parser whether the plugin accepts syntax mode
183	 * <tt>$aMode</tt> within its own markup.
184	 *
185	 * <p>
186	 * This method returns <tt>FALSE</tt> since no other (DokuWiki)
187	 * types are allowed within a <tt>diff</tt> section.
188	 * </p>
189	 * @param $aMode String The requested syntaxmode.
190	 * @return Boolean <tt>FALSE</tt> (always).
191	 * @public
192	 */
193	function accepts($aMode) {
194		return FALSE;
195	} // accepts()
196
197	/**
198	 * Connect lookup pattern to lexer.
199	 *
200	 * @param $aMode String The desired rendermode.
201	 * @public
202	 * @see render()
203	 */
204	function connectTo($aMode) {
205		$this->Lexer->addEntryPattern(
206			'\x3Cdiff(?=[^\n\r]*?\x3E.*?\n\x3C\x2Fdiff\x3E)',
207			$aMode, 'plugin_diff');
208	} // connectTo()
209
210	/**
211	 * Get an associative array with plugin info.
212	 *
213	 * <p>
214	 * The returned array holds the following fields:
215	 * <dl>
216	 * <dt>author</dt><dd>Author of the plugin</dd>
217	 * <dt>email</dt><dd>Email address to contact the author</dd>
218	 * <dt>date</dt><dd>Last modified date of the plugin in
219	 * <tt>YYYY-MM-DD</tt> format</dd>
220	 * <dt>name</dt><dd>Name of the plugin</dd>
221	 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd>
222	 * <dt>url</dt><dd>Website with more information on the plugin
223	 * (eg. syntax description)</dd>
224	 * </dl>
225	 * @return Array Information about this plugin class.
226	 * @public
227	 * @static
228	 */
229	function getInfo() {
230		return array(
231			'author' =>	'Matthias Watermann',
232			'email' =>	'support@mwat.de',
233			'date' =>	'2010-02-22',
234			'name' =>	'diff Syntax Plugin',
235			'desc' =>	'Add diff Style	[<diff> ... </diff>]',
236			'url' =>	'http://www.dokuwiki.org/plugin:diff');
237	} // getInfo()
238
239	/**
240	 * Define how this plugin is handled regarding paragraphs.
241	 *
242	 * <p>
243	 * This method is important for correct XHTML nesting. It returns
244	 * one of the following values:
245	 * </p>
246	 * <dl>
247	 * <dt>normal</dt><dd>The plugin can be used inside paragraphs.</dd>
248	 * <dt>block</dt><dd>Open paragraphs need to be closed before
249	 * plugin output.</dd>
250	 * <dt>stack</dt><dd>Special case. Plugin wraps other paragraphs.</dd>
251	 * </dl>
252	 * @return String <tt>'block'</tt> .
253	 * @public
254	 * @static
255	 */
256	function getPType() {
257		return 'block';
258	} // getPType()
259
260	/**
261	 * Where to sort in?
262	 *
263	 * <p>
264	 * This method returns <tt>174</tt> an arbitrary value between
265	 * <tt>Doku_Parser_Mode_unformatted</tt> and
266	 * <tt>Doku_Parser_Mode_php</tt> (180).
267	 * </p>
268	 * @return Integer <tt>174</tt>.
269	 * @public
270	 * @static
271	 */
272	function getSort() {
273		return 174;
274	} // getSort()
275
276	/**
277	 * Get the type of syntax this plugin defines.
278	 *
279	 * @return String <tt>'protected'</tt>.
280	 * @public
281	 * @static
282	 */
283	function getType() {
284		return 'protected';
285	} // getType()
286
287	/**
288	 * Handler to prepare matched data for the rendering process.
289	 *
290	 * <p>
291	 * The <tt>$aState</tt> parameter gives the type of pattern which
292	 * triggered the call to this method:
293	 * </p><dl>
294	 * <dt>DOKU_LEXER_ENTER</dt>
295	 * <dd>a pattern set by <tt>addEntryPattern()</tt></dd>
296	 * <dt>DOKU_LEXER_EXIT</dt>
297	 * <dd> a pattern set by <tt>addExitPattern()</tt></dd>
298	 * <dt>DOKU_LEXER_UNMATCHED</dt>
299	 * <dd>ordinary text encountered within the plugin's syntax mode
300	 * which doesn't match any pattern.</dd>
301	 * </dl>
302	 * @param $aMatch String The text matched by the patterns.
303	 * @param $aState Integer The lexer state for the match.
304	 * @param $aPos Integer The character position of the matched text.
305	 * @param $aHandler Object Reference to the Doku_Handler object.
306	 * @return Array Index <tt>[0]</tt> holds the current
307	 * <tt>$aState</tt>, index <tt>[1]</tt> the diff type (i.e. either
308	 * <tt>'c'</tt> for 'context' format, <tt>'u'</tt> for 'unified'
309	 * format or <tt>'s'</tt> for the 'simple' format) and
310	 * index <tt>[2]</tt> holding the diff's patch text.
311	 * @public
312	 * @see render()
313	 * @static
314	 */
315	function handle($aMatch, $aState, $aPos, &$aHandler) {
316		if (DOKU_LEXER_UNMATCHED == $aState) {
317			$aMatch = explode('>', $aMatch, 2);
318			if ("\n" != $aMatch[1]{0}) {
319				// A leading LF is needed to recognize and handle
320				// the very first line with all the REs used.
321				$aMatch[1] = "\n" . $aMatch[1];
322			} // if
323		} else {
324			return array($aState);
325		} // if
326		$aMatch[0] = strtolower(trim($aMatch[0])) . '?';
327		switch ($aMatch[0] = $aMatch[0]{0}) {
328			case 'u':	// DIFF cmdline switch for 'unified'
329			case 'c':	// DIFF cmdline switch for 'context'
330			case 'n':	// DIFF cmdline switch for 'RCS'
331			case 's':
332				// We believe the format hint ...
333				// (or should we be more suspicious?)
334				break;
335			case 'r':	// Mnemonic for 'RCS'
336				$aMatch[0] = 'n';
337				break;
338			default:	// try to figure out the diff format actually used
339				if (preg_match(
340					'|\n(?:\x2A{5,}\n\x2A{3}\s[1-9]+.*?\x2A{4}\n.+?)+|s',
341					$aMatch[1])) {
342					$aMatch[0] = 'c';
343				} else if (preg_match(
344					'|\n@@\s\-[0-9]+,[0-9]+[ \+,0-9]+?@@\n.+\n|s',
345					$aMatch[1])) {
346					$aMatch[0] = 'u';
347				} else if (preg_match(
348					'|\n[ad][0-9]+\s+[0-9]+\r?\n|', $aMatch[1])) {
349					// We've to check this _before_ 'simple' since the REs
350					// are similar (this one is slightly more specific)
351					$aMatch[0] = 'n';
352				} else if (preg_match(
353					'|\n(?:[0-9a-z]+(?:,[0-9a-z]+)*)(?:[^\n]*\n.*?)+|',
354					$aMatch[1])) {
355					$aMatch[0] = 's';
356				} else {
357					$aMatch[0] = '?';
358				} // if
359		} // switch
360		return array($aState, $aMatch[0], str_replace(
361			array('&', '<', '>', "\t"),
362			array('&#38;', '&#60;', '&#62;', '    '),
363			$aMatch[1]));
364	} // handle()
365
366	/**
367	 * Add exit pattern to lexer.
368	 *
369	 * @public
370	 */
371	function postConnect() {
372		$this->Lexer->addExitPattern('(?<=\n)\x3C\x2Fdiff\x3E', 'plugin_diff');
373	} // postConnect()
374
375	/**
376	 * Handle the actual output creation.
377	 *
378	 * <p>
379	 * The method checks for the given <tt>$aMode</tt> and returns
380	 * <tt>FALSE</tt> when a mode isn't supported. <tt>$aRenderer</tt>
381	 * contains a reference to the renderer object which is currently
382	 * handling the rendering. The contents of <tt>$aData</tt> is the
383	 * return value of the <tt>handle()</tt> method.
384	 * </p>
385	 * @param $aFormat String The output format to being tendered.
386	 * @param $aRenderer Object A reference to the renderer object.
387	 * @param $aData Array The data created by the <tt>handle()</tt>
388	 * method.
389	 * @return Boolean <tt>TRUE</tt> if rendered correctly, or
390	 * <tt>FALSE</tt> otherwise.
391	 * @public
392	 * @see handle()
393	 */
394	function render($aFormat, &$aRenderer, &$aData) {
395		if ('xhtml' != $aFormat) {
396			return FALSE;
397		} // if
398		switch ($aData[0]) {
399			case DOKU_LEXER_UNMATCHED:
400				return $this->_addDiff($aData[2], $aData[1], $aRenderer->doc);
401			case DOKU_LEXER_ENTER:
402				$aRenderer->doc .= '<pre class="code diff">';
403				return TRUE;
404			case DOKU_LEXER_EXIT:
405				$aRenderer->doc .= '</pre>';
406			default:
407				return TRUE;
408		} // switch
409	} // render()
410
411	//@}
412} // class syntax_plugin_diff
413} // if
414?>
415