MDL-49807 mod_wiki: minimum heading level should always be <h3>
[moodle.git] / mod / wiki / parser / markups / wikimarkup.php
CommitLineData
00710f4c
DC
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
593b8385 9 * @package mod_wiki
00710f4c
DC
10 */
11
12abstract 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
00710f4c
DC
21 protected $repeated_sections;
22
23 protected $section_editing = true;
24
25 //header & ToC
26 protected $toc = array();
b4de35e6 27 protected $maxheaderdepth = 3;
00710f4c
DC
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':
349565d0 94 $callback = explode(':', $o);
00710f4c
DC
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':
349565d0 109 $callback = explode(':', $o);
00710f4c
DC
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':
349565d0 124 $callback = explode(':', $o);
00710f4c
DC
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 $text = trim($text);
182
183 if (!$this->pretty_print && $level == 1) {
414f7ef8
FM
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'));
00710f4c
DC
187 }
188
b4de35e6 189 if ($level <= $this->maxheaderdepth) {
00710f4c
DC
190 $this->toc[] = array($level, $text);
191 $num = count($this->toc);
192 $text = parser_utils::h('a', "", array('name' => "toc-$num")) . $text;
193 }
194
3d89ccb3
MG
195 // Display headers as <h3> and lower for accessibility.
196 return parser_utils::h('h' . min(6, $level + 2), $text) . "\n\n";
00710f4c
DC
197 }
198
199 /**
200 * Table of contents processing after parsing
201 */
202 protected function process_toc() {
203 if (empty($this->toc)) {
204 return;
205 }
206
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:
3d89ccb3 232 continue 2;
00710f4c
DC
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 }
2c13bbba
LB
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"));
00710f4c
DC
244 $i++;
245 }
246
a572f17d 247 $this->returnvalues['toc'] = "<div class=\"wiki-toc\"><p class=\"wiki-toc-title\">" . get_string('tableofcontents', 'wiki') . "</p>$toc</div>";
00710f4c
DC
248 }
249
250 /**
251 * List helpers
252 */
253
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);
259
260 if ($li[1][0] == '*') {
261 $type = 'ul';
262 } else {
263 $type = 'ol';
264 }
265
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 }
271
272 /**
273 * List generation function from an array of array(level, text)
274 */
275
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;
284
285 $text = $cli[1];
286
287 $current_depth = $next_depth;
288 $next_depth = $nli ? $nli[0] : null;
289
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;
294
295 $list .= "<li>" . $text . "\n";
296 $list .= "<" . $nli[2] . ">" . "\n";
297 $liststack[] = $nli[2];
298 } else {
299 $list .= parser_utils::h('li', $text) . "\n";
300
301 for ($lv = $next_depth; $lv < $current_depth; $lv++) {
302 $type = array_pop($liststack);
303 $list .= "</$type>" . "\n" . "</li>" . "\n";
304 }
305 }
306 }
307
308 for ($lv = 1; $lv < $current_depth; $lv++) {
309 $type = array_pop($liststack);
310 $list .= "</$type>" . "\n" . "</li>" . "\n";
311 }
312
313 return $list;
314 }
315
316 /**
317 * Table generation functions
318 */
319
320 protected function generate_table($table) {
321 $table_html = call_user_func_array($this->tablegeneratorcallback, array($table));
322
323 return $table_html;
324 }
325
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 }
330
331 protected function real_path($url) {
332 $callbackargs = array_merge(array($url), $this->realpathcallbackargs);
333 return call_user_func_array($this->realpathcallback, $callbackargs);
334 }
335
336 /**
337 * Link internal callback
338 */
339
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));
349
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 }
358
359 /**
360 * Format links
361 */
362
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 }
375
376 $link['content'] = $content;
377 } else {
378 $link = $this->link($text);
379 }
380
381 if (isset($link['new']) && $link['new']) {
382 $options = array('class' => 'wiki_newentry');
383 } else {
384 $options = array();
385 }
386
387 $link['content'] = $this->protect($link['content']);
388 $link['url'] = $this->protect($link['url']);
389
390 $options['href'] = $link['url'];
391
392 if ($this->printable) {
393 $options['href'] = '#'; //no target for the link
394 }
395 return array($link['content'], $options);
396 }
397
398 /**
399 * Section editing
400 */
401
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 }
408
c159e2aa
FM
409 $regex = "/(.*?)(=\ *".preg_quote($header, '/')."\ *=*\n.*?)((?:\n=[^=]+.*)|$)/is";
410 preg_match($regex, $text, $match);
00710f4c
DC
411
412 if (!empty($match)) {
413 return array($match[1], $match[2], $match[3]);
414 } else {
415 return false;
416 }
417 }
418
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 }
423
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 }
435
436 return $match[0];
437 }
438
439}