about summary refs log tree commit diff stats
path: root/wiki/inc/parser
diff options
context:
space:
mode:
Diffstat (limited to 'wiki/inc/parser')
-rw-r--r--wiki/inc/parser/code.php73
-rw-r--r--wiki/inc/parser/handler.php1811
-rw-r--r--wiki/inc/parser/lexer.php614
-rw-r--r--wiki/inc/parser/metadata.php694
-rw-r--r--wiki/inc/parser/parser.php1034
-rw-r--r--wiki/inc/parser/renderer.php883
-rw-r--r--wiki/inc/parser/xhtml.php1970
-rw-r--r--wiki/inc/parser/xhtmlsummary.php89
8 files changed, 7168 insertions, 0 deletions
diff --git a/wiki/inc/parser/code.php b/wiki/inc/parser/code.php
new file mode 100644
index 0000000..f91f1d2
--- /dev/null
+++ b/wiki/inc/parser/code.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * A simple renderer that allows downloading of code and file snippets
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+if(!defined('DOKU_INC')) die('meh.');
+
+class Doku_Renderer_code extends Doku_Renderer {
+    var $_codeblock = 0;
+
+    /**
+     * Send the wanted code block to the browser
+     *
+     * When the correct block was found it exits the script.
+     *
+     * @param string $text
+     * @param string $language
+     * @param string $filename
+     */
+    function code($text, $language = null, $filename = '') {
+        global $INPUT;
+        if(!$language) $language = 'txt';
+        $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
+        if(!$filename) $filename = 'snippet.'.$language;
+        $filename = utf8_basename($filename);
+        $filename = utf8_stripspecials($filename, '_');
+
+        // send CRLF to Windows clients
+        if(strpos($INPUT->server->str('HTTP_USER_AGENT'), 'Windows') !== false) {
+            $text = str_replace("\n", "\r\n", $text);
+        }
+
+        if($this->_codeblock == $INPUT->str('codeblock')) {
+            header("Content-Type: text/plain; charset=utf-8");
+            header("Content-Disposition: attachment; filename=$filename");
+            header("X-Robots-Tag: noindex");
+            echo trim($text, "\r\n");
+            exit;
+        }
+
+        $this->_codeblock++;
+    }
+
+    /**
+     * Wraps around code()
+     *
+     * @param string $text
+     * @param string $language
+     * @param string $filename
+     */
+    function file($text, $language = null, $filename = '') {
+        $this->code($text, $language, $filename);
+    }
+
+    /**
+     * This should never be reached, if it is send a 404
+     */
+    function document_end() {
+        http_status(404);
+        echo '404 - Not found';
+        exit;
+    }
+
+    /**
+     * Return the format of the renderer
+     *
+     * @returns string 'code'
+     */
+    function getFormat() {
+        return 'code';
+    }
+}
diff --git a/wiki/inc/parser/handler.php b/wiki/inc/parser/handler.php
new file mode 100644
index 0000000..780c6cf
--- /dev/null
+++ b/wiki/inc/parser/handler.php
@@ -0,0 +1,1811 @@
+<?php
+if(!defined('DOKU_INC')) die('meh.');
+if (!defined('DOKU_PARSER_EOL')) define('DOKU_PARSER_EOL',"\n");   // add this to make handling test cases simpler
+
+class Doku_Handler {
+
+    var $Renderer = null;
+
+    var $CallWriter = null;
+
+    var $calls = array();
+
+    var $status = array(
+        'section' => false,
+        'doublequote' => 0,
+    );
+
+    var $rewriteBlocks = true;
+
+    function __construct() {
+        $this->CallWriter = new Doku_Handler_CallWriter($this);
+    }
+
+    /**
+     * @param string $handler
+     * @param mixed $args
+     * @param integer|string $pos
+     */
+    function _addCall($handler, $args, $pos) {
+        $call = array($handler,$args, $pos);
+        $this->CallWriter->writeCall($call);
+    }
+
+    function addPluginCall($plugin, $args, $state, $pos, $match) {
+        $call = array('plugin',array($plugin, $args, $state, $match), $pos);
+        $this->CallWriter->writeCall($call);
+    }
+
+    function _finalize(){
+
+        $this->CallWriter->finalise();
+
+        if ( $this->status['section'] ) {
+            $last_call = end($this->calls);
+            array_push($this->calls,array('section_close',array(), $last_call[2]));
+        }
+
+        if ( $this->rewriteBlocks ) {
+            $B = new Doku_Handler_Block();
+            $this->calls = $B->process($this->calls);
+        }
+
+        trigger_event('PARSER_HANDLER_DONE',$this);
+
+        array_unshift($this->calls,array('document_start',array(),0));
+        $last_call = end($this->calls);
+        array_push($this->calls,array('document_end',array(),$last_call[2]));
+    }
+
+    /**
+     * fetch the current call and advance the pointer to the next one
+     *
+     * @return bool|mixed
+     */
+    function fetch() {
+        $call = current($this->calls);
+        if($call !== false) {
+            next($this->calls); //advance the pointer
+            return $call;
+        }
+        return false;
+    }
+
+
+    /**
+     * Special plugin handler
+     *
+     * This handler is called for all modes starting with 'plugin_'.
+     * An additional parameter with the plugin name is passed
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     *
+     * @param string|integer $match
+     * @param string|integer $state
+     * @param integer $pos
+     * @param $pluginname
+     *
+     * @return bool
+     */
+    function plugin($match, $state, $pos, $pluginname){
+        $data = array($match);
+        /** @var DokuWiki_Syntax_Plugin $plugin */
+        $plugin = plugin_load('syntax',$pluginname);
+        if($plugin != null){
+            $data = $plugin->handle($match, $state, $pos, $this);
+        }
+        if ($data !== false) {
+            $this->addPluginCall($pluginname,$data,$state,$pos,$match);
+        }
+        return true;
+    }
+
+    function base($match, $state, $pos) {
+        switch ( $state ) {
+            case DOKU_LEXER_UNMATCHED:
+                $this->_addCall('cdata',array($match), $pos);
+                return true;
+            break;
+        }
+    }
+
+    function header($match, $state, $pos) {
+        // get level and title
+        $title = trim($match);
+        $level = 7 - strspn($title,'=');
+        if($level < 1) $level = 1;
+        $title = trim($title,'=');
+        $title = trim($title);
+
+        if ($this->status['section']) $this->_addCall('section_close',array(),$pos);
+
+        $this->_addCall('header',array($title,$level,$pos), $pos);
+
+        $this->_addCall('section_open',array($level),$pos);
+        $this->status['section'] = true;
+        return true;
+    }
+
+    function notoc($match, $state, $pos) {
+        $this->_addCall('notoc',array(),$pos);
+        return true;
+    }
+
+    function nocache($match, $state, $pos) {
+        $this->_addCall('nocache',array(),$pos);
+        return true;
+    }
+
+    function linebreak($match, $state, $pos) {
+        $this->_addCall('linebreak',array(),$pos);
+        return true;
+    }
+
+    function eol($match, $state, $pos) {
+        $this->_addCall('eol',array(),$pos);
+        return true;
+    }
+
+    function hr($match, $state, $pos) {
+        $this->_addCall('hr',array(),$pos);
+        return true;
+    }
+
+    /**
+     * @param string|integer $match
+     * @param string|integer $state
+     * @param integer $pos
+     * @param string $name
+     */
+    function _nestingTag($match, $state, $pos, $name) {
+        switch ( $state ) {
+            case DOKU_LEXER_ENTER:
+                $this->_addCall($name.'_open', array(), $pos);
+            break;
+            case DOKU_LEXER_EXIT:
+                $this->_addCall($name.'_close', array(), $pos);
+            break;
+            case DOKU_LEXER_UNMATCHED:
+                $this->_addCall('cdata',array($match), $pos);
+            break;
+        }
+    }
+
+    function strong($match, $state, $pos) {
+        $this->_nestingTag($match, $state, $pos, 'strong');
+        return true;
+    }
+
+    function emphasis($match, $state, $pos) {
+        $this->_nestingTag($match, $state, $pos, 'emphasis');
+        return true;
+    }
+
+    function underline($match, $state, $pos) {
+        $this->_nestingTag($match, $state, $pos, 'underline');
+        return true;
+    }
+
+    function monospace($match, $state, $pos) {
+        $this->_nestingTag($match, $state, $pos, 'monospace');
+        return true;
+    }
+
+    function subscript($match, $state, $pos) {
+        $this->_nestingTag($match, $state, $pos, 'subscript');
+        return true;
+    }
+
+    function superscript($match, $state, $pos) {
+        $this->_nestingTag($match, $state, $pos, 'superscript');
+        return true;
+    }
+
+    function deleted($match, $state, $pos) {
+        $this->_nestingTag($match, $state, $pos, 'deleted');
+        return true;
+    }
+
+
+    function footnote($match, $state, $pos) {
+//        $this->_nestingTag($match, $state, $pos, 'footnote');
+        if (!isset($this->_footnote)) $this->_footnote = false;
+
+        switch ( $state ) {
+            case DOKU_LEXER_ENTER:
+                // footnotes can not be nested - however due to limitations in lexer it can't be prevented
+                // we will still enter a new footnote mode, we just do nothing
+                if ($this->_footnote) {
+                    $this->_addCall('cdata',array($match), $pos);
+                    break;
+                }
+
+                $this->_footnote = true;
+
+                $ReWriter = new Doku_Handler_Nest($this->CallWriter,'footnote_close');
+                $this->CallWriter = & $ReWriter;
+                $this->_addCall('footnote_open', array(), $pos);
+            break;
+            case DOKU_LEXER_EXIT:
+                // check whether we have already exitted the footnote mode, can happen if the modes were nested
+                if (!$this->_footnote) {
+                    $this->_addCall('cdata',array($match), $pos);
+                    break;
+                }
+
+                $this->_footnote = false;
+
+                $this->_addCall('footnote_close', array(), $pos);
+                $this->CallWriter->process();
+                $ReWriter = & $this->CallWriter;
+                $this->CallWriter = & $ReWriter->CallWriter;
+            break;
+            case DOKU_LEXER_UNMATCHED:
+                $this->_addCall('cdata', array($match), $pos);
+            break;
+        }
+        return true;
+    }
+
+    function listblock($match, $state, $pos) {
+        switch ( $state ) {
+            case DOKU_LEXER_ENTER:
+                $ReWriter = new Doku_Handler_List($this->CallWriter);
+                $this->CallWriter = & $ReWriter;
+                $this->_addCall('list_open', array($match), $pos);
+            break;
+            case DOKU_LEXER_EXIT:
+                $this->_addCall('list_close', array(), $pos);
+                $this->CallWriter->process();
+                $ReWriter = & $this->CallWriter;
+                $this->CallWriter = & $ReWriter->CallWriter;
+            break;
+            case DOKU_LEXER_MATCHED:
+                $this->_addCall('list_item', array($match), $pos);
+            break;
+            case DOKU_LEXER_UNMATCHED:
+                $this->_addCall('cdata', array($match), $pos);
+            break;
+        }
+        return true;
+    }
+
+    function unformatted($match, $state, $pos) {
+        if ( $state == DOKU_LEXER_UNMATCHED ) {
+            $this->_addCall('unformatted',array($match), $pos);
+        }
+        return true;
+    }
+
+    function php($match, $state, $pos) {
+        global $conf;
+        if ( $state == DOKU_LEXER_UNMATCHED ) {
+            $this->_addCall('php',array($match), $pos);
+        }
+        return true;
+    }
+
+    function phpblock($match, $state, $pos) {
+        global $conf;
+        if ( $state == DOKU_LEXER_UNMATCHED ) {
+            $this->_addCall('phpblock',array($match), $pos);
+        }
+        return true;
+    }
+
+    function html($match, $state, $pos) {
+        global $conf;
+        if ( $state == DOKU_LEXER_UNMATCHED ) {
+            $this->_addCall('html',array($match), $pos);
+        }
+        return true;
+    }
+
+    function htmlblock($match, $state, $pos) {
+        global $conf;
+        if ( $state == DOKU_LEXER_UNMATCHED ) {
+            $this->_addCall('htmlblock',array($match), $pos);
+        }
+        return true;
+    }
+
+    function preformatted($match, $state, $pos) {
+        switch ( $state ) {
+            case DOKU_LEXER_ENTER:
+                $ReWriter = new Doku_Handler_Preformatted($this->CallWriter);
+                $this->CallWriter = $ReWriter;
+                $this->_addCall('preformatted_start',array(), $pos);
+            break;
+            case DOKU_LEXER_EXIT:
+                $this->_addCall('preformatted_end',array(), $pos);
+                $this->CallWriter->process();
+                $ReWriter = & $this->CallWriter;
+                $this->CallWriter = & $ReWriter->CallWriter;
+            break;
+            case DOKU_LEXER_MATCHED:
+                $this->_addCall('preformatted_newline',array(), $pos);
+            break;
+            case DOKU_LEXER_UNMATCHED:
+                $this->_addCall('preformatted_content',array($match), $pos);
+            break;
+        }
+
+        return true;
+    }
+
+    function quote($match, $state, $pos) {
+
+        switch ( $state ) {
+
+            case DOKU_LEXER_ENTER:
+                $ReWriter = new Doku_Handler_Quote($this->CallWriter);
+                $this->CallWriter = & $ReWriter;
+                $this->_addCall('quote_start',array($match), $pos);
+            break;
+
+            case DOKU_LEXER_EXIT:
+                $this->_addCall('quote_end',array(), $pos);
+                $this->CallWriter->process();
+                $ReWriter = & $this->CallWriter;
+                $this->CallWriter = & $ReWriter->CallWriter;
+            break;
+
+            case DOKU_LEXER_MATCHED:
+                $this->_addCall('quote_newline',array($match), $pos);
+            break;
+
+            case DOKU_LEXER_UNMATCHED:
+                $this->_addCall('cdata',array($match), $pos);
+            break;
+
+        }
+
+        return true;
+    }
+
+    /**
+     * Internal function for parsing highlight options.
+     * $options is parsed for key value pairs separated by commas.
+     * A value might also be missing in which case the value will simple
+     * be set to true. Commas in strings are ignored, e.g. option="4,56"
+     * will work as expected and will only create one entry.
+     *
+     * @param string $options space separated list of key-value pairs,
+     *                        e.g. option1=123, option2="456"
+     * @return array|null     Array of key-value pairs $array['key'] = 'value';
+     *                        or null if no entries found
+     */
+    protected function parse_highlight_options ($options) {
+        $result = array();
+        preg_match_all('/(\w+(?:="[^"]*"))|(\w+(?:=[^\s]*))|(\w+[^=\s\]])(?:\s*)/', $options, $matches, PREG_SET_ORDER);
+        foreach ($matches as $match) {
+            $equal_sign = strpos($match [0], '=');
+            if ($equal_sign === false) {
+                $key = trim($match[0]);
+                $result [$key] = 1;
+            } else {
+                $key = substr($match[0], 0, $equal_sign);
+                $value = substr($match[0], $equal_sign+1);
+                $value = trim($value, '"');
+                if (strlen($value) > 0) {
+                    $result [$key] = $value;
+                } else {
+                    $result [$key] = 1;
+                }
+            }
+        }
+
+        // Check for supported options
+        $result = array_intersect_key(
+            $result,
+            array_flip(array(
+                'enable_line_numbers',
+                'start_line_numbers_at',
+                'highlight_lines_extra',
+                'enable_keyword_links')
+            )
+        );
+
+        // Sanitize values
+        if(isset($result['enable_line_numbers'])) {
+            if($result['enable_line_numbers'] === 'false') {
+                $result['enable_line_numbers'] = false;
+            }
+            $result['enable_line_numbers'] = (bool) $result['enable_line_numbers'];
+        }
+        if(isset($result['highlight_lines_extra'])) {
+            $result['highlight_lines_extra'] = array_map('intval', explode(',', $result['highlight_lines_extra']));
+            $result['highlight_lines_extra'] = array_filter($result['highlight_lines_extra']);
+            $result['highlight_lines_extra'] = array_unique($result['highlight_lines_extra']);
+        }
+        if(isset($result['start_line_numbers_at'])) {
+            $result['start_line_numbers_at'] = (int) $result['start_line_numbers_at'];
+        }
+        if(isset($result['enable_keyword_links'])) {
+            if($result['enable_keyword_links'] === 'false') {
+                $result['enable_keyword_links'] = false;
+            }
+            $result['enable_keyword_links'] = (bool) $result['enable_keyword_links'];
+        }
+        if (count($result) == 0) {
+            return null;
+        }
+
+        return $result;
+    }
+
+    function file($match, $state, $pos) {
+        return $this->code($match, $state, $pos, 'file');
+    }
+
+    function code($match, $state, $pos, $type='code') {
+        if ( $state == DOKU_LEXER_UNMATCHED ) {
+            $matches = explode('>',$match,2);
+            // Cut out variable options enclosed in []
+            preg_match('/\[.*\]/', $matches[0], $options);
+            if (!empty($options[0])) {
+                $matches[0] = str_replace($options[0], '', $matches[0]);
+            }
+            $param = preg_split('/\s+/', $matches[0], 2, PREG_SPLIT_NO_EMPTY);
+            while(count($param) < 2) array_push($param, null);
+            // We shortcut html here.
+            if ($param[0] == 'html') $param[0] = 'html4strict';
+            if ($param[0] == '-') $param[0] = null;
+            array_unshift($param, $matches[1]);
+            if (!empty($options[0])) {
+                $param [] = $this->parse_highlight_options ($options[0]);
+            }
+            $this->_addCall($type, $param, $pos);
+        }
+        return true;
+    }
+
+    function acronym($match, $state, $pos) {
+        $this->_addCall('acronym',array($match), $pos);
+        return true;
+    }
+
+    function smiley($match, $state, $pos) {
+        $this->_addCall('smiley',array($match), $pos);
+        return true;
+    }
+
+    function wordblock($match, $state, $pos) {
+        $this->_addCall('wordblock',array($match), $pos);
+        return true;
+    }
+
+    function entity($match, $state, $pos) {
+        $this->_addCall('entity',array($match), $pos);
+        return true;
+    }
+
+    function multiplyentity($match, $state, $pos) {
+        preg_match_all('/\d+/',$match,$matches);
+        $this->_addCall('multiplyentity',array($matches[0][0],$matches[0][1]), $pos);
+        return true;
+    }
+
+    function singlequoteopening($match, $state, $pos) {
+        $this->_addCall('singlequoteopening',array(), $pos);
+        return true;
+    }
+
+    function singlequoteclosing($match, $state, $pos) {
+        $this->_addCall('singlequoteclosing',array(), $pos);
+        return true;
+    }
+
+    function apostrophe($match, $state, $pos) {
+        $this->_addCall('apostrophe',array(), $pos);
+        return true;
+    }
+
+    function doublequoteopening($match, $state, $pos) {
+        $this->_addCall('doublequoteopening',array(), $pos);
+        $this->status['doublequote']++;
+        return true;
+    }
+
+    function doublequoteclosing($match, $state, $pos) {
+        if ($this->status['doublequote'] <= 0) {
+            $this->doublequoteopening($match, $state, $pos);
+        } else {
+            $this->_addCall('doublequoteclosing',array(), $pos);
+            $this->status['doublequote'] = max(0, --$this->status['doublequote']);
+        }
+        return true;
+    }
+
+    function camelcaselink($match, $state, $pos) {
+        $this->_addCall('camelcaselink',array($match), $pos);
+        return true;
+    }
+
+    /*
+    */
+    function internallink($match, $state, $pos) {
+        // Strip the opening and closing markup
+        $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match);
+
+        // Split title from URL
+        $link = explode('|',$link,2);
+        if ( !isset($link[1]) ) {
+            $link[1] = null;
+        } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) {
+            // If the title is an image, convert it to an array containing the image details
+            $link[1] = Doku_Handler_Parse_Media($link[1]);
+        }
+        $link[0] = trim($link[0]);
+
+        //decide which kind of link it is
+
+        if ( link_isinterwiki($link[0]) ) {
+            // Interwiki
+            $interwiki = explode('>',$link[0],2);
+            $this->_addCall(
+                'interwikilink',
+                array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]),
+                $pos
+                );
+        }elseif ( preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u',$link[0]) ) {
+            // Windows Share
+            $this->_addCall(
+                'windowssharelink',
+                array($link[0],$link[1]),
+                $pos
+                );
+        }elseif ( preg_match('#^([a-z0-9\-\.+]+?)://#i',$link[0]) ) {
+            // external link (accepts all protocols)
+            $this->_addCall(
+                    'externallink',
+                    array($link[0],$link[1]),
+                    $pos
+                    );
+        }elseif ( preg_match('<'.PREG_PATTERN_VALID_EMAIL.'>',$link[0]) ) {
+            // E-Mail (pattern above is defined in inc/mail.php)
+            $this->_addCall(
+                'emaillink',
+                array($link[0],$link[1]),
+                $pos
+                );
+        }elseif ( preg_match('!^#.+!',$link[0]) ){
+            // local link
+            $this->_addCall(
+                'locallink',
+                array(substr($link[0],1),$link[1]),
+                $pos
+                );
+        }else{
+            // internal link
+            $this->_addCall(
+                'internallink',
+                array($link[0],$link[1]),
+                $pos
+                );
+        }
+
+        return true;
+    }
+
+    function filelink($match, $state, $pos) {
+        $this->_addCall('filelink',array($match, null), $pos);
+        return true;
+    }
+
+    function windowssharelink($match, $state, $pos) {
+        $this->_addCall('windowssharelink',array($match, null), $pos);
+        return true;
+    }
+
+    function media($match, $state, $pos) {
+        $p = Doku_Handler_Parse_Media($match);
+
+        $this->_addCall(
+              $p['type'],
+              array($p['src'], $p['title'], $p['align'], $p['width'],
+                     $p['height'], $p['cache'], $p['linking']),
+              $pos
+             );
+        return true;
+    }
+
+    function rss($match, $state, $pos) {
+        $link = preg_replace(array('/^\{\{rss>/','/\}\}$/'),'',$match);
+
+        // get params
+        list($link,$params) = explode(' ',$link,2);
+
+        $p = array();
+        if(preg_match('/\b(\d+)\b/',$params,$match)){
+            $p['max'] = $match[1];
+        }else{
+            $p['max'] = 8;
+        }
+        $p['reverse'] = (preg_match('/rev/',$params));
+        $p['author']  = (preg_match('/\b(by|author)/',$params));
+        $p['date']    = (preg_match('/\b(date)/',$params));
+        $p['details'] = (preg_match('/\b(desc|detail)/',$params));
+        $p['nosort']  = (preg_match('/\b(nosort)\b/',$params));
+
+        if (preg_match('/\b(\d+)([dhm])\b/',$params,$match)) {
+            $period = array('d' => 86400, 'h' => 3600, 'm' => 60);
+            $p['refresh'] = max(600,$match[1]*$period[$match[2]]);  // n * period in seconds, minimum 10 minutes
+        } else {
+            $p['refresh'] = 14400;   // default to 4 hours
+        }
+
+        $this->_addCall('rss',array($link,$p),$pos);
+        return true;
+    }
+
+    function externallink($match, $state, $pos) {
+        $url   = $match;
+        $title = null;
+
+        // add protocol on simple short URLs
+        if(substr($url,0,3) == 'ftp' && (substr($url,0,6) != 'ftp://')){
+            $title = $url;
+            $url   = 'ftp://'.$url;
+        }
+        if(substr($url,0,3) == 'www' && (substr($url,0,7) != 'http://')){
+            $title = $url;
+            $url = 'http://'.$url;
+        }
+
+        $this->_addCall('externallink',array($url, $title), $pos);
+        return true;
+    }
+
+    function emaillink($match, $state, $pos) {
+        $email = preg_replace(array('/^</','/>$/'),'',$match);
+        $this->_addCall('emaillink',array($email, null), $pos);
+        return true;
+    }
+
+    function table($match, $state, $pos) {
+        switch ( $state ) {
+
+            case DOKU_LEXER_ENTER:
+
+                $ReWriter = new Doku_Handler_Table($this->CallWriter);
+                $this->CallWriter = & $ReWriter;
+
+                $this->_addCall('table_start', array($pos + 1), $pos);
+                if ( trim($match) == '^' ) {
+                    $this->_addCall('tableheader', array(), $pos);
+                } else {
+                    $this->_addCall('tablecell', array(), $pos);
+                }
+            break;
+
+            case DOKU_LEXER_EXIT:
+                $this->_addCall('table_end', array($pos), $pos);
+                $this->CallWriter->process();
+                $ReWriter = & $this->CallWriter;
+                $this->CallWriter = & $ReWriter->CallWriter;
+            break;
+
+            case DOKU_LEXER_UNMATCHED:
+                if ( trim($match) != '' ) {
+                    $this->_addCall('cdata',array($match), $pos);
+                }
+            break;
+
+            case DOKU_LEXER_MATCHED:
+                if ( $match == ' ' ){
+                    $this->_addCall('cdata', array($match), $pos);
+                } else if ( preg_match('/:::/',$match) ) {
+                    $this->_addCall('rowspan', array($match), $pos);
+                } else if ( preg_match('/\t+/',$match) ) {
+                    $this->_addCall('table_align', array($match), $pos);
+                } else if ( preg_match('/ {2,}/',$match) ) {
+                    $this->_addCall('table_align', array($match), $pos);
+                } else if ( $match == "\n|" ) {
+                    $this->_addCall('table_row', array(), $pos);
+                    $this->_addCall('tablecell', array(), $pos);
+                } else if ( $match == "\n^" ) {
+                    $this->_addCall('table_row', array(), $pos);
+                    $this->_addCall('tableheader', array(), $pos);
+                } else if ( $match == '|' ) {
+                    $this->_addCall('tablecell', array(), $pos);
+                } else if ( $match == '^' ) {
+                    $this->_addCall('tableheader', array(), $pos);
+                }
+            break;
+        }
+        return true;
+    }
+}
+
+//------------------------------------------------------------------------
+function Doku_Handler_Parse_Media($match) {
+
+    // Strip the opening and closing markup
+    $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match);
+
+    // Split title from URL
+    $link = explode('|',$link,2);
+
+    // Check alignment
+    $ralign = (bool)preg_match('/^ /',$link[0]);
+    $lalign = (bool)preg_match('/ $/',$link[0]);
+
+    // Logic = what's that ;)...
+    if ( $lalign & $ralign ) {
+        $align = 'center';
+    } else if ( $ralign ) {
+        $align = 'right';
+    } else if ( $lalign ) {
+        $align = 'left';
+    } else {
+        $align = null;
+    }
+
+    // The title...
+    if ( !isset($link[1]) ) {
+        $link[1] = null;
+    }
+
+    //remove aligning spaces
+    $link[0] = trim($link[0]);
+
+    //split into src and parameters (using the very last questionmark)
+    $pos = strrpos($link[0], '?');
+    if($pos !== false){
+        $src   = substr($link[0],0,$pos);
+        $param = substr($link[0],$pos+1);
+    }else{
+        $src   = $link[0];
+        $param = '';
+    }
+
+    //parse width and height
+    if(preg_match('#(\d+)(x(\d+))?#i',$param,$size)){
+        !empty($size[1]) ? $w = $size[1] : $w = null;
+        !empty($size[3]) ? $h = $size[3] : $h = null;
+    } else {
+        $w = null;
+        $h = null;
+    }
+
+    //get linking command
+    if(preg_match('/nolink/i',$param)){
+        $linking = 'nolink';
+    }else if(preg_match('/direct/i',$param)){
+        $linking = 'direct';
+    }else if(preg_match('/linkonly/i',$param)){
+        $linking = 'linkonly';
+    }else{
+        $linking = 'details';
+    }
+
+    //get caching command
+    if (preg_match('/(nocache|recache)/i',$param,$cachemode)){
+        $cache = $cachemode[1];
+    }else{
+        $cache = 'cache';
+    }
+
+    // Check whether this is a local or remote image or interwiki
+    if (media_isexternal($src) || link_isinterwiki($src)){
+        $call = 'externalmedia';
+    } else {
+        $call = 'internalmedia';
+    }
+
+    $params = array(
+        'type'=>$call,
+        'src'=>$src,
+        'title'=>$link[1],
+        'align'=>$align,
+        'width'=>$w,
+        'height'=>$h,
+        'cache'=>$cache,
+        'linking'=>$linking,
+    );
+
+    return $params;
+}
+
+//------------------------------------------------------------------------
+interface Doku_Handler_CallWriter_Interface {
+    public function writeCall($call);
+    public function writeCalls($calls);
+    public function finalise();
+}
+
+class Doku_Handler_CallWriter implements Doku_Handler_CallWriter_Interface {
+
+    var $Handler;
+
+    /**
+     * @param Doku_Handler $Handler
+     */
+    function __construct(Doku_Handler $Handler) {
+        $this->Handler = $Handler;
+    }
+
+    function writeCall($call) {
+        $this->Handler->calls[] = $call;
+    }
+
+    function writeCalls($calls) {
+        $this->Handler->calls = array_merge($this->Handler->calls, $calls);
+    }
+
+    // function is required, but since this call writer is first/highest in
+    // the chain it is not required to do anything
+    function finalise() {
+        unset($this->Handler);
+    }
+}
+
+//------------------------------------------------------------------------
+/**
+ * Generic call writer class to handle nesting of rendering instructions
+ * within a render instruction. Also see nest() method of renderer base class
+ *
+ * @author    Chris Smith <chris@jalakai.co.uk>
+ */
+class Doku_Handler_Nest implements Doku_Handler_CallWriter_Interface {
+
+    var $CallWriter;
+    var $calls = array();
+
+    var $closingInstruction;
+
+    /**
+     * constructor
+     *
+     * @param  Doku_Handler_CallWriter $CallWriter     the renderers current call writer
+     * @param  string     $close          closing instruction name, this is required to properly terminate the
+     *                                    syntax mode if the document ends without a closing pattern
+     */
+    function __construct(Doku_Handler_CallWriter_Interface $CallWriter, $close="nest_close") {
+        $this->CallWriter = $CallWriter;
+
+        $this->closingInstruction = $close;
+    }
+
+    function writeCall($call) {
+        $this->calls[] = $call;
+    }
+
+    function writeCalls($calls) {
+        $this->calls = array_merge($this->calls, $calls);
+    }
+
+    function finalise() {
+        $last_call = end($this->calls);
+        $this->writeCall(array($this->closingInstruction,array(), $last_call[2]));
+
+        $this->process();
+        $this->CallWriter->finalise();
+        unset($this->CallWriter);
+    }
+
+    function process() {
+        // merge consecutive cdata
+        $unmerged_calls = $this->calls;
+        $this->calls = array();
+
+        foreach ($unmerged_calls as $call) $this->addCall($call);
+
+        $first_call = reset($this->calls);
+        $this->CallWriter->writeCall(array("nest", array($this->calls), $first_call[2]));
+    }
+
+    function addCall($call) {
+        $key = count($this->calls);
+        if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) {
+            $this->calls[$key-1][1][0] .= $call[1][0];
+        } else if ($call[0] == 'eol') {
+            // do nothing (eol shouldn't be allowed, to counter preformatted fix in #1652 & #1699)
+        } else {
+            $this->calls[] = $call;
+        }
+    }
+}
+
+class Doku_Handler_List implements Doku_Handler_CallWriter_Interface {
+
+    var $CallWriter;
+
+    var $calls = array();
+    var $listCalls = array();
+    var $listStack = array();
+
+    const NODE = 1;
+
+    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
+        $this->CallWriter = $CallWriter;
+    }
+
+    function writeCall($call) {
+        $this->calls[] = $call;
+    }
+
+    // Probably not needed but just in case...
+    function writeCalls($calls) {
+        $this->calls = array_merge($this->calls, $calls);
+#        $this->CallWriter->writeCalls($this->calls);
+    }
+
+    function finalise() {
+        $last_call = end($this->calls);
+        $this->writeCall(array('list_close',array(), $last_call[2]));
+
+        $this->process();
+        $this->CallWriter->finalise();
+        unset($this->CallWriter);
+    }
+
+    //------------------------------------------------------------------------
+    function process() {
+
+        foreach ( $this->calls as $call ) {
+            switch ($call[0]) {
+                case 'list_item':
+                    $this->listOpen($call);
+                break;
+                case 'list_open':
+                    $this->listStart($call);
+                break;
+                case 'list_close':
+                    $this->listEnd($call);
+                break;
+                default:
+                    $this->listContent($call);
+                break;
+            }
+        }
+
+        $this->CallWriter->writeCalls($this->listCalls);
+    }
+
+    //------------------------------------------------------------------------
+    function listStart($call) {
+        $depth = $this->interpretSyntax($call[1][0], $listType);
+
+        $this->initialDepth = $depth;
+        //                   array(list type, current depth, index of current listitem_open)
+        $this->listStack[] = array($listType, $depth, 1);
+
+        $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]);
+        $this->listCalls[] = array('listitem_open',array(1),$call[2]);
+        $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+    }
+
+    //------------------------------------------------------------------------
+    function listEnd($call) {
+        $closeContent = true;
+
+        while ( $list = array_pop($this->listStack) ) {
+            if ( $closeContent ) {
+                $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+                $closeContent = false;
+            }
+            $this->listCalls[] = array('listitem_close',array(),$call[2]);
+            $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]);
+        }
+    }
+
+    //------------------------------------------------------------------------
+    function listOpen($call) {
+        $depth = $this->interpretSyntax($call[1][0], $listType);
+        $end = end($this->listStack);
+        $key = key($this->listStack);
+
+        // Not allowed to be shallower than initialDepth
+        if ( $depth < $this->initialDepth ) {
+            $depth = $this->initialDepth;
+        }
+
+        //------------------------------------------------------------------------
+        if ( $depth == $end[1] ) {
+
+            // Just another item in the list...
+            if ( $listType == $end[0] ) {
+                $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+                $this->listCalls[] = array('listitem_close',array(),$call[2]);
+                $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]);
+                $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+                // new list item, update list stack's index into current listitem_open
+                $this->listStack[$key][2] = count($this->listCalls) - 2;
+
+            // Switched list type...
+            } else {
+
+                $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+                $this->listCalls[] = array('listitem_close',array(),$call[2]);
+                $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]);
+                $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
+                $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
+                $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+                array_pop($this->listStack);
+                $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
+            }
+
+        //------------------------------------------------------------------------
+        // Getting deeper...
+        } else if ( $depth > $end[1] ) {
+
+            $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+            $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
+            $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
+            $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+            // set the node/leaf state of this item's parent listitem_open to NODE
+            $this->listCalls[$this->listStack[$key][2]][1][1] = self::NODE;
+
+            $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
+
+        //------------------------------------------------------------------------
+        // Getting shallower ( $depth < $end[1] )
+        } else {
+            $this->listCalls[] = array('listcontent_close',array(),$call[2]);
+            $this->listCalls[] = array('listitem_close',array(),$call[2]);
+            $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]);
+
+            // Throw away the end - done
+            array_pop($this->listStack);
+
+            while (1) {
+                $end = end($this->listStack);
+                $key = key($this->listStack);
+
+                if ( $end[1] <= $depth ) {
+
+                    // Normalize depths
+                    $depth = $end[1];
+
+                    $this->listCalls[] = array('listitem_close',array(),$call[2]);
+
+                    if ( $end[0] == $listType ) {
+                        $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]);
+                        $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+                        // new list item, update list stack's index into current listitem_open
+                        $this->listStack[$key][2] = count($this->listCalls) - 2;
+
+                    } else {
+                        // Switching list type...
+                        $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]);
+                        $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]);
+                        $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]);
+                        $this->listCalls[] = array('listcontent_open',array(),$call[2]);
+
+                        array_pop($this->listStack);
+                        $this->listStack[] = array($listType, $depth, count($this->listCalls) - 2);
+                    }
+
+                    break;
+
+                // Haven't dropped down far enough yet.... ( $end[1] > $depth )
+                } else {
+
+                    $this->listCalls[] = array('listitem_close',array(),$call[2]);
+                    $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]);
+
+                    array_pop($this->listStack);
+
+                }
+
+            }
+
+        }
+    }
+
+    //------------------------------------------------------------------------
+    function listContent($call) {
+        $this->listCalls[] = $call;
+    }
+
+    //------------------------------------------------------------------------
+    function interpretSyntax($match, & $type) {
+        if ( substr($match,-1) == '*' ) {
+            $type = 'u';
+        } else {
+            $type = 'o';
+        }
+        // Is the +1 needed? It used to be count(explode(...))
+        // but I don't think the number is seen outside this handler
+        return substr_count(str_replace("\t",'  ',$match), '  ') + 1;
+    }
+}
+
+//------------------------------------------------------------------------
+class Doku_Handler_Preformatted implements Doku_Handler_CallWriter_Interface {
+
+    var $CallWriter;
+
+    var $calls = array();
+    var $pos;
+    var $text ='';
+
+
+
+    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
+        $this->CallWriter = $CallWriter;
+    }
+
+    function writeCall($call) {
+        $this->calls[] = $call;
+    }
+
+    // Probably not needed but just in case...
+    function writeCalls($calls) {
+        $this->calls = array_merge($this->calls, $calls);
+#        $this->CallWriter->writeCalls($this->calls);
+    }
+
+    function finalise() {
+        $last_call = end($this->calls);
+        $this->writeCall(array('preformatted_end',array(), $last_call[2]));
+
+        $this->process();
+        $this->CallWriter->finalise();
+        unset($this->CallWriter);
+    }
+
+    function process() {
+        foreach ( $this->calls as $call ) {
+            switch ($call[0]) {
+                case 'preformatted_start':
+                    $this->pos = $call[2];
+                break;
+                case 'preformatted_newline':
+                    $this->text .= "\n";
+                break;
+                case 'preformatted_content':
+                    $this->text .= $call[1][0];
+                break;
+                case 'preformatted_end':
+                    if (trim($this->text)) {
+                        $this->CallWriter->writeCall(array('preformatted',array($this->text),$this->pos));
+                    }
+                    // see FS#1699 & FS#1652, add 'eol' instructions to ensure proper triggering of following p_open
+                    $this->CallWriter->writeCall(array('eol',array(),$this->pos));
+                    $this->CallWriter->writeCall(array('eol',array(),$this->pos));
+                break;
+            }
+        }
+    }
+
+}
+
+//------------------------------------------------------------------------
+class Doku_Handler_Quote implements Doku_Handler_CallWriter_Interface {
+
+    var $CallWriter;
+
+    var $calls = array();
+
+    var $quoteCalls = array();
+
+    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
+        $this->CallWriter = $CallWriter;
+    }
+
+    function writeCall($call) {
+        $this->calls[] = $call;
+    }
+
+    // Probably not needed but just in case...
+    function writeCalls($calls) {
+        $this->calls = array_merge($this->calls, $calls);
+    }
+
+    function finalise() {
+        $last_call = end($this->calls);
+        $this->writeCall(array('quote_end',array(), $last_call[2]));
+
+        $this->process();
+        $this->CallWriter->finalise();
+        unset($this->CallWriter);
+    }
+
+    function process() {
+
+        $quoteDepth = 1;
+
+        foreach ( $this->calls as $call ) {
+            switch ($call[0]) {
+
+                case 'quote_start':
+
+                    $this->quoteCalls[] = array('quote_open',array(),$call[2]);
+
+                case 'quote_newline':
+
+                    $quoteLength = $this->getDepth($call[1][0]);
+
+                    if ( $quoteLength > $quoteDepth ) {
+                        $quoteDiff = $quoteLength - $quoteDepth;
+                        for ( $i = 1; $i <= $quoteDiff; $i++ ) {
+                            $this->quoteCalls[] = array('quote_open',array(),$call[2]);
+                        }
+                    } else if ( $quoteLength < $quoteDepth ) {
+                        $quoteDiff = $quoteDepth - $quoteLength;
+                        for ( $i = 1; $i <= $quoteDiff; $i++ ) {
+                            $this->quoteCalls[] = array('quote_close',array(),$call[2]);
+                        }
+                    } else {
+                        if ($call[0] != 'quote_start') $this->quoteCalls[] = array('linebreak',array(),$call[2]);
+                    }
+
+                    $quoteDepth = $quoteLength;
+
+                break;
+
+                case 'quote_end':
+
+                    if ( $quoteDepth > 1 ) {
+                        $quoteDiff = $quoteDepth - 1;
+                        for ( $i = 1; $i <= $quoteDiff; $i++ ) {
+                            $this->quoteCalls[] = array('quote_close',array(),$call[2]);
+                        }
+                    }
+
+                    $this->quoteCalls[] = array('quote_close',array(),$call[2]);
+
+                    $this->CallWriter->writeCalls($this->quoteCalls);
+                break;
+
+                default:
+                    $this->quoteCalls[] = $call;
+                break;
+            }
+        }
+    }
+
+    function getDepth($marker) {
+        preg_match('/>{1,}/', $marker, $matches);
+        $quoteLength = strlen($matches[0]);
+        return $quoteLength;
+    }
+}
+
+//------------------------------------------------------------------------
+class Doku_Handler_Table implements Doku_Handler_CallWriter_Interface {
+
+    var $CallWriter;
+
+    var $calls = array();
+    var $tableCalls = array();
+    var $maxCols = 0;
+    var $maxRows = 1;
+    var $currentCols = 0;
+    var $firstCell = false;
+    var $lastCellType = 'tablecell';
+    var $inTableHead = true;
+    var $currentRow = array('tableheader' => 0, 'tablecell' => 0);
+    var $countTableHeadRows = 0;
+
+    function __construct(Doku_Handler_CallWriter_Interface $CallWriter) {
+        $this->CallWriter = $CallWriter;
+    }
+
+    function writeCall($call) {
+        $this->calls[] = $call;
+    }
+
+    // Probably not needed but just in case...
+    function writeCalls($calls) {
+        $this->calls = array_merge($this->calls, $calls);
+    }
+
+    function finalise() {
+        $last_call = end($this->calls);
+        $this->writeCall(array('table_end',array(), $last_call[2]));
+
+        $this->process();
+        $this->CallWriter->finalise();
+        unset($this->CallWriter);
+    }
+
+    //------------------------------------------------------------------------
+    function process() {
+        foreach ( $this->calls as $call ) {
+            switch ( $call[0] ) {
+                case 'table_start':
+                    $this->tableStart($call);
+                break;
+                case 'table_row':
+                    $this->tableRowClose($call);
+                    $this->tableRowOpen(array('tablerow_open',$call[1],$call[2]));
+                break;
+                case 'tableheader':
+                case 'tablecell':
+                    $this->tableCell($call);
+                break;
+                case 'table_end':
+                    $this->tableRowClose($call);
+                    $this->tableEnd($call);
+                break;
+                default:
+                    $this->tableDefault($call);
+                break;
+            }
+        }
+        $this->CallWriter->writeCalls($this->tableCalls);
+    }
+
+    function tableStart($call) {
+        $this->tableCalls[] = array('table_open',$call[1],$call[2]);
+        $this->tableCalls[] = array('tablerow_open',array(),$call[2]);
+        $this->firstCell = true;
+    }
+
+    function tableEnd($call) {
+        $this->tableCalls[] = array('table_close',$call[1],$call[2]);
+        $this->finalizeTable();
+    }
+
+    function tableRowOpen($call) {
+        $this->tableCalls[] = $call;
+        $this->currentCols = 0;
+        $this->firstCell = true;
+        $this->lastCellType = 'tablecell';
+        $this->maxRows++;
+        if ($this->inTableHead) {
+            $this->currentRow = array('tablecell' => 0, 'tableheader' => 0);
+        }
+    }
+
+    function tableRowClose($call) {
+        if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) {
+            $this->countTableHeadRows++;
+        }
+        // Strip off final cell opening and anything after it
+        while ( $discard = array_pop($this->tableCalls ) ) {
+
+            if ( $discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') {
+                break;
+            }
+            if (!empty($this->currentRow[$discard[0]])) {
+                $this->currentRow[$discard[0]]--;
+            }
+        }
+        $this->tableCalls[] = array('tablerow_close', array(), $call[2]);
+
+        if ( $this->currentCols > $this->maxCols ) {
+            $this->maxCols = $this->currentCols;
+        }
+    }
+
+    function isTableHeadRow() {
+        $td = $this->currentRow['tablecell'];
+        $th = $this->currentRow['tableheader'];
+
+        if (!$th || $td > 2) return false;
+        if (2*$td > $th) return false;
+
+        return true;
+    }
+
+    function tableCell($call) {
+        if ($this->inTableHead) {
+            $this->currentRow[$call[0]]++;
+        }
+        if ( !$this->firstCell ) {
+
+            // Increase the span
+            $lastCall = end($this->tableCalls);
+
+            // A cell call which follows an open cell means an empty cell so span
+            if ( $lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open' ) {
+                 $this->tableCalls[] = array('colspan',array(),$call[2]);
+
+            }
+
+            $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]);
+            $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
+            $this->lastCellType = $call[0];
+
+        } else {
+
+            $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
+            $this->lastCellType = $call[0];
+            $this->firstCell = false;
+
+        }
+
+        $this->currentCols++;
+    }
+
+    function tableDefault($call) {
+        $this->tableCalls[] = $call;
+    }
+
+    function finalizeTable() {
+
+        // Add the max cols and rows to the table opening
+        if ( $this->tableCalls[0][0] == 'table_open' ) {
+            // Adjust to num cols not num col delimeters
+            $this->tableCalls[0][1][] = $this->maxCols - 1;
+            $this->tableCalls[0][1][] = $this->maxRows;
+            $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]);
+        } else {
+            trigger_error('First element in table call list is not table_open');
+        }
+
+        $lastRow = 0;
+        $lastCell = 0;
+        $cellKey = array();
+        $toDelete = array();
+
+        // if still in tableheader, then there can be no table header
+        // as all rows can't be within <THEAD>
+        if ($this->inTableHead) {
+            $this->inTableHead = false;
+            $this->countTableHeadRows = 0;
+        }
+
+        // Look for the colspan elements and increment the colspan on the
+        // previous non-empty opening cell. Once done, delete all the cells
+        // that contain colspans
+        for ($key = 0 ; $key < count($this->tableCalls) ; ++$key) {
+            $call = $this->tableCalls[$key];
+
+            switch ($call[0]) {
+                case 'table_open' :
+                    if($this->countTableHeadRows) {
+                        array_splice($this->tableCalls, $key+1, 0, array(
+                              array('tablethead_open', array(), $call[2]))
+                        );
+                    }
+                    break;
+
+                case 'tablerow_open':
+
+                    $lastRow++;
+                    $lastCell = 0;
+                    break;
+
+                case 'tablecell_open':
+                case 'tableheader_open':
+
+                    $lastCell++;
+                    $cellKey[$lastRow][$lastCell] = $key;
+                    break;
+
+                case 'table_align':
+
+                    $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open'));
+                    $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close'));
+                    // If the cell is empty, align left
+                    if ($prev && $next) {
+                        $this->tableCalls[$key-1][1][1] = 'left';
+
+                    // If the previous element was a cell open, align right
+                    } elseif ($prev) {
+                        $this->tableCalls[$key-1][1][1] = 'right';
+
+                    // If the next element is the close of an element, align either center or left
+                    } elseif ( $next) {
+                        if ( $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right' ) {
+                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
+                        } else {
+                            $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
+                        }
+
+                    }
+
+                    // Now convert the whitespace back to cdata
+                    $this->tableCalls[$key][0] = 'cdata';
+                    break;
+
+                case 'colspan':
+
+                    $this->tableCalls[$key-1][1][0] = false;
+
+                    for($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) {
+
+                        if ( $this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open' ) {
+
+                            if ( false !== $this->tableCalls[$i][1][0] ) {
+                                $this->tableCalls[$i][1][0]++;
+                                break;
+                            }
+
+                        }
+                    }
+
+                    $toDelete[] = $key-1;
+                    $toDelete[] = $key;
+                    $toDelete[] = $key+1;
+                    break;
+
+                case 'rowspan':
+
+                    if ( $this->tableCalls[$key-1][0] == 'cdata' ) {
+                        // ignore rowspan if previous call was cdata (text mixed with :::) we don't have to check next call as that wont match regex
+                        $this->tableCalls[$key][0] = 'cdata';
+
+                    } else {
+
+                        $spanning_cell = null;
+
+                        // can't cross thead/tbody boundary
+                        if (!$this->countTableHeadRows || ($lastRow-1 != $this->countTableHeadRows)) {
+                            for($i = $lastRow-1; $i > 0; $i--) {
+
+                                if ( $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' || $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open' ) {
+
+                                    if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
+                                        $spanning_cell = $i;
+                                        break;
+                                    }
+
+                                }
+                            }
+                        }
+                        if (is_null($spanning_cell)) {
+                            // No spanning cell found, so convert this cell to
+                            // an empty one to avoid broken tables
+                            $this->tableCalls[$key][0] = 'cdata';
+                            $this->tableCalls[$key][1][0] = '';
+                            continue;
+                        }
+                        $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
+
+                        $this->tableCalls[$key-1][1][2] = false;
+
+                        $toDelete[] = $key-1;
+                        $toDelete[] = $key;
+                        $toDelete[] = $key+1;
+                    }
+                    break;
+
+                case 'tablerow_close':
+
+                    // Fix broken tables by adding missing cells
+                    $moreCalls = array();
+                    while (++$lastCell < $this->maxCols) {
+                        $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]);
+                        $moreCalls[] = array('cdata', array(''), $call[2]);
+                        $moreCalls[] = array('tablecell_close', array(), $call[2]);
+                    }
+                    $moreCallsLength = count($moreCalls);
+                    if($moreCallsLength) {
+                        array_splice($this->tableCalls, $key, 0, $moreCalls);
+                        $key += $moreCallsLength;
+                    }
+
+                    if($this->countTableHeadRows == $lastRow) {
+                        array_splice($this->tableCalls, $key+1, 0, array(
+                              array('tablethead_close', array(), $call[2])));
+                    }
+                    break;
+
+            }
+        }
+
+        // condense cdata
+        $cnt = count($this->tableCalls);
+        for( $key = 0; $key < $cnt; $key++){
+            if($this->tableCalls[$key][0] == 'cdata'){
+                $ckey = $key;
+                $key++;
+                while($this->tableCalls[$key][0] == 'cdata'){
+                    $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
+                    $toDelete[] = $key;
+                    $key++;
+                }
+                continue;
+            }
+        }
+
+        foreach ( $toDelete as $delete ) {
+            unset($this->tableCalls[$delete]);
+        }
+        $this->tableCalls = array_values($this->tableCalls);
+    }
+}
+
+
+/**
+ * Handler for paragraphs
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+class Doku_Handler_Block {
+    var $calls = array();
+    var $skipEol = false;
+    var $inParagraph = false;
+
+    // Blocks these should not be inside paragraphs
+    var $blockOpen = array(
+            'header',
+            'listu_open','listo_open','listitem_open','listcontent_open',
+            'table_open','tablerow_open','tablecell_open','tableheader_open','tablethead_open',
+            'quote_open',
+            'code','file','hr','preformatted','rss',
+            'htmlblock','phpblock',
+            'footnote_open',
+        );
+
+    var $blockClose = array(
+            'header',
+            'listu_close','listo_close','listitem_close','listcontent_close',
+            'table_close','tablerow_close','tablecell_close','tableheader_close','tablethead_close',
+            'quote_close',
+            'code','file','hr','preformatted','rss',
+            'htmlblock','phpblock',
+            'footnote_close',
+        );
+
+    // Stacks can contain paragraphs
+    var $stackOpen = array(
+        'section_open',
+        );
+
+    var $stackClose = array(
+        'section_close',
+        );
+
+
+    /**
+     * Constructor. Adds loaded syntax plugins to the block and stack
+     * arrays
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     */
+    function __construct(){
+        global $DOKU_PLUGINS;
+        //check if syntax plugins were loaded
+        if(empty($DOKU_PLUGINS['syntax'])) return;
+        foreach($DOKU_PLUGINS['syntax'] as $n => $p){
+            $ptype = $p->getPType();
+            if($ptype == 'block'){
+                $this->blockOpen[]  = 'plugin_'.$n;
+                $this->blockClose[] = 'plugin_'.$n;
+            }elseif($ptype == 'stack'){
+                $this->stackOpen[]  = 'plugin_'.$n;
+                $this->stackClose[] = 'plugin_'.$n;
+            }
+        }
+    }
+
+    function openParagraph($pos){
+        if ($this->inParagraph) return;
+        $this->calls[] = array('p_open',array(), $pos);
+        $this->inParagraph = true;
+        $this->skipEol = true;
+    }
+
+    /**
+     * Close a paragraph if needed
+     *
+     * This function makes sure there are no empty paragraphs on the stack
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     *
+     * @param string|integer $pos
+     */
+    function closeParagraph($pos){
+        if (!$this->inParagraph) return;
+        // look back if there was any content - we don't want empty paragraphs
+        $content = '';
+        $ccount = count($this->calls);
+        for($i=$ccount-1; $i>=0; $i--){
+            if($this->calls[$i][0] == 'p_open'){
+                break;
+            }elseif($this->calls[$i][0] == 'cdata'){
+                $content .= $this->calls[$i][1][0];
+            }else{
+                $content = 'found markup';
+                break;
+            }
+        }
+
+        if(trim($content)==''){
+            //remove the whole paragraph
+            //array_splice($this->calls,$i); // <- this is much slower than the loop below
+            for($x=$ccount; $x>$i; $x--) array_pop($this->calls);
+        }else{
+            // remove ending linebreaks in the paragraph
+            $i=count($this->calls)-1;
+            if ($this->calls[$i][0] == 'cdata') $this->calls[$i][1][0] = rtrim($this->calls[$i][1][0],DOKU_PARSER_EOL);
+            $this->calls[] = array('p_close',array(), $pos);
+        }
+
+        $this->inParagraph = false;
+        $this->skipEol = true;
+    }
+
+    function addCall($call) {
+        $key = count($this->calls);
+        if ($key and ($call[0] == 'cdata') and ($this->calls[$key-1][0] == 'cdata')) {
+            $this->calls[$key-1][1][0] .= $call[1][0];
+        } else {
+            $this->calls[] = $call;
+        }
+    }
+
+    // simple version of addCall, without checking cdata
+    function storeCall($call) {
+        $this->calls[] = $call;
+    }
+
+    /**
+     * Processes the whole instruction stack to open and close paragraphs
+     *
+     * @author Harry Fuecks <hfuecks@gmail.com>
+     * @author Andreas Gohr <andi@splitbrain.org>
+     *
+     * @param array $calls
+     *
+     * @return array
+     */
+    function process($calls) {
+        // open first paragraph
+        $this->openParagraph(0);
+        foreach ( $calls as $key => $call ) {
+            $cname = $call[0];
+            if ($cname == 'plugin') {
+                $cname='plugin_'.$call[1][0];
+                $plugin = true;
+                $plugin_open = (($call[1][2] == DOKU_LEXER_ENTER) || ($call[1][2] == DOKU_LEXER_SPECIAL));
+                $plugin_close = (($call[1][2] == DOKU_LEXER_EXIT) || ($call[1][2] == DOKU_LEXER_SPECIAL));
+            } else {
+                $plugin = false;
+            }
+            /* stack */
+            if ( in_array($cname,$this->stackClose ) && (!$plugin || $plugin_close)) {
+                $this->closeParagraph($call[2]);
+                $this->storeCall($call);
+                $this->openParagraph($call[2]);
+                continue;
+            }
+            if ( in_array($cname,$this->stackOpen ) && (!$plugin || $plugin_open) ) {
+                $this->closeParagraph($call[2]);
+                $this->storeCall($call);
+                $this->openParagraph($call[2]);
+                continue;
+            }
+            /* block */
+            // If it's a substition it opens and closes at the same call.
+            // To make sure next paragraph is correctly started, let close go first.
+            if ( in_array($cname, $this->blockClose) && (!$plugin || $plugin_close)) {
+                $this->closeParagraph($call[2]);
+                $this->storeCall($call);
+                $this->openParagraph($call[2]);
+                continue;
+            }
+            if ( in_array($cname, $this->blockOpen) && (!$plugin || $plugin_open)) {
+                $this->closeParagraph($call[2]);
+                $this->storeCall($call);
+                continue;
+            }
+            /* eol */
+            if ( $cname == 'eol' ) {
+                // Check this isn't an eol instruction to skip...
+                if ( !$this->skipEol ) {
+                    // Next is EOL => double eol => mark as paragraph
+                    if ( isset($calls[$key+1]) && $calls[$key+1][0] == 'eol' ) {
+                        $this->closeParagraph($call[2]);
+                        $this->openParagraph($call[2]);
+                    } else {
+                        //if this is just a single eol make a space from it
+                        $this->addCall(array('cdata',array(DOKU_PARSER_EOL), $call[2]));
+                    }
+                }
+                continue;
+            }
+            /* normal */
+            $this->addCall($call);
+            $this->skipEol = false;
+        }
+        // close last paragraph
+        $call = end($this->calls);
+        $this->closeParagraph($call[2]);
+        return $this->calls;
+    }
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/wiki/inc/parser/lexer.php b/wiki/inc/parser/lexer.php
new file mode 100644
index 0000000..ba6a653
--- /dev/null
+++ b/wiki/inc/parser/lexer.php
@@ -0,0 +1,614 @@
+<?php
+/**
+ * Author Markus Baker: http://www.lastcraft.com
+ * Version adapted from Simple Test: http://sourceforge.net/projects/simpletest/
+ * For an intro to the Lexer see:
+ * https://web.archive.org/web/20120125041816/http://www.phppatterns.com/docs/develop/simple_test_lexer_notes
+ * @author Marcus Baker
+ * @package Doku
+ * @subpackage Lexer
+ * @version $Id: lexer.php,v 1.1 2005/03/23 23:14:09 harryf Exp $
+ */
+
+/**
+ * Init path constant
+ */
+if(!defined('DOKU_INC')) die('meh.');
+
+/**#@+
+ * lexer mode constant
+ */
+define("DOKU_LEXER_ENTER", 1);
+define("DOKU_LEXER_MATCHED", 2);
+define("DOKU_LEXER_UNMATCHED", 3);
+define("DOKU_LEXER_EXIT", 4);
+define("DOKU_LEXER_SPECIAL", 5);
+/**#@-*/
+
+/**
+ * Compounded regular expression. Any of
+ * the contained patterns could match and
+ * when one does it's label is returned.
+ *
+ * @package Doku
+ * @subpackage Lexer
+ */
+class Doku_LexerParallelRegex {
+    var $_patterns;
+    var $_labels;
+    var $_regex;
+    var $_case;
+
+    /**
+     * Constructor. Starts with no patterns.
+     *
+     * @param boolean $case    True for case sensitive, false
+     *                         for insensitive.
+     * @access public
+     */
+    function __construct($case) {
+        $this->_case = $case;
+        $this->_patterns = array();
+        $this->_labels = array();
+        $this->_regex = null;
+    }
+
+    /**
+     * Adds a pattern with an optional label.
+     *
+     * @param mixed       $pattern Perl style regex. Must be UTF-8
+     *                             encoded. If its a string, the (, )
+     *                             lose their meaning unless they
+     *                             form part of a lookahead or
+     *                             lookbehind assertation.
+     * @param bool|string $label   Label of regex to be returned
+     *                             on a match. Label must be ASCII
+     * @access public
+     */
+    function addPattern($pattern, $label = true) {
+        $count = count($this->_patterns);
+        $this->_patterns[$count] = $pattern;
+        $this->_labels[$count] = $label;
+        $this->_regex = null;
+    }
+
+    /**
+     * Attempts to match all patterns at once against a string.
+     *
+     * @param string $subject      String to match against.
+     * @param string $match        First matched portion of
+     *                             subject.
+     * @return boolean             True on success.
+     * @access public
+     */
+    function match($subject, &$match) {
+        if (count($this->_patterns) == 0) {
+            return false;
+        }
+        if (! preg_match($this->_getCompoundedRegex(), $subject, $matches)) {
+            $match = "";
+            return false;
+        }
+
+        $match = $matches[0];
+        $size = count($matches);
+        for ($i = 1; $i < $size; $i++) {
+            if ($matches[$i] && isset($this->_labels[$i - 1])) {
+                return $this->_labels[$i - 1];
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Attempts to split the string against all patterns at once
+     *
+     * @param string $subject      String to match against.
+     * @param array $split         The split result: array containing, pre-match, match & post-match strings
+     * @return boolean             True on success.
+     * @access public
+     *
+     * @author Christopher Smith <chris@jalakai.co.uk>
+     */
+    function split($subject, &$split) {
+        if (count($this->_patterns) == 0) {
+            return false;
+        }
+
+        if (! preg_match($this->_getCompoundedRegex(), $subject, $matches)) {
+            if(function_exists('preg_last_error')){
+                $err = preg_last_error();
+                switch($err){
+                    case PREG_BACKTRACK_LIMIT_ERROR:
+                        msg('A PCRE backtrack error occured. Try to increase the pcre.backtrack_limit in php.ini',-1);
+                        break;
+                    case PREG_RECURSION_LIMIT_ERROR:
+                        msg('A PCRE recursion error occured. Try to increase the pcre.recursion_limit in php.ini',-1);
+                        break;
+                    case PREG_BAD_UTF8_ERROR:
+                        msg('A PCRE UTF-8 error occured. This might be caused by a faulty plugin',-1);
+                        break;
+                    case PREG_INTERNAL_ERROR:
+                        msg('A PCRE internal error occured. This might be caused by a faulty plugin',-1);
+                        break;
+                }
+            }
+
+            $split = array($subject, "", "");
+            return false;
+        }
+
+        $idx = count($matches)-2;
+        list($pre, $post) = preg_split($this->_patterns[$idx].$this->_getPerlMatchingFlags(), $subject, 2);
+        $split = array($pre, $matches[0], $post);
+
+        return isset($this->_labels[$idx]) ? $this->_labels[$idx] : true;
+    }
+
+    /**
+     * Compounds the patterns into a single
+     * regular expression separated with the
+     * "or" operator. Caches the regex.
+     * Will automatically escape (, ) and / tokens.
+     *
+     * @internal array $_patterns List of patterns in order.
+     * @return null|string
+     * @access private
+     */
+    function _getCompoundedRegex() {
+        if ($this->_regex == null) {
+            $cnt = count($this->_patterns);
+            for ($i = 0; $i < $cnt; $i++) {
+
+                /*
+                 * decompose the input pattern into "(", "(?", ")",
+                 * "[...]", "[]..]", "[^]..]", "[...[:...:]..]", "\x"...
+                 * elements.
+                 */
+                preg_match_all('/\\\\.|' .
+                               '\(\?|' .
+                               '[()]|' .
+                               '\[\^?\]?(?:\\\\.|\[:[^]]*:\]|[^]\\\\])*\]|' .
+                               '[^[()\\\\]+/', $this->_patterns[$i], $elts);
+
+                $pattern = "";
+                $level = 0;
+
+                foreach ($elts[0] as $elt) {
+                    /*
+                     * for "(", ")" remember the nesting level, add "\"
+                     * only to the non-"(?" ones.
+                     */
+
+                    switch($elt) {
+                        case '(':
+                            $pattern .= '\(';
+                            break;
+                        case ')':
+                            if ($level > 0)
+                                $level--; /* closing (? */
+                            else
+                                $pattern .= '\\';
+                            $pattern .= ')';
+                            break;
+                        case '(?':
+                            $level++;
+                            $pattern .= '(?';
+                            break;
+                        default:
+                            if (substr($elt, 0, 1) == '\\')
+                                $pattern .= $elt;
+                            else
+                                $pattern .= str_replace('/', '\/', $elt);
+                    }
+                }
+                $this->_patterns[$i] = "($pattern)";
+            }
+            $this->_regex = "/" . implode("|", $this->_patterns) . "/" . $this->_getPerlMatchingFlags();
+        }
+        return $this->_regex;
+    }
+
+    /**
+     * Accessor for perl regex mode flags to use.
+     * @return string       Perl regex flags.
+     * @access private
+     */
+    function _getPerlMatchingFlags() {
+        return ($this->_case ? "msS" : "msSi");
+    }
+}
+
+/**
+ * States for a stack machine.
+ * @package Lexer
+ * @subpackage Lexer
+ */
+class Doku_LexerStateStack {
+    var $_stack;
+
+    /**
+     * Constructor. Starts in named state.
+     * @param string $start        Starting state name.
+     * @access public
+     */
+    function __construct($start) {
+        $this->_stack = array($start);
+    }
+
+    /**
+     * Accessor for current state.
+     * @return string       State.
+     * @access public
+     */
+    function getCurrent() {
+        return $this->_stack[count($this->_stack) - 1];
+    }
+
+    /**
+     * Adds a state to the stack and sets it
+     * to be the current state.
+     * @param string $state        New state.
+     * @access public
+     */
+    function enter($state) {
+        array_push($this->_stack, $state);
+    }
+
+    /**
+     * Leaves the current state and reverts
+     * to the previous one.
+     * @return boolean    False if we drop off
+     *                    the bottom of the list.
+     * @access public
+     */
+    function leave() {
+        if (count($this->_stack) == 1) {
+            return false;
+        }
+        array_pop($this->_stack);
+        return true;
+    }
+}
+
+/**
+ * Accepts text and breaks it into tokens.
+ * Some optimisation to make the sure the
+ * content is only scanned by the PHP regex
+ * parser once. Lexer modes must not start
+ * with leading underscores.
+ * @package Doku
+ * @subpackage Lexer
+ */
+class Doku_Lexer {
+    var $_regexes;
+    var $_parser;
+    var $_mode;
+    var $_mode_handlers;
+    var $_case;
+
+    /**
+     * Sets up the lexer in case insensitive matching
+     * by default.
+     * @param Doku_Parser $parser  Handling strategy by
+     *                                 reference.
+     * @param string $start            Starting handler.
+     * @param boolean $case            True for case sensitive.
+     * @access public
+     */
+    function __construct($parser, $start = "accept", $case = false) {
+        $this->_case = $case;
+        /** @var Doku_LexerParallelRegex[] _regexes */
+        $this->_regexes = array();
+        $this->_parser = $parser;
+        $this->_mode = new Doku_LexerStateStack($start);
+        $this->_mode_handlers = array();
+    }
+
+    /**
+     * Adds a token search pattern for a particular
+     * parsing mode. The pattern does not change the
+     * current mode.
+     * @param string $pattern      Perl style regex, but ( and )
+     *                             lose the usual meaning.
+     * @param string $mode         Should only apply this
+     *                             pattern when dealing with
+     *                             this type of input.
+     * @access public
+     */
+    function addPattern($pattern, $mode = "accept") {
+        if (! isset($this->_regexes[$mode])) {
+            $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case);
+        }
+        $this->_regexes[$mode]->addPattern($pattern);
+    }
+
+    /**
+     * Adds a pattern that will enter a new parsing
+     * mode. Useful for entering parenthesis, strings,
+     * tags, etc.
+     * @param string $pattern      Perl style regex, but ( and )
+     *                             lose the usual meaning.
+     * @param string $mode         Should only apply this
+     *                             pattern when dealing with
+     *                             this type of input.
+     * @param string $new_mode     Change parsing to this new
+     *                             nested mode.
+     * @access public
+     */
+    function addEntryPattern($pattern, $mode, $new_mode) {
+        if (! isset($this->_regexes[$mode])) {
+            $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case);
+        }
+        $this->_regexes[$mode]->addPattern($pattern, $new_mode);
+    }
+
+    /**
+     * Adds a pattern that will exit the current mode
+     * and re-enter the previous one.
+     * @param string $pattern      Perl style regex, but ( and )
+     *                             lose the usual meaning.
+     * @param string $mode         Mode to leave.
+     * @access public
+     */
+    function addExitPattern($pattern, $mode) {
+        if (! isset($this->_regexes[$mode])) {
+            $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case);
+        }
+        $this->_regexes[$mode]->addPattern($pattern, "__exit");
+    }
+
+    /**
+     * Adds a pattern that has a special mode. Acts as an entry
+     * and exit pattern in one go, effectively calling a special
+     * parser handler for this token only.
+     * @param string $pattern      Perl style regex, but ( and )
+     *                             lose the usual meaning.
+     * @param string $mode         Should only apply this
+     *                             pattern when dealing with
+     *                             this type of input.
+     * @param string $special      Use this mode for this one token.
+     * @access public
+     */
+    function addSpecialPattern($pattern, $mode, $special) {
+        if (! isset($this->_regexes[$mode])) {
+            $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case);
+        }
+        $this->_regexes[$mode]->addPattern($pattern, "_$special");
+    }
+
+    /**
+     * Adds a mapping from a mode to another handler.
+     * @param string $mode        Mode to be remapped.
+     * @param string $handler     New target handler.
+     * @access public
+     */
+    function mapHandler($mode, $handler) {
+        $this->_mode_handlers[$mode] = $handler;
+    }
+
+    /**
+     * Splits the page text into tokens. Will fail
+     * if the handlers report an error or if no
+     * content is consumed. If successful then each
+     * unparsed and parsed token invokes a call to the
+     * held listener.
+     * @param string $raw        Raw HTML text.
+     * @return boolean           True on success, else false.
+     * @access public
+     */
+    function parse($raw) {
+        if (! isset($this->_parser)) {
+            return false;
+        }
+        $initialLength = strlen($raw);
+        $length = $initialLength;
+        $pos = 0;
+        while (is_array($parsed = $this->_reduce($raw))) {
+            list($unmatched, $matched, $mode) = $parsed;
+            $currentLength = strlen($raw);
+            $matchPos = $initialLength - $currentLength - strlen($matched);
+            if (! $this->_dispatchTokens($unmatched, $matched, $mode, $pos, $matchPos)) {
+                return false;
+            }
+            if ($currentLength == $length) {
+                return false;
+            }
+            $length = $currentLength;
+            $pos = $initialLength - $currentLength;
+        }
+        if (!$parsed) {
+            return false;
+        }
+        return $this->_invokeParser($raw, DOKU_LEXER_UNMATCHED, $pos);
+    }
+
+    /**
+     * Sends the matched token and any leading unmatched
+     * text to the parser changing the lexer to a new
+     * mode if one is listed.
+     * @param string $unmatched Unmatched leading portion.
+     * @param string $matched Actual token match.
+     * @param bool|string $mode Mode after match. A boolean
+     *                             false mode causes no change.
+     * @param int $initialPos
+     * @param int $matchPos
+     *                             Current byte index location in raw doc
+     *                             thats being parsed
+     * @return boolean             False if there was any error
+     *                             from the parser.
+     * @access private
+     */
+    function _dispatchTokens($unmatched, $matched, $mode = false, $initialPos, $matchPos) {
+        if (! $this->_invokeParser($unmatched, DOKU_LEXER_UNMATCHED, $initialPos) ){
+            return false;
+        }
+        if ($this->_isModeEnd($mode)) {
+            if (! $this->_invokeParser($matched, DOKU_LEXER_EXIT, $matchPos)) {
+                return false;
+            }
+            return $this->_mode->leave();
+        }
+        if ($this->_isSpecialMode($mode)) {
+            $this->_mode->enter($this->_decodeSpecial($mode));
+            if (! $this->_invokeParser($matched, DOKU_LEXER_SPECIAL, $matchPos)) {
+                return false;
+            }
+            return $this->_mode->leave();
+        }
+        if (is_string($mode)) {
+            $this->_mode->enter($mode);
+            return $this->_invokeParser($matched, DOKU_LEXER_ENTER, $matchPos);
+        }
+        return $this->_invokeParser($matched, DOKU_LEXER_MATCHED, $matchPos);
+    }
+
+    /**
+     * Tests to see if the new mode is actually to leave
+     * the current mode and pop an item from the matching
+     * mode stack.
+     * @param string $mode    Mode to test.
+     * @return boolean        True if this is the exit mode.
+     * @access private
+     */
+    function _isModeEnd($mode) {
+        return ($mode === "__exit");
+    }
+
+    /**
+     * Test to see if the mode is one where this mode
+     * is entered for this token only and automatically
+     * leaves immediately afterwoods.
+     * @param string $mode    Mode to test.
+     * @return boolean        True if this is the exit mode.
+     * @access private
+     */
+    function _isSpecialMode($mode) {
+        return (strncmp($mode, "_", 1) == 0);
+    }
+
+    /**
+     * Strips the magic underscore marking single token
+     * modes.
+     * @param string $mode    Mode to decode.
+     * @return string         Underlying mode name.
+     * @access private
+     */
+    function _decodeSpecial($mode) {
+        return substr($mode, 1);
+    }
+
+    /**
+     * Calls the parser method named after the current
+     * mode. Empty content will be ignored. The lexer
+     * has a parser handler for each mode in the lexer.
+     * @param string $content Text parsed.
+     * @param boolean $is_match Token is recognised rather
+     *                               than unparsed data.
+     * @param int $pos Current byte index location in raw doc
+     *                             thats being parsed
+     * @return bool
+     * @access private
+     */
+    function _invokeParser($content, $is_match, $pos) {
+        if (($content === "") || ($content === false)) {
+            return true;
+        }
+        $handler = $this->_mode->getCurrent();
+        if (isset($this->_mode_handlers[$handler])) {
+            $handler = $this->_mode_handlers[$handler];
+        }
+
+        // modes starting with plugin_ are all handled by the same
+        // handler but with an additional parameter
+        if(substr($handler,0,7)=='plugin_'){
+            list($handler,$plugin) = explode('_',$handler,2);
+            return $this->_parser->$handler($content, $is_match, $pos, $plugin);
+        }
+
+            return $this->_parser->$handler($content, $is_match, $pos);
+        }
+
+    /**
+     * Tries to match a chunk of text and if successful
+     * removes the recognised chunk and any leading
+     * unparsed data. Empty strings will not be matched.
+     * @param string $raw         The subject to parse. This is the
+     *                            content that will be eaten.
+     * @return array              Three item list of unparsed
+     *                            content followed by the
+     *                            recognised token and finally the
+     *                            action the parser is to take.
+     *                            True if no match, false if there
+     *                            is a parsing error.
+     * @access private
+     */
+    function _reduce(&$raw) {
+        if (! isset($this->_regexes[$this->_mode->getCurrent()])) {
+            return false;
+        }
+        if ($raw === "") {
+            return true;
+        }
+        if ($action = $this->_regexes[$this->_mode->getCurrent()]->split($raw, $split)) {
+            list($unparsed, $match, $raw) = $split;
+            return array($unparsed, $match, $action);
+        }
+        return true;
+    }
+}
+
+/**
+ * Escapes regex characters other than (, ) and /
+ *
+ * @TODO
+ *
+ * @param string $str
+ *
+ * @return mixed
+ */
+function Doku_Lexer_Escape($str) {
+    //$str = addslashes($str);
+    $chars = array(
+        '/\\\\/',
+        '/\./',
+        '/\+/',
+        '/\*/',
+        '/\?/',
+        '/\[/',
+        '/\^/',
+        '/\]/',
+        '/\$/',
+        '/\{/',
+        '/\}/',
+        '/\=/',
+        '/\!/',
+        '/\</',
+        '/\>/',
+        '/\|/',
+        '/\:/'
+        );
+
+    $escaped = array(
+        '\\\\\\\\',
+        '\.',
+        '\+',
+        '\*',
+        '\?',
+        '\[',
+        '\^',
+        '\]',
+        '\$',
+        '\{',
+        '\}',
+        '\=',
+        '\!',
+        '\<',
+        '\>',
+        '\|',
+        '\:'
+        );
+    return preg_replace($chars, $escaped, $str);
+}
+
+//Setup VIM: ex: et ts=4 sw=4 :
diff --git a/wiki/inc/parser/metadata.php b/wiki/inc/parser/metadata.php
new file mode 100644
index 0000000..9b1b5c9
--- /dev/null
+++ b/wiki/inc/parser/metadata.php
@@ -0,0 +1,694 @@
+<?php
+/**
+ * Renderer for metadata
+ *
+ * @author Esther Brunner <wikidesign@gmail.com>
+ */
+if(!defined('DOKU_INC')) die('meh.');
+
+if(!defined('DOKU_LF')) {
+    // Some whitespace to help View > Source
+    define ('DOKU_LF', "\n");
+}
+
+if(!defined('DOKU_TAB')) {
+    // Some whitespace to help View > Source
+    define ('DOKU_TAB', "\t");
+}
+
+/**
+ * The MetaData Renderer
+ *
+ * Metadata is additional information about a DokuWiki page that gets extracted mainly from the page's content
+ * but also it's own filesystem data (like the creation time). All metadata is stored in the fields $meta and
+ * $persistent.
+ *
+ * Some simplified rendering to $doc is done to gather the page's (text-only) abstract.
+ */
+class Doku_Renderer_metadata extends Doku_Renderer {
+    /** the approximate byte lenght to capture for the abstract */
+    const ABSTRACT_LEN = 250;
+
+    /** the maximum UTF8 character length for the abstract */
+    const ABSTRACT_MAX = 500;
+
+    /** @var array transient meta data, will be reset on each rendering */
+    public $meta = array();
+
+    /** @var array persistent meta data, will be kept until explicitly deleted */
+    public $persistent = array();
+
+    /** @var array the list of headers used to create unique link ids */
+    protected $headers = array();
+
+    /** @var string temporary $doc store */
+    protected $store = '';
+
+    /** @var string keeps the first image reference */
+    protected $firstimage = '';
+
+    /** @var bool determines if enough data for the abstract was collected, yet */
+    public $capture = true;
+
+    /** @var int number of bytes captured for abstract */
+    protected $captured = 0;
+
+    /**
+     * Returns the format produced by this renderer.
+     *
+     * @return string always 'metadata'
+     */
+    function getFormat() {
+        return 'metadata';
+    }
+
+    /**
+     * Initialize the document
+     *
+     * Sets up some of the persistent info about the page if it doesn't exist, yet.
+     */
+    function document_start() {
+        global $ID;
+
+        $this->headers = array();
+
+        // external pages are missing create date
+        if(!$this->persistent['date']['created']) {
+            $this->persistent['date']['created'] = filectime(wikiFN($ID));
+        }
+        if(!isset($this->persistent['user'])) {
+            $this->persistent['user'] = '';
+        }
+        if(!isset($this->persistent['creator'])) {
+            $this->persistent['creator'] = '';
+        }
+        // reset metadata to persistent values
+        $this->meta = $this->persistent;
+    }
+
+    /**
+     * Finalize the document
+     *
+     * Stores collected data in the metadata
+     */
+    function document_end() {
+        global $ID;
+
+        // store internal info in metadata (notoc,nocache)
+        $this->meta['internal'] = $this->info;
+
+        if(!isset($this->meta['description']['abstract'])) {
+            // cut off too long abstracts
+            $this->doc = trim($this->doc);
+            if(strlen($this->doc) > self::ABSTRACT_MAX) {
+                $this->doc = utf8_substr($this->doc, 0, self::ABSTRACT_MAX).'…';
+            }
+            $this->meta['description']['abstract'] = $this->doc;
+        }
+
+        $this->meta['relation']['firstimage'] = $this->firstimage;
+
+        if(!isset($this->meta['date']['modified'])) {
+            $this->meta['date']['modified'] = filemtime(wikiFN($ID));
+        }
+
+    }
+
+    /**
+     * Render plain text data
+     *
+     * This function takes care of the amount captured data and will stop capturing when
+     * enough abstract data is available
+     *
+     * @param $text
+     */
+    function cdata($text) {
+        if(!$this->capture) return;
+
+        $this->doc .= $text;
+
+        $this->captured += strlen($text);
+        if($this->captured > self::ABSTRACT_LEN) $this->capture = false;
+    }
+
+    /**
+     * Add an item to the TOC
+     *
+     * @param string $id       the hash link
+     * @param string $text     the text to display
+     * @param int    $level    the nesting level
+     */
+    function toc_additem($id, $text, $level) {
+        global $conf;
+
+        //only add items within configured levels
+        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
+            // the TOC is one of our standard ul list arrays ;-)
+            $this->meta['description']['tableofcontents'][] = array(
+                'hid'   => $id,
+                'title' => $text,
+                'type'  => 'ul',
+                'level' => $level - $conf['toptoclevel'] + 1
+            );
+        }
+
+    }
+
+    /**
+     * Render a heading
+     *
+     * @param string $text  the text to display
+     * @param int    $level header level
+     * @param int    $pos   byte position in the original source
+     */
+    function header($text, $level, $pos) {
+        if(!isset($this->meta['title'])) $this->meta['title'] = $text;
+
+        // add the header to the TOC
+        $hid = $this->_headerToLink($text, true);
+        $this->toc_additem($hid, $text, $level);
+
+        // add to summary
+        $this->cdata(DOKU_LF.$text.DOKU_LF);
+    }
+
+    /**
+     * Open a paragraph
+     */
+    function p_open() {
+        $this->cdata(DOKU_LF);
+    }
+
+    /**
+     * Close a paragraph
+     */
+    function p_close() {
+        $this->cdata(DOKU_LF);
+    }
+
+    /**
+     * Create a line break
+     */
+    function linebreak() {
+        $this->cdata(DOKU_LF);
+    }
+
+    /**
+     * Create a horizontal line
+     */
+    function hr() {
+        $this->cdata(DOKU_LF.'----------'.DOKU_LF);
+    }
+
+    /**
+     * Callback for footnote start syntax
+     *
+     * All following content will go to the footnote instead of
+     * the document. To achieve this the previous rendered content
+     * is moved to $store and $doc is cleared
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     */
+    function footnote_open() {
+        if($this->capture) {
+            // move current content to store and record footnote
+            $this->store = $this->doc;
+            $this->doc   = '';
+        }
+    }
+
+    /**
+     * Callback for footnote end syntax
+     *
+     * All rendered content is moved to the $footnotes array and the old
+     * content is restored from $store again
+     *
+     * @author Andreas Gohr
+     */
+    function footnote_close() {
+        if($this->capture) {
+            // restore old content
+            $this->doc   = $this->store;
+            $this->store = '';
+        }
+    }
+
+    /**
+     * Open an unordered list
+     */
+    function listu_open() {
+        $this->cdata(DOKU_LF);
+    }
+
+    /**
+     * Open an ordered list
+     */
+    function listo_open() {
+        $this->cdata(DOKU_LF);
+    }
+
+    /**
+     * Open a list item
+     *
+     * @param int $level the nesting level
+     * @param bool $node true when a node; false when a leaf
+     */
+    function listitem_open($level,$node=false) {
+        $this->cdata(str_repeat(DOKU_TAB, $level).'* ');
+    }
+
+    /**
+     * Close a list item
+     */
+    function listitem_close() {
+        $this->cdata(DOKU_LF);
+    }
+
+    /**
+     * Output preformatted text
+     *
+     * @param string $text
+     */
+    function preformatted($text) {
+        $this->cdata($text);
+    }
+
+    /**
+     * Start a block quote
+     */
+    function quote_open() {
+        $this->cdata(DOKU_LF.DOKU_TAB.'"');
+    }
+
+    /**
+     * Stop a block quote
+     */
+    function quote_close() {
+        $this->cdata('"'.DOKU_LF);
+    }
+
+    /**
+     * Display text as file content, optionally syntax highlighted
+     *
+     * @param string $text text to show
+     * @param string $lang programming language to use for syntax highlighting
+     * @param string $file file path label
+     */
+    function file($text, $lang = null, $file = null) {
+        $this->cdata(DOKU_LF.$text.DOKU_LF);
+    }
+
+    /**
+     * Display text as code content, optionally syntax highlighted
+     *
+     * @param string $text     text to show
+     * @param string $language programming language to use for syntax highlighting
+     * @param string $file     file path label
+     */
+    function code($text, $language = null, $file = null) {
+        $this->cdata(DOKU_LF.$text.DOKU_LF);
+    }
+
+    /**
+     * Format an acronym
+     *
+     * Uses $this->acronyms
+     *
+     * @param string $acronym
+     */
+    function acronym($acronym) {
+        $this->cdata($acronym);
+    }
+
+    /**
+     * Format a smiley
+     *
+     * Uses $this->smiley
+     *
+     * @param string $smiley
+     */
+    function smiley($smiley) {
+        $this->cdata($smiley);
+    }
+
+    /**
+     * Format an entity
+     *
+     * Entities are basically small text replacements
+     *
+     * Uses $this->entities
+     *
+     * @param string $entity
+     */
+    function entity($entity) {
+        $this->cdata($entity);
+    }
+
+    /**
+     * Typographically format a multiply sign
+     *
+     * Example: ($x=640, $y=480) should result in "640×480"
+     *
+     * @param string|int $x first value
+     * @param string|int $y second value
+     */
+    function multiplyentity($x, $y) {
+        $this->cdata($x.'×'.$y);
+    }
+
+    /**
+     * Render an opening single quote char (language specific)
+     */
+    function singlequoteopening() {
+        global $lang;
+        $this->cdata($lang['singlequoteopening']);
+    }
+
+    /**
+     * Render a closing single quote char (language specific)
+     */
+    function singlequoteclosing() {
+        global $lang;
+        $this->cdata($lang['singlequoteclosing']);
+    }
+
+    /**
+     * Render an apostrophe char (language specific)
+     */
+    function apostrophe() {
+        global $lang;
+        $this->cdata($lang['apostrophe']);
+    }
+
+    /**
+     * Render an opening double quote char (language specific)
+     */
+    function doublequoteopening() {
+        global $lang;
+        $this->cdata($lang['doublequoteopening']);
+    }
+
+    /**
+     * Render an closinging double quote char (language specific)
+     */
+    function doublequoteclosing() {
+        global $lang;
+        $this->cdata($lang['doublequoteclosing']);
+    }
+
+    /**
+     * Render a CamelCase link
+     *
+     * @param string $link The link name
+     * @see http://en.wikipedia.org/wiki/CamelCase
+     */
+    function camelcaselink($link) {
+        $this->internallink($link, $link);
+    }
+
+    /**
+     * Render a page local link
+     *
+     * @param string $hash hash link identifier
+     * @param string $name name for the link
+     */
+    function locallink($hash, $name = null) {
+        if(is_array($name)) {
+            $this->_firstimage($name['src']);
+            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
+        }
+    }
+
+    /**
+     * keep track of internal links in $this->meta['relation']['references']
+     *
+     * @param string            $id   page ID to link to. eg. 'wiki:syntax'
+     * @param string|array|null $name name for the link, array for media file
+     */
+    function internallink($id, $name = null) {
+        global $ID;
+
+        if(is_array($name)) {
+            $this->_firstimage($name['src']);
+            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
+        }
+
+        $parts = explode('?', $id, 2);
+        if(count($parts) === 2) {
+            $id = $parts[0];
+        }
+
+        $default = $this->_simpleTitle($id);
+
+        // first resolve and clean up the $id
+        resolve_pageid(getNS($ID), $id, $exists);
+        @list($page) = explode('#', $id, 2);
+
+        // set metadata
+        $this->meta['relation']['references'][$page] = $exists;
+        // $data = array('relation' => array('isreferencedby' => array($ID => true)));
+        // p_set_metadata($id, $data);
+
+        // add link title to summary
+        if($this->capture) {
+            $name = $this->_getLinkTitle($name, $default, $id);
+            $this->doc .= $name;
+        }
+    }
+
+    /**
+     * Render an external link
+     *
+     * @param string            $url  full URL with scheme
+     * @param string|array|null $name name for the link, array for media file
+     */
+    function externallink($url, $name = null) {
+        if(is_array($name)) {
+            $this->_firstimage($name['src']);
+            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
+        }
+
+        if($this->capture) {
+            $this->doc .= $this->_getLinkTitle($name, '<'.$url.'>');
+        }
+    }
+
+    /**
+     * Render an interwiki link
+     *
+     * You may want to use $this->_resolveInterWiki() here
+     *
+     * @param string       $match     original link - probably not much use
+     * @param string|array $name      name for the link, array for media file
+     * @param string       $wikiName  indentifier (shortcut) for the remote wiki
+     * @param string       $wikiUri   the fragment parsed from the original link
+     */
+    function interwikilink($match, $name = null, $wikiName, $wikiUri) {
+        if(is_array($name)) {
+            $this->_firstimage($name['src']);
+            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
+        }
+
+        if($this->capture) {
+            list($wikiUri) = explode('#', $wikiUri, 2);
+            $name = $this->_getLinkTitle($name, $wikiUri);
+            $this->doc .= $name;
+        }
+    }
+
+    /**
+     * Link to windows share
+     *
+     * @param string       $url  the link
+     * @param string|array $name name for the link, array for media file
+     */
+    function windowssharelink($url, $name = null) {
+        if(is_array($name)) {
+            $this->_firstimage($name['src']);
+            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
+        }
+
+        if($this->capture) {
+            if($name) $this->doc .= $name;
+            else $this->doc .= '<'.$url.'>';
+        }
+    }
+
+    /**
+     * Render a linked E-Mail Address
+     *
+     * Should honor $conf['mailguard'] setting
+     *
+     * @param string       $address Email-Address
+     * @param string|array $name    name for the link, array for media file
+     */
+    function emaillink($address, $name = null) {
+        if(is_array($name)) {
+            $this->_firstimage($name['src']);
+            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
+        }
+
+        if($this->capture) {
+            if($name) $this->doc .= $name;
+            else $this->doc .= '<'.$address.'>';
+        }
+    }
+
+    /**
+     * Render an internal media file
+     *
+     * @param string $src     media ID
+     * @param string $title   descriptive text
+     * @param string $align   left|center|right
+     * @param int    $width   width of media in pixel
+     * @param int    $height  height of media in pixel
+     * @param string $cache   cache|recache|nocache
+     * @param string $linking linkonly|detail|nolink
+     */
+    function internalmedia($src, $title = null, $align = null, $width = null,
+                           $height = null, $cache = null, $linking = null) {
+        if($this->capture && $title) $this->doc .= '['.$title.']';
+        $this->_firstimage($src);
+        $this->_recordMediaUsage($src);
+    }
+
+    /**
+     * Render an external media file
+     *
+     * @param string $src     full media URL
+     * @param string $title   descriptive text
+     * @param string $align   left|center|right
+     * @param int    $width   width of media in pixel
+     * @param int    $height  height of media in pixel
+     * @param string $cache   cache|recache|nocache
+     * @param string $linking linkonly|detail|nolink
+     */
+    function externalmedia($src, $title = null, $align = null, $width = null,
+                           $height = null, $cache = null, $linking = null) {
+        if($this->capture && $title) $this->doc .= '['.$title.']';
+        $this->_firstimage($src);
+    }
+
+    /**
+     * Render the output of an RSS feed
+     *
+     * @param string $url    URL of the feed
+     * @param array  $params Finetuning of the output
+     */
+    function rss($url, $params) {
+        $this->meta['relation']['haspart'][$url] = true;
+
+        $this->meta['date']['valid']['age'] =
+            isset($this->meta['date']['valid']['age']) ?
+                min($this->meta['date']['valid']['age'], $params['refresh']) :
+                $params['refresh'];
+    }
+
+    #region Utils
+
+    /**
+     * Removes any Namespace from the given name but keeps
+     * casing and special chars
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     *
+     * @param string $name
+     *
+     * @return mixed|string
+     */
+    function _simpleTitle($name) {
+        global $conf;
+
+        if(is_array($name)) return '';
+
+        if($conf['useslash']) {
+            $nssep = '[:;/]';
+        } else {
+            $nssep = '[:;]';
+        }
+        $name = preg_replace('!.*'.$nssep.'!', '', $name);
+        //if there is a hash we use the anchor name only
+        $name = preg_replace('!.*#!', '', $name);
+        return $name;
+    }
+
+    /**
+     * Creates a linkid from a headline
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     * @param string  $title   The headline title
+     * @param boolean $create  Create a new unique ID?
+     * @return string
+     */
+    function _headerToLink($title, $create = false) {
+        if($create) {
+            return sectionID($title, $this->headers);
+        } else {
+            $check = false;
+            return sectionID($title, $check);
+        }
+    }
+
+    /**
+     * Construct a title and handle images in titles
+     *
+     * @author Harry Fuecks <hfuecks@gmail.com>
+     * @param string|array|null $title    either string title or media array
+     * @param string            $default  default title if nothing else is found
+     * @param null|string       $id       linked page id (used to extract title from first heading)
+     * @return string title text
+     */
+    function _getLinkTitle($title, $default, $id = null) {
+        if(is_array($title)) {
+            if($title['title']) {
+                return '['.$title['title'].']';
+            } else {
+                return $default;
+            }
+        } else if(is_null($title) || trim($title) == '') {
+            if(useHeading('content') && $id) {
+                $heading = p_get_first_heading($id, METADATA_DONT_RENDER);
+                if($heading) return $heading;
+            }
+            return $default;
+        } else {
+            return $title;
+        }
+    }
+
+    /**
+     * Remember first image
+     *
+     * @param string $src image URL or ID
+     */
+    function _firstimage($src) {
+        if($this->firstimage) return;
+        global $ID;
+
+        list($src) = explode('#', $src, 2);
+        if(!media_isexternal($src)) {
+            resolve_mediaid(getNS($ID), $src, $exists);
+        }
+        if(preg_match('/.(jpe?g|gif|png)$/i', $src)) {
+            $this->firstimage = $src;
+        }
+    }
+
+    /**
+     * Store list of used media files in metadata
+     *
+     * @param string $src media ID
+     */
+    function _recordMediaUsage($src) {
+        global $ID;
+
+        list ($src) = explode('#', $src, 2);
+        if(media_isexternal($src)) return;
+        resolve_mediaid(getNS($ID), $src, $exists);
+        $this->meta['relation']['media'][$src] = $exists;
+    }
+
+    #endregion
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/wiki/inc/parser/parser.php b/wiki/inc/parser/parser.php
new file mode 100644
index 0000000..8cff2b8
--- /dev/null
+++ b/wiki/inc/parser/parser.php
@@ -0,0 +1,1034 @@
+<?php
+if(!defined('DOKU_INC')) die('meh.');
+require_once DOKU_INC . 'inc/parser/lexer.php';
+require_once DOKU_INC . 'inc/parser/handler.php';
+
+
+/**
+ * Define various types of modes used by the parser - they are used to
+ * populate the list of modes another mode accepts
+ */
+global $PARSER_MODES;
+$PARSER_MODES = array(
+    // containers are complex modes that can contain many other modes
+    // hr breaks the principle but they shouldn't be used in tables / lists
+    // so they are put here
+    'container'    => array('listblock','table','quote','hr'),
+
+    // some mode are allowed inside the base mode only
+    'baseonly'     => array('header'),
+
+    // modes for styling text -- footnote behaves similar to styling
+    'formatting'   => array('strong', 'emphasis', 'underline', 'monospace',
+                            'subscript', 'superscript', 'deleted', 'footnote'),
+
+    // modes where the token is simply replaced - they can not contain any
+    // other modes
+    'substition'   => array('acronym','smiley','wordblock','entity',
+                            'camelcaselink', 'internallink','media',
+                            'externallink','linebreak','emaillink',
+                            'windowssharelink','filelink','notoc',
+                            'nocache','multiplyentity','quotes','rss'),
+
+    // modes which have a start and end token but inside which
+    // no other modes should be applied
+    'protected'    => array('preformatted','code','file','php','html','htmlblock','phpblock'),
+
+    // inside this mode no wiki markup should be applied but lineendings
+    // and whitespace isn't preserved
+    'disabled'     => array('unformatted'),
+
+    // used to mark paragraph boundaries
+    'paragraphs'   => array('eol')
+);
+
+//-------------------------------------------------------------------
+
+/**
+ * Sets up the Lexer with modes and points it to the Handler
+ * For an intro to the Lexer see: wiki:parser
+ */
+class Doku_Parser {
+
+    var $Handler;
+
+    /**
+     * @var Doku_Lexer $Lexer
+     */
+    var $Lexer;
+
+    var $modes = array();
+
+    var $connected = false;
+
+    /**
+     * @param Doku_Parser_Mode_base $BaseMode
+     */
+    function addBaseMode($BaseMode) {
+        $this->modes['base'] = $BaseMode;
+        if ( !$this->Lexer ) {
+            $this->Lexer = new Doku_Lexer($this->Handler,'base', true);
+        }
+        $this->modes['base']->Lexer = $this->Lexer;
+    }
+
+    /**
+     * PHP preserves order of associative elements
+     * Mode sequence is important
+     *
+     * @param string $name
+     * @param Doku_Parser_Mode_Interface $Mode
+     */
+    function addMode($name, Doku_Parser_Mode_Interface $Mode) {
+        if ( !isset($this->modes['base']) ) {
+            $this->addBaseMode(new Doku_Parser_Mode_base());
+        }
+        $Mode->Lexer = $this->Lexer;
+        $this->modes[$name] = $Mode;
+    }
+
+    function connectModes() {
+
+        if ( $this->connected ) {
+            return;
+        }
+
+        foreach ( array_keys($this->modes) as $mode ) {
+
+            // Base isn't connected to anything
+            if ( $mode == 'base' ) {
+                continue;
+            }
+            $this->modes[$mode]->preConnect();
+
+            foreach ( array_keys($this->modes) as $cm ) {
+
+                if ( $this->modes[$cm]->accepts($mode) ) {
+                    $this->modes[$mode]->connectTo($cm);
+                }
+
+            }
+
+            $this->modes[$mode]->postConnect();
+        }
+
+        $this->connected = true;
+    }
+
+    function parse($doc) {
+        if ( $this->Lexer ) {
+            $this->connectModes();
+            // Normalize CRs and pad doc
+            $doc = "\n".str_replace("\r\n","\n",$doc)."\n";
+            $this->Lexer->parse($doc);
+            $this->Handler->_finalize();
+            return $this->Handler->calls;
+        } else {
+            return false;
+        }
+    }
+
+}
+
+//-------------------------------------------------------------------
+
+/**
+ * Class Doku_Parser_Mode_Interface
+ *
+ * Defines a mode (syntax component) in the Parser
+ */
+interface Doku_Parser_Mode_Interface {
+    /**
+     * returns a number used to determine in which order modes are added
+     */
+    public function getSort();
+
+    /**
+     * Called before any calls to connectTo
+     * @return void
+     */
+    function preConnect();
+
+    /**
+     * Connects the mode
+     *
+     * @param string $mode
+     * @return void
+     */
+    function connectTo($mode);
+
+    /**
+     * Called after all calls to connectTo
+     * @return void
+     */
+    function postConnect();
+
+    /**
+     * Check if given mode is accepted inside this mode
+     *
+     * @param string $mode
+     * @return bool
+     */
+    function accepts($mode);
+}
+
+/**
+ * This class and all the subclasses below are used to reduce the effort required to register
+ * modes with the Lexer.
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+class Doku_Parser_Mode implements Doku_Parser_Mode_Interface {
+    /**
+     * @var Doku_Lexer $Lexer
+     */
+    var $Lexer;
+    var $allowedModes = array();
+
+    function getSort() {
+        trigger_error('getSort() not implemented in '.get_class($this), E_USER_WARNING);
+    }
+
+    function preConnect() {}
+    function connectTo($mode) {}
+    function postConnect() {}
+    function accepts($mode) {
+        return in_array($mode, (array) $this->allowedModes );
+    }
+}
+
+/**
+ * Basically the same as Doku_Parser_Mode but extends from DokuWiki_Plugin
+ *
+ * Adds additional functions to syntax plugins
+ */
+class Doku_Parser_Mode_Plugin extends DokuWiki_Plugin implements Doku_Parser_Mode_Interface {
+    /**
+     * @var Doku_Lexer $Lexer
+     */
+    var $Lexer;
+    var $allowedModes = array();
+
+    /**
+     * Sort for applying this mode
+     *
+     * @return int
+     */
+    function getSort() {
+        trigger_error('getSort() not implemented in '.get_class($this), E_USER_WARNING);
+    }
+
+    function preConnect() {}
+    function connectTo($mode) {}
+    function postConnect() {}
+    function accepts($mode) {
+        return in_array($mode, (array) $this->allowedModes );
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_base extends Doku_Parser_Mode {
+
+    function __construct() {
+        global $PARSER_MODES;
+
+        $this->allowedModes = array_merge (
+                $PARSER_MODES['container'],
+                $PARSER_MODES['baseonly'],
+                $PARSER_MODES['paragraphs'],
+                $PARSER_MODES['formatting'],
+                $PARSER_MODES['substition'],
+                $PARSER_MODES['protected'],
+                $PARSER_MODES['disabled']
+            );
+    }
+
+    function getSort() {
+        return 0;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_footnote extends Doku_Parser_Mode {
+
+    function __construct() {
+        global $PARSER_MODES;
+
+        $this->allowedModes = array_merge (
+                $PARSER_MODES['container'],
+                $PARSER_MODES['formatting'],
+                $PARSER_MODES['substition'],
+                $PARSER_MODES['protected'],
+                $PARSER_MODES['disabled']
+            );
+
+        unset($this->allowedModes[array_search('footnote', $this->allowedModes)]);
+    }
+
+    function connectTo($mode) {
+        $this->Lexer->addEntryPattern(
+            '\x28\x28(?=.*\x29\x29)',$mode,'footnote'
+            );
+    }
+
+    function postConnect() {
+        $this->Lexer->addExitPattern(
+            '\x29\x29','footnote'
+            );
+    }
+
+    function getSort() {
+        return 150;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_header extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        //we're not picky about the closing ones, two are enough
+        $this->Lexer->addSpecialPattern(
+                            '[ \t]*={2,}[^\n]+={2,}[ \t]*(?=\n)',
+                            $mode,
+                            'header'
+                        );
+    }
+
+    function getSort() {
+        return 50;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_notoc extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addSpecialPattern('~~NOTOC~~',$mode,'notoc');
+    }
+
+    function getSort() {
+        return 30;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_nocache extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addSpecialPattern('~~NOCACHE~~',$mode,'nocache');
+    }
+
+    function getSort() {
+        return 40;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_linebreak extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addSpecialPattern('\x5C{2}(?:[ \t]|(?=\n))',$mode,'linebreak');
+    }
+
+    function getSort() {
+        return 140;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_eol extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $badModes = array('listblock','table');
+        if ( in_array($mode, $badModes) ) {
+            return;
+        }
+        // see FS#1652, pattern extended to swallow preceding whitespace to avoid issues with lines that only contain whitespace
+        $this->Lexer->addSpecialPattern('(?:^[ \t]*)?\n',$mode,'eol');
+    }
+
+    function getSort() {
+        return 370;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_hr extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addSpecialPattern('\n[ \t]*-{4,}[ \t]*(?=\n)',$mode,'hr');
+    }
+
+    function getSort() {
+        return 160;
+    }
+}
+
+//-------------------------------------------------------------------
+/**
+ * This class sets the markup for bold (=strong),
+ * italic (=emphasis), underline etc.
+ */
+class Doku_Parser_Mode_formatting extends Doku_Parser_Mode {
+    var $type;
+
+    var $formatting = array (
+        'strong' => array (
+            'entry'=>'\*\*(?=.*\*\*)',
+            'exit'=>'\*\*',
+            'sort'=>70
+            ),
+
+        'emphasis'=> array (
+            'entry'=>'//(?=[^\x00]*[^:])', //hack for bugs #384 #763 #1468
+            'exit'=>'//',
+            'sort'=>80
+            ),
+
+        'underline'=> array (
+            'entry'=>'__(?=.*__)',
+            'exit'=>'__',
+            'sort'=>90
+            ),
+
+        'monospace'=> array (
+            'entry'=>'\x27\x27(?=.*\x27\x27)',
+            'exit'=>'\x27\x27',
+            'sort'=>100
+            ),
+
+        'subscript'=> array (
+            'entry'=>'<sub>(?=.*</sub>)',
+            'exit'=>'</sub>',
+            'sort'=>110
+            ),
+
+        'superscript'=> array (
+            'entry'=>'<sup>(?=.*</sup>)',
+            'exit'=>'</sup>',
+            'sort'=>120
+            ),
+
+        'deleted'=> array (
+            'entry'=>'<del>(?=.*</del>)',
+            'exit'=>'</del>',
+            'sort'=>130
+            ),
+        );
+
+    /**
+     * @param string $type
+     */
+    function __construct($type) {
+        global $PARSER_MODES;
+
+        if ( !array_key_exists($type, $this->formatting) ) {
+            trigger_error('Invalid formatting type '.$type, E_USER_WARNING);
+        }
+
+        $this->type = $type;
+
+        // formatting may contain other formatting but not it self
+        $modes = $PARSER_MODES['formatting'];
+        $key = array_search($type, $modes);
+        if ( is_int($key) ) {
+            unset($modes[$key]);
+        }
+
+        $this->allowedModes = array_merge (
+                $modes,
+                $PARSER_MODES['substition'],
+                $PARSER_MODES['disabled']
+            );
+    }
+
+    function connectTo($mode) {
+
+        // Can't nest formatting in itself
+        if ( $mode == $this->type ) {
+            return;
+        }
+
+        $this->Lexer->addEntryPattern(
+                $this->formatting[$this->type]['entry'],
+                $mode,
+                $this->type
+            );
+    }
+
+    function postConnect() {
+
+        $this->Lexer->addExitPattern(
+            $this->formatting[$this->type]['exit'],
+            $this->type
+            );
+
+    }
+
+    function getSort() {
+        return $this->formatting[$this->type]['sort'];
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_listblock extends Doku_Parser_Mode {
+
+    function __construct() {
+        global $PARSER_MODES;
+
+        $this->allowedModes = array_merge (
+                $PARSER_MODES['formatting'],
+                $PARSER_MODES['substition'],
+                $PARSER_MODES['disabled'],
+                $PARSER_MODES['protected'] #XXX new
+            );
+
+    //    $this->allowedModes[] = 'footnote';
+    }
+
+    function connectTo($mode) {
+        $this->Lexer->addEntryPattern('[ \t]*\n {2,}[\-\*]',$mode,'listblock');
+        $this->Lexer->addEntryPattern('[ \t]*\n\t{1,}[\-\*]',$mode,'listblock');
+
+        $this->Lexer->addPattern('\n {2,}[\-\*]','listblock');
+        $this->Lexer->addPattern('\n\t{1,}[\-\*]','listblock');
+
+    }
+
+    function postConnect() {
+        $this->Lexer->addExitPattern('\n','listblock');
+    }
+
+    function getSort() {
+        return 10;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_table extends Doku_Parser_Mode {
+
+    function __construct() {
+        global $PARSER_MODES;
+
+        $this->allowedModes = array_merge (
+                $PARSER_MODES['formatting'],
+                $PARSER_MODES['substition'],
+                $PARSER_MODES['disabled'],
+                $PARSER_MODES['protected']
+            );
+    }
+
+    function connectTo($mode) {
+        $this->Lexer->addEntryPattern('[\t ]*\n\^',$mode,'table');
+        $this->Lexer->addEntryPattern('[\t ]*\n\|',$mode,'table');
+    }
+
+    function postConnect() {
+        $this->Lexer->addPattern('\n\^','table');
+        $this->Lexer->addPattern('\n\|','table');
+        $this->Lexer->addPattern('[\t ]*:::[\t ]*(?=[\|\^])','table');
+        $this->Lexer->addPattern('[\t ]+','table');
+        $this->Lexer->addPattern('\^','table');
+        $this->Lexer->addPattern('\|','table');
+        $this->Lexer->addExitPattern('\n','table');
+    }
+
+    function getSort() {
+        return 60;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_unformatted extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addEntryPattern('<nowiki>(?=.*</nowiki>)',$mode,'unformatted');
+        $this->Lexer->addEntryPattern('%%(?=.*%%)',$mode,'unformattedalt');
+    }
+
+    function postConnect() {
+        $this->Lexer->addExitPattern('</nowiki>','unformatted');
+        $this->Lexer->addExitPattern('%%','unformattedalt');
+        $this->Lexer->mapHandler('unformattedalt','unformatted');
+    }
+
+    function getSort() {
+        return 170;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_php extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addEntryPattern('<php>(?=.*</php>)',$mode,'php');
+        $this->Lexer->addEntryPattern('<PHP>(?=.*</PHP>)',$mode,'phpblock');
+    }
+
+    function postConnect() {
+        $this->Lexer->addExitPattern('</php>','php');
+        $this->Lexer->addExitPattern('</PHP>','phpblock');
+    }
+
+    function getSort() {
+        return 180;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_html extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addEntryPattern('<html>(?=.*</html>)',$mode,'html');
+        $this->Lexer->addEntryPattern('<HTML>(?=.*</HTML>)',$mode,'htmlblock');
+    }
+
+    function postConnect() {
+        $this->Lexer->addExitPattern('</html>','html');
+        $this->Lexer->addExitPattern('</HTML>','htmlblock');
+    }
+
+    function getSort() {
+        return 190;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_preformatted extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        // Has hard coded awareness of lists...
+        $this->Lexer->addEntryPattern('\n  (?![\*\-])',$mode,'preformatted');
+        $this->Lexer->addEntryPattern('\n\t(?![\*\-])',$mode,'preformatted');
+
+        // How to effect a sub pattern with the Lexer!
+        $this->Lexer->addPattern('\n  ','preformatted');
+        $this->Lexer->addPattern('\n\t','preformatted');
+
+    }
+
+    function postConnect() {
+        $this->Lexer->addExitPattern('\n','preformatted');
+    }
+
+    function getSort() {
+        return 20;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_code extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addEntryPattern('<code\b(?=.*</code>)',$mode,'code');
+    }
+
+    function postConnect() {
+        $this->Lexer->addExitPattern('</code>','code');
+    }
+
+    function getSort() {
+        return 200;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_file extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addEntryPattern('<file\b(?=.*</file>)',$mode,'file');
+    }
+
+    function postConnect() {
+        $this->Lexer->addExitPattern('</file>','file');
+    }
+
+    function getSort() {
+        return 210;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_quote extends Doku_Parser_Mode {
+
+    function __construct() {
+        global $PARSER_MODES;
+
+        $this->allowedModes = array_merge (
+                $PARSER_MODES['formatting'],
+                $PARSER_MODES['substition'],
+                $PARSER_MODES['disabled'],
+                $PARSER_MODES['protected'] #XXX new
+            );
+            #$this->allowedModes[] = 'footnote';
+            #$this->allowedModes[] = 'preformatted';
+            #$this->allowedModes[] = 'unformatted';
+    }
+
+    function connectTo($mode) {
+        $this->Lexer->addEntryPattern('\n>{1,}',$mode,'quote');
+    }
+
+    function postConnect() {
+        $this->Lexer->addPattern('\n>{1,}','quote');
+        $this->Lexer->addExitPattern('\n','quote');
+    }
+
+    function getSort() {
+        return 220;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_acronym extends Doku_Parser_Mode {
+    // A list
+    var $acronyms = array();
+    var $pattern = '';
+
+    function __construct($acronyms) {
+        usort($acronyms,array($this,'_compare'));
+        $this->acronyms = $acronyms;
+    }
+
+    function preConnect() {
+        if(!count($this->acronyms)) return;
+
+        $bound = '[\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]';
+        $acronyms = array_map('Doku_Lexer_Escape',$this->acronyms);
+        $this->pattern = '(?<=^|'.$bound.')(?:'.join('|',$acronyms).')(?='.$bound.')';
+    }
+
+    function connectTo($mode) {
+        if(!count($this->acronyms)) return;
+
+        if ( strlen($this->pattern) > 0 ) {
+            $this->Lexer->addSpecialPattern($this->pattern,$mode,'acronym');
+        }
+    }
+
+    function getSort() {
+        return 240;
+    }
+
+    /**
+     * sort callback to order by string length descending
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return int
+     */
+    function _compare($a,$b) {
+        $a_len = strlen($a);
+        $b_len = strlen($b);
+        if ($a_len > $b_len) {
+            return -1;
+        } else if ($a_len < $b_len) {
+            return 1;
+        }
+
+        return 0;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_smiley extends Doku_Parser_Mode {
+    // A list
+    var $smileys = array();
+    var $pattern = '';
+
+    function __construct($smileys) {
+        $this->smileys = $smileys;
+    }
+
+    function preConnect() {
+        if(!count($this->smileys) || $this->pattern != '') return;
+
+        $sep = '';
+        foreach ( $this->smileys as $smiley ) {
+            $this->pattern .= $sep.'(?<=\W|^)'.Doku_Lexer_Escape($smiley).'(?=\W|$)';
+            $sep = '|';
+        }
+    }
+
+    function connectTo($mode) {
+        if(!count($this->smileys)) return;
+
+        if ( strlen($this->pattern) > 0 ) {
+            $this->Lexer->addSpecialPattern($this->pattern,$mode,'smiley');
+        }
+    }
+
+    function getSort() {
+        return 230;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_wordblock extends Doku_Parser_Mode {
+    // A list
+    var $badwords = array();
+    var $pattern = '';
+
+    function __construct($badwords) {
+        $this->badwords = $badwords;
+    }
+
+    function preConnect() {
+
+        if ( count($this->badwords) == 0 || $this->pattern != '') {
+            return;
+        }
+
+        $sep = '';
+        foreach ( $this->badwords as $badword ) {
+            $this->pattern .= $sep.'(?<=\b)(?i)'.Doku_Lexer_Escape($badword).'(?-i)(?=\b)';
+            $sep = '|';
+        }
+
+    }
+
+    function connectTo($mode) {
+        if ( strlen($this->pattern) > 0 ) {
+            $this->Lexer->addSpecialPattern($this->pattern,$mode,'wordblock');
+        }
+    }
+
+    function getSort() {
+        return 250;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_entity extends Doku_Parser_Mode {
+    // A list
+    var $entities = array();
+    var $pattern = '';
+
+    function __construct($entities) {
+        $this->entities = $entities;
+    }
+
+    function preConnect() {
+        if(!count($this->entities) || $this->pattern != '') return;
+
+        $sep = '';
+        foreach ( $this->entities as $entity ) {
+            $this->pattern .= $sep.Doku_Lexer_Escape($entity);
+            $sep = '|';
+        }
+    }
+
+    function connectTo($mode) {
+        if(!count($this->entities)) return;
+
+        if ( strlen($this->pattern) > 0 ) {
+            $this->Lexer->addSpecialPattern($this->pattern,$mode,'entity');
+        }
+    }
+
+    function getSort() {
+        return 260;
+    }
+}
+
+//-------------------------------------------------------------------
+// Implements the 640x480 replacement
+class Doku_Parser_Mode_multiplyentity extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+
+        $this->Lexer->addSpecialPattern(
+                    '(?<=\b)(?:[1-9]|\d{2,})[xX]\d+(?=\b)',$mode,'multiplyentity'
+                );
+
+    }
+
+    function getSort() {
+        return 270;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_quotes extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        global $conf;
+
+        $ws   =  '\s/\#~:+=&%@\-\x28\x29\]\[{}><"\'';   // whitespace
+        $punc =  ';,\.?!';
+
+        if($conf['typography'] == 2){
+            $this->Lexer->addSpecialPattern(
+                        "(?<=^|[$ws])'(?=[^$ws$punc])",$mode,'singlequoteopening'
+                    );
+            $this->Lexer->addSpecialPattern(
+                        "(?<=^|[^$ws]|[$punc])'(?=$|[$ws$punc])",$mode,'singlequoteclosing'
+                    );
+            $this->Lexer->addSpecialPattern(
+                        "(?<=^|[^$ws$punc])'(?=$|[^$ws$punc])",$mode,'apostrophe'
+                    );
+        }
+
+        $this->Lexer->addSpecialPattern(
+                    "(?<=^|[$ws])\"(?=[^$ws$punc])",$mode,'doublequoteopening'
+                );
+        $this->Lexer->addSpecialPattern(
+                    "\"",$mode,'doublequoteclosing'
+                );
+
+    }
+
+    function getSort() {
+        return 280;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_camelcaselink extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addSpecialPattern(
+                '\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b',$mode,'camelcaselink'
+            );
+    }
+
+    function getSort() {
+        return 290;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_internallink extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        // Word boundaries?
+        $this->Lexer->addSpecialPattern("\[\[.*?\]\](?!\])",$mode,'internallink');
+    }
+
+    function getSort() {
+        return 300;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_media extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        // Word boundaries?
+        $this->Lexer->addSpecialPattern("\{\{(?:[^\}]|(?:\}[^\}]))+\}\}",$mode,'media');
+    }
+
+    function getSort() {
+        return 320;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_rss extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        $this->Lexer->addSpecialPattern("\{\{rss>[^\}]+\}\}",$mode,'rss');
+    }
+
+    function getSort() {
+        return 310;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_externallink extends Doku_Parser_Mode {
+    var $schemes = array();
+    var $patterns = array();
+
+    function preConnect() {
+        if(count($this->patterns)) return;
+
+        $ltrs = '\w';
+        $gunk = '/\#~:.?+=&%@!\-\[\]';
+        $punc = '.:?\-;,';
+        $host = $ltrs.$punc;
+        $any  = $ltrs.$gunk.$punc;
+
+        $this->schemes = getSchemes();
+        foreach ( $this->schemes as $scheme ) {
+            $this->patterns[] = '\b(?i)'.$scheme.'(?-i)://['.$any.']+?(?=['.$punc.']*[^'.$any.'])';
+        }
+
+        $this->patterns[] = '(?<=\s)(?i)www?(?-i)\.['.$host.']+?\.['.$host.']+?['.$any.']+?(?=['.$punc.']*[^'.$any.'])';
+        $this->patterns[] = '(?<=\s)(?i)ftp?(?-i)\.['.$host.']+?\.['.$host.']+?['.$any.']+?(?=['.$punc.']*[^'.$any.'])';
+    }
+
+    function connectTo($mode) {
+
+        foreach ( $this->patterns as $pattern ) {
+            $this->Lexer->addSpecialPattern($pattern,$mode,'externallink');
+        }
+    }
+
+    function getSort() {
+        return 330;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_filelink extends Doku_Parser_Mode {
+
+    var $pattern;
+
+    function preConnect() {
+
+        $ltrs = '\w';
+        $gunk = '/\#~:.?+=&%@!\-';
+        $punc = '.:?\-;,';
+        $host = $ltrs.$punc;
+        $any  = $ltrs.$gunk.$punc;
+
+        $this->pattern = '\b(?i)file(?-i)://['.$any.']+?['.
+            $punc.']*[^'.$any.']';
+    }
+
+    function connectTo($mode) {
+        $this->Lexer->addSpecialPattern(
+            $this->pattern,$mode,'filelink');
+    }
+
+    function getSort() {
+        return 360;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_windowssharelink extends Doku_Parser_Mode {
+
+    var $pattern;
+
+    function preConnect() {
+        $this->pattern = "\\\\\\\\\w+?(?:\\\\[\w\-$]+)+";
+    }
+
+    function connectTo($mode) {
+        $this->Lexer->addSpecialPattern(
+            $this->pattern,$mode,'windowssharelink');
+    }
+
+    function getSort() {
+        return 350;
+    }
+}
+
+//-------------------------------------------------------------------
+class Doku_Parser_Mode_emaillink extends Doku_Parser_Mode {
+
+    function connectTo($mode) {
+        // pattern below is defined in inc/mail.php
+        $this->Lexer->addSpecialPattern('<'.PREG_PATTERN_VALID_EMAIL.'>',$mode,'emaillink');
+    }
+
+    function getSort() {
+        return 340;
+    }
+}
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/wiki/inc/parser/renderer.php b/wiki/inc/parser/renderer.php
new file mode 100644
index 0000000..83b51d4
--- /dev/null
+++ b/wiki/inc/parser/renderer.php
@@ -0,0 +1,883 @@
+<?php
+/**
+ * Renderer output base class
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+if(!defined('DOKU_INC')) die('meh.');
+
+/**
+ * Allowed chars in $language for code highlighting
+ * @see GeSHi::set_language()
+ */
+define('PREG_PATTERN_VALID_LANGUAGE', '#[^a-zA-Z0-9\-_]#');
+
+/**
+ * An empty renderer, produces no output
+ *
+ * Inherits from DokuWiki_Plugin for giving additional functions to render plugins
+ *
+ * The renderer transforms the syntax instructions created by the parser and handler into the
+ * desired output format. For each instruction a corresponding method defined in this class will
+ * be called. That method needs to produce the desired output for the instruction and add it to the
+ * $doc field. When all instructions are processed, the $doc field contents will be cached by
+ * DokuWiki and sent to the user.
+ */
+class Doku_Renderer extends DokuWiki_Plugin {
+    /** @var array Settings, control the behavior of the renderer */
+    public $info = array(
+        'cache' => true, // may the rendered result cached?
+        'toc'   => true, // render the TOC?
+    );
+
+    /** @var array contains the smiley configuration, set in p_render() */
+    public $smileys = array();
+    /** @var array contains the entity configuration, set in p_render() */
+    public $entities = array();
+    /** @var array contains the acronym configuration, set in p_render() */
+    public $acronyms = array();
+    /** @var array contains the interwiki configuration, set in p_render() */
+    public $interwiki = array();
+
+    /**
+     * @var string the rendered document, this will be cached after the renderer ran through
+     */
+    public $doc = '';
+
+    /**
+     * clean out any per-use values
+     *
+     * This is called before each use of the renderer object and should be used to
+     * completely reset the state of the renderer to be reused for a new document
+     */
+    function reset() {
+    }
+
+    /**
+     * Allow the plugin to prevent DokuWiki from reusing an instance
+     *
+     * Since most renderer plugins fail to implement Doku_Renderer::reset() we default
+     * to reinstantiating the renderer here
+     *
+     * @return bool   false if the plugin has to be instantiated
+     */
+    function isSingleton() {
+        return false;
+    }
+
+    /**
+     * Returns the format produced by this renderer.
+     *
+     * Has to be overidden by sub classes
+     *
+     * @return string
+     */
+    function getFormat() {
+        trigger_error('getFormat() not implemented in '.get_class($this), E_USER_WARNING);
+        return '';
+    }
+
+    /**
+     * Disable caching of this renderer's output
+     */
+    function nocache() {
+        $this->info['cache'] = false;
+    }
+
+    /**
+     * Disable TOC generation for this renderer's output
+     *
+     * This might not be used for certain sub renderer
+     */
+    function notoc() {
+        $this->info['toc'] = false;
+    }
+
+    /**
+     * Handle plugin rendering
+     *
+     * Most likely this needs NOT to be overwritten by sub classes
+     *
+     * @param string $name  Plugin name
+     * @param mixed  $data  custom data set by handler
+     * @param string $state matched state if any
+     * @param string $match raw matched syntax
+     */
+    function plugin($name, $data, $state = '', $match = '') {
+        /** @var DokuWiki_Syntax_Plugin $plugin */
+        $plugin = plugin_load('syntax', $name);
+        if($plugin != null) {
+            $plugin->render($this->getFormat(), $this, $data);
+        }
+    }
+
+    /**
+     * handle nested render instructions
+     * this method (and nest_close method) should not be overloaded in actual renderer output classes
+     *
+     * @param array $instructions
+     */
+    function nest($instructions) {
+        foreach($instructions as $instruction) {
+            // execute the callback against ourself
+            if(method_exists($this, $instruction[0])) {
+                call_user_func_array(array($this, $instruction[0]), $instruction[1] ? $instruction[1] : array());
+            }
+        }
+    }
+
+    /**
+     * dummy closing instruction issued by Doku_Handler_Nest
+     *
+     * normally the syntax mode should override this instruction when instantiating Doku_Handler_Nest -
+     * however plugins will not be able to - as their instructions require data.
+     */
+    function nest_close() {
+    }
+
+    #region Syntax modes - sub classes will need to implement them to fill $doc
+
+    /**
+     * Initialize the document
+     */
+    function document_start() {
+    }
+
+    /**
+     * Finalize the document
+     */
+    function document_end() {
+    }
+
+    /**
+     * Render the Table of Contents
+     *
+     * @return string
+     */
+    function render_TOC() {
+        return '';
+    }
+
+    /**
+     * Add an item to the TOC
+     *
+     * @param string $id       the hash link
+     * @param string $text     the text to display
+     * @param int    $level    the nesting level
+     */
+    function toc_additem($id, $text, $level) {
+    }
+
+    /**
+     * Render a heading
+     *
+     * @param string $text  the text to display
+     * @param int    $level header level
+     * @param int    $pos   byte position in the original source
+     */
+    function header($text, $level, $pos) {
+    }
+
+    /**
+     * Open a new section
+     *
+     * @param int $level section level (as determined by the previous header)
+     */
+    function section_open($level) {
+    }
+
+    /**
+     * Close the current section
+     */
+    function section_close() {
+    }
+
+    /**
+     * Render plain text data
+     *
+     * @param string $text
+     */
+    function cdata($text) {
+    }
+
+    /**
+     * Open a paragraph
+     */
+    function p_open() {
+    }
+
+    /**
+     * Close a paragraph
+     */
+    function p_close() {
+    }
+
+    /**
+     * Create a line break
+     */
+    function linebreak() {
+    }
+
+    /**
+     * Create a horizontal line
+     */
+    function hr() {
+    }
+
+    /**
+     * Start strong (bold) formatting
+     */
+    function strong_open() {
+    }
+
+    /**
+     * Stop strong (bold) formatting
+     */
+    function strong_close() {
+    }
+
+    /**
+     * Start emphasis (italics) formatting
+     */
+    function emphasis_open() {
+    }
+
+    /**
+     * Stop emphasis (italics) formatting
+     */
+    function emphasis_close() {
+    }
+
+    /**
+     * Start underline formatting
+     */
+    function underline_open() {
+    }
+
+    /**
+     * Stop underline formatting
+     */
+    function underline_close() {
+    }
+
+    /**
+     * Start monospace formatting
+     */
+    function monospace_open() {
+    }
+
+    /**
+     * Stop monospace formatting
+     */
+    function monospace_close() {
+    }
+
+    /**
+     * Start a subscript
+     */
+    function subscript_open() {
+    }
+
+    /**
+     * Stop a subscript
+     */
+    function subscript_close() {
+    }
+
+    /**
+     * Start a superscript
+     */
+    function superscript_open() {
+    }
+
+    /**
+     * Stop a superscript
+     */
+    function superscript_close() {
+    }
+
+    /**
+     * Start deleted (strike-through) formatting
+     */
+    function deleted_open() {
+    }
+
+    /**
+     * Stop deleted (strike-through) formatting
+     */
+    function deleted_close() {
+    }
+
+    /**
+     * Start a footnote
+     */
+    function footnote_open() {
+    }
+
+    /**
+     * Stop a footnote
+     */
+    function footnote_close() {
+    }
+
+    /**
+     * Open an unordered list
+     */
+    function listu_open() {
+    }
+
+    /**
+     * Close an unordered list
+     */
+    function listu_close() {
+    }
+
+    /**
+     * Open an ordered list
+     */
+    function listo_open() {
+    }
+
+    /**
+     * Close an ordered list
+     */
+    function listo_close() {
+    }
+
+    /**
+     * Open a list item
+     *
+     * @param int $level the nesting level
+     * @param bool $node true when a node; false when a leaf
+     */
+    function listitem_open($level,$node=false) {
+    }
+
+    /**
+     * Close a list item
+     */
+    function listitem_close() {
+    }
+
+    /**
+     * Start the content of a list item
+     */
+    function listcontent_open() {
+    }
+
+    /**
+     * Stop the content of a list item
+     */
+    function listcontent_close() {
+    }
+
+    /**
+     * Output unformatted $text
+     *
+     * Defaults to $this->cdata()
+     *
+     * @param string $text
+     */
+    function unformatted($text) {
+        $this->cdata($text);
+    }
+
+    /**
+     * Output inline PHP code
+     *
+     * If $conf['phpok'] is true this should evaluate the given code and append the result
+     * to $doc
+     *
+     * @param string $text The PHP code
+     */
+    function php($text) {
+    }
+
+    /**
+     * Output block level PHP code
+     *
+     * If $conf['phpok'] is true this should evaluate the given code and append the result
+     * to $doc
+     *
+     * @param string $text The PHP code
+     */
+    function phpblock($text) {
+    }
+
+    /**
+     * Output raw inline HTML
+     *
+     * If $conf['htmlok'] is true this should add the code as is to $doc
+     *
+     * @param string $text The HTML
+     */
+    function html($text) {
+    }
+
+    /**
+     * Output raw block-level HTML
+     *
+     * If $conf['htmlok'] is true this should add the code as is to $doc
+     *
+     * @param string $text The HTML
+     */
+    function htmlblock($text) {
+    }
+
+    /**
+     * Output preformatted text
+     *
+     * @param string $text
+     */
+    function preformatted($text) {
+    }
+
+    /**
+     * Start a block quote
+     */
+    function quote_open() {
+    }
+
+    /**
+     * Stop a block quote
+     */
+    function quote_close() {
+    }
+
+    /**
+     * Display text as file content, optionally syntax highlighted
+     *
+     * @param string $text text to show
+     * @param string $lang programming language to use for syntax highlighting
+     * @param string $file file path label
+     */
+    function file($text, $lang = null, $file = null) {
+    }
+
+    /**
+     * Display text as code content, optionally syntax highlighted
+     *
+     * @param string $text text to show
+     * @param string $lang programming language to use for syntax highlighting
+     * @param string $file file path label
+     */
+    function code($text, $lang = null, $file = null) {
+    }
+
+    /**
+     * Format an acronym
+     *
+     * Uses $this->acronyms
+     *
+     * @param string $acronym
+     */
+    function acronym($acronym) {
+    }
+
+    /**
+     * Format a smiley
+     *
+     * Uses $this->smiley
+     *
+     * @param string $smiley
+     */
+    function smiley($smiley) {
+    }
+
+    /**
+     * Format an entity
+     *
+     * Entities are basically small text replacements
+     *
+     * Uses $this->entities
+     *
+     * @param string $entity
+     */
+    function entity($entity) {
+    }
+
+    /**
+     * Typographically format a multiply sign
+     *
+     * Example: ($x=640, $y=480) should result in "640×480"
+     *
+     * @param string|int $x first value
+     * @param string|int $y second value
+     */
+    function multiplyentity($x, $y) {
+    }
+
+    /**
+     * Render an opening single quote char (language specific)
+     */
+    function singlequoteopening() {
+    }
+
+    /**
+     * Render a closing single quote char (language specific)
+     */
+    function singlequoteclosing() {
+    }
+
+    /**
+     * Render an apostrophe char (language specific)
+     */
+    function apostrophe() {
+    }
+
+    /**
+     * Render an opening double quote char (language specific)
+     */
+    function doublequoteopening() {
+    }
+
+    /**
+     * Render an closinging double quote char (language specific)
+     */
+    function doublequoteclosing() {
+    }
+
+    /**
+     * Render a CamelCase link
+     *
+     * @param string $link The link name
+     * @see http://en.wikipedia.org/wiki/CamelCase
+     */
+    function camelcaselink($link) {
+    }
+
+    /**
+     * Render a page local link
+     *
+     * @param string $hash hash link identifier
+     * @param string $name name for the link
+     */
+    function locallink($hash, $name = null) {
+    }
+
+    /**
+     * Render a wiki internal link
+     *
+     * @param string       $link  page ID to link to. eg. 'wiki:syntax'
+     * @param string|array $title name for the link, array for media file
+     */
+    function internallink($link, $title = null) {
+    }
+
+    /**
+     * Render an external link
+     *
+     * @param string       $link  full URL with scheme
+     * @param string|array $title name for the link, array for media file
+     */
+    function externallink($link, $title = null) {
+    }
+
+    /**
+     * Render the output of an RSS feed
+     *
+     * @param string $url    URL of the feed
+     * @param array  $params Finetuning of the output
+     */
+    function rss($url, $params) {
+    }
+
+    /**
+     * Render an interwiki link
+     *
+     * You may want to use $this->_resolveInterWiki() here
+     *
+     * @param string       $link     original link - probably not much use
+     * @param string|array $title    name for the link, array for media file
+     * @param string       $wikiName indentifier (shortcut) for the remote wiki
+     * @param string       $wikiUri  the fragment parsed from the original link
+     */
+    function interwikilink($link, $title = null, $wikiName, $wikiUri) {
+    }
+
+    /**
+     * Link to file on users OS
+     *
+     * @param string       $link  the link
+     * @param string|array $title name for the link, array for media file
+     */
+    function filelink($link, $title = null) {
+    }
+
+    /**
+     * Link to windows share
+     *
+     * @param string       $link  the link
+     * @param string|array $title name for the link, array for media file
+     */
+    function windowssharelink($link, $title = null) {
+    }
+
+    /**
+     * Render a linked E-Mail Address
+     *
+     * Should honor $conf['mailguard'] setting
+     *
+     * @param string $address Email-Address
+     * @param string|array $name name for the link, array for media file
+     */
+    function emaillink($address, $name = null) {
+    }
+
+    /**
+     * Render an internal media file
+     *
+     * @param string $src     media ID
+     * @param string $title   descriptive text
+     * @param string $align   left|center|right
+     * @param int    $width   width of media in pixel
+     * @param int    $height  height of media in pixel
+     * @param string $cache   cache|recache|nocache
+     * @param string $linking linkonly|detail|nolink
+     */
+    function internalmedia($src, $title = null, $align = null, $width = null,
+                           $height = null, $cache = null, $linking = null) {
+    }
+
+    /**
+     * Render an external media file
+     *
+     * @param string $src     full media URL
+     * @param string $title   descriptive text
+     * @param string $align   left|center|right
+     * @param int    $width   width of media in pixel
+     * @param int    $height  height of media in pixel
+     * @param string $cache   cache|recache|nocache
+     * @param string $linking linkonly|detail|nolink
+     */
+    function externalmedia($src, $title = null, $align = null, $width = null,
+                           $height = null, $cache = null, $linking = null) {
+    }
+
+    /**
+     * Render a link to an internal media file
+     *
+     * @param string $src     media ID
+     * @param string $title   descriptive text
+     * @param string $align   left|center|right
+     * @param int    $width   width of media in pixel
+     * @param int    $height  height of media in pixel
+     * @param string $cache   cache|recache|nocache
+     */
+    function internalmedialink($src, $title = null, $align = null,
+                               $width = null, $height = null, $cache = null) {
+    }
+
+    /**
+     * Render a link to an external media file
+     *
+     * @param string $src     media ID
+     * @param string $title   descriptive text
+     * @param string $align   left|center|right
+     * @param int    $width   width of media in pixel
+     * @param int    $height  height of media in pixel
+     * @param string $cache   cache|recache|nocache
+     */
+    function externalmedialink($src, $title = null, $align = null,
+                               $width = null, $height = null, $cache = null) {
+    }
+
+    /**
+     * Start a table
+     *
+     * @param int $maxcols maximum number of columns
+     * @param int $numrows NOT IMPLEMENTED
+     * @param int $pos     byte position in the original source
+     */
+    function table_open($maxcols = null, $numrows = null, $pos = null) {
+    }
+
+    /**
+     * Close a table
+     *
+     * @param int $pos byte position in the original source
+     */
+    function table_close($pos = null) {
+    }
+
+    /**
+     * Open a table header
+     */
+    function tablethead_open() {
+    }
+
+    /**
+     * Close a table header
+     */
+    function tablethead_close() {
+    }
+
+    /**
+     * Open a table body
+     */
+    function tabletbody_open() {
+    }
+
+    /**
+     * Close a table body
+     */
+    function tabletbody_close() {
+    }
+
+    /**
+     * Open a table footer
+     */
+    function tabletfoot_open() {
+    }
+
+    /**
+     * Close a table footer
+     */
+    function tabletfoot_close() {
+    }
+
+    /**
+     * Open a table row
+     */
+    function tablerow_open() {
+    }
+
+    /**
+     * Close a table row
+     */
+    function tablerow_close() {
+    }
+
+    /**
+     * Open a table header cell
+     *
+     * @param int    $colspan
+     * @param string $align left|center|right
+     * @param int    $rowspan
+     */
+    function tableheader_open($colspan = 1, $align = null, $rowspan = 1) {
+    }
+
+    /**
+     * Close a table header cell
+     */
+    function tableheader_close() {
+    }
+
+    /**
+     * Open a table cell
+     *
+     * @param int    $colspan
+     * @param string $align left|center|right
+     * @param int    $rowspan
+     */
+    function tablecell_open($colspan = 1, $align = null, $rowspan = 1) {
+    }
+
+    /**
+     * Close a table cell
+     */
+    function tablecell_close() {
+    }
+
+    #endregion
+
+    #region util functions, you probably won't need to reimplement them
+
+    /**
+     * Removes any Namespace from the given name but keeps
+     * casing and special chars
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     *
+     * @param string $name
+     * @return string
+     */
+    function _simpleTitle($name) {
+        global $conf;
+
+        //if there is a hash we use the ancor name only
+        @list($name, $hash) = explode('#', $name, 2);
+        if($hash) return $hash;
+
+        if($conf['useslash']) {
+            $name = strtr($name, ';/', ';:');
+        } else {
+            $name = strtr($name, ';', ':');
+        }
+
+        return noNSorNS($name);
+    }
+
+    /**
+     * Resolve an interwikilink
+     *
+     * @param string    $shortcut  identifier for the interwiki link
+     * @param string    $reference fragment that refers the content
+     * @param null|bool $exists    reference which returns if an internal page exists
+     * @return string interwikilink
+     */
+    function _resolveInterWiki(&$shortcut, $reference, &$exists = null) {
+        //get interwiki URL
+        if(isset($this->interwiki[$shortcut])) {
+            $url = $this->interwiki[$shortcut];
+        } else {
+            // Default to Google I'm feeling lucky
+            $url      = 'https://www.google.com/search?q={URL}&amp;btnI=lucky';
+            $shortcut = 'go';
+        }
+
+        //split into hash and url part
+        $hash = strrchr($reference, '#');
+        if($hash) {
+            $reference = substr($reference, 0, -strlen($hash));
+            $hash = substr($hash, 1);
+        }
+
+        //replace placeholder
+        if(preg_match('#\{(URL|NAME|SCHEME|HOST|PORT|PATH|QUERY)\}#', $url)) {
+            //use placeholders
+            $url    = str_replace('{URL}', rawurlencode($reference), $url);
+            //wiki names will be cleaned next, otherwise urlencode unsafe chars
+            $url    = str_replace('{NAME}', ($url{0} === ':') ? $reference :
+                                  preg_replace_callback('/[[\\\\\]^`{|}#%]/', function($match) {
+                                    return rawurlencode($match[0]);
+                                  }, $reference), $url);
+            $parsed = parse_url($reference);
+            if (empty($parsed['scheme'])) $parsed['scheme'] = '';
+            if (empty($parsed['host'])) $parsed['host'] = '';
+            if (empty($parsed['port'])) $parsed['port'] = 80;
+            if (empty($parsed['path'])) $parsed['path'] = '';
+            if (empty($parsed['query'])) $parsed['query'] = '';
+            $url = strtr($url,[
+                '{SCHEME}' => $parsed['scheme'],
+                '{HOST}' => $parsed['host'],
+                '{PORT}' => $parsed['port'],
+                '{PATH}' => $parsed['path'],
+                '{QUERY}' => $parsed['query'] ,
+            ]);
+        } else {
+            //default
+            $url = $url.rawurlencode($reference);
+        }
+        //handle as wiki links
+        if($url{0} === ':') {
+            $urlparam = null;
+            $id = $url;
+            if (strpos($url, '?') !== false) {
+                list($id, $urlparam) = explode('?', $url, 2);
+            }
+            $url    = wl(cleanID($id), $urlparam);
+            $exists = page_exists($id);
+        }
+        if($hash) $url .= '#'.rawurlencode($hash);
+
+        return $url;
+    }
+
+    #endregion
+}
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/wiki/inc/parser/xhtml.php b/wiki/inc/parser/xhtml.php
new file mode 100644
index 0000000..a3e7b45
--- /dev/null
+++ b/wiki/inc/parser/xhtml.php
@@ -0,0 +1,1970 @@
+<?php
+/**
+ * Renderer for XHTML output
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+if(!defined('DOKU_INC')) die('meh.');
+
+if(!defined('DOKU_LF')) {
+    // Some whitespace to help View > Source
+    define ('DOKU_LF', "\n");
+}
+
+if(!defined('DOKU_TAB')) {
+    // Some whitespace to help View > Source
+    define ('DOKU_TAB', "\t");
+}
+
+/**
+ * The XHTML Renderer
+ *
+ * This is DokuWiki's main renderer used to display page content in the wiki
+ */
+class Doku_Renderer_xhtml extends Doku_Renderer {
+    /** @var array store the table of contents */
+    public $toc = array();
+
+    /** @var array A stack of section edit data */
+    protected $sectionedits = array();
+    var $date_at = '';    // link pages and media against this revision
+
+    /** @var int last section edit id, used by startSectionEdit */
+    protected $lastsecid = 0;
+
+    /** @var array the list of headers used to create unique link ids */
+    protected $headers = array();
+
+    /** @var array a list of footnotes, list starts at 1! */
+    protected $footnotes = array();
+
+    /** @var int current section level */
+    protected $lastlevel = 0;
+    /** @var array section node tracker */
+    protected $node = array(0, 0, 0, 0, 0);
+
+    /** @var string temporary $doc store */
+    protected $store = '';
+
+    /** @var array global counter, for table classes etc. */
+    protected $_counter = array(); //
+
+    /** @var int counts the code and file blocks, used to provide download links */
+    protected $_codeblock = 0;
+
+    /** @var array list of allowed URL schemes */
+    protected $schemes = null;
+
+    /**
+     * Register a new edit section range
+     *
+     * @param int    $start  The byte position for the edit start
+     * @param array  $data   Associative array with section data:
+     *                       Key 'name': the section name/title
+     *                       Key 'target': the target for the section edit,
+     *                                     e.g. 'section' or 'table'
+     *                       Key 'hid': header id
+     *                       Key 'codeblockOffset': actual code block index
+     *                       Key 'start': set in startSectionEdit(),
+     *                                    do not set yourself
+     *                       Key 'range': calculated from 'start' and
+     *                                    $key in finishSectionEdit(),
+     *                                    do not set yourself
+     * @return string  A marker class for the starting HTML element
+     *
+     * @author Adrian Lang <lang@cosmocode.de>
+     */
+    public function startSectionEdit($start, $data) {
+        if (!is_array($data)) {
+            msg(
+                sprintf(
+                    'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.',
+                    hsc((string) $data)
+                ), -1
+            );
+
+            // @deprecated 2018-04-14, backward compatibility
+            $args = func_get_args();
+            $data = array();
+            if(isset($args[1])) $data['target'] = $args[1];
+            if(isset($args[2])) $data['name'] = $args[2];
+            if(isset($args[3])) $data['hid'] = $args[3];
+        }
+        $data['secid'] = ++$this->lastsecid;
+        $data['start'] = $start;
+        $this->sectionedits[] = $data;
+        return 'sectionedit'.$data['secid'];
+    }
+
+    /**
+     * Finish an edit section range
+     *
+     * @param int  $end     The byte position for the edit end; null for the rest of the page
+     *
+     * @author Adrian Lang <lang@cosmocode.de>
+     */
+    public function finishSectionEdit($end = null, $hid = null) {
+        $data = array_pop($this->sectionedits);
+        if(!is_null($end) && $end <= $data['start']) {
+            return;
+        }
+        if(!is_null($hid)) {
+            $data['hid'] .= $hid;
+        }
+        $data['range'] = $data['start'].'-'.(is_null($end) ? '' : $end);
+        unset($data['start']);
+        $this->doc .= '<!-- EDIT'.hsc(json_encode ($data)).' -->';
+    }
+
+    /**
+     * Returns the format produced by this renderer.
+     *
+     * @return string always 'xhtml'
+     */
+    function getFormat() {
+        return 'xhtml';
+    }
+
+    /**
+     * Initialize the document
+     */
+    function document_start() {
+        //reset some internals
+        $this->toc     = array();
+        $this->headers = array();
+    }
+
+    /**
+     * Finalize the document
+     */
+    function document_end() {
+        // Finish open section edits.
+        while(count($this->sectionedits) > 0) {
+            if($this->sectionedits[count($this->sectionedits) - 1]['start'] <= 1) {
+                // If there is only one section, do not write a section edit
+                // marker.
+                array_pop($this->sectionedits);
+            } else {
+                $this->finishSectionEdit();
+            }
+        }
+
+        if(count($this->footnotes) > 0) {
+            $this->doc .= '<div class="footnotes">'.DOKU_LF;
+
+            foreach($this->footnotes as $id => $footnote) {
+                // check its not a placeholder that indicates actual footnote text is elsewhere
+                if(substr($footnote, 0, 5) != "@@FNT") {
+
+                    // open the footnote and set the anchor and backlink
+                    $this->doc .= '<div class="fn">';
+                    $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">';
+                    $this->doc .= $id.')</a></sup> '.DOKU_LF;
+
+                    // get any other footnotes that use the same markup
+                    $alt = array_keys($this->footnotes, "@@FNT$id");
+
+                    if(count($alt)) {
+                        foreach($alt as $ref) {
+                            // set anchor and backlink for the other footnotes
+                            $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">';
+                            $this->doc .= ($ref).')</a></sup> '.DOKU_LF;
+                        }
+                    }
+
+                    // add footnote markup and close this footnote
+                    $this->doc .= '<div class="content">'.$footnote.'</div>';
+                    $this->doc .= '</div>'.DOKU_LF;
+                }
+            }
+            $this->doc .= '</div>'.DOKU_LF;
+        }
+
+        // Prepare the TOC
+        global $conf;
+        if($this->info['toc'] && is_array($this->toc) && $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']) {
+            global $TOC;
+            $TOC = $this->toc;
+        }
+
+        // make sure there are no empty paragraphs
+        $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc);
+    }
+
+    /**
+     * Add an item to the TOC
+     *
+     * @param string $id       the hash link
+     * @param string $text     the text to display
+     * @param int    $level    the nesting level
+     */
+    function toc_additem($id, $text, $level) {
+        global $conf;
+
+        //handle TOC
+        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
+            $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1);
+        }
+    }
+
+    /**
+     * Render a heading
+     *
+     * @param string $text  the text to display
+     * @param int    $level header level
+     * @param int    $pos   byte position in the original source
+     */
+    function header($text, $level, $pos) {
+        global $conf;
+
+        if(blank($text)) return; //skip empty headlines
+
+        $hid = $this->_headerToLink($text, true);
+
+        //only add items within configured levels
+        $this->toc_additem($hid, $text, $level);
+
+        // adjust $node to reflect hierarchy of levels
+        $this->node[$level - 1]++;
+        if($level < $this->lastlevel) {
+            for($i = 0; $i < $this->lastlevel - $level; $i++) {
+                $this->node[$this->lastlevel - $i - 1] = 0;
+            }
+        }
+        $this->lastlevel = $level;
+
+        if($level <= $conf['maxseclevel'] &&
+            count($this->sectionedits) > 0 &&
+            $this->sectionedits[count($this->sectionedits) - 1]['target'] === 'section'
+        ) {
+            $this->finishSectionEdit($pos - 1);
+        }
+
+        // write the header
+        $this->doc .= DOKU_LF.'<h'.$level;
+        if($level <= $conf['maxseclevel']) {
+            $data = array();
+            $data['target'] = 'section';
+            $data['name'] = $text;
+            $data['hid'] = $hid;
+            $data['codeblockOffset'] = $this->_codeblock;
+            $this->doc .= ' class="'.$this->startSectionEdit($pos, $data).'"';
+        }
+        $this->doc .= ' id="'.$hid.'">';
+        $this->doc .= $this->_xmlEntities($text);
+        $this->doc .= "</h$level>".DOKU_LF;
+    }
+
+    /**
+     * Open a new section
+     *
+     * @param int $level section level (as determined by the previous header)
+     */
+    function section_open($level) {
+        $this->doc .= '<div class="level'.$level.'">'.DOKU_LF;
+    }
+
+    /**
+     * Close the current section
+     */
+    function section_close() {
+        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
+    }
+
+    /**
+     * Render plain text data
+     *
+     * @param $text
+     */
+    function cdata($text) {
+        $this->doc .= $this->_xmlEntities($text);
+    }
+
+    /**
+     * Open a paragraph
+     */
+    function p_open() {
+        $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
+    }
+
+    /**
+     * Close a paragraph
+     */
+    function p_close() {
+        $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
+    }
+
+    /**
+     * Create a line break
+     */
+    function linebreak() {
+        $this->doc .= '<br/>'.DOKU_LF;
+    }
+
+    /**
+     * Create a horizontal line
+     */
+    function hr() {
+        $this->doc .= '<hr />'.DOKU_LF;
+    }
+
+    /**
+     * Start strong (bold) formatting
+     */
+    function strong_open() {
+        $this->doc .= '<strong>';
+    }
+
+    /**
+     * Stop strong (bold) formatting
+     */
+    function strong_close() {
+        $this->doc .= '</strong>';
+    }
+
+    /**
+     * Start emphasis (italics) formatting
+     */
+    function emphasis_open() {
+        $this->doc .= '<em>';
+    }
+
+    /**
+     * Stop emphasis (italics) formatting
+     */
+    function emphasis_close() {
+        $this->doc .= '</em>';
+    }
+
+    /**
+     * Start underline formatting
+     */
+    function underline_open() {
+        $this->doc .= '<em class="u">';
+    }
+
+    /**
+     * Stop underline formatting
+     */
+    function underline_close() {
+        $this->doc .= '</em>';
+    }
+
+    /**
+     * Start monospace formatting
+     */
+    function monospace_open() {
+        $this->doc .= '<code>';
+    }
+
+    /**
+     * Stop monospace formatting
+     */
+    function monospace_close() {
+        $this->doc .= '</code>';
+    }
+
+    /**
+     * Start a subscript
+     */
+    function subscript_open() {
+        $this->doc .= '<sub>';
+    }
+
+    /**
+     * Stop a subscript
+     */
+    function subscript_close() {
+        $this->doc .= '</sub>';
+    }
+
+    /**
+     * Start a superscript
+     */
+    function superscript_open() {
+        $this->doc .= '<sup>';
+    }
+
+    /**
+     * Stop a superscript
+     */
+    function superscript_close() {
+        $this->doc .= '</sup>';
+    }
+
+    /**
+     * Start deleted (strike-through) formatting
+     */
+    function deleted_open() {
+        $this->doc .= '<del>';
+    }
+
+    /**
+     * Stop deleted (strike-through) formatting
+     */
+    function deleted_close() {
+        $this->doc .= '</del>';
+    }
+
+    /**
+     * Callback for footnote start syntax
+     *
+     * All following content will go to the footnote instead of
+     * the document. To achieve this the previous rendered content
+     * is moved to $store and $doc is cleared
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     */
+    function footnote_open() {
+
+        // move current content to store and record footnote
+        $this->store = $this->doc;
+        $this->doc   = '';
+    }
+
+    /**
+     * Callback for footnote end syntax
+     *
+     * All rendered content is moved to the $footnotes array and the old
+     * content is restored from $store again
+     *
+     * @author Andreas Gohr
+     */
+    function footnote_close() {
+        /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
+        static $fnid = 0;
+        // assign new footnote id (we start at 1)
+        $fnid++;
+
+        // recover footnote into the stack and restore old content
+        $footnote    = $this->doc;
+        $this->doc   = $this->store;
+        $this->store = '';
+
+        // check to see if this footnote has been seen before
+        $i = array_search($footnote, $this->footnotes);
+
+        if($i === false) {
+            // its a new footnote, add it to the $footnotes array
+            $this->footnotes[$fnid] = $footnote;
+        } else {
+            // seen this one before, save a placeholder
+            $this->footnotes[$fnid] = "@@FNT".($i);
+        }
+
+        // output the footnote reference and link
+        $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>';
+    }
+
+    /**
+     * Open an unordered list
+     *
+     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+     */
+    function listu_open($classes = null) {
+        $class = '';
+        if($classes !== null) {
+            if(is_array($classes)) $classes = join(' ', $classes);
+            $class = " class=\"$classes\"";
+        }
+        $this->doc .= "<ul$class>".DOKU_LF;
+    }
+
+    /**
+     * Close an unordered list
+     */
+    function listu_close() {
+        $this->doc .= '</ul>'.DOKU_LF;
+    }
+
+    /**
+     * Open an ordered list
+     *
+     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+     */
+    function listo_open($classes = null) {
+        $class = '';
+        if($classes !== null) {
+            if(is_array($classes)) $classes = join(' ', $classes);
+            $class = " class=\"$classes\"";
+        }
+        $this->doc .= "<ol$class>".DOKU_LF;
+    }
+
+    /**
+     * Close an ordered list
+     */
+    function listo_close() {
+        $this->doc .= '</ol>'.DOKU_LF;
+    }
+
+    /**
+     * Open a list item
+     *
+     * @param int $level the nesting level
+     * @param bool $node true when a node; false when a leaf
+     */
+    function listitem_open($level, $node=false) {
+        $branching = $node ? ' node' : '';
+        $this->doc .= '<li class="level'.$level.$branching.'">';
+    }
+
+    /**
+     * Close a list item
+     */
+    function listitem_close() {
+        $this->doc .= '</li>'.DOKU_LF;
+    }
+
+    /**
+     * Start the content of a list item
+     */
+    function listcontent_open() {
+        $this->doc .= '<div class="li">';
+    }
+
+    /**
+     * Stop the content of a list item
+     */
+    function listcontent_close() {
+        $this->doc .= '</div>'.DOKU_LF;
+    }
+
+    /**
+     * Output unformatted $text
+     *
+     * Defaults to $this->cdata()
+     *
+     * @param string $text
+     */
+    function unformatted($text) {
+        $this->doc .= $this->_xmlEntities($text);
+    }
+
+    /**
+     * Execute PHP code if allowed
+     *
+     * @param  string $text      PHP code that is either executed or printed
+     * @param  string $wrapper   html element to wrap result if $conf['phpok'] is okff
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     */
+    function php($text, $wrapper = 'code') {
+        global $conf;
+
+        if($conf['phpok']) {
+            ob_start();
+            eval($text);
+            $this->doc .= ob_get_contents();
+            ob_end_clean();
+        } else {
+            $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper);
+        }
+    }
+
+    /**
+     * Output block level PHP code
+     *
+     * If $conf['phpok'] is true this should evaluate the given code and append the result
+     * to $doc
+     *
+     * @param string $text The PHP code
+     */
+    function phpblock($text) {
+        $this->php($text, 'pre');
+    }
+
+    /**
+     * Insert HTML if allowed
+     *
+     * @param  string $text      html text
+     * @param  string $wrapper   html element to wrap result if $conf['htmlok'] is okff
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     */
+    function html($text, $wrapper = 'code') {
+        global $conf;
+
+        if($conf['htmlok']) {
+            $this->doc .= $text;
+        } else {
+            $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper);
+        }
+    }
+
+    /**
+     * Output raw block-level HTML
+     *
+     * If $conf['htmlok'] is true this should add the code as is to $doc
+     *
+     * @param string $text The HTML
+     */
+    function htmlblock($text) {
+        $this->html($text, 'pre');
+    }
+
+    /**
+     * Start a block quote
+     */
+    function quote_open() {
+        $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
+    }
+
+    /**
+     * Stop a block quote
+     */
+    function quote_close() {
+        $this->doc .= '</div></blockquote>'.DOKU_LF;
+    }
+
+    /**
+     * Output preformatted text
+     *
+     * @param string $text
+     */
+    function preformatted($text) {
+        $this->doc .= '<pre class="code">'.trim($this->_xmlEntities($text), "\n\r").'</pre>'.DOKU_LF;
+    }
+
+    /**
+     * Display text as file content, optionally syntax highlighted
+     *
+     * @param string $text     text to show
+     * @param string $language programming language to use for syntax highlighting
+     * @param string $filename file path label
+     * @param array  $options  assoziative array with additional geshi options
+     */
+    function file($text, $language = null, $filename = null, $options=null) {
+        $this->_highlight('file', $text, $language, $filename, $options);
+    }
+
+    /**
+     * Display text as code content, optionally syntax highlighted
+     *
+     * @param string $text     text to show
+     * @param string $language programming language to use for syntax highlighting
+     * @param string $filename file path label
+     * @param array  $options  assoziative array with additional geshi options
+     */
+    function code($text, $language = null, $filename = null, $options=null) {
+        $this->_highlight('code', $text, $language, $filename, $options);
+    }
+
+    /**
+     * Use GeSHi to highlight language syntax in code and file blocks
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     * @param string $type     code|file
+     * @param string $text     text to show
+     * @param string $language programming language to use for syntax highlighting
+     * @param string $filename file path label
+     * @param array  $options  assoziative array with additional geshi options
+     */
+    function _highlight($type, $text, $language = null, $filename = null, $options = null) {
+        global $ID;
+        global $lang;
+        global $INPUT;
+
+        $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
+
+        $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
+
+        if($filename) {
+            // add icon
+            list($ext) = mimetype($filename, false);
+            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
+            $class = 'mediafile mf_'.$class;
+
+            $offset = 0;
+            if ($INPUT->has('codeblockOffset')) {
+                $offset = $INPUT->str('codeblockOffset');
+            }
+            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
+            $this->doc .= '<dt><a href="'.exportlink($ID, 'code', array('codeblock' => $offset+$this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">';
+            $this->doc .= hsc($filename);
+            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
+        }
+
+        if($text{0} == "\n") {
+            $text = substr($text, 1);
+        }
+        if(substr($text, -1) == "\n") {
+            $text = substr($text, 0, -1);
+        }
+
+        if(empty($language)) { // empty is faster than is_null and can prevent '' string
+            $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
+        } else {
+            $class = 'code'; //we always need the code class to make the syntax highlighting apply
+            if($type != 'code') $class .= ' '.$type;
+
+            $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '', $options).'</pre>'.DOKU_LF;
+        }
+
+        if($filename) {
+            $this->doc .= '</dd></dl>'.DOKU_LF;
+        }
+
+        $this->_codeblock++;
+    }
+
+    /**
+     * Format an acronym
+     *
+     * Uses $this->acronyms
+     *
+     * @param string $acronym
+     */
+    function acronym($acronym) {
+
+        if(array_key_exists($acronym, $this->acronyms)) {
+
+            $title = $this->_xmlEntities($this->acronyms[$acronym]);
+
+            $this->doc .= '<abbr title="'.$title
+                .'">'.$this->_xmlEntities($acronym).'</abbr>';
+
+        } else {
+            $this->doc .= $this->_xmlEntities($acronym);
+        }
+    }
+
+    /**
+     * Format a smiley
+     *
+     * Uses $this->smiley
+     *
+     * @param string $smiley
+     */
+    function smiley($smiley) {
+        if(array_key_exists($smiley, $this->smileys)) {
+            $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley].
+                '" class="icon" alt="'.
+                $this->_xmlEntities($smiley).'" />';
+        } else {
+            $this->doc .= $this->_xmlEntities($smiley);
+        }
+    }
+
+    /**
+     * Format an entity
+     *
+     * Entities are basically small text replacements
+     *
+     * Uses $this->entities
+     *
+     * @param string $entity
+     */
+    function entity($entity) {
+        if(array_key_exists($entity, $this->entities)) {
+            $this->doc .= $this->entities[$entity];
+        } else {
+            $this->doc .= $this->_xmlEntities($entity);
+        }
+    }
+
+    /**
+     * Typographically format a multiply sign
+     *
+     * Example: ($x=640, $y=480) should result in "640×480"
+     *
+     * @param string|int $x first value
+     * @param string|int $y second value
+     */
+    function multiplyentity($x, $y) {
+        $this->doc .= "$x&times;$y";
+    }
+
+    /**
+     * Render an opening single quote char (language specific)
+     */
+    function singlequoteopening() {
+        global $lang;
+        $this->doc .= $lang['singlequoteopening'];
+    }
+
+    /**
+     * Render a closing single quote char (language specific)
+     */
+    function singlequoteclosing() {
+        global $lang;
+        $this->doc .= $lang['singlequoteclosing'];
+    }
+
+    /**
+     * Render an apostrophe char (language specific)
+     */
+    function apostrophe() {
+        global $lang;
+        $this->doc .= $lang['apostrophe'];
+    }
+
+    /**
+     * Render an opening double quote char (language specific)
+     */
+    function doublequoteopening() {
+        global $lang;
+        $this->doc .= $lang['doublequoteopening'];
+    }
+
+    /**
+     * Render an closinging double quote char (language specific)
+     */
+    function doublequoteclosing() {
+        global $lang;
+        $this->doc .= $lang['doublequoteclosing'];
+    }
+
+    /**
+     * Render a CamelCase link
+     *
+     * @param string $link       The link name
+     * @param bool   $returnonly whether to return html or write to doc attribute
+     * @return void|string writes to doc attribute or returns html depends on $returnonly
+     *
+     * @see http://en.wikipedia.org/wiki/CamelCase
+     */
+    function camelcaselink($link, $returnonly = false) {
+        if($returnonly) {
+          return $this->internallink($link, $link, null, true);
+        } else {
+          $this->internallink($link, $link);
+        }
+    }
+
+    /**
+     * Render a page local link
+     *
+     * @param string $hash       hash link identifier
+     * @param string $name       name for the link
+     * @param bool   $returnonly whether to return html or write to doc attribute
+     * @return void|string writes to doc attribute or returns html depends on $returnonly
+     */
+    function locallink($hash, $name = null, $returnonly = false) {
+        global $ID;
+        $name  = $this->_getLinkTitle($name, $hash, $isImage);
+        $hash  = $this->_headerToLink($hash);
+        $title = $ID.' ↵';
+
+        $doc = '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
+        $doc .= $name;
+        $doc .= '</a>';
+
+        if($returnonly) {
+          return $doc;
+        } else {
+          $this->doc .= $doc;
+        }
+    }
+
+    /**
+     * Render an internal Wiki Link
+     *
+     * $search,$returnonly & $linktype are not for the renderer but are used
+     * elsewhere - no need to implement them in other renderers
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     * @param string      $id         pageid
+     * @param string|null $name       link name
+     * @param string|null $search     adds search url param
+     * @param bool        $returnonly whether to return html or write to doc attribute
+     * @param string      $linktype   type to set use of headings
+     * @return void|string writes to doc attribute or returns html depends on $returnonly
+     */
+    function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') {
+        global $conf;
+        global $ID;
+        global $INFO;
+
+        $params = '';
+        $parts  = explode('?', $id, 2);
+        if(count($parts) === 2) {
+            $id     = $parts[0];
+            $params = $parts[1];
+        }
+
+        // For empty $id we need to know the current $ID
+        // We need this check because _simpleTitle needs
+        // correct $id and resolve_pageid() use cleanID($id)
+        // (some things could be lost)
+        if($id === '') {
+            $id = $ID;
+        }
+
+        // default name is based on $id as given
+        $default = $this->_simpleTitle($id);
+
+        // now first resolve and clean up the $id
+        resolve_pageid(getNS($ID), $id, $exists, $this->date_at, true);
+
+        $link = array();
+        $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
+        if(!$isImage) {
+            if($exists) {
+                $class = 'wikilink1';
+            } else {
+                $class       = 'wikilink2';
+                $link['rel'] = 'nofollow';
+            }
+        } else {
+            $class = 'media';
+        }
+
+        //keep hash anchor
+        @list($id, $hash) = explode('#', $id, 2);
+        if(!empty($hash)) $hash = $this->_headerToLink($hash);
+
+        //prepare for formating
+        $link['target'] = $conf['target']['wiki'];
+        $link['style']  = '';
+        $link['pre']    = '';
+        $link['suf']    = '';
+        // highlight link to current page
+        if($id == $INFO['id']) {
+            $link['pre'] = '<span class="curid">';
+            $link['suf'] = '</span>';
+        }
+        $link['more']   = '';
+        $link['class']  = $class;
+        if($this->date_at) {
+            $params = $params.'&at='.rawurlencode($this->date_at);
+        }
+        $link['url']    = wl($id, $params);
+        $link['name']   = $name;
+        $link['title']  = $id;
+        //add search string
+        if($search) {
+            ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
+            if(is_array($search)) {
+                $search = array_map('rawurlencode', $search);
+                $link['url'] .= 's[]='.join('&amp;s[]=', $search);
+            } else {
+                $link['url'] .= 's='.rawurlencode($search);
+            }
+        }
+
+        //keep hash
+        if($hash) $link['url'] .= '#'.$hash;
+
+        //output formatted
+        if($returnonly) {
+            return $this->_formatLink($link);
+        } else {
+            $this->doc .= $this->_formatLink($link);
+        }
+    }
+
+    /**
+     * Render an external link
+     *
+     * @param string       $url        full URL with scheme
+     * @param string|array $name       name for the link, array for media file
+     * @param bool         $returnonly whether to return html or write to doc attribute
+     * @return void|string writes to doc attribute or returns html depends on $returnonly
+     */
+    function externallink($url, $name = null, $returnonly = false) {
+        global $conf;
+
+        $name = $this->_getLinkTitle($name, $url, $isImage);
+
+        // url might be an attack vector, only allow registered protocols
+        if(is_null($this->schemes)) $this->schemes = getSchemes();
+        list($scheme) = explode('://', $url);
+        $scheme = strtolower($scheme);
+        if(!in_array($scheme, $this->schemes)) $url = '';
+
+        // is there still an URL?
+        if(!$url) {
+            if($returnonly) {
+                return $name;
+            } else {
+                $this->doc .= $name;
+            }
+            return;
+        }
+
+        // set class
+        if(!$isImage) {
+            $class = 'urlextern';
+        } else {
+            $class = 'media';
+        }
+
+        //prepare for formating
+        $link = array();
+        $link['target'] = $conf['target']['extern'];
+        $link['style']  = '';
+        $link['pre']    = '';
+        $link['suf']    = '';
+        $link['more']   = '';
+        $link['class']  = $class;
+        $link['url']    = $url;
+        $link['rel']    = '';
+
+        $link['name']  = $name;
+        $link['title'] = $this->_xmlEntities($url);
+        if($conf['relnofollow']) $link['rel'] .= ' nofollow';
+        if($conf['target']['extern']) $link['rel'] .= ' noopener';
+
+        //output formatted
+        if($returnonly) {
+            return $this->_formatLink($link);
+        } else {
+            $this->doc .= $this->_formatLink($link);
+        }
+    }
+
+    /**
+     * Render an interwiki link
+     *
+     * You may want to use $this->_resolveInterWiki() here
+     *
+     * @param string       $match      original link - probably not much use
+     * @param string|array $name       name for the link, array for media file
+     * @param string       $wikiName   indentifier (shortcut) for the remote wiki
+     * @param string       $wikiUri    the fragment parsed from the original link
+     * @param bool         $returnonly whether to return html or write to doc attribute
+     * @return void|string writes to doc attribute or returns html depends on $returnonly
+     */
+    function interwikilink($match, $name = null, $wikiName, $wikiUri, $returnonly = false) {
+        global $conf;
+
+        $link           = array();
+        $link['target'] = $conf['target']['interwiki'];
+        $link['pre']    = '';
+        $link['suf']    = '';
+        $link['more']   = '';
+        $link['name']   = $this->_getLinkTitle($name, $wikiUri, $isImage);
+        $link['rel']    = '';
+
+        //get interwiki URL
+        $exists = null;
+        $url    = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
+
+        if(!$isImage) {
+            $class         = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
+            $link['class'] = "interwiki iw_$class";
+        } else {
+            $link['class'] = 'media';
+        }
+
+        //do we stay at the same server? Use local target
+        if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) {
+            $link['target'] = $conf['target']['wiki'];
+        }
+        if($exists !== null && !$isImage) {
+            if($exists) {
+                $link['class'] .= ' wikilink1';
+            } else {
+                $link['class'] .= ' wikilink2';
+                $link['rel'] .= ' nofollow';
+            }
+        }
+        if($conf['target']['interwiki']) $link['rel'] .= ' noopener';
+
+        $link['url']   = $url;
+        $link['title'] = htmlspecialchars($link['url']);
+
+        //output formatted
+        if($returnonly) {
+            return $this->_formatLink($link);
+        } else {
+            $this->doc .= $this->_formatLink($link);
+        }
+    }
+
+    /**
+     * Link to windows share
+     *
+     * @param string       $url        the link
+     * @param string|array $name       name for the link, array for media file
+     * @param bool         $returnonly whether to return html or write to doc attribute
+     * @return void|string writes to doc attribute or returns html depends on $returnonly
+     */
+    function windowssharelink($url, $name = null, $returnonly = false) {
+        global $conf;
+
+        //simple setup
+        $link = array();
+        $link['target'] = $conf['target']['windows'];
+        $link['pre']    = '';
+        $link['suf']    = '';
+        $link['style']  = '';
+
+        $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
+        if(!$isImage) {
+            $link['class'] = 'windows';
+        } else {
+            $link['class'] = 'media';
+        }
+
+        $link['title'] = $this->_xmlEntities($url);
+        $url           = str_replace('\\', '/', $url);
+        $url           = 'file:///'.$url;
+        $link['url']   = $url;
+
+        //output formatted
+        if($returnonly) {
+            return $this->_formatLink($link);
+        } else {
+            $this->doc .= $this->_formatLink($link);
+        }
+    }
+
+    /**
+     * Render a linked E-Mail Address
+     *
+     * Honors $conf['mailguard'] setting
+     *
+     * @param string       $address    Email-Address
+     * @param string|array $name       name for the link, array for media file
+     * @param bool         $returnonly whether to return html or write to doc attribute
+     * @return void|string writes to doc attribute or returns html depends on $returnonly
+     */
+    function emaillink($address, $name = null, $returnonly = false) {
+        global $conf;
+        //simple setup
+        $link           = array();
+        $link['target'] = '';
+        $link['pre']    = '';
+        $link['suf']    = '';
+        $link['style']  = '';
+        $link['more']   = '';
+
+        $name = $this->_getLinkTitle($name, '', $isImage);
+        if(!$isImage) {
+            $link['class'] = 'mail';
+        } else {
+            $link['class'] = 'media';
+        }
+
+        $address = $this->_xmlEntities($address);
+        $address = obfuscate($address);
+        $title   = $address;
+
+        if(empty($name)) {
+            $name = $address;
+        }
+
+        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
+
+        $link['url']   = 'mailto:'.$address;
+        $link['name']  = $name;
+        $link['title'] = $title;
+
+        //output formatted
+        if($returnonly) {
+            return $this->_formatLink($link);
+        } else {
+            $this->doc .= $this->_formatLink($link);
+        }
+    }
+
+    /**
+     * Render an internal media file
+     *
+     * @param string $src       media ID
+     * @param string $title     descriptive text
+     * @param string $align     left|center|right
+     * @param int    $width     width of media in pixel
+     * @param int    $height    height of media in pixel
+     * @param string $cache     cache|recache|nocache
+     * @param string $linking   linkonly|detail|nolink
+     * @param bool   $return    return HTML instead of adding to $doc
+     * @return void|string writes to doc attribute or returns html depends on $return
+     */
+    function internalmedia($src, $title = null, $align = null, $width = null,
+                           $height = null, $cache = null, $linking = null, $return = false) {
+        global $ID;
+        if (strpos($src, '#') !== false) {
+            list($src, $hash) = explode('#', $src, 2);
+        }
+        resolve_mediaid(getNS($ID), $src, $exists, $this->date_at, true);
+
+        $noLink = false;
+        $render = ($linking == 'linkonly') ? false : true;
+        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
+
+        list($ext, $mime) = mimetype($src, false);
+        if(substr($mime, 0, 5) == 'image' && $render) {
+            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src)), ($linking == 'direct'));
+        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
+            // don't link movies
+            $noLink = true;
+        } else {
+            // add file icons
+            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
+            $link['class'] .= ' mediafile mf_'.$class;
+            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache , 'rev'=>$this->_getLastMediaRevisionAt($src)), true);
+            if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')';
+        }
+
+        if (!empty($hash)) $link['url'] .= '#'.$hash;
+
+        //markup non existing files
+        if(!$exists) {
+            $link['class'] .= ' wikilink2';
+        }
+
+        //output formatted
+        if($return) {
+            if($linking == 'nolink' || $noLink) return $link['name'];
+            else return $this->_formatLink($link);
+        } else {
+            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
+            else $this->doc .= $this->_formatLink($link);
+        }
+    }
+
+    /**
+     * Render an external media file
+     *
+     * @param string $src     full media URL
+     * @param string $title   descriptive text
+     * @param string $align   left|center|right
+     * @param int    $width   width of media in pixel
+     * @param int    $height  height of media in pixel
+     * @param string $cache   cache|recache|nocache
+     * @param string $linking linkonly|detail|nolink
+     * @param bool   $return  return HTML instead of adding to $doc
+     * @return void|string writes to doc attribute or returns html depends on $return
+     */
+    function externalmedia($src, $title = null, $align = null, $width = null,
+                           $height = null, $cache = null, $linking = null, $return = false) {
+        if(link_isinterwiki($src)){
+            list($shortcut, $reference) = explode('>', $src, 2);
+            $exists = null;
+            $src = $this->_resolveInterWiki($shortcut, $reference, $exists);
+        }
+        list($src, $hash) = explode('#', $src, 2);
+        $noLink = false;
+        $render = ($linking == 'linkonly') ? false : true;
+        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
+
+        $link['url'] = ml($src, array('cache' => $cache));
+
+        list($ext, $mime) = mimetype($src, false);
+        if(substr($mime, 0, 5) == 'image' && $render) {
+            // link only jpeg images
+            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
+        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
+            // don't link movies
+            $noLink = true;
+        } else {
+            // add file icons
+            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
+            $link['class'] .= ' mediafile mf_'.$class;
+        }
+
+        if($hash) $link['url'] .= '#'.$hash;
+
+        //output formatted
+        if($return) {
+            if($linking == 'nolink' || $noLink) return $link['name'];
+            else return $this->_formatLink($link);
+        } else {
+            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
+            else $this->doc .= $this->_formatLink($link);
+        }
+    }
+
+    /**
+     * Renders an RSS feed
+     *
+     * @param string $url    URL of the feed
+     * @param array  $params Finetuning of the output
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     */
+    function rss($url, $params) {
+        global $lang;
+        global $conf;
+
+        require_once(DOKU_INC.'inc/FeedParser.php');
+        $feed = new FeedParser();
+        $feed->set_feed_url($url);
+
+        //disable warning while fetching
+        if(!defined('DOKU_E_LEVEL')) {
+            $elvl = error_reporting(E_ERROR);
+        }
+        $rc = $feed->init();
+        if(isset($elvl)) {
+            error_reporting($elvl);
+        }
+
+        if($params['nosort']) $feed->enable_order_by_date(false);
+
+        //decide on start and end
+        if($params['reverse']) {
+            $mod   = -1;
+            $start = $feed->get_item_quantity() - 1;
+            $end   = $start - ($params['max']);
+            $end   = ($end < -1) ? -1 : $end;
+        } else {
+            $mod   = 1;
+            $start = 0;
+            $end   = $feed->get_item_quantity();
+            $end   = ($end > $params['max']) ? $params['max'] : $end;
+        }
+
+        $this->doc .= '<ul class="rss">';
+        if($rc) {
+            for($x = $start; $x != $end; $x += $mod) {
+                $item = $feed->get_item($x);
+                $this->doc .= '<li><div class="li">';
+                // support feeds without links
+                $lnkurl = $item->get_permalink();
+                if($lnkurl) {
+                    // title is escaped by SimplePie, we unescape here because it
+                    // is escaped again in externallink() FS#1705
+                    $this->externallink(
+                        $item->get_permalink(),
+                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')
+                    );
+                } else {
+                    $this->doc .= ' '.$item->get_title();
+                }
+                if($params['author']) {
+                    $author = $item->get_author(0);
+                    if($author) {
+                        $name = $author->get_name();
+                        if(!$name) $name = $author->get_email();
+                        if($name) $this->doc .= ' '.$lang['by'].' '.hsc($name);
+                    }
+                }
+                if($params['date']) {
+                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
+                }
+                if($params['details']) {
+                    $this->doc .= '<div class="detail">';
+                    if($conf['htmlok']) {
+                        $this->doc .= $item->get_description();
+                    } else {
+                        $this->doc .= strip_tags($item->get_description());
+                    }
+                    $this->doc .= '</div>';
+                }
+
+                $this->doc .= '</div></li>';
+            }
+        } else {
+            $this->doc .= '<li><div class="li">';
+            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
+            $this->externallink($url);
+            if($conf['allowdebug']) {
+                $this->doc .= '<!--'.hsc($feed->error).'-->';
+            }
+            $this->doc .= '</div></li>';
+        }
+        $this->doc .= '</ul>';
+    }
+
+    /**
+     * Start a table
+     *
+     * @param int $maxcols maximum number of columns
+     * @param int $numrows NOT IMPLEMENTED
+     * @param int $pos byte position in the original source
+     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+     */
+    function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) {
+        // initialize the row counter used for classes
+        $this->_counter['row_counter'] = 0;
+        $class                         = 'table';
+        if($classes !== null) {
+            if(is_array($classes)) $classes = join(' ', $classes);
+            $class .= ' ' . $classes;
+        }
+        if($pos !== null) {
+            $hid = $this->_headerToLink($class, true);
+            $data = array();
+            $data['target'] = 'table';
+            $data['name'] = '';
+            $data['hid'] = $hid;
+            $class .= ' '.$this->startSectionEdit($pos, $data);
+        }
+        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
+            DOKU_LF;
+    }
+
+    /**
+     * Close a table
+     *
+     * @param int $pos byte position in the original source
+     */
+    function table_close($pos = null) {
+        $this->doc .= '</table></div>'.DOKU_LF;
+        if($pos !== null) {
+            $this->finishSectionEdit($pos);
+        }
+    }
+
+    /**
+     * Open a table header
+     */
+    function tablethead_open() {
+        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
+    }
+
+    /**
+     * Close a table header
+     */
+    function tablethead_close() {
+        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
+    }
+
+    /**
+     * Open a table body
+     */
+    function tabletbody_open() {
+        $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF;
+    }
+
+    /**
+     * Close a table body
+     */
+    function tabletbody_close() {
+        $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF;
+    }
+
+    /**
+     * Open a table footer
+     */
+    function tabletfoot_open() {
+        $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF;
+    }
+
+    /**
+     * Close a table footer
+     */
+    function tabletfoot_close() {
+        $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF;
+    }
+
+    /**
+     * Open a table row
+     *
+     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+     */
+    function tablerow_open($classes = null) {
+        // initialize the cell counter used for classes
+        $this->_counter['cell_counter'] = 0;
+        $class                          = 'row'.$this->_counter['row_counter']++;
+        if($classes !== null) {
+            if(is_array($classes)) $classes = join(' ', $classes);
+            $class .= ' ' . $classes;
+        }
+        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
+    }
+
+    /**
+     * Close a table row
+     */
+    function tablerow_close() {
+        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
+    }
+
+    /**
+     * Open a table header cell
+     *
+     * @param int    $colspan
+     * @param string $align left|center|right
+     * @param int    $rowspan
+     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
+     */
+    function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
+        $class = 'class="col'.$this->_counter['cell_counter']++;
+        if(!is_null($align)) {
+            $class .= ' '.$align.'align';
+        }
+        if($classes !== null) {
+            if(is_array($classes)) $classes = join(' ', $classes);
+            $class .= ' ' . $classes;
+        }
+        $class .= '"';
+        $this->doc .= '<th '.$class;
+        if($colspan > 1) {
+            $this->_counter['cell_counter'] += $colspan - 1;
+            $this->doc .= ' colspan="'.$colspan.'"';
+        }
+        if($rowspan > 1) {
+            $this->doc .= ' rowspan="'.$rowspan.'"';
+        }
+        $this->doc .= '>';
+    }
+
+    /**
+     * Close a table header cell
+     */
+    function tableheader_close() {
+        $this->doc .= '</th>';
+    }
+
+    /**
+     * Open a table cell
+     *
+     * @param int       $colspan
+     * @param string    $align left|center|right
+     * @param int       $rowspan
+     * @param string|string[]    $classes css classes - have to be valid, do not pass unfiltered user input
+     */
+    function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
+        $class = 'class="col'.$this->_counter['cell_counter']++;
+        if(!is_null($align)) {
+            $class .= ' '.$align.'align';
+        }
+        if($classes !== null) {
+            if(is_array($classes)) $classes = join(' ', $classes);
+            $class .= ' ' . $classes;
+        }
+        $class .= '"';
+        $this->doc .= '<td '.$class;
+        if($colspan > 1) {
+            $this->_counter['cell_counter'] += $colspan - 1;
+            $this->doc .= ' colspan="'.$colspan.'"';
+        }
+        if($rowspan > 1) {
+            $this->doc .= ' rowspan="'.$rowspan.'"';
+        }
+        $this->doc .= '>';
+    }
+
+    /**
+     * Close a table cell
+     */
+    function tablecell_close() {
+        $this->doc .= '</td>';
+    }
+
+    /**
+     * Returns the current header level.
+     * (required e.g. by the filelist plugin)
+     *
+     * @return int The current header level
+     */
+    function getLastlevel() {
+        return $this->lastlevel;
+    }
+
+    #region Utility functions
+
+    /**
+     * Build a link
+     *
+     * Assembles all parts defined in $link returns HTML for the link
+     *
+     * @param array $link attributes of a link
+     * @return string
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     */
+    function _formatLink($link) {
+        //make sure the url is XHTML compliant (skip mailto)
+        if(substr($link['url'], 0, 7) != 'mailto:') {
+            $link['url'] = str_replace('&', '&amp;', $link['url']);
+            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
+        }
+        //remove double encodings in titles
+        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
+
+        // be sure there are no bad chars in url or title
+        // (we can't do this for name because it can contain an img tag)
+        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
+        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
+
+        $ret = '';
+        $ret .= $link['pre'];
+        $ret .= '<a href="'.$link['url'].'"';
+        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
+        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
+        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
+        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
+        if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"';
+        if(!empty($link['more'])) $ret .= ' '.$link['more'];
+        $ret .= '>';
+        $ret .= $link['name'];
+        $ret .= '</a>';
+        $ret .= $link['suf'];
+        return $ret;
+    }
+
+    /**
+     * Renders internal and external media
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     * @param string $src       media ID
+     * @param string $title     descriptive text
+     * @param string $align     left|center|right
+     * @param int    $width     width of media in pixel
+     * @param int    $height    height of media in pixel
+     * @param string $cache     cache|recache|nocache
+     * @param bool   $render    should the media be embedded inline or just linked
+     * @return string
+     */
+    function _media($src, $title = null, $align = null, $width = null,
+                    $height = null, $cache = null, $render = true) {
+
+        $ret = '';
+
+        list($ext, $mime) = mimetype($src);
+        if(substr($mime, 0, 5) == 'image') {
+            // first get the $title
+            if(!is_null($title)) {
+                $title = $this->_xmlEntities($title);
+            } elseif($ext == 'jpg' || $ext == 'jpeg') {
+                //try to use the caption from IPTC/EXIF
+                require_once(DOKU_INC.'inc/JpegMeta.php');
+                $jpeg = new JpegMeta(mediaFN($src));
+                if($jpeg !== false) $cap = $jpeg->getTitle();
+                if(!empty($cap)) {
+                    $title = $this->_xmlEntities($cap);
+                }
+            }
+            if(!$render) {
+                // if the picture is not supposed to be rendered
+                // return the title of the picture
+                if(!$title) {
+                    // just show the sourcename
+                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
+                }
+                return $title;
+            }
+            //add image tag
+            $ret .= '<img src="'.ml($src, array('w' => $width, 'h' => $height, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src))).'"';
+            $ret .= ' class="media'.$align.'"';
+
+            if($title) {
+                $ret .= ' title="'.$title.'"';
+                $ret .= ' alt="'.$title.'"';
+            } else {
+                $ret .= ' alt=""';
+            }
+
+            if(!is_null($width))
+                $ret .= ' width="'.$this->_xmlEntities($width).'"';
+
+            if(!is_null($height))
+                $ret .= ' height="'.$this->_xmlEntities($height).'"';
+
+            $ret .= ' />';
+
+        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
+            // first get the $title
+            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
+            if(!$render) {
+                // if the file is not supposed to be rendered
+                // return the title of the file (just the sourcename if there is no title)
+                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
+            }
+
+            $att          = array();
+            $att['class'] = "media$align";
+            if($title) {
+                $att['title'] = $title;
+            }
+
+            if(media_supportedav($mime, 'video')) {
+                //add video
+                $ret .= $this->_video($src, $width, $height, $att);
+            }
+            if(media_supportedav($mime, 'audio')) {
+                //add audio
+                $ret .= $this->_audio($src, $att);
+            }
+
+        } elseif($mime == 'application/x-shockwave-flash') {
+            if(!$render) {
+                // if the flash is not supposed to be rendered
+                // return the title of the flash
+                if(!$title) {
+                    // just show the sourcename
+                    $title = utf8_basename(noNS($src));
+                }
+                return $this->_xmlEntities($title);
+            }
+
+            $att          = array();
+            $att['class'] = "media$align";
+            if($align == 'right') $att['align'] = 'right';
+            if($align == 'left') $att['align'] = 'left';
+            $ret .= html_flashobject(
+                ml($src, array('cache' => $cache), true, '&'), $width, $height,
+                array('quality' => 'high'),
+                null,
+                $att,
+                $this->_xmlEntities($title)
+            );
+        } elseif($title) {
+            // well at least we have a title to display
+            $ret .= $this->_xmlEntities($title);
+        } else {
+            // just show the sourcename
+            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Escape string for output
+     *
+     * @param $string
+     * @return string
+     */
+    function _xmlEntities($string) {
+        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
+    }
+
+    /**
+     * Creates a linkid from a headline
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     * @param string  $title   The headline title
+     * @param boolean $create  Create a new unique ID?
+     * @return string
+     */
+    function _headerToLink($title, $create = false) {
+        if($create) {
+            return sectionID($title, $this->headers);
+        } else {
+            $check = false;
+            return sectionID($title, $check);
+        }
+    }
+
+    /**
+     * Construct a title and handle images in titles
+     *
+     * @author Harry Fuecks <hfuecks@gmail.com>
+     * @param string|array $title    either string title or media array
+     * @param string       $default  default title if nothing else is found
+     * @param bool         $isImage  will be set to true if it's a media file
+     * @param null|string  $id       linked page id (used to extract title from first heading)
+     * @param string       $linktype content|navigation
+     * @return string      HTML of the title, might be full image tag or just escaped text
+     */
+    function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
+        $isImage = false;
+        if(is_array($title)) {
+            $isImage = true;
+            return $this->_imageTitle($title);
+        } elseif(is_null($title) || trim($title) == '') {
+            if(useHeading($linktype) && $id) {
+                $heading = p_get_first_heading($id);
+                if(!blank($heading)) {
+                    return $this->_xmlEntities($heading);
+                }
+            }
+            return $this->_xmlEntities($default);
+        } else {
+            return $this->_xmlEntities($title);
+        }
+    }
+
+    /**
+     * Returns HTML code for images used in link titles
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     * @param array $img
+     * @return string HTML img tag or similar
+     */
+    function _imageTitle($img) {
+        global $ID;
+
+        // some fixes on $img['src']
+        // see internalmedia() and externalmedia()
+        list($img['src']) = explode('#', $img['src'], 2);
+        if($img['type'] == 'internalmedia') {
+            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
+        }
+
+        return $this->_media(
+            $img['src'],
+            $img['title'],
+            $img['align'],
+            $img['width'],
+            $img['height'],
+            $img['cache']
+        );
+    }
+
+    /**
+     * helperfunction to return a basic link to a media
+     *
+     * used in internalmedia() and externalmedia()
+     *
+     * @author   Pierre Spring <pierre.spring@liip.ch>
+     * @param string $src       media ID
+     * @param string $title     descriptive text
+     * @param string $align     left|center|right
+     * @param int    $width     width of media in pixel
+     * @param int    $height    height of media in pixel
+     * @param string $cache     cache|recache|nocache
+     * @param bool   $render    should the media be embedded inline or just linked
+     * @return array associative array with link config
+     */
+    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
+        global $conf;
+
+        $link           = array();
+        $link['class']  = 'media';
+        $link['style']  = '';
+        $link['pre']    = '';
+        $link['suf']    = '';
+        $link['more']   = '';
+        $link['target'] = $conf['target']['media'];
+        if($conf['target']['media']) $link['rel'] = 'noopener';
+        $link['title']  = $this->_xmlEntities($src);
+        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
+
+        return $link;
+    }
+
+    /**
+     * Embed video(s) in HTML
+     *
+     * @author Anika Henke <anika@selfthinker.org>
+     * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+     *
+     * @param string $src         - ID of video to embed
+     * @param int    $width       - width of the video in pixels
+     * @param int    $height      - height of the video in pixels
+     * @param array  $atts        - additional attributes for the <video> tag
+     * @return string
+     */
+    function _video($src, $width, $height, $atts = null) {
+        // prepare width and height
+        if(is_null($atts)) $atts = array();
+        $atts['width']  = (int) $width;
+        $atts['height'] = (int) $height;
+        if(!$atts['width']) $atts['width'] = 320;
+        if(!$atts['height']) $atts['height'] = 240;
+
+        $posterUrl = '';
+        $files = array();
+        $tracks = array();
+        $isExternal = media_isexternal($src);
+
+        if ($isExternal) {
+            // take direct source for external files
+            list(/*ext*/, $srcMime) = mimetype($src);
+            $files[$srcMime] = $src;
+        } else {
+            // prepare alternative formats
+            $extensions   = array('webm', 'ogv', 'mp4');
+            $files        = media_alternativefiles($src, $extensions);
+            $poster       = media_alternativefiles($src, array('jpg', 'png'));
+            $tracks       = media_trackfiles($src);
+            if(!empty($poster)) {
+                $posterUrl = ml(reset($poster), '', true, '&');
+            }
+        }
+
+        $out = '';
+        // open video tag
+        $out .= '<video '.buildAttributes($atts).' controls="controls"';
+        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
+        $out .= '>'.NL;
+        $fallback = '';
+
+        // output source for each alternative video format
+        foreach($files as $mime => $file) {
+            if ($isExternal) {
+                $url = $file;
+                $linkType = 'externalmedia';
+            } else {
+                $url = ml($file, '', true, '&');
+                $linkType = 'internalmedia';
+            }
+            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
+
+            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
+            // alternative content (just a link to the file)
+            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
+        }
+
+        // output each track if any
+        foreach( $tracks as $trackid => $info ) {
+            list( $kind, $srclang ) = array_map( 'hsc', $info );
+            $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
+            $out .= "label=\"$srclang\" ";
+            $out .= 'src="'.ml($trackid, '', true).'">'.NL;
+        }
+
+        // finish
+        $out .= $fallback;
+        $out .= '</video>'.NL;
+        return $out;
+    }
+
+    /**
+     * Embed audio in HTML
+     *
+     * @author Anika Henke <anika@selfthinker.org>
+     *
+     * @param string $src       - ID of audio to embed
+     * @param array  $atts      - additional attributes for the <audio> tag
+     * @return string
+     */
+    function _audio($src, $atts = array()) {
+        $files = array();
+        $isExternal = media_isexternal($src);
+
+        if ($isExternal) {
+            // take direct source for external files
+            list(/*ext*/, $srcMime) = mimetype($src);
+            $files[$srcMime] = $src;
+        } else {
+            // prepare alternative formats
+            $extensions   = array('ogg', 'mp3', 'wav');
+            $files        = media_alternativefiles($src, $extensions);
+        }
+
+        $out = '';
+        // open audio tag
+        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
+        $fallback = '';
+
+        // output source for each alternative audio format
+        foreach($files as $mime => $file) {
+            if ($isExternal) {
+                $url = $file;
+                $linkType = 'externalmedia';
+            } else {
+                $url = ml($file, '', true, '&');
+                $linkType = 'internalmedia';
+            }
+            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
+
+            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
+            // alternative content (just a link to the file)
+            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
+        }
+
+        // finish
+        $out .= $fallback;
+        $out .= '</audio>'.NL;
+        return $out;
+    }
+
+    /**
+     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
+     * which returns an existing media revision less or equal to rev or date_at
+     *
+     * @author lisps
+     * @param string $media_id
+     * @access protected
+     * @return string revision ('' for current)
+     */
+    function _getLastMediaRevisionAt($media_id){
+        if(!$this->date_at || media_isexternal($media_id)) return '';
+        $pagelog = new MediaChangeLog($media_id);
+        return $pagelog->getLastRevisionAt($this->date_at);
+    }
+
+    #endregion
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/wiki/inc/parser/xhtmlsummary.php b/wiki/inc/parser/xhtmlsummary.php
new file mode 100644
index 0000000..867b71f
--- /dev/null
+++ b/wiki/inc/parser/xhtmlsummary.php
@@ -0,0 +1,89 @@
+<?php
+if(!defined('DOKU_INC')) die('meh.');
+
+/**
+ * The summary XHTML form selects either up to the first two paragraphs
+ * it find in a page or the first section (whichever comes first)
+ * It strips out the table of contents if one exists
+ * Section divs are not used - everything should be nested in a single
+ * div with CSS class "page"
+ * Headings have their a name link removed and section editing links
+ * removed
+ * It also attempts to capture the first heading in a page for
+ * use as the title of the page.
+ *
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ * @todo   Is this currently used anywhere? Should it?
+ */
+class Doku_Renderer_xhtmlsummary extends Doku_Renderer_xhtml {
+
+    // Namespace these variables to
+    // avoid clashes with parent classes
+    var $sum_paragraphs = 0;
+    var $sum_capture = true;
+    var $sum_inSection = false;
+    var $sum_summary = '';
+    var $sum_pageTitle = false;
+
+    function document_start() {
+        $this->doc .= DOKU_LF.'<div>'.DOKU_LF;
+    }
+
+    function document_end() {
+        $this->doc = $this->sum_summary;
+        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
+    }
+
+    // FIXME not supported anymore
+    function toc_open() {
+        $this->sum_summary .= $this->doc;
+    }
+
+    // FIXME not supported anymore
+    function toc_close() {
+        $this->doc = '';
+    }
+
+    function header($text, $level, $pos) {
+        if ( !$this->sum_pageTitle ) {
+            $this->info['sum_pagetitle'] = $text;
+            $this->sum_pageTitle = true;
+        }
+        $this->doc .= DOKU_LF.'<h'.$level.'>';
+        $this->doc .= $this->_xmlEntities($text);
+        $this->doc .= "</h$level>".DOKU_LF;
+    }
+
+    function section_open($level) {
+        if ( $this->sum_capture ) {
+            $this->sum_inSection = true;
+        }
+    }
+
+    function section_close() {
+        if ( $this->sum_capture && $this->sum_inSection ) {
+            $this->sum_summary .= $this->doc;
+            $this->sum_capture = false;
+        }
+    }
+
+    function p_open() {
+        if ( $this->sum_capture && $this->sum_paragraphs < 2 ) {
+            $this->sum_paragraphs++;
+        }
+        parent :: p_open();
+    }
+
+    function p_close() {
+        parent :: p_close();
+        if ( $this->sum_capture && $this->sum_paragraphs >= 2 ) {
+            $this->sum_summary .= $this->doc;
+            $this->sum_capture = false;
+        }
+    }
+
+}
+
+
+//Setup VIM: ex: et ts=2 :