diff options
Diffstat (limited to 'wiki/inc/parser')
-rw-r--r-- | wiki/inc/parser/code.php | 73 | ||||
-rw-r--r-- | wiki/inc/parser/handler.php | 1811 | ||||
-rw-r--r-- | wiki/inc/parser/lexer.php | 614 | ||||
-rw-r--r-- | wiki/inc/parser/metadata.php | 694 | ||||
-rw-r--r-- | wiki/inc/parser/parser.php | 1034 | ||||
-rw-r--r-- | wiki/inc/parser/renderer.php | 883 | ||||
-rw-r--r-- | wiki/inc/parser/xhtml.php | 1970 | ||||
-rw-r--r-- | wiki/inc/parser/xhtmlsummary.php | 89 |
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}&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×$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'] .= '&'; + if(is_array($search)) { + $search = array_map('rawurlencode', $search); + $link['url'] .= 's[]='.join('&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('&', '&', $link['url']); + $link['url'] = str_replace('&amp;', '&', $link['url']); + } + //remove double encodings in titles + $link['title'] = str_replace('&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('>' => '>', '<' => '<', '"' => '"')); + + $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 : |