Merge branch 'MDL-48307-master' of git://github.com/marinaglancy/moodle
[moodle.git] / mod / wiki / parser / markups / wikimarkup.php
1 <?php
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  */
12 abstract class wiki_markup_parser extends generic_parser {
14     protected $pretty_print = false;
15     protected $printable = false;
17     //page id
18     protected $wiki_page_id;
20     //sections
21     protected $repeated_sections;
23     protected $section_editing = true;
25     //header & ToC
26     protected $toc = array();
27     protected $maxheaderdepth = 3;
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();
37     /**
38      * Table generator callback
39      */
41     private $tablegeneratorcallback = array('parser_utils', 'wiki_parser_table_callback');
43     /**
44      * Get real path from relative path
45      */
46     private $realpathcallback = array('parser_utils', 'wiki_parser_real_path');
47     private $realpathcallbackargs = array();
49     /**
50      * Before and after parsing...
51      */
53     protected function before_parsing() {
54         $this->toc = array();
56         $this->string = preg_replace('/\r\n/', "\n", $this->string);
57         $this->string = preg_replace('/\r/', "\n", $this->string);
59         $this->string .= "\n\n";
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     }
67     protected function after_parsing() {
68         if (!$this->printable) {
69             $this->returnvalues['repeated_sections'] = array_unique($this->returnvalues['repeated_sections']);
70         }
72         $this->process_toc();
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     }
80     /**
81      * Set options
82      */
84     protected function set_options($options) {
85         parent::set_options($options);
87         $this->returnvalues['link_count'] = array();
88         $this->returnvalues['repeated_sections'] = array();
89         $this->returnvalues['toc'] = "";
91         foreach ($options as $name => $o) {
92             switch ($name) {
93             case 'link_callback':
94                 $callback = explode(':', $o);
96                 global $CFG;
97                 require_once($CFG->dirroot . $callback[0]);
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);
111                 global $CFG;
112                 require_once($CFG->dirroot . $callback[0]);
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);
126                 global $CFG;
127                 require_once($CFG->dirroot . $callback[0]);
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     }
150     /**
151      * Generic block rules
152      */
154     protected function line_break_block_rule($match) {
155         return '<hr />';
156     }
158     protected function list_block_rule($match) {
159         preg_match_all("/^\ *([\*\#]{1,5})\ *((?:[^\n]|\n(?!(?:\ *[\*\#])|\n))+)/im", $match[1], $listitems, PREG_SET_ORDER);
161         return $this->process_block_list($listitems) . $match[2];
162     }
164     protected function nowiki_block_rule($match) {
165         return parser_utils::h('pre', $this->protect($match[1]));
166     }
168     /**
169      * Generic tag rules
170      */
172     protected function nowiki_tag_rule($match) {
173         return parser_utils::h('tt', $this->protect($match[1]));
174     }
176     /**
177      * Header generation
178      */
180     protected function generate_header($text, $level) {
181         $toctext = $text = trim($text);
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         }
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         }
197         // Display headers as <h3> and lower for accessibility.
198         return parser_utils::h('h' . min(6, $level + 2), $text) . "\n\n";
199     }
201     /**
202      * Table of contents processing after parsing
203      */
204     protected function process_toc() {
205         if (empty($this->toc)) {
206             return;
207         }
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         }
249         $this->returnvalues['toc'] = "<div class=\"wiki-toc\"><p class=\"wiki-toc-title\">" . get_string('tableofcontents', 'wiki') . "</p>$toc</div>";
250     }
252     /**
253      * List helpers
254      */
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);
262             if ($li[1][0] == '*') {
263                 $type = 'ul';
264             } else {
265                 $type = 'ol';
266             }
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     }
274     /**
275      * List generation function from an array of array(level, text)
276      */
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;
287             $text = $cli[1];
289             $current_depth = $next_depth;
290             $next_depth = $nli ? $nli[0] : null;
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;
297                 $list .= "<li>" . $text . "\n";
298                 $list .= "<" . $nli[2] . ">" . "\n";
299                 $liststack[] = $nli[2];
300             } else {
301                 $list .= parser_utils::h('li', $text) . "\n";
303                 for ($lv = $next_depth; $lv < $current_depth; $lv++) {
304                     $type = array_pop($liststack);
305                     $list .= "</$type>" . "\n" . "</li>" . "\n";
306                 }
307             }
308         }
310         for ($lv = 1; $lv < $current_depth; $lv++) {
311             $type = array_pop($liststack);
312             $list .= "</$type>" . "\n" . "</li>" . "\n";
313         }
315         return $list;
316     }
318     /**
319      * Table generation functions
320      */
322     protected function generate_table($table) {
323         $table_html = call_user_func_array($this->tablegeneratorcallback, array($table));
325         return $table_html;
326     }
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     }
333     protected function real_path($url) {
334         $callbackargs = array_merge(array($url), $this->realpathcallbackargs);
335         return call_user_func_array($this->realpathcallback, $callbackargs);
336     }
338     /**
339      * Link internal callback
340      */
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));
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     }
361     /**
362      * Format links
363      */
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             }
378             $link['content'] = $content;
379         } else {
380             $link = $this->link($text);
381         }
383         if (isset($link['new']) && $link['new']) {
384             $options = array('class' => 'wiki_newentry');
385         } else {
386             $options = array();
387         }
389         $link['content'] = $this->protect($link['content']);
390         $link['url'] = $this->protect($link['url']);
392         $options['href'] = $link['url'];
394         if ($this->printable) {
395             $options['href'] = '#'; //no target for the link
396             }
397         return array($link['content'], $options);
398     }
400     /**
401      * Section editing
402      */
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         }
411         $regex = "/(.*?)(=\ *".preg_quote($header, '/')."\ *=*\n.*?)((?:\n=[^=]+.*)|$)/is";
412         preg_match($regex, $text, $match);
414         if (!empty($match)) {
415             return array($match[1], $match[2], $match[3]);
416         } else {
417             return false;
418         }
419     }
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     }
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         }
438         return $match[0];
439     }