index : flyspray | |
Archlinux32 customized Flyspray installation | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | plugins/dokuwiki/inc/parser/handler.php | 1653 | ||||
-rw-r--r-- | plugins/dokuwiki/inc/parser/lexer.php | 607 | ||||
-rw-r--r-- | plugins/dokuwiki/inc/parser/parser.php | 932 | ||||
-rw-r--r-- | plugins/dokuwiki/inc/parser/renderer.php | 212 | ||||
-rw-r--r-- | plugins/dokuwiki/inc/parser/xhtml.php | 1097 |
diff --git a/plugins/dokuwiki/inc/parser/handler.php b/plugins/dokuwiki/inc/parser/handler.php new file mode 100644 index 0000000..bd307d6 --- /dev/null +++ b/plugins/dokuwiki/inc/parser/handler.php @@ -0,0 +1,1653 @@ +<?php +if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); + +class Doku_Handler { + + var $Renderer = NULL; + + var $CallWriter = NULL; + + var $calls = array(); + + var $status = array( + 'section' => FALSE, + 'section_edit_start' => -1, + 'section_edit_level' => 1, + 'section_edit_title' => '' + ); + + var $rewriteBlocks = TRUE; + + function __construct() { + $this->CallWriter = new Doku_Handler_CallWriter($this); + } + + function _addCall($handler, $args, $pos) { + $call = array($handler,$args, $pos); + $this->CallWriter->writeCall($call); + } + + function _finalize(){ + + $this->CallWriter->finalise(); + + if ( $this->status['section'] ) { + $last_call = end($this->calls); + array_push($this->calls,array('section_close',array(), $last_call[2])); + if ($this->status['section_edit_start']>1) { + // ignore last edit section if there is only one header + array_push($this->calls,array('section_edit',array($this->status['section_edit_start'], 0, $this->status['section_edit_level'], $this->status['section_edit_title']), $last_call[2])); + } + } + + if ( $this->rewriteBlocks ) { + $B = new Doku_Handler_Block(); + $this->calls = $B->process($this->calls); + } + + trigger_event('PARSER_HANDLER_DONE',$this); + + array_unshift($this->calls,array('document_start',array(),0)); + $last_call = end($this->calls); + array_push($this->calls,array('document_end',array(),$last_call[2])); + } + + function fetch() { + $call = each($this->calls); + if ( $call ) { + return $call['value']; + } + return FALSE; + } + + + /** + * Special plugin handler + * + * This handler is called for all modes starting with 'plugin_'. + * An additional parameter with the plugin name is passed + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function plugin($match, $state, $pos, $pluginname){ + $data = array($match); + $plugin =& plugin_load('syntax',$pluginname); + if($plugin != null){ + $data = $plugin->handle($match, $state, $pos, $this); + } + $this->_addCall('plugin',array($pluginname,$data,$state),$pos); + return TRUE; + } + + function base($match, $state, $pos) { + switch ( $state ) { + case DOKU_LEXER_UNMATCHED: + $this->_addCall('cdata',array($match), $pos); + return TRUE; + break; + + } + } + + function header($match, $state, $pos) { + global $conf; + + // 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); + + if ($level<=$conf['maxseclevel']) { + $this->_addCall('section_edit',array($this->status['section_edit_start'], $pos-1, $this->status['section_edit_level'], $this->status['section_edit_title']), $pos); + $this->status['section_edit_start'] = $pos; + $this->status['section_edit_level'] = $level; + $this->status['section_edit_title'] = $title; + } + + $this->_addCall('header',array($title,$level,$pos), $pos); + + $this->_addCall('section_open',array($level),$pos); + $this->status['section'] = TRUE; + return TRUE; + } + + function notoc($match, $state, $pos) { + $this->_addCall('notoc',array(),$pos); + return TRUE; + } + + function nocache($match, $state, $pos) { + $this->_addCall('nocache',array(),$pos); + return TRUE; + } + + function linebreak($match, $state, $pos) { + $this->_addCall('linebreak',array(),$pos); + return TRUE; + } + + function eol($match, $state, $pos) { + $this->_addCall('eol',array(),$pos); + return TRUE; + } + + function hr($match, $state, $pos) { + $this->_addCall('hr',array(),$pos); + return TRUE; + } + + function _nestingTag($match, $state, $pos, $name) { + switch ( $state ) { + case DOKU_LEXER_ENTER: + $this->_addCall($name.'_open', array(), $pos); + break; + case DOKU_LEXER_EXIT: + $this->_addCall($name.'_close', array(), $pos); + break; + case DOKU_LEXER_UNMATCHED: + $this->_addCall('cdata',array($match), $pos); + break; + } + } + + function strong($match, $state, $pos) { + $this->_nestingTag($match, $state, $pos, 'strong'); + return TRUE; + } + + function emphasis($match, $state, $pos) { + $this->_nestingTag($match, $state, $pos, 'emphasis'); + return TRUE; + } + + function underline($match, $state, $pos) { + $this->_nestingTag($match, $state, $pos, 'underline'); + return TRUE; + } + + function monospace($match, $state, $pos) { + $this->_nestingTag($match, $state, $pos, 'monospace'); + return TRUE; + } + + function subscript($match, $state, $pos) { + $this->_nestingTag($match, $state, $pos, 'subscript'); + return TRUE; + } + + function superscript($match, $state, $pos) { + $this->_nestingTag($match, $state, $pos, 'superscript'); + return TRUE; + } + + function deleted($match, $state, $pos) { + $this->_nestingTag($match, $state, $pos, 'deleted'); + return TRUE; + } + + + function footnote($match, $state, $pos) { +// $this->_nestingTag($match, $state, $pos, 'footnote'); + if (!isset($this->_footnote)) $this->_footnote = false; + + switch ( $state ) { + case DOKU_LEXER_ENTER: + // footnotes can not be nested - however due to limitations in lexer it can't be prevented + // we will still enter a new footnote mode, we just do nothing + if ($this->_footnote) { + $this->_addCall('cdata',array($match), $pos); + break; + } + + $this->_footnote = true; + + $ReWriter = new Doku_Handler_Nest($this->CallWriter,'footnote_close'); + $this->CallWriter = & $ReWriter; + $this->_addCall('footnote_open', array(), $pos); + break; + case DOKU_LEXER_EXIT: + // check whether we have already exitted the footnote mode, can happen if the modes were nested + if (!$this->_footnote) { + $this->_addCall('cdata',array($match), $pos); + break; + } + + $this->_footnote = false; + + $this->_addCall('footnote_close', array(), $pos); + $this->CallWriter->process(); + $ReWriter = & $this->CallWriter; + $this->CallWriter = & $ReWriter->CallWriter; + break; + case DOKU_LEXER_UNMATCHED: + $this->_addCall('cdata', array($match), $pos); + break; + } + return TRUE; + } + + function listblock($match, $state, $pos) { + switch ( $state ) { + case DOKU_LEXER_ENTER: + $ReWriter = new Doku_Handler_List($this->CallWriter); + $this->CallWriter = & $ReWriter; + $this->_addCall('list_open', array($match), $pos); + break; + case DOKU_LEXER_EXIT: + $this->_addCall('list_close', array(), $pos); + $this->CallWriter->process(); + $ReWriter = & $this->CallWriter; + $this->CallWriter = & $ReWriter->CallWriter; + break; + case DOKU_LEXER_MATCHED: + $this->_addCall('list_item', array($match), $pos); + break; + case DOKU_LEXER_UNMATCHED: + $this->_addCall('cdata', array($match), $pos); + break; + } + return TRUE; + } + + function unformatted($match, $state, $pos) { + if ( $state == DOKU_LEXER_UNMATCHED ) { + $this->_addCall('unformatted',array($match), $pos); + } + return TRUE; + } + + function php($match, $state, $pos) { + global $conf; + if ( $state == DOKU_LEXER_UNMATCHED ) { + if ($conf['phpok']) { + $this->_addCall('php',array($match), $pos); + } else { + $this->_addCall('file',array($match), $pos); + } + } + return TRUE; + } + + function html($match, $state, $pos) { + global $conf; + if ( $state == DOKU_LEXER_UNMATCHED ) { + if($conf['htmlok']){ + $this->_addCall('html',array($match), $pos); + } else { + $this->_addCall('file',array($match), $pos); + } + } + return TRUE; + } + + function preformatted($match, $state, $pos) { + switch ( $state ) { + case DOKU_LEXER_ENTER: + $ReWriter = new Doku_Handler_Preformatted($this->CallWriter); + $this->CallWriter = & $ReWriter; + $this->_addCall('preformatted_start',array(), $pos); + break; + case DOKU_LEXER_EXIT: + $this->_addCall('preformatted_end',array(), $pos); + $this->CallWriter->process(); + $ReWriter = & $this->CallWriter; + $this->CallWriter = & $ReWriter->CallWriter; + break; + case DOKU_LEXER_MATCHED: + $this->_addCall('preformatted_newline',array(), $pos); + break; + case DOKU_LEXER_UNMATCHED: + $this->_addCall('preformatted_content',array($match), $pos); + break; + } + + return TRUE; + } + + function file($match, $state, $pos) { + if ( $state == DOKU_LEXER_UNMATCHED ) { + $this->_addCall('file',array($match), $pos); + } + return TRUE; + } + + function quote($match, $state, $pos) { + + switch ( $state ) { + + case DOKU_LEXER_ENTER: + $ReWriter = new Doku_Handler_Quote($this->CallWriter); + $this->CallWriter = & $ReWriter; + $this->_addCall('quote_start',array($match), $pos); + break; + + case DOKU_LEXER_EXIT: + $this->_addCall('quote_end',array(), $pos); + $this->CallWriter->process(); + $ReWriter = & $this->CallWriter; + $this->CallWriter = & $ReWriter->CallWriter; + break; + + case DOKU_LEXER_MATCHED: + $this->_addCall('quote_newline',array($match), $pos); + break; + + case DOKU_LEXER_UNMATCHED: + $this->_addCall('cdata',array($match), $pos); + break; + + } + + return TRUE; + } + + function code($match, $state, $pos) { + switch ( $state ) { + case DOKU_LEXER_UNMATCHED: + $matches = preg_split('/>/u',$match,2); + $matches[0] = trim($matches[0]); + if ( trim($matches[0]) == '' ) { + $matches[0] = NULL; + } + # $matches[0] contains name of programming language + # if available, We shortcut html here. + if($matches[0] == 'html') $matches[0] = 'html4strict'; + $this->_addCall( + 'code', + array($matches[1],$matches[0]), + $pos + ); + break; + } + return TRUE; + } + + function acronym($match, $state, $pos) { + $this->_addCall('acronym',array($match), $pos); + return TRUE; + } + + function smiley($match, $state, $pos) { + $this->_addCall('smiley',array($match), $pos); + return TRUE; + } + + function wordblock($match, $state, $pos) { + $this->_addCall('wordblock',array($match), $pos); + return TRUE; + } + + function entity($match, $state, $pos) { + $this->_addCall('entity',array($match), $pos); + return TRUE; + } + + function multiplyentity($match, $state, $pos) { + preg_match_all('/\d+/',$match,$matches); + $this->_addCall('multiplyentity',array($matches[0][0],$matches[0][1]), $pos); + return TRUE; + } + + function singlequoteopening($match, $state, $pos) { + $this->_addCall('singlequoteopening',array(), $pos); + return TRUE; + } + + function singlequoteclosing($match, $state, $pos) { + $this->_addCall('singlequoteclosing',array(), $pos); + return TRUE; + } + + function doublequoteopening($match, $state, $pos) { + $this->_addCall('doublequoteopening',array(), $pos); + return TRUE; + } + + function doublequoteclosing($match, $state, $pos) { + $this->_addCall('doublequoteclosing',array(), $pos); + return TRUE; + } + + function camelcaselink($match, $state, $pos) { + $this->_addCall('camelcaselink',array($match), $pos); + return TRUE; + } + + /* + */ + function internallink($match, $state, $pos) { + // Strip the opening and closing markup + $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match); + + // Split title from URL + $link = preg_split('/\|/u',$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 ( preg_match('/^[a-zA-Z\.]+>{1}.*$/u',$link[0]) ) { + // Interwiki + $interwiki = preg_split('/>/u',$link[0]); + $this->_addCall( + 'interwikilink', + array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]), + $pos + ); + }elseif ( preg_match('/^\\\\\\\\[\w.:?\-;,]+?\\\\/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('#([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i',$link[0]) ) { + // E-Mail + $this->_addCall( + 'emaillink', + array($link[0],$link[1]), + $pos + ); + }elseif ( preg_match('!^#.+!',$link[0]) ){ + // local link + $this->_addCall( + 'locallink', + array(substr($link[0],1),$link[1]), + $pos + ); + }else{ + // internal link + $this->_addCall( + 'internallink', + array($link[0],$link[1]), + $pos + ); + } + + return TRUE; + } + + function filelink($match, $state, $pos) { + $this->_addCall('filelink',array($match, NULL), $pos); + return TRUE; + } + + function windowssharelink($match, $state, $pos) { + $this->_addCall('windowssharelink',array($match, NULL), $pos); + return TRUE; + } + + function media($match, $state, $pos) { + $p = Doku_Handler_Parse_Media($match); + + $this->_addCall( + $p['type'], + array($p['src'], $p['title'], $p['align'], $p['width'], + $p['height'], $p['cache'], $p['linking']), + $pos + ); + return TRUE; + } + + function rss($match, $state, $pos) { + $link = preg_replace(array('/^\{\{rss>/','/\}\}$/'),'',$match); + + // get params + list($link,$params) = explode(' ',$link,2); + + $p = array(); + if(preg_match('/\b(\d+)\b/',$params,$match)){ + $p['max'] = $match[1]; + }else{ + $p['max'] = 8; + } + $p['reverse'] = (preg_match('/rev/',$params)); + $p['author'] = (preg_match('/\b(by|author)/',$params)); + $p['date'] = (preg_match('/\b(date)/',$params)); + $p['details'] = (preg_match('/\b(desc|detail)/',$params)); + + if (preg_match('/\b(\d+)([dhm])\b/',$params,$match)) { + $period = array('d' => 86400, 'h' => 3600, 'm' => 60); + $p['refresh'] = max(600,$match[1]*$period[$match[2]]); // n * period in seconds, minimum 10 minutes + } else { + $p['refresh'] = 14400; // default to 4 hours + } + + $this->_addCall('rss',array($link,$p),$pos); + return TRUE; + } + + function externallink($match, $state, $pos) { + // Prevent use of multibyte strings in URLs + // See: http://www.boingboing.net/2005/02/06/shmoo_group_exploit_.html + // Not worried about other charsets so long as page is output as UTF-8 + /*if ( strlen($match) != utf8_strlen($match) ) { + $this->_addCall('cdata',array($match), $pos); + } else {*/ + + $this->_addCall('externallink',array($match, NULL), $pos); + //} + return TRUE; + } + + function emaillink($match, $state, $pos) { + $email = preg_replace(array('/^</','/>$/'),'',$match); + $this->_addCall('emaillink',array($email, NULL), $pos); + return TRUE; + } + + function table($match, $state, $pos) { + switch ( $state ) { + + case DOKU_LEXER_ENTER: + + $ReWriter = new Doku_Handler_Table($this->CallWriter); + $this->CallWriter = & $ReWriter; + + $this->_addCall('table_start', array(), $pos); + //$this->_addCall('table_row', array(), $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); + $this->CallWriter->process(); + $ReWriter = & $this->CallWriter; + $this->CallWriter = & $ReWriter->CallWriter; + break; + + case DOKU_LEXER_UNMATCHED: + if ( trim($match) != '' ) { + $this->_addCall('cdata',array($match), $pos); + } + break; + + case DOKU_LEXER_MATCHED: + if ( $match == ' ' ){ + $this->_addCall('cdata', array($match), $pos); + } else if ( preg_match('/\t+/',$match) ) { + $this->_addCall('table_align', array($match), $pos); + } else if ( preg_match('/ {2,}/',$match) ) { + $this->_addCall('table_align', array($match), $pos); + } else if ( $match == "\n|" ) { + $this->_addCall('table_row', array(), $pos); + $this->_addCall('tablecell', array(), $pos); + } else if ( $match == "\n^" ) { + $this->_addCall('table_row', array(), $pos); + $this->_addCall('tableheader', array(), $pos); + } else if ( $match == '|' ) { + $this->_addCall('tablecell', array(), $pos); + } else if ( $match == '^' ) { + $this->_addCall('tableheader', array(), $pos); + } + break; + } + return TRUE; + } +} + +//------------------------------------------------------------------------ +function Doku_Handler_Parse_Media($match) { + + // Strip the opening and closing markup + $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match); + + // Split title from URL + $link = preg_split('/\|/u',$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)){ + ($size[1]) ? $w = $size[1] : $w = NULL; + ($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{ + $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 + if ( preg_match('#^(https?|ftp)#i',$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; +} + +//------------------------------------------------------------------------ +class Doku_Handler_CallWriter { + + var $Handler; + + function __construct(& $Handler) { + $this->Handler = & $Handler; + } + + function writeCall($call) { + $this->Handler->calls[] = $call; + } + + function writeCalls($calls) { + $this->Handler->calls = array_merge($this->Handler->calls, $calls); + } + + // function is required, but since this call writer is first/highest in + // the chain it is not required to do anything + function finalise() { + } +} + +//------------------------------------------------------------------------ +/** + * Generic call writer class to handle nesting of rendering instructions + * within a render instruction. Also see nest() method of renderer base class + * + * @author Chris Smith <chris@jalakai.co.uk> + */ +class Doku_Handler_Nest { + + var $CallWriter; + var $calls = array(); + + var $closingInstruction; + + /** + * constructor + * + * @param object $CallWriter the renderers current call writer + * @param string $close closing instruction name, this is required to properly terminate the + * syntax mode if the document ends without a closing pattern + */ + function __construct(& $CallWriter, $close="nest_close") { + $this->CallWriter = & $CallWriter; + + $this->closingInstruction = $close; + } + + function writeCall($call) { + $this->calls[] = $call; + } + + function writeCalls($calls) { + $this->calls = array_merge($this->calls, $calls); + } + + function finalise() { + $last_call = end($this->calls); + $this->writeCall(array($this->closingInstruction,array(), $last_call[2])); + + $this->process(); + $this->CallWriter->finalise(); + } + + function process() { + $first_call = reset($this->calls); + $this->CallWriter->writeCall(array("nest", array($this->calls), $first_call[2])); + } +} + +class Doku_Handler_List { + + var $CallWriter; + + var $calls = array(); + var $listCalls = array(); + var $listStack = array(); + + function __construct(& $CallWriter) { + $this->CallWriter = & $CallWriter; + } + + function writeCall($call) { + $this->calls[] = $call; + } + + // Probably not needed but just in case... + function writeCalls($calls) { + $this->calls = array_merge($this->calls, $calls); +# $this->CallWriter->writeCalls($this->calls); + } + + function finalise() { + $last_call = end($this->calls); + $this->writeCall(array('list_close',array(), $last_call[2])); + + $this->process(); + $this->CallWriter->finalise(); + } + + //------------------------------------------------------------------------ + function process() { + + foreach ( $this->calls as $call ) { + switch ($call[0]) { + case 'list_item': + $this->listOpen($call); + break; + case 'list_open': + $this->listStart($call); + break; + case 'list_close': + $this->listEnd($call); + break; + default: + $this->listContent($call); + break; + } + } + + $this->CallWriter->writeCalls($this->listCalls); + } + + //------------------------------------------------------------------------ + function listStart($call) { + $depth = $this->interpretSyntax($call[1][0], $listType); + + $this->initialDepth = $depth; + $this->listStack[] = array($listType, $depth); + + $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]); + $this->listCalls[] = array('listitem_open',array(1),$call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + } + + //------------------------------------------------------------------------ + function listEnd($call) { + $closeContent = TRUE; + + while ( $list = array_pop($this->listStack) ) { + if ( $closeContent ) { + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $closeContent = FALSE; + } + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]); + } + } + + //------------------------------------------------------------------------ + function listOpen($call) { + $depth = $this->interpretSyntax($call[1][0], $listType); + $end = end($this->listStack); + + // Not allowed to be shallower than initialDepth + if ( $depth < $this->initialDepth ) { + $depth = $this->initialDepth; + } + + //------------------------------------------------------------------------ + if ( $depth == $end[1] ) { + + // Just another item in the list... + if ( $listType == $end[0] ) { + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + // Switched list type... + } else { + + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); + $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); + $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + array_pop($this->listStack); + $this->listStack[] = array($listType, $depth); + } + + //------------------------------------------------------------------------ + // Getting deeper... + } else if ( $depth > $end[1] ) { + + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); + $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + $this->listStack[] = array($listType, $depth); + + //------------------------------------------------------------------------ + // Getting shallower ( $depth < $end[1] ) + } else { + $this->listCalls[] = array('listcontent_close',array(),$call[2]); + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); + + // Throw away the end - done + array_pop($this->listStack); + + while (1) { + $end = end($this->listStack); + + if ( $end[1] <= $depth ) { + + // Normalize depths + $depth = $end[1]; + + $this->listCalls[] = array('listitem_close',array(),$call[2]); + + if ( $end[0] == $listType ) { + $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + } else { + // Switching list type... + $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); + $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); + $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); + $this->listCalls[] = array('listcontent_open',array(),$call[2]); + + array_pop($this->listStack); + $this->listStack[] = array($listType, $depth); + } + + break; + + // Haven't dropped down far enough yet.... ( $end[1] > $depth ) + } else { + + $this->listCalls[] = array('listitem_close',array(),$call[2]); + $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); + + array_pop($this->listStack); + + } + + } + + } + } + + //------------------------------------------------------------------------ + function listContent($call) { + $this->listCalls[] = $call; + } + + //------------------------------------------------------------------------ + function interpretSyntax($match, & $type) { + if ( substr($match,-1) == '*' ) { + $type = 'u'; + } else { + $type = 'o'; + } + return count(explode(' ',str_replace("\t",' ',$match))); + } +} + +//------------------------------------------------------------------------ +class Doku_Handler_Preformatted { + + var $CallWriter; + + var $calls = array(); + var $pos; + var $text =''; + + + + function __construct(& $CallWriter) { + $this->CallWriter = & $CallWriter; + } + + function writeCall($call) { + $this->calls[] = $call; + } + + // Probably not needed but just in case... + function writeCalls($calls) { + $this->calls = array_merge($this->calls, $calls); +# $this->CallWriter->writeCalls($this->calls); + } + + function finalise() { + $last_call = end($this->calls); + $this->writeCall(array('preformatted_end',array(), $last_call[2])); + + $this->process(); + $this->CallWriter->finalise(); + } + + function process() { + foreach ( $this->calls as $call ) { + switch ($call[0]) { + case 'preformatted_start': + $this->pos = $call[2]; + break; + case 'preformatted_newline': + $this->text .= "\n"; + break; + case 'preformatted_content': + $this->text .= $call[1][0]; + break; + case 'preformatted_end': + $this->CallWriter->writeCall(array('preformatted',array($this->text),$this->pos)); + break; + } + } + } + +} + +//------------------------------------------------------------------------ +class Doku_Handler_Quote { + + var $CallWriter; + + var $calls = array(); + + var $quoteCalls = array(); + + function __construct(& $CallWriter) { + $this->CallWriter = & $CallWriter; + } + + function writeCall($call) { + $this->calls[] = $call; + } + + // Probably not needed but just in case... + function writeCalls($calls) { + $this->calls = array_merge($this->calls, $calls); +# $this->CallWriter->writeCalls($this->calls); + } + + function finalise() { + $last_call = end($this->calls); + $this->writeCall(array('quote_end',array(), $last_call[2])); + + $this->process(); + $this->CallWriter->finalise(); + } + + function process() { + + $quoteDepth = 1; + + foreach ( $this->calls as $call ) { + switch ($call[0]) { + + case 'quote_start': + + $this->quoteCalls[] = array('quote_open',array(),$call[2]); + + case 'quote_newline': + + $quoteLength = $this->getDepth($call[1][0]); + + if ( $quoteLength > $quoteDepth ) { + $quoteDiff = $quoteLength - $quoteDepth; + for ( $i = 1; $i <= $quoteDiff; $i++ ) { + $this->quoteCalls[] = array('quote_open',array(),$call[2]); + } + } else if ( $quoteLength < $quoteDepth ) { + $quoteDiff = $quoteDepth - $quoteLength; + for ( $i = 1; $i <= $quoteDiff; $i++ ) { + $this->quoteCalls[] = array('quote_close',array(),$call[2]); + } + } else { + if ($call[0] != 'quote_start') $this->quoteCalls[] = array('linebreak',array(),$call[2]); + } + + $quoteDepth = $quoteLength; + + break; + + case 'quote_end': + + if ( $quoteDepth > 1 ) { + $quoteDiff = $quoteDepth - 1; + for ( $i = 1; $i <= $quoteDiff; $i++ ) { + $this->quoteCalls[] = array('quote_close',array(),$call[2]); + } + } + + $this->quoteCalls[] = array('quote_close',array(),$call[2]); + + $this->CallWriter->writeCalls($this->quoteCalls); + break; + + default: + $this->quoteCalls[] = $call; + break; + } + } + } + + function getDepth($marker) { + preg_match('/>{1,}/', $marker, $matches); + $quoteLength = strlen($matches[0]); + return $quoteLength; + } +} + +//------------------------------------------------------------------------ +class Doku_Handler_Table { + + var $CallWriter; + + var $calls = array(); + var $tableCalls = array(); + var $maxCols = 0; + var $maxRows = 1; + var $currentCols = 0; + var $firstCell = FALSE; + var $lastCellType = 'tablecell'; + + function __construct(& $CallWriter) { + $this->CallWriter = & $CallWriter; + } + + function writeCall($call) { + $this->calls[] = $call; + } + + // Probably not needed but just in case... + function writeCalls($calls) { + $this->calls = array_merge($this->calls, $calls); +# $this->CallWriter->writeCalls($this->calls); + } + + function finalise() { + $last_call = end($this->calls); + $this->writeCall(array('table_end',array(), $last_call[2])); + + $this->process(); + $this->CallWriter->finalise(); + } + + //------------------------------------------------------------------------ + function process() { + foreach ( $this->calls as $call ) { + switch ( $call[0] ) { + case 'table_start': + $this->tableStart($call); + break; + case 'table_row': + $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); + $this->tableRowOpen(array('tablerow_open',$call[1],$call[2])); + break; + case 'tableheader': + case 'tablecell': + $this->tableCell($call); + break; + case 'table_end': + $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); + $this->tableEnd($call); + break; + default: + $this->tableDefault($call); + break; + } + } + $this->CallWriter->writeCalls($this->tableCalls); + } + + function tableStart($call) { + $this->tableCalls[] = array('table_open',array(),$call[2]); + $this->tableCalls[] = array('tablerow_open',array(),$call[2]); + $this->firstCell = TRUE; + } + + function tableEnd($call) { + $this->tableCalls[] = array('table_close',array(),$call[2]); + $this->finalizeTable(); + } + + function tableRowOpen($call) { + $this->tableCalls[] = $call; + $this->currentCols = 0; + $this->firstCell = TRUE; + $this->lastCellType = 'tablecell'; + $this->maxRows++; + } + + function tableRowClose($call) { + // Strip off final cell opening and anything after it + while ( $discard = array_pop($this->tableCalls ) ) { + + if ( $discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') { + + // Its a spanning element - put it back and close it + if ( $discard[1][0] > 1 ) { + + $this->tableCalls[] = $discard; + if ( strstr($discard[0],'cell') ) { + $name = 'tablecell'; + } else { + $name = 'tableheader'; + } + $this->tableCalls[] = array($name.'_close',array(),$call[2]); + } + + break; + } + } + $this->tableCalls[] = $call; + + if ( $this->currentCols > $this->maxCols ) { + $this->maxCols = $this->currentCols; + } + } + + function tableCell($call) { + if ( !$this->firstCell ) { + + // Increase the span + $lastCall = end($this->tableCalls); + + // A cell call which follows an open cell means an empty cell so span + if ( $lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open' ) { + $this->tableCalls[] = array('colspan',array(),$call[2]); + + } + + $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]); + $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); + $this->lastCellType = $call[0]; + + } else { + + $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); + $this->lastCellType = $call[0]; + $this->firstCell = FALSE; + + } + + $this->currentCols++; + } + + function tableDefault($call) { + $this->tableCalls[] = $call; + } + + function finalizeTable() { + + // Add the max cols and rows to the table opening + if ( $this->tableCalls[0][0] == 'table_open' ) { + // Adjust to num cols not num col delimeters + $this->tableCalls[0][1][] = $this->maxCols - 1; + $this->tableCalls[0][1][] = $this->maxRows; + } else { + trigger_error('First element in table call list is not table_open'); + } + + $lastRow = 0; + $lastCell = 0; + $toDelete = array(); + + // Look for the colspan elements and increment the colspan on the + // previous non-empty opening cell. Once done, delete all the cells + // that contain colspans + foreach ( $this->tableCalls as $key => $call ) { + + if ( $call[0] == 'tablerow_open' ) { + + $lastRow = $key; + + } else if ( $call[0] == 'tablecell_open' || $call[0] == 'tableheader_open' ) { + + $lastCell = $key; + + } else if ( $call[0] == 'table_align' ) { + + // If the previous element was a cell open, align right + if ( $this->tableCalls[$key-1][0] == 'tablecell_open' || $this->tableCalls[$key-1][0] == 'tableheader_open' ) { + $this->tableCalls[$key-1][1][1] = 'right'; + + // If the next element if the close of an element, align either center or left + } else if ( $this->tableCalls[$key+1][0] == 'tablecell_close' || $this->tableCalls[$key+1][0] == 'tableheader_close' ) { + if ( $this->tableCalls[$lastCell][1][1] == 'right' ) { + $this->tableCalls[$lastCell][1][1] = 'center'; + } else { + $this->tableCalls[$lastCell][1][1] = 'left'; + } + + } + + // Now convert the whitespace back to cdata + $this->tableCalls[$key][0] = 'cdata'; + + } else if ( $call[0] == 'colspan' ) { + + $this->tableCalls[$key-1][1][0] = FALSE; + + for($i = $key-2; $i > $lastRow; $i--) { + + if ( $this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open' ) { + + if ( FALSE !== $this->tableCalls[$i][1][0] ) { + $this->tableCalls[$i][1][0]++; + break; + } + + + } + } + + $toDelete[] = $key-1; + $toDelete[] = $key; + $toDelete[] = $key+1; + } + } + + + // condense cdata + $cnt = count($this->tableCalls); + for( $key = 0; $key < $cnt; $key++){ + if($this->tableCalls[$key][0] == 'cdata'){ + $ckey = $key; + $key++; + while($this->tableCalls[$key][0] == 'cdata'){ + $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0]; + $toDelete[] = $key; + $key++; + } + continue; + } + } + + foreach ( $toDelete as $delete ) { + unset($this->tableCalls[$delete]); + } + $this->tableCalls = array_values($this->tableCalls); + } +} + +//------------------------------------------------------------------------ +class Doku_Handler_Section { + + function process($calls) { + + $sectionCalls = array(); + $inSection = FALSE; + + foreach ( $calls as $call ) { + + if ( $call[0] == 'header' ) { + + if ( $inSection ) { + $sectionCalls[] = array('section_close',array(), $call[2]); + } + + $sectionCalls[] = $call; + $sectionCalls[] = array('section_open',array($call[1][1]), $call[2]); + $inSection = TRUE; + + } else { + + if ($call[0] == 'section_open' ) { + $inSection = TRUE; + } else if ($call[0] == 'section_open' ) { + $inSection = FALSE; + } + $sectionCalls[] = $call; + } + } + + if ( $inSection ) { + $sectionCalls[] = array('section_close',array(), $call[2]); + } + + return $sectionCalls; + } + +} + +/** + * Handler for paragraphs + * + * @author Harry Fuecks <hfuecks@gmail.com> + */ +class Doku_Handler_Block { + + var $calls = array(); + + var $blockStack = array(); + + var $inParagraph = FALSE; + var $atStart = TRUE; + var $skipEolKey = -1; + + // Blocks these should not be inside paragraphs + var $blockOpen = array( + 'header', + 'listu_open','listo_open','listitem_open','listcontent_open', + 'table_open','tablerow_open','tablecell_open','tableheader_open', + 'quote_open', + 'section_open', // Needed to prevent p_open between header and section_open + 'code','file','hr','preformatted','rss', + ); + + var $blockClose = array( + 'header', + 'listu_close','listo_close','listitem_close','listcontent_close', + 'table_close','tablerow_close','tablecell_close','tableheader_close', + 'quote_close', + 'section_close', // Needed to prevent p_close after section_close + 'code','file','hr','preformatted','rss', + ); + + // Stacks can contain paragraphs + var $stackOpen = array( + 'footnote_open','section_open', + ); + + var $stackClose = array( + 'footnote_close','section_close', + ); + + + /** + * Constructor. Adds loaded syntax plugins to the block and stack + * arrays + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function __construct(){ + global $DOKU_PLUGINS; + //check if syntax plugins were loaded + if(empty($DOKU_PLUGINS['syntax'])) return; + foreach($DOKU_PLUGINS['syntax'] as $n => $p){ + $ptype = $p->getPType(); + if($ptype == 'block'){ + $this->blockOpen[] = 'plugin_'.$n; + $this->blockClose[] = 'plugin_'.$n; + }elseif($ptype == 'stack'){ + $this->stackOpen[] = 'plugin_'.$n; + $this->stackClose[] = 'plugin_'.$n; + } + } + } + + /** + * Close a paragraph if needed + * + * This function makes sure there are no empty paragraphs on the stack + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function closeParagraph($pos){ + // look back if there was any content - we don't want empty paragraphs + $content = ''; + for($i=count($this->calls)-1; $i>=0; $i--){ + if($this->calls[$i][0] == 'p_open'){ + break; + }elseif($this->calls[$i][0] == 'cdata'){ + $content .= $this->calls[$i][1][0]; + }else{ + $content = 'found markup'; + break; + } + } + + if(trim($content)==''){ + //remove the whole paragraph + array_splice($this->calls,$i); + }else{ + if ($this->calls[count($this->calls)-1][0] == 'section_edit') { + $tmp = array_pop($this->calls); + $this->calls[] = array('p_close',array(), $pos); + $this->calls[] = $tmp; + } else { + $this->calls[] = array('p_close',array(), $pos); + } + } + + $this->inParagraph = FALSE; + } + + /** + * Processes the whole instruction stack to open and close paragraphs + * + * @author Harry Fuecks <hfuecks@gmail.com> + * @author Andreas Gohr <andi@splitbrain.org> + * @todo This thing is really messy and should be rewritten + */ + function process($calls) { + foreach ( $calls as $key => $call ) { + $cname = $call[0]; + if($cname == 'plugin') { + $cname='plugin_'.$call[1][0]; + + $plugin = true; + $plugin_open = (($call[1][2] == DOKU_LEXER_ENTER) || ($call[1][2] == DOKU_LEXER_SPECIAL)); + $plugin_close = (($call[1][2] == DOKU_LEXER_EXIT) || ($call[1][2] == DOKU_LEXER_SPECIAL)); + } else { + $plugin = false; + } + + // Process blocks which are stack like... (contain linefeeds) + if ( in_array($cname,$this->stackOpen ) && (!$plugin || $plugin_open) ) { + + $this->calls[] = $call; + + // Hack - footnotes shouldn't immediately contain a p_open + if ( $cname != 'footnote_open' ) { + $this->addToStack(); + } else { + $this->addToStack(FALSE); + } + continue; + } + + if ( in_array($cname,$this->stackClose ) && (!$plugin || $plugin_close)) { + + if ( $this->inParagraph ) { + $this->closeParagraph($call[2]); + } + $this->calls[] = $call; + $this->removeFromStack(); + continue; + } + + if ( !$this->atStart ) { + + if ( $cname == 'eol' ) { + + // Check this isn't an eol instruction to skip... + if ( $this->skipEolKey != $key ) { + // Look to see if the next instruction is an EOL + if ( isset($calls[$key+1]) && $calls[$key+1][0] == 'eol' ) { + + if ( $this->inParagraph ) { + //$this->calls[] = array('p_close',array(), $call[2]); + $this->closeParagraph($call[2]); + } + + $this->calls[] = array('p_open',array(), $call[2]); + $this->inParagraph = TRUE; + + + // Mark the next instruction for skipping + $this->skipEolKey = $key+1; + + }else{ + //if this is just a single eol make a space from it + $this->calls[] = array('cdata',array(" "), $call[2]); + } + } + + + } else { + + $storeCall = TRUE; + if ( $this->inParagraph && (in_array($cname, $this->blockOpen) && (!$plugin || $plugin_open))) { + $this->closeParagraph($call[2]); + $this->calls[] = $call; + $storeCall = FALSE; + } + + if ( in_array($cname, $this->blockClose) && (!$plugin || $plugin_close)) { + if ( $this->inParagraph ) { + $this->closeParagraph($call[2]); + } + if ( $storeCall ) { + $this->calls[] = $call; + $storeCall = FALSE; + } + + // This really sucks and suggests this whole class sucks but... + if ( isset($calls[$key+1])) { + $cname_plusone = $calls[$key+1][0]; + if ($cname_plusone == 'plugin') { + $cname_plusone = 'plugin'.$calls[$key+1][1][0]; + + // plugin test, true if plugin has a state which precludes it requiring blockOpen or blockClose + $plugin_plusone = true; + $plugin_test = ($call[$key+1][1][2] == DOKU_LEXER_MATCHED) || ($call[$key+1][1][2] == DOKU_LEXER_MATCHED); + } else { + $plugin_plusone = false; + } + if ((!in_array($cname_plusone, $this->blockOpen) && !in_array($cname_plusone, $this->blockClose)) || + ($plugin_plusone && $plugin_test) + ) { + + $this->calls[] = array('p_open',array(), $call[2]); + $this->inParagraph = TRUE; + } + } + } + + if ( $storeCall ) { + $this->calls[] = $call; + } + + } + + + } else { + + // Unless there's already a block at the start, start a paragraph + if ( !in_array($cname,$this->blockOpen) ) { + $this->calls[] = array('p_open',array(), $call[2]); + if ( $call[0] != 'eol' ) { + $this->calls[] = $call; + } + $this->atStart = FALSE; + $this->inParagraph = TRUE; + } else { + $this->calls[] = $call; + $this->atStart = FALSE; + } + + } + + } + + if ( $this->inParagraph ) { + if ( $cname == 'p_open' ) { + // Ditch the last call + array_pop($this->calls); + } else if ( !in_array($cname, $this->blockClose) ) { + //$this->calls[] = array('p_close',array(), $call[2]); + $this->closeParagraph($call[2]); + } else { + $last_call = array_pop($this->calls); + //$this->calls[] = array('p_close',array(), $call[2]); + $this->closeParagraph($call[2]); + $this->calls[] = $last_call; + } + } + + return $this->calls; + } + + function addToStack($newStart = TRUE) { + $this->blockStack[] = array($this->atStart, $this->inParagraph); + $this->atStart = $newStart; + $this->inParagraph = FALSE; + } + + function removeFromStack() { + $state = array_pop($this->blockStack); + $this->atStart = $state[0]; + $this->inParagraph = $state[1]; + } +} + +//Setup VIM: ex: et ts=4 enc=utf-8 : diff --git a/plugins/dokuwiki/inc/parser/lexer.php b/plugins/dokuwiki/inc/parser/lexer.php new file mode 100644 index 0000000..249048f --- /dev/null +++ b/plugins/dokuwiki/inc/parser/lexer.php @@ -0,0 +1,607 @@ +<?php +/** +* Author Markus Baker: http://www.lastcraft.com +* Version adapted from Simple Test: http://sourceforge.net/projects/simpletest/ +* For an intro to the Lexer see: +* http://www.phppatterns.com/index.php/article/articleview/106/1/2/ +* @author Marcus Baker +* @package Doku +* @subpackage Lexer +* @version $Id$ +*/ + +/** +* Init path constant +*/ +if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); + +/**#@+ + * lexer mode constant + */ +define("DOKU_LEXER_ENTER", 1); +define("DOKU_LEXER_MATCHED", 2); +define("DOKU_LEXER_UNMATCHED", 3); +define("DOKU_LEXER_EXIT", 4); +define("DOKU_LEXER_SPECIAL", 5); +/**#@-*/ + +/** + * Compounded regular expression. Any of + * the contained patterns could match and + * when one does it's label is returned. + * @package Doku + * @subpackage Lexer + */ +class Doku_LexerParallelRegex { + var $_patterns; + var $_labels; + var $_regex; + var $_case; + + /** + * Constructor. Starts with no patterns. + * @param boolean $case True for case sensitive, false + * for insensitive. + * @access public + */ + function __construct($case) { + $this->_case = $case; + $this->_patterns = array(); + $this->_labels = array(); + $this->_regex = null; + } + + /** + * Adds a pattern with an optional label. + * @param mixed $pattern Perl style regex. Must be UTF-8 + * encoded. If its a string, the (, ) + * lose their meaning unless they + * form part of a lookahead or + * lookbehind assertation. + * @param string $label Label of regex to be returned + * on a match. Label must be ASCII + * @access public + */ + function addPattern($pattern, $label = true) { + $duplicate = false; + // Nux: check if not duplicate + // Note! This prevents regexp overflow when viewing Flyspray issues with many comments + if (in_array($pattern, $this->_patterns)) { + $index = array_search($pattern, $this->_patterns, true); + // label also have to be the same + if (isset($this->_labels[$index]) && $this->_labels[$index] === $label) { + $duplicate = true; + } + } + // only add new pattern if not duplicate... + if (!$duplicate) { + $count = count($this->_patterns); + $this->_patterns[$count] = $pattern; + $this->_labels[$count] = $label; + } + $this->_regex = null; + } + + /** + * Attempts to match all patterns at once against + * a string. + * @param string $subject String to match against. + * @param string $match First matched portion of + * subject. + * @return boolean True on success. + * @access public + */ + function match($subject, &$match) { + if (count($this->_patterns) == 0) { + return false; + } + if (! preg_match($this->_getCompoundedRegex(), $subject, $matches)) { + $match = ""; + return false; + } + + $match = $matches[0]; + $size = count($matches); + for ($i = 1; $i < $size; $i++) { + if ($matches[$i] && isset($this->_labels[$i - 1])) { + return $this->_labels[$i - 1]; + } + } + return true; + } + + /** + * Attempts to split the string against all patterns at once + * + * @param string $subject String to match against. + * @param array $split The split result: array containing, pre-match, match & post-match strings + * @return boolean True on success. + * @access public + * + * @author Christopher Smith <chris@jalakai.co.uk> + */ + function explode($subject, &$split) { + if (count($this->_patterns) == 0) { + return false; + } + + if (! preg_match($this->_getCompoundedRegex(), $subject, $matches)) { + $split = array($subject, "", ""); + return false; + } + + $idx = count($matches)-2; + + list($pre, $post) = preg_split($this->_patterns[$idx].$this->_getPerlMatchingFlags(), $subject, 2); + + $split = array($pre, $matches[0], $post); + return isset($this->_labels[$idx]) ? $this->_labels[$idx] : true; + } + + /** + * Compounds the patterns into a single + * regular expression separated with the + * "or" operator. Caches the regex. + * Will automatically escape (, ) and / tokens. + * @param array $patterns List of patterns in order. + * @access private + */ + function _getCompoundedRegex() { + if ($this->_regex == null) { + $cnt = count($this->_patterns); + for ($i = 0; $i < $cnt; $i++) { + + // Replace lookaheads / lookbehinds with marker + $m = "\1\1"; + $pattern = preg_replace( + array ( + '/\(\?(i|m|s|x|U)\)/U', + '/\(\?(\-[i|m|s|x|U])\)/U', + '/\(\?\=(.*)\)/sU', + '/\(\?\!(.*)\)/sU', + '/\(\?\<\=(.*)\)/sU', + '/\(\?\<\!(.*)\)/sU', + '/\(\?\:(.*)\)/sU', + ), + array ( + $m.'SO:\\1'.$m, + $m.'SOR:\\1'.$m, + $m.'LA:IS:\\1'.$m, + $m.'LA:NOT:\\1'.$m, + $m.'LB:IS:\\1'.$m, + $m.'LB:NOT:\\1'.$m, + $m.'GRP:\\1'.$m, + ), + $this->_patterns[$i] + ); + // Quote the rest + $pattern = str_replace( + array('/', '(', ')'), + array('\/', '\(', '\)'), + $pattern + ); + + // Restore lookaheads / lookbehinds + $pattern = preg_replace( + array ( + '/'.$m.'SO:(.{1})'.$m.'/', + '/'.$m.'SOR:(.{2})'.$m.'/', + '/'.$m.'LA:IS:(.*)'.$m.'/sU', + '/'.$m.'LA:NOT:(.*)'.$m.'/sU', + '/'.$m.'LB:IS:(.*)'.$m.'/sU', + '/'.$m.'LB:NOT:(.*)'.$m.'/sU', + '/'.$m.'GRP:(.*)'.$m.'/sU', + ), + array ( + '(?\\1)', + '(?\\1)', + '(?=\\1)', + '(?!\\1)', + '(?<=\\1)', + '(?<!\\1)', + '(?:\\1)', + ), + $pattern + ); + + $this->_patterns[$i] = '('.$pattern.')'; + } + $this->_regex = "/" . implode("|", $this->_patterns) . "/" . $this->_getPerlMatchingFlags(); + } + return $this->_regex; + } + + /** + * Accessor for perl regex mode flags to use. + * @return string Perl regex flags. + * @access private + */ + function _getPerlMatchingFlags() { + return ($this->_case ? "msS" : "msSi"); + } +} + +/** + * States for a stack machine. + * @package Lexer + * @subpackage Lexer + */ +class Doku_LexerStateStack { + var $_stack; + + /** + * Constructor. Starts in named state. + * @param string $start Starting state name. + * @access public + */ + function __construct($start) { + $this->_stack = array($start); + } + + /** + * Accessor for current state. + * @return string State. + * @access public + */ + function getCurrent() { + return $this->_stack[count($this->_stack) - 1]; + } + + /** + * Adds a state to the stack and sets it + * to be the current state. + * @param string $state New state. + * @access public + */ + function enter($state) { + array_push($this->_stack, $state); + } + + /** + * Leaves the current state and reverts + * to the previous one. + * @return boolean False if we drop off + * the bottom of the list. + * @access public + */ + function leave() { + if (count($this->_stack) == 1) { + return false; + } + array_pop($this->_stack); + return true; + } +} + +/** + * Accepts text and breaks it into tokens. + * Some optimisation to make the sure the + * content is only scanned by the PHP regex + * parser once. Lexer modes must not start + * with leading underscores. + * @package Doku + * @subpackage Lexer + */ +class Doku_Lexer { + var $_regexes; + var $_parser; + var $_mode; + var $_mode_handlers; + var $_case; + + /** + * Sets up the lexer in case insensitive matching + * by default. + * @param Doku_Parser $parser Handling strategy by + * reference. + * @param string $start Starting handler. + * @param boolean $case True for case sensitive. + * @access public + */ + function __construct(&$parser, $start = "accept", $case = false) { + $this->_case = $case; + $this->_regexes = array(); + $this->_parser = &$parser; + $this->_mode = new Doku_LexerStateStack($start); + $this->_mode_handlers = array(); + } + + /** + * Adds a token search pattern for a particular + * parsing mode. The pattern does not change the + * current mode. + * @param string $pattern Perl style regex, but ( and ) + * lose the usual meaning. + * @param string $mode Should only apply this + * pattern when dealing with + * this type of input. + * @access public + */ + function addPattern($pattern, $mode = "accept") { + if (! isset($this->_regexes[$mode])) { + $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case); + } + $this->_regexes[$mode]->addPattern($pattern); + } + + /** + * Adds a pattern that will enter a new parsing + * mode. Useful for entering parenthesis, strings, + * tags, etc. + * @param string $pattern Perl style regex, but ( and ) + * lose the usual meaning. + * @param string $mode Should only apply this + * pattern when dealing with + * this type of input. + * @param string $new_mode Change parsing to this new + * nested mode. + * @access public + */ + function addEntryPattern($pattern, $mode, $new_mode) { + if (! isset($this->_regexes[$mode])) { + $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case); + } + $this->_regexes[$mode]->addPattern($pattern, $new_mode); + } + + /** + * Adds a pattern that will exit the current mode + * and re-enter the previous one. + * @param string $pattern Perl style regex, but ( and ) + * lose the usual meaning. + * @param string $mode Mode to leave. + * @access public + */ + function addExitPattern($pattern, $mode) { + if (! isset($this->_regexes[$mode])) { + $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case); + } + $this->_regexes[$mode]->addPattern($pattern, "__exit"); + } + + /** + * Adds a pattern that has a special mode. Acts as an entry + * and exit pattern in one go, effectively calling a special + * parser handler for this token only. + * @param string $pattern Perl style regex, but ( and ) + * lose the usual meaning. + * @param string $mode Should only apply this + * pattern when dealing with + * this type of input. + * @param string $special Use this mode for this one token. + * @access public + */ + function addSpecialPattern($pattern, $mode, $special) { + if (! isset($this->_regexes[$mode])) { + $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case); + } + $this->_regexes[$mode]->addPattern($pattern, "_$special"); + } + + /** + * Adds a mapping from a mode to another handler. + * @param string $mode Mode to be remapped. + * @param string $handler New target handler. + * @access public + */ + function mapHandler($mode, $handler) { + $this->_mode_handlers[$mode] = $handler; + } + + /** + * Splits the page text into tokens. Will fail + * if the handlers report an error or if no + * content is consumed. If successful then each + * unparsed and parsed token invokes a call to the + * held listener. + * @param string $raw Raw HTML text. + * @return boolean True on success, else false. + * @access public + */ + function parse($raw) { + if (! isset($this->_parser)) { + return false; + } + $initialLength = strlen($raw); + $length = $initialLength; + $pos = 0; + while (is_array($parsed = $this->_reduce($raw))) { + list($unmatched, $matched, $mode) = $parsed; + $currentLength = strlen($raw); + $matchPos = $initialLength - $currentLength - strlen($matched); + if (! $this->_dispatchTokens($unmatched, $matched, $mode, $pos, $matchPos)) { + return false; + } + if ($currentLength == $length) { + return false; + } + $length = $currentLength; + $pos = $initialLength - $currentLength; + } + if (!$parsed) { + return false; + } + return $this->_invokeParser($raw, DOKU_LEXER_UNMATCHED, $pos); + } + + /** + * Sends the matched token and any leading unmatched + * text to the parser changing the lexer to a new + * mode if one is listed. + * @param string $unmatched Unmatched leading portion. + * @param string $matched Actual token match. + * @param string $mode Mode after match. A boolean + * false mode causes no change. + * @param int $pos Current byte index location in raw doc + * thats being parsed + * @return boolean False if there was any error + * from the parser. + * @access private + */ + function _dispatchTokens($unmatched, $matched, $mode = false, $initialPos, $matchPos) { + if (! $this->_invokeParser($unmatched, DOKU_LEXER_UNMATCHED, $initialPos) ){ + return false; + } + if ($this->_isModeEnd($mode)) { + if (! $this->_invokeParser($matched, DOKU_LEXER_EXIT, $matchPos)) { + return false; + } + return $this->_mode->leave(); + } + if ($this->_isSpecialMode($mode)) { + $this->_mode->enter($this->_decodeSpecial($mode)); + if (! $this->_invokeParser($matched, DOKU_LEXER_SPECIAL, $matchPos)) { + return false; + } + return $this->_mode->leave(); + } + if (is_string($mode)) { + $this->_mode->enter($mode); + return $this->_invokeParser($matched, DOKU_LEXER_ENTER, $matchPos); + } + return $this->_invokeParser($matched, DOKU_LEXER_MATCHED, $matchPos); + } + + /** + * Tests to see if the new mode is actually to leave + * the current mode and pop an item from the matching + * mode stack. + * @param string $mode Mode to test. + * @return boolean True if this is the exit mode. + * @access private + */ + function _isModeEnd($mode) { + return ($mode === "__exit"); + } + + /** + * Test to see if the mode is one where this mode + * is entered for this token only and automatically + * leaves immediately afterwoods. + * @param string $mode Mode to test. + * @return boolean True if this is the exit mode. + * @access private + */ + function _isSpecialMode($mode) { + return (strncmp($mode, "_", 1) == 0); + } + + /** + * Strips the magic underscore marking single token + * modes. + * @param string $mode Mode to decode. + * @return string Underlying mode name. + * @access private + */ + function _decodeSpecial($mode) { + return substr($mode, 1); + } + + /** + * Calls the parser method named after the current + * mode. Empty content will be ignored. The lexer + * has a parser handler for each mode in the lexer. + * @param string $content Text parsed. + * @param boolean $is_match Token is recognised rather + * than unparsed data. + * @param int $pos Current byte index location in raw doc + * thats being parsed + * @access private + */ + function _invokeParser($content, $is_match, $pos) { + if (($content === "") || ($content === false)) { + return true; + } + $handler = $this->_mode->getCurrent(); + if (isset($this->_mode_handlers[$handler])) { + $handler = $this->_mode_handlers[$handler]; + } + + // modes starting with plugin_ are all handled by the same + // handler but with an additional parameter + if(substr($handler,0,7)=='plugin_'){ + list($handler,$plugin) = explode('_',$handler,2); + return $this->_parser->$handler($content, $is_match, $pos, $plugin); + } + + return $this->_parser->$handler($content, $is_match, $pos); + } + + /** + * Tries to match a chunk of text and if successful + * removes the recognised chunk and any leading + * unparsed data. Empty strings will not be matched. + * @param string $raw The subject to parse. This is the + * content that will be eaten. + * @return array Three item list of unparsed + * content followed by the + * recognised token and finally the + * action the parser is to take. + * True if no match, false if there + * is a parsing error. + * @access private + */ + function _reduce(&$raw) { + if (! isset($this->_regexes[$this->_mode->getCurrent()])) { + return false; + } + if ($raw === "") { + return true; + } + if ($action = $this->_regexes[$this->_mode->getCurrent()]->explode($raw, $split)) { + list($unparsed, $match, $raw) = $split; + return array($unparsed, $match, $action); + } + return true; + } +} + +/** +* Escapes regex characters other than (, ) and / +* @TODO +*/ +function Doku_Lexer_Escape($str) { + //$str = addslashes($str); + $chars = array( + '/\\\\/', + '/\./', + '/\+/', + '/\*/', + '/\?/', + '/\[/', + '/\^/', + '/\]/', + '/\$/', + '/\{/', + '/\}/', + '/\=/', + '/\!/', + '/\</', + '/\>/', + '/\|/', + '/\:/' + ); + + $escaped = array( + '\\\\\\\\', + '\.', + '\+', + '\*', + '\?', + '\[', + '\^', + '\]', + '\$', + '\{', + '\}', + '\=', + '\!', + '\<', + '\>', + '\|', + '\:' + ); + return preg_replace($chars, $escaped, $str); +} + +//Setup VIM: ex: et ts=4 enc=utf-8 : diff --git a/plugins/dokuwiki/inc/parser/parser.php b/plugins/dokuwiki/inc/parser/parser.php new file mode 100644 index 0000000..b77af88 --- /dev/null +++ b/plugins/dokuwiki/inc/parser/parser.php @@ -0,0 +1,932 @@ +<?php + +if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); + +require_once DOKU_INC . 'inc/parser/lexer.php'; +require_once DOKU_INC . 'inc/parser/handler.php'; + + +/** + * Define various types of modes used by the parser - they are used to + * populate the list of modes another mode accepts + */ +global $PARSER_MODES; +$PARSER_MODES = array( + // containers are complex modes that can contain many other modes + // hr breaks the principle but they shouldn't be used in tables / lists + // so they are put here + 'container' => array('listblock','table','quote','hr'), + + // some mode are allowed inside the base mode only + 'baseonly' => array('header'), + + // modes for styling text -- footnote behaves similar to styling + 'formatting' => array('strong', 'emphasis', 'underline', 'monospace', + 'subscript', 'superscript', 'deleted', 'footnote'), + + // modes where the token is simply replaced - they can not contain any + // other modes + 'substition' => array('acronym','smiley','wordblock','entity', + 'camelcaselink', 'internallink','media', + 'externallink','linebreak','emaillink', + 'windowssharelink','filelink','notoc', + 'nocache','multiplyentity','quotes','rss'), + + // modes which have a start and end token but inside which + // no other modes should be applied + 'protected' => array('preformatted','code','file','php','html'), + + // inside this mode no wiki markup should be applied but lineendings + // and whitespace isn't preserved + 'disabled' => array('unformatted'), + + // used to mark paragraph boundaries + 'paragraphs' => array('eol') +); + +//------------------------------------------------------------------- + +/** +* Sets up the Lexer with modes and points it to the Handler +* For an intro to the Lexer see: wiki:parser +*/ +class Doku_Parser { + + var $Handler; + + var $Lexer; + + var $modes = array(); + + var $connected = FALSE; + + function addBaseMode(& $BaseMode) { + $this->modes['base'] = & $BaseMode; + if ( !$this->Lexer ) { + $this->Lexer = new Doku_Lexer($this->Handler,'base', TRUE); + } + $this->modes['base']->Lexer = & $this->Lexer; + } + + /** + * PHP preserves order of associative elements + * Mode sequence is important + */ + function addMode($name, & $Mode) { + if ( !isset($this->modes['base']) ) { + $this->addBaseMode(new Doku_Parser_Mode_base()); + } + $Mode->Lexer = & $this->Lexer; + $this->modes[$name] = & $Mode; + } + + function connectModes() { + + if ( $this->connected ) { + return; + } + + foreach ( array_keys($this->modes) as $mode ) { + + // Base isn't connected to anything + if ( $mode == 'base' ) { + continue; + } + + $this->modes[$mode]->preConnect(); + + foreach ( array_keys($this->modes) as $cm ) { + + if ( $this->modes[$cm]->accepts($mode) ) { + $this->modes[$mode]->connectTo($cm); + } + + } + + $this->modes[$mode]->postConnect(); + } + + $this->connected = TRUE; + } + + function parse($doc) { + if ( $this->Lexer ) { + $this->connectModes(); + // Normalize CRs and pad doc + $doc = "\n".str_replace("\r\n","\n",$doc)."\n"; + $this->Lexer->parse($doc); + $this->Handler->_finalize(); + return $this->Handler->calls; + } else { + return FALSE; + } + } + +} + +//------------------------------------------------------------------- +/** + * This class and all the subclasses below are + * used to reduce the effort required to register + * modes with the Lexer. For performance these + * could all be eliminated later perhaps, or + * the Parser could be serialized to a file once + * all modes are registered + * + * @author Harry Fuecks <hfuecks@gmail.com> +*/ +class Doku_Parser_Mode { + + var $Lexer; + + var $allowedModes = array(); + + // returns a number used to determine in which order modes are added + function getSort() { + trigger_error('getSort() not implemented in '.get_class($this), E_USER_WARNING); + } + + // Called before any calls to connectTo + function preConnect() {} + + // Connects the mode + function connectTo($mode) {} + + // Called after all calls to connectTo + function postConnect() {} + + function accepts($mode) { + return in_array($mode, $this->allowedModes ); + } + +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_base extends Doku_Parser_Mode { + + function __construct() { + global $PARSER_MODES; + + $this->allowedModes = array_merge ( + $PARSER_MODES['container'], + $PARSER_MODES['baseonly'], + $PARSER_MODES['paragraphs'], + $PARSER_MODES['formatting'], + $PARSER_MODES['substition'], + $PARSER_MODES['protected'], + $PARSER_MODES['disabled'] + ); + } + + function getSort() { + return 0; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_footnote extends Doku_Parser_Mode { + + function __construct() { + global $PARSER_MODES; + + $this->allowedModes = array_merge ( + $PARSER_MODES['container'], + $PARSER_MODES['formatting'], + $PARSER_MODES['substition'], + $PARSER_MODES['protected'], + $PARSER_MODES['disabled'] + ); + + unset($this->allowedModes[array_search('footnote', $this->allowedModes)]); + } + + function connectTo($mode) { + $this->Lexer->addEntryPattern( + '\x28\x28(?=.*\x29\x29)',$mode,'footnote' + ); + } + + function postConnect() { + $this->Lexer->addExitPattern( + '\x29\x29','footnote' + ); + } + + function getSort() { + return 150; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_header extends Doku_Parser_Mode { + + function preConnect() { + //we're not picky about the closing ones, two are enough + $this->Lexer->addSpecialPattern( + '[ \t]*={2,}[^\n]+={2,}[ \t]*(?=\n)', + 'base', + 'header' + ); + } + + function getSort() { + return 50; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_notoc extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addSpecialPattern('~~NOTOC~~',$mode,'notoc'); + } + + function getSort() { + return 30; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_nocache extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addSpecialPattern('~~NOCACHE~~',$mode,'nocache'); + } + + function getSort() { + return 40; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_linebreak extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addSpecialPattern('\x5C{2}(?=\s)',$mode,'linebreak'); + } + + function getSort() { + return 140; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_eol extends Doku_Parser_Mode { + + function connectTo($mode) { + $badModes = array('listblock','table'); + if ( in_array($mode, $badModes) ) { + return; + } + $this->Lexer->addSpecialPattern('\n',$mode,'eol'); + } + + function getSort() { + return 370; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_hr extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addSpecialPattern('\n[ \t]*-{4,}[ \t]*(?=\n)',$mode,'hr'); + } + + function getSort() { + return 160; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_formatting extends Doku_Parser_Mode { + var $type; + + var $formatting = array ( + 'strong' => array ( + 'entry'=>'\*\*(?=.*\*\*)', + 'exit'=>'\*\*', + 'sort'=>70 + ), + + 'emphasis'=> array ( + 'entry'=>'//(?=[^\x00]*[^:]//)', //hack for bug #384 + 'exit'=>'//', + 'sort'=>80 + ), + + 'underline'=> array ( + 'entry'=>'__(?=.*__)', + 'exit'=>'__', + 'sort'=>90 + ), + + 'monospace'=> array ( + 'entry'=>'\x27\x27(?=.*\x27\x27)', + 'exit'=>'\x27\x27', + 'sort'=>100 + ), + + 'subscript'=> array ( + 'entry'=>'<sub>(?=.*</sub>)', + 'exit'=>'</sub>', + 'sort'=>110 + ), + + 'superscript'=> array ( + 'entry'=>'<sup>(?=.*</sup>)', + 'exit'=>'</sup>', + 'sort'=>120 + ), + + 'deleted'=> array ( + 'entry'=>'<del>(?=.*</del>)', + 'exit'=>'</del>', + 'sort'=>130 + ), + ); + + function __construct($type) { + global $PARSER_MODES; + + if ( !array_key_exists($type, $this->formatting) ) { + trigger_error('Invalid formatting type '.$type, E_USER_WARNING); + } + + $this->type = $type; + + // formatting may contain other formatting but not it self + $modes = $PARSER_MODES['formatting']; + $key = array_search($type, $modes); + if ( is_int($key) ) { + unset($modes[$key]); + } + + $this->allowedModes = array_merge ( + $modes, + $PARSER_MODES['substition'], + $PARSER_MODES['disabled'] + ); + } + + function connectTo($mode) { + + // Can't nest formatting in itself + if ( $mode == $this->type ) { + return; + } + + $this->Lexer->addEntryPattern( + $this->formatting[$this->type]['entry'], + $mode, + $this->type + ); + } + + function postConnect() { + + $this->Lexer->addExitPattern( + $this->formatting[$this->type]['exit'], + $this->type + ); + + } + + function getSort() { + return $this->formatting[$this->type]['sort']; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_listblock extends Doku_Parser_Mode { + + function __construct() { + global $PARSER_MODES; + + $this->allowedModes = array_merge ( + $PARSER_MODES['formatting'], + $PARSER_MODES['substition'], + $PARSER_MODES['disabled'], + $PARSER_MODES['protected'] #XXX new + ); + + // $this->allowedModes[] = 'footnote'; + } + + function connectTo($mode) { + $this->Lexer->addEntryPattern('\n {2,}[\-\*]',$mode,'listblock'); + $this->Lexer->addEntryPattern('\n\t{1,}[\-\*]',$mode,'listblock'); + + $this->Lexer->addPattern('\n {2,}[\-\*]','listblock'); + $this->Lexer->addPattern('\n\t{1,}[\-\*]','listblock'); + + } + + function postConnect() { + $this->Lexer->addExitPattern('\n','listblock'); + } + + function getSort() { + return 10; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_table extends Doku_Parser_Mode { + + function __construct() { + global $PARSER_MODES; + + $this->allowedModes = array_merge ( + $PARSER_MODES['formatting'], + $PARSER_MODES['substition'], + $PARSER_MODES['disabled'], + $PARSER_MODES['protected'] + ); + } + + function connectTo($mode) { + $this->Lexer->addEntryPattern('\n\^',$mode,'table'); + $this->Lexer->addEntryPattern('\n\|',$mode,'table'); + } + + function postConnect() { + $this->Lexer->addPattern('\n\^','table'); + $this->Lexer->addPattern('\n\|','table'); + #$this->Lexer->addPattern(' {2,}','table'); + $this->Lexer->addPattern('[\t ]+','table'); + $this->Lexer->addPattern('\^','table'); + $this->Lexer->addPattern('\|','table'); + $this->Lexer->addExitPattern('\n','table'); + } + + function getSort() { + return 60; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_unformatted extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addEntryPattern('<nowiki>(?=.*</nowiki>)',$mode,'unformatted'); + $this->Lexer->addEntryPattern('%%(?=.*%%)',$mode,'unformattedalt'); + } + + function postConnect() { + $this->Lexer->addExitPattern('</nowiki>','unformatted'); + $this->Lexer->addExitPattern('%%','unformattedalt'); + $this->Lexer->mapHandler('unformattedalt','unformatted'); + } + + function getSort() { + return 170; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_php extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addEntryPattern('<php>(?=.*</php>)',$mode,'php'); + } + + function postConnect() { + $this->Lexer->addExitPattern('</php>','php'); + } + + function getSort() { + return 180; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_html extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addEntryPattern('<html>(?=.*</html>)',$mode,'html'); + } + + function postConnect() { + $this->Lexer->addExitPattern('</html>','html'); + } + + function getSort() { + return 190; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_preformatted extends Doku_Parser_Mode { + + function connectTo($mode) { + // Has hard coded awareness of lists... + $this->Lexer->addEntryPattern('\n (?![\*\-])',$mode,'preformatted'); + $this->Lexer->addEntryPattern('\n\t(?![\*\-])',$mode,'preformatted'); + + // How to effect a sub pattern with the Lexer! + $this->Lexer->addPattern('\n ','preformatted'); + $this->Lexer->addPattern('\n\t','preformatted'); + + } + + function postConnect() { + $this->Lexer->addExitPattern('\n','preformatted'); + } + + function getSort() { + return 20; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_code extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addEntryPattern('<code(?=.*</code>)',$mode,'code'); + } + + function postConnect() { + $this->Lexer->addExitPattern('</code>','code'); + } + + function getSort() { + return 200; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_file extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addEntryPattern('<file>(?=.*</file>)',$mode,'file'); + } + + function postConnect() { + $this->Lexer->addExitPattern('</file>','file'); + } + + function getSort() { + return 210; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_quote extends Doku_Parser_Mode { + + function __construct() { + global $PARSER_MODES; + + $this->allowedModes = array_merge ( + $PARSER_MODES['formatting'], + $PARSER_MODES['substition'], + $PARSER_MODES['disabled'], + $PARSER_MODES['protected'] #XXX new + ); + #$this->allowedModes[] = 'footnote'; + #$this->allowedModes[] = 'preformatted'; + #$this->allowedModes[] = 'unformatted'; + } + + function connectTo($mode) { + $this->Lexer->addEntryPattern('\n>{1,}',$mode,'quote'); + } + + function postConnect() { + $this->Lexer->addPattern('\n>{1,}','quote'); + $this->Lexer->addExitPattern('\n','quote'); + } + + function getSort() { + return 220; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_acronym extends Doku_Parser_Mode { + // A list + var $acronyms = array(); + var $pattern = ''; + + function __construct($acronyms) { + $this->acronyms = $acronyms; + } + + function preConnect() { + if(!count($this->acronyms)) return; + + $bound = '[\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]'; + $acronyms = array_map('Doku_Lexer_Escape',$this->acronyms); + $this->pattern = '(?<=^|'.$bound.')(?:'.join('|',$acronyms).')(?='.$bound.')'; + } + + function connectTo($mode) { + if(!count($this->acronyms)) return; + + if ( strlen($this->pattern) > 0 ) { + $this->Lexer->addSpecialPattern($this->pattern,$mode,'acronym'); + } + } + + function getSort() { + return 240; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_smiley extends Doku_Parser_Mode { + // A list + var $smileys = array(); + var $pattern = ''; + + function __construct($smileys) { + $this->smileys = $smileys; + } + + function preConnect() { + if(!count($this->smileys)) return; + + $sep = ''; + // Nux: fix for potential pattern overflow... + $this->pattern = ''; + foreach ( $this->smileys as $smiley ) { + $this->pattern .= $sep.Doku_Lexer_Escape($smiley); + $sep = '|'; + } + } + + function connectTo($mode) { + if(!count($this->smileys)) return; + + if ( strlen($this->pattern) > 0 ) { + $this->Lexer->addSpecialPattern($this->pattern,$mode,'smiley'); + } + } + + function getSort() { + return 230; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_wordblock extends Doku_Parser_Mode { + // A list + var $badwords = array(); + var $pattern = ''; + + function __construct($badwords) { + $this->badwords = $badwords; + } + + function preConnect() { + + if ( count($this->badwords) == 0 ) { + return; + } + + $sep = ''; + // Nux: fix for potential pattern overflow... + $this->pattern = ''; + foreach ( $this->badwords as $badword ) { + $this->pattern .= $sep.'(?<=\b)(?i)'.Doku_Lexer_Escape($badword).'(?-i)(?=\b)'; + $sep = '|'; + } + + } + + function connectTo($mode) { + if ( strlen($this->pattern) > 0 ) { + $this->Lexer->addSpecialPattern($this->pattern,$mode,'wordblock'); + } + } + + function getSort() { + return 250; + } +} + +//------------------------------------------------------------------- +/** +* @TODO Quotes and 640x480 are not supported - just straight replacements here +*/ +class Doku_Parser_Mode_entity extends Doku_Parser_Mode { + // A list + var $entities = array(); + var $pattern = ''; + + function __construct($entities) { + $this->entities = $entities; + } + + function preConnect() { + if(!count($this->entities)) return; + + $sep = ''; + // Nux: fix for potential pattern overflow... + $this->pattern = ''; + foreach ( $this->entities as $entity ) { + $this->pattern .= $sep.Doku_Lexer_Escape($entity); + $sep = '|'; + } + } + + function connectTo($mode) { + if(!count($this->entities)) return; + + if ( strlen($this->pattern) > 0 ) { + $this->Lexer->addSpecialPattern($this->pattern,$mode,'entity'); + } + } + + function getSort() { + return 260; + } +} + +//------------------------------------------------------------------- +// Implements the 640x480 replacement +class Doku_Parser_Mode_multiplyentity extends Doku_Parser_Mode { + + function connectTo($mode) { + + $this->Lexer->addSpecialPattern( + '(?<=\b)\d+[xX]\d+(?=\b)',$mode,'multiplyentity' + ); + + } + + function getSort() { + return 270; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_quotes extends Doku_Parser_Mode { + + function connectTo($mode) { + + $this->Lexer->addSpecialPattern( + '(?<=^|\s)\'(?=\S)',$mode,'singlequoteopening' + ); + $this->Lexer->addSpecialPattern( + '(?<=^|\S)\'',$mode,'singlequoteclosing' + ); + $this->Lexer->addSpecialPattern( + '(?<=^|\s)"(?=\S)',$mode,'doublequoteopening' + ); + $this->Lexer->addSpecialPattern( + '(?<=^|\S)"',$mode,'doublequoteclosing' + ); + + } + + function getSort() { + return 280; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_camelcaselink extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addSpecialPattern( + '\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b',$mode,'camelcaselink' + ); + } + + function getSort() { + return 290; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_internallink extends Doku_Parser_Mode { + + function connectTo($mode) { + // Word boundaries? + $this->Lexer->addSpecialPattern("\[\[.+?\]\]",$mode,'internallink'); + } + + function getSort() { + return 300; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_media extends Doku_Parser_Mode { + + function connectTo($mode) { + // Word boundaries? + $this->Lexer->addSpecialPattern("\{\{[^\}]+\}\}",$mode,'media'); + } + + function getSort() { + return 320; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_rss extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addSpecialPattern("\{\{rss>[^\}]+\}\}",$mode,'rss'); + } + + function getSort() { + return 310; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_externallink extends Doku_Parser_Mode { + var $schemes = array('http','https','telnet','gopher','wais','ftp','ed2k','irc','ldap'); + var $patterns = array(); + + function preConnect() { + + $ltrs = '\w'; + $gunk = '/\#~:.?+=&%@!\-'; + $punc = '.:?\-;,'; + $host = $ltrs.$punc; + $any = $ltrs.$gunk.$punc; + + foreach ( $this->schemes as $scheme ) { + $this->patterns[] = '\b(?i)'.$scheme.'(?-i)://['.$any.']+?(?=['.$punc.']*[^'.$any.'])'; + } + + $this->patterns[] = '\b(?i)www?(?-i)\.['.$host.']+?\.['.$host.']+?['.$any.']+?(?=['.$punc.']*[^'.$any.'])'; + $this->patterns[] = '\b(?i)ftp?(?-i)\.['.$host.']+?\.['.$host.']+?['.$any.']+?(?=['.$punc.']*[^'.$any.'])'; + + } + + function connectTo($mode) { + foreach ( $this->patterns as $pattern ) { + $this->Lexer->addSpecialPattern($pattern,$mode,'externallink'); + } + } + + function getSort() { + return 330; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_filelink extends Doku_Parser_Mode { + + var $pattern; + + function preConnect() { + + $ltrs = '\w'; + $gunk = '/\#~:.?+=&%@!\-'; + $punc = '.:?\-;,'; + $host = $ltrs.$punc; + $any = $ltrs.$gunk.$punc; + + $this->pattern = '\b(?i)file(?-i)://['.$any.']+?['. + $punc.']*[^'.$any.']'; + } + + function connectTo($mode) { + $this->Lexer->addSpecialPattern( + $this->pattern,$mode,'filelink'); + } + + function getSort() { + return 360; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_windowssharelink extends Doku_Parser_Mode { + + var $pattern; + + function preConnect() { + $this->pattern = "\\\\\\\\\w+?(?:\\\\[\w$]+)+"; + } + + function connectTo($mode) { + $this->Lexer->addSpecialPattern( + $this->pattern,$mode,'windowssharelink'); + } + + function getSort() { + return 350; + } +} + +//------------------------------------------------------------------- +class Doku_Parser_Mode_emaillink extends Doku_Parser_Mode { + + function connectTo($mode) { + $this->Lexer->addSpecialPattern("<[\w0-9\-_.]+?@[\w\-]+\.[\w\-\.]+\.*[\w]+>",$mode,'emaillink'); + } + + function getSort() { + return 340; + } +} + + +//Setup VIM: ex: et ts=4 enc=utf-8 : diff --git a/plugins/dokuwiki/inc/parser/renderer.php b/plugins/dokuwiki/inc/parser/renderer.php new file mode 100644 index 0000000..f9dcaab --- /dev/null +++ b/plugins/dokuwiki/inc/parser/renderer.php @@ -0,0 +1,212 @@ +<?php +/** + * Renderer output base class + * + * @author Harry Fuecks <hfuecks@gmail.com> + * @author Andreas Gohr <andi@splitbrain.org> + */ +if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); + +require_once DOKU_INC . 'inc/parser/renderer.php'; +require_once DOKU_INC . 'inc/pluginutils.php'; + +class Doku_Renderer { + var $info = array( + 'cache' => TRUE, // may the rendered result cached? + 'toc' => TRUE, // render the TOC? + ); + + + function nocache() { + $this->info['cache'] = FALSE; + } + + function notoc() { + $this->info['toc'] = FALSE; + } + + //handle plugin rendering + function plugin($name,$data){ + $plugin =& plugin_load('syntax',$name); + if($plugin != null){ + // determine mode from renderer class name - format = "Doku_Renderer_<mode>" + $mode = substr(get_class($this), 14); + $plugin->render($mode,$this,$data); + } + } + + /** + * handle nested render instructions + * this method (and nest_close method) should not be overloaded in actual renderer output classes + */ + function nest($instructions) { + + foreach ( $instructions as $instruction ) { + // execute the callback against ourself + call_user_func_array(array(&$this, $instruction[0]),$instruction[1]); + } + } + + // dummy closing instruction issued by Doku_Handler_Nest, normally the syntax mode should + // override this instruction when instantiating Doku_Handler_Nest - however plugins will not + // be able to - as their instructions require data. + function nest_close() {} + + function document_start() {} + + function document_end() {} + + function render_TOC() { return ''; } + + function header($text, $level, $pos) {} + + function section_edit($start, $end, $level, $name) {} + + function section_open($level) {} + + function section_close() {} + + function cdata($text) {} + + function p_open() {} + + function p_close() {} + + function linebreak() {} + + function hr() {} + + function strong_open() {} + + function strong_close() {} + + function emphasis_open() {} + + function emphasis_close() {} + + function underline_open() {} + + function underline_close() {} + + function monospace_open() {} + + function monospace_close() {} + + function subscript_open() {} + + function subscript_close() {} + + function superscript_open() {} + + function superscript_close() {} + + function deleted_open() {} + + function deleted_close() {} + + function footnote_open() {} + + function footnote_close() {} + + function listu_open() {} + + function listu_close() {} + + function listo_open() {} + + function listo_close() {} + + function listitem_open($level) {} + + function listitem_close() {} + + function listcontent_open() {} + + function listcontent_close() {} + + function unformatted($text) {} + + function php($text) {} + + function html($text) {} + + function preformatted($text) {} + + function file($text) {} + + function quote_open() {} + + function quote_close() {} + + function code($text, $lang = NULL) {} + + function acronym($acronym) {} + + function smiley($smiley) {} + + function wordblock($word) {} + + function entity($entity) {} + + // 640x480 ($x=640, $y=480) + function multiplyentity($x, $y) {} + + function singlequoteopening() {} + + function singlequoteclosing() {} + + function doublequoteopening() {} + + function doublequoteclosing() {} + + // $link like 'SomePage' + function camelcaselink($link) {} + + // $link like 'wiki:syntax', $title could be an array (media) + function internallink($link, $title = NULL) {} + + // $link is full URL with scheme, $title could be an array (media) + function externallink($link, $title = NULL) {} + + // $link is the original link - probably not much use + // $wikiName is an indentifier for the wiki + // $wikiUri is the URL fragment to append to some known URL + function interwikilink($link, $title = NULL, $wikiName, $wikiUri) {} + + // Link to file on users OS, $title could be an array (media) + function filelink($link, $title = NULL) {} + + // Link to a Windows share, , $title could be an array (media) + function windowssharelink($link, $title = NULL) {} + +// function email($address, $title = NULL) {} + function emaillink($address, $name = NULL) {} + + function internalmedialink ( + $src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL + ) {} + + function externalmedialink( + $src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL + ) {} + + function table_open($maxcols = NULL, $numrows = NULL){} + + function table_close(){} + + function tablerow_open(){} + + function tablerow_close(){} + + function tableheader_open($colspan = 1, $align = NULL){} + + function tableheader_close(){} + + function tablecell_open($colspan = 1, $align = NULL){} + + function tablecell_close(){} + +} + + +//Setup VIM: ex: et ts=4 enc=utf-8 : diff --git a/plugins/dokuwiki/inc/parser/xhtml.php b/plugins/dokuwiki/inc/parser/xhtml.php new file mode 100644 index 0000000..a220819 --- /dev/null +++ b/plugins/dokuwiki/inc/parser/xhtml.php @@ -0,0 +1,1097 @@ +<?php +/** + * Renderer for XHTML output + * + * @author Harry Fuecks <hfuecks@gmail.com> + * @author Andreas Gohr <andi@splitbrain.org> + */ + +if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); + +if ( !defined('DOKU_LF') ) { + // Some whitespace to help View > Source + define ('DOKU_LF',"\n"); +} + +if ( !defined('DOKU_TAB') ) { + // Some whitespace to help View > Source + define ('DOKU_TAB',"\t"); +} + +require_once DOKU_INC . 'inc/parser/renderer.php'; +require_once DOKU_INC . 'inc/html.php'; + +/** + * The Renderer + */ +class Doku_Renderer_xhtml extends Doku_Renderer { + + // @access public + var $doc = ''; // will contain the whole document + var $toc = array(); // will contain the Table of Contents + + + var $headers = array(); + + var $footnotes = array(); + + var $acronyms = array(); + var $smileys = array(); + var $badwords = array(); + var $entities = array(); + var $interwiki = array(); + + var $lastsec = 0; + + var $store = ''; + + function document_start() { + //reset some internals + $this->toc = array(); + $this->headers = array(); + } + + function document_end() { + if ( count ($this->footnotes) > 0 ) { + $this->doc .= '<div class="footnotes">'.DOKU_LF; + + $id = 0; + foreach ( $this->footnotes as $footnote ) { + $id++; // the number of the current 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 .= '<a href="#fnt__'.$id.'" id="fn__'.$id.'" name="fn__'.$id.'" class="fn_bot">'; + $this->doc .= $id.')</a> '.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 .= ', <a href="#fnt__'.($ref+1).'" id="fn__'.($ref+1).'" name="fn__'.($ref+1).'" class="fn_bot">'; + $this->doc .= ($ref+1).')</a> '.DOKU_LF; + } + } + + // add footnote markup and close this footnote + $this->doc .= $footnote; + $this->doc .= '</div>' . DOKU_LF; + } + } + $this->doc .= '</div>'.DOKU_LF; + } + + // prepend the TOC + if($this->info['toc']){ + $this->doc = $this->render_TOC().$this->doc; + } + + // make sure there are no empty paragraphs + $this->doc = preg_replace('#<p>\s*</p>#','',$this->doc); + } + + /** + * Return the TOC rendered to XHTML + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function render_TOC(){ + if(count($this->toc) < 3) return ''; + global $lang; + $out = '<div class="toc">'.DOKU_LF; + $out .= '<div class="tocheader toctoggle" id="toc__header">'; + $out .= $lang['toc']; + $out .= '</div>'.DOKU_LF; + $out .= '<div id="toc__inside">'.DOKU_LF; + $out .= html_buildlist($this->toc,'toc',array($this,'_tocitem')); + $out .= '</div>'.DOKU_LF.'</div>'.DOKU_LF; + return $out; + } + + /** + * Callback for html_buildlist + */ + function _tocitem($item){ + return '<span class="li"><a href="#'.$item['hid'].'" class="toc">'. + $this->_xmlEntities($item['title']).'</a></span>'; + } + + function header($text, $level, $pos) { + global $conf; + + // create a unique header id + $hid = $this->_headerToLink($text,'true'); + + //handle TOC + if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']){ + // the TOC is one of our standard ul list arrays ;-) + $this->toc[] = array( 'hid' => $hid, + 'title' => $text, + 'type' => 'ul', + 'level' => $level-$conf['toptoclevel']+1); + } + + // write the header + $this->doc .= DOKU_LF.'<h'.$level.' id="'.$hid.'">'.$this->_xmlEntities($text)."</h$level>".DOKU_LF; + } + + /** + * Section edit marker is replaced by an edit button when + * the page is editable. Replacement done in 'inc/html.php#html_secedit' + * + * @author Andreas Gohr <andi@splitbrain.org> + * @author Ben Coburn <btcoburn@silicodon.net> + */ + function section_edit($start, $end, $level, $name) { + global $conf; + + if ($start!=-1 && $level<=$conf['maxseclevel']) { + $name = str_replace('"', '', $name); + $this->doc .= '<!-- SECTION "'.$name.'" ['.$start.'-'.(($end===0)?'':$end).'] -->'; + } + } + + function section_open($level) { + $this->doc .= "<div class=\"level$level\">".DOKU_LF; + } + + function section_close() { + $this->doc .= DOKU_LF.'</div>'.DOKU_LF; + } + + function cdata($text) { + $this->doc .= $this->_xmlEntities($text); + } + + function p_open() { + $this->doc .= DOKU_LF.'<p>'.DOKU_LF; + } + + function p_close() { + $this->doc .= DOKU_LF.'</p>'.DOKU_LF; + } + + function linebreak() { + $this->doc .= '<br/>'.DOKU_LF; + } + + function hr() { + $this->doc .= '<hr />'.DOKU_LF; + } + + function strong_open() { + $this->doc .= '<strong>'; + } + + function strong_close() { + $this->doc .= '</strong>'; + } + + function emphasis_open() { + $this->doc .= '<em>'; + } + + function emphasis_close() { + $this->doc .= '</em>'; + } + + function underline_open() { + $this->doc .= '<em class="u">'; + } + + function underline_close() { + $this->doc .= '</em>'; + } + + function monospace_open() { + $this->doc .= '<code>'; + } + + function monospace_close() { + $this->doc .= '</code>'; + } + + function subscript_open() { + $this->doc .= '<sub>'; + } + + function subscript_close() { + $this->doc .= '</sub>'; + } + + function superscript_open() { + $this->doc .= '<sup>'; + } + + function superscript_close() { + $this->doc .= '</sup>'; + } + + function deleted_open() { + $this->doc .= '<del>'; + } + + function deleted_close() { + $this->doc .= '</del>'; + } + + /** + * Callback for footnote start syntax + * + * All following content will go to the footnote instead of + * the document. To achieve this the previous rendered content + * is moved to $store and $doc is cleared + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function footnote_open() { + + // move current content to store and record footnote + $this->store = $this->doc; + $this->doc = ''; + } + + /** + * Callback for footnote end syntax + * + * All rendered content is moved to the $footnotes array and the old + * content is restored from $store again + * + * @author Andreas Gohr + */ + function footnote_close() { + + // 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 + $id = count($this->footnotes)+1; + $this->footnotes[count($this->footnotes)] = $footnote; + } else { + // seen this one before, translate the index to an id and save a placeholder + $i++; + $id = count($this->footnotes)+1; + $this->footnotes[count($this->footnotes)] = "@@FNT".($i); + } + + // output the footnote reference and link, incl. onmouseover for insitu footnote popup + $this->doc .= '<a href="#fn__'.$id.'" name="fnt__'.$id.'" id="fnt__'.$id.'" class="fn_top" onmouseover="fnt(\''.$id.'\', this, event);">'.$id.')</a>'; + } + + function listu_open() { + $this->doc .= '<ul>'.DOKU_LF; + } + + function listu_close() { + $this->doc .= '</ul>'.DOKU_LF; + } + + function listo_open() { + $this->doc .= '<ol>'.DOKU_LF; + } + + function listo_close() { + $this->doc .= '</ol>'.DOKU_LF; + } + + function listitem_open($level) { + $this->doc .= '<li class="level'.$level.'">'; + } + + function listitem_close() { + $this->doc .= '</li>'.DOKU_LF; + } + + function listcontent_open() { + $this->doc .= '<div class="li">'; + } + + function listcontent_close() { + $this->doc .= '</div>'.DOKU_LF; + } + + function unformatted($text) { + $this->doc .= $this->_xmlEntities($text); + } + + /** + * Execute PHP code if allowed + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function php($text) { + global $conf; + if($conf['phpok']){ + ob_start(); + eval($text); + $this->doc .= ob_get_contents(); + ob_end_clean(); + }else{ + $this->file($text); + } + } + + /** + * Insert HTML if allowed + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function html($text) { + global $conf; + if($conf['htmlok']){ + $this->doc .= $text; + }else{ + $this->file($text); + } + } + + function preformatted($text) { + $this->doc .= '<pre class="code">' . $this->_xmlEntities($text) . '</pre>'. DOKU_LF; + } + + function file($text) { + $this->doc .= '<pre class="file">' . $this->_xmlEntities($text). '</pre>'. DOKU_LF; + } + + function quote_open() { + $this->doc .= '<blockquote><div class="no">'.DOKU_LF; + } + + function quote_close() { + $this->doc .= '</div></blockquote>'.DOKU_LF; + } + + /** + * Callback for code text + * + * Uses GeSHi to highlight language syntax + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function code($text, $language = NULL) { + global $conf; + + if ( is_null($language) ) { + $this->preformatted($text); + } else { + //strip leading and trailing blank line + $text = preg_replace('/^\s*?\n/','',$text); + $text = preg_replace('/\s*?\n$/','',$text); + $this->doc .= p_xhtml_cached_geshi($text, $language); + } + } + + function geshi_cached($text = '') { + $this->doc .= $text; + } + + function acronym($acronym) { + + if ( array_key_exists($acronym, $this->acronyms) ) { + + $title = $this->_xmlEntities($this->acronyms[$acronym]); + + $this->doc .= '<acronym title="'.$title + .'">'.$this->_xmlEntities($acronym).'</acronym>'; + + } else { + $this->doc .= $this->_xmlEntities($acronym); + } + } + + function smiley($smiley) { + if ( array_key_exists($smiley, $this->smileys) ) { + $title = $this->_xmlEntities($this->smileys[$smiley]); + $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley]. + '" class="middle" alt="'. + $this->_xmlEntities($smiley).'" />'; + } else { + $this->doc .= $this->_xmlEntities($smiley); + } + } + + /* + * not used + function wordblock($word) { + if ( array_key_exists($word, $this->badwords) ) { + $this->doc .= '** BLEEP **'; + } else { + $this->doc .= $this->_xmlEntities($word); + } + } + */ + + function entity($entity) { + if ( array_key_exists($entity, $this->entities) ) { + $this->doc .= $this->entities[$entity]; + } else { + $this->doc .= $this->_xmlEntities($entity); + } + } + + function multiplyentity($x, $y) { + $this->doc .= "$x×$y"; + } + + function singlequoteopening() { + $this->doc .= "‘"; + } + + function singlequoteclosing() { + $this->doc .= "’"; + } + + function doublequoteopening() { + $this->doc .= "“"; + } + + function doublequoteclosing() { + $this->doc .= "”"; + } + + /** + */ + function camelcaselink($link) { + $this->internallink($link,$link); + } + + + function locallink($hash, $name = NULL){ + global $ID; + $name = $this->_getLinkTitle($name, $hash, $isImage); + $hash = $this->_headerToLink($hash); + $title = $ID.' ↵'; + $this->doc .= '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">'; + $this->doc .= $name; + $this->doc .= '</a>'; + } + + /** + * Render an internal Wiki Link + * + * $search and $returnonly are not for the renderer but are used + * elsewhere - no need to implement them in other renderers + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function internallink($id, $name = NULL, $search=NULL,$returnonly=false) { + global $conf; + global $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); + $name = $this->_getLinkTitle($name, $default, $isImage, $id); + if ( !$isImage ) { + if ( $exists ) { + $class='wikilink1'; + } else { + $class='wikilink2'; + } + } else { + $class='media'; + } + + //keep hash anchor + list($id,$hash) = explode('#',$id,2); + + //prepare for formating + $link['target'] = $conf['target']['wiki']; + $link['style'] = ''; + $link['pre'] = ''; + $link['suf'] = ''; + // highlight link to current page + if ($id == $ID) { + $link['pre'] = '<span class="curid">'; + $link['suf'] = '</span>'; + } + $link['more'] = ''; + $link['class'] = $class; + $link['url'] = wl($id); + $link['name'] = $name; + $link['title'] = $id; + //add search string + if($search){ + ($conf['userewrite']) ? $link['url'].='?s=' : $link['url'].='&s='; + $link['url'] .= rawurlencode($search); + } + + //keep hash + if($hash) $link['url'].='#'.$hash; + + //output formatted + if($returnonly){ + return $this->_formatLink($link); + }else{ + $this->doc .= $this->_formatLink($link); + } + } + + function externallink($url, $name = NULL) { + global $conf; + + $name = $this->_getLinkTitle($name, $url, $isImage); + + // add protocol on simple short URLs + if(substr($url,0,3) == 'ftp' && (substr($url,0,6) != 'ftp://')) $url = 'ftp://'.$url; + if(substr($url,0,3) == 'www') $url = 'http://'.$url; + + if ( !$isImage ) { + $class='urlextern'; + } else { + $class='media'; + } + + //prepare for formating + $link['target'] = $conf['target']['extern']; + $link['style'] = ''; + $link['pre'] = ''; + $link['suf'] = ''; + $link['more'] = ''; + $link['class'] = $class; + $link['url'] = $url; + + $link['name'] = $name; + $link['title'] = $this->_xmlEntities($url); + if($conf['relnofollow']) $link['more'] .= ' rel="nofollow"'; + + //output formatted + $this->doc .= $this->_formatLink($link); + } + + /** + */ + function interwikilink($match, $name = NULL, $wikiName, $wikiUri) { + global $conf; + + $link = array(); + $link['target'] = $conf['target']['interwiki']; + $link['pre'] = ''; + $link['suf'] = ''; + $link['more'] = ''; + $link['name'] = $this->_getLinkTitle($name, $wikiUri, $isImage); + + //get interwiki URL + if ( isset($this->interwiki[$wikiName]) ) { + $url = $this->interwiki[$wikiName]; + } else { + // Default to Google I'm feeling lucky + $url = 'http://www.google.com/search?q={URL}&btnI=lucky'; + $wikiName = 'go'; + } + + 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 ){ + $link['target'] = $conf['target']['wiki']; + } + + //split into hash and url part + list($wikiUri,$hash) = explode('#',$wikiUri,2); + + //replace placeholder + if(preg_match('#\{(URL|NAME|SCHEME|HOST|PORT|PATH|QUERY)\}#',$url)){ + //use placeholders + $url = str_replace('{URL}',rawurlencode($wikiUri),$url); + $url = str_replace('{NAME}',$wikiUri,$url); + $parsed = parse_url($wikiUri); + if(!$parsed['port']) $parsed['port'] = 80; + $url = str_replace('{SCHEME}',$parsed['scheme'],$url); + $url = str_replace('{HOST}',$parsed['host'],$url); + $url = str_replace('{PORT}',$parsed['port'],$url); + $url = str_replace('{PATH}',$parsed['path'],$url); + $url = str_replace('{QUERY}',$parsed['query'],$url); + $link['url'] = $url; + }else{ + //default + $link['url'] = $url.rawurlencode($wikiUri); + } + if($hash) $link['url'] .= '#'.rawurlencode($hash); + + $link['title'] = htmlspecialchars($link['url']); + + //output formatted + $this->doc .= $this->_formatLink($link); + } + + /** + */ + function windowssharelink($url, $name = NULL) { + global $conf; + global $lang; + //simple setup + $link['target'] = $conf['target']['windows']; + $link['pre'] = ''; + $link['suf'] = ''; + $link['style'] = ''; + //Display error on browsers other than IE + $link['more'] = 'onclick="if(document.all == null){alert(\''. + $this->_xmlEntities($lang['nosmblinks'],ENT_QUOTES). + '\');}" onkeypress="if(document.all == null){alert(\''. + $this->_xmlEntities($lang['nosmblinks'],ENT_QUOTES).'\');}"'; + + $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 + $this->doc .= $this->_formatLink($link); + } + + function emaillink($address, $name = NULL) { + global $conf; + //simple setup + $link = array(); + $link['target'] = ''; + $link['pre'] = ''; + $link['suf'] = ''; + $link['style'] = ''; + $link['more'] = ''; + + $name = $this->_getLinkTitle($name, $address, $isImage); + if ( !$isImage ) { + $link['class']='mail JSnocheck'; + } else { + $link['class']='media JSnocheck'; + } + + $address = $this->_xmlEntities($address); + $address = obfuscate($address); + $title = $address; + + if(empty($name)){ + $name = $address; + } +#elseif($isImage{ +# $name = $this->_xmlEntities($name); +# } + + if($conf['mailguard'] == 'visible') $address = rawurlencode($address); + + $link['url'] = 'mailto:'.$address; + $link['name'] = $name; + $link['title'] = $title; + + //output formatted + $this->doc .= $this->_formatLink($link); + } + + function internalmedia ($src, $title=NULL, $align=NULL, $width=NULL, + $height=NULL, $cache=NULL, $linking=NULL) { + global $conf; + global $ID; + resolve_mediaid(getNS($ID),$src, $exists); + + $link = array(); + $link['class'] = 'media'; + $link['style'] = ''; + $link['pre'] = ''; + $link['suf'] = ''; + $link['more'] = ''; + $link['target'] = $conf['target']['media']; + $noLink = false; + + $link['title'] = $this->_xmlEntities($src); + list($ext,$mime) = mimetype($src); + if(substr($mime,0,5) == 'image'){ + $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),($linking=='direct')); + }elseif($mime == 'application/x-shockwave-flash'){ + // don't link flash 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),true); + } + $link['name'] = $this->_media ($src, $title, $align, $width, $height, $cache); + + //output formatted + if ($linking == 'nolink' || $noLink) $this->doc .= $link['name']; + else $this->doc .= $this->_formatLink($link); + } + + /** + * @todo don't add link for flash + */ + function externalmedia ($src, $title=NULL, $align=NULL, $width=NULL, + $height=NULL, $cache=NULL, $linking=NULL) { + global $conf; + + $link = array(); + $link['class'] = 'media'; + $link['style'] = ''; + $link['pre'] = ''; + $link['suf'] = ''; + $link['more'] = ''; + $link['target'] = $conf['target']['media']; + + $link['title'] = $this->_xmlEntities($src); + $link['url'] = ml($src,array('cache'=>$cache)); + $link['name'] = $this->_media ($src, $title, $align, $width, $height, $cache); + $noLink = false; + + list($ext,$mime) = mimetype($src); + if(substr($mime,0,5) == 'image'){ + // link only jpeg images + // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = TRUE; + }elseif($mime == 'application/x-shockwave-flash'){ + // don't link flash movies + $noLink = TRUE; + }else{ + // add file icons + $link['class'] .= ' mediafile mf_'.$ext; + } + + //output formatted + if ($linking == 'nolink' || $noLink) $this->doc .= $link['name']; + else $this->doc .= $this->_formatLink($link); + } + + /** + * Renders an RSS feed + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function rss ($url,$params){ + global $lang; + global $conf; + + require_once(DOKU_INC.'inc/FeedParser.php'); + $feed = new FeedParser(); + $feed->feed_url($url); + + //disable warning while fetching + if (!defined('DOKU_E_LEVEL')) { $elvl = error_reporting(E_ERROR); } + $rc = $feed->init(); + if (!defined('DOKU_E_LEVEL')) { error_reporting($elvl); } + + //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">'; + $this->externallink($item->get_permalink(), + $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'].' '.$name; + } + } + if($params['date']){ + $this->doc .= ' ('.$item->get_date($conf['dformat']).')'; + } + if($params['details']){ + $this->doc .= '<div class="detail">'; + if($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); + $this->doc .= '</div></li>'; + } + $this->doc .= '</ul>'; + } + + // $numrows not yet implemented + function table_open($maxcols = NULL, $numrows = NULL){ + $this->doc .= '<table class="inline">'.DOKU_LF; + } + + function table_close(){ + $this->doc .= '</table>'.DOKU_LF; + } + + function tablerow_open(){ + $this->doc .= DOKU_TAB . '<tr>' . DOKU_LF . DOKU_TAB . DOKU_TAB; + } + + function tablerow_close(){ + $this->doc .= DOKU_LF . DOKU_TAB . '</tr>' . DOKU_LF; + } + + function tableheader_open($colspan = 1, $align = NULL){ + $this->doc .= '<th'; + if ( !is_null($align) ) { + $this->doc .= ' class="'.$align.'align"'; + } + if ( $colspan > 1 ) { + $this->doc .= ' colspan="'.$colspan.'"'; + } + $this->doc .= '>'; + } + + function tableheader_close(){ + $this->doc .= '</th>'; + } + + function tablecell_open($colspan = 1, $align = NULL){ + $this->doc .= '<td'; + if ( !is_null($align) ) { + $this->doc .= ' class="'.$align.'align"'; + } + if ( $colspan > 1 ) { + $this->doc .= ' colspan="'.$colspan.'"'; + } + $this->doc .= '>'; + } + + function tablecell_close(){ + $this->doc .= '</td>'; + } + + //---------------------------------------------------------- + // Utils + + /** + * Build a link + * + * Assembles all parts defined in $link returns HTML for the link + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function _formatLink($link){ + //make sure the url is XHTML compliant (skip mailto) + if(substr($link['url'],0,7) != 'mailto:'){ + $link['url'] = str_replace('&','&',$link['url']); + $link['url'] = str_replace('&amp;','&',$link['url']); + } + //remove double encodings in titles + $link['title'] = str_replace('&amp;','&',$link['title']); + + // be sure there are no bad chars in url or title + // (we can't do this for name because it can contain an img tag) + $link['url'] = strtr($link['url'],array('>'=>'%3E','<'=>'%3C','"'=>'%22')); + $link['title'] = strtr($link['title'],array('>'=>'>','<'=>'<','"'=>'"')); + + $ret = ''; + $ret .= $link['pre']; + $ret .= '<a href="'.$link['url'].'"'; + if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"'; + if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"'; + if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"'; + if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"'; + if(!empty($link['more'])) $ret .= ' '.$link['more']; + $ret .= '>'; + $ret .= $link['name']; + $ret .= '</a>'; + $ret .= $link['suf']; + return $ret; + } + + /** + * Removes any Namespace from the given name but keeps + * casing and special chars + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + 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; + + //trim colons of a namespace link + $name = rtrim($name,':'); + + if($conf['useslash']){ + $nssep = '[:;/]'; + }else{ + $nssep = '[:;]'; + } + $name = preg_replace('!.*'.$nssep.'!','',$name); + + if(!$name) return $this->_simpleTitle($conf['start']); + return $name; + } + + /** + * Renders internal and external media + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function _media ($src, $title=NULL, $align=NULL, $width=NULL, + $height=NULL, $cache=NULL) { + + $ret = ''; + + list($ext,$mime) = mimetype($src); + if(substr($mime,0,5) == 'image'){ + //add image tag + $ret .= '<img src="'.ml($src,array('w'=>$width,'h'=>$height,'cache'=>$cache)).'"'; + $ret .= ' class="media'.$align.'"'; + + if (!is_null($title)) { + $ret .= ' title="'.$this->_xmlEntities($title).'"'; + $ret .= ' alt="'.$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($cap){ + $ret .= ' title="'.$this->_xmlEntities($cap).'"'; + $ret .= ' alt="'.$this->_xmlEntities($cap).'"'; + } + }else{ + $ret .= ' alt=""'; + } + + if ( !is_null($width) ) + $ret .= ' width="'.$this->_xmlEntities($width).'"'; + + if ( !is_null($height) ) + $ret .= ' height="'.$this->_xmlEntities($height).'"'; + + $ret .= ' />'; + + }elseif($mime == 'application/x-shockwave-flash'){ + $ret .= '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'. + ' codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"'; + if ( !is_null($width) ) $ret .= ' width="'.$this->_xmlEntities($width).'"'; + if ( !is_null($height) ) $ret .= ' height="'.$this->_xmlEntities($height).'"'; + $ret .= '>'.DOKU_LF; + $ret .= '<param name="movie" value="'.ml($src).'" />'.DOKU_LF; + $ret .= '<param name="quality" value="high" />'.DOKU_LF; + $ret .= '<embed src="'.ml($src).'"'. + ' quality="high"'; + if ( !is_null($width) ) $ret .= ' width="'.$this->_xmlEntities($width).'"'; + if ( !is_null($height) ) $ret .= ' height="'.$this->_xmlEntities($height).'"'; + $ret .= ' type="application/x-shockwave-flash"'. + ' pluginspage="http://www.macromedia.com/go/getflashplayer"></embed>'.DOKU_LF; + $ret .= '</object>'.DOKU_LF; + + }elseif(!is_null($title)){ + // well at least we have a title to display + $ret .= $this->_xmlEntities($title); + }else{ + // just show the sourcename + $ret .= $this->_xmlEntities(noNS($src)); + } + + return $ret; + } + + function _xmlEntities($string) { + return htmlspecialchars($string); + } + + /** + * Creates a linkid from a headline + * + * @param string $title The headline title + * @param boolean $create Create a new unique ID? + * @author Andreas Gohr <andi@splitbrain.org> + */ + function _headerToLink($title,$create=false) { + $title = str_replace(':','',cleanID($title)); + $title = ltrim($title,'0123456789._-'); + if(empty($title)) $title='section'; + + if($create){ + // make sure tiles are unique + $num = ''; + while(in_array($title.$num,$this->headers)){ + ($num) ? $num++ : $num = 1; + } + $title = $title.$num; + $this->headers[] = $title; + } + + return $title; + } + + /** + * Construct a title and handle images in titles + * + * @author Harry Fuecks <hfuecks@gmail.com> + */ + function _getLinkTitle($title, $default, & $isImage, $id=NULL) { + global $conf; + + $isImage = FALSE; + if ( is_null($title) ) { + if ($conf['useheading'] && $id) { + $heading = p_get_first_heading($id); + if ($heading) { + return $this->_xmlEntities($heading); + } + } + return $this->_xmlEntities($default); + } else if ( is_string($title) ) { + return $this->_xmlEntities($title); + } else if ( is_array($title) ) { + $isImage = TRUE; + return $this->_imageTitle($title); + } + } + + /** + * Returns an HTML code for images used in link titles + * + * @todo Resolve namespace on internal images + * @author Andreas Gohr <andi@splitbrain.org> + */ + function _imageTitle($img) { + return $this->_media($img['src'], + $img['title'], + $img['align'], + $img['width'], + $img['height'], + $img['cache']); + } +} + +//Setup VIM: ex: et ts=4 enc=utf-8 : |