, http://ur001.habrahabr.ru * @version 1.01 * * История версий: * 1.01 * + cfgSetAutoReplace теперь регистронезависимый * + Возможность указать через cfgSetTagIsEmpty теги с пустым содержанием, которые не будут адалены парсером (rus.engine) * + фикс бага удаления контента тега при разном регистре открывающего и закрывающего тегов (rus.engine) * + Исправлено поведение парсера при установке правила sfgParamsAutoAdd(). Теперь * параметр устанавливается только в том случае, если его вообще нет в * обрабатываемом тексте. Если есть - оставляется оригинальное значение. (deadyaga) * 1.00 * + Исправлен баг с закрывающимися тегами приводящий к созданию непарного тега рушащего вёрстку * 1.00 RC2 * + Небольшая чистка кода * 1.00 RC1 * + Добавлен символьный класс Jevix::RUS для определния русских символов * + Авторасстановка пробелов после пунктуации только для кирилицы * + Добавлена настройка cfgSetTagNoTypography() отключающая типографирование в указанном теге * + Немного переделан алгоритм обработки кавычек. Он стал более строгим * + Знак дюйма 33" больше не превращается в открывающуюся кавычку. Однако варриант "мой 24" монитор" - парсер не переварит. * 0.99 * + Расширена функциональность для проверки атрибутов тега: * можно указать тип атрибута ( 'colspan'=>'#int', 'value' => '#text' ) * в Jevix, по-умолчанию, определён массив типов для нескольких стандартных атрибутов (src, href, width, height) * 0.98 * + Расширена функциональность для проверки атрибутов тега: * можно задавать список дозможных значений атрибута ( 'align'=>array('left', 'right', 'center') ) * 0.97 * + Обычные "кавычки" сохраняются как "e; если они были так написаны * 0.96 * + Добавлены разрешённые протоколы https и ftp для ссылок (a href="https://...) * 0.95 * + Исправлено типографирование ?.. и !.. (две точки в конце больше не превращаются в троеточие) * + Отключено автоматическое добавление пробела после точки для латиницы из-за чего невозможно было написать * index.php или .htaccess * 0.94 * + Добавлена настройка автодобавления параметров тегов. Непример rel = "nofolow" для ссылок. * Спасибо Myroslav Holyak (vbhjckfd@gmail.com) * 0.93 * + Исправлен баг с удалением пробелов (например в "123 — 123") * + Исправлена ошибка из-за которой иногда не срабатывало автоматическое преобразования URL в ссылу * + Добавлена настройка cfgSetAutoLinkMode для отключения автоматического преобразования URL в ссылки * + Автодобавление пробела после точки, если после неё идёт русский символ * 0.92 * + Добавлена настройка cfgSetAutoBrMode. При установке в false, переносы строк не будут автоматически заменяться на BR * + Изменена обработка HTML-сущностей. Теперь все сущности имеющие эквивалент в Unicode (за исключением <>) * автоматически преобразуются в символ * 0.91 * + Добавлена обработка преформатированных тегов
, . Для задания используйте cfgSetTagPreformatted()
 *  + Добавлена настройка cfgSetXHTMLMode. При отключении пустые теги будут оформляться как 
, при включенном -
* + Несколько незначительных багфиксов * 0.9 * + Первый бета-релиз */ class Jevix{ const PRINATABLE = 0x1; const ALPHA = 0x2; const LAT = 0x4; const RUS = 0x8; const NUMERIC = 0x10; const SPACE = 0x20; const NAME = 0x40; const URL = 0x100; const NOPRINT = 0x200; const PUNCTUATUON = 0x400; //const = 0x800; //const = 0x1000; const HTML_QUOTE = 0x2000; const TAG_QUOTE = 0x4000; const QUOTE_CLOSE = 0x8000; const NL = 0x10000; const QUOTE_OPEN = 0; const STATE_TEXT = 0; const STATE_TAG_PARAMS = 1; const STATE_TAG_PARAM_VALUE = 2; const STATE_INSIDE_TAG = 3; const STATE_INSIDE_NOTEXT_TAG = 4; const STATE_INSIDE_PREFORMATTED_TAG = 5; public $tagsRules = array(); public $entities0 = array('"'=>'"', "'"=>''', '&'=>'&', '<'=>'<', '>'=>'>'); public $entities1 = array(); public $entities2 = array('<'=>'<', '>'=>'>', '"'=>'"'); public $textQuotes = array(array('«', '»'), array('„', '“')); public $dash = " — "; public $apostrof = "’"; public $dotes = "…"; public $nl = "\r\n"; public $defaultTagParamRules = array('href' => '#link', 'src' => '#image', 'width' => '#int', 'height' => '#int', 'text' => '#text', 'title' => '#text'); protected $text; protected $textBuf; protected $textLen = 0; protected $curPos; protected $curCh; protected $curChOrd; protected $curChClass; protected $states; protected $quotesOpened = 0; protected $brAdded = 0; protected $state; protected $tagsStack; protected $openedTag; protected $autoReplace; // Автозамена protected $isXHTMLMode = true; //
, protected $isAutoBrMode = true; // \n =
protected $isAutoLinkMode = true; protected $br = "
"; protected $noTypoMode = false; public $outBuffer = ''; public $errors; /** * Константы для класификации тегов * */ const TR_TAG_ALLOWED = 1; // Тег позволен const TR_PARAM_ALLOWED = 2; // Параметр тега позволен (a->title, a->src, i->alt) const TR_PARAM_REQUIRED = 3; // Параметр тега влятся необходимым (a->href, img->src) const TR_TAG_SHORT = 4; // Тег может быть коротким (img, br) const TR_TAG_CUT = 5; // Тег необходимо вырезать вместе с контентом (script, iframe) const TR_TAG_CHILD = 6; // Тег может содержать другие теги const TR_TAG_CONTAINER = 7; // Тег может содержать лишь указанные теги. В нём не может быть текста const TR_TAG_CHILD_TAGS = 8; // Теги которые может содержать внутри себя другой тег const TR_TAG_PARENT = 9; // Тег в котором должен содержаться данный тег const TR_TAG_PREFORMATTED = 10; // Преформатированные тег, в котором всё заменяется на HTML сущности типа
 сохраняя все отступы и пробелы
        const TR_PARAM_AUTO_ADD = 11;    // Auto add parameters + default values (a->rel[=nofollow])
        const TR_TAG_NO_TYPOGRAPHY = 12; // Отключение типографирования для тега
        const TR_TAG_IS_EMPTY = 13;              // Не короткий тег с пустым содержанием имеет право существовать

        /**
         * Классы символов генерируются symclass.php
         *
         * @var array
         */
        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,);

        /**
         * Установка конфигурационного флага для одного или нескольких тегов
         *
         * @param array|string $tags тег(и)
         * @param int $flag флаг
         * @param mixed $value значеник=е флага
         * @param boolean $createIfNoExists если тег ещё не определён - создть его
         */
        protected function _cfgSetTagsFlag($tags, $flag, $value, $createIfNoExists = true){
                if(!is_array($tags)) $tags = array($tags);
                foreach($tags as $tag){
                        if(!isset($this->tagsRules[$tag])) {
                                if($createIfNoExists){
                                        $this->tagsRules[$tag] = array();
                                } else {
                                        throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
                                }
                        }
                        $this->tagsRules[$tag][$flag] = $value;
                }
        }

        /**
         * КОНФИГУРАЦИЯ: Разрешение или запрет тегов
         * Все не разрешённые теги считаются запрещёнными
         * @param array|string $tags тег(и)
         */
        function cfgAllowTags($tags){
                $this->_cfgSetTagsFlag($tags, self::TR_TAG_ALLOWED, true);
        }

        /**
         * КОНФИГУРАЦИЯ: Коротие теги типа 
         * @param array|string $tags тег(и)
         */
        function cfgSetTagShort($tags){
                $this->_cfgSetTagsFlag($tags, self::TR_TAG_SHORT, true, false);
        }

        /**
         * КОНФИГУРАЦИЯ: Преформатированные теги, в которых всё заменяется на HTML сущности типа 
         * @param array|string $tags тег(и)
         */
        function cfgSetTagPreformatted($tags){
                $this->_cfgSetTagsFlag($tags, self::TR_TAG_PREFORMATTED, true, false);
        }

        /**
         * КОНФИГУРАЦИЯ: Теги в которых отключено типографирование типа 
         * @param array|string $tags тег(и)
         */
        function cfgSetTagNoTypography($tags){
                $this->_cfgSetTagsFlag($tags, self::TR_TAG_NO_TYPOGRAPHY, true, false);
        }

        /**
         * КОНФИГУРАЦИЯ: Не короткие теги которые не нужно удалять с пустым содержанием, например, 
         * @param array|string $tags тег(и)
         */
        function cfgSetTagIsEmpty($tags){
                $this->_cfgSetTagsFlag($tags, self::TR_TAG_IS_EMPTY, true, false);
        }

        /**
         * КОНФИГУРАЦИЯ: Тег необходимо вырезать вместе с контентом (script, iframe)
         * @param array|string $tags тег(и)
         */
        function cfgSetTagCutWithContent($tags){
                $this->_cfgSetTagsFlag($tags, self::TR_TAG_CUT, true);
        }

        /**
         * КОНФИГУРАЦИЯ: Добавление разрешённых параметров тега
         * @param string $tag тег
         * @param string|array $params разрешённые параметры
         */
        function cfgAllowTagParams($tag, $params){
                if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
                if(!is_array($params)) $params = array($params);
                // Если ключа со списком разрешенных параметров не существует - создаём ео
                if(!isset($this->tagsRules[$tag][self::TR_PARAM_ALLOWED])) {
                        $this->tagsRules[$tag][self::TR_PARAM_ALLOWED] = array();
                }
                foreach($params as $key => $value){
                        if(is_string($key)){
                                $this->tagsRules[$tag][self::TR_PARAM_ALLOWED][$key] = $value;
                        } else {
                                $this->tagsRules[$tag][self::TR_PARAM_ALLOWED][$value] = true;
                        }
                }
        }

        /**
         * КОНФИГУРАЦИЯ: Добавление необходимых параметров тега
         * @param string $tag тег
         * @param string|array $params разрешённые параметры
         */
        function cfgSetTagParamsRequired($tag, $params){
                if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
                if(!is_array($params)) $params = array($params);
                // Если ключа со списком разрешенных параметров не существует - создаём ео
                if(!isset($this->tagsRules[$tag][self::TR_PARAM_REQUIRED])) {
                        $this->tagsRules[$tag][self::TR_PARAM_REQUIRED] = array();
                }
                foreach($params as $param){
                        $this->tagsRules[$tag][self::TR_PARAM_REQUIRED][$param] = true;
                }
        }

        /* КОНФИГУРАЦИЯ: Установка тегов которые может содержать тег-контейнер
         * @param string $tag тег
         * @param string|array $childs разрешённые теги
         * @param boolean $isContainerOnly тег является только контейнером других тегов и не может содержать текст
         * @param boolean $isChildOnly вложенные теги не могут присутствовать нигде кроме указанного тега
         */
        function cfgSetTagChilds($tag, $childs, $isContainerOnly = false, $isChildOnly = false){
                if(!isset($this->tagsRules[$tag])) throw new Exception("Тег $tag отсутствует в списке разрешённых тегов");
                if(!is_array($childs)) $childs = array($childs);
                // Тег является контейнером и не может содержать текст
                if($isContainerOnly) $this->tagsRules[$tag][self::TR_TAG_CONTAINER] = true;
                // Если ключа со списком разрешенных тегов не существует - создаём ео
                if(!isset($this->tagsRules[$tag][self::TR_TAG_CHILD_TAGS])) {
                        $this->tagsRules[$tag][self::TR_TAG_CHILD_TAGS] = array();
                }
                foreach($childs as $child){
                        $this->tagsRules[$tag][self::TR_TAG_CHILD_TAGS][$child] = true;
                        //  Указанный тег должен сущеаствовать в списке тегов
                        if(!isset($this->tagsRules[$child])) throw new Exception("Тег $child отсутствует в списке разрешённых тегов");
                        if(!isset($this->tagsRules[$child][self::TR_TAG_PARENT])) $this->tagsRules[$child][self::TR_TAG_PARENT] = array();
                        $this->tagsRules[$child][self::TR_TAG_PARENT][$tag] = true;
                        // Указанные разрешённые теги могут находится только внтутри тега-контейнера
                        if($isChildOnly) $this->tagsRules[$child][self::TR_TAG_CHILD] = true;
                }
        }

    /**
     * CONFIGURATION: Adding autoadd attributes and their values to tag
     * @param string $tag tag
     * @param string|array $params array of pairs attributeName => attributeValue
     */
    function cfgSetTagParamsAutoAdd($tag, $params){
        if(!isset($this->tagsRules[$tag])) throw new Exception("Tag $tag is missing in allowed tags list");
        if(!is_array($params)) $params = array($params);
        if(!isset($this->tagsRules[$tag][self::TR_PARAM_AUTO_ADD])) {
            $this->tagsRules[$tag][self::TR_PARAM_AUTO_ADD] = array();
        }
        foreach($params as $param => $value){
            $this->tagsRules[$tag][self::TR_PARAM_AUTO_ADD][$param] = $value;
        }
    }


        /**
         * Автозамена
         *
         * @param array $from с
         * @param array $to на
         */
        function cfgSetAutoReplace($from, $to){
                $this->autoReplace = array('from' => $from, 'to' => $to);
        }

        /**
         * Включение или выключение режима XTML
         *
         * @param boolean $isXHTMLMode
         */
        function cfgSetXHTMLMode($isXHTMLMode){
                $this->br = $isXHTMLMode ? '
' : '
'; $this->isXHTMLMode = $isXHTMLMode; } /** * Включение или выключение режима замены новых строк на
* * @param boolean $isAutoBrMode */ function cfgSetAutoBrMode($isAutoBrMode){ $this->isAutoBrMode = $isAutoBrMode; } /** * Включение или выключение режима автоматического определения ссылок * * @param boolean $isAutoLinkMode */ function cfgSetAutoLinkMode($isAutoLinkMode){ $this->isAutoLinkMode = $isAutoLinkMode; } protected function &strToArray($str){ $chars = null; preg_match_all('/./su', $str, $chars); return $chars[0]; } function parse($text, &$errors){ $this->curPos = -1; $this->curCh = null; $this->curChOrd = 0; $this->state = self::STATE_TEXT; $this->states = array(); $this->quotesOpened = 0; $this->noTypoMode = false; // Авто растановка BR? if($this->isAutoBrMode) { $this->text = preg_replace('/(\r\n|\n\r|\n)?/ui', $this->nl, $text); } else { $this->text = $text; } if(!empty($this->autoReplace)){ $this->text = str_ireplace($this->autoReplace['from'], $this->autoReplace['to'], $this->text); } $this->textBuf = $this->strToArray($this->text); $this->textLen = count($this->textBuf); $this->getCh(); $content = ''; $this->outBuffer=''; $this->brAdded=0; $this->tagsStack = array(); $this->openedTag = null; $this->errors = array(); $this->skipSpaces(); $this->anyThing($content); $errors = $this->errors; return $content; } /** * Получение следующего символа из входной строки * @return string считанный символ */ protected function getCh(){ return $this->goToPosition($this->curPos+1); } /** * Перемещение на указанную позицию во входной строке и считывание символа * @return string символ в указанной позиции */ protected function goToPosition($position){ $this->curPos = $position; if($this->curPos < $this->textLen){ $this->curCh = $this->textBuf[$this->curPos]; $this->curChOrd = uniord($this->curCh); $this->curChClass = $this->getCharClass($this->curChOrd); } else { $this->curCh = null; $this->curChOrd = 0; $this->curChClass = 0; } return $this->curCh; } /** * Сохранить текущее состояние * */ protected function saveState(){ $state = array( 'pos' => $this->curPos, 'ch' => $this->curCh, 'ord' => $this->curChOrd, 'class' => $this->curChClass, ); $this->states[] = $state; return count($this->states)-1; } /** * Восстановить * */ protected function restoreState($index = null){ if(!count($this->states)) throw new Exception('Конец стека'); if($index == null){ $state = array_pop($this->states); } else { if(!isset($this->states[$index])) throw new Exception('Неверный индекс стека'); $state = $this->states[$index]; $this->states = array_slice($this->states, 0, $index); } $this->curPos = $state['pos']; $this->curCh = $state['ch']; $this->curChOrd = $state['ord']; $this->curChClass = $state['class']; } /** * Проверяет точное вхождение символа в текущей позиции * Если символ соответствует указанному автомат сдвигается на следующий * * @param string $ch * @return boolean */ protected function matchCh($ch, $skipSpaces = false){ if($this->curCh == $ch) { $this->getCh(); if($skipSpaces) $this->skipSpaces(); return true; } return false; } /** * Проверяет точное вхождение символа указанного класса в текущей позиции * Если символ соответствует указанному классу автомат сдвигается на следующий * * @param int $chClass класс символа * @return string найденый символ или false */ protected function matchChClass($chClass, $skipSpaces = false){ if(($this->curChClass & $chClass) == $chClass) { $ch = $this->curCh; $this->getCh(); if($skipSpaces) $this->skipSpaces(); return $ch; } return false; } /** * Проверка на точное совпадение строки в текущей позиции * Если строка соответствует указанной автомат сдвигается на следующий после строки символ * * @param string $str * @return boolean */ protected function matchStr($str, $skipSpaces = false){ $this->saveState(); $len = strlen($str); $test = ''; while($len-- && $this->curChClass){ $test.=$this->curCh; $this->getCh(); } if($test == $str) { if($skipSpaces) $this->skipSpaces(); return true; } else { $this->restoreState(); return false; } } /** * Пропуск текста до нахождения указанного символа * * @param string $ch сиимвол * @return string найденый символ или false */ protected function skipUntilCh($ch){ $chPos = strpos($this->text, $ch, $this->curPos); if($chPos){ return $this->goToPosition($chPos); } else { return false; } } /** * Пропуск текста до нахождения указанной строки или символа * * @param string $str строка или символ ля поиска * @return boolean */ protected function skipUntilStr($str){ $str = $this->strToArray($str); $firstCh = $str[0]; $len = count($str); while($this->curChClass){ if($this->curCh == $firstCh){ $this->saveState(); $this->getCh(); $strOK = true; for($i = 1; $i<$len ; $i++){ // Конец строки if(!$this->curChClass){ return false; } // текущий символ не равен текущему символу проверяемой строки? if($this->curCh != $str[$i]){ $strOK = false; break; } // Следующий символ $this->getCh(); } // При неудаче откатываемся с переходим на следующий символ if(!$strOK){ $this->restoreState(); } else { return true; } } // Следующий символ $this->getCh(); } return false; } /** * Возвращает класс символа * * @return int */ protected function getCharClass($ord){ return isset($this->chClasses[$ord]) ? $this->chClasses[$ord] : self::PRINATABLE; } /*function isSpace(){ return $this->curChClass == slf::SPACE; }*/ /** * Пропуск пробелов * */ protected function skipSpaces(&$count = 0){ while($this->curChClass == self::SPACE) { $this->getCh(); $count++; } return $count > 0; } /** * Получает име (тега, параметра) по принципу 1 сиивол далее цифра или символ * * @param string $name */ protected function name(&$name = '', $minus = false){ if(($this->curChClass & self::LAT) == self::LAT){ $name.=$this->curCh; $this->getCh(); } else { return false; } while((($this->curChClass & self::NAME) == self::NAME || ($minus && $this->curCh=='-'))){ $name.=$this->curCh; $this->getCh(); } $this->skipSpaces(); return true; } protected function tag(&$tag, &$params, &$content, &$short){ $this->saveState(); $params = array(); $tag = ''; $closeTag = ''; $params = array(); $short = false; if(!$this->tagOpen($tag, $params, $short)) return false; // Короткая запись тега if($short) return true; // Сохраняем кавычки и состояние //$oldQuotesopen = $this->quotesOpened; $oldState = $this->state; $oldNoTypoMode = $this->noTypoMode; //$this->quotesOpened = 0; // Если в теге не должно быть текста, а только другие теги // Переходим в состояние self::STATE_INSIDE_NOTEXT_TAG if(!empty($this->tagsRules[$tag][self::TR_TAG_PREFORMATTED])){ $this->state = self::STATE_INSIDE_PREFORMATTED_TAG; } elseif(!empty($this->tagsRules[$tag][self::TR_TAG_CONTAINER])){ $this->state = self::STATE_INSIDE_NOTEXT_TAG; } elseif(!empty($this->tagsRules[$tag][self::TR_TAG_NO_TYPOGRAPHY])) { $this->noTypoMode = true; $this->state = self::STATE_INSIDE_TAG; } else { $this->state = self::STATE_INSIDE_TAG; } // Контент тега array_push($this->tagsStack, $tag); $this->openedTag = $tag; $content = ''; if($this->state == self::STATE_INSIDE_PREFORMATTED_TAG){ $this->preformatted($content, $tag); } else { $this->anyThing($content, $tag); } array_pop($this->tagsStack); $this->openedTag = !empty($this->tagsStack) ? array_pop($this->tagsStack) : null; $isTagClose = $this->tagClose($closeTag); if($isTagClose && ($tag != $closeTag)) { $this->eror("Неверный закрывающийся тег $closeTag. Ожидалось закрытие $tag"); //$this->restoreState(); } // Восстанавливаем предыдущее состояние и счетчик кавычек $this->state = $oldState; $this->noTypoMode = $oldNoTypoMode; //$this->quotesOpened = $oldQuotesopen; return true; } protected function preformatted(&$content = '', $insideTag = null){ while($this->curChClass){ if($this->curCh == '<'){ $tag = ''; $this->saveState(); // Пытаемся найти закрывающийся тег $isClosedTag = $this->tagClose($tag); // Возвращаемся назад, если тег был найден if($isClosedTag) $this->restoreState(); // Если закрылось то, что открылось - заканчиваем и возвращаем true if($isClosedTag && $tag == $insideTag) return; } $content.= isset($this->entities2[$this->curCh]) ? $this->entities2[$this->curCh] : $this->curCh; $this->getCh(); } } protected function tagOpen(&$name, &$params, &$short = false){ $restore = $this->saveState(); // Открытие if(!$this->matchCh('<')) return false; $this->skipSpaces(); if(!$this->name($name)){ $this->restoreState(); return false; } $name=strtolower($name); // Пробуем получить список атрибутов тега if($this->curCh != '>' && $this->curCh != '/') $this->tagParams($params); // Короткая запись тега $short = !empty($this->tagsRules[$name][self::TR_TAG_SHORT]); // Short && XHTML && !Slash || Short && !XHTML && !Slash = ERROR $slash = $this->matchCh('/'); //if(($short && $this->isXHTMLMode && !$slash) || (!$short && !$this->isXHTMLMode && $slash)){ if(!$short && $slash){ $this->restoreState(); return false; } $this->skipSpaces(); // Закрытие if(!$this->matchCh('>')) { $this->restoreState($restore); return false; } $this->skipSpaces(); return true; } protected function tagParams(&$params = array()){ $name = null; $value = null; while($this->tagParam($name, $value)){ $params[$name] = $value; $name = ''; $value = ''; } return count($params) > 0; } protected function tagParam(&$name, &$value){ $this->saveState(); if(!$this->name($name, true)) return false; if(!$this->matchCh('=', true)){ // Стремная штука - параметр без значения , if(($this->curCh=='>' || ($this->curChClass & self::LAT) == self::LAT)){ $value = null; return true; } else { $this->restoreState(); return false; } } $quote = $this->matchChClass(self::TAG_QUOTE, true); if(!$this->tagParamValue($value, $quote)){ $this->restoreState(); return false; } if($quote && !$this->matchCh($quote, true)){ $this->restoreState(); return false; } $this->skipSpaces(); return true; } protected function tagParamValue(&$value, $quote){ if($quote !== false){ // Нормальный параметр с кавычкамию Получаем пока не кавычки и не конец $escape = false; while($this->curChClass && ($this->curCh != $quote || $escape)){ $escape = false; // Экранируем символы HTML которые не могут быть в параметрах $value.=isset($this->entities1[$this->curCh]) ? $this->entities1[$this->curCh] : $this->curCh; // Символ ескейпа if($this->curCh == '\\') $escape = true; $this->getCh(); } } else { // долбаный параметр без кавычек. получаем его пока не пробел и не > и не конец while($this->curChClass && !($this->curChClass & self::SPACE) && $this->curCh != '>'){ // Экранируем символы HTML которые не могут быть в параметрах $value.=isset($this->entities1[$this->curCh]) ? $this->entities1[$this->curCh] : $this->curCh; $this->getCh(); } } return true; } protected function tagClose(&$name){ $this->saveState(); if(!$this->matchCh('<')) return false; $this->skipSpaces(); if(!$this->matchCh('/')) { $this->restoreState(); return false; } $this->skipSpaces(); if(!$this->name($name)){ $this->restoreState(); return false; } $name=strtolower($name); $this->skipSpaces(); if(!$this->matchCh('>')) { $this->restoreState(); return false; } return true; } protected function makeTag($tag, $params, $content, $short, $parentTag = null){ $tag = strtolower($tag); // Получаем правила фильтрации тега $tagRules = isset($this->tagsRules[$tag]) ? $this->tagsRules[$tag] : null; // Проверка - родительский тег - контейнер, содержащий только другие теги (ul, table, etc) $parentTagIsContainer = $parentTag && isset($this->tagsRules[$parentTag][self::TR_TAG_CONTAINER]); // Вырезать тег вместе с содержанием if($tagRules && isset($this->tagsRules[$tag][self::TR_TAG_CUT])) return ''; // Позволен ли тег if(!$tagRules || empty($tagRules[self::TR_TAG_ALLOWED])) return $parentTagIsContainer ? '' : $content; // Если тег находится внутри другого - может ли он там находится? if($parentTagIsContainer){ if(!isset($this->tagsRules[$parentTag][self::TR_TAG_CHILD_TAGS][$tag])) return ''; } // Тег может находится только внтури другого тега if(isset($tagRules[self::TR_TAG_CHILD])){ if(!isset($tagRules[self::TR_TAG_PARENT][$parentTag])) return $content; } $resParams = array(); foreach($params as $param=>$value){ $param = strtolower($param); $value = trim($value); if(empty($value)) continue; // Атрибут тега разрешён? Какие возможны значения? Получаем список правил $paramAllowedValues = isset($tagRules[self::TR_PARAM_ALLOWED][$param]) ? $tagRules[self::TR_PARAM_ALLOWED][$param] : false; if(empty($paramAllowedValues)) continue; // Если есть список разрешённых параметров тега if(is_array($paramAllowedValues) && !in_array($value, $paramAllowedValues)) { $this->eror("Недопустимое значение для атрибута тега $tag $param=$value"); continue; // Если атрибут тега помечен как разрешённый, но правила не указаны - смотрим в массив стандартных правил для атрибутов } elseif($paramAllowedValues === true && !empty($this->defaultTagParamRules[$param])){ $paramAllowedValues = $this->defaultTagParamRules[$param]; } if(is_string($paramAllowedValues)){ switch($paramAllowedValues){ case '#int': if(!is_numeric($value)) { $this->eror("Недопустимое значение для атрибута тега $tag $param=$value. Ожидалось число"); continue(2); } break; case '#text': $value = htmlspecialchars($value); break; case '#link': // Ява-скрипт в ссылке if(preg_match('/javascript:/ui', $value)) { $this->eror('Попытка вставить JavaScript в URI'); continue(2); } // Первый символ должен быть a-z0-9! if(!preg_match('/^[a-z0-9\/]/ui', $value)) { $this->eror('URI: Первый символ адреса должен быть буквой или цифрой'); continue(2); } // HTTP в начале если нет if(!preg_match('/^(http|https|ftp):\/\//ui', $value) && !preg_match('/^\//ui', $value)) $value = 'http://'.$value; break; case '#image': // Ява-скрипт в пути к картинке if(preg_match('/javascript:/ui', $value)) { $this->eror('Попытка вставить JavaScript в пути к изображению'); continue(2); } // HTTP в начале если нет if(!preg_match('/^http:\/\//ui', $value) && !preg_match('/^\//ui', $value)) $value = 'http://'.$value; break; default: $this->eror("Неверное описание атрибута тега в настройке Jevix: $param => $paramAllowedValues"); continue(2); break; } } $resParams[$param] = $value; } // Проверка обязятельных параметров тега // Если нет обязательных параметров возвращаем только контент $requiredParams = isset($tagRules[self::TR_PARAM_REQUIRED]) ? array_keys($tagRules[self::TR_PARAM_REQUIRED]) : array(); if($requiredParams){ foreach($requiredParams as $requiredParam){ if(empty($resParams[$requiredParam])) return $content; } } // Автодобавляемые параметры if(!empty($tagRules[self::TR_PARAM_AUTO_ADD])){ foreach($tagRules[self::TR_PARAM_AUTO_ADD] as $name => $value) { // If there isn't such attribute - setup it if(!array_key_exists($name, $resParams)) { $resParams[$name] = $value; } } } // Пустой некороткий тег удаляем кроме исключений if (!isset($tagRules[self::TR_TAG_IS_EMPTY]) or !$tagRules[self::TR_TAG_IS_EMPTY]) { if(!$short && empty($content)) return ''; } // Собираем тег $text='<'.$tag; // Параметры foreach($resParams as $param=>$value) $text.=' '.$param.'="'.$value.'"'; // Закрытие тега (если короткий то без контента) $text.= $short && $this->isXHTMLMode ? '/>' : '>'; if(isset($tagRules[self::TR_TAG_CONTAINER])) $text .= "\r\n"; if(!$short) $text.= $content.''; if($parentTagIsContainer) $text .= "\r\n"; if($tag == 'br') $text.="\r\n"; return $text; } protected function comment(){ if(!$this->matchStr(''); } protected function anyThing(&$content = '', $parentTag = null){ $this->skipNL(); while($this->curChClass){ $tag = ''; $params = null; $text = null; $shortTag = false; $name = null; // Если мы находимся в режиме тега без текста // пропускаем контент пока не встретится < if($this->state == self::STATE_INSIDE_NOTEXT_TAG && $this->curCh!='<'){ $this->skipUntilCh('<'); } // <Тег> кекст if($this->curCh == '<' && $this->tag($tag, $params, $text, $shortTag)){ // Преобразуем тег в текст $tagText = $this->makeTag($tag, $params, $text, $shortTag, $parentTag); $content.=$tagText; // Пропускаем пробелы после
и запрещённых тегов, которые вырезаются парсером if ($tag=='br') { $this->skipNL(); } elseif (empty($tagText)){ $this->skipSpaces(); } // Коментарий } elseif($this->curCh == '<' && $this->comment()){ continue; // Конец тега или символ < } elseif($this->curCh == '<') { // Если встречается <, но это не тег // то это либо закрывающийся тег либо знак < $this->saveState(); if($this->tagClose($name)){ // Если это закрывающийся тег, то мы делаем откат // и выходим из функции // Но если мы не внутри тега, то просто пропускаем его if($this->state == self::STATE_INSIDE_TAG || $this->state == self::STATE_INSIDE_NOTEXT_TAG) { $this->restoreState(); return false; } else { $this->eror('Не ожидалось закрывающегося тега '.$name); } } else { if($this->state != self::STATE_INSIDE_NOTEXT_TAG) $content.=$this->entities2['<']; $this->getCh(); } // Текст } elseif($this->text($text)){ $content.=$text; } } return true; } /** * Пропуск переводов строк подсчет кол-ва * * @param int $count ссылка для возвращения числа переводов строк * @return boolean */ protected function skipNL(&$count = 0){ if(!($this->curChClass & self::NL)) return false; $count++; $firstNL = $this->curCh; $nl = $this->getCh(); while($this->curChClass & self::NL){ // Если символ новый строки ткой же как и первый увеличиваем счетчик // новых строк. Это сработает при любых сочетаниях // \r\n\r\n, \r\r, \n\n - две перевода if($nl == $firstNL) $count++; $nl = $this->getCh(); // Между переводами строки могут встречаться пробелы $this->skipSpaces(); } return true; } protected function dash(&$dash){ if($this->curCh != '-') return false; $dash = ''; $this->saveState(); $this->getCh(); // Несколько подряд while($this->curCh == '-') $this->getCh(); if(!$this->skipNL() && !$this->skipSpaces()){ $this->restoreState(); return false; } $dash = $this->dash; return true; } protected function punctuation(&$punctuation){ if(!($this->curChClass & self::PUNCTUATUON)) return false; $this->saveState(); $punctuation = $this->curCh; $this->getCh(); // Проверяем ... и !!! и ?.. и !.. if($punctuation == '.' && $this->curCh == '.'){ while($this->curCh == '.') $this->getCh(); $punctuation = $this->dotes; } elseif($punctuation == '!' && $this->curCh == '!'){ while($this->curCh == '!') $this->getCh(); $punctuation = '!!!'; } elseif (($punctuation == '?' || $punctuation == '!') && $this->curCh == '.'){ while($this->curCh == '.') $this->getCh(); $punctuation.= '..'; } // Далее идёт слово - добавляем пробел if($this->curChClass & self::RUS) { if($punctuation != '.') $punctuation.= ' '; return true; // Далее идёт пробел, перенос строки, конец текста } elseif(($this->curChClass & self::SPACE) || ($this->curChClass & self::NL) || !$this->curChClass){ return true; } else { $this->restoreState(); return false; } } protected function number(&$num){ if(!(($this->curChClass & self::NUMERIC) == self::NUMERIC)) return false; $num = $this->curCh; $this->getCh(); while(($this->curChClass & self::NUMERIC) == self::NUMERIC){ $num.= $this->curCh; $this->getCh(); } return true; } protected function htmlEntity(&$entityCh){ if($this->curCh<>'&') return false; $this->saveState(); $this->matchCh('&'); if($this->matchCh('#')){ $entityCode = 0; if(!$this->number($entityCode) || !$this->matchCh(';')){ $this->restoreState(); return false; } $entityCh = html_entity_decode("&#$entityCode;", ENT_COMPAT, 'UTF-8'); return true; } else{ $entityName = ''; if(!$this->name($entityName) || !$this->matchCh(';')){ $this->restoreState(); return false; } $entityCh = html_entity_decode("&$entityName;", ENT_COMPAT, 'UTF-8'); return true; } } /** * Кавычка * * @param boolean $spacesBefore были до этого пробелы * @param string $quote кавычка * @param boolean $closed закрывающаяся * @return boolean */ protected function quote($spacesBefore, &$quote, &$closed){ $this->saveState(); $quote = $this->curCh; $this->getCh(); // Если не одна кавычка ещё не была открыта и следующий символ - не буква - то это нифига не кавычка if($this->quotesOpened == 0 && !(($this->curChClass & self::ALPHA) || ($this->curChClass & self::NUMERIC))) { $this->restoreState(); return false; } // Закрывается тогда, одна из кавычек была открыта и (до кавычки не было пробела или пробел или пунктуация есть после кавычки) // Или, если открыто больше двух кавычек - точно закрываем $closed = ($this->quotesOpened >= 2) || (($this->quotesOpened > 0) && (!$spacesBefore || $this->curChClass & self::SPACE || $this->curChClass & self::PUNCTUATUON)); return true; } protected function makeQuote($closed, $level){ $levels = count($this->textQuotes); if($level > $levels) $level = $levels; return $this->textQuotes[$level][$closed ? 1 : 0]; } protected function text(&$text){ $text = ''; //$punctuation = ''; $dash = ''; $newLine = true; $newWord = true; // Возможно начало нового слова $url = null; $href = null; // Включено типографирование? //$typoEnabled = true; $typoEnabled = !$this->noTypoMode; // Первый символ может быть <, это значит что tag() вернул false // и < к тагу не относится while(($this->curCh != '<') && $this->curChClass){ $brCount = 0; $spCount = 0; $quote = null; $closed = false; $punctuation = null; $entity = null; $this->skipSpaces($spCount); // автопреобразование сущностей... if (!$spCount && $this->curCh == '&' && $this->htmlEntity($entity)){ $text.= isset($this->entities2[$entity]) ? $this->entities2[$entity] : $entity; } elseif ($typoEnabled && ($this->curChClass & self::PUNCTUATUON) && $this->punctuation($punctuation)){ // Автопунктуация выключена // Если встретилась пунктуация - добавляем ее // Сохраняем пробел перед точкой если класс следующий символ - латиница if($spCount && $punctuation == '.' && ($this->curChClass & self::LAT)) $punctuation = ' '.$punctuation; $text.=$punctuation; $newWord = true; } elseif ($typoEnabled && ($spCount || $newLine) && $this->curCh == '-' && $this->dash($dash)){ // Тире $text.=$dash; $newWord = true; } elseif ($typoEnabled && ($this->curChClass & self::HTML_QUOTE) && $this->quote($spCount, $quote, $closed)){ // Кавычки $this->quotesOpened+=$closed ? -1 : 1; // Исправляем ситуацию если кавычка закрыввается раньше чем открывается if($this->quotesOpened<0){ $closed = false; $this->quotesOpened=1; } $quote = $this->makeQuote($closed, $closed ? $this->quotesOpened : $this->quotesOpened-1); if($spCount) $quote = ' '.$quote; $text.= $quote; $newWord = true; } elseif ($spCount>0){ $text.=' '; // после пробелов снова возможно новое слово $newWord = true; } elseif ($this->isAutoBrMode && $this->skipNL($brCount)){ // Перенос строки $br = $this->br.$this->nl; $text.= $brCount == 1 ? $br : $br.$br; // Помечаем что новая строка и новое слово $newLine = true; $newWord = true; // !!!Добавление слова } elseif ($newWord && $this->isAutoLinkMode && ($this->curChClass & self::LAT) && $this->openedTag!='a' && $this->url($url, $href)){ // URL $text.= $this->makeTag('a' , array('href' => $href), $url, false); } elseif($this->curChClass & self::PRINATABLE){ // Экранируем символы HTML которые нельзя сувать внутрь тега (но не те? которые не могут быть в параметрах) $text.=isset($this->entities2[$this->curCh]) ? $this->entities2[$this->curCh] : $this->curCh; $this->getCh(); $newWord = false; $newLine = false; // !!!Добавление к слова } else { // Совершенно непечатаемые символы которые никуда не годятся $this->getCh(); } } // Пробелы $this->skipSpaces(); return $text != ''; } protected function url(&$url, &$href){ $this->saveState(); $url = ''; //$name = $this->name(); //switch($name) $urlChMask = self::URL | self::ALPHA; if($this->matchStr('http://')){ while($this->curChClass & $urlChMask){ $url.= $this->curCh; $this->getCh(); } if(!strlen($url)) { $this->restoreState(); return false; } $href = 'http://'.$url; return true; } elseif($this->matchStr('www.')){ while($this->curChClass & $urlChMask){ $url.= $this->curCh; $this->getCh(); } if(!strlen($url)) { $this->restoreState(); return false; } $url = 'www.'.$url; $href = 'http://'.$url; return true; } $this->restoreState(); return false; } protected function eror($message){ $str = ''; $strEnd = min($this->curPos + 8, $this->textLen); for($i = $this->curPos; $i < $strEnd; $i++){ $str.=$this->textBuf[$i]; } $this->errors[] = array( 'message' => $message, 'pos' => $this->curPos, 'ch' => $this->curCh, 'line' => 0, 'str' => $str, ); } } /** * Функция ord() для мультибайтовы строк * * @param string $c символ utf-8 * @return int код символа */ function uniord($c) { $h = ord($c{0}); if ($h <= 0x7F) { return $h; } else if ($h < 0xC2) { return false; } else if ($h <= 0xDF) { return ($h & 0x1F) << 6 | (ord($c{1}) & 0x3F); } else if ($h <= 0xEF) { return ($h & 0x0F) << 12 | (ord($c{1}) & 0x3F) << 6 | (ord($c{2}) & 0x3F); } else if ($h <= 0xF4) { return ($h & 0x0F) << 18 | (ord($c{1}) & 0x3F) << 12 | (ord($c{2}) & 0x3F) << 6 | (ord($c{3}) & 0x3F); } else { return false; } } /** * Функция chr() для мультибайтовы строк * * @param int $c код символа * @return string символ utf-8 */ function unichr($c) { if ($c <= 0x7F) { return chr($c); } else if ($c <= 0x7FF) { return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F); } else if ($c <= 0xFFFF) { return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F) . chr(0x80 | $c & 0x3F); } else if ($c <= 0x10FFFF) { return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F) . chr(0x80 | $c >> 6 & 0x3F) . chr(0x80 | $c & 0x3F); } else { return false; } } ?>