OSDN Git Service

Import current code.
[osdn-codes/wiki-parser.git] / sfjp / wiki / processor / trac.php
1 <?php
2 namespace sfjp\Wiki\Processor;
3 use sfjp\Wiki\Formatter;
4 use sfjp\Wiki\Exception;
5 class Trac extends Base {
6         protected $inlinestyle_rules = array(
7                                              array("bolditalic", "'''''"),
8                                              array("bold", "'''", ''),
9                                              array("italic", "(?<!')''"),
10                                              array("underline", "__"),
11                                              array("strike", "~~"),
12                                              array("subscript", ",,"),
13                                              array("superscript", "\^"),
14                                              array("monospace", "`"),
15                                              );
16
17         protected $blockstyle_rules = array(
18                                             array("list", '^( +)(\*|(?:\d+|[ivx]+|[IVX]+|[a-z]+|[A-Z]+)\.) '),
19                                             array("escaped", '^ +(!)(\*|(?:\d+|[ivx]+|[IVX]+|[a-z]+|[A-Z]+)\.) '),
20                                             array("quote", "^(>(?:\s*>)*) "),
21                                             array("escaped", "^(!)>+ "),
22                                             array("heading", '^(=+)\s+(.*?)(?:\s+\1)?(?:\s+#(\S+))?\s*\r?$'),
23                                             array("escaped", '^(!)=+ '),
24                                             array("define_list", '^(.+?)::\r?$'),
25                                             array("line", '^----+'),
26                                             array("escaped", '^(!)----'),
27                                             array("table", '^\|\|(.*\|\|)+\r?$'),
28                                             array("escaped", '^(!)\|\|'),
29                                             array("clear_all", '^\r?$'),
30                                             );
31
32         protected $uri_rules = array(
33                                      array('(?:https?|ftp)', '\/\/[^\x00-\x20"<>]+'),
34                                      array('wiki',           '(?:"[^\x00-\x1f\x22\x27\x5b-\x5d\x60:]+"|(?:[a-z0-9-]+:)?[^\x00-\x22\x24-\x2c\x3a-\x40\x5b-\x5e\x60\x7b-\x7e]+)'),
35                                      array('(?:tracker|ticket)', '\d+'),
36                                      array('(?:cvs|svn)',    '\S+'),
37                                      array('(?:id|users?)',  '[a-zA-Z0-9_-]+'),
38                                      array('comment',        '\d+:\d+:\d+'),
39                                      array('release',        '\S+'),
40                                      array('isbn',           '[A-Za-z0-9-]+'),
41                                      array('prweb',          '[\x21\x23-\x26\x28-\x3b\x3d\x3f-\x5a\x5e-\x7e]+'),
42                                      array('projects?',      '[a-z0-9-]+'),
43                                      array('mailto',         '[!#$%&*+\/=?^_+~0-9A-Za-z.-]+@[0-9A-Za-z.-]+'),
44                                      );
45
46         public $block_stack;
47         public $inline_stack;
48         protected $_pending_paragraph;
49
50         protected static $_static_var_cache = array();
51
52         function __construct($args = null) {
53                 parent::__construct($args);
54                 $this->formatter = new Formatter\HTML();
55                 $this->formatter->setProcessor($this);
56                 // TODO: really OK? This makes cyclic reference. PHP don't support weak ref?
57                 $this->const_cache_base = "SFJP_WIKIPROCESSOR_CACHE";
58                 $this->interwiki_processor = null;
59                 $this->reset();
60         }
61
62         public function getCurrentLine() {
63                 return $this->current_line;
64         }
65
66         public function getCurrentLineNo() {
67                 return $this->current_lineno;
68         }
69
70         protected function getConstCache($name) {
71                 $const_name = $this->const_cache_base . "_${name}";
72                 return defined($const_name) ? constant($const_name) : null;
73         }
74
75         protected function setConstCache($name, $value) {
76                 $const_name = $this->const_cache_base . "_${name}";
77                 if (defined($const_name))
78                         throw new Exception\InternalError("[BUG] const '$name' was already initialized.");
79                 define($const_name, $value);
80                 return $value;
81         }
82
83         public function getUriRegex($complete = true) {
84                 $ret = $this->getConstCache("uri_regex");
85                 if (isset($ret))
86                         return $complete ? "/(!)?${ret}/" :$ret;
87
88                 $ret = $this->getConstCache("uri_regex");
89
90                 $regex = array();
91                 foreach ($this->uri_rules as $rule) {
92                         $regex []= $rule[0].":".$rule[1];
93                 }
94                 $ret = "(?:" . join('|', $regex) . ")";
95
96                 $this->setConstCache("uri_regex", $ret);
97                 return $complete ? "/(!)?${ret}/" :$ret;
98         }
99
100         public function getInlinestyleRegex($complete = true) {
101                 $ret = $this->getConstCache("inlinestyle_regex");
102                 if (isset($ret))
103                         return $complete ? "/(!)?${ret}/" :$ret;
104     
105                 $regex = '(?:' .
106                         join('|',
107                              array_map(create_function('$a', 'return "($a[1])";'),
108                                        $this->inlinestyle_rules))
109                         . ')';
110
111                 $ret = $this->setConstCache("inlinestyle_regex", $regex);
112                 return $complete ? "/(!)?${ret}/" :$ret;
113         }
114
115         public function getBlockstyleRules() {
116                 return $this->blockstyle_rules;
117         }
118
119         public function reset() {
120                 parent::reset();
121                 $this->block_stack = array();
122                 $this->inline_stack = array();
123                 $this->current_lineno = 0;
124                 $this->current_line = "";
125                 $this->_pending_paragraph = false;
126         }
127
128         public function getInterwikiProcessor() {
129                 return $this->interwiki_processor;
130         }
131
132         public function setInterwikiProcessor($f) {
133                 $this->interwiki_processor($f);
134         }
135
136         /**
137          * process
138          * 
139          * @param mixed $string 
140          * @access public
141          * @return return self instance
142          */
143         public function process($text = null) {
144                 $ret       = "";
145                 $buf_proc  = "";
146                 if (isset($text))
147                         $this->text = $text;
148                 $child_processor = null;
149
150                 $lines = preg_split("/(?<=\n)/", $this->text);
151                 foreach ($lines as $line) {
152                         $this->current_lineno++;
153                         $this->current_line = str_replace("\r", "", $line);
154                         if (isset($child_processor)) {
155                                 if (preg_match('/^\}\}\}/', $line)) {
156                                         $ret .= $child_processor->process($buf_proc)->getFormattedText();
157                                         $child_processor->setContext(array("parent_processor" => null));
158                                         $child_processor = null;
159                                         $buf_proc = "";
160                                         continue;
161                                 }
162                                 $buf_proc .= $line;
163                                 continue;
164                         }
165                         if (preg_match('/^\{\{\{(?:\s+(.*))?$/', $line, $match)) {
166                                 $args = preg_split('/\s+/', trim($match[1]));
167                                 $name = (count($args) > 0 && $args[0]) ? strtolower(array_shift($args)) : 'auto';
168                                 $ret .= $this->clear_blockstyle('indent');
169                                 $ret .= $this->clear_blockstyle('table');
170                                 $ret .= $this->clear_blockstyle('define');
171
172                                 try {
173                                         $child_processor = $this->get_plugin_instance("Processor\\$name");
174                                 } catch (Exception\Plugin_Error $e) {
175                                         $orig_error = $e;
176                                         try {
177                                                 $child_processor = $this->get_plugin_instance("Processor\\Pre");
178                                         } catch (Exception\Plugin_Error $e) {
179                                                 $ret .= $this->format_plugin_error($orig_error);
180                                                 continue;
181                                         }
182                                 }
183                                                          
184                                 $child_processor->setContext($this->getContext());
185                                 $child_processor->setContext(array("parent_processor" => $this));
186                                 $child_processor->setFormatter($this->getFormatter());
187                                 $child_processor->setArgs($args);
188                                 continue;
189                         }
190                         $ret .= $this->parse_line($line);
191                 }
192                 if (isset($child_processor)) {
193                         $ret .= $child_processor->process($buf_proc)->getFormattedText();
194                 }
195                 $ret .= $this->clear_inlinestyle();
196                 $ret .= $this->clear_blockstyle();
197                 if (!$this->hasContext("parent_processor")
198                     && !$this->getContext("disable_formatter_cleanup"))
199                         $ret .= $this->getFormatter()->cleanup();
200                 $this->formatted_text = $ret;
201                 return $this;
202         }
203
204         protected function run_processor($name, $text) {
205                 // TODO: select processor
206                 $processor = new Pre();
207                 return $processor->process($text);
208         }
209
210         public function parse_line($line) {
211                 $match = array();
212                 $name  = null;
213                 $pat   = null;
214                 $ret   = "";
215                 foreach ($this->blockstyle_rules as $rule) {
216                         $name = $rule[0];
217                         $pat  = $rule[1];
218                         if (preg_match("/$pat/", $line, $match, PREG_OFFSET_CAPTURE))
219                                 break;
220                 }
221                 if (count($match) == 0) {
222                         $name = "_default";
223                 } else {
224                         $ret .= $this->clear_inlinestyle();
225                 }
226
227                 if (is_callable(array(&$this, 'process_block_'.$name))) {
228                         $ret .= call_user_func(array(&$this, 'process_block_'.$name), $match, $line);
229                 } else {
230                         error_log("[BUG] can't process block '$name'.");
231                         $ret .= $this->process_block__default($match, $line);
232                 }
233
234                 return $ret;
235         }
236
237         protected function process_block_heading($match, $line) {
238                 $ret = "";
239                 $level = strlen($match[1][0]) + intval($this->getContext('head_excess'));
240                 $elem_level = $level <= 6 ? $level : 6;
241                 $headid = "";
242                 if (array_key_exists(3, $match)) {
243                         $headid = trim($match[3][0]);
244                 } else {
245                         $headid = "h${level}-"
246                                 . preg_replace_callback($this->getInlinestyleRegex(),
247                                                         create_function('$m', 'return empty($m[1]) ? "" : substr($m[0], 1);'),
248                                                         trim($match[2][0]));
249                 }
250     
251                 $c = $this->incrementCounter("id:{$headid}");
252                 if ($c > 1) $headid .= "-{$c}";
253
254                 $elem_opt = array();
255                 if ($this->getContext('gen_head_id')) {
256                         $elem_opt['id'] = str_replace('%', '.', rawurlencode($headid));
257                 }
258
259                 $ret .= $this->clear_inlinestyle();
260                 $ret .= $this->clear_blockstyle();
261                 $ret .= $this->formatter->open_element("heading${elem_level}", $elem_opt);
262                 $ret .= $this->parse_inline($match[2][0]);
263                 $ret .= $this->formatter->close_element("heading{$elem_level}");
264                 return $ret;
265         }
266
267         protected function process_block_list($match, $line) {
268                 $ret  = $this->clear_blockstyle('indent');
269                 $ret .= $this->clear_blockstyle('paragraph');
270                 $ret .= $this->clear_blockstyle('table');
271                 $ret .= $this->clear_blockstyle('quote');
272                 $ret .= $this->clear_blockstyle('define');
273                 $level = intval((strlen($match[1][0])+1)/2);
274                 $mark = $match[2][0];
275                 $mark_types = array('list_mark', 'list_num', 'list_roma', 'list_ROMA', 'list_alpha', 'list_ALPHA');
276                 if ($mark == "*") {
277                         $type = "list_mark";
278                 } else if (preg_match('/^\d+\./', $mark)) {
279                         $type = "list_num";
280                 } else if (preg_match('/^[ivx]+\./', $mark)) {
281                         $type = "list_roma";
282                 } else if (preg_match('/^[IVX]+\./', $mark)) {
283                         $type = "list_ROMA";
284                 } else if (preg_match('/^[a-z]+\./', $mark)) {
285                         $type = "list_alpha";
286                 } else if (preg_match('/^[A-Z]+\./', $mark)) {
287                         $type = "list_ALPHA";
288                 } else {
289                         $msg = $this->getFormatter()->raw_node("[BUG] unkown mark '$mark'");
290                         error_log($msg);
291                         $ret = $this->getFormatter()->raw_node('<span class="wiki-system-error">'."$msg</span>");
292                 }
293                 $cur_level = $this->count_in_stack($mark_types, $this->block_stack);
294                 if ($level == $cur_level) {
295                         $ret .= $this->pop_blockstyle("list_item");
296                 } else if ($level > $cur_level) {
297                         while (1) {
298                                 $ret .= $this->push_blockstyle($type);
299                                 if ($this->count_in_stack($mark_types, $this->block_stack) >= $level) {
300                                         break;
301                                 }
302                                 $ret .= $this->push_blockstyle("list_item");
303                         }
304                 } else {
305                         while ($this->count_in_stack($mark_types, $this->block_stack) > $level) {
306                                 $ret .= $this->pop_blockstyle("list_item");
307                         }
308                 }
309                 if ($type != end($this->block_stack)) {
310                         $ret .= $this->pop_blockstyle(end($this->block_stack));
311                         $ret .= $this->push_blockstyle($type);
312                 }
313                 $ret .= $this->push_blockstyle("list_item");
314                 $ret .= $this->parse_inline(substr($line, strlen($match[0][0])));
315                 return $ret;
316         }
317
318         protected function process_block_table($match, $line) {
319                 $cells = explode('||', $line);
320                 array_shift($cells);
321                 array_pop($cells);
322                 $ret   = "";
323
324                 if (!$this->in_stack_p("table")) 
325                         $ret .= $this->push_blockstyle("table");
326
327                 $ret .= $this->push_blockstyle("table_row");
328
329                 foreach($cells as $c) {
330                         $ret .= $this->push_blockstyle("table_col");
331                         $ret .= $this->parse_inline($c);
332                         $ret .= $this->pop_blockstyle("table_col");
333                 }
334
335                 $ret .= $this->pop_blockstyle("table_row");
336
337                 $this->last_table_cells = count($cells);
338                 return $ret;
339         }
340
341         protected function process_block_line($match, $line) {
342                 return $this->clear_blockstyle()
343                         . $this->formatter->open_element("line")
344                         . $this->formatter->close_element("line");
345         }
346
347         protected function process_block_quote($match, $line) {
348                 $level = count(explode('>', $match[1][0]))-1;
349                 $ret = '';
350                 $ret .= $this->clear_blockstyle('indent');
351                 $ret .= $this->clear_blockstyle('table');
352                 $ret .= $this->clear_blockstyle('define');
353                 $ret .= $this->clear_blockstyle('list');
354                 while ($level < $this->count_in_stack('quote', $this->block_stack)) {
355                         $ret .= $this->pop_blockstyle('quote');
356                 }
357                 while ($level > $this->count_in_stack('quote', $this->block_stack)) {
358                         if (end($this->block_stack) == 'paragraph') {
359                                 $ret .= $this->pop_blockstyle('paragraph');
360                         }
361                         $ret .= $this->push_blockstyle('quote');
362                 }
363     
364                 if (end($this->block_stack) != 'paragraph') {
365                         $ret .= $this->push_blockstyle("paragraph");
366                 }
367                 $ret .= $this->parse_inline(substr($line, strlen($match[0][0])));
368                 return $ret;
369         }
370
371         protected function process_block_define_list($match, $line) {
372                 return $this->getFormatter()->raw_node("<div class=\"wiki-system-error\">def list not implemented yet.</div>"); # TODO:
373         }
374
375         protected function process_block_escaped($match, $line) {
376                 $l = substr($line, 0, $match[1][1]) . substr($line, $match[1][1]+1);
377                 return $this->process_block__default(null, $l);
378         }
379
380         protected function process_block_clear_all($match, $line) {
381                 return $this->clear_blockstyle();
382         }
383
384         protected function process_block__default($match, $line) {
385                 $ret = "";
386                 $last = end($this->block_stack);
387                 $cur_level = $last ? $this->count_in_stack($last, $this->block_stack) : 0;
388                 $match = null;
389                 preg_match("/^ +/", $line, $match);
390                 $level = count($match) ? intval((strlen($match[0])+1)/2) : 0;
391                 if ($last == "list_item" && count($match)) {
392                         // TODO: BAD WAY!
393                         if ($level >= $cur_level &&
394                             $level <= $cur_level + (prev($this->block_stack) == "list_mark" ? 1 : 2)) {
395                                 $level = $cur_level;
396                         }
397                 }
398
399                 if ($level && $last && ($last == "indent" || $last == "list_item") &&
400                     $level == $this->count_in_stack($last, $this->block_stack)) {
401                         // nop to continue current block element
402                 } elseif ($level) {
403                         $ret .= $this->clear_blockstyle();
404                         for ($i = 0; $level > $i; $i++) {
405                                 $this->clear_inlinestyle();
406                                 $ret .= $this->push_blockstyle("indent");
407                         }
408                 } elseif (!$last || $last != "paragraph" ||
409                           ($level == 0 && count($this->block_stack) > 1)) {
410                         $ret .= $this->clear_blockstyle();
411                         $this->_pending_paragraph = true;
412                 }
413
414                 $ret .= $this->parse_inline(substr($line, count($match) ? strlen($match[0]) : 0));
415                 return $ret;
416         }
417
418         protected function parse_inline($line) {
419                 $ret = $this->parse_bracket_and_plugin($line);
420                 if ($this->getContext('trac.keep_newline'))
421                         $ret .= $this->getFormatter()->open_element('newline');
422                 return $ret;
423         }
424
425         protected function parse_bracket_and_plugin($text) {
426                 $match = array();
427                 $fmt_text = "";
428                 $regex = null;
429                 $cache_name = 'parse_bracket_and_plugin';
430                 if (!$regex = $this->getConstCache($cache_name)) {
431                         $regex = 
432                                 '/(!)?                                             # 1: escape
433                                  (?:\{\{\{(.*?)\}\}\}                              # 2: {{{preformatted}}}
434                                  | \[\[([a-zA-Z_].*?)\]\]                          # 3: [[plugin()]]
435                                  | \[(?:wiki:)?\x22([^"]+?)\x22(?:\s+([^\]]+))?\]  # 4, 5: ["quoted" lebel]
436                                  | \[([^\x20\]]+?)(?:\s+([^\]]+))?\]               # 6, 7: [link lebel]
437                                  | wiki:\x22(.+?)\x22                              # 8: Quoted WikiName
438                                  )/x';
439                         $this->setConstCache($cache_name, $regex);
440                 }
441                 while (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE)) {
442                         $str = $match[0][0];
443                         if ($match[0][1] > 0) {
444                                 $fmt_text .= $this->_flush_pending_paragraph();
445                                 $fmt_text .= $this->parse_inlinestyle(substr($text, 0, $match[0][1]));
446                         }
447                         $text = substr($text, $match[0][1]+ strlen($match[0][0]));
448                         if (isset($match[1]) && strlen($match[1][0])) { /* escaped */
449                                 $fmt_text .= $this->_flush_pending_paragraph();
450                                 $fmt_text .= $this->formatter->text_node(substr($str, 1));
451                                 continue;
452                         } elseif (isset($match[2]) && strlen($match[2][0])) { /* inline preformatted */
453                                 $fmt_text .= $this->_flush_pending_paragraph();
454                                 $fmt_text .= $this->formatter->open_element('monospace');
455                                 $fmt_text .= $this->formatter->text_node($match[2][0]);
456                                 $fmt_text .= $this->formatter->close_element('monospace');
457                         } elseif (isset($match[3]) && strlen($match[3][0])) { /* plugin */
458                                 $fmt_text .= $this->process_plugin($match[3][0]);
459                                 continue;
460                         } else {
461                                 $fmt_text .= $this->_flush_pending_paragraph();
462                                 $link  = null;
463                                 $label = null;
464                                 if (isset($match[3]) && strlen($match[4][0])) { /* quoted bracket */
465                                         if (!$this->getContext('disable.link.quoted_bracket')) {
466                                                 $link  = $this->gen_uri_link("wiki:\"{$match[4][0]}\"");
467                                                 $label = (isset($match[5]) && strlen($match[5][0])) ? $match[5][0] : $match[4][0];
468                                         }
469                                 } elseif (isset($match[6]) && strlen($match[6][0])) { /* bracket link */
470                                         if (!$this->getContext('disable.link.bracket')) {
471                                                 $link = $this->gen_uri_link($match[6][0]);
472                                                 if (!$link && !strrchr($match[6][0], ':')) {
473                                                         // forced as wikiname
474                                                         $link = $this->gen_uri_link("wiki:{$match[6][0]}");
475                                                 }
476                                                 $label = (isset($match[7]) && strlen($match[7][0])) ? $match[7][0] : $match[6][0];
477                                         }
478                                 } elseif (isset($match[8]) && strlen($match[8][0])) { /* quoted wikiname */
479                                         $link = $this->gen_uri_link($str);
480                                 }
481                                 $fmt_text .= isset($link) ? $this->create_link((isset($label) ? $label : $str), $link) : $this->formatter->text_node($str);
482                                 continue;
483                         }
484                 }
485                 $fmt_text .= $this->_flush_pending_paragraph();
486                 $fmt_text .= $this->parse_inlinestyle($text);
487                 return $fmt_text;
488         }
489
490         protected function parse_inlinestyle($text) {
491                 $match = array();
492                 $formatted_text = "";
493                 while (preg_match($this->getInlinestyleRegex(), $text, $match, PREG_OFFSET_CAPTURE)) {
494                         $leading_text = $this->parse_links(substr($text, 0, $match[0][1]));
495                         $replace_elem = $this->inlinestyle_callback($match);
496                         $formatted_text .= $leading_text . $replace_elem;
497                         $text = substr($text, $match[0][1]+ strlen($match[0][0]));
498                 }
499                 $formatted_text .= $this->parse_links($text);
500                 return $formatted_text;
501         }
502
503         protected function parse_links($text) {
504                 $match = array();
505                 $fmt_text = "";
506                 $regex = null;
507                 $cache_name = "parse_links";
508
509                 if (!$regex = $this->getConstCache($cache_name)) {
510                         $regex =
511                                 '/(!)?                                 # 1: escape
512                                 (?:\#(\d+)                             # 2: #nnn tracker
513                                 | (?<![A-Za-z0-9.#-&-])r(\d+)(?![A-Za-z0-9.#-&-]) # 3: subversion revision
514                                 | (' . $this->getUriRegex(false) . ')  # 4: URI
515                                 | (?<!\w)(?:[A-Z][a-z0-9]+){2,}        # WikiName
516                                 )/x';
517                         $this->setConstCache($cache_name, $regex);
518                 }
519
520                 while (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE)) {
521                         $link      = null;
522                         $str       = $match[0][0];
523                         $fmt_text .= $this->formatter->text_node(substr($text, 0, $match[0][1]));
524                         $text      = substr($text, $match[0][1]+ strlen($match[0][0]));
525                         if (isset($match[1]) && strlen($match[1][0])) { /* escaped */
526                                 $fmt_text .= $this->formatter->text_node(substr($str, 1));
527                                 continue;
528                         } elseif (isset($match[2]) && strlen($match[2][0])) { /* #nnnn tracker */
529                                 $link = $this->gen_uri_link("ticket:{$match[2][0]}");
530                         } elseif (isset($match[3]) && strlen($match[3][0])) { /* SVN */
531                                 if (!$this->getContext('disable.link.svn_revision')) {
532                                         $link = $this->getContext('sfjp.svn_rev_base_url') . $match[3][0];
533                                         $link = array("href" => $link, "class" => "svn");
534                                 }
535                         } elseif (isset($match[4]) && strlen($match[4][0])) { /* URI */
536                                 $link = $this->gen_uri_link($str);
537                         } else { /* WikiName */
538                                 if (!$this->getContext('disable.link.CamelCase')) {
539                                         $link = $this->getContext('wiki_baseurl');
540                                         if (substr($link, -1) !== "/")
541                                                 $link .= '/';
542                                         $link .= rawurlencode($str);
543                                 }
544                         }
545
546                         if (isset($link)) {
547                                 $fmt_text .= $this->create_link($str, $link);
548                         } else {
549                                 $fmt_text .= $this->formatter->text_node($str);
550                         }
551                 }
552                 $fmt_text .= $this->formatter->text_node($text);
553                 return $fmt_text;
554         }
555
556         public function gen_uri_link($str) {
557                 $ret = null;
558                 if (strpos($str, '/') === 0)
559                         return array("href" => $str);
560                 $part = explode(':', $str, 2);
561                 if (count($part) == 1) return null;
562                 $scheme = $part[0];
563                 $body = $part[1];
564                 if ($this->getContext("disable.link.scheme.{$scheme}"))
565                         return null;
566                 switch($scheme) {
567                         // built-in schemes
568                 case "http":
569                 case "https":
570                 case "ftp":
571                         if (!preg_match('!//[^\x00-\x20"<>]+!', $body)) break;
572                         $ret = array("href" => $str);
573                         if (!($this->getContext("internal_url_regex") &&
574                               preg_match("/".str_replace('/', '\/', $this->getContext("internal_url_regex"))."/", $str))) {
575                                 $ret["class"] = "external";
576                                 if ($this->getContext('nofollow_on_external_links'))
577                                         $ret["rel"] = "nofollow";
578                         }
579                         break;
580                 case "wiki":
581                         $wiki_allow_chars = '[^:\x00-\x1f\x21-\x23\x27\x5b-\x5d\x60]';
582                         $m = null;
583                         $fragment = null;
584                         if (substr($body, 0, 1) == '"' && substr($body, -1, 1) == '"')
585                                 $body = substr($body, 1, strlen($body)-2);
586                         if (($fragpos = strpos($body, '#')) !== false) {
587                                 $fragment = substr($body, $fragpos+1);
588                                 $body = substr($body, 0, $fragpos);
589                         }
590                         if (preg_match("/^([a-z0-9-]+):(${wiki_allow_chars}*)\$/", $body, $m)) { # wiki:group:PageName
591                                         $ret = $this->getContext('site_root_url')
592                                         . "/projects/$m[1]/wiki/"
593                                         . rawurlencode($m[2]);
594                                 $ret = array("href" => $ret, "class" => "external-wiki");
595                         } elseif (preg_match("/^${wiki_allow_chars}+\$/", $body)) {
596                                 $ret = $this->getContext('wiki_baseurl');
597                                 if (substr($ret, -1) !== "/")
598                                         $ret .= '/';
599                                 $ret .= rawurlencode($body);
600                         }
601                         if (isset($fragment)) {
602                                 if (!isset($ret))    $ret = array("href" => "");
603                                 if (!is_array($ret)) $ret = array("href" => $ret);
604                                 $ret["href"] .= "#" . Formatter\Base::escape_id_value(str_replace('%', '.', rawurlencode($fragment)));
605                         }
606                         break;
607                 case "tracker":
608                 case "ticket":
609                         if (preg_match('/^[0-9]+$/', $body)) {
610                                 if ($body > 50000) {
611                                         $ret = array("href" => $this->getContext('site_root_url').'/ticket/detail.php?id='.$body,
612                                                      "class" => "tracker");
613                                 } else {
614                                         $ret = array("href" => $this->getContext('site_root_url')."/tracker.php?id={$body}",
615                                                      "class" => "tracker");
616                                 }
617                         }
618                 break;
619                 case "cvs":
620                         if (preg_match('/^[a-z0-9_-]+:/', $body)) {
621                                 list($group, $path)= explode(':', $body, 2);
622                         } else {
623                                 $group = $this->getContext('sfjp.group_name');
624                                 $path  = $body;
625                         }
626                         if (substr($body, 0, 1) != '/')
627                                 $path = "/$path";
628                         $ret = $this->getContext('cvs_base_url') . "/${group}${path}";
629                         $ret = array("href" => $ret, "class" => "cvs");
630                         break;
631                 case "svn":
632                         if (preg_match('/^[a-z0-9_-]+:/', $body)) {
633                                 list($group, $path)= explode(':', $body, 2);
634                         } else {
635                                 $group = $this->getContext('sfjp.group_name');
636                                 $path  = $body;
637                         }
638                         if (substr($body, 0, 1) != '/')
639                                 $path = "/$path";
640                         $ret = $this->getContext('sfjp.svn_file_base_url') . $path;
641                         $ret = array("href" => $ret, "class" => "svn");
642                         break;
643                 case "user":
644                 case "users":
645                 case "id":
646                         if (preg_match('/^[a-z0-9_-]+$/', $body)) {
647                                 $ret = "/users/" . $body;
648                                 $ret = array("href" => $ret, "class" => "user");
649                                 if ($this->getContext('individual_usericon')) {
650                                         if (empty($ret['style'])) $ret['style'] = '';
651                                         $ret['style'] .= "background-image: url(".$this->getContext('individual_usericon')."{$body});";
652                                 }
653                                 if ($this->getContext('override_usericon_size')) {
654                                         if (empty($ret['style'])) $ret['style'] = '';
655                                         $ret['style'] .= "padding-left: ".$this->getContext('override_usericon_size')."px;";
656                                 }
657                         }
658                 break;
659                 case "comment":
660                         $parts = explode(':', $body, 3);
661                         if (count($parts) == 2) {
662                                 $ret = $this->getContext('site_root_url')
663                                         . '/ticket/detail.php?id='.$parts[0].'&cid='.$parts[1];
664                         } else if (!$parts[0]) {
665                                 $ret = $this->getContext('site_root_url')
666                                         . '/ticket/detail.php?id='.$parts[1].'&cid='.$parts[2];
667                         } else {
668                                 $ret = $this->getContext('site_root_url')
669                                         . "/ticket/browse.php?group_id=${parts[0]}&tid=${parts[1]}#comment:${body}";
670                         }
671                         $ret = array("href" => $ret, "class" => "ticket");
672                         break;
673                 case "project":
674                 case "projects":
675                         if (preg_match('/^[a-z0-9_-]+$/', $body)) {
676                                 $ret = $this->getContext('site_root_url')
677                                         . "/projects/$body/";
678                                 $ret = array("href" => $ret, "class" => "project");
679                         }
680                 break;
681                 case "prweb":
682                         $m = array();
683                         preg_match('/^https?:\/\/([^\/]+)/', $this->getContext('site_root_url'), $m);
684                         $site_domain = $m[1];
685                         $host = $this->getContext('sfjp.group_name').".${site_domain}";
686                         $path = $body;
687
688                         if (preg_match('/^([a-z0-9_-]+):(.*)$/', $body, $m)) { # prweb:project:path
689                                         $host = "$m[1].${site_domain}";
690                                 $path = $m[2];
691                         }
692                         if (substr($path, 0, 1) != "/")
693                                 $path = "/$path";
694                         $ret = "http://${host}${path}";
695                         $ret = array("href" => $ret, "class" => "project-web");
696                         break;
697                 case "release":
698                         $ret = $this->getContext('')
699                                 . "/projects/" . $body;
700                         $ret = array("href" => $ret, "class" => "release");
701                         break;
702                 case "isbn":
703                         $ret = array("href" => "http://www.amazon.co.jp/gp/product/$body",
704                                      "class" => "isbnbook", "rel" => "nofollow");
705                         if ($aid = $this->getContext('amazon_affiliate_id')) {
706                                 $ret['href'] .= "?tag={$aid}&linkCode=as2&camp=247&creative=1211&creativeASIN={$body}";
707                         }
708                         break;
709                 case "mailto":
710                         $ret = array("href" => "mailto:{$body}", "class" => "mail");
711                         break;
712                 default:
713                         if ($this->getInterwikiProcessor() &&
714                             is_callable(array($this->getInterwikiProcessor(), 'process'),
715                                         $body)){
716                                 $ret = $this->getInterwikiProcessor()->process($body);
717                         }
718                         break;
719                 }
720                 return $ret;
721         }
722
723         public function create_link($text, $args, $no_escape_html = false) {
724                 if (!is_array($args))
725                         $args = array("href" => $args);
726     
727                 if (array_key_exists('href', $args)) {
728                         $args["href"] = $this->encode_url_badchar($args["href"]);
729                 }
730
731                 $fmt = $this->formatter;
732                 $ret = "";
733                 $ret .= $fmt->open_element("link", $args);
734                 $ret .= $no_escape_html ? $fmt->raw_node($text) : $fmt->text_node($text);
735                 $ret .= $fmt->close_element("link");
736                 return $ret;
737         }
738
739         public function encode_url_badchar($str) {
740                 return preg_replace_callback('/[^\x21\x23-\x26\x28-\x3b\x3d\x3f-\x5a\x5e-\x7e]+/',
741                                              create_function('$m', 'return rawurlencode($m[0]);'),
742                                              $str);
743         }
744
745         public function process_plugin($str) {
746                 $match = null;
747                 if (!preg_match('/^([^()]+)(?:\((.*)\))?$/', $str, $match))
748                         return "";
749                 $name = $match[1];
750                 $args = isset($match[2]) && !empty($match[2]) ? preg_split('/\s*,\s*/', trim($match[2])) : array();
751
752                 try {
753                         $plugin_obj = $this->get_plugin_instance("Plugin\\{$name}");
754                         $plug_ret = call_user_func(array($plugin_obj, "process"), $args);
755                         if (!$this->is_vary) $this->is_vary = $plugin_obj->is_vary;
756                         if ($plugin_obj->is_block) {
757                                 if ($this->_pending_paragraph) {
758                                         $this->_pending_paragraph = false;
759                                 }
760                                 if ($this->in_stack_p('paragraph')) {
761                                         $plug_ret = $this->pop_blockstyle('paragraph')
762                                                 . $plug_ret . $this->push_blockstyle('paragraph');
763                                 }
764                         } else {
765                                 $plug_ret = $this->_flush_pending_paragraph() . $plug_ret;
766                         }
767                         return $plug_ret;
768                 } catch (Exception\Plugin_Error $e) {
769                         return $this->format_plugin_error($e);
770                 }
771         }
772
773         public function format_plugin_error($e) {
774                 if ($this->getContext("suppress_plugin_error")) {
775                         return '';
776                 } else {
777                         return $this->formatter->element('error', "Plugin Error: ".$e->getMessage());
778                 }
779         }
780
781         public function count_in_stack($name, &$stack) {
782                 if (!is_array($name))
783                         $name = array($name);
784                 $count = array_count_values($stack);
785                 $ret = 0;
786                 foreach ($name as $n) {
787                         $ret += array_key_exists($n, $count) ? $count[$n] : 0;
788                 }
789                 return $ret;
790         }
791
792         public function in_stack_p($name) {
793                 return in_array($name, $this->inline_stack) || in_array($name, $this->block_stack);
794         }
795
796         protected function push_blockstyle($name) {
797                 $this->block_stack[] = $name;
798                 return $this->formatter->open_element($name);
799         }
800
801         protected function pop_blockstyle($name) {
802                 return $this->clear_blockstyle($name, 1);
803         }
804
805         protected function clear_inlinestyle($name = null) {
806                 return $this->clear_stylestack($this->inline_stack, $name);
807         }
808
809         protected function clear_blockstyle($name = null, $max = null) {
810                 $ret = $this->clear_inlinestyle();
811                 $ret .= $this->clear_stylestack($this->block_stack, $name, $max);
812                 return $ret;
813         }
814
815         protected function clear_stylestack(&$stack, $name, $max = null) {
816                 $ret = "";
817                 $i = 1;
818                 if (isset($name) && !in_array($name, $stack)) {
819                         // return if $name setted and not found in stack.
820                         return $ret;
821                 }
822                 while ($elem = array_pop($stack)) {
823                         $ret .= $this->formatter->close_element($elem);
824                         if (isset($name) && $name == $elem) {
825                                 $i++;
826                                 if ($max && $i > $max || !in_array($name, $stack))
827                                         break;
828                         }
829                 }
830                 return $ret;
831         }
832
833         protected function inlinestyle_callback($match) {
834                 $rule = $this->get_matched_rule($match, $this->inlinestyle_rules);
835                 if (!isset($rule) && $match[1][0] === "!")
836                         return $this->formatter->text_node(substr($match[0][0], 1));
837                 $name = $rule[0];
838                 if (!in_array($name, $this->inline_stack)) {
839                         $this->inline_stack[] = $name;
840                         return $this->formatter->open_element($name);
841                 } else {
842                         $ret = "";
843                         while ($name != ($cur_elem = array_pop($this->inline_stack))) { 
844                                 $ret .= $this->formatter->close_element($cur_elem);
845                         }
846                         $ret .= $this->formatter->close_element($name);
847                         return $ret;
848                 }
849         }
850
851         protected function get_matched_rule($match, $rules, $rule_have_escape = true) {
852                 if ($rule_have_escape && strlen($match[1][0])) /* ! escaped */
853                         return null;
854                 $excess = $rule_have_escape ? 2 : 1;
855                 for($i = $excess; isset($match[$i]); $i++) {
856                         if ($match[$i][1] >= 0)
857                                 break;
858                 }
859                 return $rules[$i - $excess];
860         }
861
862         private function _flush_pending_paragraph() {
863                 if (!$this->_pending_paragraph) return '';
864                 $this->_pending_paragraph = false;
865                 return $this->push_blockstyle("paragraph");
866         }
867 }