View file super2/bbcode/bbcode.lib.php

File size: 70.29Kb
<?php

/******************************************************************************
 *                                                                            *
 *   bbcode.lib.php, v 0.24 2007/03/06 - This is part of xBB library          *
 *   Copyright (C) 2006-2007  Dmitriy Skorobogatov  dima@pc.uz                *
 *                                                                            *
 *   This program is free 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA *
 *                                                                            *
 ******************************************************************************/

class bbcode {
	/* Описания свойств и методов смотрите в документации. */
    var $tag = '';
    var $attrib = array();
    var $text = '';
    var $syntax = array();
    var $tree = array();
    var $tags = array(
//        '*'       => 'bb_li',
        'a'       => 'bb_a',
//        'abbr'    => 'bb_abbr',
//        'acronym' => 'bb_acronym',
//        'address' => 'bb_address',
//        'align'   => 'bb_align',
//        'anchor'  => 'bb_a',
        'b'       => 'bb_strong',
//        'bbcode'  => 'bb_bbcode',
//        'caption' => 'bb_caption',
//        'center'  => 'bb_align',
//        'code'    => 'bb_code',
//        'color'   => 'bb_color',
        'email'   => 'bb_email',
//        'font'    => 'bb_font',
//        'google'  => 'bb_google',
//        'h1'      => 'bb_h1',
//        'h2'      => 'bb_h2',
//        'h3'      => 'bb_h3',
//        'hr'      => 'bb_hr',
        'i'       => 'bb_i',
//        'img'     => 'bb_img',
//        'justify' => 'bb_align',
//        'left'    => 'bb_align',
//        'list'    => 'bb_list',
//        'nobb'    => 'bb_nobb',
//        'php'     => 'bb_php',
//        'quote'   => 'bb_quote',
//        'right'   => 'bb_align',
//        's'       => 'bb_del',
//        'size'    => 'bb_size',
//        'strike'  => 'bb_del',
//        'sub'     => 'bb_sub',
//        'sup'     => 'bb_sup',
//        'table'   => 'bb_table',
//        'td'      => 'bb_td',
//        'th'      => 'bb_th',
//        'tr'      => 'bb_tr',
//        'tt'      => 'bb_tt',
        'u'       => 'bb_u',
        'url'     => 'bb_a'
    );
    var $children = array(
        'a','abbr','acronym','address','align','anchor','b','bbcode','center',
        'code','color','email','font','google','h1','h2','h3','hr','i','img',
        'justify','left','list','nobb','php','quote','right','s','size',
        'strike','sub','sup','table','tt','u','url'
    );
    var $mnemonics = array();
    var $autolinks = false;
    var $is_close = false;
    var $lbr = 0;
    var $rbr = 0;

    function bbcode($code = '') {
        if (is_array($code)) {
            $is_tree = false;
            foreach ($code as $key => $val) {
                if (isset($val['val'])) {
                	$this -> tree = $code;
                	$this -> syntax = $this -> get_syntax();
                	$is_tree = true;
                	break;
                }
            }
            if (! $is_tree) {
                $this -> syntax = $code;
                $this -> get_tree();
            }
            $this -> text = '';
            foreach ($this -> syntax as $val) {
                $this -> text .= $val['str'];
            }
        } elseif ($code) {
        	$this -> text = $code;
        	$this -> parse();
        }
    }

    function get_tokens() {
        $length = strlen($this -> text);
        $tokens = array();
        $token_key = -1;
        $type_of_char = null;
        for ($i=0; $i < $length; ++$i) {
            $previous_type = $type_of_char;
            switch ($this -> text{$i}) {
                case '[':
                    $type_of_char = 0;
                    break;
                case ']':
                    $type_of_char = 1;
                    break;
                case '"':
                    $type_of_char = 2;
                    break;
                case "'":
                    $type_of_char = 3;
                    break;
                case "=":
                    $type_of_char = 4;
                    break;
                case '/':
                    $type_of_char = 5;
                    break;
                case ' ':
                    $type_of_char = 6;
                    break;
                case "\t":
                    $type_of_char = 6;
                    break;
                case "\n":
                    $type_of_char = 6;
                    break;
                case "\r":
                    $type_of_char = 6;
                    break;
                case "\0":
                    $type_of_char = 6;
                    break;
                case "\x0B":
                    $type_of_char = 6;
                    break;
                default:
                    $type_of_char = 7;
            }
            if (7 == $previous_type && $type_of_char != $previous_type) {
                $word = strtolower($tokens[$token_key][1]);
                if (isset($this -> tags[$word])) {
                    $tokens[$token_key][0] = 8;
                }
            }
            switch ($type_of_char) {
                case 6:
                    if (6 == $previous_type) {
                        $tokens[$token_key][1] .= $this -> text{$i};
                    } else {
                    	$tokens[++$token_key] = array(6, $this -> text{$i});
                    }
                    break;
                case 7:
                    if (7 == $previous_type) {
                        $tokens[$token_key][1] .= $this -> text{$i};
                    } else {
                    	$tokens[++$token_key] = array(7, $this -> text{$i});
                    }
                    break;
                default:
                    $tokens[++$token_key] = array(
                        $type_of_char, $this -> text{$i}
                    );
            }
        }
        return $tokens;
    }

    function parse($code = '') {
        if ($code) {
            $this -> bbcode($code);
            return;
        }
        /*
        Используем метод конечных автоматов
        Список возможных состояний автомата:
        0  - Начало сканирования или находимся вне тега. Ожидаем что угодно.
        1  - Встретили символ "[", который считаем началом тега. Ожидаем имя
             тега, или символ "/".
        2  - Нашли в теге неожидавшийся символ "[". Считаем предыдущую строку
             ошибкой. Ожидаем имя тега, или символ "/".
        3  - Нашли в теге синтаксическую ошибку. Текущий символ не является "[".
             Ожидаем что угодно.
        4  - Сразу после "[" нашли символ "/". Предполагаем, что попали в
             закрывающий тег. Ожидаем имя тега или символ "]".
        5  - Сразу после "[" нашли имя тега. Считаем, что находимся в
             открывающем теге. Ожидаем пробел или "=" или "/" или "]".
        6  - Нашли завершение тега "]". Ожидаем что угодно.
        7  - Сразу после "[/" нашли имя тега. Ожидаем "]".
        8  - В открывающем теге нашли "=". Ожидаем пробел или значение атрибута.
        9  - В открывающем теге нашли "/", означающий закрытие тега. Ожидаем
             "]".
        10 - В открывающем теге нашли пробел после имени тега или имени
             атрибута. Ожидаем "=" или имя другого атрибута или "/" или "]".
        11 - Нашли '"' начинающую значение атрибута, ограниченное кавычками.
             Ожидаем что угодно.
        12 - Нашли "'" начинающий значение атрибута, ограниченное апострофами.
             Ожидаем что угодно.
        13 - Нашли начало незакавыченного значения атрибута. Ожидаем что угодно.
        14 - В открывающем теге после "=" нашли пробел. Ожидаем значение
             атрибута.
        15 - Нашли имя атрибута. Ожидаем пробел или "=" или "/" или "]".
        16 - Находимся внутри значения атрибута, ограниченного кавычками.
             Ожидаем что угодно.
        17 - Завершение значения атрибута. Ожидаем пробел или имя следующего
             атрибута или "/" или "]".
        18 - Находимся внутри значения атрибута, ограниченного апострофами.
             Ожидаем что угодно.
        19 - Находимся внутри незакавыченного значения атрибута. Ожидаем что
             угодно.
        20 - Нашли пробел после значения атрибута. Ожидаем имя следующего
             атрибута или "/" или "]".

        Описание конечного автомата:
        */
        $finite_automaton = array(
               // Предыдущие |   Состояния для текущих событий (лексем)   |
               //  состояния |  0 |  1 |  2 |  3 |  4 |  5 |  6 |  7 |  8 |
                   0 => array(  1 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 )
                ,  1 => array(  2 ,  3 ,  3 ,  3 ,  3 ,  4 ,  3 ,  3 ,  5 )
                ,  2 => array(  2 ,  3 ,  3 ,  3 ,  3 ,  4 ,  3 ,  3 ,  5 )
                ,  3 => array(  1 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 )
                ,  4 => array(  2 ,  6 ,  3 ,  3 ,  3 ,  3 ,  3 ,  3 ,  7 )
                ,  5 => array(  2 ,  6 ,  3 ,  3 ,  8 ,  9 , 10 ,  3 ,  3 )
                ,  6 => array(  1 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 ,  0 )
                ,  7 => array(  2 ,  6 ,  3 ,  3 ,  3 ,  3 ,  3 ,  3 ,  3 )
                ,  8 => array( 13 , 13 , 11 , 12 , 13 , 13 , 14 , 13 , 13 )
                ,  9 => array(  2 ,  6 ,  3 ,  3 ,  3 ,  3 ,  3 ,  3 ,  3 )
                , 10 => array(  2 ,  6 ,  3 ,  3 ,  8 ,  9 ,  3 , 15 , 15 )
                , 11 => array( 16 , 16 , 17 , 16 , 16 , 16 , 16 , 16 , 16 )
                , 12 => array( 18 , 18 , 18 , 17 , 18 , 18 , 18 , 18 , 18 )
                , 13 => array( 19 ,  6 , 19 , 19 , 19 , 19 , 17 , 19 , 19 )
                , 14 => array(  2 ,  3 , 11 , 12 , 13 , 13 ,  3 , 13 , 13 )
                , 15 => array(  2 ,  6 ,  3 ,  3 ,  8 ,  9 , 10 ,  3 ,  3 )
                , 16 => array( 16 , 16 , 17 , 16 , 16 , 16 , 16 , 16 , 16 )
                , 17 => array(  2 ,  6 ,  3 ,  3 ,  3 ,  9 , 20 , 15 , 15 )
                , 18 => array( 18 , 18 , 18 , 17 , 18 , 18 , 18 , 18 , 18 )
                , 19 => array( 19 ,  6 , 19 , 19 , 19 , 19 , 20 , 19 , 19 )
                , 20 => array(  2 ,  6 ,  3 ,  3 ,  3 ,  9 ,  3 , 15 , 15 )
            );
        // Закончили описание конечного автомата
        $mode = 0;
        $result = array();
        $tag_decomposition = array();
        $token_key = -1;
        $value = '';
        // Сканируем массив лексем с помощью построенного автомата:
        foreach ($this -> get_tokens() as $token) {
            $previous_mode = $mode;
            $mode = $finite_automaton[$previous_mode][$token[0]];
            switch ($mode) {
                case 0:
                    if (-1 < $token_key && 'text'==$result[$token_key]['type']) {
                        $result[$token_key]['str'] .= $token[1];
                    } else {
                        $result[++$token_key] = array(
                                'type' => 'text',
                                'str' => $token[1]
                            );
                    }
                    break;
                case 1:
                    $tag_decomposition['name']     = '';
                    $tag_decomposition['type']     = '';
                    $tag_decomposition['str']      = '[';
                    $tag_decomposition['layout'][] = array( 0, '[' );
                    break;
                case 2:
                    if (-1<$token_key && 'text'==$result[$token_key]['type']) {
                        $result[$token_key]['str'] .= $tag_decomposition['str'];
                    } else {
                        $result[++$token_key] = array(
                                'type' => 'text',
                                'str' => $tag_decomposition['str']
                            );
                    }
                    $tag_decomposition = array();
                    $tag_decomposition['name']     = '';
                    $tag_decomposition['type']     = '';
                    $tag_decomposition['str']      = '[';
                    $tag_decomposition['layout'][] = array( 0, '[' );
                    break;
                case 3:
                    if (-1<$token_key && 'text'==$result[$token_key]['type']) {
                        $result[$token_key]['str'] .= $tag_decomposition['str'];
                        $result[$token_key]['str'] .= $token[1];
                    } else {
                        $result[++$token_key] = array(
                                'type' => 'text',
                                'str' => $tag_decomposition['str'].$token[1]
                            );
                    }
                    $tag_decomposition = array();
                    break;
                case 4:
                    $tag_decomposition['type'] = 'close';
                    $tag_decomposition['str'] .= '/';
                    $tag_decomposition['layout'][] = array( 1, '/' );
                    break;
                case 5:
                    $tag_decomposition['type'] = 'open';
                    $name = strtolower($token[1]);
                    $tag_decomposition['name'] = $name;
                    $tag_decomposition['str'] .= $token[1];
                    $tag_decomposition['layout'][] = array( 2, $token[1] );
                    $tag_decomposition['attrib'][$name] = '';
                    break;
                case 6:
                    if (! isset($tag_decomposition['name'])) {
                        $tag_decomposition['name'] = '';
                    }
                    if (13 == $previous_mode || 19 == $previous_mode) {
                        $tag_decomposition['layout'][] = array( 7, $value );
                    }
                    $tag_decomposition['str'] .= ']';
                    $tag_decomposition['layout'][] = array( 0, ']' );
                    $result[++$token_key] = $tag_decomposition;
                    $tag_decomposition = array();
                    break;
                case 7:
                    $tag_decomposition['name'] = strtolower($token[1]);
                    $tag_decomposition['str'] .= $token[1];
                    $tag_decomposition['layout'][] = array( 2, $token[1] );
                    break;
                case 8:
                    $tag_decomposition['str'] .= '=';
                    $tag_decomposition['layout'][] = array( 3, '=' );
                    break;
                case 9:
                    $tag_decomposition['type'] = 'open/close';
                    $tag_decomposition['str'] .= '/';
                    $tag_decomposition['layout'][] = array( 1, '/' );
                    break;
                case 10:
                    $tag_decomposition['str'] .= $token[1];
                    $tag_decomposition['layout'][] = array( 4, $token[1] );
                    break;
                case 11:
                    $tag_decomposition['str'] .= '"';
                    $tag_decomposition['layout'][] = array( 5, '"' );
                    $value = '';
                    break;
                case 12:
                    $tag_decomposition['str'] .= "'";
                    $tag_decomposition['layout'][] = array( 5, "'" );
                    $value = '';
                    break;
                case 13:
                    $tag_decomposition['attrib'][$name] = $token[1];
                    $value = $token[1];
                    $tag_decomposition['str'] .= $token[1];
                    break;
                case 14:
                    $tag_decomposition['str'] .= $token[1];
                    $tag_decomposition['layout'][] = array( 4, $token[1] );
                    break;
                case 15:
                    $name = strtolower($token[1]);
                    $tag_decomposition['str'] .= $token[1];
                    $tag_decomposition['layout'][] = array( 6, $token[1] );
                    $tag_decomposition['attrib'][$name] = '';
                    break;
                case 16:
                    $tag_decomposition['str'] .= $token[1];
                    $tag_decomposition['attrib'][$name] .= $token[1];
                    $value .= $token[1];
                    break;
                case 17:
                    $tag_decomposition['str'] .= $token[1];
                    $tag_decomposition['layout'][] = array( 7, $value );
                    $value = '';
                    $tag_decomposition['layout'][] = array( 5, $token[1] );
                    break;
                case 18:
                    $tag_decomposition['str'] .= $token[1];
                    $tag_decomposition['attrib'][$name] .= $token[1];
                    $value .= $token[1];
                    break;
                case 19:
                    $tag_decomposition['str'] .= $token[1];
                    $tag_decomposition['attrib'][$name] .= $token[1];
                    $value .= $token[1];
                    break;
                case 20:
                    $tag_decomposition['str'] .= $token[1];
                    if ( 13 == $previous_mode || 19 == $previous_mode ) {
                        $tag_decomposition['layout'][] = array( 7, $value );
                    }
                    $value = '';
                    $tag_decomposition['layout'][] = array( 4, $token[1] );
                    break;
            }
        }
        if (count($tag_decomposition)) {
            if ( -1 < $token_key && 'text' == $result[$token_key]['type'] ) {
                $result[$token_key]['str'] .= $tag_decomposition['str'];
            } else {
                $result[++$token_key] = array(
                        'type' => 'text',
                        'str' => $tag_decomposition['str']
                    );
            }
        }
        $this -> syntax = $result;
        $this -> get_tree();
        return $result;
    }

    function specialchars($string) {
        $chars = array(
            '[' => '@l;',
            ']' => '@r;',
            '"' => '@q;',
            "'" => '@a;',
            '@' => '@at;'
        );
        return strtr($string, $chars);
    }

    function unspecialchars($string) {
        $chars = array(
            '@l;'  => '[',
            '@r;'  => ']',
            '@q;'  => '"',
            '@a;'  => "'",
            '@at;' => '@'
        );
        return strtr($string, $chars);
    }

    function must_close_tag($current, $next) {
        $class_vars = get_class_vars($this -> tags[$current]);
        $must_close = in_array($next, $class_vars['ends']);
        $class_vars = get_class_vars($this -> tags[$next]);
        if (! $must_close && isset($class_vars['stop'])) {
            $must_close = in_array($current, $class_vars['stop']);
        }
        return $must_close;
    }

    function normalize_bracket($syntax) {
        $structure = array();
        $structure_key = -1;
        $level = 0;
        $open_tags = array();
        foreach ($syntax as $syntax_key => $val) {
            unset($val['layout']);
            switch ($val['type']) {
                case 'text':
                    $val['str'] = $this -> unspecialchars($val['str']);
                    $type = (-1 < $structure_key)
                        ? $structure[$structure_key]['type'] : false;
                    if ('text' == $type) {
                        $structure[$structure_key]['str'] .= $val['str'];
                    } else {
                        $structure[++$structure_key] = $val;
                        $structure[$structure_key]['level'] = $level;
                    }
                    break;
                case 'open/close':
                    $val['attrib'] = array_map(
            	        array(&$this, 'unspecialchars'), $val['attrib']
            	    );
                    foreach (array_reverse($open_tags,true) as $ult_key => $ultimate) {
                        if ($this -> must_close_tag($ultimate, $val['name'])) {
                            $structure[++$structure_key] = array(
                                    'type'  => 'close',
                                    'name'  => $ultimate,
                                    'str'   => '',
                                    'level' => --$level
                                );
                            unset($open_tags[$ult_key]);
                        } else {
                        	break;
                        }
                    }
                    $structure[++$structure_key] = $val;
                    $structure[$structure_key]['level'] = $level;
                    break;
                case 'open':
                    $val['attrib'] = array_map(
            	        array(&$this, 'unspecialchars'), $val['attrib']
            	    );
                    foreach (array_reverse($open_tags,true) as $ult_key => $ultimate) {
                        if ($this -> must_close_tag($ultimate, $val['name'])) {
                            $structure[++$structure_key] = array(
                                    'type'  => 'close',
                                    'name'  => $ultimate,
                                    'str'   => '',
                                    'level' => --$level
                                );
                            unset($open_tags[$ult_key]);
                        } else { break; }
                    }
                    $class_vars = get_class_vars($this -> tags[$val['name']]);
                    if ($class_vars['is_close']) {
                        $val['type'] = 'open/close';
                        $structure[++$structure_key] = $val;
                        $structure[$structure_key]['level'] = $level;
                    } else {
                        $structure[++$structure_key] = $val;
                        $structure[$structure_key]['level'] = $level++;
                        $open_tags[] = $val['name'];
                    }
                    break;
                case 'close':
                    if (! count($open_tags)) {
                        $type = (-1 < $structure_key)
                            ? $structure[$structure_key]['type'] : false;
                        if ( 'text' == $type ) {
                            $structure[$structure_key]['str'] .= $val['str'];
                        } else {
                            $structure[++$structure_key] = array(
                                    'type'  => 'text',
                                    'str'   => $val['str'],
                                    'level' => 0
                                );
                        }
                        break;
                    }
                    if (! $val['name']) {
                        end($open_tags);
                        list($ult_key, $ultimate) = each($open_tags);
                        $val['name'] = $ultimate;
                        $structure[++$structure_key] = $val;
                        $structure[$structure_key]['level'] = --$level;
                        unset($open_tags[$ult_key]);
                        break;
                    }
                    if (! in_array($val['name'],$open_tags)) {
                        $type = (-1 < $structure_key)
                            ? $structure[$structure_key]['type'] : false;
                        if ('text' == $type) {
                            $structure[$structure_key]['str'] .= $val['str'];
                        } else {
                            $structure[++$structure_key] = array(
                                    'type'  => 'text',
                                    'str'   => $val['str'],
                                    'level' => $level
                                );
                        }
                        break;
                    }
                    foreach (array_reverse($open_tags,true) as $ult_key => $ultimate) {
                        if ($ultimate != $val['name']) {
                            $structure[++$structure_key] = array(
                                    'type'  => 'close',
                                    'name'  => $ultimate,
                                    'str'   => '',
                                    'level' => --$level
                                );
                            unset($open_tags[$ult_key]);
                        } else {
                        	break;
                        }
                    }
                    $structure[++$structure_key] = $val;
                    $structure[$structure_key]['level'] = --$level;
                    unset($open_tags[$ult_key]);
            }
        }
        foreach (array_reverse($open_tags,true) as $ult_key => $ultimate) {
            $structure[++$structure_key] = array(
                    'type'  => 'close',
                    'name'  => $ultimate,
                    'str'   => '',
                    'level' => --$level
                );
            unset($open_tags[$ult_key]);
        }
        return $structure;
    }

    function get_tree() {
        /* Превращаем $this -> syntax в правильную скобочную структуру */
        $structure = $this -> normalize_bracket($this -> syntax);
        /* Отслеживаем, имеют ли элементы неразрешенные подэлементы.
           Соответственно этому исправляем $structure. */
        $normalized = array();
        $normal_key = -1;
        $level = 0;
        $open_tags = array();
        $not_tags = array();
        foreach ($structure as $structure_key => $val) {
            switch ($val['type']) {
                case 'text':
                    $type = (-1 < $normal_key)
                        ? $normalized[$normal_key]['type'] : false;
                    if ('text' == $type) {
                        $normalized[$normal_key]['str'] .= $val['str'];
                    } else {
                        $normalized[++$normal_key] = $val;
                        $normalized[$normal_key]['level'] = $level;
                    }
                    break;
                case 'open/close':
                    $is_open = count($open_tags);
                    end($open_tags);
                    $info = get_class_vars($this -> tags[$val['name']]);
                    if ($is_open) {
                        $class_vars = get_class_vars(
                            $this -> tags[current($open_tags)]
                        );
                        $children = $class_vars['children'];
                    } else {
                        $children = array();
                    }
                    if (isset($info['top_level'])) {
                        $top_level = $info['top_level'];
                    } else {
                        $top_level = in_array($val['name'], $this -> children);
                    }
                    $is_child = in_array($val['name'], $children);
                    if (isset($info['parent']) && ! $is_child) {
                        if (in_array(current($open_tags), $info['parent'])) {
                            $is_child = true;
                        }
                    }
                    if (! $level && ! $top_level || $is_open && ! $is_child) {
                        $type = (-1 < $normal_key)
                            ? $normalized[$normal_key]['type'] : false;
                        if ( 'text' == $type ) {
                            $normalized[$normal_key]['str'] .= $val['str'];
                        } else {
                            $normalized[++$normal_key] = array(
                                    'type'  => 'text',
                                    'str'   => $val['str'],
                                    'level' => $level
                                );
                        }
                        break;
                    }
                    $normalized[++$normal_key] = $val;
                    $normalized[$normal_key]['level'] = $level;
                    break;
                case 'open':
                    $is_open = count($open_tags);
                    end($open_tags);
                    $info = get_class_vars($this -> tags[$val['name']]);
                    if ($is_open) {
                        $class_vars = get_class_vars(
                            $this -> tags[current($open_tags)]
                        );
                        $children = $class_vars['children'];
                    } else {
                        $children = array();
                    }
                    if (isset($info['top_level'])) {
                        $top_level = $info['top_level'];
                    } else {
                        $top_level = in_array($val['name'], $this -> children);
                    }
                    $is_child = in_array($val['name'], $children);
                    if (isset($info['parent']) && ! $is_child) {
                        if (in_array(current($open_tags), $info['parent'])) {
                            $is_child = true;
                        }
                    }
                    if (! $level && ! $top_level || $is_open && ! $is_child) {
                        $not_tags[$val['level']] = $val['name'];
                        $type = (-1 < $normal_key)
                            ? $normalized[$normal_key]['type'] : false;
                        if ( 'text' == $type ) {
                            $normalized[$normal_key]['str'] .= $val['str'];
                        } else {
                            $normalized[++$normal_key] = array(
                                    'type'  => 'text',
                                    'str'   => $val['str'],
                                    'level' => $level
                                );
                        }
                        break;
                    }
                    $normalized[++$normal_key] = $val;
                    $normalized[$normal_key]['level'] = $level++;
                    $ult_key = count($open_tags);
                    $open_tags[$ult_key] = $val['name'];
                    break;
                case 'close':
                    $not_normal = isset($not_tags[$val['level']])
                        && $not_tags[$val['level']] = $val['name'];
                    if ( $not_normal ) {
                        unset($not_tags[$val['level']]);
                        $type = (-1 < $normal_key)
                            ? $normalized[$normal_key]['type'] : false;
                        if ( 'text' == $type ) {
                            $normalized[$normal_key]['str'] .= $val['str'];
                        } else {
                            $normalized[++$normal_key] = array(
                                    'type'  => 'text',
                                    'str'   => $val['str'],
                                    'level' => $level
                                );
                        }
                        break;
                    }
                    $normalized[++$normal_key] = $val;
                    $normalized[$normal_key]['level'] = --$level;
                    $ult_key = count($open_tags) - 1;
                    unset($open_tags[$ult_key]);
                    break;
            }
        }
        unset($structure);
        // Формируем дерево элементов
        $result = array();
        $result_key = -1;
        $open_tags = array();
        $val_key = -1;
        foreach ($normalized as $normal_key => $val) {
            switch ($val['type']) {
                case 'text':
                    if (! $val['level']) {
                        $result[++$result_key] = array(
                                'type' => 'text',
                                'str' => $val['str']
                            );
                        break;
                    }
                    $open_tags[$val['level']-1]['val'][] = array(
                            'type' => 'text',
                            'str' => $val['str']
                        );
                    break;
                case 'open/close':
                    if (! $val['level']) {
                        $result[++$result_key] = array(
                                'type'   => 'item',
                                'name'   => $val['name'],
                                'attrib' => $val['attrib'],
                                'val'    => array()
                            );
                        break;
                    }
                    $open_tags[$val['level']-1]['val'][] = array(
                            'type'   => 'item',
                            'name'   => $val['name'],
                            'attrib' => $val['attrib'],
                            'val'    => array()
                        );
                    break;
                case 'open':
                    $open_tags[$val['level']] = array(
                            'type'   => 'item',
                            'name'   => $val['name'],
                            'attrib' => $val['attrib'],
                            'val'    => array()
                        );
                    break;
                case 'close':
                    if ( ! $val['level'] ) {
                        $result[++$result_key] = $open_tags[0];
                        unset($open_tags[0]);
                        break;
                    }
                    $open_tags[$val['level']-1]['val'][] = $open_tags[$val['level']];
                    unset($open_tags[$val['level']]);
                    break;
            }
        }
        $this -> tree = $result;
        return $result;
    }

    function get_syntax($tree = false) {
        if (! is_array($tree)) {
            $tree = $this -> tree;
        }
        $syntax = array();
        foreach ($tree as $elem) {
            if ('text' == $elem['type']) {
            	$syntax[] = array(
            	    'type' => 'text',
            	    'str' => $this -> specialchars($elem['str'])
            	);
            } else {
                $sub_elems = $this -> get_syntax($elem['val']);
                $str = '';
                $layout = array(array(0, '['));
                foreach ($elem['attrib'] as $name => $val) {
                    $val = $this -> specialchars($val);
                    if ($str) {
                    	$str .= ' ';
                    	$layout[] = array(4, ' ');
                    	$layout[] = array(6, $name);
                    } else {
                        $layout[] = array(2, $name);
                    }
                    $str .= $name;
                    if ($val) {
                    	$str .= '="'.$val.'"';
                    	$layout[] = array(3, '=');
                    	$layout[] = array(5, '"');
                    	$layout[] = array(7, $val);
                    	$layout[] = array(5, '"');
                    }
                }
                if (count($sub_elems)) {
                	$str = '['.$str.']';
                } else {
                    $str = '['.$str.' /]';
                    $layout[] = array(4, ' ');
                    $layout[] = array(1, '/');
                }
                $layout[] = array(0, ']');
                $syntax[] = array(
                    'type' => count($sub_elems) ? 'open' : 'open/close',
                    'str' => $str,
                    'name' => $elem['name'],
                    'attrib' => $elem['attrib'],
                    'layout' => $layout
                );
                foreach ($sub_elems as $sub_elem) { $syntax[] = $sub_elem; }
                if (count($sub_elems)) {
                	$syntax[] = array(
                	    'type' => 'close',
                	    'str' => '[/'.$elem['name'].']',
                	    'name' => $elem['name'],
                	    'layout' => array(
                	        array(0, '['),
                	        array(1, '/'),
                	        array(2, $elem['name']),
                	        array(0, ']')
                	    )
                	);
                }
            }
        }
        return $syntax;
    }

    function insert_smiles($text) {
        $text = nl2br(htmlspecialchars($text,ENT_NOQUOTES));
        $text = str_replace('  ', '&#160;&#160;', $text);
        if ($this -> autolinks) {
            $uri = "[\w\d-]+\.[\w\d-]+[^\s<\"\']*[^.,;\s<\"\'\)]+";
            $search = array(
                "'(.)((http|https|ftp)://".$uri.")'si",
                "'([^/])(www\.".$uri.")'si",
                "'([^\w\d-\.])([\w\d-\.]+@[\w\d-\.]+\.[\w]+[^.,;\s<\"\'\)]+)'si"
            );
            $replace = array(
//                '$1<a href="$2" target="_blank">$2</a>',
//                '$1<a href="http://$2" target="_blank">$2</a>',
                '$1<a href="$2">$2</a>',
                '$1<a href="http://$2">$2</a>',
                '$1<a href="mailto:$2">$2</a>'
            );
            $text = preg_replace($search, $replace, $text);
        }
        foreach ($this -> mnemonics as $mnemonic => $value) {
            $text = str_replace($mnemonic, $value, $text);
        }
        return $text;
    }

    function highlight() {
        $chars = array(
            '@l;'  => '<span class="bb_spec_char">@l;</span>',
            '@r;'  => '<span class="bb_spec_char">@r;</span>',
            '@q;'  => '<span class="bb_spec_char">@q;</span>',
            '@a;'  => '<span class="bb_spec_char">@a;</span>',
            '@at;' => '<span class="bb_spec_char">@at;</span>'
        );
        $uri = "[\w\d-]+\.[\w\d-]+[^\s<\"\']*[^.,;\s<\"\'\)]+";
        $search = array(
            "'(.)((http|https|ftp)://".$uri.")'si",
            "'([^/])(www\.".$uri.")'si",
            "'([^\w\d-\.])([\w\d-\.]+@[\w\d-\.]+\.[\w]+[^.,;\s<\"\'\)]+)'si"
        );
        $replace = array(
            '$1<span class="bb_autolink">$2</span>',
            '$1<span class="bb_autolink">$2</span>',
            '$1<span class="bb_autolink">$2</span>'
        );
        $str = '';
        foreach($this -> syntax as $elem) {
            if ('text' == $elem['type']) {
                $elem['str'] = strtr(htmlspecialchars($elem['str']), $chars);
                foreach ($this -> mnemonics as $mnemonic => $value) {
                    $elem['str'] = str_replace(
                        $mnemonic,
                        '<span class="bb_mnemonic">'.$mnemonic.'</span>',
                        $elem['str']
                    );
                }
                $elem['str'] = preg_replace($search, $replace, $elem['str']);
                $str .= $elem['str'];
            } else {
                $str .= '<span class="bb_tag">';
                foreach ($elem['layout'] as $val) {
                    switch ($val[0]) {
                        case 0:
                            $str .= '<span class="bb_bracket">'.$val[1]
                                .'</span>';
                            break;
                        case 1:
                            $str .= '<span class="bb_slash">/</span>';
                            break;
                        case 2:
                            $str .= '<span class="bb_tagname">'.$val[1]
                                .'</span>';
                            break;
                        case 3:
                            $str .= '<span class="bb_equal">=</span>';
                            break;
                        case 4:
                            $str .= $val[1];
                            break;
                        case 5:
                            if (! trim($val[1])) {
                            	$str .= $val[1];
                            } else {
                                $str .= '<span class="bb_quote">'.$val[1]
                                    .'</span>';
                            }
                            break;
                        case 6:
                            $str .= '<span class="bb_attrib_name">'
                                .htmlspecialchars($val[1]).'</span>';
                            break;
                        case 7:
                            if (! trim($val[1])) {
                            	$str .= $val[1];
                            } else {
                                $str .= '<span class="bb_attrib_val">'
                                    .strtr(htmlspecialchars($val[1]), $chars)
                                    .'</span>';
                            }
                            break;
                        default:
                            $str .= $val[1];
                    }
                }
                $str .= '</span>';
            }
        }
        $str = nl2br($str);
        $str = str_replace('  ', '&#160;&#160;', $str);
        return '<code class="bb_code">'.$str.'</code>';
    }

    function get_html($elems = false) {
        if (! is_array($elems)) {
            $elems = $this -> tree;
        }
        $result = '';
        $lbr = 0;
        $rbr = 0;
        foreach ($elems as $elem) {
            if ('text' == $elem['type']) {
                $elem['str'] = $this -> insert_smiles($elem['str']);
                for ($i=0; $i < $rbr; ++$i) {
                    $elem['str'] = ltrim($elem['str']);
                    if ('<br />' == substr($elem['str'], 0, 6)) {
                        $elem['str'] = substr_replace($elem['str'], '', 0, 6);
                    }
                }
                $result .= $elem['str'];
            } else {
                $class_vars = get_class_vars($this -> tags[$elem['name']]);
                $lbr = $class_vars['lbr'];
                $rbr = $class_vars['rbr'];
                for ($i=0; $i < $lbr; ++$i) {
                    $result = rtrim($result);
                    if ('<br />' == substr($result, -6)) {
                        $result = substr_replace($result, '', -6, 6);
                    }
                }
                $handler = $this -> tags[$elem['name']];
                if (class_exists($handler)) {
                    $tag = new $handler;
                    $tag -> tag = $elem['name'];
                    $tag -> attrib = $elem['attrib'];
                    $tag -> tags = $this -> tags;
                    $tag -> mnemonics = $this -> mnemonics;
                    $tag -> autolinks = $this -> autolinks;
                    $tag -> tree = $elem['val'];
                    $result .= $tag -> get_html();
                } else {
                    $result .= bbcode::get_html($elem['val']);
                }
            }
        }
        return $result;
    }
}

// Класс для тегов [a], [anchor] и [url]
class bb_a extends bbcode {
    var $ends = array(
        '*','align','center','h1','h2','h3','hr','justify','left','list','php',
        'quote','right','table','td','th','tr'
    );
    var $children = array(
        'abbr','acronym','b','bbcode','code','color','font','i','img','nobb',
        's','size','strike','sub','sup','tt','u'
    );
    function get_html() {
        $text = '';
        foreach ($this -> tree as $val) {
            if ('text' == $val['type']) { $text .= $val['str']; }
        }
        $href = '';
        if (isset($this -> attrib['url'])) {
            $href = $this -> attrib['url'];
        }
        if (! $href && isset($this -> attrib['a'])) {
            $href = $this -> attrib['a'];
        }
        if (! $href && isset($this -> attrib['href'])) {
            $href = $this -> attrib['href'];
        }
        if (! $href && ! isset($this -> attrib['anchor'])) { $href = $text; }
        $protocols = array(
            'http://',  'https://',  'ftp://',  'file://',  'mailto:',
            '#',        '/',         '?',       './',       '../'
        );
        $is_http = false;
        foreach ($protocols as $val) {
            if ($val == substr($href, 0, strlen($val))) {
                $is_http = true;
                break;
            }
        }
        if ($href && ! $is_http) { $href = 'http://'.$href; }
//        $attr = 'class="bb"';
        if ($href) {
        	$attr .= ' href="'.htmlspecialchars($href).'"';
        }
        if (isset($this -> attrib['title'])) {
            $title = $this -> attrib['title'];
            $attr .= ' title="'.htmlspecialchars($title).'"';
        }
        $id = '';
//        if (isset($this -> attrib['name'])) {
//            $id = $this -> attrib['name'];
//        }
//        if (isset($this -> attrib['id'])) {
//            $id = $this -> attrib['id'];
//        }
//        if (isset($this -> attrib['anchor'])) {
//            $id = $this -> attrib['anchor'];
//            if (! $id) { $id = $text; }
//        }
//        if ($id) {
//        	if ($id{0} < 'A' || $id{0} > 'z') { $id = 'bb'.$id; }
//        	$attr .= ' id="'.htmlspecialchars($id).'"';
//        }
//        if (isset($this -> attrib['target'])) {
//            $target = $this -> attrib['target'];
//            $attr .= ' target="'.htmlspecialchars($target).'"';
//        }
        return '<a '.$attr.'>'.parent::get_html($this -> tree).'</a>';
    }
}

// Класс для тега [abbr]
class bb_abbr extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        $attrib = 'class="bb"';
        if ($this -> attrib['abbr']) {
            $attrib .= ' title="'.htmlspecialchars($this -> attrib['abbr']).'"';
        }
        return '<abbr '.$attrib.'>'.parent::get_html($this -> tree).'</abbr>';
    }
}

// Класс для тега [acronym]
class bb_acronym extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        $attrib = 'class="bb"';
        if ($this -> attrib['acronym']) {
            $attrib .= ' title="'.htmlspecialchars($this -> attrib['acronym'])
                .'"';
        }
        return '<acronym '.$attrib.'>'.parent::get_html($this -> tree)
            .'</acronym>';
    }
}

// Класс для тега [address]
class bb_address extends bbcode {
    var $rbr = 1;
    var $ends = array('*','tr','td','th');
    function get_html() {
        return '<address class="bb">'.parent::get_html($this -> tree)
            .'</address>';
    }
}

// Класс для тегов [align], [center], [justify], [left] и [right]
class bb_align extends bbcode {
    var $rbr = 1;
    var $ends = array('*','tr','td','th');
    function get_html() {
        $align = '';
        if (isset($this -> attrib['justify'])) { $align = 'justify'; }
        if (isset($this -> attrib['left'])) { $align = 'left'; }
        if (isset($this -> attrib['right'])) { $align = 'right'; }
        if (isset($this -> attrib['center'])) { $align = 'center'; }
        if (! $align && isset($this -> attrib['align'])) {
            switch (strtolower($this -> attrib['align'])) {
                case 'left':
                    $align = 'left';
                    break;
                case 'right':
                    $align = 'right';
                    break;
                case 'center':
                    $align = 'center';
                    break;
                case 'justify':
                    $align = 'justify';
                    break;
            }
        }
        return '<div class="bb" align="'.$align.'">'
            .parent::get_html($this -> tree).'</div>';
    }
}

// Класс для тега [bbcode]
class bb_bbcode extends bbcode {
    var $ends = array();
    var $children = array();
    function get_html() {
        $str = '';
        foreach ($this -> tree as $item) {
            if ('item' == $item['type']) { continue; }
            $str .= $item['str'];
        }
        $bb = new bbcode();
        $bb -> tags = $this -> tags;
        $bb -> mnemonics = $this -> mnemonics;
        $bb -> autolinks = $this -> autolinks;
        $bb -> parse($str);
        return $bb -> highlight();
    }
}

// Класс для тега [caption]
class bb_caption extends bbcode {
    var $ends = array('tr');
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','center','code','email',
        'font','google','i','nobb','s','size','strike','sub','sup','tt','u',
        'url'
    );
    function get_html() {
        return '<caption class="bb">'.parent::get_html($this -> tree)
            .'</caption>';
    }
}

// Класс для тега [code]
class bb_code extends bbcode {
    var $ends = array();
    var $children = array();
    function get_html() {
        $str = '<code class="bb">';
        foreach ($this -> tree as $item) {
            if ('item' == $item['type']) { continue; }
            $str .= htmlspecialchars($item['str']);
        }
        $str .= '</code>';
        return str_replace('  ', '&#160;&#160;', nl2br($str));
    }
}

// Класс для тега [color]
class bb_color extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        $color = htmlspecialchars($this -> attrib['color']);
        return '<font color="'.$color.'">'.parent::get_html($this -> tree)
            .'</font>';
    }
}

// Класс для тегов [s] и [strike]
class bb_del extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        return '<del>'.parent::get_html($this -> tree).'</del>';
    }
}

// Класс для тега [email]
class bb_email extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'abbr','acronym','b','bbcode','code','color','email','font','i','img',
        'nobb','s','size','strike','sub','sup','tt','u'
    );
    function get_html() {
//        $attr = ' class="bb_email"';
        $href = $this -> attrib['email'];
        if (! $href) {
            foreach ($this -> tree as $text) {
                if ('text' == $text['type']) { $href .= $text['str']; }
            }
        }
        $protocols = array('mailto:');
        $is_http = false;
        foreach ($protocols as $val) {
            if ($val == substr($href,0,strlen($val))) {
                $is_http = true;
                break;
            }
        }
        if (! $is_http) { $href = 'mailto:'.$href; }
        if ($href) { $attr .= ' href="'.htmlspecialchars($href).'"'; }
        $title = isset($this -> attrib['title']) ? $this -> attrib['title'] : '';
        if ($title) { $attr .= ' title="'.htmlspecialchars($title).'"'; }
        $name = isset($this -> attrib['name']) ? $this -> attrib['name'] : '';
        if ($name) { $attr .= ' name="'.htmlspecialchars($name).'"'; }
        $target = isset($this -> attrib['target']) ? $this -> attrib['target'] : '';
        if ($target) { $attr .= ' target="'.htmlspecialchars($target).'"'; }
        return '<a'.$attr.'>'.parent::get_html($this -> tree).'</a>';
    }
}

// Класс для тега [font]
class bb_font extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','font','google','i','img','nobb','s','size','strike','sub','sup',
        'tt','u','url'
    );
    function get_html() {
        $face = $this -> attrib['font'];
        $attr = ' face="'.htmlspecialchars($face).'"';
        $color = isset($this -> attrib['color']) ? $this -> attrib['color'] : '';
        if ($color) { $attr .= ' color="'.htmlspecialchars($color).'"'; }
        $size = isset($this -> attrib['size']) ? $this -> attrib['size'] : '';
        if ($size) { $attr .= ' size="'.htmlspecialchars($size).'"'; }
        return '<font'.$attr.'>'.parent::get_html($this -> tree).'</font>';
    }
}

// Класс для тега [google]
class bb_google extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'abbr','acronym','b','bbcode','code','color','font','i','img','nobb',
        's','size','strike','sub','sup','tt','u'
    );
    function get_html() {
        $attr = htmlspecialchars(urlencode($this -> attrib['google']));
        $attr = ' href="http://www.google.com/search?q='.$attr.'"';
        $title = isset($this -> attrib['title']) ? $this -> attrib['title'] : '';
        if ($title) { $attr .= ' title="'.htmlspecialchars($title).'"'; }
        $name = isset($this -> attrib['name']) ? $this -> attrib['name'] : '';
        if ($name) { $attr .= ' name="'.htmlspecialchars($name).'"'; }
        $target = isset($this -> attrib['target']) ? $this -> attrib['target'] : '';
        if ($target) { $attr .= ' target="'.htmlspecialchars($target).'"'; }
        return '<a class="bb_google" '.$attr.'>'.parent::get_html($this -> tree).'</a>';
    }
}

// Класс для тега [h1]
class bb_h1 extends bbcode {
    var $lbr = 1;
    var $rbr = 2;
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        $attr = ' class="bb"';
        $align = isset($this -> attrib['align']) ? $this -> attrib['align'] : '';
        if ($align) { $attr .= ' align="'.htmlspecialchars($align).'"'; }
        return '<h1'.$attr.'>'.parent::get_html($this -> tree).'</h1>';
    }
}

// Класс для тега [h2]
class bb_h2 extends bbcode {
    var $lbr = 1;
    var $rbr = 2;
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','sub','strike','sup','tt',
        'u','url'
    );
    function get_html() {
        $attr = ' class="bb"';
        $align = isset($this -> attrib['align']) ? $this -> attrib['align'] : '';
        if ($align) { $attr .= ' align="'.htmlspecialchars($align).'"'; }
        return '<h2'.$attr.'>'.parent::get_html($this -> tree).'</h2>';
    }
}

// Класс для тега [h3]
class bb_h3 extends bbcode {
    var $lbr = 1;
    var $rbr = 2;
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        $attr = ' class="bb"';
        $align = isset($this -> attrib['align']) ? $this -> attrib['align'] : '';
        if ($align) { $attr .= ' align="'.htmlspecialchars($align).'"'; }
        return '<h3'.$attr.'>'.parent::get_html($this -> tree).'</h3>';
    }
}

// Класс для тега [hr]
class bb_hr extends bbcode {
    var $is_close = true;
    var $rbr = 1;
    var $ends = array();
    var $children = array();
    function get_html() {
        return '<hr class="bb" />';
    }
}

// Класс для тега [i]
class bb_i extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        return '<em>'.parent::get_html($this -> tree).'</em>';
    }
}

// Класс для тега [img]
class bb_img extends bbcode {
    var $ends = array();
    var $children = array();
    function get_html() {
        $attr = 'alt=""';
        if (isset($this -> attrib['width'])) {
            $width = (int) $this -> attrib['width'];
            $attr .= $width ? ' width="'.$width.'"' : '';
        }
        if (isset($this -> attrib['height'])) {
            $height = (int) $this -> attrib['height'];
            $attr .= $height ? ' height="'.$height.'"' : '';
        }
        if (isset($this -> attrib['border'])) {
            $border = (int) $this -> attrib['border'];
            $attr .= ' border="'.$border.'"';
        }
        $src = '';
        foreach ($this -> tree as $text) {
            if ('text' == $text['type']) { $src .= $text['str']; }
        }
        $src = htmlentities($src, ENT_QUOTES);
        $src = str_replace('.', '&#'.ord('.').';', $src);
        $src = str_replace(':', '&#'.ord(':').';', $src);
        $src = str_replace('(', '&#'.ord('(').';', $src);
        $src = str_replace(')', '&#'.ord(')').';', $src);
        return '<img src="'.$src.'" '.$attr.' />';
    }
}

// Класс для тега [*]
class bb_li extends bbcode {
    var $ends = array('*','tr','td','th');
    function get_html() {
        $attrib = 'class="bb"';
        if ('' !== $this -> attrib['*']) {
            $this -> attrib['*'] = (int) $this -> attrib['*'];
            $attrib .= ' value="'.$this -> attrib['*'].'"';
        }
        return '<li '.$attrib.'>'.parent::get_html($this -> tree).'</li>';
    }
}

// Класс для тега [list]
class bb_list extends bbcode {
    var $lbr = 0;
    var $rbr = 2;
    var $ends = array();
    var $children = array('*');
    function get_html() {
        $attr = ' class="bb"';
        $list_attr = strtolower($this -> attrib['list']);
        switch ($list_attr) {
            case '1':
                $tag_name = 'ol';
                $attr .= ' type="1"';
                break;
            case 'a':
                $tag_name = 'ol';
                $attr .= ' type="a"';
                break;
            default:
                $tag_name = 'ul';
        }
        $str = '<'.$tag_name.$attr.'>';
        foreach ($this -> tree as $key => $item) {
            if ('text' == $item['type']) { unset($this -> tree[$key]); }
        }
        $str .= parent::get_html($this -> tree).'</'.$tag_name.'>';
        return $str;
    }
}

// Класс для тега [nobb]
class bb_nobb extends bbcode {
    var $ends = array();
    var $children = array();
    function get_html() {
        $str = '';
        foreach ($this -> tree as $item) {
            if ('text' == $item['type']) {
                $str .= nl2br(htmlspecialchars($item['str']));
            }
        }
        $str = str_replace('  ', '&#160;&#160;', $str);
        return $str;
    }
}

// Класс для тега [php]
class bb_php extends bbcode {
    var $ends = array();
    var $children = array();
    function get_html() {
        $str = '';
        foreach ($this -> tree as $item) {
            if ('text' == $item['type']) { $str .= $item['str']; }
        }
        if (false !== strpos($str, '<?')) {
            $str = highlight_string($str, true);
        } else {
            $str = '<?php '.$str.' ?>';
            $str = highlight_string($str, true);
            $str = str_replace('&lt;?php ', '', $str);
            $str = str_replace('&lt;?php&#160;', '', $str);
            $str = str_replace('>?&gt;<', '><', $str);
        }
        $str = preg_replace("'<span [^>]*></span>'si", '', $str);
        return '<div class="php">'.$str.'</div>';
    }
}

// Класс для тега [quote]
class bb_quote extends bbcode {
    var $rbr = 1;
    var $ends = array();
    function get_html() {
        if ($author = htmlspecialchars($this -> attrib['quote'])) {
            $author = '<div class="bb_quote_author">'.$author.'</div>';
        }
        return $author.'<div class="bb_quote">'.parent::get_html($this -> tree)
            .'</div>';
    }
}

// Класс для тега [size]
class bb_size extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        $sign = '';
        if (strlen($this -> attrib['size'])) {
            $sign = $this -> attrib['size']{0};
        }
        if ('+' != $sign) { $sign = ''; }
        $size = (int) $this -> attrib['size'];
        if (7 < $size) {
        	$size = 7;
        	$sign = '';
        }
        if (-6 > $size) {
            $size = '-6';
        	$sign = '';
        }
        if (0 == $size) {
            $size = 3;
        }
        $size = $sign.$size;
        return '<font size="'.$size.'">'.parent::get_html($this -> tree).'</font>';
    }
}

// Класс для тега [b]
class bb_strong extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        return '<strong>'.parent::get_html($this -> tree).'</strong>';
    }
}

// Класс для тега [sub]
class bb_sub extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        return '<sub>'.parent::get_html($this -> tree).'</sub>';
    }
}

// Класс для тега [sup]
class bb_sup extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        return '<sup>'.parent::get_html($this -> tree).'</sup>';
    }
}

// Класс для тега [table]
class bb_table extends bbcode {
    var $rbr = 1;
    var $ends = array('table');
    var $children = array('tr','caption');
    function get_html() {
        $attr = ' class="bb"';
        $border = isset($this -> attrib['border'])
            ? (int) $this -> attrib['border']
            : null;
        if (null !== $border) { $attr .= ' border="'.$border.'"'; }
        $width = isset($this -> attrib['width']) ? $this -> attrib['width'] : '';
        if ($width) { $attr .= ' width="'.htmlspecialchars($width).'"'; }
        $cellspacing = isset($this -> attrib['cellspacing'])
            ? (int) $this -> attrib['cellspacing']
            : null;
        if (null !== $cellspacing) { $attr .= ' cellspacing="'.$cellspacing.'"'; }
        $cellpadding = isset($this -> attrib['cellpadding'])
            ? (int) $this -> attrib['cellpadding']
            : null;
        if (null !== $cellpadding) { $attr .= ' cellpadding="'.$cellpadding.'"'; }
        $align = isset($this -> attrib['align']) ? $this -> attrib['align'] : '';
        if ($align) { $attr .= ' align="'.htmlspecialchars($align).'"'; }
        $str = '<table'.$attr.'>';
        foreach ($this -> tree as $key => $item) {
            if ('text' == $item['type']) { unset($this -> tree[$key]); }
        }
        $str .= parent::get_html($this -> tree).'</table>';
        return $str;
    }
}

// Класс для тега [td]
class bb_td extends bbcode {
    var $ends = array('td','th','tr');
    function get_html() {
        $attr = 'class="bb"';
        $width = isset($this -> attrib['width']) ? $this -> attrib['width'] : '';
        if ($width) { $attr .= ' width="'.htmlspecialchars($width).'"'; }
        $height = isset($this -> attrib['height']) ? $this -> attrib['height'] : '';
        if ($height) { $attr .= ' height="'.htmlspecialchars($height).'"'; }
        $align = isset($this -> attrib['align']) ? $this -> attrib['align'] : '';
        if ($align) { $attr .= ' align="'.htmlspecialchars($align).'"'; }
        $valign = isset($this -> attrib['valign']) ? $this -> attrib['valign'] : '';
        if ($valign) { $attr .= ' valign="'.htmlspecialchars($valign).'"'; }
        if (isset($this -> attrib['colspan'])) {
            $colspan = (int) $this -> attrib['colspan'];
            if ($colspan) { $attr .= ' colspan="'.$colspan.'"'; }
        }
        if (isset($this -> attrib['rowspan'])) {
            $rowspan = (int) $this -> attrib['rowspan'];
            if ($rowspan) { $attr .= ' rowspan="'.$rowspan.'"'; }
        }
        return '<td '.$attr.'>'.parent::get_html($this -> tree).'</td>';
    }
}

// Класс для тега [th]
class bb_th extends bbcode {
    var $ends = array('td','th','tr');
    function get_html() {
        $attr = ' class="bb"';
        $width = isset($this -> attrib['width']) ? $this -> attrib['width'] : '';
        if ($width) { $attr .= ' width="'.htmlspecialchars($width).'"'; }
        $height = isset($this -> attrib['height']) ? $this -> attrib['height'] : '';
        if ($height) { $attr .= ' height="'.htmlspecialchars($height).'"'; }
        $align = isset($this -> attrib['align']) ? $this -> attrib['align'] : '';
        if ($align) { $attr .= ' align="'.htmlspecialchars($align).'"'; }
        $valign = isset($this -> attrib['valign']) ? $this -> attrib['valign'] : '';
        if ($valign) { $attr .= ' valign="'.htmlspecialchars($valign).'"'; }
        if (isset($this -> attrib['colspan'])) {
            $colspan = (int) $this -> attrib['colspan'];
            if ($colspan) { $attr .= ' colspan="'.$colspan.'"'; }
        }
        if (isset($this -> attrib['rowspan'])) {
            $rowspan = (int) $this -> attrib['rowspan'];
            if ($rowspan) { $attr .= ' rowspan="'.$rowspan.'"'; }
        }
        return '<th'.$attr.'>'.parent::get_html($this -> tree).'</th>';
    }
}

// Класс для тега [tr]
class bb_tr extends bbcode {
    var $ends = array('tr');
    var $children = array('td','th');
    function get_html() {
        $str = '<tr class="bb">';
        foreach ($this -> tree as $key => $item) {
            if ('text' == $item['type']) { unset($this -> tree[$key]); }
        }
        $str .= parent::get_html($this -> tree).'</tr>';
        return $str;
    }
}

// Класс для тега [tt]
class bb_tt extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr','justify',
        'left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'google','i','img','nobb','s','size','strike','sub','sup','tt','u','url'
    );
    function get_html() {
        return '<tt>'.parent::get_html($this -> tree).'</tt>';
    }
}

// Класс для тега [u]
class bb_u extends bbcode {
    var $ends = array(
        '*','address','align','center','h1','h2','h3','hr',
        'justify','left','list','php','quote','right','table','td','th','tr'
    );
    var $children = array(
        'a','abbr','acronym','anchor','b','bbcode','code','color','email',
        'font','google','i','img','nobb','s','size','strike','sub','sup','tt',
        'u','url'
    );
    function get_html() {
        return '<u>'.parent::get_html($this -> tree).'</u>';
    }
}

?>