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

Source for file Jevix.class.php

Documentation is available at Jevix.class.php

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