1<?php 2 3/** 4 * DokuWiki Plugin doxycode (Snippet Syntax Component) 5 * 6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 7 * @author Lukas Probsthain <lukas.probsthain@gmail.com> 8 */ 9 10use dokuwiki\Extension\SyntaxPlugin; 11use dokuwiki\Cache\Cache; 12 13/** 14 * Class syntax_plugin_doxycode_snippet 15 * 16 * This is the main syntax of the doxycode plugin. 17 * It takes the code from a code snippet and renders it with doxygen for cross referencing. 18 * 19 * The rendering is split into building of doxygen XML files with the helper_plugin_doxycode_buildmanager 20 * helper and parsing of the XML files to HTML with the helper_plugin_doxycode_parser helper. 21 * 22 * If the sqlite plugin is installed it builds the XML through task runner jobs/task if enabled by the user, 23 * force enabled for a tag file or if a doxygen instance is already running. 24 * 25 * Which tag files and which cache files are used in the page is stored in the meta data of the page. This 26 * then used in the action_plugin_doxycode plugin for invalidating the cache. 27 * 28 * If a snippet is build through the task runner a marker is placed in the code snippet for dynamically loading 29 * the snippet and informing the user of the build progress through AJAX calls that are handled by the 30 * action_plugin_doxycode plugin. 31 */ 32class syntax_plugin_doxycode_snippet extends SyntaxPlugin 33{ 34 private $doc; 35 36 public function getType() 37 { 38 return 'substition'; 39 } 40 41 public function getSort() 42 { 43 return 158; 44 } 45 46 public function connectTo($mode) 47 { 48 $this->Lexer->addEntryPattern('<doxycode.*?>(?=.*?</doxycode>)', $mode, 'plugin_doxycode_snippet'); 49 $this->Lexer->addSpecialPattern('<doxycode.*?/>', $mode, 'plugin_doxycode_snippet'); 50 } 51 52 public function postConnect() 53 { 54 $this->Lexer->addExitPattern('</doxycode>', 'plugin_doxycode_snippet'); 55 } 56 57 public function handle($match, $state, $pos, Doku_Handler $handler) 58 { 59 static $args; 60 switch ($state) { 61 case DOKU_LEXER_ENTER: 62 case DOKU_LEXER_SPECIAL: 63 // Parse the attributes and content here 64 $args = $this->parseAttributes($match); 65 return [$state, $args]; 66 case DOKU_LEXER_UNMATCHED: 67 // Handle internal content if any 68 return [$state, ['conf' => $args, 'text' => $match]]; 69 case DOKU_LEXER_EXIT: 70 return [$state, $args]; 71 } 72 return []; 73 } 74 75 private function parseAttributes($string) 76 { 77 // Use regular expressions to parse attributes 78 // Return an associative array of attributes 79 80 $args = []; 81 82 // Split the string by spaces and get the last element as the filename 83 $parts = preg_split('/\s+/', trim($string)); 84 $lastPart = array_pop($parts); // Potentially the filename 85 86 // Remove ">" if it is at the end of the last part 87 $lastPart = rtrim($lastPart, '>'); 88 89 // Check if the last part is a filename with an extension 90 if (preg_match('/^\w+\.\w+$/', $lastPart)) { 91 $args['filename'] = $lastPart; 92 } else { 93 // If it's not a filename, add it back to the parts array 94 $parts[] = $lastPart; 95 } 96 97 // Re-join the parts without the filename 98 $remainingString = implode(' ', $parts); 99 100 // Regular expression to match key="value" pairs and flag options 101 $pattern = '/(\w+)=(?:"([^"]*)"|([^"\s]*))|(\w+)/'; 102 preg_match_all($pattern, $remainingString, $matches, PREG_SET_ORDER); 103 104 foreach ($matches as $m) { 105 if (!empty($m[1])) { 106 if (!empty($m[2])) { 107 // This is a key="value" argument 108 $args[$m[1]] = $m[2]; 109 } elseif (!empty($m[3])) { 110 // This is a key=value argument 111 $args[$m[1]] = $m[3]; 112 } 113 } elseif (!empty($m[4])) { 114 // This is a flag option 115 $args[$m[4]] = 1; 116 } 117 } 118 119 unset($args['doxycode']); 120 121 // validate the settings 122 // we need at least $text from DOKU_LEXER_UNMATCHED or VCS src 123 // TODO: if VCS import is implemented later we need to implement this check here! 124 125 // if we don't have filename, we need the language extension! 126 if (!isset($args['language']) && isset($args['filename'])) { 127 $args['language'] = pathinfo($args['filename'], PATHINFO_EXTENSION); 128 } 129 130 // TODO: sort arguments, so hashes for the attributes always stay the same 131 // otherwise the hash might change if we change the order of the arguments in the page 132 133 return $args; 134 } 135 136 /** 137 * Prepare the content of the code snippet. 138 * 139 * Currently this only removes newlines at the start and end. 140 * 141 * @param String &$text The code snippet content 142 */ 143 private function prepareText(&$text) 144 { 145 146 if ($text[0] == "\n") { 147 $text = substr($text, 1); 148 } 149 if (substr($text, -1) == "\n") { 150 $text = substr($text, 0, -1); 151 } 152 } 153 154 public function render($mode, Doku_Renderer $renderer, $data) 155 { 156 157 list($state, $data) = $data; 158 if ($mode === 'xhtml') { 159 $this->doc = ''; 160 161 // DOKU_LEXER_ENTER and DOKU_LEXER_SPECIAL: output the start of the code block 162 if ($state == DOKU_LEXER_SPECIAL || $state == DOKU_LEXER_ENTER) { 163 $this->startCodeBlock("file", $data['filename']); 164 } 165 166 // DOKU_LEXER_UNMATCHED: call renderer and output the content to the document 167 if ($state == DOKU_LEXER_UNMATCHED) { 168 $conf = $data['conf']; 169 $text = $data['text']; 170 171 // strip empty lines at start and end 172 $this->prepareText($text); 173 174 if (!isset($conf['language'])) { 175 $renderer->doc .= $this->getLang('error_language_missing'); 176 return; 177 } 178 179 // load helpers 180 // the helper functions were split so that tagmanager can be used alone in admin.php, 181 // parser can be reused by other plugins, better structure, ... 182 183 /** @var helper_plugin_doxycode_tagmanager $tagmanager */ 184 $tagmanager = plugin_load('helper', 'doxycode_tagmanager'); 185 /** @var helper_plugin_doxycode_parser $parser */ 186 $parser = plugin_load('helper', 'doxycode_parser'); 187 /** @var helper_plugin_doxycode_buildmanager $buildmanager */ 188 $buildmanager = plugin_load('helper', 'doxycode_buildmanager'); 189 /** @var helper_plugin_doxycode $helper */ 190 $helper = plugin_load('helper', 'doxycode'); 191 192 // get the tag file configuration from the tag file name list from the syntax 193 $tag_conf = $tagmanager->getFilteredTagConfig($conf['tagfiles']); 194 195 196 // load HTML from cache 197 198 // TODO: is it ok to reuse the same HTML file for multiple instances with the same settings? 199 // example problems: ACL? tag file settings per page? 200 201 // the cache name is the hash from options + code 202 $html_cacheID = md5( 203 json_encode($buildmanager->filterDoxygenAttributes($conf, true)) . $text 204 ); // cache identifier for this code snippet 205 $xml_cacheID = md5( 206 json_encode($buildmanager->filterDoxygenAttributes($conf, false)) . $text 207 ); // cache identifier for this code snippet 208 209 $html_cache = new Cache($html_cacheID, '.html'); 210 $xml_cache = new Cache($xml_cacheID, '.xml'); 211 212 // use the helper for loading the file dependencies (conf, tag_conf, tagfiles) 213 $depends = []; 214 $helper->getHTMLFileDependencies($depends, $xml_cacheID, $tag_conf); 215 216 // check if we have parsed HTML ready 217 if ($html_cache->useCache($depends)) { 218 // we have a valid HTML! 219 220 if ($cachedContent = @file_get_contents($html_cache->cache)) { 221 // append cached HTML to document 222 $renderer->doc .= $cachedContent; 223 } else { 224 msg($this->getLang('error_cache_not_readable'), 2); 225 } 226 227 // do not invoke other actions! 228 return; 229 } 230 231 // no valid HTML was found 232 // we now try to use the cached XML 233 234 $depends = []; 235 $helper->getXMLFileDependencies($depends, $tag_conf); 236 237 // this variable makes it easier to decide 238 // if we want to try to parse the XML output of doxygen at the end 239 // - cache is valid 240 // - assume STATE_FINISHED 241 // - cache was invalidated (purge, dependencies) 242 // - try directly build 243 // - direct build successful 244 // -> set STATE_FINISHED manually 245 // - build was scheduled (doxygen already running) 246 // -> $job_state reflects actual state 247 // - schedule build 248 // -> $job_state reflects actual state 249 $job_state = helper_plugin_doxycode_buildmanager::STATE_FINISHED; 250 251 if (!$xml_cache->useCache($depends)) { 252 // no valid XML cache available 253 254 // the taskID is the md5 of only the doxygen configuration 255 $conf['taskID'] = md5(json_encode($buildmanager->filterDoxygenAttributes($conf))); 256 257 // if the "render_task" option is set: 258 // output file to tmp folder for a configuration and save task in sqlite 259 // 'task runner' -> is doxygen task runner available for this page? 260 // -> loop over all meta entries 261 // -> each meta entry: unique settings comination for doxygen (tag files) 262 // -> run doxygen 263 // -> then check if rendered version is available? otherwise output information here 264 if (!$conf['render_task']) { 265 $conf['render_task'] = $tagmanager->isForceRenderTaskSet($tag_conf); 266 } 267 268 // if job handling through sqlite is not available, we get STATE_NON_EXISTENT 269 // if job handling is available the building of the XML might be already in progress 270 $job_state = $buildmanager->getJobState($xml_cacheID); 271 272 $buildsuccess = false; // vary output depending on availability of job handling and doxygen builder 273 274 // if the state is finished or non existent, we need to either schedule or build now 275 if ( 276 $job_state == helper_plugin_doxycode_buildmanager::STATE_FINISHED 277 || $job_state == helper_plugin_doxycode_buildmanager::STATE_NON_EXISTENT 278 || $job_state == helper_plugin_doxycode_buildmanager::STATE_ERROR 279 ) { 280 if (!$conf['render_task'] || plugin_isdisabled('sqlite')) { 281 // either job handling is not available or this snippet should immediately be rendered 282 283 // if lock is present: try to append as job! 284 $buildsuccess = $buildmanager->tryBuildNow($xml_cacheID, $conf, $text, $tag_conf); 285 } else { 286 // append as job 287 $buildmanager->addBuildJob($xml_cacheID, $conf, $text, $tag_conf); 288 } 289 } 290 291 // if snippet could not be build immediately or run through job handling 292 if (!$buildsuccess || $conf['render_task']) { 293 // get job state again 294 $job_state = $buildmanager->getJobState($xml_cacheID); 295 296 // add marker for javascript dynamic loading of snippet 297 $renderer->doc .= '<div class="doxycode_marker" data-doxycode-xml-hash="' . $xml_cacheID . 298 '" data-doxycode-html-hash="' . $html_cacheID . '">'; 299 300 // check if we have a job for this snippet and what its state is 301 switch ($job_state) { 302 case helper_plugin_doxycode_buildmanager::STATE_FINISHED: { 303 // this should be a good sign - next try to load the file 304 break; 305 } 306 case helper_plugin_doxycode_buildmanager::STATE_NON_EXISTENT: { 307 // task runner not available (missing sqlite?) 308 $renderer->doc .= $this->getLang('msg_not_existent'); 309 break; 310 } 311 case helper_plugin_doxycode_buildmanager::STATE_RUNNING: { 312 $renderer->doc .= $this->getLang('msg_running'); 313 break; 314 } 315 case helper_plugin_doxycode_buildmanager::STATE_SCHEDULED: { 316 $renderer->doc .= $this->getLang('msg_scheduled'); 317 break; 318 } 319 case helper_plugin_doxycode_buildmanager::STATE_ERROR: { 320 // task runner not available (missing sqlite?) 321 $renderer->doc .= $this->getLang('msg_error'); 322 break; 323 } 324 } 325 326 $renderer->doc .= '</div>'; 327 } else { 328 // if buildsuccess==true we want to parse the XML now 329 $job_state = helper_plugin_doxycode_buildmanager::STATE_FINISHED; 330 } 331 } 332 333 // render task is only set if we previously determined with a missing XML cache file that 334 // the snippet should be built through job handling 335 if ($job_state == helper_plugin_doxycode_buildmanager::STATE_FINISHED) { 336 // here we ignore the default decision 337 // the XML should be available in this case 338 // otherwise purging leaves us with empty code snippets 339 if (file_exists($xml_cache->cache)) { 340 // we have a valid XML! 341 342 $xml_content = @file_get_contents($xml_cache->cache); 343 344 $rendered_text = $parser->renderXMLToDokuWikiCode( 345 $xml_content, 346 $conf['linenumbers'], 347 $tag_conf 348 ); 349 350 // save content to cache 351 @file_put_contents($html_cache->cache, $rendered_text); 352 353 // append cached HTML to document 354 $renderer->doc .= $rendered_text; 355 } 356 } 357 358 return true; 359 } 360 361 // DOKU_LEXER_EXIT: output the end of the code block 362 if ($state == DOKU_LEXER_EXIT) { 363 $this->endCodeBlock("file", $data['filename']); 364 } 365 366 $renderer->doc .= $this->doc; 367 } elseif ($mode === 'metadata') { 368 if ($state == DOKU_LEXER_SPECIAL || $state == DOKU_LEXER_ENTER) { 369 /** @var helper_plugin_doxycode_tagmanager $tagmanager */ 370 $tagmanager = plugin_load('helper', 'doxycode_tagmanager'); 371 372 $tag_conf = $tagmanager->getFilteredTagConfig($data['tagfiles']); 373 374 // save used tag files in this page for cache invalidation if a newer tag file is available 375 // TODO: what happens if a tag file is already present in the meta data? 376 foreach ($tag_conf as $key => $conf) { 377 $renderer->meta['doxycode']['tagfiles'][] = $key; 378 } 379 } 380 381 if ($state == DOKU_LEXER_UNMATCHED) { 382 /** @var helper_plugin_doxycode_buildmanager $buildmanager */ 383 $buildmanager = plugin_load('helper', 'doxycode_buildmanager'); 384 $conf = $data['conf']; 385 $text = $data['text']; 386 387 // this is needed so the cacheID is the same as in the xhtml context 388 $this->prepareText($text); 389 390 $xml_cacheID = md5(json_encode($buildmanager->filterDoxygenAttributes($conf, false)) . $text); 391 $html_cacheID = md5(json_encode($buildmanager->filterDoxygenAttributes($conf, true)) . $text); 392 393 // add cache files to render context so page cache is invalidated if a new XML or HTML is available 394 $renderer->meta['doxycode']['xml_cachefiles'][] = $xml_cacheID; 395 $renderer->meta['doxycode']['html_cachefiles'][] = $html_cacheID; 396 } 397 } 398 399 400 401 return true; 402 } 403 404 private function startCodeBlock($type, $filename = null) 405 { 406 global $INPUT; 407 global $ID; 408 global $lang; 409 410 $ext = ''; 411 if ($filename) { 412 // add icon 413 list($ext) = mimetype($filename, false); 414 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext); 415 $class = 'mediafile mf_' . $class; 416 417 $offset = 0; 418 if ($INPUT->has('codeblockOffset')) { 419 $offset = $INPUT->str('codeblockOffset'); 420 } 421 $this->doc .= '<dl class="' . $type . '">' . DOKU_LF; 422 $this->doc .= '<dt><a href="' . 423 exportlink( 424 $ID, 425 'code', 426 array('codeblock' => $offset + 0) 427 ) . '" title="' . $lang['download'] . '" class="' . $class . '">'; 428 $this->doc .= hsc($filename); 429 $this->doc .= '</a></dt>' . DOKU_LF . '<dd>'; 430 } 431 432 $class = 'code'; //we always need the code class to make the syntax highlighting apply 433 if ($type != 'code') $class .= ' ' . $type; 434 435 $this->doc .= "<pre class=\"$class $ext\">"; 436 } 437 438 private function endCodeBlock($type, $filename = null) 439 { 440 $class = 'code'; //we always need the code class to make the syntax highlighting apply 441 if ($type != 'code') $class .= ' ' . $type; 442 443 $this->doc .= '</pre>' . DOKU_LF; 444 445 if ($filename) { 446 $this->doc .= ' </dd></dl>' . DOKU_LF; 447 } 448 } 449} 450