Přidání vlastní syntaxe

Tato kapitola popisuje, jak přidat do Texy zcela nové markup konstrukce, které standardně neexistují. Pokud chcete pouze změnit chování existujících prvků (například upravit zpracování obrázků nebo odkazů), přečtěte si kapitolu Úprava chování prvků.

Představte si, že chcete v dokumentaci automaticky vytvářet odkazy na uživatelské profily zápisem @@username. Nebo potřebujete speciální bloky pro upozornění typu :::warning. Texy tyto konstrukce nezná a nemůžete je vytvořit úpravou existujících prvků.

Vlastní syntaxe vám umožní definovat nové markup konstrukce. Zadáte, jak má konstrukce vypadat (pomocí regulárního výrazu), a napíšete funkci, která ji zpracuje. Texy pak vaši syntaxi rozpozná stejně jako své standardní konstrukce.

Registrace syntaxe

Texy poskytuje dvě metody pro registraci vlastní syntaxe podle toho, zda jde o inline nebo blokový prvek.

Line syntaxe

Line syntaxe slouží pro inline konstrukce uvnitř řádků textu. Registrujete ji metodou registerLinePattern():

$texy->registerLinePattern(
	callable $handler,
	string $pattern,
	string $name,
	?string $againTest = null,
);

Parametr $handler je callback funkce, která se zavolá při nálezu syntaxe. Může to být název funkce, anonymní funkce nebo pole [$object, 'method'].

Parametr $pattern je regulární výraz (PCRE), který definuje, jak vaše syntaxe vypadá v textu. Pattern by neměl být kotvený na začátek řádku (^), protože se hledá kdekoliv v textu. Použijte capturing groups pro zachycení dat, která potřebujete zpracovat.

Parametr $name je unikátní název syntaxe. Používá se v poli $texy->allowed pro zapnutí/vypnutí a předává se do handleru pro identifikaci. Doporučujeme používat prefixový styl jako custom/username nebo myapp/profile.

Parametr $againTest je volitelný regex pro optimalizaci. Pokud je zadán, Texy nejprve zkontroluje, zda text vůbec obsahuje něco, co by mohlo matchnout váš pattern. Teprve pokud $againTest uspěje, spustí se komplexnější pattern. To výrazně zrychlí zpracování, pokud máte složitý pattern a používá se jen zřídka.

Příklad registrace:

$texy->registerLinePattern(
	'usernameHandler',
	'#@@([a-z0-9_]+)#i',
	'custom/username',
);

Block syntaxe

Block syntaxe slouží pro víceřádkové blokové konstrukce. Registrujete ji metodou registerBlockPattern():

$texy->registerBlockPattern(
	callable $handler,
	string $pattern,
	string $name,
);

Parametry $handler a $name mají stejný význam jako u line syntaxí.

Parametr $pattern je regulární výraz, který musí být kotvený na začátek řádku (^) a často i na konec ($). BlockParser automaticky přidá modifikátory Am (anchored, multiline), takže je do patternu nepřidávejte. Pattern by měl matchnout celý blok nebo alespoň jeho začátek.

Příklad registrace:

$texy->registerBlockPattern(
	'alertHandler',
	'#^:::(warning|info|danger)\n(.+)$#s',
	'custom/alert',
);

Syntax handler

Syntax handler je funkce volaná parserem, když najde výskyt vaší syntaxe v textu. Jeho úkolem je zpracovat nalezená data a vrátit HTML element nebo řetězec.

Podrobné vysvětlení role syntax handleru v architektuře Texy najdete v kapitole Architektura a principy.

Pro line syntaxe

Signatura syntax handleru pro line syntaxe:

function(
	Texy\LineParser $parser,
	array $matches,
	string $name,
): Texy\HtmlElement|string|null

Parametr $parser poskytuje přístup k parseru a Texy objektu. Nejčastěji použijete $parser->getTexy() pro získání Texy instance.

Parametr $matches obsahuje výsledky regex matche. $matches[0] je celý matchnutý řetězec, $matches[1], $matches[2] atd. jsou capturing groups z vašeho patternu.

Parametr $name je název syntaxe, který jste zadali při registraci. Užitečné, pokud jeden handler zpracovává více syntaxí.

Návratová hodnota může být Texy\HtmlElement pro strukturovaný HTML výstup, string pro přímý HTML kód (který musíte protectovat), nebo null pro odmítnutí zpracování.

Handler může nastavit $parser->again = true, pokud chce, aby se obsah vytvořeného elementu znovu parsoval pro nalezení vnořených syntaxí.

Pro block syntaxe

Signatura syntax handleru pro block syntaxe:

function(
	Texy\BlockParser $parser,
	array $matches,
	string $name,
): Texy\HtmlElement|string|null

Parametry mají stejný význam jako u line syntaxí, jen dostáváte Texy\BlockParser místo LineParser.

BlockParser poskytuje metody pro práci s víceřádkovými strukturami:

  • $parser->next($pattern, &$matches) – matchne další řádek proti patternu a vrátí true/false
  • $parser->moveBackward($lines) – vrátí se o zadaný počet řádků zpět
  • $parser->isIndented() – vrací true, pokud je aktuální blok odsazený

LineParser API

Při práci s line syntaxemi máte k dispozici několik užitečných vlastností a metod.

Property $again řídí, zda se má právě zpracovaná syntaxe hledat znovu na stejné pozici po zpracování. Výchozí hodnota je false. Nastavte na true, pokud vytváříte element s obsahem, který může obsahovat další syntaxe:

function(
	Texy\LineParser $parser,
	array $matches,
	string $name,
): Texy\HtmlElement
{
	$el = new Texy\HtmlElement('span');
	$el->setText($matches[1]);

	// obsah může obsahovat další formátování
	$parser->again = true;

	return $el;
}

Metoda getTexy() vrací instanci Texy objektu, což potřebujete pro práci s protect() nebo přístup ke konfiguraci.

BlockParser API

Při práci s block syntaxemi máte k dispozici metody pro práce s víceřádkovými strukturami.

Metoda next($pattern, &$matches) zkusí matchnout další řádek v textu proti zadanému patternu. Pokud uspěje, naplní $matches výsledkem a posune interní pozici za tento řádek. Vrací true při úspěchu, false při neúspěchu:

while ($parser->next('#^\-\s+(.+)$#', $matches)) {
	// zpracuj další položku seznamu
	$item = $matches[1];
}

Metoda moveBackward($lines = 1) vrátí interní pozici o zadaný počet řádků zpět. Užitečné, když váš pattern matchnul víc než začátek bloku a chcete se vrátit na začátek:

// pattern matchnul 3 řádky, ale chceme číst od prvního
$parser->moveBackward(2);

Metoda isIndented() vrací true, pokud je aktuální blok odsazený (začíná mezerou nebo tabulátorem). To naznačuje, že jde o vnořený obsah.

Praktické příklady

Následující příklady ukazují reálné use-case pro vlastní syntaxe.

Uživatelské profily

Automatické vytváření odkazů na profily zápisem @@username:

$texy->registerLinePattern(
	function(
		Texy\LineParser $parser,
		array $matches,
		string $name,
	): Texy\HtmlElement
	{
		$username = $matches[1];

		$el = new Texy\HtmlElement('a');
		$el->attrs['href'] = '/user/' . urlencode($username);
		$el->attrs['class'][] = 'user-profile';
		$el->setText('@' . $username);

		return $el;
	},
	'#@@([a-z0-9_]+)#i',
	'custom/username'
);

Použití v textu:

Podívejte se na profil @@johndoe nebo @@jane_smith.

Alert boxy

Speciální bloky pro upozornění s různými typy:

$texy->registerBlockPattern(
	function(
		Texy\BlockParser $parser,
		array $matches,
		string $name,
	): Texy\HtmlElement
	{
		$type = $matches[1];  // warning, info, danger
		$content = $matches[2];

		$el = new Texy\HtmlElement('div');
		$el->attrs['class'][] = 'alert';
		$el->attrs['class'][] = 'alert-' . $type;

		$texy = $parser->getTexy();
		$el->parseBlock($texy, trim($content));

		return $el;
	},
	'#^:::(warning|info|danger)\n(.+?)(?=\n:::|$)#s',
	'custom/alert'
);

Použití v textu:

:::warning
Toto je důležité upozornění!
:::

:::info
Pro informaci: aktualizace proběhne zítra.
:::

Hashtagy

Automatické vytváření odkazů z hashtagů:

$texy->registerLinePattern(
	function(
		Texy\LineParser $parser,
		array $matches,
		string $name,
	): Texy\HtmlElement
	{
		$tag = $matches[1];

		$el = new Texy\HtmlElement('a');
		$el->attrs['href'] = '/tag/' . urlencode($tag);
		$el->attrs['class'][] = 'hashtag';
		$el->setText('#' . $tag);

		return $el;
	},
	'#\#([a-z0-9_]+)#i',
	'custom/hashtag',
	'#\##'  // optimalizace - hledej jen pokud je # v textu
);

Použití:

Článek o #php a #webdesign.

Zkratky

Automatické rozbalení zkratek s vysvětlením:

$abbreviations = [
	'HTML' => 'HyperText Markup Language',
	'CSS' => 'Cascading Style Sheets',
	'PHP' => 'PHP: Hypertext Preprocessor',
];

$texy->registerLinePattern(
	function(
		Texy\LineParser $parser,
		array $matches,
		string $name
	) use ($abbreviations): ?Texy\HtmlElement
	{
		$abbr = $matches[1];

		if (!isset($abbreviations[$abbr])) {
			return null;  // neznámá zkratka
		}

		$el = new Texy\HtmlElement('abbr');
		$el->attrs['title'] = $abbreviations[$abbr];
		$el->setText($abbr);

		return $el;
	},
	'#\b([A-Z]{2,})\b#',
	'custom/abbreviation'
);

Inline ikony

Vkládání ikon pomocí speciální syntaxe:

$texy->registerLinePattern(
	function(
		Texy\LineParser $parser,
		array $matches,
		string $name,
	): Texy\HtmlElement
	{
		$icon = $matches[1];

		$el = new Texy\HtmlElement('i');
		$el->attrs['class'][] = 'icon';
		$el->attrs['class'][] = 'icon-' . $icon;
		$el->attrs['aria-hidden'] = 'true';

		return $el;
	},
	'#:icon-([a-z-]+):#',
	'custom/icon'
);

Použití:

Klikněte na tlačítko :icon-download: pro stažení.

Poznámkový blok

Blok pro poznámky pod čarou:

$texy->registerBlockPattern(
	function(
		Texy\BlockParser $parser,
		array $matches,
		string $name
	): Texy\HtmlElement
	{
		$parser->moveBackward();

		$content = '';
		while ($parser->next('#^NOTE:\s*(.+)$#', $matches)) {
			$content .= $matches[1] . "\n";
		}

		$el = new Texy\HtmlElement('aside');
		$el->attrs['class'][] = 'note';

		$texy = $parser->getTexy();
		$el->parseBlock($texy, trim($content));

		return $el;
	},
	'#^NOTE:\s*(.+)$#m',
	'custom/note'
);

Použití:

NOTE: Toto je důležitá poznámka.
NOTE: Může být víceřádková.

Vlastní citace s autorem

Rozšířená syntaxe pro citace s uvedením autora:

$texy->registerBlockPattern(
	function(
		Texy\BlockParser $parser,
		array $matches,
		string $name,
	): Texy\HtmlElement
	{
		$author = $matches[1];
		$quote = $matches[2];

		$blockquote = new Texy\HtmlElement('blockquote');

		$texy = $parser->getTexy();
		$blockquote->parseBlock($texy, trim($quote));

		$cite = new Texy\HtmlElement('cite');
		$cite->setText($author);
		$blockquote->add($cite);

		return $blockquote;
	},
	'#^QUOTE\[([^\]]+)\]:\n(.+?)(?=\n\n|$)#s',
	'custom/quote'
);

Použití:

QUOTE[Albert Einstein]:
Fantazie je důležitější než vědění,
protože vědění je omezené.

Galerie obrázků

Speciální blok pro vytvoření galerie z více obrázků:

$texy->registerBlockPattern(
	function(
		Texy\BlockParser $parser,
		array $matches,
		string $name,
	): Texy\HtmlElement
	{
		$parser->moveBackward();

		$gallery = new Texy\HtmlElement('div');
		$gallery->attrs['class'][] = 'gallery';

		while ($parser->next('#^\[G\]\s*(.+)$#', $matches)) {
			$img = new Texy\HtmlElement('img');
			$img->attrs['src'] = trim($matches[1]);
			$img->attrs['loading'] = 'lazy';
			$gallery->add($img);
		}

		return $gallery;
	},
	'#^\[G\]\s*(.+)$#m',
	'custom/gallery'
);

Použití:

[G] image1.jpg
[G] image2.jpg
[G] image3.jpg

Kolize syntaxí

Když registrujete vlastní syntaxi, musíte dávat pozor, aby nekolidovala s existujícími syntaxemi Texy nebo s jinými vlastními syntaxemi.

Pořadí registrace záleží. Line syntaxe se hledají v pořadí, jak byly registrovány. Pokud více syntaxí může matchnout na stejné pozici, vyhrává ta, která byla registrována dříve. Proto registrujte specifičtější syntaxe před obecnějšími.

Buďte specifičtí v patterns. Čím konkrétnější je váš pattern, tím menší je riziko kolize. Pattern #\#\w+# matchne i #hashtag, což by mohlo kolidovat s nadpisy. Lepší je #(?<=\s)\#[a-z0-9_]+#i, který vyžaduje mezeru před hashtagem.

Testujte kombinace. Vyzkoušejte, jak vaše syntaxe funguje v kombinaci s existujícími konstrukcemi. Co se stane, když je váš markup uvnitř odkazu? Co když je uvnitř code bloku?

Používejte prefixované názvy. Místo username použijte custom/username nebo myapp/username. To zabrání konfliktům, pokud by Texy v budoucnu přidalo syntaxi stejného názvu.

Best practices

Vracejte null při neúspěchu. Pokud handler zjistí, že nemůže nebo nechce zpracovat daný match (například neznámá zkratka), vraťte null. Parser pak zkusí další syntaxe.

Používejte protect() pro HTML. Pokud vracíte raw HTML string místo HtmlElement, musíte ho protectovat pomocí $texy->protect($html, Texy::CONTENT_...). Jinak bude escapován.

Nastavte $parser->again správně. Pro line syntaxe, které vytváří element s textovým obsahem, který může obsahovat další syntaxe (formátování, odkazy), nastavte $parser->again = true.

Respektujte $texy->allowed. Pokud vytváříte modul s více syntaxemi, kontrolujte $texy->allowed[$name] před registrací patternu nebo v handleru před zpracováním.