1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
4 class MTrackWikiParser {
6 const EMAIL_LOOKALIKE_PATTERN =
7 "[a-zA-Z0-9.'=+_-]+@(?:[a-zA-Z0-9_-]+\.)+[a-zA-Z](?:[-a-zA-Z\d]*[a-zA-Z\d])?";
8 const BOLDITALIC_TOKEN = "'''''";
9 const BOLD_TOKEN = "'''";
10 const ITALIC_TOKEN = "''";
11 const UNDERLINE_TOKEN = "__";
12 const STRIKE_TOKEN = "~~";
13 const SUBSCRIPT_TOKEN = ",,";
14 const SUPERSCRIPT_TOKEN = "\^";
15 const INLINE_TOKEN = "`";
16 const STARTBLOCK_TOKEN = "\{\{\{";
17 const STARTBLOCK = "{{{";
18 const ENDBLOCK_TOKEN = "\}\}\}";
19 const ENDBLOCK = "}}}";
20 const LINK_SCHEME = "[\w.+-]+"; # as per RFC 2396
21 const INTERTRAC_SCHEME = "[a-zA-Z.+-]*?"; # no digits (support for shorthand links)
23 const QUOTED_STRING = "'[^']+'|\"[^\"]+\"";
25 const SHREF_TARGET_FIRST = "[\w/?!#@](?<!_)"; # we don't want "_"
26 const SHREF_TARGET_MIDDLE = "(?:\|(?=[^|\s])|[^|<>\s])";
27 const SHREF_TARGET_LAST = "[\w/=](?<!_)"; # we don't want "_"
29 const LHREF_RELATIVE_TARGET = "[/#][^\s\]]*|\.\.?(?:[/#][^\s\]]*)?";
31 # See http://www.w3.org/TR/REC-xml/#id
32 const XML_NAME = "[\w:](?<!\d)[\w:.-]*";
34 const LOWER = '(?<![A-Z0-9_])';
35 const UPPER = '(?<![a-z0-9_])';
37 static $pre_rules = array(
38 array("(?P<bolditalic>!?%s)", self::BOLDITALIC_TOKEN),
39 array("(?P<bold>!?%s)" , self::BOLD_TOKEN),
40 array("(?P<italic>!?%s)" , self::ITALIC_TOKEN),
41 array("(?P<underline>!?%s)" , self::UNDERLINE_TOKEN),
42 array("(?P<strike>!?%s)" , self::STRIKE_TOKEN),
43 array("(?P<subscript>!?%s)" , self::SUBSCRIPT_TOKEN),
44 array("(?P<superscript>!?%s)" , self::SUPERSCRIPT_TOKEN),
45 array("(?P<inlinecode>!?%s(?P<inline>.*?)%s)" ,
46 self::STARTBLOCK_TOKEN, self::ENDBLOCK_TOKEN),
47 array("(?P<inlinecode2>!?%s(?P<inline2>.*?)%s)",
48 self::INLINE_TOKEN, self::INLINE_TOKEN),
50 static $post_rules = array(
52 array("(?P<wikipagename>!?(?<!/)\\b\w%s(?:\w%s)+(?:\w%s(?:\w%s)*[\w/]%s)+(?:@\d+)?(?:#%s)?(?=:(?:\Z|\s)|[^:a-zA-Z]|\s|\Z))",
53 self::UPPER, self::LOWER, self::UPPER, self::LOWER, self::LOWER, self::XML_NAME),
54 # [WikiPageName with label]
55 array("(?P<wikipagenamewithlabel>!?\[\w%s(?:\w%s)+(?:\w%s(?:\w%s)*[\w/]%s)+(?:@\d+)?(?:#%s)?(?=:(?:\Z|\s)|[^:a-zA-Z]|\s|\Z)\s+(?:%s|[^\]]+)\])",
56 self::UPPER, self::LOWER, self::UPPER, self::LOWER, self::LOWER, self::XML_NAME, self::QUOTED_STRING),
59 "(?P<svnchangeset>!?\[(?:(?:[a-zA-Z]+)?\d+|[a-fA-F0-9]+)\])",
61 "(?P<ticket>!?#(?:(?:[a-zA-Z]+)?\d+|[a-fA-F0-9]+))",
63 "(?P<report>!?\{([^}]*)\})",
66 array("(?P<email>!?%s)" , self::EMAIL_LOOKALIKE_PATTERN),
68 "(?P<citation>^(?P<cdepth>>(?: *>)*))",
69 # &, < and > to &, < and >
70 "(?P<htmlspecialcharsape>[&<>])",
73 "(?P<shref>!?((?P<sns>%s):(?P<stgt>%s|%s(?:%s*%s)?)))",
74 self::LINK_SCHEME, self::QUOTED_STRING,
75 self::SHREF_TARGET_FIRST, self::SHREF_TARGET_MIDDLE,
76 self::SHREF_TARGET_LAST),
78 # [wiki:TracLinks with optional label] or [/relative label]
80 "(?P<lhref>!?\[(?:(?P<rel>%s)|(?P<lns>%s):(?P<ltgt>%s|[^\]\s]*))(?:\s+(?P<label>%s|[^\]]+))?\])",
81 self::LHREF_RELATIVE_TARGET, self::LINK_SCHEME,
82 self::QUOTED_STRING, self::QUOTED_STRING),
85 "(?P<macro>!?\[\[(?P<macroname>[\w/+-]+)(\]\]|\((?P<macroargs>.*?)\)\]\]))",
86 # == heading == #hanchor
88 "(?P<heading>^\s*(?P<hdepth>=+)\s.*\s(?P=hdepth)\s*(?P<hanchor>#%s)?(?:\s|$))", self::XML_NAME),
90 "(?P<list>^(?P<ldepth>\s+)(?:[-*]|\d+\.|[a-zA-Z]\.|[ivxIVX]{1,5}\.) )",
93 "(?P<definition>^\s+((?:%s[^%s]*%s|%s(?:%s{,2}[^%s])*?%s|[^%s%s:]+|:[^:]+)+::)(?:\s+|$))",
94 self::INLINE_TOKEN, self::INLINE_TOKEN, self::INLINE_TOKEN,
95 self::STARTBLOCK_TOKEN, '}', '}',
96 self::ENDBLOCK_TOKEN, self::INLINE_TOKEN, '{'),
98 "(?P<indent>^(?P<idepth>\s+)(?=\S))",
100 "(?P<last_table_cell>\|\|\s*$)",
101 "(?P<table_cell>\|\|)",
104 function get_rules() {
105 $this->prepare_rules();
106 return $this->compiled_rules;
109 private function _build_rule(&$rules, $rule_def) {
110 foreach ($rule_def as $rule) {
111 if (is_array($rule)) {
112 $fmt = array_shift($rule);
113 $rule = vsprintf($fmt, $rule);
119 var $compiled_rules = null;
121 function prepare_rules() {
122 if ($this->compiled_rules) {
123 return $this->compiled_rules;
128 $this->_build_rule($syntax, self::$pre_rules);
129 $this->_build_rule($syntax, self::$post_rules);
131 foreach ($syntax as $rule) {
132 if (preg_match_all("/\?P<([a-z\d_]+)>/", $rule, $matches)) {
133 $helpers[] = $matches[1][0];
136 $this->helper_patterns = $helpers;
138 /* now compose it into a big regex */
139 $this->compiled_rules = "/" .
140 str_replace("/", "\\/", join('|', $syntax)) .
146 static $macros = array();
147 static $processors = array();
149 static function format_to_html($text) {
150 $f = new MTrackWikiHTMLFormatter;
153 if (false) { /* saveHTML messes with the encoding */
155 @$d = DOMDocument::loadHTML($html);
157 $d->formatOutput = true;
158 $d->substituteEntities = false;
159 $html = $d->saveHTML();
160 $html = preg_replace("/^.*<body>/sm", '', $html);
161 $html = preg_replace(",</body>.*,sm", '', $html);
167 static function format_to_oneliner($text) {
168 $f = new MTrackWikiOneLinerFormatter;
172 static function format_wiki_page($name) {
173 $d = MTrackWikiItem::loadByPageName($name);
175 return self::format_to_html($d->content);
180 static function register_macro($name, $callback) {
181 self::$macros[$name] = $callback;
184 static function register_processor($name, $callback) {
185 self::$processors[$name] = $callback;
188 static function macro_IncludeWiki($pagename) {
189 return self::format_wiki_page($pagename);
191 static function macro_IncludeHelp($pagename) {
192 return self::format_to_html(file_get_contents(
193 dirname(__FILE__) . '/../defaults/help/' . basename($pagename)));
195 static function macro_comment() {
198 static function processor_comment($name, $content) {
201 static function processor_html($name, $content) {
202 return join("\n", $content);
204 static function processor_dataset($name, $content) {
205 $res = '<table class="report wiki dataset">';
206 while (count($content)) {
207 $row = array_shift($content);
208 $next_row = array_shift($content);
209 $cols = preg_split("/\s*\|\s*/", $row);
210 if ($next_row[0] == '-') {
212 $res .= '<thead><tr>';
213 foreach ($cols as $c) {
214 $res .= "<th>" . htmlentities($c, ENT_QUOTES, 'utf-8') . "</th>\n";
216 $res .= "</tr></thead><tbody>";
218 if (is_string($next_row)) {
219 array_unshift($content, $next_row);
223 foreach ($cols as $c) {
224 $res .= "<td>" . htmlentities($c, ENT_QUOTES, 'utf-8') . "</td>\n";
229 $res .= "</tbody></table>\n";
233 MTrackWiki::register_macro('IncludeWikiPage',
234 array('MTrackWiki', 'macro_IncludeWiki'));
235 MTrackWiki::register_macro('IncludeHelpPage',
236 array('MTrackWiki', 'macro_IncludeHelp'));
237 MTrackWiki::register_macro('Comment',
238 array('MTrackWiki', 'macro_comment'));
239 MTrackWiki::register_processor('comment',
240 array('MTrackWiki', 'processor_comment'));
241 MTrackWiki::register_processor('html',
242 array('MTrackWiki', 'processor_html'));
243 MTrackWiki::register_processor('dataset',
244 array('MTrackWiki', 'processor_dataset'));
246 class MTrackWikiHTMLFormatter {
250 var $table_row_count = 0;
261 function __construct() {
262 $this->parser = new MTrackWikiParser;
266 $this->open_tags = array();
267 $this->list_stack = array();
268 $this->quote_stack = array();
269 $this->tabstops = array();
270 $this->in_code_block = 0;
271 $this->in_table = false;
272 $this->in_def_list = false;
273 $this->in_table_cell = false;
274 $this->paragraph_open = false;
277 function _apply_rules($line) {
278 $rules = $this->parser->get_rules();
279 /* slightly tricky bit of code here, because preg_replace_callback
280 * doesn't seem to support named groups */
282 if (preg_match_all($rules, $line, $matches, PREG_OFFSET_CAPTURE)) {
284 foreach ($matches as $key => $info) {
285 if (is_string($key)) {
286 foreach ($info as $nmatch => $item) {
287 if (!is_array($item)) {
293 if (strlen($match) && $offset >= 0) {
294 if ($match[0] == '!') {
295 $repl[$offset] = array(null, $match, null);
297 $func = '_' . $key . '_formatter';
298 if (method_exists($this, $func)) {
299 $repl[$offset] = array($func, $match, $nmatch);
301 @$this->missing[$func]++;
309 /* order matches by match offset */
311 /* and now we can generate for each fragment */
313 foreach ($repl as $offset => $bits) {
314 list($func, $match, $nmatch) = $bits;
316 if ($offset > $sol) {
318 // $this->out .= "Copying from $sol to $offset\n";
319 $this->out .= substr($line, $sol, $offset - $sol);
322 if ($func === null) {
323 $this->out .= htmlspecialchars(substr($match, 1),
324 ENT_COMPAT, 'utf-8');
326 // $this->out .= "invoking $func on $match of len " . strlen($match) . "\n";
327 // $this->out .= var_export($matches, true) . "\n";
328 $this->$func($match, $matches, $nmatch);
331 $sol = $offset + strlen($match);
333 $this->out .= substr($line, $sol);
344 function format($text, $escape_newlines = false) {
347 foreach (preg_split("!\r?\n!", $text) as $line) {
348 if ($this->in_code_block || trim($line) == MTrackWikiParser::STARTBLOCK) {
349 $this->handle_code_block($line);
352 if (!strncmp($line, "----", 4)) {
353 $this->close_table();
354 $this->close_paragraph();
355 $this->close_indentation();
357 $this->close_def_list();
358 $this->out .= "<hr />\n";
361 if (strlen($line) == 0) {
362 $this->close_paragraph();
363 $this->close_indentation();
365 $this->close_def_list();
366 $this->close_table();
369 if (strncmp($line, "||", 2)) {
370 // Doesn't look like a valid table row line, so break any || in the line
371 $line = str_replace("||", "|", $line);
373 // Tag expansion and clear tabstops if no indent
374 $line = str_replace("\t", " ", $line);
375 if ($line[0] != ' ') {
376 $this->tabstops = array();
379 $this->in_list_item = false;
380 $this->in_quote = false;
384 $result = $this->_apply_rules($line);
385 $newbit = $this->out;
387 if (!($this->in_list_item || $this->in_def_list || $this->in_table)) {
388 $this->open_paragraph();
391 if (!$this->in_list_item) {
394 if (!$this->in_quote) {
395 $this->close_indentation();
397 if ($this->in_def_list && $line[0] != ' ') {
398 $this->close_def_list();
400 if ($this->in_table && strncmp(ltrim($line), '||', 2)) {
401 $this->close_table();
403 $this->out .= $newbit;
405 if (!($this->in_list_item || $this->in_def_list || $this->in_table)) {
406 if (strlen($result)) {
407 $this->open_paragraph();
409 if ($escape_newlines && !preg_match(",<br />\s*$,", $line)) {
413 $this->out .= $result . $sep;
414 $this->close_table_row();
416 $this->close_table();
417 $this->close_paragraph();
418 $this->close_indentation();
420 $this->close_def_list();
421 $this->close_code_blocks();
424 function _parse_heading($match, $info, $nmatch, $shorten) {
425 $match = trim($match);
426 $depth = min(strlen($info['hdepth'][$nmatch][0]), 5);
427 if (isset($info['hanchor']) && is_array($info['hanchor'])
428 && is_array($info['hanchor'][$nmatch])
429 && strlen($info['hanchor'][$nmatch][0])) {
430 $anchor = $info['hanchor'][$nmatch][0];
434 $heading_text = substr($match, $depth+1, - $depth - 1 - strlen($anchor));
435 $heading = MTrackWiki::format_to_oneliner($heading_text);
437 $anchor = substr($anchor, 1);
439 $anchor = preg_replace("/[^\w:.-]+/", "", $heading_text);
440 if (ctype_digit($anchor[0])) {
441 $anchor = 'a' . $anchor;
444 return array($depth, $heading, $anchor);
447 function _heading_formatter($match, $info, $nmatch) {
448 $this->close_table();
449 $this->close_paragraph();
450 $this->close_indentation();
452 $this->close_def_list();
453 list($depth, $heading, $anchor) =
454 $this->_parse_heading($match, $info, $nmatch, false);
456 $this->out .= sprintf('<h%d id="%s"><a class="wiki" name="%s">%s</a></h%d>',
457 $depth, $anchor, $anchor, $heading, $depth);
460 function tag_open_p($tag) {
461 /* do we currently have any open tag with $tag as end-tag? */
462 return in_array($tag, $this->open_tags);
465 function open_tag($open_tag, $close_tag) {
466 $this->open_tags[] = array($open_tag, $close_tag);
469 function simple_tag_handler($match, $open_tag, $close_tag) {
470 if ($this->tag_open_p(array($open_tag, $close_tag))) {
471 $this->out .= $this->close_tag($close_tag);
474 $this->open_tag($open_tag, $close_tag);
475 $this->out .= $open_tag;
478 function close_tag($tag) {
480 /* walk backwards until we find the tag, closing out
482 $keys = array_reverse(array_keys($this->open_tags));
483 foreach ($keys as $k) {
484 $pair = $this->open_tags[$k];
486 if ($pair[1] == $tag) {
487 unset($this->open_tags[$k]);
488 foreach ($this->open_tags as $k2 => $pair) {
500 function _bolditalic_formatter($match, $info) {
501 $italic = array('<i>', '</i>');
502 $open = $this->tag_open_p($italic);
505 $this->out .= $italic[1];
506 $this->close_tag($italic[1]);
508 $this->_bold_formatter($match, $info);
510 $this->out .= $italic[0];
511 $this->open_tag($italic[0], $italic[1]);
515 function _bold_formatter($match, $info) {
516 $this->simple_tag_handler($match, '<strong>', '</strong>');
518 function _italic_formatter($match, $info) {
519 $this->simple_tag_handler($match, '<i>', '</i>');
521 function _underline_formatter($match, $info) {
522 $this->simple_tag_handler($match,
523 '<span class="underline">', '</span>');
525 function _strike_formatter($match, $info) {
526 $this->simple_tag_handler($match, '<del>', '</del>');
528 function _subscript_formatter($match, $info) {
529 $this->simple_tag_handler($match, '<sub>', '</sub>');
531 function _superscript_formatter($match, $info) {
532 $this->simple_tag_handler($match, '<sup>', '</sup>');
535 function _email_formatter($match, $info) {
536 $this->out .= "<a href=\"mailto:" .
537 htmlspecialchars($match, ENT_QUOTES, 'utf-8') .
538 "\">" . htmlspecialchars($match, ENT_COMPAT, 'utf-8') . "</a>";
541 function _htmlspecialcharsape_formatter($match, $info) {
542 $this->out .= htmlspecialchars($match, ENT_QUOTES, 'utf-8');
545 function _make_link($ns, $target, $match, $label) {
549 if ($label[0] == '"' || $label[0] == "'") {
550 $label = substr($label, 1, -1);
552 if (preg_match('/^(.*)#(.*)$/', $target, $M)) {
562 if ($ns == 'ticket' &&
563 (strpos($target, '-') !== false || strpos($target, ',') !== false)) {
564 /* ranged ticket query */
566 $target = 'id=' . $target;
571 $this->out .= mtrack_ticket($target, array(
578 if (strpos($target, ',') !== false) {
579 list($repo, $cs) = explode(',', $target, 2);
580 $this->out .= mtrack_changeset($cs, $repo);
582 $this->out .= mtrack_changeset($target);
587 $label = htmlspecialchars(urldecode($target), ENT_QUOTES, 'utf-8');
588 $target = $ABSWEB . "$ns.php/" . $target;
590 $this->out .= "<span class='milestone";
591 $ms = MTrackMilestone::loadByName($target);
592 if ($ms->deleted || $ms->completed) {
593 $this->out .= " completed";
595 $this->out .= "'><a href=\"$target\">$label</a></span>";
599 $this->out .= mtrack_wiki($target, array(
606 if (!empty($anchor)) {
607 $target .= "#$anchor";
610 "<a class='wikilink' href='{$ABSWEB}help.php/$target'>$label</a>";
614 $this->out .= mtrack_username($target);
618 $target = $ABSWEB . "browse.php/$target";
622 if ($target == '/') {
623 $target = mtrack_defrepo();
625 $target = $ABSWEB . "$ns.php/$target";
630 $target = $ABSWEB . "$ns.php/$target";
633 @list($file, $rev) = explode('#', $target, 2);
634 $file = ltrim($file, '/');
635 /* some legacy handling here; there are three cases:
636 * owner/repo/path -> repo = owner/repo
637 * repo/path -> repo = default/repo
638 * path -> repo = config.ini default repo
640 $bits = explode('/', $file);
642 if (count($bits) > 2) {
643 /* maybe owner/repo */
644 $repo = MTrackRepo::loadByName($bits[0] . '/' . $bits[1]);
646 $repo = $repo->getBrowseRootName();
649 if ($repo === null && count($bits) > 1) {
650 $repo = MTrackRepo::loadByName('default/' . $bits[0]);
652 $repo = $repo->getBrowseRootName();
653 array_unshift($bits, 'default');
656 if ($repo === null) {
657 $defrep = mtrack_defrepo();
659 if (strpos($defrep, '/') === false) {
660 $defrep = "default/$defrep";
662 $repo = MTrackRepo::loadByName($defrep);
664 $repo = $repo->getBrowseRootName();
665 array_unshift($bits, $repo);
669 $file = join($bits, '/');
672 $target = $ABSWEB . "file.php/$file@$rev";
674 $target = $ABSWEB . "file.php/$file";
678 if (preg_match('/^(\d+):ticket:(.*)$/', $target, $M)) {
679 $this->out .= mtrack_ticket($M[2],
681 '#' => 'comment:' . $M[1],
687 $target = "#comment:$target";
692 $target = "$ns:$target";
693 if (strlen($anchor)) {
694 $target .= "#$anchor";
699 $label = htmlspecialchars($label, ENT_QUOTES, 'utf-8');
701 $label = "<del>$label</del>";
703 $link = "<a href=\"$target\">$label</a>";
707 function _ticket_formatter($match, $info, $nmatch) {
708 $ticket = substr($match, 1);
709 $this->_make_link('ticket', $ticket, $ticket, $match);
712 function _report_formatter($match, $info, $nmatch) {
713 $ticket = substr($match, 1, -1);
714 $this->_make_link('report', $ticket, $ticket, $match);
717 function _svnchangeset_formatter($match, $info, $nmatch) {
718 $rev = substr($match, 1, -1);
719 $this->_make_link('changeset', $rev, $rev, $match);
722 function _wikipagename_formatter($match, $info, $nmatch) {
723 $this->_make_link('wiki', $match, $match, $match);
725 function _wikipagenamewithlabel_formatter($match, $info, $nmatch) {
726 $match = substr($match, 1, -1);
727 list($page, $label) = explode(" ", $match, 2);
728 $label = $this->_unquote(trim($label));
729 $this->_make_link('wiki', $page, $match, $label);
733 function _shref_formatter($match, $info, $nmatch) {
734 $ns = $info['sns'][$nmatch][0];
735 $target = $this->_unquote($info['stgt'][$nmatch][0]);
736 $shref = $info['shref'][$nmatch][0];
737 $this->_make_link($ns, $target, $match, $match);
740 function _lhref_formatter($match, $info, $nmatch) {
741 $rel = $info['rel'][$nmatch][0];
742 $ns = $info['lns'][$nmatch][0];
743 $target = $info['ltgt'][$nmatch][0];
744 $label = isset($info['label'][$nmatch][0]) ? $info['label'][$nmatch][0] : '';
746 // var_dump($rel, $ns, $target, $label);
748 if (!strlen($label)) {
749 /* [http://target] or [wiki:target] */
750 if (strlen($target)) {
751 if (!strncmp($target, "//", 2)) {
752 /* for [http://target], label is http://target */
753 $label = "$ns:$target";
755 /* for [wiki:target], label is target */
763 $label = $this->_unquote($label);
766 list($path, $query, $frag) = $this->split_link($rel);
767 if (!strncmp($path, '//', 2)) {
768 $path = '/' . ltrim($path, '/');
769 } elseif ($path[0] == '/') {
770 $path = $GLOBALS['ABSWEB'] . substr($path, 1);
773 if (strlen($query)) {
774 $target .= "?$query";
779 $this->out .= "<a href=\"$target\">$label</a>";
781 $this->_make_link($ns, $target, $match, $label);
785 function _inlinecode_formatter($match, $info, $nmatch) {
786 $this->out .= "<tt>" .
787 nl2br(htmlspecialchars($info['inline'][$nmatch][0],
788 ENT_COMPAT, 'utf-8')) .
791 function _inlinecode2_formatter($match, $info, $nmatch) {
792 $this->out .= "<tt>" .
793 nl2br(htmlspecialchars($info['inline2'][$nmatch][0],
794 ENT_COMPAT, 'utf-8')) .
798 function _macro_formatter($match, $info, $nmatch) {
799 $name = $info['macroname'][$nmatch][0];
800 if (!strcasecmp($name, 'br')) {
801 $this->out .= "<br />";
804 if (isset(MTrackWiki::$macros[$name])) {
805 $args = explode(',', $info['macroargs'][$nmatch][0]);
806 $this->out .= call_user_func_array(MTrackWiki::$macros[$name], $args);
808 $this->out .= "<tt>" .
809 htmlspecialchars($match, ENT_QUOTES, 'utf-8') . "</tt>";
814 function split_link($target) {
815 @list($query, $frag) = explode('#', $target, 2);
816 @list($target, $query) = explode('?', $query, 2);
817 return array($target, $query, $frag);
820 function _unquote($text) {
821 return preg_replace("/^(['\"])(.*)(\\1)$/", "\\2", $text);
824 function close_list() {
825 $this->_set_list_depth(0, null, null, null);
828 private function _get_list_depth() {
829 // Return the space offset associated to the deepest opened list
830 if (count($this->list_stack)) {
831 $e = end($this->list_stack);
837 private function _open_list($depth, $new_type, $list_class, $start) {
838 $this->close_table();
839 $this->close_paragraph();
840 $this->close_indentation();
841 $this->list_stack[] = array($new_type, $depth);
842 $this->_set_tab($depth);
844 $list_class = "wikilist $list_class";
846 $list_class = "wikilist";
848 $class_attr = $list_class ? sprintf(' class="%s"', $list_class) : '';
849 $start_attr = $start ? sprintf(' start="%s"', $start) : '';
850 $this->out .= "<$new_type$class_attr$start_attr><li>";
852 private function _close_list($type) {
853 array_pop($this->list_stack);
854 $this->out .= "</li></$type>";
857 private function _set_list_depth($depth, $new_type, $list_class, $start) {
858 if ($depth > $this->_get_list_depth()) {
859 $this->_open_list($depth, $new_type, $list_class, $start);
862 while (count($this->list_stack)) {
863 list($deepest_type, $deepest_offset) = end($this->list_stack);
864 if ($depth >= $deepest_offset) {
867 $this->_close_list($deepest_type);
870 if (count($this->list_stack)) {
871 list($old_type, $old_offset) = end($this->list_stack);
872 if ($new_type && $new_type != $old_type) {
873 $this->_close_list($old_type);
874 $this->_open_list($depth, $new_type, $list_class, $start);
876 if ($old_offset != $depth) {
877 array_pop($this->list_stack);
878 $this->list_stack[] = array($old_type, $depth);
880 $this->out .= "</li><li>";
883 $this->_open_list($depth, $new_type, $list_class, $start);
888 function close_indentation() {
889 $this->_set_quote_depth(0);
892 private function _get_quote_depth() {
893 // Return the space offset associated to the deepest opened quote
894 if (count($this->quote_stack)) {
895 $e = end($this->quote_stack);
901 private function _open_one_quote($d, $citation) {
902 $this->quote_stack[] = $d;
904 $class_attr = $citation ? ' class="citation"' : '';
905 $this->out .= "<blockquote$class_attr>\n";
908 private function _open_quote($quote_depth, $depth, $citation) {
909 $this->close_table();
910 $this->close_paragraph();
914 for ($d = $quote_depth + 1; $d < $depth+1; $d++) {
915 $this->_open_one_quote($d, $citation);
918 $this->_open_one_quote($depth, $citation);
922 private function _close_quote() {
923 $this->close_table();
924 $this->close_paragraph();
925 array_pop($this->quote_stack);
926 $this->out .= "</blockquote>\n";
929 private function _set_quote_depth($depth, $citation = false) {
930 $quote_depth = $this->_get_quote_depth();
931 if ($depth > $quote_depth) {
932 $this->_set_tab($depth);
933 $tabstops = $this->tabstops;
935 while (count($tabstops)) {
936 $tab = array_pop($tabstops);
937 if ($tab > $quote_depth) {
938 $this->_open_quote($quote_depth, $tab, $citation);
942 while ($this->quote_stack) {
943 $deepest_offset = end($this->quote_stack);
944 if ($depth >= $deepest_offset) {
947 $this->_close_quote();
949 if (!$citation && $depth > 0) {
950 if (count($this->quote_stack)) {
951 $old_offset = end($this->quote_stack);
952 if ($old_offset != $depth) {
953 array_pop($this->quote_stack);
954 $this->quote_stack[] = $depth;
957 $this->_open_quote($quote_depth, $depth, $citation);
962 $this->in_quote = true;
966 function open_paragraph() {
967 if (!$this->paragraph_open) {
968 $this->out .= "<p>\n";
969 $this->paragraph_open = true;
973 function close_paragraph() {
974 if ($this->paragraph_open) {
975 while (count($this->open_tags)) {
976 $t = array_pop($this->open_tags);
979 $this->out .= "</p>\n";
980 $this->paragraph_open = false;
984 function _last_table_cell_formatter($match, $info, $nmatch) {
988 function _table_cell_formatter($match, $info, $nmatch) {
990 $this->open_table_row();
991 $tag = $this->table_row_count == 1 ? 'th' : 'td';
992 if ($this->in_table_cell) {
993 $this->out .= "</$tag><$tag>";
996 $this->in_table_cell = 1;
997 $this->out .= "<$tag>";
1001 function open_table() {
1002 if (!$this->in_table) {
1003 $this->close_paragraph();
1004 $this->close_list();
1005 $this->close_def_list();
1006 $this->in_table = 1;
1007 $this->table_row_count = 0;
1008 $this->out .= "<table class='report wiki'>\n";
1012 function open_table_row() {
1013 if (!$this->in_table_row) {
1014 $this->open_table();
1015 if ($this->table_row_count == 0) {
1016 $this->out .= "<thead><tr>";
1017 } else if ($this->table_row_count == 1) {
1018 $this->out .= "<tbody><tr>";
1020 $this->out .= "<tr>";
1022 $this->in_table_row = 1;
1023 $this->table_row_count++;
1027 function close_table_row() {
1028 if ($this->in_table_row) {
1029 $tag = $this->table_row_count == 1 ? 'th' : 'td';
1030 $this->in_table_row = 0;
1031 if ($this->in_table_cell) {
1032 $this->in_table_cell = 0;
1033 $this->out .= "</$tag>";
1035 if ($this->table_row_count == 1) {
1036 $this->out .= "</tr></thead>";
1038 $this->out .= "</tr>";
1043 function close_table() {
1044 if ($this->in_table) {
1045 $this->close_table_row();
1046 if ($this->table_row_count == 1) {
1047 $this->out .= "</thead></table>\n";
1049 $this->out .= "</tbody></table>\n";
1051 $this->in_table = 0;
1055 function close_def_list() {
1056 if ($this->in_def_list) {
1057 $this->out .= "</dd></dl>\n";
1059 $this->in_def_list = false;
1062 function handle_code_block($line) {
1063 if (trim($line) == MTrackWikiParser::STARTBLOCK) {
1064 $this->in_code_block++;
1065 if ($this->in_code_block == 1) {
1066 $this->code_buf = array();
1068 $this->code_buf[] = $line;
1070 } elseif (trim($line) == MTrackWikiParser::ENDBLOCK) {
1071 $this->in_code_block--;
1072 if ($this->in_code_block == 0) {
1073 // FIXME process the code here
1074 if (preg_match("/^#!(\S+)$/", $this->code_buf[0], $M)
1075 && isset(MTrackWiki::$processors[$M[1]])) {
1076 $func = MTrackWiki::$processors[$M[1]];
1077 array_shift($this->code_buf);
1078 $this->out .= call_user_func($func, $M[1], $this->code_buf);
1080 $this->out .= "<pre>" .
1081 htmlspecialchars(join("\n", $this->code_buf), ENT_COMPAT, 'utf-8') .
1085 $this->code_buf[] = $line;
1088 $this->code_buf[] = $line;
1092 function close_code_blocks() {
1093 while ($this->in_code_block) {
1094 $this->handle_code_block(MTrackWikiParser::ENDBLOCK);
1098 function _set_tab($depth) {
1099 /* Append a new tab if needed and truncate tabs deeper than `depth`
1100 given: -*-----*--*---*--
1102 results in: -*-----*-*-------
1104 $tabstops = array();
1105 foreach ($this->tabstops as $ts) {
1106 if ($ts >= $depth) {
1111 $tabstops[] = $depth;
1112 $this->tabstops = $tabstops;
1115 function _list_formatter($match, $info, $nmatch) {
1116 $ldepth = strlen($info['ldepth'][$nmatch][0]);
1117 $listid = $match[$ldepth];
1118 $this->in_list_item = true;
1121 if ($listid == '-' || $listid == '*') {
1127 case '0': $class = 'arabiczero'; break;
1128 case 'i': $class = 'lowerroman'; break;
1129 case 'I': $class = 'upperroman'; break;
1131 if (preg_match("/(\d+)\./", substr($match, $ldepth), $d)) {
1132 $start = (int)$d[1];
1133 } elseif (ctype_lower($listid)) {
1134 $class = 'loweralpha';
1135 } elseif (ctype_upper($listid)) {
1136 $class = 'upperalpha';
1140 $this->_set_list_depth($ldepth, $type, $class, $start);
1143 function _definition_formatter($match, $info, $nmatch) {
1144 $tmp = $this->in_def_list ? '</dd>' : '<dl class="wikidl">';
1145 list($def) = explode('::', $match, 2);
1146 $tmp .= sprintf("<dt>%s</dt><dd>",
1147 MTrackWiki::format_to_oneliner(trim($def)));
1148 $this->in_def_list = true;
1152 function _indent_formatter($match, $info, $nmatch) {
1153 $idepth = strlen($info['idepth'][$nmatch][0]);
1154 if (count($this->list_stack)) {
1155 list($ltype, $ldepth) = end($this->list_stack);
1156 if ($idepth < $ldepth) {
1157 foreach ($this->list_stack as $pair) {
1159 if ($idepth > $ldepth) {
1160 $this->in_list_item = true;
1161 $this->_set_list_depth($idepth, null, null, null);
1165 } elseif ($idepth <= $ldepth + ($ltype == 'ol' ? 3 : 2)) {
1166 $this->in_list_item = true;
1170 if (!$this->in_def_list) {
1171 $this->_set_quote_depth($idepth);
1175 function _citation_formatter($match, $info, $nmatch) {
1176 $cdepth = strlen(str_replace(' ', '', $info['cdepth'][$nmatch][0]));
1177 $this->_set_quote_depth($cdepth, true);
1183 class MTrackWikiOneLinerFormatter extends MTrackWikiHTMLFormatter {
1184 function format($text, $escape_newlines = false) {
1185 if (!strlen($text)) return;
1189 foreach (preg_split("!\r?\n!", $text) as $line) {
1190 if ($num++) $this->out .= ' ';
1192 if ($this->in_code_block || trim($line) == MTrackWikiParser::STARTBLOCK) {
1194 } elseif (trim($line) == MTrackWikiParser::ENDBLOCK) {
1195 if ($in_code_block) {
1197 if ($in_code_block == 0) {
1198 $result .= " [...]\n";
1201 } elseif (!$in_code_block) {
1202 $result .= "$line\n";
1205 $result = $this->_apply_rules(rtrim($result, "\r\n"));
1206 $this->out .= $result;
1207 $this->close_tag(null);
1213 #error_reporting(E_NOTICE);
1214 $f = new MTrackWikiHTMLFormatter;
1215 $f->format(file_get_contents("WikiFormatting"));
1216 #$f->format("* '''wooot'''\noh '''yeah'''\n\n");
1217 #$f->format(" < wez@php.net http://foo.com/bar [https://baz.com/flib Flib] [/foo Shoe]\n");
1229 print_r($f->missing);