xref: /dokuwiki/inc/parser/xhtml.php (revision 9b505d59adc0f5dd5a481c7a9a2e9d1f4f6780f3)
1<?php
2/**
3 * Renderer for XHTML output
4 *
5 * @author Harry Fuecks <hfuecks@gmail.com>
6 * @author Andreas Gohr <andi@splitbrain.org>
7 */
8if(!defined('DOKU_INC')) die('meh.');
9
10if(!defined('DOKU_LF')) {
11    // Some whitespace to help View > Source
12    define ('DOKU_LF', "\n");
13}
14
15if(!defined('DOKU_TAB')) {
16    // Some whitespace to help View > Source
17    define ('DOKU_TAB', "\t");
18}
19
20/**
21 * The XHTML Renderer
22 *
23 * This is DokuWiki's main renderer used to display page content in the wiki
24 */
25class Doku_Renderer_xhtml extends Doku_Renderer {
26    /** @var array store the table of contents */
27    public $toc = array();
28
29    /** @var array A stack of section edit data */
30    protected $sectionedits = array();
31    var $date_at = '';    // link pages and media against this revision
32
33    /** @var int last section edit id, used by startSectionEdit */
34    protected $lastsecid = 0;
35
36    /** @var array the list of headers used to create unique link ids */
37    protected $headers = array();
38
39    /** @var array a list of footnotes, list starts at 1! */
40    protected $footnotes = array();
41
42    /** @var int current section level */
43    protected $lastlevel = 0;
44    /** @var array section node tracker */
45    protected $node = array(0, 0, 0, 0, 0);
46
47    /** @var string temporary $doc store */
48    protected $store = '';
49
50    /** @var array global counter, for table classes etc. */
51    protected $_counter = array(); //
52
53    /** @var int counts the code and file blocks, used to provide download links */
54    protected $_codeblock = 0;
55
56    /** @var array list of allowed URL schemes */
57    protected $schemes = null;
58
59    /**
60     * Register a new edit section range
61     *
62     * @param string $type   The section type identifier
63     * @param string $title  The section title
64     * @param int    $start  The byte position for the edit start
65     * @return string  A marker class for the starting HTML element
66     *
67     * @author Adrian Lang <lang@cosmocode.de>
68     */
69    public function startSectionEdit($start, $type, $title = null) {
70        $this->sectionedits[] = array(++$this->lastsecid, $start, $type, $title);
71        return 'sectionedit'.$this->lastsecid;
72    }
73
74    /**
75     * Finish an edit section range
76     *
77     * @param int  $end     The byte position for the edit end; null for the rest of the page
78     *
79     * @author Adrian Lang <lang@cosmocode.de>
80     */
81    public function finishSectionEdit($end = null) {
82        list($id, $start, $type, $title) = array_pop($this->sectionedits);
83        if(!is_null($end) && $end <= $start) {
84            return;
85        }
86        $this->doc .= "<!-- EDIT$id ".strtoupper($type).' ';
87        if(!is_null($title)) {
88            $this->doc .= '"'.str_replace('"', '', $title).'" ';
89        }
90        $this->doc .= "[$start-".(is_null($end) ? '' : $end).'] -->';
91    }
92
93    /**
94     * Returns the format produced by this renderer.
95     *
96     * @return string always 'xhtml'
97     */
98    function getFormat() {
99        return 'xhtml';
100    }
101
102    /**
103     * Initialize the document
104     */
105    function document_start() {
106        //reset some internals
107        $this->toc     = array();
108        $this->headers = array();
109    }
110
111    /**
112     * Finalize the document
113     */
114    function document_end() {
115        // Finish open section edits.
116        while(count($this->sectionedits) > 0) {
117            if($this->sectionedits[count($this->sectionedits) - 1][1] <= 1) {
118                // If there is only one section, do not write a section edit
119                // marker.
120                array_pop($this->sectionedits);
121            } else {
122                $this->finishSectionEdit();
123            }
124        }
125
126        if(count($this->footnotes) > 0) {
127            $this->doc .= '<div class="footnotes">'.DOKU_LF;
128
129            foreach($this->footnotes as $id => $footnote) {
130                // check its not a placeholder that indicates actual footnote text is elsewhere
131                if(substr($footnote, 0, 5) != "@@FNT") {
132
133                    // open the footnote and set the anchor and backlink
134                    $this->doc .= '<div class="fn">';
135                    $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">';
136                    $this->doc .= $id.')</a></sup> '.DOKU_LF;
137
138                    // get any other footnotes that use the same markup
139                    $alt = array_keys($this->footnotes, "@@FNT$id");
140
141                    if(count($alt)) {
142                        foreach($alt as $ref) {
143                            // set anchor and backlink for the other footnotes
144                            $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">';
145                            $this->doc .= ($ref).')</a></sup> '.DOKU_LF;
146                        }
147                    }
148
149                    // add footnote markup and close this footnote
150                    $this->doc .= $footnote;
151                    $this->doc .= '</div>'.DOKU_LF;
152                }
153            }
154            $this->doc .= '</div>'.DOKU_LF;
155        }
156
157        // Prepare the TOC
158        global $conf;
159        if($this->info['toc'] && is_array($this->toc) && $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']) {
160            global $TOC;
161            $TOC = $this->toc;
162        }
163
164        // make sure there are no empty paragraphs
165        $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc);
166    }
167
168    /**
169     * Add an item to the TOC
170     *
171     * @param string $id       the hash link
172     * @param string $text     the text to display
173     * @param int    $level    the nesting level
174     */
175    function toc_additem($id, $text, $level) {
176        global $conf;
177
178        //handle TOC
179        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
180            $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1);
181        }
182    }
183
184    /**
185     * Render a heading
186     *
187     * @param string $text  the text to display
188     * @param int    $level header level
189     * @param int    $pos   byte position in the original source
190     */
191    function header($text, $level, $pos) {
192        global $conf;
193
194        if(!$text) return; //skip empty headlines
195
196        $hid = $this->_headerToLink($text, true);
197
198        //only add items within configured levels
199        $this->toc_additem($hid, $text, $level);
200
201        // adjust $node to reflect hierarchy of levels
202        $this->node[$level - 1]++;
203        if($level < $this->lastlevel) {
204            for($i = 0; $i < $this->lastlevel - $level; $i++) {
205                $this->node[$this->lastlevel - $i - 1] = 0;
206            }
207        }
208        $this->lastlevel = $level;
209
210        if($level <= $conf['maxseclevel'] &&
211            count($this->sectionedits) > 0 &&
212            $this->sectionedits[count($this->sectionedits) - 1][2] === 'section'
213        ) {
214            $this->finishSectionEdit($pos - 1);
215        }
216
217        // write the header
218        $this->doc .= DOKU_LF.'<h'.$level;
219        if($level <= $conf['maxseclevel']) {
220            $this->doc .= ' class="'.$this->startSectionEdit($pos, 'section', $text).'"';
221        }
222        $this->doc .= ' id="'.$hid.'">';
223        $this->doc .= $this->_xmlEntities($text);
224        $this->doc .= "</h$level>".DOKU_LF;
225    }
226
227    /**
228     * Open a new section
229     *
230     * @param int $level section level (as determined by the previous header)
231     */
232    function section_open($level) {
233        $this->doc .= '<div class="level'.$level.'">'.DOKU_LF;
234    }
235
236    /**
237     * Close the current section
238     */
239    function section_close() {
240        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
241    }
242
243    /**
244     * Render plain text data
245     *
246     * @param $text
247     */
248    function cdata($text) {
249        $this->doc .= $this->_xmlEntities($text);
250    }
251
252    /**
253     * Open a paragraph
254     */
255    function p_open() {
256        $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
257    }
258
259    /**
260     * Close a paragraph
261     */
262    function p_close() {
263        $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
264    }
265
266    /**
267     * Create a line break
268     */
269    function linebreak() {
270        $this->doc .= '<br/>'.DOKU_LF;
271    }
272
273    /**
274     * Create a horizontal line
275     */
276    function hr() {
277        $this->doc .= '<hr />'.DOKU_LF;
278    }
279
280    /**
281     * Start strong (bold) formatting
282     */
283    function strong_open() {
284        $this->doc .= '<strong>';
285    }
286
287    /**
288     * Stop strong (bold) formatting
289     */
290    function strong_close() {
291        $this->doc .= '</strong>';
292    }
293
294    /**
295     * Start emphasis (italics) formatting
296     */
297    function emphasis_open() {
298        $this->doc .= '<em>';
299    }
300
301    /**
302     * Stop emphasis (italics) formatting
303     */
304    function emphasis_close() {
305        $this->doc .= '</em>';
306    }
307
308    /**
309     * Start underline formatting
310     */
311    function underline_open() {
312        $this->doc .= '<em class="u">';
313    }
314
315    /**
316     * Stop underline formatting
317     */
318    function underline_close() {
319        $this->doc .= '</em>';
320    }
321
322    /**
323     * Start monospace formatting
324     */
325    function monospace_open() {
326        $this->doc .= '<code>';
327    }
328
329    /**
330     * Stop monospace formatting
331     */
332    function monospace_close() {
333        $this->doc .= '</code>';
334    }
335
336    /**
337     * Start a subscript
338     */
339    function subscript_open() {
340        $this->doc .= '<sub>';
341    }
342
343    /**
344     * Stop a subscript
345     */
346    function subscript_close() {
347        $this->doc .= '</sub>';
348    }
349
350    /**
351     * Start a superscript
352     */
353    function superscript_open() {
354        $this->doc .= '<sup>';
355    }
356
357    /**
358     * Stop a superscript
359     */
360    function superscript_close() {
361        $this->doc .= '</sup>';
362    }
363
364    /**
365     * Start deleted (strike-through) formatting
366     */
367    function deleted_open() {
368        $this->doc .= '<del>';
369    }
370
371    /**
372     * Stop deleted (strike-through) formatting
373     */
374    function deleted_close() {
375        $this->doc .= '</del>';
376    }
377
378    /**
379     * Callback for footnote start syntax
380     *
381     * All following content will go to the footnote instead of
382     * the document. To achieve this the previous rendered content
383     * is moved to $store and $doc is cleared
384     *
385     * @author Andreas Gohr <andi@splitbrain.org>
386     */
387    function footnote_open() {
388
389        // move current content to store and record footnote
390        $this->store = $this->doc;
391        $this->doc   = '';
392    }
393
394    /**
395     * Callback for footnote end syntax
396     *
397     * All rendered content is moved to the $footnotes array and the old
398     * content is restored from $store again
399     *
400     * @author Andreas Gohr
401     */
402    function footnote_close() {
403        /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
404        static $fnid = 0;
405        // assign new footnote id (we start at 1)
406        $fnid++;
407
408        // recover footnote into the stack and restore old content
409        $footnote    = $this->doc;
410        $this->doc   = $this->store;
411        $this->store = '';
412
413        // check to see if this footnote has been seen before
414        $i = array_search($footnote, $this->footnotes);
415
416        if($i === false) {
417            // its a new footnote, add it to the $footnotes array
418            $this->footnotes[$fnid] = $footnote;
419        } else {
420            // seen this one before, save a placeholder
421            $this->footnotes[$fnid] = "@@FNT".($i);
422        }
423
424        // output the footnote reference and link
425        $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>';
426    }
427
428    /**
429     * Open an unordered list
430     */
431    function listu_open() {
432        $this->doc .= '<ul>'.DOKU_LF;
433    }
434
435    /**
436     * Close an unordered list
437     */
438    function listu_close() {
439        $this->doc .= '</ul>'.DOKU_LF;
440    }
441
442    /**
443     * Open an ordered list
444     */
445    function listo_open() {
446        $this->doc .= '<ol>'.DOKU_LF;
447    }
448
449    /**
450     * Close an ordered list
451     */
452    function listo_close() {
453        $this->doc .= '</ol>'.DOKU_LF;
454    }
455
456    /**
457     * Open a list item
458     *
459     * @param int $level the nesting level
460     * @param bool $node true when a node; false when a leaf
461     */
462    function listitem_open($level, $node=false) {
463        $branching = $node ? ' node' : '';
464        $this->doc .= '<li class="level'.$level.$branching.'">';
465    }
466
467    /**
468     * Close a list item
469     */
470    function listitem_close() {
471        $this->doc .= '</li>'.DOKU_LF;
472    }
473
474    /**
475     * Start the content of a list item
476     */
477    function listcontent_open() {
478        $this->doc .= '<div class="li">';
479    }
480
481    /**
482     * Stop the content of a list item
483     */
484    function listcontent_close() {
485        $this->doc .= '</div>'.DOKU_LF;
486    }
487
488    /**
489     * Output unformatted $text
490     *
491     * Defaults to $this->cdata()
492     *
493     * @param string $text
494     */
495    function unformatted($text) {
496        $this->doc .= $this->_xmlEntities($text);
497    }
498
499    /**
500     * Execute PHP code if allowed
501     *
502     * @param  string $text      PHP code that is either executed or printed
503     * @param  string $wrapper   html element to wrap result if $conf['phpok'] is okff
504     *
505     * @author Andreas Gohr <andi@splitbrain.org>
506     */
507    function php($text, $wrapper = 'code') {
508        global $conf;
509
510        if($conf['phpok']) {
511            ob_start();
512            eval($text);
513            $this->doc .= ob_get_contents();
514            ob_end_clean();
515        } else {
516            $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper);
517        }
518    }
519
520    /**
521     * Output block level PHP code
522     *
523     * If $conf['phpok'] is true this should evaluate the given code and append the result
524     * to $doc
525     *
526     * @param string $text The PHP code
527     */
528    function phpblock($text) {
529        $this->php($text, 'pre');
530    }
531
532    /**
533     * Insert HTML if allowed
534     *
535     * @param  string $text      html text
536     * @param  string $wrapper   html element to wrap result if $conf['htmlok'] is okff
537     *
538     * @author Andreas Gohr <andi@splitbrain.org>
539     */
540    function html($text, $wrapper = 'code') {
541        global $conf;
542
543        if($conf['htmlok']) {
544            $this->doc .= $text;
545        } else {
546            $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper);
547        }
548    }
549
550    /**
551     * Output raw block-level HTML
552     *
553     * If $conf['htmlok'] is true this should add the code as is to $doc
554     *
555     * @param string $text The HTML
556     */
557    function htmlblock($text) {
558        $this->html($text, 'pre');
559    }
560
561    /**
562     * Start a block quote
563     */
564    function quote_open() {
565        $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
566    }
567
568    /**
569     * Stop a block quote
570     */
571    function quote_close() {
572        $this->doc .= '</div></blockquote>'.DOKU_LF;
573    }
574
575    /**
576     * Output preformatted text
577     *
578     * @param string $text
579     */
580    function preformatted($text) {
581        $this->doc .= '<pre class="code">'.trim($this->_xmlEntities($text), "\n\r").'</pre>'.DOKU_LF;
582    }
583
584    /**
585     * Display text as file content, optionally syntax highlighted
586     *
587     * @param string $text     text to show
588     * @param string $language programming language to use for syntax highlighting
589     * @param string $filename file path label
590     */
591    function file($text, $language = null, $filename = null) {
592        $this->_highlight('file', $text, $language, $filename);
593    }
594
595    /**
596     * Display text as code content, optionally syntax highlighted
597     *
598     * @param string $text     text to show
599     * @param string $language programming language to use for syntax highlighting
600     * @param string $filename file path label
601     */
602    function code($text, $language = null, $filename = null) {
603        $this->_highlight('code', $text, $language, $filename);
604    }
605
606    /**
607     * Use GeSHi to highlight language syntax in code and file blocks
608     *
609     * @author Andreas Gohr <andi@splitbrain.org>
610     * @param string $type     code|file
611     * @param string $text     text to show
612     * @param string $language programming language to use for syntax highlighting
613     * @param string $filename file path label
614     */
615    function _highlight($type, $text, $language = null, $filename = null) {
616        global $ID;
617        global $lang;
618
619        if($filename) {
620            // add icon
621            list($ext) = mimetype($filename, false);
622            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
623            $class = 'mediafile mf_'.$class;
624
625            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
626            $this->doc .= '<dt><a href="'.exportlink($ID, 'code', array('codeblock' => $this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">';
627            $this->doc .= hsc($filename);
628            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
629        }
630
631        if($text{0} == "\n") {
632            $text = substr($text, 1);
633        }
634        if(substr($text, -1) == "\n") {
635            $text = substr($text, 0, -1);
636        }
637
638        if(is_null($language)) {
639            $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
640        } else {
641            $class = 'code'; //we always need the code class to make the syntax highlighting apply
642            if($type != 'code') $class .= ' '.$type;
643
644            $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '').'</pre>'.DOKU_LF;
645        }
646
647        if($filename) {
648            $this->doc .= '</dd></dl>'.DOKU_LF;
649        }
650
651        $this->_codeblock++;
652    }
653
654    /**
655     * Format an acronym
656     *
657     * Uses $this->acronyms
658     *
659     * @param string $acronym
660     */
661    function acronym($acronym) {
662
663        if(array_key_exists($acronym, $this->acronyms)) {
664
665            $title = $this->_xmlEntities($this->acronyms[$acronym]);
666
667            $this->doc .= '<abbr title="'.$title
668                .'">'.$this->_xmlEntities($acronym).'</abbr>';
669
670        } else {
671            $this->doc .= $this->_xmlEntities($acronym);
672        }
673    }
674
675    /**
676     * Format a smiley
677     *
678     * Uses $this->smiley
679     *
680     * @param string $smiley
681     */
682    function smiley($smiley) {
683        if(array_key_exists($smiley, $this->smileys)) {
684            $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley].
685                '" class="icon" alt="'.
686                $this->_xmlEntities($smiley).'" />';
687        } else {
688            $this->doc .= $this->_xmlEntities($smiley);
689        }
690    }
691
692    /**
693     * Format an entity
694     *
695     * Entities are basically small text replacements
696     *
697     * Uses $this->entities
698     *
699     * @param string $entity
700     */
701    function entity($entity) {
702        if(array_key_exists($entity, $this->entities)) {
703            $this->doc .= $this->entities[$entity];
704        } else {
705            $this->doc .= $this->_xmlEntities($entity);
706        }
707    }
708
709    /**
710     * Typographically format a multiply sign
711     *
712     * Example: ($x=640, $y=480) should result in "640×480"
713     *
714     * @param string|int $x first value
715     * @param string|int $y second value
716     */
717    function multiplyentity($x, $y) {
718        $this->doc .= "$x&times;$y";
719    }
720
721    /**
722     * Render an opening single quote char (language specific)
723     */
724    function singlequoteopening() {
725        global $lang;
726        $this->doc .= $lang['singlequoteopening'];
727    }
728
729    /**
730     * Render a closing single quote char (language specific)
731     */
732    function singlequoteclosing() {
733        global $lang;
734        $this->doc .= $lang['singlequoteclosing'];
735    }
736
737    /**
738     * Render an apostrophe char (language specific)
739     */
740    function apostrophe() {
741        global $lang;
742        $this->doc .= $lang['apostrophe'];
743    }
744
745    /**
746     * Render an opening double quote char (language specific)
747     */
748    function doublequoteopening() {
749        global $lang;
750        $this->doc .= $lang['doublequoteopening'];
751    }
752
753    /**
754     * Render an closinging double quote char (language specific)
755     */
756    function doublequoteclosing() {
757        global $lang;
758        $this->doc .= $lang['doublequoteclosing'];
759    }
760
761    /**
762     * Render a CamelCase link
763     *
764     * @param string $link The link name
765     * @see http://en.wikipedia.org/wiki/CamelCase
766     */
767    function camelcaselink($link) {
768        $this->internallink($link, $link);
769    }
770
771    /**
772     * Render a page local link
773     *
774     * @param string $hash hash link identifier
775     * @param string $name name for the link
776     */
777    function locallink($hash, $name = null) {
778        global $ID;
779        $name  = $this->_getLinkTitle($name, $hash, $isImage);
780        $hash  = $this->_headerToLink($hash);
781        $title = $ID.' ↵';
782        $this->doc .= '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
783        $this->doc .= $name;
784        $this->doc .= '</a>';
785    }
786
787    /**
788     * Render an internal Wiki Link
789     *
790     * $search,$returnonly & $linktype are not for the renderer but are used
791     * elsewhere - no need to implement them in other renderers
792     *
793     * @author Andreas Gohr <andi@splitbrain.org>
794     * @param string      $id         pageid
795     * @param string|null $name       link name
796     * @param string|null $search     adds search url param
797     * @param bool        $returnonly whether to return html or write to doc attribute
798     * @param string      $linktype   type to set use of headings
799     * @return void|string writes to doc attribute or returns html depends on $returnonly
800     */
801    function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') {
802        global $conf;
803        global $ID;
804        global $INFO;
805
806        $params = '';
807        $parts  = explode('?', $id, 2);
808        if(count($parts) === 2) {
809            $id     = $parts[0];
810            $params = $parts[1];
811        }
812
813        // For empty $id we need to know the current $ID
814        // We need this check because _simpleTitle needs
815        // correct $id and resolve_pageid() use cleanID($id)
816        // (some things could be lost)
817        if($id === '') {
818            $id = $ID;
819        }
820
821        // default name is based on $id as given
822        $default = $this->_simpleTitle($id);
823
824        // now first resolve and clean up the $id
825        resolve_pageid(getNS($ID), $id, $exists, $this->date_at, true);
826
827        $link = array();
828        $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
829        if(!$isImage) {
830            if($exists) {
831                $class = 'wikilink1';
832            } else {
833                $class       = 'wikilink2';
834                $link['rel'] = 'nofollow';
835            }
836        } else {
837            $class = 'media';
838        }
839
840        //keep hash anchor
841        @list($id, $hash) = explode('#', $id, 2);
842        if(!empty($hash)) $hash = $this->_headerToLink($hash);
843
844        //prepare for formating
845        $link['target'] = $conf['target']['wiki'];
846        $link['style']  = '';
847        $link['pre']    = '';
848        $link['suf']    = '';
849        // highlight link to current page
850        if($id == $INFO['id']) {
851            $link['pre'] = '<span class="curid">';
852            $link['suf'] = '</span>';
853        }
854        $link['more']   = '';
855        $link['class']  = $class;
856        if($this->date_at) {
857            $params['at'] = $this->date_at;
858        }
859        $link['url']    = wl($id, $params);
860        $link['name']   = $name;
861        $link['title']  = $id;
862        //add search string
863        if($search) {
864            ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
865            if(is_array($search)) {
866                $search = array_map('rawurlencode', $search);
867                $link['url'] .= 's[]='.join('&amp;s[]=', $search);
868            } else {
869                $link['url'] .= 's='.rawurlencode($search);
870            }
871        }
872
873        //keep hash
874        if($hash) $link['url'] .= '#'.$hash;
875
876        //output formatted
877        if($returnonly) {
878            return $this->_formatLink($link);
879        } else {
880            $this->doc .= $this->_formatLink($link);
881        }
882    }
883
884    /**
885     * Render an external link
886     *
887     * @param string       $url  full URL with scheme
888     * @param string|array $name name for the link, array for media file
889     */
890    function externallink($url, $name = null) {
891        global $conf;
892
893        $name = $this->_getLinkTitle($name, $url, $isImage);
894
895        // url might be an attack vector, only allow registered protocols
896        if(is_null($this->schemes)) $this->schemes = getSchemes();
897        list($scheme) = explode('://', $url);
898        $scheme = strtolower($scheme);
899        if(!in_array($scheme, $this->schemes)) $url = '';
900
901        // is there still an URL?
902        if(!$url) {
903            $this->doc .= $name;
904            return;
905        }
906
907        // set class
908        if(!$isImage) {
909            $class = 'urlextern';
910        } else {
911            $class = 'media';
912        }
913
914        //prepare for formating
915        $link = array();
916        $link['target'] = $conf['target']['extern'];
917        $link['style']  = '';
918        $link['pre']    = '';
919        $link['suf']    = '';
920        $link['more']   = '';
921        $link['class']  = $class;
922        $link['url']    = $url;
923
924        $link['name']  = $name;
925        $link['title'] = $this->_xmlEntities($url);
926        if($conf['relnofollow']) $link['more'] .= ' rel="nofollow"';
927
928        //output formatted
929        $this->doc .= $this->_formatLink($link);
930    }
931
932    /**
933     * Render an interwiki link
934     *
935     * You may want to use $this->_resolveInterWiki() here
936     *
937     * @param string       $match     original link - probably not much use
938     * @param string|array $name      name for the link, array for media file
939     * @param string       $wikiName  indentifier (shortcut) for the remote wiki
940     * @param string       $wikiUri   the fragment parsed from the original link
941     */
942    function interwikilink($match, $name = null, $wikiName, $wikiUri) {
943        global $conf;
944
945        $link           = array();
946        $link['target'] = $conf['target']['interwiki'];
947        $link['pre']    = '';
948        $link['suf']    = '';
949        $link['more']   = '';
950        $link['name']   = $this->_getLinkTitle($name, $wikiUri, $isImage);
951
952        //get interwiki URL
953        $exists = null;
954        $url    = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
955
956        if(!$isImage) {
957            $class         = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
958            $link['class'] = "interwiki iw_$class";
959        } else {
960            $link['class'] = 'media';
961        }
962
963        //do we stay at the same server? Use local target
964        if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) {
965            $link['target'] = $conf['target']['wiki'];
966        }
967        if($exists !== null && !$isImage) {
968            if($exists) {
969                $link['class'] .= ' wikilink1';
970            } else {
971                $link['class'] .= ' wikilink2';
972                $link['rel'] = 'nofollow';
973            }
974        }
975
976        $link['url']   = $url;
977        $link['title'] = htmlspecialchars($link['url']);
978
979        //output formatted
980        $this->doc .= $this->_formatLink($link);
981    }
982
983    /**
984     * Link to windows share
985     *
986     * @param string       $url  the link
987     * @param string|array $name name for the link, array for media file
988     */
989    function windowssharelink($url, $name = null) {
990        global $conf;
991
992        //simple setup
993        $link = array();
994        $link['target'] = $conf['target']['windows'];
995        $link['pre']    = '';
996        $link['suf']    = '';
997        $link['style']  = '';
998
999        $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
1000        if(!$isImage) {
1001            $link['class'] = 'windows';
1002        } else {
1003            $link['class'] = 'media';
1004        }
1005
1006        $link['title'] = $this->_xmlEntities($url);
1007        $url           = str_replace('\\', '/', $url);
1008        $url           = ltrim($url,'/');
1009        $url           = 'file:///'.$url;
1010        $link['url']   = $url;
1011
1012        //output formatted
1013        $this->doc .= $this->_formatLink($link);
1014    }
1015
1016    /**
1017     * Render a linked E-Mail Address
1018     *
1019     * Honors $conf['mailguard'] setting
1020     *
1021     * @param string       $address Email-Address
1022     * @param string|array $name    name for the link, array for media file
1023     */
1024    function emaillink($address, $name = null) {
1025        global $conf;
1026        //simple setup
1027        $link           = array();
1028        $link['target'] = '';
1029        $link['pre']    = '';
1030        $link['suf']    = '';
1031        $link['style']  = '';
1032        $link['more']   = '';
1033
1034        $name = $this->_getLinkTitle($name, '', $isImage);
1035        if(!$isImage) {
1036            $link['class'] = 'mail';
1037        } else {
1038            $link['class'] = 'media';
1039        }
1040
1041        $address = $this->_xmlEntities($address);
1042        $address = obfuscate($address);
1043        $title   = $address;
1044
1045        if(empty($name)) {
1046            $name = $address;
1047        }
1048
1049        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
1050
1051        $link['url']   = 'mailto:'.$address;
1052        $link['name']  = $name;
1053        $link['title'] = $title;
1054
1055        //output formatted
1056        $this->doc .= $this->_formatLink($link);
1057    }
1058
1059    /**
1060     * Render an internal media file
1061     *
1062     * @param string $src       media ID
1063     * @param string $title     descriptive text
1064     * @param string $align     left|center|right
1065     * @param int    $width     width of media in pixel
1066     * @param int    $height    height of media in pixel
1067     * @param string $cache     cache|recache|nocache
1068     * @param string $linking   linkonly|detail|nolink
1069     * @param bool   $return    return HTML instead of adding to $doc
1070     * @return void|string
1071     */
1072    function internalmedia($src, $title = null, $align = null, $width = null,
1073                           $height = null, $cache = null, $linking = null, $return = false) {
1074        global $ID;
1075        list($src, $hash) = explode('#', $src, 2);
1076        resolve_mediaid(getNS($ID), $src, $exists, $this->date_at, true);
1077
1078        $noLink = false;
1079        $render = ($linking == 'linkonly') ? false : true;
1080        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1081
1082        list($ext, $mime) = mimetype($src, false);
1083        if(substr($mime, 0, 5) == 'image' && $render) {
1084            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src)), ($linking == 'direct'));
1085        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1086            // don't link movies
1087            $noLink = true;
1088        } else {
1089            // add file icons
1090            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1091            $link['class'] .= ' mediafile mf_'.$class;
1092            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache , 'rev'=>$this->_getLastMediaRevisionAt($src)), true);
1093            if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')';
1094        }
1095
1096        if($hash) $link['url'] .= '#'.$hash;
1097
1098        //markup non existing files
1099        if(!$exists) {
1100            $link['class'] .= ' wikilink2';
1101        }
1102
1103        //output formatted
1104        if($return) {
1105            if($linking == 'nolink' || $noLink) return $link['name'];
1106            else return $this->_formatLink($link);
1107        } else {
1108            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1109            else $this->doc .= $this->_formatLink($link);
1110        }
1111    }
1112
1113    /**
1114     * Render an external media file
1115     *
1116     * @param string $src     full media URL
1117     * @param string $title   descriptive text
1118     * @param string $align   left|center|right
1119     * @param int    $width   width of media in pixel
1120     * @param int    $height  height of media in pixel
1121     * @param string $cache   cache|recache|nocache
1122     * @param string $linking linkonly|detail|nolink
1123     * @param bool   $return  return HTML instead of adding to $doc
1124     */
1125    function externalmedia($src, $title = null, $align = null, $width = null,
1126                           $height = null, $cache = null, $linking = null, $return = false) {
1127        list($src, $hash) = explode('#', $src, 2);
1128        $noLink = false;
1129        $render = ($linking == 'linkonly') ? false : true;
1130        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1131
1132        $link['url'] = ml($src, array('cache' => $cache));
1133
1134        list($ext, $mime) = mimetype($src, false);
1135        if(substr($mime, 0, 5) == 'image' && $render) {
1136            // link only jpeg images
1137            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
1138        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1139            // don't link movies
1140            $noLink = true;
1141        } else {
1142            // add file icons
1143            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1144            $link['class'] .= ' mediafile mf_'.$class;
1145        }
1146
1147        if($hash) $link['url'] .= '#'.$hash;
1148
1149        //output formatted
1150        if($return) {
1151            if($linking == 'nolink' || $noLink) return $link['name'];
1152            else return $this->_formatLink($link);
1153        } else {
1154            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1155            else $this->doc .= $this->_formatLink($link);
1156        }
1157    }
1158
1159    /**
1160     * Renders an RSS feed
1161     *
1162     * @author Andreas Gohr <andi@splitbrain.org>
1163     */
1164    function rss($url, $params) {
1165        global $lang;
1166        global $conf;
1167
1168        require_once(DOKU_INC.'inc/FeedParser.php');
1169        $feed = new FeedParser();
1170        $feed->set_feed_url($url);
1171
1172        //disable warning while fetching
1173        if(!defined('DOKU_E_LEVEL')) {
1174            $elvl = error_reporting(E_ERROR);
1175        }
1176        $rc = $feed->init();
1177        if(isset($elvl)) {
1178            error_reporting($elvl);
1179        }
1180
1181        if($params['nosort']) $feed->enable_order_by_date(false);
1182
1183        //decide on start and end
1184        if($params['reverse']) {
1185            $mod   = -1;
1186            $start = $feed->get_item_quantity() - 1;
1187            $end   = $start - ($params['max']);
1188            $end   = ($end < -1) ? -1 : $end;
1189        } else {
1190            $mod   = 1;
1191            $start = 0;
1192            $end   = $feed->get_item_quantity();
1193            $end   = ($end > $params['max']) ? $params['max'] : $end;
1194        }
1195
1196        $this->doc .= '<ul class="rss">';
1197        if($rc) {
1198            for($x = $start; $x != $end; $x += $mod) {
1199                $item = $feed->get_item($x);
1200                $this->doc .= '<li><div class="li">';
1201                // support feeds without links
1202                $lnkurl = $item->get_permalink();
1203                if($lnkurl) {
1204                    // title is escaped by SimplePie, we unescape here because it
1205                    // is escaped again in externallink() FS#1705
1206                    $this->externallink(
1207                        $item->get_permalink(),
1208                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')
1209                    );
1210                } else {
1211                    $this->doc .= ' '.$item->get_title();
1212                }
1213                if($params['author']) {
1214                    $author = $item->get_author(0);
1215                    if($author) {
1216                        $name = $author->get_name();
1217                        if(!$name) $name = $author->get_email();
1218                        if($name) $this->doc .= ' '.$lang['by'].' '.$name;
1219                    }
1220                }
1221                if($params['date']) {
1222                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
1223                }
1224                if($params['details']) {
1225                    $this->doc .= '<div class="detail">';
1226                    if($conf['htmlok']) {
1227                        $this->doc .= $item->get_description();
1228                    } else {
1229                        $this->doc .= strip_tags($item->get_description());
1230                    }
1231                    $this->doc .= '</div>';
1232                }
1233
1234                $this->doc .= '</div></li>';
1235            }
1236        } else {
1237            $this->doc .= '<li><div class="li">';
1238            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
1239            $this->externallink($url);
1240            if($conf['allowdebug']) {
1241                $this->doc .= '<!--'.hsc($feed->error).'-->';
1242            }
1243            $this->doc .= '</div></li>';
1244        }
1245        $this->doc .= '</ul>';
1246    }
1247
1248    /**
1249     * Start a table
1250     *
1251     * @param int $maxcols maximum number of columns
1252     * @param int $numrows NOT IMPLEMENTED
1253     * @param int $pos     byte position in the original source
1254     */
1255    function table_open($maxcols = null, $numrows = null, $pos = null) {
1256        // initialize the row counter used for classes
1257        $this->_counter['row_counter'] = 0;
1258        $class                         = 'table';
1259        if($pos !== null) {
1260            $class .= ' '.$this->startSectionEdit($pos, 'table');
1261        }
1262        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1263            DOKU_LF;
1264    }
1265
1266    /**
1267     * Close a table
1268     *
1269     * @param int $pos byte position in the original source
1270     */
1271    function table_close($pos = null) {
1272        $this->doc .= '</table></div>'.DOKU_LF;
1273        if($pos !== null) {
1274            $this->finishSectionEdit($pos);
1275        }
1276    }
1277
1278    /**
1279     * Open a table header
1280     */
1281    function tablethead_open() {
1282        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1283    }
1284
1285    /**
1286     * Close a table header
1287     */
1288    function tablethead_close() {
1289        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1290    }
1291
1292    /**
1293     * Open a table row
1294     */
1295    function tablerow_open() {
1296        // initialize the cell counter used for classes
1297        $this->_counter['cell_counter'] = 0;
1298        $class                          = 'row'.$this->_counter['row_counter']++;
1299        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
1300    }
1301
1302    /**
1303     * Close a table row
1304     */
1305    function tablerow_close() {
1306        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
1307    }
1308
1309    /**
1310     * Open a table header cell
1311     *
1312     * @param int    $colspan
1313     * @param string $align left|center|right
1314     * @param int    $rowspan
1315     */
1316    function tableheader_open($colspan = 1, $align = null, $rowspan = 1) {
1317        $class = 'class="col'.$this->_counter['cell_counter']++;
1318        if(!is_null($align)) {
1319            $class .= ' '.$align.'align';
1320        }
1321        $class .= '"';
1322        $this->doc .= '<th '.$class;
1323        if($colspan > 1) {
1324            $this->_counter['cell_counter'] += $colspan - 1;
1325            $this->doc .= ' colspan="'.$colspan.'"';
1326        }
1327        if($rowspan > 1) {
1328            $this->doc .= ' rowspan="'.$rowspan.'"';
1329        }
1330        $this->doc .= '>';
1331    }
1332
1333    /**
1334     * Close a table header cell
1335     */
1336    function tableheader_close() {
1337        $this->doc .= '</th>';
1338    }
1339
1340    /**
1341     * Open a table cell
1342     *
1343     * @param int    $colspan
1344     * @param string $align left|center|right
1345     * @param int    $rowspan
1346     */
1347    function tablecell_open($colspan = 1, $align = null, $rowspan = 1) {
1348        $class = 'class="col'.$this->_counter['cell_counter']++;
1349        if(!is_null($align)) {
1350            $class .= ' '.$align.'align';
1351        }
1352        $class .= '"';
1353        $this->doc .= '<td '.$class;
1354        if($colspan > 1) {
1355            $this->_counter['cell_counter'] += $colspan - 1;
1356            $this->doc .= ' colspan="'.$colspan.'"';
1357        }
1358        if($rowspan > 1) {
1359            $this->doc .= ' rowspan="'.$rowspan.'"';
1360        }
1361        $this->doc .= '>';
1362    }
1363
1364    /**
1365     * Close a table cell
1366     */
1367    function tablecell_close() {
1368        $this->doc .= '</td>';
1369    }
1370
1371    #region Utility functions
1372
1373    /**
1374     * Build a link
1375     *
1376     * Assembles all parts defined in $link returns HTML for the link
1377     *
1378     * @author Andreas Gohr <andi@splitbrain.org>
1379     */
1380    function _formatLink($link) {
1381        //make sure the url is XHTML compliant (skip mailto)
1382        if(substr($link['url'], 0, 7) != 'mailto:') {
1383            $link['url'] = str_replace('&', '&amp;', $link['url']);
1384            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1385        }
1386        //remove double encodings in titles
1387        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1388
1389        // be sure there are no bad chars in url or title
1390        // (we can't do this for name because it can contain an img tag)
1391        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1392        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1393
1394        $ret = '';
1395        $ret .= $link['pre'];
1396        $ret .= '<a href="'.$link['url'].'"';
1397        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1398        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1399        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1400        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1401        if(!empty($link['rel'])) $ret .= ' rel="'.$link['rel'].'"';
1402        if(!empty($link['more'])) $ret .= ' '.$link['more'];
1403        $ret .= '>';
1404        $ret .= $link['name'];
1405        $ret .= '</a>';
1406        $ret .= $link['suf'];
1407        return $ret;
1408    }
1409
1410    /**
1411     * Renders internal and external media
1412     *
1413     * @author Andreas Gohr <andi@splitbrain.org>
1414     * @param string $src       media ID
1415     * @param string $title     descriptive text
1416     * @param string $align     left|center|right
1417     * @param int    $width     width of media in pixel
1418     * @param int    $height    height of media in pixel
1419     * @param string $cache     cache|recache|nocache
1420     * @param bool   $render    should the media be embedded inline or just linked
1421     * @return string
1422     */
1423    function _media($src, $title = null, $align = null, $width = null,
1424                    $height = null, $cache = null, $render = true) {
1425
1426        $ret = '';
1427
1428        list($ext, $mime) = mimetype($src);
1429        if(substr($mime, 0, 5) == 'image') {
1430            // first get the $title
1431            if(!is_null($title)) {
1432                $title = $this->_xmlEntities($title);
1433            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1434                //try to use the caption from IPTC/EXIF
1435                require_once(DOKU_INC.'inc/JpegMeta.php');
1436                $jpeg = new JpegMeta(mediaFN($src));
1437                if($jpeg !== false) $cap = $jpeg->getTitle();
1438                if(!empty($cap)) {
1439                    $title = $this->_xmlEntities($cap);
1440                }
1441            }
1442            if(!$render) {
1443                // if the picture is not supposed to be rendered
1444                // return the title of the picture
1445                if(!$title) {
1446                    // just show the sourcename
1447                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
1448                }
1449                return $title;
1450            }
1451            //add image tag
1452            $ret .= '<img src="'.ml($src, array('w' => $width, 'h' => $height, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src))).'"';
1453            $ret .= ' class="media'.$align.'"';
1454
1455            if($title) {
1456                $ret .= ' title="'.$title.'"';
1457                $ret .= ' alt="'.$title.'"';
1458            } else {
1459                $ret .= ' alt=""';
1460            }
1461
1462            if(!is_null($width))
1463                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1464
1465            if(!is_null($height))
1466                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1467
1468            $ret .= ' />';
1469
1470        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1471            // first get the $title
1472            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1473            if(!$render) {
1474                // if the file is not supposed to be rendered
1475                // return the title of the file (just the sourcename if there is no title)
1476                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
1477            }
1478
1479            $att          = array();
1480            $att['class'] = "media$align";
1481            if($title) {
1482                $att['title'] = $title;
1483            }
1484
1485            if(media_supportedav($mime, 'video')) {
1486                //add video
1487                $ret .= $this->_video($src, $width, $height, $att);
1488            }
1489            if(media_supportedav($mime, 'audio')) {
1490                //add audio
1491                $ret .= $this->_audio($src, $att);
1492            }
1493
1494        } elseif($mime == 'application/x-shockwave-flash') {
1495            if(!$render) {
1496                // if the flash is not supposed to be rendered
1497                // return the title of the flash
1498                if(!$title) {
1499                    // just show the sourcename
1500                    $title = utf8_basename(noNS($src));
1501                }
1502                return $this->_xmlEntities($title);
1503            }
1504
1505            $att          = array();
1506            $att['class'] = "media$align";
1507            if($align == 'right') $att['align'] = 'right';
1508            if($align == 'left') $att['align'] = 'left';
1509            $ret .= html_flashobject(
1510                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1511                array('quality' => 'high'),
1512                null,
1513                $att,
1514                $this->_xmlEntities($title)
1515            );
1516        } elseif($title) {
1517            // well at least we have a title to display
1518            $ret .= $this->_xmlEntities($title);
1519        } else {
1520            // just show the sourcename
1521            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
1522        }
1523
1524        return $ret;
1525    }
1526
1527    /**
1528     * Escape string for output
1529     *
1530     * @param $string
1531     * @return string
1532     */
1533    function _xmlEntities($string) {
1534        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1535    }
1536
1537    /**
1538     * Creates a linkid from a headline
1539     *
1540     * @author Andreas Gohr <andi@splitbrain.org>
1541     * @param string  $title   The headline title
1542     * @param boolean $create  Create a new unique ID?
1543     * @return string
1544     */
1545    function _headerToLink($title, $create = false) {
1546        if($create) {
1547            return sectionID($title, $this->headers);
1548        } else {
1549            $check = false;
1550            return sectionID($title, $check);
1551        }
1552    }
1553
1554    /**
1555     * Construct a title and handle images in titles
1556     *
1557     * @author Harry Fuecks <hfuecks@gmail.com>
1558     * @param string|array $title    either string title or media array
1559     * @param string       $default  default title if nothing else is found
1560     * @param bool         $isImage  will be set to true if it's a media file
1561     * @param null|string  $id       linked page id (used to extract title from first heading)
1562     * @param string       $linktype content|navigation
1563     * @return string      HTML of the title, might be full image tag or just escaped text
1564     */
1565    function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1566        $isImage = false;
1567        if(is_array($title)) {
1568            $isImage = true;
1569            return $this->_imageTitle($title);
1570        } elseif(is_null($title) || trim($title) == '') {
1571            if(useHeading($linktype) && $id) {
1572                $heading = p_get_first_heading($id);
1573                if($heading) {
1574                    return $this->_xmlEntities($heading);
1575                }
1576            }
1577            return $this->_xmlEntities($default);
1578        } else {
1579            return $this->_xmlEntities($title);
1580        }
1581    }
1582
1583    /**
1584     * Returns HTML code for images used in link titles
1585     *
1586     * @author Andreas Gohr <andi@splitbrain.org>
1587     * @param array $img
1588     * @return string HTML img tag or similar
1589     */
1590    function _imageTitle($img) {
1591        global $ID;
1592
1593        // some fixes on $img['src']
1594        // see internalmedia() and externalmedia()
1595        list($img['src']) = explode('#', $img['src'], 2);
1596        if($img['type'] == 'internalmedia') {
1597            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
1598        }
1599
1600        return $this->_media(
1601            $img['src'],
1602            $img['title'],
1603            $img['align'],
1604            $img['width'],
1605            $img['height'],
1606            $img['cache']
1607        );
1608    }
1609
1610    /**
1611     * helperfunction to return a basic link to a media
1612     *
1613     * used in internalmedia() and externalmedia()
1614     *
1615     * @author   Pierre Spring <pierre.spring@liip.ch>
1616     * @param string $src       media ID
1617     * @param string $title     descriptive text
1618     * @param string $align     left|center|right
1619     * @param int    $width     width of media in pixel
1620     * @param int    $height    height of media in pixel
1621     * @param string $cache     cache|recache|nocache
1622     * @param bool   $render    should the media be embedded inline or just linked
1623     * @return array associative array with link config
1624     */
1625    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1626        global $conf;
1627
1628        $link           = array();
1629        $link['class']  = 'media';
1630        $link['style']  = '';
1631        $link['pre']    = '';
1632        $link['suf']    = '';
1633        $link['more']   = '';
1634        $link['target'] = $conf['target']['media'];
1635        $link['title']  = $this->_xmlEntities($src);
1636        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1637
1638        return $link;
1639    }
1640
1641    /**
1642     * Embed video(s) in HTML
1643     *
1644     * @author Anika Henke <anika@selfthinker.org>
1645     *
1646     * @param string $src         - ID of video to embed
1647     * @param int    $width       - width of the video in pixels
1648     * @param int    $height      - height of the video in pixels
1649     * @param array  $atts        - additional attributes for the <video> tag
1650     * @return string
1651     */
1652    function _video($src, $width, $height, $atts = null) {
1653        // prepare width and height
1654        if(is_null($atts)) $atts = array();
1655        $atts['width']  = (int) $width;
1656        $atts['height'] = (int) $height;
1657        if(!$atts['width']) $atts['width'] = 320;
1658        if(!$atts['height']) $atts['height'] = 240;
1659
1660        $posterUrl = '';
1661        $files = array();
1662        $isExternal = media_isexternal($src);
1663
1664        if ($isExternal) {
1665            // take direct source for external files
1666            list(/*ext*/, $srcMime) = mimetype($src);
1667            $files[$srcMime] = $src;
1668        } else {
1669            // prepare alternative formats
1670            $extensions   = array('webm', 'ogv', 'mp4');
1671            $files        = media_alternativefiles($src, $extensions);
1672            $poster       = media_alternativefiles($src, array('jpg', 'png'));
1673            if(!empty($poster)) {
1674                $posterUrl = ml(reset($poster), '', true, '&');
1675            }
1676        }
1677
1678        $out = '';
1679        // open video tag
1680        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1681        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1682        $out .= '>'.NL;
1683        $fallback = '';
1684
1685        // output source for each alternative video format
1686        foreach($files as $mime => $file) {
1687            if ($isExternal) {
1688                $url = $file;
1689                $linkType = 'externalmedia';
1690            } else {
1691                $url = ml($file, '', true, '&');
1692                $linkType = 'internalmedia';
1693            }
1694            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1695
1696            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1697            // alternative content (just a link to the file)
1698            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1699        }
1700
1701        // finish
1702        $out .= $fallback;
1703        $out .= '</video>'.NL;
1704        return $out;
1705    }
1706
1707    /**
1708     * Embed audio in HTML
1709     *
1710     * @author Anika Henke <anika@selfthinker.org>
1711     *
1712     * @param string $src       - ID of audio to embed
1713     * @param array  $atts      - additional attributes for the <audio> tag
1714     * @return string
1715     */
1716    function _audio($src, $atts = array()) {
1717        $files = array();
1718        $isExternal = media_isexternal($src);
1719
1720        if ($isExternal) {
1721            // take direct source for external files
1722            list(/*ext*/, $srcMime) = mimetype($src);
1723            $files[$srcMime] = $src;
1724        } else {
1725            // prepare alternative formats
1726            $extensions   = array('ogg', 'mp3', 'wav');
1727            $files        = media_alternativefiles($src, $extensions);
1728        }
1729
1730        $out = '';
1731        // open audio tag
1732        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1733        $fallback = '';
1734
1735        // output source for each alternative audio format
1736        foreach($files as $mime => $file) {
1737            if ($isExternal) {
1738                $url = $file;
1739                $linkType = 'externalmedia';
1740            } else {
1741                $url = ml($file, '', true, '&');
1742                $linkType = 'internalmedia';
1743            }
1744            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1745
1746            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1747            // alternative content (just a link to the file)
1748            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1749        }
1750
1751        // finish
1752        $out .= $fallback;
1753        $out .= '</audio>'.NL;
1754        return $out;
1755    }
1756
1757    /**
1758     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1759     * which returns an existing media revision less or equal to rev or date_at
1760     *
1761     * @author lisps
1762     * @param string $media_id
1763     * @access protected
1764     * @return string revision ('' for current)
1765     */
1766    function _getLastMediaRevisionAt($media_id){
1767        if(!$this->date_at || media_isexternal($media_id)) return '';
1768        $pagelog = new MediaChangeLog($media_id);
1769        return $pagelog->getLastRevisionAt($this->date_at);
1770    }
1771
1772    #endregion
1773}
1774
1775//Setup VIM: ex: et ts=4 :
1776