Automatizovaná tvorba hezkého URL v šabloně Twig

Admin 11 minuty, 15 sekundy 309 grav twig šablony tipy

Twig filtr hyphenize obsažený v Gravu umí korektně zpracovat pouze tisknutelné znaky v základní ASCII tabulce (32-127). Pokud tedy chcete konvertovat na hezká URL i znaky, které jsou obsaženy i v jiných jazycích, než je angličtina, může vám pomoci následující tip.

Kdysi dávno jsem potřeboval zajistit, aby se z plného uživatelského jména autora vytvořil odkaz na jeho příspěvky, tj. aby například příspěvky autora publikujícího pod jménem Vít Petira byly dostupné pod URL /autor/vit-petira. Jinými slovy, aby se automaticky generovalo hezké URL, stejně jako je tomu v případě, když vytváříte novou stránku přes Admin Panel a z názvu se vám automaticky generuje strojový zápis.

Bulldozer

Použil jsem tedy dostupný Twig filtr hyphenize, avšak narazil jsem na problém, že se neumí vypořádat s diakritikou, resp. přesněji řečeno, že korektně pracuje pouze se znaky anglické abecedy, tedy umožňuje zpracovat pouze tisknutelné znaky v základní ASCII tabulce (32-127). Ostatní znaky pak zcela ignoruje a nahradí je spojovníkem. Naštěstí vždy pouze jedním, nicméně to na (ne)fukčnosti nic nemění. V případě hypotetického autora jménem Tomáš Černý dojde ke konverzi řetězce na tom-ern-, namísto očekávaného tomas-cerny.

Druhým nedostatkem pak bylo, že v případě specifických znaků uvedených na konci řetězce (a patrně i na začátku, to jsem tehdy nezkoušel) došlo rovněž k jejich nahrazení spojovníkem, což nebylo žádoucí. Příkladem může být – kromě shora uvedeného písmene „ý“ na konci příjmení – znak závorky, kdy z řetězce Page(s) vzniklo page-s-, namísto očekávaného page-s.

Logickým krokem tedy bylo, že jsem použil Twig filtr, resp. Twig funkci regex_replace pro nahrazení znaků v řetězci. Ten tehdy bohužel (z doposud mi neznámých důvodů) ignoroval proměnné stránky, v mém případě tedy page.header.author, v níž bylo uloženo plné jméno autora příspěvku. Paradoxní bylo, že pokud se řetězec zpracovával přímo, vše fungovalo. Uvedené nedostatky jsem tedy poslal jako návrh na vylepšení, avšak výsledkem byla pouze stručná výměna informací s hlavním vývojářem Gravu a vše se uložilo k ledu. Pro komplexnost uvádím, že požadavek jsem tehdy vyřešil jiným způsobem, a to sekundárním zápisem strojového jména autora do proměnné page.header.authorlink, což nebylo zcela ideální, ale účel to splnilo.

A ještě jedna odbočka – co se týče autorů a jejich vazeb na konkrétní příspěvky, je daleko vhodnější použít taxonomii, kterou má Grav velmi sofistikovaně propracovanou. Současně je pak daleko vhodnější vydat se opačnou cestou, tedy nepoužívat plné uživatelské jméno, ale pouze strojové uživatelské jméno (login), z něhož lze následně získat další údaje, a to nejen plné uživatelské jméno, ale rovněž i e-mail, web, roli apod. Nový koncept Flex-Objects integrovaný plně do Gravu od verze 1.7 dává uvedenému další rozměr, neboť lze zpracovávát i data autorů, kteří nemusejí být nezbytně registrováni jako uživatelé. Tolik na vysvětlenou, v budoucnu se k tomuto vrátím. Uvedené názorně ilustruje, že Grav je zcela flexibilní systém a nemá žádná výrazná omezení.

Důvodem, proč o uvedeném nyní píši je, že před pár týdny byl tento problém v rámci kontroly již vyřešených, potažmo neřešitelných a odložených problémů, jedním z autorů Gravu bez dalšího uzavřen, což mě vedlo k opětovnému vyzkoušení shora uvedených postupů. Světe div se, dva ze tří problémů se vyřešily. Konkrétně pak nedochází k doplňování spojovníku na konci vygenerovaného řetězce a především lze zpracovávat řetězec přímo z proměnné stránky. Co zůstává dosud nefunkční, je zpracování neanglických znaků.

Následná diskuse zapříčinila opětovné otevření příspěvku a možný příslib integrace konverze znaků s diakritikou v jiných, než anglických jazycích, přičemž příspěvek byl označen štítkem enhancement (vylepšení), což je značně pozitvní. Otázkou zůstává, zda a kdy k uvedenému nakonec opravdu dojde.

Pokud jste uvedený požadavek také řešili a nechcete čekat, až autoři Gravu implementují požadované rozšíření, lze použít následující řešení, které splní vaše očekávání. Vycházím z výchozího souboru i18n-ascii.example.txt, který je v různých obměnách k nalezení v rozličných projektech na internetu. Obvykle se liší v několika bajtech, podle toho, jak si jej jednotliví autoři upravili pro své potřeby (např. znak Ä je ve výchozím souboru transformován na Ae, nicméně někdo si jej upraví na A). Jedinou významnou změnou, kterou jsem provedl, je úprava malých písmen azbuky, která jsou ve výchozím souboru konvertována ne zcela logicky na velká (s azbukou – a nejen jí – je spojen ještě jeden problém, který je blíže popsán v posledním odstavci). Po odstranění nadbytečných řádků je k dispozici celkem 561 znaků a stejný počet jejich konvertovaných verzí (obvykle jednoznakových, avšak v některýh případech i víceznakových, popřípadě i prázdných). Původní soubor i18n-ascii.example.txt, aktualizovaný o shora uvedené změny, jsem následně zkonvertoval a použil do následujícího Twig kódu:

{% set string = page.header.string %}

{% set diacritics = ['/À/', '/Á/', '/Â/', '/Ã/', '/Ä/', '/Å/', '/Æ/', '/Ā/', '/Ą/', '/Ă/', '/Ç/', '/Ć/', '/Č/', '/Ĉ/', '/Ċ/', '/Ď/', '/Đ/', '/È/', '/É/', '/Ê/', '/Ë/', '/Ē/', '/Ę/', '/Ě/', '/Ĕ/', '/Ė/', '/Ĝ/', '/Ğ/', '/Ġ/', '/Ģ/', '/Ĥ/', '/Ħ/', '/Ì/', '/Í/', '/Î/', '/Ï/', '/Ī/', '/Ĩ/', '/Ĭ/', '/Į/', '/İ/', '/IJ/', '/Ĵ/', '/Ķ/', '/Ľ/', '/Ĺ/', '/Ļ/', '/Ŀ/', '/Ł/', '/Ñ/', '/Ń/', '/Ň/', '/Ņ/', '/Ŋ/', '/Ò/', '/Ó/', '/Ô/', '/Õ/', '/Ö/', '/Ø/', '/Ō/', '/Ő/', '/Ŏ/', '/Œ/', '/Ŕ/', '/Ř/', '/Ŗ/', '/Ś/', '/Ş/', '/Ŝ/', '/Ș/', '/Š/', '/Ť/', '/Ţ/', '/Ŧ/', '/Ț/', '/Ù/', '/Ú/', '/Û/', '/Ü/', '/Ū/', '/Ů/', '/Ű/', '/Ŭ/', '/Ũ/', '/Ų/', '/Ŵ/', '/Ŷ/', '/Ÿ/', '/Ý/', '/Ź/', '/Ż/', '/Ž/', '/à/', '/á/', '/â/', '/ã/', '/ä/', '/ā/', '/ą/', '/ă/', '/å/', '/æ/', '/ç/', '/ć/', '/č/', '/ĉ/', '/ċ/', '/ď/', '/đ/', '/è/', '/é/', '/ê/', '/ë/', '/ē/', '/ę/', '/ě/', '/ĕ/', '/ė/', '/ƒ/', '/ĝ/', '/ğ/', '/ġ/', '/ģ/', '/ĥ/', '/ħ/', '/ì/', '/í/', '/î/', '/ï/', '/ī/', '/ĩ/', '/ĭ/', '/į/', '/ı/', '/ij/', '/ĵ/', '/ķ/', '/ĸ/', '/ł/', '/ľ/', '/ĺ/', '/ļ/', '/ŀ/', '/ñ/', '/ń/', '/ň/', '/ņ/', '/ʼn/', '/ŋ/', '/ò/', '/ó/', '/ô/', '/õ/', '/ö/', '/ø/', '/ō/', '/ő/', '/ŏ/', '/œ/', '/ŕ/', '/ř/', '/ŗ/', '/ś/', '/š/', '/ş/', '/ť/', '/ţ/', '/ù/', '/ú/', '/û/', '/ü/', '/ū/', '/ů/', '/ű/', '/ŭ/', '/ũ/', '/ų/', '/ŵ/', '/ÿ/', '/ý/', '/ŷ/', '/ż/', '/ź/', '/ž/', '/ß/', '/ſ/', '/Α/', '/Ά/', '/Ἀ/', '/Ἁ/', '/Ἂ/', '/Ἃ/', '/Ἄ/', '/Ἅ/', '/Ἆ/', '/Ἇ/', '/ᾈ/', '/ᾉ/', '/ᾊ/', '/ᾋ/', '/ᾌ/', '/ᾍ/', '/ᾎ/', '/ᾏ/', '/Ᾰ/', '/Ᾱ/', '/Ὰ/', '/Ά/', '/ᾼ/', '/Β/', '/Γ/', '/Δ/', '/Ε/', '/Έ/', '/Ἐ/', '/Ἑ/', '/Ἒ/', '/Ἓ/', '/Ἔ/', '/Ἕ/', '/Έ/', '/Ὲ/', '/Ζ/', '/Η/', '/Ή/', '/Ἠ/', '/Ἡ/', '/Ἢ/', '/Ἣ/', '/Ἤ/', '/Ἥ/', '/Ἦ/', '/Ἧ/', '/ᾘ/', '/ᾙ/', '/ᾚ/', '/ᾛ/', '/ᾜ/', '/ᾝ/', '/ᾞ/', '/ᾟ/', '/Ὴ/', '/Ή/', '/ῌ/', '/Θ/', '/Ι/', '/Ί/', '/Ϊ/', '/Ἰ/', '/Ἱ/', '/Ἲ/', '/Ἳ/', '/Ἴ/', '/Ἵ/', '/Ἶ/', '/Ἷ/', '/Ῐ/', '/Ῑ/', '/Ὶ/', '/Ί/', '/Κ/', '/Λ/', '/Μ/', '/Ν/', '/Ξ/', '/Ο/', '/Ό/', '/Ὀ/', '/Ὁ/', '/Ὂ/', '/Ὃ/', '/Ὄ/', '/Ὅ/', '/Ὸ/', '/Ό/', '/Π/', '/Ρ/', '/Ῥ/', '/Σ/', '/Τ/', '/Υ/', '/Ύ/', '/Ϋ/', '/Ὑ/', '/Ὓ/', '/Ὕ/', '/Ὗ/', '/Ῠ/', '/Ῡ/', '/Ὺ/', '/Ύ/', '/Φ/', '/Χ/', '/Ψ/', '/Ω/', '/Ώ/', '/Ὠ/', '/Ὡ/', '/Ὢ/', '/Ὣ/', '/Ὤ/', '/Ὥ/', '/Ὦ/', '/Ὧ/', '/ᾨ/', '/ᾩ/', '/ᾪ/', '/ᾫ/', '/ᾬ/', '/ᾭ/', '/ᾮ/', '/ᾯ/', '/Ὼ/', '/Ώ/', '/ῼ/', '/α/', '/ά/', '/ἀ/', '/ἁ/', '/ἂ/', '/ἃ/', '/ἄ/', '/ἅ/', '/ἆ/', '/ἇ/', '/ᾀ/', '/ᾁ/', '/ᾂ/', '/ᾃ/', '/ᾄ/', '/ᾅ/', '/ᾆ/', '/ᾇ/', '/ὰ/', '/ά/', '/ᾰ/', '/ᾱ/', '/ᾲ/', '/ᾳ/', '/ᾴ/', '/ᾶ/', '/ᾷ/', '/β/', '/γ/', '/δ/', '/ε/', '/έ/', '/ἐ/', '/ἑ/', '/ἒ/', '/ἓ/', '/ἔ/', '/ἕ/', '/ὲ/', '/έ/', '/ζ/', '/η/', '/ή/', '/ἠ/', '/ἡ/', '/ἢ/', '/ἣ/', '/ἤ/', '/ἥ/', '/ἦ/', '/ἧ/', '/ᾐ/', '/ᾑ/', '/ᾒ/', '/ᾓ/', '/ᾔ/', '/ᾕ/', '/ᾖ/', '/ᾗ/', '/ὴ/', '/ή/', '/ῂ/', '/ῃ/', '/ῄ/', '/ῆ/', '/ῇ/', '/θ/', '/ι/', '/ί/', '/ϊ/', '/ΐ/', '/ἰ/', '/ἱ/', '/ἲ/', '/ἳ/', '/ἴ/', '/ἵ/', '/ἶ/', '/ἷ/', '/ὶ/', '/ί/', '/ῐ/', '/ῑ/', '/ῒ/', '/ΐ/', '/ῖ/', '/ῗ/', '/κ/', '/λ/', '/μ/', '/ν/', '/ξ/', '/ο/', '/ό/', '/ὀ/', '/ὁ/', '/ὂ/', '/ὃ/', '/ὄ/', '/ὅ/', '/ὸ/', '/ό/', '/π/', '/ρ/', '/ῤ/', '/ῥ/', '/σ/', '/ς/', '/τ/', '/υ/', '/ύ/', '/ϋ/', '/ΰ/', '/ὐ/', '/ὑ/', '/ὒ/', '/ὓ/', '/ὔ/', '/ὕ/', '/ὖ/', '/ὗ/', '/ὺ/', '/ύ/', '/ῠ/', '/ῡ/', '/ῢ/', '/ΰ/', '/ῦ/', '/ῧ/', '/φ/', '/χ/', '/ψ/', '/ω/', '/ώ/', '/ὠ/', '/ὡ/', '/ὢ/', '/ὣ/', '/ὤ/', '/ὥ/', '/ὦ/', '/ὧ/', '/ᾠ/', '/ᾡ/', '/ᾢ/', '/ᾣ/', '/ᾤ/', '/ᾥ/', '/ᾦ/', '/ᾧ/', '/ὼ/', '/ώ/', '/ῲ/', '/ῳ/', '/ῴ/', '/ῶ/', '/ῷ/', '/¨/', '/΅/', '/᾿/', '/῾/', '/῍/', '/῝/', '/῎/', '/῞/', '/῏/', '/῟/', '/῀/', '/῁/', '/΄/', '/΅/', '/`/', '/῭/', '/ͺ/', '/᾽/', '/А/', '/Б/', '/В/', '/Г/', '/Д/', '/Е/', '/Ё/', '/Ж/', '/З/', '/И/', '/Й/', '/К/', '/Л/', '/М/', '/Н/', '/О/', '/П/', '/Р/', '/С/', '/Т/', '/У/', '/Ф/', '/Х/', '/Ц/', '/Ч/', '/Ш/', '/Щ/', '/Ы/', '/Э/', '/Ю/', '/Я/', '/а/', '/б/', '/в/', '/г/', '/д/', '/е/', '/ё/', '/ж/', '/з/', '/и/', '/й/', '/к/', '/л/', '/м/', '/н/', '/о/', '/п/', '/р/', '/с/', '/т/', '/у/', '/ф/', '/х/', '/ц/', '/ч/', '/ш/', '/щ/', '/ы/', '/э/', '/ю/', '/я/', '/Ъ/', '/ъ/', '/Ь/', '/ь/', '/ð/', '/Ð/', '/þ/', '/Þ/'] %}

{% set characters = ['A', 'A', 'A', 'A', 'Ae', 'A', 'A', 'A', 'A', 'A', 'C', 'C', 'C', 'C', 'C', 'D', 'D', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'G', 'G', 'G', 'G', 'H', 'H', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'IJ', 'J', 'K', 'K', 'K', 'K', 'K', 'L', 'N', 'N', 'N', 'N', 'N', 'O', 'O', 'O', 'O', 'Oe', 'O', 'O', 'O', 'O', 'OE', 'R', 'R', 'R', 'S', 'S', 'S', 'S', 'S', 'T', 'T', 'T', 'T', 'U', 'U', 'U', 'Ue', 'U', 'U', 'U', 'U', 'U', 'U', 'W', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'a', 'a', 'a', 'a', 'ae', 'a', 'a', 'a', 'a', 'ae', 'c', 'c', 'c', 'c', 'c', 'd', 'd', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'f', 'g', 'g', 'g', 'g', 'h', 'h', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'ij', 'j', 'k', 'k', 'l', 'l', 'l', 'l', 'l', 'n', 'n', 'n', 'n', 'n', 'n', 'o', 'o', 'o', 'o', 'oe', 'o', 'o', 'o', 'o', 'oe', 'r', 'r', 'r', 's', 's', 's', 't', 't', 'u', 'u', 'u', 'ue', 'u', 'u', 'u', 'u', 'u', 'u', 'w', 'y', 'y', 'y', 'z', 'z', 'z', 'ss', 'ss', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'B', 'G', 'D', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'Z', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'TH', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'K', 'L', 'M', 'N', 'KS', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'P', 'R', 'R', 'S', 'T', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'F', 'X', 'PS', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'b', 'g', 'd', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'z', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'th', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'k', 'l', 'm', 'n', 'ks', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'p', 'r', 'r', 'r', 's', 's', 't', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'f', 'x', 'ps', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'A', 'B', 'V', 'G', 'D', 'E', 'E', 'ZH', 'Z', 'I', 'I', 'K', 'L', 'M', 'N', 'O', 'P', 'R', 'S', 'T', 'U', 'F', 'KH', 'TS', 'CH', 'SH', 'SHCH', 'Y', 'E', 'YU', 'YA', 'a', 'b', 'v', 'g', 'd', 'e', 'e', 'zh', 'z', 'i', 'i', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'f', 'kh', 'ts', 'ch', 'sh', 'shch', 'y', 'e', 'yu', 'ya', '', '', '', '', 'd', 'D', 'th', 'TH'] %}

{{ string }}

{{ string|hyphenize }}

{{ string|regex_replace(diacritics, characters) }}

{{ string|regex_replace(diacritics, characters)|hyphenize }}

Uvedený Twig kód umístěný do šablony stránky dynamicky generuje požadovaný výstup, který snadno integrujeme do tagů pro tvorbu odkazu. Není nezbytně nutné, aby zdrojový řetězec vycházel výhradně z proměnné stránky, neboť může být zadán rovněž přímo do proměnné string, popř. naopak pocházel ze vstupního formuláře umístěného na stránce (což mě inspiruje pro napsání dalšího příkladu, který – doufám – přidám někdy v budoucnu a pokud si vzpomenu, tak zde vložím i odkaz). Hlavička stránky (frontmatter) vypadá následovně:

string: 'Příliš žluťoučký kůň úpěl ďábelské ódy, přičemž jej slyšeli všichni kolemjdoucí, ať již se jednalo o českého uměleckého kováře inženýra Tomáše Černého, německého zpěváka Jürgena Großmanna, francouzského malíře Françoise Funèse nebo ruského umělce Александра Юрьевича Яковлева, což je Α a Ω úplně všeho, jakože 2 Π r = O, € a $ jsou měny a symboly © a ® značí ochranné prvky.'

Závěrem asi tolik, že ani toto řešení není zcela ideální, protože z příkladu je zřejmé, že některé znaky cyrilice nebyly zkonvertovány zcela korektně, např. Ю => YU => y-u. To je však již nad rámec tohoto článku. Pro úplnost pouze doplním, že uvedené lze jednoduše opravit nahrazením dvojice znaků YU znaky Yu, což je koneckonců i správné řešení (není důvod, proč by druhé písmeno mělo být velké, viz Ä => Ae). Totéž platí i pro další dvojznaky, nejen v cyrilici (azbuce), nehledě na jejich hypotetickou výslovnost, která nutně nemusí odrážet skutečný stav (např. v češtině by bylo vhodnější převést znaky azbuky následovně Ю => Ju, ж => z nebo х => ch). Otázkou je, jak se například zachovat ke znaku Œ, tj. zda jej převést na OE nebo Oe, potažmo pak ke znakům řecké abecedy, kdy dochází nepřehledné konverzi Ω => O nebo Π => P (zde by bylo spíše žádoucí převést je do plného znění Ω => Omega nebo Π => Pi) a nakonec i k symbolům majícím základ v písmu, které nejsou – v konečném důsledku asi logicky – v souvislosti s překlady vůbec řešeny (€ => Euro nebo © => Copyright), ale to je již skutečně zcela samostatná kapitola, a je to na volnosti každého programátora, jak si nahrazování přizpůsobí svým potřebám, optimálně po dohodě s koncovým zákazníkem, popř. i konzultaci s jazykovědcem.

Přílohy:

Předchozí příspěvek Následující příspěvek