[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/parser/ -> xhtml.php (source)

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