xref: /dokuwiki/_test/tests/Parsing/Markdown/SpecCompatRenderer.php (revision eb15e634e1400f6c4d78f5fb40179ca25f41574d)
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    public function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null)
30    {
31        // Production DW wraps `<table>` in `<div class="table"><table class="inline">`;
32        // the spec expects bare `<table>`.
33        $this->doc .= "<table>\n";
34    }
35
36    public function table_close($pos = null)
37    {
38        // Drop the matching `</div>` from the production wrapper.
39        $this->doc .= "</table>";
40    }
41
42    public function tablerow_open($classes = null)
43    {
44        // Strip DW's `class="rowN"` row counter — spec rows have no class.
45        $this->doc .= "<tr>\n";
46    }
47
48    public function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null)
49    {
50        // Production DW emits alignment as `class="...align"`; the spec uses
51        // an `align="..."` attribute. Drop the `class="colN"` counter too.
52        $this->doc .= '<th' . $this->alignAttr($align) . '>';
53    }
54
55    public function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null)
56    {
57        $this->doc .= '<td' . $this->alignAttr($align) . '>';
58    }
59
60    private function alignAttr(?string $align): string
61    {
62        if ($align === null) return '';
63        return ' align="' . $align . '"';
64    }
65
66    public function internalmedia(
67        $src,
68        $title = null,
69        $align = null,
70        $width = null,
71        $height = null,
72        $cache = null,
73        $linking = null,
74        $return = false
75    ) {
76        $this->doc .= $this->specImg($src, $title, $width, $height);
77    }
78
79    public function externalmedia(
80        $src,
81        $title = null,
82        $align = null,
83        $width = null,
84        $height = null,
85        $cache = null,
86        $linking = null,
87        $return = false
88    ) {
89        $this->doc .= $this->specImg($src, $title, $width, $height);
90    }
91
92    public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content')
93    {
94        $this->doc .= $this->specLink($id, $name);
95    }
96
97    public function externallink($url, $name = null, $returnonly = false)
98    {
99        $this->doc .= $this->specLink($url, $name);
100    }
101
102    public function interwikilink($match, $name, $wikiName, $wikiUri, $returnonly = false)
103    {
104        // Spec has no interwiki expectations; emit the raw `wp>Page` form as
105        // href so the mode is still visible but obviously non-standard.
106        $this->doc .= $this->specLink($match, $name);
107    }
108
109    public function emaillink($address, $name = null, $returnonly = false)
110    {
111        $this->doc .= $this->specLink('mailto:' . $address, $name ?? $address);
112    }
113
114    public function locallink($hash, $name = null, $returnonly = false)
115    {
116        $this->doc .= $this->specLink('#' . $hash, $name ?? $hash);
117    }
118
119    public function windowssharelink($url, $name = null, $returnonly = false)
120    {
121        $this->doc .= $this->specLink($url, $name);
122    }
123
124    public function code($text, $language = null, $filename = null, $options = null)
125    {
126        $this->doc .= $this->specCode($text, $language);
127    }
128
129    public function linebreak()
130    {
131        // Production DW emits `<br/>` (no space); the spec expects the
132        // XHTML-classic `<br />` (space before the slash).
133        $this->doc .= '<br />' . DOKU_LF;
134    }
135
136    public function quote_open()
137    {
138        // Production DW wraps blockquote content in `<div class="no">`;
139        // the spec expects bare `<blockquote>...</blockquote>`.
140        $this->doc .= "<blockquote>\n";
141    }
142
143    public function quote_close()
144    {
145        $this->doc .= "</blockquote>\n";
146    }
147
148    public function listu_open($classes = null)
149    {
150        $this->doc .= "<ul>\n";
151    }
152
153    public function listu_close()
154    {
155        $this->doc .= "</ul>\n";
156    }
157
158    public function listo_open($classes = null)
159    {
160        $this->doc .= "<ol>\n";
161    }
162
163    public function listo_open_start($start = 1)
164    {
165        $start = (int) $start;
166        if ($start === 1) {
167            $this->listo_open();
168            return;
169        }
170        $this->doc .= '<ol start="' . $start . "\">\n";
171    }
172
173    public function listo_close()
174    {
175        $this->doc .= "</ol>\n";
176    }
177
178    public function listitem_open($level, $node = false)
179    {
180        $this->doc .= '<li>';
181    }
182
183    public function listitem_close()
184    {
185        $this->doc .= "</li>\n";
186    }
187
188    public function listcontent_open()
189    {
190        // GFM has no per-item content wrapper - tight items put text directly
191        // inside <li>, loose items wrap it in <p>. The handler emits/strips
192        // p_open / p_close to drive that distinction; the wrapper itself
193        // produces no output here.
194    }
195
196    public function listcontent_close()
197    {
198    }
199
200    public function file($text, $language = null, $filename = null, $options = null)
201    {
202        $this->doc .= $this->specCode($text, $language);
203    }
204
205    public function preformatted($text)
206    {
207        // The Preformatted CallWriter rewriter collapses start/content/
208        // newline/end into one `preformatted` call. GFM expects the body
209        // to end with a newline (spec example 104); DW's internal text
210        // loses it to `trim()`, so we re-append here.
211        $this->doc .= $this->specCode($text . "\n", null);
212    }
213
214    /**
215     * GFM shape: <pre><code class="language-xxx">...</code></pre>. The
216     * production DW renderer emits <pre class="code"> with no inner
217     * <code>, which diverges byte-for-byte.
218     */
219    private function specCode($text, $language): string
220    {
221        $classAttr = '';
222        if ($language !== null && $language !== '') {
223            $classAttr = ' class="language-' . hsc((string) $language) . '"';
224        }
225        return '<pre><code' . $classAttr . '>' . hsc((string) $text) . '</code></pre>';
226    }
227
228    private function specImg($src, $alt, $width, $height): string
229    {
230        $out = '<img src="' . hsc((string) $src) . '"';
231        $out .= ' alt="' . hsc((string) $alt) . '"';
232        if ($width !== null)  $out .= ' width="' . (int) $width . '"';
233        if ($height !== null) $out .= ' height="' . (int) $height . '"';
234        $out .= ' />';
235        return $out;
236    }
237
238    /**
239     * Emit a bare <a href="...">label</a>. If the label is a media
240     * descriptor array (the shape Media::parseMedia() returns, passed by
241     * Internallink / GfmLink when the label is `{{img}}` / `![alt](img)`),
242     * render the <img> inside the <a>.
243     */
244    private function specLink($href, $label): string
245    {
246        if (is_array($label) && isset($label['type'])) {
247            $img = $this->specImg(
248                $label['src'],
249                $label['title'],
250                $label['width'] ?? null,
251                $label['height'] ?? null
252            );
253            return '<a href="' . hsc((string) $href) . '">' . $img . '</a>';
254        }
255        $text = ($label === null || $label === '') ? $href : $label;
256        return '<a href="' . hsc((string) $href) . '">' . hsc((string) $text) . '</a>';
257    }
258}
259