Translations of this page?:

Парсер Dokuwiki

В этом документе излагаются детали функционирования парсера Dokuwiki, которые могут понадобиться разработчикам для модификации поведения парсера или получения контроля над выходным потоком документа.

Обзор

Парсер разбивает процесс трансформации исходного документа dokuwiki в финальный выходной документ (обычно XHTML) на дискретные стадии. Каждая стадия представлена одним или несколькими PHP классами.

В общем рассмотрении этими элементами являются;

  1. Лексический анализатор 1): сканирует 2) исходный документ DokuWiki и выводит последовательность “вхождений” 3), соответствующих синтаксической структуре документа.
  2. Обработчик 4): получает вхождения от анализатора и преобразует их в последовательность “инструкций” 5). Он описывает, как должет быть сформирован выходной документ, от начала до конца.
  3. Собственно Парсер 6): “связывает” анализатор с обработчиком, предоставляя синтаксические правила dokuwiki, а также точку доступа к системе (метод Parser::parse())
  4. Преобразователь 7): принимает инструкции от обработчика и “отрисовывает” готовый к выводу документ (например, в виде XHTML).

Механизм, связывающий обработчик с преобразователем, отсутствует - для этого требуется программирование посредством специфического прецедента.

Схематическая диаграмма связей между компонентами;

      +-----------+          +-----------+
      |           |   Ввод   | Клиентский|
      |  Парсер   |<---------|   Код     |
      |           |  Строка  |           |
      +-----.-----+          +-----|-----+
    Режимы  |                     /|\
      +     |           Инструкции |
    Строка  |      Преобразователя |
    Ввода  \|/                     |
      +-----'-----+          +-----------+
      |           |          |           |
      |Анализатор |--------->| Обработчик|
      |           | Вхождения|           |
      +-----.-----+          +-----------+
            |
            |
       +----+---+
       | Режимы |-+
       +--------+ |-+
         +--------+ |
           +--------+

“Клиентский код” (код, использующий парсер) вызывает парсер, передавая ему входную строку. В ответ ему возвращается перечень “инструкций” преобразователя, построенных обработчиком. Они могут быть использованы неким объектом, реализующим преобразователь.

Замечание: Критическим моментом здесь является намерение позволить преобразователю быть настолько “тупым”, насколько это возможно. От него не требуется осуществлять дальнейшую интерпретацию / модификацию переданных инструкций, но полностью сконцентрироваться на формировании выходных данных (например, XHTML) - в особенности, преобразователю не следует отслеживать состояния. Соблюдение этого принципа, и, кроме того, составление преобразователя достаточно простым для реализации (сосредоточенной исключтельно на том, что следует выводить), также сделает возможным преобразователю быть взаимозаменяемым (например, вывод PDF в качестве альтернативы XHTML). В то же самое время, выходные инструкции обработчика направляются для преобразования в XHTML и не всегда могут быть пригодынми для всех выходных форматов.

Лексический анализатор

Определяется в inc/parser/lexer.php.

В самом общем смысле, он реализует инструмент для управления комплексными регулярными выражениями, где важным является состояние. Анализатор появился из Простого теста, но содержит три модификации (читай: хака ;-));

  • поддержка шаблонов с просмотром вперед и назад 8)
  • поддержка изменения модификаторов шаблона в пределах шаблона
  • уведомление обработчика об индексе стартового байта в исходном тексте, когда найдено вхождение

Анализатор как целое состоит из трех основных классов;

  • Doku_LexerParallelRegex: позволяет регулярному выражению состоять из множества отдельных шаблонов, каждый шаблон связан с идентифицирующей “меткой” , класс объединяет их в единое регулярное выражение, используя подшаблоны. Используя анализатор, вам не нужно беспокоится об этом классе.
  • Doku_LexerStateStack: реализует простую машину состояний (state mashine 9)), так что анализ может быть “осведомленным о контексте”. Используя анализатор, вам не нужно беспокоится об этом классе.
  • Doku_Lexer: реализует точку доступа для клиентского кода, использующего анализатор. Управляет множеством объектов ParallelRegex, используя StateStack 10) для применения корректных объектов ParallelRegex, в зависимости от “контекста”. При нахождении “интересного текста” он вызывает функции реализуемого пользователем объекта (обработчика).

Необходимость в состояниях

Синтаксис вики, используемый в dokuwiki, содержит разметку, “внутри” которой применяются только определенные синтаксические правила. Самый очевидный пример – тэг <code/> , внутри которого синтаксис вики не будет распознаваться анализатором. Для других синтаксических конструкций, таких как списки или таблицы, следует позволять использовать некоторую разметку, но не всю, например, в списка можно использовать ссылки, но не таблицы.

Анализатор обеспечивает “осведомленность о состояниях”, позволяющую применять корректные синтаксические правила в зависимости от текущий позиции (контекста) в сканируемом тексте. Если он видит открывающий тэг <code>, он переключается в другое состояние, в пределах которого другие синтаксические правила не применяются (т. е. что-либо, что выглядит как синтаксис вики должно восприниматься как “простой” текст), до тех пор, пока не найдет закрывающий тэг </code>.

Режимы анализатора

Термин режим обозначает особенное состояние лексического анализа 11). Код, использующий анализатор, регистрирует один или более шаблон регулярного выражения с особенным наименованием режима. Затем анализатор, сравнивая эти паттерны со сканируемым текстом, вызывает функции обработчика с тем же самым наименованием режима (если метод mapHandler не был использован для создания псевдонимов - см. ниже).

API анализатора

Краткое введение в лексический анализатор можно найти в Simple Test Lexer Notes. Здесь предлагается более подробное описание.

Ключевыми методами анализатора являются;

Конструктор

Принимает ссылку на обработчик, наименование начального режима и (необязательно) логический флаг чувствительности сравнения шаблона к регистру.

Пример;

$Handler = & new MyHandler();
$Lexer = & new Doku_Lexer($Handler, 'base', TRUE);

Здесь указан начальный режим 'base'.

addEntryPattern / addExitPattern

Используется, чтобы зарегистрировать шаблон при входе и выходе из особенного режима обработки. Например;

// arg0: регулярное выражение для сравнения - заметьте, что нет необходимости добавлять ограничители шаблона
// arg1: наименование режима, где этот шаблон может быть использован
// arg2: наимерование режима, в который следует войти
$Lexer->addEntryPattern('<file>','base','file');
 
// arg0: регулярное выражение для сравнения
// arg1: наименование режима, из которого следует выйти
$Lexer->addExitPattern('</file>','file');

Код, приведенный выше, позволяет тэгу <file/> быть использованный при входе из базового в новый режим (file). Если в дальнейшем следует применить режимы, пока анализатор находится в режиме file, они должны быть зарегистрированы с режимом file.

Замечание: В паттернах не требуется использование ограничителей.

addPattern

Используется, чтобы реагировать на дополнительные “вхождения” внутри существующего режима (без переходов). Он принимает паттерн и наименование режима, внутри которого должен использоваться.

Это наиболее наглядно видно из разбора парсером синтаксиса списков. Синтаксис списков выглядит в dokuwiki следующим образом;

До списка
  * Ненумерованный элемент списка
  * Ненумерованный элемент списка
  * Ненумерованный элемент списка
После списка

Использование addPattern делает возможным сравнивать полный список, одновременно корректно захватывая каждый элемент списка;

// Сравнить начальный элемент списка и изменить режим
$Lexer->addEntryPattern('\n {2,}[\*]','base','list');
 
// Сравнить новый элемент списка, но остаться в режиме ''list''
$Lexer->addPattern('\n {2,}[\*]','list');
 
// Если строка не совпадает с указанным выше правилом addPattern, выйти из режима
$Lexer->addExitPattern('\n','list');
addSpecialPattern

Используется для входа в новый режим только для сравнения, затем возвращается в “родительский” режим. Принимает в качестве аргументов паттерн, наименование режима, внутри которого его (паттерн) можно применять, и наименование “временного режима”, в который нужно войти для сравнения. Обычно может быть использовано, если вы хотите заменить разметку вики на что-нибудь другое. Например, сравнить смайл вроде :-);

$Lexer->addSpecialPattern(':-)','base','smiley');
mapHandler

Позволяет особому режиму быть прикрепленным к методу с разными наименованиями в обработчике. Это может быть полезным, когда различные синтаксические конструкции следует обрабатывать таким образом, как конструкции 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>, до входа в состояние.

Замечание: В просмотре вперед требуется использование шестнадцатеричных символов, поскольку в хаке :-x анализатора, позволяющем делать просмотре вперед, возможно, есть баг. FIXME Требуется исследование.

Обработчик

Определяется в 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: содержит константу, которая описывает как именно было найдено совпадение;
    1. DOKU_LEXER_ENTER: найден входной паттерн (см. Lexer::addEntryPattern)
    2. DOKU_LEXER_MATCHED: найден паттерн (см. Lexer::addPattern)
    3. DOKU_LEXER_UNMATCHED: внутри режима не было совпадений
    4. DOKU_LEXER_EXIT: найден выходной паттерн (см. Lexer::addExitPattern)
    5. 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

В результате превратиться в последовательность вхождений вроде;

  1. base: "This is not a list", DOKU_LEXER_UNMATCHED
  2. listblock: "\n *", DOKU_LEXER_ENTER
  3. listblock: " This is the opening list item", DOKU_LEXER_UNMATCHED
  4. listblock: "\n *", DOKU_LEXER_MATCHED
  5. listblock: " This is the second list item", DOKU_LEXER_UNMATCHED
  6. listblock: "\n *", DOKU_LEXER_MATCHED
  7. listblock: " This is the last list item", DOKU_LEXER_UNMATCHED
  8. listblock: "\n", DOKU_LEXER_EXIT
  9. base: "This is also not a list", DOKU_LEXER_UNMATCHED

Но чтобы быть использованными Преобразователем, это может быть конвертировано в следующие инструкции;

  1. p_open:
  2. cdata: "This is not a list"
  3. p_close:
  4. listu_open:
  5. listitem_open:
  6. cdata: " This is the opening list item"
  7. listitem_open:
  8. listitem_open:
  9. cdata: " This is the second list item"
  10. listitem_open:
  11. listitem_open:
  12. cdata: " This is the last list item"
  13. listitem_open:
  14. list_close:
  15. p_open:
  16. cdata: "This is also not a list"
  17. 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) {}
    • Требуется проверить только годные адреса доступа к общим ресурсам Windows, но все равно лучше проверить плюс проверка $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 может принимать три возможных типа значений;

  1. NULL: у документа вики нет заголовка.
  2. строка: в качестве заголовка использована простая тестовая стровка
  3. массив (хэш): в качестве заголовка использовано изображение.

Если $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 хранит части некоторых шаблонов во внешних файлах (например, смайлы). Поскольку парсинг и вывод документа являются отдельными стадиями, обрабатываемыми различными компонентами, при использовании данных также требуется дифференцированный подход.

К