\input csmac % Makra pro češtinu \pageheight=9.5in \fullpageheight=9.8in \setpage %\nocon % omit table of contents \datethis % print date on listing \def\begitems{\medskip\bgroup\catcode`\*=13 \narrower\narrower} \def\enditems{\par\egroup\medskip} {\catcode`\*=13 \gdef*{\par\noindent\llap{$\bullet$\ }\ignorespaces}} @* PROGRAM VLNA. Program čte vstupní textový soubor a nahrazuje za specifikovanými jednopísmennými slovy (např.~v, k, u) mezery symbolem \uv{\.{\char126}}. To zabrání při následném zpracování \TeX{}em zlomit řádek na nevhodných místech, která jsou v rozporu s typografickou normou. Program sestává z těchto hlavních celků: @c @@/ @@/ @@/ @@/ @ @ Definujeme |BANNER|, což je text, který se objevi při startu programu a obsahuje číslo verze programu. Zde je názorně vidět, že míchání dvou jazyků se nevyhneme. Při tisku textů na terminál nesmíme předpokládat, že tam budou české fonty. V~této dokumentaci se setkáme se třemi jazyky: angličtinou (většinou v~kódu programu, cestinou v~/* komentářích */ a češtinou jinde. Tu cestinu si vynutil fakt, že DOS-ovská varianta \.{tangle} a \.{weave} se nesnáší s~akcentovanými písmeny v~/* komentářích */. A~nyní už slíbený (vícejazyčný) |BANNER|. @d BANNER "This is program vlna, version 1.2, (c) 1995, 2002 Petr Olsak\n" @ V programu jsou použity knihovní funkce, jejíchž prototypy jsou definovány ve třech standardních hlavičkových souborech. @= #include #include #include @ Definujeme konstanty pro návratový kód. |OK| pro úspěšný běh, |WARNING| při výskytu aspoň jedné varovné zprávy, |IO_ERR| pro chybu v~přístupu ke vtupním nebo výstupním souborům, |BAD_OPTIONS| pro syntaktickou chybu na příkazové řádce a |BAD_PROGRAM| pro případ havárie programu. Ta by neměla nikdy nastat. Proměnná |status| bude obsahovat návratový kód a proměnná |prog_name| bude ukazovat na text nultého parametru příkazové řádky. @d OK 0 @d WARNING 1 @d IO_ERR 2 @d BAD_OPTIONS 3 @d BAD_PROGRAM 4 @= char *prog_name; int status; @ Základní rozvržení funkce |main|. @= int main (argc,argv) int argc; char **argv; { @; prog_name=argv[0]; status = OK; @; if (!silent) fprintf (stderr, BANNER); @; @; return status; } @* Parametry příkazového řádku. Program čte z~příkazového řádku postupně (nepovinné) parametry, které začínají znakem \uv{\.{-}}. Pak následují jména vstupních a výstupních souborů. \begitems * \.{-f} \dots\ program pracuje jako filtr (viz sekce |@|). Není-li tento parametr použit, program pracuje v tzv. standardním režimu, kdy jednotlivé soubory jsou vstupní i výstupní. * \.{-s} \dots\ program nevypíše |BANNER|, ani sumarizaci, ani varování, při nichž není program předčasně ukončen. Všechny tyto výpisy směřují do |stderr|, takže pokud program pracuje v režimu \uv{filtr}, není nutné tento parametr použít. * \.{-r} \dots\ program maže pracovní soubor (soubory), které vytváří ve standardním režimu (tj. není použit \.{-f}). V reřimu filter nemá tento parametr vliv. * \.{-v} \dots\ parametr definuje skupinu písmen, které budou interpretovány jako neslabičné předložky. Např. \.{-v KkSsVvZzOoUuAI}. Pokud není parametr uveden, je použita skupina uvedená v tomto příkladě. * \.{-m} \dots\ program neprovádí kontrolu math/text módů, tj. vlnkuje i uvnitř matematického módu \TeX{}u. (Implicite tam nevlnkuje). * \.{-n} \dots\ prorgram neprovádí kontrolu verbatim módu, tj. vlnkuje i uvnitř verbatim módu definovaném běžnými prostředími. Imlicite ve verbatim prostředí nevlnkuje. * \.{-l} \dots\ La\TeX{} režim. Při kontrole text-math-verbatim módů jsou brány v úvahu další sekvence, obvyklé v La\TeX{}ových dokumentech. * \.{-w} \dots\ WEB režim. Ohraničení verbatim módu je doplněno znaky používanými v dokumentech WEB (např. tento dokument). Důsledek: program vlnkuje dokumentační část každé sekce, ale nikoli kód. \enditems Definujeme funkci |printusage|, které tiskne (při chybě) stručný přehled možných parametrů. Nepodařilo se mi zjistit, jak se ve WEBu napíše kulturně dlouhý string obsahující \.{\char92n} s formátovacími požadavky. Byl jsem nucen to takto nehezky zapsat. @= void printusage () { fprintf(stderr, "usage: vlna [opt] [filenames]\n" " opt -f : filter mode: file1 file2 ... file1->file2\n" " file1 ... file1->stdout\n" " ... stdin->stdout\n" " nofilter: file1 [file2 file3 ...] all are in/out\n" " -s : silent: no messages to stderr\n" " -r : rmbackup: if nofilter, removes temporary files\n" " -v charset : set of lettres to add tie, default: KkSsVvZzOoUuAI\n" " -m : nomath: ignores math modes\n" " -n : noverb: ignores verbatim modes\n" " -l : LaTeX mode\n" " -w : web mode\n"); } @ Proměnné |isfilter|, |silent|, |rmbackup|, |nomath|, |noverb|, |latex|, resp. |web| říkají, že je nastaven parametr \.{-f}, \.{-s}, \.{-r}, \.{-m}, \.{-n}, \.{-l}, resp. \.{-w}. Proměnná |charset| ukazuje buď na implicitní skupinu znaků |charsetdefault|, nebo (při použití parametru \.{-v}) na text uvedený v příkazovém řádku. @= int isfilter=0, silent=0, rmbackup=0, nomath=0, noverb=0, web=0, latex=0; char charsetdefault[]="KkSsVvZzOoUuAI"; char *charset=charsetdefault; @ @= while (argc>1 && argv[1][0] == '-') { if (argv[1][2] != 0) printusage (), exit (BAD_OPTIONS); switch(argv[1][1]) { case 'f': isfilter = 1; break; case 's': silent = 1; break; case 'r': rmbackup = 1; break; case 'v': argv++; argc--; charset = argv[1]; break; case 'm': nomath = 1; break; case 'n': noverb = 1; break; case 'l': latex = 1; break; case 'w': web = 1; break; default: printusage (), exit (BAD_OPTIONS); /* nezn\'am\'y parametr */ } argc--; argv++; } @* Zpracování souborů. Parametr |MAXLEN| definuje maximální možnou délku jména souboru, který vytvoříme jako přechodný, nebo zálohový. Dále deklarujeme proměnné typu \uv{stream}. @d MAXLEN 120 @= FILE *infile, *outfile; char backup[MAXLEN]; int j; @ Definujeme funkci pro výpis chybového hlášení při neúspěšném otevření souboru. @= void ioerr (f) char *f; { fprintf(stderr, "%s: cannot open file %s\n", prog_name, f); } @ Způsob zpracování souborů rozlišíme podle režimu daným přepínačem \.{-f}. @= if (isfilter) @ @/ else @ @ V režimu |isfilter==1| je další zpracování závislé na počtu souborů v příkazové řádce: \begitems * nula souborů -- vstup je |stdin| a výstup je |stdout|, * jeden soubor -- je vstupní, výstup je |stdout|, * dva soubory -- první je vstupní, druhý výstupní, * více souborů -- program skončí s chybou. \enditems @= { if (argc > 3) printusage (), exit (BAD_OPTIONS) ; infile = stdin; outfile = stdout; if (argc >= 2) infile = fopen (argv[1], "r"); if (infile == NULL) ioerr (argv[1]), exit (IO_ERR); if (argc == 3) outfile = fopen(argv[2], "w"); if (outfile == NULL) ioerr (argv[2]), exit (IO_ERR); if (argc >= 2) filename = argv[1]; else filename = NULL; tie (infile, outfile); if (outfile != stdout) fclose (outfile); if (infile != stdin) fclose (infile); } @ V~režimu |isfilter==0| jsou jednotlivé soubory v~příkazovém řádku interpretovány jako vstupní i výstupní. Více souborů v~příkazovém řádku má stejný efekt, jako opakované volání programu na jednotlivé soubory. V~\UNIX/u lze tedy např. napsat \.{\jobname\ *.tex} a program doplní vlnky do všech souborů s~příponou~\.{tex}. Toto neplatí v~DOSu, protože interpretace masky je v~\UNIX/u starostí shellu a nikoli programu samotného. Náš program masku nebude interpretovat. Je-li v~tomto režimu nulový počet souborů, program se ukončí s~chybou. @= { if (argc==1) printusage (), exit(BAD_OPTIONS); while (argc>1) { argc--; argv++; @; if (infile == NULL) { ioerr (argv[0]); continue; } outfile = fopen (argv[0], "w"); if (outfile == NULL) { ioerr (argv[0]); rename (backup, argv[0]); status = WARNING; continue; } filename = argv[0]; tie (infile, outfile); fclose (outfile), fclose (infile); if (rmbackup) remove (backup); } } @ Při |isfilter==0| program přejmenuje každý zpracovávaný soubor tak, že změní poslední písmeno názvu souboru na znak \.{\char126}. Tento přejmenovaný soubor bude otevřen jako vstupní a výstupem bude původní soubor. Vstupní soubor při |rmbackup==0| zůstane zachován jako záloha. Proč vlnku nepřidáváme na konec názvu souboru, ale měníme ji za poslední znak souboru? Protože chceme, aby program fungoval i v tak nemožných systémech, jako je DOS. @= infile = NULL; j = strlen (argv[0]) - 1; if (j >= MAXLEN || argv[0][j] == '~') { if (!silent) fprintf (stderr, "%s: the conflict of file name %s\n", prog_name, argv[0]); } else { strcpy (backup, argv[0]); backup[j] = '~'; remove (backup); j = rename (argv[0], backup); if (j == 0) infile = fopen (backup, "r"); } @* Patterny. Abychom mohli účelně definovat chování programu v~různých situacích, zavedeme datovou strukturu |PATTERN|. Zhruba řečeno, budeme sledovat vstup znak po znaku a pokud bude část vstupu souhlasit s~definovaným patternem, provedeme námi požadovanou akci. Například nejčastější aktivitu, přidání vlnky uvnitř řádku, spustíme v~okamžiku, kdy vstupní text odpovídá patternu \uv{\.{\ (v\ p}}, kde \uv{\.{\ }} znamená jedna nebo více mezer a tabelátorů, \uv{\.{(}} je nula nebo více otevíracích závorek všeho druhu, \uv{\.{v}} znamená jedno písmeno z~množiny předložek (viz |charset|) a \uv{\.{p}} zde znamená libovolné písmeno. Příklad zde není zcela přesný. Přesně jsou všechny patterny pro náš program definovány v~závěrečných sekcích tohoto povídání. Pattern bude znamenat konečnou sekvenci tzv. pozic patternu (|PATITEM|). Cykly uvnitř pozic pro jednoduchost nepřipustíme. Každá pozice obsahuje řetězec znaků, uvažovaný pro danou pozici (v~příkladu pozice~\uv{\.{\ }} by obsahovala mezeru a tabelátor, zatímco pozice \.{v} odpovídá |charset|). Každá pozice má svůj přepínač (|flag|), který obsahuje informaci o~tom, zda shodu testovaného znaku s~některým prvkem v~množině znaků budeme považovat za úspěch či neúspěch a zda pozice se ve zkoumaném řetězci může vyskytovat právě jednou nebo opakovaně. Jako druhý případ stačí implementovat \uv{nula nebo více} protože \uv{jedna nebo více} lze popsat pomocí dvou pozic, první \uv{právě jednou} a následující \uv{nula nebo více}. Jednotlivé pozice jsou zřetězeny ukazatelem |next|, poslední pozice má |next==NULL|. Stejně tak jednotlivé patterny budeme sestavovat do seznamů a budou rovněž zřetězeny ukazatelem |next|. Pattern kromě řetězu pozic obsahuje ukazatel na funkci (proceduru) |proc|, která se má vykonat v~případě, že testovaný řetězec vyhovuje patternu. @d ONE 1 /* flag: prave jeden vyskyt */ @d ANY 2 /* flag: nula nebo vice */ @d ONE_NOT -1 /* flag: prave jednou, znak nesmi byt v mnozine */ @d ANY_NOT -2 /* flag: nula nebo vice, znak nesmi byt v mnozine */ @= typedef struct PATITEM { /* jedna pozice patternu */ char *str; /* seznam znaku na teto pozici */ int flag; /* vyznam seznamu znaku */ struct PATITEM *next ; /* nasledujici pozice patternu */ } PATITEM; typedef struct PATTERN { /* jeden pattern */ PATITEM *patt; /* ukazatel na prvni pozici */ void (*proc)(); /* procedura spustena pri souhlasu patternu */ struct PATTERN *next ; /* nasledujici v seznamu vsech patternu */ } PATTERN; @ Deklarujeme některé globální proměnné pro práci s~patterny. |lapi| je pole obsahující ukazatele na aktuální pozice v~otevřených patternech. Říkáme, že \uv{pattern je otevřen}, pokud zkoumaný řetězec s~ním {\it začíná\/} souhlasit. Pattern se uzavře, pokud nastane jedna ze dvou možností: zkoumaný řetězec s~mím souhlasí až do konce (v~takovém případě se provede procedura |proc|), nebo při vyšetřování dalších znaků ze zkoumaného řetězce přestane řetězec s~patternem souhlasit. V~dané chvíli může být pattern otevřen několikrát. Např. pattern \.{abac} je při stringu \.{aba} při výskytu druhého \.{a} otevřen podruhé. Proto pole obsahuje ukazatele na právě aktuální pozici patternu a nikoli na pattern jako takový. V~poli |lapi| budou na počátku samá |NULL| (to se při překladu inicializuje samo) a přemazání ukazatele na pozici konstantou |NULL| budeme považovat za zavření patternu. Vedle pole |lapi| souměrně udržujeme pole |lapt|, do něhož budeme ukládat ukazatele na odpovídající otevřený pattern. Tuto informaci použijeme v~případě, že potřebujeme např, znát |proc| patternu. |listpatt| bude ukazovat na začátek aktuálního seznamu patternů. Seznamy budeme mít dva. Jeden se použije, nacházíme-li se mimo komentář a druhý v~případě, že se nacházíme v~prostoru \TeX{}ovského komentáře (tj. za procentem). Starty těchto seznamů patternů jsou |normallist| a |commentlist| a aktivní |listpatt| má vždy jednu z~těchto dvou hodnot. Proměnné |lastpt| a |lastpi| použijeme pro budování řetězové struktury patternů. Proměnná |c| obsahuje právě testovaný znak ze vstupu (který se rovněž přepíše do bufferu |buff|). Z~bufferu občas ukládáme data do výstupního proudu. Děláme to ale vždy jen v~okamžiku, kdy není otevřen žádný pattern. Tehdy totiž \uv{nehrozí} situace, že by nějaká procedura vyvolaná souhlasem patternu požadovala v~tomto bufferu nějaké změny se zpětnou platností. O~vyprázdnění bufferu se začneme zajímat až v~okamžiku, kdy je zaplněn aspoň na hodnotu |BUFI|, abychom proceduru přepisu bufferu do výstupního proudu neaktivovali zbytečně často. @d MAXPATT 200 /* maximalni pocet patternu */ @d MAXBUFF 500 /* velikost bufferu pro operace */ @d BUFI 300 /* velikost stredniho zaplneni */ @= PATITEM *lapi[MAXPATT]; /* pole ukazatelu na aktualni pozice */ PATTERN *lapt[MAXPATT]; /* pole odpovidajicich ukazatelu na patterny */ PATTERN *listpatt, *normallist, *commentlist, *pt, *lastpt=NULL; PATITEM *pi, *lastpi=NULL; char c; /* zrovna nacetny znak */ char buff[MAXBUFF]; /* prechodny buffer */ int ind; /* aktualni pozice prechodneho bufferu */ @ Nyní definujeme pomocné funkce |setpattern|, |setpi| a |normalpattern|. Tyto funkce alokují paměť pomocí standardní funkce |malloc|. Abychom mohli ohlídat případnou chybu při alokaci, budeme allokovat paměť zprostředkovaně pomocí funkce |myalloc|. @= void *myalloc (size) int size; { void *p; p = malloc (size); if (p == NULL) { fprintf (stderr, "%s, no memory, malloc failed\n", prog_name); exit (BAD_PROGRAM) ; } return p; } @ Funkce |setpattern| alokuje paměťové místo struktury |PATTERN| a napojí ji pomocí proměnné |lastpt| na už alokovaný řetěz patternů. Vrátí ukazatel na nově alokované místo. Jednotlivé pozice patternu se musí následovně alokovat pomocí |setpi|. @= PATTERN *setpattern (proc) @/ void (*proc)(); { PATTERN *pp; pp = myalloc (sizeof (PATTERN)); pp->proc = proc; pp->next = NULL; pp->patt = NULL; if (lastpt != NULL) lastpt->next = pp; lastpt = pp; lastpi = NULL; return pp; } @ Funkce |setpi| alokuje paměťové místo pro jednu pozici patternu. Provede zřetězení tak, aby první pozice řetězu pozic byla zaznamenána v položce |patt| ve struktuře |PATTERN| a další byly provázány položkou |next| ve struktuře |PATITEM|. Poslední pozice má |next==NULL|. @= void setpi (str, flag) char *str; int flag; { PATITEM* p; p = myalloc (sizeof (PATITEM)); p->str = str; p->flag = flag; p->next = NULL; if (lastpi == NULL) lastpt->patt = p; else lastpi->next = p; lastpi = p; } @ Připravme si půdu pro funkci |normalpattern|. Tato funkce alokuje strukturu pro jeden pattern včetně pozic patternu na základě vstupního stringu. Každá pozice patternu obsahuje v~množině znaků jediný znak a má |flag=ONE|. Znaky ve vstupním stringu odpovídají po řadě jednotlivým pozicím. Vytvoří se vlastně jakýsi absolutní pattern, tj. testovaný řetězec se musí přesně shodovat s~uvedeným stringem. Výjimku tvoří znak |"."|, který se interpretuje jako nula nebo více mezer. Chceme-li tečku vnutit do patternu, napíšeme dvě tečky za sebou. Nejdříve deklarujeme pole všech možných jednopísmenných stringů. @= char strings[512]; int i; @ Inicializujeme toto pole (znak, nula, znak, nula, atd...). @= for (i=0; i<256; i++) { strings[2*i] = (char) i; strings[2*i+1] = 0; } @ Definujme funkci |normalpattern|. @= PATTERN *normalpattern (proc, str) @/ void (*proc)(); char *str; { PATTERN *pp; int j=0; pp = setpattern (proc); while (str[j]) { if (str[j]=='.') { j++; if (str[j]!='.') { setpi (blankscr, ANY); continue; } } setpi (&strings[(unsigned char)str[j]*2], ONE); j++; } return pp; } @ Funkce |match|. Definujeme funkci, která na základě hodnoty znaku |c| (proměnná |c| je definována jako globální), a pozice patternu |p| (parametr funkce) vrátí informaci o tom, zda znak souhlasí s patternem. Záporná čísla |FOUND|, resp. |NOFOUND| znamenají, že je třeba uzavřít pattern s tím, že vzor odpovídá, resp. neodpovídá patternu. Nezáporné číslo vrátí v případě, že zkoumaný vstup stále souhlasí s patternem, ale není ještě rozhodnuto. Velikost návratové hodnoty v takovém případě udává, o kolik pozic je třeba se posunout v patternu, abychom měli ukazatel na pozici patternu v souhlase s novou situací, způsobenou znakem |c|. Pokud je |c| v množině znaků pro danou pozici |p->str|, bude |m==1|, jinak je |m==-1|. Pokud tímto číslem pronásobíme hodnotu |p->flag|, nemusíme větvení podle |p->flag| programovat dvakrát. Hodnoty |flag| jsou totiž symetrické podle nuly, např. |ANY==-ANY_NOT|. @d FOUND -1 @d NOFOUND -2 @= int match (p) PATITEM *p; { int m; if (strchr (p->str, c) != NULL) m = 1; /* Znak nalezen */ else m = -1; /* Znak nenalezen */ switch (m * p->flag) { case ANY: return 0; /* Souhas, neni nutny posun */ case ONE: if (p->next == NULL) return FOUND; return 1; /* Souhas, nutny posun o 1 */ case ONE_NOT: return NOFOUND; /* Nesouhlas */ case ANY_NOT: @; } return 0; /* Tady bychom nikdy nemeli byt, return pro potlaceni varovani */ } @ O kolik pozic je třeba se posunout a s jakým výsledkem zjistíme rekurzivním voláním funkce |match|. @= switch (m = match (p->next)) { case NOFOUND: return NOFOUND; case FOUND: return FOUND; default: return 1 + m; } @* Vlnkovací funkce. Nejprve připravíme globální deklarace pro \uv{vlnkovací} funkci |tie|. Funkce |tie| \uv{ovlnkuje} vstupní soubor |infile| a vytvoří soubor |outfile|. Při |silent=0| tiskne závěrečnou zprávu o zpracování. V této zprávě se objeví jméno souboru, které se funkce \uv{dozví} prostřednictvím globální proměnné |filename|. Proměnná |numline| počítá řádky, proměnná |numchanges| sčítá změny, tj. počet doplněných vlnek. Proměnná |mode| nabývý některé z hodnot |TEXTMODE|, |MATHMODE|, |DISPLAYMODE| a |VERBMODE| podle stavu ve čteném textu. @d TEXTMODE 0 @d MATHMODE 1 @d DISPLAYMODE 2 @d VERBMODE 3 @= char *filename; /* jmeno zpracovavaneho souboru */ long int numline, numchanges; /* pro zaverecnou statistiku */ int mode; @ Nyní definujeme vlnkovací funkci |tie|. Veškerá činnost se opírá o strukturu patternů. Výhodné je (z důvodu rychlosti) \uv{natvrdo} zde implementovat jen přepínání mezi stavem čtení z oblasti komentáře (|listpatt==commentlist|) a mimo komentář (|listpatt==normallist|); @= void tie (input, output) FILE *input, *output; { int ap; /* ap je pocet otevrenych patternu */ register int k, m, n; int ic; PATTERN *pp; PATITEM *pi; @; while (!feof(input)) { @; if (ap == 0 && ind > BUFI && c !='\\') @; if (ind >= MAXBUFF) { fprintf (stderr, "Operating buffer overflow, is anything wrong?\n"); exit (BAD_PROGRAM); } if ((ic = getc(input)) == EOF) /* opravil Cejka Rudolf */ break; buff[ind++] = c = ic; if (c == '\n') numline++, listpatt = normallist; if (c == '%' && mode!=VERBMODE && buff[ind-2] != '\\') listpatt = commentlist; @; } @; if (!web) checkmode (); /* zaverecna kontrola modu */ if (!silent) @; } @ @= for (k=0; k= { buff[ind] = 0; fputs (&buff[1], output); ind = 1; } @ Při procházení otevřenými patterny posunujeme v poli |lapi| pozice jednotlivých patternů podle pokynů funkce |match|, případně pattern zavřeme a případně vyvoláme proceduru patternu. Některé patterny v poli |lapi| už mohou být zavřeny, takže je nutno s tímto polem pracovat jako s jakýmsi děravým sýrem. @= n = ap; k = 0; while (n) { while (lapi[k]==NULL) k++; /* zastav se na prvnim ukazateli na pattern */ switch (m = match (lapi[k])) { case FOUND: (*lapt[k]->proc)(); /* Pattern nalezen, spustit proceduru */ case NOFOUND: lapi[k] = NULL; /* Deaktivace patternu */ ap--; break; default: while (m--) lapi[k] = lapi[k]->next; /* dalsi pozice patternu */ } k++; n--; } @ Při otevírání nových patternů, které nejsou v tuto chvíli zablokovány, se hned vypořádáme s takovými patterny, které nám dávají rovnou odpověď typu |FOUND| nebo |NOFOUND|. V takových případech ani nezanášíme ukazatel na pozici do pole |lapi|. @= pp = listpatt; while (pp != NULL) { switch (m = match (pp->patt)) { case FOUND: (*pp->proc)(); /* spustit proceduru */ case NOFOUND: break; default: @; } pp=pp->next; } @ Není-li hned známa odpověď, zda pattern vyhovuje či nikoli, překontrolujeme nejdříve, zda už není pattern ve stejné pozici otevřený. Pak najdeme první \uv{díru} v tabulce |lapi| a tam uhnízdíme nový ukazatel na pozici v patternu. @= pi = pp->patt; while (m--) pi = pi->next; n = ap; k = 0; while (n) { if (lapi[k]==pi) break; if (lapi[k++] != NULL) n--; } if (!n) { k = 0; while (lapi[k] != NULL) k++; if (k >= MAXPATT) { fprintf (stderr, "I cannot allocate pp, is anything wrong?\n"); exit (BAD_PROGRAM); } lapt[k] = pp; lapi[k] = pi; ap++; } @ Poslední věcí ve funci |tie| je tisk závěrečné statistiky zpracování. @= fprintf (stderr, "~~~ file: %s\t lines: %ld, changes: %ld\n", filename, numline, numchanges); @* Inicializace patternů. Po vytvoření předchozího kódu opírajícího se o~patterny máme nyní v~ruce poměrně silný nástroj na definování různých činností programu prostým vytvořením patternu a příslušné jeho procedury. Pokud budeme chtít v~budoucnu nějaký rys programu přidat, pravděpodobně to bude snadné. Nejprve deklarujeme některé často používané skupiny znaků v~patternech. @= char tblanks[] = " ~\t"; char blanks[] = " \t"; char blankscr[] = " \t\n"; char tblankscr[] = " ~\t\n"; char nochar[] = "%~\n"; char cr[] = "\n"; char prefixes[] = "[({~"; char dolar[] = "$"; char backslash[] = "\\"; char openbrace[] = "{"; char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; PATTERN *vlnkalist, *mathlist, *parcheck, *verblist ; @ Začneme definicí nejčastěji používaného patternu na vlnkování uvnitř řádku. Připomeňme, že opakované volání funkce |setpattern| vytváří interně seznam patternů, přičemž o~jejich propojení se nemusíme starat. Vyzvedneme si z~návratového kódu funkce pouze ukazatel na první položku seznamu |normallist|. Stejně tak opakované volání funkce |setpi| vytváří seznam pozic pro naposledy deklarovaný pattern. @= vlnkalist = setpattern (vlnkain); setpi (tblankscr, ONE); setpi (tblanks, ANY); setpi (prefixes, ANY); setpi (charset, ONE); setpi (blanks, ONE); setpi (blanks, ANY); setpi (nochar, ONE_NOT); @ @= listpatt = normallist = vlnkalist; @ @= void vlnkain() { char p; ind--; p = buff[ind--]; while (strchr(blanks, buff[ind]) !=NULL) ind--; ind++; buff[ind++] = '~'; buff[ind++] = p; numchanges++; } @ Podobně pro tvorbu vlnky \uv{přes řádek} vytvoříme pattern a kód procedury. @= setpattern (vlnkacr); setpi (tblankscr, ONE); setpi (tblanks, ANY); setpi (prefixes, ANY); setpi (charset, ONE); setpi (blanks, ANY); setpi (cr, ONE); setpi (blanks, ANY); setpi (nochar, ONE_NOT); @ V proceduře k tomuto patternu musíme ošetřit případ typu \uv{\.{a\char126v\char92np}}, kdy nelze prostě přehodit \uv{\.{\char92n}} za \uv{\.{v}}, protože bychom roztrhli mezeru svázanou vlnkou už dříve. Proto musíme vyhledat vhodné místo pro roztržení řádku, které bude až {\it před\/} znakem \uv{\.{a}}. Při důsledném ošetření tohoto fenoménu můžeme dokonce narazit na situaci \uv{\.{\char92n\ v\char126v\char126v\char92np}}, kde nemůžeme vložit \uv{\.{\char92n}} před první výskyt \uv{\.{v}}, protože bychom dostali \uv{\.{\char92n\char92n}}, tedy prázdný řádek. Ten je v \TeX{}u interperetován odlišně. V této výjimečné situaci pouze zrušíme stávající (v pořadí druhé) \uv{\.{\char92n}} a nebudeme vytvářet nové. Na výstupu bude soubor o jeden řádek kratší. @= void vlnkacr() { char p; int i, j; ind--; p = buff[ind--]; while (strchr(blankscr, buff[ind]) !=NULL) ind--; i = ind; /* misto predlozky, kterou chceme vazat */ while (i >= 0 && (strchr(blankscr, buff[i]) == NULL)) i--; j = i; while (i >= 0 && (strchr(blanks, buff[i]) != NULL)) i--; if (i >= 0 && buff[i] == '\n') j = -1; if (j >= 0) buff[j] = '\n'; else numline--; ind++; buff[ind++] = '~'; buff[ind++] = p; numchanges++; } @ Nyní vytvoříme patterny pro případy typu \.{\char92uv\char`\{v lese\char`\}}. @= setpattern (vlnkain); /* na radku */ setpi (tblankscr, ONE); setpi (backslash, ONE); setpi (letters, ONE); setpi (letters, ANY); setpi (openbrace, ONE); setpi (prefixes, ANY); setpi (charset, ONE); setpi (blanks, ONE); setpi (blanks, ANY); setpi (nochar, ONE_NOT); setpattern (vlnkacr); /* pres radek */ setpi (tblankscr, ONE); setpi (backslash, ONE); setpi (letters, ONE); setpi (letters, ANY); setpi (openbrace, ONE); setpi (prefixes, ANY); setpi (charset, ONE); setpi (blanks, ANY); setpi (cr, ONE); setpi (blanks, ANY); setpi (nochar, ONE_NOT); @ Vytvoříme patterny a proceduru pro potlatčení tvorby vlnky u písmen těsně následujících sekvence \.{\char92TeX} a \.{\char92LaTeX}. Tj. nechceme, aby např z textu \uv{\.{Vlastnosti~\char92TeX~u~jsou...}} jsme dostali text s nesprávně vázaným písmenem \uv{\.{Vlastnosti~\char92TeX~u\char126jsou...}}. @= normalpattern (tielock, "\\TeX"); setpi (blankscr, ONE); normalpattern (tielock, "\\LaTeX"); setpi (blankscr, ONE); @ Procedura |tielock| obsahuje nečistý trik. Při provádění procedury je právě načten znak z |blankscr| a je uložen do buff. Testy na otevírání nových patternů pro tento znak teprve budou následovat a testují se na hodnotu proměnné |c|. Stačí tedy změnit hodnotu |c| a vlnkovací patterny se neotevřou. @= void tielock () { c = 1; } @ Ošetříme nyní přechod do/z matematického režimu \TeX{}u. Uvnitř math módu vlnky neděláme. Při zjištěném nesouladu v přechodech mezi math-módy spustíme následující proceduru. @= void printwarning () { if (!silent) fprintf (stderr, "~!~ warning: text/math/verb mode mismatch, file: %s, line: %ld\n", filename, numline - (c=='\n'?1:0)); status = WARNING; } @ Začneme patterny pro přechod do/z matematického režimu, ohraničeného jedním dolarem, nebo v La\TeX{}u příslušnými sekvencemi. Sekvence La\TeX{}u \.{\char92(} a \.{\char92)} nejsou zahrnuty, protože bývají často předefinovány k jiným užitečnějším věcem. @= if (!nomath) { mathlist = setpattern (onedollar); setpi (dolar, ONE); setpi (dolar, ONE_NOT); if (latex) { normalpattern (mathin, "\\begin.{math}"); normalpattern (mathout, "\\end.{math}"); } } @ @= void mathin () { if (mode!=TEXTMODE) printwarning (); mode = MATHMODE; normallist = listpatt = mathlist; } void mathout () { if (mode!=MATHMODE) printwarning (); mode = TEXTMODE; normallist = listpatt = vlnkalist; } @ Při programování procedury |onedollar| nesmíme zapomenout na výskyt sekvence \.{\char92\$}. V tom případě akci ignorujeme. Podobně u sekvence \.{\$\$} souhlasí ten druhý dolar s naším patternem, ale to už jsme uvnitř display módu. V takovém případě také nic neděláme. @= void onedollar () { if (buff[ind-3]=='\\' || (buff[ind-3]=='$' && buff[ind-4]!='\\')) return; if (mode==DISPLAYMODE) printwarning (); else { if (mode==TEXTMODE) mathin(); else mathout(); } } @ Pokud najdeme prázdný řádek, překontrolujeme, zda náhodou nejsme v math-módu. Pokud ano, vypíšeme varování a přejdeme do textového módu. @= parcheck = setpattern (checkmode); setpi (cr, ONE); setpi (blanks, ANY); setpi (cr, ONE); @ @= void checkmode () { if (mode!=TEXTMODE) { printwarning (); mode = TEXTMODE; normallist = listpatt = vlnkalist; } } @ Nyní ošetříme výskyt dvou dolarů, tj. vstup do/z display módu. Rovněž mysleme na La\TeX{}isty a jejich prostředí pro display-mód. Protože je možná alternativa s hvězdičkou na konci názvu prostředí, raději už uzavírací závorku do patternu nezahrnujeme. @= if (!nomath) { normalpattern (twodollars, "$$"); if (latex) { normalpattern (displayin, "\\begin.{displaymath"); normalpattern (displayin, "\\begin.{equation"); normalpattern (displayout, "\\end.{displaymath"); normalpattern (displayout, "\\end.{equation"); } } @ @= void displayin () { if (mode!=TEXTMODE) printwarning (); mode = DISPLAYMODE; normallist = listpatt = parcheck; } void displayout () { if (mode!=DISPLAYMODE) printwarning(); mode = TEXTMODE; normallist = listpatt = vlnkalist; } void twodollars () { if (buff[ind-3]=='\\') return; if (mode==DISPLAYMODE) displayout (); else displayin (); } @ Následuje ošetření tzv. verbatim módu. Pro plain i La\TeX{} jsou nejčastější závorky pro verbatim mod tyto (variantu s \.{\char92begtt} používám s oblibou já). @= if (!noverb) { verblist = normalpattern (verbinchar, "\\verb"); setpi (blankscr, ANY); setpi (blankscr, ONE_NOT); normalpattern (verbin, "\\begtt"); if (latex) normalpattern (verbin, "\\begin.{verbatim"); } if (web) { normalpattern (verbin, "@@<"); normalpattern (verbin, "@@d"); } if (!noverb) { verboutlist[0] = setpattern (verbout); setpi (verbchar, ONE); verboutlist[1] = normalpattern (verbout, "\\endtt"); if (latex) verboutlist[2] = normalpattern (verbout, "\\end{verbatim"); } if (web) { verboutlist[3] = normalpattern (verbout, "@@ "); normalpattern (verbout, "@@*"); normalpattern (verbout, "@@>|"); } @ Procedura |verbinchar| se od \uv{společné} procedury |verbin| liší v tom, že zavede do stringu |verbchar| momentální hodnotu proměnné |c|. Proto druhý výskyt této hodnoty verbatim režim ukončí. @= int prevmode; PATTERN *prevlist, *verboutlist[4]; char verbchar[2]; void verbinchar () { prevmode = mode; verbchar[0] = c; c = 1; listpatt = normallist = verboutlist[0]; prevlist = listpatt->next; listpatt->next = NULL; mode = VERBMODE; } @ Při programování \uv{obecné} funkce |verbin| musíme dbát na to, aby zůstal aktivní pouze odpovídající \uv{výstupní} pattern k danému vstupnímu. Také si zapamatujeme mód, ze kterého jsme do verbatim oblasti vstoupili, abychom se k němu mohli vrátit (např. uvnitř math. módu může být \.{\char92hbox} a v něm lokálně verbatim konstrukce). @= void verbin () { int i; i = 0; prevmode = mode; switch (c) { case 't': i = 1; break; case 'm': i = 2; break; case '<': ; case 'd': i = 3; if (buff[ind-3]=='@@') return; /* dvojity @@ ignorovat */ break; } listpatt = normallist = verboutlist[i]; prevlist = listpatt->next; if (c != '<' && c != 'd') listpatt->next = NULL; mode = VERBMODE; } @ @= void verbout () { if (mode!=VERBMODE) return; if (web && buff[ind-2] == '@@' && buff[ind-3] == '@@') return; mode = prevmode; normallist->next = prevlist; switch (mode) { case DISPLAYMODE: normallist = listpatt = parcheck; break; case MATHMODE: normallist = listpatt = mathlist; break ; case TEXTMODE: normallist = listpatt = vlnkalist; break; } } @ Nyní implementujeme vlastnost dříve používaného programu vlnka, tj. že lze jeho činnost vypnout a opět zapnout v komentářích. Vytváříme druhý nezávislý seznam patternů a proto nejprve pronulujeme |lastpt|. @= lastpt = 0; commentlist = normalpattern (tieoff, "%.~.-"); normalpattern (tieon, "%.~.+"); @ @= void tieoff () { normallist = NULL; } void tieon () { normallist = vlnkalist; } @ Další plánovaná vylepšení. Program by mohl číst definici svého chování nejen z~příkazové řádky, ale v~mnohem kompletnější podobě, včetně uživatelsky definovaných patternů, z komentářové oblasti ve čteném souboru. Parametry zde uvedené by mohly mít vyšší prioritu, než parametry z~příkazové řádky a mohl by se třeba rozšiřovat seznam sekvencí, za nimiž písmena nemají být vázana vlnkou (zatím je implemenováno na pevno jen \.{\char92TeX} a \.{\char92LaTeX}). @* Rejstřík.