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