13440a8c0SAndreas Gohr<?php 23440a8c0SAndreas Gohr 33440a8c0SAndreas Gohrnamespace dokuwiki\test\Parsing\Markdown; 43440a8c0SAndreas Gohr 53440a8c0SAndreas Gohruse Doku_Renderer_xhtml; 63440a8c0SAndreas Gohr 73440a8c0SAndreas Gohr/** 83440a8c0SAndreas Gohr * XHTML renderer tuned to emit the minimal HTML shape GFM's spec.txt uses. 93440a8c0SAndreas Gohr * 103440a8c0SAndreas Gohr * DokuWiki's production XHTML renderer wraps internal media in details 113440a8c0SAndreas Gohr * links pointing at `/lib/exe/fetch.php?media=...` / `/lib/exe/detail.php?media=...`, 123440a8c0SAndreas Gohr * rewrites internal link hrefs to `/doku.php?id=...`, and adds wiki-specific 133440a8c0SAndreas Gohr * classes and attributes. All of this is correct for live wiki pages but 143440a8c0SAndreas Gohr * diverges byte-for-byte from GFM's bare `<img src="...">` and 153440a8c0SAndreas Gohr * `<a href="...">...</a>`. 163440a8c0SAndreas Gohr * 173440a8c0SAndreas Gohr * This renderer is used only by {@see GfmSpecTest} so the spec roundtrip 183440a8c0SAndreas Gohr * can compare against byte-level spec HTML. Production rendering is 193440a8c0SAndreas Gohr * unchanged. Methods not overridden here fall through to the XHTML 203440a8c0SAndreas Gohr * renderer (paragraphs, emphasis, code spans, lists, etc.) — those render 213440a8c0SAndreas Gohr * the same shape the spec expects. 223440a8c0SAndreas Gohr * 233440a8c0SAndreas Gohr * Note: title attributes on links/images are discarded at handle time 243440a8c0SAndreas Gohr * (no DW instruction slot), so spec examples that expect `title="..."` 253440a8c0SAndreas Gohr * still don't pass and stay in `skip.php`. 263440a8c0SAndreas Gohr */ 273440a8c0SAndreas Gohrclass SpecCompatRenderer extends Doku_Renderer_xhtml 283440a8c0SAndreas Gohr{ 29b1c59bedSAndreas Gohr 303440a8c0SAndreas Gohr public function internalmedia( 313440a8c0SAndreas Gohr $src, 323440a8c0SAndreas Gohr $title = null, 333440a8c0SAndreas Gohr $align = null, 343440a8c0SAndreas Gohr $width = null, 353440a8c0SAndreas Gohr $height = null, 363440a8c0SAndreas Gohr $cache = null, 373440a8c0SAndreas Gohr $linking = null, 383440a8c0SAndreas Gohr $return = false 393440a8c0SAndreas Gohr ) { 403440a8c0SAndreas Gohr $this->doc .= $this->specImg($src, $title, $width, $height); 413440a8c0SAndreas Gohr } 423440a8c0SAndreas Gohr 433440a8c0SAndreas Gohr public function externalmedia( 443440a8c0SAndreas Gohr $src, 453440a8c0SAndreas Gohr $title = null, 463440a8c0SAndreas Gohr $align = null, 473440a8c0SAndreas Gohr $width = null, 483440a8c0SAndreas Gohr $height = null, 493440a8c0SAndreas Gohr $cache = null, 503440a8c0SAndreas Gohr $linking = null, 513440a8c0SAndreas Gohr $return = false 523440a8c0SAndreas Gohr ) { 533440a8c0SAndreas Gohr $this->doc .= $this->specImg($src, $title, $width, $height); 543440a8c0SAndreas Gohr } 553440a8c0SAndreas Gohr 563440a8c0SAndreas Gohr public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') 573440a8c0SAndreas Gohr { 583440a8c0SAndreas Gohr $this->doc .= $this->specLink($id, $name); 593440a8c0SAndreas Gohr } 603440a8c0SAndreas Gohr 613440a8c0SAndreas Gohr public function externallink($url, $name = null, $returnonly = false) 623440a8c0SAndreas Gohr { 633440a8c0SAndreas Gohr $this->doc .= $this->specLink($url, $name); 643440a8c0SAndreas Gohr } 653440a8c0SAndreas Gohr 663440a8c0SAndreas Gohr public function interwikilink($match, $name, $wikiName, $wikiUri, $returnonly = false) 673440a8c0SAndreas Gohr { 683440a8c0SAndreas Gohr // Spec has no interwiki expectations; emit the raw `wp>Page` form as 693440a8c0SAndreas Gohr // href so the mode is still visible but obviously non-standard. 703440a8c0SAndreas Gohr $this->doc .= $this->specLink($match, $name); 713440a8c0SAndreas Gohr } 723440a8c0SAndreas Gohr 733440a8c0SAndreas Gohr public function emaillink($address, $name = null, $returnonly = false) 743440a8c0SAndreas Gohr { 753440a8c0SAndreas Gohr $this->doc .= $this->specLink('mailto:' . $address, $name ?? $address); 763440a8c0SAndreas Gohr } 773440a8c0SAndreas Gohr 783440a8c0SAndreas Gohr public function locallink($hash, $name = null, $returnonly = false) 793440a8c0SAndreas Gohr { 803440a8c0SAndreas Gohr $this->doc .= $this->specLink('#' . $hash, $name ?? $hash); 813440a8c0SAndreas Gohr } 823440a8c0SAndreas Gohr 833440a8c0SAndreas Gohr public function windowssharelink($url, $name = null, $returnonly = false) 843440a8c0SAndreas Gohr { 853440a8c0SAndreas Gohr $this->doc .= $this->specLink($url, $name); 863440a8c0SAndreas Gohr } 873440a8c0SAndreas Gohr 88b1c59bedSAndreas Gohr public function code($text, $language = null, $filename = null, $options = null) 89b1c59bedSAndreas Gohr { 90b1c59bedSAndreas Gohr $this->doc .= $this->specCode($text, $language); 91b1c59bedSAndreas Gohr } 92b1c59bedSAndreas Gohr 93*685560ebSAndreas Gohr public function listu_open($classes = null) 94*685560ebSAndreas Gohr { 95*685560ebSAndreas Gohr $this->doc .= "<ul>\n"; 96*685560ebSAndreas Gohr } 97*685560ebSAndreas Gohr 98*685560ebSAndreas Gohr public function listu_close() 99*685560ebSAndreas Gohr { 100*685560ebSAndreas Gohr $this->doc .= "</ul>\n"; 101*685560ebSAndreas Gohr } 102*685560ebSAndreas Gohr 103*685560ebSAndreas Gohr public function listo_open($classes = null, $start = 1) 104*685560ebSAndreas Gohr { 105*685560ebSAndreas Gohr if ((int) $start !== 1) { 106*685560ebSAndreas Gohr $this->doc .= '<ol start="' . (int) $start . "\">\n"; 107*685560ebSAndreas Gohr } else { 108*685560ebSAndreas Gohr $this->doc .= "<ol>\n"; 109*685560ebSAndreas Gohr } 110*685560ebSAndreas Gohr } 111*685560ebSAndreas Gohr 112*685560ebSAndreas Gohr public function listo_close() 113*685560ebSAndreas Gohr { 114*685560ebSAndreas Gohr $this->doc .= "</ol>\n"; 115*685560ebSAndreas Gohr } 116*685560ebSAndreas Gohr 117*685560ebSAndreas Gohr public function listitem_open($level, $node = false) 118*685560ebSAndreas Gohr { 119*685560ebSAndreas Gohr $this->doc .= '<li>'; 120*685560ebSAndreas Gohr } 121*685560ebSAndreas Gohr 122*685560ebSAndreas Gohr public function listitem_close() 123*685560ebSAndreas Gohr { 124*685560ebSAndreas Gohr $this->doc .= "</li>\n"; 125*685560ebSAndreas Gohr } 126*685560ebSAndreas Gohr 127*685560ebSAndreas Gohr public function listcontent_open() 128*685560ebSAndreas Gohr { 129*685560ebSAndreas Gohr // GFM has no per-item content wrapper - tight items put text directly 130*685560ebSAndreas Gohr // inside <li>, loose items wrap it in <p>. The handler emits/strips 131*685560ebSAndreas Gohr // p_open / p_close to drive that distinction; the wrapper itself 132*685560ebSAndreas Gohr // produces no output here. 133*685560ebSAndreas Gohr } 134*685560ebSAndreas Gohr 135*685560ebSAndreas Gohr public function listcontent_close() 136*685560ebSAndreas Gohr { 137*685560ebSAndreas Gohr } 138*685560ebSAndreas Gohr 139b1c59bedSAndreas Gohr public function file($text, $language = null, $filename = null, $options = null) 140b1c59bedSAndreas Gohr { 141b1c59bedSAndreas Gohr $this->doc .= $this->specCode($text, $language); 142b1c59bedSAndreas Gohr } 143b1c59bedSAndreas Gohr 144b1c59bedSAndreas Gohr public function preformatted($text) 145b1c59bedSAndreas Gohr { 146b1c59bedSAndreas Gohr // The Preformatted CallWriter rewriter collapses start/content/ 147b1c59bedSAndreas Gohr // newline/end into one `preformatted` call. GFM expects the body 148b1c59bedSAndreas Gohr // to end with a newline (spec example 104); DW's internal text 149b1c59bedSAndreas Gohr // loses it to `trim()`, so we re-append here. 150b1c59bedSAndreas Gohr $this->doc .= $this->specCode($text . "\n", null); 151b1c59bedSAndreas Gohr } 152b1c59bedSAndreas Gohr 153b1c59bedSAndreas Gohr /** 154b1c59bedSAndreas Gohr * GFM shape: <pre><code class="language-xxx">...</code></pre>. The 155b1c59bedSAndreas Gohr * production DW renderer emits <pre class="code"> with no inner 156b1c59bedSAndreas Gohr * <code>, which diverges byte-for-byte. 157b1c59bedSAndreas Gohr */ 158b1c59bedSAndreas Gohr private function specCode($text, $language): string 159b1c59bedSAndreas Gohr { 160b1c59bedSAndreas Gohr $classAttr = ''; 161b1c59bedSAndreas Gohr if ($language !== null && $language !== '') { 162b1c59bedSAndreas Gohr $classAttr = ' class="language-' . hsc((string) $language) . '"'; 163b1c59bedSAndreas Gohr } 164b1c59bedSAndreas Gohr return '<pre><code' . $classAttr . '>' . hsc((string) $text) . '</code></pre>'; 165b1c59bedSAndreas Gohr } 166b1c59bedSAndreas Gohr 1673440a8c0SAndreas Gohr private function specImg($src, $alt, $width, $height): string 1683440a8c0SAndreas Gohr { 1693440a8c0SAndreas Gohr $out = '<img src="' . hsc((string) $src) . '"'; 1703440a8c0SAndreas Gohr $out .= ' alt="' . hsc((string) $alt) . '"'; 1713440a8c0SAndreas Gohr if ($width !== null) $out .= ' width="' . (int) $width . '"'; 1723440a8c0SAndreas Gohr if ($height !== null) $out .= ' height="' . (int) $height . '"'; 1733440a8c0SAndreas Gohr $out .= ' />'; 1743440a8c0SAndreas Gohr return $out; 1753440a8c0SAndreas Gohr } 1763440a8c0SAndreas Gohr 1773440a8c0SAndreas Gohr /** 1783440a8c0SAndreas Gohr * Emit a bare <a href="...">label</a>. If the label is a media 1793440a8c0SAndreas Gohr * descriptor array (the shape Media::parseMedia() returns, passed by 1803440a8c0SAndreas Gohr * Internallink / GfmLink when the label is `{{img}}` / ``), 1813440a8c0SAndreas Gohr * render the <img> inside the <a>. 1823440a8c0SAndreas Gohr */ 1833440a8c0SAndreas Gohr private function specLink($href, $label): string 1843440a8c0SAndreas Gohr { 1853440a8c0SAndreas Gohr if (is_array($label) && isset($label['type'])) { 1863440a8c0SAndreas Gohr $img = $this->specImg( 1873440a8c0SAndreas Gohr $label['src'], 1883440a8c0SAndreas Gohr $label['title'], 1893440a8c0SAndreas Gohr $label['width'] ?? null, 1903440a8c0SAndreas Gohr $label['height'] ?? null 1913440a8c0SAndreas Gohr ); 1923440a8c0SAndreas Gohr return '<a href="' . hsc((string) $href) . '">' . $img . '</a>'; 1933440a8c0SAndreas Gohr } 1943440a8c0SAndreas Gohr $text = ($label === null || $label === '') ? $href : $label; 1953440a8c0SAndreas Gohr return '<a href="' . hsc((string) $href) . '">' . hsc((string) $text) . '</a>'; 1963440a8c0SAndreas Gohr } 1973440a8c0SAndreas Gohr} 198