OSDN Git Service

Import current code. master
authorTatsuki Sugiura <sugi@nemui.org>
Tue, 28 Jul 2015 10:04:11 +0000 (19:04 +0900)
committerTatsuki Sugiura <sugi@nemui.org>
Tue, 28 Jul 2015 10:04:11 +0000 (19:04 +0900)
31 files changed:
sfjp/wiki/exception/base.php [new file with mode: 0644]
sfjp/wiki/exception/internal_error.php [new file with mode: 0644]
sfjp/wiki/exception/invalid_argument.php [new file with mode: 0644]
sfjp/wiki/exception/plugin_error.php [new file with mode: 0644]
sfjp/wiki/formatter/base.php [new file with mode: 0644]
sfjp/wiki/formatter/buffered_delegator.php [new file with mode: 0644]
sfjp/wiki/formatter/html.php [new file with mode: 0644]
sfjp/wiki/parser.php [new file with mode: 0644]
sfjp/wiki/plugin/base.php [new file with mode: 0644]
sfjp/wiki/plugin/br.php [new file with mode: 0644]
sfjp/wiki/plugin/footnote.php [new file with mode: 0644]
sfjp/wiki/plugin/pagebreak.php [new file with mode: 0644]
sfjp/wiki/plugin/pageoutline.php [new file with mode: 0644]
sfjp/wiki/processor/auto.php [new file with mode: 0644]
sfjp/wiki/processor/base.php [new file with mode: 0644]
sfjp/wiki/processor/code.php [new file with mode: 0644]
sfjp/wiki/processor/comment.php [new file with mode: 0644]
sfjp/wiki/processor/denied.php [new file with mode: 0644]
sfjp/wiki/processor/html.php [new file with mode: 0644]
sfjp/wiki/processor/pre.php [new file with mode: 0644]
sfjp/wiki/processor/trac.php [new file with mode: 0644]
sfjp/wiki/processor/trac_oneline.php [new file with mode: 0644]
sfjp/wiki/storage/base.php [new file with mode: 0644]
sfjp/wiki/storage/dummy.php [new file with mode: 0644]
sfjp/wiki/storage/file.php [new file with mode: 0644]
sfjpWikiParser.php [new file with mode: 0644]
test/FormatTest.php [new file with mode: 0644]
test/ParserTest.php [new file with mode: 0644]
test/StorageTest.php [new file with mode: 0644]
test/all.php [new file with mode: 0755]
test/lib/kses.php [new file with mode: 0644]

diff --git a/sfjp/wiki/exception/base.php b/sfjp/wiki/exception/base.php
new file mode 100644 (file)
index 0000000..93b853a
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+namespace sfjp\Wiki\Exception;
+class Base extends \RuntimeException {}
\ No newline at end of file
diff --git a/sfjp/wiki/exception/internal_error.php b/sfjp/wiki/exception/internal_error.php
new file mode 100644 (file)
index 0000000..888cb95
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+namespace sfjp\Wiki\Exception;
+class Internal_Error extends Base {}
+
diff --git a/sfjp/wiki/exception/invalid_argument.php b/sfjp/wiki/exception/invalid_argument.php
new file mode 100644 (file)
index 0000000..b2e3372
--- /dev/null
@@ -0,0 +1,4 @@
+<?php
+namespace sfjp\Wiki\Exception;
+class Invalid_Argument extends Base {}
+
diff --git a/sfjp/wiki/exception/plugin_error.php b/sfjp/wiki/exception/plugin_error.php
new file mode 100644 (file)
index 0000000..3ddac7f
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+namespace sfjp\Wiki\Exception;
+class Plugin_Error extends Base {};
\ No newline at end of file
diff --git a/sfjp/wiki/formatter/base.php b/sfjp/wiki/formatter/base.php
new file mode 100644 (file)
index 0000000..d4ff1c5
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+namespace sfjp\Wiki\Formatter;
+abstract class Base {
+       protected $processor;
+       function __construct() {
+       }
+
+       function __destruct() {
+               unset($this->processor);
+       }
+
+       public function cleanup() {}
+
+       public function reset() {}
+
+       public function setProcessor($proc) {
+               $this->processor = $proc;
+       }
+
+       public function getProcessor() {
+               return $this->processor;
+       }
+
+       public function setContext($c) {
+               return $this->getProcessor()->setContext($c);
+       }
+
+       public function getContext($key = null) {
+               return $this->getProcessor()->getContext($key);
+       }
+
+       public function __($text, $args=array()) {
+               return $this->getProcessor()->__($text, $args);
+       }
+
+       abstract public function raw_node($string);
+       abstract public function text_node($text);
+       abstract public function open_element($neme, $opt = null);
+       abstract public function close_element($name, $opt = null);
+
+       static public function escape_id_value($str) {
+               $str = preg_replace_callback('/[^A-Za-z0-9_:.-]+/',
+                                            create_function('$m', '$p = unpack("H*", $m[0]); return $p[1];'),
+                                            $str);
+               if (!ctype_alpha(substr($str, 0, 1))) $str = "badid-".$str;
+               return $str;
+       }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/formatter/buffered_delegator.php b/sfjp/wiki/formatter/buffered_delegator.php
new file mode 100644 (file)
index 0000000..45cfd8b
--- /dev/null
@@ -0,0 +1,166 @@
+<?php
+namespace sfjp\Wiki\Formatter;
+class Buffered_DocNode {
+       public $name;
+       public $data;
+       public $parent;
+       public $children = array();
+
+       function __construct($name, $data=null) {
+               $this->name = $name;
+               $this->data = $data;
+       }
+
+       function __destruct() {
+               $this->destroy();
+       }
+
+       public function destroy() {
+               if (isset($this->children)) {
+                       foreach ($this->children as $c) {
+                               $c->destroy();
+                       }
+               }
+               unset($this->parent);
+               unset($this->name);
+               unset($this->data);
+               unset($this->children);
+       }
+
+       public function __toString() {
+               return '';
+       }
+
+       public function getParent() {
+               return $this->parent;
+       }
+
+       public function getPrev() {
+               $prev = null;
+               $found = false;
+               foreach ($this->parent->children as $c) {
+                       if ($c === $this) {
+                               $found = true;
+                               break;
+                       }
+                       $prev = $c;
+               }
+               return $found ? $prev : null;
+       }
+
+       public function getNext() {
+               $idx = null;
+               $list = $this->parent->children;
+               foreach ($list as $i => $c) {
+                       if ($c === $this) {
+                               $idx = $i;
+                               break;
+                       }
+               }
+               return (isset($i) && array_key_exists($idx+1, $list)) ? $list[$idx+1] : null;
+       }
+
+       public function hasChildren() {
+               return !empty($this->children);
+       }
+
+       public function addChild($name, $data=null) {
+               $myclass = get_called_class();
+               $child = new $myclass($name, $data);
+               $child->parent = $this;
+               $this->children []= $child;
+               return $child;
+       }
+}
+
+class Buffered_Delegator extends Base {
+       public $root_node;
+       public $cur_node;
+       public $formatter;
+  
+       function __construct() {
+               parent::__construct();
+               $this->reset();
+       }
+
+       function __destruct() {
+               if ($this->root_node) $this->root_node->destroy();
+               unset($this->root_node);
+               unset($this->cur_node);
+               unset($this->formatter);
+       }
+
+       public function reset() {
+               if ($this->root_node) $this->root_node->destroy();
+               $this->root_node = new Buffered_DocNode('*ROOT');
+               $this->cur_node = $this->root_node;
+               if (!$this->formatter) {
+                       $this->formatter = new HTML();
+               }
+               $this->formatter->reset();
+       }
+
+       public function cleanup() {
+               return $this->render();
+       }
+
+       public function render() {
+               return $this->render_node($this->root_node);
+       }
+
+       protected function render_node($node) {
+               $fmt = $this->formatter;
+               $ret = '';
+               if ($node->name === "*ROOT") {
+                       # nothing
+                               } elseif ($node->name === "*RAW") {
+                       $ret .= $fmt->raw_node($node->data);
+               } elseif ($node->name === "*TEXT") {
+                       $ret .= $fmt->text_node($node->data);
+               } else {
+                       $ret .= $fmt->open_element($node->name, $node->data);
+               }
+               foreach ($node->children as $child) {
+                       $ret .= $this->render_node($child);
+               }
+               if (substr($node->name, 0, 1) != "*") {
+                       $ret .= $fmt->close_element($node->name, $node->data);
+               }
+               return $ret;
+       }
+
+       public function open_element($name, $opt=null) {
+               $this->cur_node = $this->cur_node->addChild($name, $opt);
+               return $this->cur_node;
+       }
+
+       public function close_element($name, $opt=null) {
+               // TODO: now all options are ignored.
+               $this->cur_node = $this->cur_node->parent;
+               return $this->cur_node;
+       }
+
+       public function text_node($text) {
+               return $this->cur_node->addChild('*TEXT', $text);
+       }
+
+       public function raw_node($string) {
+               return $this->cur_node->addChild('*RAW', $string);
+       }
+
+       public function _dump_tree() {
+               return $this->_inspect_node($this->root_node, 0);
+       }
+
+       public function _inspect_node($node, $level) {
+               $ret = '';
+               for ($i = 0; $level*2 > $i; $i++) {
+                       $ret .= '&nbsp;';
+               }
+               $ret .= "+ [" . $node->name . "] (" . htmlspecialchars(substr(print_r($node->data, 1), 0, 80)) . ")<br />";
+               foreach ($node->children as $c) {
+                       $ret .= $this->_inspect_node($c, $level+1);
+               }
+               return $ret;
+       }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/formatter/html.php b/sfjp/wiki/formatter/html.php
new file mode 100644 (file)
index 0000000..acef207
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+namespace sfjp\Wiki\Formatter;
+class HTML extends Base {
+       static public $singleton_tags = array('line', 'hr', 'image', 'img', 'newline', 'br');
+
+       public $tagmap;
+
+       function __construct() {
+               $this->tagmap = array(
+                                     "bold"        => "strong",
+                                     "italic"      => "em",
+                                     "strike"      => "del",
+                                     "underline"   => array('span', array('style' => 'text-decoration: underline;')),
+                                     "monospace"   => "tt",
+                                     "superscript" => "sup",
+                                     "subscript"   => "sub",
+                                     "table_row"   => "tr",
+                                     "table_col"   => "td",
+                                     "list_mark"   => "ul",
+                                     "list_num"    => "ol",
+                                     "list_roma"   => array("ol", array('style' => 'list-style-type: lower-roman;')),
+                                     "list_ROMA"   => array("ol", array('style' => 'list-style-type: upper-roman;')),
+                                     "list_alpha"  => array("ol", array('style' => 'list-style-type: lower-alpha;')),
+                                     "list_ALPHA"  => array("ol", array('style' => 'list-style-type: upper-alpha;')),
+                                     "list_item"   => "li",
+                                     "quote"       => array("blockquote", array("class" => "citation")),
+                                     "paragraph"   => "p",
+                                     "heading1"    => "h1",
+                                     "heading2"    => "h2",
+                                     "heading3"    => "h3",
+                                     "heading4"    => "h4",
+                                     "heading5"    => "h5",
+                                     "heading6"    => "h6",
+                                     "line"        => "hr",
+                                     "indent"      => array('div', array('class' => 'indent')),
+                                     "image"       => "img",
+                                     "newline"     => "br",
+                                     "error"       => array("span", array('class' => 'wiki-system-error')),
+                                     );
+       }
+
+       public function reset() {
+               ; # nothing
+                         }
+
+       public function cleanup() {
+    
+       }
+
+       public function raw_node($string) {
+               return $string;
+       }
+
+       public function text_node($text) {
+               return htmlspecialchars($text);
+       }
+
+       public function open_element($name, $opt=null) {
+               if (!isset($opt)) $opt = array();
+               switch ($name) {
+               case "bolditalic":
+                       return self::tag_builder("strong", $opt)."<em>";
+               case "table":
+                       return
+                               self::tag_builder("table",
+                                                 array_merge(array("class" => "wikitable"),
+                                                             $opt))
+                               . '<tbody>';
+               case "link":
+                       return self::tag_builder("a", $opt);
+               default:
+                       if (array_key_exists($name, $this->tagmap)) {
+                               $taginfo = self::merge_tagopt($this->tagmap[$name], $opt);
+                               return self::tag_builder($taginfo[0], $taginfo[1]);
+                       } else {
+                               return self::merge_tagopt($name, $opt);
+                       }
+               }
+       }
+
+       public function close_element($name, $opt=array()) {
+               if (in_array($name, static::$singleton_tags))
+                       return ''; # no close tag.
+               switch ($name) {
+               case "bolditalic":
+               return "</em></strong>";
+               case "table":
+                       return "</tbody></table>";
+               case "link":
+                       return "</a>";
+               default:
+                       if (array_key_exists($name, $this->tagmap)) {
+                               $taginfo = self::merge_tagopt($this->tagmap[$name], $opt);
+                               return self::tag_builder($taginfo[0], $taginfo[1], false);
+                       } else {
+                               return "</$name>";
+                       }
+               }
+       }
+
+       public function element($name, $text = '', $opts = array()) {
+               return $this->open_element($name, $opts) . 
+                       $this->text_node($text) .
+                       $this->close_element($name, $opts);
+       }
+
+       static protected function merge_tagopt($taginfo, $newopt) {
+               if (!is_array($taginfo))
+                       $taginfo = array($taginfo, array());
+               if (!isset($newopt)) return $taginfo;
+               $taginfo[1] = $taginfo[1] + $newopt;
+               return $taginfo;
+       }
+
+       static function tag_builder($name, $opt=null, $open=true) {
+               $ret = "<".($open ? '' : '/').htmlspecialchars($name);
+
+               if ($name == "a" && array_key_exists("href", $opt) &&
+                   !(array_key_exists("pass_unsecure", $opt) && $opt["pass_unsecure"])) {
+                       $opt["href"] = preg_replace('/^(javascript|telnet|tel):/i', '', $opt["href"]);
+               }
+
+               if ($open && isset($opt) && is_array($opt) && count($opt) > 0 ) {
+                       foreach($opt as $pname => $pvalue) {
+                               if ($pname === "id")  {
+                                       $pvalue = self::escape_id_value($pvalue);
+                               }
+                               if (isset($pvalue)) {
+                                       $ret .= " " . htmlspecialchars($pname) . '="'. htmlspecialchars($pvalue) .'"';
+                               } else {
+                                       $ret .= " " . htmlspecialchars($pname);
+                               }
+                       }
+               } else if ($open && isset($opt) && !empty($opt)) {
+                       $ret .= " $opt";
+               }
+               
+               $close = in_array($name, static::$singleton_tags) ? ' />' : '>';
+               return $ret . $close;
+       }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/parser.php b/sfjp/wiki/parser.php
new file mode 100644 (file)
index 0000000..14feee5
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+namespace sfjp\Wiki;
+
+class Parser {
+       public $text;
+       public $processor;
+
+       function __construct($context = null, $processor = null, $formatter = null) {
+               if ($processor) {
+                       $this->processor = $processor;
+               } else {
+                       $this->processor = new Processor\Trac();
+               }
+               if ($context)
+                       $this->processor->setContext($context);
+               if ($formatter)
+                       $this->processor->setFormatter($formatter);
+               $storage_class = $this->processor->getContext('storage.class');
+               if (!$storage_class) $storage_class = 'Dummy';
+               $c = new \ReflectionClass("sfjp\\Wiki\\Storage\\{$storage_class}");
+               $storage = $c->newInstance();
+               $this->processor->setContext(array('storage' => $storage));
+       }
+
+       public function parse($text) {
+               $this->processor->reset();
+               return $this->processor->process($text)->getFormattedText();
+       }
+
+       public function getFormattedText() {
+               return $this->processor->getFormattedText();
+       }
+
+       public function isVary() {
+               return $this->processor->isVary();
+       }
+  
+       public function setContext($c) {
+               $this->processor->setContext($c);
+       }
+
+       public function getContext($key = null) {
+               return $this->processor->getContext($key);
+       }
+
+       public function removeContext($key) {
+               $this->processor->removeContext($key);
+       }
+
+       public function clearContext() {
+               $this->processor->clearContext();
+       }
+
+       public function reset() {
+               $this->processor->reset();
+       }
+}
diff --git a/sfjp/wiki/plugin/base.php b/sfjp/wiki/plugin/base.php
new file mode 100644 (file)
index 0000000..d058b83
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+abstract class Base {
+       public $is_vary  = true;
+       public $is_block = false;
+       public $processor;
+
+       function __construct($processor) {
+               $this->processor = $processor;
+       }
+
+       public function setContext($ctx) {
+               return $this->processor->setContext($ctx);
+       }
+
+       public function getContext($key=null) {
+               return $this->processor->getContext($key);
+       }
+
+       public function error($msg) {
+               $this->setContext(array('whole_page_cachable' => false));
+               throw new Plugin_Error($msg);
+       }
+
+       public function __($text, $args=array()) {
+               if (!$this->processor->hasContext('i18n')) {
+                       return $text;
+               } else {
+                       return $this->getContext('i18n')->__($text, $args);
+               }
+       }
+
+       public function getProcessor() {
+               return $this->processor;
+       }
+
+       public function getFormatter() {
+               return $this->getProcessor()->getFormatter();
+       }
+
+       public function parseArgs($args, $list = array(), $default = null) {
+               $opt = array();
+               foreach ($args as $arg) {
+                       if (strpos($arg, '=')) {
+                               list($key, $value) = explode('=', $arg, 2);
+                               if (!$key)
+                                       continue;
+                       } elseif (isset($default) && !empty($default)) {
+                               $key = $default;
+                               $value = $arg;
+                       }
+                       if (!in_array($key, $list) && !array_key_exists($key, $list)) {
+                               $this->error("unknown argument $arg");
+                       }
+                       $opt[$key] = $value;
+               }
+               return $opt;
+       }
+
+       public function render_table($data) {
+               $fmt = $this->getFormatter();
+               $ret = '';
+
+               $ret .= $fmt->open_element('table');
+               foreach ($data as $cols) {
+                       $ret .= $fmt->open_element('table_row');
+                       foreach ($cols as $col) {
+                               $ret .= $fmt->open_element('table_col');
+                               $ret .= $col;
+                               $ret .= $fmt->close_element('table_col');
+                       }
+                       $ret .= $fmt->close_element('table_row');
+               }
+               $ret .= $fmt->close_element('table');
+               return $fmt->raw_node($ret);
+       }
+
+       public function render_date_separated_list($data) {
+               $wikistr = '';
+               $last_date = null;
+
+               foreach ($data as $item) {
+                       if ($last_date != date('Y-m-d', $item[0])) 
+                               $wikistr .= "==== " . date('Y-m-d', $item[0]) . " ====\n";
+                       $last_date = date('Y-m-d', $item[0]);
+                       $wikistr .= " * " . $item[1];
+               }
+               $p = new \sfjp\Wiki\Parser($this->getContext());
+               return $this->getFormatter()->raw_node($p->parse($wikistr));
+       }
+
+}
\ No newline at end of file
diff --git a/sfjp/wiki/plugin/br.php b/sfjp/wiki/plugin/br.php
new file mode 100644 (file)
index 0000000..d1eab7b
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+class Br extends Base {
+       public $is_vary = false;
+       public function process($args) {
+               $opt = '';
+               if (isset($args[0]) && !empty($args[0])) {
+                       list ($key, $val) = explode('=', strtolower(trim($args[0])));
+                       if ($key == "clear" && ($val == 'left' || $val == 'all' || $val == 'right')) {
+                               $opt = " clear=\"${val}\"";
+                       }
+               }
+               return $this->getFormatter()->open_element('newline');
+       }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/plugin/footnote.php b/sfjp/wiki/plugin/footnote.php
new file mode 100644 (file)
index 0000000..22d8c39
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+class FootNoteProc {
+       public $count = 0;
+       public $message = '';
+       public $formatter;
+
+       public function process() {
+               $fmt = $this->formatter;
+               $ret = '';
+               if ($this->count == 1) {
+                       $ret .= $fmt->raw_node('<hr><ol id="footnote" class="footnote">');
+               }
+               $ret .= $fmt->raw_node("<li id=\"_fn_note-{$this->count}\">"
+                                      . "<a class=\"footnote-revref footnote-counter\" href=\"#_fn_ref-{$this->count}\">*{$this->count}</a>"
+                                      . "{$this->message}</li>");
+               if (FootNote::$counter - 1 == $this->count) {
+                       $ret .= $fmt->raw_node('</ol>');
+               }
+               return $ret;
+       }
+}
+
+class FootNote extends Base {
+  static public $counter = 1;
+  public $is_vary = false;
+  public function process($args) {
+    $oneline_trac = new \sfjp\Wiki\Processor\Trac_oneline();
+    $oneline_trac->enable_plugin = false;
+
+    $proc = new FootNoteProc();
+    $proc->count     = self::$counter++;
+    $proc->formatter = $this->getFormatter();
+    $proc->message   = $oneline_trac->process($args[0])->getFormattedText();
+
+    $this->processor->addPostProc($proc);
+
+    return $this->getFormatter()->raw_node(
+      "<span id=\"_fn_ref-{$proc->count}\" class=\"footnote-ref\">"
+      . "<a href=\"#_fn_note-{$proc->count}\">*{$proc->count}</a></span>");
+  }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/plugin/pagebreak.php b/sfjp/wiki/plugin/pagebreak.php
new file mode 100644 (file)
index 0000000..d53c2a5
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+class PageBreak extends Base {
+       public $is_vary = false;
+       public $is_block = true;
+       public function process($args) {
+               return $this->getFormatter()->raw_node('<div style="page-break-after: always;"></div>');
+       }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/plugin/pageoutline.php b/sfjp/wiki/plugin/pageoutline.php
new file mode 100644 (file)
index 0000000..bae632f
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+class PageOutline extends Base {
+       public $is_vary = false;
+       public $is_block = true;
+       public function process($args) {
+               $proc = $this->processor;
+               $opt = array('start' => 1, 'depth' => 3, 'type' => 'ordered');
+               $opt['title'] = $this->__('Outline');
+               $counter = array();
+
+               foreach ($args as $arg) {
+                       if (!strpos($arg, '='))
+                               return $this->error("unknown argument $arg");
+                       list($key, $value) = explode('=', $arg, 2);
+                       if (!$key || !array_key_exists($key, $opt))
+                               return $this->error("unknown argument $arg");
+                       $opt[$key] = $value;
+               }
+
+               $text  = "";
+               $match = array();
+               $orig  = $proc->getText();
+               $orig  = preg_replace('/^{{{.*?^}}}/sm', '', $orig);
+               foreach($proc->getBlockstyleRules() as $r) {
+                       if ($r[0] != "heading")
+                               continue;
+                       $headre = $r[1];
+                       break;
+               }
+               $parser = new \sfjp\Wiki\Parser(array());
+               preg_match_all("/$headre/m", $orig, $match);
+               for($h = 0; array_key_exists($h, $match[1]); $h++) {
+                       $level  = strlen($match[1][$h]);
+                       $label  = preg_replace('/<.*?>/', '', $parser->parse(preg_replace('/\[\[.*?\]\]/', '', $match[0][$h])));
+                       if (isset($match[3][$h]) && strlen($match[3][$h])) {
+                               $linkid = trim($match[3][$h]);
+                       } else {
+                               $linkid = "h" . ($level + intval($proc->getContext('head_excess'))) . "-";
+                               $linkid .= preg_replace_callback($parser->processor->getInlinestyleRegex(),
+                                                                create_function('$m', 'return empty($m[1]) ? "" : substr($m[0], 1);'),
+                                                                trim($match[2][$h]));
+                       }
+                       if (!isset($counter)) $counter[$linkid] = 0;
+                       $c =& $counter[$linkid];
+                       if (++$c > 1) $linkid .= "-{$c}";
+                       $linkid = str_replace('%', '.', rawurlencode($linkid));
+
+                       $outlevel = $level - ($opt['start']-1);
+                       if ($outlevel > 0 && $outlevel <= $opt['depth'])
+                               $text .= sprintf("%".($outlevel*2)."s%s [#%s %s]\n", '',
+                                                ($opt['type'] == 'unordered' ? '*' : '1.'),
+                                                $linkid, htmlspecialchars_decode($label));
+               }
+               return $this->getFormatter()->raw_node(
+                                                      '<div class="pageoutline"><div class="pageoutline-title">'
+                                                      . '<div class="action">'
+                                                      . '<button type="button" onClick="javascript:togglePageOutline(this)">'
+                                                      . '<img src="//static.sourceforge.jp/wiki/images/icons/roll-up.gif" border="0"></button>'
+                                                      . '</div>'
+                                                      . $this->__($opt['title']) 
+                                                      . '</div>' . $parser->parse($text) . '</div>');
+       }
+
+
+}
\ No newline at end of file
diff --git a/sfjp/wiki/processor/auto.php b/sfjp/wiki/processor/auto.php
new file mode 100644 (file)
index 0000000..75cea59
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+namespace sfjp\Wiki\Processor;
+class Auto extends Base {
+       public function process($text) {
+               $match = null;
+               $typemap = array();
+               if (preg_match('/\A#!\/\S+\/(?:env\s+)?(ada|asm|asw|bash|csh|perl|python|ruby|sh|tcsh|zsh|php)/',
+                              $text, $match)) {
+                       $type = array_key_exists($match[1], $typemap) ? $typemap[$match[1]] : $match[1];
+                       $p = new Code(array($type));
+               } else {
+                       $p = new Pre();
+               }
+               $p->setContext($this->getContext());
+               $p->setFormatter($this->getFormatter());
+               $this->formatted_text = $p->process($text)->getFormattedText();
+               if (!$this->hasContext("parent_processor"))
+                       $this->formatted_text .= $this->getFormatter()->cleanup();
+               return $this;
+       }
+}
diff --git a/sfjp/wiki/processor/base.php b/sfjp/wiki/processor/base.php
new file mode 100644 (file)
index 0000000..8b8711f
--- /dev/null
@@ -0,0 +1,280 @@
+<?php
+namespace sfjp\Wiki\Processor;
+use sfjp\Wiki\Exception;
+abstract class Base {
+       private $context;
+       public $is_vary;
+       public $text;
+       public $formatted_text;
+       public $preproc;
+       public $postproc;
+       public $formatter;
+       public $args = array();
+
+       protected static $reserved = array('__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor');
+
+       protected static $context_key_rename_rules = array(
+               'plugin_include_path' => 'extension.additional_include_path',
+               'plugin.order' => 'extension.acl_order',
+               'plugin.allow' => 'extension.allow',
+               'plugin.deny' => 'extension.deny',
+               'supress_plugin_error' => 'extension.hide_error',
+       );
+
+       function __construct($args = null) {
+               $default_ctx = array("wiki_baseurl"  => ".",
+                                    "site_root_url" => (empty($_SERVER['HTTPS']) ? 'http' : 'https') . "://" . (empty($_SERVER['HTTP_HOST']) ? 'localhost.localdomain' : $_SERVER['HTTP_HOST']),
+                                    "svn_base_url"  => "http://svn.sourceforge.jp/view",
+                                    "cvs_base_url"  => "http://cvs.sourceforge.jp/view",
+                                    "sfjp.group_name"    => null,
+                                    "sfjp.group_id"      => null,
+                                    "extension.additional_include_path" => null,
+                                    "extension.acl_order" => "deny,allow",
+                                    "extension.allow"  => array(),
+                                    "extension.deny"   => array(),
+                                    "extension.hide_error" => false,
+                                    "self_url"      => array_key_exists('REQUEST_URI', $_SERVER) ? $_SERVER['REQUEST_URI'] : '',
+                                    "cache_manager" => null,
+                                    "cache_depends" => array(),
+                                    "whole_page_cachable" => true,
+                                    "head_excess"   => 0,
+                                    "gen_head_id"   => true,
+                                    "internal_url_regex" => null,
+                                    "nofollow_on_external_links" => true,
+                                    );
+               $this->context = $default_ctx;
+               $this->clearPreProc();
+               $this->clearPostProc();
+       }
+
+       function __destruct() {
+               unset($this->formatter);
+       }
+
+       public function setArgs($args) {
+               if (!is_array($args))
+                       $args = array($args);
+               $this->args = $args;
+       }
+
+       public function getArgs($pos = null) {
+               if (is_null($pos))
+                       return $this->args;
+               return $this->args[$pos];
+       }
+
+       public function getContext($key = null) {
+               if (isset($key)) {
+                       $key = static::renameContextKey($key);
+                       return array_key_exists($key, $this->context) ? $this->context[$key] : null;
+               } else {
+                       return $this->context;
+               }
+       }
+
+       static public function renameContextKey($key) {
+               if (!array_key_exists($key, static::$context_key_rename_rules))
+                       return $key;
+               return static::$context_key_rename_rules[$key];
+       }
+
+
+       public function hasContext($key) {
+               return array_key_exists(static::renameContextKey($key), $this->context);
+       }
+
+       public function setContext($c) {
+               foreach ($c as $key => $val) {
+                       $key = static::renameContextKey($key);
+                       $this->context[$key] = $val;
+               }
+               $this->context = array_merge($this->context, $c);
+       }
+
+       public function removeContext($key) {
+               unset($this->context[static::renameContextKey($key)]);
+       }
+  
+       public function clearContext() {
+               $this->context = array();
+       }
+
+       public function incrementCounter($name) {
+               $c = &$this->getCounter($name);
+               return ++$c;
+       }
+
+       public function &getCounter($name) {
+               $c = &$this->context["counters"];
+               if (!isset($c[$name])) {
+                       $c[$name] = 0;
+               }
+               return $c[$name];
+       }
+
+       public function setCounter($name, $val) {
+               $c = &$this->context["counters"];
+               $c[$name] = $val;
+       }
+
+       public function clearCounter($name = null) {
+               if (isset($name)) {
+                       $this->setCounter($name, 0);
+               } else {
+                       $this->setContext(array("counters" => array()));
+               }
+       }
+
+       public function &getCacheDepends() {
+               return $this->context["cache_depends"];
+       }
+
+       public function addCacheDepends($deps) {
+               if (empty($deps)) return;
+               if (!is_array($deps)) $deps = array($deps);
+               $c = &$this->context["cache_depends"];
+               foreach ($deps as $dep) {
+                       $c []= $dep;
+               }
+       }
+
+       public function clearCacheDepends() {
+               $this->context["cache_depends"] = array();
+       }
+
+       public function reset() {
+               $this->is_vary = false;
+               $this->text = "";
+               $this->formatted_text = "";
+               $this->clearCounter();
+               $this->clearPreProc();
+               $this->clearPostProc();
+               $this->clearCacheDepends();
+               if ($this->getFormatter())
+                       $this->getFormatter()->reset();
+       }
+
+       public function isVary() {
+               return $this->is_vary;
+       }
+
+       public function setText($text) {
+               $old = $this->text;
+               $this->text = $text;
+               return $old;
+       }
+
+       public function getText() {
+               return $this->text;
+       }
+
+       public function getFormattedText() {
+               $ret = '';
+               foreach ($this->preproc as $p) {
+                       $ret .= $p->process();
+               }
+               $ret .= $this->formatted_text;
+               foreach ($this->postproc as $p) {
+                       $ret .= $p->process();
+               }
+               return $ret;
+       }
+
+       public function get_plugin_instance($class_name) {
+               $check_name = str_replace('\\', '/', $class_name);
+               $name = basename($check_name);
+
+               if (!$this->check_plugin_allowed($check_name))
+                       throw new Exception\Plugin_Error("Load Denied: $name");
+               if (!preg_match('/^[A-Za-z0-9._-]+$/', $name))
+                       throw new Exception\Plugin_error("Wrong Plugin Name: $name");
+
+               if (in_array(strtolower($name), self::$reserved)) {
+                       $class_name = substr($class_name, 0, strlen($class_name) - strlen($name)) . "_{$name}";
+               }
+
+               try {
+                       $orig_include_path = null;
+                       if ($this->getContext('extension.additional_include_path')) {
+                               $orig_include_path = ini_get('include_path');
+                               ini_set('include_path', $this->getContext('extension.additional_include_path').":$orig_include_path");
+                       }
+                       $class = new \ReflectionClass("\\sfjp\\Wiki\\{$class_name}");
+                       if ($orig_include_path)
+                               ini_set('include_path', $orig_include_path);
+                       $instance = $class->newInstance($this);
+                       if (!is_callable(array($instance, "process")))
+                               throw new \ReflectionException();
+               } catch (\ReflectionException $e) {
+                       ini_set('include_path', $orig_include_path);
+                       error_log("Plugin '$name' load failed.");
+                       throw new Exception\Plugin_Error("Not Found: {$name}");
+               }
+               return $instance;
+       }
+
+
+
+       protected function check_plugin_allowed($name) {
+               $order = $this->getContext("plugin.order");
+               $allow = $this->getContext("plugin.allow");
+               $deny  = $this->getContext("plugin.deny");
+               $ret   = false;
+               if (!$order) $order = "deny,allow";
+               $order = strtolower($order);
+               if (!is_array($allow)) $allow = array($allow);
+               if (!is_array($deny)) $deny = array($deny);
+               $deny  = array_map('strtolower', $deny);
+               $allow = array_map('strtolower', $allow);
+               $name  = strtolower($name);
+
+               if ($order === "deny,allow") {
+                       $ret = true;
+                       if (in_array('all', $deny))  $ret = false;
+                       if (in_array($name, $deny))  $ret = false;
+                       if (in_array('all', $allow)) return true;
+                       if (in_array($name, $allow)) return true;
+               } else { # "allow,deny"
+                               $ret = false;
+                       if (in_array('all', $allow)) $ret = true;
+                       if (in_array($name, $allow)) $ret = true;
+                       if (in_array('all', $deny))  return false;
+                       if (in_array($name, $deny))  return false;
+               }
+               return $ret;
+       }
+
+       public function addPreProc($obj) {
+               $this->preproc[] = $obj;
+       }
+
+       public function clearPreProc() {
+               $this->preproc = array();
+       }
+
+       public function addPostProc($obj) {
+               $this->postproc[] = $obj;
+       }
+
+       public function clearPostProc() {
+               $this->postproc = array();
+       }
+
+       public function getFormatter() {
+               return $this->formatter;
+       }
+
+       public function setFormatter($f) {
+               $this->formatter = $f;
+               if (!$this->formatter->getProcessor())
+                       $this->formatter->setProcessor($this);
+       }
+
+       public function __($text, $args=array()) {
+               if (!$this->hasContext('i18n')) {
+                       return $text;
+               } else {
+                       return $this->getContext('i18n')->__($text, $args);
+               }
+       }
+}
diff --git a/sfjp/wiki/processor/code.php b/sfjp/wiki/processor/code.php
new file mode 100644 (file)
index 0000000..26f1570
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+namespace sfjp\Wiki\Processor;
+require_once 'include/general/syntax-highlight.php';
+class Code extends Base {
+       public function process($text) {
+               $this->setText($text);
+               $lang = $this->getArgs(0);
+               $fmt = $this->getFormatter();
+               $ret = $fmt->raw_node(highlight_syntax(trim($text, "\r\n"), $lang));
+               if (!$this->hasContext("parent_processor"))
+                       $ret .= $this->getFormatter()->cleanup();
+               $this->formatted_text = $ret;
+               return $this;
+       }
+}
diff --git a/sfjp/wiki/processor/comment.php b/sfjp/wiki/processor/comment.php
new file mode 100644 (file)
index 0000000..7ac4322
--- /dev/null
@@ -0,0 +1,8 @@
+<?php
+namespace sfjp\Wiki\Processor;
+class Comment extends Base {
+       public function process($text) {
+               $this->formatted_text = '';
+               return $this;
+       }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/processor/denied.php b/sfjp/wiki/processor/denied.php
new file mode 100644 (file)
index 0000000..ef481bc
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace sfjp\Wiki\Processor;
+class Denied extends Base {
+       function __construct($args = null) {
+               parent::__construct();
+               $this->name = $args[0];
+       }
+
+       public function process($text) {
+               $this->setContext(array('whole_page_cachable' => false));
+               if ($this->getContext("suppress_plugin_error")) {
+                       $this->formatted_text = '';
+               } else {
+                       $this->formatted_text = '<span class="wiki-system-error">[Plugin Load Denied: ' . htmlspecialchars($this->name) . ']</span>';
+               }
+               return $this;
+       }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/processor/html.php b/sfjp/wiki/processor/html.php
new file mode 100644 (file)
index 0000000..50d543c
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+namespace sfjp\Wiki\Processor;
+if (!function_exists("kses"))
+  require_once("kses.php");
+
+class HTML extends Base {
+  static public $allowed = array(
+    'h1' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+    'h2' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+    'h3' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+    'h4' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+    'h5' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+    'h6' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+    'div' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+    'span' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+    'address' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'em' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'strong' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'dfn' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'code' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'samp' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'kbd' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'var' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'abbr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'acronym' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'blockquote' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'cite' => 1),
+    'q' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'cite' => 1),
+    'sub' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'sup' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'p' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+    'br' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'clear' => 1),
+    'pre' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'width' => 1),
+    'ins' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'cite' => 1, 'datetime' => 1),
+    'del' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'cite' => 1, 'datetime' => 1),
+    'ul' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'type' => 1),
+    'ol' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'type' => 1, 'start' => 1),
+    'li' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'type' => 1, 'value' => 1),
+    'dl' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'dt' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'dd' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'table' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'width' => 1, 'summary' => 1, 'border' => 1, 'cellspacing' => 1, 'cellpadding' => 1, 'align' => 1, 'valign' => 1, 'bgcolor' => 1),
+    'tbody' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1),
+    'thead' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1),
+    'tfoot' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1),
+    'colgroup' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'width' => 1, 'span' => 1, 'align' => 1, 'valign' => 1),
+    'col' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'width' => 1, 'span' => 1, 'align' => 1, 'valign' => 1),
+    'tr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1, 'bgcolor' => 1),
+    'th' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1, 'abbr' => 1, 'axis' => 1, 'headers' => 1, 'scope' => 1, 'rowspan' => 1, 'colspan' => 1, 'bgcolor' => 1),
+    'td' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1, 'abbr' => 1, 'axis' => 1, 'headers' => 1, 'scope' => 1, 'rowspan' => 1, 'colspan' => 1, 'bgcolor' => 1),
+    'a' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'href' => 1, 'name' => 1, 'rel' => 1, 'rev' => 1, 'accesskey' => 1, 'tabindex' => 1, 'title' => 1),
+    'img' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'src' => 1, 'alt' => 1, 'name' => 1, 'height' => 1, 'width' => 1, 'border' => 1, 'title' => 1, 'align' => 1),
+    'tt' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'i' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'b' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'big' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'small' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    's' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'u' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'font' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'size' => 1, 'color' => 1, 'face' => 1),
+    'hr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'noshade' => 1, 'align' => 1, 'size' => 1, 'width' => 1),
+    'wbr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+    'nobr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+  );
+
+  public function __construct($args = null) {
+    parent::__construct();
+  }
+
+  public function process($text) {
+    $this->formatted_text = $this->getFormatter()->raw_node(kses($text, self::$allowed, array('http', 'https', 'ftp')));
+    return $this;
+  }
+}
diff --git a/sfjp/wiki/processor/pre.php b/sfjp/wiki/processor/pre.php
new file mode 100644 (file)
index 0000000..4c965cb
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+namespace sfjp\Wiki\Processor;
+class Pre extends Base {
+       public function process($text) {
+               $fmt = $this->getFormatter();
+               $this->formatted_text = $fmt->raw_node("<pre>" . htmlspecialchars($text) . "</pre>");
+               if (!$this->hasContext("parent_processor"))
+                       $this->formatted_text .= $fmt->cleanup();
+               return $this;
+       }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/processor/trac.php b/sfjp/wiki/processor/trac.php
new file mode 100644 (file)
index 0000000..bdfb5e4
--- /dev/null
@@ -0,0 +1,867 @@
+<?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");
+       }
+}
diff --git a/sfjp/wiki/processor/trac_oneline.php b/sfjp/wiki/processor/trac_oneline.php
new file mode 100644 (file)
index 0000000..aa7984b
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+namespace sfjp\Wiki\Processor;
+class Trac_oneline extends Trac {
+       public $enable_plugin = false;
+
+       public function process($text = null) {
+               $ret ='';
+               $ret .= $this->parse_inline($text);
+               $ret .= $this->clear_inlinestyle();
+               $ret .= $this->getFormatter()->cleanup();
+               $this->formatted_text = $ret;
+               return $this;
+       }
+
+       public function process_plugin($str) {
+               if ($this->enable_plugin) {
+                       return parent::process_plugin($str);
+               } else {
+                       return '';
+               }
+       }
+}
diff --git a/sfjp/wiki/storage/base.php b/sfjp/wiki/storage/base.php
new file mode 100644 (file)
index 0000000..c0c4ff9
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+namespace sfjp\Wiki\Storage;
+abstract class Base {
+       protected $context;
+       public function __construct($context = array()) {
+               $this->context = $context;
+       }
+       abstract public function get($page);
+       abstract public function set($page, $text);
+       abstract public function exists($page);
+       abstract public function remove($page);
+       abstract public function get_list();
+}
\ No newline at end of file
diff --git a/sfjp/wiki/storage/dummy.php b/sfjp/wiki/storage/dummy.php
new file mode 100644 (file)
index 0000000..0a9944b
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+namespace sfjp\Wiki\Storage;
+class Dummy extends Base {
+       public function get($page) {
+               return false;
+       }
+       public function set($page, $text) {
+               return;
+       }
+       public function exists($page) {
+               return true;
+       }
+       public function remove($page) { return; }
+       public function get_list() { return array(); }
+}
\ No newline at end of file
diff --git a/sfjp/wiki/storage/file.php b/sfjp/wiki/storage/file.php
new file mode 100644 (file)
index 0000000..a3d5691
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+namespace sfjp\Wiki\Storage;
+class File extends Base {
+       protected $datadir = './wikitexts';
+
+       function __construct($context = array()) {
+               parent::__construct($context);
+               if (!empty($context['storage.file.datadir']))
+                       $this->datadir = $context['storage.file.datadir'];
+       }
+
+       public function get($page) {
+               return file_get_contents("{$this->datadir}/{$page}");
+       }
+
+       public function set($page, $text) {
+               if (!is_dir($this->datadir))
+                       mkdir($this->datadir, 0755, true);
+               $file = "{$this->datadir}/{$page}";
+               $tmp  = "{$this->datadir}/.{$page}.tmp.".getmypid();
+               file_put_contents($tmp, $text);
+               rename($tmp, $file);
+       }
+
+       public function exists($page) {
+               file_exists("{$this->datadir}/{$page}");
+       }
+
+       public function remove($page) {
+               unlink("{$this->datadir}/{$page}");
+       }
+
+       public function get_list() {
+               $dh = opendir($this->datadir);
+               if (!$dh) return array();
+       
+               $ret = array();
+               while ($ent = readdir($dh)) {
+                       if (substr($ent, 0, 1) == '.')
+                               continue;
+                       if (substr($ent, -1) == '~')
+                               continue;
+                       $ret []= $ent;
+               }
+               closedir($dh);
+               sort($ret, SORT_NATURAL|SORT_FLAG_CASE);
+               return $ret;
+       }
+}
\ No newline at end of file
diff --git a/sfjpWikiParser.php b/sfjpWikiParser.php
new file mode 100644 (file)
index 0000000..dbe6369
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+if (!spl_autoload_functions() || !in_array('spl_autoload', spl_autoload_functions())) {
+       spl_autoload_extensions('.php,.class');
+       spl_autoload_register('spl_autoload');
+       
+}
+
+class_alias('sfjp\Wiki\Exception\Base', 'sfjpWikiParseException');
+class_alias('sfjp\Wiki\Exception\Invalid_Argument', 'sfjpWikiProcessor_InvalidArgument');
+class_alias('sfjp\Wiki\Exception\Internal_Error', 'sfjpWikiProcessor_InternalError');
+
+class_alias('sfjp\Wiki\Parser', 'sfjpWikiParser');
+
+class_alias('sfjp\Wiki\Processor\Base', 'sfjpWikiProcessor_base');
+class_alias('sfjp\Wiki\Processor\Trac', 'sfjpWikiProcessor_trac');
+class_alias('sfjp\Wiki\Processor\Trac_Oneline', 'sfjpWikiProcessor_trac_oneline');
+class_alias('sfjp\Wiki\Processor\Auto', 'sfjpWikiProcessor_auto');
+class_alias('sfjp\Wiki\Processor\Pre', 'sfjpWikiProcessor_pre');
+class_alias('sfjp\Wiki\Processor\Code', 'sfjpWikiProcessor_code');
+class_alias('sfjp\Wiki\Processor\Comment', 'sfjpWikiProcessor_comment');
+class_alias('sfjp\Wiki\Processor\Denied', 'sfjpWikiProcessor_denied');
diff --git a/test/FormatTest.php b/test/FormatTest.php
new file mode 100644 (file)
index 0000000..efdc72e
--- /dev/null
@@ -0,0 +1,572 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+
+class FormatTest extends PHPUnit_Framework_TestCase {
+    protected function setUp() {
+        $context = array(
+            'sfjp.group_name' => 'unittest',
+            'site_root_url' => 'http://sourceforge.jp',
+            'internal_url_regex' => '^http://sourceforge\.jp/',
+            'svn_base_url' => 'http://svn/view',
+        );
+        $this->p = new \sfjp\Wiki\Parser($context);
+    }
+
+    protected function tearDown() {
+
+    }
+
+    public function testNew() {
+        self::assertTrue(isset($this->p));
+    }
+
+    public function testParseSimpleRun() {
+        $this->try_parse('', '');
+        $this->try_parse('abc', '<p>abc</p>');
+        $this->try_parse("abc abc\nabc", "<p>abc abc\nabc</p>");
+    }
+
+    public function testParseHeadings() {
+        $this->try_parse('= abc =', '<h1 id="h1-abc">abc</h1>');
+        $this->try_parse("= 日本語 =\r\n", '<h1 id="h1-.E6.97.A5.E6.9C.AC.E8.AA.9E">日本語</h1>');
+        $this->try_parse('= abc', '<h1 id="h1-abc">abc</h1>');
+        $this->try_parse('= abc = ', '<h1 id="h1-abc">abc</h1>');
+        $this->try_parse('= abc ', '<h1 id="h1-abc">abc</h1>');
+        $this->try_parse('= abc = #moge', '<h1 id="moge">abc</h1>');
+        $this->try_parse('= abc #moge', '<h1 id="moge">abc</h1>');
+        $this->try_parse('=abc', '<p>=abc</p>');
+        $this->try_parse(' = abc', '<div class="indent">= abc</div>');
+    }
+
+    public function testParseListMark() {
+        $text = '
+  * list1
+    * level2
+   * level2, too.
+     continue list item.
+   continue!
+    continue also!
+    * moge
+ * level1 item
+  * level1
+  *this not list
+';
+
+        $expect = '<ul><li>list1
+<ul><li>level2
+</li><li>level2, too.
+continue list item.
+continue!
+continue also!
+</li><li>moge
+</li></ul></li><li>level1 item
+</li><li>level1
+*this not list
+</li></ul>';
+        self::assertEquals($expect, $this->p->parse($text));
+    }
+
+    public function testParseMixedList() {
+        $text = '
+  * ul
+  1. ol num
+  a. ol alpha
+  A. ol ALPHA
+  * ul
+  i. ol roma
+  * ul
+  I. ol ROMA
+';
+
+        $expect = '<ul><li>ul
+</li></ul><ol><li>ol num
+</li></ol><ol style="list-style-type: lower-alpha;"><li>ol alpha
+</li></ol><ol style="list-style-type: upper-alpha;"><li>ol ALPHA
+</li></ol><ul><li>ul
+</li></ul><ol style="list-style-type: lower-roman;"><li>ol roma
+</li></ol><ul><li>ul
+</li></ul><ol style="list-style-type: upper-roman;"><li>ol ROMA
+</li></ol>';
+        self::assertEquals($expect, $this->p->parse($text));
+
+        $text = '
+  * list1
+    1. level2
+    100. ordered
+    99. moge
+  * level1 item
+    * level2 unordered
+      1. abc
+      1. def
+    * foo
+  1. level1 ordered
+    * unordered in ordered
+  2. ordered list
+';
+
+        $expect = '<ul><li>list1
+<ol><li>level2
+</li><li>ordered
+</li><li>moge
+</li></ol></li><li>level1 item
+<ul><li>level2 unordered
+<ol><li>abc
+</li><li>def
+</li></ol></li><li>foo
+</li></ul></li></ul><ol><li>level1 ordered
+<ul><li>unordered in ordered
+</li></ul></li><li>ordered list
+</li></ol>';
+        self::assertEquals($expect, $this->p->parse($text));
+    }
+
+  /*
+    public function testParseParagraph() {
+        self::assertEquals('yet', $this->p->parse('=abc'));
+    }
+  */
+
+    public function testParseListNum() {
+        $text = '
+  1. list1
+    3. level2
+   999999. level2, too.
+    1234. moge
+ 3. level1 item
+  0000. level1
+  1.3. this not list
+';
+
+        $expect = '<ol><li>list1
+<ol><li>level2
+</li><li>level2, too.
+</li><li>moge
+</li></ol></li><li>level1 item
+</li><li>level1
+1.3. this not list
+</li></ol>';
+        self::assertEquals($expect, $this->p->parse($text));
+    }
+
+  /*
+    public function testParseDefinitionList() {
+        self::assertEquals('yet', $this->p->parse('=abc'));
+    }
+  */
+
+    public function testParseHTMLEscape() {
+        $this->try_parse("'''>moge'''<", '<p><strong>&gt;moge</strong>&lt;</p>');
+    }
+
+    public function testParseInlineBold() {
+        $this->try_parse("'''moge'''", '<p><strong>moge</strong></p>');
+    }
+
+    public function testParseInlineItalic() {
+        $this->try_parse("''abc''", '<p><em>abc</em></p>');
+        $this->try_parse("''ab", '<p><em>ab</em></p>');
+        $this->try_parse("ab ''bc\n''cd", "<p>ab <em>bc\n</em>cd</p>");
+    }
+
+    public function testParseInlineBolditalic() {
+        $this->try_parse("'''''moge'''''",
+            '<p><strong><em>moge</em></strong></p>');
+        $this->try_parse("'''''''''abc'",
+            "<p><strong><em><strong>'abc'</strong></em></strong></p>");
+    }
+
+    public function testParseInlineUndeline() {
+        $this->try_parse("__abc__",
+            '<p><span style="text-decoration: underline;">abc</span></p>');
+    }
+
+    public function testParseInlineStrike() {
+        $this->try_parse('~~abc~~', '<p><del>abc</del></p>');
+        $this->try_parse('~~~abcef~~~~~', '<p><del>~abcef</del><del>~</del></p>');
+    }
+
+    public function testParseInlineSuperscript() {
+        $this->try_parse('^ abc^', '<p><sup> abc</sup></p>');
+        $this->try_parse('^^^ abc ^^', '<p><sup></sup><sup> abc </sup><sup></sup></p>');
+        $this->try_parse("head ^ abc\ndef^tail", "<p>head <sup> abc\ndef</sup>tail</p>");
+    }
+
+    public function testParseInlineSubscript() {
+        $this->try_parse(',,abc ,,', '<p><sub>abc </sub></p>');
+    }
+
+    public function testParseLinks() {
+        $this->try_parse('http://url.com/path', '<p><a href="http://url.com/path" class="external" rel="nofollow">http://url.com/path</a></p>');
+        $this->try_parse('https://url.com/path', '<p><a href="https://url.com/path" class="external" rel="nofollow">https://url.com/path</a></p>');
+    }
+
+    public function testParseBlacketLink() {
+        self::assertEquals(
+            '<p><a href="./a%2Fb%2F%E6%97%A5%E6%9C%AC%E8%AA%9E">a/b/日本語</a></p>',
+            $this->p->parse("[a/b/日本語]")
+        );
+    }
+
+    public function testParseLinkFragment() {
+        $this->try_parse('[test1#frag]', '<p><a href="./test1#frag">test1#frag</a></p>');
+        $this->try_parse('[test2#frag text]', '<p><a href="./test2#frag">text</a></p>');
+        $this->try_parse('wiki:test3#frag', '<p><a href="./test3#frag">wiki:test3#frag</a></p>');
+
+        $this->try_parse('wiki:"test4#frag"', '<p><a href="./test4#frag">wiki:&quot;test4#frag&quot;</a></p>');
+        $this->try_parse('["test5#frag" text]', '<p><a href="./test5#frag">text</a></p>');
+        $this->try_parse('[wiki:"test6#frag" text]', '<p><a href="./test6#frag">text</a></p>');
+        $this->try_parse('[wiki:"test7 page name#frag" text]', '<p><a href="./test7%20page%20name#frag">text</a></p>');
+        $this->try_parse('[#frag]', '<p><a href="#frag">#frag</a></p>');
+        $this->try_parse('["#frag" text]', '<p><a href="#frag">text</a></p>');
+
+    }
+
+    public function testParseQuote() {
+        self::assertEquals(
+            "<blockquote class=\"citation\"><blockquote class=\"citation\"><p>abc
+</p></blockquote><p>def
+ghi
+</p><blockquote class=\"citation\"><blockquote class=\"citation\"><p>jkl
+</p></blockquote></blockquote></blockquote><p>normal</p>",
+            $this->p->parse(">> abc\n> def\n> ghi\n>>> jkl\nnormal")
+        );
+$this->try_parse("> abc\n> > with space\n> >   >  3rd", '<blockquote class="citation"><p>abc
+</p><blockquote class="citation"><p>with space
+</p><blockquote class="citation"><p> 3rd</p></blockquote></blockquote></blockquote>');
+    }
+
+    public function testParseIndent() {
+      $this->try_parse(" abc", '<div class="indent">abc</div>');
+      $this->try_parse(" abc\n  def\nghi",
+                         '<div class="indent">abc
+def
+</div><p>ghi</p>');
+      self::assertEquals('<div class="indent">abc
+def
+</div><div class="indent"><div class="indent">2nd nest
+</div></div><div class="indent"><div class="indent"><div class="indent">3rd
+</div></div></div><div class="indent"><div class="indent">2nd
+</div></div><ul><li>clear by list</li></ul>',
+                         $this->p->parse(" abc\n  def\n    2nd nest\n      3rd\n    2nd\n * clear by list"));
+    }
+
+    public function testParseInternalURIPrweb() {
+        $this->try_parse("prweb:/", '<p><a href="http://unittest.sourceforge.jp/" class="project-web">prweb:/</a></p>');
+        $this->try_parse("prweb:/path/to/file", '<p><a href="http://unittest.sourceforge.jp/path/to/file" class="project-web">prweb:/path/to/file</a></p>');
+        $this->try_parse("prweb:project-name:/url-to/the.page", '<p><a href="http://project-name.sourceforge.jp/url-to/the.page" class="project-web">prweb:project-name:/url-to/the.page</a></p>');
+    }
+
+    public function testParseInternalURIUser() {
+        $this->try_parse("user:sugi", '<p><a href="/users/sugi" class="user">user:sugi</a></p>');
+        $this->try_parse("id:sugi", '<p><a href="/users/sugi" class="user">id:sugi</a></p>');
+        $this->try_parse("users:sugi", '<p><a href="/users/sugi" class="user">users:sugi</a></p>');
+    }
+
+    public function testParseURIMailTo() {
+        $this->try_parse("mailto:sugi@osdn.jp", '<p><a href="mailto:sugi@osdn.jp" class="mail">mailto:sugi@osdn.jp</a></p>');
+        $this->try_parse("[mailto:a.b=c+d@e.f メール]", '<p><a href="mailto:a.b=c+d@e.f" class="mail">メール</a></p>');
+        $this->try_parse("mailto:bad@てすと", '<p>mailto:bad@てすと</p>');
+    }
+
+    public function testParseEscape() {
+        $this->try_parse("!`", '<p>`</p>');
+        $this->try_parse("!^てすと!^", '<p>^てすと^</p>');
+        $this->try_parse("!~~", '<p>~~</p>');
+        $this->try_parse("!__", '<p>__</p>');
+        $this->try_parse("!WikiName", '<p>WikiName</p>');
+        $this->try_parse("![[Plugin]]", '<p>[[Plugin]]</p>');
+        $this->try_parse("!!", '<p>!!</p>');
+    }
+
+    public function testParseEscapeBlock() {
+        $this->try_parse("!> Equote", '<p>&gt; Equote</p>');
+        $this->try_parse("!------", '<p>------</p>');
+        $this->try_parse("!||escaped||table||", '<p>||escaped||table||</p>');
+        $this->try_parse("!= not header =", '<p>= not header =</p>');
+        $this->try_parse(" !* abc", '<div class="indent">* abc</div>');
+    }
+
+    public function testHttpUrl() {
+        self::assertTrue(!!$this->p->processor->gen_uri_link("http://てすと"));
+        self::assertTrue(!!$this->p->processor->gen_uri_link("http://abc"));
+        self::assertTrue(!$this->p->processor->gen_uri_link("http://"));
+        self::assertTrue(!$this->p->processor->gen_uri_link("http:// abc"));
+    }
+
+    public function testIsbnLink() {
+        $this->try_parse("isbn:123",
+            '<p><a href="http://www.amazon.co.jp/gp/product/123" class="isbnbook" rel="nofollow">isbn:123</a></p>');
+        $this->p->setContext(array('amazon_affiliate_id' => 'afid-test'));
+        $this->try_parse("isbn:123",
+            '<p><a href="http://www.amazon.co.jp/gp/product/123?tag=afid-test&amp;linkCode=as2&amp;camp=247&amp;creative=1211&amp;creativeASIN=123" class="isbnbook" rel="nofollow">isbn:123</a></p>');
+    }
+
+    public function testParseGroupWikiPage() {
+        $this->try_parse('wiki:groupname:PageName', '<p><a href="http://sourceforge.jp/projects/groupname/wiki/PageName" class="external-wiki">wiki:groupname:PageName</a></p>');
+        $this->try_parse('wiki:group-name:PageName', '<p><a href="http://sourceforge.jp/projects/group-name/wiki/PageName" class="external-wiki">wiki:group-name:PageName</a></p>');
+        $this->try_parse('wiki:group_name:PageName', '<p><a href="./group_name">wiki:group_name</a>:<a href="./PageName">PageName</a></p>');
+
+    }
+
+    public function testParseOrer() {
+        $this->try_parse('[PageName [[BR]]]',
+            '<p><a href="./PageName">[[BR</a>]]</p>');
+      /* do not support currently
+      $this->try_parse("abc'''def[PageName la''bee''eel]tex'''t",
+                       '<p>abc<b>def<a href="./PageName">la<i>bee</i>eel</a>tex</b>t</p>');
+       */
+        $this->try_parse("abc'''def[PageName la''bee''eel]tex'''t",
+            "<p>abc<strong>def<a href=\"./PageName\">la''bee''eel</a>tex</strong>t</p>");
+
+        $this->try_parse('[__Page,,Name^^ label]', '<p><a href="./__Page%2C%2CName%5E%5E">label</a></p>');
+        $this->try_parse('["__Page,,Name^^" label]', '<p><a href="./__Page%2C%2CName%5E%5E">label</a></p>');
+        $this->try_parse('[wiki:"__Page,,Name^^" label]', '<p><a href="./__Page%2C%2CName%5E%5E">label</a></p>');
+        $this->try_parse('http://aaaa[bbb]ccc', '<p><a href="http://aaaa" class="external" rel="nofollow">http://aaaa</a><a href="./bbb">bbb</a>ccc</p>');
+        $this->try_parse('!http://aaaa[bbb]ccc', '<p>http://aaaa<a href="./bbb">bbb</a>ccc</p>');
+        $this->try_parse('http://aaaa![bbb]ccc', '<p><a href="http://aaaa" class="external" rel="nofollow">http://aaaa</a>[bbb]ccc</p>');
+        $this->try_parse('!http://aaaa![bbb]ccc', '<p>http://aaaa[bbb]ccc</p>');
+    }
+
+    public function testQuotedLink() {
+        $this->try_parse('["test page__ name"]', '<p><a href="./test%20page__%20name">test page__ name</a></p>');
+        $this->try_parse('["a b c" label]', '<p><a href="./a%20b%20c">label</a></p>');
+        $this->try_parse('[wiki:"a b c" label]', '<p><a href="./a%20b%20c">label</a></p>');
+        $this->try_parse('wiki:",,a b c__"', '<p><a href="./%2C%2Ca%20b%20c__">wiki:&quot;,,a b c__&quot;</a></p>');
+    }
+
+    public function testBracket() {
+        $this->try_parse('[http://www label]', '<p><a href="http://www" class="external" rel="nofollow">label</a></p>');
+        $this->try_parse('[WikiName label]', '<p><a href="./WikiName">label</a></p>');
+        $this->try_parse('[normal text]', '<p><a href="./normal">text</a></p>');
+    }
+
+    public function testParseFalseLink() {
+        $this->try_parse("[0]", '<p><a href="./0">0</a></p>');
+        $this->try_parse('["0"]', '<p><a href="./0">0</a></p>');
+        $this->try_parse("[0 text]", '<p><a href="./0">text</a></p>');
+        $this->try_parse("[0#0]", '<p><a href="./0#badid-0">0#0</a></p>');
+        $this->try_parse('["0#0" text]', '<p><a href="./0#badid-0">text</a></p>');
+        $this->try_parse("wiki:0", '<p><a href="./0">wiki:0</a></p>');
+        $this->try_parse("wiki:0#0", '<p><a href="./0#badid-0">wiki:0#0</a></p>');
+        $this->try_parse("[#0]", '<p><a href="#badid-0">#0</a></p>');
+        $this->try_parse("[#0 text]", '<p><a href="#badid-0">text</a></p>');
+    }
+
+    public function testTableAndHr() {
+        $this->try_parse('
+----
+||table||
+----
+||table||
+----
+',
+                         '<hr /><table class="wikitable"><tbody><tr><td>table</td></tr></tbody></table><hr /><table class="wikitable"><tbody><tr><td>table</td></tr></tbody></table><hr />');
+    }
+
+
+    public function testBlock() {
+        $this->try_parse("{{{
+moge
+}}}
+", '<pre>moge
+</pre>');
+        $this->try_parse("{{{
+moge
+", '<pre>moge
+</pre>');
+    }
+
+    public function testInlinePreformatted() {
+      $this->try_parse("''ab''c''d{{{ef  g''}} }}}h''}}}i''j.",
+                       "<p><em>ab</em>c<em>d<tt>ef  g''}} </tt>h</em>}}}i<em>j.</em></p>");
+    }
+
+    public function testInternalLink() {
+      $this->try_parse("[http://www.yahoo.co.jp/ external]",
+                       '<p><a href="http://www.yahoo.co.jp/" class="external" rel="nofollow">external</a></p>');
+      $this->try_parse("[http://sourceforge.jp/ internal]",
+                       '<p><a href="http://sourceforge.jp/">internal</a></p>');
+      $this->try_parse("[http://sourceforge.jp/projects/test/moge internal]",
+                       '<p><a href="http://sourceforge.jp/projects/test/moge">internal</a></p>');
+      $this->try_parse("[http://test.sourceforge.jp/ external]",
+                       '<p><a href="http://test.sourceforge.jp/" class="external" rel="nofollow">external</a></p>');
+    }
+
+    public function testHeadingCounter() {
+      $this->try_parse("
+== a
+== b
+== a
+== a
+",
+                       '<h2 id="h2-a">a</h2><h2 id="h2-b">b</h2><h2 id="h2-a-2">a</h2><h2 id="h2-a-3">a</h2>');
+    }
+
+    public function testUselessParagraph() {
+      $po_out = '<div class="pageoutline"><div class="pageoutline-title"><div class="action"><button type="button" onClick="javascript:togglePageOutline(this)"><img src="//static.sourceforge.jp/wiki/images/icons/roll-up.gif" border="0"></button></div>Outline</div></div>';
+      $this->try_parse("[[PageOutline]]
+
+a
+
+[[PageOutline]]
+
+b
+",
+                       "$po_out
+<p>a
+</p>$po_out
+<p>b
+</p>");
+
+      $this->try_parse("
+
+{{{
+pre
+}}}
+
+
+",
+                       '<pre>pre
+</pre>');
+
+      $this->try_parse("a
+
+{{{ html
+<i>html block</i>
+}}}
+
+b
+",
+                       '<p>a
+</p><i>html block</i>
+<p>b
+</p>');
+
+    }
+
+    public function testTableAndHTMLBlockBug() {
+        $this->try_parse('||table||
+{{{ html
+<ins>HTML</ins>
+}}}', '<table class="wikitable"><tbody><tr><td>table</td></tr></tbody></table><ins>HTML</ins>
+');
+    }
+
+    public function testSlashLinks() {
+        $this->try_parse('[/path/to/]', '<p><a href="/path/to/">/path/to/</a></p>');
+        $this->try_parse('[/path/to/ moge]', '<p><a href="/path/to/">moge</a></p>');
+        $this->try_parse('[//server.com/path moge]', '<p><a href="//server.com/path">moge</a></p>');
+    }
+
+    public function testTicketComment() {
+        $this->try_parse('comment:4:15142:1235097254',
+            '<p><a href="http://sourceforge.jp/ticket/browse.php?group_id=4&amp;tid=15142#comment:4:15142:1235097254" class="ticket">comment:4:15142:1235097254</a></p>');
+        $this->try_parse('comment:1 comment:foo', '<p>comment:1 comment:foo</p>');
+        $this->try_parse('[comment:4:15142:1235097254 hiromichi-m] への返信',
+            '<p><a href="http://sourceforge.jp/ticket/browse.php?group_id=4&amp;tid=15142#comment:4:15142:1235097254" class="ticket">hiromichi-m</a> への返信</p>');
+       $this->try_parse('[comment::123:456 グループID省略]なコメント', '<p><a href="http://sourceforge.jp/ticket/detail.php?id=123&amp;cid=456" class="ticket">グループID省略</a>なコメント</p>');
+       $this->try_parse('[comment::123:456 グループID0]なコメントリンク', '<p><a href="http://sourceforge.jp/ticket/detail.php?id=123&amp;cid=456" class="ticket">グループID0</a>なコメントリンク</p>');
+       $this->try_parse('[comment:123:456 2引数]なコメントリンク', '<p><a href="http://sourceforge.jp/ticket/detail.php?id=123&amp;cid=456" class="ticket">2引数</a>なコメントリンク</p>');
+    }
+
+    public function testIRI() {
+        $iri  = 'http://假定された有機交流電燈の.ひとつの青い照明です/';
+        $url = 'http://%E5%81%87%E5%AE%9A%E3%81%95%E3%82%8C%E3%81%9F%E6%9C%89%E6%A9%9F%E4%BA%A4%E6%B5%81%E9%9B%BB%E7%87%88%E3%81%AE.%E3%81%B2%E3%81%A8%E3%81%A4%E3%81%AE%E9%9D%92%E3%81%84%E7%85%A7%E6%98%8E%E3%81%A7%E3%81%99/';
+        $this->try_parse("$iri ひかりはたもち、その電燈は失はれ", "<p><a href=\"{$url}\" class=\"external\" rel=\"nofollow\">$iri</a> ひかりはたもち、その電燈は失はれ</p>");
+        $this->try_parse("[$iri]", "<p><a href=\"$url\" class=\"external\" rel=\"nofollow\">$iri</a></p>");
+        $this->try_parse("[$iri ひかりはたもち、その電燈は失はれ]", "<p><a href=\"$url\" class=\"external\" rel=\"nofollow\">ひかりはたもち、その電燈は失はれ</a></p>");
+        $this->try_parse('[/納豆.html ひきわり]', '<p><a href="/%E7%B4%8D%E8%B1%86.html">ひきわり</a></p>');
+    }
+
+    public function testSVNRev() {
+        $a_attr = 'href="http://svn/view?view=rev&amp;root=unittest&amp;revision=123" class="svn"';
+        $this->try_parse('r123', "<p><a $a_attr>r123</a></p>");
+        $this->try_parse('abcr123', '<p>abcr123</p>');
+        $this->try_parse('r123abc', '<p>r123abc</p>');
+        $this->try_parse('lead r123 trail', "<p>lead <a $a_attr>r123</a> trail</p>");
+        $this->try_parse('日本語r123テキスト', "<p>日本語<a $a_attr>r123</a>テキスト</p>");
+    }
+
+
+    public function testWithUTF8() {
+           ini_set('default_charset', 'utf-8');
+           ini_set('mbstring.internal_encoding', 'utf-8');
+           ini_set('mbstring.detect_order', 'EUC-JP,UTF-8,SJIS,JIS');
+           ini_set('mbstring.language', 'Japanese');
+           $input = '!SourceForge.JPシステムのバグを見つけた場合には [/projects/sourceforge/ticket?type%5B%5D=113&status%5B%5D=1 バグ]、
+CVSリポジトリの調整やアカウント削除等のサポート要求は [/projects/sourceforge/ticket?type%5B%5D=114&status%5B%5D=1 サポートリクエスト]へ、システムへの追加機能の要望等は [/projects/sourceforge/ticket?type%5B%5D=115&status%5B%5D=1 機能リクエスト]へ登録いただくようお願いいたします。その他の問い合わせについては、[/docs/SourceForge.JP%E3%81%AE%E9%80%A3%E7%B5%A1%E5%85%88 連絡先についての文書]をよくお読みください。';
+           $exp = '<p>SourceForge.JPシステムのバグを見つけた場合には <a href="/projects/sourceforge/ticket?type%5B%5D=113&amp;status%5B%5D=1">バグ</a>、
+CVSリポジトリの調整やアカウント削除等のサポート要求は <a href="/projects/sourceforge/ticket?type%5B%5D=114&amp;status%5B%5D=1">サポートリクエスト</a>へ、システムへの追加機能の要望等は <a href="/projects/sourceforge/ticket?type%5B%5D=115&amp;status%5B%5D=1">機能リクエスト</a>へ登録いただくようお願いいたします。その他の問い合わせについては、<a href="/docs/SourceForge.JP%E3%81%AE%E9%80%A3%E7%B5%A1%E5%85%88">連絡先についての文書</a>をよくお読みください。</p>';
+           $this->try_parse($input, $exp);
+    }
+
+    public function testTracKeepNewLineMode() {
+           $this->p->setContext(array('trac.keep_newline' => true));
+           $this->try_parse('
+改行を
+全部
+br に
+マップする
+モード
+
+
+
+
+パラグラフは適切に分割される必要がある
+
+> block
+> quote
+> text
+さて
+', '<p>改行を
+<br />全部
+<br />br に
+<br />マップする
+<br />モード
+<br /></p><p>パラグラフは適切に分割される必要がある
+<br /></p><blockquote class="citation"><p>block
+<br />quote
+<br />text
+<br /></p></blockquote><p>さて
+<br /></p>');
+           $this->try_parse('
+  * リストでも改行保持
+    のテスト
+  * です
+', '<ul><li>リストでも改行保持
+<br />のテスト
+<br /></li><li>です
+<br /></li></ul>');
+    }
+
+    public function testInlinePluginAtStartOfLine() {
+           $this->try_parse('[[br]]test!', '<p><br />test!</p>');
+    }
+
+    public function testHashTrackerLink() {
+           $this->try_parse('#10', '<p><a href="http://sourceforge.jp/tracker.php?id=10" class="tracker">#10</a></p>');
+           $this->try_parse('#50010', '<p><a href="http://sourceforge.jp/ticket/detail.php?id=50010" class="tracker">#50010</a></p>');
+    }
+
+
+    public function testDisabledLinkWithExclamation() {
+        $context = array(
+            'sfjp.group_name' => 'unittest',
+            'site_root_url' => 'http://sourceforge.jp',
+            'internal_url_regex' => '^http://sourceforge\.jp/',
+            'svn_base_url' => 'http://svn/view',
+            'disable.link.CamelCase' => true,
+        );
+        $p = new \sfjp\Wiki\Parser($context);
+        self::assertEquals('<p>CamelCase</p>', $p->parse('!CamelCase'));
+    }
+
+
+    protected function try_parse($text, $expect) {
+        self::assertEquals($expect, $this->p->parse($text));
+    }
+
+}
+
+// vim: set sts=4 sw=4 expandtab:
diff --git a/test/ParserTest.php b/test/ParserTest.php
new file mode 100644 (file)
index 0000000..fde38ed
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+
+class ParserTest extends PHPUnit_Framework_TestCase {
+    protected function setUp() {
+        $context = array(
+            'sfjp.group_name' => 'unittest',
+            'site_root_url' => 'http://sourceforge.jp',
+            'internal_url_regex' => '^http://sourceforge\.jp/',
+            'svn_base_url' => 'http://svn/view',
+        );
+        $this->p = new \sfjp\Wiki\Parser($context);
+    }
+
+    protected function tearDown() {
+
+    }
+
+    public function testContextKeyTransition() {
+           $v = 'hoge';
+           $this->p->setContext(array('plugin.order' => $v));
+           self::assertEquals($v, $this->p->getContext('plugin.order'));
+           self::assertEquals($v, $this->p->getContext('extension.acl_order'));
+
+           $v = 'fuga';
+           $this->p->setContext(array('extension.acl_order' => $v));
+           self::assertEquals($v, $this->p->getContext('plugin.order'));
+           self::assertEquals($v, $this->p->getContext('extension.acl_order'));
+    }
+}
+
+// vim: set sts=4 sw=4 expandtab:
diff --git a/test/StorageTest.php b/test/StorageTest.php
new file mode 100644 (file)
index 0000000..5b48f5d
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+
+class StorageTest extends PHPUnit_Framework_TestCase {
+       protected $storage_path;
+       protected $s;
+
+       protected function setUp(){
+               $this->storage_path = dirname(__FILE__) . "filestore-test";
+               $this->s = new sfjp\Wiki\Storage\File(array('storage.file.datadir' => $this->storage_path));
+       }
+
+       protected function tearDown() {
+               $this->cleanup();
+       }
+
+       protected function cleanup() {
+               if (!is_dir($this->storage_path))
+                       return;
+               $dh = opendir($this->storage_path);
+               if (!$dh) return;
+               while ($ent = readdir($dh)) {
+                       if ($ent == '.' || $ent == '..')
+                               continue;
+                       unlink("{$this->storage_path}/{$ent}");
+               }
+               closedir($dh);
+               rmdir($this->storage_path);
+       }
+
+       public function testNew() {
+               self::assertTrue(isset($this->s));
+       }
+
+
+       public function testGetSet() {
+               $text = "hoge\nfuga\r\nyeah!";
+               $this->s->set('page1', $text);
+               self::assertEquals($text, $this->s->get('page1'));
+       }
+
+       public function testList() {
+               $this->cleanup();
+               $this->s->set('foo', 'foo 1');
+               $this->s->set('Bar', 'bar 2');
+               self::assertTrue(in_array('foo', $this->s->get_list()));
+               self::assertTrue(in_array('Bar', $this->s->get_list()));
+               self::assertTrue(!in_array('boo', $this->s->get_list()));
+       }
+
+       public function testRemove() {
+               $this->s->set('hoge', '1');
+               self::assertTrue(in_array('hoge', $this->s->get_list()));
+               $this->s->remove('hoge');
+               self::assertTrue(!in_array('hoge', $this->s->get_list()));
+       }
+}
diff --git a/test/all.php b/test/all.php
new file mode 100755 (executable)
index 0000000..a93e083
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/phpunit
+<?php
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once dirname(__FILE__)."/lib/kses.php";
+
+spl_autoload_extensions('.php');
+spl_autoload_register('spl_autoload');
+chdir(dirname(__FILE__)."/..");
+
+class AllTests
+{
+       public static function suite()
+       {
+               $suite = new PHPUnit_Framework_TestSuite('sfjpWikiAll');
+               
+               $dir = dirname(__FILE__);
+               $dh = opendir($dir);
+               while (($file = readdir($dh)) !== false) {
+                       if (!preg_match('/^(.*Test)\.php$/', $file, $m))
+                               continue;
+                       require_once "{$dir}/{$file}";
+                       $suite->addTestSuite($m[1]);
+               }
+               closedir($dh);
+               return $suite;
+       }
+
+       protected function setUp()
+       {
+               spl_autoload_register();
+       }
+}
+
diff --git a/test/lib/kses.php b/test/lib/kses.php
new file mode 100644 (file)
index 0000000..9009f1a
--- /dev/null
@@ -0,0 +1,628 @@
+<?php
+
+# kses 0.2.2 - HTML/XHTML filter that only allows some elements and attributes
+# Copyright (C) 2002, 2003, 2005  Ulf Harnhammar
+#
+# This program is free software and open source software; you can redistribute
+# it and/or modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the License,
+# or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA  or visit
+# http://www.gnu.org/licenses/gpl.html
+#
+# *** CONTACT INFORMATION ***
+#
+# E-mail:      metaur at users dot sourceforge dot net
+# Web page:    http://sourceforge.net/projects/kses
+# Paper mail:  Ulf Harnhammar
+#              Ymergatan 17 C
+#              753 25  Uppsala
+#              SWEDEN
+#
+# [kses strips evil scripts!]
+#
+
+#
+# Changes:
+#  * 2008-01-18 Tatsuki Sugiura <sugi@osdn.jp>
+#    * add callback attribute filter
+#
+
+
+function kses($string, $allowed_html, $allowed_protocols =
+               array('http', 'https', 'ftp', 'news', 'nntp', 'telnet',
+                     'gopher', 'mailto'))
+###############################################################################
+# This function makes sure that only the allowed HTML element names, attribute
+# names and attribute values plus only sane HTML entities will occur in
+# $string. You have to remove any slashes from PHP's magic quotes before you
+# call this function.
+###############################################################################
+{
+  $string = kses_no_null($string);
+  $string = kses_js_entities($string);
+  $string = kses_normalize_entities($string);
+  $string = kses_hook($string);
+  $allowed_html_fixed = kses_array_lc($allowed_html);
+  return kses_split($string, $allowed_html_fixed, $allowed_protocols);
+} # function kses
+
+
+function kses_hook($string)
+###############################################################################
+# You add any kses hooks here.
+###############################################################################
+{
+  return $string;
+} # function kses_hook
+
+
+function kses_version()
+###############################################################################
+# This function returns kses' version number.
+###############################################################################
+{
+  return '0.2.2';
+} # function kses_version
+
+
+function kses_split($string, $allowed_html, $allowed_protocols)
+###############################################################################
+# This function searches for HTML tags, no matter how malformed. It also
+# matches stray ">" characters.
+###############################################################################
+{
+  return preg_replace_callback('%(<'.   # EITHER: <
+                      '[^>]*'. # things that aren't >
+                      '(>|$)'. # > or end of string
+                      '|>)%', # OR: just a >
+                      function ($m) use ($allowed_html, $allowed_protocols) { return kses_split2($m[1], $allowed_html, $allowed_protocols); },
+                      $string);
+} # function kses_split
+
+
+function kses_split2($string, $allowed_html, $allowed_protocols)
+###############################################################################
+# This function does a lot of work. It rejects some very malformed things
+# like <:::>. It returns an empty string, if the element isn't allowed (look
+# ma, no strip_tags()!). Otherwise it splits the tag into an element and an
+# attribute list.
+###############################################################################
+{
+  $string = kses_stripslashes($string);
+
+  if (substr($string, 0, 1) != '<')
+    return '&gt;';
+    # It matched a ">" character
+
+  if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?$%', $string, $matches))
+    return '';
+    # It's seriously malformed
+
+  $slash = trim($matches[1]);
+  $elem = $matches[2];
+  $attrlist = $matches[3];
+
+  if (!@isset($allowed_html[strtolower($elem)]))
+    return '';
+    # They are using a not allowed HTML element
+
+  if ($slash != '')
+    return "<$slash$elem>";
+  # No attributes are allowed for closing elements
+
+  return kses_attr("$slash$elem", $attrlist, $allowed_html,
+                   $allowed_protocols);
+} # function kses_split2
+
+
+function kses_attr($element, $attr, $allowed_html, $allowed_protocols)
+###############################################################################
+# This function removes all attributes, if none are allowed for this element.
+# If some are allowed it calls kses_hair() to split them further, and then it
+# builds up new HTML code from the data that kses_hair() returns. It also
+# removes "<" and ">" characters, if there are any left. One more thing it
+# does is to check if the tag has a closing XHTML slash, and if it does,
+# it puts one in the returned code as well.
+###############################################################################
+{
+# Is there a closing XHTML slash at the end of the attributes?
+
+  $xhtml_slash = '';
+  if (preg_match('%\s/\s*$%', $attr))
+    $xhtml_slash = ' /';
+
+# Are any attributes allowed at all for this element?
+
+  if (@count($allowed_html[strtolower($element)]) == 0)
+    return "<$element$xhtml_slash>";
+
+# Split it
+
+  $attrarr = kses_hair($attr, $allowed_protocols);
+
+# Go through $attrarr, and save the allowed attributes for this element
+# in $attr2
+
+  $attr2 = '';
+
+  foreach ($attrarr as $arreach)
+  {
+    if (!@isset($allowed_html[strtolower($element)]
+                            [strtolower($arreach['name'])]))
+      continue; # the attribute is not allowed
+
+    $current = $allowed_html[strtolower($element)]
+                            [strtolower($arreach['name'])];
+
+    if (!is_array($current))
+      $attr2 .= ' '.$arreach['whole'];
+    # there are no checks
+
+    else
+    {
+    # there are some checks
+      $ok = true;
+      foreach ($current as $currkey => $currval)
+        if (!kses_check_attr_val($arreach['value'], $arreach['vless'],
+                                 $currkey, $currval))
+        { $ok = false; break; }
+
+      if ($ok)
+        $attr2 .= ' '.$arreach['whole']; # it passed them
+    } # if !is_array($current)
+  } # foreach
+
+# Remove any "<" or ">" characters
+
+  $attr2 = preg_replace('/[<>]/', '', $attr2);
+
+  return "<$element$attr2$xhtml_slash>";
+} # function kses_attr
+
+
+function kses_hair($attr, $allowed_protocols)
+###############################################################################
+# This function does a lot of work. It parses an attribute list into an array
+# with attribute data, and tries to do the right thing even if it gets weird
+# input. It will add quotes around attribute values that don't have any quotes
+# or apostrophes around them, to make it easier to produce HTML code that will
+# conform to W3C's HTML specification. It will also remove bad URL protocols
+# from attribute values.
+###############################################################################
+{
+  $attrarr = array();
+  $mode = 0;
+  $attrname = '';
+
+# Loop through the whole attribute list
+
+  while (strlen($attr) != 0)
+  {
+    $working = 0; # Was the last operation successful?
+
+    switch ($mode)
+    {
+      case 0: # attribute name, href for instance
+
+        if (preg_match('/^([-a-zA-Z]+)/', $attr, $match))
+        {
+          $attrname = $match[1];
+          $working = $mode = 1;
+          $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr);
+        }
+
+        break;
+
+      case 1: # equals sign or valueless ("selected")
+
+        if (preg_match('/^\s*=\s*/', $attr)) # equals sign
+        {
+          $working = 1; $mode = 2;
+          $attr = preg_replace('/^\s*=\s*/', '', $attr);
+          break;
+        }
+
+        if (preg_match('/^\s+/', $attr)) # valueless
+        {
+          $working = 1; $mode = 0;
+          $attrarr[] = array
+                        ('name'  => $attrname,
+                         'value' => '',
+                         'whole' => $attrname,
+                         'vless' => 'y');
+          $attr = preg_replace('/^\s+/', '', $attr);
+        }
+
+        break;
+
+      case 2: # attribute value, a URL after href= for instance
+
+        if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match))
+         # "value"
+        {
+          if (strtolower($attrname) !== "style") {
+            $thisval = kses_bad_protocol($match[1], $allowed_protocols);
+          } else {
+            $thisval = $match[1];
+          }
+
+          $attrarr[] = array
+                        ('name'  => $attrname,
+                         'value' => $thisval,
+                         'whole' => "$attrname=\"$thisval\"",
+                         'vless' => 'n');
+          $working = 1; $mode = 0;
+          $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr);
+          break;
+        }
+
+        if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match))
+         # 'value'
+        {
+          if (strtolower($attrname) !== "style") {
+            $thisval = kses_bad_protocol($match[1], $allowed_protocols);
+          } else {
+            $thisval = $match[1];
+          }
+
+          $attrarr[] = array
+                        ('name'  => $attrname,
+                         'value' => $thisval,
+                         'whole' => "$attrname='$thisval'",
+                         'vless' => 'n');
+          $working = 1; $mode = 0;
+          $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr);
+          break;
+        }
+
+        if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match))
+         # value
+        {
+          if (strtolower($attrname) !== "style") {
+            $thisval = kses_bad_protocol($match[1], $allowed_protocols);
+          } else {
+            $thisval = $match[1];
+          }
+
+          $attrarr[] = array
+                        ('name'  => $attrname,
+                         'value' => $thisval,
+                         'whole' => "$attrname=\"$thisval\"",
+                         'vless' => 'n');
+                         # We add quotes to conform to W3C's HTML spec.
+          $working = 1; $mode = 0;
+          $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr);
+        }
+
+        break;
+    } # switch
+
+    if ($working == 0) # not well formed, remove and try again
+    {
+      $attr = kses_html_error($attr);
+      $mode = 0;
+    }
+  } # while
+
+  if ($mode == 1)
+  # special case, for when the attribute list ends with a valueless
+  # attribute like "selected"
+    $attrarr[] = array
+                  ('name'  => $attrname,
+                   'value' => '',
+                   'whole' => $attrname,
+                   'vless' => 'y');
+
+  return $attrarr;
+} # function kses_hair
+
+
+function kses_check_attr_val($value, $vless, $checkname, $checkvalue)
+###############################################################################
+# This function performs different checks for attribute values. The currently
+# implemented checks are "maxlen", "minlen", "maxval", "minval" and "valueless"
+# with even more checks to come soon.
+###############################################################################
+{
+  $ok = true;
+
+  switch (strtolower($checkname))
+  {
+    case 'maxlen':
+    # The maxlen check makes sure that the attribute value has a length not
+    # greater than the given value. This can be used to avoid Buffer Overflows
+    # in WWW clients and various Internet servers.
+
+      if (strlen($value) > $checkvalue)
+        $ok = false;
+      break;
+
+    case 'minlen':
+    # The minlen check makes sure that the attribute value has a length not
+    # smaller than the given value.
+
+      if (strlen($value) < $checkvalue)
+        $ok = false;
+      break;
+
+    case 'maxval':
+    # The maxval check does two things: it checks that the attribute value is
+    # an integer from 0 and up, without an excessive amount of zeroes or
+    # whitespace (to avoid Buffer Overflows). It also checks that the attribute
+    # value is not greater than the given value.
+    # This check can be used to avoid Denial of Service attacks.
+
+      if (!preg_match('/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value))
+        $ok = false;
+      if ($value > $checkvalue)
+        $ok = false;
+      break;
+
+    case 'minval':
+    # The minval check checks that the attribute value is a positive integer,
+    # and that it is not smaller than the given value.
+
+      if (!preg_match('/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value))
+        $ok = false;
+      if ($value < $checkvalue)
+        $ok = false;
+      break;
+
+    case 'valueless':
+    # The valueless check checks if the attribute has a value
+    # (like <a href="blah">) or not (<option selected>). If the given value
+    # is a "y" or a "Y", the attribute must not have a value.
+    # If the given value is an "n" or an "N", the attribute must have one.
+
+      if (strtolower($checkvalue) != $vless)
+        $ok = false;
+      break;
+
+    case 'cssfilter':
+    # The value is checked CSS expression
+      $ok = $checkvalue ? kses_verify_css($value) : true;
+      break;
+
+    case 'callback':
+    # The value is checked specified function
+      if (is_callable($checkvalue))
+      {
+        $ok = call_user_func($checkvalue, $value);
+      }
+      else
+      {
+        $ok = false;
+        trigger_error("can't call callback function '$checkvalue'");
+      }
+      break;
+  } # switch
+
+  return $ok;
+} # function kses_check_attr_val
+
+
+function kses_bad_protocol($string, $allowed_protocols)
+###############################################################################
+# This function removes all non-allowed protocols from the beginning of
+# $string. It ignores whitespace and the case of the letters, and it does
+# understand HTML entities. It does its work in a while loop, so it won't be
+# fooled by a string like "javascript:javascript:alert(57)".
+###############################################################################
+{
+  $string = kses_no_null($string);
+  # commented out to stop breaking multibyte chars
+  # see http://groups.google.com/group/wp-ja-pkg/browse_thread/thread/36c994dbd9276754/c3c0adb86a8b725d for details.
+  # - sugi 2008-06-12
+
+  #$string = preg_replace('/\xad+/', '', $string); # deals with Opera "feature"
+  $string2 = $string.'a';
+
+  while ($string != $string2)
+  {
+    $string2 = $string;
+    $string = kses_bad_protocol_once($string, $allowed_protocols);
+  } # while
+
+  return $string;
+} # function kses_bad_protocol
+
+
+function kses_no_null($string)
+###############################################################################
+# This function removes any NULL characters in $string.
+###############################################################################
+{
+  $string = preg_replace('/\0+/', '', $string);
+  $string = preg_replace('/(\\\\0)+/', '', $string);
+
+  return $string;
+} # function kses_no_null
+
+
+function kses_stripslashes($string)
+###############################################################################
+# This function changes the character sequence  \"  to just  "
+# It leaves all other slashes alone. It's really weird, but the quoting from
+# preg_replace(//e) seems to require this.
+###############################################################################
+{
+  return preg_replace('%\\\\"%', '"', $string);
+} # function kses_stripslashes
+
+
+function kses_array_lc($inarray)
+###############################################################################
+# This function goes through an array, and changes the keys to all lower case.
+###############################################################################
+{
+  $outarray = array();
+
+  foreach ($inarray as $inkey => $inval)
+  {
+    $outkey = strtolower($inkey);
+    $outarray[$outkey] = array();
+
+    foreach ($inval as $inkey2 => $inval2)
+    {
+      $outkey2 = strtolower($inkey2);
+      $outarray[$outkey][$outkey2] = $inval2;
+    } # foreach $inval
+  } # foreach $inarray
+
+  return $outarray;
+} # function kses_array_lc
+
+
+function kses_js_entities($string)
+###############################################################################
+# This function removes the HTML JavaScript entities found in early versions of
+# Netscape 4.
+###############################################################################
+{
+  return preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
+} # function kses_js_entities
+
+
+function kses_html_error($string)
+###############################################################################
+# This function deals with parsing errors in kses_hair(). The general plan is
+# to remove everything to and including some whitespace, but it deals with
+# quotes and apostrophes as well.
+###############################################################################
+{
+  return preg_replace('/^("[^"]*("|$)|\'[^\']*(\'|$)|\S)*\s*/', '', $string);
+} # function kses_html_error
+
+
+function kses_bad_protocol_once($string, $allowed_protocols)
+###############################################################################
+# This function searches for URL protocols at the beginning of $string, while
+# handling whitespace and HTML entities.
+###############################################################################
+{
+  return preg_replace_callback('/^((&[^;]*;|[\sA-Za-z0-9])*)'.
+                      '(:|&#58;|&#[Xx]3[Aa];)\s*/',
+                     function ($m) { return kses_bad_protocol_once2($m[1], $allowed_protocols); },
+                      $string);
+} # function kses_bad_protocol_once
+
+
+function kses_bad_protocol_once2($string, $allowed_protocols)
+###############################################################################
+# This function processes URL protocols, checks to see if they're in the white-
+# list or not, and returns different data depending on the answer.
+###############################################################################
+{
+  $string2 = kses_decode_entities($string);
+  $string2 = preg_replace('/\s/', '', $string2);
+  $string2 = kses_no_null($string2);
+  $string2 = preg_replace('/\xad+/', '', $string2);
+   # deals with Opera "feature"
+  $string2 = strtolower($string2);
+
+  $allowed = false;
+  foreach ($allowed_protocols as $one_protocol)
+    if (strtolower($one_protocol) == $string2)
+    {
+      $allowed = true;
+      break;
+    }
+
+  if ($allowed)
+    return "$string2:";
+  else
+    return '';
+} # function kses_bad_protocol_once2
+
+
+function kses_normalize_entities($string)
+###############################################################################
+# This function normalizes HTML entities. It will convert "AT&T" to the correct
+# "AT&amp;T", "&#00058;" to "&#58;", "&#XYZZY;" to "&amp;#XYZZY;" and so on.
+###############################################################################
+{
+# Disarm all entities by converting & to &amp;
+
+  $string = str_replace('&', '&amp;', $string);
+
+# Change back the allowed entities in our entity whitelist
+
+  $string = preg_replace('/&amp;([A-Za-z][A-Za-z0-9]{0,19});/',
+                         '&\\1;', $string);
+  $string = preg_replace_callback('/&amp;#0*([0-9]{1,5});/',
+                         function ($m) { return kses_normalize_entities2($m[1]); },
+                        $string);
+  $string = preg_replace('/&amp;#([Xx])0*(([0-9A-Fa-f]{2}){1,2});/',
+                         '&#\\1\\2;', $string);
+
+  return $string;
+} # function kses_normalize_entities
+
+
+function kses_normalize_entities2($i)
+###############################################################################
+# This function helps kses_normalize_entities() to only accept 16 bit values
+# and nothing more for &#number; entities.
+###############################################################################
+{
+  return (($i > 65535) ? "&amp;#$i;" : "&#$i;");
+} # function kses_normalize_entities2
+
+
+function kses_decode_entities($string)
+###############################################################################
+# This function decodes numeric HTML entities (&#65; and &#x41;). It doesn't
+# do anything with other entities like &auml;, but we don't need them in the
+# URL protocol whitelisting system anyway.
+###############################################################################
+{
+  $string = preg_replace('/&#([0-9]+);/', function($m){return chr($m[1]);}, $string);
+  $string = preg_replace('/&#[Xx]([0-9A-Fa-f]+);/', function($m){return chr(hexdec($m[1]));},
+                         $string);
+
+  return $string;
+} # function kses_decode_entities
+
+function _sanitize_hexentity($match) {
+  return '&#' . intval("0".substr($match[0], 2, strlen($match[0]) -3)) . ';';
+}
+
+function kses_verify_css($cssstr)
+###############################################################################
+# CSS expression checker for IE.
+# See URLs below for details;
+#   - http://openmya.hacker.jp/hasegawa/security/expression.txt
+#   - http://archive.openmya.devnull.jp/2006.08/msg00369.html
+#   - https://www.webappsec.jp/modules/bwiki/index.php?IE%A4%CEexpression%A4%C8url
+###############################################################################
+{
+  if (!$cssstr) return true;
+  $ok = false;
+  $cssstr = preg_replace_callback('/&#x[0-9A-F]+;/i', '_sanitize_hexentity', $cssstr);
+  $cssstr = mb_decode_numericentity($cssstr, array(0, 0xFFFFFF, 0, 0xFFFFFF), 'UTF-8');
+  $cssstr = preg_replace('/\x5c(\d+)/', '&#x\1;', $cssstr);
+  $cssstr = preg_replace_callback('/&#x[0-9A-F]+;/i', '_sanitize_hexentity', $cssstr);
+  $cssstr = mb_decode_numericentity($cssstr, array(0, 0xFFFFFF, 0, 0xFFFFFF), 'UTF-8');
+  $cssstr = preg_replace('{/\*.*?\*/|\x00+}', '', $cssstr);
+  $enc_orig = mb_regex_encoding();
+  mb_regex_encoding('UTF-8');
+  #   e    x    p    r    e    s    s    i    o    n
+  #  FF45 FF58 FF50 FF52 FF45 FF53 FF53 FF49 FF4F FF4E
+  #  FF25 FF38 FF30 FF32 FF25 FF33 FF33 FF29 FF2F FF2E
+  #                 0280                          0274
+  #                                               027F
+  $ok = !mb_eregi('javascript:|@import|[e\x{ff45}\x{ff25}][x\x{ff58}\x{ff38}][p\x{ff50}\x{ff30}][r\x{ff52}\x{ff32}\x{0280}][e\x{ff45}\x{ff25}][s\x{ff53}\x{ff33}]{2}[i\x{ff49}\x{ff29}][o\x{ff4f}\x{ff2f}][n\x{ff4e}\x{ff2e}\x{0274}\x{207f}]', $cssstr);
+  mb_regex_encoding($enc_orig);
+  return $ok;
+}
+
+?>