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