Annotation of mandoc/term.c, Revision 1.134
1.134 ! joerg 1: /* $Id: term.c,v 1.133 2010/05/12 16:01:01 kristaps Exp $ */
1.1 kristaps 2: /*
1.75 kristaps 3: * Copyright (c) 2008, 2009 Kristaps Dzonsons <kristaps@kth.se>
1.1 kristaps 4: *
5: * Permission to use, copy, modify, and distribute this software for any
1.74 kristaps 6: * purpose with or without fee is hereby granted, provided that the above
7: * copyright notice and this permission notice appear in all copies.
1.1 kristaps 8: *
1.74 kristaps 9: * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10: * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11: * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12: * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13: * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14: * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15: * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1.1 kristaps 16: */
1.128 kristaps 17: #ifdef HAVE_CONFIG_H
18: #include "config.h"
19: #endif
20:
1.126 kristaps 21: #include <sys/types.h>
22:
1.1 kristaps 23: #include <assert.h>
1.122 kristaps 24: #include <ctype.h>
1.22 kristaps 25: #include <stdio.h>
1.1 kristaps 26: #include <stdlib.h>
27: #include <string.h>
1.113 kristaps 28: #include <time.h>
1.1 kristaps 29:
1.101 kristaps 30: #include "chars.h"
1.107 kristaps 31: #include "out.h"
1.71 kristaps 32: #include "term.h"
33: #include "man.h"
34: #include "mdoc.h"
1.105 kristaps 35: #include "main.h"
1.1 kristaps 36:
1.134 ! joerg 37: static struct termp *term_alloc(enum termenc, size_t);
1.71 kristaps 38: static void term_free(struct termp *);
1.125 kristaps 39: static void spec(struct termp *, const char *, size_t);
40: static void res(struct termp *, const char *, size_t);
41: static void buffera(struct termp *, const char *, size_t);
42: static void bufferc(struct termp *, char);
43: static void adjbuf(struct termp *p, size_t);
44: static void encode(struct termp *, const char *, size_t);
1.1 kristaps 45:
46:
1.71 kristaps 47: void *
1.134 ! joerg 48: ascii_alloc(size_t width)
1.10 kristaps 49: {
1.1 kristaps 50:
1.134 ! joerg 51: return(term_alloc(TERMENC_ASCII, width));
1.1 kristaps 52: }
53:
54:
1.99 kristaps 55: void
1.71 kristaps 56: terminal_free(void *arg)
1.11 kristaps 57: {
58:
1.71 kristaps 59: term_free((struct termp *)arg);
1.11 kristaps 60: }
61:
62:
1.71 kristaps 63: static void
64: term_free(struct termp *p)
1.14 kristaps 65: {
66:
1.71 kristaps 67: if (p->buf)
68: free(p->buf);
1.102 kristaps 69: if (p->symtab)
1.101 kristaps 70: chars_free(p->symtab);
1.14 kristaps 71:
1.71 kristaps 72: free(p);
1.14 kristaps 73: }
74:
75:
1.71 kristaps 76: static struct termp *
1.134 ! joerg 77: term_alloc(enum termenc enc, size_t width)
1.14 kristaps 78: {
1.71 kristaps 79: struct termp *p;
1.14 kristaps 80:
1.117 kristaps 81: p = calloc(1, sizeof(struct termp));
82: if (NULL == p) {
1.120 kristaps 83: perror(NULL);
1.117 kristaps 84: exit(EXIT_FAILURE);
85: }
1.71 kristaps 86: p->enc = enc;
1.134 ! joerg 87: /* Enforce some lower boundary. */
! 88: if (width < 60)
! 89: width = 60;
! 90: p->defrmargin = width - 2;
1.71 kristaps 91: return(p);
1.14 kristaps 92: }
93:
94:
1.71 kristaps 95: /*
96: * Flush a line of text. A "line" is loosely defined as being something
97: * that should be followed by a newline, regardless of whether it's
98: * broken apart by newlines getting there. A line can also be a
1.130 kristaps 99: * fragment of a columnar list (`Bl -tag' or `Bl -column'), which does
100: * not have a trailing newline.
1.71 kristaps 101: *
1.130 kristaps 102: * The following flags may be specified:
1.71 kristaps 103: *
104: * - TERMP_NOLPAD: when beginning to write the line, don't left-pad the
105: * offset value. This is useful when doing columnar lists where the
106: * prior column has right-padded.
107: *
108: * - TERMP_NOBREAK: this is the most important and is used when making
109: * columns. In short: don't print a newline and instead pad to the
110: * right margin. Used in conjunction with TERMP_NOLPAD.
111: *
1.91 kristaps 112: * - TERMP_TWOSPACE: when padding, make sure there are at least two
113: * space characters of padding. Otherwise, rather break the line.
114: *
1.84 kristaps 115: * - TERMP_DANGLE: don't newline when TERMP_NOBREAK is specified and
116: * the line is overrun, and don't pad-right if it's underrun.
117: *
118: * - TERMP_HANG: like TERMP_DANGLE, but doesn't newline when
119: * overruning, instead save the position and continue at that point
120: * when the next invocation.
1.71 kristaps 121: *
122: * In-line line breaking:
123: *
124: * If TERMP_NOBREAK is specified and the line overruns the right
125: * margin, it will break and pad-right to the right margin after
126: * writing. If maxrmargin is violated, it will break and continue
1.114 kristaps 127: * writing from the right-margin, which will lead to the above scenario
128: * upon exit. Otherwise, the line will break at the right margin.
1.71 kristaps 129: */
130: void
131: term_flushln(struct termp *p)
1.53 kristaps 132: {
1.114 kristaps 133: int i; /* current input position in p->buf */
134: size_t vis; /* current visual position on output */
135: size_t vbl; /* number of blanks to prepend to output */
136: size_t vsz; /* visual characters to write to output */
137: size_t bp; /* visual right border position */
138: int j; /* temporary loop index */
139: size_t maxvis, mmax;
1.53 kristaps 140:
1.71 kristaps 141: /*
142: * First, establish the maximum columns of "visible" content.
143: * This is usually the difference between the right-margin and
144: * an indentation, but can be, for tagged lists or columns, a
1.115 kristaps 145: * small set of values.
1.71 kristaps 146: */
1.53 kristaps 147:
1.71 kristaps 148: assert(p->offset < p->rmargin);
1.92 kristaps 149:
1.129 kristaps 150: maxvis = (int)(p->rmargin - p->offset) - p->overstep < 0 ?
1.119 kristaps 151: /* LINTED */
1.129 kristaps 152: 0 : p->rmargin - p->offset - p->overstep;
153: mmax = (int)(p->maxrmargin - p->offset) - p->overstep < 0 ?
1.119 kristaps 154: /* LINTED */
1.129 kristaps 155: 0 : p->maxrmargin - p->offset - p->overstep;
1.92 kristaps 156:
1.71 kristaps 157: bp = TERMP_NOBREAK & p->flags ? mmax : maxvis;
1.115 kristaps 158:
159: /*
160: * FIXME: if bp is zero, we still output the first word before
161: * breaking the line.
162: */
163:
1.71 kristaps 164: vis = 0;
1.84 kristaps 165:
1.71 kristaps 166: /*
167: * If in the standard case (left-justified), then begin with our
168: * indentation, otherwise (columns, etc.) just start spitting
169: * out text.
170: */
1.53 kristaps 171:
1.71 kristaps 172: if ( ! (p->flags & TERMP_NOLPAD))
173: /* LINTED */
174: for (j = 0; j < (int)p->offset; j++)
175: putchar(' ');
176:
177: for (i = 0; i < (int)p->col; i++) {
178: /*
179: * Count up visible word characters. Control sequences
180: * (starting with the CSI) aren't counted. A space
181: * generates a non-printing word, which is valid (the
182: * space is printed according to regular spacing rules).
183: */
184:
185: /* LINTED */
1.132 kristaps 186: for (j = i, vsz = 0; j < (int)p->col; j++) {
1.93 kristaps 187: if (j && ' ' == p->buf[j])
1.71 kristaps 188: break;
1.130 kristaps 189: if (8 == p->buf[j])
1.89 kristaps 190: vsz--;
1.71 kristaps 191: else
192: vsz++;
193: }
1.53 kristaps 194:
1.71 kristaps 195: /*
1.81 kristaps 196: * Choose the number of blanks to prepend: no blank at the
197: * beginning of a line, one between words -- but do not
198: * actually write them yet.
1.71 kristaps 199: */
1.130 kristaps 200:
1.81 kristaps 201: vbl = (size_t)(0 == vis ? 0 : 1);
1.71 kristaps 202:
1.81 kristaps 203: /*
204: * Find out whether we would exceed the right margin.
1.132 kristaps 205: * If so, break to the next line. Otherwise, write the chosen
206: * number of blanks.
1.81 kristaps 207: */
1.130 kristaps 208:
1.81 kristaps 209: if (vis && vis + vbl + vsz > bp) {
210: putchar('\n');
211: if (TERMP_NOBREAK & p->flags) {
212: for (j = 0; j < (int)p->rmargin; j++)
213: putchar(' ');
214: vis = p->rmargin - p->offset;
215: } else {
1.71 kristaps 216: for (j = 0; j < (int)p->offset; j++)
217: putchar(' ');
218: vis = 0;
1.81 kristaps 219: }
1.130 kristaps 220:
1.129 kristaps 221: /* Remove the p->overstep width. */
1.130 kristaps 222:
1.112 kristaps 223: bp += (int)/* LINTED */
1.129 kristaps 224: p->overstep;
225: p->overstep = 0;
1.81 kristaps 226: } else {
227: for (j = 0; j < (int)vbl; j++)
1.71 kristaps 228: putchar(' ');
1.81 kristaps 229: vis += vbl;
1.71 kristaps 230: }
1.53 kristaps 231:
1.130 kristaps 232: /* Write out the [remaining] word. */
233: for ( ; i < (int)p->col; i++)
1.71 kristaps 234: if (' ' == p->buf[i])
235: break;
1.130 kristaps 236: else if (31 == p->buf[i])
1.121 kristaps 237: putchar(' ');
238: else
239: putchar(p->buf[i]);
1.130 kristaps 240:
1.71 kristaps 241: vis += vsz;
242: }
1.111 kristaps 243:
1.91 kristaps 244: p->col = 0;
1.129 kristaps 245: p->overstep = 0;
1.15 kristaps 246:
1.91 kristaps 247: if ( ! (TERMP_NOBREAK & p->flags)) {
248: putchar('\n');
1.15 kristaps 249: return;
1.71 kristaps 250: }
1.15 kristaps 251:
1.91 kristaps 252: if (TERMP_HANG & p->flags) {
253: /* We need one blank after the tag. */
1.129 kristaps 254: p->overstep = /* LINTED */
1.92 kristaps 255: vis - maxvis + 1;
1.91 kristaps 256:
257: /*
258: * Behave exactly the same way as groff:
1.92 kristaps 259: * If we have overstepped the margin, temporarily move
260: * it to the right and flag the rest of the line to be
261: * shorter.
1.91 kristaps 262: * If we landed right at the margin, be happy.
1.92 kristaps 263: * If we are one step before the margin, temporarily
264: * move it one step LEFT and flag the rest of the line
265: * to be longer.
1.91 kristaps 266: */
1.129 kristaps 267: if (p->overstep >= -1) {
268: assert((int)maxvis + p->overstep >= 0);
1.92 kristaps 269: /* LINTED */
1.129 kristaps 270: maxvis += p->overstep;
1.92 kristaps 271: } else
1.129 kristaps 272: p->overstep = 0;
1.91 kristaps 273:
274: } else if (TERMP_DANGLE & p->flags)
275: return;
1.15 kristaps 276:
1.92 kristaps 277: /* Right-pad. */
278: if (maxvis > vis + /* LINTED */
279: ((TERMP_TWOSPACE & p->flags) ? 1 : 0))
1.91 kristaps 280: for ( ; vis < maxvis; vis++)
281: putchar(' ');
1.92 kristaps 282: else { /* ...or newline break. */
1.71 kristaps 283: putchar('\n');
1.91 kristaps 284: for (i = 0; i < (int)p->rmargin; i++)
285: putchar(' ');
286: }
1.15 kristaps 287: }
288:
289:
1.71 kristaps 290: /*
291: * A newline only breaks an existing line; it won't assert vertical
292: * space. All data in the output buffer is flushed prior to the newline
293: * assertion.
294: */
295: void
296: term_newln(struct termp *p)
1.15 kristaps 297: {
298:
1.71 kristaps 299: p->flags |= TERMP_NOSPACE;
300: if (0 == p->col) {
301: p->flags &= ~TERMP_NOLPAD;
1.15 kristaps 302: return;
1.16 kristaps 303: }
1.71 kristaps 304: term_flushln(p);
305: p->flags &= ~TERMP_NOLPAD;
1.16 kristaps 306: }
307:
308:
1.71 kristaps 309: /*
310: * Asserts a vertical space (a full, empty line-break between lines).
311: * Note that if used twice, this will cause two blank spaces and so on.
312: * All data in the output buffer is flushed prior to the newline
313: * assertion.
314: */
315: void
316: term_vspace(struct termp *p)
1.16 kristaps 317: {
318:
1.62 kristaps 319: term_newln(p);
1.71 kristaps 320: putchar('\n');
1.16 kristaps 321: }
322:
323:
1.71 kristaps 324: static void
1.125 kristaps 325: spec(struct termp *p, const char *word, size_t len)
1.17 kristaps 326: {
1.71 kristaps 327: const char *rhs;
328: size_t sz;
1.17 kristaps 329:
1.101 kristaps 330: rhs = chars_a2ascii(p->symtab, word, len, &sz);
1.125 kristaps 331: if (rhs)
332: encode(p, rhs, sz);
1.94 kristaps 333: }
334:
335:
336: static void
1.125 kristaps 337: res(struct termp *p, const char *word, size_t len)
1.94 kristaps 338: {
339: const char *rhs;
340: size_t sz;
341:
1.101 kristaps 342: rhs = chars_a2res(p->symtab, word, len, &sz);
1.125 kristaps 343: if (rhs)
344: encode(p, rhs, sz);
345: }
346:
347:
348: void
349: term_fontlast(struct termp *p)
350: {
351: enum termfont f;
352:
353: f = p->fontl;
354: p->fontl = p->fontq[p->fonti];
355: p->fontq[p->fonti] = f;
356: }
357:
358:
359: void
360: term_fontrepl(struct termp *p, enum termfont f)
361: {
362:
363: p->fontl = p->fontq[p->fonti];
364: p->fontq[p->fonti] = f;
365: }
366:
367:
368: void
369: term_fontpush(struct termp *p, enum termfont f)
370: {
371:
372: assert(p->fonti + 1 < 10);
373: p->fontl = p->fontq[p->fonti];
374: p->fontq[++p->fonti] = f;
375: }
376:
377:
378: const void *
379: term_fontq(struct termp *p)
380: {
381:
382: return(&p->fontq[p->fonti]);
383: }
384:
385:
386: enum termfont
387: term_fonttop(struct termp *p)
388: {
389:
390: return(p->fontq[p->fonti]);
391: }
392:
393:
394: void
395: term_fontpopq(struct termp *p, const void *key)
396: {
397:
398: while (p->fonti >= 0 && key != &p->fontq[p->fonti])
399: p->fonti--;
400: assert(p->fonti >= 0);
401: }
1.94 kristaps 402:
1.125 kristaps 403:
404: void
405: term_fontpop(struct termp *p)
406: {
407:
408: assert(p->fonti);
409: p->fonti--;
1.17 kristaps 410: }
411:
412:
1.71 kristaps 413: /*
414: * Handle pwords, partial words, which may be either a single word or a
415: * phrase that cannot be broken down (such as a literal string). This
416: * handles word styling.
417: */
1.86 kristaps 418: void
419: term_word(struct termp *p, const char *word)
1.65 kristaps 420: {
1.124 kristaps 421: const char *sv, *seq;
1.125 kristaps 422: int sz;
1.124 kristaps 423: size_t ssz;
424: enum roffdeco deco;
1.71 kristaps 425:
1.100 kristaps 426: sv = word;
427:
1.123 kristaps 428: if (word[0] && '\0' == word[1])
1.100 kristaps 429: switch (word[0]) {
430: case('.'):
431: /* FALLTHROUGH */
432: case(','):
433: /* FALLTHROUGH */
434: case(';'):
435: /* FALLTHROUGH */
436: case(':'):
437: /* FALLTHROUGH */
438: case('?'):
439: /* FALLTHROUGH */
440: case('!'):
441: /* FALLTHROUGH */
442: case(')'):
443: /* FALLTHROUGH */
444: case(']'):
445: if ( ! (TERMP_IGNDELIM & p->flags))
446: p->flags |= TERMP_NOSPACE;
447: break;
448: default:
449: break;
450: }
1.65 kristaps 451:
1.133 kristaps 452: if ( ! (TERMP_NOSPACE & p->flags)) {
1.125 kristaps 453: bufferc(p, ' ');
1.133 kristaps 454: if (TERMP_SENTENCE & p->flags)
455: bufferc(p, ' ');
456: }
1.65 kristaps 457:
1.71 kristaps 458: if ( ! (p->flags & TERMP_NONOSPACE))
459: p->flags &= ~TERMP_NOSPACE;
1.133 kristaps 460:
461: p->flags &= ~TERMP_SENTENCE;
1.65 kristaps 462:
1.125 kristaps 463: /* FIXME: use strcspn. */
1.124 kristaps 464:
465: while (*word) {
466: if ('\\' != *word) {
1.125 kristaps 467: encode(p, word, 1);
1.124 kristaps 468: word++;
469: continue;
470: }
471:
472: seq = ++word;
473: sz = a2roffdeco(&deco, &seq, &ssz);
474:
475: switch (deco) {
476: case (DECO_RESERVED):
1.125 kristaps 477: res(p, seq, ssz);
1.124 kristaps 478: break;
479: case (DECO_SPECIAL):
1.125 kristaps 480: spec(p, seq, ssz);
1.124 kristaps 481: break;
482: case (DECO_BOLD):
1.125 kristaps 483: term_fontrepl(p, TERMFONT_BOLD);
1.124 kristaps 484: break;
485: case (DECO_ITALIC):
1.125 kristaps 486: term_fontrepl(p, TERMFONT_UNDER);
1.124 kristaps 487: break;
488: case (DECO_ROMAN):
1.125 kristaps 489: term_fontrepl(p, TERMFONT_NONE);
1.124 kristaps 490: break;
491: case (DECO_PREVIOUS):
1.125 kristaps 492: term_fontlast(p);
1.124 kristaps 493: break;
494: default:
495: break;
496: }
1.127 kristaps 497:
1.124 kristaps 498: word += sz;
1.127 kristaps 499: if (DECO_NOSPACE == deco && '\0' == *word)
500: p->flags |= TERMP_NOSPACE;
1.124 kristaps 501: }
1.65 kristaps 502:
1.131 kristaps 503: /*
504: * Note that we don't process the pipe: the parser sees it as
505: * punctuation, but we don't in terms of typography.
506: */
1.100 kristaps 507: if (sv[0] && 0 == sv[1])
508: switch (sv[0]) {
509: case('('):
510: /* FALLTHROUGH */
511: case('['):
512: p->flags |= TERMP_NOSPACE;
513: break;
514: default:
515: break;
516: }
1.65 kristaps 517: }
518:
519:
1.71 kristaps 520: static void
1.125 kristaps 521: adjbuf(struct termp *p, size_t sz)
1.51 kristaps 522: {
523:
1.125 kristaps 524: if (0 == p->maxcols)
525: p->maxcols = 1024;
526: while (sz >= p->maxcols)
527: p->maxcols <<= 2;
528:
529: p->buf = realloc(p->buf, p->maxcols);
530: if (NULL == p->buf) {
531: perror(NULL);
532: exit(EXIT_FAILURE);
1.71 kristaps 533: }
1.51 kristaps 534: }
535:
1.79 kristaps 536:
537: static void
1.125 kristaps 538: buffera(struct termp *p, const char *word, size_t sz)
1.79 kristaps 539: {
1.125 kristaps 540:
541: if (p->col + sz >= p->maxcols)
542: adjbuf(p, p->col + sz);
543:
1.126 kristaps 544: memcpy(&p->buf[(int)p->col], word, sz);
1.125 kristaps 545: p->col += sz;
546: }
547:
548:
549: static void
550: bufferc(struct termp *p, char c)
551: {
552:
553: if (p->col + 1 >= p->maxcols)
554: adjbuf(p, p->col + 1);
555:
1.126 kristaps 556: p->buf[(int)p->col++] = c;
1.125 kristaps 557: }
558:
559:
560: static void
561: encode(struct termp *p, const char *word, size_t sz)
562: {
563: enum termfont f;
564: int i;
565:
566: /*
567: * Encode and buffer a string of characters. If the current
568: * font mode is unset, buffer directly, else encode then buffer
569: * character by character.
570: */
571:
572: if (TERMFONT_NONE == (f = term_fonttop(p))) {
573: buffera(p, word, sz);
574: return;
575: }
576:
577: for (i = 0; i < (int)sz; i++) {
578: if ( ! isgraph((u_char)word[i])) {
579: bufferc(p, word[i]);
580: continue;
1.79 kristaps 581: }
1.125 kristaps 582:
583: if (TERMFONT_UNDER == f)
584: bufferc(p, '_');
585: else
586: bufferc(p, word[i]);
587:
588: bufferc(p, 8);
589: bufferc(p, word[i]);
1.79 kristaps 590: }
591: }
1.106 kristaps 592:
593:
1.107 kristaps 594: size_t
595: term_vspan(const struct roffsu *su)
1.106 kristaps 596: {
597: double r;
598:
1.107 kristaps 599: switch (su->unit) {
1.106 kristaps 600: case (SCALE_CM):
1.107 kristaps 601: r = su->scale * 2;
1.106 kristaps 602: break;
603: case (SCALE_IN):
1.107 kristaps 604: r = su->scale * 6;
1.106 kristaps 605: break;
606: case (SCALE_PC):
1.107 kristaps 607: r = su->scale;
1.106 kristaps 608: break;
609: case (SCALE_PT):
1.107 kristaps 610: r = su->scale / 8;
1.106 kristaps 611: break;
612: case (SCALE_MM):
1.107 kristaps 613: r = su->scale / 1000;
1.106 kristaps 614: break;
615: case (SCALE_VS):
1.107 kristaps 616: r = su->scale;
1.106 kristaps 617: break;
618: default:
1.107 kristaps 619: r = su->scale - 1;
1.106 kristaps 620: break;
621: }
622:
623: if (r < 0.0)
624: r = 0.0;
1.107 kristaps 625: return(/* LINTED */(size_t)
1.106 kristaps 626: r);
627: }
628:
629:
1.107 kristaps 630: size_t
631: term_hspan(const struct roffsu *su)
1.106 kristaps 632: {
633: double r;
634:
1.108 kristaps 635: /* XXX: CM, IN, and PT are approximations. */
636:
1.107 kristaps 637: switch (su->unit) {
1.106 kristaps 638: case (SCALE_CM):
1.108 kristaps 639: r = 4 * su->scale;
1.106 kristaps 640: break;
641: case (SCALE_IN):
1.108 kristaps 642: /* XXX: this is an approximation. */
643: r = 10 * su->scale;
1.106 kristaps 644: break;
645: case (SCALE_PC):
1.108 kristaps 646: r = (10 * su->scale) / 6;
1.106 kristaps 647: break;
648: case (SCALE_PT):
1.108 kristaps 649: r = (10 * su->scale) / 72;
1.106 kristaps 650: break;
651: case (SCALE_MM):
1.107 kristaps 652: r = su->scale / 1000; /* FIXME: double-check. */
1.106 kristaps 653: break;
654: case (SCALE_VS):
1.107 kristaps 655: r = su->scale * 2 - 1; /* FIXME: double-check. */
1.106 kristaps 656: break;
657: default:
1.107 kristaps 658: r = su->scale;
1.106 kristaps 659: break;
660: }
661:
662: if (r < 0.0)
663: r = 0.0;
1.107 kristaps 664: return((size_t)/* LINTED */
1.106 kristaps 665: r);
666: }
667:
668:
CVSweb