xref: /dokuwiki/inc/parser/xhtml.php (revision ab6ac843b05c4030087d58182c1bcea3dcc4e0af)
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        //decide on start and end
1182        if($params['reverse']) {
1183            $mod   = -1;
1184            $start = $feed->get_item_quantity() - 1;
1185            $end   = $start - ($params['max']);
1186            $end   = ($end < -1) ? -1 : $end;
1187        } else {
1188            $mod   = 1;
1189            $start = 0;
1190            $end   = $feed->get_item_quantity();
1191            $end   = ($end > $params['max']) ? $params['max'] : $end;
1192        }
1193
1194        $this->doc .= '<ul class="rss">';
1195        if($rc) {
1196            for($x = $start; $x != $end; $x += $mod) {
1197                $item = $feed->get_item($x);
1198                $this->doc .= '<li><div class="li">';
1199                // support feeds without links
1200                $lnkurl = $item->get_permalink();
1201                if($lnkurl) {
1202                    // title is escaped by SimplePie, we unescape here because it
1203                    // is escaped again in externallink() FS#1705
1204                    $this->externallink(
1205                        $item->get_permalink(),
1206                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')
1207                    );
1208                } else {
1209                    $this->doc .= ' '.$item->get_title();
1210                }
1211                if($params['author']) {
1212                    $author = $item->get_author(0);
1213                    if($author) {
1214                        $name = $author->get_name();
1215                        if(!$name) $name = $author->get_email();
1216                        if($name) $this->doc .= ' '.$lang['by'].' '.$name;
1217                    }
1218                }
1219                if($params['date']) {
1220                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
1221                }
1222                if($params['details']) {
1223                    $this->doc .= '<div class="detail">';
1224                    if($conf['htmlok']) {
1225                        $this->doc .= $item->get_description();
1226                    } else {
1227                        $this->doc .= strip_tags($item->get_description());
1228                    }
1229                    $this->doc .= '</div>';
1230                }
1231
1232                $this->doc .= '</div></li>';
1233            }
1234        } else {
1235            $this->doc .= '<li><div class="li">';
1236            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
1237            $this->externallink($url);
1238            if($conf['allowdebug']) {
1239                $this->doc .= '<!--'.hsc($feed->error).'-->';
1240            }
1241            $this->doc .= '</div></li>';
1242        }
1243        $this->doc .= '</ul>';
1244    }
1245
1246    /**
1247     * Start a table
1248     *
1249     * @param int $maxcols maximum number of columns
1250     * @param int $numrows NOT IMPLEMENTED
1251     * @param int $pos     byte position in the original source
1252     */
1253    function table_open($maxcols = null, $numrows = null, $pos = null) {
1254        // initialize the row counter used for classes
1255        $this->_counter['row_counter'] = 0;
1256        $class                         = 'table';
1257        if($pos !== null) {
1258            $class .= ' '.$this->startSectionEdit($pos, 'table');
1259        }
1260        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1261            DOKU_LF;
1262    }
1263
1264    /**
1265     * Close a table
1266     *
1267     * @param int $pos byte position in the original source
1268     */
1269    function table_close($pos = null) {
1270        $this->doc .= '</table></div>'.DOKU_LF;
1271        if($pos !== null) {
1272            $this->finishSectionEdit($pos);
1273        }
1274    }
1275
1276    /**
1277     * Open a table header
1278     */
1279    function tablethead_open() {
1280        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1281    }
1282
1283    /**
1284     * Close a table header
1285     */
1286    function tablethead_close() {
1287        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1288    }
1289
1290    /**
1291     * Open a table row
1292     */
1293    function tablerow_open() {
1294        // initialize the cell counter used for classes
1295        $this->_counter['cell_counter'] = 0;
1296        $class                          = 'row'.$this->_counter['row_counter']++;
1297        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
1298    }
1299
1300    /**
1301     * Close a table row
1302     */
1303    function tablerow_close() {
1304        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
1305    }
1306
1307    /**
1308     * Open a table header cell
1309     *
1310     * @param int    $colspan
1311     * @param string $align left|center|right
1312     * @param int    $rowspan
1313     */
1314    function tableheader_open($colspan = 1, $align = null, $rowspan = 1) {
1315        $class = 'class="col'.$this->_counter['cell_counter']++;
1316        if(!is_null($align)) {
1317            $class .= ' '.$align.'align';
1318        }
1319        $class .= '"';
1320        $this->doc .= '<th '.$class;
1321        if($colspan > 1) {
1322            $this->_counter['cell_counter'] += $colspan - 1;
1323            $this->doc .= ' colspan="'.$colspan.'"';
1324        }
1325        if($rowspan > 1) {
1326            $this->doc .= ' rowspan="'.$rowspan.'"';
1327        }
1328        $this->doc .= '>';
1329    }
1330
1331    /**
1332     * Close a table header cell
1333     */
1334    function tableheader_close() {
1335        $this->doc .= '</th>';
1336    }
1337
1338    /**
1339     * Open a table cell
1340     *
1341     * @param int    $colspan
1342     * @param string $align left|center|right
1343     * @param int    $rowspan
1344     */
1345    function tablecell_open($colspan = 1, $align = null, $rowspan = 1) {
1346        $class = 'class="col'.$this->_counter['cell_counter']++;
1347        if(!is_null($align)) {
1348            $class .= ' '.$align.'align';
1349        }
1350        $class .= '"';
1351        $this->doc .= '<td '.$class;
1352        if($colspan > 1) {
1353            $this->_counter['cell_counter'] += $colspan - 1;
1354            $this->doc .= ' colspan="'.$colspan.'"';
1355        }
1356        if($rowspan > 1) {
1357            $this->doc .= ' rowspan="'.$rowspan.'"';
1358        }
1359        $this->doc .= '>';
1360    }
1361
1362    /**
1363     * Close a table cell
1364     */
1365    function tablecell_close() {
1366        $this->doc .= '</td>';
1367    }
1368
1369    #region Utility functions
1370
1371    /**
1372     * Build a link
1373     *
1374     * Assembles all parts defined in $link returns HTML for the link
1375     *
1376     * @author Andreas Gohr <andi@splitbrain.org>
1377     */
1378    function _formatLink($link) {
1379        //make sure the url is XHTML compliant (skip mailto)
1380        if(substr($link['url'], 0, 7) != 'mailto:') {
1381            $link['url'] = str_replace('&', '&amp;', $link['url']);
1382            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1383        }
1384        //remove double encodings in titles
1385        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1386
1387        // be sure there are no bad chars in url or title
1388        // (we can't do this for name because it can contain an img tag)
1389        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1390        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1391
1392        $ret = '';
1393        $ret .= $link['pre'];
1394        $ret .= '<a href="'.$link['url'].'"';
1395        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1396        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1397        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1398        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1399        if(!empty($link['rel'])) $ret .= ' rel="'.$link['rel'].'"';
1400        if(!empty($link['more'])) $ret .= ' '.$link['more'];
1401        $ret .= '>';
1402        $ret .= $link['name'];
1403        $ret .= '</a>';
1404        $ret .= $link['suf'];
1405        return $ret;
1406    }
1407
1408    /**
1409     * Renders internal and external media
1410     *
1411     * @author Andreas Gohr <andi@splitbrain.org>
1412     * @param string $src       media ID
1413     * @param string $title     descriptive text
1414     * @param string $align     left|center|right
1415     * @param int    $width     width of media in pixel
1416     * @param int    $height    height of media in pixel
1417     * @param string $cache     cache|recache|nocache
1418     * @param bool   $render    should the media be embedded inline or just linked
1419     * @return string
1420     */
1421    function _media($src, $title = null, $align = null, $width = null,
1422                    $height = null, $cache = null, $render = true) {
1423
1424        $ret = '';
1425
1426        list($ext, $mime) = mimetype($src);
1427        if(substr($mime, 0, 5) == 'image') {
1428            // first get the $title
1429            if(!is_null($title)) {
1430                $title = $this->_xmlEntities($title);
1431            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1432                //try to use the caption from IPTC/EXIF
1433                require_once(DOKU_INC.'inc/JpegMeta.php');
1434                $jpeg = new JpegMeta(mediaFN($src));
1435                if($jpeg !== false) $cap = $jpeg->getTitle();
1436                if(!empty($cap)) {
1437                    $title = $this->_xmlEntities($cap);
1438                }
1439            }
1440            if(!$render) {
1441                // if the picture is not supposed to be rendered
1442                // return the title of the picture
1443                if(!$title) {
1444                    // just show the sourcename
1445                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
1446                }
1447                return $title;
1448            }
1449            //add image tag
1450            $ret .= '<img src="'.ml($src, array('w' => $width, 'h' => $height, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src))).'"';
1451            $ret .= ' class="media'.$align.'"';
1452
1453            if($title) {
1454                $ret .= ' title="'.$title.'"';
1455                $ret .= ' alt="'.$title.'"';
1456            } else {
1457                $ret .= ' alt=""';
1458            }
1459
1460            if(!is_null($width))
1461                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1462
1463            if(!is_null($height))
1464                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1465
1466            $ret .= ' />';
1467
1468        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1469            // first get the $title
1470            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1471            if(!$render) {
1472                // if the file is not supposed to be rendered
1473                // return the title of the file (just the sourcename if there is no title)
1474                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
1475            }
1476
1477            $att          = array();
1478            $att['class'] = "media$align";
1479            if($title) {
1480                $att['title'] = $title;
1481            }
1482
1483            if(media_supportedav($mime, 'video')) {
1484                //add video
1485                $ret .= $this->_video($src, $width, $height, $att);
1486            }
1487            if(media_supportedav($mime, 'audio')) {
1488                //add audio
1489                $ret .= $this->_audio($src, $att);
1490            }
1491
1492        } elseif($mime == 'application/x-shockwave-flash') {
1493            if(!$render) {
1494                // if the flash is not supposed to be rendered
1495                // return the title of the flash
1496                if(!$title) {
1497                    // just show the sourcename
1498                    $title = utf8_basename(noNS($src));
1499                }
1500                return $this->_xmlEntities($title);
1501            }
1502
1503            $att          = array();
1504            $att['class'] = "media$align";
1505            if($align == 'right') $att['align'] = 'right';
1506            if($align == 'left') $att['align'] = 'left';
1507            $ret .= html_flashobject(
1508                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1509                array('quality' => 'high'),
1510                null,
1511                $att,
1512                $this->_xmlEntities($title)
1513            );
1514        } elseif($title) {
1515            // well at least we have a title to display
1516            $ret .= $this->_xmlEntities($title);
1517        } else {
1518            // just show the sourcename
1519            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
1520        }
1521
1522        return $ret;
1523    }
1524
1525    /**
1526     * Escape string for output
1527     *
1528     * @param $string
1529     * @return string
1530     */
1531    function _xmlEntities($string) {
1532        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1533    }
1534
1535    /**
1536     * Creates a linkid from a headline
1537     *
1538     * @author Andreas Gohr <andi@splitbrain.org>
1539     * @param string  $title   The headline title
1540     * @param boolean $create  Create a new unique ID?
1541     * @return string
1542     */
1543    function _headerToLink($title, $create = false) {
1544        if($create) {
1545            return sectionID($title, $this->headers);
1546        } else {
1547            $check = false;
1548            return sectionID($title, $check);
1549        }
1550    }
1551
1552    /**
1553     * Construct a title and handle images in titles
1554     *
1555     * @author Harry Fuecks <hfuecks@gmail.com>
1556     * @param string|array $title    either string title or media array
1557     * @param string       $default  default title if nothing else is found
1558     * @param bool         $isImage  will be set to true if it's a media file
1559     * @param null|string  $id       linked page id (used to extract title from first heading)
1560     * @param string       $linktype content|navigation
1561     * @return string      HTML of the title, might be full image tag or just escaped text
1562     */
1563    function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1564        $isImage = false;
1565        if(is_array($title)) {
1566            $isImage = true;
1567            return $this->_imageTitle($title);
1568        } elseif(is_null($title) || trim($title) == '') {
1569            if(useHeading($linktype) && $id) {
1570                $heading = p_get_first_heading($id);
1571                if($heading) {
1572                    return $this->_xmlEntities($heading);
1573                }
1574            }
1575            return $this->_xmlEntities($default);
1576        } else {
1577            return $this->_xmlEntities($title);
1578        }
1579    }
1580
1581    /**
1582     * Returns HTML code for images used in link titles
1583     *
1584     * @author Andreas Gohr <andi@splitbrain.org>
1585     * @param array $img
1586     * @return string HTML img tag or similar
1587     */
1588    function _imageTitle($img) {
1589        global $ID;
1590
1591        // some fixes on $img['src']
1592        // see internalmedia() and externalmedia()
1593        list($img['src']) = explode('#', $img['src'], 2);
1594        if($img['type'] == 'internalmedia') {
1595            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
1596        }
1597
1598        return $this->_media(
1599            $img['src'],
1600            $img['title'],
1601            $img['align'],
1602            $img['width'],
1603            $img['height'],
1604            $img['cache']
1605        );
1606    }
1607
1608    /**
1609     * helperfunction to return a basic link to a media
1610     *
1611     * used in internalmedia() and externalmedia()
1612     *
1613     * @author   Pierre Spring <pierre.spring@liip.ch>
1614     * @param string $src       media ID
1615     * @param string $title     descriptive text
1616     * @param string $align     left|center|right
1617     * @param int    $width     width of media in pixel
1618     * @param int    $height    height of media in pixel
1619     * @param string $cache     cache|recache|nocache
1620     * @param bool   $render    should the media be embedded inline or just linked
1621     * @return array associative array with link config
1622     */
1623    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1624        global $conf;
1625
1626        $link           = array();
1627        $link['class']  = 'media';
1628        $link['style']  = '';
1629        $link['pre']    = '';
1630        $link['suf']    = '';
1631        $link['more']   = '';
1632        $link['target'] = $conf['target']['media'];
1633        $link['title']  = $this->_xmlEntities($src);
1634        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1635
1636        return $link;
1637    }
1638
1639    /**
1640     * Embed video(s) in HTML
1641     *
1642     * @author Anika Henke <anika@selfthinker.org>
1643     *
1644     * @param string $src         - ID of video to embed
1645     * @param int    $width       - width of the video in pixels
1646     * @param int    $height      - height of the video in pixels
1647     * @param array  $atts        - additional attributes for the <video> tag
1648     * @return string
1649     */
1650    function _video($src, $width, $height, $atts = null) {
1651        // prepare width and height
1652        if(is_null($atts)) $atts = array();
1653        $atts['width']  = (int) $width;
1654        $atts['height'] = (int) $height;
1655        if(!$atts['width']) $atts['width'] = 320;
1656        if(!$atts['height']) $atts['height'] = 240;
1657
1658        $posterUrl = '';
1659        $files = array();
1660        $isExternal = media_isexternal($src);
1661
1662        if ($isExternal) {
1663            // take direct source for external files
1664            list(/*ext*/, $srcMime) = mimetype($src);
1665            $files[$srcMime] = $src;
1666        } else {
1667            // prepare alternative formats
1668            $extensions   = array('webm', 'ogv', 'mp4');
1669            $files        = media_alternativefiles($src, $extensions);
1670            $poster       = media_alternativefiles($src, array('jpg', 'png'));
1671            if(!empty($poster)) {
1672                $posterUrl = ml(reset($poster), '', true, '&');
1673            }
1674        }
1675
1676        $out = '';
1677        // open video tag
1678        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1679        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1680        $out .= '>'.NL;
1681        $fallback = '';
1682
1683        // output source for each alternative video format
1684        foreach($files as $mime => $file) {
1685            if ($isExternal) {
1686                $url = $file;
1687                $linkType = 'externalmedia';
1688            } else {
1689                $url = ml($file, '', true, '&');
1690                $linkType = 'internalmedia';
1691            }
1692            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1693
1694            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1695            // alternative content (just a link to the file)
1696            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1697        }
1698
1699        // finish
1700        $out .= $fallback;
1701        $out .= '</video>'.NL;
1702        return $out;
1703    }
1704
1705    /**
1706     * Embed audio in HTML
1707     *
1708     * @author Anika Henke <anika@selfthinker.org>
1709     *
1710     * @param string $src       - ID of audio to embed
1711     * @param array  $atts      - additional attributes for the <audio> tag
1712     * @return string
1713     */
1714    function _audio($src, $atts = array()) {
1715        $files = array();
1716        $isExternal = media_isexternal($src);
1717
1718        if ($isExternal) {
1719            // take direct source for external files
1720            list(/*ext*/, $srcMime) = mimetype($src);
1721            $files[$srcMime] = $src;
1722        } else {
1723            // prepare alternative formats
1724            $extensions   = array('ogg', 'mp3', 'wav');
1725            $files        = media_alternativefiles($src, $extensions);
1726        }
1727
1728        $out = '';
1729        // open audio tag
1730        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1731        $fallback = '';
1732
1733        // output source for each alternative audio format
1734        foreach($files as $mime => $file) {
1735            if ($isExternal) {
1736                $url = $file;
1737                $linkType = 'externalmedia';
1738            } else {
1739                $url = ml($file, '', true, '&');
1740                $linkType = 'internalmedia';
1741            }
1742            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1743
1744            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1745            // alternative content (just a link to the file)
1746            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1747        }
1748
1749        // finish
1750        $out .= $fallback;
1751        $out .= '</audio>'.NL;
1752        return $out;
1753    }
1754
1755    /**
1756     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1757     * which returns an existing media revision less or equal to rev or date_at
1758     *
1759     * @author lisps
1760     * @param string $media_id
1761     * @access protected
1762     * @return string revision ('' for current)
1763     */
1764    function _getLastMediaRevisionAt($media_id){
1765        if(!$this->date_at || media_isexternal($media_id)) return '';
1766        $pagelog = new MediaChangeLog($media_id);
1767        return $pagelog->getLastRevisionAt($this->date_at);
1768    }
1769
1770    #endregion
1771}
1772
1773//Setup VIM: ex: et ts=4 :
1774