\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.5, (c) 1995, 2002, 2009, 2010 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 (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ě. * \.{-x} \dots\ parametr vymezuje pomocí hexadecimálního zápisu string, který program vkládá na vyhledaná místa. Implicitně vkládá vlnku. Například \.{-x C2A0} způsobí, že program bude vkládat místo vlnky dva byty, první s kódem \.{C2} a druhý s kódem \.{A0}. * \.{-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. @= static void printusage (void) { 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" " -x code : code for tie symbol, default: 7E, example -x C2A0\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; @ String |tiestr| obsahuje string, kterým se má nahradit vyhledané místo. Pokud není použit parametr \.{-u}, je tento string jadnoznakový a obsahuje vlnku. Jinak obsahuje string konvertovaný z parametru \.{-u}. String má délku |tiestrlen| bez ohledu na to, zda obsahuje nebo neobsahuje nulové znaky (C-čková konvence pro stringy není použita). @= unsigned char tiestr[MAXLEN]; int tiestrlen; @ @= tiestr[0] = '~'; tiestrlen = 1; 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': if (argc<2) printusage (), exit (BAD_OPTIONS); argv++; argc--; charset = argv[1]; break; case 'x': if (argc<2) printusage (), exit (BAD_OPTIONS); argv++; argc--; settiestr(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++; } @ Vyřešíme konverzi kódu zapsaného za parametrem \.{-x} na string |tiestr|. @= static unsigned char hexnum(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'F') return c - 'A' + 10; if (c >= 'a' && c <= 'f') return c - 'a' + 10; printusage (), exit (BAD_OPTIONS); } static void settiestr(char *s) { int i, j; i = strlen(s); if ((i > 2*MAXLEN) || i%2 == 1) printusage (), exit (BAD_OPTIONS); tiestrlen = i/2; j = 0; for (i=0; i= 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. @= static void ioerr (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], "wb"); 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], "wb"); 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)(void); /* 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 *lastpi=NULL; char c; /* zrovna nacetny znak */ char buff[MAXBUFF]; /* prechodny buffer */ int ind; /* aktualni pozice prechodneho bufferu */ @ Dne 30. 4. 2009 jsem přidal možnost čtení vstupu, který obsahuje nulové byty. Takové nuly se přepisují do výstupu, ale program si jich nevšímá při procházení patternů. Tím je možno program použít na soubory kódované v UTF16, ačkoli patterny obsahují jen jednobytové ASCII znaky. Buffer |buff| může obsahovat i nulové byty, které je třeba přepsat do výstupu. Na druhé straně buffer |buffnz| obsahuje jen nenulové byty, na které se někdy ptáme při pohledu dozadu. Nejdelší pohled dozadu je o čtyři byty. Udělám tedy |buffnz| osmibytový, začnu jej plnit od |buffnz|[4] a kdykoli je buffer zcela zaplněn, přesunu horní čtyři byty na spodní a dále pokračuji v plnění bufferu od pozice |buffnz|[4]. @= char buffnz[8]; int inz; @ 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|. @= static void *myalloc (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|. @= static PATTERN *setpattern (void (*proc)(void)) { 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|. @= static void setpi (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|. @= static PATTERN *normalpattern (void (*proc)(void), const 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 @= static int match (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|); @= static void tie (FILE *input, FILE *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 !='\\') @; @; /* 1. 2. 2010: prohozene poradi */ 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 == 0) continue; /* 30. 4. 2009 */ if (inz>=8) { for (inz=0; inz<4; inz++) buffnz[inz] = buffnz[inz+4]; inz=4; } buffnz[inz++] = c; if (c == '\n') numline++, listpatt = normallist; if (c == '%' && mode!=VERBMODE && buffnz[inz-2] != '\\') listpatt = commentlist; @; } @; if (!web) checkmode (); /* zaverecna kontrola modu */ if (!silent) @; } @ @= for (k=0; k= { fwrite (&buff[1], ind-1, 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; if (c) 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; @ Vložení vlnky znamená vykonat následující práci: Zapamatovat si znak za skupinou mezer (do proměnné |p|). Pokud před tímto znakem předchází nulový byte, použijeme ho později, proto si jej uložíme do proměnné |z|. Dále se posuneme v bufferu vlevo přes všechny mezery, tabelátory (přesněji |blanks|) a přeskakujeme přitom všechny nulové byty. Index |ind| se zastaví na předložce. Posuneme jej doprava za předložku (++|ind|) a pokud tam je nulový byte a první znak |tiestr| není nulový, posuneme se až za tento nulový byte. Dále vložíme string |tiestr|, neboli vlnku. Nakonec připojíme zapamatovaný znak |p|, ovšem pokud před ním byla nula, vložíme ji ještě před znak |p|. @= static void vlnkain(void) { int i; char p, z; ind--; p = buff[ind--]; z = buff[ind]; while (!buff[ind] || (strchr(blanks, buff[ind]) !=NULL)) ind--; if (!buff[++ind] && tiestr[0]) ind++; for (i=0; i= 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ší. @= static void vlnkacr(void) { char p, z; int i, j; ind--; p = buff[ind--]; z = buff[ind]; while (!buff[ind] || (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 && (!buff[ind] || (strchr(blanks, buff[i]) != NULL))) i--; if (i >= 0 && buff[i] == '\n') j = -1; if (j >= 0) buff[j] = '\n'; else numline--; if (!buff[++ind] && tiestr[0]) ind++; for (i=0; i= 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. @= static void tielock (void) { 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. @= static void printwarning (void) { 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}"); } } @ @= static void mathin (void) { if (mode!=TEXTMODE) printwarning (); mode = MATHMODE; normallist = listpatt = mathlist; } static void mathout (void) { 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. @= static void onedollar (void) { if (buffnz[inz-3]=='\\' || (buffnz[inz-3]=='$' && buffnz[inz-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); @ @= static void checkmode (void) { 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"); } } @ @= static void displayin (void) { if (mode!=TEXTMODE) printwarning (); mode = DISPLAYMODE; normallist = listpatt = parcheck; } static void displayout (void) { if (mode!=DISPLAYMODE) printwarning(); mode = TEXTMODE; normallist = listpatt = vlnkalist; } static void twodollars (void) { if (buffnz[inz-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]; static void verbinchar (void) { 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). @= static void verbin (void) { int i; i = 0; prevmode = mode; switch (c) { case 't': i = 1; break; case 'm': i = 2; break; case '<': ; case 'd': i = 3; if (buffnz[inz-3]=='@@') return; /* dvojity @@ ignorovat */ break; } listpatt = normallist = verboutlist[i]; prevlist = listpatt->next; if (c != '<' && c != 'd') listpatt->next = NULL; mode = VERBMODE; } @ @= static void verbout (void) { if (mode!=VERBMODE) return; if (web && buffnz[inz-2] == '@@' && buffnz[inz-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, "%.~.+"); @ @= static void tieoff (void) { normallist = NULL; } static void tieon (void) { 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.