xref: /dokuwiki/_test/tests/Parsing/Markdown/SpecCompatRenderer.php (revision 685560eb3044321b3bdd0be40985871ced5f1d05)
1<?php
2
3namespace dokuwiki\test\Parsing\Markdown;
4
5use Doku_Renderer_xhtml;
6
7/**
8 * XHTML renderer tuned to emit the minimal HTML shape GFM's spec.txt uses.
9 *
10 * DokuWiki's production XHTML renderer wraps internal media in details
11 * links pointing at `/lib/exe/fetch.php?media=...` / `/lib/exe/detail.php?media=...`,
12 * rewrites internal link hrefs to `/doku.php?id=...`, and adds wiki-specific
13 * classes and attributes. All of this is correct for live wiki pages but
14 * diverges byte-for-byte from GFM's bare `<img src="...">` and
15 * `<a href="...">...</a>`.
16 *
17 * This renderer is used only by {@see GfmSpecTest} so the spec roundtrip
18 * can compare against byte-level spec HTML. Production rendering is
19 * unchanged. Methods not overridden here fall through to the XHTML
20 * renderer (paragraphs, emphasis, code spans, lists, etc.) — those render
21 * the same shape the spec expects.
22 *
23 * Note: title attributes on links/images are discarded at handle time
24 * (no DW instruction slot), so spec examples that expect `title="..."`
25 * still don't pass and stay in `skip.php`.
26 */
27class SpecCompatRenderer extends Doku_Renderer_xhtml
28{
29
30    public function internalmedia(
31        $src,
32        $title = null,
33        $align = null,
34        $width = null,
35        $height = null,
36        $cache = null,
37        $linking = null,
38        $return = false
39    ) {
40        $this->doc .= $this->specImg($src, $title, $width, $height);
41    }
42
43    public function externalmedia(
44        $src,
45        $title = null,
46        $align = null,
47        $width = null,
48        $height = null,
49        $cache = null,
50        $linking = null,
51        $return = false
52    ) {
53        $this->doc .= $this->specImg($src, $title, $width, $height);
54    }
55
56    public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content')
57    {
58        $this->doc .= $this->specLink($id, $name);
59    }
60
61    public function externallink($url, $name = null, $returnonly = false)
62    {
63        $this->doc .= $this->specLink($url, $name);
64    }
65
66    public function interwikilink($match, $name, $wikiName, $wikiUri, $returnonly = false)
67    {
68        // Spec has no interwiki expectations; emit the raw `wp>Page` form as
69        // href so the mode is still visible but obviously non-standard.
70        $this->doc .= $this->specLink($match, $name);
71    }
72
73    public function emaillink($address, $name = null, $returnonly = false)
74    {
75        $this->doc .= $this->specLink('mailto:' . $address, $name ?? $address);
76    }
77
78    public function locallink($hash, $name = null, $returnonly = false)
79    {
80        $this->doc .= $this->specLink('#' . $hash, $name ?? $hash);
81    }
82
83    public function windowssharelink($url, $name = null, $returnonly = false)
84    {
85        $this->doc .= $this->specLink($url, $name);
86    }
87
88    public function code($text, $language = null, $filename = null, $options = null)
89    {
90        $this->doc .= $this->specCode($text, $language);
91    }
92
93    public function listu_open($classes = null)
94    {
95        $this->doc .= "<ul>\n";
96    }
97
98    public function listu_close()
99    {
100        $this->doc .= "</ul>\n";
101    }
102
103    public function listo_open($classes = null, $start = 1)
104    {
105        if ((int) $start !== 1) {
106            $this->doc .= '<ol start="' . (int) $start . "\">\n";
107        } else {
108            $this->doc .= "<ol>\n";
109        }
110    }
111
112    public function listo_close()
113    {
114        $this->doc .= "</ol>\n";
115    }
116
117    public function listitem_open($level, $node = false)
118    {
119        $this->doc .= '<li>';
120    }
121
122    public function listitem_close()
123    {
124        $this->doc .= "</li>\n";
125    }
126
127    public function listcontent_open()
128    {
129        // GFM has no per-item content wrapper - tight items put text directly
130        // inside <li>, loose items wrap it in <p>. The handler emits/strips
131        // p_open / p_close to drive that distinction; the wrapper itself
132        // produces no output here.
133    }
134
135    public function listcontent_close()
136    {
137    }
138
139    public function file($text, $language = null, $filename = null, $options = null)
140    {
141        $this->doc .= $this->specCode($text, $language);
142    }
143
144    public function preformatted($text)
145    {
146        // The Preformatted CallWriter rewriter collapses start/content/
147        // newline/end into one `preformatted` call. GFM expects the body
148        // to end with a newline (spec example 104); DW's internal text
149        // loses it to `trim()`, so we re-append here.
150        $this->doc .= $this->specCode($text . "\n", null);
151    }
152
153    /**
154     * GFM shape: <pre><code class="language-xxx">...</code></pre>. The
155     * production DW renderer emits <pre class="code"> with no inner
156     * <code>, which diverges byte-for-byte.
157     */
158    private function specCode($text, $language): string
159    {
160        $classAttr = '';
161        if ($language !== null && $language !== '') {
162            $classAttr = ' class="language-' . hsc((string) $language) . '"';
163        }
164        return '<pre><code' . $classAttr . '>' . hsc((string) $text) . '</code></pre>';
165    }
166
167    private function specImg($src, $alt, $width, $height): string
168    {
169        $out = '<img src="' . hsc((string) $src) . '"';
170        $out .= ' alt="' . hsc((string) $alt) . '"';
171        if ($width !== null)  $out .= ' width="' . (int) $width . '"';
172        if ($height !== null) $out .= ' height="' . (int) $height . '"';
173        $out .= ' />';
174        return $out;
175    }
176
177    /**
178     * Emit a bare <a href="...">label</a>. If the label is a media
179     * descriptor array (the shape Media::parseMedia() returns, passed by
180     * Internallink / GfmLink when the label is `{{img}}` / `![alt](img)`),
181     * render the <img> inside the <a>.
182     */
183    private function specLink($href, $label): string
184    {
185        if (is_array($label) && isset($label['type'])) {
186            $img = $this->specImg(
187                $label['src'],
188                $label['title'],
189                $label['width'] ?? null,
190                $label['height'] ?? null
191            );
192            return '<a href="' . hsc((string) $href) . '">' . $img . '</a>';
193        }
194        $text = ($label === null || $label === '') ? $href : $label;
195        return '<a href="' . hsc((string) $href) . '">' . hsc((string) $text) . '</a>';
196    }
197}
198