Hlavní navigace

TeX pro každého - píšeme makra

Stanislav Brabec 25. 11. 2002

Dnešní díl věnujeme psaní vlastních maker. Dobrých učebnic jednoduchých maker najdeme dost, ale my si dnes do detailu rozebereme několik sice krátkých, ale netriviálních maker z praxe a ukážeme si, jakými cestami se ubírá TeXista, když chce napsat něco složitějšího. Dnešní díl je tedy určen pro budoucí TeXperty, nezávisle na nadstavbě, kterou používají.

U pomocných maker bývá zvykem, že jako součást jména použijeme znak @. Ten je po dobu definice maker definován v kategorii písmeno, ale při běžné práci v kategoriine­písmeno, takže takové makro nelze v běžné sazbě použít.

\catcode`@=11

Často stojíme nad dilematem, zda napsat jednoduché makro, které trpí různými omezeními nebo nepraktickým ovládáním, nebo se zamyslet, a napsat makro sice náročnější, ale čisté.

Makro \basevskip pro skoky zachovávající rejstřík

Jak již bylo řečeno, vzdálenost pro skoky se počítá poměrně složitě od spodní dotažnice v závislosti na pěti parametrech (hloubka horního boxu, výška spodního boxu, hodnoty \baselineskip,\li­neskip a \lineskiplimit). Toto makro se chová podobně jako \vskip, ale měří vzdálenost od účaří. Je užitečné při sazbě na rejstřík (tedy tehdy, když potřebujeme, aby řádky na stránkách byly v přesně určených výškách; takto napsané makro dodrží rejstřík, pokud je výška spodního boxu dostatečně malá).

ukázka použití makra \basevskip

Zdrojový kód k obrázku naleznete zde.

\def\basevskip{\par
  \ifdim\prevdepth>\z@\vskip-\prevdepth\fi
  \begingroup
  \afterassignment\bas@vskip\skip@=}
\def\bas@vskip{\vskip\skip@\endgroup
  \prevdepth\z@}

Nejdříve se asi zeptáte, proč jsme nepoužili jednodušší a srozumitelnější makro, které by dělalo totéž:

\def\basevskip#1{\ifdim\prevdepth>0pt
  \vskip-\prevdepth \fi
  \vskip#1%
  \prevdepth0pt}

Je to jednoduché. Takto napsané makro by vyžadovalo uzavřít dimenzi vždy do závorek, zatímco výše uvedený zápis je nepotřebuje. Makro definujeme bez parametrů. Ty si makro načte až během práce. Jak, to si vysvětlíme později.

\par

Makro \par ukončí případný rozpracovaný odstavec, zalomí ho a přejde do vertikálního režimu (a přitom naplní hodnotu \prevdepth). Pokud TeX již ve vertikálním režimu byl, neuškodí.

\ifdim\prevdepth>\z@\vskip-\prevdepth\fi

Tento úsek si testuje hodnotu \prevdepth (primitivum TeXu). Pokud poslední box přidaný do vertikálního seznamu měl kladnou hloubku, provede se skok zpět o tuto hodnotu. TeX nyní stojí na účaří.

V zápisu makra se několikrát nachází hodnota \z@. Proč? — Pokud napíšeme řetězec 0pt, jsou to tři znaky a pro TeX tři symboly (tokeny). Zabírají tedy tři políčka paměti a zpracovávají se více než tři jednotky času (po jejich zpracování následuje převod čísla a délkových jednotek). Ve výše uvedené zjednodušené definici se dokonce jednou zapracuje i konec řádku (převedený na mezeru; naštěstí se v tomto kontextu neprojeví v sazbě), takže půjde dokonce o čtyři symboly. Proto jsou nejběžnější konstanty uloženy v nadstavbách TeXu do několika vyhrazených registrů. Totéž platí o číslech – hodnota2 sice zabere jen jedno políčko paměti, ale její zpracování trvá déle než použití konstanty \tw@.

\begingroup

V dalším kódu použijeme pomocný registr (\skip@ je označení registru \skip0) k načtení dimenze. Abychom nemuseli při použití makra myslet na to, že tento registr používá, otevřeme skupinu. Všechna běžná přiřazení provedená uvnitř budou po ukončení skupiny zapomenuta a do pomocného registru se vrátí původní hodnota.

\afterassignment\bas@vskip\skip@=

Následující konstrukce říká zhruba toto: Makro \bas@vskip neexpanduj hned, ale počkej s tím až do nejbližšího přiřazovacího příkazu. Ten následuje hned za touto konstrukcí a je jím přiřazení do pomocného registru skoku.

Za zmínku stojí i znak = na konci. U přiřazení je nepovinný, ale přesto jsem jej do makra vložil. Má to jediný víceméně kosmetický důvod – pokud by nebyl součástí makra, byla by konstrukce \basevskip=3cm korektní. Ale protože jsem se chtěl přiblížit co nejvíce chování primitiva \vskip, rovnítko jsem vložil již do makra.

Tím však makro „nenadále“ končí. Co se tedy bude odehrávat? Expanze makra je dokončená a TeX se chystá přiřadit hodnotu do registru skoku. Bude tedy pokračovat dále expanzí textu, který stojí za voláním makra. Tímto trikem jsme TeX donutili načíst hodnotu skoku, aniž bychom ji uzavřeli do závorek!

Až bude přiřazení provedeno, přijde ke slovu odložená expanze makra \bas@vskip.

\vskip\skip@\endgroup

Nyní provedeme vertikální skok o hodnotu, uloženou do pomocného registru. Pak uzavřeme skupinu a hodnota registru je vrácena na svou starou hodnotu.

\prevdepth\z@

Protože si TeX dosud pamatuje hloubku posledního boxu a mohl by na ní zakládat své další počínání, je třeba ji vynulovat.

Tím bychom mohli popis makra skončit. Ale neskončíme – každý správný TeXista zavětří, končí-li jakéholiv makro číselnou hodnotou nebo dimenzí. Špatně provedená makra tohoto typu jsou příčinou množství záludných a těžko naleznutelných chyb.

Toto makro vypadá zcela korektně:

\def\cmskok{\hskip1cm}

A zde bude skutečně fungovat:

Mezi těmito slovy bude
centimetrový\cmskok skok.

Myslíte si, že bude fungovat i zde?

Taková špatně napsaná makra mají své
velké\cmskok minus -- jsou záludná.

Omyl!

! Missing number, treated as zero.
<to be read again>

Co se stalo? Zatímco nás to ani nenapadne, TeX přesně podle své logiky pokračuje v expanzi hodnoty skoku, dokud to jde. A protože shodou okolností najde volitelný řetězec minus, zpracuje ho a pak si oprávněně stěžuje, že jej nenásleduje hodnota, o kterou může skok zkrátit.

Jaké je řešení? TeX v sobě obsahuje primitivum, které nic nedělá. A přesně sem jej potřebujeme:

\def\cmskok{\hskip1cm\relax}

Pokud se jedná o expanzi čísel, je někdy možné použít ve stejném významu i mezeru.

Nyní se vrátíme ke konstrukci \prevdepth\z@. Proč tam nic takového nehrozí? Jednoduše proto, že \prevdepth je hodnota typu dimenze. Přiřazením \z@ se zcela naplnila a její expanze tím skončila.

Asi nejkurióznější demonstrací podobné chyby je následující konstrukce, která zcela nečekaně dá odpověď ne:

\dimen0=3cm\ifdim\dimen0>2cm ano\else ne\fi

Důvod, proč zde chyba nastane, zatímco v případě \z@ ne, je nyní ještě o stupeň hlubší – TeX už sice načetl celou hodnotu dimenze, ale zůstává v režimu čtení čísla až do okamžiku příchodu dalšího symbolu do jeho typografické části. Ale protože primitivum \ifdim se expanduje již v části pro expanzi maker, do typografické části dorazí první symbol až po provedení expanze, a to je v tomto případě již pozdě. Zde chování opraví dokonce i obyčejné mezera:

\dimen0=3cm \ifdim\dimen0>2cm ano\else ne\fi

Makro \totokse a \totoksb

TeX má registy typu toks, které umožňují uchovávat skupiny symbolů (tokenů – tedy maker a řetězců). Nenabízí však jednoduchý způsob, jak přidat nějaké symboly na konec registru. Napíšeme si na to makro, abycho mohli udělat například toto:

\long\def\totokse#1#2{#1%
  \expandafter{\the#1#2}}
\newtoks\mytoks
\mytoks{velký} \totokse\mytoks{ pes}

Makro má dva parametry – první je jméno registru, druhý je řetězec. Je definováno jako \long, aby nezhavarovalo, pokud se v registru vyskytne \par.

Po vložení parametrů bude situace vypadat asi takto:

\mytoks\expandafter{\the\mytoks  pes}

(Druhá z mezer před slovem pes je skutečná a zůstává v textu i po expanzi, ale my bohužel takový stav neumíme v běžném zápisu zachytit.)

Primitivum \expandafter umožňuje změnit pořadí expanze – říká zhruba toto – proveď nejdříve expanzi symbolu, který stojí až za následujícím symbolem (tedy ob jeden symbol). Pokud stojí uprostřed přiřazovacího příkazu, má stejný význam, jako by stálo i před ním:

\expandafter\mytoks\expandafter{%
  \the\mytoks  pes}

Expandovaným symbolem je \the. Toto primitivum však při své expanzi načte následující symbol a expanduje se na jeho hodnotu, tedy velký:

\mytoks{velký pes}

A to je přesně to, co jsme potřebovali.

Možná si řeknete – přidat něco na konec bylo jednoduché, ale co na začátek? I to je možné, ale půjdeme na to oklikou – v pomocném makru přehodíme pořadí argumentů:

\long\def\totoksb#1#2{%
  \expandafter\tot@ksb
  \expandafter{\the#1}{#2}{#1}}
\long\def\tot@ksb#1#2#3{#3{#2#1}}
\newtoks\mytoks
\mytoks{pes} \totoksb\mytoks{velký }

Po vložení parametrů dostaneme:

\expandafter\tot@ksb\expandafter{%
\the\mytoks}{velký }{\mytoks}

Díky \expandafter se bude jako první v pořadí expandovat \the:

\tot@ksb{pes}{velký }{\mytoks}

Teď přijde na řadu druhé makro a po vložení parametrů dostaneme:

\mytoks{velký pes}

Takže ani to nebylo nijak složité!

Je třeba se zmínit, že makra \totokse a \totoksbmají jednu velkou nectnost – pokud je obsah toks registru dlouhý, pracují pomalu, protože se při expanzi pracuje s celým jejich obsahem.

Makro \sanitize

Často se stane, že potřebujeme zapsat obsah makra do souboru, aniž by docházelo k expanzi. Běžně se to řeší vkládáním \noexpand nebo tzv. robustními definicemi (při jejich expanzi v režimu zápisu do souboru vytvářejí \noexpandsamy na sebe).

Toto makro ukazuje jeden z TeXových triků, jak zajistit totéž. Využije se při tom skutečnosti, že primitivum \meaning, jehož původním účelem je usnadnit ladění maker a které vypisuje obsah makra, vydává všechny znaky v kategorii nepísmeno.

\def\sanitize{\expandafter
  \@gobblemeaning\meaning}
\def\@gobblemeaning#1:->{}
\def\next{text\nobreak\space pokračuje}
\write0{\next}
\write0{\sanitize\next}

Co se děje při zpracovávání prvního zápisu? Dojde k úplné expanzi maker (tedy nejen jednou, jak to dělá \expandafter, ale až do primitiv):

text\penalty \@M  pokračuje

Taková expanze je nežádoucí: Při zpětném načítání souboru bude zřejmě znak @ v kategorii nepísmeno a\@M se tedy nebude načítat jako symbol \@M (registr s konstantou 10000), ale na nedefinované makro \@ a písmeno M. Mezeru, kterou vygenerovalo makro \space, TeX přeskočí.

A jak dopadne expanze druhého zápisu?

\sanitize\next se vyexpanduje na:

\expandafter\@gobblemeaning\meaning\next

Díky \expandafter se jako první vyexpanduje \meaning\next:

\@gobblemeaning macro:->text\nobreak
\space pokračuje

(\nobreak a \space však jiš nyní nejsou pro TeX makry, ale pouhými seskupeními znaků v kategorii nepísmeno. To však nelze v zápisu zachytit.)

Nyní se vyexpanduje makro \@gobblemeaning. To přijme jako svůj parametr vše až do řetězce : → (včetně) a nevydá při expanzi nic. Dostáváme řetězec složený ze znaků v kategorii nepísmeno, které jsou vůči další expanzi imunní:

text\nobreak \space pokračuje

Vidíme, že z TeXového hlediska je to ekvivalentní původnímu textu.

Makro \verzalky

A teď něco pro budoucí TeXové čaroděje (TeX-wizards). Představme si situaci, kdy máme sazbu, kde se používá kurzívní vyznačování ve stylu:

Tento text je {\em zvýrazněný}.

Nyní si však autor vzpomněl, že bude chtít místo kurzívy verzálky (velká písmena). Na to má sice TeX primitivum, ale to se bohužel volá jiným způsobem:

Tento text je \uppercase{verzálkami}.

Problém našeho makra \verzalky tedy spočívá v převedení pořadí argumentů, abychom mohli jednoduše zadat \let\em=\verzalky. Všechny jednoduché pokusy o takové makro selžou. TeXový čaroděj však vysype makro z rukávu:

\def\verzalky{%
  \aftergroup\uppercase\aftergroup{}%
}

Pro začátečníka takové makro vypadá podivně a nesrozumitelně.

TeX však čte text symbol po symbolu. A co udělá? Poté, co jsme závorkou otevřeli skupinu, narazí na naše makro a začne jej expandovat. V něm najde dvě primitiva \aftergroup, která odloží expanzi následujícího symbolu až za uzavření skupiny (půjde tedy o dva odložené symboly \uppercase{). Celá konstrukce bude ve výsledku ekvivalentní:

Tento text je {}\uppercase{zvýrazněný}.

Pokud nám při takových tricích nevycházejí závorky, pomáháme si náhradami za \bgroup a \egroup nebo „požráním“ nežádoucí závorky, tedy konstrukcemi typu:

\bgroup\aftergroup{}
{\let\next}

Při definicích takových maker jdou závorky do páru, ale při expanzi nikoliv – některé z nich jsou kompenzovány alternativním zápisem nebo pohlceny příkazem \let.

A nakonec vrátíme kategorii znaku @ a skončíme:

\catcode`@=12

(Makra jsou převzata z projektů upages a eplain.)

Našli jste v článku chybu?

28. 11. 2002 13:33

HK (neregistrovaný)

Zamyslel jsem se nad tim, proc je XSLT prave v XML. Jako jediny duvod vidim to, ze se meni jeho specifikace. Taky jsem nevidel nejaky jednodussi nastroj pro transformaci XML do neceho jineho. Kdyby se totiz XSLT nevyvijelo (nebo by se nepredpokladal vyvoj), nebyl by celkem problem vymyslet nejaky jazyk pro tranformace XML do jinych jazyku.

No, debata se presunula od TeXu ke XML :-) Sam XML pouzivam, ale porad mi hodne vadi ta rezie XML editoru, debuggeru a parseru, to mi proste nevymluvite :-…

27. 11. 2002 15:24

Jirka Kosek (neregistrovaný)

"XML se mozna nevyviji, ale vyviji se mnoho souvisejicich formatu, zalozenych na XML, to uplne staci."

A vam treba vadi, ze se pisi nove programy v jazyce C? Prece byste nevinil autory jazyka C, ze si v nem ostatni dovoluji psat nejake dalsi a nove aplikace.

"P.S. A nezlobte se na me, ze tento prispevek je trochu ironicky a provokativni, ale formulace typu: "Nevite o cem mluvite" nesnasim a lidi kteri je pouzivaji nemam rad."

Ja tu formulaci take nesnasim. Take ale nesnasim, kdyz nekdo …





Měšec.cz: U levneELEKTRO.cz už reklamaci nevyřídíte

U levneELEKTRO.cz už reklamaci nevyřídíte

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Vitalia.cz: Z tohoto konopí dělají léčivé masti

Z tohoto konopí dělají léčivé masti

120na80.cz: Co všechno ovlivňuje ženskou plodnost?

Co všechno ovlivňuje ženskou plodnost?

Vitalia.cz: Znáte „černý detox“? Ani to nezkoušejte

Znáte „černý detox“? Ani to nezkoušejte

DigiZone.cz: Recenze Westworld: zavraždit a...

Recenze Westworld: zavraždit a...

Měšec.cz: Kdy vám stát dá na stěhování 50 000 Kč?

Kdy vám stát dá na stěhování 50 000 Kč?

Podnikatel.cz: Chaos u EET pokračuje. Jsou tu další návrhy

Chaos u EET pokračuje. Jsou tu další návrhy

Podnikatel.cz: Podnikatelům dorazí varování od BSA

Podnikatelům dorazí varování od BSA

Root.cz: Certifikáty zadarmo jsou horší než za peníze?

Certifikáty zadarmo jsou horší než za peníze?

DigiZone.cz: ČT má dalšího zástupce v EBU

ČT má dalšího zástupce v EBU

Lupa.cz: Insolvenční řízení kvůli cookies? Vítejte v ČR

Insolvenční řízení kvůli cookies? Vítejte v ČR

120na80.cz: Pánové, pečujte o svoje přirození a prostatu

Pánové, pečujte o svoje přirození a prostatu

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

Lupa.cz: Teletext je „internetem hipsterů“

Teletext je „internetem hipsterů“

Měšec.cz: Jak levně odeslat balík přímo z domu?

Jak levně odeslat balík přímo z domu?

120na80.cz: Horní cesty dýchací. Zkuste fytofarmaka

Horní cesty dýchací. Zkuste fytofarmaka

Měšec.cz: Jak vymáhat výživné zadarmo?

Jak vymáhat výživné zadarmo?

Podnikatel.cz: Babiš: E-shopy z EET možná vyjmeme

Babiš: E-shopy z EET možná vyjmeme