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 .= "
  • "; } 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>"; 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 .= ""; } 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); } }