parser = new MTrack_Wiki_Parser;
}
static function registerLinkHandler(MTrack_Interface_WikiLinkHandler $li)
{
self::$linkHandler = $li;
}
function reset() {
$this->open_tags = array();
$this->list_stack = array();
$this->quote_stack = array();
$this->tabstops = array();
$this->in_code_block = 0;
$this->in_table = false;
$this->in_def_list = false;
$this->in_table_cell = false;
$this->paragraph_open = false;
}
function _apply_rules($line) {
$rules = $this->parser->get_rules();
/* slightly tricky bit of code here, because preg_replace_callback
* doesn't seem to support named groups */
$matches = array();
if (preg_match_all($rules, $line, $matches, PREG_OFFSET_CAPTURE)) {
$repl = array();
foreach ($matches as $key => $info) {
if (is_string($key)) {
foreach ($info as $nmatch => $item) {
if (!is_array($item)) {
continue;
}
$match = $item[0];
$offset = $item[1];
if (strlen($match) && $offset >= 0) {
if ($match[0] == '!') {
$repl[$offset] = array(null, $match, null);
} else {
$func = '_' . $key . '_formatter';
if (method_exists($this, $func)) {
$repl[$offset] = array($func, $match, $nmatch);
} else {
@$this->missing[$func]++;
}
}
}
}
}
}
if (count($repl)) {
/* order matches by match offset */
ksort($repl);
/* and now we can generate for each fragment */
$sol = 0;
foreach ($repl as $offset => $bits) {
list($func, $match, $nmatch) = $bits;
if ($offset > $sol) {
/* emit verbatim */
// $this->out .= "Copying from $sol to $offset\n";
$this->out .= substr($line, $sol, $offset - $sol);
}
if ($func === null) {
$this->out .= htmlspecialchars(substr($match, 1),
ENT_COMPAT, 'utf-8');
} else {
// $this->out .= "invoking $func on $match of len " . strlen($match) . "\n";
//print_r($matches);
// $this->out .= var_export($matches, true) . "\n";
$this->$func($match, $matches, $nmatch);
}
$sol = $offset + strlen($match);
}
$this->out .= substr($line, $sol);
$result = '';
} else {
$result = $line;
}
} else {
$result = $line;
}
return $result;
}
function format($text, $escape_newlines = false) {
$this->out = '';
$this->reset();
foreach (preg_split("!\r?\n!", $text) as $line) {
if ($this->in_code_block || trim($line) == MTrack_Wiki_Parser::STARTBLOCK) {
$this->handle_code_block($line);
continue;
}
if (!strncmp($line, "----", 4)) {
$this->close_table();
$this->close_paragraph();
$this->close_indentation();
$this->close_list();
$this->close_def_list();
$this->out .= "
\n";
continue;
}
if (strlen($line) == 0) {
$this->close_paragraph();
$this->close_indentation();
$this->close_list();
$this->close_def_list();
$this->close_table();
continue;
}
if (strncmp($line, "||", 2)) {
// Doesn't look like a valid table row line, so break any || in the line
$line = str_replace("||", "|", $line);
}
// Tag expansion and clear tabstops if no indent
$line = str_replace("\t", " ", $line);
if ($line[0] != ' ') {
$this->tabstops = array();
}
$this->in_list_item = false;
$this->in_quote = false;
$save = $this->out;
$this->out = '';
$result = $this->_apply_rules($line);
$newbit = $this->out;
$this->out = $save;
if (!($this->in_list_item || $this->in_def_list || $this->in_table)) {
$this->open_paragraph();
}
if (!$this->in_list_item) {
$this->close_list();
}
if (!$this->in_quote) {
$this->close_indentation();
}
if ($this->in_def_list && $line[0] != ' ') {
$this->close_def_list();
}
if ($this->in_table && strncmp(ltrim($line), '||', 2)) {
$this->close_table();
}
$this->out .= $newbit;
$sep = "\n";
if (!($this->in_list_item || $this->in_def_list || $this->in_table)) {
if (strlen($result)) {
$this->open_paragraph();
}
if ($escape_newlines && !preg_match(",
\s*$,", $line)) {
$sep = "
\n";
}
}
$this->out .= $result . $sep;
$this->close_table_row();
}
$this->close_table();
$this->close_paragraph();
$this->close_indentation();
$this->close_list();
$this->close_def_list();
$this->close_code_blocks();
}
function _parse_heading($match, $info, $nmatch, $shorten) {
$match = trim($match);
$depth = min(strlen($info['hdepth'][$nmatch][0]), 5);
if (isset($info['hanchor']) && is_array($info['hanchor'])
&& is_array($info['hanchor'][$nmatch])
&& strlen($info['hanchor'][$nmatch][0])) {
$anchor = $info['hanchor'][$nmatch][0];
} else {
$anchor = '';
}
$heading_text = substr($match, $depth+1, - $depth - 1 - strlen($anchor));
$heading = MTrack_Wiki::format_to_oneliner($heading_text);
if ($anchor) {
$anchor = substr($anchor, 1);
} else {
$anchor = preg_replace("/[^\w:.-]+/", "", $heading_text);
if (ctype_digit($anchor[0])) {
$anchor = 'a' . $anchor;
}
}
return array($depth, $heading, $anchor);
}
function _heading_formatter($match, $info, $nmatch) {
$this->close_table();
$this->close_paragraph();
$this->close_indentation();
$this->close_list();
$this->close_def_list();
list($depth, $heading, $anchor) =
$this->_parse_heading($match, $info, $nmatch, false);
$this->out .= sprintf('%s',
$depth, $anchor, $anchor, $heading, $depth);
}
function tag_open_p($tag) {
/* do we currently have any open tag with $tag as end-tag? */
return in_array($tag, $this->open_tags);
}
function open_tag($open_tag, $close_tag) {
$this->open_tags[] = array($open_tag, $close_tag);
}
function simple_tag_handler($match, $open_tag, $close_tag) {
if ($this->tag_open_p(array($open_tag, $close_tag))) {
$this->out .= $this->close_tag($close_tag);
return;
}
$this->open_tag($open_tag, $close_tag);
$this->out .= $open_tag;
}
function close_tag($tag) {
$tmp = '';
/* walk backwards until we find the tag, closing out
* as we go */
$keys = array_reverse(array_keys($this->open_tags));
foreach ($keys as $k) {
$pair = $this->open_tags[$k];
$tmp .= $pair[1];
if ($pair[1] == $tag) {
unset($this->open_tags[$k]);
foreach ($this->open_tags as $k2 => $pair) {
if ($k2 == $k) {
break;
}
$tmp .= $pair[0];
}
break;
}
}
return $tmp;
}
function _bolditalic_formatter($match, $info) {
$italic = array('', '');
$open = $this->tag_open_p($italic);
$tmp = '';
if ($open) {
$this->out .= $italic[1];
$this->close_tag($italic[1]);
}
$this->_bold_formatter($match, $info);
if (!$open) {
$this->out .= $italic[0];
$this->open_tag($italic[0], $italic[1]);
}
}
function _bold_formatter($match, $info) {
$this->simple_tag_handler($match, '', '');
}
function _italic_formatter($match, $info) {
$this->simple_tag_handler($match, '', '');
}
function _underline_formatter($match, $info) {
$this->simple_tag_handler($match,
'', '');
}
function _strike_formatter($match, $info) {
$this->simple_tag_handler($match, '', '');
}
function _subscript_formatter($match, $info) {
$this->simple_tag_handler($match, '', '');
}
function _superscript_formatter($match, $info) {
$this->simple_tag_handler($match, '', '');
}
function _email_formatter($match, $info) {
$this->out .= "" . htmlspecialchars($match, ENT_COMPAT, 'utf-8') . "";
}
function _htmlspecialcharsape_formatter($match, $info) {
$this->out .= htmlspecialchars($match, ENT_QUOTES, 'utf-8');
}
function _make_link($ns, $target, $match, $label) {
if ($label[0] == '"' || $label[0] == "'") {
$label = substr($label, 1, -1);
}
if (preg_match('/^(.*)#(.*)$/', $target, $M)) {
$target = $M[1];
$anchor = $M[2];
} else {
$anchor = null;
}
if (strlen($ns)) {
/* special cases */
if ($ns == 'ticket' &&
(strpos($target, '-') !== false || strpos($target, ',') !== false)) {
/* ranged ticket query */
$ns = 'query';
$target = 'id=' . $target;
}
switch ($ns) {
case 'ticket':
$this->out .= self::$linkHandler->ticket($target, array(
'display' => $label,
'#' => $anchor,
));
return;
case 'changeset':
if (strpos($target, ',') !== false) {
list($repo, $cs) = explode(',', $target, 2);
$this->out .= self::$linkHandler->changeset($cs, $repo);
return;
}
$this->out .= self::$linkHandler->changeset($target);
return;
case 'milestone':
$this->out .= self::$linkHandler->milestone($target);
return;
case 'wiki':
$this->out .= self::$linkHandler->wiki($target, array(
'#' => $anchor,
'display' => $label
));
return;
case 'help':
$this->out .= self::$linkHandler->help($target,$label,$anchor);
return;
case 'user':
$this->out .= self::$linkHandler->username($target);
return;
case 'repo':
$this->out .= self::$linkHandler->browse($target,$label);
return;
case 'log':
//if ($target == '/') {
// $target = MTrack_Repo::defaultRepo(
// empty($ff->MTrack['default_repo']) ? null : $ff->MTrack['default_repo']
// ); ///???
//}
$this->out .= $this->log($target, $label);
break;
case 'query':
case 'report':
$this->out .= self::$linkHandler->{$ns}($target,$label);
return;
case 'source':
// wiki should not know about repo's.. -
@list($file, $rev) = explode('#', $target, 2);
$file = ltrim($file, '/');
/* some legacy handling here; there are three cases:
* owner/repo/path -> repo = owner/repo
* repo/path -> repo = default/repo
* path -> repo = config.ini default repo
*/
$bits = explode('/', $file);
$repo = null;
//if (count($bits) > 2) {
/* maybe owner/repo */
//$repo = MTrack_Repo::loadByName($bits[0] . '/' . $bits[1]);
//if ($repo) {
// $repo = $repo->getBrowseRootName();
//}
//}
//if ($repo === null && count($bits) > 1) {
//$repo = MTrack_Repo::loadByName('default/' . $bits[0]);
//if ($repo) {
// $repo = $repo->getBrowseRootName();
// array_unshift($bits, 'default');
//}
//}
//if ($repo === null) {
//$target = MTrack_Repo::defaultRepo(
// empty($ff->MTrack['default_repo']) ? null : $ff->MTrack['default_repo']
// ); ///???
//if ($defrep) {
// if (strpos($defrep, '/') === false) {
// $defrep = "default/$defrep";
// }
// $repo = MTrack_Repo::loadByName($defrep);
// if ($repo) {
// $repo = $repo->getBrowseRootName();
// array_unshift($bits, $repo);
// }
//}
//}
$file = join($bits, '/');
$out .= self::$linkHandler->file($file . ($rev ? '@'. $rev : ''));
return;
case 'comment':
if (preg_match('/^(\d+):ticket:(.*)$/', $target, $M)) {
$this->out .= self::$linkHandler->ticket($M[2],
array(
'#' => 'comment:' . $M[1],
'display' => $label
)
);
return;
}
$this->out .= '' .htmlspecialchars($label). '';
return;
case 'http':
if (strlen($anchor)) {
$target .= "#$anchor";
}
$this->out .= '' .htmlspecialchars($label). '';
return;
case 'file': // not sure if this should be supported...
$this->out .= '' .htmlspecialchars($label). '';
return;
default:
$this->out .= htmlspecialchars($label);
return;
throw new Exception("unknown target " . $ns);
$target = "$ns:$target";
if (strlen($anchor)) {
$target .= "#$anchor";
}
break;
}
}
}
function _ticket_formatter($match, $info, $nmatch) {
$ticket = substr($match, 1);
$this->_make_link('ticket', $ticket, $ticket, $match);
}
function _report_formatter($match, $info, $nmatch) {
$ticket = substr($match, 1, -1);
$this->_make_link('report', $ticket, $ticket, $match);
}
function _svnchangeset_formatter($match, $info, $nmatch) {
$rev = substr($match, 1, -1);
$this->_make_link('changeset', $rev, $rev, $match);
}
function _wikipagename_formatter($match, $info, $nmatch) {
$this->_make_link('wiki', $match, $match, $match);
}
function _wikipagenamewithlabel_formatter($match, $info, $nmatch) {
$match = substr($match, 1, -1);
list($page, $label) = explode(" ", $match, 2);
$label = $this->_unquote(trim($label));
$this->_make_link('wiki', $page, $match, $label);
}
function _shref_formatter($match, $info, $nmatch) {
$ns = $info['sns'][$nmatch][0];
$target = $this->_unquote($info['stgt'][$nmatch][0]);
$shref = $info['shref'][$nmatch][0];
$this->_make_link($ns, $target, $match, $match);
}
function _lhref_formatter($match, $info, $nmatch) {
$rel = $info['rel'][$nmatch][0];
$ns = $info['lns'][$nmatch][0];
$target = $info['ltgt'][$nmatch][0];
$label = isset($info['label'][$nmatch][0]) ? $info['label'][$nmatch][0] : '';
// var_dump($rel, $ns, $target, $label);
if (!strlen($label)) {
/* [http://target] or [wiki:target] */
if (strlen($target)) {
if (!strncmp($target, "//", 2)) {
/* for [http://target], label is http://target */
$label = "$ns:$target";
} else {
/* for [wiki:target], label is target */
$label = $target;
}
} else {
/* [search:] */
$label = $ns;
}
} else {
$label = $this->_unquote($label);
}
if (strlen($rel)) {
list($path, $query, $frag) = $this->split_link($rel);
if (!strncmp($path, '//', 2)) {
$path = '/' . ltrim($path, '/');
} elseif ($path[0] == '/') {
$path = $GLOBALS['ABSWEB'] . substr($path, 1);
}
$target = $path;
if (strlen($query)) {
$target .= "?$query";
}
if (strlen($frag)) {
$target .= "#$frag";
}
$this->out .= "$label";
} else {
$this->_make_link($ns, $target, $match, $label);
}
}
function _inlinecode_formatter($match, $info, $nmatch) {
$this->out .= "" .
nl2br(htmlspecialchars($info['inline'][$nmatch][0],
ENT_COMPAT, 'utf-8')) .
"";
}
function _inlinecode2_formatter($match, $info, $nmatch) {
$this->out .= "" .
nl2br(htmlspecialchars($info['inline2'][$nmatch][0],
ENT_COMPAT, 'utf-8')) .
"";
}
function _macro_formatter($match, $info, $nmatch) {
$name = $info['macroname'][$nmatch][0];
if (!strcasecmp($name, 'br')) {
$this->out .= "
";
return;
}
if (isset(MTrack_Wiki::$macros[$name])) {
$args = explode(',', $info['macroargs'][$nmatch][0]);
$this->out .= call_user_func_array(MTrack_Wiki::$macros[$name], $args);
} else {
$this->out .= "" .
htmlspecialchars($match, ENT_QUOTES, 'utf-8') . "";
}
}
function split_link($target) {
@list($query, $frag) = explode('#', $target, 2);
@list($target, $query) = explode('?', $query, 2);
return array($target, $query, $frag);
}
function _unquote($text) {
return preg_replace("/^(['\"])(.*)(\\1)$/", "\\2", $text);
}
function close_list() {
$this->_set_list_depth(0, null, null, null);
}
private function _get_list_depth() {
// Return the space offset associated to the deepest opened list
if (count($this->list_stack)) {
$e = end($this->list_stack);
return $e[1];
}
return 0;
}
private function _open_list($depth, $new_type, $list_class, $start) {
$this->close_table();
$this->close_paragraph();
$this->close_indentation();
$this->list_stack[] = array($new_type, $depth);
$this->_set_tab($depth);
if ($list_class) {
$list_class = "wikilist $list_class";
} else {
$list_class = "wikilist";
}
$class_attr = $list_class ? sprintf(' class="%s"', $list_class) : '';
$start_attr = $start ? sprintf(' start="%s"', $start) : '';
$this->out .= "<$new_type$class_attr$start_attr>";
}
private function _close_list($type) {
array_pop($this->list_stack);
$this->out .= "$type>";
}
private function _set_list_depth($depth, $new_type, $list_class, $start) {
if ($depth > $this->_get_list_depth()) {
$this->_open_list($depth, $new_type, $list_class, $start);
return;
}
while (count($this->list_stack)) {
list($deepest_type, $deepest_offset) = end($this->list_stack);
if ($depth >= $deepest_offset) {
break;
}
$this->_close_list($deepest_type);
}
if ($depth > 0) {
if (count($this->list_stack)) {
list($old_type, $old_offset) = end($this->list_stack);
if ($new_type && $new_type != $old_type) {
$this->_close_list($old_type);
$this->_open_list($depth, $new_type, $list_class, $start);
} else {
if ($old_offset != $depth) {
array_pop($this->list_stack);
$this->list_stack[] = array($old_type, $depth);
}
$this->out .= "";
}
} else {
$this->_open_list($depth, $new_type, $list_class, $start);
}
}
}
function close_indentation() {
$this->_set_quote_depth(0);
}
private function _get_quote_depth() {
// Return the space offset associated to the deepest opened quote
if (count($this->quote_stack)) {
$e = end($this->quote_stack);
return $e;
}
return 0;
}
private function _open_one_quote($d, $citation) {
$this->quote_stack[] = $d;
$this->_set_tab($d);
$class_attr = $citation ? ' class="citation"' : '';
$this->out .= "\n";
}
private function _open_quote($quote_depth, $depth, $citation) {
$this->close_table();
$this->close_paragraph();
$this->close_list();
if ($citation) {
for ($d = $quote_depth + 1; $d < $depth+1; $d++) {
$this->_open_one_quote($d, $citation);
}
} else {
$this->_open_one_quote($depth, $citation);
}
}
private function _close_quote() {
$this->close_table();
$this->close_paragraph();
array_pop($this->quote_stack);
$this->out .= "
\n";
}
private function _set_quote_depth($depth, $citation = false) {
$quote_depth = $this->_get_quote_depth();
if ($depth > $quote_depth) {
$this->_set_tab($depth);
$tabstops = $this->tabstops;
while (count($tabstops)) {
$tab = array_pop($tabstops);
if ($tab > $quote_depth) {
$this->_open_quote($quote_depth, $tab, $citation);
}
}
} else {
while ($this->quote_stack) {
$deepest_offset = end($this->quote_stack);
if ($depth >= $deepest_offset) {
break;
}
$this->_close_quote();
}
if (!$citation && $depth > 0) {
if (count($this->quote_stack)) {
$old_offset = end($this->quote_stack);
if ($old_offset != $depth) {
array_pop($this->quote_stack);
$this->quote_stack[] = $depth;
}
} else {
$this->_open_quote($quote_depth, $depth, $citation);
}
}
}
if ($depth > 0) {
$this->in_quote = true;
}
}
function open_paragraph() {
if (!$this->paragraph_open) {
$this->out .= "\n";
$this->paragraph_open = true;
}
}
function close_paragraph() {
if ($this->paragraph_open) {
while (count($this->open_tags)) {
$t = array_pop($this->open_tags);
$this->out .= $t[1];
}
$this->out .= "
\n";
$this->paragraph_open = false;
}
}
function _last_table_cell_formatter($match, $info, $nmatch) {
return;
}
function _table_cell_formatter($match, $info, $nmatch) {
$this->open_table();
$this->open_table_row();
$tag = $this->table_row_count == 1 ? 'th' : 'td';
if ($this->in_table_cell) {
$this->out .= "$tag><$tag>";
return;
}
$this->in_table_cell = 1;
$this->out .= "<$tag>";
}
function open_table() {
if (!$this->in_table) {
$this->close_paragraph();
$this->close_list();
$this->close_def_list();
$this->in_table = 1;
$this->table_row_count = 0;
$this->out .= "\n";
}
}
function open_table_row() {
if (!$this->in_table_row) {
$this->open_table();
if ($this->table_row_count == 0) {
$this->out .= "";
} else if ($this->table_row_count == 1) {
$this->out .= "
";
} else {
$this->out .= "
";
}
$this->in_table_row = 1;
$this->table_row_count++;
}
}
function close_table_row() {
if ($this->in_table_row) {
$tag = $this->table_row_count == 1 ? 'th' : 'td';
$this->in_table_row = 0;
if ($this->in_table_cell) {
$this->in_table_cell = 0;
$this->out .= "$tag>";
}
if ($this->table_row_count == 1) {
$this->out .= "
";
} else {
$this->out .= "";
}
}
}
function close_table() {
if ($this->in_table) {
$this->close_table_row();
if ($this->table_row_count == 1) {
$this->out .= "
\n";
} else {
$this->out .= "\n";
}
$this->in_table = 0;
}
}
function close_def_list() {
if ($this->in_def_list) {
$this->out .= "\n";
}
$this->in_def_list = false;
}
function handle_code_block($line) {
if (trim($line) == MTrack_Wiki_Parser::STARTBLOCK) {
$this->in_code_block++;
if ($this->in_code_block == 1) {
$this->code_buf = array();
} else {
$this->code_buf[] = $line;
}
} elseif (trim($line) == MTrack_Wiki_Parser::ENDBLOCK) {
$this->in_code_block--;
if ($this->in_code_block == 0) {
// FIXME process the code here
if (preg_match("/^#!(\S+)$/", $this->code_buf[0], $M)
&& isset(MTrack_Wiki::$processors[$M[1]])) {
$func = MTrack_Wiki::$processors[$M[1]];
array_shift($this->code_buf);
$this->out .= call_user_func($func, $M[1], $this->code_buf);
} else {
$this->out .= "" .
htmlspecialchars(join("\n", $this->code_buf), ENT_COMPAT, 'utf-8') .
"
";
}
} else {
$this->code_buf[] = $line;
}
} else {
$this->code_buf[] = $line;
}
}
function close_code_blocks() {
while ($this->in_code_block) {
$this->handle_code_block(MTrack_Wiki_Parser::ENDBLOCK);
}
}
function _set_tab($depth) {
/* Append a new tab if needed and truncate tabs deeper than `depth`
given: -*-----*--*---*--
setting: *
results in: -*-----*-*-------
*/
$tabstops = array();
foreach ($this->tabstops as $ts) {
if ($ts >= $depth) {
break;
}
$tabstops[] = $ts;
}
$tabstops[] = $depth;
$this->tabstops = $tabstops;
}
function _list_formatter($match, $info, $nmatch) {
$ldepth = strlen($info['ldepth'][$nmatch][0]);
$listid = $match[$ldepth];
$this->in_list_item = true;
$class = '';
$start = '';
if ($listid == '-' || $listid == '*') {
$type = 'ul';
} else {
$type = 'ol';
switch ($listid) {
case '1': break;
case '0': $class = 'arabiczero'; break;
case 'i': $class = 'lowerroman'; break;
case 'I': $class = 'upperroman'; break;
default:
if (preg_match("/(\d+)\./", substr($match, $ldepth), $d)) {
$start = (int)$d[1];
} elseif (ctype_lower($listid)) {
$class = 'loweralpha';
} elseif (ctype_upper($listid)) {
$class = 'upperalpha';
}
}
}
$this->_set_list_depth($ldepth, $type, $class, $start);
}
function _definition_formatter($match, $info, $nmatch) {
$tmp = $this->in_def_list ? '' : '';
list($def) = explode('::', $match, 2);
$tmp .= sprintf("- %s
- ",
MTrack_Wiki::format_to_oneliner(trim($def)));
$this->in_def_list = true;
$this->out .= $tmp;
}
function _indent_formatter($match, $info, $nmatch) {
$idepth = strlen($info['idepth'][$nmatch][0]);
if (count($this->list_stack)) {
list($ltype, $ldepth) = end($this->list_stack);
if ($idepth < $ldepth) {
foreach ($this->list_stack as $pair) {
$ldepth = $pair[1];
if ($idepth > $ldepth) {
$this->in_list_item = true;
$this->_set_list_depth($idepth, null, null, null);
return;
}
}
} elseif ($idepth <= $ldepth + ($ltype == 'ol' ? 3 : 2)) {
$this->in_list_item = true;
return;
}
}
if (!$this->in_def_list) {
$this->_set_quote_depth($idepth);
}
}
function _citation_formatter($match, $info, $nmatch) {
$cdepth = strlen(str_replace(' ', '', $info['cdepth'][$nmatch][0]));
$this->_set_quote_depth($cdepth, true);
}
}