diff --git a/_test/tests/inc/parser/parser_code.test.php b/_test/tests/inc/parser/parser_code.test.php index c50d2d328dd05e2fc5fe1f615528793ae5400567..f4db5e184681dbbba2dc040def2a5e03ade1ba66 100644 --- a/_test/tests/inc/parser/parser_code.test.php +++ b/_test/tests/inc/parser/parser_code.test.php @@ -1,6 +1,11 @@ <?php require_once 'parser.inc.php'; +/** + * Tests to ensure functionality of the <code> syntax tag. + * + * @group parser_code + */ class TestOfDoku_Parser_Code extends TestOfDoku_Parser { function setUp() { @@ -68,5 +73,281 @@ class TestOfDoku_Parser_Code extends TestOfDoku_Parser { ); $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); } + + function testCodeOptionsArray_OneOption() { + $this->P->parse('Foo <code C [enable_line_numbers]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_TwoOptions() { + $this->P->parse('Foo <code C [enable_line_numbers, highlight_lines_extra="3"]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1, + 'highlight_lines_extra' => array(3) + ))), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_UnknownOption() { + $this->P->parse('Foo <code C [unknown="I will be deleted/ignored!"]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, null)), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_EnableLineNumbers1() { + $this->P->parse('Foo <code C [enable_line_numbers]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_EnableLineNumbers2() { + $this->P->parse('Foo <code C [enable_line_numbers="1"]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_EnableLineNumbers3() { + $this->P->parse('Foo <code C [enable_line_numbers="0"]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 0) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_EnableLineNumbers4() { + $this->P->parse('Foo <code C [enable_line_numbers=""]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_HighlightLinesExtra1() { + $this->P->parse('Foo <code C [enable_line_numbers, highlight_lines_extra="42, 123, 456, 789"]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1, + 'highlight_lines_extra' => array(42, 123, 456, 789) + ))), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_HighlightLinesExtra2() { + $this->P->parse('Foo <code C [enable_line_numbers, highlight_lines_extra]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1, + 'highlight_lines_extra' => array(1)) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_HighlightLinesExtra3() { + $this->P->parse('Foo <code C [enable_line_numbers, highlight_lines_extra=""]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1, + 'highlight_lines_extra' => array(1)) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_StartLineNumbersAt1() { + $this->P->parse('Foo <code C [enable_line_numbers, [enable_line_numbers, start_line_numbers_at="42"]]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1, + 'start_line_numbers_at' => 42) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_StartLineNumbersAt2() { + $this->P->parse('Foo <code C [enable_line_numbers, [enable_line_numbers, start_line_numbers_at]]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1, + 'start_line_numbers_at' => 1) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_StartLineNumbersAt3() { + $this->P->parse('Foo <code C [enable_line_numbers, [enable_line_numbers, start_line_numbers_at=""]]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_line_numbers' => 1, + 'start_line_numbers_at' => 1) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_EnableKeywordLinks1() { + $this->P->parse('Foo <code C [enable_keyword_links="false"]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_keyword_links' => false) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + + function testCodeOptionsArray_EnableKeywordLinks2() { + $this->P->parse('Foo <code C [enable_keyword_links="true"]>Test</code> Bar'); + $calls = array ( + array('document_start',array()), + array('p_open',array()), + array('cdata',array("\n".'Foo ')), + array('p_close',array()), + array('code',array('Test','C', null, + array('enable_keyword_links' => true) + )), + array('p_open',array()), + array('cdata',array(' Bar')), + array('p_close',array()), + array('document_end',array()), + ); + $this->assertEquals(array_map('stripbyteindex',$this->H->calls),$calls); + } + } diff --git a/inc/parser/handler.php b/inc/parser/handler.php index e31bf05cdd3e2ff6730fe9fd4c283d1c209fa86b..8fa1ab6140d4aef4c4d94766a6a7881426e7e34a 100644 --- a/inc/parser/handler.php +++ b/inc/parser/handler.php @@ -363,6 +363,71 @@ class Doku_Handler { return true; } + /** + * Internal function for parsing highlight options. + * $options is parsed for key value pairs separated by commas. + * A value might also be missing in which case the value will simple + * be set to true. Commas in strings are ignored, e.g. option="4,56" + * will work as expected and will only create one entry. + * + * @param string $options Comma separated list of key-value pairs, + * e.g. option1=123, option2="456" + * @return array|null Array of key-value pairs $array['key'] = 'value'; + * or null if no entries found + */ + protected function parse_highlight_options ($options) { + $result = array(); + preg_match_all('/(\w+(?:="[^"]*"))|(\w+[^=,\]])(?:,*)/', $options, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $equal_sign = strpos($match [0], '='); + if ($equal_sign === false) { + $key = trim($match[0],','); + $result [$key] = 1; + } else { + $key = substr($match[0], 0, $equal_sign); + $value = substr($match[0], $equal_sign+1); + $value = trim($value, '"'); + if (strlen($value) > 0) { + $result [$key] = $value; + } else { + $result [$key] = 1; + } + } + } + + // Check for supported options + $result = array_intersect_key( + $result, + array_flip(array( + 'enable_line_numbers', + 'start_line_numbers_at', + 'highlight_lines_extra', + 'enable_keyword_links') + ) + ); + + // Sanitize values + if(isset($result['enable_line_numbers'])) { + $result['enable_line_numbers'] = (bool) $result['enable_line_numbers']; + } + if(isset($result['highlight_lines_extra'])) { + $result['highlight_lines_extra'] = array_map('intval', explode(',', $result['highlight_lines_extra'])); + $result['highlight_lines_extra'] = array_filter($result['highlight_lines_extra']); + $result['highlight_lines_extra'] = array_unique($result['highlight_lines_extra']); + } + if(isset($result['start_line_numbers_at'])) { + $result['start_line_numbers_at'] = (int) $result['start_line_numbers_at']; + } + if(isset($result['enable_keyword_links'])) { + $result['enable_keyword_links'] = ($result['enable_keyword_links'] !== 'false'); + } + if (count($result) == 0) { + return null; + } + + return $result; + } + function file($match, $state, $pos) { return $this->code($match, $state, $pos, 'file'); } @@ -370,15 +435,20 @@ class Doku_Handler { function code($match, $state, $pos, $type='code') { if ( $state == DOKU_LEXER_UNMATCHED ) { $matches = explode('>',$match,2); - + // Cut out variable options enclosed in [] + preg_match('/\[.*\]/', $matches[0], $options); + if (!empty($options[0])) { + $matches[0] = str_replace($options[0], '', $matches[0]); + } $param = preg_split('/\s+/', $matches[0], 2, PREG_SPLIT_NO_EMPTY); while(count($param) < 2) array_push($param, null); - // We shortcut html here. if ($param[0] == 'html') $param[0] = 'html4strict'; if ($param[0] == '-') $param[0] = null; array_unshift($param, $matches[1]); - + if (!empty($options[0])) { + $param [] = $this->parse_highlight_options ($options[0]); + } $this->_addCall($type, $param, $pos); } return true; diff --git a/inc/parser/xhtml.php b/inc/parser/xhtml.php index e60d7d38b991f3c1b50f846a741a62ca35726912..2ff12f0d914906c1365b2124ac41d1fc81027f5e 100644 --- a/inc/parser/xhtml.php +++ b/inc/parser/xhtml.php @@ -601,9 +601,10 @@ class Doku_Renderer_xhtml extends Doku_Renderer { * @param string $text text to show * @param string $language programming language to use for syntax highlighting * @param string $filename file path label + * @param array $options assoziative array with additional geshi options */ - function file($text, $language = null, $filename = null) { - $this->_highlight('file', $text, $language, $filename); + function file($text, $language = null, $filename = null, $options=null) { + $this->_highlight('file', $text, $language, $filename, $options); } /** @@ -612,9 +613,10 @@ class Doku_Renderer_xhtml extends Doku_Renderer { * @param string $text text to show * @param string $language programming language to use for syntax highlighting * @param string $filename file path label + * @param array $options assoziative array with additional geshi options */ - function code($text, $language = null, $filename = null) { - $this->_highlight('code', $text, $language, $filename); + function code($text, $language = null, $filename = null, $options=null) { + $this->_highlight('code', $text, $language, $filename, $options); } /** @@ -625,8 +627,9 @@ class Doku_Renderer_xhtml extends Doku_Renderer { * @param string $text text to show * @param string $language programming language to use for syntax highlighting * @param string $filename file path label + * @param array $options assoziative array with additional geshi options */ - function _highlight($type, $text, $language = null, $filename = null) { + function _highlight($type, $text, $language = null, $filename = null, $options = null) { global $ID; global $lang; @@ -655,7 +658,7 @@ class Doku_Renderer_xhtml extends Doku_Renderer { $class = 'code'; //we always need the code class to make the syntax highlighting apply if($type != 'code') $class .= ' '.$type; - $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '').'</pre>'.DOKU_LF; + $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '', $options).'</pre>'.DOKU_LF; } if($filename) { diff --git a/inc/parserutils.php b/inc/parserutils.php index 92a5047730f31784be497484bf8b6df6e1ff9503..7933adaf3dde235fc0c89540b09806bd37e7a430 100644 --- a/inc/parserutils.php +++ b/inc/parserutils.php @@ -745,20 +745,20 @@ function p_get_first_heading($id, $render=METADATA_RENDER_USING_SIMPLE_CACHE){ * @author Christopher Smith <chris@jalakai.co.uk> * @author Andreas Gohr <andi@splitbrain.org> */ -function p_xhtml_cached_geshi($code, $language, $wrapper='pre') { +function p_xhtml_cached_geshi($code, $language, $wrapper='pre', array $options=null) { global $conf, $config_cascade, $INPUT; $language = strtolower($language); // remove any leading or trailing blank lines $code = preg_replace('/^\s*?\n|\s*?\n$/','',$code); - $cache = getCacheName($language.$code,".code"); + $optionsmd5 = md5(serialize($options)); + $cache = getCacheName($language.$code.$optionsmd5,".code"); $ctime = @filemtime($cache); if($ctime && !$INPUT->bool('purge') && $ctime > filemtime(DOKU_INC.'vendor/composer/installed.json') && // libraries changed $ctime > filemtime(reset($config_cascade['main']['default']))){ // dokuwiki changed $highlighted_code = io_readFile($cache, false); - } else { $geshi = new GeSHi($code, $language); @@ -766,6 +766,13 @@ function p_xhtml_cached_geshi($code, $language, $wrapper='pre') { $geshi->enable_classes(); $geshi->set_header_type(GESHI_HEADER_PRE); $geshi->set_link_target($conf['target']['extern']); + if($options !== null) { + foreach ($options as $function => $params) { + if(is_callable(array($geshi, $function))) { + $geshi->$function($params); + } + } + } // remove GeSHi's wrapper element (we'll replace it with our own later) // we need to use a GeSHi wrapper to avoid <BR> throughout the highlighted text diff --git a/lib/styles/geshi.less b/lib/styles/geshi.less index 5551eae00c15816e5bef7587b14742ecc6a41e59..69749e8329dcead2048363c0a2afdd0fab2b5d47 100644 --- a/lib/styles/geshi.less +++ b/lib/styles/geshi.less @@ -132,4 +132,13 @@ .re1, .st0, .st_h { color: #ff0000; } + + li, .li1 { + font-weight: normal; + vertical-align:top; + } + + .ln-xtra { + background-color: #ffc; + } }