bb0085e1d9
See also f4fb91ae. PHP8 now supports named arguments in calls to
the `call_user_func_array` method, which can result in differing
behaviour compared to PHP7, e.g. in this case it was throwing an
exception due to an unknown named parameter.
442 lines
13 KiB
PHP
442 lines
13 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Generic & abstract parser functions & skeleton. It has some functions & generic stuff.
|
|
*
|
|
* @author Josep Arús
|
|
*
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
|
* @package mod_wiki
|
|
*/
|
|
|
|
abstract class wiki_markup_parser extends generic_parser {
|
|
|
|
protected $pretty_print = false;
|
|
protected $printable = false;
|
|
|
|
//page id
|
|
protected $wiki_page_id;
|
|
|
|
//sections
|
|
protected $repeated_sections;
|
|
|
|
protected $section_editing = true;
|
|
|
|
//header & ToC
|
|
protected $toc = array();
|
|
protected $maxheaderdepth = 3;
|
|
|
|
/**
|
|
* function wiki_parser_link_callback($link = "")
|
|
*
|
|
* Returns array('content' => "Inside the link", 'url' => "http://url.com/Wiki/Entry", 'new' => false).
|
|
*/
|
|
private $linkgeneratorcallback = array('parser_utils', 'wiki_parser_link_callback');
|
|
private $linkgeneratorcallbackargs = array();
|
|
|
|
/**
|
|
* Table generator callback
|
|
*/
|
|
|
|
private $tablegeneratorcallback = array('parser_utils', 'wiki_parser_table_callback');
|
|
|
|
/**
|
|
* Get real path from relative path
|
|
*/
|
|
private $realpathcallback = array('parser_utils', 'wiki_parser_real_path');
|
|
private $realpathcallbackargs = array();
|
|
|
|
/**
|
|
* Before and after parsing...
|
|
*/
|
|
|
|
protected function before_parsing() {
|
|
$this->toc = array();
|
|
|
|
$this->string = preg_replace('/\r\n/', "\n", $this->string);
|
|
$this->string = preg_replace('/\r/', "\n", $this->string);
|
|
|
|
$this->string .= "\n\n";
|
|
|
|
if (!$this->printable && $this->section_editing) {
|
|
$this->returnvalues['unparsed_text'] = $this->string;
|
|
$this->string = $this->get_repeated_sections($this->string);
|
|
}
|
|
}
|
|
|
|
protected function after_parsing() {
|
|
if (!$this->printable) {
|
|
$this->returnvalues['repeated_sections'] = array_unique($this->returnvalues['repeated_sections']);
|
|
}
|
|
|
|
$this->process_toc();
|
|
|
|
$this->string = preg_replace("/\n\s/", "\n", $this->string);
|
|
$this->string = preg_replace("/\n{2,}/", "\n", $this->string);
|
|
$this->string = trim($this->string);
|
|
$this->string .= "\n";
|
|
}
|
|
|
|
/**
|
|
* Set options
|
|
*/
|
|
|
|
protected function set_options($options) {
|
|
parent::set_options($options);
|
|
|
|
$this->returnvalues['link_count'] = array();
|
|
$this->returnvalues['repeated_sections'] = array();
|
|
$this->returnvalues['toc'] = "";
|
|
|
|
foreach ($options as $name => $o) {
|
|
switch ($name) {
|
|
case 'link_callback':
|
|
$callback = explode(':', $o);
|
|
|
|
global $CFG;
|
|
require_once($CFG->dirroot . $callback[0]);
|
|
|
|
if (function_exists($callback[1])) {
|
|
$this->linkgeneratorcallback = $callback[1];
|
|
}
|
|
break;
|
|
case 'link_callback_args':
|
|
if (is_array($o)) {
|
|
$this->linkgeneratorcallbackargs = $o;
|
|
}
|
|
break;
|
|
case 'real_path_callback':
|
|
$callback = explode(':', $o);
|
|
|
|
global $CFG;
|
|
require_once($CFG->dirroot . $callback[0]);
|
|
|
|
if (function_exists($callback[1])) {
|
|
$this->realpathcallback = $callback[1];
|
|
}
|
|
break;
|
|
case 'real_path_callback_args':
|
|
if (is_array($o)) {
|
|
$this->realpathcallbackargs = $o;
|
|
}
|
|
break;
|
|
case 'table_callback':
|
|
$callback = explode(':', $o);
|
|
|
|
global $CFG;
|
|
require_once($CFG->dirroot . $callback[0]);
|
|
|
|
if (function_exists($callback[1])) {
|
|
$this->tablegeneratorcallback = $callback[1];
|
|
}
|
|
break;
|
|
case 'pretty_print':
|
|
if ($o) {
|
|
$this->pretty_print = true;
|
|
}
|
|
break;
|
|
case 'pageid':
|
|
$this->wiki_page_id = $o;
|
|
break;
|
|
case 'printable':
|
|
if ($o) {
|
|
$this->printable = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generic block rules
|
|
*/
|
|
|
|
protected function line_break_block_rule($match) {
|
|
return '<hr />';
|
|
}
|
|
|
|
protected function list_block_rule($match) {
|
|
preg_match_all("/^\ *([\*\#]{1,5})\ *((?:[^\n]|\n(?!(?:\ *[\*\#])|\n))+)/im", $match[1], $listitems, PREG_SET_ORDER);
|
|
|
|
return $this->process_block_list($listitems) . $match[2];
|
|
}
|
|
|
|
protected function nowiki_block_rule($match) {
|
|
return parser_utils::h('pre', $this->protect($match[1]));
|
|
}
|
|
|
|
/**
|
|
* Generic tag rules
|
|
*/
|
|
|
|
protected function nowiki_tag_rule($match) {
|
|
return parser_utils::h('tt', $this->protect($match[1]));
|
|
}
|
|
|
|
/**
|
|
* Header generation
|
|
*/
|
|
|
|
protected function generate_header($text, $level) {
|
|
$toctext = $text = trim($text);
|
|
|
|
if (!$this->pretty_print && $level == 1) {
|
|
$editlink = '[' . get_string('editsection', 'wiki') . ']';
|
|
$url = array('href' => "edit.php?pageid={$this->wiki_page_id}§ion=" . urlencode($text),
|
|
'class' => 'wiki_edit_section');
|
|
$text .= ' ' . parser_utils::h('a', $this->protect($editlink), $url);
|
|
$toctext .= ' ' . parser_utils::h('a', $editlink, $url);
|
|
}
|
|
|
|
if ($level <= $this->maxheaderdepth) {
|
|
$this->toc[] = array($level, $toctext);
|
|
$num = count($this->toc);
|
|
$text = parser_utils::h('a', "", array('name' => "toc-$num")) . $text;
|
|
}
|
|
|
|
// Display headers as <h3> and lower for accessibility.
|
|
return parser_utils::h('h' . min(6, $level + 2), $text) . "\n\n";
|
|
}
|
|
|
|
/**
|
|
* Table of contents processing after parsing
|
|
*/
|
|
protected function process_toc() {
|
|
if (empty($this->toc)) {
|
|
return;
|
|
}
|
|
|
|
$toc = "";
|
|
$currentsection = array(0, 0, 0);
|
|
$i = 1;
|
|
foreach ($this->toc as & $header) {
|
|
switch ($header[0]) {
|
|
case 1:
|
|
$currentsection = array($currentsection[0] + 1, 0, 0);
|
|
break;
|
|
case 2:
|
|
$currentsection[1]++;
|
|
$currentsection[2] = 0;
|
|
if ($currentsection[0] == 0) {
|
|
$currentsection[0]++;
|
|
}
|
|
break;
|
|
case 3:
|
|
$currentsection[2]++;
|
|
if ($currentsection[1] == 0) {
|
|
$currentsection[1]++;
|
|
}
|
|
if ($currentsection[0] == 0) {
|
|
$currentsection[0]++;
|
|
}
|
|
break;
|
|
default:
|
|
continue 2;
|
|
}
|
|
$number = "$currentsection[0]";
|
|
if (!empty($currentsection[1])) {
|
|
$number .= ".$currentsection[1]";
|
|
if (!empty($currentsection[2])) {
|
|
$number .= ".$currentsection[2]";
|
|
}
|
|
}
|
|
$toc .= parser_utils::h('p', $number . ". " .
|
|
parser_utils::h('a', str_replace(array('[[', ']]'), '', $header[1]), array('href' => "#toc-$i")),
|
|
array('class' => 'wiki-toc-section-' . $header[0] . " wiki-toc-section"));
|
|
$i++;
|
|
}
|
|
|
|
$this->returnvalues['toc'] = "<div class=\"wiki-toc\"><p class=\"wiki-toc-title\">" . get_string('tableofcontents', 'wiki') . "</p>$toc</div>";
|
|
}
|
|
|
|
/**
|
|
* List helpers
|
|
*/
|
|
|
|
private function process_block_list($listitems) {
|
|
$list = array();
|
|
foreach ($listitems as $li) {
|
|
$text = str_replace("\n", "", $li[2]);
|
|
$this->rules($text);
|
|
|
|
if ($li[1][0] == '*') {
|
|
$type = 'ul';
|
|
} else {
|
|
$type = 'ol';
|
|
}
|
|
|
|
$list[] = array(strlen($li[1]), $text, $type);
|
|
}
|
|
$type = $list[0][2];
|
|
return "<$type>" . "\n" . $this->generate_list($list) . "\n" . "</$type>" . "\n";
|
|
}
|
|
|
|
/**
|
|
* List generation function from an array of array(level, text)
|
|
*/
|
|
|
|
protected function generate_list($listitems) {
|
|
$list = "";
|
|
$current_depth = 1;
|
|
$next_depth = 1;
|
|
$liststack = array();
|
|
for ($lc = 0; $lc < count($listitems) && $next_depth; $lc++) {
|
|
$cli = $listitems[$lc];
|
|
$nli = isset($listitems[$lc + 1]) ? $listitems[$lc + 1] : null;
|
|
|
|
$text = $cli[1];
|
|
|
|
$current_depth = $next_depth;
|
|
$next_depth = $nli ? $nli[0] : null;
|
|
|
|
if ($next_depth == $current_depth || $next_depth == null) {
|
|
$list .= parser_utils::h('li', $text) . "\n";
|
|
} else if ($next_depth > $current_depth) {
|
|
$next_depth = $current_depth + 1;
|
|
|
|
$list .= "<li>" . $text . "\n";
|
|
$list .= "<" . $nli[2] . ">" . "\n";
|
|
$liststack[] = $nli[2];
|
|
} else {
|
|
$list .= parser_utils::h('li', $text) . "\n";
|
|
|
|
for ($lv = $next_depth; $lv < $current_depth; $lv++) {
|
|
$type = array_pop($liststack);
|
|
$list .= "</$type>" . "\n" . "</li>" . "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
for ($lv = 1; $lv < $current_depth; $lv++) {
|
|
$type = array_pop($liststack);
|
|
$list .= "</$type>" . "\n" . "</li>" . "\n";
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
/**
|
|
* Table generation functions
|
|
*/
|
|
|
|
protected function generate_table($table) {
|
|
$table_html = call_user_func_array($this->tablegeneratorcallback, array($table));
|
|
|
|
return $table_html;
|
|
}
|
|
|
|
protected function format_image($src, $alt, $caption = "", $align = 'left') {
|
|
$src = $this->real_path($src);
|
|
return parser_utils::h('div', parser_utils::h('p', $caption) . '<img src="' . $src . '" alt="' . $alt . '" />', array('class' => 'wiki_image_' . $align));
|
|
}
|
|
|
|
protected function real_path($url) {
|
|
$callbackargs = array_merge(array($url), $this->realpathcallbackargs);
|
|
return call_user_func_array($this->realpathcallback, array_values($callbackargs));
|
|
}
|
|
|
|
/**
|
|
* Link internal callback
|
|
*/
|
|
|
|
protected function link($link, $anchor = "") {
|
|
$link = trim($link);
|
|
if (preg_match("/^(https?|s?ftp):\/\/.+$/i", $link)) {
|
|
$link = trim($link, ",.?!");
|
|
return array('content' => $link, 'url' => $link);
|
|
} else {
|
|
$callbackargs = $this->linkgeneratorcallbackargs;
|
|
$callbackargs['anchor'] = $anchor;
|
|
$link = call_user_func_array($this->linkgeneratorcallback, array($link, $callbackargs));
|
|
|
|
if (isset($link['link_info'])) {
|
|
$l = $link['link_info']['link'];
|
|
unset($link['link_info']['link']);
|
|
$this->returnvalues['link_count'][$l] = $link['link_info'];
|
|
}
|
|
return $link;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format links
|
|
*/
|
|
|
|
protected function format_link($text) {
|
|
$matches = array();
|
|
if (preg_match("/^([^\|]+)\|(.+)$/i", $text, $matches)) {
|
|
$link = $matches[1];
|
|
$content = trim($matches[2]);
|
|
if (preg_match("/(.+)#(.*)/is", $link, $matches)) {
|
|
$link = $this->link($matches[1], $matches[2]);
|
|
} else if ($link[0] == '#') {
|
|
$link = array('url' => "#" . urlencode(substr($link, 1)));
|
|
} else {
|
|
$link = $this->link($link);
|
|
}
|
|
|
|
$link['content'] = $content;
|
|
} else {
|
|
$link = $this->link($text);
|
|
}
|
|
|
|
if (isset($link['new']) && $link['new']) {
|
|
$options = array('class' => 'wiki_newentry');
|
|
} else {
|
|
$options = array();
|
|
}
|
|
|
|
$link['content'] = $this->protect($link['content']);
|
|
$link['url'] = $this->protect($link['url']);
|
|
|
|
$options['href'] = $link['url'];
|
|
|
|
if ($this->printable) {
|
|
$options['href'] = '#'; //no target for the link
|
|
}
|
|
return array($link['content'], $options);
|
|
}
|
|
|
|
/**
|
|
* Section editing
|
|
*/
|
|
|
|
public function get_section($header, $text, $clean = false) {
|
|
if ($clean) {
|
|
$text = preg_replace('/\r\n/', "\n", $text);
|
|
$text = preg_replace('/\r/', "\n", $text);
|
|
$text .= "\n\n";
|
|
}
|
|
|
|
$regex = "/(.*?)(=\ *".preg_quote($header, '/')."\ *=*\n.*?)((?:\n=[^=]+.*)|$)/is";
|
|
preg_match($regex, $text, $match);
|
|
|
|
if (!empty($match)) {
|
|
return array($match[1], $match[2], $match[3]);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected function get_repeated_sections(&$text, $repeated = array()) {
|
|
$this->repeated_sections = $repeated;
|
|
return preg_replace_callback($this->blockrules['header']['expression'], array($this, 'get_repeated_sections_callback'), $text);
|
|
}
|
|
|
|
protected function get_repeated_sections_callback($match) {
|
|
$num = strlen($match[1]);
|
|
$text = trim($match[2]);
|
|
if ($num == 1) {
|
|
if (in_array($text, $this->repeated_sections)) {
|
|
$this->returnvalues['repeated_sections'][] = $text;
|
|
return $text . "\n";
|
|
} else {
|
|
$this->repeated_sections[] = $text;
|
|
}
|
|
}
|
|
|
|
return $match[0];
|
|
}
|
|
|
|
}
|