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}}` / ``), 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