wiki:devel:parser
Различия
Показаны различия между двумя версиями страницы.
| — | wiki:devel:parser [2025/01/17 13:53] (текущий) – создано - внешнее изменение 127.0.0.1 | ||
|---|---|---|---|
| Строка 1: | Строка 1: | ||
| + | ====== Парсер «Докувики» ====== | ||
| + | |||
| + | В этом документе излагаются детали функционирования парсера «[[wiki: | ||
| + | |||
| + | ===== Обзор ===== | ||
| + | |||
| + | Парсер разбивает процесс трансформации исходного документа «Докувики» в финальный выходной документ (обычно XHTML) на дискретные стадии. Каждая стадия представлена одним или несколькими PHP-классами. | ||
| + | |||
| + | В общем рассмотрении этими элементами являются; | ||
| + | |||
| + | - Lexer((__L__exer относится к классу '' | ||
| + | - Handler((__H__andler относится к классу '' | ||
| + | - Parser((__P__arser относится к классу '' | ||
| + | - Renderer((__R__enderer (:!: от «to render» //в значении// | ||
| + | |||
| + | Механизма для соединения Handler (обработчика) | ||
| + | |||
| + | Схематическая диаграмма связей между компонентами; | ||
| + | |||
| + | ┌────────────┐ | ||
| + | │ │ | ||
| + | │ | ||
| + | │ │ Строка | ||
| + | └─────┬──────┘ | ||
| + | Режимы | ||
| + | + | ||
| + | Строка | ||
| + | ввода | ||
| + | ┌─────▼──────┐ | ||
| + | │ │ | ||
| + | │ Анализатор ├───────────► Обработчик │ | ||
| + | │ │ Вхождения │ │ | ||
| + | └─────┬──────┘ | ||
| + | │ | ||
| + | │ | ||
| + | | ||
| + | │ Режимы ├─┐ | ||
| + | | ||
| + | | ||
| + | | ||
| + | |||
| + | «Клиентский код» (код, использующий парсер) вызывает парсер, | ||
| + | |||
| + | **Примечание: | ||
| + | |||
| + | ==== Лексический анализатор ==== | ||
| + | |||
| + | Определяется в [[xref> | ||
| + | |||
| + | В самом общем смысле, | ||
| + | |||
| + | * поддержка шаблонов просмотра назад и просмотра вперед((Прим. переводчика: | ||
| + | * поддержка изменения модификаторов шаблона в пределах шаблона; | ||
| + | * уведомление обработчика о начальном индексе байта в необработанном тексте, | ||
| + | |||
| + | Короче говоря, | ||
| + | |||
| + | В целом Lexer состоит из трех основных классов; | ||
| + | |||
| + | * [[xref> | ||
| + | * [[xref> | ||
| + | * [[xref> | ||
| + | |||
| + | === Необходимость в состояниях === | ||
| + | |||
| + | Синтаксис вики, используемый в «Докувики», | ||
| + | |||
| + | Анализатор обеспечивает «осведомлённость о состояниях», | ||
| + | |||
| + | === Режимы анализатора === | ||
| + | |||
| + | Термин // | ||
| + | |||
| + | === API анализатора === | ||
| + | |||
| + | Краткое введение в лексический анализатор можно найти в [[wiki: | ||
| + | |||
| + | Ключевыми методами анализатора являются: | ||
| + | |||
| + | == Конструктор == | ||
| + | |||
| + | Принимает ссылку на объект Handler, имя начального режима, | ||
| + | |||
| + | Пример: | ||
| + | |||
| + | <code php> | ||
| + | $handler | ||
| + | $lexer | ||
| + | </ | ||
| + | |||
| + | Здесь указан начальный режим ' | ||
| + | |||
| + | == addEntryPattern / addExitPattern == | ||
| + | |||
| + | '' | ||
| + | <code php Lexer.php [enable_line_numbers=" | ||
| + | public function addEntryPattern($pattern, | ||
| + | { | ||
| + | if (! isset($this-> | ||
| + | $this-> | ||
| + | } | ||
| + | $this-> | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | '' | ||
| + | <code php Lexer.php [enable_line_numbers=" | ||
| + | public function addExitPattern($pattern, | ||
| + | { | ||
| + | if (! isset($this-> | ||
| + | $this-> | ||
| + | } | ||
| + | $this-> | ||
| + | } | ||
| + | </ | ||
| + | [[xref> | ||
| + | |||
| + | <code php> | ||
| + | // arg0: регулярное выражение для сопоставления — обратите внимание, | ||
| + | // arg1: имя режима, | ||
| + | // arg2: имя режима для ввода | ||
| + | $lexer -> addEntryPattern ( '< | ||
| + | |||
| + | // arg0: регулярное выражение для сопоставления | ||
| + | // arg1: имя режима для выхода | ||
| + | $lexer -> addExitPattern ( '</ | ||
| + | </ | ||
| + | |||
| + | Код, приведённый выше, позволяет тэгу %%< | ||
| + | |||
| + | **Замечание: | ||
| + | |||
| + | == addPattern == | ||
| + | |||
| + | '' | ||
| + | <code php Lexer.php [enable_line_numbers=" | ||
| + | public function addPattern($pattern, | ||
| + | { | ||
| + | if (! isset($this-> | ||
| + | $this-> | ||
| + | } | ||
| + | $this-> | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | '' | ||
| + | <code php ParallelRegex.php [enable_line_numbers=" | ||
| + | public function addPattern($pattern, | ||
| + | { | ||
| + | $count = count($this-> | ||
| + | $this-> | ||
| + | $this-> | ||
| + | $this-> | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | [[xref> | ||
| + | |||
| + | Это наиболее наглядно видно из разбора парсером синтаксиса списков. Синтаксис списков выглядит в «Докувики» следующим образом; | ||
| + | |||
| + | < | ||
| + | До списка | ||
| + | * Ненумерованный элемент списка | ||
| + | * Ненумерованный элемент списка | ||
| + | * Ненумерованный элемент списка | ||
| + | После списка | ||
| + | </ | ||
| + | |||
| + | Использование '' | ||
| + | |||
| + | <code php> | ||
| + | // Сопоставляем открывающий элемент списка и меняем режим | ||
| + | $lexer -> addEntryPattern ( '\n {2, | ||
| + | |||
| + | // Сопоставить новые элементы списка, | ||
| + | $lexer -> addPattern ( '\n {2, | ||
| + | |||
| + | // Если это перевод строки, | ||
| + | $lexer -> addExitPattern ( ' | ||
| + | </ | ||
| + | |||
| + | == addSpecialPattern == | ||
| + | |||
| + | '' | ||
| + | <code php Lexer.php [enable_line_numbers=" | ||
| + | public function addSpecialPattern($pattern, | ||
| + | { | ||
| + | if (! isset($this-> | ||
| + | $this-> | ||
| + | } | ||
| + | $this-> | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | [[xref> | ||
| + | |||
| + | <code php> | ||
| + | $lexer -> addSpecialPattern ( ': | ||
| + | </ | ||
| + | |||
| + | == mapHandler == | ||
| + | |||
| + | '' | ||
| + | <code php Lexer.php [enable_line_numbers=" | ||
| + | public function mapHandler($mode, | ||
| + | { | ||
| + | $this-> | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | [[xref> | ||
| + | |||
| + | <code php> | ||
| + | $lexer -> addEntryPattern ( '< | ||
| + | $lexer -> addEntryPattern ( ' | ||
| + | $lexer -> addExitPattern ( '</ | ||
| + | $lexer -> addExitPattern ( ' | ||
| + | |||
| + | // Оба синтаксиса должны обрабатываться одинаково... | ||
| + | $lexer -> mapHandler ( ' | ||
| + | </ | ||
| + | |||
| + | === Подшаблоны не допускаются === | ||
| + | |||
| + | Поскольку Lexer (анализатор) сам использует **подшаблоны** (внутри класса '' | ||
| + | |||
| + | **Замечание: | ||
| + | |||
| + | === Синтаксические ошибки и состояния === | ||
| + | |||
| + | Для предотвращение «плохо форматируемой» (особенно при пропуске закрывающих тэгов) разметки, | ||
| + | |||
| + | <code php> | ||
| + | // Использование просмотра вперёд во входном шаблоне... | ||
| + | // Использовать предпросмотр в шаблоне записи... | ||
| + | $lexer -> addEntryPattern ( '< | ||
| + | $lexer -> addExitPattern ( '</ | ||
| + | </ | ||
| + | |||
| + | Шаблон входа проверяет, | ||
| + | |||
| + | |||
| + | ==== Handler (Обработчик) ==== | ||
| + | |||
| + | Определено в [[xref> | ||
| + | |||
| + | < | ||
| + | |AAA||AAA{text-align: | ||
| + | |)|BBB|BBB{text-align: | ||
| + | |)|CCC|CCC{text-align: | ||
| + | |)|DDD|DDD{text-align: | ||
| + | |)|EEE|EEE{text-align: | ||
| + | |)|FFF|FFF{text-align: | ||
| + | |)|GGG|GGG{text-align: | ||
| + | |)|KKK|KKK{text-align: | ||
| + | |)|LLL|LLL{text-align: | ||
| + | |)|MMM|MMM{text-align: | ||
| + | |`|NNN|NNN{text-align: | ||
| + | </ | ||
| + | |||
| + | Handler — это класс, предоставляющий методы, | ||
| + | |||
| + | Обработчик в целом содержит следующие классы: | ||
| + | |||
| + | * [[wiki: | ||
| + | |||
| + | '' | ||
| + | <code php SyntaxPlugin.php > | ||
| + | | ||
| + | 57 * @see Doku_Handler_Block | ||
| + | 75 * @param Doku_Handler $handler The Doku_Handler object | ||
| + | 77 * @param | ||
| + | 80 abstract public function handle($match, | ||
| + | </ | ||
| + | '' | ||
| + | <code php Parser.php > | ||
| + | 6 use Doku_Handler; | ||
| + | 17 /** @var Doku_Handler */ | ||
| + | 32 * @param Doku_Handler $handler | ||
| + | 34 public function __construct(Doku_Handler $handler) | ||
| + | </ | ||
| + | '' | ||
| + | <code php handler.php > | ||
| + | 16 * Class Doku_Handler | ||
| + | 18 class Doku_Handler { | ||
| + | 40 * Doku_Handler constructor. | ||
| + | 795 $link[1] = Doku_Handler_Parse_Media($link[1]); | ||
| + | 878 $p = Doku_Handler_Parse_Media($match); | ||
| + | 1023 function Doku_Handler_Parse_Media($match) { | ||
| + | </ | ||
| + | |||
| + | * [[xref> | ||
| + | * [[xref> | ||
| + | * [[xref> | ||
| + | * [[xref> | ||
| + | * [[xref> | ||
| + | * [[xref> | ||
| + | * [[xref> | ||
| + | * [[xref> | ||
| + | |||
| + | === Методы токенов обработчиков === | ||
| + | |||
| + | Обработчик должен предоставлять методы, | ||
| + | |||
| + | Например, | ||
| + | |||
| + | <code php> | ||
| + | $lexer-> | ||
| + | $lexer-> | ||
| + | </ | ||
| + | |||
| + | Обработчику понадобится такой метод: | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Handler { | ||
| + | |||
| + | /** | ||
| + | * @param string match содержит совпавший текст | ||
| + | * @param int state - тип найденного соответствия (см. ниже) | ||
| + | * @param int pos - индекс байта, где было найдено совпадение | ||
| + | */ | ||
| + | public function file($match, | ||
| + | return true; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **Примечание: | ||
| + | |||
| + | Аргументы, | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | - '' | ||
| + | - '' | ||
| + | - '' | ||
| + | - '' | ||
| + | - '' | ||
| + | * '' | ||
| + | |||
| + | В качестве более сложного примера, | ||
| + | |||
| + | <code php> | ||
| + | function connectTo($mode) { | ||
| + | $this-> | ||
| + | $this-> | ||
| + | | ||
| + | $this-> | ||
| + | $this-> | ||
| + | | ||
| + | } | ||
| + | | ||
| + | function postConnect() { | ||
| + | $this-> | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Метод '' | ||
| + | |||
| + | <code php> | ||
| + | function listblock($match, | ||
| + | | ||
| + | switch ( $state ) { | ||
| + | | ||
| + | // Начало списка... | ||
| + | case DOKU_LEXER_ENTER: | ||
| + | // Создать List rewrite, пропуская текущий CallWriter | ||
| + | $ReWriter = & new Doku_Handler_List($this-> | ||
| + | | ||
| + | // Заменить текущий CallWriter на List rewriter | ||
| + | // все поступающие вхождения (даже, если они не являются вхождениями list) | ||
| + | // теперь направляются в list | ||
| + | $this-> | ||
| + | | ||
| + | $this-> | ||
| + | break; | ||
| + | | ||
| + | // Для конца списка | ||
| + | case DOKU_LEXER_EXIT: | ||
| + | $this-> | ||
| + | | ||
| + | // Дать указание List rewriter об очистке | ||
| + | $this-> | ||
| + | | ||
| + | // Восстановить прежний CallWriter | ||
| + | $ReWriter = & $this-> | ||
| + | $this-> | ||
| + | | ||
| + | break; | ||
| + | | ||
| + | case DOKU_LEXER_MATCHED: | ||
| + | $this-> | ||
| + | break; | ||
| + | | ||
| + | case DOKU_LEXER_UNMATCHED: | ||
| + | $this-> | ||
| + | break; | ||
| + | } | ||
| + | return TRUE; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | === Конвертирование вхождений === | ||
| + | |||
| + | «Тонкая обработка» задействует вставку символа дроби «/», переименование или удаление вхождений, | ||
| + | |||
| + | Например, | ||
| + | |||
| + | < | ||
| + | This is not a list | ||
| + | * This is the opening list item | ||
| + | * This is the second list item | ||
| + | * This is the last list item | ||
| + | This is also not a list | ||
| + | </ | ||
| + | |||
| + | в результате превратиться в последовательность вхождений вроде; | ||
| + | |||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | |||
| + | Но чтобы быть использованными преобразователем, | ||
| + | |||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | -'' | ||
| + | |||
| + | В случае со списками, | ||
| + | |||
| + | ==== Parser (Парсер) ==== | ||
| + | |||
| + | Определено в [[xref> | ||
| + | |||
| + | dokuwiki [[xref> | ||
| + | |||
| + | Использование парсера обычно выглядит так: | ||
| + | |||
| + | <code php> | ||
| + | // Создаем обработчик Handler | ||
| + | $handler = new Doku_Handler(); | ||
| + | |||
| + | // Создаем парсер с обработчиком | ||
| + | $parser = new dokuwiki\Parsing\Parser($handler); | ||
| + | |||
| + | // Добавить требуемые режимы синтаксиса в парсер | ||
| + | $parser-> | ||
| + | $parser-> | ||
| + | $parser-> | ||
| + | # etc. | ||
| + | |||
| + | $doc = file_get_contents(' | ||
| + | $instructions = $parser-> | ||
| + | </ | ||
| + | |||
| + | Более подробные примеры приведены ниже. | ||
| + | |||
| + | В целом Parser также содержит классы, | ||
| + | |||
| + | Причина представления режимов с помощью классов заключается в том, чтобы избежать повторных вызовов методов Lexer. Без них пришлось бы жестко кодировать каждое правило шаблона для каждого режима, | ||
| + | |||
| + | <code php> | ||
| + | $lexer-> | ||
| + | $lexer-> | ||
| + | $lexer-> | ||
| + | $lexer-> | ||
| + | $lexer-> | ||
| + | $lexer-> | ||
| + | // и т.д. | ||
| + | </ | ||
| + | |||
| + | Каждый режим, которому разрешено содержать ссылки CamelCase, должен быть явно назван. | ||
| + | |||
| + | Вместо того, чтобы жестко кодировать это, вместо этого это реализовано с использованием одного класса, | ||
| + | |||
| + | <code php> | ||
| + | namespace dokuwiki\Parsing\ParserMode; | ||
| + | |||
| + | class CamelCaseLink extends AbstractMode { | ||
| + | |||
| + | public function connectTo($mode) { | ||
| + | $this-> | ||
| + | ' | ||
| + | ); | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | При настройке лексического анализатора парсер вызывает '' | ||
| + | |||
| + | За счет усложнения понимания настройки лексического анализатора это позволяет сделать код более гибким при добавлении нового синтаксиса. | ||
| + | |||
| + | |||
| + | |||
| + | ==== Формат данных инструкций ==== | ||
| + | |||
| + | [[wiki: | ||
| + | |||
| + | Исходный текст (содержит таблицу): | ||
| + | |||
| + | < | ||
| + | abc | ||
| + | | Row 0 Col 1 | Row 0 Col 2 | Row 0 Col 3 | | ||
| + | | Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 | | ||
| + | def | ||
| + | </ | ||
| + | |||
| + | После обработки будет возвращён следующий массив PHP (описан ниже): | ||
| + | |||
| + | < | ||
| + | Array( | ||
| + | [0] => Array( | ||
| + | [0] => document_start | ||
| + | [1] => Array() | ||
| + | [2] => 0 | ||
| + | ) | ||
| + | [1] => Array( | ||
| + | [0] => p_open | ||
| + | [1] => Array() | ||
| + | [2] => 0 | ||
| + | ) | ||
| + | [2] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => | ||
| + | |||
| + | abc | ||
| + | ) | ||
| + | [2] => 0 | ||
| + | ) | ||
| + | [3] => Array( | ||
| + | [0] => p_close | ||
| + | [1] => Array() | ||
| + | [2] => 5 | ||
| + | ) | ||
| + | [4] => Array( | ||
| + | [0] => table_open | ||
| + | [1] => Array( | ||
| + | [0] => 3 | ||
| + | [1] => 2 | ||
| + | ) | ||
| + | [2] => 5 | ||
| + | ) | ||
| + | [5] => Array( | ||
| + | [0] => tablerow_open | ||
| + | [1] => Array() | ||
| + | [2] => 5 | ||
| + | ) | ||
| + | [6] => Array( | ||
| + | [0] => tablecell_open | ||
| + | [1] => Array( | ||
| + | [0] => 1 | ||
| + | [1] => left | ||
| + | ) | ||
| + | [2] => 5 | ||
| + | ) | ||
| + | [7] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => Row 0 Col 1 | ||
| + | ) | ||
| + | [2] => 7 | ||
| + | ) | ||
| + | [8] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => | ||
| + | ) | ||
| + | [2] => 19 | ||
| + | ) | ||
| + | [9] => Array( | ||
| + | [0] => tablecell_close | ||
| + | [1] => Array() | ||
| + | [2] => 23 | ||
| + | ) | ||
| + | [10] => Array( | ||
| + | [0] => tablecell_open | ||
| + | [1] => Array( | ||
| + | [0] => 1 | ||
| + | [1] => left | ||
| + | ) | ||
| + | [2] => 23 | ||
| + | ) | ||
| + | [11] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => Row 0 Col 2 | ||
| + | ) | ||
| + | [2] => 24 | ||
| + | ) | ||
| + | [12] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => | ||
| + | ) | ||
| + | [2] => 36 | ||
| + | ) | ||
| + | [13] => Array( | ||
| + | [0] => tablecell_close | ||
| + | [1] => Array() | ||
| + | [2] => 41 | ||
| + | ) | ||
| + | [14] => Array( | ||
| + | [0] => tablecell_open | ||
| + | [1] => Array( | ||
| + | [0] => 1 | ||
| + | [1] => left | ||
| + | ) | ||
| + | [2] => 41 | ||
| + | ) | ||
| + | [15] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => Row 0 Col 3 | ||
| + | ) | ||
| + | [2] => 42 | ||
| + | ) | ||
| + | [16] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => | ||
| + | ) | ||
| + | [2] => 54 | ||
| + | ) | ||
| + | [17] => Array( | ||
| + | [0] => tablecell_close | ||
| + | [1] => Array() | ||
| + | [2] => 62 | ||
| + | ) | ||
| + | [18] => Array( | ||
| + | [0] => tablerow_close | ||
| + | [1] => Array() | ||
| + | [2] => 63 | ||
| + | ) | ||
| + | [19] => Array( | ||
| + | [0] => tablerow_open | ||
| + | [1] => Array() | ||
| + | [2] => 63 | ||
| + | ) | ||
| + | [20] => Array( | ||
| + | [0] => tablecell_open | ||
| + | [1] => Array( | ||
| + | [0] => 1 | ||
| + | [1] => left | ||
| + | ) | ||
| + | [2] => 63 | ||
| + | ) | ||
| + | [21] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => Row 1 Col 1 | ||
| + | ) | ||
| + | [2] => 65 | ||
| + | ) | ||
| + | [22] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => | ||
| + | ) | ||
| + | [2] => 77 | ||
| + | ) | ||
| + | [23] => Array( | ||
| + | [0] => tablecell_close | ||
| + | [1] => Array() | ||
| + | [2] => 81 | ||
| + | ) | ||
| + | [24] => Array( | ||
| + | [0] => tablecell_open | ||
| + | [1] => Array( | ||
| + | [0] => 1 | ||
| + | [1] => left | ||
| + | ) | ||
| + | [2] => 81 | ||
| + | ) | ||
| + | [25] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => Row 1 Col 2 | ||
| + | ) | ||
| + | [2] => 82 | ||
| + | ) | ||
| + | [26] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => | ||
| + | ) | ||
| + | [2] => 94 | ||
| + | ) | ||
| + | [27] => Array( | ||
| + | [0] => tablecell_close | ||
| + | [1] => Array() | ||
| + | [2] => 99 | ||
| + | ) | ||
| + | [28] => Array( | ||
| + | [0] => tablecell_open | ||
| + | [1] => Array( | ||
| + | [0] => 1 | ||
| + | [1] => left | ||
| + | ) | ||
| + | [2] => 99 | ||
| + | ) | ||
| + | [29] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => Row 1 Col 3 | ||
| + | ) | ||
| + | [2] => 100 | ||
| + | ) | ||
| + | [30] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => | ||
| + | ) | ||
| + | [2] => 112 | ||
| + | ) | ||
| + | [31] => Array( | ||
| + | [0] => tablecell_close | ||
| + | [1] => Array() | ||
| + | [2] => 120 | ||
| + | ) | ||
| + | [32] => Array( | ||
| + | [0] => tablerow_close | ||
| + | [1] => Array() | ||
| + | [2] => 121 | ||
| + | ) | ||
| + | [33] => Array( | ||
| + | [0] => table_close | ||
| + | [1] => Array() | ||
| + | [2] => 121 | ||
| + | ) | ||
| + | [34] => Array( | ||
| + | [0] => p_open | ||
| + | [1] => Array() | ||
| + | [2] => 121 | ||
| + | ) | ||
| + | [35] => Array( | ||
| + | [0] => cdata | ||
| + | [1] => Array( | ||
| + | [0] => def | ||
| + | |||
| + | ) | ||
| + | [2] => 122 | ||
| + | ) | ||
| + | [36] => Array( | ||
| + | [0] => p_close | ||
| + | [1] => Array() | ||
| + | [2] => 122 | ||
| + | ) | ||
| + | [37] => Array( | ||
| + | [0] => document_end | ||
| + | [1] => Array() | ||
| + | [2] => 122 | ||
| + | ) | ||
| + | ) | ||
| + | </ | ||
| + | |||
| + | Верхний уровень массива --- это просто список. Каждый из его дочерних элементов описывает возвратную функцию, | ||
| + | |||
| + | === Единственная инструкция === | ||
| + | |||
| + | Рассмотрим единственный элемент, | ||
| + | |||
| + | < | ||
| + | [35] => Array | ||
| + | ( | ||
| + | [0] => cdata | ||
| + | [1] => Array | ||
| + | ( | ||
| + | [0] => def | ||
| + | |||
| + | ) | ||
| + | [2] => 122 | ||
| + | ) | ||
| + | </ | ||
| + | |||
| + | Первый элемент (индекс 0) — это имя метода или функции в Renderer, которую необходимо выполнить. | ||
| + | |||
| + | Второй элемент (индекс 1) сам по себе является массивом, | ||
| + | |||
| + | В этом случае имеется один аргумент со значением '' | ||
| + | |||
| + | <code php> | ||
| + | $Render-> | ||
| + | </ | ||
| + | |||
| + | Третий элемент (индекс 2) — это индекс байта первого символа, | ||
| + | |||
| + | **Примечание**: | ||
| + | |||
| + | Пример массив инструкций страницы с описанием [[wiki: | ||
| + | |||
| + | ==== Renderer (преобразователь) ==== | ||
| + | |||
| + | Renderer (преобразователь) --- это класс (или коллекция функций), | ||
| + | |||
| + | <code php> | ||
| + | <?php | ||
| + | class Doku_Renderer { | ||
| + | |||
| + | // вырезка | ||
| + | | ||
| + | public function header($text, | ||
| + | | ||
| + | public function section_open($level) {} | ||
| + | | ||
| + | public function section_close() {} | ||
| + | | ||
| + | public function cdata($text) {} | ||
| + | | ||
| + | public function p_open() {} | ||
| + | | ||
| + | public function p_close() {} | ||
| + | | ||
| + | public function linebreak() {} | ||
| + | | ||
| + | public function hr() {} | ||
| + | | ||
| + | // вырезка | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Он используется для документирования Renderer, хотя его также можно расширить, | ||
| + | |||
| + | Основной принцип того, как инструкции, | ||
| + | |||
| + | === Вызов рендерера с инструкциями === | ||
| + | |||
| + | Клиентскому коду, использующему Parser, остается выполнить список инструкций для Renderer. Обычно это делается с помощью функции PHP [[phpfn> | ||
| + | |||
| + | <code php> | ||
| + | // Получить список инструкций от парсера | ||
| + | $instructions = $parser-> | ||
| + | |||
| + | // Создаем рендерер | ||
| + | $renderer = new Doku_Renderer_xhtml(); | ||
| + | |||
| + | // Проходим по инструкциям | ||
| + | foreach ($instructions as $instruction) { | ||
| + | // Выполняем обратный вызов для Renderer | ||
| + | call_user_func_array([$renderer, | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | === Методы связи с рендерером === | ||
| + | |||
| + | Ключевые методы Renderer для обработки различных типов ссылок: | ||
| + | |||
| + | * '' | ||
| + | *Вероятно, | ||
| + | * '' | ||
| + | *Хотя '' | ||
| + | * '' | ||
| + | *Оба изображения '' | ||
| + | * '' | ||
| + | *Необходимость '' | ||
| + | * '' | ||
| + | *Технически должны совпадать только действительные '' | ||
| + | * '' | ||
| + | *Должен соответствовать только допустимым URL-адресам общих ресурсов Windows, но в любом случае проверять наличие '' | ||
| + | * '' | ||
| + | *'' | ||
| + | * '' | ||
| + | *Это не требует проверки — должно ссылаться только на локальные изображения. '' | ||
| + | * '' | ||
| + | *'' | ||
| + | |||
| + | Особого внимания требуют методы, | ||
| + | |||
| + | <code html> | ||
| + | <a href=" | ||
| + | </ | ||
| + | |||
| + | Аргумент '' | ||
| + | |||
| + | - '' | ||
| + | - string: в качестве заголовка использовалась простая текстовая строка | ||
| + | - array (hash): в качестве заголовка использовано изображение. | ||
| + | |||
| + | Если это '' | ||
| + | |||
| + | <code php> | ||
| + | $title = [ | ||
| + | // Может быть ' | ||
| + | ' | ||
| + | | ||
| + | // URL-адрес изображения (может быть URL-адресом wiki или https:// | ||
| + | ' | ||
| + | | ||
| + | // Для атрибута alt - строка или null | ||
| + | ' | ||
| + | | ||
| + | // ' | ||
| + | ' | ||
| + | | ||
| + | // Ширина в пикселях или null | ||
| + | ' | ||
| + | | ||
| + | // Высота в пикселях или null | ||
| + | ' | ||
| + | | ||
| + | // Кэшировать ли изображение (для внешних изображений) | ||
| + | ' | ||
| + | ]; | ||
| + | </ | ||
| + | |||
| + | ===== Примеры ===== | ||
| + | |||
| + | Следующие примеры показывают общие задачи, | ||
| + | |||
| + | ==== Основной вызов ==== | ||
| + | |||
| + | Чтобы вызвать парсер со // | ||
| + | |||
| + | <code php> | ||
| + | require_once DOKU_INC . ' | ||
| + | |||
| + | // Создать парсер | ||
| + | $Parser = & new Doku_Parser(); | ||
| + | |||
| + | // Добавить обработчик | ||
| + | $Parser-> | ||
| + | |||
| + | // Загрузить все режимы | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | |||
| + | $formats = array ( | ||
| + | ' | ||
| + | ' | ||
| + | ); | ||
| + | foreach ( $formats as $format ) { | ||
| + | $Parser-> | ||
| + | } | ||
| + | |||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | |||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | |||
| + | // Здесь требуются данные. Функции '' | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | |||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | |||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | |||
| + | // Загрузить исходный документ вики | ||
| + | $doc = file_get_contents(DOKU_DATA . ' | ||
| + | |||
| + | // Получить список инструкций | ||
| + | $instructions = $Parser-> | ||
| + | |||
| + | // Создать преобразователь | ||
| + | require_once DOKU_INC . ' | ||
| + | $Renderer = & new Doku_Renderer_XHTML(); | ||
| + | |||
| + | # Здесь загрузите в преобразователь данные (например, | ||
| + | |||
| + | // Проходимся по всем инструкциям | ||
| + | foreach ( $instructions as $instruction ) { | ||
| + | | ||
| + | // Выполняем обратный вызов через преобразователь | ||
| + | call_user_func_array(array(& | ||
| + | } | ||
| + | |||
| + | // Отображаем выходные данные | ||
| + | echo $Renderer-> | ||
| + | </ | ||
| + | |||
| + | ==== Выбор текста (для фрагментов) ==== | ||
| + | |||
| + | Следующий код показывает, | ||
| + | |||
| + | <code php> | ||
| + | // Создаём парсер | ||
| + | $Parser = & new Doku_Parser(); | ||
| + | |||
| + | // Добавляем обработчик | ||
| + | $Parser-> | ||
| + | |||
| + | // Загружаем режим header для поиска заголовков | ||
| + | $Parser-> | ||
| + | |||
| + | // Загружаем режимы, | ||
| + | // которая может быть принята за заголовок | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | |||
| + | // Загружаем исходный документ вики | ||
| + | $doc = file_get_contents(DOKU_DATA . ' | ||
| + | |||
| + | // Получаем перечень инструкций | ||
| + | $instructions = $Parser-> | ||
| + | |||
| + | // Используем эти переменные, | ||
| + | // находимся ли мы внутри необходимого фрагмента | ||
| + | $inSection = FALSE; | ||
| + | $startPos = 0; | ||
| + | $endPos = 0; | ||
| + | |||
| + | // Проходимся по всем инструкциям | ||
| + | foreach ( $instructions as $instruction ) { | ||
| + | | ||
| + | if ( !$inSection ) { | ||
| + | | ||
| + | // Ищем заголовки в списках | ||
| + | if ( $instruction[0] == ' | ||
| + | trim($instruction[1][0]) == ' | ||
| + | | ||
| + | $startPos = $instruction[2]; | ||
| + | $inSection = TRUE; | ||
| + | } | ||
| + | } else { | ||
| + | | ||
| + | // Ищем конец фрагмента | ||
| + | if ( $instruction[0] == ' | ||
| + | $endPos = $instruction[2]; | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // Нормализуем и разбиваем документ | ||
| + | $doc = " | ||
| + | |||
| + | // Получаем текст, идущий перед фрагментом, | ||
| + | $before = substr($doc, | ||
| + | $section = substr($doc, | ||
| + | $after = substr($doc, | ||
| + | </ | ||
| + | |||
| + | ==== Управление входными файлами с данными в шаблонах ==== | ||
| + | |||
| + | «Докувики» хранит части некоторых шаблонов во внешних файлах (например, | ||
| + | |||
| + | Каждый подходящий режим принимает простой список элементов, | ||
| + | |||
| + | Например: | ||
| + | |||
| + | <code php> | ||
| + | // Простой список вхождений смайлов... | ||
| + | $smileys = array( | ||
| + | ': | ||
| + | ': | ||
| + | '; | ||
| + | // и т. д. | ||
| + | ); | ||
| + | | ||
| + | // Создать режим | ||
| + | $SmileyMode = & new Doku_Parser_Mode_Smiley($smileys); | ||
| + | |||
| + | // Добавить режим в парсер | ||
| + | $Parser-> | ||
| + | </ | ||
| + | |||
| + | Для парсера не имеет значения выходной формат смайлов. | ||
| + | |||
| + | Другие режимы, | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | Конструктор каждого класса принимает в качестве параметра список указанных элементов. | ||
| + | |||
| + | На практике возникает необходимость в функциях для извлечения данных из конфигурационных файлов и размещение ассоциативных массивов в статической переменной, | ||
| + | |||
| + | <code php> | ||
| + | function getSmileys() { | ||
| + | | ||
| + | static $smileys = NULL; | ||
| + | | ||
| + | if ( !$smileys ) { | ||
| + | | ||
| + | $smileys = array(); | ||
| + | | ||
| + | $lines = file( DOKU_CONF . ' | ||
| + | | ||
| + | foreach($lines as $line){ | ||
| + | | ||
| + | // игнорировать комментарии | ||
| + | $line = preg_replace('/# | ||
| + | | ||
| + | $line = trim($line); | ||
| + | | ||
| + | if(empty($line)) continue; | ||
| + | | ||
| + | $smiley = preg_split('/ | ||
| + | | ||
| + | // Собрать ассоциативный массив | ||
| + | $smileys[$smiley[0]] = $smiley[1]; | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | return $smileys; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Эта функция может быть использована следующим образом: | ||
| + | |||
| + | <code php> | ||
| + | // Загрузить шаблоны смайлов в режим | ||
| + | $SmileyMode = & new Doku_Parser_Mode_Smiley(array_keys(getSmileys())); | ||
| + | </ | ||
| + | |||
| + | <code php> | ||
| + | // Загрузить ассоциативный массив в Преобразователь | ||
| + | $Renderer-> | ||
| + | </ | ||
| + | |||
| + | **Замечание: | ||
| + | |||
| + | ==== Проверка ссылок на спам ==== | ||
| + | |||
| + | В идеале ссылки требуется проверять на спам //до// размещения документа (после редактирования). | ||
| + | |||
| + | > Этот пример следует использовать осторожно. Он создаёт полезные точки связывания, | ||
| + | |||
| + | Это можно сделать, | ||
| + | |||
| + | Требуется функция для загрузки файла '' | ||
| + | |||
| + | > Недавно протестировал этот подход (единственное регулярное выражение) с использованием последней версии «чёрного списка» с [[http:// | ||
| + | |||
| + | <code php> | ||
| + | function getSpamPattern() { | ||
| + | static $spamPattern = NULL; | ||
| + | | ||
| + | if ( is_null($spamPattern) ) { | ||
| + | | ||
| + | $lines = @file(DOKU_CONF . ' | ||
| + | | ||
| + | if ( !$lines ) { | ||
| + | | ||
| + | $spamPattern = ''; | ||
| + | | ||
| + | } else { | ||
| + | | ||
| + | $spamPattern = '#'; | ||
| + | $sep = ''; | ||
| + | | ||
| + | foreach($lines as $line){ | ||
| + | | ||
| + | $line = preg_replace('/# | ||
| + | | ||
| + | // Игнорировать пустые строки | ||
| + | $line = trim($line); | ||
| + | if(empty($line)) continue; | ||
| + | | ||
| + | $spamPattern.= $sep.$line; | ||
| + | | ||
| + | $sep = ' | ||
| + | } | ||
| + | | ||
| + | $spamPattern .= '# | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | return $spamPattern; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Теперь нам нужно расширить основной преобразователь ещё одним, который проверяет только ссылки: | ||
| + | |||
| + | <code php> | ||
| + | require_once DOKU_INC . ' | ||
| + | |||
| + | class Doku_Renderer_SpamCheck extends Doku_Renderer { | ||
| + | | ||
| + | // Здесь должен быть код, выполняющий инструкции | ||
| + | var $currentCall; | ||
| + | | ||
| + | // Массив инструкций, | ||
| + | var $spamFound = array(); | ||
| + | | ||
| + | // PCRE-шаблон для нахождения спама | ||
| + | var $spamPattern = '# | ||
| + | | ||
| + | function internallink($link, | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | function externallink($link, | ||
| + | $this-> | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | function interwikilink($link, | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | function filelink($link, | ||
| + | $this-> | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | function windowssharelink($link, | ||
| + | $this-> | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | function email($address, | ||
| + | $this-> | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | function internalmedialink ($src) { | ||
| + | $this-> | ||
| + | } | ||
| + | |||
| + | function externalmedialink($src) { | ||
| + | $this-> | ||
| + | } | ||
| + | |||
| + | function __checkTitle($title) { | ||
| + | if ( is_array($title) && isset($title[' | ||
| + | $this-> | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | // Поиск по шаблону осуществляется здесь | ||
| + | function __checkLinkForSpam($link) { | ||
| + | if( preg_match($this-> | ||
| + | $spam = $this-> | ||
| + | $spam[3] = $link; | ||
| + | $this-> | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Обратите внимание на строку '' | ||
| + | |||
| + | Наконец мы можем использовать преобразователь с проверкой на спам: | ||
| + | |||
| + | <code php> | ||
| + | // Создать парсер | ||
| + | $Parser = & new Doku_Parser(); | ||
| + | |||
| + | // Добавить обработчик | ||
| + | $Parser-> | ||
| + | |||
| + | // Добавить режимы, | ||
| + | // которая ошибочно будет принята за ссылку | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | |||
| + | // Загружаем режим link... | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | |||
| + | // Загружаем исходный документ вики | ||
| + | $doc = file_get_contents(DOKU_DATA . ' | ||
| + | |||
| + | // Получить список инструкций | ||
| + | $instructions = $Parser-> | ||
| + | |||
| + | // Создать преобразователь | ||
| + | require_once DOKU_INC . ' | ||
| + | $Renderer = & new Doku_Renderer_SpamCheck(); | ||
| + | |||
| + | // Загрузить шаблон спама | ||
| + | $Renderer-> | ||
| + | |||
| + | // Пройтись по всем инструкциям | ||
| + | foreach ( $instructions as $instruction ) { | ||
| + | | ||
| + | // Сохранить текущую инструкцию | ||
| + | $Renderer-> | ||
| + | | ||
| + | call_user_func_array(array(& | ||
| + | } | ||
| + | |||
| + | // Что за спам был найден? | ||
| + | echo '< | ||
| + | print_r($Renderer-> | ||
| + | echo '</ | ||
| + | </ | ||
| + | |||
| + | Поскольку нам не нужны все режимы синтаксиса, | ||
| + | |||
| + | ==== Добавление синтаксической конструкции ==== | ||
| + | |||
| + | **Предупреждение: | ||
| + | |||
| + | Простая задача по модификации парсера: | ||
| + | |||
| + | Синтаксис для тэга будет таким: | ||
| + | |||
| + | < | ||
| + | |||
| + | Строка «Моя закладка» является наименование закладки, | ||
| + | |||
| + | <code html>< | ||
| + | |||
| + | Добавление этой синтаксической конструкции требует следующих шагов: | ||
| + | |||
| + | - создать синтаксический режим парсера, | ||
| + | - обновить код функции '' | ||
| + | - обновить код обработчика, | ||
| + | - обновление абстрактного класса преобразователя и какого-нибудь конкретного преобразователя. | ||
| + | |||
| + | Создание режима парсера подразумевает расширение класса '' | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Parser_Mode_Bookmark extends Doku_Parser_Mode { | ||
| + | | ||
| + | function connectTo($mode) { | ||
| + | // Разрешаются слова и пробелы | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Будет осуществляться поиск целой закладки с использованием единственного шаблона (извлечение имени закладки из остального синтаксиса будет осущевляться обработчиком). Используется метод '' | ||
| + | |||
| + | **Замечание: | ||
| + | |||
| + | Поскольку ничто // | ||
| + | |||
| + | Следующая функция '' | ||
| + | |||
| + | <code php> | ||
| + | function Doku_Parser_Substition() { | ||
| + | $modes = array( | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | | ||
| + | ); | ||
| + | return $modes; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Эта функция лишь помогает в регистрации режима с другими режимами, | ||
| + | |||
| + | **Замечание: | ||
| + | |||
| + | Описав синтаксис, | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Handler { | ||
| + | |||
| + | // ... | ||
| + | | ||
| + | // $match - строка, | ||
| + | // с регулярным выражением для закладок | ||
| + | // $state идентифицирует тип совпадения (см. выше) | ||
| + | // $pos - индекс байта первого символа совпадения в исходном документе | ||
| + | function bookmark($match, | ||
| + | | ||
| + | // Технически не следует беспокоится о состоянии: | ||
| + | // оно всегда будет DOKU_LEXER_SPECIAL, | ||
| + | // нет серьёзных багов | ||
| + | switch ( $state ) { | ||
| + | | ||
| + | case DOKU_LEXER_SPECIAL: | ||
| + | | ||
| + | // Попытка извлечения наименования закладки | ||
| + | if ( preg_match('/ | ||
| + | | ||
| + | $name = $nameMatch[1]; | ||
| + | | ||
| + | // arg0: наименование вызываемого метода преобразователя | ||
| + | // arg1: массив аргументов для метода преобразователя | ||
| + | // arg2: индекс байта | ||
| + | $this-> | ||
| + | | ||
| + | // Если у закладки нет годного имени, | ||
| + | // пропускаем не меняя как cdata | ||
| + | } else { | ||
| + | | ||
| + | $this-> | ||
| + | | ||
| + | } | ||
| + | break; | ||
| + | | ||
| + | } | ||
| + | | ||
| + | // Должно вернуть TRUE или анализатор будет остановлен | ||
| + | return TRUE; | ||
| + | } | ||
| + | | ||
| + | // ... | ||
| + | | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Последний этап --- обновление кода преобразователя ('' | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Renderer { | ||
| + | |||
| + | // ... | ||
| + | | ||
| + | function bookmark($name) {} | ||
| + | |||
| + | // ... | ||
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Renderer_XHTML { | ||
| + | |||
| + | // ... | ||
| + | | ||
| + | function bookmark($name) { | ||
| + | $name = $this-> | ||
| + | | ||
| + | // id is required in XHTML while name still supported in 1.0 | ||
| + | echo '<a class=" | ||
| + | | ||
| + | } | ||
| + | |||
| + | // ... | ||
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | См. скрипт '' | ||
| + | |||
| + | ==== Добавление синтаксиса форматирования (с состоянием) ==== | ||
| + | |||
| + | **Предупреждение: | ||
| + | |||
| + | Для того, чтобы показать расширенное использование анализатора, | ||
| + | |||
| + | Разметка будет выглядеть так: | ||
| + | |||
| + | < | ||
| + | < | ||
| + | Это чёрный цвет | ||
| + | < | ||
| + | Это тоже чёрный цвет | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | Шаги, необходимые для внедрения данной возможности, | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Parser_Mode_TextColors extends Doku_Parser_Mode { | ||
| + | | ||
| + | var $color; | ||
| + | | ||
| + | var $colors = array(' | ||
| + | | ||
| + | function Doku_Parser_Mode_TextColor($color) { | ||
| + | | ||
| + | // Предотвращает ошибки использования этого режима | ||
| + | if ( !array_key_exists($color, | ||
| + | trigger_error(' | ||
| + | } | ||
| + | | ||
| + | $this-> | ||
| + | | ||
| + | // Этот режим принимает другие режимы: | ||
| + | $this-> | ||
| + | Doku_Parser_Formatting($color), | ||
| + | Doku_Parser_Substition(), | ||
| + | Doku_Parser_Disabled() | ||
| + | ); | ||
| + | | ||
| + | } | ||
| + | | ||
| + | // connectTo вызывается однократно для каждого режима, | ||
| + | function connectTo($mode) { | ||
| + | | ||
| + | // Шаблон с просмотром вперёд проверяет наличие закрывающего тэга... | ||
| + | $pattern = '<' | ||
| + | | ||
| + | // arg0: шаблон сравнения при входе в режим; | ||
| + | // arg1: другие режимы, | ||
| + | // arg2: наименование режима. | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | // post connect вызывается однократно | ||
| + | function postConnect() { | ||
| + | | ||
| + | // arg0: шаблон сравнения при выходе из режима; | ||
| + | // arg1: наименование режима. | ||
| + | $this-> | ||
| + | | ||
| + | } | ||
| + | | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Некоторые особенности вышеприведённых классов: | ||
| + | |||
| + | - В действительности представляют множество режимов, | ||
| + | - Эти режимы могут содержать, | ||
| + | - Когда регистрируется входной шаблон, | ||
| + | - Входной шаблон требует регистрации для каждого режима, | ||
| + | |||
| + | Когда с классом режимами обработки покончено, | ||
| + | |||
| + | <code php> | ||
| + | function Doku_Parser_Formatting($remove = '' | ||
| + | $modes = array( | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ); | ||
| + | $key = array_search($remove, | ||
| + | if ( is_int($key) ) { | ||
| + | unset($modes[$key]); | ||
| + | } | ||
| + | | ||
| + | return $modes; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **Замечание: | ||
| + | |||
| + | Далее обработчик должен быть обновлён методами для каждого цвета: | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Handler { | ||
| + | |||
| + | // ... | ||
| + | | ||
| + | function red($match, $state, $pos) { | ||
| + | // Метод nestingTag в обработчике предотвращает | ||
| + | // многократное повторение одного и того же кода. | ||
| + | // Он создаёт открывающий и закрывающий инструкции | ||
| + | // для входных и выходных шаблонов, | ||
| + | // пропуская остальные как cdata. | ||
| + | $this-> | ||
| + | return TRUE; | ||
| + | } | ||
| + | | ||
| + | function yellow($match, | ||
| + | $this-> | ||
| + | return TRUE; | ||
| + | } | ||
| + | | ||
| + | function green($match, | ||
| + | $this-> | ||
| + | return TRUE; | ||
| + | } | ||
| + | | ||
| + | // ... | ||
| + | | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Наконец мы может обновить преобразователи: | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Renderer { | ||
| + | |||
| + | // ... | ||
| + | | ||
| + | function red_open() {} | ||
| + | function red_close() {} | ||
| + | | ||
| + | function yellow_open() {} | ||
| + | function yellow_close() {} | ||
| + | | ||
| + | function green_open() {} | ||
| + | function green_close() {} | ||
| + | |||
| + | // ... | ||
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Renderer_XHTML { | ||
| + | |||
| + | // ... | ||
| + | | ||
| + | function red_open() { | ||
| + | echo '< | ||
| + | } | ||
| + | function red_close() { | ||
| + | echo '</ | ||
| + | } | ||
| + | | ||
| + | function yellow_open() { | ||
| + | echo '< | ||
| + | } | ||
| + | function yellow_close() { | ||
| + | echo '</ | ||
| + | } | ||
| + | | ||
| + | function green_open() { | ||
| + | echo '< | ||
| + | } | ||
| + | function green_close() { | ||
| + | echo '</ | ||
| + | } | ||
| + | |||
| + | // ... | ||
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | См. скрипт '' | ||
| + | |||
| + | ==== Добавление блочных синтаксических конструкций ==== | ||
| + | |||
| + | **Предупреждение: | ||
| + | |||
| + | Развивая предыдущий пример, | ||
| + | |||
| + | < | ||
| + | |||
| + | ===== Синтаксис цитирования в вики ===== | ||
| + | |||
| + | Этот синтаксис позволяет | ||
| + | |||
| + | < | ||
| + | Опишите синтаксис цитирования '>' | ||
| + | </ | ||
| + | |||
| + | Другой текст | ||
| + | </ | ||
| + | |||
| + | Этот синтаксис позволяет искать страницы вики и находить вопросы, | ||
| + | |||
| + | Особенностью данного синтаксиса является то, что он должен отображаться в отдельном блоке документа (например, | ||
| + | |||
| + | Режим парсера для этого синтаксиса может быть таким: | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Parser_Mode_Todo extends Doku_Parser_Mode { | ||
| + | | ||
| + | function Doku_Parser_Mode_Todo() { | ||
| + | | ||
| + | $this-> | ||
| + | Doku_Parser_Formatting(), | ||
| + | Doku_Parser_Substition(), | ||
| + | Doku_Parser_Disabled() | ||
| + | ); | ||
| + | | ||
| + | } | ||
| + | | ||
| + | function connectTo($mode) { | ||
| + | | ||
| + | $pattern = '< | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | function postConnect() { | ||
| + | $this-> | ||
| + | } | ||
| + | | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Затем этот режим добавляется в функцию '' | ||
| + | |||
| + | <code php> | ||
| + | function Doku_Parser_BlockContainers() { | ||
| + | $modes = array( | ||
| + | ' | ||
| + | // горизонтальные разрывы нарушают принцип, | ||
| + | // так что вставляем их сюда | ||
| + | ' | ||
| + | ' | ||
| + | ); | ||
| + | return $modes; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Обновление класса '' | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Handler { | ||
| + | |||
| + | // ... | ||
| + | | ||
| + | function todo($match, | ||
| + | $this-> | ||
| + | return TRUE; | ||
| + | } | ||
| + | | ||
| + | // ... | ||
| + | | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Класс '' | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Handler_Block { | ||
| + | | ||
| + | // ... | ||
| + | | ||
| + | var $blockOpen = array( | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ); | ||
| + | | ||
| + | var $blockClose = array( | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ); | ||
| + | | ||
| + | </ | ||
| + | |||
| + | Регистрация '' | ||
| + | |||
| + | После этого должен быть обновлён код преобразователя: | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Renderer { | ||
| + | |||
| + | // ... | ||
| + | | ||
| + | function todo_open() {} | ||
| + | function todo_close() {} | ||
| + | | ||
| + | // ... | ||
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | <code php> | ||
| + | class Doku_Renderer_XHTML { | ||
| + | |||
| + | // ... | ||
| + | | ||
| + | function todo_open() { | ||
| + | echo '< | ||
| + | } | ||
| + | function todo_close() { | ||
| + | echo '</ | ||
| + | } | ||
| + | | ||
| + | // ... | ||
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== Сериализация инструкций преобразователя ==== | ||
| + | |||
| + | Список выводимых обработчиком инструкций можно сериализировать, | ||
| + | |||
| + | Самая простая реализация может быть такой: | ||
| + | |||
| + | <code php> | ||
| + | $ID = DOKU_DATA . ' | ||
| + | $cacheID = DOKU_CACHE . $ID.' | ||
| + | |||
| + | // Если кэш-файл отсутствует или утратил актуальность | ||
| + | // (исходный документ модифицирован), | ||
| + | if ( !file_exists($cacheID) || (filemtime($ID) > filemtime($cacheID)) ) { | ||
| + | | ||
| + | require_once DOKU_INC . ' | ||
| + | | ||
| + | // Создать парсер | ||
| + | $Parser = & new Doku_Parser(); | ||
| + | | ||
| + | // Добавить обработчик | ||
| + | $Parser-> | ||
| + | | ||
| + | // Загрузить все режимы | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | | ||
| + | // и т. д., и т. п. | ||
| + | | ||
| + | $instructions = $Parser-> | ||
| + | | ||
| + | // Сериализировать и кэшировать | ||
| + | $sInstructions = serialize($instructions); | ||
| + | | ||
| + | if ($fh = @fopen($cacheID, | ||
| + | | ||
| + | if (fwrite($fh, | ||
| + | die(" | ||
| + | } | ||
| + | | ||
| + | fclose($fh); | ||
| + | } | ||
| + | | ||
| + | } else { | ||
| + | // Загрузить и десериализировать | ||
| + | $sInstructions = file_get_contents($cacheID); | ||
| + | $instructions = unserialize($sInstructions); | ||
| + | } | ||
| + | |||
| + | $Renderer = & new Doku_Renderer_XHTML(); | ||
| + | |||
| + | foreach ( $instructions as $instruction ) { | ||
| + | call_user_func_array( | ||
| + | array(& | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | echo $Renderer-> | ||
| + | </ | ||
| + | |||
| + | **Замечание: | ||
| + | |||
| + | ==== Сериализация парсера ==== | ||
| + | |||
| + | По аналогии с приведённым выше примером, | ||
| + | |||
| + | Если коротко, | ||
| + | |||
| + | <code php> | ||
| + | $cacheId = DOKU_CACHE . ' | ||
| + | |||
| + | if ( !file_exists($cacheId) ) { | ||
| + | |||
| + | // Создаём парсер | ||
| + | $Parser = & new Doku_Parser(); | ||
| + | $Parser-> | ||
| + | | ||
| + | // Загружаем все режимы | ||
| + | $Parser-> | ||
| + | $Parser-> | ||
| + | | ||
| + | # и т. д., и т. п. | ||
| + | | ||
| + | // ВАЖНО: вызов connectModes() | ||
| + | $Parser-> | ||
| + | | ||
| + | // Сериализация | ||
| + | $sParser = serialize($Parser); | ||
| + | | ||
| + | // Запись в файл | ||
| + | if ($fh = @fopen($cacheID, | ||
| + | | ||
| + | if (fwrite($fh, | ||
| + | die(" | ||
| + | } | ||
| + | | ||
| + | fclose($fh); | ||
| + | } | ||
| + | |||
| + | } else { | ||
| + | // Загружаем сериализированную версию | ||
| + | $sParser = file_get_contents($cacheID); | ||
| + | $Parser = unserialize($sParser); | ||
| + | } | ||
| + | |||
| + | $Parser-> | ||
| + | </ | ||
| + | |||
| + | Некоторые замечания по реализации, | ||
| + | |||
| + | * Для некоторых файлов вместо записи требуется блокировка, | ||
| + | * Что следует делать, | ||
| + | * Могут быть различные версии парсера (например, | ||
| + | |||
| + | ===== Тестирование ===== | ||
| + | |||
| + | [[http:// | ||
| + | |||
| + | Для парсера «Докувики» тесты проводились по всем внедряемым синтаксическим конструкциям, | ||
| + | |||
| + | Чтобы запустить тесты, вам следует модифицировать файл '' | ||
| + | |||
| + | Некоторые заметки и рекомендации: | ||
| + | |||
| + | - Повторно запускайте тесты каждый раз, когда вы меняете что-нибудь в парсере --- проблемы немедленно выплывают на поверхность, | ||
| + | - Это только тесты для специфических ситуаций. Они не гарантируют отсутствие ошибок, | ||
| + | - Если найдена ошибка, | ||
| + | |||
| + | ===== Ошибки и проблемы ===== | ||
| + | |||
| + | Некоторые вопросы остаются за рамками подробного рассмотрения. | ||
| + | |||
| + | ==== Важность порядка добавления режимов ==== | ||
| + | |||
| + | Требуется выполение не столько «правил», | ||
| + | |||
| + | В общем случае рекомендуется загружать режимы в порядке, | ||
| + | |||
| + | > По моим наработкам, | ||
| + | |||
| + | ==== Замены «блокиратора слов» ==== | ||
| + | |||
| + | В оригинале функционирование «блокиратора слов» '' | ||
| + | |||
| + | Рекомендация --- файл '' | ||
| + | |||
| + | ==== Слабые моменты ==== | ||
| + | |||
| + | С точки зрения архитектуры, | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | «Inline re-writers» используются, | ||
| + | |||
| + | // | ||
| + | |||
| + | Также // | ||
| + | |||
| + | Самое «больное место» --- это класс '' | ||
| + | |||
| + | ==== «Жадные» тэги ==== | ||
| + | |||
| + | Рассмотрим следующий синтаксис вики: | ||
| + | |||
| + | < | ||
| + | Привет, | ||
| + | |||
| + | ---- | ||
| + | |||
| + | < | ||
| + | </ | ||
| + | |||
| + | Пользователь забыл закрыть первый тэг %%< | ||
| + | |||
| + | В результате получится: | ||
| + | |||
| + | Привет, | ||
| + | |||
| + | ---- | ||
| + | |||
| + | < | ||
| + | |||
| + | Первый тэг %%< | ||
| + | |||
| + | Это применимо ко всем подобным режимам. Входные шаблоны проверяют наличие закрывающего тэга, но также они должны проверять, | ||
| + | |||
| + | ==== Сноски через список ==== | ||
| + | |||
| + | В сущности, | ||
| + | |||
| + | < | ||
| + | *((A)) | ||
| + | *(( B | ||
| + | * C )) | ||
| + | </ | ||
| + | |||
| + | Это будет происходить до тех пор, пока пользователи не поправят страницу. Решение --- разбить захват элементов списка в многострочные режимы (сейчас для списков есть только единственный режим '' | ||
| + | |||
| + | ==== Захват «обёрточных» символов ==== | ||
| + | |||
| + | Баг № [[bug> | ||
| + | |||
| + | Поскольку синтаксис заголовка, | ||
| + | |||
| + | < | ||
| + | До списка | ||
| + | - Элемент списка | ||
| + | - Элемент списка | ||
| + | | Ячейка A | Ячейка B | | ||
| + | | Ячейка C | Ячейка D | | ||
| + | После таблицы | ||
| + | </ | ||
| + | |||
| + | Выдаёт: | ||
| + | |||
| + | До списка | ||
| + | - Элемент списка | ||
| + | - Элемент списка | ||
| + | | Ячейка A | Ячейка B | | ||
| + | | Ячейка C | Ячейка D | | ||
| + | После таблицы | ||
| + | |||
| + | Заметьте, | ||
| + | |||
| + | Чтобы скорректировать это, синтаксис вики должен иметь дополнительную «обёртку» между списком и таблицей: | ||
| + | |||
| + | < | ||
| + | До списка | ||
| + | - Элемент списка | ||
| + | - Элемент списка | ||
| + | |||
| + | | Ячейка A | Ячейка B | | ||
| + | | Ячейка C | Ячейка D | | ||
| + | После таблицы | ||
| + | </ | ||
| + | |||
| + | Что будет выглядеть следующим образом: | ||
| + | |||
| + | До списка | ||
| + | - Элемент списка | ||
| + | - Элемент списка | ||
| + | |||
| + | | Ячейка A | Ячейка B | | ||
| + | | Ячейка C | Ячейка D | | ||
| + | После таблицы | ||
| + | |||
| + | Без сканирования текста множества раз (некая разновидность «предварительных» операций, | ||
| + | |||
| + | ==== Проблемы списков, | ||
| + | |||
| + | Для синтаксиса списков, | ||
| + | |||
| + | < | ||
| + | | Cell A | < | ||
| + | | Cell C | Cell D</ | ||
| + | | Cell E | Cell F | | ||
| + | </ | ||
| + | |||
| + | выдаёт: | ||
| + | |||
| + | | Cell A | < | ||
| + | | Cell C | Cell D</ | ||
| + | | Cell E | Cell F | | ||
| + | |||
| + | В идеале должно быть преобразовано так: | ||
| + | |||
| + | | Cell A | %%< | ||
| + | | Cell C | Cell D%%</ | ||
| + | | Cell E | Cell F | | ||
| + | |||
| + | Т. е. открывающий тэг '' | ||
| + | |||
| + | Для устранения этого требуется поддержка многострочного режима внутри таблиц, | ||
| + | |||
| + | ==== Сноски и блоки ==== | ||
| + | |||
| + | Внутри сносок блоки игнорируются, | ||
| + | |||
| + | Устраняется модификацией класса '' | ||
| + | |||
| + | ==== Заголовки ==== | ||
| + | |||
| + | Текущие заголовки могут находиться на той же строке, | ||
| + | |||
| + | < | ||
| + | До заголовка есть | ||
| + | Некоторый текст == Заголовок == | ||
| + | После заголовка | ||
| + | </ | ||
| + | |||
| + | Если бы поведение было бы таким же, как в оригинальном парсере «Докувики», | ||
| + | |||
| + | До заголовка есть | ||
| + | Некоторый текст %%== Заголовок ==%% | ||
| + | После заголовка | ||
| + | |||
| + | Но в результате будет: | ||
| + | |||
| + | До заголовка есть | ||
| + | Некоторый текст == Заголовок == | ||
| + | После заголовка | ||
| + | |||
| + | ==== Конфликт блоков и списков ==== | ||
| + | |||
| + | Существует проблема: | ||
| + | | ||
| + | * list item | ||
| + | * list item 2 | ||
| + | |||
| + | ===== Что ещё необходимо сделать ===== | ||
| + | |||
| + | Вот некоторые вопросы, | ||
| + | |||
| + | ==== Больше состояний для закрывающих инструкций ==== | ||
| + | |||
| + | Для преобразования в иные форматы, | ||
| + | |||
| + | > Почему бы просто не «преобразовать» в XML и затем применить к нему некоторые парсеры XSLT/XML? | ||
| + | |||
| + | ==== Подрежимы для таблиц, | ||
| + | |||
| + | Анализатор с множественными режимами для предотвращения случаев вложенности состояний друг в друга. | ||
| + | |||
| + | ===== Обсуждение ===== | ||
| + | |||
| + | Спасибо за перевод! :-) | ||
wiki/devel/parser.1737057969.txt.gz · Последнее изменение: (внешнее изменение)
