<?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, $callbackargs);
> 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];
}
}