Hlavní navigace

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

25. 11. 2002
Doba čtení: 9 minut

Sdílet

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.

CS24_early

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

\catcode`@=12

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

Byl pro vás článek přínosný?