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{ 293dabe4e0SAndreas Gohr public function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) 303dabe4e0SAndreas Gohr { 313dabe4e0SAndreas Gohr // Production DW wraps `<table>` in `<div class="table"><table class="inline">`; 323dabe4e0SAndreas Gohr // the spec expects bare `<table>`. 333dabe4e0SAndreas Gohr $this->doc .= "<table>\n"; 343dabe4e0SAndreas Gohr } 353dabe4e0SAndreas Gohr 363dabe4e0SAndreas Gohr public function table_close($pos = null) 373dabe4e0SAndreas Gohr { 383dabe4e0SAndreas Gohr // Drop the matching `</div>` from the production wrapper. 393dabe4e0SAndreas Gohr $this->doc .= "</table>"; 403dabe4e0SAndreas Gohr } 413dabe4e0SAndreas Gohr 423dabe4e0SAndreas Gohr public function tablerow_open($classes = null) 433dabe4e0SAndreas Gohr { 443dabe4e0SAndreas Gohr // Strip DW's `class="rowN"` row counter — spec rows have no class. 453dabe4e0SAndreas Gohr $this->doc .= "<tr>\n"; 463dabe4e0SAndreas Gohr } 473dabe4e0SAndreas Gohr 483dabe4e0SAndreas Gohr public function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) 493dabe4e0SAndreas Gohr { 503dabe4e0SAndreas Gohr // Production DW emits alignment as `class="...align"`; the spec uses 513dabe4e0SAndreas Gohr // an `align="..."` attribute. Drop the `class="colN"` counter too. 523dabe4e0SAndreas Gohr $this->doc .= '<th' . $this->alignAttr($align) . '>'; 533dabe4e0SAndreas Gohr } 543dabe4e0SAndreas Gohr 553dabe4e0SAndreas Gohr public function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) 563dabe4e0SAndreas Gohr { 573dabe4e0SAndreas Gohr $this->doc .= '<td' . $this->alignAttr($align) . '>'; 583dabe4e0SAndreas Gohr } 593dabe4e0SAndreas Gohr 603dabe4e0SAndreas Gohr private function alignAttr(?string $align): string 613dabe4e0SAndreas Gohr { 623dabe4e0SAndreas Gohr if ($align === null) return ''; 633dabe4e0SAndreas Gohr return ' align="' . $align . '"'; 643dabe4e0SAndreas Gohr } 65b1c59bedSAndreas Gohr 663440a8c0SAndreas Gohr public function internalmedia( 673440a8c0SAndreas Gohr $src, 683440a8c0SAndreas Gohr $title = null, 693440a8c0SAndreas Gohr $align = null, 703440a8c0SAndreas Gohr $width = null, 713440a8c0SAndreas Gohr $height = null, 723440a8c0SAndreas Gohr $cache = null, 733440a8c0SAndreas Gohr $linking = null, 743440a8c0SAndreas Gohr $return = false 753440a8c0SAndreas Gohr ) { 763440a8c0SAndreas Gohr $this->doc .= $this->specImg($src, $title, $width, $height); 773440a8c0SAndreas Gohr } 783440a8c0SAndreas Gohr 793440a8c0SAndreas Gohr public function externalmedia( 803440a8c0SAndreas Gohr $src, 813440a8c0SAndreas Gohr $title = null, 823440a8c0SAndreas Gohr $align = null, 833440a8c0SAndreas Gohr $width = null, 843440a8c0SAndreas Gohr $height = null, 853440a8c0SAndreas Gohr $cache = null, 863440a8c0SAndreas Gohr $linking = null, 873440a8c0SAndreas Gohr $return = false 883440a8c0SAndreas Gohr ) { 893440a8c0SAndreas Gohr $this->doc .= $this->specImg($src, $title, $width, $height); 903440a8c0SAndreas Gohr } 913440a8c0SAndreas Gohr 923440a8c0SAndreas Gohr public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') 933440a8c0SAndreas Gohr { 943440a8c0SAndreas Gohr $this->doc .= $this->specLink($id, $name); 953440a8c0SAndreas Gohr } 963440a8c0SAndreas Gohr 973440a8c0SAndreas Gohr public function externallink($url, $name = null, $returnonly = false) 983440a8c0SAndreas Gohr { 993440a8c0SAndreas Gohr $this->doc .= $this->specLink($url, $name); 1003440a8c0SAndreas Gohr } 1013440a8c0SAndreas Gohr 1023440a8c0SAndreas Gohr public function interwikilink($match, $name, $wikiName, $wikiUri, $returnonly = false) 1033440a8c0SAndreas Gohr { 1043440a8c0SAndreas Gohr // Spec has no interwiki expectations; emit the raw `wp>Page` form as 1053440a8c0SAndreas Gohr // href so the mode is still visible but obviously non-standard. 1063440a8c0SAndreas Gohr $this->doc .= $this->specLink($match, $name); 1073440a8c0SAndreas Gohr } 1083440a8c0SAndreas Gohr 1093440a8c0SAndreas Gohr public function emaillink($address, $name = null, $returnonly = false) 1103440a8c0SAndreas Gohr { 1113440a8c0SAndreas Gohr $this->doc .= $this->specLink('mailto:' . $address, $name ?? $address); 1123440a8c0SAndreas Gohr } 1133440a8c0SAndreas Gohr 1143440a8c0SAndreas Gohr public function locallink($hash, $name = null, $returnonly = false) 1153440a8c0SAndreas Gohr { 1163440a8c0SAndreas Gohr $this->doc .= $this->specLink('#' . $hash, $name ?? $hash); 1173440a8c0SAndreas Gohr } 1183440a8c0SAndreas Gohr 1193440a8c0SAndreas Gohr public function windowssharelink($url, $name = null, $returnonly = false) 1203440a8c0SAndreas Gohr { 1213440a8c0SAndreas Gohr $this->doc .= $this->specLink($url, $name); 1223440a8c0SAndreas Gohr } 1233440a8c0SAndreas Gohr 124b1c59bedSAndreas Gohr public function code($text, $language = null, $filename = null, $options = null) 125b1c59bedSAndreas Gohr { 126b1c59bedSAndreas Gohr $this->doc .= $this->specCode($text, $language); 127b1c59bedSAndreas Gohr } 128b1c59bedSAndreas Gohr 129*309a0852SAndreas Gohr public function quote_open() 130*309a0852SAndreas Gohr { 131*309a0852SAndreas Gohr // Production DW wraps blockquote content in `<div class="no">`; 132*309a0852SAndreas Gohr // the spec expects bare `<blockquote>...</blockquote>`. 133*309a0852SAndreas Gohr $this->doc .= "<blockquote>\n"; 134*309a0852SAndreas Gohr } 135*309a0852SAndreas Gohr 136*309a0852SAndreas Gohr public function quote_close() 137*309a0852SAndreas Gohr { 138*309a0852SAndreas Gohr $this->doc .= "</blockquote>\n"; 139*309a0852SAndreas Gohr } 140*309a0852SAndreas Gohr 141685560ebSAndreas Gohr public function listu_open($classes = null) 142685560ebSAndreas Gohr { 143685560ebSAndreas Gohr $this->doc .= "<ul>\n"; 144685560ebSAndreas Gohr } 145685560ebSAndreas Gohr 146685560ebSAndreas Gohr public function listu_close() 147685560ebSAndreas Gohr { 148685560ebSAndreas Gohr $this->doc .= "</ul>\n"; 149685560ebSAndreas Gohr } 150685560ebSAndreas Gohr 151f7c6e4acSAndreas Gohr public function listo_open($classes = null) 152685560ebSAndreas Gohr { 153685560ebSAndreas Gohr $this->doc .= "<ol>\n"; 154685560ebSAndreas Gohr } 155f7c6e4acSAndreas Gohr 156f7c6e4acSAndreas Gohr public function listo_open_start($start = 1) 157f7c6e4acSAndreas Gohr { 158f7c6e4acSAndreas Gohr $start = (int) $start; 159f7c6e4acSAndreas Gohr if ($start === 1) { 160f7c6e4acSAndreas Gohr $this->listo_open(); 161f7c6e4acSAndreas Gohr return; 162f7c6e4acSAndreas Gohr } 163f7c6e4acSAndreas Gohr $this->doc .= '<ol start="' . $start . "\">\n"; 164685560ebSAndreas Gohr } 165685560ebSAndreas Gohr 166685560ebSAndreas Gohr public function listo_close() 167685560ebSAndreas Gohr { 168685560ebSAndreas Gohr $this->doc .= "</ol>\n"; 169685560ebSAndreas Gohr } 170685560ebSAndreas Gohr 171685560ebSAndreas Gohr public function listitem_open($level, $node = false) 172685560ebSAndreas Gohr { 173685560ebSAndreas Gohr $this->doc .= '<li>'; 174685560ebSAndreas Gohr } 175685560ebSAndreas Gohr 176685560ebSAndreas Gohr public function listitem_close() 177685560ebSAndreas Gohr { 178685560ebSAndreas Gohr $this->doc .= "</li>\n"; 179685560ebSAndreas Gohr } 180685560ebSAndreas Gohr 181685560ebSAndreas Gohr public function listcontent_open() 182685560ebSAndreas Gohr { 183685560ebSAndreas Gohr // GFM has no per-item content wrapper - tight items put text directly 184685560ebSAndreas Gohr // inside <li>, loose items wrap it in <p>. The handler emits/strips 185685560ebSAndreas Gohr // p_open / p_close to drive that distinction; the wrapper itself 186685560ebSAndreas Gohr // produces no output here. 187685560ebSAndreas Gohr } 188685560ebSAndreas Gohr 189685560ebSAndreas Gohr public function listcontent_close() 190685560ebSAndreas Gohr { 191685560ebSAndreas Gohr } 192685560ebSAndreas Gohr 193b1c59bedSAndreas Gohr public function file($text, $language = null, $filename = null, $options = null) 194b1c59bedSAndreas Gohr { 195b1c59bedSAndreas Gohr $this->doc .= $this->specCode($text, $language); 196b1c59bedSAndreas Gohr } 197b1c59bedSAndreas Gohr 198b1c59bedSAndreas Gohr public function preformatted($text) 199b1c59bedSAndreas Gohr { 200b1c59bedSAndreas Gohr // The Preformatted CallWriter rewriter collapses start/content/ 201b1c59bedSAndreas Gohr // newline/end into one `preformatted` call. GFM expects the body 202b1c59bedSAndreas Gohr // to end with a newline (spec example 104); DW's internal text 203b1c59bedSAndreas Gohr // loses it to `trim()`, so we re-append here. 204b1c59bedSAndreas Gohr $this->doc .= $this->specCode($text . "\n", null); 205b1c59bedSAndreas Gohr } 206b1c59bedSAndreas Gohr 207b1c59bedSAndreas Gohr /** 208b1c59bedSAndreas Gohr * GFM shape: <pre><code class="language-xxx">...</code></pre>. The 209b1c59bedSAndreas Gohr * production DW renderer emits <pre class="code"> with no inner 210b1c59bedSAndreas Gohr * <code>, which diverges byte-for-byte. 211b1c59bedSAndreas Gohr */ 212b1c59bedSAndreas Gohr private function specCode($text, $language): string 213b1c59bedSAndreas Gohr { 214b1c59bedSAndreas Gohr $classAttr = ''; 215b1c59bedSAndreas Gohr if ($language !== null && $language !== '') { 216b1c59bedSAndreas Gohr $classAttr = ' class="language-' . hsc((string) $language) . '"'; 217b1c59bedSAndreas Gohr } 218b1c59bedSAndreas Gohr return '<pre><code' . $classAttr . '>' . hsc((string) $text) . '</code></pre>'; 219b1c59bedSAndreas Gohr } 220b1c59bedSAndreas Gohr 2213440a8c0SAndreas Gohr private function specImg($src, $alt, $width, $height): string 2223440a8c0SAndreas Gohr { 2233440a8c0SAndreas Gohr $out = '<img src="' . hsc((string) $src) . '"'; 2243440a8c0SAndreas Gohr $out .= ' alt="' . hsc((string) $alt) . '"'; 2253440a8c0SAndreas Gohr if ($width !== null) $out .= ' width="' . (int) $width . '"'; 2263440a8c0SAndreas Gohr if ($height !== null) $out .= ' height="' . (int) $height . '"'; 2273440a8c0SAndreas Gohr $out .= ' />'; 2283440a8c0SAndreas Gohr return $out; 2293440a8c0SAndreas Gohr } 2303440a8c0SAndreas Gohr 2313440a8c0SAndreas Gohr /** 2323440a8c0SAndreas Gohr * Emit a bare <a href="...">label</a>. If the label is a media 2333440a8c0SAndreas Gohr * descriptor array (the shape Media::parseMedia() returns, passed by 2343440a8c0SAndreas Gohr * Internallink / GfmLink when the label is `{{img}}` / ``), 2353440a8c0SAndreas Gohr * render the <img> inside the <a>. 2363440a8c0SAndreas Gohr */ 2373440a8c0SAndreas Gohr private function specLink($href, $label): string 2383440a8c0SAndreas Gohr { 2393440a8c0SAndreas Gohr if (is_array($label) && isset($label['type'])) { 2403440a8c0SAndreas Gohr $img = $this->specImg( 2413440a8c0SAndreas Gohr $label['src'], 2423440a8c0SAndreas Gohr $label['title'], 2433440a8c0SAndreas Gohr $label['width'] ?? null, 2443440a8c0SAndreas Gohr $label['height'] ?? null 2453440a8c0SAndreas Gohr ); 2463440a8c0SAndreas Gohr return '<a href="' . hsc((string) $href) . '">' . $img . '</a>'; 2473440a8c0SAndreas Gohr } 2483440a8c0SAndreas Gohr $text = ($label === null || $label === '') ? $href : $label; 2493440a8c0SAndreas Gohr return '<a href="' . hsc((string) $href) . '">' . hsc((string) $text) . '</a>'; 2503440a8c0SAndreas Gohr } 2513440a8c0SAndreas Gohr} 252