+<?php
+namespace sfjp\Wiki\Processor;
+use sfjp\Wiki\Formatter;
+use sfjp\Wiki\Exception;
+class Trac extends Base {
+ protected $inlinestyle_rules = array(
+ array("bolditalic", "'''''"),
+ array("bold", "'''", ''),
+ array("italic", "(?<!')''"),
+ array("underline", "__"),
+ array("strike", "~~"),
+ array("subscript", ",,"),
+ array("superscript", "\^"),
+ array("monospace", "`"),
+ );
+
+ protected $blockstyle_rules = array(
+ array("list", '^( +)(\*|(?:\d+|[ivx]+|[IVX]+|[a-z]+|[A-Z]+)\.) '),
+ array("escaped", '^ +(!)(\*|(?:\d+|[ivx]+|[IVX]+|[a-z]+|[A-Z]+)\.) '),
+ array("quote", "^(>(?:\s*>)*) "),
+ array("escaped", "^(!)>+ "),
+ array("heading", '^(=+)\s+(.*?)(?:\s+\1)?(?:\s+#(\S+))?\s*\r?$'),
+ array("escaped", '^(!)=+ '),
+ array("define_list", '^(.+?)::\r?$'),
+ array("line", '^----+'),
+ array("escaped", '^(!)----'),
+ array("table", '^\|\|(.*\|\|)+\r?$'),
+ array("escaped", '^(!)\|\|'),
+ array("clear_all", '^\r?$'),
+ );
+
+ protected $uri_rules = array(
+ array('(?:https?|ftp)', '\/\/[^\x00-\x20"<>]+'),
+ array('wiki', '(?:"[^\x00-\x1f\x22\x27\x5b-\x5d\x60:]+"|(?:[a-z0-9-]+:)?[^\x00-\x22\x24-\x2c\x3a-\x40\x5b-\x5e\x60\x7b-\x7e]+)'),
+ array('(?:tracker|ticket)', '\d+'),
+ array('(?:cvs|svn)', '\S+'),
+ array('(?:id|users?)', '[a-zA-Z0-9_-]+'),
+ array('comment', '\d+:\d+:\d+'),
+ array('release', '\S+'),
+ array('isbn', '[A-Za-z0-9-]+'),
+ array('prweb', '[\x21\x23-\x26\x28-\x3b\x3d\x3f-\x5a\x5e-\x7e]+'),
+ array('projects?', '[a-z0-9-]+'),
+ array('mailto', '[!#$%&*+\/=?^_+~0-9A-Za-z.-]+@[0-9A-Za-z.-]+'),
+ );
+
+ public $block_stack;
+ public $inline_stack;
+ protected $_pending_paragraph;
+
+ protected static $_static_var_cache = array();
+
+ function __construct($args = null) {
+ parent::__construct($args);
+ $this->formatter = new Formatter\HTML();
+ $this->formatter->setProcessor($this);
+ // TODO: really OK? This makes cyclic reference. PHP don't support weak ref?
+ $this->const_cache_base = "SFJP_WIKIPROCESSOR_CACHE";
+ $this->interwiki_processor = null;
+ $this->reset();
+ }
+
+ public function getCurrentLine() {
+ return $this->current_line;
+ }
+
+ public function getCurrentLineNo() {
+ return $this->current_lineno;
+ }
+
+ protected function getConstCache($name) {
+ $const_name = $this->const_cache_base . "_${name}";
+ return defined($const_name) ? constant($const_name) : null;
+ }
+
+ protected function setConstCache($name, $value) {
+ $const_name = $this->const_cache_base . "_${name}";
+ if (defined($const_name))
+ throw new Exception\InternalError("[BUG] const '$name' was already initialized.");
+ define($const_name, $value);
+ return $value;
+ }
+
+ public function getUriRegex($complete = true) {
+ $ret = $this->getConstCache("uri_regex");
+ if (isset($ret))
+ return $complete ? "/(!)?${ret}/" :$ret;
+
+ $ret = $this->getConstCache("uri_regex");
+
+ $regex = array();
+ foreach ($this->uri_rules as $rule) {
+ $regex []= $rule[0].":".$rule[1];
+ }
+ $ret = "(?:" . join('|', $regex) . ")";
+
+ $this->setConstCache("uri_regex", $ret);
+ return $complete ? "/(!)?${ret}/" :$ret;
+ }
+
+ public function getInlinestyleRegex($complete = true) {
+ $ret = $this->getConstCache("inlinestyle_regex");
+ if (isset($ret))
+ return $complete ? "/(!)?${ret}/" :$ret;
+
+ $regex = '(?:' .
+ join('|',
+ array_map(create_function('$a', 'return "($a[1])";'),
+ $this->inlinestyle_rules))
+ . ')';
+
+ $ret = $this->setConstCache("inlinestyle_regex", $regex);
+ return $complete ? "/(!)?${ret}/" :$ret;
+ }
+
+ public function getBlockstyleRules() {
+ return $this->blockstyle_rules;
+ }
+
+ public function reset() {
+ parent::reset();
+ $this->block_stack = array();
+ $this->inline_stack = array();
+ $this->current_lineno = 0;
+ $this->current_line = "";
+ $this->_pending_paragraph = false;
+ }
+
+ public function getInterwikiProcessor() {
+ return $this->interwiki_processor;
+ }
+
+ public function setInterwikiProcessor($f) {
+ $this->interwiki_processor($f);
+ }
+
+ /**
+ * process
+ *
+ * @param mixed $string
+ * @access public
+ * @return return self instance
+ */
+ public function process($text = null) {
+ $ret = "";
+ $buf_proc = "";
+ if (isset($text))
+ $this->text = $text;
+ $child_processor = null;
+
+ $lines = preg_split("/(?<=\n)/", $this->text);
+ foreach ($lines as $line) {
+ $this->current_lineno++;
+ $this->current_line = str_replace("\r", "", $line);
+ if (isset($child_processor)) {
+ if (preg_match('/^\}\}\}/', $line)) {
+ $ret .= $child_processor->process($buf_proc)->getFormattedText();
+ $child_processor->setContext(array("parent_processor" => null));
+ $child_processor = null;
+ $buf_proc = "";
+ continue;
+ }
+ $buf_proc .= $line;
+ continue;
+ }
+ if (preg_match('/^\{\{\{(?:\s+(.*))?$/', $line, $match)) {
+ $args = preg_split('/\s+/', trim($match[1]));
+ $name = (count($args) > 0 && $args[0]) ? strtolower(array_shift($args)) : 'auto';
+ $ret .= $this->clear_blockstyle('indent');
+ $ret .= $this->clear_blockstyle('table');
+ $ret .= $this->clear_blockstyle('define');
+
+ try {
+ $child_processor = $this->get_plugin_instance("Processor\\$name");
+ } catch (Exception\Plugin_Error $e) {
+ $orig_error = $e;
+ try {
+ $child_processor = $this->get_plugin_instance("Processor\\Pre");
+ } catch (Exception\Plugin_Error $e) {
+ $ret .= $this->format_plugin_error($orig_error);
+ continue;
+ }
+ }
+
+ $child_processor->setContext($this->getContext());
+ $child_processor->setContext(array("parent_processor" => $this));
+ $child_processor->setFormatter($this->getFormatter());
+ $child_processor->setArgs($args);
+ continue;
+ }
+ $ret .= $this->parse_line($line);
+ }
+ if (isset($child_processor)) {
+ $ret .= $child_processor->process($buf_proc)->getFormattedText();
+ }
+ $ret .= $this->clear_inlinestyle();
+ $ret .= $this->clear_blockstyle();
+ if (!$this->hasContext("parent_processor")
+ && !$this->getContext("disable_formatter_cleanup"))
+ $ret .= $this->getFormatter()->cleanup();
+ $this->formatted_text = $ret;
+ return $this;
+ }
+
+ protected function run_processor($name, $text) {
+ // TODO: select processor
+ $processor = new Pre();
+ return $processor->process($text);
+ }
+
+ public function parse_line($line) {
+ $match = array();
+ $name = null;
+ $pat = null;
+ $ret = "";
+ foreach ($this->blockstyle_rules as $rule) {
+ $name = $rule[0];
+ $pat = $rule[1];
+ if (preg_match("/$pat/", $line, $match, PREG_OFFSET_CAPTURE))
+ break;
+ }
+ if (count($match) == 0) {
+ $name = "_default";
+ } else {
+ $ret .= $this->clear_inlinestyle();
+ }
+
+ if (is_callable(array(&$this, 'process_block_'.$name))) {
+ $ret .= call_user_func(array(&$this, 'process_block_'.$name), $match, $line);
+ } else {
+ error_log("[BUG] can't process block '$name'.");
+ $ret .= $this->process_block__default($match, $line);
+ }
+
+ return $ret;
+ }
+
+ protected function process_block_heading($match, $line) {
+ $ret = "";
+ $level = strlen($match[1][0]) + intval($this->getContext('head_excess'));
+ $elem_level = $level <= 6 ? $level : 6;
+ $headid = "";
+ if (array_key_exists(3, $match)) {
+ $headid = trim($match[3][0]);
+ } else {
+ $headid = "h${level}-"
+ . preg_replace_callback($this->getInlinestyleRegex(),
+ create_function('$m', 'return empty($m[1]) ? "" : substr($m[0], 1);'),
+ trim($match[2][0]));
+ }
+
+ $c = $this->incrementCounter("id:{$headid}");
+ if ($c > 1) $headid .= "-{$c}";
+
+ $elem_opt = array();
+ if ($this->getContext('gen_head_id')) {
+ $elem_opt['id'] = str_replace('%', '.', rawurlencode($headid));
+ }
+
+ $ret .= $this->clear_inlinestyle();
+ $ret .= $this->clear_blockstyle();
+ $ret .= $this->formatter->open_element("heading${elem_level}", $elem_opt);
+ $ret .= $this->parse_inline($match[2][0]);
+ $ret .= $this->formatter->close_element("heading{$elem_level}");
+ return $ret;
+ }
+
+ protected function process_block_list($match, $line) {
+ $ret = $this->clear_blockstyle('indent');
+ $ret .= $this->clear_blockstyle('paragraph');
+ $ret .= $this->clear_blockstyle('table');
+ $ret .= $this->clear_blockstyle('quote');
+ $ret .= $this->clear_blockstyle('define');
+ $level = intval((strlen($match[1][0])+1)/2);
+ $mark = $match[2][0];
+ $mark_types = array('list_mark', 'list_num', 'list_roma', 'list_ROMA', 'list_alpha', 'list_ALPHA');
+ if ($mark == "*") {
+ $type = "list_mark";
+ } else if (preg_match('/^\d+\./', $mark)) {
+ $type = "list_num";
+ } else if (preg_match('/^[ivx]+\./', $mark)) {
+ $type = "list_roma";
+ } else if (preg_match('/^[IVX]+\./', $mark)) {
+ $type = "list_ROMA";
+ } else if (preg_match('/^[a-z]+\./', $mark)) {
+ $type = "list_alpha";
+ } else if (preg_match('/^[A-Z]+\./', $mark)) {
+ $type = "list_ALPHA";
+ } else {
+ $msg = $this->getFormatter()->raw_node("[BUG] unkown mark '$mark'");
+ error_log($msg);
+ $ret = $this->getFormatter()->raw_node('<span class="wiki-system-error">'."$msg</span>");
+ }
+ $cur_level = $this->count_in_stack($mark_types, $this->block_stack);
+ if ($level == $cur_level) {
+ $ret .= $this->pop_blockstyle("list_item");
+ } else if ($level > $cur_level) {
+ while (1) {
+ $ret .= $this->push_blockstyle($type);
+ if ($this->count_in_stack($mark_types, $this->block_stack) >= $level) {
+ break;
+ }
+ $ret .= $this->push_blockstyle("list_item");
+ }
+ } else {
+ while ($this->count_in_stack($mark_types, $this->block_stack) > $level) {
+ $ret .= $this->pop_blockstyle("list_item");
+ }
+ }
+ if ($type != end($this->block_stack)) {
+ $ret .= $this->pop_blockstyle(end($this->block_stack));
+ $ret .= $this->push_blockstyle($type);
+ }
+ $ret .= $this->push_blockstyle("list_item");
+ $ret .= $this->parse_inline(substr($line, strlen($match[0][0])));
+ return $ret;
+ }
+
+ protected function process_block_table($match, $line) {
+ $cells = explode('||', $line);
+ array_shift($cells);
+ array_pop($cells);
+ $ret = "";
+
+ if (!$this->in_stack_p("table"))
+ $ret .= $this->push_blockstyle("table");
+
+ $ret .= $this->push_blockstyle("table_row");
+
+ foreach($cells as $c) {
+ $ret .= $this->push_blockstyle("table_col");
+ $ret .= $this->parse_inline($c);
+ $ret .= $this->pop_blockstyle("table_col");
+ }
+
+ $ret .= $this->pop_blockstyle("table_row");
+
+ $this->last_table_cells = count($cells);
+ return $ret;
+ }
+
+ protected function process_block_line($match, $line) {
+ return $this->clear_blockstyle()
+ . $this->formatter->open_element("line")
+ . $this->formatter->close_element("line");
+ }
+
+ protected function process_block_quote($match, $line) {
+ $level = count(explode('>', $match[1][0]))-1;
+ $ret = '';
+ $ret .= $this->clear_blockstyle('indent');
+ $ret .= $this->clear_blockstyle('table');
+ $ret .= $this->clear_blockstyle('define');
+ $ret .= $this->clear_blockstyle('list');
+ while ($level < $this->count_in_stack('quote', $this->block_stack)) {
+ $ret .= $this->pop_blockstyle('quote');
+ }
+ while ($level > $this->count_in_stack('quote', $this->block_stack)) {
+ if (end($this->block_stack) == 'paragraph') {
+ $ret .= $this->pop_blockstyle('paragraph');
+ }
+ $ret .= $this->push_blockstyle('quote');
+ }
+
+ if (end($this->block_stack) != 'paragraph') {
+ $ret .= $this->push_blockstyle("paragraph");
+ }
+ $ret .= $this->parse_inline(substr($line, strlen($match[0][0])));
+ return $ret;
+ }
+
+ protected function process_block_define_list($match, $line) {
+ return $this->getFormatter()->raw_node("<div class=\"wiki-system-error\">def list not implemented yet.</div>"); # TODO:
+ }
+
+ protected function process_block_escaped($match, $line) {
+ $l = substr($line, 0, $match[1][1]) . substr($line, $match[1][1]+1);
+ return $this->process_block__default(null, $l);
+ }
+
+ protected function process_block_clear_all($match, $line) {
+ return $this->clear_blockstyle();
+ }
+
+ protected function process_block__default($match, $line) {
+ $ret = "";
+ $last = end($this->block_stack);
+ $cur_level = $last ? $this->count_in_stack($last, $this->block_stack) : 0;
+ $match = null;
+ preg_match("/^ +/", $line, $match);
+ $level = count($match) ? intval((strlen($match[0])+1)/2) : 0;
+ if ($last == "list_item" && count($match)) {
+ // TODO: BAD WAY!
+ if ($level >= $cur_level &&
+ $level <= $cur_level + (prev($this->block_stack) == "list_mark" ? 1 : 2)) {
+ $level = $cur_level;
+ }
+ }
+
+ if ($level && $last && ($last == "indent" || $last == "list_item") &&
+ $level == $this->count_in_stack($last, $this->block_stack)) {
+ // nop to continue current block element
+ } elseif ($level) {
+ $ret .= $this->clear_blockstyle();
+ for ($i = 0; $level > $i; $i++) {
+ $this->clear_inlinestyle();
+ $ret .= $this->push_blockstyle("indent");
+ }
+ } elseif (!$last || $last != "paragraph" ||
+ ($level == 0 && count($this->block_stack) > 1)) {
+ $ret .= $this->clear_blockstyle();
+ $this->_pending_paragraph = true;
+ }
+
+ $ret .= $this->parse_inline(substr($line, count($match) ? strlen($match[0]) : 0));
+ return $ret;
+ }
+
+ protected function parse_inline($line) {
+ $ret = $this->parse_bracket_and_plugin($line);
+ if ($this->getContext('trac.keep_newline'))
+ $ret .= $this->getFormatter()->open_element('newline');
+ return $ret;
+ }
+
+ protected function parse_bracket_and_plugin($text) {
+ $match = array();
+ $fmt_text = "";
+ $regex = null;
+ $cache_name = 'parse_bracket_and_plugin';
+ if (!$regex = $this->getConstCache($cache_name)) {
+ $regex =
+ '/(!)? # 1: escape
+ (?:\{\{\{(.*?)\}\}\} # 2: {{{preformatted}}}
+ | \[\[([a-zA-Z_].*?)\]\] # 3: [[plugin()]]
+ | \[(?:wiki:)?\x22([^"]+?)\x22(?:\s+([^\]]+))?\] # 4, 5: ["quoted" lebel]
+ | \[([^\x20\]]+?)(?:\s+([^\]]+))?\] # 6, 7: [link lebel]
+ | wiki:\x22(.+?)\x22 # 8: Quoted WikiName
+ )/x';
+ $this->setConstCache($cache_name, $regex);
+ }
+ while (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE)) {
+ $str = $match[0][0];
+ if ($match[0][1] > 0) {
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $fmt_text .= $this->parse_inlinestyle(substr($text, 0, $match[0][1]));
+ }
+ $text = substr($text, $match[0][1]+ strlen($match[0][0]));
+ if (isset($match[1]) && strlen($match[1][0])) { /* escaped */
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $fmt_text .= $this->formatter->text_node(substr($str, 1));
+ continue;
+ } elseif (isset($match[2]) && strlen($match[2][0])) { /* inline preformatted */
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $fmt_text .= $this->formatter->open_element('monospace');
+ $fmt_text .= $this->formatter->text_node($match[2][0]);
+ $fmt_text .= $this->formatter->close_element('monospace');
+ } elseif (isset($match[3]) && strlen($match[3][0])) { /* plugin */
+ $fmt_text .= $this->process_plugin($match[3][0]);
+ continue;
+ } else {
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $link = null;
+ $label = null;
+ if (isset($match[3]) && strlen($match[4][0])) { /* quoted bracket */
+ if (!$this->getContext('disable.link.quoted_bracket')) {
+ $link = $this->gen_uri_link("wiki:\"{$match[4][0]}\"");
+ $label = (isset($match[5]) && strlen($match[5][0])) ? $match[5][0] : $match[4][0];
+ }
+ } elseif (isset($match[6]) && strlen($match[6][0])) { /* bracket link */
+ if (!$this->getContext('disable.link.bracket')) {
+ $link = $this->gen_uri_link($match[6][0]);
+ if (!$link && !strrchr($match[6][0], ':')) {
+ // forced as wikiname
+ $link = $this->gen_uri_link("wiki:{$match[6][0]}");
+ }
+ $label = (isset($match[7]) && strlen($match[7][0])) ? $match[7][0] : $match[6][0];
+ }
+ } elseif (isset($match[8]) && strlen($match[8][0])) { /* quoted wikiname */
+ $link = $this->gen_uri_link($str);
+ }
+ $fmt_text .= isset($link) ? $this->create_link((isset($label) ? $label : $str), $link) : $this->formatter->text_node($str);
+ continue;
+ }
+ }
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $fmt_text .= $this->parse_inlinestyle($text);
+ return $fmt_text;
+ }
+
+ protected function parse_inlinestyle($text) {
+ $match = array();
+ $formatted_text = "";
+ while (preg_match($this->getInlinestyleRegex(), $text, $match, PREG_OFFSET_CAPTURE)) {
+ $leading_text = $this->parse_links(substr($text, 0, $match[0][1]));
+ $replace_elem = $this->inlinestyle_callback($match);
+ $formatted_text .= $leading_text . $replace_elem;
+ $text = substr($text, $match[0][1]+ strlen($match[0][0]));
+ }
+ $formatted_text .= $this->parse_links($text);
+ return $formatted_text;
+ }
+
+ protected function parse_links($text) {
+ $match = array();
+ $fmt_text = "";
+ $regex = null;
+ $cache_name = "parse_links";
+
+ if (!$regex = $this->getConstCache($cache_name)) {
+ $regex =
+ '/(!)? # 1: escape
+ (?:\#(\d+) # 2: #nnn tracker
+ | (?<![A-Za-z0-9.#-&-])r(\d+)(?![A-Za-z0-9.#-&-]) # 3: subversion revision
+ | (' . $this->getUriRegex(false) . ') # 4: URI
+ | (?<!\w)(?:[A-Z][a-z0-9]+){2,} # WikiName
+ )/x';
+ $this->setConstCache($cache_name, $regex);
+ }
+
+ while (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE)) {
+ $link = null;
+ $str = $match[0][0];
+ $fmt_text .= $this->formatter->text_node(substr($text, 0, $match[0][1]));
+ $text = substr($text, $match[0][1]+ strlen($match[0][0]));
+ if (isset($match[1]) && strlen($match[1][0])) { /* escaped */
+ $fmt_text .= $this->formatter->text_node(substr($str, 1));
+ continue;
+ } elseif (isset($match[2]) && strlen($match[2][0])) { /* #nnnn tracker */
+ $link = $this->gen_uri_link("ticket:{$match[2][0]}");
+ } elseif (isset($match[3]) && strlen($match[3][0])) { /* SVN */
+ if (!$this->getContext('disable.link.svn_revision')) {
+ $link = $this->getContext('sfjp.svn_rev_base_url') . $match[3][0];
+ $link = array("href" => $link, "class" => "svn");
+ }
+ } elseif (isset($match[4]) && strlen($match[4][0])) { /* URI */
+ $link = $this->gen_uri_link($str);
+ } else { /* WikiName */
+ if (!$this->getContext('disable.link.CamelCase')) {
+ $link = $this->getContext('wiki_baseurl');
+ if (substr($link, -1) !== "/")
+ $link .= '/';
+ $link .= rawurlencode($str);
+ }
+ }
+
+ if (isset($link)) {
+ $fmt_text .= $this->create_link($str, $link);
+ } else {
+ $fmt_text .= $this->formatter->text_node($str);
+ }
+ }
+ $fmt_text .= $this->formatter->text_node($text);
+ return $fmt_text;
+ }
+
+ public function gen_uri_link($str) {
+ $ret = null;
+ if (strpos($str, '/') === 0)
+ return array("href" => $str);
+ $part = explode(':', $str, 2);
+ if (count($part) == 1) return null;
+ $scheme = $part[0];
+ $body = $part[1];
+ if ($this->getContext("disable.link.scheme.{$scheme}"))
+ return null;
+ switch($scheme) {
+ // built-in schemes
+ case "http":
+ case "https":
+ case "ftp":
+ if (!preg_match('!//[^\x00-\x20"<>]+!', $body)) break;
+ $ret = array("href" => $str);
+ if (!($this->getContext("internal_url_regex") &&
+ preg_match("/".str_replace('/', '\/', $this->getContext("internal_url_regex"))."/", $str))) {
+ $ret["class"] = "external";
+ if ($this->getContext('nofollow_on_external_links'))
+ $ret["rel"] = "nofollow";
+ }
+ break;
+ case "wiki":
+ $wiki_allow_chars = '[^:\x00-\x1f\x21-\x23\x27\x5b-\x5d\x60]';
+ $m = null;
+ $fragment = null;
+ if (substr($body, 0, 1) == '"' && substr($body, -1, 1) == '"')
+ $body = substr($body, 1, strlen($body)-2);
+ if (($fragpos = strpos($body, '#')) !== false) {
+ $fragment = substr($body, $fragpos+1);
+ $body = substr($body, 0, $fragpos);
+ }
+ if (preg_match("/^([a-z0-9-]+):(${wiki_allow_chars}*)\$/", $body, $m)) { # wiki:group:PageName
+ $ret = $this->getContext('site_root_url')
+ . "/projects/$m[1]/wiki/"
+ . rawurlencode($m[2]);
+ $ret = array("href" => $ret, "class" => "external-wiki");
+ } elseif (preg_match("/^${wiki_allow_chars}+\$/", $body)) {
+ $ret = $this->getContext('wiki_baseurl');
+ if (substr($ret, -1) !== "/")
+ $ret .= '/';
+ $ret .= rawurlencode($body);
+ }
+ if (isset($fragment)) {
+ if (!isset($ret)) $ret = array("href" => "");
+ if (!is_array($ret)) $ret = array("href" => $ret);
+ $ret["href"] .= "#" . Formatter\Base::escape_id_value(str_replace('%', '.', rawurlencode($fragment)));
+ }
+ break;
+ case "tracker":
+ case "ticket":
+ if (preg_match('/^[0-9]+$/', $body)) {
+ if ($body > 50000) {
+ $ret = array("href" => $this->getContext('site_root_url').'/ticket/detail.php?id='.$body,
+ "class" => "tracker");
+ } else {
+ $ret = array("href" => $this->getContext('site_root_url')."/tracker.php?id={$body}",
+ "class" => "tracker");
+ }
+ }
+ break;
+ case "cvs":
+ if (preg_match('/^[a-z0-9_-]+:/', $body)) {
+ list($group, $path)= explode(':', $body, 2);
+ } else {
+ $group = $this->getContext('sfjp.group_name');
+ $path = $body;
+ }
+ if (substr($body, 0, 1) != '/')
+ $path = "/$path";
+ $ret = $this->getContext('cvs_base_url') . "/${group}${path}";
+ $ret = array("href" => $ret, "class" => "cvs");
+ break;
+ case "svn":
+ if (preg_match('/^[a-z0-9_-]+:/', $body)) {
+ list($group, $path)= explode(':', $body, 2);
+ } else {
+ $group = $this->getContext('sfjp.group_name');
+ $path = $body;
+ }
+ if (substr($body, 0, 1) != '/')
+ $path = "/$path";
+ $ret = $this->getContext('sfjp.svn_file_base_url') . $path;
+ $ret = array("href" => $ret, "class" => "svn");
+ break;
+ case "user":
+ case "users":
+ case "id":
+ if (preg_match('/^[a-z0-9_-]+$/', $body)) {
+ $ret = "/users/" . $body;
+ $ret = array("href" => $ret, "class" => "user");
+ if ($this->getContext('individual_usericon')) {
+ if (empty($ret['style'])) $ret['style'] = '';
+ $ret['style'] .= "background-image: url(".$this->getContext('individual_usericon')."{$body});";
+ }
+ if ($this->getContext('override_usericon_size')) {
+ if (empty($ret['style'])) $ret['style'] = '';
+ $ret['style'] .= "padding-left: ".$this->getContext('override_usericon_size')."px;";
+ }
+ }
+ break;
+ case "comment":
+ $parts = explode(':', $body, 3);
+ if (count($parts) == 2) {
+ $ret = $this->getContext('site_root_url')
+ . '/ticket/detail.php?id='.$parts[0].'&cid='.$parts[1];
+ } else if (!$parts[0]) {
+ $ret = $this->getContext('site_root_url')
+ . '/ticket/detail.php?id='.$parts[1].'&cid='.$parts[2];
+ } else {
+ $ret = $this->getContext('site_root_url')
+ . "/ticket/browse.php?group_id=${parts[0]}&tid=${parts[1]}#comment:${body}";
+ }
+ $ret = array("href" => $ret, "class" => "ticket");
+ break;
+ case "project":
+ case "projects":
+ if (preg_match('/^[a-z0-9_-]+$/', $body)) {
+ $ret = $this->getContext('site_root_url')
+ . "/projects/$body/";
+ $ret = array("href" => $ret, "class" => "project");
+ }
+ break;
+ case "prweb":
+ $m = array();
+ preg_match('/^https?:\/\/([^\/]+)/', $this->getContext('site_root_url'), $m);
+ $site_domain = $m[1];
+ $host = $this->getContext('sfjp.group_name').".${site_domain}";
+ $path = $body;
+
+ if (preg_match('/^([a-z0-9_-]+):(.*)$/', $body, $m)) { # prweb:project:path
+ $host = "$m[1].${site_domain}";
+ $path = $m[2];
+ }
+ if (substr($path, 0, 1) != "/")
+ $path = "/$path";
+ $ret = "http://${host}${path}";
+ $ret = array("href" => $ret, "class" => "project-web");
+ break;
+ case "release":
+ $ret = $this->getContext('')
+ . "/projects/" . $body;
+ $ret = array("href" => $ret, "class" => "release");
+ break;
+ case "isbn":
+ $ret = array("href" => "http://www.amazon.co.jp/gp/product/$body",
+ "class" => "isbnbook", "rel" => "nofollow");
+ if ($aid = $this->getContext('amazon_affiliate_id')) {
+ $ret['href'] .= "?tag={$aid}&linkCode=as2&camp=247&creative=1211&creativeASIN={$body}";
+ }
+ break;
+ case "mailto":
+ $ret = array("href" => "mailto:{$body}", "class" => "mail");
+ break;
+ default:
+ if ($this->getInterwikiProcessor() &&
+ is_callable(array($this->getInterwikiProcessor(), 'process'),
+ $body)){
+ $ret = $this->getInterwikiProcessor()->process($body);
+ }
+ break;
+ }
+ return $ret;
+ }
+
+ public function create_link($text, $args, $no_escape_html = false) {
+ if (!is_array($args))
+ $args = array("href" => $args);
+
+ if (array_key_exists('href', $args)) {
+ $args["href"] = $this->encode_url_badchar($args["href"]);
+ }
+
+ $fmt = $this->formatter;
+ $ret = "";
+ $ret .= $fmt->open_element("link", $args);
+ $ret .= $no_escape_html ? $fmt->raw_node($text) : $fmt->text_node($text);
+ $ret .= $fmt->close_element("link");
+ return $ret;
+ }
+
+ public function encode_url_badchar($str) {
+ return preg_replace_callback('/[^\x21\x23-\x26\x28-\x3b\x3d\x3f-\x5a\x5e-\x7e]+/',
+ create_function('$m', 'return rawurlencode($m[0]);'),
+ $str);
+ }
+
+ public function process_plugin($str) {
+ $match = null;
+ if (!preg_match('/^([^()]+)(?:\((.*)\))?$/', $str, $match))
+ return "";
+ $name = $match[1];
+ $args = isset($match[2]) && !empty($match[2]) ? preg_split('/\s*,\s*/', trim($match[2])) : array();
+
+ try {
+ $plugin_obj = $this->get_plugin_instance("Plugin\\{$name}");
+ $plug_ret = call_user_func(array($plugin_obj, "process"), $args);
+ if (!$this->is_vary) $this->is_vary = $plugin_obj->is_vary;
+ if ($plugin_obj->is_block) {
+ if ($this->_pending_paragraph) {
+ $this->_pending_paragraph = false;
+ }
+ if ($this->in_stack_p('paragraph')) {
+ $plug_ret = $this->pop_blockstyle('paragraph')
+ . $plug_ret . $this->push_blockstyle('paragraph');
+ }
+ } else {
+ $plug_ret = $this->_flush_pending_paragraph() . $plug_ret;
+ }
+ return $plug_ret;
+ } catch (Exception\Plugin_Error $e) {
+ return $this->format_plugin_error($e);
+ }
+ }
+
+ public function format_plugin_error($e) {
+ if ($this->getContext("suppress_plugin_error")) {
+ return '';
+ } else {
+ return $this->formatter->element('error', "Plugin Error: ".$e->getMessage());
+ }
+ }
+
+ public function count_in_stack($name, &$stack) {
+ if (!is_array($name))
+ $name = array($name);
+ $count = array_count_values($stack);
+ $ret = 0;
+ foreach ($name as $n) {
+ $ret += array_key_exists($n, $count) ? $count[$n] : 0;
+ }
+ return $ret;
+ }
+
+ public function in_stack_p($name) {
+ return in_array($name, $this->inline_stack) || in_array($name, $this->block_stack);
+ }
+
+ protected function push_blockstyle($name) {
+ $this->block_stack[] = $name;
+ return $this->formatter->open_element($name);
+ }
+
+ protected function pop_blockstyle($name) {
+ return $this->clear_blockstyle($name, 1);
+ }
+
+ protected function clear_inlinestyle($name = null) {
+ return $this->clear_stylestack($this->inline_stack, $name);
+ }
+
+ protected function clear_blockstyle($name = null, $max = null) {
+ $ret = $this->clear_inlinestyle();
+ $ret .= $this->clear_stylestack($this->block_stack, $name, $max);
+ return $ret;
+ }
+
+ protected function clear_stylestack(&$stack, $name, $max = null) {
+ $ret = "";
+ $i = 1;
+ if (isset($name) && !in_array($name, $stack)) {
+ // return if $name setted and not found in stack.
+ return $ret;
+ }
+ while ($elem = array_pop($stack)) {
+ $ret .= $this->formatter->close_element($elem);
+ if (isset($name) && $name == $elem) {
+ $i++;
+ if ($max && $i > $max || !in_array($name, $stack))
+ break;
+ }
+ }
+ return $ret;
+ }
+
+ protected function inlinestyle_callback($match) {
+ $rule = $this->get_matched_rule($match, $this->inlinestyle_rules);
+ if (!isset($rule) && $match[1][0] === "!")
+ return $this->formatter->text_node(substr($match[0][0], 1));
+ $name = $rule[0];
+ if (!in_array($name, $this->inline_stack)) {
+ $this->inline_stack[] = $name;
+ return $this->formatter->open_element($name);
+ } else {
+ $ret = "";
+ while ($name != ($cur_elem = array_pop($this->inline_stack))) {
+ $ret .= $this->formatter->close_element($cur_elem);
+ }
+ $ret .= $this->formatter->close_element($name);
+ return $ret;
+ }
+ }
+
+ protected function get_matched_rule($match, $rules, $rule_have_escape = true) {
+ if ($rule_have_escape && strlen($match[1][0])) /* ! escaped */
+ return null;
+ $excess = $rule_have_escape ? 2 : 1;
+ for($i = $excess; isset($match[$i]); $i++) {
+ if ($match[$i][1] >= 0)
+ break;
+ }
+ return $rules[$i - $excess];
+ }
+
+ private function _flush_pending_paragraph() {
+ if (!$this->_pending_paragraph) return '';
+ $this->_pending_paragraph = false;
+ return $this->push_blockstyle("paragraph");
+ }
+}