Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]

   1  <?php
   2  
   3  /**
   4   * Generic & abstract parser functions & skeleton. It has some functions & generic stuff.
   5   *
   6   * @author Josep ArĂºs
   7   *
   8   * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
   9   * @package mod_wiki
  10   */
  11  
  12  abstract class wiki_markup_parser extends generic_parser {
  13  
  14      protected $pretty_print = false;
  15      protected $printable = false;
  16  
  17      //page id
  18      protected $wiki_page_id;
  19  
  20      //sections
  21      protected $repeated_sections;
  22  
  23      protected $section_editing = true;
  24  
  25      //header & ToC
  26      protected $toc = array();
  27      protected $maxheaderdepth = 3;
  28  
  29      /**
  30       * function wiki_parser_link_callback($link = "")
  31       *
  32       * Returns array('content' => "Inside the link", 'url' => "http://url.com/Wiki/Entry", 'new' => false).
  33       */
  34      private $linkgeneratorcallback = array('parser_utils', 'wiki_parser_link_callback');
  35      private $linkgeneratorcallbackargs = array();
  36  
  37      /**
  38       * Table generator callback
  39       */
  40  
  41      private $tablegeneratorcallback = array('parser_utils', 'wiki_parser_table_callback');
  42  
  43      /**
  44       * Get real path from relative path
  45       */
  46      private $realpathcallback = array('parser_utils', 'wiki_parser_real_path');
  47      private $realpathcallbackargs = array();
  48  
  49      /**
  50       * Before and after parsing...
  51       */
  52  
  53      protected function before_parsing() {
  54          $this->toc = array();
  55  
  56          $this->string = preg_replace('/\r\n/', "\n", $this->string);
  57          $this->string = preg_replace('/\r/', "\n", $this->string);
  58  
  59          $this->string .= "\n\n";
  60  
  61          if (!$this->printable && $this->section_editing) {
  62              $this->returnvalues['unparsed_text'] = $this->string;
  63              $this->string = $this->get_repeated_sections($this->string);
  64          }
  65      }
  66  
  67      protected function after_parsing() {
  68          if (!$this->printable) {
  69              $this->returnvalues['repeated_sections'] = array_unique($this->returnvalues['repeated_sections']);
  70          }
  71  
  72          $this->process_toc();
  73  
  74          $this->string = preg_replace("/\n\s/", "\n", $this->string);
  75          $this->string = preg_replace("/\n{2,}/", "\n", $this->string);
  76          $this->string = trim($this->string);
  77          $this->string .= "\n";
  78      }
  79  
  80      /**
  81       * Set options
  82       */
  83  
  84      protected function set_options($options) {
  85          parent::set_options($options);
  86  
  87          $this->returnvalues['link_count'] = array();
  88          $this->returnvalues['repeated_sections'] = array();
  89          $this->returnvalues['toc'] = "";
  90  
  91          foreach ($options as $name => $o) {
  92              switch ($name) {
  93              case 'link_callback':
  94                  $callback = explode(':', $o);
  95  
  96                  global $CFG;
  97                  require_once($CFG->dirroot . $callback[0]);
  98  
  99                  if (function_exists($callback[1])) {
 100                      $this->linkgeneratorcallback = $callback[1];
 101                  }
 102                  break;
 103              case 'link_callback_args':
 104                  if (is_array($o)) {
 105                      $this->linkgeneratorcallbackargs = $o;
 106                  }
 107                  break;
 108              case 'real_path_callback':
 109                  $callback = explode(':', $o);
 110  
 111                  global $CFG;
 112                  require_once($CFG->dirroot . $callback[0]);
 113  
 114                  if (function_exists($callback[1])) {
 115                      $this->realpathcallback = $callback[1];
 116                  }
 117                  break;
 118              case 'real_path_callback_args':
 119                  if (is_array($o)) {
 120                      $this->realpathcallbackargs = $o;
 121                  }
 122                  break;
 123              case 'table_callback':
 124                  $callback = explode(':', $o);
 125  
 126                  global $CFG;
 127                  require_once($CFG->dirroot . $callback[0]);
 128  
 129                  if (function_exists($callback[1])) {
 130                      $this->tablegeneratorcallback = $callback[1];
 131                  }
 132                  break;
 133              case 'pretty_print':
 134                  if ($o) {
 135                      $this->pretty_print = true;
 136                  }
 137                  break;
 138              case 'pageid':
 139                  $this->wiki_page_id = $o;
 140                  break;
 141              case 'printable':
 142                  if ($o) {
 143                      $this->printable = true;
 144                  }
 145                  break;
 146              }
 147          }
 148      }
 149  
 150      /**
 151       * Generic block rules
 152       */
 153  
 154      protected function line_break_block_rule($match) {
 155          return '<hr />';
 156      }
 157  
 158      protected function list_block_rule($match) {
 159          preg_match_all("/^\ *([\*\#]{1,5})\ *((?:[^\n]|\n(?!(?:\ *[\*\#])|\n))+)/im", $match[1], $listitems, PREG_SET_ORDER);
 160  
 161          return $this->process_block_list($listitems) . $match[2];
 162      }
 163  
 164      protected function nowiki_block_rule($match) {
 165          return parser_utils::h('pre', $this->protect($match[1]));
 166      }
 167  
 168      /**
 169       * Generic tag rules
 170       */
 171  
 172      protected function nowiki_tag_rule($match) {
 173          return parser_utils::h('tt', $this->protect($match[1]));
 174      }
 175  
 176      /**
 177       * Header generation
 178       */
 179  
 180      protected function generate_header($text, $level) {
 181          $toctext = $text = trim($text);
 182  
 183          if (!$this->pretty_print && $level == 1) {
 184              $editlink = '[' . get_string('editsection', 'wiki') . ']';
 185              $url = array('href' => "edit.php?pageid={$this->wiki_page_id}&section=" . urlencode($text),
 186                  'class' => 'wiki_edit_section');
 187              $text .= ' ' . parser_utils::h('a', $this->protect($editlink), $url);
 188              $toctext .= ' ' . parser_utils::h('a', $editlink, $url);
 189          }
 190  
 191          if ($level <= $this->maxheaderdepth) {
 192              $this->toc[] = array($level, $toctext);
 193              $num = count($this->toc);
 194              $text = parser_utils::h('a', "", array('name' => "toc-$num")) . $text;
 195          }
 196  
 197          // Display headers as <h3> and lower for accessibility.
 198          return parser_utils::h('h' . min(6, $level + 2), $text) . "\n\n";
 199      }
 200  
 201      /**
 202       * Table of contents processing after parsing
 203       */
 204      protected function process_toc() {
 205          if (empty($this->toc)) {
 206              return;
 207          }
 208  
 209          $toc = "";
 210          $currentsection = array(0, 0, 0);
 211          $i = 1;
 212          foreach ($this->toc as & $header) {
 213              switch ($header[0]) {
 214              case 1:
 215                  $currentsection = array($currentsection[0] + 1, 0, 0);
 216                  break;
 217              case 2:
 218                  $currentsection[1]++;
 219                  $currentsection[2] = 0;
 220                  if ($currentsection[0] == 0) {
 221                      $currentsection[0]++;
 222                  }
 223                  break;
 224              case 3:
 225                  $currentsection[2]++;
 226                  if ($currentsection[1] == 0) {
 227                      $currentsection[1]++;
 228                  }
 229                  if ($currentsection[0] == 0) {
 230                      $currentsection[0]++;
 231                  }
 232                  break;
 233              default:
 234                  continue 2;
 235              }
 236              $number = "$currentsection[0]";
 237              if (!empty($currentsection[1])) {
 238                  $number .= ".$currentsection[1]";
 239                  if (!empty($currentsection[2])) {
 240                      $number .= ".$currentsection[2]";
 241                  }
 242              }
 243              $toc .= parser_utils::h('p', $number . ". " .
 244                 parser_utils::h('a', str_replace(array('[[', ']]'), '', $header[1]), array('href' => "#toc-$i")),
 245                 array('class' => 'wiki-toc-section-' . $header[0] . " wiki-toc-section"));
 246              $i++;
 247          }
 248  
 249          $this->returnvalues['toc'] = "<div class=\"wiki-toc\"><p class=\"wiki-toc-title\">" . get_string('tableofcontents', 'wiki') . "</p>$toc</div>";
 250      }
 251  
 252      /**
 253       * List helpers
 254       */
 255  
 256      private function process_block_list($listitems) {
 257          $list = array();
 258          foreach ($listitems as $li) {
 259              $text = str_replace("\n", "", $li[2]);
 260              $this->rules($text);
 261  
 262              if ($li[1][0] == '*') {
 263                  $type = 'ul';
 264              } else {
 265                  $type = 'ol';
 266              }
 267  
 268              $list[] = array(strlen($li[1]), $text, $type);
 269          }
 270          $type = $list[0][2];
 271          return "<$type>" . "\n" . $this->generate_list($list) . "\n" . "</$type>" . "\n";
 272      }
 273  
 274      /**
 275       * List generation function from an array of array(level, text)
 276       */
 277  
 278      protected function generate_list($listitems) {
 279          $list = "";
 280          $current_depth = 1;
 281          $next_depth = 1;
 282          $liststack = array();
 283          for ($lc = 0; $lc < count($listitems) && $next_depth; $lc++) {
 284              $cli = $listitems[$lc];
 285              $nli = isset($listitems[$lc + 1]) ? $listitems[$lc + 1] : null;
 286  
 287              $text = $cli[1];
 288  
 289              $current_depth = $next_depth;
 290              $next_depth = $nli ? $nli[0] : null;
 291  
 292              if ($next_depth == $current_depth || $next_depth == null) {
 293                  $list .= parser_utils::h('li', $text) . "\n";
 294              } else if ($next_depth > $current_depth) {
 295                  $next_depth = $current_depth + 1;
 296  
 297                  $list .= "<li>" . $text . "\n";
 298                  $list .= "<" . $nli[2] . ">" . "\n";
 299                  $liststack[] = $nli[2];
 300              } else {
 301                  $list .= parser_utils::h('li', $text) . "\n";
 302  
 303                  for ($lv = $next_depth; $lv < $current_depth; $lv++) {
 304                      $type = array_pop($liststack);
 305                      $list .= "</$type>" . "\n" . "</li>" . "\n";
 306                  }
 307              }
 308          }
 309  
 310          for ($lv = 1; $lv < $current_depth; $lv++) {
 311              $type = array_pop($liststack);
 312              $list .= "</$type>" . "\n" . "</li>" . "\n";
 313          }
 314  
 315          return $list;
 316      }
 317  
 318      /**
 319       * Table generation functions
 320       */
 321  
 322      protected function generate_table($table) {
 323          $table_html = call_user_func_array($this->tablegeneratorcallback, array($table));
 324  
 325          return $table_html;
 326      }
 327  
 328      protected function format_image($src, $alt, $caption = "", $align = 'left') {
 329          $src = $this->real_path($src);
 330          return parser_utils::h('div', parser_utils::h('p', $caption) . '<img src="' . $src . '" alt="' . $alt . '" />', array('class' => 'wiki_image_' . $align));
 331      }
 332  
 333      protected function real_path($url) {
 334          $callbackargs = array_merge(array($url), $this->realpathcallbackargs);
 335          return call_user_func_array($this->realpathcallback, $callbackargs);
 336      }
 337  
 338      /**
 339       * Link internal callback
 340       */
 341  
 342      protected function link($link, $anchor = "") {
 343          $link = trim($link);
 344          if (preg_match("/^(https?|s?ftp):\/\/.+$/i", $link)) {
 345              $link = trim($link, ",.?!");
 346              return array('content' => $link, 'url' => $link);
 347          } else {
 348              $callbackargs = $this->linkgeneratorcallbackargs;
 349              $callbackargs['anchor'] = $anchor;
 350              $link = call_user_func_array($this->linkgeneratorcallback, array($link, $callbackargs));
 351  
 352              if (isset($link['link_info'])) {
 353                  $l = $link['link_info']['link'];
 354                  unset($link['link_info']['link']);
 355                  $this->returnvalues['link_count'][$l] = $link['link_info'];
 356              }
 357              return $link;
 358          }
 359      }
 360  
 361      /**
 362       * Format links
 363       */
 364  
 365      protected function format_link($text) {
 366          $matches = array();
 367          if (preg_match("/^([^\|]+)\|(.+)$/i", $text, $matches)) {
 368              $link = $matches[1];
 369              $content = trim($matches[2]);
 370              if (preg_match("/(.+)#(.*)/is", $link, $matches)) {
 371                  $link = $this->link($matches[1], $matches[2]);
 372              } else if ($link[0] == '#') {
 373                  $link = array('url' => "#" . urlencode(substr($link, 1)));
 374              } else {
 375                  $link = $this->link($link);
 376              }
 377  
 378              $link['content'] = $content;
 379          } else {
 380              $link = $this->link($text);
 381          }
 382  
 383          if (isset($link['new']) && $link['new']) {
 384              $options = array('class' => 'wiki_newentry');
 385          } else {
 386              $options = array();
 387          }
 388  
 389          $link['content'] = $this->protect($link['content']);
 390          $link['url'] = $this->protect($link['url']);
 391  
 392          $options['href'] = $link['url'];
 393  
 394          if ($this->printable) {
 395              $options['href'] = '#'; //no target for the link
 396              }
 397          return array($link['content'], $options);
 398      }
 399  
 400      /**
 401       * Section editing
 402       */
 403  
 404      public function get_section($header, $text, $clean = false) {
 405          if ($clean) {
 406              $text = preg_replace('/\r\n/', "\n", $text);
 407              $text = preg_replace('/\r/', "\n", $text);
 408              $text .= "\n\n";
 409          }
 410  
 411          $regex = "/(.*?)(=\ *".preg_quote($header, '/')."\ *=*\n.*?)((?:\n=[^=]+.*)|$)/is";
 412          preg_match($regex, $text, $match);
 413  
 414          if (!empty($match)) {
 415              return array($match[1], $match[2], $match[3]);
 416          } else {
 417              return false;
 418          }
 419      }
 420  
 421      protected function get_repeated_sections(&$text, $repeated = array()) {
 422          $this->repeated_sections = $repeated;
 423          return preg_replace_callback($this->blockrules['header']['expression'], array($this, 'get_repeated_sections_callback'), $text);
 424      }
 425  
 426      protected function get_repeated_sections_callback($match) {
 427          $num = strlen($match[1]);
 428          $text = trim($match[2]);
 429          if ($num == 1) {
 430              if (in_array($text, $this->repeated_sections)) {
 431                  $this->returnvalues['repeated_sections'][] = $text;
 432                  return $text . "\n";
 433              } else {
 434                  $this->repeated_sections[] = $text;
 435              }
 436          }
 437  
 438          return $match[0];
 439      }
 440  
 441  }