<?php
if (! class_exists('syntax_plugin_diff')) {
	if (! defined('DOKU_PLUGIN')) {
		if (! defined('DOKU_INC')) {
			define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/');
		} // if
		define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
	} // if
	// Include parent class:
	require_once(DOKU_PLUGIN . 'syntax.php');

/**
 * <tt>syntax_plugin_diff.php </tt>- A PHP4 class that implements a
 * plugin for highlighting <tt>diff</tt> output in <tt>DokuWiki</tt>
 * pages.
 *
 * <p>
 * The purpose of this plugin is to provide a facility for inserting
 * a <tt>diff</tt> file into a Wiki page. While this could be done by
 * using the <tt>code</tt> tag this plugin additionally provides some
 * visual feedback (so-called "syntax highlighting") by emphasizing
 * added/deleted lines using CSS rules.
 * </p>
 * <p>
 * Three types of <tt>diff</tt> output formats are supported:
 * </p>
 * <dl>
 * <dt><tt>unified</tt></dt>
 * <dd>The output of the <tt>diff</tt> program with the <tt>-u</tt>
 * commandline format option.</dd>
 * <dt><tt>context</tt></dt>
 * <dd>The output of the <tt>diff</tt> program with the <tt>-c</tt>
 * commandline format option.</dd>
 * <dt><tt>context</tt></dt>
 * <dd>The output of the <tt>diff</tt> program with the <tt>-n</tt>
 * commandline format option.</dd>
 * <dt><tt>simple</tt></dt>
 * <dd>The output of the <tt>diff</tt> program without any commandline
 * format option.</dd>
 * </dl><pre>
 *	Copyright (C) 2005, 2010  M.Watermann, D-10247 Berlin, FRG
 *			All rights reserved
 *		EMail : &lt;support@mwat.de&gt;
 * </pre>
 * <div class="disclaimer">
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either
 * <a href="http://www.gnu.org/licenses/gpl.html">version 3</a> of the
 * License, or (at your option) any later version.<br>
 * This software is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * </div>
 * @author <a href="mailto:support@mwat.de">Matthias Watermann</a>
 * @version <tt>$Id: syntax_plugin_diff.php,v 1.11 2010/02/22 10:49:59 matthias Exp $</tt>
 * @since created 14-Aug-2005
 */
class syntax_plugin_diff extends DokuWiki_Syntax_Plugin {

	/**
	 * @privatesection
	 */
	//@{

	/**
	 * Prepare the markup to render the DIFF text.
	 *
	 * @param $aText String The DIFF text to markup.
	 * @param $aFormat String The DIFF format used ('u', 'c', 'n|r', 's').
	 * @param $aDoc String Reference to the current renderer's
	 * <tt>doc</tt> property.
	 * @return Boolean <tt>TRUE</tt>.
	 * @private
	 * @see render()
	 */
	function _addDiff(&$aText, &$aFormat, &$aDoc) {
		// Since we're inside a PRE block we need the leading LFs:
		$ADD = "\n" . '<span class="diff-addedline">';
		$DEL = "\n" . '<span class="diff-deletedline">';
		$HEAD = "\n" . '<span class="diff-blockheader">';
		$CLOSE = '</span>';
		// Common headers for all formats;
		// the RegEx needs at least ')#' appended!
		$DiffHead = '#\n((?:diff\s[^\n]*)|(?:Index:\s[^\n]*)|(?:={60,})'
			. '|(?:RCS file:\s[^\n]*)|(?:retrieving revision [0-9][^\n]*)';
		switch ($aFormat) {
			case 'u':	// unified output
				$aDoc .= preg_replace(
					array($DiffHead . '|(?:@@[^\n]*))#',
						'|\n(\+[^\n]*)|',
						'|\n(\-[^\n]*)|'),
					array($HEAD . '\1' . $CLOSE,
						$ADD . '\1' . $CLOSE,
						$DEL . '\1' . $CLOSE),
					$aText);
				return TRUE;
			case 'c':	// context output
				$sections = preg_split('|(\n\*{5,})|',
					preg_replace($DiffHead . ')#',
						$HEAD . '\1' . $CLOSE,
						$aText),
					-1, PREG_SPLIT_DELIM_CAPTURE);
				$sections[0] = preg_replace(
					array('|\n(\-{3}[^\n]*)|',
						'|\n(\*{3}[^\n]*)|'),
					array($ADD . '\1' . $CLOSE,
						$DEL . '\1' . $CLOSE),
					$sections[0]);
				$c = count($sections);
				for ($i = 1; $c > $i; ++$i) {
					$hits = array();
					if (preg_match('|^\n(\*{5,})|',
						$sections[$i], $hits)) {
						unset($hits[0]);
						$sections[$i] = $HEAD . $hits[1] . $CLOSE;
					} else if (preg_match('|^\n(\x2A{3}\s[^\n]*)(.*)|s',
						$sections[$i], $hits)) {
						unset($hits[0]);	// free mem
						$parts = preg_split('|\n(\-{3}\s[^\n]*)|',
							$hits[2], -1, PREG_SPLIT_DELIM_CAPTURE);
						// $parts[0] == OLD code
						$parts[0] = preg_replace('|\n([!\-][^\n]*)|',
							$DEL . '\1' . $CLOSE, $parts[0]);
						// $parts[1] == head of NEW code
						$parts[1] = $ADD . $parts[1] . $CLOSE;
						// $parts[2] == NEW code
						$parts[2] = preg_replace(
							array('|\n([!\x2B][^\n]*)|',
								'|\n(\x2A{3}[^\n]*)|'),
							array($ADD . '\1' . $CLOSE,
								$DEL . '\1' . $CLOSE),
							$parts[2]);
						if (isset($parts[3])) {
							// TRUE when handling multi-file patches
							$parts[3] = preg_replace('|^(\x2D{3}[^\n]*)|',
								$ADD . '\1' . $CLOSE, $parts[3]);
						} // if
						$sections[$i] = $DEL . $hits[1] . $CLOSE
							. implode('', $parts);
					} // if
					// ELSE: leave $sections[$i] as is
				} // for
				$aDoc .= implode('', $sections);
				return TRUE;
			case 'n':	// RCS output
				// Only added lines are there so we highlight just the
				// diff indicators while leaving the text alone.
				$aDoc .= preg_replace(
					array($DiffHead . ')#',
						'|\n(d[0-9]+\s+[0-9]+)|',
						'|\n(a[0-9]+\s+[0-9]+)|'),
					array($HEAD . '\1' . $CLOSE,
						$DEL . '\1' . $CLOSE,
						$ADD . '\1' . $CLOSE),
					$aText);
				return TRUE;
			case 's':	// simple output
				$aDoc .= preg_replace(
					array($DiffHead . '|((?:[0-9a-z]+(?:,[0-9a-z]+)*)(?:[^\n]*)))#',
						'|\n(\x26#60;[^\n]*)|',
						'|\n(\x26#62;[^\n]*)|'),
					array($HEAD . '\1' . $CLOSE,
						$DEL . '\1' . $CLOSE,
						$ADD . '\1' . $CLOSE),
					$aText);
				return TRUE;
			default:	// unknown diff format
				$aDoc .= $aText;	// just append any unrecognized text
				return TRUE;
		} // switch
	} // _addDiff()

	//@}
	/**
	 * @publicsection
	 */
	//@{

	/**
	 * Tell the parser whether the plugin accepts syntax mode
	 * <tt>$aMode</tt> within its own markup.
	 *
	 * <p>
	 * This method returns <tt>FALSE</tt> since no other (DokuWiki)
	 * types are allowed within a <tt>diff</tt> section.
	 * </p>
	 * @param $aMode String The requested syntaxmode.
	 * @return Boolean <tt>FALSE</tt> (always).
	 * @public
	 */
	function accepts($aMode) {
		return FALSE;
	} // accepts()

	/**
	 * Connect lookup pattern to lexer.
	 *
	 * @param $aMode String The desired rendermode.
	 * @public
	 * @see render()
	 */
	function connectTo($aMode) {
		$this->Lexer->addEntryPattern(
			'\x3Cdiff(?=[^\n\r]*?\x3E.*?\n\x3C\x2Fdiff\x3E)',
			$aMode, 'plugin_diff');
	} // connectTo()

	/**
	 * Get an associative array with plugin info.
	 *
	 * <p>
	 * The returned array holds the following fields:
	 * <dl>
	 * <dt>author</dt><dd>Author of the plugin</dd>
	 * <dt>email</dt><dd>Email address to contact the author</dd>
	 * <dt>date</dt><dd>Last modified date of the plugin in
	 * <tt>YYYY-MM-DD</tt> format</dd>
	 * <dt>name</dt><dd>Name of the plugin</dd>
	 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd>
	 * <dt>url</dt><dd>Website with more information on the plugin
	 * (eg. syntax description)</dd>
	 * </dl>
	 * @return Array Information about this plugin class.
	 * @public
	 * @static
	 */
	function getInfo() {
		return array(
			'author' =>	'Matthias Watermann',
			'email' =>	'support@mwat.de',
			'date' =>	'2010-02-22',
			'name' =>	'diff Syntax Plugin',
			'desc' =>	'Add diff Style	[<diff> ... </diff>]',
			'url' =>	'http://www.dokuwiki.org/plugin:diff');
	} // getInfo()

	/**
	 * Define how this plugin is handled regarding paragraphs.
	 *
	 * <p>
	 * This method is important for correct XHTML nesting. It returns
	 * one of the following values:
	 * </p>
	 * <dl>
	 * <dt>normal</dt><dd>The plugin can be used inside paragraphs.</dd>
	 * <dt>block</dt><dd>Open paragraphs need to be closed before
	 * plugin output.</dd>
	 * <dt>stack</dt><dd>Special case. Plugin wraps other paragraphs.</dd>
	 * </dl>
	 * @return String <tt>'block'</tt> .
	 * @public
	 * @static
	 */
	function getPType() {
		return 'block';
	} // getPType()

	/**
	 * Where to sort in?
	 *
	 * <p>
	 * This method returns <tt>174</tt> an arbitrary value between
	 * <tt>Doku_Parser_Mode_unformatted</tt> and
	 * <tt>Doku_Parser_Mode_php</tt> (180).
	 * </p>
	 * @return Integer <tt>174</tt>.
	 * @public
	 * @static
	 */
	function getSort() {
		return 174;
	} // getSort()

	/**
	 * Get the type of syntax this plugin defines.
	 *
	 * @return String <tt>'protected'</tt>.
	 * @public
	 * @static
	 */
	function getType() {
		return 'protected';
	} // getType()

	/**
	 * Handler to prepare matched data for the rendering process.
	 *
	 * <p>
	 * The <tt>$aState</tt> parameter gives the type of pattern which
	 * triggered the call to this method:
	 * </p><dl>
	 * <dt>DOKU_LEXER_ENTER</dt>
	 * <dd>a pattern set by <tt>addEntryPattern()</tt></dd>
	 * <dt>DOKU_LEXER_EXIT</dt>
	 * <dd> a pattern set by <tt>addExitPattern()</tt></dd>
	 * <dt>DOKU_LEXER_UNMATCHED</dt>
	 * <dd>ordinary text encountered within the plugin's syntax mode
	 * which doesn't match any pattern.</dd>
	 * </dl>
	 * @param $aMatch String The text matched by the patterns.
	 * @param $aState Integer The lexer state for the match.
	 * @param $aPos Integer The character position of the matched text.
	 * @param $aHandler Object Reference to the Doku_Handler object.
	 * @return Array Index <tt>[0]</tt> holds the current
	 * <tt>$aState</tt>, index <tt>[1]</tt> the diff type (i.e. either
	 * <tt>'c'</tt> for 'context' format, <tt>'u'</tt> for 'unified'
	 * format or <tt>'s'</tt> for the 'simple' format) and
	 * index <tt>[2]</tt> holding the diff's patch text. 
	 * @public
	 * @see render()
	 * @static
	 */
	function handle($aMatch, $aState, $aPos, &$aHandler) {
		if (DOKU_LEXER_UNMATCHED == $aState) {
			$aMatch = explode('>', $aMatch, 2);
			if ("\n" != $aMatch[1]{0}) {
				// A leading LF is needed to recognize and handle
				// the very first line with all the REs used.
				$aMatch[1] = "\n" . $aMatch[1];
			} // if
		} else {
			return array($aState);
		} // if
		$aMatch[0] = strtolower(trim($aMatch[0])) . '?';
		switch ($aMatch[0] = $aMatch[0]{0}) {
			case 'u':	// DIFF cmdline switch for 'unified'
			case 'c':	// DIFF cmdline switch for 'context'
			case 'n':	// DIFF cmdline switch for 'RCS'
			case 's':
				// We believe the format hint ...
				// (or should we be more suspicious?)
				break;
			case 'r':	// Mnemonic for 'RCS'
				$aMatch[0] = 'n';
				break;
			default:	// try to figure out the diff format actually used
				if (preg_match(
					'|\n(?:\x2A{5,}\n\x2A{3}\s[1-9]+.*?\x2A{4}\n.+?)+|s',
					$aMatch[1])) {
					$aMatch[0] = 'c';
				} else if (preg_match(
					'|\n@@\s\-[0-9]+,[0-9]+[ \+,0-9]+?@@\n.+\n|s',
					$aMatch[1])) {
					$aMatch[0] = 'u';
				} else if (preg_match(
					'|\n[ad][0-9]+\s+[0-9]+\r?\n|', $aMatch[1])) {
					// We've to check this _before_ 'simple' since the REs
					// are similar (this one is slightly more specific)
					$aMatch[0] = 'n';
				} else if (preg_match(
					'|\n(?:[0-9a-z]+(?:,[0-9a-z]+)*)(?:[^\n]*\n.*?)+|',
					$aMatch[1])) {
					$aMatch[0] = 's';
				} else {
					$aMatch[0] = '?';
				} // if
		} // switch
		return array($aState, $aMatch[0], str_replace(
			array('&', '<', '>', "\t"),
			array('&#38;', '&#60;', '&#62;', '    '),
			$aMatch[1]));
	} // handle()

	/**
	 * Add exit pattern to lexer.
	 *
	 * @public
	 */
	function postConnect() {
		$this->Lexer->addExitPattern('(?<=\n)\x3C\x2Fdiff\x3E', 'plugin_diff');
	} // postConnect()

	/**
	 * Handle the actual output creation.
	 *
	 * <p>
	 * The method checks for the given <tt>$aMode</tt> and returns
	 * <tt>FALSE</tt> when a mode isn't supported. <tt>$aRenderer</tt>
	 * contains a reference to the renderer object which is currently
	 * handling the rendering. The contents of <tt>$aData</tt> is the
	 * return value of the <tt>handle()</tt> method.
	 * </p>
	 * @param $aFormat String The output format to being tendered.
	 * @param $aRenderer Object A reference to the renderer object.
	 * @param $aData Array The data created by the <tt>handle()</tt>
	 * method.
	 * @return Boolean <tt>TRUE</tt> if rendered correctly, or
	 * <tt>FALSE</tt> otherwise.
	 * @public
	 * @see handle()
	 */
	function render($aFormat, &$aRenderer, &$aData) {
		if ('xhtml' != $aFormat) {
			return FALSE;
		} // if
		switch ($aData[0]) {
			case DOKU_LEXER_UNMATCHED:
				return $this->_addDiff($aData[2], $aData[1], $aRenderer->doc);
			case DOKU_LEXER_ENTER:
				$aRenderer->doc .= '<pre class="code diff">';
				return TRUE;
			case DOKU_LEXER_EXIT:
				$aRenderer->doc .= '</pre>';
			default:
				return TRUE;
		} // switch
	} // render()

	//@}
} // class syntax_plugin_diff
} // if
?>
