<?php
//
//	guiedit - PukiWiki Plugin
//
//	License:
//		GNU General Public License Version 2 or later (GPL)
//		http://www.gnu.org/licenses/gpl.html
//
//	Copyright (C) 2006-2008 garand
//	PukiWiki : Copyright (C) 2001-2006 PukiWiki Developers Team
//	FCKeditor : Copyright (C) 2003-2008 Frederico Caldeira Knabben
//      PukiWiki Plus! : Copyright (C) 2009 Katsumi Saito
//  PukiWiki : Copyright (C) 2003-2020 PukiWiki Developers Team
//	CKEditor : Copyright (C) 2003-2020 CKSource sp. z o.o. sp.k. All rights reserved.
//
//
//	File:
//	  wiki2htmlv.php
//	  PukiWiki の構文を HTML5 に変換
//
// PHP8対応、細かいところを修正 byはいふん

//	設定の読み込み
//require_once('easyedit/guiedit/guiedit.ini.php');

//	PukiWiki の構文を HTML5 に変換
function guiedit_convert_html($lines)
{
    global $vars;
    
    if (!isset($lines)||empty($lines)) {
        return '<div id="contents">'. "\n" .'<div id="body">'. "\n" .'<p>' . ($vars['refer'] ? "<a href=\"" . $vars['refer'] . "\">" . $vars['refer'] . "</a>" : $vars['page'] . "<br />") . '</p>'. "\n" .'</div>'. "\n" .'</div>';
    }
    if (! is_array($lines)) {
        $lines = explode("\n", $lines);
    }

    $body = new BodyEx();
    $body->parse($lines);

    $body2 = '<div id="contents"><div id="body">' . $body->toString() . '</div></div>';
    
    return $body2;
}

// インライン要素の変換
function guiedit_make_link($line)
{
    $obj = new InlineConverterEx();
    return $obj->convert($line);
}

// 添付ファイルプラグインの変換
function guiedit_convert_ref($args, $div = false)
{
    $options = htmlsc(join(',', $args));

    $filename = array_shift($args);
    $_title = array();
    $params = array(
        'left'   => 0, // 左寄せ
        'center' => 0, // 中央寄せ
        'right'  => 0, // 右寄せ
        'wrap'   => 0, // TABLEで囲む
        'nowrap' => 0, // TABLEで囲まない
        'around' => 0, // 回り込み
        'noicon' => 0, // アイコンを表示しない
        'nolink' => 0, // 元ファイルへのリンクを張らない
        'noimg'  => 0, // 画像を展開しない
        'zoom'   => 0, // 縦横比を保持する
        '_w'     => 0,     // 幅
        '_h'     => 0,     // 高さ
        '_size'  => '%'
    );

    // パラメータ解析
    foreach ($args as $arg) {
        $s_arg = strtolower($arg);
        if (array_key_exists($s_arg, $params)) {
            $params[$s_arg] = 1;
        } elseif (preg_match('/^([0-9]+)x([0-9]+)$/', $arg, $matches)) {
            $params['_w'] = $matches[1];
            $params['_h'] = $matches[2];
            $params['_size'] = 'px';
        } elseif (preg_match('/^([0-9.]+)%$/', $arg, $matches) && $matches[1] > 0) {
            $params['_w'] = $matches[1];
        } else {
            $_title[] = $arg;
        }
    }

    $align = '';
    if ($params['left']) {
        $align = 'left';
    } elseif ($params['center']) {
        $align = 'center';
    } elseif ($params['right']) {
        $align = 'right';
    }

    $alt = !empty($_title) ? htmlsc(join(',', $_title)) : '';
    $alt = preg_replace("/^,/", '', $alt);

    $attribute = 'class="ref" contenteditable="false"' . ((UA_NAME == "MSIE") ? '' : ' style="cursor:default"');
    $attribute .= ' _filename="' . $filename . '"';
    $attribute .= ' _alt="' . $alt . '"';
    $attribute .= ' _width="' . ($params['_w'] ? $params['_w'] : '') . '"';
    $attribute .= ' _height="' . ($params['_h'] ? $params['_h'] : '') . '"';
    $attribute .= ' _size="' . $params['_size'] . '"';
    $attribute .= ' _align="' . $align . '"';
    $attribute .= ' _wrap="' . $params['wrap'] . '"';
    $attribute .= ' _around="' . $params['around'] . '"';
    $attribute .= ' _nolink="' . $params['nolink'] . '"';
    $attribute .= ' _noicon="' . $params['noicon'] . '"';
    $attribute .= ' _noimg="' . $params['noimg'] . '"';
    $attribute .= ' _zoom="' . $params['zoom'] . '"';
    if (preg_match('/(.*?)\.([^.]+$)/', $filename, $matches)) {
        $filename_base = $matches[1];
        $filename_extension = $matches[2];
        if (preg_match('/^(png|jpe?g|gif|swf)$/i', $filename_extension)) {
            if ($div) {
                return "<div class=\"embededplugin\" _plugin=\"ref\" _type=\"" . $filename_extension . "\" _pkwkargs=\"" . $options . "\"><img src=\"" . $filename . "\" style=\"width:" . ($params['_w'] ? $params['_w'] : '') . "px;height:" . ($params['_h'] ? $params['_h'] : '') . "px;\" /></div>";
            } else {
                return "<span class=\"embededplugin\" _plugin=\"ref\" _type=\"" . $filename_extension . "\" _pkwkargs=\"" . $options . "\"><img src=\"" . $filename . "\" style=\"width:" . ($params['_w'] ? $params['_w'] : '') . "px;height:" . ($params['_h'] ? $params['_h'] : '') . "px;\" /></span>";
            }
        }
    }
    if ($div) {
        $tags = "<div $attribute>#ref($options)</div>";
    } else {
        $tags = "<span $attribute>&ref($options);</span>";
    }
    
    return $tags;
}



function guiedit_make_line_rules($line)
{
    global $line_rules;
    global $usefacemark;
    static $pattern, $replace;
    $guiedit_line_rules = array();
    if (!isset($pattern)) {
        if ($usefacemark) {
            $guiedit_line_rules += $line_rules;
        }
        $pattern = array_map(function ($a) {
            return '/' . $a . '/';
        }, array_keys($guiedit_line_rules));
        $replace = array_values($guiedit_line_rules);
        unset($guiedit_line_rules);
    }
    
    return preg_replace($pattern, $replace, $line);
}

// インライン変換クラス
class InlineConverterEx
{
    public function convert($line, $link = true, $enc = true)
    {
        if ($enc) {
            $line = preg_replace("/&amp;/", "&#038;", $line);
            $line = htmlsc($line);
        }

        // インライン・プラグイン
        $pattern = '/&amp;(\w+)(?:\(((?:(?!\)[;{]).)*)\))?(?:\{((?:(?R)|(?!};).)*)\})?;/';
        $line = preg_replace_callback($pattern, array(&$this, 'convert_plugin'), $line);
        
        // ルールの変換
        $line = guiedit_make_line_rules($line);
        
        // 文字サイズの変換
        $pattern = "/<span\s(style=\"font-size:(\d+)px|class=\"size([1-7])).*?>/";
        $line = preg_replace_callback($pattern, array(&$this, 'convert_size'), $line);
        // 色の変換
        $pattern = "/<span\sstyle=\"color:([#0-9a-z]+)(; background-color:([#0-9a-z]+))?\">/";
        $line = preg_replace_callback($pattern, array(&$this, 'convert_color'), $line);
        // 注釈
        $line = preg_replace("/\(\(((?:(?R)|(?!\)\)).)*)\)\)/", "<span class=\"note\"><img alt=\"Note\" src=\"./easyedit/plugins/note/icons/note2.png\" />$1</span>", $line);
        // 参照文字
        $line = preg_replace('/&amp;(#?[a-z0-9]+);/', "&$1;", $line);
        // 上付き文字
        $line = preg_replace('/SUP{(.*?)}/', "<sup>$1</sup>", $line);
        // 下付き文字・添え字
        $line = preg_replace('/SUB{(.*?)}/', "<sub>$1</sub>", $line);
        
        if (exist_plugin("sup")) {
            // 上付き文字
            $line = preg_replace('/&amp;#038;sup{(.*?)};/', "<sup>$1</sup>", $line);
        }
        if (exist_plugin("sub")) {
            // 下付き文字
            $line = preg_replace('/&amp;#038;sub{(.*?)};/', "<sub>$1</sub>", $line);
        }

        // リンク
        if ($link) {
            //$pattern = "/\(\(((?:(?R)|(?!\)\)).)*)\)\)/";
            //$replace = "<img alt=\"Note\" title=\"$1\" />";
            $line = $this->make_link($line);
        }
        
        if (preg_match("/^<br\s\/>$/", $line)) {
            $line .= "\n&nbsp;";
        }

        return $line;
    }

    // 文からリンクを検出し、link_replace を呼び出す
    public function make_link($line)
    {
        $link_rules = "/(
			(?:\[\[((?:(?!\]\]).)+):)? 
			((?:https?|ftp|news)(?::\/\/[!~*'();\/?:\@&=+\$,%#\w.-]+))
			(?(2)\]\])
			|
			 (\[\[
			  (?:
			   (?:((?:(?!\]\]).)+))
			   (?:&gt;)
			  )?
			  (?:
			   (\#(?:[a-zA-Z][\w-]*)?)
			   |
			   ((?:(?!\]\]).)*)
			  )?
			 \]\])
		)/x";

        return preg_replace_callback($link_rules, array(&$this,'link_replace'), $line);
    }

    // make_link で検出したリンクにリンクタグを付加する
    public function link_replace($matches)
    {
        if ($matches[3] != '') {
            if (!$matches[2]) {
                return $matches[3];
            }
            $url = $matches[3];
            $alias = empty($matches[2]) ? $url : $matches[2];
            return '<a href="'.$url.'">'.$alias.'</a>';
        }
        if ($matches[6] != '') {
            $str = empty($matches[5]) ? $matches[6] : $matches[5];
            return '<a href="' . $matches[6] . '">' . $str.'</a>';
        }
        if ($matches[7] != '') {
            $str = empty($matches[5]) ? $matches[7] : $matches[5];
            return '<a href="' . $matches[7] . '">' . $str . '</a>';
        }
        return $matches[0];
    }
    
    // インラインプラグイン処理メソッド
    public function convert_plugin($matches)
    {
        $aryargs = (!empty($matches[2])) ? explode(',', $matches[2]) : array();
        $name = strtolower($matches[1]);
        $body = empty($matches[3]) ? '' : $matches[3];
        //	プラグインが存在しない場合はそのまま返す。
        //if (!file_exists(PLUGIN_DIR . $name . '.inc.php')) {
        if (!exist_plugin($name)) {
            return $matches[0];
        }

        switch ($name) {
            case 'aname':
                return '<a name="'.$aryargs[0].'">'.$body.'</a>';
            case 'br':
                return '<br />';
            case 'button':
                return '<span _pkwkargs="' . implode($aryargs) . $body . '" _plugin="button" class="embededplugin"><button type="button" style="text-indent:0px;line-height:1em;vertical-align:middle">' . implode($aryargs) . $body . '</button></span>';
            case 'color':
                $color = $aryargs[0];
                $bgcolor = $aryargs[1];
                if ($body == '') {
                    return '';
                }
                if ($color != '' && !preg_match('/^(#[0-9a-f]+|[\w-]+)$/i', $color)) {
                    return $body;
                }
                if ($bgcolor != '' && !preg_match('/^(#[0-9a-f]+|[\w-]+)$/i', $bgcolor)) {
                    return $body;
                }
                if ($color != '') {
                    $color = 'color:'.$color;
                }
                if ($bgcolor != '') {
                    $bgcolor = ($color ? '; ' : '') . 'background-color:'.$bgcolor;
                }
                return '<span style="'.$color.$bgcolor.'">' . $this->convert($body, true, false) . '</span>';
            case 'sup':
            case 'sub':
                return '<'.$name.'>'.$body.'</'.$name.'>';
            case 'submit':
                return '<span _pkwkargs="' . implode($aryargs) . $body . '" _plugin="submit" class="embededplugin"><button type="submit" style="text-indent:0px;line-height:1em;vertical-align:middle">' . implode($aryargs) . $body . '</button></span>';
            case 'size':
                $size = $aryargs[0];
                if ($size == '' || $body == '') {
                    return '';
                }
                if (!preg_match('/^\d+$/', $size)) {
                    return $body;
                }
                return '<span style="font-size:' . $size . 'px;line-height:130%">' .
                       $this->convert($body, true, false) . "</span>";
            case 'ref':
                return guiedit_convert_ref($aryargs, false);
        }
        
        if ($body) {
            $pattern = array("%%", "''", "[[", "]]", "{", "|", "}");
            $replace = array("&#037;&#037;", "&#039;&#039;", "&#091;&#091;",
                "&#093;&#093;", "&#123;", "&#124;", "&#125;");
            $body = str_replace($pattern, $replace, $body);
        }
        
        $inner = '&' . $matches[1] . ($matches[2] ? '('.$matches[2].')' : '') . ($body ? '{'.$body.'}' : '') . ';';
        $style = (UA_NAME == "MSIE") ? '' : ' style="cursor:default"';
        
        return '<span class="plugin" contenteditable="false"'.$style.'>'.$inner.'</span>';
    }
    
    // 色の変換
    public function convert_color($matches)
    {
        $color = $matches[1];
        $bgcolor = $matches[3];
        if ($bgcolor && preg_match("/^#[0-9a-z]{3}$/i", $bgcolor)) {
            $bgcolor = "; background-color:" . preg_replace('/[0-9a-f]/i', "$0$0", $bgcolor);
        }
        if (preg_match("/^#[0-9a-z]{3}$/i", $color)) {
            $color = preg_replace('/[0-9a-f]/i', "$0$0", $color);
        }
        
        // return "<sapn\sstyle=\"color:$color$bgcolor\">";
        // UPK
        return '<span style="color:'.$color.$bgcolor.'">';
    }

    // 文字サイズの変換
    public function convert_size($matches)
    {
        if ($matches[2]) {
            $size = $matches[2];
            
            if ($size <=  8) {
                $size = 8;
            } elseif ($size <=  9) {
                $size = 9;
            } elseif ($size <= 10) {
                $size = 10;
            } elseif ($size <= 11) {
                $size = 11;
            } elseif ($size <= 12) {
                $size = 12;
            } elseif ($size <= 14) {
                $size = 14;
            } elseif ($size <= 16) {
                $size = 16;
            } elseif ($size <= 18) {
                $size = 18;
            } elseif ($size <= 22) {
                $size = 20;
            } elseif ($size <= 26) {
                $size = 24;
            } elseif ($size <= 30) {
                $size = 28;
            } elseif ($size <= 36) {
                $size = 32;
            } elseif ($size <= 44) {
                $size = 40;
            } elseif ($size <= 52) {
                $size = 48;
            } else {
                $size = 60;
            }
            
            return '<span style="font-size:' . $size . 'px; line-height:130%">';
        }
        
        switch ($matches[3]) {
            case 1:	$size = 'xx-small';
            // no break
            case 2: $size = 'x-small';
            // no break
            case 3:	$size = 'small';
            // no break
            case 4:	$size = 'medium';
            // no break
            case 5:	$size = 'large';
            // no break
            case 6:	$size = 'x-large';
            // no break
            case 7:	$size = 'xx-large';
        }
        
        return '<span style="font-size:'.$size.'; line-height:130%">';
    }
}


// Block elements
class ElementEx
{
    public $parent;
    public $elements; // References of childs
    public $last;     // Insert new one at the back of the $last

    public function __construct()
    {
        $this->elements = array();
        $this->last     = & $this;
    }

    public function ElementEx()
    {
        $this->__construct();
    }

    public function setParent(& $parent)
    {
        $this->parent = & $parent;
    }

    public function & add(& $obj)
    {
        if ($this->canContain($obj)) {
            return $this->insert($obj);
        } else {
            return $this->parent->add($obj);
        }
    }

    public function & insert(& $obj)
    {
        $obj->setParent($this);
        $this->elements[] = & $obj;

        return $this->last = & $obj->last;
    }

    public function canContain(& $obj)
    {
        return true;
    }

    public function wrap($string, $tag, $param = '', $canomit = true)
    {
        return ($canomit && $string == '') ? '' :
            '<' . $tag . $param . '>' . $string . '</' . $tag . '>';
    }

    public function toString()
    {
        $ret = array();
        foreach (array_keys($this->elements) as $key) {
            $ret[] = $this->elements[$key]->toString();
        }
        return join("\n", $ret);
    }

    public function dump($indent = 0)
    {
        $ret = str_repeat(' ', $indent) . get_class($this) . "\n";
        $indent += 2;
        foreach (array_keys($this->elements) as $key) {
            $ret .= is_object($this->elements[$key]) ?
                $this->elements[$key]->dump($indent) : '';
            //str_repeat(' ', $indent) . $this->elements[$key];
        }
        return $ret;
    }
}

// Returns inline-related object
function & Factory_InlineEx($text)
{
    // Check the first letter of the line
    if (substr($text, 0, 1) == '~') {
        // return new ParagraphEx(' ' . substr($text, 1));
        $obj = new ParagraphEx(' ' . substr($text, 1));
    } else {
        // return new InlineEx($text);
        $obj = new InlineEx($text);
    }
    return $obj;
}

function & Factory_DListEx(& $root, $text)
{
    $out = explode('|', ltrim($text), 2);
    if (count($out) < 2) {
        return Factory_InlineEx($text);
    } else {
        return new DListEx($out);
    }
}

// '|'-separated table
function & Factory_TableEx(& $root, $text)
{
    if (! preg_match('/^\|(.+)\|([hHfFcC]?)$/', $text, $out)) {
        return Factory_InlineEx($text);
    } else {
        // return new TableEx($out);
        $obj = new TableEx($out);
        return $obj;
    }
}

// Comma-separated table
function & Factory_YTableEx(& $root, $text)
{
    if ($text == ',') {
        return Factory_InlineEx($text);
    } else {
        // return new YTableEx(csv_explode(',', substr($text, 1)));
        $obj = new YTableEx(csv_explode(',', substr($text, 1)));
        return $obj;
    }
}

function & Factory_DivEx(& $root, $text)
{
    $matches = array();

    // Seems block plugin?
    if (PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK) {
        // Usual code
        if (preg_match('/^\#([^\(]+)(?:\((.*)\))?/', $text, $matches)) {
            // return new DivEx($matches);
            $obj = new DivEx($matches);
            return $obj;
        }
    } else {
        // Hack code
        if (preg_match('/^#([^\(\{]+)(?:\(([^\r]*)\))?(\{*)/', $text, $matches)) {
            $len  = strlen($matches[3]);
            $body = array();
            if ($len == 0) {
                // return new DivEx($matches); // Seems legacy block plugin
                $obj = new DivEx($matches);
                return $obj;
            } elseif (preg_match('/\{{' . $len . '}\s*\r(.*)\r\}{' . $len . '}/', $text, $body)) {
                $matches[3] .= "\r" . $body[1] . "\r" . str_repeat('}', $len);
                // return new DivEx($matches); // Seems multiline-enabled block plugin
                $obj = new DivEx($matches);
                return $obj;
            }
        }
    }

    // return new ParagraphEx($text);
    $obj = new ParagraphEx($text);
    return $obj;
}

// InlineEx elements
class InlineEx extends ElementEx
{
    public function __construct($text)
    {
        parent::__construct();
        $this->elements[] = trim((substr($text, 0, 1) == "\n") ?
            $text : guiedit_make_link($text));
    }
    
    public function InlineEx($text)
    {
        $this->__construct($text);
    }

    public function & insert(& $obj)
    {
        $this->elements[] = $obj->elements[0];
        return $this;
    }

    public function canContain(& $obj)
    {
        return is_a($obj, 'InlineEx');
    }

    public function toString()
    {
        global $line_break;
        return join(($line_break ? '<br />' . "\n" : "\n"), $this->elements);
    }

    public function & toPara($class = '')
    {
        $obj = new ParagraphEx('', $class);
        $obj->insert($this);
        return $obj;
    }
}

// ParagraphEx: blank-line-separated sentences
class ParagraphEx extends ElementEx
{
    public $param;

    public function __construct($text, $param = '')
    {
        parent::__construct();
        $this->param = $param;
        if ($text == '') {
            return;
        }

        if (substr($text, 0, 1) == '~') {
            $text = ' ' . substr($text, 1);
        }

        $this->insert(Factory_InlineEx($text));
    }

    public function ParagraphEx($text, $param = '')
    {
        $this->__construct($text, $param);
    }

    public function canContain(& $obj)
    {
        return is_a($obj, 'InlineEx');
    }

    public function toString()
    {
        return $this->wrap(parent::toString(), 'p', $this->param);
    }
}

// * HeadingEx1
// ** HeadingEx2
// *** HeadingEx3
class HeadingEx extends ElementEx
{
    public $level;
    public $id;
    public $msg_top;

    public function __construct(& $root, $text)
    {
        parent::__construct();

        $this->level = min(3, strspn($text, '*'));
        
        $text = substr($text, $this->level);
        if (preg_match('/\s*\[#(\w+)\]/', $text, $matches)) {
            $this->id = $matches[1];
        }
        $text = preg_replace('/\s*\[#\w+\]/', '', $text);
        
        $this->insert(Factory_InlineEx($text));
        $this->level++; // h2,h3,h4
    }

    public function HeadingEx(& $root, $text)
    {
        $this->__construct($root, $text);
    }

    public function & insert(& $obj)
    {
        parent::insert($obj);
        return $this->last = & $this;
    }

    public function canContain(& $obj)
    {
        return false;
    }

    public function toString()
    {
        return $this->wrap(
            parent::toString(),
            'h' . $this->level,
            ' id="' . $this->id . '"'
        );
    }
}

// ----
// Horizontal Rule
class HRuleEx extends ElementEx
{
    public function __construct(& $root, $text)
    {
        parent::__construct();
    }
    
    public function HRuleEx(& $root, $text)
    {
        $this->__construct($root, $text);
    }


    public function canContain(& $obj)
    {
        return false;
    }

    public function toString()
    {
        global $hr;
        return $hr;
    }
}

// Lists (UL, OL, DL)
class ListContainerEx extends ElementEx
{
    public $tag;
    public $tag2;
    public $level;
    public $style;
    public $margin;
    public $left_margin;

    public function __construct($tag, $tag2, $head, $text)
    {
        parent::__construct();

        $var_margin      = '_' . $tag . '_margin';
        $var_left_margin = '_' . $tag . '_left_margin';
        global $$var_margin, $$var_left_margin;

        $this->margin      = $$var_margin;
        $this->left_margin = $$var_left_margin;

        $this->tag   = $tag;
        $this->tag2  = $tag2;
        $this->level = min(3, strspn($text, $head));
        $text = ltrim(substr($text, $this->level));

        parent::insert(new ListElementEx($this->level, $tag2));
        if ($text != '') {
            $this->last = & $this->last->insert(Factory_InlineEx($text));
        }
    }

    public function ListContainerEx($tag, $tag2, $head, $text)
    {
        $this->__construct($tag, $tag2, $head, $text);
    }

    public function canContain(& $obj)
    {
        return (! is_a($obj, 'ListContainerEx')
            || ($this->tag == $obj->tag && $this->level == $obj->level));
    }

    public function setParent(& $parent)
    {
        global $_list_pad_str;

        parent::setParent($parent);

        $step = $this->level;
        if (isset($parent->parent) && is_a($parent->parent, 'ListContainerEx')) {
            $step -= $parent->parent->level;
        }

        $margin = $this->margin * $step;
        if ($step == $this->level) {
            $margin += $this->left_margin;
        }

        $this->style = sprintf($_list_pad_str, $this->level, $margin, $margin);
    }

    public function & insert(& $obj)
    {
        if (! is_a($obj, get_class($this))) {
            return $this->last = & $this->last->insert($obj);
        }

        // Break if no elements found (BugTrack/524)
        if (count($obj->elements) == 1 && empty($obj->elements[0]->elements)) {
            return $this->last->parent;
        } // up to ListElementEx

        // Move elements
        foreach (array_keys($obj->elements) as $key) {
            parent::insert($obj->elements[$key]);
        }

        return $this->last;
    }

    public function toString()
    {
        return $this->wrap(parent::toString(), $this->tag, $this->style);
    }
}

class ListElementEx extends ElementEx
{
    public function __construct($level, $head)
    {
        parent::__construct();
        $this->level = $level;
        $this->head  = $head;
    }

    public function ListElementEx($level, $head)
    {
        $this->__construct($level, $head);
    }

    public function canContain(& $obj)
    {
        return (! is_a($obj, 'ListContainerEx') || ($obj->level > $this->level));
    }

    public function toString()
    {
        return $this->wrap(parent::toString(), $this->head);
    }
}

// - One
// - Two
// - Three
class UListEx extends ListContainerEx
{
    public function __construct(& $root, $text)
    {
        parent::__construct('ul', 'li', '-', $text);
    }
    
    public function UListEx(& $root, $text)
    {
        $this->__construct($root, $text);
    }
}

// + One
// + Two
// + Three
class OListEx extends ListContainerEx
{
    public function __construct(& $root, $text)
    {
        parent::__construct('ol', 'li', '+', $text);
    }
    
    public function OListEx(& $root, $text)
    {
        $this->__construct($root, $text);
    }
}

// : definition1 | description1
// : definition2 | description2
// : definition3 | description3
class DListEx extends ListContainerEx
{
    public function __construct($out)
    {
        parent::__construct('dl', 'dt', ':', $out[0]);
        $this->last = & ElementEx::insert(new ListElementEx($this->level, 'dd'));
        if ($out[1] != '') {
            $this->last = & $this->last->insert(Factory_InlineEx($out[1]));
        }
    }
    
    public function DListEx($out)
    {
        $this->__construct($out);
    }
}

// > Someting cited
// > like E-mail text
class BQuoteEx extends ElementEx
{
    public $level;

    public function BQuoteEx(& $root, $text)
    {
        $this->__construct($root, $text);
    }

    public function __construct(& $root, $text)
    {
        parent::__construct();

        $head = substr($text, 0, 1);
        $this->level = min(3, strspn($text, $head));
        $text = ltrim(substr($text, $this->level));

        if ($head == '<') { // Blockquote close
            $level       = $this->level;
            $this->level = 0;
            $this->last  = & $this->end($root, $level);
            if ($text != '') {
                $this->last = & $this->last->insert(Factory_InlineEx($text));
            }
        } else {
            $this->insert(Factory_InlineEx($text));
        }
    }

    public function canContain(& $obj)
    {
        return (! is_a($obj, get_class($this)) || $obj->level >= $this->level);
    }

    public function & insert(& $obj)
    {
        // BugTrack/521, BugTrack/545
        if (is_a($obj, 'InlineEx')) {
            return parent::insert($obj->toPara(' class="quotation"'));
        }

        if (is_a($obj, 'BQuoteEx') && $obj->level == $this->level && count($obj->elements)) {
            $obj = & $obj->elements[0];
            if (is_a($this->last, 'ParagraphEx') && count($obj->elements)) {
                $obj = & $obj->elements[0];
            }
        }
        return parent::insert($obj);
    }

    public function toString()
    {
        return $this->wrap(parent::toString(), 'blockquote');
    }

    public function & end(& $root, $level)
    {
        $parent = & $root->last;

        while (is_object($parent)) {
            if (is_a($parent, 'BQuoteEx') && $parent->level == $level) {
                return $parent->parent;
            }
            $parent = & $parent->parent;
        }
        return $this;
    }
}

class TableCellEx extends ElementEx
{
    public $tag = 'td'; // {td|th}
    public $colspan = 1;
    public $rowspan = 1;
    public $style; // is array('width'=>, 'align'=>...);
    public $is_template;
    public $hspace = 0;
    public $fspace = 0;

    public function __construct($text, $is_template = false)
    {
        parent::__construct();
        $this->style = $matches = array();
        $this->is_template = $is_template;

        while (preg_match(
            '/^(?:(LEFT|CENTER|RIGHT)|(BG)?COLOR\(([#\w]+)\)|SIZE\((\d+)\)):(.*)$/',
            $text,
            $matches
        )) {
            if ($matches[1]) {
                $this->style['align'] = ' align="' . strtolower($matches[1]) . '"';
                $text = $matches[5];
            } elseif ($matches[3]) {
                $name = $matches[2] ? 'background-color' : 'color';
                $color = $matches[3];
                if (preg_match("/^#[0-9a-f]{3}$/i", $color)) {
                    $color = preg_replace("/[0-9a-f]/i", "$0$0", $color);
                }
                $this->style[$name] = $name . ':' . htmlsc($color) . ';';
                $text = $matches[5];
            } elseif ($matches[4]) {
                $this->style['size'] = 'font-size:' . htmlsc($matches[4]) . 'px;';
                $text = $matches[5];
            }
        }
        
        if ($is_template && is_numeric($text)) {
            $this->style['width'] = ' width="' . $text . '"';
        }

        if ($text == '>') {
            $this->colspan = 0;
        }
        
        if ($is_template) {
            $this->tag = 'col';
        } elseif ($text == '~') {
            $this->rowspan = 0;
        } elseif (substr($text, 0, 1) == '~') {
            $this->tag = 'th';
            $text      = substr($text, 1);
        }

        if ($text != '' && $text[0] == '#') {
            // Try using DivEx class for this $text
            $obj = & Factory_DivEx($this, $text);
            if (is_a($obj, 'ParagraphEx')) {
                $obj = & $obj->elements[0];
            }
        } else {
            if (preg_match("/^(\s+)?.+?(\s+)?$/", $text, $matches)) {
                $this->hspace = isset($matches[1]) ? strlen($matches[1]) : 0;
                $this->fspace = isset($matches[2]) ? strlen($matches[2]) : 0;
            }
            $obj = & Factory_InlineEx($text);
        }

        $this->insert($obj);
    }

    public function TableCellEx($text, $is_template = false)
    {
        $this->__construct($text, $is_template);
    }

    public function setStyle(& $style)
    {
        foreach ($style as $key=>$value) {
            if (! isset($this->style[$key])) {
                $this->style[$key] = $value;
            }
        }
    }

    public function toString()
    {
        if ($this->is_template) {
            $param = '';
        } else {
            if ($this->rowspan == 0 || $this->colspan == 0) {
                return '';
            }

            $param = ' class="style_' . $this->tag . '"'
                   . ' _hspace="' . $this->hspace . '"'
                   . ' _fspace="' . $this->fspace . '"';
            if ($this->rowspan > 1) {
                $param .= ' rowspan="' . $this->rowspan . '"';
            }
            if ($this->colspan > 1) {
                $param .= ' colspan="' . $this->colspan . '"';
                unset($this->style['width']);
            }
        }

        if (! empty($this->style)) {
            foreach ($this->style as $key=>$value) {
                if ($key == 'align' || $key == 'width') {
                    $param .= $value;
                    unset($this->style[$key]);
                }
            }
            $param .= ' style="' . join(' ', $this->style) . '"';
        }
        
        return $this->wrap($this->is_template ? '' : parent::toString(), $this->tag, $param, false);
    }
}

// | title1 | title2 | title3 |
// | cell1  | cell2  | cell3  |
// | cell4  | cell5  | cell6  |
class TableEx extends ElementEx
{
    public $type;
    public $types;
    public $col; // number of column

    public function __construct($out)
    {
        parent::__construct();

        $cells       = explode('|', $out[1]);
        $this->col   = count($cells);
        $this->type  = strtolower($out[2]);
        $this->types = array($this->type);
        $is_template = ($this->type == 'c');
        $row = array();
        foreach ($cells as $cell) {
            $row[] = new TableCellEx($cell, $is_template);
        }
        $this->elements[] = $row;
    }

    public function TableEx($out)
    {
        $this->__construct($out);
    }

    public function canContain(& $obj)
    {
        return is_a($obj, 'TableEx') && ($obj->col == $this->col);
    }

    public function & insert(& $obj)
    {
        $this->elements[] = $obj->elements[0];
        $this->types[]    = $obj->type;
        return $this;
    }

    public function toString()
    {
        static $parts = array('h'=>'thead', 'f'=>'tfoot', ''=>'tbody');

        // Set rowspan (from bottom, to top)
        for ($ncol = 0; $ncol < $this->col; $ncol++) {
            $rowspan = 1;
            foreach (array_reverse(array_keys($this->elements)) as $nrow) {
                $row = & $this->elements[$nrow];
                if ($row[$ncol]->rowspan == 0) {
                    ++$rowspan;
                    continue;
                }
                $row[$ncol]->rowspan = $rowspan;
                // Inherits row type
                while (--$rowspan) {
                    $this->types[$nrow + $rowspan] = $this->types[$nrow];
                }
                $rowspan = 1;
            }
        }

        // Set colspan and style
        $stylerow = null;
        foreach (array_keys($this->elements) as $nrow) {
            $row = & $this->elements[$nrow];
            $colspan = 1;
            
            if ($this->types[$nrow] == 'c') {
                for ($i = count($row) - 2; $i >= 0; $i--) {
                    if ($row[$i]->colspan == 0) {
                        $row[$i]->setStyle($row[$i + 1]->style);
                    }
                }
            }
            
            foreach (array_keys($row) as $ncol) {
                if ($row[$ncol]->colspan == 0) {
                    ++$colspan;
                    continue;
                }
                $row[$ncol]->colspan = $colspan;
                if ($stylerow !== null) {
                    $row[$ncol]->setStyle($stylerow[$ncol]->style);
                    // Inherits column style
                    while (--$colspan) {
                        $row[$ncol - $colspan]->setStyle($stylerow[$ncol]->style);
                    }
                }
                $colspan = 1;
            }
        }

        // toString
        $string = '';
        $part_string = '';
        $old_type = '';
        foreach (array_keys($this->elements) as $nrow) {
            if (($old_type != $this->types[$nrow]) && ($part_string != '')) {
                $string .= ($old_type == 'c') ? $part_string : $this->wrap($part_string, $parts[$old_type]);
                $part_string = '';
            }
            $row        = & $this->elements[$nrow];
            $row_string = '';
            foreach (array_keys($row) as $ncol) {
                $row_string .= $row[$ncol]->toString();
            }
            $part_string .= $this->wrap($row_string, (($this->types[$nrow] == 'c') ? 'colgroup' : 'tr'));
            $old_type = $this->types[$nrow];
        }
        $string .= ($old_type == 'c') ? $part_string : $this->wrap($part_string, $parts[$old_type]);

        return $this->wrap($string, 'table', ' class="style_table" cellspacing="1" border="0" align="center"');
    }
}

// , title1 , title2 , title3
// , cell1  , cell2  , cell3
// , cell4  , cell5  , cell6
class YTableEx extends ElementEx
{
    public $col;

    public function __construct($_value)
    {
        parent::__construct();

        $align = $value = $matches = array();
        foreach ($_value as $val) {
            if (preg_match('/^(\s+)?(.+?)(\s+)?$/', $val, $matches)) {
                $align[] =($matches[1] != '') ?
                    (
                        (isset($matches[3]) && $matches[3] != '') ?
                        ' align="center"' :
                        ' align="right"'
                    ) : '';
                $value[] = $matches[2];
            } else {
                $align[] = '';
                $value[] = $val;
            }
        }
        $this->col = count($value);
        $colspan = array();
        foreach ($value as $val) {
            $colspan[] = ($val == '==') ? 0 : 1;
        }
        $str = '';
        $count = count($value);
        for ($i = 0; $i < $count; $i++) {
            if ($colspan[$i]) {
                while ($i + $colspan[$i] < $count && $value[$i + $colspan[$i]] == '==') {
                    $colspan[$i]++;
                }
                $colspan[$i] = ($colspan[$i] > 1) ? ' colspan="' . $colspan[$i] . '"' : '';
                $str .= '<td class="style_td"' . $align[$i] . $colspan[$i] . '>' . guiedit_make_link($value[$i]) . '</td>';
            }
        }
        $this->elements[] = $str;
    }

    public function YTableEx($_value)
    {
        $this->__construct($_value);
    }

    public function canContain(& $obj)
    {
        return is_a($obj, 'YTableEx') && ($obj->col == $this->col);
    }

    public function & insert(& $obj)
    {
        $this->elements[] = $obj->elements[0];
        return $this;
    }

    public function toString()
    {
        $rows = '';
        foreach ($this->elements as $str) {
            $rows .= "\n" . '<tr class="style_tr">' . $str . '</tr>' . "\n";
        }
        $rows = $this->wrap($rows, 'table', ' class="style_table" cellspacing="1" border="0"');
        return $this->wrap($rows, 'div', ' class="ie5"');
    }
}

// ' 'Space-beginning sentence
// ' 'Space-beginning sentence
// ' 'Space-beginning sentence
class PreEx extends ElementEx
{
    public function __construct(& $root, $text)
    {
        $text  = str_replace(array('&amp;', '&gt;', '&lt;'), array('&', '>', '<'), $text);

        global $preformat_ltrim;
        parent::__construct();
        $this->elements[] = htmlsc(htmlsc(
            (! $preformat_ltrim || $text == '' || $text[0] != ' ') ? $text : substr($text, 1)
        ));
    }

    public function PreEx(& $root, $text)
    {
        $this->__construct($root, $text);
    }

    public function canContain(& $obj)
    {
        return is_a($obj, 'PreEx');
    }

    public function & insert(& $obj)
    {
        $this->elements[] = $obj->elements[0];
        return $this;
    }

    public function toString()
    {
        return $this->wrap(join("<br />", $this->elements), 'pre');
    }
}

// Block plugin: #something (started with '#')
class DivEx extends ElementEx
{
    public $text;
    public $name;
    public $param;

    public function __construct($out)
    {
        parent::__construct();
        list(, $this->name, $this->param, $this->text) = array_pad($out, 4, '');
    }

    public function DivEx($out)
    {
        $this->__construct($out);
    }

    public function canContain(& $obj)
    {
        return false;
    }

    public function toString()
    {
        switch ($this->name) {
            case 'br':
                return "<br />\n&nbsp;";
            case 'button':
                return '<div _pkwkargs="' . str_replace(',', '', $this->param) . '" _plugin="button" class="embededplugin"><button type="button" style="text-indent:0px;line-height:1em;vertical-align:middle">' . str_replace(',', '', $this->param) . '</button></div>';
            case 'hr':
                return '<hr class="short_line" />';
            case 'pagebreak':
                return '<span style="page-break-after: always;"><span style="display: none;">&nbsp;</span></span>';
            case 'ref':
                $param = ($this->param != '') ? explode(',', $this->param) : array();
                return guiedit_convert_ref($param, true);
            case 'submit':
                return '<div _pkwkargs="' . str_replace(',', '', $this->param) . '" _plugin="submit" class="embededplugin"><button type="submit" style="text-indent:0px;line-height:1em;vertical-align:middle">' . str_replace(',', '', $this->param) . '</button></div>';
        }
        
        if ($this->text) {
            $this->text = preg_replace("/\r/", "<br />", $this->text);
        }
        
        $inner = "#$this->name" . ($this->param ? "($this->param)" : '') . $this->text;
        $style = (UA_NAME == "MSIE") ? '' : ' style="cursor:default"';
        return "<p>".$this->wrap($inner, 'span', ' class="plugin"' . $style)."</p>";
    }
}
// LEFT:/CENTER:/RIGHT:
class AlignEx extends ElementEx
{
    public $align;

    public function __construct($align)
    {
        parent::__construct();
        $this->align = $align;
    }

    public function AlignEx($align)
    {
        $this->__construct($align);
    }

    public function canContain(& $obj)
    {
        return is_a($obj, 'InlineEx');
    }

    public function toString()
    {
        return $this->wrap(parent::toString(), 'div', ' style="text-align: ' . $this->align . '"');
    }
}

// BodyEx
class BodyEx extends ElementEx
{
    public $classes = array(
        '-' => 'UListEx',
        '+' => 'OListEx',
        '>' => 'BQuoteEx',
        '<' => 'BQuoteEx',
        '&gt;' => 'BQuoteEx',
        '&lt;' => 'BQuoteEx',
    );
    public $factories = array(
        ':' => 'DListEx',
        '|' => 'TableEx',
        ',' => 'YTableEx',
        '#' => 'DivEx',
    );
    
    public $comments = array();

    public function __construct()
    {
        parent::__construct();
    }

    public function BodyEx()
    {
        $this->__construct();
    }

    public function parse(& $lines)
    {
        $this->last = & $this;
        $matches = array();

        while (! empty($lines)) {
            $line = array_shift($lines);

            // Escape comments
            if (substr($line, 0, 2) == '//') {
                $this->comments[] = substr($line, 2);
                $line = '___COMMENT___';
            }

            if (preg_match('/^(LEFT|CENTER|RIGHT):(.*)$/', $line, $matches)) {
                // <div style="text-align:...">
                $this->last = & $this->last->add(new AlignEx(strtolower($matches[1])));
                if ($matches[2] == '') {
                    continue;
                }
                $line = $matches[2];
            }

            $line = rtrim($line, "\r\n");

            // Empty
            if ($line == '') {
                $this->last = & $this;
                continue;
            }

            // Horizontal Rule
            if (substr($line, 0, 4) == '----') {
                $this->insert(new HRuleEx($this, $line));
                continue;
            }

            // Multiline-enabled block plugin
            if (!PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK &&
                preg_match('/^#[^{]+(\{\{+)\s*$/', $line, $matches)) {
                $len = strlen($matches[1]);
                $line .= "\r"; // Delimiter
                while (! empty($lines)) {
                    $next_line = preg_replace("/[\r\n]*$/", '', array_shift($lines));
                    // UPK
                    
                    $next_line = htmlsc($next_line);
                    if (preg_match('/\}{' . $len . '}/', $next_line)) {
                        $line .= $next_line;
                        break;
                    } else {
                        $line .= $next_line .= "\r"; // Delimiter
                    }
                }
            }

            // The first character
            $head = $line[0];

            $line = preg_replace('/<(.*?)>/u', '&lt;$1&gt;', $line);
            $line = str_replace(array('&lt;', '&gt;'), array('&amp;lt;', '&amp;gt;'), $line);

            
            // HeadingEx
            if ($head == '*') {
                $this->insert(new HeadingEx($this, $line));
                continue;
            }

            // PreEx
            if ($head == ' ' || $head == "\t") {
                $this->last = & $this->last->add(new PreEx($this, $line));
                continue;
            }

            // Line Break
            if (substr($line, -1) == '~') {
                $line = substr($line, 0, -1) . "\r";
            }

            // Other Character
            if (isset($this->classes[$head])) {
                $classname  = $this->classes[$head];
                $this->last = & $this->last->add(new $classname($this, $line));
                continue;
            }

            // Other Character
            if (isset($this->factories[$head])) {
                $factoryname = 'Factory_' . $this->factories[$head];
                $this->last  = & $this->last->add($factoryname($this, $line));
                continue;
            }

            // Default
            $this->last = & $this->last->add(Factory_InlineEx($line));
        }
    }

    public function & insert(& $obj)
    {
        if (is_a($obj, 'InlineEx')) {
            $obj = & $obj->toPara();
        }
        return parent::insert($obj);
    }

    public function toString()
    {
        global $vars;

        $text = parent::toString();
        
        $text = preg_replace_callback("/___COMMENT___(\n___COMMENT___)*/", array(&$this, 'comment'), $text);

        return $text . "\n";
    }
    
    public function comment($matches)
    {
        $comments = explode("\n", $matches[0]);
        foreach ($comments as $key=>$comment) {
            $comments[$key] = array_shift($this->comments);
        }
        $result = '';
        foreach ($comments as $comment) {
            $result .= '<span class="comment" style="color:#355e3b"><img alt="Comment" src="./easyedit/plugins/comment/icons/comment.png" />'.htmlsc($comment).'</span>' . "<br />";
        }
        return $result;
    }
}
