MDL-49807 mod_wiki: minimum heading level should always be <h3>
[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         $text = trim($text);
183         if (!$this->pretty_print && $level == 1) {
184             $text .= ' ' . parser_utils::h('a', '['.get_string('editsection', 'wiki').']',
185                 array('href' => "edit.php?pageid={$this->wiki_page_id}&section=" . urlencode($text),
186                     'class' => 'wiki_edit_section'));
187         }
189         if ($level <= $this->maxheaderdepth) {
190             $this->toc[] = array($level, $text);
191             $num = count($this->toc);
192             $text = parser_utils::h('a', "", array('name' => "toc-$num")) . $text;
193         }
195         // Display headers as <h3> and lower for accessibility.
196         return parser_utils::h('h' . min(6, $level + 2), $text) . "\n\n";
197     }
199     /**
200      * Table of contents processing after parsing
201      */
202     protected function process_toc() {
203         if (empty($this->toc)) {
204             return;
205         }
207         $toc = "";
208         $currentsection = array(0, 0, 0);
209         $i = 1;
210         foreach ($this->toc as & $header) {
211             switch ($header[0]) {
212             case 1:
213                 $currentsection = array($currentsection[0] + 1, 0, 0);
214                 break;
215             case 2:
216                 $currentsection[1]++;
217                 $currentsection[2] = 0;
218                 if ($currentsection[0] == 0) {
219                     $currentsection[0]++;
220                 }
221                 break;
222             case 3:
223                 $currentsection[2]++;
224                 if ($currentsection[1] == 0) {
225                     $currentsection[1]++;
226                 }
227                 if ($currentsection[0] == 0) {
228                     $currentsection[0]++;
229                 }
230                 break;
231             default:
232                 continue 2;
233             }
234             $number = "$currentsection[0]";
235             if (!empty($currentsection[1])) {
236                 $number .= ".$currentsection[1]";
237                 if (!empty($currentsection[2])) {
238                     $number .= ".$currentsection[2]";
239                 }
240             }
241             $toc .= parser_utils::h('p', $number . ". " .
242                parser_utils::h('a', str_replace(array('[[', ']]'), '', $header[1]), array('href' => "#toc-$i")),
243                array('class' => 'wiki-toc-section-' . $header[0] . " wiki-toc-section"));
244             $i++;
245         }
247         $this->returnvalues['toc'] = "<div class=\"wiki-toc\"><p class=\"wiki-toc-title\">" . get_string('tableofcontents', 'wiki') . "</p>$toc</div>";
248     }
250     /**
251      * List helpers
252      */
254     private function process_block_list($listitems) {
255         $list = array();
256         foreach ($listitems as $li) {
257             $text = str_replace("\n", "", $li[2]);
258             $this->rules($text);
260             if ($li[1][0] == '*') {
261                 $type = 'ul';
262             } else {
263                 $type = 'ol';
264             }
266             $list[] = array(strlen($li[1]), $text, $type);
267         }
268         $type = $list[0][2];
269         return "<$type>" . "\n" . $this->generate_list($list) . "\n" . "</$type>" . "\n";
270     }
272     /**
273      * List generation function from an array of array(level, text)
274      */
276     protected function generate_list($listitems) {
277         $list = "";
278         $current_depth = 1;
279         $next_depth = 1;
280         $liststack = array();
281         for ($lc = 0; $lc < count($listitems) && $next_depth; $lc++) {
282             $cli = $listitems[$lc];
283             $nli = isset($listitems[$lc + 1]) ? $listitems[$lc + 1] : null;
285             $text = $cli[1];
287             $current_depth = $next_depth;
288             $next_depth = $nli ? $nli[0] : null;
290             if ($next_depth == $current_depth || $next_depth == null) {
291                 $list .= parser_utils::h('li', $text) . "\n";
292             } else if ($next_depth > $current_depth) {
293                 $next_depth = $current_depth + 1;
295                 $list .= "<li>" . $text . "\n";
296                 $list .= "<" . $nli[2] . ">" . "\n";
297                 $liststack[] = $nli[2];
298             } else {
299                 $list .= parser_utils::h('li', $text) . "\n";
301                 for ($lv = $next_depth; $lv < $current_depth; $lv++) {
302                     $type = array_pop($liststack);
303                     $list .= "</$type>" . "\n" . "</li>" . "\n";
304                 }
305             }
306         }
308         for ($lv = 1; $lv < $current_depth; $lv++) {
309             $type = array_pop($liststack);
310             $list .= "</$type>" . "\n" . "</li>" . "\n";
311         }
313         return $list;
314     }
316     /**
317      * Table generation functions
318      */
320     protected function generate_table($table) {
321         $table_html = call_user_func_array($this->tablegeneratorcallback, array($table));
323         return $table_html;
324     }
326     protected function format_image($src, $alt, $caption = "", $align = 'left') {
327         $src = $this->real_path($src);
328         return parser_utils::h('div', parser_utils::h('p', $caption) . '<img src="' . $src . '" alt="' . $alt . '" />', array('class' => 'wiki_image_' . $align));
329     }
331     protected function real_path($url) {
332         $callbackargs = array_merge(array($url), $this->realpathcallbackargs);
333         return call_user_func_array($this->realpathcallback, $callbackargs);
334     }
336     /**
337      * Link internal callback
338      */
340     protected function link($link, $anchor = "") {
341         $link = trim($link);
342         if (preg_match("/^(https?|s?ftp):\/\/.+$/i", $link)) {
343             $link = trim($link, ",.?!");
344             return array('content' => $link, 'url' => $link);
345         } else {
346             $callbackargs = $this->linkgeneratorcallbackargs;
347             $callbackargs['anchor'] = $anchor;
348             $link = call_user_func_array($this->linkgeneratorcallback, array($link, $callbackargs));
350             if (isset($link['link_info'])) {
351                 $l = $link['link_info']['link'];
352                 unset($link['link_info']['link']);
353                 $this->returnvalues['link_count'][$l] = $link['link_info'];
354             }
355             return $link;
356         }
357     }
359     /**
360      * Format links
361      */
363     protected function format_link($text) {
364         $matches = array();
365         if (preg_match("/^([^\|]+)\|(.+)$/i", $text, $matches)) {
366             $link = $matches[1];
367             $content = trim($matches[2]);
368             if (preg_match("/(.+)#(.*)/is", $link, $matches)) {
369                 $link = $this->link($matches[1], $matches[2]);
370             } else if ($link[0] == '#') {
371                 $link = array('url' => "#" . urlencode(substr($link, 1)));
372             } else {
373                 $link = $this->link($link);
374             }
376             $link['content'] = $content;
377         } else {
378             $link = $this->link($text);
379         }
381         if (isset($link['new']) && $link['new']) {
382             $options = array('class' => 'wiki_newentry');
383         } else {
384             $options = array();
385         }
387         $link['content'] = $this->protect($link['content']);
388         $link['url'] = $this->protect($link['url']);
390         $options['href'] = $link['url'];
392         if ($this->printable) {
393             $options['href'] = '#'; //no target for the link
394             }
395         return array($link['content'], $options);
396     }
398     /**
399      * Section editing
400      */
402     public function get_section($header, $text, $clean = false) {
403         if ($clean) {
404             $text = preg_replace('/\r\n/', "\n", $text);
405             $text = preg_replace('/\r/', "\n", $text);
406             $text .= "\n\n";
407         }
409         $regex = "/(.*?)(=\ *".preg_quote($header, '/')."\ *=*\n.*?)((?:\n=[^=]+.*)|$)/is";
410         preg_match($regex, $text, $match);
412         if (!empty($match)) {
413             return array($match[1], $match[2], $match[3]);
414         } else {
415             return false;
416         }
417     }
419     protected function get_repeated_sections(&$text, $repeated = array()) {
420         $this->repeated_sections = $repeated;
421         return preg_replace_callback($this->blockrules['header']['expression'], array($this, 'get_repeated_sections_callback'), $text);
422     }
424     protected function get_repeated_sections_callback($match) {
425         $num = strlen($match[1]);
426         $text = trim($match[2]);
427         if ($num == 1) {
428             if (in_array($text, $this->repeated_sections)) {
429                 $this->returnvalues['repeated_sections'][] = $text;
430                 return $text . "\n";
431             } else {
432                 $this->repeated_sections[] = $text;
433             }
434         }
436         return $match[0];
437     }