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