1<?php 2/** 3 * DokuWiki Plugin prosemirror (Renderer Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Andreas Gohr <gohr@cosmocode.de> 7 */ 8 9// must be run within Dokuwiki 10use dokuwiki\plugin\prosemirror\schema\Mark; 11use dokuwiki\plugin\prosemirror\schema\Node; 12use dokuwiki\plugin\prosemirror\schema\NodeStack; 13 14if (!defined('DOKU_INC')) { 15 die(); 16} 17 18require_once DOKU_INC . 'inc/parser/renderer.php'; 19 20class renderer_plugin_prosemirror extends Doku_Renderer 21{ 22 23 /** @var NodeStack */ 24 public $nodestack; 25 26 /** @var NodeStack[] */ 27 protected $nodestackBackup = []; 28 29 /** @var array list of currently active formatting marks */ 30 protected $marks = []; 31 32 /** @var int column counter for table handling */ 33 protected $colcount = 0; 34 35 /** 36 * The format this renderer produces 37 */ 38 public function getFormat() 39 { 40 return 'prosemirror'; 41 } 42 43 public function addToNodestackTop(Node $node) 44 { 45 $this->nodestack->addTop($node); 46 } 47 48 public function addToNodestack(Node $node) 49 { 50 $this->nodestack->add($node); 51 } 52 53 public function dropFromNodeStack($nodeType) 54 { 55 $this->nodestack->drop($nodeType); 56 } 57 58 public function getCurrentMarks() 59 { 60 return $this->marks; 61 } 62 63 /** 64 * If there is a block scope open, close it. 65 */ 66 protected function clearBlock() 67 { 68 $parentNode = $this->nodestack->current()->getType(); 69 if (in_array($parentNode, ['paragraph'])) { 70 $this->nodestack->drop($parentNode); 71 } 72 } 73 74 // FIXME implement all methods of Doku_Renderer here 75 76 /** @inheritDoc */ 77 public function document_start() 78 { 79 $this->nodestack = new NodeStack(); 80 } 81 82 /** @inheritDoc */ 83 public function document_end() 84 { 85 if ($this->nodestack->isEmpty()) { 86 $this->p_open(); 87 $this->p_close(); 88 } 89 $this->doc = json_encode($this->nodestack->doc(), JSON_PRETTY_PRINT); 90 } 91 92 public function nocache() { 93 $docNode = $this->nodestack->getDocNode(); 94 $docNode->attr('nocache', true); 95 } 96 97 public function notoc() 98 { 99 $docNode = $this->nodestack->getDocNode(); 100 $docNode->attr('notoc', true); 101 } 102 103 /** @inheritDoc */ 104 public function p_open() 105 { 106 $this->nodestack->addTop(new Node('paragraph')); 107 } 108 109 /** @inheritdoc */ 110 public function p_close() 111 { 112 $this->nodestack->drop('paragraph'); 113 } 114 115 /** @inheritDoc */ 116 public function quote_open() 117 { 118 if ($this->nodestack->current()->getType() === 'paragraph') { 119 $this->nodestack->drop('paragraph'); 120 } 121 $this->nodestack->addTop(new Node('blockquote')); 122 } 123 124 /** @inheritDoc */ 125 public function quote_close() 126 { 127 if ($this->nodestack->current()->getType() === 'paragraph') { 128 $this->nodestack->drop('paragraph'); 129 } 130 $this->nodestack->drop('blockquote'); 131 } 132 133 #region lists 134 135 /** @inheritDoc */ 136 public function listu_open() 137 { 138 if ($this->nodestack->current()->getType() === 'paragraph') { 139 $this->nodestack->drop('paragraph'); 140 } 141 142 $this->nodestack->addTop(new Node('bullet_list')); 143 } 144 145 /** @inheritDoc */ 146 public function listu_close() 147 { 148 $this->nodestack->drop('bullet_list'); 149 } 150 151 /** @inheritDoc */ 152 public function listo_open() 153 { 154 if ($this->nodestack->current()->getType() === 'paragraph') { 155 $this->nodestack->drop('paragraph'); 156 } 157 158 $this->nodestack->addTop(new Node('ordered_list')); 159 } 160 161 /** @inheritDoc */ 162 public function listo_close() 163 { 164 $this->nodestack->drop('ordered_list'); 165 } 166 167 /** @inheritDoc */ 168 public function listitem_open($level, $node = false) 169 { 170 $this->nodestack->addTop(new Node('list_item')); 171 172 $paragraphNode = new Node('paragraph'); 173 $this->nodestack->addTop($paragraphNode); 174 } 175 176 /** @inheritDoc */ 177 public function listitem_close() 178 { 179 180 if ($this->nodestack->current()->getType() === 'paragraph') { 181 $this->nodestack->drop('paragraph'); 182 } 183 $this->nodestack->drop('list_item'); 184 } 185 186 #endregion lists 187 188 #region table 189 190 /** @inheritDoc */ 191 public function table_open($maxcols = null, $numrows = null, $pos = null) 192 { 193 $this->nodestack->addTop(new Node('table')); 194 } 195 196 /** @inheritDoc */ 197 public function table_close($pos = null) 198 { 199 $this->nodestack->drop('table'); 200 } 201 202 /** @inheritDoc */ 203 public function tablerow_open() 204 { 205 $this->nodestack->addTop(new Node('table_row')); 206 $this->colcount = 0; 207 } 208 209 /** @inheritDoc */ 210 public function tablerow_close() 211 { 212 $node = $this->nodestack->drop('table_row'); 213 $node->attr('columns', $this->colcount); 214 } 215 216 /** @inheritDoc */ 217 public function tablecell_open($colspan = 1, $align = null, $rowspan = 1) 218 { 219 $this->openTableCell('table_cell', $colspan, $align, $rowspan); 220 } 221 222 /** @inheritdoc */ 223 public function tablecell_close() 224 { 225 $this->closeTableCell('table_cell'); 226 } 227 228 /** @inheritDoc */ 229 public function tableheader_open($colspan = 1, $align = null, $rowspan = 1) 230 { 231 $this->openTableCell('table_header', $colspan, $align, $rowspan); 232 } 233 234 /** @inheritdoc */ 235 public function tableheader_close() 236 { 237 $this->closeTableCell('table_header'); 238 } 239 240 /** 241 * Add a new table cell to the top of the stack 242 * 243 * @param string $type either table_cell or table_header 244 * @param int $colspan 245 * @param string|null $align either null/left, center or right 246 * @param int $rowspan 247 */ 248 protected function openTableCell($type, $colspan, $align, $rowspan) { 249 $this->colcount += $colspan; 250 251 $node = new Node($type); 252 $node->attr('colspan', $colspan); 253 $node->attr('rowspan', $rowspan); 254 $node->attr('align', $align); 255 $this->nodestack->addTop($node); 256 257 $node = new Node('paragraph'); 258 $this->nodestack->addTop($node); 259 } 260 261 /** 262 * Remove a table cell from the top of the stack 263 * 264 * @param string $type either table_cell or table_header 265 */ 266 protected function closeTableCell($type) 267 { 268 if ($this->nodestack->current()->getType() === 'paragraph') { 269 $this->nodestack->drop('paragraph'); 270 } 271 272 $curNode = $this->nodestack->current(); 273 $curNode->trimContentLeft(); 274 $curNode->trimContentRight(); 275 $this->nodestack->drop($type); 276 } 277 278 #endregion table 279 280 /** @inheritDoc */ 281 public function header($text, $level, $pos) 282 { 283 $node = new Node('heading'); 284 $node->attr('level', $level); 285 286 $tnode = new Node('text'); 287 $tnode->setText($text); 288 $node->addChild($tnode); 289 290 $this->nodestack->add($node); 291 } 292 293 /** @inheritDoc */ 294 public function cdata($text) 295 { 296 if ($text === '') { 297 return; 298 } 299 300 $parentNode = $this->nodestack->current()->getType(); 301 302 if (in_array($parentNode, ['paragraph', 'footnote'])) { 303 $text = str_replace("\n", ' ', $text); 304 } 305 306 if ($parentNode === 'list_item') { 307 $node = new Node('paragraph'); 308 $this->nodestack->addTop($node); 309 } 310 311 if ($parentNode === 'blockquote') { 312 $node = new Node('paragraph'); 313 $this->nodestack->addTop($node); 314 } 315 316 if ($parentNode === 'doc') { 317 $node = new Node('paragraph'); 318 $this->nodestack->addTop($node); 319 } 320 321 $node = new Node('text'); 322 $node->setText($text); 323 foreach (array_keys($this->marks) as $mark) { 324 $node->addMark(new Mark($mark)); 325 } 326 $this->nodestack->add($node); 327 } 328 329 public function preformatted($text) 330 { 331 $this->clearBlock(); 332 $node = new Node('preformatted'); 333 $this->nodestack->addTop($node); 334 $this->cdata($text); 335 $this->nodestack->drop('preformatted'); 336 } 337 338 public function code($text, $lang = null, $file = null) 339 { 340 $this->clearBlock(); 341 $node = new Node('code_block'); 342 $node->attr('class', 'code ' . $lang); 343 $node->attr('data-language', $lang); 344 $node->attr('data-filename', $file); 345 $this->nodestack->addTop($node); 346 $this->cdata(trim($text, "\n")); 347 $this->nodestack->drop('code_block'); 348 } 349 350 public function file($text, $lang = null, $file = null) 351 { 352 $this->code($text, $lang, $file); 353 } 354 355 public function html($text) 356 { 357 $node = new Node('html_inline'); 358 $node->attr('class', 'html_inline'); 359 $this->nodestack->addTop($node); 360 $this->cdata(str_replace("\n", ' ', $text)); 361 $this->nodestack->drop('html_inline'); 362 } 363 364 public function htmlblock($text) 365 { 366 $this->clearBlock(); 367 $node = new Node('html_block'); 368 $node->attr('class', 'html_block'); 369 $this->nodestack->addTop($node); 370 $this->cdata(trim($text, "\n")); 371 $this->nodestack->drop('html_block'); 372 } 373 374 public function php($text) 375 { 376 $node = new Node('php_inline'); 377 $node->attr('class', 'php_inline'); 378 $this->nodestack->addTop($node); 379 $this->cdata(str_replace("\n", ' ', $text)); 380 $this->nodestack->drop('php_inline'); 381 } 382 383 public function phpblock($text) 384 { 385 $this->clearBlock(); 386 $node = new Node('php_block'); 387 $node->attr('class', 'php_block'); 388 $this->nodestack->addTop($node); 389 $this->cdata(trim($text, "\n")); 390 $this->nodestack->drop('php_block'); 391 } 392 393 /** 394 * @inheritDoc 395 */ 396 public function rss($url, $params) 397 { 398 $this->clearBlock(); 399 $node = new Node('rss'); 400 $node->attr('url', hsc($url)); 401 $node->attr('max', $params['max']); 402 $node->attr('reverse', (bool)$params['reverse']); 403 $node->attr('author', (bool)$params['author']); 404 $node->attr('date', (bool)$params['date']); 405 $node->attr('details', (bool)$params['details']); 406 407 if ($params['refresh'] % 86400 === 0) { 408 $refresh = $params['refresh']/86400 . 'd'; 409 } else if ($params['refresh'] % 3600 === 0) { 410 $refresh = $params['refresh']/3600 . 'h'; 411 } else { 412 $refresh = $params['refresh']/60 . 'm'; 413 } 414 415 $node->attr('refresh', trim($refresh)); 416 $this->nodestack->add($node); 417 } 418 419 420 public function footnote_open() 421 { 422 $footnoteNode = new Node('footnote'); 423 $this->nodestack->addTop($footnoteNode); 424 $this->nodestackBackup[] = $this->nodestack; 425 $this->nodestack = new NodeStack(); 426 427 } 428 429 public function footnote_close() 430 { 431 $json = json_encode($this->nodestack->doc()); 432 $this->nodestack = array_pop($this->nodestackBackup); 433 $this->nodestack->current()->attr('contentJSON', $json); 434 $this->nodestack->drop('footnote'); 435 } 436 437 /** 438 * @inheritDoc 439 */ 440 public function internalmedia( 441 $src, 442 $title = null, 443 $align = null, 444 $width = null, 445 $height = null, 446 $cache = null, 447 $linking = null 448 ) { 449 450 // FIXME how do we handle non-images, e.g. pdfs or audio? 451 \dokuwiki\plugin\prosemirror\parser\ImageNode::render( 452 $this, 453 $src, 454 $title, 455 $align, 456 $width, 457 $height, 458 $cache, 459 $linking 460 ); 461 } 462 463 /** 464 * @inheritDoc 465 */ 466 public function externalmedia( 467 $src, 468 $title = null, 469 $align = null, 470 $width = null, 471 $height = null, 472 $cache = null, 473 $linking = null 474 ) { 475 \dokuwiki\plugin\prosemirror\parser\ImageNode::render( 476 $this, 477 $src, 478 $title, 479 $align, 480 $width, 481 $height, 482 $cache, 483 $linking 484 ); 485 } 486 487 488 public function locallink($hash, $name = null) 489 { 490 \dokuwiki\plugin\prosemirror\parser\LocalLinkNode::render($this, $hash, $name); 491 } 492 493 /** 494 * @inheritDoc 495 */ 496 public function internallink($id, $name = null) 497 { 498 \dokuwiki\plugin\prosemirror\parser\InternalLinkNode::render($this, $id, $name); 499 } 500 501 public function externallink($link, $title = null) 502 { 503 \dokuwiki\plugin\prosemirror\parser\ExternalLinkNode::render($this, $link, $title); 504 } 505 506 public function interwikilink($link, $title, $wikiName, $wikiUri) 507 { 508 \dokuwiki\plugin\prosemirror\parser\InterwikiLinkNode::render($this, $title, $wikiName, $wikiUri); 509 } 510 511 public function emaillink($address, $name = null) 512 { 513 \dokuwiki\plugin\prosemirror\parser\EmailLinkNode::render($this, $address, $name); 514 } 515 516 public function windowssharelink($link, $title = null) 517 { 518 \dokuwiki\plugin\prosemirror\parser\WindowsShareLinkNode::render($this, $link, $title); 519 } 520 521 /** @inheritDoc */ 522 public function linebreak() 523 { 524 $this->nodestack->add(new Node('hard_break')); 525 } 526 527 /** @inheritDoc */ 528 public function hr() 529 { 530 $this->nodestack->add(new Node('horizontal_rule')); 531 } 532 533 public function plugin($name, $data, $state = '', $match = '') 534 { 535 if (empty($match)) { 536 return; 537 } 538 $eventData = [ 539 'name' => $name, 540 'data' => $data, 541 'state' => $state, 542 'match' => $match, 543 'renderer' => $this, 544 ]; 545 $event = new Doku_Event('PROSEMIRROR_RENDER_PLUGIN', $eventData); 546 if ($event->advise_before()) { 547 if ($this->nodestack->current()->getType() === 'paragraph') { 548 $nodetype = 'dwplugin_inline'; 549 } else { 550 $nodetype = 'dwplugin_block'; 551 } 552 $node = new Node($nodetype); 553 $node->attr('class', 'dwplugin'); 554 $node->attr('data-pluginname', $name); 555 $this->nodestack->addTop($node); 556 $this->cdata($match); 557 $this->nodestack->drop($nodetype); 558 } 559 } 560 561 public function smiley($smiley) 562 { 563 if(array_key_exists($smiley, $this->smileys)) { 564 $node = new Node('smiley'); 565 $node->attr('icon', $this->smileys[$smiley]); 566 $node->attr('syntax', $smiley); 567 $this->nodestack->add($node); 568 } else { 569 $this->cdata($smiley); 570 } 571 } 572 573 #region elements with no special WYSIWYG representation 574 575 /** @inheritDoc */ 576 public function entity($entity) 577 { 578 $this->cdata($entity); // FIXME should we handle them special? 579 } 580 581 /** @inheritDoc */ 582 public function multiplyentity($x, $y) 583 { 584 $this->cdata($x . 'x' . $y); 585 } 586 587 /** @inheritDoc */ 588 public function acronym($acronym) 589 { 590 $this->cdata($acronym); 591 } 592 593 /** @inheritDoc */ 594 public function apostrophe() 595 { 596 $this->cdata("'"); 597 } 598 599 /** @inheritDoc */ 600 public function singlequoteopening() 601 { 602 $this->cdata("'"); 603 } 604 605 /** @inheritDoc */ 606 public function singlequoteclosing() 607 { 608 $this->cdata("'"); 609 } 610 611 /** @inheritDoc */ 612 public function doublequoteopening() 613 { 614 $this->cdata('"'); 615 } 616 617 /** @inheritDoc */ 618 public function doublequoteclosing() 619 { 620 $this->cdata('"'); 621 } 622 623 /** @inheritDoc */ 624 public function camelcaselink($link) 625 { 626 $this->cdata($link); // FIXME should/could we decorate it? 627 } 628 629 #endregion 630 631 #region formatter marks 632 633 /** @inheritDoc */ 634 public function strong_open() 635 { 636 $this->marks['strong'] = 1; 637 } 638 639 /** @inheritDoc */ 640 public function strong_close() 641 { 642 if (isset($this->marks['strong'])) { 643 unset($this->marks['strong']); 644 } 645 } 646 647 /** @inheritDoc */ 648 public function emphasis_open() 649 { 650 $this->marks['em'] = 1; 651 } 652 653 /** @inheritDoc */ 654 public function emphasis_close() 655 { 656 if (isset($this->marks['em'])) { 657 unset($this->marks['em']); 658 } 659 } 660 661 /** @inheritdoc */ 662 public function subscript_open() 663 { 664 $this->marks['subscript'] = 1; 665 } 666 667 /** @inheritDoc */ 668 public function subscript_close() 669 { 670 if (isset($this->marks['subscript'])) { 671 unset($this->marks['subscript']); 672 } 673 } 674 675 /** @inheritdoc */ 676 public function superscript_open() 677 { 678 $this->marks['superscript'] = 1; 679 } 680 681 /** @inheritDoc */ 682 public function superscript_close() 683 { 684 if (isset($this->marks['superscript'])) { 685 unset($this->marks['superscript']); 686 } 687 } 688 689 /** @inheritDoc */ 690 public function monospace_open() 691 { 692 $this->marks['code'] = 1; 693 } 694 695 /** @inheritDoc */ 696 public function monospace_close() 697 { 698 if (isset($this->marks['code'])) { 699 unset($this->marks['code']); 700 } 701 } 702 703 /** @inheritDoc */ 704 public function deleted_open() 705 { 706 $this->marks['deleted'] = 1; 707 } 708 709 /** @inheritDoc */ 710 public function deleted_close() 711 { 712 if (isset($this->marks['deleted'])) { 713 unset($this->marks['deleted']); 714 } 715 } 716 717 /** @inheritDoc */ 718 public function underline_open() 719 { 720 $this->marks['underline'] = 1; 721 } 722 723 /** @inheritDoc */ 724 public function underline_close() 725 { 726 if (isset($this->marks['underline'])) { 727 unset($this->marks['underline']); 728 } 729 } 730 731 732 /** @inheritDoc */ 733 public function unformatted($text) 734 { 735 $this->marks['unformatted'] = 1; 736 parent::unformatted($text); 737 unset($this->marks['unformatted']); 738 } 739 740 741 #endregion formatter marks 742} 743