xref: /dokuwiki/inc/parser/xhtml.php (revision 53df38b0e4465894a67a5890f74a6f5f82e827de)
10cecf9d5Sandi<?php
20c3a5702SAndreas Gohr
30c3a5702SAndreas Gohruse dokuwiki\ChangeLog\MediaChangeLog;
42cd6cc0aSAndreas Gohruse dokuwiki\File\MediaResolver;
52cd6cc0aSAndreas Gohruse dokuwiki\File\PageResolver;
60c3a5702SAndreas Gohr
7b625487dSandi/**
8b625487dSandi * Renderer for XHTML output
9b625487dSandi *
10b4f2363aSAndreas Gohr * This is DokuWiki's main renderer used to display page content in the wiki
11b4f2363aSAndreas Gohr *
12b625487dSandi * @author Harry Fuecks <hfuecks@gmail.com>
13b625487dSandi * @author Andreas Gohr <andi@splitbrain.org>
143dd5c225SAndreas Gohr *
150cecf9d5Sandi */
16ac83b9d8Sandiclass Doku_Renderer_xhtml extends Doku_Renderer {
173dd5c225SAndreas Gohr    /** @var array store the table of contents */
183dd5c225SAndreas Gohr    public $toc = array();
190cecf9d5Sandi
203dd5c225SAndreas Gohr    /** @var array A stack of section edit data */
213dd5c225SAndreas Gohr    protected $sectionedits = array();
22de369923SAndreas Gohr
233dd5c225SAndreas Gohr    /** @var int last section edit id, used by startSectionEdit */
243dd5c225SAndreas Gohr    protected $lastsecid = 0;
250cecf9d5Sandi
2616ec3e37SAndreas Gohr    /** @var array a list of footnotes, list starts at 1! */
273dd5c225SAndreas Gohr    protected $footnotes = array();
287764a90aSandi
293dd5c225SAndreas Gohr    /** @var int current section level */
303dd5c225SAndreas Gohr    protected $lastlevel = 0;
313dd5c225SAndreas Gohr    /** @var array section node tracker */
323dd5c225SAndreas Gohr    protected $node = array(0, 0, 0, 0, 0);
333dd5c225SAndreas Gohr
343dd5c225SAndreas Gohr    /** @var string temporary $doc store */
353dd5c225SAndreas Gohr    protected $store = '';
363dd5c225SAndreas Gohr
373dd5c225SAndreas Gohr    /** @var array global counter, for table classes etc. */
383dd5c225SAndreas Gohr    protected $_counter = array(); //
393dd5c225SAndreas Gohr
403dd5c225SAndreas Gohr    /** @var int counts the code and file blocks, used to provide download links */
413dd5c225SAndreas Gohr    protected $_codeblock = 0;
423dd5c225SAndreas Gohr
433dd5c225SAndreas Gohr    /** @var array list of allowed URL schemes */
443dd5c225SAndreas Gohr    protected $schemes = null;
45b5742cedSPierre Spring
4690df9a4dSAdrian Lang    /**
4790df9a4dSAdrian Lang     * Register a new edit section range
4890df9a4dSAdrian Lang     *
4942ea7f44SGerrit Uitslag     * @param int    $start  The byte position for the edit start
50ec57f119SLarsDW223     * @param array  $data   Associative array with section data:
51ec57f119SLarsDW223     *                       Key 'name': the section name/title
52ec57f119SLarsDW223     *                       Key 'target': the target for the section edit,
53ec57f119SLarsDW223     *                                     e.g. 'section' or 'table'
54ec57f119SLarsDW223     *                       Key 'hid': header id
55ec57f119SLarsDW223     *                       Key 'codeblockOffset': actual code block index
56ec57f119SLarsDW223     *                       Key 'start': set in startSectionEdit(),
57ec57f119SLarsDW223     *                                    do not set yourself
58ec57f119SLarsDW223     *                       Key 'range': calculated from 'start' and
59ec57f119SLarsDW223     *                                    $key in finishSectionEdit(),
60ec57f119SLarsDW223     *                                    do not set yourself
6190df9a4dSAdrian Lang     * @return string  A marker class for the starting HTML element
6242ea7f44SGerrit Uitslag     *
6390df9a4dSAdrian Lang     * @author Adrian Lang <lang@cosmocode.de>
6490df9a4dSAdrian Lang     */
65ec57f119SLarsDW223    public function startSectionEdit($start, $data) {
66ec57f119SLarsDW223        if (!is_array($data)) {
67ac025fdfSAndreas Gohr            msg(
68ac025fdfSAndreas Gohr                sprintf(
69ac025fdfSAndreas Gohr                    'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.',
70ac025fdfSAndreas Gohr                    hsc((string) $data)
71ac025fdfSAndreas Gohr                ), -1
72ac025fdfSAndreas Gohr            );
73ac025fdfSAndreas Gohr
74ac025fdfSAndreas Gohr            // @deprecated 2018-04-14, backward compatibility
75ac025fdfSAndreas Gohr            $args = func_get_args();
76ac025fdfSAndreas Gohr            $data = array();
77ac025fdfSAndreas Gohr            if(isset($args[1])) $data['target'] = $args[1];
78ac025fdfSAndreas Gohr            if(isset($args[2])) $data['name'] = $args[2];
79ac025fdfSAndreas Gohr            if(isset($args[3])) $data['hid'] = $args[3];
80ec57f119SLarsDW223        }
81ec57f119SLarsDW223        $data['secid'] = ++$this->lastsecid;
82ec57f119SLarsDW223        $data['start'] = $start;
83ec57f119SLarsDW223        $this->sectionedits[] = $data;
84ec57f119SLarsDW223        return 'sectionedit'.$data['secid'];
8590df9a4dSAdrian Lang    }
8690df9a4dSAdrian Lang
8790df9a4dSAdrian Lang    /**
8890df9a4dSAdrian Lang     * Finish an edit section range
8990df9a4dSAdrian Lang     *
9042ea7f44SGerrit Uitslag     * @param int  $end     The byte position for the edit end; null for the rest of the page
9142ea7f44SGerrit Uitslag     *
9290df9a4dSAdrian Lang     * @author Adrian Lang <lang@cosmocode.de>
9390df9a4dSAdrian Lang     */
942571786cSLarsDW223    public function finishSectionEdit($end = null, $hid = null) {
95ad43fdbfSasivery        if(count($this->sectionedits) == 0) {
960d9f02ecSasivery            return;
970d9f02ecSasivery        }
98ec57f119SLarsDW223        $data = array_pop($this->sectionedits);
99ec57f119SLarsDW223        if(!is_null($end) && $end <= $data['start']) {
10000c13053SAdrian Lang            return;
10100c13053SAdrian Lang        }
1022571786cSLarsDW223        if(!is_null($hid)) {
103ec57f119SLarsDW223            $data['hid'] .= $hid;
1042571786cSLarsDW223        }
105ec57f119SLarsDW223        $data['range'] = $data['start'].'-'.(is_null($end) ? '' : $end);
106ec57f119SLarsDW223        unset($data['start']);
107ada0d779SMichael Hamann        $this->doc .= '<!-- EDIT'.hsc(json_encode ($data)).' -->';
10890df9a4dSAdrian Lang    }
10990df9a4dSAdrian Lang
1103dd5c225SAndreas Gohr    /**
1113dd5c225SAndreas Gohr     * Returns the format produced by this renderer.
1123dd5c225SAndreas Gohr     *
1133dd5c225SAndreas Gohr     * @return string always 'xhtml'
1143dd5c225SAndreas Gohr     */
115de369923SAndreas Gohr    public function getFormat() {
1165f70445dSAndreas Gohr        return 'xhtml';
1175f70445dSAndreas Gohr    }
1185f70445dSAndreas Gohr
1193dd5c225SAndreas Gohr    /**
1203dd5c225SAndreas Gohr     * Initialize the document
1213dd5c225SAndreas Gohr     */
122de369923SAndreas Gohr    public function document_start() {
123c5a8fd96SAndreas Gohr        //reset some internals
124c5a8fd96SAndreas Gohr        $this->toc     = array();
1250cecf9d5Sandi    }
1260cecf9d5Sandi
1273dd5c225SAndreas Gohr    /**
1283dd5c225SAndreas Gohr     * Finalize the document
1293dd5c225SAndreas Gohr     */
130de369923SAndreas Gohr    public function document_end() {
13190df9a4dSAdrian Lang        // Finish open section edits.
13290df9a4dSAdrian Lang        while(count($this->sectionedits) > 0) {
133ec57f119SLarsDW223            if($this->sectionedits[count($this->sectionedits) - 1]['start'] <= 1) {
13490df9a4dSAdrian Lang                // If there is only one section, do not write a section edit
13590df9a4dSAdrian Lang                // marker.
13690df9a4dSAdrian Lang                array_pop($this->sectionedits);
13790df9a4dSAdrian Lang            } else {
138d9e36cbeSAdrian Lang                $this->finishSectionEdit();
13990df9a4dSAdrian Lang            }
14090df9a4dSAdrian Lang        }
14190df9a4dSAdrian Lang
1420cecf9d5Sandi        if(count($this->footnotes) > 0) {
143a2d649c4Sandi            $this->doc .= '<div class="footnotes">'.DOKU_LF;
144d74aace9Schris
14516ec3e37SAndreas Gohr            foreach($this->footnotes as $id => $footnote) {
146d74aace9Schris                // check its not a placeholder that indicates actual footnote text is elsewhere
147d74aace9Schris                if(substr($footnote, 0, 5) != "@@FNT") {
148d74aace9Schris
149d74aace9Schris                    // open the footnote and set the anchor and backlink
150d74aace9Schris                    $this->doc .= '<div class="fn">';
15116cc7ed7SAnika Henke                    $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">';
15229bfcd16SAndreas Gohr                    $this->doc .= $id.')</a></sup> '.DOKU_LF;
153d74aace9Schris
154d74aace9Schris                    // get any other footnotes that use the same markup
155d74aace9Schris                    $alt = array_keys($this->footnotes, "@@FNT$id");
156d74aace9Schris
157d74aace9Schris                    if(count($alt)) {
158d74aace9Schris                        foreach($alt as $ref) {
159d74aace9Schris                            // set anchor and backlink for the other footnotes
16016ec3e37SAndreas Gohr                            $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">';
16116ec3e37SAndreas Gohr                            $this->doc .= ($ref).')</a></sup> '.DOKU_LF;
162d74aace9Schris                        }
163d74aace9Schris                    }
164d74aace9Schris
165d74aace9Schris                    // add footnote markup and close this footnote
166694afa06SAnika Henke                    $this->doc .= '<div class="content">'.$footnote.'</div>';
167d74aace9Schris                    $this->doc .= '</div>'.DOKU_LF;
168d74aace9Schris                }
1690cecf9d5Sandi            }
170a2d649c4Sandi            $this->doc .= '</div>'.DOKU_LF;
1710cecf9d5Sandi        }
172c5a8fd96SAndreas Gohr
173b8595a66SAndreas Gohr        // Prepare the TOC
174851f2e89SAnika Henke        global $conf;
17564159a61SAndreas Gohr        if(
17664159a61SAndreas Gohr            $this->info['toc'] &&
17764159a61SAndreas Gohr            is_array($this->toc) &&
17864159a61SAndreas Gohr            $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']
17964159a61SAndreas Gohr        ) {
180b8595a66SAndreas Gohr            global $TOC;
181b8595a66SAndreas Gohr            $TOC = $this->toc;
1820cecf9d5Sandi        }
1833e55d035SAndreas Gohr
1843e55d035SAndreas Gohr        // make sure there are no empty paragraphs
18527918226Schris        $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc);
186e41c4da9SAndreas Gohr    }
1870cecf9d5Sandi
1883dd5c225SAndreas Gohr    /**
1893dd5c225SAndreas Gohr     * Add an item to the TOC
1903dd5c225SAndreas Gohr     *
1913dd5c225SAndreas Gohr     * @param string $id       the hash link
1923dd5c225SAndreas Gohr     * @param string $text     the text to display
1933dd5c225SAndreas Gohr     * @param int    $level    the nesting level
1943dd5c225SAndreas Gohr     */
195de369923SAndreas Gohr    public function toc_additem($id, $text, $level) {
196af587fa8Sandi        global $conf;
197af587fa8Sandi
198c5a8fd96SAndreas Gohr        //handle TOC
199c5a8fd96SAndreas Gohr        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
2007d91652aSAndreas Gohr            $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1);
201c5a8fd96SAndreas Gohr        }
202e7856beaSchris    }
203e7856beaSchris
2043dd5c225SAndreas Gohr    /**
2053dd5c225SAndreas Gohr     * Render a heading
2063dd5c225SAndreas Gohr     *
2073dd5c225SAndreas Gohr     * @param string $text       the text to display
2083dd5c225SAndreas Gohr     * @param int    $level      header level
2093dd5c225SAndreas Gohr     * @param int    $pos        byte position in the original source
210e3c00e6eSIain Hallam     * @param bool   $returnonly whether to return html or write to doc attribute
211e3c00e6eSIain Hallam     * @return void|string writes to doc attribute or returns html depends on $returnonly
2123dd5c225SAndreas Gohr     */
213e3c00e6eSIain Hallam    public function header($text, $level, $pos, $returnonly = false) {
21490df9a4dSAdrian Lang        global $conf;
21590df9a4dSAdrian Lang
216f515db7fSAndreas Gohr        if(blank($text)) return; //skip empty headlines
217e7856beaSchris
218e7856beaSchris        $hid = $this->_headerToLink($text, true);
219e7856beaSchris
220e7856beaSchris        //only add items within configured levels
221e7856beaSchris        $this->toc_additem($hid, $text, $level);
222c5a8fd96SAndreas Gohr
22391459163SAnika Henke        // adjust $node to reflect hierarchy of levels
22491459163SAnika Henke        $this->node[$level - 1]++;
22591459163SAnika Henke        if($level < $this->lastlevel) {
22691459163SAnika Henke            for($i = 0; $i < $this->lastlevel - $level; $i++) {
22791459163SAnika Henke                $this->node[$this->lastlevel - $i - 1] = 0;
22891459163SAnika Henke            }
22991459163SAnika Henke        }
23091459163SAnika Henke        $this->lastlevel = $level;
23191459163SAnika Henke
23290df9a4dSAdrian Lang        if($level <= $conf['maxseclevel'] &&
23390df9a4dSAdrian Lang            count($this->sectionedits) > 0 &&
234ec57f119SLarsDW223            $this->sectionedits[count($this->sectionedits) - 1]['target'] === 'section'
2353dd5c225SAndreas Gohr        ) {
2366c1f778cSAdrian Lang            $this->finishSectionEdit($pos - 1);
23790df9a4dSAdrian Lang        }
23890df9a4dSAdrian Lang
239e3c00e6eSIain Hallam        // build the header
240e3c00e6eSIain Hallam        $header = DOKU_LF.'<h'.$level;
24190df9a4dSAdrian Lang        if($level <= $conf['maxseclevel']) {
242ec57f119SLarsDW223            $data = array();
243ec57f119SLarsDW223            $data['target'] = 'section';
244ec57f119SLarsDW223            $data['name'] = $text;
245ec57f119SLarsDW223            $data['hid'] = $hid;
246ec57f119SLarsDW223            $data['codeblockOffset'] = $this->_codeblock;
247e3c00e6eSIain Hallam            $header .= ' class="'.$this->startSectionEdit($pos, $data).'"';
24890df9a4dSAdrian Lang        }
249e3c00e6eSIain Hallam        $header .= ' id="'.$hid.'">';
250e3c00e6eSIain Hallam        $header .= $this->_xmlEntities($text);
251e3c00e6eSIain Hallam        $header .= "</h$level>".DOKU_LF;
252e3c00e6eSIain Hallam
253e3c00e6eSIain Hallam        if ($returnonly) {
254e3c00e6eSIain Hallam            return $header;
255e3c00e6eSIain Hallam        } else {
256e3c00e6eSIain Hallam            $this->doc .= $header;
257e3c00e6eSIain Hallam        }
2580cecf9d5Sandi    }
2590cecf9d5Sandi
2603dd5c225SAndreas Gohr    /**
2613dd5c225SAndreas Gohr     * Open a new section
2623dd5c225SAndreas Gohr     *
2633dd5c225SAndreas Gohr     * @param int $level section level (as determined by the previous header)
2643dd5c225SAndreas Gohr     */
265de369923SAndreas Gohr    public function section_open($level) {
2669864e7b1SAdrian Lang        $this->doc .= '<div class="level'.$level.'">'.DOKU_LF;
2670cecf9d5Sandi    }
2680cecf9d5Sandi
2693dd5c225SAndreas Gohr    /**
2703dd5c225SAndreas Gohr     * Close the current section
2713dd5c225SAndreas Gohr     */
272de369923SAndreas Gohr    public function section_close() {
273a2d649c4Sandi        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
2740cecf9d5Sandi    }
2750cecf9d5Sandi
2763dd5c225SAndreas Gohr    /**
2773dd5c225SAndreas Gohr     * Render plain text data
2783dd5c225SAndreas Gohr     *
2793dd5c225SAndreas Gohr     * @param $text
2803dd5c225SAndreas Gohr     */
281de369923SAndreas Gohr    public function cdata($text) {
282a2d649c4Sandi        $this->doc .= $this->_xmlEntities($text);
2830cecf9d5Sandi    }
2840cecf9d5Sandi
2853dd5c225SAndreas Gohr    /**
2863dd5c225SAndreas Gohr     * Open a paragraph
2873dd5c225SAndreas Gohr     */
288de369923SAndreas Gohr    public function p_open() {
28959869a4bSAnika Henke        $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
2900cecf9d5Sandi    }
2910cecf9d5Sandi
2923dd5c225SAndreas Gohr    /**
2933dd5c225SAndreas Gohr     * Close a paragraph
2943dd5c225SAndreas Gohr     */
295de369923SAndreas Gohr    public function p_close() {
29659869a4bSAnika Henke        $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
2970cecf9d5Sandi    }
2980cecf9d5Sandi
2993dd5c225SAndreas Gohr    /**
3003dd5c225SAndreas Gohr     * Create a line break
3013dd5c225SAndreas Gohr     */
302de369923SAndreas Gohr    public function linebreak() {
303a2d649c4Sandi        $this->doc .= '<br/>'.DOKU_LF;
3040cecf9d5Sandi    }
3050cecf9d5Sandi
3063dd5c225SAndreas Gohr    /**
3073dd5c225SAndreas Gohr     * Create a horizontal line
3083dd5c225SAndreas Gohr     */
309de369923SAndreas Gohr    public function hr() {
3104beabca9SAnika Henke        $this->doc .= '<hr />'.DOKU_LF;
3110cecf9d5Sandi    }
3120cecf9d5Sandi
3133dd5c225SAndreas Gohr    /**
3143dd5c225SAndreas Gohr     * Start strong (bold) formatting
3153dd5c225SAndreas Gohr     */
316de369923SAndreas Gohr    public function strong_open() {
317a2d649c4Sandi        $this->doc .= '<strong>';
3180cecf9d5Sandi    }
3190cecf9d5Sandi
3203dd5c225SAndreas Gohr    /**
3213dd5c225SAndreas Gohr     * Stop strong (bold) formatting
3223dd5c225SAndreas Gohr     */
323de369923SAndreas Gohr    public function strong_close() {
324a2d649c4Sandi        $this->doc .= '</strong>';
3250cecf9d5Sandi    }
3260cecf9d5Sandi
3273dd5c225SAndreas Gohr    /**
3283dd5c225SAndreas Gohr     * Start emphasis (italics) formatting
3293dd5c225SAndreas Gohr     */
330de369923SAndreas Gohr    public function emphasis_open() {
331a2d649c4Sandi        $this->doc .= '<em>';
3320cecf9d5Sandi    }
3330cecf9d5Sandi
3343dd5c225SAndreas Gohr    /**
3353dd5c225SAndreas Gohr     * Stop emphasis (italics) formatting
3363dd5c225SAndreas Gohr     */
337de369923SAndreas Gohr    public function emphasis_close() {
338a2d649c4Sandi        $this->doc .= '</em>';
3390cecf9d5Sandi    }
3400cecf9d5Sandi
3413dd5c225SAndreas Gohr    /**
3423dd5c225SAndreas Gohr     * Start underline formatting
3433dd5c225SAndreas Gohr     */
344de369923SAndreas Gohr    public function underline_open() {
34502e51121SAnika Henke        $this->doc .= '<em class="u">';
3460cecf9d5Sandi    }
3470cecf9d5Sandi
3483dd5c225SAndreas Gohr    /**
3493dd5c225SAndreas Gohr     * Stop underline formatting
3503dd5c225SAndreas Gohr     */
351de369923SAndreas Gohr    public function underline_close() {
35202e51121SAnika Henke        $this->doc .= '</em>';
3530cecf9d5Sandi    }
3540cecf9d5Sandi
3553dd5c225SAndreas Gohr    /**
3563dd5c225SAndreas Gohr     * Start monospace formatting
3573dd5c225SAndreas Gohr     */
358de369923SAndreas Gohr    public function monospace_open() {
359a2d649c4Sandi        $this->doc .= '<code>';
3600cecf9d5Sandi    }
3610cecf9d5Sandi
3623dd5c225SAndreas Gohr    /**
3633dd5c225SAndreas Gohr     * Stop monospace formatting
3643dd5c225SAndreas Gohr     */
365de369923SAndreas Gohr    public function monospace_close() {
366a2d649c4Sandi        $this->doc .= '</code>';
3670cecf9d5Sandi    }
3680cecf9d5Sandi
3693dd5c225SAndreas Gohr    /**
3703dd5c225SAndreas Gohr     * Start a subscript
3713dd5c225SAndreas Gohr     */
372de369923SAndreas Gohr    public function subscript_open() {
373a2d649c4Sandi        $this->doc .= '<sub>';
3740cecf9d5Sandi    }
3750cecf9d5Sandi
3763dd5c225SAndreas Gohr    /**
3773dd5c225SAndreas Gohr     * Stop a subscript
3783dd5c225SAndreas Gohr     */
379de369923SAndreas Gohr    public function subscript_close() {
380a2d649c4Sandi        $this->doc .= '</sub>';
3810cecf9d5Sandi    }
3820cecf9d5Sandi
3833dd5c225SAndreas Gohr    /**
3843dd5c225SAndreas Gohr     * Start a superscript
3853dd5c225SAndreas Gohr     */
386de369923SAndreas Gohr    public function superscript_open() {
387a2d649c4Sandi        $this->doc .= '<sup>';
3880cecf9d5Sandi    }
3890cecf9d5Sandi
3903dd5c225SAndreas Gohr    /**
3913dd5c225SAndreas Gohr     * Stop a superscript
3923dd5c225SAndreas Gohr     */
393de369923SAndreas Gohr    public function superscript_close() {
394a2d649c4Sandi        $this->doc .= '</sup>';
3950cecf9d5Sandi    }
3960cecf9d5Sandi
3973dd5c225SAndreas Gohr    /**
3983dd5c225SAndreas Gohr     * Start deleted (strike-through) formatting
3993dd5c225SAndreas Gohr     */
400de369923SAndreas Gohr    public function deleted_open() {
401a2d649c4Sandi        $this->doc .= '<del>';
4020cecf9d5Sandi    }
4030cecf9d5Sandi
4043dd5c225SAndreas Gohr    /**
4053dd5c225SAndreas Gohr     * Stop deleted (strike-through) formatting
4063dd5c225SAndreas Gohr     */
407de369923SAndreas Gohr    public function deleted_close() {
408a2d649c4Sandi        $this->doc .= '</del>';
4090cecf9d5Sandi    }
4100cecf9d5Sandi
4113fd0b676Sandi    /**
4123fd0b676Sandi     * Callback for footnote start syntax
4133fd0b676Sandi     *
4143fd0b676Sandi     * All following content will go to the footnote instead of
415d74aace9Schris     * the document. To achieve this the previous rendered content
4163fd0b676Sandi     * is moved to $store and $doc is cleared
4173fd0b676Sandi     *
4183fd0b676Sandi     * @author Andreas Gohr <andi@splitbrain.org>
4193fd0b676Sandi     */
420de369923SAndreas Gohr    public function footnote_open() {
4217764a90aSandi
4227764a90aSandi        // move current content to store and record footnote
4237764a90aSandi        $this->store = $this->doc;
4247764a90aSandi        $this->doc   = '';
4250cecf9d5Sandi    }
4260cecf9d5Sandi
4273fd0b676Sandi    /**
4283fd0b676Sandi     * Callback for footnote end syntax
4293fd0b676Sandi     *
4303fd0b676Sandi     * All rendered content is moved to the $footnotes array and the old
4313fd0b676Sandi     * content is restored from $store again
4323fd0b676Sandi     *
4333fd0b676Sandi     * @author Andreas Gohr
4343fd0b676Sandi     */
435de369923SAndreas Gohr    public function footnote_close() {
43616ec3e37SAndreas Gohr        /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
43716ec3e37SAndreas Gohr        static $fnid = 0;
43816ec3e37SAndreas Gohr        // assign new footnote id (we start at 1)
43916ec3e37SAndreas Gohr        $fnid++;
4407764a90aSandi
441d74aace9Schris        // recover footnote into the stack and restore old content
442d74aace9Schris        $footnote    = $this->doc;
4437764a90aSandi        $this->doc   = $this->store;
4447764a90aSandi        $this->store = '';
445d74aace9Schris
446d74aace9Schris        // check to see if this footnote has been seen before
447d74aace9Schris        $i = array_search($footnote, $this->footnotes);
448d74aace9Schris
449d74aace9Schris        if($i === false) {
450d74aace9Schris            // its a new footnote, add it to the $footnotes array
45116ec3e37SAndreas Gohr            $this->footnotes[$fnid] = $footnote;
452d74aace9Schris        } else {
45316ec3e37SAndreas Gohr            // seen this one before, save a placeholder
45416ec3e37SAndreas Gohr            $this->footnotes[$fnid] = "@@FNT".($i);
455d74aace9Schris        }
456d74aace9Schris
4576b379cbfSAndreas Gohr        // output the footnote reference and link
45816ec3e37SAndreas Gohr        $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>';
4590cecf9d5Sandi    }
4600cecf9d5Sandi
4613dd5c225SAndreas Gohr    /**
4623dd5c225SAndreas Gohr     * Open an unordered list
4630c4c0281SGerrit Uitslag     *
4647d769f75SAndreas Gohr     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
4653dd5c225SAndreas Gohr     */
466de369923SAndreas Gohr    public function listu_open($classes = null) {
4670c4c0281SGerrit Uitslag        $class = '';
4680c4c0281SGerrit Uitslag        if($classes !== null) {
4692e0ebe60SAndreas Gohr            if(is_array($classes)) $classes = join(' ', $classes);
4700c4c0281SGerrit Uitslag            $class = " class=\"$classes\"";
4710c4c0281SGerrit Uitslag        }
4720c4c0281SGerrit Uitslag        $this->doc .= "<ul$class>".DOKU_LF;
4730cecf9d5Sandi    }
4740cecf9d5Sandi
4753dd5c225SAndreas Gohr    /**
4763dd5c225SAndreas Gohr     * Close an unordered list
4773dd5c225SAndreas Gohr     */
478de369923SAndreas Gohr    public function listu_close() {
479a2d649c4Sandi        $this->doc .= '</ul>'.DOKU_LF;
4800cecf9d5Sandi    }
4810cecf9d5Sandi
4823dd5c225SAndreas Gohr    /**
4833dd5c225SAndreas Gohr     * Open an ordered list
4840c4c0281SGerrit Uitslag     *
4857d769f75SAndreas Gohr     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
4863dd5c225SAndreas Gohr     */
487de369923SAndreas Gohr    public function listo_open($classes = null) {
4880c4c0281SGerrit Uitslag        $class = '';
4890c4c0281SGerrit Uitslag        if($classes !== null) {
4902e0ebe60SAndreas Gohr            if(is_array($classes)) $classes = join(' ', $classes);
4910c4c0281SGerrit Uitslag            $class = " class=\"$classes\"";
4920c4c0281SGerrit Uitslag        }
4930c4c0281SGerrit Uitslag        $this->doc .= "<ol$class>".DOKU_LF;
4940cecf9d5Sandi    }
4950cecf9d5Sandi
4963dd5c225SAndreas Gohr    /**
4973dd5c225SAndreas Gohr     * Close an ordered list
4983dd5c225SAndreas Gohr     */
499de369923SAndreas Gohr    public function listo_close() {
500a2d649c4Sandi        $this->doc .= '</ol>'.DOKU_LF;
5010cecf9d5Sandi    }
5020cecf9d5Sandi
5033dd5c225SAndreas Gohr    /**
5043dd5c225SAndreas Gohr     * Open a list item
5053dd5c225SAndreas Gohr     *
5063dd5c225SAndreas Gohr     * @param int $level the nesting level
507e3a24861SChristopher Smith     * @param bool $node true when a node; false when a leaf
5083dd5c225SAndreas Gohr     */
509de369923SAndreas Gohr    public function listitem_open($level, $node=false) {
510e3a24861SChristopher Smith        $branching = $node ? ' node' : '';
511e3a24861SChristopher Smith        $this->doc .= '<li class="level'.$level.$branching.'">';
5120cecf9d5Sandi    }
5130cecf9d5Sandi
5143dd5c225SAndreas Gohr    /**
5153dd5c225SAndreas Gohr     * Close a list item
5163dd5c225SAndreas Gohr     */
517de369923SAndreas Gohr    public function listitem_close() {
518a2d649c4Sandi        $this->doc .= '</li>'.DOKU_LF;
5190cecf9d5Sandi    }
5200cecf9d5Sandi
5213dd5c225SAndreas Gohr    /**
5223dd5c225SAndreas Gohr     * Start the content of a list item
5233dd5c225SAndreas Gohr     */
524de369923SAndreas Gohr    public function listcontent_open() {
52590db23d7Schris        $this->doc .= '<div class="li">';
5260cecf9d5Sandi    }
5270cecf9d5Sandi
5283dd5c225SAndreas Gohr    /**
5293dd5c225SAndreas Gohr     * Stop the content of a list item
5303dd5c225SAndreas Gohr     */
531de369923SAndreas Gohr    public function listcontent_close() {
53259869a4bSAnika Henke        $this->doc .= '</div>'.DOKU_LF;
5330cecf9d5Sandi    }
5340cecf9d5Sandi
5353dd5c225SAndreas Gohr    /**
5363dd5c225SAndreas Gohr     * Output unformatted $text
5373dd5c225SAndreas Gohr     *
5383dd5c225SAndreas Gohr     * Defaults to $this->cdata()
5393dd5c225SAndreas Gohr     *
5403dd5c225SAndreas Gohr     * @param string $text
5413dd5c225SAndreas Gohr     */
542de369923SAndreas Gohr    public function unformatted($text) {
543a2d649c4Sandi        $this->doc .= $this->_xmlEntities($text);
5440cecf9d5Sandi    }
5450cecf9d5Sandi
5460cecf9d5Sandi    /**
5473dd5c225SAndreas Gohr     * Start a block quote
5483dd5c225SAndreas Gohr     */
549de369923SAndreas Gohr    public function quote_open() {
55096331712SAnika Henke        $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
5510cecf9d5Sandi    }
5520cecf9d5Sandi
5533dd5c225SAndreas Gohr    /**
5543dd5c225SAndreas Gohr     * Stop a block quote
5553dd5c225SAndreas Gohr     */
556de369923SAndreas Gohr    public function quote_close() {
55796331712SAnika Henke        $this->doc .= '</div></blockquote>'.DOKU_LF;
5580cecf9d5Sandi    }
5590cecf9d5Sandi
5603dd5c225SAndreas Gohr    /**
5613dd5c225SAndreas Gohr     * Output preformatted text
5623dd5c225SAndreas Gohr     *
5633dd5c225SAndreas Gohr     * @param string $text
5643dd5c225SAndreas Gohr     */
565de369923SAndreas Gohr    public function preformatted($text) {
566c9250713SAnika Henke        $this->doc .= '<pre class="code">'.trim($this->_xmlEntities($text), "\n\r").'</pre>'.DOKU_LF;
5673d491f75SAndreas Gohr    }
5683d491f75SAndreas Gohr
5693dd5c225SAndreas Gohr    /**
5703dd5c225SAndreas Gohr     * Display text as file content, optionally syntax highlighted
5713dd5c225SAndreas Gohr     *
5723dd5c225SAndreas Gohr     * @param string $text     text to show
5733dd5c225SAndreas Gohr     * @param string $language programming language to use for syntax highlighting
5743dd5c225SAndreas Gohr     * @param string $filename file path label
575e2d88156SLarsDW223     * @param array  $options  assoziative array with additional geshi options
5763dd5c225SAndreas Gohr     */
577de369923SAndreas Gohr    public function file($text, $language = null, $filename = null, $options=null) {
578e2d88156SLarsDW223        $this->_highlight('file', $text, $language, $filename, $options);
5793d491f75SAndreas Gohr    }
5803d491f75SAndreas Gohr
5813dd5c225SAndreas Gohr    /**
5823dd5c225SAndreas Gohr     * Display text as code content, optionally syntax highlighted
5833dd5c225SAndreas Gohr     *
5843dd5c225SAndreas Gohr     * @param string $text     text to show
5853dd5c225SAndreas Gohr     * @param string $language programming language to use for syntax highlighting
5863dd5c225SAndreas Gohr     * @param string $filename file path label
587e2d88156SLarsDW223     * @param array  $options  assoziative array with additional geshi options
5883dd5c225SAndreas Gohr     */
589de369923SAndreas Gohr    public function code($text, $language = null, $filename = null, $options=null) {
590e2d88156SLarsDW223        $this->_highlight('code', $text, $language, $filename, $options);
5913d491f75SAndreas Gohr    }
5923d491f75SAndreas Gohr
5930cecf9d5Sandi    /**
5943d491f75SAndreas Gohr     * Use GeSHi to highlight language syntax in code and file blocks
5953fd0b676Sandi     *
5963fd0b676Sandi     * @author Andreas Gohr <andi@splitbrain.org>
5973dd5c225SAndreas Gohr     * @param string $type     code|file
5983dd5c225SAndreas Gohr     * @param string $text     text to show
5993dd5c225SAndreas Gohr     * @param string $language programming language to use for syntax highlighting
6003dd5c225SAndreas Gohr     * @param string $filename file path label
601e2d88156SLarsDW223     * @param array  $options  assoziative array with additional geshi options
6020cecf9d5Sandi     */
603de369923SAndreas Gohr    public function _highlight($type, $text, $language = null, $filename = null, $options = null) {
6043d491f75SAndreas Gohr        global $ID;
6053d491f75SAndreas Gohr        global $lang;
606ec57f119SLarsDW223        global $INPUT;
6073d491f75SAndreas Gohr
608bf8f8509SAndreas Gohr        $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language ?? '');
60956bd9509SPhy
6103d491f75SAndreas Gohr        if($filename) {
611190c56e8SAndreas Gohr            // add icon
61227bf7924STom N Harris            list($ext) = mimetype($filename, false);
613190c56e8SAndreas Gohr            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
614190c56e8SAndreas Gohr            $class = 'mediafile mf_'.$class;
615190c56e8SAndreas Gohr
616ec57f119SLarsDW223            $offset = 0;
617ec57f119SLarsDW223            if ($INPUT->has('codeblockOffset')) {
618ec57f119SLarsDW223                $offset = $INPUT->str('codeblockOffset');
619ec57f119SLarsDW223            }
6203d491f75SAndreas Gohr            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
62164159a61SAndreas Gohr            $this->doc .= '<dt><a href="' .
62264159a61SAndreas Gohr                exportlink(
62364159a61SAndreas Gohr                    $ID,
62464159a61SAndreas Gohr                    'code',
62564159a61SAndreas Gohr                    array('codeblock' => $offset + $this->_codeblock)
62664159a61SAndreas Gohr                ) . '" title="' . $lang['download'] . '" class="' . $class . '">';
6273d491f75SAndreas Gohr            $this->doc .= hsc($filename);
6283d491f75SAndreas Gohr            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
6293d491f75SAndreas Gohr        }
6300cecf9d5Sandi
6312401f18dSSyntaxseed        if($text[0] == "\n") {
632d43aac1cSGina Haeussge            $text = substr($text, 1);
633d43aac1cSGina Haeussge        }
634d43aac1cSGina Haeussge        if(substr($text, -1) == "\n") {
635d43aac1cSGina Haeussge            $text = substr($text, 0, -1);
636d43aac1cSGina Haeussge        }
637d43aac1cSGina Haeussge
638a056e285SPhy        if(empty($language)) { // empty is faster than is_null and can prevent '' string
6393d491f75SAndreas Gohr            $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
6400cecf9d5Sandi        } else {
6413d491f75SAndreas Gohr            $class = 'code'; //we always need the code class to make the syntax highlighting apply
6423d491f75SAndreas Gohr            if($type != 'code') $class .= ' '.$type;
6433d491f75SAndreas Gohr
64464159a61SAndreas Gohr            $this->doc .= "<pre class=\"$class $language\">" .
64564159a61SAndreas Gohr                p_xhtml_cached_geshi($text, $language, '', $options) .
64664159a61SAndreas Gohr                '</pre>' . DOKU_LF;
6470cecf9d5Sandi        }
6483d491f75SAndreas Gohr
6493d491f75SAndreas Gohr        if($filename) {
6503d491f75SAndreas Gohr            $this->doc .= '</dd></dl>'.DOKU_LF;
6513d491f75SAndreas Gohr        }
6523d491f75SAndreas Gohr
6533d491f75SAndreas Gohr        $this->_codeblock++;
6540cecf9d5Sandi    }
6550cecf9d5Sandi
6563dd5c225SAndreas Gohr    /**
6573dd5c225SAndreas Gohr     * Format an acronym
6583dd5c225SAndreas Gohr     *
6593dd5c225SAndreas Gohr     * Uses $this->acronyms
6603dd5c225SAndreas Gohr     *
6613dd5c225SAndreas Gohr     * @param string $acronym
6623dd5c225SAndreas Gohr     */
663de369923SAndreas Gohr    public function acronym($acronym) {
6640cecf9d5Sandi
6650cecf9d5Sandi        if(array_key_exists($acronym, $this->acronyms)) {
6660cecf9d5Sandi
667433bef32Sandi            $title = $this->_xmlEntities($this->acronyms[$acronym]);
6680cecf9d5Sandi
669940db3a3SAnika Henke            $this->doc .= '<abbr title="'.$title
670940db3a3SAnika Henke                .'">'.$this->_xmlEntities($acronym).'</abbr>';
6710cecf9d5Sandi
6720cecf9d5Sandi        } else {
673a2d649c4Sandi            $this->doc .= $this->_xmlEntities($acronym);
6740cecf9d5Sandi        }
6750cecf9d5Sandi    }
6760cecf9d5Sandi
6773dd5c225SAndreas Gohr    /**
6783dd5c225SAndreas Gohr     * Format a smiley
6793dd5c225SAndreas Gohr     *
6803dd5c225SAndreas Gohr     * Uses $this->smiley
6813dd5c225SAndreas Gohr     *
6823dd5c225SAndreas Gohr     * @param string $smiley
6833dd5c225SAndreas Gohr     */
684de369923SAndreas Gohr    public function smiley($smiley) {
685b09504a9SAndreas Gohr        if (isset($this->smileys[$smiley])) {
686f62ea8a1Sandi            $this->doc .= '<img src="' . DOKU_BASE . 'lib/images/smileys/' . $this->smileys[$smiley] .
687b09504a9SAndreas Gohr                '" class="icon smiley" alt="' . $this->_xmlEntities($smiley) . '" />';
6880cecf9d5Sandi        } else {
689a2d649c4Sandi            $this->doc .= $this->_xmlEntities($smiley);
6900cecf9d5Sandi        }
6910cecf9d5Sandi    }
6920cecf9d5Sandi
6933dd5c225SAndreas Gohr    /**
6943dd5c225SAndreas Gohr     * Format an entity
6953dd5c225SAndreas Gohr     *
6963dd5c225SAndreas Gohr     * Entities are basically small text replacements
6973dd5c225SAndreas Gohr     *
6983dd5c225SAndreas Gohr     * Uses $this->entities
6993dd5c225SAndreas Gohr     *
7003dd5c225SAndreas Gohr     * @param string $entity
7014de671bcSandi     */
702de369923SAndreas Gohr    public function entity($entity) {
7030cecf9d5Sandi        if(array_key_exists($entity, $this->entities)) {
704a2d649c4Sandi            $this->doc .= $this->entities[$entity];
7050cecf9d5Sandi        } else {
706a2d649c4Sandi            $this->doc .= $this->_xmlEntities($entity);
7070cecf9d5Sandi        }
7080cecf9d5Sandi    }
7090cecf9d5Sandi
7103dd5c225SAndreas Gohr    /**
7113dd5c225SAndreas Gohr     * Typographically format a multiply sign
7123dd5c225SAndreas Gohr     *
7133dd5c225SAndreas Gohr     * Example: ($x=640, $y=480) should result in "640×480"
7143dd5c225SAndreas Gohr     *
7153dd5c225SAndreas Gohr     * @param string|int $x first value
7163dd5c225SAndreas Gohr     * @param string|int $y second value
7173dd5c225SAndreas Gohr     */
718de369923SAndreas Gohr    public function multiplyentity($x, $y) {
719a2d649c4Sandi        $this->doc .= "$x&times;$y";
7200cecf9d5Sandi    }
7210cecf9d5Sandi
7223dd5c225SAndreas Gohr    /**
7233dd5c225SAndreas Gohr     * Render an opening single quote char (language specific)
7243dd5c225SAndreas Gohr     */
725de369923SAndreas Gohr    public function singlequoteopening() {
72671b40da2SAnika Henke        global $lang;
72771b40da2SAnika Henke        $this->doc .= $lang['singlequoteopening'];
7280cecf9d5Sandi    }
7290cecf9d5Sandi
7303dd5c225SAndreas Gohr    /**
7313dd5c225SAndreas Gohr     * Render a closing single quote char (language specific)
7323dd5c225SAndreas Gohr     */
733de369923SAndreas Gohr    public function singlequoteclosing() {
73471b40da2SAnika Henke        global $lang;
73571b40da2SAnika Henke        $this->doc .= $lang['singlequoteclosing'];
7360cecf9d5Sandi    }
7370cecf9d5Sandi
7383dd5c225SAndreas Gohr    /**
7393dd5c225SAndreas Gohr     * Render an apostrophe char (language specific)
7403dd5c225SAndreas Gohr     */
741de369923SAndreas Gohr    public function apostrophe() {
74257d757d1SAndreas Gohr        global $lang;
743a8bd192aSAndreas Gohr        $this->doc .= $lang['apostrophe'];
74457d757d1SAndreas Gohr    }
74557d757d1SAndreas Gohr
7463dd5c225SAndreas Gohr    /**
7473dd5c225SAndreas Gohr     * Render an opening double quote char (language specific)
7483dd5c225SAndreas Gohr     */
749de369923SAndreas Gohr    public function doublequoteopening() {
75071b40da2SAnika Henke        global $lang;
75171b40da2SAnika Henke        $this->doc .= $lang['doublequoteopening'];
7520cecf9d5Sandi    }
7530cecf9d5Sandi
7543dd5c225SAndreas Gohr    /**
7553dd5c225SAndreas Gohr     * Render an closinging double quote char (language specific)
7563dd5c225SAndreas Gohr     */
757de369923SAndreas Gohr    public function doublequoteclosing() {
75871b40da2SAnika Henke        global $lang;
75971b40da2SAnika Henke        $this->doc .= $lang['doublequoteclosing'];
7600cecf9d5Sandi    }
7610cecf9d5Sandi
7620cecf9d5Sandi    /**
7633dd5c225SAndreas Gohr     * Render a CamelCase link
7643dd5c225SAndreas Gohr     *
7653dd5c225SAndreas Gohr     * @param string $link       The link name
766122f2d46SAndreas Böhler     * @param bool   $returnonly whether to return html or write to doc attribute
7670c4c0281SGerrit Uitslag     * @return void|string writes to doc attribute or returns html depends on $returnonly
7680c4c0281SGerrit Uitslag     *
7693dd5c225SAndreas Gohr     * @see http://en.wikipedia.org/wiki/CamelCase
7700cecf9d5Sandi     */
771de369923SAndreas Gohr    public function camelcaselink($link, $returnonly = false) {
772122f2d46SAndreas Böhler        if($returnonly) {
773122f2d46SAndreas Böhler          return $this->internallink($link, $link, null, true);
774122f2d46SAndreas Böhler        } else {
77511d0aa47Sandi          $this->internallink($link, $link);
7760cecf9d5Sandi        }
777122f2d46SAndreas Böhler    }
7780cecf9d5Sandi
7793dd5c225SAndreas Gohr    /**
7803dd5c225SAndreas Gohr     * Render a page local link
7813dd5c225SAndreas Gohr     *
7823dd5c225SAndreas Gohr     * @param string $hash       hash link identifier
7833dd5c225SAndreas Gohr     * @param string $name       name for the link
784122f2d46SAndreas Böhler     * @param bool   $returnonly whether to return html or write to doc attribute
7850c4c0281SGerrit Uitslag     * @return void|string writes to doc attribute or returns html depends on $returnonly
7863dd5c225SAndreas Gohr     */
787de369923SAndreas Gohr    public function locallink($hash, $name = null, $returnonly = false) {
7880b7c14c2Sandi        global $ID;
7890b7c14c2Sandi        $name  = $this->_getLinkTitle($name, $hash, $isImage);
7900b7c14c2Sandi        $hash  = $this->_headerToLink($hash);
791e260f93bSAnika Henke        $title = $ID.' ↵';
792122f2d46SAndreas Böhler
793122f2d46SAndreas Böhler        $doc = '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
794122f2d46SAndreas Böhler        $doc .= $name;
795122f2d46SAndreas Böhler        $doc .= '</a>';
796122f2d46SAndreas Böhler
797122f2d46SAndreas Böhler        if($returnonly) {
798122f2d46SAndreas Böhler          return $doc;
799122f2d46SAndreas Böhler        } else {
800122f2d46SAndreas Böhler          $this->doc .= $doc;
801122f2d46SAndreas Böhler        }
8020b7c14c2Sandi    }
8030b7c14c2Sandi
804cffcc403Sandi    /**
8053fd0b676Sandi     * Render an internal Wiki Link
8063fd0b676Sandi     *
807fe9ec250SChris Smith     * $search,$returnonly & $linktype are not for the renderer but are used
808cffcc403Sandi     * elsewhere - no need to implement them in other renderers
8093fd0b676Sandi     *
8103dd5c225SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
811f23eef27SGerrit Uitslag     * @param string      $id         pageid
812f23eef27SGerrit Uitslag     * @param string|null $name       link name
813f23eef27SGerrit Uitslag     * @param string|null $search     adds search url param
814f23eef27SGerrit Uitslag     * @param bool        $returnonly whether to return html or write to doc attribute
815f23eef27SGerrit Uitslag     * @param string      $linktype   type to set use of headings
816f23eef27SGerrit Uitslag     * @return void|string writes to doc attribute or returns html depends on $returnonly
817cffcc403Sandi     */
818de369923SAndreas Gohr    public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') {
819ba11bd29Sandi        global $conf;
82037e34a5eSandi        global $ID;
821c4dda6afSAnika Henke        global $INFO;
82244653a53SAdrian Lang
8233d5e07d9SAdrian Lang        $params = '';
8243d5e07d9SAdrian Lang        $parts  = explode('?', $id, 2);
8253d5e07d9SAdrian Lang        if(count($parts) === 2) {
8263d5e07d9SAdrian Lang            $id     = $parts[0];
8273d5e07d9SAdrian Lang            $params = $parts[1];
82844653a53SAdrian Lang        }
82944653a53SAdrian Lang
830fda14ffcSIzidor Matušov        // For empty $id we need to know the current $ID
831fda14ffcSIzidor Matušov        // We need this check because _simpleTitle needs
832fda14ffcSIzidor Matušov        // correct $id and resolve_pageid() use cleanID($id)
833fda14ffcSIzidor Matušov        // (some things could be lost)
834fda14ffcSIzidor Matušov        if($id === '') {
835fda14ffcSIzidor Matušov            $id = $ID;
836fda14ffcSIzidor Matušov        }
837fda14ffcSIzidor Matušov
8380339c872Sjan        // default name is based on $id as given
8390339c872Sjan        $default = $this->_simpleTitle($id);
840ad32e47eSAndreas Gohr
8410339c872Sjan        // now first resolve and clean up the $id
8428c6be208SAndreas Gohr        $id = (new PageResolver($ID))->resolveId($id, $this->date_at, true);
8438c6be208SAndreas Gohr        $exists = page_exists($id, $this->date_at, false, true);
844fda14ffcSIzidor Matušov
84559bc3b48SGerrit Uitslag        $link = array();
846fe9ec250SChris Smith        $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
8470e1c636eSandi        if(!$isImage) {
8480e1c636eSandi            if($exists) {
849ba11bd29Sandi                $class = 'wikilink1';
8500cecf9d5Sandi            } else {
851ba11bd29Sandi                $class       = 'wikilink2';
85244a6b4c7SAndreas Gohr                $link['rel'] = 'nofollow';
8530cecf9d5Sandi            }
8540cecf9d5Sandi        } else {
855ba11bd29Sandi            $class = 'media';
8560cecf9d5Sandi        }
8570cecf9d5Sandi
858a1685bedSandi        //keep hash anchor
859ec34bb30SAndreas Gohr        list($id, $hash) = sexplode('#', $id, 2);
860943dedc6SAndreas Gohr        if(!empty($hash)) $hash = $this->_headerToLink($hash);
861a1685bedSandi
862ba11bd29Sandi        //prepare for formating
863ba11bd29Sandi        $link['target'] = $conf['target']['wiki'];
864ba11bd29Sandi        $link['style']  = '';
865ba11bd29Sandi        $link['pre']    = '';
866ba11bd29Sandi        $link['suf']    = '';
867bbac1489SPhy        $link['more']   = 'data-wiki-id="'.$id.'"'; // id is already cleaned
868ba11bd29Sandi        $link['class']  = $class;
8695c2eed9aSlisps        if($this->date_at) {
870912a6d48SPhy            $params = $params.'&at='.rawurlencode($this->date_at);
8715c2eed9aSlisps        }
87244653a53SAdrian Lang        $link['url']    = wl($id, $params);
873ba11bd29Sandi        $link['name']   = $name;
874ba11bd29Sandi        $link['title']  = $id;
875723d78dbSandi        //add search string
876723d78dbSandi        if($search) {
877546d3a99SAndreas Gohr            ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
878546d3a99SAndreas Gohr            if(is_array($search)) {
879546d3a99SAndreas Gohr                $search = array_map('rawurlencode', $search);
880546d3a99SAndreas Gohr                $link['url'] .= 's[]='.join('&amp;s[]=', $search);
881546d3a99SAndreas Gohr            } else {
882546d3a99SAndreas Gohr                $link['url'] .= 's='.rawurlencode($search);
883546d3a99SAndreas Gohr            }
884723d78dbSandi        }
885723d78dbSandi
886a1685bedSandi        //keep hash
887a1685bedSandi        if($hash) $link['url'] .= '#'.$hash;
888a1685bedSandi
889ba11bd29Sandi        //output formatted
890cffcc403Sandi        if($returnonly) {
891cffcc403Sandi            return $this->_formatLink($link);
892cffcc403Sandi        } else {
893a2d649c4Sandi            $this->doc .= $this->_formatLink($link);
8940cecf9d5Sandi        }
895cffcc403Sandi    }
8960cecf9d5Sandi
8973dd5c225SAndreas Gohr    /**
8983dd5c225SAndreas Gohr     * Render an external link
8993dd5c225SAndreas Gohr     *
9003dd5c225SAndreas Gohr     * @param string       $url        full URL with scheme
9013dd5c225SAndreas Gohr     * @param string|array $name       name for the link, array for media file
902122f2d46SAndreas Böhler     * @param bool         $returnonly whether to return html or write to doc attribute
9030c4c0281SGerrit Uitslag     * @return void|string writes to doc attribute or returns html depends on $returnonly
9043dd5c225SAndreas Gohr     */
905de369923SAndreas Gohr    public function externallink($url, $name = null, $returnonly = false) {
906b625487dSandi        global $conf;
9070cecf9d5Sandi
908433bef32Sandi        $name = $this->_getLinkTitle($name, $url, $isImage);
9096f0c5dbfSandi
910b52b1596SAndreas Gohr        // url might be an attack vector, only allow registered protocols
911b52b1596SAndreas Gohr        if(is_null($this->schemes)) $this->schemes = getSchemes();
912b52b1596SAndreas Gohr        list($scheme) = explode('://', $url);
913b52b1596SAndreas Gohr        $scheme = strtolower($scheme);
914b52b1596SAndreas Gohr        if(!in_array($scheme, $this->schemes)) $url = '';
915b52b1596SAndreas Gohr
916b52b1596SAndreas Gohr        // is there still an URL?
917b52b1596SAndreas Gohr        if(!$url) {
91849cef4fdSAndreas Böhler            if($returnonly) {
91949cef4fdSAndreas Böhler                return $name;
92049cef4fdSAndreas Böhler            } else {
921b52b1596SAndreas Gohr                $this->doc .= $name;
92249cef4fdSAndreas Böhler            }
923b52b1596SAndreas Gohr            return;
924b52b1596SAndreas Gohr        }
925b52b1596SAndreas Gohr
926b52b1596SAndreas Gohr        // set class
9270cecf9d5Sandi        if(!$isImage) {
928b625487dSandi            $class = 'urlextern';
9290cecf9d5Sandi        } else {
930b625487dSandi            $class = 'media';
9310cecf9d5Sandi        }
9320cecf9d5Sandi
933b625487dSandi        //prepare for formating
93459bc3b48SGerrit Uitslag        $link = array();
935b625487dSandi        $link['target'] = $conf['target']['extern'];
936b625487dSandi        $link['style']  = '';
937b625487dSandi        $link['pre']    = '';
938b625487dSandi        $link['suf']    = '';
9395e163278SAndreas Gohr        $link['more']   = '';
940b625487dSandi        $link['class']  = $class;
941b625487dSandi        $link['url']    = $url;
942914045f3SAndreas Gohr        $link['rel']    = '';
943e1c10e4dSchris
944b625487dSandi        $link['name']  = $name;
945433bef32Sandi        $link['title'] = $this->_xmlEntities($url);
9465ddd0bbbSStarArmy        if($conf['relnofollow']) $link['rel'] .= ' ugc nofollow';
947914045f3SAndreas Gohr        if($conf['target']['extern']) $link['rel'] .= ' noopener';
9480cecf9d5Sandi
949b625487dSandi        //output formatted
950122f2d46SAndreas Böhler        if($returnonly) {
951122f2d46SAndreas Böhler            return $this->_formatLink($link);
952122f2d46SAndreas Böhler        } else {
953a2d649c4Sandi            $this->doc .= $this->_formatLink($link);
9540cecf9d5Sandi        }
955122f2d46SAndreas Böhler    }
9560cecf9d5Sandi
9570cecf9d5Sandi    /**
9583dd5c225SAndreas Gohr     * Render an interwiki link
9593dd5c225SAndreas Gohr     *
9603dd5c225SAndreas Gohr     * You may want to use $this->_resolveInterWiki() here
9613dd5c225SAndreas Gohr     *
9623dd5c225SAndreas Gohr     * @param string       $match      original link - probably not much use
9633dd5c225SAndreas Gohr     * @param string|array $name       name for the link, array for media file
9643dd5c225SAndreas Gohr     * @param string       $wikiName   indentifier (shortcut) for the remote wiki
9653dd5c225SAndreas Gohr     * @param string       $wikiUri    the fragment parsed from the original link
966122f2d46SAndreas Böhler     * @param bool         $returnonly whether to return html or write to doc attribute
9670c4c0281SGerrit Uitslag     * @return void|string writes to doc attribute or returns html depends on $returnonly
9680cecf9d5Sandi     */
969de369923SAndreas Gohr    public function interwikilink($match, $name, $wikiName, $wikiUri, $returnonly = false) {
970b625487dSandi        global $conf;
9710cecf9d5Sandi
97297a3e4e3Sandi        $link           = array();
97397a3e4e3Sandi        $link['target'] = $conf['target']['interwiki'];
97497a3e4e3Sandi        $link['pre']    = '';
97597a3e4e3Sandi        $link['suf']    = '';
9765e163278SAndreas Gohr        $link['more']   = '';
977433bef32Sandi        $link['name']   = $this->_getLinkTitle($name, $wikiUri, $isImage);
978914045f3SAndreas Gohr        $link['rel']    = '';
9790cecf9d5Sandi
98097a3e4e3Sandi        //get interwiki URL
9816496c33fSGerrit Uitslag        $exists = null;
9826496c33fSGerrit Uitslag        $url    = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
9830cecf9d5Sandi
98497a3e4e3Sandi        if(!$isImage) {
9859d2ddea4SAndreas Gohr            $class         = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
9869d2ddea4SAndreas Gohr            $link['class'] = "interwiki iw_$class";
9871c2d1019SAndreas Gohr        } else {
9881c2d1019SAndreas Gohr            $link['class'] = 'media';
98997a3e4e3Sandi        }
9900cecf9d5Sandi
99197a3e4e3Sandi        //do we stay at the same server? Use local target
9922345e871SGerrit Uitslag        if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) {
99397a3e4e3Sandi            $link['target'] = $conf['target']['wiki'];
99497a3e4e3Sandi        }
9956496c33fSGerrit Uitslag        if($exists !== null && !$isImage) {
9966496c33fSGerrit Uitslag            if($exists) {
9976496c33fSGerrit Uitslag                $link['class'] .= ' wikilink1';
9986496c33fSGerrit Uitslag            } else {
9996496c33fSGerrit Uitslag                $link['class'] .= ' wikilink2';
1000914045f3SAndreas Gohr                $link['rel'] .= ' nofollow';
10016496c33fSGerrit Uitslag            }
10026496c33fSGerrit Uitslag        }
1003914045f3SAndreas Gohr        if($conf['target']['interwiki']) $link['rel'] .= ' noopener';
10040cecf9d5Sandi
100597a3e4e3Sandi        $link['url']   = $url;
1006f7711f2bSAndreas Gohr        $link['title'] = $this->_xmlEntities($link['url']);
100797a3e4e3Sandi
100897a3e4e3Sandi        // output formatted
1009122f2d46SAndreas Böhler        if($returnonly) {
1010abde5980SPhy            if($url == '') return $link['name'];
1011122f2d46SAndreas Böhler            return $this->_formatLink($link);
1012122f2d46SAndreas Böhler        } else {
1013abde5980SPhy            if($url == '') $this->doc .= $link['name'];
1014abde5980SPhy            else $this->doc .= $this->_formatLink($link);
10150cecf9d5Sandi        }
1016122f2d46SAndreas Böhler    }
10170cecf9d5Sandi
10180cecf9d5Sandi    /**
10193dd5c225SAndreas Gohr     * Link to windows share
10203dd5c225SAndreas Gohr     *
10213dd5c225SAndreas Gohr     * @param string       $url        the link
10223dd5c225SAndreas Gohr     * @param string|array $name       name for the link, array for media file
1023122f2d46SAndreas Böhler     * @param bool         $returnonly whether to return html or write to doc attribute
10240c4c0281SGerrit Uitslag     * @return void|string writes to doc attribute or returns html depends on $returnonly
10250cecf9d5Sandi     */
1026de369923SAndreas Gohr    public function windowssharelink($url, $name = null, $returnonly = false) {
10271d47afe1Sandi        global $conf;
10283dd5c225SAndreas Gohr
10291d47afe1Sandi        //simple setup
103059bc3b48SGerrit Uitslag        $link = array();
10311d47afe1Sandi        $link['target'] = $conf['target']['windows'];
10321d47afe1Sandi        $link['pre']    = '';
10331d47afe1Sandi        $link['suf']    = '';
10341d47afe1Sandi        $link['style']  = '';
10350cecf9d5Sandi
1036433bef32Sandi        $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
10370cecf9d5Sandi        if(!$isImage) {
10381d47afe1Sandi            $link['class'] = 'windows';
10390cecf9d5Sandi        } else {
10401d47afe1Sandi            $link['class'] = 'media';
10410cecf9d5Sandi        }
10420cecf9d5Sandi
1043433bef32Sandi        $link['title'] = $this->_xmlEntities($url);
10441d47afe1Sandi        $url           = str_replace('\\', '/', $url);
10451d47afe1Sandi        $url           = 'file:///'.$url;
10461d47afe1Sandi        $link['url']   = $url;
10470cecf9d5Sandi
10481d47afe1Sandi        //output formatted
1049122f2d46SAndreas Böhler        if($returnonly) {
1050122f2d46SAndreas Böhler            return $this->_formatLink($link);
1051122f2d46SAndreas Böhler        } else {
1052a2d649c4Sandi            $this->doc .= $this->_formatLink($link);
10530cecf9d5Sandi        }
1054122f2d46SAndreas Böhler    }
10550cecf9d5Sandi
10563dd5c225SAndreas Gohr    /**
10573dd5c225SAndreas Gohr     * Render a linked E-Mail Address
10583dd5c225SAndreas Gohr     *
10593dd5c225SAndreas Gohr     * Honors $conf['mailguard'] setting
10603dd5c225SAndreas Gohr     *
10613dd5c225SAndreas Gohr     * @param string       $address    Email-Address
10623dd5c225SAndreas Gohr     * @param string|array $name       name for the link, array for media file
1063122f2d46SAndreas Böhler     * @param bool         $returnonly whether to return html or write to doc attribute
10640c4c0281SGerrit Uitslag     * @return void|string writes to doc attribute or returns html depends on $returnonly
10653dd5c225SAndreas Gohr     */
1066de369923SAndreas Gohr    public function emaillink($address, $name = null, $returnonly = false) {
106771352defSandi        global $conf;
106871352defSandi        //simple setup
106971352defSandi        $link           = array();
107071352defSandi        $link['target'] = '';
107171352defSandi        $link['pre']    = '';
107271352defSandi        $link['suf']    = '';
107371352defSandi        $link['style']  = '';
107471352defSandi        $link['more']   = '';
10750cecf9d5Sandi
1076c078fc55SAndreas Gohr        $name = $this->_getLinkTitle($name, '', $isImage);
10770cecf9d5Sandi        if(!$isImage) {
1078be96545cSAnika Henke            $link['class'] = 'mail';
10790cecf9d5Sandi        } else {
1080be96545cSAnika Henke            $link['class'] = 'media';
10810cecf9d5Sandi        }
10820cecf9d5Sandi
108307738714SAndreas Gohr        $address = $this->_xmlEntities($address);
108400a7b5adSEsther Brunner        $address = obfuscate($address);
108500a7b5adSEsther Brunner        $title   = $address;
10868c128049SAndreas Gohr
108771352defSandi        if(empty($name)) {
108800a7b5adSEsther Brunner            $name = $address;
108971352defSandi        }
10900cecf9d5Sandi
1091776b36ecSAndreas Gohr        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
1092776b36ecSAndreas Gohr
1093776b36ecSAndreas Gohr        $link['url']   = 'mailto:'.$address;
109471352defSandi        $link['name']  = $name;
109571352defSandi        $link['title'] = $title;
10960cecf9d5Sandi
109771352defSandi        //output formatted
1098122f2d46SAndreas Böhler        if($returnonly) {
1099122f2d46SAndreas Böhler            return $this->_formatLink($link);
1100122f2d46SAndreas Böhler        } else {
1101a2d649c4Sandi            $this->doc .= $this->_formatLink($link);
11020cecf9d5Sandi        }
1103122f2d46SAndreas Böhler    }
11040cecf9d5Sandi
11053dd5c225SAndreas Gohr    /**
11063dd5c225SAndreas Gohr     * Render an internal media file
11073dd5c225SAndreas Gohr     *
11083dd5c225SAndreas Gohr     * @param string $src       media ID
11093dd5c225SAndreas Gohr     * @param string $title     descriptive text
11103dd5c225SAndreas Gohr     * @param string $align     left|center|right
11113dd5c225SAndreas Gohr     * @param int    $width     width of media in pixel
11123dd5c225SAndreas Gohr     * @param int    $height    height of media in pixel
11133dd5c225SAndreas Gohr     * @param string $cache     cache|recache|nocache
11143dd5c225SAndreas Gohr     * @param string $linking   linkonly|detail|nolink
11153dd5c225SAndreas Gohr     * @param bool   $return    return HTML instead of adding to $doc
11160c4c0281SGerrit Uitslag     * @return void|string writes to doc attribute or returns html depends on $return
11173dd5c225SAndreas Gohr     */
1118de369923SAndreas Gohr    public function internalmedia($src, $title = null, $align = null, $width = null,
11193dd5c225SAndreas Gohr                           $height = null, $cache = null, $linking = null, $return = false) {
112037e34a5eSandi        global $ID;
11218f34cf3dSMichael Große        if (strpos($src, '#') !== false) {
1122ec34bb30SAndreas Gohr            list($src, $hash) = sexplode('#', $src, 2);
11238f34cf3dSMichael Große        }
11248c6be208SAndreas Gohr        $src = (new MediaResolver($ID))->resolveId($src,$this->date_at,true);
11258c6be208SAndreas Gohr        $exists = media_exists($src);
11260cecf9d5Sandi
1127d98d4540SBen Coburn        $noLink = false;
11288acb3108SAndreas Gohr        $render = ($linking == 'linkonly') ? false : true;
1129b739ff0fSPierre Spring        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
11303685f775Sandi
11313dd5c225SAndreas Gohr        list($ext, $mime) = mimetype($src, false);
1132b739ff0fSPierre Spring        if(substr($mime, 0, 5) == 'image' && $render) {
113364159a61SAndreas Gohr            $link['url'] = ml(
113464159a61SAndreas Gohr                $src,
113564159a61SAndreas Gohr                array(
113664159a61SAndreas Gohr                    'id' => $ID,
113764159a61SAndreas Gohr                    'cache' => $cache,
113864159a61SAndreas Gohr                    'rev' => $this->_getLastMediaRevisionAt($src)
113964159a61SAndreas Gohr                ),
114064159a61SAndreas Gohr                ($linking == 'direct')
114164159a61SAndreas Gohr            );
1142f50634f0SAnika Henke        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
11432a2a2ba2SAnika Henke            // don't link movies
114444881bd0Shenning.noren            $noLink = true;
114555efc227SAndreas Gohr        } else {
11462ca14335SEsther Brunner            // add file icons
11479d2ddea4SAndreas Gohr            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
11489d2ddea4SAndreas Gohr            $link['class'] .= ' mediafile mf_'.$class;
114964159a61SAndreas Gohr            $link['url'] = ml(
115064159a61SAndreas Gohr                $src,
115164159a61SAndreas Gohr                array(
115264159a61SAndreas Gohr                    'id' => $ID,
115364159a61SAndreas Gohr                    'cache' => $cache,
115464159a61SAndreas Gohr                    'rev' => $this->_getLastMediaRevisionAt($src)
115564159a61SAndreas Gohr                ),
115664159a61SAndreas Gohr                true
115764159a61SAndreas Gohr            );
115891328684SMichael Hamann            if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')';
115955efc227SAndreas Gohr        }
11603685f775Sandi
11618f34cf3dSMichael Große        if (!empty($hash)) $link['url'] .= '#'.$hash;
116291df343aSAndreas Gohr
11636fe20453SGina Haeussge        //markup non existing files
11644a24b459SKate Arzamastseva        if(!$exists) {
11656fe20453SGina Haeussge            $link['class'] .= ' wikilink2';
11664a24b459SKate Arzamastseva        }
11676fe20453SGina Haeussge
11683685f775Sandi        //output formatted
1169f50634f0SAnika Henke        if($return) {
1170f50634f0SAnika Henke            if($linking == 'nolink' || $noLink) return $link['name'];
1171f50634f0SAnika Henke            else return $this->_formatLink($link);
1172f50634f0SAnika Henke        } else {
1173dc673a5bSjoe.lapp            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
11742ca14335SEsther Brunner            else $this->doc .= $this->_formatLink($link);
11750cecf9d5Sandi        }
1176f50634f0SAnika Henke    }
11770cecf9d5Sandi
11783dd5c225SAndreas Gohr    /**
11793dd5c225SAndreas Gohr     * Render an external media file
11803dd5c225SAndreas Gohr     *
11813dd5c225SAndreas Gohr     * @param string $src     full media URL
11823dd5c225SAndreas Gohr     * @param string $title   descriptive text
11833dd5c225SAndreas Gohr     * @param string $align   left|center|right
11843dd5c225SAndreas Gohr     * @param int    $width   width of media in pixel
11853dd5c225SAndreas Gohr     * @param int    $height  height of media in pixel
11863dd5c225SAndreas Gohr     * @param string $cache   cache|recache|nocache
11873dd5c225SAndreas Gohr     * @param string $linking linkonly|detail|nolink
1188410ee62aSAnika Henke     * @param bool   $return  return HTML instead of adding to $doc
11890c4c0281SGerrit Uitslag     * @return void|string writes to doc attribute or returns html depends on $return
11903dd5c225SAndreas Gohr     */
1191de369923SAndreas Gohr    public function externalmedia($src, $title = null, $align = null, $width = null,
1192410ee62aSAnika Henke                           $height = null, $cache = null, $linking = null, $return = false) {
11936efc45a2SDmitry Katsubo        if(link_isinterwiki($src)){
1194ec34bb30SAndreas Gohr            list($shortcut, $reference) = sexplode('>', $src, 2, '');
11956efc45a2SDmitry Katsubo            $exists = null;
11966efc45a2SDmitry Katsubo            $src = $this->_resolveInterWiki($shortcut, $reference, $exists);
1197abde5980SPhy            if($src == '' && empty($title)){
1198abde5980SPhy                // make sure at least something will be shown in this case
1199abde5980SPhy                $title = $reference;
1200abde5980SPhy            }
12016efc45a2SDmitry Katsubo        }
1202ec34bb30SAndreas Gohr        list($src, $hash) = sexplode('#', $src, 2);
1203d98d4540SBen Coburn        $noLink = false;
1204abde5980SPhy        if($src == '') {
1205abde5980SPhy            // only output plaintext without link if there is no src
1206abde5980SPhy            $noLink = true;
1207abde5980SPhy        }
12088acb3108SAndreas Gohr        $render = ($linking == 'linkonly') ? false : true;
1209b739ff0fSPierre Spring        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1210b739ff0fSPierre Spring
1211b739ff0fSPierre Spring        $link['url'] = ml($src, array('cache' => $cache));
12123685f775Sandi
12133dd5c225SAndreas Gohr        list($ext, $mime) = mimetype($src, false);
1214b739ff0fSPierre Spring        if(substr($mime, 0, 5) == 'image' && $render) {
12152ca14335SEsther Brunner            // link only jpeg images
121644881bd0Shenning.noren            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
1217f50634f0SAnika Henke        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
12182a2a2ba2SAnika Henke            // don't link movies
121944881bd0Shenning.noren            $noLink = true;
12202ca14335SEsther Brunner        } else {
12212ca14335SEsther Brunner            // add file icons
122227bf7924STom N Harris            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
122327bf7924STom N Harris            $link['class'] .= ' mediafile mf_'.$class;
12242ca14335SEsther Brunner        }
12252ca14335SEsther Brunner
122691df343aSAndreas Gohr        if($hash) $link['url'] .= '#'.$hash;
122791df343aSAndreas Gohr
12283685f775Sandi        //output formatted
1229410ee62aSAnika Henke        if($return) {
1230410ee62aSAnika Henke            if($linking == 'nolink' || $noLink) return $link['name'];
1231410ee62aSAnika Henke            else return $this->_formatLink($link);
1232410ee62aSAnika Henke        } else {
1233dc673a5bSjoe.lapp            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
12342ca14335SEsther Brunner            else $this->doc .= $this->_formatLink($link);
12350cecf9d5Sandi        }
1236410ee62aSAnika Henke    }
12370cecf9d5Sandi
12384826ab45Sandi    /**
12393db95becSAndreas Gohr     * Renders an RSS feed
1240b625487dSandi     *
12410c4c0281SGerrit Uitslag     * @param string $url    URL of the feed
12420c4c0281SGerrit Uitslag     * @param array  $params Finetuning of the output
12430c4c0281SGerrit Uitslag     *
1244b625487dSandi     * @author Andreas Gohr <andi@splitbrain.org>
1245b625487dSandi     */
1246de369923SAndreas Gohr    public function rss($url, $params) {
1247b625487dSandi        global $lang;
12483db95becSAndreas Gohr        global $conf;
12493db95becSAndreas Gohr
12503db95becSAndreas Gohr        require_once(DOKU_INC.'inc/FeedParser.php');
12513db95becSAndreas Gohr        $feed = new FeedParser();
125200077af8SAndreas Gohr        $feed->set_feed_url($url);
1253b625487dSandi
1254b625487dSandi        //disable warning while fetching
12553dd5c225SAndreas Gohr        if(!defined('DOKU_E_LEVEL')) {
12563dd5c225SAndreas Gohr            $elvl = error_reporting(E_ERROR);
12573dd5c225SAndreas Gohr        }
12583db95becSAndreas Gohr        $rc = $feed->init();
12593dd5c225SAndreas Gohr        if(isset($elvl)) {
12603dd5c225SAndreas Gohr            error_reporting($elvl);
12613dd5c225SAndreas Gohr        }
1262b625487dSandi
126338c6f603SRobin H. Johnson        if($params['nosort']) $feed->enable_order_by_date(false);
126438c6f603SRobin H. Johnson
12653db95becSAndreas Gohr        //decide on start and end
12663db95becSAndreas Gohr        if($params['reverse']) {
12673db95becSAndreas Gohr            $mod   = -1;
12683db95becSAndreas Gohr            $start = $feed->get_item_quantity() - 1;
12693db95becSAndreas Gohr            $end   = $start - ($params['max']);
1270b2a412b0SAndreas Gohr            $end   = ($end < -1) ? -1 : $end;
12713db95becSAndreas Gohr        } else {
12723db95becSAndreas Gohr            $mod   = 1;
12733db95becSAndreas Gohr            $start = 0;
12743db95becSAndreas Gohr            $end   = $feed->get_item_quantity();
1275d91ab76fSMatt Perry            $end   = ($end > $params['max']) ? $params['max'] : $end;
12763db95becSAndreas Gohr        }
12773db95becSAndreas Gohr
1278a2d649c4Sandi        $this->doc .= '<ul class="rss">';
12793db95becSAndreas Gohr        if($rc) {
12803db95becSAndreas Gohr            for($x = $start; $x != $end; $x += $mod) {
12811bde1582SAndreas Gohr                $item = $feed->get_item($x);
12823db95becSAndreas Gohr                $this->doc .= '<li><div class="li">';
1283*53df38b0SAndreas Gohr
1284d2ea3363SAndreas Gohr                $lnkurl = $item->get_permalink();
1285*53df38b0SAndreas Gohr                $title = html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8');
1286*53df38b0SAndreas Gohr
1287*53df38b0SAndreas Gohr                // support feeds without links
1288d2ea3363SAndreas Gohr                if($lnkurl) {
1289*53df38b0SAndreas Gohr                    $this->externallink($item->get_permalink(), $title);
1290d2ea3363SAndreas Gohr                } else {
1291*53df38b0SAndreas Gohr                    $this->doc .= ' '.hsc($item->get_title());
1292d2ea3363SAndreas Gohr                }
12933db95becSAndreas Gohr                if($params['author']) {
12941bde1582SAndreas Gohr                    $author = $item->get_author(0);
12951bde1582SAndreas Gohr                    if($author) {
12961bde1582SAndreas Gohr                        $name = $author->get_name();
12971bde1582SAndreas Gohr                        if(!$name) $name = $author->get_email();
1298163c2842SPhy                        if($name) $this->doc .= ' '.$lang['by'].' '.hsc($name);
12991bde1582SAndreas Gohr                    }
13003db95becSAndreas Gohr                }
13013db95becSAndreas Gohr                if($params['date']) {
13022e7e0c29SAndreas Gohr                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
13033db95becSAndreas Gohr                }
13041bde1582SAndreas Gohr                if($params['details']) {
1305*53df38b0SAndreas Gohr                    $desc = $item->get_description();
1306*53df38b0SAndreas Gohr                    $desc = strip_tags($desc);
1307*53df38b0SAndreas Gohr                    $desc = html_entity_decode($desc, ENT_QUOTES, 'UTF-8');
13083db95becSAndreas Gohr                    $this->doc .= '<div class="detail">';
1309*53df38b0SAndreas Gohr                    $this->doc .= hsc($desc);
13103db95becSAndreas Gohr                    $this->doc .= '</div>';
13113db95becSAndreas Gohr                }
13123db95becSAndreas Gohr
13133db95becSAndreas Gohr                $this->doc .= '</div></li>';
1314b625487dSandi            }
1315b625487dSandi        } else {
13163db95becSAndreas Gohr            $this->doc .= '<li><div class="li">';
1317a2d649c4Sandi            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
1318b625487dSandi            $this->externallink($url);
131945e147ccSAndreas Gohr            if($conf['allowdebug']) {
132045e147ccSAndreas Gohr                $this->doc .= '<!--'.hsc($feed->error).'-->';
132145e147ccSAndreas Gohr            }
13223db95becSAndreas Gohr            $this->doc .= '</div></li>';
1323b625487dSandi        }
1324a2d649c4Sandi        $this->doc .= '</ul>';
1325b625487dSandi    }
1326b625487dSandi
13273dd5c225SAndreas Gohr    /**
13283dd5c225SAndreas Gohr     * Start a table
13293dd5c225SAndreas Gohr     *
13303dd5c225SAndreas Gohr     * @param int $maxcols maximum number of columns
13313dd5c225SAndreas Gohr     * @param int $numrows NOT IMPLEMENTED
13323dd5c225SAndreas Gohr     * @param int $pos byte position in the original source
13337d769f75SAndreas Gohr     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
13343dd5c225SAndreas Gohr     */
1335de369923SAndreas Gohr    public function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) {
1336b5742cedSPierre Spring        // initialize the row counter used for classes
1337b5742cedSPierre Spring        $this->_counter['row_counter'] = 0;
1338619736fdSAdrian Lang        $class                         = 'table';
13390c4c0281SGerrit Uitslag        if($classes !== null) {
13402e0ebe60SAndreas Gohr            if(is_array($classes)) $classes = join(' ', $classes);
13410c4c0281SGerrit Uitslag            $class .= ' ' . $classes;
13420c4c0281SGerrit Uitslag        }
1343619736fdSAdrian Lang        if($pos !== null) {
134406917fceSMichael Große            $hid = $this->_headerToLink($class, true);
1345ec57f119SLarsDW223            $data = array();
1346ec57f119SLarsDW223            $data['target'] = 'table';
1347ec57f119SLarsDW223            $data['name'] = '';
1348ec57f119SLarsDW223            $data['hid'] = $hid;
1349ec57f119SLarsDW223            $class .= ' '.$this->startSectionEdit($pos, $data);
1350619736fdSAdrian Lang        }
1351619736fdSAdrian Lang        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1352619736fdSAdrian Lang            DOKU_LF;
13530cecf9d5Sandi    }
13540cecf9d5Sandi
13553dd5c225SAndreas Gohr    /**
13563dd5c225SAndreas Gohr     * Close a table
13573dd5c225SAndreas Gohr     *
13583dd5c225SAndreas Gohr     * @param int $pos byte position in the original source
13593dd5c225SAndreas Gohr     */
1360de369923SAndreas Gohr    public function table_close($pos = null) {
1361a8574918SAnika Henke        $this->doc .= '</table></div>'.DOKU_LF;
1362619736fdSAdrian Lang        if($pos !== null) {
136390df9a4dSAdrian Lang            $this->finishSectionEdit($pos);
13640cecf9d5Sandi        }
1365619736fdSAdrian Lang    }
13660cecf9d5Sandi
13673dd5c225SAndreas Gohr    /**
13683dd5c225SAndreas Gohr     * Open a table header
13693dd5c225SAndreas Gohr     */
1370de369923SAndreas Gohr    public function tablethead_open() {
1371f05a1cc5SGerrit Uitslag        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1372f05a1cc5SGerrit Uitslag    }
1373f05a1cc5SGerrit Uitslag
13743dd5c225SAndreas Gohr    /**
13753dd5c225SAndreas Gohr     * Close a table header
13763dd5c225SAndreas Gohr     */
1377de369923SAndreas Gohr    public function tablethead_close() {
1378f05a1cc5SGerrit Uitslag        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1379f05a1cc5SGerrit Uitslag    }
1380f05a1cc5SGerrit Uitslag
13813dd5c225SAndreas Gohr    /**
13825a93f869SAnika Henke     * Open a table body
13835a93f869SAnika Henke     */
1384de369923SAndreas Gohr    public function tabletbody_open() {
13855a93f869SAnika Henke        $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF;
13865a93f869SAnika Henke    }
13875a93f869SAnika Henke
13885a93f869SAnika Henke    /**
13895a93f869SAnika Henke     * Close a table body
13905a93f869SAnika Henke     */
1391de369923SAndreas Gohr    public function tabletbody_close() {
13925a93f869SAnika Henke        $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF;
13935a93f869SAnika Henke    }
13945a93f869SAnika Henke
13955a93f869SAnika Henke    /**
1396d2a99739SAndreas Gohr     * Open a table footer
1397d2a99739SAndreas Gohr     */
1398de369923SAndreas Gohr    public function tabletfoot_open() {
139944f5d1c1SAndreas Gohr        $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF;
1400d2a99739SAndreas Gohr    }
1401d2a99739SAndreas Gohr
1402d2a99739SAndreas Gohr    /**
1403d2a99739SAndreas Gohr     * Close a table footer
1404d2a99739SAndreas Gohr     */
1405de369923SAndreas Gohr    public function tabletfoot_close() {
140644f5d1c1SAndreas Gohr        $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF;
1407d2a99739SAndreas Gohr    }
1408d2a99739SAndreas Gohr
1409d2a99739SAndreas Gohr    /**
14103dd5c225SAndreas Gohr     * Open a table row
14110c4c0281SGerrit Uitslag     *
14127d769f75SAndreas Gohr     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
14133dd5c225SAndreas Gohr     */
1414de369923SAndreas Gohr    public function tablerow_open($classes = null) {
1415b5742cedSPierre Spring        // initialize the cell counter used for classes
1416b5742cedSPierre Spring        $this->_counter['cell_counter'] = 0;
1417b5742cedSPierre Spring        $class                          = 'row'.$this->_counter['row_counter']++;
14180c4c0281SGerrit Uitslag        if($classes !== null) {
14192e0ebe60SAndreas Gohr            if(is_array($classes)) $classes = join(' ', $classes);
14200c4c0281SGerrit Uitslag            $class .= ' ' . $classes;
14210c4c0281SGerrit Uitslag        }
1422b5742cedSPierre Spring        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
14230cecf9d5Sandi    }
14240cecf9d5Sandi
14253dd5c225SAndreas Gohr    /**
14263dd5c225SAndreas Gohr     * Close a table row
14273dd5c225SAndreas Gohr     */
1428de369923SAndreas Gohr    public function tablerow_close() {
1429a2d649c4Sandi        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
14300cecf9d5Sandi    }
14310cecf9d5Sandi
14323dd5c225SAndreas Gohr    /**
14333dd5c225SAndreas Gohr     * Open a table header cell
14343dd5c225SAndreas Gohr     *
14353dd5c225SAndreas Gohr     * @param int    $colspan
14363dd5c225SAndreas Gohr     * @param string $align left|center|right
14373dd5c225SAndreas Gohr     * @param int    $rowspan
14387d769f75SAndreas Gohr     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
14393dd5c225SAndreas Gohr     */
1440de369923SAndreas Gohr    public function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1441b5742cedSPierre Spring        $class = 'class="col'.$this->_counter['cell_counter']++;
14420cecf9d5Sandi        if(!is_null($align)) {
1443b5742cedSPierre Spring            $class .= ' '.$align.'align';
14440cecf9d5Sandi        }
14450c4c0281SGerrit Uitslag        if($classes !== null) {
14462e0ebe60SAndreas Gohr            if(is_array($classes)) $classes = join(' ', $classes);
14470c4c0281SGerrit Uitslag            $class .= ' ' . $classes;
14480c4c0281SGerrit Uitslag        }
1449b5742cedSPierre Spring        $class .= '"';
1450b5742cedSPierre Spring        $this->doc .= '<th '.$class;
14510cecf9d5Sandi        if($colspan > 1) {
1452a28fd914SAndreas Gohr            $this->_counter['cell_counter'] += $colspan - 1;
1453a2d649c4Sandi            $this->doc .= ' colspan="'.$colspan.'"';
14540cecf9d5Sandi        }
145525b97867Shakan.sandell        if($rowspan > 1) {
145625b97867Shakan.sandell            $this->doc .= ' rowspan="'.$rowspan.'"';
145725b97867Shakan.sandell        }
1458a2d649c4Sandi        $this->doc .= '>';
14590cecf9d5Sandi    }
14600cecf9d5Sandi
14613dd5c225SAndreas Gohr    /**
14623dd5c225SAndreas Gohr     * Close a table header cell
14633dd5c225SAndreas Gohr     */
1464de369923SAndreas Gohr    public function tableheader_close() {
1465a2d649c4Sandi        $this->doc .= '</th>';
14660cecf9d5Sandi    }
14670cecf9d5Sandi
14683dd5c225SAndreas Gohr    /**
14693dd5c225SAndreas Gohr     * Open a table cell
14703dd5c225SAndreas Gohr     *
14713dd5c225SAndreas Gohr     * @param int       $colspan
14723dd5c225SAndreas Gohr     * @param string    $align left|center|right
14733dd5c225SAndreas Gohr     * @param int       $rowspan
14747d769f75SAndreas Gohr     * @param string|string[]    $classes css classes - have to be valid, do not pass unfiltered user input
14753dd5c225SAndreas Gohr     */
1476de369923SAndreas Gohr    public function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1477b5742cedSPierre Spring        $class = 'class="col'.$this->_counter['cell_counter']++;
14780cecf9d5Sandi        if(!is_null($align)) {
1479b5742cedSPierre Spring            $class .= ' '.$align.'align';
14800cecf9d5Sandi        }
14810c4c0281SGerrit Uitslag        if($classes !== null) {
14822e0ebe60SAndreas Gohr            if(is_array($classes)) $classes = join(' ', $classes);
14830c4c0281SGerrit Uitslag            $class .= ' ' . $classes;
14840c4c0281SGerrit Uitslag        }
1485b5742cedSPierre Spring        $class .= '"';
1486b5742cedSPierre Spring        $this->doc .= '<td '.$class;
14870cecf9d5Sandi        if($colspan > 1) {
1488a28fd914SAndreas Gohr            $this->_counter['cell_counter'] += $colspan - 1;
1489a2d649c4Sandi            $this->doc .= ' colspan="'.$colspan.'"';
14900cecf9d5Sandi        }
149125b97867Shakan.sandell        if($rowspan > 1) {
149225b97867Shakan.sandell            $this->doc .= ' rowspan="'.$rowspan.'"';
149325b97867Shakan.sandell        }
1494a2d649c4Sandi        $this->doc .= '>';
14950cecf9d5Sandi    }
14960cecf9d5Sandi
14973dd5c225SAndreas Gohr    /**
14983dd5c225SAndreas Gohr     * Close a table cell
14993dd5c225SAndreas Gohr     */
1500de369923SAndreas Gohr    public function tablecell_close() {
1501a2d649c4Sandi        $this->doc .= '</td>';
15020cecf9d5Sandi    }
15030cecf9d5Sandi
1504cea664bdSLarsDW223    /**
1505cea664bdSLarsDW223     * Returns the current header level.
1506cea664bdSLarsDW223     * (required e.g. by the filelist plugin)
1507cea664bdSLarsDW223     *
1508cea664bdSLarsDW223     * @return int The current header level
1509cea664bdSLarsDW223     */
1510de369923SAndreas Gohr    public function getLastlevel() {
1511cea664bdSLarsDW223        return $this->lastlevel;
1512cea664bdSLarsDW223    }
1513cea664bdSLarsDW223
15143dd5c225SAndreas Gohr    #region Utility functions
15150cecf9d5Sandi
1516ba11bd29Sandi    /**
15173fd0b676Sandi     * Build a link
15183fd0b676Sandi     *
15193fd0b676Sandi     * Assembles all parts defined in $link returns HTML for the link
1520ba11bd29Sandi     *
15210c4c0281SGerrit Uitslag     * @param array $link attributes of a link
15220c4c0281SGerrit Uitslag     * @return string
15230c4c0281SGerrit Uitslag     *
1524ba11bd29Sandi     * @author Andreas Gohr <andi@splitbrain.org>
1525ba11bd29Sandi     */
1526de369923SAndreas Gohr    public function _formatLink($link) {
1527ba11bd29Sandi        //make sure the url is XHTML compliant (skip mailto)
1528ba11bd29Sandi        if(substr($link['url'], 0, 7) != 'mailto:') {
1529ba11bd29Sandi            $link['url'] = str_replace('&', '&amp;', $link['url']);
1530ba11bd29Sandi            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1531ba11bd29Sandi        }
1532ba11bd29Sandi        //remove double encodings in titles
1533ba11bd29Sandi        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1534ba11bd29Sandi
1535453493f2SAndreas Gohr        // be sure there are no bad chars in url or title
1536453493f2SAndreas Gohr        // (we can't do this for name because it can contain an img tag)
1537453493f2SAndreas Gohr        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1538453493f2SAndreas Gohr        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1539453493f2SAndreas Gohr
1540ba11bd29Sandi        $ret = '';
1541ba11bd29Sandi        $ret .= $link['pre'];
1542ba11bd29Sandi        $ret .= '<a href="'.$link['url'].'"';
1543bb4866bdSchris        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1544bb4866bdSchris        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1545bb4866bdSchris        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1546bb4866bdSchris        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1547914045f3SAndreas Gohr        if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"';
1548bb4866bdSchris        if(!empty($link['more'])) $ret .= ' '.$link['more'];
1549ba11bd29Sandi        $ret .= '>';
1550ba11bd29Sandi        $ret .= $link['name'];
1551ba11bd29Sandi        $ret .= '</a>';
1552ba11bd29Sandi        $ret .= $link['suf'];
1553ba11bd29Sandi        return $ret;
1554ba11bd29Sandi    }
1555ba11bd29Sandi
1556ba11bd29Sandi    /**
15573fd0b676Sandi     * Renders internal and external media
15583fd0b676Sandi     *
15593fd0b676Sandi     * @author Andreas Gohr <andi@splitbrain.org>
15603dd5c225SAndreas Gohr     * @param string $src       media ID
15613dd5c225SAndreas Gohr     * @param string $title     descriptive text
15623dd5c225SAndreas Gohr     * @param string $align     left|center|right
15633dd5c225SAndreas Gohr     * @param int    $width     width of media in pixel
15643dd5c225SAndreas Gohr     * @param int    $height    height of media in pixel
15653dd5c225SAndreas Gohr     * @param string $cache     cache|recache|nocache
15663dd5c225SAndreas Gohr     * @param bool   $render    should the media be embedded inline or just linked
15673dd5c225SAndreas Gohr     * @return string
15683fd0b676Sandi     */
1569de369923SAndreas Gohr    public function _media($src, $title = null, $align = null, $width = null,
15700ea51e63SMatt Perry                    $height = null, $cache = null, $render = true) {
15713fd0b676Sandi
15723fd0b676Sandi        $ret = '';
15733fd0b676Sandi
15743dd5c225SAndreas Gohr        list($ext, $mime) = mimetype($src);
15753fd0b676Sandi        if(substr($mime, 0, 5) == 'image') {
1576b739ff0fSPierre Spring            // first get the $title
1577b739ff0fSPierre Spring            if(!is_null($title)) {
1578b739ff0fSPierre Spring                $title = $this->_xmlEntities($title);
1579b739ff0fSPierre Spring            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1580b739ff0fSPierre Spring                //try to use the caption from IPTC/EXIF
1581b739ff0fSPierre Spring                require_once(DOKU_INC.'inc/JpegMeta.php');
158267f9913dSAndreas Gohr                $jpeg = new JpegMeta(mediaFN($src));
1583b739ff0fSPierre Spring                if($jpeg !== false) $cap = $jpeg->getTitle();
15843dd5c225SAndreas Gohr                if(!empty($cap)) {
1585b739ff0fSPierre Spring                    $title = $this->_xmlEntities($cap);
1586b739ff0fSPierre Spring                }
1587b739ff0fSPierre Spring            }
1588b739ff0fSPierre Spring            if(!$render) {
1589b739ff0fSPierre Spring                // if the picture is not supposed to be rendered
1590b739ff0fSPierre Spring                // return the title of the picture
1591768be5a3SPhy                if($title === null || $title === "") {
1592b739ff0fSPierre Spring                    // just show the sourcename
15938cbc5ee8SAndreas Gohr                    $title = $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
1594b739ff0fSPierre Spring                }
1595b739ff0fSPierre Spring                return $title;
1596b739ff0fSPierre Spring            }
15973fd0b676Sandi            //add image tag
159864159a61SAndreas Gohr            $ret .= '<img src="' . ml(
159964159a61SAndreas Gohr                    $src,
160064159a61SAndreas Gohr                    array(
160164159a61SAndreas Gohr                        'w' => $width, 'h' => $height,
160264159a61SAndreas Gohr                        'cache' => $cache,
160364159a61SAndreas Gohr                        'rev' => $this->_getLastMediaRevisionAt($src)
160464159a61SAndreas Gohr                    )
160564159a61SAndreas Gohr                ) . '"';
16063fd0b676Sandi            $ret .= ' class="media'.$align.'"';
16074732b197SAndreas Gohr            $ret .= ' loading="lazy"';
16083fd0b676Sandi
1609b739ff0fSPierre Spring            if($title) {
1610b739ff0fSPierre Spring                $ret .= ' title="'.$title.'"';
1611b739ff0fSPierre Spring                $ret .= ' alt="'.$title.'"';
16123fd0b676Sandi            } else {
16133fd0b676Sandi                $ret .= ' alt=""';
16143fd0b676Sandi            }
16153fd0b676Sandi
16163fd0b676Sandi            if(!is_null($width))
16173fd0b676Sandi                $ret .= ' width="'.$this->_xmlEntities($width).'"';
16183fd0b676Sandi
16193fd0b676Sandi            if(!is_null($height))
16203fd0b676Sandi                $ret .= ' height="'.$this->_xmlEntities($height).'"';
16213fd0b676Sandi
16223fd0b676Sandi            $ret .= ' />';
16233fd0b676Sandi
162417954bb5SAnika Henke        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
16252a2a2ba2SAnika Henke            // first get the $title
1626a8dcb874Sbleistivt            $title = !is_null($title) ? $title : false;
16272a2a2ba2SAnika Henke            if(!$render) {
162817954bb5SAnika Henke                // if the file is not supposed to be rendered
162917954bb5SAnika Henke                // return the title of the file (just the sourcename if there is no title)
1630a8dcb874Sbleistivt                return $this->_xmlEntities($title ? $title : \dokuwiki\Utf8\PhpString::basename(noNS($src)));
16312a2a2ba2SAnika Henke            }
16322a2a2ba2SAnika Henke
16332a2a2ba2SAnika Henke            $att          = array();
16342a2a2ba2SAnika Henke            $att['class'] = "media$align";
163517954bb5SAnika Henke            if($title) {
163617954bb5SAnika Henke                $att['title'] = $title;
163717954bb5SAnika Henke            }
16382a2a2ba2SAnika Henke
163917954bb5SAnika Henke            if(media_supportedav($mime, 'video')) {
164017954bb5SAnika Henke                //add video
164179e53fe5SAnika Henke                $ret .= $this->_video($src, $width, $height, $att);
1642b44a5dceSAnika Henke            }
164317954bb5SAnika Henke            if(media_supportedav($mime, 'audio')) {
1644b44a5dceSAnika Henke                //add audio
1645b44a5dceSAnika Henke                $ret .= $this->_audio($src, $att);
164617954bb5SAnika Henke            }
1647b44a5dceSAnika Henke
16483fd0b676Sandi        } elseif($mime == 'application/x-shockwave-flash') {
16491c882ba8SAndreas Gohr            if(!$render) {
16501c882ba8SAndreas Gohr                // if the flash is not supposed to be rendered
16511c882ba8SAndreas Gohr                // return the title of the flash
16521c882ba8SAndreas Gohr                if(!$title) {
16531c882ba8SAndreas Gohr                    // just show the sourcename
16548cbc5ee8SAndreas Gohr                    $title = \dokuwiki\Utf8\PhpString::basename(noNS($src));
16551c882ba8SAndreas Gohr                }
165607bf32b2SAndreas Gohr                return $this->_xmlEntities($title);
16571c882ba8SAndreas Gohr            }
16581c882ba8SAndreas Gohr
165907bf32b2SAndreas Gohr            $att          = array();
166007bf32b2SAndreas Gohr            $att['class'] = "media$align";
166107bf32b2SAndreas Gohr            if($align == 'right') $att['align'] = 'right';
166207bf32b2SAndreas Gohr            if($align == 'left') $att['align'] = 'left';
16633dd5c225SAndreas Gohr            $ret .= html_flashobject(
16643dd5c225SAndreas Gohr                ml($src, array('cache' => $cache), true, '&'), $width, $height,
166507bf32b2SAndreas Gohr                array('quality' => 'high'),
166607bf32b2SAndreas Gohr                null,
166707bf32b2SAndreas Gohr                $att,
16683dd5c225SAndreas Gohr                $this->_xmlEntities($title)
16693dd5c225SAndreas Gohr            );
16700f428d7dSAndreas Gohr        } elseif($title) {
16713fd0b676Sandi            // well at least we have a title to display
16723fd0b676Sandi            $ret .= $this->_xmlEntities($title);
16733fd0b676Sandi        } else {
16745291ca3aSAndreas Gohr            // just show the sourcename
16758cbc5ee8SAndreas Gohr            $ret .= $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
16763fd0b676Sandi        }
16773fd0b676Sandi
16783fd0b676Sandi        return $ret;
16793fd0b676Sandi    }
16803fd0b676Sandi
16813dd5c225SAndreas Gohr    /**
16823dd5c225SAndreas Gohr     * Escape string for output
16833dd5c225SAndreas Gohr     *
16843dd5c225SAndreas Gohr     * @param $string
16853dd5c225SAndreas Gohr     * @return string
16863dd5c225SAndreas Gohr     */
1687de369923SAndreas Gohr    public function _xmlEntities($string) {
1688f7711f2bSAndreas Gohr        return hsc($string);
16890cecf9d5Sandi    }
16900cecf9d5Sandi
1691de369923SAndreas Gohr
16920cecf9d5Sandi
1693af587fa8Sandi    /**
16943fd0b676Sandi     * Construct a title and handle images in titles
16953fd0b676Sandi     *
16960b7c14c2Sandi     * @author Harry Fuecks <hfuecks@gmail.com>
16973dd5c225SAndreas Gohr     * @param string|array $title    either string title or media array
16983dd5c225SAndreas Gohr     * @param string       $default  default title if nothing else is found
16993dd5c225SAndreas Gohr     * @param bool         $isImage  will be set to true if it's a media file
17003dd5c225SAndreas Gohr     * @param null|string  $id       linked page id (used to extract title from first heading)
17013dd5c225SAndreas Gohr     * @param string       $linktype content|navigation
17023dd5c225SAndreas Gohr     * @return string      HTML of the title, might be full image tag or just escaped text
17033fd0b676Sandi     */
1704de369923SAndreas Gohr    public function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
170544881bd0Shenning.noren        $isImage = false;
170629657f9eSAndreas Gohr        if(is_array($title)) {
170729657f9eSAndreas Gohr            $isImage = true;
170829657f9eSAndreas Gohr            return $this->_imageTitle($title);
170929657f9eSAndreas Gohr        } elseif(is_null($title) || trim($title) == '') {
1710fe9ec250SChris Smith            if(useHeading($linktype) && $id) {
171167c15eceSMichael Hamann                $heading = p_get_first_heading($id);
1712f515db7fSAndreas Gohr                if(!blank($heading)) {
1713433bef32Sandi                    return $this->_xmlEntities($heading);
1714bb0a59d4Sjan                }
1715bb0a59d4Sjan            }
1716433bef32Sandi            return $this->_xmlEntities($default);
171768c26e6dSMichael Klier        } else {
171868c26e6dSMichael Klier            return $this->_xmlEntities($title);
17190cecf9d5Sandi        }
17200cecf9d5Sandi    }
17210cecf9d5Sandi
17220cecf9d5Sandi    /**
17233dd5c225SAndreas Gohr     * Returns HTML code for images used in link titles
17243fd0b676Sandi     *
17253fd0b676Sandi     * @author Andreas Gohr <andi@splitbrain.org>
1726e0c26282SGerrit Uitslag     * @param array $img
17273dd5c225SAndreas Gohr     * @return string HTML img tag or similar
17280cecf9d5Sandi     */
1729de369923SAndreas Gohr    public function _imageTitle($img) {
1730d9baf1a7SKazutaka Miyasaka        global $ID;
1731d9baf1a7SKazutaka Miyasaka
1732d9baf1a7SKazutaka Miyasaka        // some fixes on $img['src']
1733d9baf1a7SKazutaka Miyasaka        // see internalmedia() and externalmedia()
17343dd5c225SAndreas Gohr        list($img['src']) = explode('#', $img['src'], 2);
1735d9baf1a7SKazutaka Miyasaka        if($img['type'] == 'internalmedia') {
17368c6be208SAndreas Gohr            $img['src'] = (new MediaResolver($ID))->resolveId($img['src'], $this->date_at, true);
1737d9baf1a7SKazutaka Miyasaka        }
1738d9baf1a7SKazutaka Miyasaka
17393dd5c225SAndreas Gohr        return $this->_media(
17403dd5c225SAndreas Gohr            $img['src'],
17414826ab45Sandi            $img['title'],
17424826ab45Sandi            $img['align'],
17434826ab45Sandi            $img['width'],
17444826ab45Sandi            $img['height'],
17453dd5c225SAndreas Gohr            $img['cache']
17463dd5c225SAndreas Gohr        );
17470cecf9d5Sandi    }
1748b739ff0fSPierre Spring
1749b739ff0fSPierre Spring    /**
17503dd5c225SAndreas Gohr     * helperfunction to return a basic link to a media
17513dd5c225SAndreas Gohr     *
17523dd5c225SAndreas Gohr     * used in internalmedia() and externalmedia()
1753b739ff0fSPierre Spring     *
1754b739ff0fSPierre Spring     * @author   Pierre Spring <pierre.spring@liip.ch>
17553dd5c225SAndreas Gohr     * @param string $src       media ID
17563dd5c225SAndreas Gohr     * @param string $title     descriptive text
17573dd5c225SAndreas Gohr     * @param string $align     left|center|right
17583dd5c225SAndreas Gohr     * @param int    $width     width of media in pixel
17593dd5c225SAndreas Gohr     * @param int    $height    height of media in pixel
17603dd5c225SAndreas Gohr     * @param string $cache     cache|recache|nocache
17613dd5c225SAndreas Gohr     * @param bool   $render    should the media be embedded inline or just linked
17623dd5c225SAndreas Gohr     * @return array associative array with link config
1763b739ff0fSPierre Spring     */
1764de369923SAndreas Gohr    public function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1765b739ff0fSPierre Spring        global $conf;
1766b739ff0fSPierre Spring
1767b739ff0fSPierre Spring        $link           = array();
1768b739ff0fSPierre Spring        $link['class']  = 'media';
1769b739ff0fSPierre Spring        $link['style']  = '';
1770b739ff0fSPierre Spring        $link['pre']    = '';
1771b739ff0fSPierre Spring        $link['suf']    = '';
1772b739ff0fSPierre Spring        $link['more']   = '';
1773b739ff0fSPierre Spring        $link['target'] = $conf['target']['media'];
1774bc3d2252SAndreas Gohr        if($conf['target']['media']) $link['rel'] = 'noopener';
1775b739ff0fSPierre Spring        $link['title']  = $this->_xmlEntities($src);
1776b739ff0fSPierre Spring        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1777b739ff0fSPierre Spring
1778b739ff0fSPierre Spring        return $link;
1779b739ff0fSPierre Spring    }
178091459163SAnika Henke
17812a2a2ba2SAnika Henke    /**
17822a2a2ba2SAnika Henke     * Embed video(s) in HTML
17832a2a2ba2SAnika Henke     *
17842a2a2ba2SAnika Henke     * @author Anika Henke <anika@selfthinker.org>
17850877a1f1SSchplurtz le Déboulonné     * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
17862a2a2ba2SAnika Henke     *
17872a2a2ba2SAnika Henke     * @param string $src         - ID of video to embed
17882a2a2ba2SAnika Henke     * @param int    $width       - width of the video in pixels
17892a2a2ba2SAnika Henke     * @param int    $height      - height of the video in pixels
17902a2a2ba2SAnika Henke     * @param array  $atts        - additional attributes for the <video> tag
1791f50634f0SAnika Henke     * @return string
17922a2a2ba2SAnika Henke     */
1793de369923SAndreas Gohr    public function _video($src, $width, $height, $atts = null) {
17942a2a2ba2SAnika Henke        // prepare width and height
17952a2a2ba2SAnika Henke        if(is_null($atts)) $atts = array();
17962a2a2ba2SAnika Henke        $atts['width']  = (int) $width;
17972a2a2ba2SAnika Henke        $atts['height'] = (int) $height;
17982a2a2ba2SAnika Henke        if(!$atts['width']) $atts['width'] = 320;
17992a2a2ba2SAnika Henke        if(!$atts['height']) $atts['height'] = 240;
18002a2a2ba2SAnika Henke
1801410ee62aSAnika Henke        $posterUrl = '';
1802410ee62aSAnika Henke        $files = array();
18030877a1f1SSchplurtz le Déboulonné        $tracks = array();
1804410ee62aSAnika Henke        $isExternal = media_isexternal($src);
1805410ee62aSAnika Henke
1806410ee62aSAnika Henke        if ($isExternal) {
1807410ee62aSAnika Henke            // take direct source for external files
1808702e97d3SAnika Henke            list(/*ext*/, $srcMime) = mimetype($src);
1809410ee62aSAnika Henke            $files[$srcMime] = $src;
1810410ee62aSAnika Henke        } else {
18113d7a9e0aSAnika Henke            // prepare alternative formats
18123d7a9e0aSAnika Henke            $extensions   = array('webm', 'ogv', 'mp4');
1813410ee62aSAnika Henke            $files        = media_alternativefiles($src, $extensions);
181459bc3b48SGerrit Uitslag            $poster       = media_alternativefiles($src, array('jpg', 'png'));
18150877a1f1SSchplurtz le Déboulonné            $tracks       = media_trackfiles($src);
181699f943f6SAnika Henke            if(!empty($poster)) {
18172d338eabSAndreas Gohr                $posterUrl = ml(reset($poster), '', true, '&');
181899f943f6SAnika Henke            }
1819410ee62aSAnika Henke        }
18202a2a2ba2SAnika Henke
1821f50634f0SAnika Henke        $out = '';
182279e53fe5SAnika Henke        // open video tag
1823f50634f0SAnika Henke        $out .= '<video '.buildAttributes($atts).' controls="controls"';
18243641199aSAnika Henke        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1825f50634f0SAnika Henke        $out .= '>'.NL;
18263641199aSAnika Henke        $fallback = '';
182779e53fe5SAnika Henke
182879e53fe5SAnika Henke        // output source for each alternative video format
1829410ee62aSAnika Henke        foreach($files as $mime => $file) {
1830410ee62aSAnika Henke            if ($isExternal) {
1831410ee62aSAnika Henke                $url = $file;
1832410ee62aSAnika Henke                $linkType = 'externalmedia';
1833410ee62aSAnika Henke            } else {
18342d338eabSAndreas Gohr                $url = ml($file, '', true, '&');
1835410ee62aSAnika Henke                $linkType = 'internalmedia';
1836410ee62aSAnika Henke            }
1837056bf31fSDamien Regad            $title = !empty($atts['title'])
1838056bf31fSDamien Regad                ? $atts['title']
1839056bf31fSDamien Regad                : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
18403d7a9e0aSAnika Henke
1841f50634f0SAnika Henke            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
184279e53fe5SAnika Henke            // alternative content (just a link to the file)
184364159a61SAndreas Gohr            $fallback .= $this->$linkType(
184464159a61SAndreas Gohr                $file,
184564159a61SAndreas Gohr                $title,
184664159a61SAndreas Gohr                null,
184764159a61SAndreas Gohr                null,
184864159a61SAndreas Gohr                null,
184964159a61SAndreas Gohr                $cache = null,
185064159a61SAndreas Gohr                $linking = 'linkonly',
185164159a61SAndreas Gohr                $return = true
185264159a61SAndreas Gohr            );
18533d7a9e0aSAnika Henke        }
18542a2a2ba2SAnika Henke
18550877a1f1SSchplurtz le Déboulonné        // output each track if any
18560877a1f1SSchplurtz le Déboulonné        foreach( $tracks as $trackid => $info ) {
185723c61bbeSSchplurtz le Déboulonné            list( $kind, $srclang ) = array_map( 'hsc', $info );
185823c61bbeSSchplurtz le Déboulonné            $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
185923c61bbeSSchplurtz le Déboulonné            $out .= "label=\"$srclang\" ";
18600877a1f1SSchplurtz le Déboulonné            $out .= 'src="'.ml($trackid, '', true).'">'.NL;
18610877a1f1SSchplurtz le Déboulonné        }
18620877a1f1SSchplurtz le Déboulonné
18632a2a2ba2SAnika Henke        // finish
18643641199aSAnika Henke        $out .= $fallback;
1865f50634f0SAnika Henke        $out .= '</video>'.NL;
1866f50634f0SAnika Henke        return $out;
18672a2a2ba2SAnika Henke    }
18682a2a2ba2SAnika Henke
1869b44a5dceSAnika Henke    /**
1870b44a5dceSAnika Henke     * Embed audio in HTML
1871b44a5dceSAnika Henke     *
1872b44a5dceSAnika Henke     * @author Anika Henke <anika@selfthinker.org>
1873b44a5dceSAnika Henke     *
1874b44a5dceSAnika Henke     * @param string $src       - ID of audio to embed
18756d4af72aSAnika Henke     * @param array  $atts      - additional attributes for the <audio> tag
1876f50634f0SAnika Henke     * @return string
1877b44a5dceSAnika Henke     */
1878de369923SAndreas Gohr    public function _audio($src, $atts = array()) {
1879702e97d3SAnika Henke        $files = array();
1880410ee62aSAnika Henke        $isExternal = media_isexternal($src);
1881b44a5dceSAnika Henke
1882410ee62aSAnika Henke        if ($isExternal) {
1883410ee62aSAnika Henke            // take direct source for external files
1884702e97d3SAnika Henke            list(/*ext*/, $srcMime) = mimetype($src);
1885410ee62aSAnika Henke            $files[$srcMime] = $src;
1886410ee62aSAnika Henke        } else {
1887b44a5dceSAnika Henke            // prepare alternative formats
1888b44a5dceSAnika Henke            $extensions   = array('ogg', 'mp3', 'wav');
1889410ee62aSAnika Henke            $files        = media_alternativefiles($src, $extensions);
1890410ee62aSAnika Henke        }
1891b44a5dceSAnika Henke
1892f50634f0SAnika Henke        $out = '';
1893b44a5dceSAnika Henke        // open audio tag
1894f50634f0SAnika Henke        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
18953641199aSAnika Henke        $fallback = '';
1896b44a5dceSAnika Henke
1897b44a5dceSAnika Henke        // output source for each alternative audio format
1898410ee62aSAnika Henke        foreach($files as $mime => $file) {
1899410ee62aSAnika Henke            if ($isExternal) {
1900410ee62aSAnika Henke                $url = $file;
1901410ee62aSAnika Henke                $linkType = 'externalmedia';
1902410ee62aSAnika Henke            } else {
19032d338eabSAndreas Gohr                $url = ml($file, '', true, '&');
1904410ee62aSAnika Henke                $linkType = 'internalmedia';
1905410ee62aSAnika Henke            }
19068cbc5ee8SAndreas Gohr            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
1907b44a5dceSAnika Henke
1908f50634f0SAnika Henke            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1909b44a5dceSAnika Henke            // alternative content (just a link to the file)
191064159a61SAndreas Gohr            $fallback .= $this->$linkType(
191164159a61SAndreas Gohr                $file,
191264159a61SAndreas Gohr                $title,
191364159a61SAndreas Gohr                null,
191464159a61SAndreas Gohr                null,
191564159a61SAndreas Gohr                null,
191664159a61SAndreas Gohr                $cache = null,
191764159a61SAndreas Gohr                $linking = 'linkonly',
191864159a61SAndreas Gohr                $return = true
191964159a61SAndreas Gohr            );
1920b44a5dceSAnika Henke        }
1921b44a5dceSAnika Henke
1922b44a5dceSAnika Henke        // finish
19233641199aSAnika Henke        $out .= $fallback;
1924f50634f0SAnika Henke        $out .= '</audio>'.NL;
1925f50634f0SAnika Henke        return $out;
1926b44a5dceSAnika Henke    }
1927b44a5dceSAnika Henke
19285c2eed9aSlisps    /**
192952dc5eadSlisps     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
19305c2eed9aSlisps     * which returns an existing media revision less or equal to rev or date_at
19315c2eed9aSlisps     *
19325c2eed9aSlisps     * @author lisps
19335c2eed9aSlisps     * @param string $media_id
19345c2eed9aSlisps     * @access protected
19355c2eed9aSlisps     * @return string revision ('' for current)
19365c2eed9aSlisps     */
1937de369923SAndreas Gohr    protected function _getLastMediaRevisionAt($media_id) {
193852dc5eadSlisps        if (!$this->date_at || media_isexternal($media_id)) return '';
1939252acce3SSatoshi Sahara        $changelog = new MediaChangeLog($media_id);
1940252acce3SSatoshi Sahara        return $changelog->getLastRevisionAt($this->date_at);
19415c2eed9aSlisps    }
19425c2eed9aSlisps
19433dd5c225SAndreas Gohr    #endregion
19440cecf9d5Sandi}
19450cecf9d5Sandi
1946e3776c06SMichael Hamann//Setup VIM: ex: et ts=4 :
1947