xref: /plugin/dw2pdf/renderer.php (revision 3ddddb3b2485c18a6aa6809af789c7abb6977134)
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