root/trunk/engine/lib/external/Jevix/jevix.class.php

Revision 1359, 56.5 KB (checked in by ort, 5 months ago)

Доработка jevix - возможность обрабатывать в collback весь тег, а не только его контент

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