В этом документе излагаются детали функционирования парсера Dokuwiki, которые могут понадобиться разработчикам для модификации поведения парсера или получения контроля над выходным потоком документа.
Парсер разбивает процесс трансформации исходного документа dokuwiki в финальный выходной документ (обычно XHTML) на дискретные стадии. Каждая стадия представлена одним или несколькими PHP классами.
В общем рассмотрении этими элементами являются;
Parser::parse())Механизм, связывающий обработчик с преобразователем, отсутствует - для этого требуется программирование посредством специфического прецедента.
Схематическая диаграмма связей между компонентами;
+-----------+ +-----------+
| | Ввод | Клиентский|
| Парсер |<---------| Код |
| | Строка | |
+-----.-----+ +-----|-----+
Режимы | /|\
+ | Инструкции |
Строка | Преобразователя |
Ввода \|/ |
+-----'-----+ +-----------+
| | | |
|Анализатор |--------->| Обработчик|
| | Вхождения| |
+-----.-----+ +-----------+
|
|
+----+---+
| Режимы |-+
+--------+ |-+
+--------+ |
+--------+
“Клиентский код” (код, использующий парсер) вызывает парсер, передавая ему входную строку. В ответ ему возвращается перечень “инструкций” преобразователя, построенных обработчиком. Они могут быть использованы неким объектом, реализующим преобразователь.
Замечание: Критическим моментом здесь является намерение позволить преобразователю быть настолько “тупым”, насколько это возможно. От него не требуется осуществлять дальнейшую интерпретацию / модификацию переданных инструкций, но полностью сконцентрироваться на формировании выходных данных (например, XHTML) - в особенности, преобразователю не следует отслеживать состояния. Соблюдение этого принципа, и, кроме того, составление преобразователя достаточно простым для реализации (сосредоточенной исключтельно на том, что следует выводить), также сделает возможным преобразователю быть взаимозаменяемым (например, вывод PDF в качестве альтернативы XHTML). В то же самое время, выходные инструкции обработчика направляются для преобразования в XHTML и не всегда могут быть пригодынми для всех выходных форматов.
Определяется в inc/parser/lexer.php.
В самом общем смысле, он реализует инструмент для управления комплексными регулярными выражениями, где важным является состояние. Анализатор появился из Простого теста, но содержит три модификации (читай: хака
);
Анализатор как целое состоит из трех основных классов;
Doku_LexerParallelRegex: позволяет регулярному выражению состоять из множества отдельных шаблонов, каждый шаблон связан с идентифицирующей “меткой” , класс объединяет их в единое регулярное выражение, используя подшаблоны. Используя анализатор, вам не нужно беспокоится об этом классе.Doku_LexerStateStack: реализует простую машину состояний (state mashine 9)), так что анализ может быть “осведомленным о контексте”. Используя анализатор, вам не нужно беспокоится об этом классе.Doku_Lexer: реализует точку доступа для клиентского кода, использующего анализатор. Управляет множеством объектов ParallelRegex, используя StateStack 10) для применения корректных объектов ParallelRegex, в зависимости от “контекста”. При нахождении “интересного текста” он вызывает функции реализуемого пользователем объекта (обработчика).Синтаксис вики, используемый в dokuwiki, содержит разметку, “внутри” которой применяются только определенные синтаксические правила. Самый очевидный пример – тэг <code/> , внутри которого синтаксис вики не будет распознаваться анализатором. Для других синтаксических конструкций, таких как списки или таблицы, следует позволять использовать некоторую разметку, но не всю, например, в списка можно использовать ссылки, но не таблицы.
Анализатор обеспечивает “осведомленность о состояниях”, позволяющую применять корректные синтаксические правила в зависимости от текущий позиции (контекста) в сканируемом тексте. Если он видит открывающий тэг <code>, он переключается в другое состояние, в пределах которого другие синтаксические правила не применяются (т. е. что-либо, что выглядит как синтаксис вики должно восприниматься как “простой” текст), до тех пор, пока не найдет закрывающий тэг </code>.
Термин режим обозначает особенное состояние лексического анализа 11). Код, использующий анализатор, регистрирует один или более шаблон регулярного выражения с особенным наименованием режима. Затем анализатор, сравнивая эти паттерны со сканируемым текстом, вызывает функции обработчика с тем же самым наименованием режима (если метод mapHandler не был использован для создания псевдонимов - см. ниже).
Краткое введение в лексический анализатор можно найти в Simple Test Lexer Notes. Здесь предлагается более подробное описание.
Ключевыми методами анализатора являются;
Принимает ссылку на обработчик, наименование начального режима и (необязательно) логический флаг чувствительности сравнения шаблона к регистру.
Пример;
$Handler = & new MyHandler(); $Lexer = & new Doku_Lexer($Handler, 'base', TRUE);
Здесь указан начальный режим 'base'.
Используется, чтобы зарегистрировать шаблон при входе и выходе из особенного режима обработки. Например;
// arg0: регулярное выражение для сравнения - заметьте, что нет необходимости добавлять ограничители шаблона // arg1: наименование режима, где этот шаблон может быть использован // arg2: наимерование режима, в который следует войти $Lexer->addEntryPattern('<file>','base','file'); // arg0: регулярное выражение для сравнения // arg1: наименование режима, из которого следует выйти $Lexer->addExitPattern('</file>','file');
Код, приведенный выше, позволяет тэгу <file/> быть использованный при входе из базового в новый режим (file). Если в дальнейшем следует применить режимы, пока анализатор находится в режиме file, они должны быть зарегистрированы с режимом file.
Замечание: В паттернах не требуется использование ограничителей.
Используется, чтобы реагировать на дополнительные “вхождения” внутри существующего режима (без переходов). Он принимает паттерн и наименование режима, внутри которого должен использоваться.
Это наиболее наглядно видно из разбора парсером синтаксиса списков. Синтаксис списков выглядит в dokuwiki следующим образом;
До списка * Ненумерованный элемент списка * Ненумерованный элемент списка * Ненумерованный элемент списка После списка
Использование addPattern делает возможным сравнивать полный список, одновременно корректно захватывая каждый элемент списка;
// Сравнить начальный элемент списка и изменить режим $Lexer->addEntryPattern('\n {2,}[\*]','base','list'); // Сравнить новый элемент списка, но остаться в режиме ''list'' $Lexer->addPattern('\n {2,}[\*]','list'); // Если строка не совпадает с указанным выше правилом addPattern, выйти из режима $Lexer->addExitPattern('\n','list');
Используется для входа в новый режим только для сравнения, затем возвращается в “родительский” режим. Принимает в качестве аргументов паттерн, наименование режима, внутри которого его (паттерн) можно применять, и наименование “временного режима”, в который нужно войти для сравнения. Обычно может быть использовано, если вы хотите заменить разметку вики на что-нибудь другое. Например, сравнить смайл вроде :-);
$Lexer->addSpecialPattern(':-)','base','smiley');
Позволяет особому режиму быть прикрепленным к методу с разными наименованиями в обработчике. Это может быть полезным, когда различные синтаксические конструкции следует обрабатывать таким образом, как конструкции dokuwiki, отключающие другие синтаксические конструкции в особенном текстовом блоке;
$Lexer->addEntryPattern('<nowiki>','base','unformatted'); $Lexer->addEntryPattern('%%','base','unformattedalt'); $Lexer->addExitPattern('</nowiki>','unformatted'); $Lexer->addExitPattern('%%','unformattedalt'); // Оба вида синтаксических конструкций должны обрабатываться одинаковым образом... $Lexer->mapHandler('unformattedalt','unformatted');
Поскольку анализатор сам использует Подшаблоны (внутри класса ParallelRegex), код, использующий анализатор, этого не может. Иногода это может пригодиться, но, по общему правилу, метод addPattern может быть применен для решения проблем, когда обычно применяются подшаблоны. Его преимуществом является упрощение регулярных выражений, таким образом, управления ими.
Замечание: Если вы используете в шаблоне круглые скобки, они будут автоматически пропущены анализатором.
Для предотвращение “плохо форматируемой” (особенно при пропуске закрывающих тэгов) разметки, приводящей к тому, что анализатор входит в состояние (режим), который он никогда не покинет, может быть полезным использование паттерна просмотра вперед для проверки наличия закрывающей разметки 12). Например;
// Использование просмотра вперед во входном шаблоне... $Lexer->addEntryPattern('<file>(?=.*\x3C/file\x3E)','base','file'); $Lexer->addExitPattern('</file>','file');
Входной шаблон проверяет, может ли он найти закрывающий тэг </file>, до входа в состояние.
Замечание: В просмотре вперед требуется использование шестнадцатеричных символов, поскольку в хаке
анализатора, позволяющем делать просмотре вперед, возможно, есть баг.
Требуется исследование.
Определяется в inc/parser/handler.php
Обработчик - это класс, реализующий методы, которые вызываются лексическим анализатором, когда тот обнаруживает вхождения. Затем он “тонко преобразует” вхождения в последовательность инструкций, готовых для передачи Преобразователю.
Обработчик как целое состоит из следующих классов:
Doku_Handler: все вызовы из анализатора адресованы этому классу. Для каждого режима, зарегистрированного анализатором, будет соответствующий ему метод в обработчикеDoku_Handler_CallWriter: реализует “прокладку” между массивом инструкций (массив Doku_Handler::$calls) и методами обработчика, записывающими эти инструкции. Пока идет лексический анализ, он будет временно перемещен другими объектами, вроде Doku_Handler_List.Doku_Handler_List: отвечает за трансформацию перечня вхождений в инструкции, пока идет лексический разборDoku_Handler_Preformatted: отвечает за трансформацию предварительно отформатированных вхождений (врезки в dokuwiki) в инструкции, пока идет лексический разборDoku_Handler_Quote: отвечает за трансформацию вхождений цитат (текста, начинающего с одной или более >) в инструкции, пока идет лексический разборDoku_Handler_Table: отвечает за трансформацию вхождений таблиц в инструкции, пока идет лексический разборDoku_Handler_Section: отвечает за вставку инстукций секций, основываясь на позиции инструкций заголовоков, только когда лексический анализ завершен - повторяется однократноDoku_Handler_Block: отвечает за вставку 'p_open' и 'p_close' инструкций, будучи осведомленным об инструкциях 'block level' instructions, только когда лексический анализ завершен (т.е. он повторяется однократно посредством, выдавая полный перечень инструкций и вставляет дополнитльеный инструкции)Doku_Handler_Toc: отвечает за добавление инструкций таблицы содержания в начало последовательности, основываясь на инструкциях заголовка, только когда лексический анализ завершен (т.е. он повторяется однократно посредством, выдавая полный перечень инструкций и вставляет дополнитльеный инструкции)
Обработчик должен реализовывать методы, соответствующие режимам, зарегистрированным анализатором (подразумевается метод mapHandler() анализатора - см. выше).
Например, если вы зарегистировали в анализаторе режим file наподобие;
$Lexer->addEntryPattern('<file>(?=.*\x3C/file\x3E)','base','file'); $Lexer->addExitPattern('</file>','file');
Обработчику требуется метод вроде;
class Doku_Handler { /** * @строковый параметр match содержит текст, который был обнаружен * @целочисленный параметр state - тип совпадения (см. ниже) * @целочисленный параметр pos - индекс байта, где было найдено совпадение */ function file($match, $state, $pos) { return TRUE; } }
Замечание: метод обработчика обязан вернуть TRUE или анализатор будет немедленно остановлен. Подобное поведение может быть полезным, когда встречаются другие проблемы обработки, но в парсере DokuWiki все методы обработчика всегда возвращают TRUE.
Аргументы, реализумые методом обработчика;
$match: текст, который был обнаружен$state: содержит константу, которая описывает как именно было найдено совпадение;DOKU_LEXER_ENTER: найден входной паттерн (см. Lexer::addEntryPattern)DOKU_LEXER_MATCHED: найден паттерн (см. Lexer::addPattern)DOKU_LEXER_UNMATCHED: внутри режима не было совпаденийDOKU_LEXER_EXIT: найден выходной паттерн (см. Lexer::addExitPattern)DOKU_LEXER_SPECIAL: найден специальный паттерн (см. Lexer::addSpecialPattern)$pos: это индекс байта (длина строки от начала), где было найдено начало вхождения. $pos + strlen($match) дает индекс байта конца совпаденияВ качестве более сложного примера, для поиска списков в Парсере определено следующее;
function connectTo($mode) { $this->Lexer->addEntryPattern('\n {2,}[\-\*]',$mode,'listblock'); $this->Lexer->addEntryPattern('\n\t{1,}[\-\*]',$mode,'listblock'); $this->Lexer->addPattern('\n {2,}[\-\*]','listblock'); $this->Lexer->addPattern('\n\t{1,}[\-\*]','listblock'); } function postConnect() { $this->Lexer->addExitPattern('\n','listblock'); }
Метод listblock обработчике (вызов просто list приводит к ошибке обработчика PHP, поскольку list зарезервировано PHP) выглядит как;
function listblock($match, $state, $pos) { switch ( $state ) { // Начало списка... case DOKU_LEXER_ENTER: // Создать List rewrite, пропуская текущий CallWriter $ReWriter = & new Doku_Handler_List($this->CallWriter); // Заменить текущий CallWriter на List rewriter // все поступающие вхождения (даже, если они не являются вхождениями list) // теперь направляются в list $this->CallWriter = & $ReWriter; $this->__addCall('list_open', array($match), $pos); break; // Для конца списка case DOKU_LEXER_EXIT: $this->__addCall('list_close', array(), $pos); // Дать указание List rewriter об очистке $this->CallWriter->process(); // Восстановить прежний CallWriter $ReWriter = & $this->CallWriter; $this->CallWriter = & $ReWriter->CallWriter; break; case DOKU_LEXER_MATCHED: $this->__addCall('list_item', array($match), $pos); break; case DOKU_LEXER_UNMATCHED: $this->__addCall('cdata', array($match), $pos); 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
В результате превратиться в последовательность вхождений вроде;
base: "This is not a list", DOKU_LEXER_UNMATCHEDlistblock: "\n *", DOKU_LEXER_ENTERlistblock: " This is the opening list item", DOKU_LEXER_UNMATCHEDlistblock: "\n *", DOKU_LEXER_MATCHEDlistblock: " This is the second list item", DOKU_LEXER_UNMATCHEDlistblock: "\n *", DOKU_LEXER_MATCHEDlistblock: " This is the last list item", DOKU_LEXER_UNMATCHEDlistblock: "\n", DOKU_LEXER_EXITbase: "This is also not a list", DOKU_LEXER_UNMATCHEDНо чтобы быть использованными Преобразователем, это может быть конвертировано в следующие инструкции;
p_open:cdata: "This is not a list"p_close:listu_open:listitem_open:cdata: " This is the opening list item"listitem_open:listitem_open:cdata: " This is the second list item"listitem_open:listitem_open:cdata: " This is the last list item"listitem_open:list_close:p_open:cdata: "This is also not a list"p_close:
В случае со списками, это требует помощи класса Doku_Handler_List, который принимает вхождения, заменяя их на корректные инструкции для Преобразователя.
Парсер играет роль переднего рубежа для внешнего кода и устанавливает для лексического анализатора паттерны и режимы, описывающие синтаксис DokuWiki.
Испльзование Парсера в общем случае выглядит следущим образом;
// Создать парсер $Parser = & new Doku_Parser(); // Создать обработчик и поместить в парсер $Parser->Handler = & new Doku_Handler(); // Добавить требуемые синтаксические режимы в парсер $Parser->addMode('footnote',new Doku_Parser_Mode_Footnote()); $Parser->addMode('hr',new Doku_Parser_Mode_HR()); $Parser->addMode('unformatted',new Doku_Parser_Mode_Unformatted()); # etc. $doc = file_get_contents('wikipage.txt.'); $instructions = $Parser->parse($doc);
Более подробные примеры приведены ниже.
В целом, Парсер также содержит классы, предсталяющие по отдельности каждый из доступных режимов, базовым классом для всех них является Doku_Parser_Mode. Поведение этих режимов лучше всего понять, посмотрев на примеры добавления синтаксиса ниже в этом документе.
Причиной для представления режимов как классов является желание избежать повторяющихся вызовов методов анализатора. Без них было бы необходимо упорно разрабатывать каждое правило паттерна для каждого режима, в котором паттерн мог бы сравниваться, например, для регистрации единого правила паттерна для синтаксиса ссылок CamelCase требовалось бы что-то вроде;
$Lexer->addSpecialPattern('\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b','base','camelcaselink'); $Lexer->addSpecialPattern('\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b','footnote','camelcaselink'); $Lexer->addSpecialPattern('\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b','table','camelcaselink'); $Lexer->addSpecialPattern('\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b','listblock','camelcaselink'); $Lexer->addSpecialPattern('\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b','strong','camelcaselink'); $Lexer->addSpecialPattern('\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b','underline','camelcaselink'); // etc.
Каждый режим, который позволяет содержать ссылки CamelCase должен был быть явно указан.
Вместо этого используется единый класс вроде;
class Doku_Parser_Mode_CamelCaseLink extends Doku_Parser_Mode { function connectTo($mode) { $this->Lexer->addSpecialPattern( '\b[A-Z]+[a-z]+[A-Z][A-Za-z]*\b',$mode,'camelcaselink' ); } }
При установке параметров лексического анализатора, Парсер вызывает метод connectTo объекта Doku_Parser_Mode_CamelCaseLink для любого режима, кторый принимает синтаксис CamelCase.
Это позволяет коду быть более гибким при добавлении новых синтаксических конструкций.
Следующее показывает пример исходного текста вики и соответствующий вывод парсера;
Исходный текст (содержит таблицу);
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 ) - это наименование метода или функции, исполняемой Преобразователем.
Второй элемент (индекс 1) сам является массивом, каждый из элементов которого будет аргументом вызываемого метода Преобразователя.
В этом случае это будет единственный аргумент со значением "def\n", так что вызов метода будет;
$Render->cdata("def\n");
Третий элемент (индекс 2) является индексом байта первого символа, на котором “сработает” эта инструкция в исходном тексте. Он должен быть точно таким же, как и значение, возвращенное PHP функцией strpos. Это может использоваться для обнаружения секции исходного текста вики, основанной на позиции сгенерированной инструкции (позже будет пример).
Замечание: Метод parse Парсера разбивает исходный вики текст на предыдущий и последующий символы, чтобы гарантировать корректный выход анализатора из состояний, так что вам требуется вычесть 1 из индекса байта, чтобы получить корректную позицию оригинального исходного вики текста. Также Парсер нормализует строки под стиль Unix (т.е. все\r\n становятся \n), так что документ, который видит анализатоор может быть меньше, чем тот, который вы в действительности загрузили.
Пример массива инструкций страницы с описанием синтаксиса можно найти здесь
Преобразователь - это класс (или коллекция функций), определяемый вами. Его интерфейс описан в файле inc/parser/renderer.php и выглядит так;
<?php class Doku_Renderer { // snip function header($text, $level) {} function section_open($level) {} function section_close() {} function cdata($text) {} function p_open() {} function p_close() {} function linebreak() {} function hr() {} // snip }
Он используется для документирования Преобразователя, хотя он также может быть расширен, если вы захотите написать Преобразователь, который лишь перехватывает определенный вызовы.
Основной принцип того, как инструкции, возвращаемые Парсером, используются Преобразователем, близок по смыслу к SAX XML API - инструкции являются перечнем имен функций / методов и их аргуменов. Каждая инструкция может быть вызвана через Преобразователь (т.е. реализуемые им методы являются обратными). В отличие от SAX API, где доступно совсем немного, достаточно общих, обратновызываемых методов (например, tag_start, tag_end, cdata и т. д.), Преобразователь определяет более точную API, где методы обычно соответствуют один-к-одному действию по генерации выходных данных.
Во фрагменте Преобразователя, показанном выше методы p_open и p_close будут использованы для вывода тэгов <p> и </p> в XHTML, соответственно, в то время, как функция header принимает два аргумента - некоторый текст для отображения и “уровень” заголовка, так что вызов типа header('Some Title',1) выведет в XHTML <h1>Some Title</h1>.
К клиентскому коду относится использование Парсера для выполнения перечня инструкций через Преобразователь. Обычно это делается использованием PHP функцииcall_user_func_array. Например;
// Получить перечень инструкций из парсера $instructions = $Parser->parse($rawDoc); // Создать Преобразователь $Renderer = & new Doku_Renderer_XHTML(); // Пройтись по всем инструкциям foreach ( $instructions as $instruction ) { // Выполнить инструкции через Преобразователь call_user_func_array(array(&$Renderer, $instruction[0]),$instruction[1]); }
Ключевыми методами Преобразователя для обработки различного рода ссылок являются;
function camelcaselink($link) {} // $link вида "SomePage"function internallink($link, $title = NULL) {} // $link вида "[[wiki:syntax]]"$link сама по себе является внутренней, $title может быть недоступным изображением, так что требуется проверкаfunction externallink($link, $title = NULL) {}$link, и $title (изображение) требуют проверкиfunction interwikilink($link, $title = NULL, $wikiName, $wikiUri) {}$title требует проверки для изображенийfunction filelink($link, $title = NULL) {}file:// URL будут совпадать, но все равно лучше проверить плюс проверка $title, которое может быть недоступным изображениемfunction windowssharelink($link, $title = NULL) {}$title, которое может быть недоступным изображениемfunction email($address, $title = NULL) {}$title может быть изображением. Проверять ли email?function internalmedialink ($src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL) {}$title сам по себе не может быть изображениемfunction externalmedialink($src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL) {}$src требует проверки
Особое внимание следует уделит методам, принимающим в качестве параметра $title, который представляет видимый текст ссылки, например;
<a href="http://www.example.com">This is the title</a>
Аргумент $title может принимать три возможных типа значений;
NULL: у документа вики нет заголовка.
Если $title является массивом, он будет содержать ассоциированные значения, описывающие изображение;
$title = array( // Может быть 'internalmedia' (локальное изображение) или 'externalmedia' (внешнее изображение) 'type'=>'internalmedia', // URL изображения (может быть вики-URL или http://static.example.com/img.png) 'src'=>'wiki:php-powered.png', // Для альтернативного атрибута - a string or NULL 'title'=>'Powered by PHP', // 'left', 'right', 'center' или NULL 'align'=>'right', // Ширина в пикселях или NULL 'width'=> 50, // Высота в пикселях или NULL 'height'=>75, // Следует ли кэшировать изображение (для внешних изображений) 'cache'=>FALSE, );
Следующие примеры показывают общие задачи, которые будут решаться с помощью парсера.
Чтобы вызвать парсер со всеми режимами, и обработать синтаксис документа Dokuwiki;
require_once DOKU_INC . 'parser/parser.php'; // Создать парсер $Parser = & new Doku_Parser(); // Добавить обработчик $Parser->Handler = & new Doku_Handler(); // Загручть все режимы $Parser->addMode('listblock',new Doku_Parser_Mode_ListBlock()); $Parser->addMode('preformatted',new Doku_Parser_Mode_Preformatted()); $Parser->addMode('notoc',new Doku_Parser_Mode_NoToc()); $Parser->addMode('header',new Doku_Parser_Mode_Header()); $Parser->addMode('table',new Doku_Parser_Mode_Table()); $formats = array ( 'strong', 'emphasis', 'underline', 'monospace', 'subscript', 'superscript', 'deleted', ); foreach ( $formats as $format ) { $Parser->addMode($format,new Doku_Parser_Mode_Formatting($format)); } $Parser->addMode('linebreak',new Doku_Parser_Mode_Linebreak()); $Parser->addMode('footnote',new Doku_Parser_Mode_Footnote()); $Parser->addMode('hr',new Doku_Parser_Mode_HR()); $Parser->addMode('unformatted',new Doku_Parser_Mode_Unformatted()); $Parser->addMode('php',new Doku_Parser_Mode_PHP()); $Parser->addMode('html',new Doku_Parser_Mode_HTML()); $Parser->addMode('code',new Doku_Parser_Mode_Code()); $Parser->addMode('file',new Doku_Parser_Mode_File()); $Parser->addMode('quote',new Doku_Parser_Mode_Quote()); // Здесь требуются данные. Функции ''get*''остаются на ваше усмотрение $Parser->addMode('acronym',new Doku_Parser_Mode_Acronym(array_keys(getAcronyms()))); $Parser->addMode('wordblock',new Doku_Parser_Mode_Wordblock(array_keys(getBadWords()))); $Parser->addMode('smiley',new Doku_Parser_Mode_Smiley(array_keys(getSmileys()))); $Parser->addMode('entity',new Doku_Parser_Mode_Entity(array_keys(getEntities()))); $Parser->addMode('multiplyentity',new Doku_Parser_Mode_MultiplyEntity()); $Parser->addMode('quotes',new Doku_Parser_Mode_Quotes()); $Parser->addMode('camelcaselink',new Doku_Parser_Mode_CamelCaseLink()); $Parser->addMode('internallink',new Doku_Parser_Mode_InternalLink()); $Parser->addMode('media',new Doku_Parser_Mode_Media()); $Parser->addMode('externallink',new Doku_Parser_Mode_ExternalLink()); $Parser->addMode('email',new Doku_Parser_Mode_Email()); $Parser->addMode('windowssharelink',new Doku_Parser_Mode_WindowsShareLink()); $Parser->addMode('filelink',new Doku_Parser_Mode_FileLink()); $Parser->addMode('eol',new Doku_Parser_Mode_Eol()); // Загрузить исходный документ вики $doc = file_get_contents(DOKU_DATA . 'wiki/syntax.txt'); // Получить список инструкций $instructions = $Parser->parse($doc); // Создать Преобразователь require_once DOKU_INC . 'parser/xhtml.php'; $Renderer = & new Doku_Renderer_XHTML(); # Здесь загрузите в Преобразователь данные типа смайлов // Проходимся по всем инструкциям foreach ( $instructions as $instruction ) { // Выполняем обратный вызов через Преобразователь call_user_func_array(array(&$Renderer, $instruction[0]),$instruction[1]); } // Отображаем выходные данные echo $Renderer->doc;
Следующий код показывает, как выбрать фрагмент исходного текста, используя инструкции, полученные из Парсера;
// Создаем Парсер $Parser = & new Doku_Parser(); // Добавляем Обработчик $Parser->Handler = & new Doku_Handler(); // Загружаем режим header для поиска заголовков $Parser->addMode('header',new Doku_Parser_Mode_Header()); // Загружаем режимы, которые могут содержать разметку, которая может быть принята за заголовок $Parser->addMode('listblock',new Doku_Parser_Mode_ListBlock()); $Parser->addMode('preformatted',new Doku_Parser_Mode_Preformatted()); $Parser->addMode('table',new Doku_Parser_Mode_Table()); $Parser->addMode('unformatted',new Doku_Parser_Mode_Unformatted()); $Parser->addMode('php',new Doku_Parser_Mode_PHP()); $Parser->addMode('html',new Doku_Parser_Mode_HTML()); $Parser->addMode('code',new Doku_Parser_Mode_Code()); $Parser->addMode('file',new Doku_Parser_Mode_File()); $Parser->addMode('quote',new Doku_Parser_Mode_Quote()); $Parser->addMode('footnote',new Doku_Parser_Mode_Footnote()); $Parser->addMode('internallink',new Doku_Parser_Mode_InternalLink()); $Parser->addMode('media',new Doku_Parser_Mode_Media()); $Parser->addMode('externallink',new Doku_Parser_Mode_ExternalLink()); $Parser->addMode('email',new Doku_Parser_Mode_Email()); $Parser->addMode('windowssharelink',new Doku_Parser_Mode_WindowsShareLink()); $Parser->addMode('filelink',new Doku_Parser_Mode_FileLink()); // Загружаем исходный документ вики $doc = file_get_contents(DOKU_DATA . 'wiki/syntax.txt'); // Получаем перечень инструкций $instructions = $Parser->parse($doc); // Испльзуем эти переменные, чтобы узнать, находимся ли мы внутри необходимого фрагмента $inSection = FALSE; $startPos = 0; $endPos = 0; // Проходимся по всем инструкциям foreach ( $instructions as $instruction ) { if ( !$inSection ) { // Ищем заголовки в списках if ( $instruction[0] == 'header' && trim($instruction[1][0]) == 'Lists' ) { $startPos = $instruction[2]; $inSection = TRUE; } } else { // Ищем конец фрагмента if ( $instruction[0] == 'section_close' ) { $endPos = $instruction[2]; break; } } } // Нормализуем и разбиваем документ $doc = "\n".str_replace("\r\n","\n",$doc)."\n"; // Получаем текст, идущий перед фрагментом, который нам необходим $before = substr($doc, 0, $startPos); $section = substr($doc, $startPos, ($endPos-$startPos)); $after = substr($doc, $endPos);
Dokuwiki хранит части некоторых шаблонов во внешних файлах (например, смайлы). Поскольку парсинг и вывод документа являются отдельными стадиями, обрабатываемыми различными компонентами, при использовании данных также требуется дифференцированный подход.
К