1<?php 2 3namespace DokuWiki\Plugin\Mdpage; 4 5trait MarkdownRendererTrait { 6 private $isParsed = false; 7 private $renderPos = 0; 8 private $listLevel = 0; 9 10 abstract protected function getDokuWikiVersion(); 11 12 abstract protected function renderAbsy($blocks); 13 14 abstract protected function parse($content); 15 16 public function parseOnce($content) { 17 if ($this->isParsed) { 18 return false; 19 } 20 21 $this->isParsed = true; 22 $this->renderPos = strlen($this->renderer->doc); 23 24 return $this->parse($content); 25 } 26 27 private function getRenderResult($escapedPos = null) { 28 if ($escapedPos === null) { 29 $renderPos = $this->renderPos; 30 } else { 31 $renderPos = $escapedPos; 32 } 33 34 $result = substr($this->renderer->doc, $renderPos); 35 $this->renderPos = strlen($this->renderer->doc); 36 37 return $result; 38 } 39 40 protected function collectText($blocks) { 41 $result = ''; 42 43 foreach ($blocks as $block) { 44 if ($block[0] == 'text') { 45 $result .= $block[1]; 46 } 47 } 48 49 return $result; 50 } 51 52 // Parser 53 54 protected function renderParagraph($block) { 55 $escapedPos = $this->renderPos; 56 57 $this->renderer->p_open(); 58 $this->renderAbsy($block['content']); 59 $this->renderer->p_close(); 60 61 return $this->getRenderResult($escapedPos); 62 } 63 64 // Markdown 65 66 protected function renderText($block) { 67 $contentLines = preg_split('/ +\n/', $block[1]); 68 69 $first = true; 70 foreach ($contentLines as $contentLine) { 71 if ($first) { 72 $first = false; 73 } else { 74 $this->renderer->linebreak(); 75 } 76 $this->renderer->cdata(html_entity_decode($contentLine)); 77 } 78 79 return $this->getRenderResult(); 80 } 81 82 // block\CodeTrait 83 84 protected function renderCode($block) { 85 $lang = null; 86 if (array_key_exists('language', $block)) { 87 $lang = $block['language']; 88 } 89 90 $this->renderer->code($block['content'], $lang); 91 92 return $this->getRenderResult(); 93 } 94 95 // block\HeadlineTrait 96 97 protected function renderHeadline($block) { 98 $content = $this->collectText($block['content']); 99 100 $this->renderer->header(html_entity_decode($content), $block['level'], $this->rendererContext['pos']); 101 102 return $this->getRenderResult(); 103 } 104 105 // block\HtmlTrait 106 107 private function isCommentOnlyXMLString($content) { 108 if (preg_match('/^\s*<!--.+-->\s*$/', $content)) { 109 return true; 110 } 111 112 return false; 113 } 114 115 // Note: Fallback html rendering for DokuWiki 2018-04-22a 116 // 117 // See https://github.com/splitbrain/dokuwiki/issues/2563 118 // We should fallback for DokuWiki 2018-04-22a to avoid `Function create_function() is deprecated` 119 private function isGeshiFallbackVersion() { 120 return phpversion() >= '7.2' 121 && substr($this->getDokuWikiVersion(), 0, 10) == '2018-04-22'; 122 } 123 124 protected function renderHtml($block) { 125 $content = $block['content']."\n"; 126 127 if ($this->isCommentOnlyXMLString($content)) { 128 return ''; 129 } 130 131 global $conf; 132 if ($this->isGeshiFallbackVersion() && !$conf['htmlok']) { 133 $this->renderer->monospace_open(); 134 $this->renderer->cdata($content); 135 $this->renderer->monospace_close(); 136 } else { 137 $this->renderer->htmlblock($content); 138 } 139 140 return $this->getRenderResult(); 141 } 142 143 protected function renderInlineHtml($block) { 144 $content = $block[1]; 145 146 if ($this->isCommentOnlyXMLString($content)) { 147 return ''; 148 } 149 150 global $conf; 151 if ($this->isGeshiFallbackVersion() && !$conf['htmlok']) { 152 $this->renderer->monospace_open(); 153 $this->renderer->cdata($content); 154 $this->renderer->monospace_close(); 155 } else { 156 $this->renderer->html($content); 157 } 158 159 return $this->getRenderResult(); 160 } 161 162 // block\ListTrait 163 164 protected function renderList($block) { 165 $escapedPos = $this->renderPos; 166 167 if ($block['list'] == 'ol') { 168 $this->renderer->listo_open(); 169 } else { 170 $this->renderer->listu_open(); 171 } 172 173 foreach ($block['items'] as $item => $itemLines) { 174 $this->renderer->listitem_open($this->listLevel); 175 $this->listLevel = $this->listLevel + 1; 176 177 $this->renderer->listcontent_open(); 178 $this->renderAbsy($itemLines); 179 $this->renderer->listcontent_close(); 180 181 $this->listLevel = $this->listLevel - 1; 182 $this->renderer->listitem_close(); 183 } 184 185 if ($block['list'] == 'ol') { 186 $this->renderer->listo_close(); 187 } else { 188 $this->renderer->listu_close(); 189 } 190 191 return $this->getRenderResult($escapedPos); 192 } 193 194 // block\QuoteTrait 195 196 protected function renderQuote($block) { 197 $escapedPos = $this->renderPos; 198 199 $this->renderer->quote_open(); 200 $this->renderAbsy($block['content']); 201 $this->renderer->quote_close(); 202 203 return $this->getRenderResult($escapedPos); 204 } 205 206 // block\RuleTrait 207 208 protected function renderHr($block) { 209 $this->renderer->hr(); 210 211 return $this->getRenderResult(); 212 } 213 214 // block\TableTrait 215 216 protected function renderTable($block) { 217 $escapedPos = $this->renderPos; 218 219 $this->renderer->table_open(); 220 221 $cols = $block['cols']; 222 $first = true; 223 foreach ($block['rows'] as $row) { 224 if ($first) { 225 $first = false; 226 227 $this->renderer->tablethead_open(); 228 foreach ($row as $c => $cell) { 229 $align = empty($cols[$c]) ? null : $cols[$c]; 230 $this->renderer->tableheader_open(1, $align); 231 $this->renderAbsy($cell); 232 $this->renderer->tableheader_close(); 233 } 234 $this->renderer->tablethead_close(); 235 236 continue; 237 } 238 239 $this->renderer->tablerow_open(); 240 foreach ($row as $c => $cell) { 241 $align = empty($cols[$c]) ? null : $cols[$c]; 242 $this->renderer->tablecell_open(1, $align); 243 $this->renderAbsy($cell); 244 $this->renderer->tablecell_close(); 245 } 246 $this->renderer->tablerow_close(); 247 } 248 249 $this->renderer->table_close(); 250 251 return $this->getRenderResult($escapedPos); 252 } 253 254 // inline\CodeTrait 255 256 protected function renderInlineCode($block) { 257 $this->renderer->monospace_open(); 258 $this->renderer->cdata($block[1]); 259 $this->renderer->monospace_close(); 260 261 return $this->getRenderResult(); 262 } 263 264 // inline\EmphStrongTrait 265 266 protected function renderStrong($block) { 267 $escapedPos = $this->renderPos; 268 269 $this->renderer->strong_open(); 270 $this->renderAbsy($block[1]); 271 $this->renderer->strong_close(); 272 273 return $this->getRenderResult($escapedPos); 274 } 275 276 protected function renderEmph($block) { 277 $escapedPos = $this->renderPos; 278 279 $this->renderer->emphasis_open(); 280 $this->renderAbsy($block[1]); 281 $this->renderer->emphasis_close(); 282 283 return $this->getRenderResult($escapedPos); 284 } 285 286 // inline\LinkTrait 287 288 protected function renderEmail($block) { 289 $this->renderer->emaillink($block[1]); 290 291 return $this->getRenderResult(); 292 } 293 294 protected function renderUrl($block) { 295 $this->renderer->externallink($block[1]); 296 297 return $this->getRenderResult(); 298 } 299 300 abstract protected function lookupReference($key); 301 302 abstract protected function parseInline($line); 303 304 private function lookupRefKeyWithFallback($prefix, $block) { 305 if (!isset($block['refkey'])) { 306 return $block; 307 } 308 309 if (($ref = $this->lookupReference($block['refkey'])) !== false) { 310 return array_merge($block, $ref); 311 } 312 313 $prefix_len = strlen($prefix); 314 if (strncmp($block['orig'], $prefix, $prefix_len) === 0) { 315 $this->renderer->cdata($prefix); 316 $this->renderAbsy($this->parseInline(substr($block['orig'], $prefix_len))); 317 } else { 318 $this->renderer->cdata($block['orig']); 319 } 320 321 return false; 322 } 323 324 /** 325 * Note: Avoid License Conflicting for Links with Titles 326 * 327 * DokuWiki is not supported links with titles, but Markdown is supported it. 328 * We decided not to support links with titles before 2.1.0. However, since 329 * many users voting the feature, we support it from 2.2.0. 330 * 331 * The simple way to support links with titles is copying methods from DokuWiki 332 * and modifying. However, DokuWiki is licensed under the GPL-2.0-or-later and 333 * this plugin is licensed under the Apache-2.0 OR GPL-2.0-or-later. So, we cannot 334 * use parts of DokuWiki's source codes. Therefore, we use dangerous operations to 335 * support links with titles for user needs. Be careful for this feature. 336 * 337 * Ref: https://github.com/mizunashi-mana/dokuwiki-plugin-mdpage/issues/35 338 */ 339 protected function renderLink($block) { 340 $escapedPos = $this->renderPos; 341 342 if (($block = $this->lookupRefKeyWithFallback('[', $block)) === false) { 343 return $this->getRenderResult($escapedPos); 344 } 345 346 // See https://github.com/splitbrain/dokuwiki/blob/cbaf278c50e5baf946b3bd606c369735fe0953be/inc/parser/handler.php#L527 347 $url = $block['url']; 348 $text = $this->collectText($block['text']); 349 $title = $block['title']; 350 351 if (link_isinterwiki($url)) { 352 // Interwiki 353 $interwiki = explode('>', $url, 2); 354 $this->renderDokuWikiInterwikiLink($url, $text, strtolower($interwiki[0]), $interwiki[1], $title); 355 } elseif (preg_match('#^([a-z0-9\-\.+]+?)://#i', $url)) { 356 // external link (accepts all protocols) 357 $this->renderDokuWikiExternalLink($url, $text, $title); 358 } elseif (preg_match('!^#.+!', $url)) { 359 // local link 360 $this->renderDokuWikiLocalLink(substr($url, 1), $text, $title); 361 } else { 362 // internal link 363 $this->renderDokuWikiInternalLink($url, $text, $title); 364 } 365 366 return $this->getRenderResult($escapedPos); 367 } 368 369 private function renderDokuWikiInterwikiLink($match, $name, $wikiName, $wikiUri, $title = null) { 370 $escapedPos = $this->renderPos; 371 372 $this->renderer->interwikilink($match, $name, $wikiName, $wikiUri); 373 374 if ($title === null) { 375 return; 376 } 377 378 // See the note "Avoid License Conflicting for Links with Titles" 379 $renderedContent = substr($this->renderer->doc, $escapedPos); 380 $replacedContent = $this->replaceDokuWikiLinkTitle($renderedContent, $title); 381 $this->renderer->doc = substr_replace($this->renderer->doc, $replacedContent, $escapedPos); 382 } 383 384 private function renderDokuWikiExternalLink($url, $name, $title = null) { 385 $escapedPos = $this->renderPos; 386 387 $this->renderer->externallink($url, $name); 388 389 if ($title === null) { 390 return; 391 } 392 393 // See the note "Avoid License Conflicting for Links with Titles" 394 $renderedContent = substr($this->renderer->doc, $escapedPos); 395 $replacedContent = $this->replaceDokuWikiLinkTitle($renderedContent, $title); 396 $this->renderer->doc = substr_replace($this->renderer->doc, $replacedContent, $escapedPos); 397 } 398 399 private function renderDokuWikiLocalLink($hash, $name, $title = null) { 400 $escapedPos = $this->renderPos; 401 402 $this->renderer->locallink($hash, $name); 403 404 if ($title === null) { 405 return; 406 } 407 408 // See the note "Avoid License Conflicting for Links with Titles" 409 $renderedContent = substr($this->renderer->doc, $escapedPos); 410 $replacedContent = $this->replaceDokuWikiLinkTitle($renderedContent, $title); 411 $this->renderer->doc = substr_replace($this->renderer->doc, $replacedContent, $escapedPos); 412 } 413 414 private function renderDokuWikiInternalLink($id, $name, $title = null) { 415 $escapedPos = $this->renderPos; 416 417 $this->renderer->internallink($id, $name); 418 419 if ($title === null) { 420 return; 421 } 422 423 // See the note "Avoid License Conflicting for Links with Titles" 424 $renderedContent = substr($this->renderer->doc, $escapedPos); 425 $replacedContent = $this->replaceDokuWikiLinkTitle($renderedContent, $title); 426 $this->renderer->doc = substr_replace($this->renderer->doc, $replacedContent, $escapedPos); 427 } 428 429 /** 430 * Ref: https://github.com/splitbrain/dokuwiki/blob/release_stable_2020-07-29/inc/parser/xhtml.php#L1601. 431 */ 432 private function replaceDokuWikiLinkTitle($linkContent, $title) { 433 $replacedTitle = strtr( 434 htmlspecialchars($title), 435 [ 436 '>' => '%3E', 437 '<' => '%3C', 438 '"' => '%22', 439 ] 440 ); 441 442 return preg_replace( 443 '/<a href=([^>]*) title="([^"]*)"([^>]*)>/', 444 '<a href=$1 title="'.$replacedTitle.'"$3>', 445 $linkContent 446 ); 447 } 448 449 protected function renderImage($block) { 450 $escapedPos = $this->renderPos; 451 452 if (($block = $this->lookupRefKeyWithFallback('![', $block)) === false) { 453 return $this->getRenderResult($escapedPos); 454 } 455 456 // See https://github.com/splitbrain/dokuwiki/blob/cbaf278c50e5baf946b3bd606c369735fe0953be/inc/parser/handler.php#L722 457 $url = $block['url']; 458 $text = $block['text']; 459 460 if (media_isexternal($url) || link_isinterwiki($url)) { 461 $this->renderer->externalmedia($url, $text); 462 } else { 463 $this->renderer->internalmedia($url, $text); 464 } 465 466 return $this->getRenderResult($escapedPos); 467 } 468 469 // inline\StrikeoutTrait 470 471 protected function renderStrike($block) { 472 $escapedPos = $this->renderPos; 473 474 $this->renderer->deleted_open(); 475 $this->renderAbsy($block[1]); 476 $this->renderer->deleted_close(); 477 478 return $this->getRenderResult($escapedPos); 479 } 480 481 // inline\UrlLinkTrait 482 483 protected function renderAutoUrl($block) { 484 $this->renderer->externallink($block[1]); 485 486 return $this->getRenderResult(); 487 } 488} 489