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