Architektura a principy

Texy je nástroj pro převod textu napsaného ve vlastním markup jazyce do HTML. Na rozdíl od jednoduchých převodníků, které text zpracovávají lineárně pomocí série náhrad, používá Texy sofistikovaný systém založený na parsování, modulární architektuře a postupném budování DOM stromu.

Základní tok zpracování probíhá ve čtyřech hlavních fázích:

  1. Předzpracování textu – normalizace, úprava mezer a tabulátorů, volání notification handlerů pro přípravu
  2. Parsování – rozpoznání syntaxí pomocí regulárních výrazů a postupné budování DOM stromu
  3. Post-processing – typografické úpravy, zpracování dlouhých slov, wellforming HTML
  4. Finální sestavení – konverze DOM stromu do HTML řetězce

Klíčovým rozdílem oproti naivním přístupům je oddělení fáze rozpoznávání syntaxí od jejich zpracování. Parser nejprve identifikuje, kde se v textu nachází která syntaktická konstrukce, a teprve poté předává nalezené části jednotlivým modulům ke zpracování. To umožňuje vnořování syntaxí a jejich postupné rozbalování.

Poznámka: všechny třídy se nacházejí ve jmenném prostoru Texy, takže pokud dokument zmiňuje například třídu HtmlElement, její plný název je Texy\HtmlElement. Moduly jsou ve jmenném prostoru Texy\Modules

Klíčové komponenty

Architektura Texy se skládá z několika hlavních komponent, z nichž každá má jasně vymezenou zodpovědnost:

Třída Texy funguje jako centrální orchestrátor celého systému. Obsahuje odkazy na všechny moduly, spravuje registrované syntaxe a handlery, udržuje stav zpracování a koordinuje jednotlivé fáze konverze. Je to jediné místo, kde se jednotlivé komponenty propojují.

Moduly představují funkční jednotky zodpovědné za konkrétní oblasti markup jazyka. Každý modul při své konstrukci registruje syntaxe, které rozpoznává, a element handlery, které je zpracovávají. Například PhraseModule se stará o inline formátování jako tučný nebo kurzívou psaný text, zatímco TableModule zpracovává tabulky. Moduly jsou navrženy jako samostatné, znovupoužitelné jednotky s vlastní konfigurací přístupnou přes veřejné properties.

Parsery existují ve dvou variantách podle typu zpracovávaného obsahu. BlockParser zpracovává blokové struktury jako odstavce, nadpisy, seznamy nebo tabulky. Prochází text po řádcích, hledá začátky blokových konstrukcí a předává je syntax handlerům. LineParser se stará o inline syntaxe uvnitř řádků – odkazy, obrázky, formátování textu. Na rozdíl od BlockParser umožňuje vnořování syntaxí a jejich postupné rozbalování.

Základní terminologie

Pro správné pochopení fungování Texy je nutné rozlišovat několik klíčových pojmů, které se v dokumentaci často objevují.

Syntax označuje pojmenovanou syntaktickou konstrukci markup jazyka. Každá syntax má jedinečný název, například phrase/strong pro tučný text nebo image pro obrázky. Název syntaxe se používá pro její zapnutí či vypnutí v poli Texy::$allowed a předává se jako parametr do syntax handlerů pro rozlišení, která konkrétní syntax byla nalezena.

Pattern je regulární výraz, který definuje, jak syntax vypadá v textu. Pattern je implementační detail syntaxe – autor syntaxe musí napsat regex, který ji rozpozná, ale z pohledu uživatele Texy je podstatnější název syntaxe a její význam. Jeden modul typicky registruje více syntaxí s různými patterny.

Syntax handler je funkce volaná parserem ve chvíli, kdy najde výskyt syntaxe v textu. Dostává nalezený text a vrací HtmlElement nebo řetězec, který se vloží na původní místo. Syntax handler je místem, kde se rozhoduje, co se s nalezenou syntaxí stane – typicky vyvolá element handler pro vlastní zpracování.

Element je prvek, pro který se generuje HTML reprezentace. Například image je element pro obrázky, linkURL pro odkazy, phrase pro inline formátování. Každý element má svůj výchozí element handler, který se stará o standardní zpracování.

Element handler je funkce registrovaná pro určitý typ prvků a volaná přes systém HandlerInvocation. Charakteristické je použití metody proceed(), která umožňuje delegovat zpracování na další handler v řetězu nebo na výchozí handler modulu. Element handlery slouží k modifikaci nebo nahrazení výchozího chování.

Notification handler je funkce volaná pro notifikaci o určité události. Na rozdíl od element handlerů nevrací žádnou hodnotu a nemůže ovlivnit výsledek zpracování. Používá se pro přípravu dat, logování nebo modifikace již vytvořeného DOM stromu.

Rozdíl mezi jednotlivými handlery je klíčový pro pochopení architektury. Syntax handler je těsně svázán s parserem a konkrétním patternem – řeší otázku co dělat, když parser najde tento pattern. Element handlery jsou na vyšší úrovni abstrakce – řeší otázku jak zpracovat tento typ prvku, bez ohledu na to, která konkrétní syntax ji vytvořila.

Celkový tok zpracování

Když Texy dostane vstupní text, projde následujícím procesem zpracování.

V předzpracování dochází k normalizaci textu. Koncové značky řádků se sjednotí na Unix formát, mezery se standardizují a tabulátory se případně nahradí mezerami. Následně se vyvolají notification handlery registrované pro událost beforeParse. Tyto handlery mohou provést přípravu dat, například načíst definice referencí nebo upravit konfiguraci podle obsahu textu.

Samotné parsování začíná vytvořením kořenového HtmlElement, který reprezentuje dokument. Texy pak rozhodne, zda text zpracovat jako jeden řádek nebo jako kompletní dokument s blokovými strukturami. V případě blokového zpracování se vytvoří BlockParser, který postupně prochází text a hledá jednotlivé blokové konstrukce.

LineParser pracuje jinak než BlockParser. Neprochází text lineárně, ale postupně hledá nejbližší výskyt jakékoliv registrované syntaxe. Když nějakou najde, zavolá příslušný syntax handler, který vytvoří odpovídající HTML element. Tento element se pomocí speciálního maskování vloží zpět do textu a parser pokračuje dál. Díky tomu může najít a zpracovat syntaxe vnořené uvnitř již zpracovaných konstrukcí.

Po dokončení parsování vznikne kompletní DOM strom reprezentující strukturu dokumentu. Texy vyvolá notification handlery pro událost afterParse, které mohou provést závěrečné úpravy stromu, například doplnit identifikátory nadpisů nebo sestavit obsah.

Post-processing probíhá během konverze DOM stromu na HTML řetězec. Každý element se rekurzivně převádí na HTML kód, přičemž se aplikují typografické úpravy jako nahrazení uvozovek, pomlček nebo vkládání nezlomitelných mezer. Dále se provádí wellforming HTML – automatické uzavírání tagů, oprava špatně vnořených elementů, formátování a odsazování kódu.

Finální fází je dekódování všech maskovaných částí zpět na HTML tagy, odstranění pomocných značek a sestavení výsledného HTML řetězce.

Systém syntaxí

Syntax v terminologii Texy představuje pojmenovanou syntaktickou konstrukci markup jazyka. Je to abstraktní koncept spojující několik prvků: unikátní název, regulární výraz pro rozpoznání a způsob zpracování. Název syntaxe slouží jako identifikátor v celém systému – používá se v poli Texy::$allowed pro zapnutí či vypnutí, předává se do handlerů pro rozlišení typu konstrukce a objevuje se v dokumentaci a konfiguračních souborech.

Jmenné konvence syntaxí následují dva hlavní vzory. Jednodušší syntaxe mají jednoslovný název odpovídající jejich účelu, například image, table nebo script. Složitější oblasti používají hierarchické pojmenování se lomítkem, například phrase/strong, phrase/em nebo link/reference. Lomítko slouží k logickému seskupení souvisejících syntaxí a usnadňuje hromadné operace s nimi.

Line syntaxe

Line syntaxe slouží k rozpoznávání inline prvků uvnitř řádků textu. Typicky jde o formátování jako tučný nebo kurzívou psaný text, odkazy, obrázky nebo inline kód. Charakteristické pro line syntaxe je, že mohou být vnořené do sebe a parser je postupně rozbaluje.

Registrace line syntaxe probíhá voláním Texy::registerLinePattern() s několika parametry. První je syntax handler, tedy callback volaný při nálezu. Druhý parametr je regulární výraz definující podobu syntaxe v textu. Třetí parametr je název syntaxe používaný v celém systému. Volitelný čtvrtý parametr je další regex pro test, zda má smysl pattern vůbec hledat – používá se pro optimalizaci, aby se nespouštěl komplexní pattern na textu, který určitě nemůže matchnout.

Pattern jako regulární výraz musí splňovat určitá pravidla. Nesmí být kotvený na začátek textu, protože se hledá kdekoliv v řádku. Měl by být co nejkonkrétnější, aby nedošlo k falešným matchům.

Inline syntaxe uvnitř řádků textu zpracovává LineParser. Když najde match, zavolá příslušný syntax handler. Ten dostává tři parametry. První je instance LineParser, která poskytuje přístup k Texy objektu a dalším informacím o kontextu. Druhý parametr je pole s výsledky regex matche včetně podvýrazů. Třetí parametr je název syntaxe, který je užitečný, když stejný callback obsluhuje více syntaxí. Handler musí vrátit buď HtmlElement, nebo řetězec, nebo null pokud zpracování odmítá.

Block syntaxe

Block syntaxe rozpoznávají víceřádkové blokové konstrukce jako nadpisy, seznamy, tabulky, citace nebo speciální bloky. Na rozdíl od line syntaxí se block syntaxe nikdy nepřekrývají – každý řádek textu patří maximálně do jedné blokové konstrukce.

Registrace block syntaxe používá Texy::registerBlockPattern() se třemi parametry. Syntax handler, regulární výraz a název syntaxe. Pattern jako regulární výraz musí splňovat určitá pravidla. Musí matchnout od začátku řádku a často obsahuje kotvu pro konec řádku. BlockParser automaticky přidává modifikátory Am, takže pattern by je neměl obsahovat.

Block syntaxe uvnitř dokumentu zpracovává BlockParser. Když najde match, zavolá příslušný syntax handler. Ten dostává podobné parametry jako u line syntaxí – BlockParser instanci, pole s matchem a název syntaxe. Vrací HtmlElement reprezentující celý zpracovaný blok, nebo null při odmítnutí zpracování.

Zapnutí a vypnutí syntaxe

Pole Texy::$allowed poskytuje jemně granulární kontrolu nad tím, které syntaxe jsou v Texy aktivní. Je to jednoduchý, ale mocný mechanismus pro konfiguraci chování bez nutnosti měnit kód modulů. Když zakážete syntaxi phrase/strong tímto nastavením, parser přestane hledat konstrukci tučného textu:

$texy->allowed['phrase/strong'] = false;

Kontrola probíhá jednou při začátku parsování, takže dynamická změna $allowed během zpracování nemá efekt.

Při konstrukci modulů se pro většinu syntaxí nastavuje výchozí hodnota $allowed. Některé syntaxe jsou ve výchozím stavu zapnuté, protože tvoří základ markup jazyka. Jiné jsou vypnuté, protože jsou pokročilé nebo potenciálně nebezpečné. Například emotikony jsou vypnuté, protože ne každý dokument je potřebuje, zatímco základní formátování je zapnuté.

Bezpečný režim je situace, kdy zpracováváte nedůvěryhodný vstup, například komentáře od uživatelů. Chcete povolit základní formátování, ale zakázat obrázky, skripty nebo HTML tagy. Texy\Configurator::safeMode() nastaví $allowed pro bezpečnou kombinaci syntaxí. Typicky zakáže image, figure, script a HTML tagy, ale ponechá odkazy a formátování.

Parsery

Syntax handler

Jak jsme si říkali v předchozí části, LineParser nebo BlockParser prochází text a hledají všechny registrované patterny. Když najde match, zavolá příslušný syntax handler a předá mu informace o nálezu – zejména pole s výsledky regex matche.

Syntax handler analyzuje nalezený text a připravuje data pro zpracování. Může extrahovat části textu z regex skupin, vytvořit pomocné objekty jako Link nebo Image, parsovat modifikátory. Rozhoduje také, jaký element handler vyvolat. Zavolá Texy::invokeAroundHandlers() s názvem elementu a připravenými parametry. Tím začne jejich vykonávání. Vrácený výsledek se dostává zpět do syntax handleru, který ho vrátí parseru.

Element handler

Element handlery implementují vzor chain of responsibility, který umožňuje složit výsledné chování z více vrstev.

Registrace element handleru probíhá voláním Texy::addHandler() se dvěma parametry – názvem elementu a funkcí handleru. Jeden název elementu může mít zaregistrováno více handlerů, které se pak vykonávají v pořadí od posledně registrovaného k prvnímu.

Název elementu identifikuje, o jaký typ zpracování jde, například phrase pro formátování, image pro obrázky nebo link pro odkazy (pozor: jde o něco jiného než názvy syntaxe). Někdy se používají složené názvy jako linkReference nebo linkEmail pro rozlišení různých druhů odkazů. Názvy jsou obecnější než názvy syntaxí – zatímco syntaxe phrase/strong je specifická konstrukce, element phrase pokrývá všechny druhy inline formátování.

Vyvolání element handleru používá metodu Texy::invokeAroundHandlers(). Tato metoda dostává název prvku, instanci parseru a pole parametrů. Vytvoří HandlerInvocation objekt, který zapouzdřuje celý řetěz zaregistrovaných handlerů. První handler v řetězu dostane kontrolu a rozhoduje, zda zavolat HandlerInvocation::proceed() pro pokračování na další handler, nebo vrátit vlastní výsledek.

HandlerInvocation objekt je klíčem k pochopení, jak řetězení funguje. Obsahuje zásobník všech handlerů pro daný prvek a aktuální pozici v tomto zásobníku. Když handler zavolá proceed(), HandlerInvocation posune pozici o jedno místo zpět v zásobníku a zavolá další handler. Pokud handler zavolá proceed() s modifikovanými parametry, tyto nové parametry se předají všem následujícím handlerům. Pokud handler vůbec nezavolá proceed(), řetěz se přeruší a jeho návratová hodnota se stane výsledkem celého zpracování.

Pořadí vykonávání handlerů je od posledně zaregistrovaného k prvnímu. To znamená, že uživatelský handler zaregistrovaný dodatečně dostane kontrolu první a může rozhodnout, zda vůbec zavolá výchozí handler modulu. Toto pořadí umožňuje uživatelům přepsat výchozí chování bez nutnosti měnit kód modulu.

Typické použití element handleru vypadá následovně. Handler zkontroluje vstupní parametry a rozhodne, zda chce zasáhnout do zpracování. Pokud ano, upraví data, zavolá proceed() s novými parametry a případně ještě upraví vrácený výsledek. Pokud chce handler úplně nahradit výchozí zpracování, vytvoří vlastní výsledek a vrátí ho bez volání proceed().

Notification handler

Notification handlery představují jednodušší, jednosměrný komunikační mechanismus. Na rozdíl od element handlerů neslouží k transformaci dat, ale k provedení vedlejších akcí.

Registrace notification handleru používá stejnou metodu Texy::addHandler() jako element handlery. Rozdíl je v tom, jak se handler používá – notification handler nevrací žádnou hodnotu a nemá přístup k HandlerInvocation. První parametr je název události. Používají se popisné názvy jako beforeParse a afterParse pro globální události okolo parsování, nebo specifičtější jako afterTable, afterList, afterBlockquote pro události po vytvoření konkrétní struktury. Prefix before/after jasně indikuje časování události.

Vyvolání notification handlerů používá metodu Texy::invokeHandlers(). Tato metoda jednoduše zavolá všechny zaregistrované handlery v pořadí a ignoruje jejich návratové hodnoty. Notification handlery dostanou parametry předané při vyvolání, ale nemohou je měnit pro další handlery v řadě.

Typické použití notification handlerů zahrnuje několik scénářů. Handler pro událost beforeParse může načíst definice referencí z textu ještě před začátkem parsování. Handler afterParse může projít vytvořený DOM strom a doplnit chybějící atributy nebo sestavit tabulku obsahu. Handlery jako afterTable nebo afterList umožňují modulům provést závěrečné úpravy vytvořených struktur.

Důležitý rozdíl oproti element handlerům je v tom, že notification handlery nemohou zabránit dalšímu zpracování. Všechny zaregistrované handlery se vždy vykonají, žádný nemůže přerušit řetěz. To je zamýšlené chování – notification handlery jsou o vedlejších efektech, ne o kontrole toku.

LineParser

LineParser zpracovává inline syntaxe uvnitř řádků textu postupným způsobem, který umožňuje vnořování a složité interakce mezi syntaxemi.

Základní princip spočívá v hledání prvního výskytu jakékoliv syntaxe. V každé iteraci projde všechny syntaxe a zjistí, která z nich matchuje nejblíže aktuální pozici v textu. Tato syntax vyhrává a zpracuje se. Pokud více syntaxí matchuje na stejné pozici, vyhrává ta, která byla registrována dříve – to je priorita podle pořadí registrace.

Když parser najde nejbližší match, zavolá příslušný syntax handler. Ten vrátí výsledek, který může být HtmlElement nebo řetězec. A tímto výsledkem se přepíše nalezený match v textu.

Poté hledá znovu od aktuální pozice. Tento systém zajišťuje, že parser vždy vidí aktuální stav textu. Když nahradíme match novým textem, který může obsahovat další syntaxe, tyto syntaxe se najdou v příští iteraci.

Property $again na LineParser objektu slouží k jemnému řízení toho, zda by se měla právě matchnutá syntaxe hledat znovu na stejné pozici po zpracování aktuálního matche. Výchozí hodnota je false, která říká: Na této pozici už nemá smysl hledat tuto stejnou syntaxi. Posuň se dál.

Průchod končí, když parser dojde na konec textu nebo když už žádná syntaxe nemá další match. Výsledkem je text, kde všechny rozpoznatelné syntaxe byly zpracovány a nahrazeny výsledky, připravený pro finální konverzi.

Vnořování

Schopnost zpracovat vnořené syntaxe je jednou z klíčových vlastností LineParser a představuje základní výzvu – jak zabránit tomu, aby již zpracované HTML tagy byly omylem interpretovány jako další syntax k zpracování.

Když parser zpracovává text obsahující vnořené syntaxe, nejprve najde vnější konstrukci. Například v textu "odkaz **tučný** text":URL parser nejprve najde syntaxi pro odkaz s uvozovkami. Pattern pro tuto syntaxi matchuje celý řetězec od první uvozovky po dvojtečku a URL. Syntax handler vytvoří HtmlElement pro tag <a> a obsah odkaz **tučný** text se přidá jako potomek elementu. Tento řetězec vloží zpět do textu a pokračuje v hledání dalších syntaxí (**tučný**, který představuje tučný text).

Ale teď má problém – v textu jsou také HTML značky, která může matchnout jako začátek jiné syntaxe. Parser by začal zpracovávat již hotové HTML tagy jako kdyby byly část původního textu.

Nechceme, aby parser viděl HTML tagy. Potřebujeme nějaký způsob, jak rozlišit již zpracované části od částí čekajících na zpracování. Metoda Texy::protect() řeší tyto problémy elegantním způsobem – nahradí HTML tagy unikátním placeholderem složeným z control characters – speciálních bytů mimo tisknutelné ASCII.

Když se tedy HtmlElement převádí na řetězec (pomocí toString()), výsledek nevypadá jako <a href="...">odkaz **tučný** text</a>, ale například jako \x17\x18\x19\x17odkaz **tučný** text\x17\x18\x1A\x17.

V textu během parsování tak nejsou nikdy přítomny skutečné HTML tagy. Místo nich jsou pouze placeholdery. Ale vnitřní text zůstává a parser ho normálně vidí a může v něm hledat další syntaxe. To umožňuje postupné vnořování – vnější syntax se zamaskuje, ale její obsah je stále přístupný pro vnitřní syntaxe.

Na konci zpracování metoda Texy::unProtect() projde výsledný HTML řetězec a nahradí všechny placeholdery jejich skutečnými hodnotami. Teprve v tomto okamžiku se do výstupu dostanou skutečné HTML tagy.

Úrovně maskování

Různé druhy obsahu používají různé control characters pro své placeholdery, což umožňuje syntaxím selektivně rozhodnout, co mohou obsahovat.

  • Patterns::CONTENT_MARKUP označuje běžný HTML markup jako tagy pro formátování nebo odkazy. Je to nejběžnější typ a používá ho většina inline elementů. Placeholder začíná a končí \x17.
  • Patterns::CONTENT_REPLACED označuje obsah, který byl nahrazen něčím jiným, typicky obrázky nebo jiné replaced elementy. Používá \x16 jako marker.
  • Patterns::CONTENT_TEXTUAL označuje text, který byl escapován nebo jinak ošetřen, aby se nezpracovával. Používá se pro konstrukce jako code nebo notexy, kde chceme zobrazit původní text včetně markup symbolů, ne jejich interpretaci.
  • Patterns::CONTENT_BLOCK označuje blokové elementy. Je to nejnižší úroveň v hierarchii. Používá \x14 jako marker.

Hierarchie těchto typů není jen konvence, ale má praktický důsledek. Konstantní Patterns::MARK je definována jako \x14-\x1F, tedy rozsah pokrývající všechny tyto typy plus rezervu. Syntaxe používají tuto konstantu ve svých patterns pro vyloučení maskovaných částí.

Různé syntaxe mohou mít různé požadavky na to, co mohou obsahovat za placeholdery. Pattern, který chce vidět pouze čistý text bez jakýchkoliv maskovaných částí, použije vyloučení [^\x14-\x1F]. To odmítne všechny placeholdery všech typů. Příkladem je pattern pro obrázky – URL obrázku by neměla obsahovat žádné HTML tagy ani bloky.

Pattern, který akceptuje nižší úrovně, ale odmítá vyšší, použije užší rozsah. Například [^\x17-\x1F] odmítne pouze CONTENT_MARKUP a výš, ale akceptuje CONTENT_BLOCK, CONTENT_TEXTUAL a CONTENT_REPLACED. To je užitečné, pokud chceme povolit bloky, ale ne inline markup. Praktickým příkladem je TypographyModule, který provádí typografické úpravy jako nahrazení uvozovek nebo vkládání nezlomitelných mezer. Tyto úpravy by se měly aplikovat na běžný text, ale ne uvnitř bloků kódu nebo preformátovaného textu.

Kolize syntaxí

Kolize nastává, když více syntaxí může matchnout na stejné pozici, a systém musí vybrat jednu z nich.

Typickým příkladem jsou různé délky stejného symbolu. Syntaxe phrase/strong+em používá tři hvězdičky pro kombinaci tučného a kurzívy. Syntaxe phrase/strong používá dvě hvězdičky pro samotný tučný text. Syntaxe phrase/em-alt používá jednu hvězdičku pro kurzívu. Když parser najde text začínající třemi hvězdičkami, všechny tři syntaxe technicky mohou matchnout.

PhraseModule řeší tuto kolizi registrací syntaxí v pořadí od nejdelší po nejkratší. Nejprve registruje phrase/strong+em s patternem pro tři hvězdičky. Pak phrase/strong s patternem pro dvě hvězdičky. Nakonec phrase/em-alt s patternem pro jednu hvězdičku. Díky tomuto pořadí se při nálezu tří hvězdiček zpracuje nejprve phrase/strong+em a kratší syntaxe nedostanou šanci.

Další příklad jsou odkazy v různých formátech. Syntaxe phrase/wikilink používá pattern pro [text|url]. Syntaxe link/reference používá pattern pro [ref]. Oba začínají otevírací hranatou závorkou. Pokud je v textu [text|url], oba patterns technicky mohou začít matchnout.

Řešením je opět specifičnost patterns. Pattern pro phrase/wikilink je specifičtější – vyžaduje svislítko uvnitř závorek. Pokud text obsahuje svislítko, matchne phrase/wikilink. Pokud ne, pattern selže a link/reference má šanci. Pořadí registrace zde také hraje roli – phrase/wikilink by měla být registrována dříve než link/reference.

BlockParser

BlockParser používá fundamentálně odlišný přístup k zpracování, který reflektuje povahu blokových konstrukcí. Základní rozdíl je v absenci prolínání. Zatímco LineParser umožňuje, aby syntaxe byly vnořené do sebe a postupně se rozbalovaly, BlockParser pracuje s předpokladem, že každý blok je samostatná jednotka. Jeden řádek nebo skupina řádků patří k maximálně jednomu bloku. Bloky se nepřekrývají, nekříží a nevnoří na úrovni BlockParser.

BlockParser začíná vyhledáním všech bloků, respektive jejich začátků. Parser projde všechny registrované block syntaxe a najde všechny jejich výskyty. Pokud více syntaxí matchuje na stejné pozici, použije se pořadí registrace – dříve registrovaná syntaxe má přednost.

API pro syntax handler

BlockParser poskytuje syntax handlerům API pro práci s víceřádkovými strukturami.

Metoda BlockParser::moveBackward() slouží k návratu na předchozí řádky. Přijímá počet řádků, o které se má vrátit. Parser posune svou interní pozici směrem k začátku textu, dokud nepřejde přes specifikovaný počet konců řádků. To umožňuje callbacku začít číst od začátku struktury, i když pattern matchnul až uprostřed nebo na konci.

Metoda BlockParser::next() slouží k čtení dalšího řádku odpovídajícího určitému patternu. Přijímá regex pattern (automaticky přidá modifikátory Am) a referenci na proměnnou pro výsledek matche. Pokud další řádek v textu matchuje poskytnutý pattern, metoda naplní výsledek, posune interní pozici za tento řádek a vrátí true. Pokud další řádek nematchuje, metoda vrátí false a pozice se nezmění.

Moduly

Moduly jsou základní organizační jednotkou v architektuře Texy. Každý modul zapouzdřuje kompletní funkcionalitu pro určitou oblast markup jazyka.

Primární zodpovědností modulu je registrace syntaxí. V konstruktoru modul volá Texy::registerLinePattern() nebo registerBlockPattern() pro všechny syntaxe, které chce zpracovávat. Tím říká parseru: Když najdeš tyto patterny, zavolej mě. Modul tak definuje, které konstrukce v textu rozpoznává.

Druhá zodpovědnost je implementace element handlerů. Modul registruje handlery pro elementy, které jeho syntaxe vyvolávají. Tyto handlery obsahují logiku pro převod nalezených konstrukcí na HTML elementy. Element handler rozhoduje, jaký element vytvořit, jaké atributy nastavit a jak zpracovat obsah.

Třetí zodpovědnost je poskytnutí konfigurace. Moduly mají veřejné properties, které umožňují uživatelům Texy upravit chování modulu bez nutnosti měnit jeho kód. Například ImageModule má properties pro nastavení root cesty k obrázkům nebo výchozího alt textu.

Čtvrtá zodpovědnost je správa stavu specifického pro modul. Například HeadingModule sleduje všechny nalezené nadpisy v poli TOC pro sestavení obsahu. LinkModule spravuje slovník referencí pro odkazy. Tento stav je soukromý pro modul a ostatní části systému k němu nepřistupují přímo.

Moduly jsou navrženy jako nezávislé jednotky. Každý modul může fungovat samostatně a neměl by záviset na implementačních detailech jiných modulů. Komunikace mezi moduly probíhá přes sdílené objekty jako Link nebo Image, ne přes přímé volání metod.

Struktura typického modulu

Většina modulů v Texy následuje podobnou strukturu, která reflektuje jejich úlohu v systému.

Modul dědí od základní třídy Module, která poskytuje přístup k Texy objektu přes protected property $texy. Konstruktor modulu přijímá instanci Texy a uloží si ji. To umožňuje modulu přistupovat ke konfiguraci a volat metody na Texy objektu.

V konstruktoru probíhá veškerá inicializace. Modul nastaví výchozí hodnoty konfiguračních properties, případně nastaví výchozí hodnoty v poli Texy::$allowed pro své syntaxe. Pak registruje své syntaxe voláním registerLinePattern() nebo registerBlockPattern(). Každá registrace spojuje pattern, syntax handler a název syntaxe. Nakonec modul registruje své element handlery voláním addHandler().

Syntax handlery jsou metody modulu, které parser volá při nálezu syntaxe. Tyto metody typicky extrahují části z regex matche, vytvářejí pomocné objekty a vyvolávají element handlery. Syntax handler rozhoduje, jaký element handler vyvolat a jaké parametry předat.

Element handlery jsou metody implementující skutečné zpracování. Dostávají HandlerInvocation objekt jako první parametr, následovaný parametry specifickými pro daný element. Element handler vytváří HtmlElement, aplikuje modifikátory, zpracovává obsah a vrací výsledek. Je to místo, kde se rozhoduje o finální podobě HTML.

Veřejné properties slouží jako rozhraní pro konfiguraci. Uživatel Texy může nastavit tyto properties pro přizpůsobení chování modulu. Properties jsou typicky primitivní typy nebo pole, ne složité objekty, aby konfigurace byla jednoduchá.

Přehled klíčových modulů

Standardní distribuce Texy obsahuje několik modulů pokrývajících různé aspekty markup jazyka.

  • PhraseModule zpracovává inline formátování textu. Registruje syntaxe pro tučný text, kurzívu, podtržení, horní a dolní index, kód a další. Všechny tyto syntaxe vyvolávají společný handler pro element phrase a handler rozlišuje podle názvu syntaxe, jaký tag vytvořit. Modul umožňuje konfigurovat, které tagy se použijí pro jednotlivé druhy formátování.
  • LinkModule spravuje odkazy v dokumentu. Registruje syntaxe pro různé formáty odkazů – explicitní URL, emailové adresy, reference na definované odkazy. Poskytuje factory metody pro vytváření Link objektů a spravuje slovník referencí. Modul umožňuje konfigurovat root pro relativní odkazy, automatické rel="nofollow" pro externí odkazy a zkracování dlouhých URL.
  • ImageModule zpracovává obrázky podobným způsobem jako LinkModule odkazy. Registruje syntaxi pro inline obrázky a spravuje slovník referencí na definované obrázky. Poskytuje factory metody pro vytváření Image objektů a automatickou detekci rozměrů obrázků. Konfigurovatelné jsou cesty k obrázkům, výchozí alt text a CSS třídy pro zarovnání.
  • HeadingModule rozpoznává nadpisy v různých formátech – podtržené pomlčkami nebo rovnítky, obklopené mřížkami. Shromažďuje všechny nadpisy do pole TOC pro možné sestavení obsahu. Umožňuje konfigurovat generování ID, top úroveň nadpisů a režim balancování úrovní.
  • ListModule zpracovává seznamy – nečíslované, číslované a definiční. Rozpoznává různé typy odrážek a automaticky detekuje vnořování podle odsazení. Umožňuje konfigurovat, které znaky slouží jako odrážky a jaké HTML listy generovat.
  • TableModule je jedním z nejkomplexnějších modulů. Rozpoznává tabulky s hlavičkami, těly, titulky a podporuje colspan a rowspan. Zpracovává modifikátory pro řádky i buňky.
  • BlockModule zpracovává speciální bloky ohraničené /-- a \--. Podporuje různé typy bloků – code pro kód, html pro přímé HTML, div pro generický kontejner. Umožňuje uživatelům definovat vlastní handlery pro vlastní typy bloků.
  • TypographyModule provádí post-processing pro typografické úpravy. Nahrazuje tři tečky elipsou, dvojité pomlčky en-dash, přímé uvozovky typografickými a vkládá nezlomitelné mezery. Pracuje na úrovni finálního řetězce mezi blokovými elementy.
  • HtmlOutputModule formátuje finální HTML výstup. Zajišťuje wellformed HTML automatickým zavíráním tagů, opravou nesprávného vnořování, odsazením kódu a zalamováním dlouhých řádků. Umožňuje konfigurovat úroveň odsazení a šířku řádků.

Interakce mezi moduly

Ačkoliv jsou moduly navrženy jako nezávislé, v některých případech musí spolupracovat.

Sdílené objekty jsou hlavní mechanismus komunikace. Link objekt vytvořený LinkModule může být předán ImageModule pro vytvoření obrázkového odkazu. Image objekt vytvořený ImageModule může být předán FigureModule pro vytvoření obrázku s popiskem. Tyto objekty zapouzdřují veškeré potřebné informace a poskytují společné rozhraní.

Reference systém umožňuje oddělit definici od použití. LinkModule poskytuje metody addReference() a getReference() pro správu slovníku pojmenovaných odkazů. Uživatel může v jedné části dokumentu definovat referenci a v jiné ji použít. ImageModule má analogický systém pro reference na obrázky. Moduly používající reference volají factory metody, které samy kontrolují, zda jde o referenci nebo přímou hodnotu.

Element handlers mohou volat jiné element handlery. PhraseModule při zpracování phrase/span s odkazem vytvoří Link objekt a zavolá element handler LinkModule pro vytvoření odkazu. Tím deleguje odpovědnost za vytvoření a konfiguraci odkazu na specializovaný modul.

Vztahy mezi moduly jsou typicky jednostranné. PhraseModule zná LinkModule a ImageModule, protože vytváří odkazy a obrázky. Ale LinkModule a ImageModule neznají PhraseModule. To udržuje závislosti jednoduché a umožňuje snadné nahrazení nebo rozšíření modulů.

DOM reprezentace

HtmlElement reprezentuje jeden uzel v DOM stromu a poskytuje rozhraní pro jeho manipulaci a zpracování.

Základní struktura elementu obsahuje název tagu, asociativní pole atributů a pole potomků. Potomci mohou být další instance HtmlElement nebo prostě textové řetězce. Tato kombinace umožňuje reprezentovat libovolnou HTML strukturu.

Název elementu se nastavuje a získává přes metody setName() a getName(). Speciální hodnota null jako název znamená transparentní element, který nemá tagy, jen jeho obsah.

Atributy jsou veřejně přístupné přes property $attrs jako asociativní pole. Hodnoty mohou být řetězce, čísla, boolean nebo pole. Boolean true znamená atribut bez hodnoty (jako checked), false nebo null znamená atribut se vůbec nevykreslí. Pokud je hodnota pole, různé prvky se spojí podle typu atributu – pro class mezerami, pro style středníky. Metoda setAttribute() nastaví hodnotu atributu. Metoda getAttribute() vrací hodnotu atributu nebo null.

Potomci se spravují přes několik metod. Metoda add() přidává potomka na konec. Metoda insert() vkládá potomka na specifikovanou pozici, volitelně nahrazuje existujícího potomka. Metoda create() vytváří nový HtmlElement jako potomka a vrací ho pro další manipulaci. Metoda removeChildren() odstraní všechny potomky.

Element implementuje ArrayAccess interface, takže s potomky lze pracovat jako s polem. Zápis $el[0] vrací prvního potomka, $el[0] = $child nastaví prvního potomka. Tento přístup je pohodlný pro rychlou manipulaci s konkrétními potomky.

Metoda toString() prochází element a jeho potomky rekurzivně a sestavuje řetězcovou reprezentaci. HTML tagy se okamžitě zamaskují pomocí Texy::protect(), takže do výsledku jde placeholder místo skutečných HTML znaků.

Metody toHtml() a toText() vrací výsledek nemaskovaný včetně post-processingu.

Parsování obsahu

HtmlElement může rekurzivně parsovat svůj obsah, čímž umožňuje postupné budování DOM stromu.

Metoda parseLine() slouží k parsování inline syntaxí v řetězci. Vytvoří novou instanci LineParser s aktuálním elementem jako kontejnerem. Zavolá parse() na parseru s poskytnutým textem. LineParser postupně najde a zpracuje všechny inline syntaxe a výsledné elementy nebo řetězce přidá jako potomky aktuálního elementu. Metoda vrací použitý LineParser pro případné další použití.

Metoda parseBlock() parsuje text jako blokový obsah. Vytvoří BlockParser a zavolá na něm parse(). BlockParser najde všechny blokové konstrukce v textu, zpracuje je a přidá jako potomky elementu. Text mezi bloky se zpracuje jako odstavce, které interně používají LineParser. Metoda přijímá boolean parametr indikující, zda text pochází z odsazeného bloku, což ovlivňuje zpracování odstavců.

Tyto parsovací metody umožňují rekurzivní zpracování. Syntax handler může vytvořit element, nastavit jeho základní vlastnosti a pak zavolat parseLine() nebo parseBlock() pro zpracování obsahu. Výsledkem je, že obsah elementu prochází stejným procesem parsování jako hlavní dokument, včetně rozpoznávání syntaxí a vyvolávání handlerů.

Validace

HtmlElement poskytuje mechanismy pro validaci atributů a obsahu podle HTML DTD (Document Type Definition).

DTD je statické pole definující pro každý HTML tag, které atributy jsou povolené a jaký obsah může obsahovat. Texy načítá DTD ze souboru při inicializaci a uloží ho do statického pole. Struktura DTD mapuje název tagu na dvojici – pole povolených atributů a pole povoleného obsahu.

Metoda validateAttrs() kontroluje atributy elementu podle DTD. Pro daný tag získá seznam povolených atributů. Prochází všechny atributy elementu a ty, které nejsou v seznamu, odstraní. Speciální případy jsou atributy začínající data- nebo aria-, které jsou povolené, pokud je v DTD zástupný záznam data-* nebo aria-*.

Tato validace se typicky volá při aplikaci modifikátorů metodou decorate(). Zajišťuje, že i když uživatel zadá modifikátor s neplatným atributem pro daný tag, atribut se do finálního HTML nedostane. To je důležité pro bezpečnost a správnost HTML.

Metoda validateChild() kontroluje, zda daný potomek může být obsahem elementu. Přijímá potomka (HtmlElement nebo název tagu) a DTD. Pokud je element v DTD definován, metoda zkontroluje, zda potomek je v seznamu povoleného obsahu. Pokud ano, vrací true. Pokud ne, vrací false.

Tato validace se může použít při dynamickém sestavování DOM stromu pro zajištění korektní struktury. Například paragraph element nesmí obsahovat blokové elementy, takže validateChild() by odmítlo přidat div do p. V praxi Texy tuto validaci používá omezeně, protože struktura generovaná moduly je typicky správná by design.

Kombinace validateAttrs() a validateChild() poskytuje mechanismus pro zajištění validního HTML, i když vstup obsahuje nedůvěryhodná data nebo špatně formované konstrukce. Texy může být nakonfigurováno pro striktní validaci nebo může validaci vypnout pro maximální flexibilitu.

Modifikátory

Modifikátory poskytují způsob, jak přidat elementům dodatečné atributy, třídy, styly a zarovnání bez nutnosti psát přímé HTML.

Základní formát modifikátoru je tečka následovaná kombinací různých částí v kulatých, hranatých a složených závorkách: .(title)[class1 class2 #id]{style:value}<align>^valign. Celý modifikátor se píše před nebo na konec konstrukce, na kterou se aplikuje. Například "**text** .(Důležité)[highlight]{color:red}" vytvoří tučný text se třídou highlight, červenou barvou a title atributem Důležité.

Kulaté závorky obsahují title atribut nebo alt text. Text uvnitř se použije jako hodnota title atributu na výsledném elementu. Pokud element je obrázek, může se použít jako alt text. Uvnitř kulatých závorek je možné escapovat závorku zpětným lomítkem.

Hranaté závorky obsahují CSS třídy a volitelně ID. Třídy se píší jako slova oddělená mezerami. ID se píše s prefixem mřížky. Například [main-content selected #article-5] nastaví dvě třídy a jedno ID. Pokud je ID uvedeno vícekrát, použije se poslední.

Složené závorky obsahují CSS styly nebo HTML atributy. Styly se píší ve standardním CSS formátu property:value. Více stylů se odděluje středníky. Některé property jsou rozpoznány jako HTML atributy – například {href:url} se převede na atribut href, ne na CSS style. To umožňuje nastavit atributy, které není možné vyjádřit jinak.

Zarovnání se zadává pomocí speciálních znaků. < znamená vlevo, > vpravo, = pro do bloku, <> pro na střed. Vertikální zarovnání používá ^ pro nahoru, - pro střed a _ pro dolů. Tyto zkratky se převádějí buď na CSS třídy nebo inline styly podle konfigurace.

Části modifikátoru mohou být v libovolném pořadí a některé mohou být vynechány. Platný je modifikátor obsahující jen třídy .[highlight], jen title .(Poznámka) nebo jen styl .{color:blue}. Parser rozpozná jednotlivé části podle ohraničujících znaků.

Modifier třída

Třída Modifier slouží k parsování a uchovávání informací z modifikátoru.

Instance Modifier se typicky vytváří syntax handler, který předá konstruktoru text modifikátoru extrahovaný z regex matche. Konstruktor zavolá metodu setProperties(), která parsuje text a naplní properties objektu.

Veřejné properties obsahují jednotlivé části modifikátoru. Property $id obsahuje ID elementu jako řetězec nebo null. Property $classes je asociativní pole, kde klíče jsou názvy tříd a hodnoty jsou true. Property $styles je asociativní pole mapující CSS property na hodnoty. Property $attrs je asociativní pole s HTML atributy, které nejsou styly ani třídy.

Dvě speciální properties $hAlign a $vAlign obsahují horizontální a vertikální zarovnání jako řetězce left, right, center, justify nebo top, middle, bottom. Tyto hodnoty se později převádějí na CSS třídy nebo styly podle konfigurace Texy.

Property $title obsahuje text z kulatých závorek, který se použije jako title atribut nebo alt text u obrázků. Text je automaticky unescapován z HTML entit a zbaven escapovaných závorek.

Aplikace na elementy

Modifier objekt se aplikuje na HtmlElement pomocí metody Modifier::decorate().

Metoda decorate() přijímá instanci Texy a HtmlElement jako parametry. Postupně aplikuje jednotlivé části modifikátoru na element s ohledem na konfiguraci Texy, která může některé části zakázat nebo omezit.

Aplikace atributů kontroluje, které atributy jsou povolené pro daný tag podle Texy::$allowedTags konfigurace. Pokud jsou všechny atributy povolené, zkopírují se všechny atributy z Modifier do elementu. Pokud je povolen jen seznam konkrétních atributů, zkopírují se pouze ty, které jsou na seznamu.

Title atribut se vždy aplikuje, pokud je nastaven, ale text prochází typografickým post-processingem pro nahrazení uvozovek a dalších úprav.

Aplikace tříd a ID kontroluje konfiguraci Texy::$allowedClasses. Pokud jsou všechny třídy povolené, přidají se všechny třídy z Modifier do elementu a nastaví se ID. Pokud je povolen jen seznam konkrétních tříd, přidají se pouze ty, které jsou na seznamu. ID se přidá, jen pokud je v seznamu povolen řetězec začínající mřížkou.

Aplikace stylů probíhá podobně s kontrolou Texy::$allowedStyles. Povolené CSS properties se přidají do style atributu elementu. Pokud element již měl nějaké styly, modifikátorové styly se přidají nebo přepíšou existující.

Zarovnání se aplikuje buď jako CSS třída nebo inline style. Pokud je v Texy konfigurováno Texy::$alignClasses mapování pro daný typ zarovnání, přidá se odpovídající CSS třída. Pokud ne, přidá se inline style s text-align nebo vertical-align property.

Výsledkem je, že element má všechny atributy, třídy, styly a další vlastnosti z modifikátoru, ale pouze ty, které jsou povoleny aktuální konfigurací Texy. To zajišťuje bezpečnost při zpracování nedůvěryhodného vstupu.

Propagace modifikátorů

Modifikátory procházejí systémem v několika fázích, přičemž si zachovávají flexibilitu a umožňují úpravy na různých úrovních.

Syntax handler extrahuje text modifikátoru z regex matche a vytvoří novou instanci Modifier a naplní se jeho properties.

Modifier objekt se předává jako parametr do element handlerů. Handler dostává již parsovaný objekt, ne surový text. To umožňuje handleru snadno přistupovat k jednotlivým částem modifikátoru – třídám, stylům, zarovnání. Handler může modifikátor upravit před aplikací, například přidat další třídy nebo změnit styly.

Element handler vytváří HtmlElement a předá jej metodě Modifier::decorate(). V tomto okamžiku se modifikátor aplikuje na element. Metoda decorate() kontroluje konfigurace Texy a zajišťuje, že se aplikují pouze povolené části.

V některých případech modul kombinuje více modifikátorů. Například TableModule parsuje modifikátory na úrovni tabulky, řádků i buněk. Modifikátor buňky je vlastně klony modifikátoru sloupce, na kterém se pak aplikují dodatečné úpravy z modifikátoru konkrétní buňky. To umožňuje výchozí styly pro celý sloupec s možností přepsání v jednotlivých buňkách.