1<?php 2/** 3 * DokuWiki Plugin Code Prettifier 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Satoshi Sahara <sahara.satoshi@gmail.com> 7 * 8 * usage: ex. <Code:css linenums:5 lang-css | title > ... </Code> 9 */ 10 11if (!defined('DOKU_INC')) die(); 12 13class syntax_plugin_codeprettify_code extends DokuWiki_Syntax_Plugin 14{ 15 public function getType() 16 { // Syntax Type 17 return 'protected'; 18 } 19 20 public function getPType() 21 { // Paragraph Type 22 return 'block'; 23 } 24 25 /** 26 * Connect pattern to lexer 27 */ 28 protected $mode, $pattern; 29 30 public function getSort() 31 { // sort number used to determine priority of this mode 32 return 199; // < native 'code' mode (=200) 33 } 34 35 public function preConnect() 36 { 37 // syntax mode, drop 'syntax_' from class name 38 $this->mode = substr(get_class($this), 7); 39 40 // allowing nested "<angle pairs>" in title using regex atomic grouping 41 $n = 3; 42 $param = str_repeat('(?>[^<>\n]+|<', $n).str_repeat('>)*', $n); 43 44 // syntax patterns 45 $this->pattern[1] = '<Code\b'.$param.'>'.'(?=.*?</Code>)'; 46 $this->pattern[4] = '</Code>'; 47 48 // DokuWiki original syntax patterns 49 $this->pattern[11] = '<code\b.*?>(?=.*?</code>)'; 50 $this->pattern[14] = '</code>'; 51 } 52 53 public function connectTo($mode) 54 { 55 $this->Lexer->addEntryPattern($this->pattern[1], $mode, $this->mode); 56 if ($this->getConf('override')) { 57 $this->Lexer->addEntryPattern($this->pattern[11], $mode, $this->mode); 58 } 59 } 60 61 public function postConnect() 62 { 63 $this->Lexer->addExitPattern($this->pattern[4], $this->mode); 64 if ($this->getConf('override')) { 65 $this->Lexer->addExitPattern($this->pattern[14], $this->mode); 66 } 67 } 68 69 70 /** 71 * GeSHi Options Parser 72 * 73 * DokuWiki release 2018-04-22 "Greebo" supports some GeSHi options 74 * for syntax highlighting 75 * alternative of parse_highlight_options() in inc/parser/handler.php 76 * 77 * @param string $params space separated list of key-value pairs 78 * @return array 79 * @see also https://www.dokuwiki.org/syntax_highlighting 80 */ 81 private function getGeshiOption($params) 82 { 83 $opts = []; 84 // remove enclosing brackets and double-quotes 85 $params = str_replace('"', '', trim($params, '[]')); 86 if (preg_match_all('/(\w+)=?(\w+)?/', $params, $matches)) { 87 88 // make keys lowercase 89 $keys = array_map('strtolower', $matches[1]); 90 // interpret boolian string values 91 $values = array_map( 92 function($value) { 93 if (is_numeric($value)) { 94 return $value; 95 } else { 96 $s = strtolower($value); 97 if ($s == 'true') $value = 1; 98 if ($s == 'false') $value = 0; 99 return $value; 100 } 101 }, 102 $matches[2] 103 ); 104 105 // Note: last one prevails if same keys have appeared 106 $opts = array_combine($keys, $values); 107 } 108 return $opts; 109 } 110 111 /** 112 * Convert/interpret GeSHi Options to correspondent Prettifier options 113 * - enable_line_numbers=0 -> nolinenums 114 * - start_line_numbers_at=1 -> linenums:1 115 * 116 * @param array $opts GeSHi options 117 * @return string Prettifier linenums parameter 118 * @see also https://www.dokuwiki.org/syntax_highlighting 119 */ 120 private function strGeshiOptions(array $opts=[]) 121 { 122 if (isset($opts['enable_line_numbers'])) { 123 $option = &$opts['enable_line_numbers']; 124 $prefix = ($option == 0) ? 'no' : ''; 125 } 126 if (isset($opts['start_line_numbers_at'])) { 127 $option = &$opts['start_line_numbers_at']; 128 $suffix = ($option > 0) ? ':'.$option : ''; 129 } 130 return ($prefix or $suffix) ? $prefix.'linenums'.$suffix : ''; 131 } 132 133 134 /** 135 * Prettifier Options Parser 136 * 137 * @param string $params 138 * @return array 139 */ 140 private function getPrettifierOptions($params) 141 { 142 $opts = []; 143 144 // offset holds the position of the matched string 145 // if offset become 0, the first token of given params is NOT language 146 $offset = 1; 147 if (preg_match('/\b(no)?linenums(:\d+)?/', $params, $m, PREG_OFFSET_CAPTURE)) { 148 $offset = ($offset > 0) ? $m[0][1] : 1; 149 $opts['linenums'] = $m[1][0] ? '' : $m[0][0]; 150 } else { 151 $opts['linenums'] = $this->getConf('linenums') ? 'linenums' : ''; 152 } 153 if (preg_match('/\blang-\w+/', $params, $m, PREG_OFFSET_CAPTURE)) { 154 $offset = ($offset > 0) ? $m[0][1] : 1; 155 $opts['language'] = $m[0][0]; 156 } elseif ($offset) { 157 // assume the first token is language; ex. C, php, css 158 list ($lang, ) = explode(' ', $params, 2); 159 $opts['language'] = $lang ? 'lang-'.$lang : ''; 160 } 161 return $opts; 162 } 163 164 165 /** 166 * Handle the match 167 */ 168 public function handle($match, $state, $pos, Doku_Handler $handler) 169 { 170 switch ($state) { 171 case DOKU_LEXER_ENTER: 172 list($params, $title) = explode('|', substr($match, 5, -1), 2); 173 174 // title parameter 175 if ($title) { 176 // remove first "document_start" and last "document_end" instructions 177 $calls = array_slice(p_get_instructions($title), 1, -1); 178 } else { 179 $calls = null; 180 } 181 182 // prettifier parameters 183 $params = trim($params, ' :'); 184 185 if ( preg_match('/\[.*\]/', $params, $matches) ) { 186 // replace GeSHi parameters 187 $params = str_replace( 188 $matches[0], 189 $this->strGeshiOptions( $this->getGeshiOption($matches[0]) ), 190 $params 191 ); 192 } 193 194 $opts['prettify'] = 'prettyprint'; 195 $opts += $this->getPrettifierOptions($params); 196 $params= implode(' ', $opts); 197 198 return $data = [$state, $params, $calls]; 199 case DOKU_LEXER_UNMATCHED: 200 return $data = [$state, $match]; 201 case DOKU_LEXER_EXIT: 202 return $data = [$state, '']; 203 } 204 return false; 205 } 206 207 /** 208 * Create output 209 */ 210 function render($format, Doku_Renderer $renderer, $data) 211 { 212 if ($format == 'metadata') return false; 213 if (empty($data)) return false; 214 list($state, $args, $calls) = $data; 215 216 switch ($state) { 217 case DOKU_LEXER_ENTER: 218 if (isset($calls)) { 219 // title of code box 220 $renderer->doc .= '<div class="plugin_codeprettify">'; 221 $renderer->nest($calls); 222 $renderer->doc .= '</div>'; 223 } 224 $renderer->doc .= '<pre class="'.hsc($args).'">'; 225 break; 226 case DOKU_LEXER_UNMATCHED: 227 $renderer->doc .= $renderer->_xmlEntities($args); 228 break; 229 case DOKU_LEXER_EXIT: 230 $renderer->doc .= '</pre>'; 231 break; 232 } 233 return true; 234 } 235 236} 237