diff options
Diffstat (limited to 'platform/www/inc/parser')
-rw-r--r-- | platform/www/inc/parser/code.php | 71 | ||||
-rw-r--r-- | platform/www/inc/parser/handler.php | 1157 | ||||
-rw-r--r-- | platform/www/inc/parser/metadata.php | 751 | ||||
-rw-r--r-- | platform/www/inc/parser/parser.php | 99 | ||||
-rw-r--r-- | platform/www/inc/parser/renderer.php | 910 | ||||
-rw-r--r-- | platform/www/inc/parser/xhtml.php | 1999 | ||||
-rw-r--r-- | platform/www/inc/parser/xhtmlsummary.php | 84 |
7 files changed, 5071 insertions, 0 deletions
diff --git a/platform/www/inc/parser/code.php b/platform/www/inc/parser/code.php new file mode 100644 index 0000000..cded87d --- /dev/null +++ b/platform/www/inc/parser/code.php @@ -0,0 +1,71 @@ +<?php +/** + * A simple renderer that allows downloading of code and file snippets + * + * @author Andreas Gohr <andi@splitbrain.org> + */ +class Doku_Renderer_code extends Doku_Renderer { + protected $_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 + */ + public 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 = \dokuwiki\Utf8\PhpString::basename($filename); + $filename = \dokuwiki\Utf8\Clean::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 + */ + public function file($text, $language = null, $filename = '') { + $this->code($text, $language, $filename); + } + + /** + * This should never be reached, if it is send a 404 + */ + public function document_end() { + http_status(404); + echo '404 - Not found'; + exit; + } + + /** + * Return the format of the renderer + * + * @returns string 'code' + */ + public function getFormat() { + return 'code'; + } +} diff --git a/platform/www/inc/parser/handler.php b/platform/www/inc/parser/handler.php new file mode 100644 index 0000000..a360960 --- /dev/null +++ b/platform/www/inc/parser/handler.php @@ -0,0 +1,1157 @@ +<?php + +use dokuwiki\Extension\Event; +use dokuwiki\Extension\SyntaxPlugin; +use dokuwiki\Parsing\Handler\Block; +use dokuwiki\Parsing\Handler\CallWriter; +use dokuwiki\Parsing\Handler\CallWriterInterface; +use dokuwiki\Parsing\Handler\Lists; +use dokuwiki\Parsing\Handler\Nest; +use dokuwiki\Parsing\Handler\Preformatted; +use dokuwiki\Parsing\Handler\Quote; +use dokuwiki\Parsing\Handler\Table; + +/** + * Class Doku_Handler + */ +class Doku_Handler { + /** @var CallWriterInterface */ + protected $callWriter = null; + + /** @var array The current CallWriter will write directly to this list of calls, Parser reads it */ + public $calls = array(); + + /** @var array internal status holders for some modes */ + protected $status = array( + 'section' => false, + 'doublequote' => 0, + ); + + /** @var bool should blocks be rewritten? FIXME seems to always be true */ + protected $rewriteBlocks = true; + + /** + * Doku_Handler constructor. + */ + public function __construct() { + $this->callWriter = new CallWriter($this); + } + + /** + * Add a new call by passing it to the current CallWriter + * + * @param string $handler handler method name (see mode handlers below) + * @param mixed $args arguments for this call + * @param int $pos byte position in the original source file + */ + public function addCall($handler, $args, $pos) { + $call = array($handler,$args, $pos); + $this->callWriter->writeCall($call); + } + + /** + * Accessor for the current CallWriter + * + * @return CallWriterInterface + */ + public function getCallWriter() { + return $this->callWriter; + } + + /** + * Set a new CallWriter + * + * @param CallWriterInterface $callWriter + */ + public function setCallWriter($callWriter) { + $this->callWriter = $callWriter; + } + + /** + * Return the current internal status of the given name + * + * @param string $status + * @return mixed|null + */ + public function getStatus($status) { + if (!isset($this->status[$status])) return null; + return $this->status[$status]; + } + + /** + * Set a new internal status + * + * @param string $status + * @param mixed $value + */ + public function setStatus($status, $value) { + $this->status[$status] = $value; + } + + /** @deprecated 2019-10-31 use addCall() instead */ + public function _addCall($handler, $args, $pos) { + dbg_deprecated('addCall'); + $this->addCall($handler, $args, $pos); + } + + /** + * Similar to addCall, but adds a plugin call + * + * @param string $plugin name of the plugin + * @param mixed $args arguments for this call + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @param string $match matched syntax + */ + public function addPluginCall($plugin, $args, $state, $pos, $match) { + $call = array('plugin',array($plugin, $args, $state, $match), $pos); + $this->callWriter->writeCall($call); + } + + /** + * Finishes handling + * + * Called from the parser. Calls finalise() on the call writer, closes open + * sections, rewrites blocks and adds document_start and document_end calls. + * + * @triggers PARSER_HANDLER_DONE + */ + public 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 Block(); + $this->calls = $B->process($this->calls); + } + + Event::createAndTrigger('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 + * + * @fixme seems to be unused? + * @return bool|mixed + */ + public function fetch() { + $call = current($this->calls); + if($call !== false) { + next($this->calls); //advance the pointer + return $call; + } + return false; + } + + + /** + * 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; + } + + /** + * Simplifies handling for the formatting tags which all behave the same + * + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @param string $name actual mode name + */ + protected 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; + } + } + + + /** + * The following methods define the handlers for the different Syntax modes + * + * The handlers are called from dokuwiki\Parsing\Lexer\Lexer\invokeParser() + * + * @todo it might make sense to move these into their own class or merge them with the + * ParserMode classes some time. + */ + // region mode handlers + + /** + * Special plugin handler + * + * This handler is called for all modes starting with 'plugin_'. + * An additional parameter with the plugin name is passed. The plugin's handle() + * method is called here + * + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @param string $pluginname name of the plugin + * @return bool mode handled? + */ + public function plugin($match, $state, $pos, $pluginname){ + $data = array($match); + /** @var SyntaxPlugin $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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function base($match, $state, $pos) { + switch ( $state ) { + case DOKU_LEXER_UNMATCHED: + $this->addCall('cdata', array($match), $pos); + return true; + break; + } + return false; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function notoc($match, $state, $pos) { + $this->addCall('notoc', array(), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function nocache($match, $state, $pos) { + $this->addCall('nocache', array(), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function linebreak($match, $state, $pos) { + $this->addCall('linebreak', array(), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function eol($match, $state, $pos) { + $this->addCall('eol', array(), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function hr($match, $state, $pos) { + $this->addCall('hr', array(), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function strong($match, $state, $pos) { + $this->nestingTag($match, $state, $pos, 'strong'); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function emphasis($match, $state, $pos) { + $this->nestingTag($match, $state, $pos, 'emphasis'); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function underline($match, $state, $pos) { + $this->nestingTag($match, $state, $pos, 'underline'); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function monospace($match, $state, $pos) { + $this->nestingTag($match, $state, $pos, 'monospace'); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function subscript($match, $state, $pos) { + $this->nestingTag($match, $state, $pos, 'subscript'); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function superscript($match, $state, $pos) { + $this->nestingTag($match, $state, $pos, 'superscript'); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function deleted($match, $state, $pos) { + $this->nestingTag($match, $state, $pos, 'deleted'); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function footnote($match, $state, $pos) { + 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; + + $this->callWriter = new Nest($this->callWriter, 'footnote_close'); + $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); + + /** @var Nest $reWriter */ + $reWriter = $this->callWriter; + $this->callWriter = $reWriter->process(); + break; + case DOKU_LEXER_UNMATCHED: + $this->addCall('cdata', array($match), $pos); + break; + } + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function listblock($match, $state, $pos) { + switch ( $state ) { + case DOKU_LEXER_ENTER: + $this->callWriter = new Lists($this->callWriter); + $this->addCall('list_open', array($match), $pos); + break; + case DOKU_LEXER_EXIT: + $this->addCall('list_close', array(), $pos); + /** @var Lists $reWriter */ + $reWriter = $this->callWriter; + $this->callWriter = $reWriter->process(); + 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function unformatted($match, $state, $pos) { + if ( $state == DOKU_LEXER_UNMATCHED ) { + $this->addCall('unformatted', array($match), $pos); + } + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function php($match, $state, $pos) { + if ( $state == DOKU_LEXER_UNMATCHED ) { + $this->addCall('php', array($match), $pos); + } + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function phpblock($match, $state, $pos) { + if ( $state == DOKU_LEXER_UNMATCHED ) { + $this->addCall('phpblock', array($match), $pos); + } + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function html($match, $state, $pos) { + if ( $state == DOKU_LEXER_UNMATCHED ) { + $this->addCall('html', array($match), $pos); + } + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function htmlblock($match, $state, $pos) { + if ( $state == DOKU_LEXER_UNMATCHED ) { + $this->addCall('htmlblock', array($match), $pos); + } + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function preformatted($match, $state, $pos) { + switch ( $state ) { + case DOKU_LEXER_ENTER: + $this->callWriter = new Preformatted($this->callWriter); + $this->addCall('preformatted_start', array(), $pos); + break; + case DOKU_LEXER_EXIT: + $this->addCall('preformatted_end', array(), $pos); + /** @var Preformatted $reWriter */ + $reWriter = $this->callWriter; + $this->callWriter = $reWriter->process(); + 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function quote($match, $state, $pos) { + + switch ( $state ) { + + case DOKU_LEXER_ENTER: + $this->callWriter = new Quote($this->callWriter); + $this->addCall('quote_start', array($match), $pos); + break; + + case DOKU_LEXER_EXIT: + $this->addCall('quote_end', array(), $pos); + /** @var Lists $reWriter */ + $reWriter = $this->callWriter; + $this->callWriter = $reWriter->process(); + 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function file($match, $state, $pos) { + return $this->code($match, $state, $pos, 'file'); + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @param string $type either 'code' or 'file' + * @return bool mode handled? + */ + public 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function acronym($match, $state, $pos) { + $this->addCall('acronym', array($match), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function smiley($match, $state, $pos) { + $this->addCall('smiley', array($match), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function wordblock($match, $state, $pos) { + $this->addCall('wordblock', array($match), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function entity($match, $state, $pos) { + $this->addCall('entity', array($match), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function multiplyentity($match, $state, $pos) { + preg_match_all('/\d+/',$match,$matches); + $this->addCall('multiplyentity', array($matches[0][0], $matches[0][1]), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function singlequoteopening($match, $state, $pos) { + $this->addCall('singlequoteopening', array(), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function singlequoteclosing($match, $state, $pos) { + $this->addCall('singlequoteclosing', array(), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function apostrophe($match, $state, $pos) { + $this->addCall('apostrophe', array(), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function doublequoteopening($match, $state, $pos) { + $this->addCall('doublequoteopening', array(), $pos); + $this->status['doublequote']++; + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function camelcaselink($match, $state, $pos) { + $this->addCall('camelcaselink', array($match), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function filelink($match, $state, $pos) { + $this->addCall('filelink', array($match, null), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function windowssharelink($match, $state, $pos) { + $this->addCall('windowssharelink', array($match, null), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public 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; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function emaillink($match, $state, $pos) { + $email = preg_replace(array('/^</','/>$/'),'',$match); + $this->addCall('emaillink', array($email, null), $pos); + return true; + } + + /** + * @param string $match matched syntax + * @param int $state a LEXER_STATE_* constant + * @param int $pos byte position in the original source file + * @return bool mode handled? + */ + public function table($match, $state, $pos) { + switch ( $state ) { + + case DOKU_LEXER_ENTER: + + $this->callWriter = new Table($this->callWriter); + + $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); + /** @var Table $reWriter */ + $reWriter = $this->callWriter; + $this->callWriter = $reWriter->process(); + 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; + } + + // endregion modes +} + +//------------------------------------------------------------------------ +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; +} + diff --git a/platform/www/inc/parser/metadata.php b/platform/www/inc/parser/metadata.php new file mode 100644 index 0000000..849fffe --- /dev/null +++ b/platform/www/inc/parser/metadata.php @@ -0,0 +1,751 @@ +<?php +/** + * 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. + * + * @author Esther Brunner <wikidesign@gmail.com> + */ +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 whether or not data is being captured for the abstract, public to be accessible by plugins */ + public $capturing = true; + + /** @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' + */ + public function getFormat() + { + return 'metadata'; + } + + /** + * Initialize the document + * + * Sets up some of the persistent info about the page if it doesn't exist, yet. + */ + public function document_start() + { + global $ID; + + $this->headers = array(); + + // external pages are missing create date + if (!isset($this->persistent['date']['created']) || !$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 + */ + public 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 = \dokuwiki\Utf8\PhpString::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 + */ + public function cdata($text) + { + if (!$this->capture || !$this->capturing) { + 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 + */ + public 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 + */ + public 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 + */ + public function p_open() + { + $this->cdata(DOKU_LF); + } + + /** + * Close a paragraph + */ + public function p_close() + { + $this->cdata(DOKU_LF); + } + + /** + * Create a line break + */ + public function linebreak() + { + $this->cdata(DOKU_LF); + } + + /** + * Create a horizontal line + */ + public 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> + */ + public function footnote_open() + { + if ($this->capture) { + // move current content to store + // this is required to ensure safe behaviour of plugins accessed within footnotes + $this->store = $this->doc; + $this->doc = ''; + + // disable capturing + $this->capturing = false; + } + } + + /** + * Callback for footnote end syntax + * + * All content rendered whilst within footnote syntax mode is discarded, + * the previously rendered content is restored and capturing is re-enabled. + * + * @author Andreas Gohr + */ + public function footnote_close() + { + if ($this->capture) { + // re-enable capturing + $this->capturing = true; + // restore previously rendered content + $this->doc = $this->store; + $this->store = ''; + } + } + + /** + * Open an unordered list + */ + public function listu_open() + { + $this->cdata(DOKU_LF); + } + + /** + * Open an ordered list + */ + public 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 + */ + public function listitem_open($level, $node=false) + { + $this->cdata(str_repeat(DOKU_TAB, $level).'* '); + } + + /** + * Close a list item + */ + public function listitem_close() + { + $this->cdata(DOKU_LF); + } + + /** + * Output preformatted text + * + * @param string $text + */ + public function preformatted($text) + { + $this->cdata($text); + } + + /** + * Start a block quote + */ + public function quote_open() + { + $this->cdata(DOKU_LF.DOKU_TAB.'"'); + } + + /** + * Stop a block quote + */ + public 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 + */ + public 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 + */ + public function code($text, $language = null, $file = null) + { + $this->cdata(DOKU_LF.$text.DOKU_LF); + } + + /** + * Format an acronym + * + * Uses $this->acronyms + * + * @param string $acronym + */ + public function acronym($acronym) + { + $this->cdata($acronym); + } + + /** + * Format a smiley + * + * Uses $this->smiley + * + * @param string $smiley + */ + public function smiley($smiley) + { + $this->cdata($smiley); + } + + /** + * Format an entity + * + * Entities are basically small text replacements + * + * Uses $this->entities + * + * @param string $entity + */ + public 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 + */ + public function multiplyentity($x, $y) + { + $this->cdata($x.'×'.$y); + } + + /** + * Render an opening single quote char (language specific) + */ + public function singlequoteopening() + { + global $lang; + $this->cdata($lang['singlequoteopening']); + } + + /** + * Render a closing single quote char (language specific) + */ + public function singlequoteclosing() + { + global $lang; + $this->cdata($lang['singlequoteclosing']); + } + + /** + * Render an apostrophe char (language specific) + */ + public function apostrophe() + { + global $lang; + $this->cdata($lang['apostrophe']); + } + + /** + * Render an opening double quote char (language specific) + */ + public function doublequoteopening() + { + global $lang; + $this->cdata($lang['doublequoteopening']); + } + + /** + * Render an closinging double quote char (language specific) + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function interwikilink($match, $name, $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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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; + } + + /** + * 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 + */ + public function _getLinkTitle($title, $default, $id = null) + { + if (is_array($title)) { + if ($title['title']) { + return '['.$title['title'].']'; + } else { + return $default; + } + } elseif (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 + */ + protected function _firstimage($src) + { + global $ID; + + if ($this->firstimage) { + return; + } + + 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 + */ + protected 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/platform/www/inc/parser/parser.php b/platform/www/inc/parser/parser.php new file mode 100644 index 0000000..aee82f0 --- /dev/null +++ b/platform/www/inc/parser/parser.php @@ -0,0 +1,99 @@ +<?php + +use dokuwiki\Debug\PropertyDeprecationHelper; + +/** + * 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') +); + +/** + * Class Doku_Parser + * + * @deprecated 2018-05-04 + */ +class Doku_Parser extends \dokuwiki\Parsing\Parser { + use PropertyDeprecationHelper { + __set as protected deprecationHelperMagicSet; + __get as protected deprecationHelperMagicGet; + } + + /** @inheritdoc */ + public function __construct(Doku_Handler $handler = null) { + dbg_deprecated(\dokuwiki\Parsing\Parser::class); + $this->deprecatePublicProperty('modes', __CLASS__); + $this->deprecatePublicProperty('connected', __CLASS__); + + if ($handler === null) { + $handler = new Doku_Handler(); + } + + parent::__construct($handler); + } + + public function __set($name, $value) + { + + if ($name === 'Handler') { + $this->handler = $value; + return; + } + + if ($name === 'Lexer') { + $this->lexer = $value; + return; + } + + $this->deprecationHelperMagicSet($name, $value); + } + + public function __get($name) + { + if ($name === 'Handler') { + return $this->handler; + } + + if ($name === 'Lexer') { + return $this->lexer; + } + + return $this->deprecationHelperMagicGet($name); + } +} diff --git a/platform/www/inc/parser/renderer.php b/platform/www/inc/parser/renderer.php new file mode 100644 index 0000000..e4eff2a --- /dev/null +++ b/platform/www/inc/parser/renderer.php @@ -0,0 +1,910 @@ +<?php +/** + * Renderer output base class + * + * @author Harry Fuecks <hfuecks@gmail.com> + * @author Andreas Gohr <andi@splitbrain.org> + */ + +use dokuwiki\Extension\Plugin; +use dokuwiki\Extension\SyntaxPlugin; + +/** + * 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\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. + */ +abstract class Doku_Renderer extends 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 array the list of headers used to create unique link ids */ + protected $headers = 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 + */ + public function reset(){ + $this->headers = array(); + $this->doc = ''; + $this->info['cache'] = true; + $this->info['toc'] = true; + } + + /** + * 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 + */ + public function isSingleton() { + return false; + } + + /** + * Returns the format produced by this renderer. + * + * Has to be overidden by sub classes + * + * @return string + */ + abstract public function getFormat(); + + /** + * Disable caching of this renderer's output + */ + public function nocache() { + $this->info['cache'] = false; + } + + /** + * Disable TOC generation for this renderer's output + * + * This might not be used for certain sub renderer + */ + public 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 + */ + public function plugin($name, $data, $state = '', $match = '') { + /** @var SyntaxPlugin $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 + */ + public 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. + */ + public function nest_close() { + } + + #region Syntax modes - sub classes will need to implement them to fill $doc + + /** + * Initialize the document + */ + public function document_start() { + } + + /** + * Finalize the document + */ + public function document_end() { + } + + /** + * Render the Table of Contents + * + * @return string + */ + public 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 + */ + public 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 + */ + public function header($text, $level, $pos) { + } + + /** + * Open a new section + * + * @param int $level section level (as determined by the previous header) + */ + public function section_open($level) { + } + + /** + * Close the current section + */ + public function section_close() { + } + + /** + * Render plain text data + * + * @param string $text + */ + public function cdata($text) { + } + + /** + * Open a paragraph + */ + public function p_open() { + } + + /** + * Close a paragraph + */ + public function p_close() { + } + + /** + * Create a line break + */ + public function linebreak() { + } + + /** + * Create a horizontal line + */ + public function hr() { + } + + /** + * Start strong (bold) formatting + */ + public function strong_open() { + } + + /** + * Stop strong (bold) formatting + */ + public function strong_close() { + } + + /** + * Start emphasis (italics) formatting + */ + public function emphasis_open() { + } + + /** + * Stop emphasis (italics) formatting + */ + public function emphasis_close() { + } + + /** + * Start underline formatting + */ + public function underline_open() { + } + + /** + * Stop underline formatting + */ + public function underline_close() { + } + + /** + * Start monospace formatting + */ + public function monospace_open() { + } + + /** + * Stop monospace formatting + */ + public function monospace_close() { + } + + /** + * Start a subscript + */ + public function subscript_open() { + } + + /** + * Stop a subscript + */ + public function subscript_close() { + } + + /** + * Start a superscript + */ + public function superscript_open() { + } + + /** + * Stop a superscript + */ + public function superscript_close() { + } + + /** + * Start deleted (strike-through) formatting + */ + public function deleted_open() { + } + + /** + * Stop deleted (strike-through) formatting + */ + public function deleted_close() { + } + + /** + * Start a footnote + */ + public function footnote_open() { + } + + /** + * Stop a footnote + */ + public function footnote_close() { + } + + /** + * Open an unordered list + */ + public function listu_open() { + } + + /** + * Close an unordered list + */ + public function listu_close() { + } + + /** + * Open an ordered list + */ + public function listo_open() { + } + + /** + * Close an ordered list + */ + public function listo_close() { + } + + /** + * Open a list item + * + * @param int $level the nesting level + * @param bool $node true when a node; false when a leaf + */ + public function listitem_open($level,$node=false) { + } + + /** + * Close a list item + */ + public function listitem_close() { + } + + /** + * Start the content of a list item + */ + public function listcontent_open() { + } + + /** + * Stop the content of a list item + */ + public function listcontent_close() { + } + + /** + * Output unformatted $text + * + * Defaults to $this->cdata() + * + * @param string $text + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function htmlblock($text) { + } + + /** + * Output preformatted text + * + * @param string $text + */ + public function preformatted($text) { + } + + /** + * Start a block quote + */ + public function quote_open() { + } + + /** + * Stop a block quote + */ + public 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 + */ + public 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 + */ + public function code($text, $lang = null, $file = null) { + } + + /** + * Format an acronym + * + * Uses $this->acronyms + * + * @param string $acronym + */ + public function acronym($acronym) { + } + + /** + * Format a smiley + * + * Uses $this->smiley + * + * @param string $smiley + */ + public function smiley($smiley) { + } + + /** + * Format an entity + * + * Entities are basically small text replacements + * + * Uses $this->entities + * + * @param string $entity + */ + public 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 + */ + public function multiplyentity($x, $y) { + } + + /** + * Render an opening single quote char (language specific) + */ + public function singlequoteopening() { + } + + /** + * Render a closing single quote char (language specific) + */ + public function singlequoteclosing() { + } + + /** + * Render an apostrophe char (language specific) + */ + public function apostrophe() { + } + + /** + * Render an opening double quote char (language specific) + */ + public function doublequoteopening() { + } + + /** + * Render an closinging double quote char (language specific) + */ + public function doublequoteclosing() { + } + + /** + * Render a CamelCase link + * + * @param string $link The link name + * @see http://en.wikipedia.org/wiki/CamelCase + */ + public function camelcaselink($link) { + } + + /** + * Render a page local link + * + * @param string $hash hash link identifier + * @param string $name name for the link + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function interwikilink($link, $title, $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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function table_open($maxcols = null, $numrows = null, $pos = null) { + } + + /** + * Close a table + * + * @param int $pos byte position in the original source + */ + public function table_close($pos = null) { + } + + /** + * Open a table header + */ + public function tablethead_open() { + } + + /** + * Close a table header + */ + public function tablethead_close() { + } + + /** + * Open a table body + */ + public function tabletbody_open() { + } + + /** + * Close a table body + */ + public function tabletbody_close() { + } + + /** + * Open a table footer + */ + public function tabletfoot_open() { + } + + /** + * Close a table footer + */ + public function tabletfoot_close() { + } + + /** + * Open a table row + */ + public function tablerow_open() { + } + + /** + * Close a table row + */ + public function tablerow_close() { + } + + /** + * Open a table header cell + * + * @param int $colspan + * @param string $align left|center|right + * @param int $rowspan + */ + public function tableheader_open($colspan = 1, $align = null, $rowspan = 1) { + } + + /** + * Close a table header cell + */ + public function tableheader_close() { + } + + /** + * Open a table cell + * + * @param int $colspan + * @param string $align left|center|right + * @param int $rowspan + */ + public function tablecell_open($colspan = 1, $align = null, $rowspan = 1) { + } + + /** + * Close a table cell + */ + public function tablecell_close() { + } + + #endregion + + #region util functions, you probably won't need to reimplement them + + /** + * 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 + */ + public function _headerToLink($title, $create = false) { + if($create) { + return sectionID($title, $this->headers); + } else { + $check = false; + return sectionID($title, $check); + } + } + + /** + * Removes any Namespace from the given name but keeps + * casing and special chars + * + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string $name + * @return string + */ + public 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 + */ + public function _resolveInterWiki(&$shortcut, $reference, &$exists = null) { + //get interwiki URL + if(isset($this->interwiki[$shortcut])) { + $url = $this->interwiki[$shortcut]; + }elseif(isset($this->interwiki['default'])) { + $shortcut = 'default'; + $url = $this->interwiki[$shortcut]; + }else{ + // not parsable interwiki outputs '' to make sure string manipluation works + $shortcut = ''; + $url = ''; + } + + //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 if($url != '') { + // make sure when no url is defined, we keep it null + // 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/platform/www/inc/parser/xhtml.php b/platform/www/inc/parser/xhtml.php new file mode 100644 index 0000000..a135130 --- /dev/null +++ b/platform/www/inc/parser/xhtml.php @@ -0,0 +1,1999 @@ +<?php + +use dokuwiki\ChangeLog\MediaChangeLog; + +/** + * Renderer for XHTML output + * + * This is DokuWiki's main renderer used to display page content in the wiki + * + * @author Harry Fuecks <hfuecks@gmail.com> + * @author Andreas Gohr <andi@splitbrain.org> + * + */ +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 string|int link pages and media against this revision */ + public $date_at = ''; + + /** @var int last section edit id, used by startSectionEdit */ + protected $lastsecid = 0; + + /** @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' + */ + public function getFormat() { + return 'xhtml'; + } + + /** + * Initialize the document + */ + public function document_start() { + //reset some internals + $this->toc = array(); + } + + /** + * Finalize the document + */ + public 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 + */ + public 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 + */ + public 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) + */ + public function section_open($level) { + $this->doc .= '<div class="level'.$level.'">'.DOKU_LF; + } + + /** + * Close the current section + */ + public function section_close() { + $this->doc .= DOKU_LF.'</div>'.DOKU_LF; + } + + /** + * Render plain text data + * + * @param $text + */ + public function cdata($text) { + $this->doc .= $this->_xmlEntities($text); + } + + /** + * Open a paragraph + */ + public function p_open() { + $this->doc .= DOKU_LF.'<p>'.DOKU_LF; + } + + /** + * Close a paragraph + */ + public function p_close() { + $this->doc .= DOKU_LF.'</p>'.DOKU_LF; + } + + /** + * Create a line break + */ + public function linebreak() { + $this->doc .= '<br/>'.DOKU_LF; + } + + /** + * Create a horizontal line + */ + public function hr() { + $this->doc .= '<hr />'.DOKU_LF; + } + + /** + * Start strong (bold) formatting + */ + public function strong_open() { + $this->doc .= '<strong>'; + } + + /** + * Stop strong (bold) formatting + */ + public function strong_close() { + $this->doc .= '</strong>'; + } + + /** + * Start emphasis (italics) formatting + */ + public function emphasis_open() { + $this->doc .= '<em>'; + } + + /** + * Stop emphasis (italics) formatting + */ + public function emphasis_close() { + $this->doc .= '</em>'; + } + + /** + * Start underline formatting + */ + public function underline_open() { + $this->doc .= '<em class="u">'; + } + + /** + * Stop underline formatting + */ + public function underline_close() { + $this->doc .= '</em>'; + } + + /** + * Start monospace formatting + */ + public function monospace_open() { + $this->doc .= '<code>'; + } + + /** + * Stop monospace formatting + */ + public function monospace_close() { + $this->doc .= '</code>'; + } + + /** + * Start a subscript + */ + public function subscript_open() { + $this->doc .= '<sub>'; + } + + /** + * Stop a subscript + */ + public function subscript_close() { + $this->doc .= '</sub>'; + } + + /** + * Start a superscript + */ + public function superscript_open() { + $this->doc .= '<sup>'; + } + + /** + * Stop a superscript + */ + public function superscript_close() { + $this->doc .= '</sup>'; + } + + /** + * Start deleted (strike-through) formatting + */ + public function deleted_open() { + $this->doc .= '<del>'; + } + + /** + * Stop deleted (strike-through) formatting + */ + public 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> + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function listitem_open($level, $node=false) { + $branching = $node ? ' node' : ''; + $this->doc .= '<li class="level'.$level.$branching.'">'; + } + + /** + * Close a list item + */ + public function listitem_close() { + $this->doc .= '</li>'.DOKU_LF; + } + + /** + * Start the content of a list item + */ + public function listcontent_open() { + $this->doc .= '<div class="li">'; + } + + /** + * Stop the content of a list item + */ + public function listcontent_close() { + $this->doc .= '</div>'.DOKU_LF; + } + + /** + * Output unformatted $text + * + * Defaults to $this->cdata() + * + * @param string $text + */ + public 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> + */ + public 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 + */ + public 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> + */ + public 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 + */ + public function htmlblock($text) { + $this->html($text, 'pre'); + } + + /** + * Start a block quote + */ + public function quote_open() { + $this->doc .= '<blockquote><div class="no">'.DOKU_LF; + } + + /** + * Stop a block quote + */ + public function quote_close() { + $this->doc .= '</div></blockquote>'.DOKU_LF; + } + + /** + * Output preformatted text + * + * @param string $text + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function multiplyentity($x, $y) { + $this->doc .= "$x×$y"; + } + + /** + * Render an opening single quote char (language specific) + */ + public function singlequoteopening() { + global $lang; + $this->doc .= $lang['singlequoteopening']; + } + + /** + * Render a closing single quote char (language specific) + */ + public function singlequoteclosing() { + global $lang; + $this->doc .= $lang['singlequoteclosing']; + } + + /** + * Render an apostrophe char (language specific) + */ + public function apostrophe() { + global $lang; + $this->doc .= $lang['apostrophe']; + } + + /** + * Render an opening double quote char (language specific) + */ + public function doublequoteopening() { + global $lang; + $this->doc .= $lang['doublequoteopening']; + } + + /** + * Render an closinging double quote char (language specific) + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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'] = ''; + $link['more'] = 'data-wiki-id="'.$id.'"'; // id is already cleaned + $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 + */ + public 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'] .= ' ugc 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 + */ + public function interwikilink($match, $name, $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) { + if($url == '') return $link['name']; + return $this->_formatLink($link); + } else { + if($url == '') $this->doc .= $link['name']; + 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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); + if($src == '' && empty($title)){ + // make sure at least something will be shown in this case + $title = $reference; + } + } + list($src, $hash) = explode('#', $src, 2); + $noLink = false; + if($src == '') { + // only output plaintext without link if there is no src + $noLink = true; + } + $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> + */ + public 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 + */ + public 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 + */ + public function table_close($pos = null) { + $this->doc .= '</table></div>'.DOKU_LF; + if($pos !== null) { + $this->finishSectionEdit($pos); + } + } + + /** + * Open a table header + */ + public function tablethead_open() { + $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF; + } + + /** + * Close a table header + */ + public function tablethead_close() { + $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF; + } + + /** + * Open a table body + */ + public function tabletbody_open() { + $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF; + } + + /** + * Close a table body + */ + public function tabletbody_close() { + $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF; + } + + /** + * Open a table footer + */ + public function tabletfoot_open() { + $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF; + } + + /** + * Close a table footer + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function tablecell_close() { + $this->doc .= '</td>'; + } + + /** + * Returns the current header level. + * (required e.g. by the filelist plugin) + * + * @return int The current header level + */ + public 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> + */ + public 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 + */ + public 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 === null || $title === "") { + // just show the sourcename + $title = $this->_xmlEntities(\dokuwiki\Utf8\PhpString::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) ? $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 $this->_xmlEntities($title ? $title : \dokuwiki\Utf8\PhpString::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 = \dokuwiki\Utf8\PhpString::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(\dokuwiki\Utf8\PhpString::basename(noNS($src))); + } + + return $ret; + } + + /** + * Escape string for output + * + * @param $string + * @return string + */ + public function _xmlEntities($string) { + return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); + } + + + + /** + * 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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(\dokuwiki\Utf8\PhpString::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 + */ + public 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(\dokuwiki\Utf8\PhpString::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) + */ + protected 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/platform/www/inc/parser/xhtmlsummary.php b/platform/www/inc/parser/xhtmlsummary.php new file mode 100644 index 0000000..4641bf8 --- /dev/null +++ b/platform/www/inc/parser/xhtmlsummary.php @@ -0,0 +1,84 @@ +<?php +/** + * 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 + protected $sum_paragraphs = 0; + protected $sum_capture = true; + protected $sum_inSection = false; + protected $sum_summary = ''; + protected $sum_pageTitle = false; + + /** @inheritdoc */ + public function document_start() { + $this->doc .= DOKU_LF.'<div>'.DOKU_LF; + } + + /** @inheritdoc */ + public function document_end() { + $this->doc = $this->sum_summary; + $this->doc .= DOKU_LF.'</div>'.DOKU_LF; + } + + /** @inheritdoc */ + public 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; + } + + /** @inheritdoc */ + public function section_open($level) { + if ( $this->sum_capture ) { + $this->sum_inSection = true; + } + } + + /** @inheritdoc */ + public function section_close() { + if ( $this->sum_capture && $this->sum_inSection ) { + $this->sum_summary .= $this->doc; + $this->sum_capture = false; + } + } + + /** @inheritdoc */ + public function p_open() { + if ( $this->sum_capture && $this->sum_paragraphs < 2 ) { + $this->sum_paragraphs++; + } + parent :: p_open(); + } + + /** @inheritdoc */ + public 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 : |