1<?php 2 3// phpcs:disable: PSR1.Methods.CamelCapsMethodName.NotCamelCaps 4// phpcs:disable: PSR2.Methods.MethodDeclaration.Underscore 5use dokuwiki\plugin\dw2pdf\src\AbstractCollector; 6use dokuwiki\plugin\dw2pdf\src\Config; 7 8/** 9 * DokuWiki Plugin dw2pdf (Renderer Component) 10 * Render xhtml suitable as input for mpdf library 11 * 12 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 13 * @author Andreas Gohr <gohr@cosmocode.de> 14 */ 15class renderer_plugin_dw2pdf extends Doku_Renderer_xhtml 16{ 17 private $lastHeaderLevel = -1; 18 private $originalHeaderLevel = 0; 19 private $difference = 0; 20 private $header_count = []; 21 private $previous_level = 0; 22 private int $chapter = 0; 23 private ?Config $config = null; 24 25 /** 26 * The Writer will reinitialize the renderer for each export, but the object will be reused within one export. 27 * 28 * @inheritdoc 29 */ 30 public function isSingleton() 31 { 32 return true; 33 } 34 35 /** 36 * Set the active configuration 37 * 38 * @param Config $config 39 * @return void 40 */ 41 public function setConfig(Config $config): void 42 { 43 $this->config = $config; 44 } 45 46 public function document_start() 47 { 48 global $ID; 49 50 if (!$this->config instanceof Config) { 51 throw new RuntimeException('DW2PDF Renderer configuration not set'); 52 } 53 54 parent::document_start(); 55 56 //anchor for rewritten links to included pages 57 $check = false; 58 $pid = sectionID($ID, $check); 59 60 $this->doc .= "<a name=\"{$pid}__\">"; 61 $this->doc .= "</a>"; 62 63 64 $this->header_count[1] = $this->chapter; 65 $this->chapter++; 66 } 67 68 /** 69 * Make available as XHTML replacement renderer 70 * 71 * @param $format 72 * @return bool 73 */ 74 public function canRender($format) 75 { 76 if ($format == 'xhtml') { 77 return true; 78 } 79 return false; 80 } 81 82 /** 83 * Simplified header printing with PDF bookmarks 84 * 85 * @param string $text 86 * @param int $level from 1 (highest) to 6 (lowest) 87 * @param int $pos 88 */ 89 public function header($text, $level, $pos, $returnonly = false) 90 { 91 //skip empty headlines 92 if (!$text) { 93 return; 94 } 95 global $ID; 96 97 $hid = $this->_headerToLink($text, true); 98 99 //only add items within global configured levels (doesn't check the pdf toc settings) 100 $this->toc_additem($hid, $text, $level); 101 102 $check = false; 103 $pid = sectionID($ID, $check); 104 $hid = $pid . '__' . $hid; 105 106 107 // retrieve numbered headings option 108 $isnumberedheadings = $this->config->useNumberedHeaders(); 109 110 $header_prefix = ''; 111 if ($isnumberedheadings) { 112 if ($level > 0) { 113 if ($this->previous_level > $level) { 114 for ($i = $level + 1; $i <= $this->previous_level; $i++) { 115 $this->header_count[$i] = 0; 116 } 117 } 118 } 119 $this->header_count[$level] = ($this->header_count[$level] ?? 0) + 1; 120 121 // $header_prefix = ""; 122 for ($i = 1; $i <= $level; $i++) { 123 $header_prefix .= $this->header_count[$i] . "."; 124 } 125 } 126 if ($header_prefix !== '') { 127 $header_prefix .= ' '; 128 } 129 130 // add PDF bookmark 131 $bookmark = ''; 132 $maxbookmarklevel = $this->config->getMaxBookmarks(); 133 // 0: off, 1-6: show down to this level 134 if ($maxbookmarklevel && $maxbookmarklevel >= $level) { 135 $bookmarklevel = $this->calculateBookmarklevel($level); 136 $bookmark = sprintf( 137 '<bookmark content="%s %s" level="%d" />', 138 $header_prefix, 139 $this->_xmlEntities($text), 140 $bookmarklevel 141 ); 142 } 143 144 // print header 145 $this->doc .= DOKU_LF . "<h$level>$bookmark"; 146 $this->doc .= $header_prefix . "<a name=\"$hid\">"; 147 $this->doc .= $this->_xmlEntities($text); 148 $this->doc .= "</a>"; 149 $this->doc .= "</h$level>" . DOKU_LF; 150 $this->previous_level = $level; 151 } 152 153 /** 154 * Bookmark levels might increase maximal +1 per level. 155 * (note: levels start at 1, bookmarklevels at 0) 156 * 157 * @param int $level 1 (highest) to 6 (lowest) 158 * @return int 159 */ 160 protected function calculateBookmarklevel($level) 161 { 162 if ($this->lastHeaderLevel == -1) { 163 $this->lastHeaderLevel = $level; 164 } 165 $step = $level - $this->lastHeaderLevel; 166 if ($step > 1) { 167 $this->difference += $step - 1; 168 } 169 if ($step < 0) { 170 $this->difference = min($this->difference, $level - $this->originalHeaderLevel); 171 $this->difference = max($this->difference, 0); 172 } 173 174 $bookmarklevel = $level - $this->difference; 175 176 if ($step > 1) { 177 $this->originalHeaderLevel = $bookmarklevel; 178 } 179 180 $this->lastHeaderLevel = $level; 181 return $bookmarklevel - 1; //zero indexed 182 } 183 184 /** 185 * Render a page local link 186 * 187 * // modified copy of parent function 188 * 189 * @param string $hash hash link identifier 190 * @param string $name name for the link 191 * @param bool $returnonly 192 * @return string|void 193 * 194 * @see Doku_Renderer_xhtml::locallink 195 */ 196 public function locallink($hash, $name = null, $returnonly = false) 197 { 198 global $ID; 199 $name = $this->_getLinkTitle($name, $hash, $isImage); 200 $hash = $this->_headerToLink($hash); 201 $title = $ID . ' ↵'; 202 203 $check = false; 204 $pid = sectionID($ID, $check); 205 206 $this->doc .= '<a href="#' . $pid . '__' . $hash . '" title="' . $title . '" class="wikilink1">'; 207 $this->doc .= $name; 208 $this->doc .= '</a>'; 209 } 210 211 /** 212 * Wrap centered media in a div to center it 213 * 214 * @param string $src media ID 215 * @param string $title descriptive text 216 * @param string $align left|center|right 217 * @param int $width width of media in pixel 218 * @param int $height height of media in pixel 219 * @param string $cache cache|recache|nocache 220 * @param bool $render should the media be embedded inline or just linked 221 * @return string 222 */ 223 public function _media( 224 $src, 225 $title = null, 226 $align = null, 227 $width = null, 228 $height = null, 229 $cache = null, 230 $render = true 231 ) { 232 233 $out = ''; 234 if ($align == 'center') { 235 $out .= '<div align="center" style="text-align: center">'; 236 } 237 238 $out .= parent::_media($src, $title, $align, $width, $height, $cache, $render); 239 240 if ($align == 'center') { 241 $out .= '</div>'; 242 } 243 244 return $out; 245 } 246 247 /** 248 * hover info makes no sense in PDFs, so drop acronyms 249 * 250 * @param string $acronym 251 */ 252 public function acronym($acronym) 253 { 254 $this->doc .= $this->_xmlEntities($acronym); 255 } 256 257 /** 258 * reformat links if needed 259 * 260 * Because the output of this renderer will be cached, but might be part of a larger PDF 261 * including multiple pages, the links are not rewritten here. 262 * Instead they will be rewritten in the created HTML after rendering but before feeding to mPDF. 263 * 264 * @param array $link 265 * @return string 266 */ 267 public function _formatLink($link) 268 { 269 // mark internal wiki links for later processing by the writer 270 if ( 271 !empty($link['more']) && 272 str_contains($link['more'], 'data-wiki-id=') 273 ) { 274 [, $hash] = sexplode('#', $link['url'], 2, ''); 275 $target = $link['title'] ?? ''; // for internal links, 'title' holds the target page id 276 if ($target !== '') { 277 $attrs = ['data-dw2pdf-target="' . hsc($target) . '"']; 278 if ($hash !== '') { 279 $attrs[] = 'data-dw2pdf-hash="' . hsc($hash) . '"'; 280 } 281 $link['more'] = trim($link['more'] . ' ' . implode(' ', $attrs)); 282 } 283 } 284 285 // prefix interwiki links with interwiki icon 286 if ($link['name'][0] != '<' && preg_match('/\binterwiki iw_(.\w+)\b/', $link['class'], $m)) { 287 if (file_exists(DOKU_INC . 'lib/images/interwiki/' . $m[1] . '.png')) { 288 $img = DOKU_BASE . 'lib/images/interwiki/' . $m[1] . '.png'; 289 } elseif (file_exists(DOKU_INC . 'lib/images/interwiki/' . $m[1] . '.gif')) { 290 $img = DOKU_BASE . 'lib/images/interwiki/' . $m[1] . '.gif'; 291 } else { 292 $img = DOKU_BASE . 'lib/images/interwiki.png'; 293 } 294 295 $link['name'] = sprintf( 296 '<img src="%s" width="16" height="16" style="vertical-align: middle" class="%s" />%s', 297 $img, 298 $link['class'], 299 $link['name'] 300 ); 301 } 302 return parent::_formatLink($link); 303 } 304 305 /** 306 * no obfuscation for email addresses 307 * 308 * @param string $address 309 * @param bool $returnonly 310 * @return string|void 311 */ 312 public function emaillink($address, $name = null, $returnonly = false) 313 { 314 global $conf; 315 $old = $conf['mailguard']; 316 $conf['mailguard'] = 'none'; 317 parent::emaillink($address, $name, $returnonly); 318 $conf['mailguard'] = $old; 319 } 320} 321