[ 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 int last section edit id, used by startSectionEdit */
  24      protected $lastsecid = 0;
  25  
  26      /** @var array a list of footnotes, list starts at 1! */
  27      protected $footnotes = array();
  28  
  29      /** @var int current section level */
  30      protected $lastlevel = 0;
  31      /** @var array section node tracker */
  32      protected $node = array(0, 0, 0, 0, 0);
  33  
  34      /** @var string temporary $doc store */
  35      protected $store = '';
  36  
  37      /** @var array global counter, for table classes etc. */
  38      protected $_counter = array(); //
  39  
  40      /** @var int counts the code and file blocks, used to provide download links */
  41      protected $_codeblock = 0;
  42  
  43      /** @var array list of allowed URL schemes */
  44      protected $schemes = null;
  45  
  46      /**
  47       * Register a new edit section range
  48       *
  49       * @param int    $start  The byte position for the edit start
  50       * @param array  $data   Associative array with section data:
  51       *                       Key 'name': the section name/title
  52       *                       Key 'target': the target for the section edit,
  53       *                                     e.g. 'section' or 'table'
  54       *                       Key 'hid': header id
  55       *                       Key 'codeblockOffset': actual code block index
  56       *                       Key 'start': set in startSectionEdit(),
  57       *                                    do not set yourself
  58       *                       Key 'range': calculated from 'start' and
  59       *                                    $key in finishSectionEdit(),
  60       *                                    do not set yourself
  61       * @return string  A marker class for the starting HTML element
  62       *
  63       * @author Adrian Lang <lang@cosmocode.de>
  64       */
  65      public function startSectionEdit($start, $data) {
  66          if (!is_array($data)) {
  67              msg(
  68                  sprintf(
  69                      'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.',
  70                      hsc((string) $data)
  71                  ), -1
  72              );
  73  
  74              // @deprecated 2018-04-14, backward compatibility
  75              $args = func_get_args();
  76              $data = array();
  77              if(isset($args[1])) $data['target'] = $args[1];
  78              if(isset($args[2])) $data['name'] = $args[2];
  79              if(isset($args[3])) $data['hid'] = $args[3];
  80          }
  81          $data['secid'] = ++$this->lastsecid;
  82          $data['start'] = $start;
  83          $this->sectionedits[] = $data;
  84          return 'sectionedit'.$data['secid'];
  85      }
  86  
  87      /**
  88       * Finish an edit section range
  89       *
  90       * @param int  $end     The byte position for the edit end; null for the rest of the page
  91       *
  92       * @author Adrian Lang <lang@cosmocode.de>
  93       */
  94      public function finishSectionEdit($end = null, $hid = null) {
  95          if(count($this->sectionedits) == 0) {
  96              return;
  97          }
  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  
1284                  $lnkurl = $item->get_permalink();
1285                  $title = html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8');
1286  
1287                  // support feeds without links
1288                  if($lnkurl) {
1289                      $this->externallink($item->get_permalink(), $title);
1290                  } else {
1291                      $this->doc .= ' '.hsc($item->get_title());
1292                  }
1293                  if($params['author']) {
1294                      $author = $item->get_author(0);
1295                      if($author) {
1296                          $name = $author->get_name();
1297                          if(!$name) $name = $author->get_email();
1298                          if($name) $this->doc .= ' '.$lang['by'].' '.hsc($name);
1299                      }
1300                  }
1301                  if($params['date']) {
1302                      $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
1303                  }
1304                  if($params['details']) {
1305                      $desc = $item->get_description();
1306                      $desc = strip_tags($desc);
1307                      $desc = html_entity_decode($desc, ENT_QUOTES, 'UTF-8');
1308                      $this->doc .= '<div class="detail">';
1309                      $this->doc .= hsc($desc);
1310                      $this->doc .= '</div>';
1311                  }
1312  
1313                  $this->doc .= '</div></li>';
1314              }
1315          } else {
1316              $this->doc .= '<li><div class="li">';
1317              $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
1318              $this->externallink($url);
1319              if($conf['allowdebug']) {
1320                  $this->doc .= '<!--'.hsc($feed->error).'-->';
1321              }
1322              $this->doc .= '</div></li>';
1323          }
1324          $this->doc .= '</ul>';
1325      }
1326  
1327      /**
1328       * Start a table
1329       *
1330       * @param int $maxcols maximum number of columns
1331       * @param int $numrows NOT IMPLEMENTED
1332       * @param int $pos byte position in the original source
1333       * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1334       */
1335      public function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) {
1336          // initialize the row counter used for classes
1337          $this->_counter['row_counter'] = 0;
1338          $class                         = 'table';
1339          if($classes !== null) {
1340              if(is_array($classes)) $classes = join(' ', $classes);
1341              $class .= ' ' . $classes;
1342          }
1343          if($pos !== null) {
1344              $hid = $this->_headerToLink($class, true);
1345              $data = array();
1346              $data['target'] = 'table';
1347              $data['name'] = '';
1348              $data['hid'] = $hid;
1349              $class .= ' '.$this->startSectionEdit($pos, $data);
1350          }
1351          $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1352              DOKU_LF;
1353      }
1354  
1355      /**
1356       * Close a table
1357       *
1358       * @param int $pos byte position in the original source
1359       */
1360      public function table_close($pos = null) {
1361          $this->doc .= '</table></div>'.DOKU_LF;
1362          if($pos !== null) {
1363              $this->finishSectionEdit($pos);
1364          }
1365      }
1366  
1367      /**
1368       * Open a table header
1369       */
1370      public function tablethead_open() {
1371          $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1372      }
1373  
1374      /**
1375       * Close a table header
1376       */
1377      public function tablethead_close() {
1378          $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1379      }
1380  
1381      /**
1382       * Open a table body
1383       */
1384      public function tabletbody_open() {
1385          $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF;
1386      }
1387  
1388      /**
1389       * Close a table body
1390       */
1391      public function tabletbody_close() {
1392          $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF;
1393      }
1394  
1395      /**
1396       * Open a table footer
1397       */
1398      public function tabletfoot_open() {
1399          $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF;
1400      }
1401  
1402      /**
1403       * Close a table footer
1404       */
1405      public function tabletfoot_close() {
1406          $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF;
1407      }
1408  
1409      /**
1410       * Open a table row
1411       *
1412       * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1413       */
1414      public function tablerow_open($classes = null) {
1415          // initialize the cell counter used for classes
1416          $this->_counter['cell_counter'] = 0;
1417          $class                          = 'row'.$this->_counter['row_counter']++;
1418          if($classes !== null) {
1419              if(is_array($classes)) $classes = join(' ', $classes);
1420              $class .= ' ' . $classes;
1421          }
1422          $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
1423      }
1424  
1425      /**
1426       * Close a table row
1427       */
1428      public function tablerow_close() {
1429          $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
1430      }
1431  
1432      /**
1433       * Open a table header cell
1434       *
1435       * @param int    $colspan
1436       * @param string $align left|center|right
1437       * @param int    $rowspan
1438       * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1439       */
1440      public function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1441          $class = 'class="col'.$this->_counter['cell_counter']++;
1442          if(!is_null($align)) {
1443              $class .= ' '.$align.'align';
1444          }
1445          if($classes !== null) {
1446              if(is_array($classes)) $classes = join(' ', $classes);
1447              $class .= ' ' . $classes;
1448          }
1449          $class .= '"';
1450          $this->doc .= '<th '.$class;
1451          if($colspan > 1) {
1452              $this->_counter['cell_counter'] += $colspan - 1;
1453              $this->doc .= ' colspan="'.$colspan.'"';
1454          }
1455          if($rowspan > 1) {
1456              $this->doc .= ' rowspan="'.$rowspan.'"';
1457          }
1458          $this->doc .= '>';
1459      }
1460  
1461      /**
1462       * Close a table header cell
1463       */
1464      public function tableheader_close() {
1465          $this->doc .= '</th>';
1466      }
1467  
1468      /**
1469       * Open a table cell
1470       *
1471       * @param int       $colspan
1472       * @param string    $align left|center|right
1473       * @param int       $rowspan
1474       * @param string|string[]    $classes css classes - have to be valid, do not pass unfiltered user input
1475       */
1476      public function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1477          $class = 'class="col'.$this->_counter['cell_counter']++;
1478          if(!is_null($align)) {
1479              $class .= ' '.$align.'align';
1480          }
1481          if($classes !== null) {
1482              if(is_array($classes)) $classes = join(' ', $classes);
1483              $class .= ' ' . $classes;
1484          }
1485          $class .= '"';
1486          $this->doc .= '<td '.$class;
1487          if($colspan > 1) {
1488              $this->_counter['cell_counter'] += $colspan - 1;
1489              $this->doc .= ' colspan="'.$colspan.'"';
1490          }
1491          if($rowspan > 1) {
1492              $this->doc .= ' rowspan="'.$rowspan.'"';
1493          }
1494          $this->doc .= '>';
1495      }
1496  
1497      /**
1498       * Close a table cell
1499       */
1500      public function tablecell_close() {
1501          $this->doc .= '</td>';
1502      }
1503  
1504      /**
1505       * Returns the current header level.
1506       * (required e.g. by the filelist plugin)
1507       *
1508       * @return int The current header level
1509       */
1510      public function getLastlevel() {
1511          return $this->lastlevel;
1512      }
1513  
1514      #region Utility functions
1515  
1516      /**
1517       * Build a link
1518       *
1519       * Assembles all parts defined in $link returns HTML for the link
1520       *
1521       * @param array $link attributes of a link
1522       * @return string
1523       *
1524       * @author Andreas Gohr <andi@splitbrain.org>
1525       */
1526      public function _formatLink($link) {
1527          //make sure the url is XHTML compliant (skip mailto)
1528          if(substr($link['url'], 0, 7) != 'mailto:') {
1529              $link['url'] = str_replace('&', '&amp;', $link['url']);
1530              $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1531          }
1532          //remove double encodings in titles
1533          $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1534  
1535          // be sure there are no bad chars in url or title
1536          // (we can't do this for name because it can contain an img tag)
1537          $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1538          $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1539  
1540          $ret = '';
1541          $ret .= $link['pre'];
1542          $ret .= '<a href="'.$link['url'].'"';
1543          if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1544          if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1545          if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1546          if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1547          if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"';
1548          if(!empty($link['more'])) $ret .= ' '.$link['more'];
1549          $ret .= '>';
1550          $ret .= $link['name'];
1551          $ret .= '</a>';
1552          $ret .= $link['suf'];
1553          return $ret;
1554      }
1555  
1556      /**
1557       * Renders internal and external media
1558       *
1559       * @author Andreas Gohr <andi@splitbrain.org>
1560       * @param string $src       media ID
1561       * @param string $title     descriptive text
1562       * @param string $align     left|center|right
1563       * @param int    $width     width of media in pixel
1564       * @param int    $height    height of media in pixel
1565       * @param string $cache     cache|recache|nocache
1566       * @param bool   $render    should the media be embedded inline or just linked
1567       * @return string
1568       */
1569      public function _media($src, $title = null, $align = null, $width = null,
1570                      $height = null, $cache = null, $render = true) {
1571  
1572          $ret = '';
1573  
1574          list($ext, $mime) = mimetype($src);
1575          if(substr($mime, 0, 5) == 'image') {
1576              // first get the $title
1577              if(!is_null($title)) {
1578                  $title = $this->_xmlEntities($title);
1579              } elseif($ext == 'jpg' || $ext == 'jpeg') {
1580                  //try to use the caption from IPTC/EXIF
1581                  require_once (DOKU_INC.'inc/JpegMeta.php');
1582                  $jpeg = new JpegMeta(mediaFN($src));
1583                  if($jpeg !== false) $cap = $jpeg->getTitle();
1584                  if(!empty($cap)) {
1585                      $title = $this->_xmlEntities($cap);
1586                  }
1587              }
1588              if(!$render) {
1589                  // if the picture is not supposed to be rendered
1590                  // return the title of the picture
1591                  if($title === null || $title === "") {
1592                      // just show the sourcename
1593                      $title = $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
1594                  }
1595                  return $title;
1596              }
1597              //add image tag
1598              $ret .= '<img src="' . ml(
1599                      $src,
1600                      array(
1601                          'w' => $width, 'h' => $height,
1602                          'cache' => $cache,
1603                          'rev' => $this->_getLastMediaRevisionAt($src)
1604                      )
1605                  ) . '"';
1606              $ret .= ' class="media'.$align.'"';
1607              $ret .= ' loading="lazy"';
1608  
1609              if($title) {
1610                  $ret .= ' title="'.$title.'"';
1611                  $ret .= ' alt="'.$title.'"';
1612              } else {
1613                  $ret .= ' alt=""';
1614              }
1615  
1616              if(!is_null($width))
1617                  $ret .= ' width="'.$this->_xmlEntities($width).'"';
1618  
1619              if(!is_null($height))
1620                  $ret .= ' height="'.$this->_xmlEntities($height).'"';
1621  
1622              $ret .= ' />';
1623  
1624          } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1625              // first get the $title
1626              $title = !is_null($title) ? $title : false;
1627              if(!$render) {
1628                  // if the file is not supposed to be rendered
1629                  // return the title of the file (just the sourcename if there is no title)
1630                  return $this->_xmlEntities($title ? $title : \dokuwiki\Utf8\PhpString::basename(noNS($src)));
1631              }
1632  
1633              $att          = array();
1634              $att['class'] = "media$align";
1635              if($title) {
1636                  $att['title'] = $title;
1637              }
1638  
1639              if(media_supportedav($mime, 'video')) {
1640                  //add video
1641                  $ret .= $this->_video($src, $width, $height, $att);
1642              }
1643              if(media_supportedav($mime, 'audio')) {
1644                  //add audio
1645                  $ret .= $this->_audio($src, $att);
1646              }
1647  
1648          } elseif($mime == 'application/x-shockwave-flash') {
1649              if(!$render) {
1650                  // if the flash is not supposed to be rendered
1651                  // return the title of the flash
1652                  if(!$title) {
1653                      // just show the sourcename
1654                      $title = \dokuwiki\Utf8\PhpString::basename(noNS($src));
1655                  }
1656                  return $this->_xmlEntities($title);
1657              }
1658  
1659              $att          = array();
1660              $att['class'] = "media$align";
1661              if($align == 'right') $att['align'] = 'right';
1662              if($align == 'left') $att['align'] = 'left';
1663              $ret .= html_flashobject(
1664                  ml($src, array('cache' => $cache), true, '&'), $width, $height,
1665                  array('quality' => 'high'),
1666                  null,
1667                  $att,
1668                  $this->_xmlEntities($title)
1669              );
1670          } elseif($title) {
1671              // well at least we have a title to display
1672              $ret .= $this->_xmlEntities($title);
1673          } else {
1674              // just show the sourcename
1675              $ret .= $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
1676          }
1677  
1678          return $ret;
1679      }
1680  
1681      /**
1682       * Escape string for output
1683       *
1684       * @param $string
1685       * @return string
1686       */
1687      public function _xmlEntities($string) {
1688          return hsc($string);
1689      }
1690  
1691  
1692  
1693      /**
1694       * Construct a title and handle images in titles
1695       *
1696       * @author Harry Fuecks <hfuecks@gmail.com>
1697       * @param string|array $title    either string title or media array
1698       * @param string       $default  default title if nothing else is found
1699       * @param bool         $isImage  will be set to true if it's a media file
1700       * @param null|string  $id       linked page id (used to extract title from first heading)
1701       * @param string       $linktype content|navigation
1702       * @return string      HTML of the title, might be full image tag or just escaped text
1703       */
1704      public function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1705          $isImage = false;
1706          if(is_array($title)) {
1707              $isImage = true;
1708              return $this->_imageTitle($title);
1709          } elseif(is_null($title) || trim($title) == '') {
1710              if(useHeading($linktype) && $id) {
1711                  $heading = p_get_first_heading($id);
1712                  if(!blank($heading)) {
1713                      return $this->_xmlEntities($heading);
1714                  }
1715              }
1716              return $this->_xmlEntities($default);
1717          } else {
1718              return $this->_xmlEntities($title);
1719          }
1720      }
1721  
1722      /**
1723       * Returns HTML code for images used in link titles
1724       *
1725       * @author Andreas Gohr <andi@splitbrain.org>
1726       * @param array $img
1727       * @return string HTML img tag or similar
1728       */
1729      public function _imageTitle($img) {
1730          global $ID;
1731  
1732          // some fixes on $img['src']
1733          // see internalmedia() and externalmedia()
1734          list($img['src']) = explode('#', $img['src'], 2);
1735          if($img['type'] == 'internalmedia') {
1736              $img['src'] = (new MediaResolver($ID))->resolveId($img['src'], $this->date_at, true);
1737          }
1738  
1739          return $this->_media(
1740              $img['src'],
1741              $img['title'],
1742              $img['align'],
1743              $img['width'],
1744              $img['height'],
1745              $img['cache']
1746          );
1747      }
1748  
1749      /**
1750       * helperfunction to return a basic link to a media
1751       *
1752       * used in internalmedia() and externalmedia()
1753       *
1754       * @author   Pierre Spring <pierre.spring@liip.ch>
1755       * @param string $src       media ID
1756       * @param string $title     descriptive text
1757       * @param string $align     left|center|right
1758       * @param int    $width     width of media in pixel
1759       * @param int    $height    height of media in pixel
1760       * @param string $cache     cache|recache|nocache
1761       * @param bool   $render    should the media be embedded inline or just linked
1762       * @return array associative array with link config
1763       */
1764      public function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1765          global $conf;
1766  
1767          $link           = array();
1768          $link['class']  = 'media';
1769          $link['style']  = '';
1770          $link['pre']    = '';
1771          $link['suf']    = '';
1772          $link['more']   = '';
1773          $link['target'] = $conf['target']['media'];
1774          if($conf['target']['media']) $link['rel'] = 'noopener';
1775          $link['title']  = $this->_xmlEntities($src);
1776          $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1777  
1778          return $link;
1779      }
1780  
1781      /**
1782       * Embed video(s) in HTML
1783       *
1784       * @author Anika Henke <anika@selfthinker.org>
1785       * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
1786       *
1787       * @param string $src         - ID of video to embed
1788       * @param int    $width       - width of the video in pixels
1789       * @param int    $height      - height of the video in pixels
1790       * @param array  $atts        - additional attributes for the <video> tag
1791       * @return string
1792       */
1793      public function _video($src, $width, $height, $atts = null) {
1794          // prepare width and height
1795          if(is_null($atts)) $atts = array();
1796          $atts['width']  = (int) $width;
1797          $atts['height'] = (int) $height;
1798          if(!$atts['width']) $atts['width'] = 320;
1799          if(!$atts['height']) $atts['height'] = 240;
1800  
1801          $posterUrl = '';
1802          $files = array();
1803          $tracks = array();
1804          $isExternal = media_isexternal($src);
1805  
1806          if ($isExternal) {
1807              // take direct source for external files
1808              list(/*ext*/, $srcMime) = mimetype($src);
1809              $files[$srcMime] = $src;
1810          } else {
1811              // prepare alternative formats
1812              $extensions   = array('webm', 'ogv', 'mp4');
1813              $files        = media_alternativefiles($src, $extensions);
1814              $poster       = media_alternativefiles($src, array('jpg', 'png'));
1815              $tracks       = media_trackfiles($src);
1816              if(!empty($poster)) {
1817                  $posterUrl = ml(reset($poster), '', true, '&');
1818              }
1819          }
1820  
1821          $out = '';
1822          // open video tag
1823          $out .= '<video '.buildAttributes($atts).' controls="controls"';
1824          if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1825          $out .= '>'.NL;
1826          $fallback = '';
1827  
1828          // output source for each alternative video format
1829          foreach($files as $mime => $file) {
1830              if ($isExternal) {
1831                  $url = $file;
1832                  $linkType = 'externalmedia';
1833              } else {
1834                  $url = ml($file, '', true, '&');
1835                  $linkType = 'internalmedia';
1836              }
1837              $title = !empty($atts['title'])
1838                  ? $atts['title']
1839                  : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
1840  
1841              $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1842              // alternative content (just a link to the file)
1843              $fallback .= $this->$linkType(
1844                  $file,
1845                  $title,
1846                  null,
1847                  null,
1848                  null,
1849                  $cache = null,
1850                  $linking = 'linkonly',
1851                  $return = true
1852              );
1853          }
1854  
1855          // output each track if any
1856          foreach( $tracks as $trackid => $info ) {
1857              list( $kind, $srclang ) = array_map( 'hsc', $info );
1858              $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
1859              $out .= "label=\"$srclang\" ";
1860              $out .= 'src="'.ml($trackid, '', true).'">'.NL;
1861          }
1862  
1863          // finish
1864          $out .= $fallback;
1865          $out .= '</video>'.NL;
1866          return $out;
1867      }
1868  
1869      /**
1870       * Embed audio in HTML
1871       *
1872       * @author Anika Henke <anika@selfthinker.org>
1873       *
1874       * @param string $src       - ID of audio to embed
1875       * @param array  $atts      - additional attributes for the <audio> tag
1876       * @return string
1877       */
1878      public function _audio($src, $atts = array()) {
1879          $files = array();
1880          $isExternal = media_isexternal($src);
1881  
1882          if ($isExternal) {
1883              // take direct source for external files
1884              list(/*ext*/, $srcMime) = mimetype($src);
1885              $files[$srcMime] = $src;
1886          } else {
1887              // prepare alternative formats
1888              $extensions   = array('ogg', 'mp3', 'wav');
1889              $files        = media_alternativefiles($src, $extensions);
1890          }
1891  
1892          $out = '';
1893          // open audio tag
1894          $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1895          $fallback = '';
1896  
1897          // output source for each alternative audio format
1898          foreach($files as $mime => $file) {
1899              if ($isExternal) {
1900                  $url = $file;
1901                  $linkType = 'externalmedia';
1902              } else {
1903                  $url = ml($file, '', true, '&');
1904                  $linkType = 'internalmedia';
1905              }
1906              $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
1907  
1908              $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1909              // alternative content (just a link to the file)
1910              $fallback .= $this->$linkType(
1911                  $file,
1912                  $title,
1913                  null,
1914                  null,
1915                  null,
1916                  $cache = null,
1917                  $linking = 'linkonly',
1918                  $return = true
1919              );
1920          }
1921  
1922          // finish
1923          $out .= $fallback;
1924          $out .= '</audio>'.NL;
1925          return $out;
1926      }
1927  
1928      /**
1929       * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1930       * which returns an existing media revision less or equal to rev or date_at
1931       *
1932       * @author lisps
1933       * @param string $media_id
1934       * @access protected
1935       * @return string revision ('' for current)
1936       */
1937      protected function _getLastMediaRevisionAt($media_id) {
1938          if (!$this->date_at || media_isexternal($media_id)) return '';
1939          $changelog = new MediaChangeLog($media_id);
1940          return $changelog->getLastRevisionAt($this->date_at);
1941      }
1942  
1943      #endregion
1944  }
1945  
1946  //Setup VIM: ex: et ts=4 :