autor: Tomasz Przechlewski
Prezentowany zestaw makr, z racji swojej prostoty, może być bardzo łatwo modyfikowany przez użytkownika w zależności od potrzeb. Jest to podstawowa zaleta stosowania prostej TeX-niki a nie gotowych formatów. Te ostatnie są skomplikowane, a ich przystosowanie do własnych potrzeb jest z reguły bardzo trudne.
Wstawianie do dokumentu TeX-owego numerów rozdziałów, punktów, tabel czy równań oraz używanie tych numerów jako odsyłaczy jest złą praktyką. Należy przyjąć zasadę, że na etapie tworzenia pliku źródłowego ostateczne numery tych elementów i odsyłacze do nich są nam nie znane. Elementy dokumentu winny być numerowane automatycznie przez TeX-a podczas jego kompilowania i w taki sam sposób (tzn. automatycznie) wstawiane odsyłacze. Tylko postępując w ten sposób oszczędzimy sobie wiele czasu podczas pracy nad kolejnymi wersjami dokumentu.
Ideę automatycznego wstawiania odsyłaczy przedstawimy na prostym
przykładzie systemu służącego do numerowania równań matematycznych.
Niech plik fermat.tex
zawiera następujący kod:
Równanie~(\ref{eq:fermat}) na
s.~\pref{eq:fermat} przedstawia słynne
twierdzenie Fermata:
$$\eqalignno{%
x^n + y^n &= z^n&\elab{eq:fermat}}$$
Historia dowodzenia (\ref{eq:fermat})
ilustruje znaczenie dostatecznie
szerokich marginesów...
po kompilacji powinniśmy otrzymać następujący wynik:
Równanie (1) na s. 1 przedstawia słynne twierdzenie Fermata:Zauważmy, że odnośniki mogą wskazywać ,,w tył'' (do tekstu już przeczytanego) jak i ,,w przód'' (do tekstu nie przeczytanego) konieczne jest zatem dwukrotne kompilowanie dokumentu do ich prawidłowego wyznaczenia (pierwsza kompilacja) i wstawienia. Użytkownik posługuje się w tym celu trzema następującymi instrukcjami:
Historia dowodzenia (1) ilustruje znaczenie dostatecznie szerokich marginesów...
\elab{etykieta}
\pref{etykieta}
\ref{etykieta}
\elab
zwiększa wartość
licznika równań o 1, wstawia do dokumentu bieżącą wartość tego licznika
oraz przesyła do pliku dodatkowego trzy informacje: bieżący numer strony,
numer równania, etykietę.
Podczas drugiej kompilacji TeX sprawdza czy ten
plik dodatkowy istnieje i jeżeli tak to zostaje on wczytany.
Zawarte tam informacje są wykorzystywane przez instrukcje
\ref
i \pref
do prawidłowego wstawienia odsyłaczy.Przejdźmy teraz do szczegółów.
Zamiast definować od razu komendę \elab
zdefiniujemy najpierw
makro \defreference
, mające dwa parametry,
z których pierwszy będzie etykietą dla odsyłacza,
a drugi zawierać będzie sam odsyłacz
oraz numer strony, na której
znajduje się odesłanie.
Na przykład jeżeli TeX na 44 stronie
dokumentu napotkał definicję
\defreference{eq:fermat}{\the\eqnC}
1
to jej wykonanie powinno spowodować wysłanie do pliku fermat.crf
następującej linii (zakładamy, że w chwili wykonywania
\defreference
licznik \eqnC
był równy 8):
\crlab{eq:fermat}{{8}{44}}Co ma oznaczać, że odsyłaczem dla etykiety
eq:fermat
jest 8, a odesłanie
wskazuje na stronę 44. Niżej przedstawiona definicja wykonuje zadanie
zapisania odpowiedniej linii do pliku fermat.crf
.
1. \def\defreference#1#2{% 2. \edef\@tmp{\string\crlab 3. {#1}{{#2}{\noexpand\folio}}}% 4. \write\crfile\expandafter{\@tmp}}Makro to musi sobie poradzić z podstawowym problemem: zapisania jednocześnie prawidłowego numeru odnośnika i prawidłowego numeru strony na którą ten odnośnik wskazuje. Numer strony nie jest znany w momencie napotkania instrukcji
\defreference
. Jest
on ustalany w momencie wykonywania procedury wyjścia
(output routine).
Z drugiej strony odnośnik jest znany i winien być zapisany natychmiast.
Jeżeli jego rozwinięcie zostanie opóźnione to otrzymany numer
będzie bieżącym numerem odnośnika w czasie wykonywania
tej procedury.
Problem ten jest rozwiązywany w liniach 2--4 makrodefinicji
\defreference
. Linie 2--3 definiują makro \@tmp
. Zamiast
\def
użyto \edef
(expanded definition) co gwarantuje, że
zawartość definicji \@tmp
zostanie rozwinięta natychmiast.
Nie ma to znaczenia gdy piszemy \defreference{foo}{7}
, ale
gdy odnośnik jest ustalany automatycznie, np.
\defreference{foo}{\the\eqnC}
, to chodzi nam o bieżącą
wartość licznika a nie jego wartość w chwili wykonywania
output routine.
Sekwencja \noexpand\folio
spowoduje, że komenda
\folio
(określająca numer strony), nie zostanie rozwinięta przy
rozwijaniu zawartości definicji \@tmp
. Zostanie to opóźnione do
czasu rozwijania komendy \write
podczas wykonywania output
routine.
W linii 4 zawartość definicji \@tmp
zostaje wysłana do pliku
dodatkowego za pomocą instrukcji \write
. Konstrukcja:
\write\crfile\expandafter{\@tmp}jest prostym przykładem zastosowania instrukcji
\expandafter
w celu zmiany porządku rozwinięcia dwóch żetonów (tokens)
{
oraz \@tmp
. Kiedy TeX napotka konstrukcję \write\crfile
oczekuje następnie żetonu {
(por. The TeXbook str. 226), a potem
ciągu żetonów kończącego się }
, który zapisuje do pliku.
Zapis do pliku jest opóźniony, co oznacza, że cały materiał
zawarty pomiędzy klamrami {
i }
nie jest rozwijany
w chwili napotkania instrukcji \write
ale umieszczany
jako tzw. whatsit na głównej liście pionowej (main
vertical list) i rozwijany później przy wykonywaniu output
routine (por. The TeXbook str. 227).
Jednakże wykonując sekwencję instrukcji z linii 4 TeX napotyka
\expandafter
zamiast {
. Powoduje to przeczytanie
(czyli rozwinięcie) przez
TeX-a najpierw makra \@tmp
a dopiero potem
umieszczenie przed rozwiniętym już makrem \@tmp
żetonu {
.
W efekcie na główną listę pionową, do późniejszego zapisu do pliku
fermat.crf
wędruje sekwencja żetonów tworząca makro \@tmp
a nie
żeton \@tmp
, który jest od tej chwili gotowy do użycia w następnej
instrukcji \defreference
. Gdyby na główną listę pionową trafiał
żeton \@tmp
rozwijany podczas wykonywania output routine
to zawartość (meaning) wszystkich żetonów byłaby jednakowa
i równa zawartości ostatniego zdefiniowanego żetonu \@tmp
--- rezultat
całkowicie różny od poprzedniego i raczej przez nas nie oczekiwany!
Makro \elab
można zdefiniować następująco:
5. \newcount\eqnC 6. \def\elab#1{\global\advance\eqnC 1 7. \defreference{#1}{\the\eqnC}% 8. (\the\eqnC)}Po pierwszej kompilacji plik
fermat.crf
zawiera informacje o wszystkich
odsyłaczach, które wykorzystujemy przy powtórnej kompilacji dokumentu.
W tym celu najpierw zdefiniujemy komendę \crlab
. Jak widać wyżej,
posiada ona dwa parametry, z których pierwszy zawiera etykietę odsyłacza
a drugi łącznie odsyłacz oraz numer strony, na której
odesłanie się znajduje.
Zarówno odsyłacz, jak i numer strony zawarte są
w parze nawiasów klamrowych.
9. \def\crlab#1#2{% 10. \global\expandafter 11. \def\csname #1\endcsname{#2}}Wykonanie makra
\crlab{eq:fermat}{{8}{44}}
spowoduje utworzenie nowego makra o nazwie eq:fermat
rozwijającego się dokładnie do {8}{44}
.
Wykorzystanie konstrukcji \csname
...\endcsname
umożliwia definiowanie
etykiet zawierających znaki o dowolnych ,,egzotycznych'' kodach,
np. &
, :
, #
, itd. Wręcz wskazane jest umieszczenie takich znaków,
co zapobiegnie niezamierzonej zmianie znaczenia ,,normalnych'' makr
o przypadkowo identycznej nazwie.
Teraz możemy zdefiniować instrukcję \ref
. Makro to powinno
wstawiać odsyłacz a pomijać numer strony. Kopiujemy w tym celu
pomysłowe rozwiązanie tego problemu z formatu {\LaTeX}, w którym
znowu w roli głównej występuje instrukcja \expandafter
:
12. \def\@car#1#2{#1} 13. \def\ref#1{% 14. \edef\@tempa{\csname #1\endcsname} 15. \expandafter\@car\@tempa}Makrodefinicja
\ref
ma jeden argument --- etykietę odnośnika. W linii
14 tworzona jest instrukcja \@tempa
, której zawartością jest wykonanie
makrodefinicji o nazwie tożsamej z nazwą etykiety. W następnej linii
najpierw rozwijana jest instrukcja \@tempa
, co oznacza rozwinięcie
jej zawartości do postaci
{odnośnik}{strona}
.
Następnie rozwijane jest makro \@car
, które z dwóch swoich
parametrów wstawia pierwszy a pomija drugi. Proste!
Skonstruowane w analogiczny sposób makro \pref
wstawia
numer strony a pomija odnośnik:
16. \def\@cdr#1#2{#2} 17. \def\pref#1{% 18. \edef\@tempa{\csname #1\endcsname} 19. \expandafter\@cdr\@tempa}Teraz określmy wreszcie plik, z którego pobierane będą odnośniki a następnie otwórzmy go do czytania:
20. \newread\crfile 21. \openin\crfile=\jobname.crf 22. \input \jobname.crfPowyższy kod ma jeden poważny minus. Mianowicie gdyby z jakichś względów plik
fermat.crf
nie istniał (w pierwszej kompilacji
dokumentu na pewno go nie będzie)
to wtedy próba wykonania linii
\input \jobname.crf
spowoduje błąd I can't find file fermat.crf
.
Lepiej zabezpieczyć się na tę okoliczność używając komendy \ifeof
. Tak
więc w powyższym fragmencie kodu ostatnią linię należy zastąpić przez:
22. \ifeof\crfile \else 23. \input \jobname.crf \fiWreszcie pozostaje do zdefiniowania plik do którego będą wysyłane informacje o odesłaniach:
24. \newwrite\crfile 25. \openout\crfile=\jobname.crfI te 25 linii kodu pokazane wyżej wystarczą dla TeX-a do prawidłowego wstawienia odpowiednich odsyłaczy. Wystarczą TeX-owi ale nie TeX-owcowi, który z pewnością popełniać będzie błędy. Dlatego powyższe makra należy rozbudować o obsługę błędów i ostrzeżeń. W szczególności należy zadbać o ostrzeganie użytkownika o:
.log
i na ekran a także
oznakować brakujące odnośniki w składanym dokumencie,
.log
i na ekran.
@
w nazwach komend, powinny zostać one zawarte pomiędzy liniami:
\catcode`@=11 ... \catcode`@=12
\nocrwarns
\nocrfile
\makecrfile
\crstatistics
\bye
wywołuje to makro.
%% -------------------------------- %% Cross-reference generic macros %% Tomasz Przechlewski %% Date: 02.01.1995 %% -------------------------------- \catcode`@=11 \def\@crwrn#1{\if@crwrns\immediate \write16{#1}\fi} \def\@markmissingcr{{\bf ??}\@marginmarker} \def\@marginmarker{\vadjust{\vbox to0pt{% \kern-.77\normalbaselineskip \hbox{{\it\kern\hsize\kern15pt?}}\vss}}} \newif\if@crwrns \global\@crwrnstrue % default \def\nocrfile{\global\@crfilefalse} \def\nocrwrns{\global\@crwrnsfalse} \def\@car#1#2{#1} \def\@cdr#1#2{#2} \long\def\@ifundefined#1#2#3{% \expandafter\ifx\csname #1\endcsname\relax#2\else#3\fi} \def\namedef#1{\expandafter \def\csname #1\endcsname} \def\newlabel#1#2{\@ifundefined{#1}{}% {\@crwrn{-> WARNING: multiple label #1}}% \global\namedef{#1}{#2}} \newread\crfile \openin\crfile=\jobname.crf \ifeof\crfile \@crwrn{-> WARNING: CR-FILE UNDEFINED!!} \else \@crwrn{READING REFS FROM \jobname.crf} \input \jobname.crf \fi \closein\crfile \def\makecrfile{% \openout\crfile=\jobname.crf} \def\nocrfile{\@crwrn{-> WARNING: CR-FILE not created} \def\crfile{-1}} \def\ref#1{\@nextcrf\@ifundefined{#1}{% \@markmissingcr \@crwrn{undefined cr -> \string#1}}% {\edef\@tempa{\csname #1\endcsname} \expandafter \@car\@tempa}} \def\pageref#1{\@nextpcrf \@ifundefined{#1}{\@markmissingcr \@crwrn{undefined cr -> \string#1}}% {\edef\@tempa{\csname #1\endcsname}% \expandafter \@cdr\@tempa}} \def\defreference#1#2{\@nextdrf% \edef\save{\string\newlabel{#1}% {{#2}{\noexpand\folio}}}% \write\crfile\expandafter{\save}} \newcount\@crfC\newcount\@pcrfC \newcount\@dcrfC \def\@nextdrf{\global\advance\@dcrfC1} \def\@nextcrf{\global\advance\@crfC1} \def\@nextpcrf{\global\advance\@pcrfC1} \def\crstatistics{% \@crwrn{==============================} \@crwrn{= REFERENCE STATISTICS =======} \@crwrn{= refs defined.... \the\@dcrfC} \@crwrn{= refs used....... \the\@crfC} \@crwrn{= page refs used.. \the\@pcrfC} \@crwrn{==============================}} \outer\def\bye{\crstatistics\end} \catcode`@=12 \endinput
TUGboat
, 10(3): s. 394--400.