Merge pull request #2307 from splitbrain/geshi_features

Geshi Option parsing
This commit is contained in:
Andreas Gohr 2018-04-15 15:47:31 +02:00 committed by GitHub
commit 0387686cd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 465 additions and 13 deletions

View File

@ -171,4 +171,24 @@ abstract class DokuWikiTest extends PHPUnit_Framework_TestCase {
$last = $now;
return $now;
}
/**
* Allow for testing inaccessible methods (private or protected)
*
* This makes it easier to test protected methods without needing to create intermediate
* classes inheriting and changing the access.
*
* @link https://stackoverflow.com/a/8702347/172068
* @param object $obj Object in which to call the method
* @param string $func The method to call
* @param array $args The arguments to call the method with
* @return mixed
* @throws ReflectionException when the given obj/func does not exist
*/
protected static function callInaccessibleMethod($obj, $func, array $args) {
$class = new \ReflectionClass($obj);
$method = $class->getMethod($func);
$method->setAccessible(true);
return $method->invokeArgs($obj, $args);
}
}

View File

@ -0,0 +1,56 @@
<?php
class TestOfDoku_Handler_ParseHighlightOptions extends DokuWikiTest {
public function dataProvider() {
return [
['', null],
['something weird', null],
['enable_line_numbers', ['enable_line_numbers' => true]],
['enable_line_numbers=1', ['enable_line_numbers' => true]],
['enable_line_numbers="1"', ['enable_line_numbers' => true]],
['enable_line_numbers=0', ['enable_line_numbers' => false]],
['enable_line_numbers="0"', ['enable_line_numbers' => false]],
['enable_line_numbers=false', ['enable_line_numbers' => false]],
['enable_line_numbers="false"', ['enable_line_numbers' => false]],
['highlight_lines_extra', ['highlight_lines_extra' => [1]]],
['highlight_lines_extra=17', ['highlight_lines_extra' => [17]]],
['highlight_lines_extra=17,19', ['highlight_lines_extra' => [17, 19]]],
['highlight_lines_extra="17,19"', ['highlight_lines_extra' => [17, 19]]],
['highlight_lines_extra="17,19,17"', ['highlight_lines_extra' => [17, 19]]],
['start_line_numbers_at', ['start_line_numbers_at' => 1]],
['start_line_numbers_at=12', ['start_line_numbers_at' => 12]],
['start_line_numbers_at="12"', ['start_line_numbers_at' => 12]],
['enable_keyword_links', ['enable_keyword_links' => true]],
['enable_keyword_links=1', ['enable_keyword_links' => true]],
['enable_keyword_links="1"', ['enable_keyword_links' => true]],
['enable_keyword_links=0', ['enable_keyword_links' => false]],
['enable_keyword_links="0"', ['enable_keyword_links' => false]],
['enable_keyword_links=false', ['enable_keyword_links' => false]],
['enable_keyword_links="false"', ['enable_keyword_links' => false]],
[
'enable_line_numbers weird nothing highlight_lines_extra=17,19 start_line_numbers_at="12" enable_keyword_links=false',
[
'enable_line_numbers' => true,
'highlight_lines_extra' => [17, 19],
'start_line_numbers_at' => 12,
'enable_keyword_links' => false
]
],
];
}
/**
* @dataProvider dataProvider
* @param string $input options to parse
* @param array|null $expect expected outcome
* @throws ReflectionException
*/
public function testOptionParser($input, $expect) {
$h = new Doku_Handler();
$output = $this->callInaccessibleMethod($h, 'parse_highlight_options', [$input]);
$this->assertEquals($expect, $output);
}
}

View File

@ -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' => true,
'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' => 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);
}
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' => 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);
}
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' => 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_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' => 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);
}
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' => true,
'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' => true,
'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' => true,
'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' => true,
'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' => true,
'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' => true,
'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);
}
}

View File

@ -363,6 +363,77 @@ 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 space separated list of key-value pairs,
* e.g. option1=123, option2="456"
* @return array|null Array of key-value pairs $array['key'] = 'value';
* or null if no entries found
*/
protected function parse_highlight_options ($options) {
$result = array();
preg_match_all('/(\w+(?:="[^"]*"))|(\w+(?:=[^\s]*))|(\w+[^=\s\]])(?:\s*)/', $options, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$equal_sign = strpos($match [0], '=');
if ($equal_sign === false) {
$key = trim($match[0]);
$result [$key] = 1;
} else {
$key = substr($match[0], 0, $equal_sign);
$value = substr($match[0], $equal_sign+1);
$value = trim($value, '"');
if (strlen($value) > 0) {
$result [$key] = $value;
} else {
$result [$key] = 1;
}
}
}
// Check for supported options
$result = array_intersect_key(
$result,
array_flip(array(
'enable_line_numbers',
'start_line_numbers_at',
'highlight_lines_extra',
'enable_keyword_links')
)
);
// Sanitize values
if(isset($result['enable_line_numbers'])) {
if($result['enable_line_numbers'] === 'false') {
$result['enable_line_numbers'] = false;
}
$result['enable_line_numbers'] = (bool) $result['enable_line_numbers'];
}
if(isset($result['highlight_lines_extra'])) {
$result['highlight_lines_extra'] = array_map('intval', explode(',', $result['highlight_lines_extra']));
$result['highlight_lines_extra'] = array_filter($result['highlight_lines_extra']);
$result['highlight_lines_extra'] = array_unique($result['highlight_lines_extra']);
}
if(isset($result['start_line_numbers_at'])) {
$result['start_line_numbers_at'] = (int) $result['start_line_numbers_at'];
}
if(isset($result['enable_keyword_links'])) {
if($result['enable_keyword_links'] === 'false') {
$result['enable_keyword_links'] = false;
}
$result['enable_keyword_links'] = (bool) $result['enable_keyword_links'];
}
if (count($result) == 0) {
return null;
}
return $result;
}
function file($match, $state, $pos) {
return $this->code($match, $state, $pos, 'file');
}
@ -370,15 +441,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;

View File

@ -633,9 +633,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);
}
/**
@ -644,9 +645,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);
}
/**
@ -657,8 +659,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;
global $INPUT;
@ -694,7 +697,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) {

View File

@ -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

View File

@ -132,4 +132,13 @@
.re1, .st0, .st_h {
color: #ff0000;
}
li, .li1 {
font-weight: normal;
vertical-align:top;
}
.ln-xtra {
background-color: #ffc;
}
}

View File

@ -4024,7 +4024,7 @@ class GeSHi {
$parsed_code .= str_repeat('</span>', $close);
$close = 0;
}
elseif ($i + 1 < $n) {
if ($i + 1 < $n) {
$parsed_code .= "\n";
}
unset($code[$i]);