energine
[ class tree: energine ] [ index: energine ] [ all elements ]

Source for file Jevix.class.php

Documentation is available at Jevix.class.php

  1. /**
  2.  * Jevix — средство автоматического применения правил набора текстов,
  3.  * наделённое способностью унифицировать разметку HTML/XML документов,
  4.  * контролировать перечень допустимых тегов и аттрибутов,
  5.  * предотвращать возможные XSS-атаки в коде документов.
  6.  * http://code.google.com/p/jevix/
  7.  *
  8.  * @author ur001 , http://ur001.habrahabr.ru
  9.  * @version 1.01
  10.  *
  11.  *  История версий:
  12.  *  1.1:
  13.  *   + cfgSetTagParamsAutoAdd() deprecated. Вместо него следует использовать cfgSetTagParamDefault() с более удобным синтаксисом
  14.  *   + Исправлен критический баг с обработкой атрибутов тегов https://code.google.com/p/jevix/issues/detail?id=1
  15.  *   + Удаление атрибутов тегов с пустым значением. Атрибуты без значений (checked, nowrap) теперь превращаются в checked="checked"
  16.  *   + Исправлен тест, проведена небольшая ревизия кода
  17.  *  1.02:
  18.  *   + Функции для работы со строками заменены на аналогичные mb_*, чтобы не перегружать через mbstring.func_overload ([email protected])
  19.  *  1.01
  20.  *   + cfgSetAutoReplace теперь регистронезависимый
  21.  *   + Возможность указать через cfgSetTagIsEmpty теги с пустым содержанием, которые не будут адалены парсером (rus.engine)
  22.  *   + фикс бага удаления контента тега при разном регистре открывающего и закрывающего тегов  (rus.engine)
  23.  *   + Исправлено поведение парсера при установке правила sfgParamsAutoAdd(). Теперь
  24.  *     параметр устанавливается только в том случае, если его вообще нет в
  25.  *     обрабатываемом тексте. Если есть - оставляется оригинальное значение. (deadyaga)
  26.  *  1.00
  27.  *   + Исправлен баг с закрывающимися тегами приводящий к созданию непарного тега рушащего вёрстку
  28.  *  1.00 RC2
  29.  *   + Небольшая чистка кода
  30.  *  1.00 RC1
  31.  *   + Добавлен символьный класс Jevix::RUS для определния русских символов
  32.  *   + Авторасстановка пробелов после пунктуации только для кирилицы
  33.  *   + Добавлена настройка cfgSetTagNoTypography() отключающая типографирование в указанном теге
  34.  *   + Немного переделан алгоритм обработки кавычек. Он стал более строгим
  35.  *   + Знак дюйма 33" больше не превращается в открывающуюся кавычку. Однако варриант "мой 24" монитор" - парсер не переварит.
  36.  *  0.99
  37.  *   + Расширена функциональность для проверки атрибутов тега:
  38.  *     можно указать тип атрибута ( 'colspan'=>'#int', 'value' => '#text' )
  39.  *     в Jevix, по-умолчанию, определён массив типов для нескольких стандартных атрибутов (src, href, width, height)
  40.  *  0.98
  41.  *   + Расширена функциональность для проверки атрибутов тега:
  42.  *     можно задавать список дозможных значений атрибута (  'align'=>array('left', 'right', 'center') )
  43.  *  0.97
  44.  *   + Обычные "кавычки" сохраняются как "e; если они были так написаны
  45.  *  0.96
  46.  *   + Добавлены разрешённые протоколы https и ftp для ссылок (a href="https://...)
  47.  *  0.95
  48.  *   + Исправлено типографирование ?.. и !.. (две точки в конце больше не превращаются в троеточие)
  49.  *   + Отключено автоматическое добавление пробела после точки для латиницы из-за чего невозможно было написать
  50.  *     index.php или .htaccess
  51.  *  0.94
  52.  *   + Добавлена настройка автодобавления параметров тегов. Непример rel = "nofolow" для ссылок.
  53.  *     Спасибо Myroslav Holyak ([email protected])
  54.  *  0.93
  55.  *       + Исправлен баг с удалением пробелов (например в "123 — 123")
  56.  *   + Исправлена ошибка из-за которой иногда не срабатывало автоматическое преобразования URL в ссылу
  57.  *   + Добавлена настройка cfgSetAutoLinkMode для отключения автоматического преобразования URL в ссылки
  58.  *   + Автодобавление пробела после точки, если после неё идёт русский символ
  59.  *  0.92
  60.  *       + Добавлена настройка cfgSetAutoBrMode. При установке в false, переносы строк не будут автоматически заменяться на BR
  61.  *       + Изменена обработка HTML-сущностей. Теперь все сущности имеющие эквивалент в Unicode (за исключением <>)
  62.  *     автоматически преобразуются в символ
  63.  *  0.91
  64.  *       + Добавлена обработка преформатированных тегов 
    . Для задания используйте cfgSetTagPreformatted()
                        
  65.  *   + Добавлена настройка cfgSetXHTMLMode. При отключении пустые теги будут оформляться как 
    , при включенном - 
  66.  *       + Несколько незначительных багфиксов
  67.  *  0.9
  68.  *       + Первый бета-релиз
  69.  */
  70.  
  71. class Jevix{
  72.     const PRINATABLE  0x1;
  73.     const ALPHA       0x2;
  74.     const LAT    0x4;
  75.     const RUS    0x8;
  76.     const NUMERIC     0x10;
  77.     const SPACE       0x20;
  78.     const NAME  0x40;
  79.     const URL    0x100;
  80.     const NOPRINT     0x200;
  81.     const PUNCTUATUON 0x400;
  82.     //const    = 0x800;
  83.     //const    = 0x1000;
  84.     const HTML_QUOTE  0x2000;
  85.     const TAG_QUOTE   0x4000;
  86.     const QUOTE_CLOSE 0x8000;
  87.     const NL      0x10000;
  88.     const QUOTE_OPEN  0;
  89.  
  90.     const STATE_TEXT 0;
  91.     const STATE_TAG_PARAMS 1;
  92.     const STATE_TAG_PARAM_VALUE 2;
  93.     const STATE_INSIDE_TAG 3;
  94.     const STATE_INSIDE_NOTEXT_TAG 4;
  95.     const STATE_INSIDE_PREFORMATTED_TAG 5;
  96.  
  97.     public $tagsRules = array();
  98.     public $entities1 = array('"'=>'"'"'"=>''''&'=>'&''<'=>'<''>'=>'>');
  99.     public $entities2 = array('<'=>'<''>'=>'>''"'=>'"');
  100.     public $textQuotes = array(array('«''»')array('„''“'));
  101.     public $dash = " — ";
  102.     public $apostrof = "’";
  103.     public $dotes = "…";
  104.     public $nl = "\r\n";
  105.     public $defaultTagParamRules = array('href' => '#link''src' => '#image''width' => '#int''height' => '#int''text' => '#text''title' => '#text');
  106.  
  107.     protected $text;
  108.     protected $textBuf;
  109.     protected $textLen = 0;
  110.     protected $curPos;
  111.     protected $curCh;
  112.     protected $curChOrd;
  113.     protected $curChClass;
  114.     protected $curParentTag;
  115.     protected $states;
  116.     protected $quotesOpened = 0;
  117.     protected $brAdded = 0;
  118.     protected $state;
  119.     protected $tagsStack;
  120.     protected $openedTag;
  121.     protected $autoReplace// Автозамена
  122.     protected $isXHTMLMode  = true// 
  123.     protected $isAutoBrMode = true// \n = 
  124.     protected $isAutoLinkMode = true;
  125.     protected $br = "
    "
    ;
  126.  
  127.     protected $noTypoMode = false;
  128.  
  129.     public    $outBuffer = '';
  130.     public    $errors;
  131.  
  132.  
  133.     /**
  134.      * Константы для класификации тегов
  135.      *
  136.      */
  137.     const TR_TAG_ALLOWED 1;   // Тег позволен
  138.     const TR_PARAM_ALLOWED 2;      // Параметр тега позволен (a->title, a->src, i->alt)
  139.     const TR_PARAM_REQUIRED 3;     // Параметр тега влятся необходимым (a->href, img->src)
  140.     const TR_TAG_SHORT 4;   // Тег может быть коротким (img, br)
  141.     const TR_TAG_CUT 5;       // Тег необходимо вырезать вместе с контентом (script, iframe)
  142.     const TR_TAG_CHILD 6;   // Тег может содержать другие теги
  143.     const TR_TAG_CONTAINER 7;      // Тег может содержать лишь указанные теги. В нём не может быть текста
  144.     const TR_TAG_CHILD_TAGS 8;     // Теги которые может содержать внутри себя другой тег
  145.     const TR_TAG_PARENT 9;     // Тег в котором должен содержаться данный тег
  146.     const TR_TAG_PREFORMATTED 10;  // Преформатированные тег, в котором всё заменяется на HTML сущности типа 
     сохраняя все отступы и пробелы
                        
  147.     const TR_PARAM_AUTO_ADD 11;    // Auto add parameters + default values (a->rel[=nofollow])
  148.     const TR_TAG_NO_TYPOGRAPHY 12// Отключение типографирования для тега
  149.     const TR_TAG_IS_EMPTY 13;      // Не короткий тег с пустым содержанием имеет право существовать
  150.     const TR_TAG_NO_AUTO_BR 14;    // Тег в котором не нужна авто-расстановка 
  151.  
  152.     /**
  153.      * Классы символов генерируются symclass.php
  154.      *
  155.      * @var array 
  156.      */
  157.     protected $chClasses = array(0=>512,1=>512,2=>512,3=>512,4=>512,5=>512,6=>512,7=>512,8=>512,9=>32,10=>66048,11=>512,12=>512,13=>66048,14=>512,15=>512,16=>512,17=>512,18=>512,19=>512,20=>512,21=>512,22=>512,23=>512,24=>512,25=>512,26=>512,27=>512,28=>512,29=>512,30=>512,31=>512,32=>32,97=>71,98=>71,99=>71,100=>71,101=>71,102=>71,103=>71,104=>71,105=>71,106=>71,107=>71,108=>71,109=>71,110=>71,111=>71,112=>71,113=>71,114=>71,115=>71,116=>71,117=>71,118=>71,119=>71,120=>71,121=>71,122=>71,65=>71,66=>71,67=>71,68=>71,69=>71,70=>71,71=>71,72=>71,73=>71,74=>71,75=>71,76=>71,77=>71,78=>71,79=>71,80=>71,81=>71,82=>71,83=>71,84=>71,85=>71,86=>71,87=>71,88=>71,89=>71,90=>71,1072=>11,1073=>11,1074=>11,1075=>11,1076=>11,1077=>11,1078=>11,1079=>11,1080=>11,1081=>11,1082=>11,1083=>11,1084=>11,1085=>11,1086=>11,1087=>11,1088=>11,1089=>11,1090=>11,1091=>11,1092=>11,1093=>11,1094=>11,1095=>11,1096=>11,1097=>11,1098=>11,1099=>11,1100=>11,1101=>11,1102=>11,1103=>11,1040=>11,1041=>11,1042=>11,1043=>11,1044=>11,1045=>11,1046=>11,1047=>11,1048=>11,1049=>11,1050=>11,1051=>11,1052=>11,1053=>11,1054=>11,1055=>11,1056=>11,1057=>11,1058=>11,1059=>11,1060=>11,1061=>11,1062=>11,1063=>11,1064=>11,1065=>11,1066=>11,1067=>11,1068=>11,1069=>11,1070=>11,1071=>11,48=>337,49=>337,50=>337,51=>337,52=>337,53=>337,54=>337,55=>337,56=>337,57=>337,34=>57345,39=>16385,46=>1281,44=>1025,33=>1025,63=>1281,58=>1025,59=>1281,1105=>11,1025=>11,47=>257,38=>257,37=>257,45=>257,95=>257,61=>257,43=>257,35=>257,124=>257,);
  158.  
  159.     /**
  160.      * Установка конфигурационного флага для одного или нескольких тегов
  161.      *
  162.      * @param array|string$tags тег(и)
  163.      * @param int $flag флаг
  164.      * @param mixed $value значеник=е флага
  165.      * @param boolean $createIfNoExists если тег ещё не определён - создть его
  166.      */
  167.     protected function _cfgSetTagsFlag($tags$flag$value$createIfNoExists true){
  168.         if(!is_array($tags)) $tags array($tags);
  169.         foreach($tags as $tag){
  170.             if(!isset($this->tagsRules[$tag])) {
  171.                 if($createIfNoExists){
  172.                     $this->tagsRules[$tagarray();
  173.                 else {
  174.                     throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
  175.                 }
  176.             }
  177.             $this->tagsRules[$tag][$flag$value;
  178.         }
  179.     }
  180.  
  181.     /**
  182.      * КОНФИГУРАЦИЯ: Разрешение или запрет тегов
  183.      * Все не разрешённые теги считаются запрещёнными
  184.      * @param array|string$tags тег(и)
  185.      */
  186.     function cfgAllowTags($tags){
  187.         $this->_cfgSetTagsFlag($tagsself::TR_TAG_ALLOWEDtrue);
  188.     }
  189.  
  190.     /**
  191.      * КОНФИГУРАЦИЯ: Коротие теги типа 
  192.      * @param array|string$tags тег(и)
  193.      */
  194.     function cfgSetTagShort($tags){
  195.         $this->_cfgSetTagsFlag($tagsself::TR_TAG_SHORTtruefalse);
  196.     }
  197.  
  198.     /**
  199.      * КОНФИГУРАЦИЯ: Преформатированные теги, в которых всё заменяется на HTML сущности типа 
    
                        
  200.      * @param array|string$tags тег(и)
  201.      */
  202.     function cfgSetTagPreformatted($tags){
  203.         $this->_cfgSetTagsFlag($tagsself::TR_TAG_PREFORMATTEDtruefalse);
  204.     }
  205.  
  206.     /**
  207.      * КОНФИГУРАЦИЯ: Теги в которых отключено типографирование типа 
  208.      * @param array|string$tags тег(и)
  209.      */
  210.     function cfgSetTagNoTypography($tags){
  211.         $this->_cfgSetTagsFlag($tagsself::TR_TAG_NO_TYPOGRAPHYtruefalse);
  212.     }
  213.  
  214.     /**
  215.      * КОНФИГУРАЦИЯ: Не короткие теги которые не нужно удалять с пустым содержанием, например, 
  216.      * @param array|string$tags тег(и)
  217.      */
  218.     function cfgSetTagIsEmpty($tags){
  219.         $this->_cfgSetTagsFlag($tagsself::TR_TAG_IS_EMPTYtruefalse);
  220.     }
  221.  
  222.     /**
  223.      * КОНФИГУРАЦИЯ: Теги внутри который не нужна авто-расстановка 
    , например, 
       и 
      1.      * @param array|string$tags тег(и)
      2.      */
      3.     function cfgSetTagNoAutoBr($tags){
      4.         $this->_cfgSetTagsFlag($tagsself::TR_TAG_NO_AUTO_BRtruefalse);
      5.     }
      6.  
      7.     /**
      8.      * КОНФИГУРАЦИЯ: Тег необходимо вырезать вместе с контентом (script, iframe)
      9.      * @param array|string$tags тег(и)
      10.      */
      11.     function cfgSetTagCutWithContent($tags){
      12.         $this->_cfgSetTagsFlag($tagsself::TR_TAG_CUTtrue);
      13.     }
      14.  
      15.     /**
      16.      * КОНФИГУРАЦИЯ: Добавление разрешённых параметров тега
      17.      * @param string $tag тег
      18.      * @param string|array$params разрешённые параметры
      19.      */
      20.     function cfgAllowTagParams($tag$params){
      21.         if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
      22.         if(!is_array($params)) $params array($params);
      23.         // Если ключа со списком разрешенных параметров не существует - создаём ео
      24.         if(!isset($this->tagsRules[$tag][self::TR_PARAM_ALLOWED])) {
      25.             $this->tagsRules[$tag][self::TR_PARAM_ALLOWEDarray();
      26.         }
      27.         foreach($params as $key => $value){
      28.             if(is_string($key)){
      29.                 $this->tagsRules[$tag][self::TR_PARAM_ALLOWED][$key$value;
      30.             else {
      31.                 $this->tagsRules[$tag][self::TR_PARAM_ALLOWED][$valuetrue;
      32.             }
      33.         }
      34.     }
      35.  
      36.     /**
      37.      * КОНФИГУРАЦИЯ: Добавление необходимых параметров тега
      38.      * @param string $tag тег
      39.      * @param string|array$params разрешённые параметры
      40.      */
      41.     function cfgSetTagParamsRequired($tag$params){
      42.         if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
      43.         if(!is_array($params)) $params array($params);
      44.         // Если ключа со списком разрешенных параметров не существует - создаём ео
      45.         if(!isset($this->tagsRules[$tag][self::TR_PARAM_REQUIRED])) {
      46.             $this->tagsRules[$tag][self::TR_PARAM_REQUIREDarray();
      47.         }
      48.         foreach($params as $param){
      49.             $this->tagsRules[$tag][self::TR_PARAM_REQUIRED][$paramtrue;
      50.         }
      51.     }
      52.  
      53.     /* КОНФИГУРАЦИЯ: Установка тегов которые может содержать тег-контейнер
      54.      * @param string $tag тег
      55.      * @param string|array $childs разрешённые теги
      56.      * @param boolean $isContainerOnly тег является только контейнером других тегов и не может содержать текст
      57.      * @param boolean $isChildOnly вложенные теги не могут присутствовать нигде кроме указанного тега
      58.      */
      59.     function cfgSetTagChilds($tag$childs$isContainerOnly false$isChildOnly false){
      60.         if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
      61.         if(!is_array($childs)) $childs array($childs);
      62.         // Тег является контейнером и не может содержать текст
      63.         if($isContainerOnly$this->tagsRules[$tag][self::TR_TAG_CONTAINERtrue;
      64.         // Если ключа со списком разрешенных тегов не существует - создаём ео
      65.         if(!isset($this->tagsRules[$tag][self::TR_TAG_CHILD_TAGS])) {
      66.             $this->tagsRules[$tag][self::TR_TAG_CHILD_TAGSarray();
      67.         }
      68.         foreach($childs as $child){
      69.             $this->tagsRules[$tag][self::TR_TAG_CHILD_TAGS][$childtrue;
      70.             //  Указанный тег должен сущеаствовать в списке тегов
      71.             if(!isset($this->tagsRules[$child])) throw new Exception("Тег $child отсутствует в списке разрешённых тегов");
      72.             if(!isset($this->tagsRules[$child][self::TR_TAG_PARENT])) $this->tagsRules[$child][self::TR_TAG_PARENTarray();
      73.             $this->tagsRules[$child][self::TR_TAG_PARENT][$tagtrue;
      74.             // Указанные разрешённые теги могут находится только внтутри тега-контейнера
      75.             if($isChildOnly$this->tagsRules[$child][self::TR_TAG_CHILDtrue;
      76.         }
      77.     }
      78.  
      79.     /**
      80.      * CONFIGURATION: Adding autoadd attributes and their values to tag. If the 'rewrite' set as true, the attribute value will be replaced
      81.      * @param string $tag tag
      82.      * @param string|array$params array of pairs array('name'=>attributeName, 'value'=>attributeValue, 'rewrite'=>true|false)
      83.      * @deprecated устаревший синтаксис. Используйте cfgSetTagParamAutoAdd
      84.      */
      85.     function cfgSetTagParamsAutoAdd($tag$params){
      86.         throw new Exception("cfgSetTagParamsAutoAdd() is Deprecated. Use cfgSetTagParamDefault() instead");
      87.     }
      88.  
      89.     /**
      90.      * КОНФИГУРАЦИЯ: Установка дефолтных значений для атрибутов тега
      91.      * @param string $tag тег
      92.      * @param string $param атрибут
      93.      * @param string $value значение
      94.      * @param boolean $isRewrite заменять указанное значение дефолтным
      95.      */
      96.     function cfgSetTagParamDefault($tag$param$value$isRewrite false){
      97.         if(!isset($this->tagsRules[$tag])) throw new Exception("Tag $tag is missing in allowed tags list");
      98.  
      99.         if(!isset($this->tagsRules[$tag][self::TR_PARAM_AUTO_ADD])) {
      100.             $this->tagsRules[$tag][self::TR_PARAM_AUTO_ADDarray();
      101.         }
      102.  
      103.         $this->tagsRules[$tag][self::TR_PARAM_AUTO_ADD][$paramarray('value'=>$value'rewrite'=>$isRewrite);
      104.     }
      105.  
      106.  
      107.     /**
      108.      * Автозамена
      109.      *
      110.      * @param array $from с
      111.      * @param array $to на
      112.      */
      113.     function cfgSetAutoReplace($from$to){
      114.         $this->autoReplace = array('from' => $from'to' => $to);
      115.     }
      116.  
      117.     /**
      118.      * Включение или выключение режима XTML
      119.      *
      120.      * @param boolean $isXHTMLMode 
      121.      */
      122.     function cfgSetXHTMLMode($isXHTMLMode){
      123.         $this->br = $isXHTMLMode '
        '
        '
        ;
      124.         $this->isXHTMLMode = $isXHTMLMode;
      125.     }
      126.  
      127.     /**
      128.      * Включение или выключение режима замены новых строк на 
      129.      *
      130.      * @param boolean $isAutoBrMode 
      131.      */
      132.     function cfgSetAutoBrMode($isAutoBrMode){
      133.         $this->isAutoBrMode = $isAutoBrMode;
      134.     }
      135.  
      136.     /**
      137.      * Включение или выключение режима автоматического определения ссылок
      138.      *
      139.      * @param boolean $isAutoLinkMode 
      140.      */
      141.     function cfgSetAutoLinkMode($isAutoLinkMode){
      142.         $this->isAutoLinkMode = $isAutoLinkMode;
      143.     }
      144.  
      145.     protected function &strToArray($str){
      146.         $chars null;
      147.         preg_match_all('/./su'$str$chars);
      148.         return $chars[0];
      149.     }
      150.  
      151.  
      152.     function parse($text&$errors){
      153.         $this->curPos = -1;
      154.         $this->curCh = null;
      155.         $this->curChOrd = 0;
      156.         $this->state = self::STATE_TEXT;
      157.         $this->states = array();
      158.         $this->quotesOpened = 0;
      159.         $this->noTypoMode = false;
      160.  
      161.         // Авто растановка BR?
      162.         if($this->isAutoBrMode{
      163.             $this->text = preg_replace('/(\r\n|\n\r|\n)?/ui'$this->nl$text);
      164.         else {
      165.             $this->text = $text;
      166.         }
      167.  
      168.  
      169.         if(!empty($this->autoReplace)){
      170.             $this->text = str_ireplace($this->autoReplace['from']$this->autoReplace['to']$this->text);
      171.         }
      172.         $this->textBuf = $this->strToArray($this->text);
      173.         $this->textLen = count($this->textBuf);
      174.         $this->getCh();
      175.         $content '';
      176.         $this->outBuffer='';
      177.         $this->brAdded=0;
      178.         $this->tagsStack = array();
      179.         $this->openedTag = null;
      180.         $this->errors = array();
      181.         $this->skipSpaces();
      182.         $this->anyThing($content);
      183.         $errors $this->errors;
      184.         return $content;
      185.     }
      186.  
      187.     /**
      188.      * Получение следующего символа из входной строки
      189.      * @return string считанный символ
      190.      */
      191.     protected function getCh(){
      192.         return $this->goToPosition($this->curPos+1);
      193.     }
      194.  
      195.     /**
      196.      * Перемещение на указанную позицию во входной строке и считывание символа
      197.      * @return string символ в указанной позиции
      198.      */
      199.     protected function goToPosition($position){
      200.         $this->curPos = $position;
      201.         if($this->curPos < $this->textLen){
      202.             $this->curCh = $this->textBuf[$this->curPos];
      203.             $this->curChOrd = uniord($this->curCh);
      204.             $this->curChClass = $this->getCharClass($this->curChOrd);
      205.         else {
      206.             $this->curCh = null;
      207.             $this->curChOrd = 0;
      208.             $this->curChClass = 0;
      209.         }
      210.         return $this->curCh;
      211.     }
      212.  
      213.     /**
      214.      * Сохранить текущее состояние
      215.      *
      216.      */
      217.     protected function saveState(){
      218.         $state array(
      219.             'pos'   => $this->curPos,
      220.             'ch'    => $this->curCh,
      221.             'ord'   => $this->curChOrd,
      222.             'class' => $this->curChClass,
      223.         );
      224.  
      225.         $this->states[$state;
      226.         return count($this->states)-1;
      227.     }
      228.  
      229.     /**
      230.      * Восстановить
      231.      *
      232.      */
      233.     protected function restoreState($index null){
      234.         if(!count($this->states)) throw new Exception('Конец стека');
      235.         if($index == null){
      236.             $state = array_pop($this->states);
      237.         else {
      238.             if(!isset($this->states[$index])) throw new Exception('Неверный индекс стека');
      239.             $state $this->states[$index];
      240.             $this->states = array_slice($this->states0$index);
      241.         }
      242.  
      243.         $this->curPos     = $state['pos'];
      244.         $this->curCh      = $state['ch'];
      245.         $this->curChOrd   = $state['ord'];
      246.         $this->curChClass = $state['class'];
      247.     }
      248.  
      249.     /**
      250.      * Проверяет точное вхождение символа в текущей позиции
      251.      * Если символ соответствует указанному автомат сдвигается на следующий
      252.      *
      253.      * @param string $ch 
      254.      * @return boolean 
      255.      */
      256.     protected function matchCh($ch$skipSpaces false){
      257.         if($this->curCh == $ch{
      258.             $this->getCh();
      259.             if($skipSpaces$this->skipSpaces();
      260.             return true;
      261.         }
      262.  
      263.         return false;
      264.     }
      265.  
      266.     /**
      267.      * Проверяет точное вхождение символа указанного класса в текущей позиции
      268.      * Если символ соответствует указанному классу автомат сдвигается на следующий
      269.      *
      270.      * @param int $chClass класс символа
      271.      * @return string найденый символ или false
      272.      */
      273.     protected function matchChClass($chClass$skipSpaces false){
      274.         if(($this->curChClass $chClass== $chClass{
      275.             $ch $this->curCh;
      276.             $this->getCh();
      277.             if($skipSpaces$this->skipSpaces();
      278.             return $ch;
      279.         }
      280.  
      281.         return false;
      282.     }
      283.  
      284.     /**
      285.      * Проверка на точное совпадение строки в текущей позиции
      286.      * Если строка соответствует указанной автомат сдвигается на следующий после строки символ
      287.      *
      288.      * @param string $str 
      289.      * @return boolean 
      290.      */
      291.     protected function matchStr($str$skipSpaces false){
      292.         $this->saveState();
      293.         $len = mb_strlen($str'UTF-8');
      294.         $test '';
      295.         while($len-- && $this->curChClass){
      296.             $test.=$this->curCh;
      297.             $this->getCh();
      298.         }
      299.  
      300.         if($test == $str{
      301.             if($skipSpaces$this->skipSpaces();
      302.             return true;
      303.         else {
      304.             $this->restoreState();
      305.             return false;
      306.         }
      307.     }
      308.  
      309.     /**
      310.      * Пропуск текста до нахождения указанного символа
      311.      *
      312.      * @param string $ch сиимвол
      313.      * @return string найденый символ или false
      314.      */
      315.     protected function skipUntilCh($ch){
      316.         $chPos = mb_strpos($this->text$ch$this->curPos'UTF-8');
      317.         if($chPos){
      318.             return $this->goToPosition($chPos);
      319.         else {
      320.             return false;
      321.         }
      322.     }
      323.  
      324.     /**
      325.      * Пропуск текста до нахождения указанной строки или символа
      326.      *
      327.      * @param string $str строка или символ ля поиска
      328.      * @return boolean 
      329.      */
      330.     protected function skipUntilStr($str){
      331.         $str $this->strToArray($str);
      332.         $firstCh $str[0];
      333.         $len = count($str);
      334.         while($this->curChClass){
      335.             if($this->curCh == $firstCh){
      336.                 $this->saveState();
      337.                 $this->getCh();
      338.                 $strOK true;
      339.                 for($i 1$i<$len $i++){
      340.                     // Конец строки
      341.                     if(!$this->curChClass){
      342.                         return false;
      343.                     }
      344.                     // текущий символ не равен текущему символу проверяемой строки?
      345.                     if($this->curCh != $str[$i]){
      346.                         $strOK false;
      347.                         break;
      348.                     }
      349.                     // Следующий символ
      350.                     $this->getCh();
      351.                 }
      352.  
      353.                 // При неудаче откатываемся с переходим на следующий символ
      354.                 if(!$strOK){
      355.                     $this->restoreState();
      356.                 else {
      357.                     return true;
      358.                 }
      359.             }
      360.             // Следующий символ
      361.             $this->getCh();
      362.         }
      363.         return false;
      364.     }
      365.  
      366.     /**
      367.      * Возвращает класс символа
      368.      *
      369.      * @return int 
      370.      */
      371.     protected function getCharClass($ord){
      372.         return isset($this->chClasses[$ord]$this->chClasses[$ordself::PRINATABLE;
      373.     }
      374.  
      375.     /*function isSpace(){
      376.         return $this->curChClass == slf::SPACE;
      377.     }*/
      378.  
      379.     /**
      380.      * Пропуск пробелов
      381.      *
      382.      */
      383.     protected function skipSpaces(&$count 0){
      384.         while($this->curChClass == self::SPACE{
      385.             $this->getCh();
      386.             $count++;
      387.         }
      388.         return $count 0;
      389.     }
      390.  
      391.     /**
      392.      *  Получает име (тега, параметра) по принципу 1 сиивол далее цифра или символ
      393.      *
      394.      * @param string $name 
      395.      */
      396.     protected function name(&$name ''$minus false){
      397.         if(($this->curChClass self::LAT== self::LAT){
      398.             $name.=$this->curCh;
      399.             $this->getCh();
      400.         else {
      401.             return false;
      402.         }
      403.  
      404.         while((($this->curChClass self::NAME== self::NAME || ($minus && $this->curCh=='-'))){
      405.             $name.=$this->curCh;
      406.             $this->getCh();
      407.         }
      408.  
      409.         $this->skipSpaces();
      410.         return true;
      411.     }
      412.  
      413.     protected function tag(&$tag&$params&$content&$short){
      414.         $this->saveState();
      415.         $params array();
      416.         $tag '';
      417.         $closeTag '';
      418.         $params array();
      419.         $short false;
      420.         if(!$this->tagOpen($tag$params$short)) return false;
      421.         // Короткая запись тега
      422.         if($shortreturn true;
      423.  
      424.         // Сохраняем кавычки и состояние
      425.         //$oldQuotesopen = $this->quotesOpened;
      426.         $oldState $this->state;
      427.         $oldNoTypoMode $this->noTypoMode;
      428.         //$this->quotesOpened = 0;
      429.  
      430.  
      431.         // Если в теге не должно быть текста, а только другие теги
      432.         // Переходим в состояние self::STATE_INSIDE_NOTEXT_TAG
      433.         if(!empty($this->tagsRules[$tag][self::TR_TAG_PREFORMATTED])){
      434.             $this->state = self::STATE_INSIDE_PREFORMATTED_TAG;
      435.         elseif(!empty($this->tagsRules[$tag][self::TR_TAG_CONTAINER])){
      436.             $this->state = self::STATE_INSIDE_NOTEXT_TAG;
      437.         elseif(!empty($this->tagsRules[$tag][self::TR_TAG_NO_TYPOGRAPHY])) {
      438.             $this->noTypoMode = true;
      439.             $this->state = self::STATE_INSIDE_TAG;
      440.         else {
      441.             $this->state = self::STATE_INSIDE_TAG;
      442.         }
      443.  
      444.         // Контент тега
      445.         array_push($this->tagsStack$tag);
      446.         $this->openedTag = $tag;
      447.         $content '';
      448.         if($this->state == self::STATE_INSIDE_PREFORMATTED_TAG){
      449.             $this->preformatted($content$tag);
      450.         else {
      451.             $this->anyThing($content$tag);
      452.         }
      453.  
      454.         array_pop($this->tagsStack);
      455.         $this->openedTag = !empty($this->tagsStack? array_pop($this->tagsStacknull;
      456.  
      457.         $isTagClose $this->tagClose($closeTag);
      458.         if($isTagClose && ($tag != $closeTag)) {
      459.             $this->eror("Неверный закрывающийся тег $closeTag. Ожидалось закрытие $tag");
      460.             //$this->restoreState();
      461.         }
      462.  
      463.         // Восстанавливаем предыдущее состояние и счетчик кавычек
      464.         $this->state = $oldState;
      465.         $this->noTypoMode = $oldNoTypoMode;
      466.         //$this->quotesOpened = $oldQuotesopen;
      467.  
      468.         return true;
      469.     }
      470.  
      471.     protected function preformatted(&$content ''$insideTag null){
      472.         while($this->curChClass){
      473.             if($this->curCh == '<'){
      474.                 $tag '';
      475.                 $this->saveState();
      476.                 // Пытаемся найти закрывающийся тег
      477.                 $isClosedTag $this->tagClose($tag);
      478.                 // Возвращаемся назад, если тег был найден
      479.                 if($isClosedTag$this->restoreState();
      480.                 // Если закрылось то, что открылось - заканчиваем и возвращаем true
      481.                 if($isClosedTag && $tag == $insideTagreturn;
      482.             }
      483.             $content.= isset($this->entities2[$this->curCh]$this->entities2[$this->curCh$this->curCh;
      484.             $this->getCh();
      485.         }
      486.     }
      487.  
      488.     protected function tagOpen(&$name&$params&$short false){
      489.         $restore $this->saveState();
      490.  
      491.         // Открытие
      492.         if(!$this->matchCh('<')) return false;
      493.         $this->skipSpaces();
      494.         if(!$this->name($name)){
      495.             $this->restoreState();
      496.             return false;
      497.         }
      498.         $name=mb_strtolower($name'UTF-8');
      499.         // Пробуем получить список атрибутов тега
      500.         if($this->curCh != '>' && $this->curCh != '/'$this->tagParams($params);
      501.  
      502.         // Короткая запись тега
      503.         $short !empty($this->tagsRules[$name][self::TR_TAG_SHORT]);
      504.  
      505.         // Short && XHTML && !Slash || Short && !XHTML && !Slash = ERROR
      506.         $slash $this->matchCh('/');
      507.         //if(($short && $this->isXHTMLMode && !$slash) || (!$short && !$this->isXHTMLMode && $slash)){
      508.         if(!$short && $slash){
      509.             $this->restoreState();
      510.             return false;
      511.         }
      512.  
      513.         $this->skipSpaces();
      514.  
      515.         // Закрытие
      516.         if(!$this->matchCh('>')) {
      517.             $this->restoreState($restore);
      518.             return false;
      519.         }
      520.  
      521.         $this->skipSpaces();
      522.         return true;
      523.     }
      524.  
      525.  
      526.     protected function tagParams(&$params array()){
      527.         $name null;
      528.         $value null;
      529.         while($this->tagParam($name$value)){
      530.             $params[$name$value;
      531.             $name ''$value '';
      532.         }
      533.         return count($params0;
      534.     }
      535.  
      536.     protected function tagParam(&$name&$value){
      537.         $this->saveState();
      538.         if(!$this->name($nametrue)) return false;
      539.  
      540.         if(!$this->matchCh('='true)){
      541.             // Стремная штука - параметр без значения 
      542.             if(($this->curCh=='>' || ($this->curChClass self::LAT== self::LAT)){
      543.                 $value $name;
      544.                 return true;
      545.             else {
      546.                 $this->restoreState();
      547.                 return false;
      548.             }
      549.         }
      550.  
      551.         $quote $this->matchChClass(self::TAG_QUOTEtrue);
      552.  
      553.         if(!$this->tagParamValue($value$quote)){
      554.             $this->restoreState();
      555.             return false;
      556.         }
      557.  
      558.         if($quote && !$this->matchCh($quotetrue)){
      559.             $this->restoreState();
      560.             return false;
      561.         }
      562.  
      563.         $this->skipSpaces();
      564.         return true;
      565.     }
      566.  
      567.     protected function tagParamValue(&$value$quote){
      568.         if($quote !== false){
      569.             // Нормальный параметр с кавычкамию Получаем пока не кавычки и не конец
      570.             $escape false;
      571.             while($this->curChClass && ($this->curCh != $quote || $escape)){
      572.                 $escape false;
      573.                 // Экранируем символы HTML которые не могут быть в параметрах
      574.                 $value.=isset($this->entities1[$this->curCh]$this->entities1[$this->curCh$this->curCh;
      575.                 // Символ ескейпа 
      576.                 if($this->curCh == '\\'$escape true;
      577.                 $this->getCh();
      578.             }
      579.         else {
      580.             // долбаный параметр без кавычек. получаем его пока не пробел и не > и не конец
      581.             while($this->curChClass && !($this->curChClass self::SPACE&& $this->curCh != '>'){
      582.                 // Экранируем символы HTML которые не могут быть в параметрах
      583.                 $value.=isset($this->entities1[$this->curCh]$this->entities1[$this->curCh$this->curCh;
      584.                 $this->getCh();
      585.             }
      586.         }
      587.  
      588.         return true;
      589.     }
      590.  
      591.     protected function tagClose(&$name){
      592.         $this->saveState();
      593.         if(!$this->matchCh('<')) return false;
      594.         $this->skipSpaces();
      595.         if(!$this->matchCh('/')) {
      596.             $this->restoreState();
      597.             return false;
      598.         }
      599.         $this->skipSpaces();
      600.         if(!$this->name($name)){
      601.             $this->restoreState();
      602.             return false;
      603.         }
      604.         $name=mb_strtolower($name'UTF-8');
      605.         $this->skipSpaces();
      606.         if(!$this->matchCh('>')) {
      607.             $this->restoreState();
      608.             return false;
      609.         }
      610.         return true;
      611.     }
      612.  
      613.     protected function makeTag($tag$params$content$short$parentTag null){
      614.         $this->curParentTag=$parentTag;
      615.         $tag = mb_strtolower($tag'UTF-8');
      616.  
      617.         // Получаем правила фильтрации тега
      618.         $tagRules = isset($this->tagsRules[$tag]$this->tagsRules[$tagnull;
      619.  
      620.         // Проверка - родительский тег - контейнер, содержащий только другие теги (ul, table, etc)
      621.         $parentTagIsContainer $parentTag && isset($this->tagsRules[$parentTag][self::TR_TAG_CONTAINER]);
      622.  
      623.         // Вырезать тег вместе с содержанием
      624.         if($tagRules && isset($this->tagsRules[$tag][self::TR_TAG_CUT])) return '';
      625.  
      626.         // Позволен ли тег
      627.         if(!$tagRules || empty($tagRules[self::TR_TAG_ALLOWED])) return $parentTagIsContainer '' $content;
      628.  
      629.         // Если тег находится внутри другого - может ли он там находится?
      630.         if($parentTagIsContainer){
      631.             if(!isset($this->tagsRules[$parentTag][self::TR_TAG_CHILD_TAGS][$tag])) return '';
      632.         }
      633.  
      634.         // Тег может находится только внтури другого тега
      635.         if(isset($tagRules[self::TR_TAG_CHILD])){
      636.             if(!isset($tagRules[self::TR_TAG_PARENT][$parentTag])) return $content;
      637.         }
      638.  
      639.  
      640.         $resParams array();
      641.         foreach($params as $param=>$value){
      642.             $param = mb_strtolower($param'UTF-8');
      643.             $value = trim($value);
      644.             if(empty($value)) continue;
      645.  
      646.             // Атрибут тега разрешён? Какие возможны значения? Получаем список правил
      647.             $paramAllowedValues = isset($tagRules[self::TR_PARAM_ALLOWED][$param]$tagRules[self::TR_PARAM_ALLOWED][$paramfalse;
      648.             if(empty($paramAllowedValues)) continue;
      649.  
      650.             // Если есть список разрешённых параметров тега
      651.             if(is_array($paramAllowedValues&& !in_array($value$paramAllowedValues)) {
      652.                 $this->eror("Недопустимое значение для атрибута тега $tag $param=$value");
      653.                 continue;
      654.             // Если атрибут тега помечен как разрешённый, но правила не указаны - смотрим в массив стандартных правил для атрибутов
      655.             elseif($paramAllowedValues === true && !empty($this->defaultTagParamRules[$param])){
      656.                 $paramAllowedValues $this->defaultTagParamRules[$param];
      657.             }
      658.  
      659.             if(is_string($paramAllowedValues)){
      660.                 switch($paramAllowedValues){
      661.                     case '#int':
      662.                         if(!is_numeric($value)) {
      663.                             $this->eror("Недопустимое значение для атрибута тега $tag $param=$value. Ожидалось число");
      664.                             continue(2);
      665.                         }
      666.                         break;
      667.  
      668.                     case '#text':
      669.                         $value = htmlspecialchars($value);
      670.                         break;
      671.  
      672.                     case '#link':
      673.                         // Ява-скрипт в ссылке
      674.                         if(preg_match('/javascript:/ui'$value)) {
      675.                             $this->eror('Попытка вставить JavaScript в URI');
      676.                             continue(2);
      677.                         }
      678.                         // Первый символ должен быть a-z0-9 или #!
      679.                          if(!preg_match('/^[a-z0-9\/\#]/ui'$value)) {
      680.                             $this->eror('URI: Первый символ адреса должен быть буквой или цифрой');
      681.                             continue(2);
      682.                         }
      683.                         // HTTP в начале если нет
      684.                         //if(!preg_match('/^(http|https|ftp):\/\//ui', $value) && !preg_match('/^(\/|\#)/ui', $value) ) $value = 'http://'.$value;
      685.                         break;
      686.  
      687.                     case '#image':
      688.                         // Ява-скрипт в пути к картинке
      689.                         if(preg_match('/javascript:/ui'$value)) {
      690.                             $this->eror('Попытка вставить JavaScript в пути к изображению');
      691.                             continue(2);
      692.                         }
      693.                         // HTTP в начале если нет
      694.                         //if(!preg_match('/^http:\/\//ui', $value) && !preg_match('/^\//ui', $value)) $value = 'http://'.$value;
      695.                         break;
      696.  
      697.                     default:
      698.                         $this->eror("Неверное описание атрибута тега в настройке Jevix: $param => $paramAllowedValues");
      699.                         continue(2);
      700.                         break;
      701.                 }
      702.             }
      703.  
      704.             $resParams[$param$value;
      705.         }
      706.  
      707.         // Проверка обязятельных параметров тега
      708.         // Если нет обязательных параметров возвращаем только контент
      709.         $requiredParams = isset($tagRules[self::TR_PARAM_REQUIRED]? array_keys($tagRules[self::TR_PARAM_REQUIRED]array();
      710.         if($requiredParams){
      711.             foreach($requiredParams as $requiredParam){
      712.                 if(empty($resParams[$requiredParam])) return $content;
      713.             }
      714.         }
      715.  
      716.         // Автодобавляемые параметры
      717.         if(!empty($tagRules[self::TR_PARAM_AUTO_ADD])){
      718.           foreach($tagRules[self::TR_PARAM_AUTO_ADDas $name => $aValue{
      719.               // If there isn't such attribute - setup it
      720.               if(!array_key_exists($name$resParamsor ($aValue['rewrite'and $resParams[$name!= $aValue['value'])) {
      721.               $resParams[$name$aValue['value'];
      722.               }
      723.           }
      724.         }
      725.         
      726.         // Пустой некороткий тег удаляем кроме исключений
      727.         if (!isset($tagRules[self::TR_TAG_IS_EMPTY]or !$tagRules[self::TR_TAG_IS_EMPTY]{
      728.             if(!$short && empty($content)) return '';
      729.         }
      730.         // Собираем тег
      731.         $text='<'.$tag;
      732.  
      733.         // Параметры
      734.         foreach($resParams as $param => $value{
      735.             if (!empty($value)) {
      736.                 $text.=' '.$param.'="'.$value.'"';
      737.             }
      738.         }
      739.         
      740.         // Закрытие тега (если короткий то без контента)
      741.         $text.= $short && $this->isXHTMLMode ? '/>' '>';
      742.         if(isset($tagRules[self::TR_TAG_CONTAINER])) $text .= "\r\n";
      743.         if(!$short$text.= $content.'.$tag.'>';
      744.         if($parentTagIsContainer$text .= "\r\n";
      745.         if($tag == 'br'$text.="\r\n";
      746.         return $text;
      747.     }
      748.  
      749.     protected function comment(){
      750.         if(!$this->matchStr('');
      751.     }
      752.  
      753.     protected function anyThing(&$content ''$parentTag null){
      754.         $this->skipNL();
      755.         while($this->curChClass){
      756.             $tag '';
      757.             $params null;
      758.             $text null;
      759.             $shortTag false;
      760.             $name null;
      761.  
      762.             // Если мы находимся в режиме тега без текста
      763.             // пропускаем контент пока не встретится <
      764.             if($this->state == self::STATE_INSIDE_NOTEXT_TAG && $this->curCh!='<'){
      765.                 $this->skipUntilCh('<');
      766.             }
      767.  
      768.             // <Тег> кекст 
      769.             if($this->curCh == '<' && $this->tag($tag$params$text$shortTag)){
      770.                 // Преобразуем тег в текст
      771.                 $tagText $this->makeTag($tag$params$text$shortTag$parentTag);
      772.                 $content.=$tagText;
      773.                 // Пропускаем пробелы после 
         и запрещённых тегов, которые вырезаются парсером
      774.                 if ($tag=='br'{
      775.                     $this->skipNL();
      776.                 elseif (empty($tagText)){
      777.                     $this->skipSpaces();
      778.                 }
      779.  
      780.             // Коментарий 
      781.             elseif($this->curCh == '<' && $this->comment()){
      782.                 continue;
      783.  
      784.             // Конец тега или символ <
      785.             elseif($this->curCh == '<'{
      786.                 // Если встречается <, но это не тег
      787.                 // то это либо закрывающийся тег либо знак <
      788.                 $this->saveState();
      789.                 if($this->tagClose($name)){
      790.                     // Если это закрывающийся тег, то мы делаем откат
      791.                     // и выходим из функции
      792.                     // Но если мы не внутри тега, то просто пропускаем его
      793.                     if($this->state == self::STATE_INSIDE_TAG || $this->state == self::STATE_INSIDE_NOTEXT_TAG{
      794.                         $this->restoreState();
      795.                         return false;
      796.                     else {
      797.                         $this->eror('Не ожидалось закрывающегося тега '.$name);
      798.                     }
      799.                 else {
      800.                     if($this->state != self::STATE_INSIDE_NOTEXT_TAG$content.=$this->entities2['<'];
      801.                     $this->getCh();
      802.                 }
      803.  
      804.             // Текст
      805.             elseif($this->text($text)){
      806.                 $content.=$text;
      807.             }
      808.         }
      809.  
      810.         return true;
      811.     }
      812.  
      813.     /**
      814.      * Пропуск переводов строк подсчет кол-ва
      815.      *
      816.      * @param int $count ссылка для возвращения числа переводов строк
      817.      * @return boolean 
      818.      */
      819.     protected function skipNL(&$count 0){
      820.         if(!($this->curChClass self::NL)) return false;
      821.         $count++;
      822.         $firstNL $this->curCh;
      823.         $nl $this->getCh();
      824.         while($this->curChClass self::NL){
      825.             // Если символ новый строки ткой же как и первый увеличиваем счетчик
      826.             // новых строк. Это сработает при любых сочетаниях
      827.             // \r\n\r\n, \r\r, \n\n - две перевода
      828.             if($nl == $firstNL$count++;
      829.             $nl $this->getCh();
      830.             // Между переводами строки могут встречаться пробелы
      831.             $this->skipSpaces();
      832.         }
      833.         return true;
      834.     }
      835.  
      836.     protected function dash(&$dash){
      837.         if($this->curCh != '-'return false;
      838.         $dash '';
      839.         $this->saveState();
      840.         $this->getCh();
      841.         // Несколько подряд
      842.         while($this->curCh == '-'$this->getCh();
      843.         if(!$this->skipNL(&& !$this->skipSpaces()){
      844.             $this->restoreState();
      845.             return false;
      846.         }
      847.         $dash $this->dash;
      848.         return true;
      849.     }
      850.  
      851.     protected function punctuation(&$punctuation){
      852.         if(!($this->curChClass self::PUNCTUATUON)) return false;
      853.         $this->saveState();
      854.         $punctuation $this->curCh;
      855.         $this->getCh();
      856.  
      857.         // Проверяем ... и !!! и ?.. и !..
      858.         if($punctuation == '.' && $this->curCh == '.'){
      859.             while($this->curCh == '.'$this->getCh();
      860.             $punctuation $this->dotes;
      861.         elseif($punctuation == '!' && $this->curCh == '!'){
      862.             while($this->curCh == '!'$this->getCh();
      863.             $punctuation '!!!';
      864.         elseif (($punctuation == '?' || $punctuation == '!'&& $this->curCh == '.'){
      865.             while($this->curCh == '.'$this->getCh();
      866.             $punctuation.= '..';
      867.         }
      868.  
      869.         // Далее идёт слово - добавляем пробел
      870.         if($this->curChClass self::RUS{
      871.             if($punctuation != '.'$punctuation.= ' ';
      872.             return true;
      873.         // Далее идёт пробел, перенос строки, конец текста
      874.         elseif(($this->curChClass self::SPACE|| ($this->curChClass self::NL|| !$this->curChClass){
      875.             return true;
      876.         else {
      877.             $this->restoreState();
      878.             return false;
      879.         }
      880.     }
      881.  
      882.     protected function number(&$num){
      883.         if(!(($this->curChClass self::NUMERIC== self::NUMERIC)) return false;
      884.         $num $this->curCh;
      885.         $this->getCh();
      886.         while(($this->curChClass self::NUMERIC== self::NUMERIC){
      887.             $num.= $this->curCh;
      888.             $this->getCh();
      889.         }
      890.         return true;
      891.     }
      892.  
      893.     protected function htmlEntity(&$entityCh){
      894.         if($this->curCh<>'&'return false;
      895.         $this->saveState();
      896.         $this->matchCh('&');
      897.         if($this->matchCh('#')){
      898.             $entityCode 0;
      899.             if(!$this->number($entityCode|| !$this->matchCh(';')){
      900.                 $this->restoreState();
      901.                 return false;
      902.             }
      903.             $entityCh = html_entity_decode("&#$entityCode;"ENT_COMPAT'UTF-8');
      904.             return true;
      905.         else{
      906.             $entityName '';
      907.             if(!$this->name($entityName|| !$this->matchCh(';')){
      908.                 $this->restoreState();
      909.                 return false;
      910.             }
      911.             $entityCh = html_entity_decode("&$entityName;"ENT_COMPAT'UTF-8');
      912.             return true;
      913.         }
      914.     }
      915.  
      916.     /**
      917.      * Кавычка
      918.      *
      919.      * @param boolean $spacesBefore были до этого пробелы
      920.      * @param string $quote кавычка
      921.      * @param boolean $closed закрывающаяся
      922.      * @return boolean 
      923.      */
      924.     protected function quote($spacesBefore,  &$quote&$closed){
      925.         $this->saveState();
      926.         $quote $this->curCh;
      927.         $this->getCh();
      928.         // Если не одна кавычка ещё не была открыта и следующий символ - не буква - то это нифига не кавычка
      929.         if($this->quotesOpened == && !(($this->curChClass self::ALPHA|| ($this->curChClass self::NUMERIC))) {
      930.             $this->restoreState();
      931.             return false;
      932.         }
      933.         // Закрывается тогда, одна из кавычек была открыта и (до кавычки не было пробела или пробел или пунктуация есть после кавычки)
      934.         // Или, если открыто больше двух кавычек - точно закрываем
      935.         $closed =  ($this->quotesOpened >= 2||
      936.               (($this->quotesOpened >  0&&
      937.                (!$spacesBefore || $this->curChClass self::SPACE || $this->curChClass self::PUNCTUATUON));
      938.         return true;
      939.     }
      940.  
      941.     protected function makeQuote($closed$level){
      942.         $levels = count($this->textQuotes);
      943.         if($level $levels$level $levels;
      944.         return $this->textQuotes[$level][$closed 0];
      945.     }
      946.  
      947.  
      948.     protected function text(&$text){
      949.         $text '';
      950.         //$punctuation = '';
      951.         $dash '';
      952.         $newLine true;
      953.         $newWord true// Возможно начало нового слова
      954.         $url null;
      955.         $href null;
      956.  
      957.         // Включено типографирование?
      958.         //$typoEnabled = true;
      959.         $typoEnabled !$this->noTypoMode;
      960.  
      961.         // Первый символ может быть <, это значит что tag() вернул false
      962.         // и < к тагу не относится
      963.         while(($this->curCh != '<'&& $this->curChClass){
      964.             $brCount 0;
      965.             $spCount 0;
      966.             $quote null;
      967.             $closed false;
      968.             $punctuation null;
      969.             $entity null;
      970.  
      971.             $this->skipSpaces($spCount);
      972.  
      973.             // автопреобразование сущностей...
      974.             if (!$spCount && $this->curCh == '&' && $this->htmlEntity($entity)){
      975.                 $text.= isset($this->entities2[$entity]$this->entities2[$entity$entity;
      976.             elseif ($typoEnabled && ($this->curChClass self::PUNCTUATUON&& $this->punctuation($punctuation)){
      977.                 // Автопунктуация выключена
      978.                 // Если встретилась пунктуация - добавляем ее
      979.                 // Сохраняем пробел перед точкой если класс следующий символ - латиница
      980.                 if($spCount && $punctuation == '.' && ($this->curChClass self::LAT)) $punctuation ' '.$punctuation;
      981.                 $text.=$punctuation;
      982.                 $newWord true;
      983.             elseif ($typoEnabled && ($spCount || $newLine&& $this->curCh == '-' && $this->dash($dash)){
      984.                 // Тире
      985.                 $text.=$dash;
      986.                 $newWord true;
      987.             elseif ($typoEnabled && ($this->curChClass self::HTML_QUOTE&& $this->quote($spCount$quote$closed)){
      988.                 // Кавычки
      989.                 $this->quotesOpened+=$closed ? -1;
      990.                 // Исправляем ситуацию если кавычка закрыввается раньше чем открывается
      991.                 if($this->quotesOpened<0){
      992.                     $closed false;
      993.                     $this->quotesOpened=1;
      994.                 }
      995.                 $quote $this->makeQuote($closed$closed $this->quotesOpened : $this->quotesOpened-1);
      996.                 if($spCount$quote ' '.$quote;
      997.                 $text.= $quote;
      998.                 $newWord true;
      999.             elseif ($spCount>0){
      1000.                 $text.=' ';
      1001.                 // после пробелов снова возможно новое слово
      1002.                 $newWord true;
      1003.             elseif ($this->isAutoBrMode && $this->skipNL($brCount)){
      1004.                 // Перенос строки
      1005.                 if ($this->curParentTag
      1006.                   and isset($this->tagsRules[$this->curParentTag])
      1007.                   and isset($this->tagsRules[$this->curParentTag][self::TR_TAG_NO_AUTO_BR])
      1008.                   and (is_null($this->openedTagor isset($this->tagsRules[$this->openedTag][self::TR_TAG_NO_AUTO_BR]))
      1009.                   {
      1010.                   // пропускаем 
      1011.                 else {
      1012.                   $br $this->br.$this->nl;
      1013.                   $text.= $brCount == $br $br.$br;
      1014.                 }
      1015.                 // Помечаем что новая строка и новое слово
      1016.                 $newLine true;
      1017.                 $newWord true;
      1018.                 // !!!Добавление слова
      1019.             elseif ($newWord && $this->isAutoLinkMode && ($this->curChClass self::LAT&& $this->openedTag!='a' && $this->url($url$href)){
      1020.                 // URL
      1021.                 $text.= $this->makeTag('a' array('href' => $href)$urlfalse);
      1022.             elseif($this->curChClass self::PRINATABLE){
      1023.                 // Экранируем символы HTML которые нельзя сувать внутрь тега (но не те? которые не могут быть в параметрах)
      1024.                 $text.=isset($this->entities2[$this->curCh]$this->entities2[$this->curCh$this->curCh;
      1025.                 $this->getCh();
      1026.                 $newWord false;
      1027.                 $newLine false;
      1028.                 // !!!Добавление к слова
      1029.             else {
      1030.                 // Совершенно непечатаемые символы которые никуда не годятся
      1031.                 $this->getCh();
      1032.             }
      1033.         }
      1034.  
      1035.         // Пробелы
      1036.         $this->skipSpaces();
      1037.         return $text != '';
      1038.     }
      1039.  
      1040.     protected function url(&$url&$href){
      1041.         $this->saveState();
      1042.         $url '';
      1043.         //$name = $this->name();
      1044.         //switch($name)
      1045.         $urlChMask self::URL self::ALPHA;
      1046.  
      1047.         if($this->matchStr('http://')){
      1048.             while($this->curChClass $urlChMask){
      1049.                 $url.= $this->curCh;
      1050.                 $this->getCh();
      1051.             }
      1052.  
      1053.             if(!mb_strlen($url'UTF-8')) {
      1054.                 $this->restoreState();
      1055.                 return false;
      1056.             }
      1057.  
      1058.             $href 'http://'.$url;
      1059.             return true;
      1060.         elseif($this->matchStr('www.')){
      1061.             while($this->curChClass $urlChMask){
      1062.                 $url.= $this->curCh;
      1063.                 $this->getCh();
      1064.             }
      1065.  
      1066.             if(!mb_strlen($url'UTF-8')) {
      1067.                 $this->restoreState();
      1068.                 return false;
      1069.             }
      1070.  
      1071.             $url 'www.'.$url;
      1072.             $href 'http://'.$url;
      1073.             return true;
      1074.         }
      1075.         $this->restoreState();
      1076.         return false;
      1077.     }
      1078.  
      1079.     protected function eror($message){
      1080.         $str '';
      1081.         $strEnd = min($this->curPos + 8$this->textLen);
      1082.         for($i $this->curPos$i $strEnd$i++){
      1083.             $str.=$this->textBuf[$i];
      1084.         }
      1085.  
      1086.         $this->errors[array(
      1087.             'message' => $message,
      1088.             'pos'     => $this->curPos,
      1089.             'ch'      => $this->curCh,
      1090.             'line'    => 0,
      1091.             'str'     => $str,
      1092.         );
      1093.     }
      1094. }
      1095.  
      1096. /**
      1097.  * Функция ord() для мультибайтовы строк
      1098.  *
      1099.  * @param string $c символ utf-8
      1100.  * @return int код символа
      1101.  */
      1102. function uniord($c{
      1103.     $h = ord($c{0});
      1104.     if ($h <= 0x7F{
      1105.     return $h;
      1106.     else if ($h 0xC2{
      1107.     return false;
      1108.     else if ($h <= 0xDF{
      1109.     return ($h 0x1F<< (ord($c{1}0x3F);
      1110.     else if ($h <= 0xEF{
      1111.     return ($h 0x0F<< 12 (ord($c{1}0x3F<< 6
      1112.                  | (ord($c{2}0x3F);
      1113.     else if ($h <= 0xF4{
      1114.     return ($h 0x0F<< 18 (ord($c{1}0x3F<< 12
      1115.                  | (ord($c{2}0x3F<< 6
      1116.                  | (ord($c{3}0x3F);
      1117.     else {
      1118.     return false;
      1119.     }
      1120. }
      1121.  
      1122. /**
      1123.  * Функция chr() для мультибайтовы строк
      1124.  *
      1125.  * @param int $c код символа
      1126.  * @return string символ utf-8
      1127.  */
      1128. function unichr($c{
      1129.     if ($c <= 0x7F{
      1130.     return chr($c);
      1131.     else if ($c <= 0x7FF{
      1132.     return chr(0xC0 $c >> 6. chr(0x80 $c 0x3F);
      1133.     else if ($c <= 0xFFFF{
      1134.     return chr(0xE0 $c >> 12. chr(0x80 $c >> 0x3F)
      1135.                     . chr(0x80 $c 0x3F);
      1136.     else if ($c <= 0x10FFFF{
      1137.     return chr(0xF0 $c >> 18. chr(0x80 $c >> 12 0x3F)
      1138.                     . chr(0x80 $c >> 0x3F)
      1139.                     . chr(0x80 $c 0x3F);
      1140.     else {
      1141.     return false;
      1142.     }
      1143. }
      1144. ?>
      В создании документации нам помог: phpDocumentor