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