DokuWiki

It's better when it's simple

Uživatelské nástroje

Nástroje pro tento web


cs:devel:parser

DokuWiki Parser

Tento dokument vysvětluje, jak funguje DokuWiki parser a je určen pro vývojáře, kteří chtějí upravit jeho chování nebo získat kontrolu nad výsledným dokumentem (pravděpodobně upravením generovaného HTML kódu nebo implementací nějakých dalších formátů).

Přehled

Parser dělí celý proces transformace DokuWiki kódu do výsledného výstupního dokumentu (typicky xhtml, zobrazené prohlížečem) na 4 fáze. Každá fáze je reprezentována jednou nebo více PHP třídami.

  1. Lexer1): načte DokuWiki dokument a vrací posloupnost lexikálních symbolů
  2. Handler2): přijímá lexikální symboly od Lexeru a vytváří posloupnost „instrukcí“ 3). Popisuje, jak má být dokument vykreslen - vygenerován finální kód. Tyto instrukce jsou kešovány.
  3. Parser4): spojuje Lexer a Handler, zajišťuje syntaktická pravidla Dokuwiki a také vstupní bod systému (metoda Parser::parse() )
  4. Renderer5): přijímá instrukce of Handleru a vykresluje finální dokument, připraven pro zobrazení (např. jako xhtml ve webovém prohlížeči)

Není k dipozici mechanismus pro propojení Handleru a Rendereru - to je třeba nakódovat pro daný případ užití.

Hrubý diagram vztahů mezi těmito komponentami:

      +-----------+          +-----------+
      |           |  Input   |  Client   |
      |  Parser   |<---------|   Code    |
      |           |  String  |           |
      +-----.-----+          +-----|-----+
    Modes   |                     /|\
      +     |             Renderer |
    Input   |          Instructions|
    String \|/                     |
      +-----'-----+          +-----------+
      |           |          |           |
      |  Lexer    |--------->|  Handler  |
      |           |  Tokens  |           |
      +-----.-----+          +-----------+
            |
            |
       +----+---+
       | Modes  |-+
       +--------+ |-+
         +--------+ |
           +--------+

„Client Code“ (kód používaný Parserem) volá Parser, předává mu vstupní string. Dostává posloupnost intrukcí „Renderer Instructions“, které jsou vytvořeny Handlerem. Tyto instrukce mohou být předány objektu, který implementuje Renderer (najčastěji je to objekty typu Doku_Renderer_xhtml).

Poznámka: Rozhodující záměr, který stojí za tímto návrhem, je snaha umožnit Rendereru být co možná „nejhloupější“. Renderer by se již neměl zabývat interpretací nebo změnami instrukcí. Měl by řešit pouze vše kolem generování daného výstupu (např. xhtml, prostý text, pdf). Přesněji, Renderer by neměl potřebovat uchovávat informace o stavu. Při dodržení tohoto principu, vedle snadné implementace Rendereru (řeší pouze generování daného kódu - např. xhtml), je také možné velmi snadno Renderer vyměnit za jiný (např. výstup jako PDF). Zároveň ale může být výstup z Handleru (posloupnost instrukcí) určena pro generování xhtml a nejsou proto zcela vyhovující pro všechny výstupní formáty. (to platí hlavně pro výrazně odlišné formáty - např. xhtml, html, prostý text budou velmi podobné, ale xhtml a pdf nikoliv)

Lexer (Lexikální analyzátor)

Definován v inc/parser/lexer.php

Pro lepší pochopení funkce lexikálního analyzátoru doporučuji přečíst nějaký článek o překladačích, například http://cs.wikipedia.org/wiki/Překladač

V obecném smyslu poskytuje nástroj pro zacházení se složitými regulárními výrazy, u kterých je důležitý stav. Zdrojem Lexeru DokuWiki je Simple Test, upravený třemi hacky:

  • podpora pro lookback a lookahead patterny
  • podporu pro změnu modifikátoru uvnitř paternu
  • oznámení Handleru startovací bajt index v dokuwiki kódu, kde byl nalezen lexikální symbol

Stručně řečeno, lexikální analyzátor Simple Test je jednoduchý nástroj pro práci s regulárními výrazy. Spíše než jeden obří regex, napíšete více malých, mnohem jednodušších regulárních výrazů. Lexikální analyzátor se pak mnohem citlivěji stará o jejich různé kombinace.

Lexikální analyzátor je složen ze tří tříd:

  • Doku_LexerParallelRegex: umožňuje skládat regulární výrazy z více oddělených paternů. Každý patern je indentifikován „labelem“. Tato třída kombinuje patterny do jednoho regexu, použitím subpaternů. Pokud používáte Lexer, pravděpodobně se o tuto třídu nemusíte zajímat.
  • Doku_LexerStateStack: poskytuje jednoduchý automat, který zajišťuje, aby analýza mohla být „context aware“. Pokud používáte Lexer, opět se o tuto třídu s největší pravděpodobností nemusíte vůbec zajímat.
  • Doku_Lexer: poskytuje přístupový bod pro klienta (kontroler), který chce používat lexikální analyzátor. Ovládá mnoho instancí typu ParallelRegex, používá automat StateStack (rozhodne, jaká instance ParallelRegexu bude použita, závisí na kontextu - jaké lexikální symboly předcházely nebo následují). Když narazí na „zajímavý text“, zavolá funkci v objektu Handler.

Potřeba stavů

Wiki syntaxe používaná v DokuWiki obsahuje značky, uvnitř kterých platí pouze některá pravidla. Nejlepším příkladem je asi tag <code/>, uvnitř kterého nejsou rozeznávány žádné ostatní lexikální symboly. Ostatní syntaxe, jako třeba seznam nebo tabulka, mohou obsahovat jen některé značky, ale jiné zase ne. Například uvnitř seznamu můžete používat odkazy ale nikoliv tabulky.

Lexikální analyzátor poskytuje „vědomí stavu“, které umožňuje aplikovat správné syntaktické pravidlo, v závislosti na aktuální pozici (kontextu) ve skenovaném textu. Pokud se nacházíme v otevřeném tagu <code>, přepne analyzátor do jiného stavu, ve kterém neplatí ostatní pravidla, až do okamžiku, kdy narazí na uzavírací tag </code>.

Módy lexikálního analyzátoru

Výraz mód je označení pro konkrétní stav analyzátoru6). Pomocí funkce connectTo() zaregistrujeme v lexikálním analyzátoru jeden nebo více regulárních výrazů, které odpovídají konkrétnímu módu. Poté, když lexikální analyzátor najde při skenování textu tyto paterny, zavolá funkci Handleru se stejným názvem, jako má odpovídající mód. (Pokud nebyla použita metoda mapHandler pro vytvoření aliasu - viz. níže).

API lexikálního analyzátoru

Krátký úvod do lexikálního analyzátoru najdete na Simple Test Lexer Notes. Zde najdete více detailů…

Klíčové metody Lexeru jsou:

Constructor

Jako parametr přijímá referenci na objekt - Handler, název počátečního módu, ve kterém Lexer začne a příznak (volitelný parametr, typ boolen), zda-li má být hledání patternů case sensitive.

Příklad:

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

Automat začíná ve stavu 'base'.

addEntryPattern / addExitPattern

Tato metoda se používá pro zaregistrování paternu (regulární výraz) pro začátek a konec daného parsing módu. Například:

// arg0: regulární výraz pro začátek módu
// arg1: název módu, ve kterém má být tento vstupní patern použit
// arg2: název módu, který tímto paternem začíná
$Lexer->addEntryPattern('<file>','base','file');
 
// arg0: regulární výraz - patern, který daný mód ukončuje
// arg1: název módu, který se tím ukončuje
$Lexer->addExitPattern('</file>','file');

Výše uvedený kód umožňuje začít nový mód file, použitím tagu <file>, pokud je Lexer v módu base.

Poznámka: Není potřeba používat oddělovač otvíracího a zavíracího paternu.

addPattern

Se používá pro spuštění dalších lexikálních symbolů uvnitř již existujícího módu. Jako parametr přijímá patern a název módu, uvnitř kterého může být použit.

Nejlépe je to vidět na seznamu. Syntaxe seznamu vypadá v DokuWiki takto:

Before the list
  * Unordered List Item
  * Unordered List Item
  * Unordered List Item
After the list

Použitím addPattern je možné nalézt kompletní seznam najednou… (a přitom je každá položka seznamu brána jako samostatný subjekt)

// Najde začátek seznamu a změní mód na list
$Lexer->addEntryPattern('\n {2,}[\*]','base','list');
 
// Najde položky seznamu, ale zůstává v módu list
$Lexer->addPattern('\n {2,}[\*]','list');
 
// Pokud nebude nalezen předchozí patern, při novém řádku ukončí mód list
$Lexer->addExitPattern('\n','list');
addSpecialPattern

Používá se pro otevření nového módu, ale hned se vrátí zpátky do nadřazeného módu. Přijímá jako parametr patern, název módu, ve kterém je možné patern použít a název dočasného módu. Typicky je tento způsob použit, pokud chcete nahradit wiki značku něčím jiným. Například pro nahrazení smajlíku :-) byste použili:

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

Umožní konkrétnímu módu, aby byl namapován na metodu s jiným názvem. (metoda v Handleru). To může být užitečné, když je potřeba použít různé syntaxe pro stejný význam (stejná manipulace - stejná funkce v Handleru), jako například:

$Lexer->addEntryPattern('<nowiki>','base','unformatted');
$Lexer->addEntryPattern('%%','base','unformattedalt');
$Lexer->addExitPattern('</nowiki>','unformatted');
$Lexer->addExitPattern('%%','unformattedalt');
 
// Obě syntaxe budou ošetřeny stejnou metodou
$Lexer->mapHandler('unformattedalt','unformatted');

Subpaterny nejsou povoleny

Subpaterny nejsou povoleny, protože Lexer sám již subpaterny používá (uvnitř třídy ParallelRegex). Většinu případů, kde by bylo třeba použít subpaterny, je možno vyřešit pomocí metody addPattern. Má to výhodu, že se používají jednodušší paterny a díky tomu je i manipulace s nimi snadnější.

Poznámka: Když použijete závorky v paternu, budou lexerem automaticky nahrazeny / (escaped).

Chyby v syntaxi a stavy

Pro předejití chyb, konkrétně zapomenutí uzavíracího tagu (Lexer otevře daný mód a nikdy jej neopustí), je dobré použít lookahead pattern pro kontrolu uzavíracího tagu7). Například:

// Použití lookahead patternu
$Lexer->addEntryPattern('<file>(?=.*</file>)','base','file');
$Lexer->addExitPattern('</file>','file');

Otvírací patern kontroluje zároveň i zavírací tag a až pak vstoupí do daného módu.

Poznámka: V lookahead paternu je třeba HEX znaky (\x3C), protože je pravděpodobně bug v lookaheads hacku. To je potřeba ještě prozkoumat.

Handler

Definován v inc/parser/handler.php

Handler je třída, která poskytuje metody, které jsou volány Lexerem, když najde lexikální symbol (token). Pak Handler vytvoří posloupnost instrukcí, připraveny pro Renderer. Tyto instrukce jsou kešovány.

Handler se skládá z následujících tříd:

  • Doku_Handler: všechny volání z Lexeru směřují do této třídy. Pro každý mód, zaregistrovaný v Lexeru, je zde odpovídající metoda v Handleru.
  • Doku_Handler_CallWriter: poskytuje vrstvu mezi polem instrukcí (pole Doku_Handler::$calls) a metodami Handleru, které vytváří instrukce. Bude dočasně nahrazen objekty jako Doku_Handler_List, zatímco probíhá lexikální analýza.
  • Doku_Handler_List: zařizuje transformaci seznamu lexikálních symbolů na instrukce, zatímco probíhá lexikální analýza
  • Doku_Handler_Preformatted: zařizuje transformaci seznamu předformátovaných lexikálních symbolů na instrukce, zatímco probíhá lexikální analýza
  • Doku_Handler_Quote: zařizuje transformaci seznamu blokových citací lexikálních symbolů na instrukce, zatímco probíhá lexikální analýza
  • Doku_Handler_Table: zařizuje transformaci seznamu lexikálních symbolů tabulek na instrukce, zatímco probíhá lexikální analýza
  • Doku_Handler_Section: zařizuje vkládání 'section' instrukcí, založených na pozici 'header' instrukcí, když jsou všechny instrukce hotovy, jednou se všechny projdou a vloží se 'section' instrukce
  • Doku_Handler_Block: zařizuje vkládání 'p_open' a 'p_close' instrukcí
  • Doku_Handler_Toc: zařizuje vkládání obsahu

Handler Token Metody

Handler musí poskytovat metody, jejihž názvy odpovídají módům registrovaným v lexikálním analyzátoru. (bere v úvahu samozřejmě metodu mapHandler())

Například, pokud v lexikálním analyzátoru zaregistrujete mód file:

$Lexer->addEntryPattern('<file>(?=.*</file>)','base','file');
$Lexer->addExitPattern('</file>','file');

Handler musí mít metodu:

class Doku_Handler {
 
    /**
    * @param string match contains the text that was matched
    * @param int state - the type of match made (see below)
    * @param int pos - byte index where match was made
    */
    function file($match, $state, $pos) {
        return TRUE;
    }
}

Poznámka: Handler metoda musí vracet TRUE, jinak bude lexikální analýza ihned zastavena.

Argumenty poskytované Handler token metodám jsou:

  • $match: text, který byl nalezen
  • $state: konstanta, která popisuje, jak přesně došlo k nalezení paternu
    1. DOKU_LEXER_ENTER: nalezen entry pattern (podívejte se na Lexer::addEntryPattern)
    2. DOKU_LEXER_MATCHED: nalezen pattern (podívejte se na Lexer::addPattern)
    3. DOKU_LEXER_UNMATCHED: text nalezen uvnitř módu, který neodpovídá žáddnému paternu
    4. DOKU_LEXER_EXIT: nalezen exit pattern (podívejte se na Lexer::addExitPattern)
    5. DOKU_LEXER_SPECIAL: nalezen special pattern (podívejte se na Lexer::addSpecialPattern)
  • $pos: bajt index (počet znaků od začátku), kde byl nalezen začátek lexikální symbolu

Komplexnější příklad, toto je definováno v Parseru pro analýzu seznamu:

    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');
    }

Metoda listblock v handleru 8):

    function listblock($match, $state, $pos) {
 
        switch ( $state ) {
 
            // The start of the list...
            case DOKU_LEXER_ENTER:
                // Create the List rewriter, passing in the current CallWriter
                $ReWriter = & new Doku_Handler_List($this->CallWriter);
 
                // Replace the current CallWriter with the List rewriter
                // all incoming tokens (even if not list tokens)
                // are now diverted to the list
                $this->CallWriter = & $ReWriter;
 
                $this->__addCall('list_open', array($match), $pos);
            break;
 
            // The end of the list
            case DOKU_LEXER_EXIT:
                $this->__addCall('list_close', array(), $pos);
 
                // Tell the List rewriter to clean up
                $this->CallWriter->process();
 
                // Restore the old 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;
    }

Konverze lexikálních symbolů

Část z práce Handleru je také vkládání, přejmenovávání nebo odstraňování lexikálních symbolů, které nalezl lexikální analyzátor - Lexer.

Například, seznam:

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

Lexer vygeneruje posloupnonost lexikálních symbolů asi takhle:

  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

Ale aby to bylo užitečné pro Renderer, je třeba menší konverze:

  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_close:
  8. listitem_open:
  9. cdata: " This is the second list item"
  10. listitem_close:
  11. listitem_open:
  12. cdata: " This is the last list item"
  13. listitem_close:
  14. list_close:
  15. p_open:
  16. cdata: "This is also not a list"
  17. p_close:

V případě, že se jedná o seznam, je potřeba „pomoc“ třídy Doku_Handler_List, která má vlastní znalosti o stavech a lexikálních symbolech.

Parser

Parser působí jako frontend pro externí kód a nastavuje paterny do lexikálního analyzátoru a módy popisující DokuWiki syntaxi.

Použití parseru vypadá asi takto:

// Create the parser
$Parser = & new Doku_Parser();
 
// Create the handler and store in the parser
$Parser->Handler = & new Doku_Handler();
 
// Add required syntax modes to parser
$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);

Detailnější příklady naleznete níže.

Jako vše ostatní, Parser se také skládá ze tříd, reprezentujících každých syntax mód. Základní třída pro všechny tyto třídy je Doku_Parser_Mode. Z ních všechny dědí. Pro lepší pochopení chování těchto módů se podívejte na příklady níže v tomto dokumentu.

Důvod, proč je každý mód reprezentován třídou, je vyhnutí se opakování volání metod Lexeru. Bez těchto tříd by bylo nutné složitě kódovat každé pravidlo pro každý mód, ve kterém může být patern nalezen. Například, registrování jednoho pravidla pro CamelCase odkazy by vyžadovalo něco jako:

$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.

Každý mód, ve kterém je možné použít CamelCase odkaz, by musel být explicitně zadán.

Lepší než tento zdlouhavý způsob, je implementace pomocí jedné třídy:

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'
            );
    }
 
}

Když se nastavuje lexikální analyzátor, Parser zavolá metodu connectTo, objektu Doku_Parser_Mode_CamelCaseLink pro každý mód, ve kterém je možné použít CamelCase odkazy. V některých to není možné - takže metodu nevolá (například <code />)

Díky tomu je nastavení lexikálního analyzátoru složitější na pochopení, ale umožňuje kódovat mnohem flexibilněji, když rozšiřujeme syntaxi.

Formát instrukcí

Následující příklad ukazuje dokuwiki syntaxi a odpovídající výstup z Parseru - posloupnost instrukcí.

Syrový dokuwiki text - obsahuje tabulku

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

Výstup z Parseru je následující pole:

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
        )

)

První úroveň pole je prostý seznam. Každý element (dítě) popisuje callback funkci, která bude volána před Rendererem (viz. Renderer - níže), pak je tam bajt index - pozice v zdrojovém dokuwiki textu, kde byl tento konkrétní element nalezen.

Jedna instrukce

Když se podíváme na jeden element (dítě), který reprezentuje jednu instrukci:

    [35] => Array
        (
            [0] => cdata
            [1] => Array
                (
                    [0] => def

                )

            [2] => 122
        )

První element (index 0) je název metody nebo funkce, která bude spuštěna v Rendereru.

Druhý element (index 1) je také pole, každý element tohoto pole bude argument pro metodu Rendereru, jež bude volána.

V tomto případě je zde pouze jeden argument s hodnotou "def\n", takže volání metody bude vypadat asi takto:

$Render->cdata("def\n");

Třetí element (index 2) je bajt index prvního znaku této instrukce ve zdrojovém dokuwiki textu. To by měla být stejná hodnota, jako hodnota vrácená PHP funkcí strpos. To může být užitečné pro získání sekcí.

Poznámka: Metoda parse bere zdrojový text spolu s předcházejícím a následujícím linefeed znakem, takže pokud chcete dostat správnou pozici v originálním zdrojovém textu, musíte odečíst 1. Parser také normalizuje řádky (linefeeds) do UNIXového stylu (např. všechny \r\n budou \n), takže dokument, který Lexer „vidí“ může být menší, než ve skutečném zdrojovém souboru.

Dobrý příklad instrukcí najde ze zde

Renderer

Renderer je třída (nebo kolekce funkcí). Interface je definováno v souboru inc/parser/renderer.php. Je to třída, kterou rozšiřuje konkrétní Renderer (například Doku_Renderer_xhtml) a vypadá takto:

<?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
}

Základní princip spracování instrukci (vrácených Parserem, resp. vygenerovaných Handlerem), je popsán zde. Instrukce jsou funkce nebo metody a jejich argumenty. Při procházení seznamu instrukcí, je každá instrukce volána v Rendereru (metody poskytované Rendererem jsou tzv. callbacky, více zde). Narozdíl od SAX API, kde je k dispozici pouze pár základních callbacků (např. tag_start, tag_end, cdata atd.), Renderer definuje jednoznačné API, kde metody jasně odpovídají instrukcím. Takže třeba metody p_open a p_close jsou použity pro výpis tagů <p> a </p> v html kódu. Vedle toho například funkce header má dva argumenty - nějaký text a úroveň, takže volání funkce bude vypadat třeba takto: header('Some Title',1) a výsledek tohoto volání funkce bude výstup v html: <h1>Some Title</h1>.

Volání Rendereru instrukcemi

To je ponecháno na klientovi, jak Parser spracovává seznam instrukcí v Rendereru. Typicky je to řešeno pomocí PHP funkce call_user_func_array. Například:

// Get a list of instructions from the parser
$instructions = $Parser->parse($rawDoc);
 
// Create a renderer
$Renderer = & new Doku_Renderer_XHTML();
 
// Loop through the instructions
foreach ( $instructions as $instruction ) {
 
    // Execute the callback against the Renderer
    call_user_func_array(array(&$Renderer, $instruction[0]),$instruction[1]);
}

Generování různých odkazů

Klíčové metody Rendereru pro zacházení s různými typy odkazů jsou:

  • function camelcaselink($link) {} // $link like "SomePage"
    • Tato funkce nemusí být kontrolována proti SPAMu, nebude pravděpodobně možné, odkazovat touto syntaxí mimo stránku
  • function internallink($link, $title = NULL) {} // $link like "[[syntax]]"
    • Také odkaz $link je interní, $title může být obrázek mimo web, takže je třeba kontrola proti SPAMu
  • function externallink($link, $title = NULL) {}
    • Jak $link, tak $title (obrázky) je třeba kontrolovat proti SPAMu
  • function interwikilink($link, $title = NULL, $wikiName, $wikiUri) {}
    • Titulek $title je potřeba kontrolovat
  • function filelink($link, $title = NULL) {}
    • Prakticky může fungovat pouze validní file:// URL, i přesto lepší kontrolovat a $title může být opět obrázek mimo web
  • function windowssharelink($link, $title = NULL) {}
    • Měl by obsahovat pouze validní „Windows share URL“, i přesto je lepší kontrolovat a samozřejmě také $title pro obrázky
  • function email($address, $title = NULL) {}
    • $title může být opět obrázek, také by bylo dobré kontrolovat, zda je $address validní adresa
  • function internalmedialink ($src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL) {}
    • Tato funkce nepotřebuje kontrolu, může odkazovat pouze na lokální obrázky a $title nemůže být obrázek
  • function externalmedialink($src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL) {}
    • $src je třeba kontrolovat

Speciální pozornost je třeba věnovat metodám, které přijímají argument $title, který reprezentuje zobrazený text odkazu, například:

<a href="http://www.example.com">This is the title</a>

Argument $title může obsahovat tři typy hodnot:

  1. NULL: žádný titulek
  2. string: prostý textový řetězec
  3. array (hash): obrázek použitý jako titulek

Pokud je $title pole (array), bude pole (asociativní pole) obsahovat hodnoty popisující obrázek:

$title = array(
    // Could be 'internalmedia' (local image) or 'externalmedia' (offsite image)
    'type'=>'internalmedia',
 
    // The URL to the image (may be a wiki URL or http://static.example.com/img.png)
    'src'=>'wiki:php-powered.png',
 
    // For the alt attribute - a string or NULL
    'title'=>'Powered by PHP',
 
    // 'left', 'right', 'center' or NULL
    'align'=>'right',
 
    // Width in pixels or NULL
    'width'=> 50,
 
    // Height in pixels or NULL
    'height'=>75,
 
    // Whether to cache the image (for external images)
    'cache'=>FALSE,
);

FIXME Zbytek dokumentu není přeložen FIXME

Examples

The following examples show common tasks that would likely be performed with the parser, as well as raising performance considerations and notes on extending syntax.

Basic Invokation

To invoke the parser will all current modes, and parse the DokuWiki syntax document;

require_once DOKU_INC . 'parser/parser.php';
 
// Create the parser
$Parser = & new Doku_Parser();
 
// Add the Handler
$Parser->Handler = & new Doku_Handler();
 
// Load all the modes
$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());
 
// These need data files. The get* functions are left to your imagination
$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());
 
// Loads the raw wiki document
$doc = file_get_contents(DOKU_DATA . 'wiki/syntax.txt');
 
// Get a list of instructions
$instructions = $Parser->parse($doc);
 
// Create a renderer
require_once DOKU_INC . 'parser/xhtml.php';
$Renderer = & new Doku_Renderer_XHTML();
 
# Load data like smileys into the Renderer here

// Loop through the instructions
foreach ( $instructions as $instruction ) {
 
    // Execute the callback against the Renderer
    call_user_func_array(array(&$Renderer, $instruction[0]),$instruction[1]);
}
 
// Display the output
echo $Renderer->doc;

Selecting Text (for sections)

The following shows how to select a range of text from the raw document using instructions from the parser;

// Create the parser
$Parser = & new Doku_Parser();
 
// Add the Handler
$Parser->Handler = & new Doku_Handler();
 
// Load the header mode to find headers
$Parser->addMode('header',new Doku_Parser_Mode_Header());
 
// Load the modes which could contain markup that might be
// mistaken for a 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());
 
// Loads the raw wiki document
$doc = file_get_contents(DOKU_DATA . 'wiki/syntax.txt');
 
// Get a list of instructions
$instructions = $Parser->parse($doc);
 
// Use this to watch when we're inside the section we want
$inSection = FALSE;
$startPos = 0;
$endPos = 0;
 
// Loop through the instructions
foreach ( $instructions as $instruction ) {
 
    if ( !$inSection ) {
 
        // Look for the header for the "Lists" heading
        if ( $instruction[0] == 'header' &&
                trim($instruction[1][0]) == 'Lists' ) {
 
            $startPos = $instruction[2];
            $inSection = TRUE;
        }
    } else {
 
        // Look for the end of the section
        if ( $instruction[0] == 'section_close' ) {
            $endPos = $instruction[2];
            break;
        }
    }
}
 
// Normalize and pad the document in the same way the parse does
// so that byte indexes with match
$doc = "\n".str_replace("\r\n","\n",$doc)."\n";
 
// Get the text before the section we want
$before = substr($doc, 0, $startPos);
$section = substr($doc, $startPos, ($endPos-$startPos));
$after = substr($doc, $endPos);

Managing Data File Input for Patterns

DokuWiki stores parts of some patterns in external data files (e.g. the smileys). Because the parsing and output of the document are now separate stages, handled by different components, a different approach is required for using this data, compared to earlier parser versions.

For the relevant modes, each accepts a plain list of elements which it builds into a list of patterns for registering with the Lexer.

For example;

// A plain list of smiley tokens...
$smileys = array(
    ':-)',
    ':-(',
    ';-)',
    // etc.
    );
 
// Create the mode
$SmileyMode = & new Doku_Parser_Mode_Smiley($smileys);
 
// Add it to the parser
$Parser->addMode($SmileyMode);

The parser is not interested in the output format for the smileys.

The other modes this applies to are defined by the classes;

  • Doku_Parser_Mode_Acronym - for acronyms
  • Doku_Parser_Mode_Wordblock - to block specific words (e.g. bad language)
  • Doku_Parser_Mode_Entity - for typography

Each accepts a list of „interesting strings“ to it's constructor, in the same way as the smileys.

In practice it is probably worth defining functions for retrieval of the data from the configuration files and storing the associative arrays in a static value e.g.;

function getSmileys() {
 
    static $smileys = NULL;
 
    if ( !$smileys ) {
 
        $smileys = array();
 
        $lines = file( DOKU_CONF . 'smileys.conf');
 
        foreach($lines as $line){
 
            //ignore comments
            $line = preg_replace('/#.*$/','',$line);
 
            $line = trim($line);
 
            if(empty($line)) continue;
 
            $smiley = preg_split('/\s+/',$line,2);
 
            // Build the associative array
            $smileys[$smiley[0]] = $smiley[1];
        }
    }
 
    return $smileys;
}

This function can now be used like;

// Load the smiley patterns into the mode
$SmileyMode = & new Doku_Parser_Mode_Smiley(array_keys(getSmileys()));
// Load the associate array in a renderer for lookup on output
$Renderer->smileys = getSmileys();

Note: Checking for links which should be blocked is handled in a separate manner, as described below.

Ideally we want to be able to check for links to spam before storing a document (after editing).

This example should be viewed with caution. It makes a useful point of reference but having actually tested it since, it's very slow - probably easier just to use a simple function that is „syntax blind“ but searches the entire document for links which match the blacklist. Meanwhile this example could be useful as a basis for building a 'wiki map' or finding 'wanted pages' by examining internal links. Probably best run as a cron job

This could be done by building a special Renderer that examines only the link-related callbacks and checks the URL against a blacklist.

A function is needed to load the spam.conf and bundle it into a single regex;

Recently tested this approach (single regex) against the latest blacklist from http://blacklist.chongqed.org/ and got errors about the final regex being too big. This should probably split the regex into smaller pieces and return them as an array
function getSpamPattern() {
    static $spamPattern = NULL;
 
    if ( is_null($spamPattern) ) {
 
        $lines = @file(DOKU_CONF . 'spam.conf');
 
        if ( !$lines ) {
 
            $spamPattern = '';
 
        } else {
 
            $spamPattern = '#';
            $sep = '';
 
            foreach($lines as $line){
 
                // Strip comments
                $line = preg_replace('/#.*$/','',$line);
 
                // Ignore blank lines
                $line = trim($line);
                if(empty($line)) continue;
 
                $spamPattern.= $sep.$line;
 
                $sep = '|';
            }
 
            $spamPattern .= '#si';
        }
    }
 
    return $spamPattern;
}

Now we need to extend the base Renderer with one that will examine links only;

require_once DOKU_INC . 'parser/renderer.php';
 
class Doku_Renderer_SpamCheck extends Doku_Renderer {
 
    // This should be populated by the code executing the instructions
    var $currentCall;
 
    // An array of instructions that contain spam
    var $spamFound = array();
 
    // pcre pattern for finding spam
    var $spamPattern = '#^$#';
 
    function internallink($link, $title = NULL) {
        $this->__checkTitle($title);
    }
 
    function externallink($link, $title = NULL) {
        $this->__checkLinkForSpam($link);
        $this->__checkTitle($title);
    }
 
    function interwikilink($link, $title = NULL) {
        $this->__checkTitle($title);
    }
 
    function filelink($link, $title = NULL) {
        $this->__checkLinkForSpam($link);
        $this->__checkTitle($title);
    }
 
    function windowssharelink($link, $title = NULL) {
        $this->__checkLinkForSpam($link);
        $this->__checkTitle($title);
    }
 
    function email($address, $title = NULL) {
        $this->__checkLinkForSpam($address);
        $this->__checkTitle($title);
    }
 
    function internalmedialink ($src) {
        $this->__checkLinkForSpam($src);
    }
 
    function externalmedialink($src) {
        $this->__checkLinkForSpam($src);
    }
 
    function __checkTitle($title) {
        if ( is_array($title) && isset($title['src'])) {
            $this->__checkLinkForSpam($title['src']);
        }
    }
 
    // Pattern matching happens here
    function __checkLinkForSpam($link) {
        if( preg_match($this->spamPattern,$link) ) {
            $spam = $this->currentCall;
            $spam[3] = $link;
            $this->spamFound[] = $spam;
        }
    }
}

Note the line $spam[3] = $link; in the __checkLinkForSpam method. This adds an additional element to the list of spam instructions found, making it easy to determine what the bad URLs were (e.g. for logging).

Finally we can use this spam checking renderer like;

// Create the parser
$Parser = & new Doku_Parser();
 
// Add the Handler
$Parser->Handler = & new Doku_Handler();
 
// Load the modes which could contain markup that might be
// mistaken for a link
$Parser->addMode('preformatted',new Doku_Parser_Mode_Preformatted()); 
$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());
 
// Load the link modes...
$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());
 
// Loads the raw wiki document
$doc = file_get_contents(DOKU_DATA . 'wiki/spam.txt');
 
// Get a list of instructions
$instructions = $Parser->parse($doc);
 
// Create a renderer
require_once DOKU_INC . 'parser/spamcheck.php';
$Renderer = & new Doku_Renderer_SpamCheck();
 
// Load the spam regex
$Renderer->spamPattern = getSpamPattern();
 
// Loop through the instructions
foreach ( $instructions as $instruction ) {
 
    // Store the current instruction
    $Renderer->currentCall = $instruction;
 
    call_user_func_array(array(&$Renderer, $instruction[0]),$instruction[1]);
}
 
// What spam did we find?
echo '<pre>';
print_r($Renderer->spamFound);
echo '</pre>';

Because we don't need all the syntax modes, checking for spam in this manner should be faster than normal parsing of a document.

Adding Substitution Syntax

Warning: the code below hasn't been tested - just an example

As a simpler task in modifying the parser, this example will add a „bookmark“ tag, which can be used to create a named anchor in a document for linking in.

The syntax for the tag will be like;

BM{My Bookmark}

The string „My Bookmark“ is the name of the bookmark while the rest identifies it as being a bookmark. In HTML this would correspond to;

<a name="My Bookmark"></a>

Adding this syntax requires the following steps;

  1. Create a parser syntax mode to register with the Lexer
  2. Update the Doku_Parser_Substition function found at the end of parser.php, which is used to deliver a quick list of modes (used in classes like Doku_Parser_Mode_Table
  3. Update the Handler with a method to catch bookmark tokens
  4. Update the abstract Renderer as documentation and any concrete Renderer implementations that need it.

Creating the parser mode means extending the Doku_Parser_Mode class and overriding it's connectTo method;

class Doku_Parser_Mode_Bookmark extends Doku_Parser_Mode {
 
    function connectTo($mode) {
        // Allow word and space characters
        $this->Lexer->addSpecialPattern('BM\{[\w ]+\}',$mode,'bookmark');
    }
 
}

This will match the complete bookmark using a single pattern (extracting the bookmark name from the rest of the syntax will be left to the Handler). It uses the Lexer addSpecialPattern method so that the bookmark lives in it's own state.

Note the Lexer does not require the start / end pattern delimiters - it takes care of this for you.

Because nothing inside the bookmark should be considered valid wiki markup, there is no reference here to other modes which this mode might accept.

Next the Doku_Parser_Substition function in the inc/parser/parser.php file needs updating so that the new mode called bookmark is returned in the list;

function Doku_Parser_Substition() {
    $modes = array(
        'acronym','smiley','wordblock','entity','camelcaselink',
        'internallink','media','externallink','linebreak','email',
        'windowssharelink','filelink','notoc','multiplyentity',
        'quotes','bookmark',
 
    );
    return $modes;
}

This function is just to help registering these modes with other modes that accept them (e.g., lists can contain these modes - you can have a link inside a list) without having to list them in full each time they are needed.

Note: Similar functions exist, like Doku_Parser_Protected and Doku_Parser_Formatting which return different groups of modes. The grouping of different types of syntax is not entirely perfect but still useful to save lines of code.

With the syntax now described, a new method, which matches the name of the mode (i.e. bookmark) needs to be added to the Handler;

class Doku_Handler {
 
    // ...
 
    // $match is the string which matched the Lexers regex for bookmarks
    // $state identifies the type of match (see the Lexer notes above)
    // $pos is the byte index in the raw doc of the first character of the match
    function bookmark($match, $state, $pos) {
 
        // Technically don’t need to worry about the state;
        // should always be DOKU_LEXER_SPECIAL or there's
        // a very serious bug
        switch ( $state ) {
 
            case DOKU_LEXER_SPECIAL:
 
                // Attempt to extract the bookmark name
                if ( preg_match('/^BM\{(\w{1,})\}$/', $match, $nameMatch) ) {
 
                    $name = $nameMatch[1];
 
                    // arg0: name of the Renderer method to call
                    // arg1: array of arguments to the Renderer method
                    // arg2: the byte index as before
                    $this->__addCall('bookmark', array($name), $pos);
 
                // If the bookmark didn't have a valid name, simply pass it
                // through unmodified as plain text (cdata)
                } else {
 
                    $this->__addCall('cdata', array($match), $pos);
 
                }
            break;
 
        }
 
        // Must return TRUE or the lexer will halt
        return TRUE;
    }
 
    // ...
 
}

The final step is updating the Renderer (renderer.php) with a new function and implementing it in the XHTML Renderer (xhtml.php);

class Doku_Renderer {
 
    // ...
 
    function bookmark($name) {}
 
    // ...
 
}
class Doku_Renderer_XHTML {
 
    // ...
 
    function bookmark($name) {
        $name = $this->__xmlEntities($name);
 
        // id is required in XHTML while name still supported in 1.0
        echo '<a class="bookmark" name="'.$name.'" id="'.$name.'"></a>';
 
    }
 
    // ...
 
}

See the tests/parser_replacements.test.php script for examples of how you might test this code.

Adding Formatting Syntax (with state)

Warning: the code below hasn't been tested - just an example

To show more advanced use of the Lexer, this example will add markup that allows users to change the enclosed text color to red, yellow or green.

The markup would look like;

<red>This is red</red>.
This is black.
<yellow>This is yellow</yellow>.
This is also black.
<green>This is yellow</green>.

The steps required to implement this are essentially the same as the previous example, stating with the new syntax mode, but add some additional detail as other modes are involved;

class Doku_Parser_Mode_TextColors extends Doku_Parser_Mode {
 
    var $color;
 
    var $colors = array('red','green','blue');
 
    function Doku_Parser_Mode_TextColor($color) {
 
        // Just to help prevent mistakes using this mode
        if ( !array_key_exists($color, $this->colors) ) {
            trigger_error('Invalid color '.$color, E_USER_WARNING);
        }
 
        $this->color = $color;
 
        // This mode accepts other modes;
        $this->allowedModes = array_merge (
            Doku_Parser_Formatting($color),
            Doku_Parser_Substition(),
            Doku_Parser_Disabled()
        );
 
    }
 
    // connectTo is called once for every mode registered with the Lexer
    function connectTo($mode) {
 
        // The lookahead pattern makes sure there's a closing tag...
        $pattern = '<'.$this->color.'>(?=.*</'.$this->color.'>)';
 
        // arg0: pattern to match to enter this mode
        // arg1: other modes where this pattern may match
        // arg2: name of the this mode
        $this->Lexer->addEntryPattern($pattern,$mode,$this->color);
    }
 
    // post connect is only called once
    function postConnect() {
 
        // arg0: pattern to match to exit this mode
        // arg1: name of mode to exit
        $this->Lexer->addExitPattern('</'.$this->color.'>',$this->color);
 
    }
 
}

Some points on the above class.

  1. It actually represents multiple modes, one for each color. The colors need separating into different modes so that </green> doesn't end up being the closing tag for <red>, for example.
  2. These modes can contain other modes, for example <red>**Warning**</red> for bold text which is red. This is registered in the constructor for this class by assigning the accepted mode names to the allowedModes property.
  3. When registering the entry pattern, it's a good idea to check the exit pattern exists (which is done with the lookahead). This should help protect users from themselves, when they forget to add the closing tag.
  4. The entry pattern needs to be registered for each mode within which the color tags could be used. By contrast we only need one exit pattern, so this is placed in the postConnect method, so that is only executed once, after all calls to connectTo on all modes have been called.

With the parsing mode class done, the new modes now need adding to the Doku_Parser_Formatting function;

function Doku_Parser_Formatting($remove = '') {
    $modes = array(
        'strong', 'emphasis', 'underline', 'monospace', 
        'subscript', 'superscript', 'deleted',
        'red','yellow','green',
        );
    $key = array_search($remove, $modes);
    if ( is_int($key) ) {
        unset($modes[$key]);
    }
 
    return $modes;
}

Note this function is primed to unset one of the modes to prevent a formatting mode being nested inside itself (e.g. we don't want <red>A <red>warning</red> message</red> to happen).

Next the Handler needs updating with one method for each color;

class Doku_Handler {
 
    // ...
 
    function red($match, $state, $pos) {
        // The nestingTag method in the Handler is there
        // to save having to repeat the same code many
        // times. It will create an opening and closing
        // instruction for the entry and exit patterns,
        // while passing through the rest as cdata
        $this->__nestingTag($match, $state, $pos, 'red');
        return TRUE;
    }
 
    function yellow($match, $state, $pos) {
        $this->__nestingTag($match, $state, $pos, 'yellow');
        return TRUE;
    }
 
    function green($match, $state, $pos) {
        $this->__nestingTag($match, $state, $pos, 'green');
        return TRUE;
    }
 
    // ...
 
}

Finally we can update the Renderers;

class Doku_Renderer {
 
    // ...
 
    function red_open() {}
    function red_close() {}
 
    function yellow_open() {}
    function yellow_close() {}
 
    function green_open() {}
    function green_close() {}
 
    // ...
 
}
class Doku_Renderer_XHTML {
 
    // ...
 
    function red_open() {
        echo '<span class="red">';
    }
    function red_close() {
        echo '</span>';
    }
 
    function yellow_open() {
        echo '<span class="yellow">';
    }
    function yellow_close() {
        echo '</span>';
    }
 
    function green_open() {
        echo '<span class="green">';
    }
    function green_close() {
        echo '</span>';
    }
 
    // ...
 
}

See the tests/parser_formatting.test.php script for examples of how you might write unit tests for this code.

Adding Block-Level Syntax

Warning: the code below hasn't been tested - just an example

Extending the previous example, this one will create a new tag for marking up messages in the document as things still to be done. Example use might look like;

===== Wiki Quotation Syntax =====

This syntax allows

<todo>
Describe quotation syntax '>'
</todo>

Some more text

This syntax might allow a tool to be added to search wiki pages and find things that still need something doing, as well as making it stand out in the document with some eye-catching style.

What's different about this syntax is it should be displayed in a separate block in the document (e.g. inside <div/> so that it can be floated with CSS). This requires modifying the Doku_Handler_Block class, which loops through all the instructions after all tokens have been seen by the handler and takes care of adding <p/> tags.

The parser mode for this syntax might be;

class Doku_Parser_Mode_Todo extends Doku_Parser_Mode {
 
    function Doku_Parser_Mode_Todo() {
 
        $this->allowedModes = array_merge (
            Doku_Parser_Formatting(),
            Doku_Parser_Substition(),
            Doku_Parser_Disabled()
        );
 
    }
 
    function connectTo($mode) {
 
        $pattern = '<todo>(?=.*</todo>)';
        $this->Lexer->addEntryPattern($pattern,$mode,'todo');
    }
 
    function postConnect() {
        $this->Lexer->addExitPattern('</todo>','todo');
    }
 
}

This mode is then added to the Doku_Parser_BlockContainers function in parser.php;

function Doku_Parser_BlockContainers() {
    $modes = array(
        'footnote', 'listblock', 'table','quote',
        // hr breaks the principle but HRs should not be used in tables / lists 
        // so put it here
        'hr',
        'todo',
    );
    return $modes;
}

Updating the Doku_Handler class simply requires;

class Doku_Handler {
 
    // ...
 
    function todo($match, $state, $pos) {
        $this->__nestingTag($match, $state, $pos, 'todo');
        return TRUE;
    }
 
    // ...
 
}

But the Doku_Handler_Block class (found in inc/parser/handler.php) also needs updating, to register the todo opening and closing instructions;

class Doku_Handler_Block {
 
    // ...
 
    // Blocks don't contain linefeeds
    var $blockOpen = array(
            'header',
            'listu_open','listo_open','listitem_open',
            'table_open','tablerow_open','tablecell_open','tableheader_open',
            'quote_open',
            'section_open', // Needed to prevent p_open between header and section_open
            'code','file','php','html','hr','preformatted',
            'todo_open',
        );
 
    var $blockClose = array(
            'header',
            'listu_close','listo_close','listitem_close',
            'table_close','tablerow_close','tablecell_close','tableheader_close',
            'quote_close',
            'section_close', // Needed to prevent p_close after section_close
            'code','file','php','html','hr','preformatted',
            'todo_close',
        );
 

By registering the todo_open and todo_close in the $blockOpen and $blockClose arrays, it instructs the Doku_Handler_Block class that any previous open paragraphs should be closed before entering the todo section then a new paragraph should start after the todo section. Inside the todo, no additional paragraphs should be inserted.

With that done, the Renderers can be updated;

class Doku_Renderer {
 
    // ...
 
    function todo_open() {}
    function todo_close() {}
 
    // ...
 
}
class Doku_Renderer_XHTML {
 
    // ...
 
    function todo_open() {
        echo '<div class="todo">';
    }
    function todo_close() {
        echo '</div>';
    }
 
    // ...
 
}

Serializing the Renderer Instructions

It is possible to serialize the list of instructions output from the Handler, to eliminate the overhead of re-parsing the original document on each request, if the document itself hasn't changed.

A simple implementation of this might be;

$ID = DOKU_DATA . 'wiki/syntax.txt';
$cacheID = DOKU_CACHE . $ID.'.cache';
 
// If there's no cache file or it's out of date
// (the original modified), get a fresh list of instructions
if ( !file_exists($cacheID) || (filemtime($ID) > filemtime($cacheID)) ) {
 
    require_once DOKU_INC . 'parser/parser.php';
 
    // Create the parser
    $Parser = & new Doku_Parser();
 
    // Add the Handler
    $Parser->Handler = & new Doku_Handler();
 
    // Load all the modes
    $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());
 
    // etc. etc.
 
    $instructions = $Parser->parse(file_get_contents($filename));
 
    // Serialize and cache 
    $sInstructions = serialize($instructions);
 
    if ($fh = @fopen($cacheID, 'a')) {
 
        if (fwrite($fh, $sInstructions) === FALSE) {
            die("Cannot write to file ($cacheID)");
        }
 
        fclose($fh);
    }
 
} else {
    // Load the serialized instructions and unserialize
    $sInstructions = file_get_contents($cacheID);
    $instructions = unserialize($sInstructions);
}
 
$Renderer = & new Doku_Renderer_XHTML();
 
foreach ( $instructions as $instruction ) {
    call_user_func_array(
        array(&$Renderer, $instruction[0]),$instruction[1]
        );
}
 
echo $Renderer->doc;

Note this implementation is not complete. What happens if someone modifies one of the smiley.conf files to add a new smiley, for example? The change will need to trigger updating the cache, so that the new smiley is parsed. Some care over file locking (or the renaming trick) may also be also be required.

Serializing the Parser

Similar to the above example, it is also possible to serialize the Parser itself, before parsing begins. Because setting up the modes carries a fairly high overhead, this can add a small increase in performance. From loose benchmarking, parsing the wiki:syntax page on a single (slow!) system, what taking around 1.5 seconds to finish without serializing the Parser and about 1.25 seconds with the a serialized version of the Parser.

In brief it can be implemented something like;

$cacheId = DOKU_CACHE . 'parser.cache';
 
if ( !file_exists($cacheId) ) {
 
    // Create the parser...
    $Parser = & new Doku_Parser();
    $Parser->Handler = & new Doku_Handler();
 
    // Load all the modes
    $Parser->addMode('listblock',new Doku_Parser_Mode_ListBlock());
    $Parser->addMode('preformatted',new Doku_Parser_Mode_Preformatted());
    # etc.
    
    // IMPORTANT: call connectModes()
    $Parser->connectModes();
 
    // Serialize
    $sParser = serialize($Parser);
 
    // Write to file
    if ($fh = @fopen($cacheID, 'a')) {
 
        if (fwrite($fh, $sParser) === FALSE) {
            die("Cannot write to file ($cacheID)");
        }
 
        fclose($fh);
    }
 
} else {
    // Otherwise load the serialized version
    $sParser = file_get_contents($cacheID);
    $Parser = unserialize($sParser);
}
 
$Parser->parse($doc);

Some implementation notes which aren't covered above;

  • Should use some file locking when writing to the cache (or else create with different name then rename) otherwise a request may receive a partially complete cache file, if read while writing still in progress
  • What to do if one of the *.conf files is updated? Need to flush the cache.
  • May be different versions of the Parser (e.g. for spam checking) so use different cache IDs

Testing

The unit tests provided use http://www.lastcraft.com/simple_test.php. SimpleTest is an excellent tool for unit testing PHP code. In particular, the documentation shines (see http://simpletest.sourceforge.net as well as that found at http://www.lastcraft.com/simple_test.php) and the code is very mature, taking care of many issues transparently (like catching PHP errors and reporting them in the test results).

For the DokuWiki parser, tests have been provided for all the syntax implemented and I strongly recommend writing new tests if additional syntax is added.

To get the tests running, you should only need to modify the file tests/testconfig.php, to point at the correct SimpleTest and DokuWiki directories.

Some notes / recommendations;

  1. Re-run the tests every time you change something in the parser - problems will often surface immediately saving lots of time.
  2. They only test specific cases. They don't guarantee there's no bugs only that those specific cases are working properly.
  3. If bugs are found, write a test for that bug while fixing it (better yet, before fixing it), to prevent it recurring.

Bugs / Issues

Some things off the top of my head.

Order of adding modes important

Haven't entirely nailed down the „rules“ on this one but the order in which modes are added is important (and the Parser doesn't check this for you). In particular, the eol mode should be loaded last, as it eats linefeed characters that may prevent other modes like lists and tables from working properly.

In general recommend loading the modes in the order used in the first example here.

From what I have worked out, order is only important if two or more modes have patterns which can be matched by the same set of characters - in which case the mode with the lowest sort number will win out. A syntax plugin can make use of this to replace a native DokuWiki handler, for an example see code plugin ChrisS 2005-07-30

Change to Wordblock

Originally the wordblock functionality was for match link URLs against a blacklist. This has been changed. The „wordblock“ mode is used for matching things like rude words, fuck it. For prevent spam URLs, probably best to use the example above.

One recommendation here - the conf/wordblock.conf file should be renamed conf/spam.conf, containing the URL blacklist. A new file conf/badwords.conf contains a list of rude words to censor.

From the point of view of design, the worst parts of the code are in inc/parser/handler.php, namely the „re-writing“ classes;

  • Doku_Handler_List (inline re-writer)
  • Doku_Handler_Preformatted (inline re-writer)
  • Doku_Handler_Quote (inline re-writer)
  • Doku_Handler_Table (inline re-writer)
  • Doku_Handler_Section (post processing re-writer)
  • Doku_Handler_Block (post processing re-writer)
  • Doku_Handler_Toc (post processing re-writer)

The „inline re-writers“ are used while the Handler is still receiving tokens from the Lexer while the „post processing re-writers“ are invoked from Doku_Handler::__finalize() and loop once through the complete list of instructions the Handler has created (which has a performance overhead).

It may be possible to eliminate Doku_Handler_List, Doku_Handler_Quote and Doku_Handler_Table by using multiple lexing modes (each of these currently uses only a single mode).

Also it may be possible to change Doku_Handler_Section and Doku_Handler_Toc to being „inline re-writers“, triggered by header tokens received by the Handler.

The most painful is the Doku_Handler_Block class, responsible for inserting paragraphs into the instructions. There may be a value in inserting further abstractions to make it easier to maintain but, in general, can't see a way to eliminate it completely and there's probably some bugs there which have yet to be found.

Greedy Tags

Consider the following wiki syntax;

Hello <sup>World 

----

<sup>Goodbye</sup> World

The user forgot to close the first <sup> tag.

The result is;

Hello World —- <sup>Goodbye World

The first <sup> tag is being too greedy in checking for it's entry pattern.

This applies to all similar modes. The entry patterns currently check for that the closing tag exists somewhere but should also check that a second opening tag of the same sort was not found first.

Footnote across list

There's one failing test in the test suite to document this problem. In essence, if a footnote is closed across multiple list items, it can have the effect of producing an opening footnote instruction without the corresponding closing instruction. The following is an example of syntax that would cause this problem;

  *((A))
    *(( B
  * C )) 

For the time being users will have to fix pages where this has been done. The solution is to split list tokenization into multiple modes (currently there is only a single mode listblock for lists).

Linefeed grabbing

261

Because the header, horizontal rule, list, table, quote and preformatted (indented text) syntax relies on linefeed characters to mark their starts and ends, they require regexes which consume linefeed characters. This means users need to add an additional linefeed if a table appears immediately after a list, for example.

Given the following wiki syntax;

Before the list
  - List Item
  - List Item
| Cell A | Cell B |
| Cell C | Cell D |
After the table

It produces;


Before the list

  1. List Item
  2. List Item

| Cell A | Cell B |

Cell C Cell D

After the table


Notice that the first row of the table is treated as plain text.

To correct this the wiki syntax must have an additional linefeed between the list and the table (which could also contain text);

Before the list
  - List Item
  - List Item

| Cell A | Cell B |
| Cell C | Cell D |
After the table

Which looks like;


Before the list

  1. List Item
  2. List Item
Cell A Cell B
Cell C Cell D

After the table


Without scanning the text multiple times (some kind of „pre-parse“ operation which inserts linefeeds), can't see any easy solutions here.

Lists / Tables / Quote Issue

For list, table and quote syntax, there is a possibility of child syntax eating multiple „lines“. For example a table like;

| Cell A | <sup>Cell B |
| Cell C | Cell D</sup> |
| Cell E | Cell F |

Produces;


Cell A Cell B | | Cell C | Cell D
Cell E Cell F

Ideally this should be rendered like;


Cell A <sup>Cell B
Cell C Cell D</sup>
Cell E Cell F

i.e. the opening <sup> tag should be ignored because it has no valid closing tag.

Fixing this will requiring using multiple modes inside tables, lists and quotes.

Footnotes and blocks

Inside footnotes paragraph blocks are ignored and the equivalent of a <br/> instruction is used instead, to replace linefeeds. This is basically a result of the Doku_Handler_Block being awkward to maintain. Further to this, if a table, list, quote or horizontal rule is used inside a footnote, it will trigger a paragraph.

This should be fixed by modifying Doku_Handler_Block but recommend an overhaul of the design before doing so.

Headers

Currently headers can reside on the same line as other preceding text. This is a knock on effect from the „Linefeed grabbing“ issue described above and would require some kind of „pre parse“ to fix it. For example;

Before the header
Some text == Header ==
After the header

If the behaviour is to be the same as the original DokuWiki parser, this should really be interpreted as;


Before the header Some text == Header == After the header


But in fact will result in;


Before the header Some text

After the header


Block / List Issue

There is a problem if, before a list there is a blank line with two spaces, the whole including the list will be interpreted as a block:

* list item
* list item 2

TODO

Některé věci, které chtějí dopracovat.

More State to State Closing Instructions

May be useful, for rendering other formats than XHTML, to add things like the indentation level to closing list instructions, etc.

why not just „render“ to XML, and than apply some xslt/xml parsers on it?

Table / List / Quote sub modes

Lexer with multiple modes to prevent the issues with nesting states.

1)
Lexer odpovídá třídě Doku_Lexer a obsahu souboru inc/parser/lexer.php
2)
Handler odpovídá třídě Doku_Handler a obsahu souboru inc/parser/handler.php
3)
posloupnost instrukcí je uchovávána v poli nazvaném $calls, což je atribut třídy Handler. Je určen pro použití s call_user_func_array
4)
Parser odpovídá třídě Doku_Parser a obsahu souboru inc/parser/parser.php
5)
Render odpovídá třídě Doku_Renderer, respektive třídám, které jej implementují - podívejte se na inc/parser/renderer.php a inc/parser/xhtml.php - XHTML je již konkrétní Parser - třída Doku_Renderer_xhtml, namísto xhtml rendereru můžeme implementovat třeba výstup do formátu pdf nebo prostého textu
6)
Výraz „stav“ a „mód“ jsou zde používány v zaměnitelném významu
7)
Fakt, že je v syntaxi chyba, není použitelný v DokuWiki parseru, je to navrženo pouze pro řešení situací, že uživatel zapomněl uzavřít tag - tag bude kompletně ignorován
8)
Kdybych ji nazvali jednoduše list, nástala by chyba, protože list je PHP klíčové slovo, proto jsme použili listblock
cs/devel/parser.txt · Poslední úprava: 2013-03-06 15:38 autor: Klap-in

Kromě míst, kde je explicitně uvedeno jinak, je obsah této wiki licencován pod následující licencí: CC Attribution-Share Alike 4.0 International
CC Attribution-Share Alike 4.0 International Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki