]> git.cameronkatri.com Git - mandoc.git/blob - mdocterm.c
d4df8a39be082a958a4cd244e55d3d123fd17358
[mandoc.git] / mdocterm.c
1 /* $Id: mdocterm.c,v 1.22 2009/03/01 13:06:49 kristaps Exp $ */
2 /*
3 * Copyright (c) 2008 Kristaps Dzonsons <kristaps@kth.se>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the
7 * above copyright notice and this permission notice appear in all
8 * copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
11 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
12 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
13 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
14 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
15 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
16 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17 * PERFORMANCE OF THIS SOFTWARE.
18 */
19 #include <sys/utsname.h>
20
21 #include <assert.h>
22 #include <ctype.h>
23 #include <err.h>
24 #include <getopt.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28
29 #ifndef __OpenBSD__
30 #include <time.h>
31 #endif
32
33 #include "mmain.h"
34 #include "term.h"
35
36 #define TERMSYM_RBRACK "]"
37 #define TERMSYM_LBRACK "["
38 #define TERMSYM_LARROW "<-"
39 #define TERMSYM_RARROW "->"
40 #define TERMSYM_UARROW "^"
41 #define TERMSYM_DARROW "v"
42 #define TERMSYM_LSQUOTE "`"
43 #define TERMSYM_RSQUOTE "\'"
44 #define TERMSYM_SQUOTE "\'"
45 #define TERMSYM_LDQUOTE "``"
46 #define TERMSYM_RDQUOTE "\'\'"
47 #define TERMSYM_DQUOTE "\""
48 #define TERMSYM_LT "<"
49 #define TERMSYM_GT ">"
50 #define TERMSYM_LE "<="
51 #define TERMSYM_GE ">="
52 #define TERMSYM_EQ "=="
53 #define TERMSYM_NEQ "!="
54 #define TERMSYM_ACUTE "\'"
55 #define TERMSYM_GRAVE "`"
56 #define TERMSYM_PI "pi"
57 #define TERMSYM_PLUSMINUS "+="
58 #define TERMSYM_INF "oo"
59 #define TERMSYM_INF2 "infinity"
60 #define TERMSYM_NAN "NaN"
61 #define TERMSYM_BAR "|"
62 #define TERMSYM_BULLET "o"
63
64 #ifdef __NetBSD__
65 #define xisspace(x) isspace((int)(x))
66 #else
67 #define xisspace(x) isspace((x))
68 #endif
69
70 enum termstyle {
71 STYLE_CLEAR,
72 STYLE_BOLD,
73 STYLE_UNDERLINE
74 };
75
76 static void body(struct termp *,
77 struct termpair *,
78 const struct mdoc_meta *,
79 const struct mdoc_node *);
80 static void header(struct termp *,
81 const struct mdoc_meta *);
82 static void footer(struct termp *,
83 const struct mdoc_meta *);
84
85 static void pword(struct termp *, const char *, size_t);
86 static void pescape(struct termp *,
87 const char *, size_t *, size_t);
88 static void nescape(struct termp *,
89 const char *, size_t);
90 static void chara(struct termp *, char);
91 static void stringa(struct termp *, const char *);
92 static void style(struct termp *, enum termstyle);
93
94 #ifdef __linux__
95 extern size_t strlcat(char *, const char *, size_t);
96 extern size_t strlcpy(char *, const char *, size_t);
97 #endif
98
99
100 int
101 main(int argc, char *argv[])
102 {
103 struct mmain *p;
104 const struct mdoc *mdoc;
105 struct termp termp;
106
107 p = mmain_alloc();
108
109 if ( ! mmain_getopt(p, argc, argv, NULL, NULL, NULL, NULL))
110 mmain_exit(p, 1);
111
112 if (NULL == (mdoc = mmain_mdoc(p)))
113 mmain_exit(p, 1);
114
115 termp.maxrmargin = 78; /* XXX */
116 termp.rmargin = termp.maxrmargin;
117 termp.maxcols = 1024;
118 termp.offset = termp.col = 0;
119 termp.flags = TERMP_NOSPACE;
120
121 if (NULL == (termp.buf = malloc(termp.maxcols)))
122 err(1, "malloc");
123
124 header(&termp, mdoc_meta(mdoc));
125 body(&termp, NULL, mdoc_meta(mdoc), mdoc_node(mdoc));
126 footer(&termp, mdoc_meta(mdoc));
127
128 free(termp.buf);
129
130 mmain_exit(p, 0);
131 /* NOTREACHED */
132 }
133
134
135 void
136 flushln(struct termp *p)
137 {
138 size_t i, j, vsz, vis, maxvis;
139
140 /*
141 * First, establish the maximum columns of "visible" content.
142 * This is usually the difference between the right-margin and
143 * an indentation, but can be, for tagged lists or columns, a
144 * small set of values.
145 */
146
147 assert(p->offset < p->rmargin);
148 maxvis = p->rmargin - p->offset;
149 vis = 0;
150
151 /*
152 * If in the standard case (left-justified), then begin with our
153 * indentation, otherwise (columns, etc.) just start spitting
154 * out text.
155 */
156
157 if ( ! (p->flags & TERMP_NOLPAD))
158 /* LINTED */
159 for (j = 0; j < p->offset; j++)
160 putchar(' ');
161
162 /*
163 * If we're literal, print out verbatim.
164 */
165 if (p->flags & TERMP_LITERAL) {
166 for (i = 0; i < p->col; i++)
167 putchar(p->buf[i]);
168 putchar('\n');
169 p->col = 0;
170 return;
171 }
172
173 for (i = 0; i < p->col; i++) {
174 /*
175 * Count up visible word characters. Control sequences
176 * (starting with the CSI) aren't counted.
177 */
178 assert( ! xisspace(p->buf[i]));
179
180 /* LINTED */
181 for (j = i, vsz = 0; j < p->col; j++) {
182 if (xisspace(p->buf[j]))
183 break;
184 else if (27 == p->buf[j]) {
185 assert(j + 4 <= p->col);
186 j += 3;
187 } else
188 vsz++;
189 }
190 assert(vsz > 0);
191
192 /*
193 * If we're breaking normally...
194 *
195 * If a word is too long and we're within a line, put it
196 * on the next line. Puke if we're being asked to write
197 * something that will exceed the right margin (i.e.,
198 * from a fresh line).
199 *
200 * If we're not breaking...
201 *
202 * Don't let the visible size exceed the full margin.
203 */
204
205 if ( ! (TERMP_NOBREAK & p->flags)) {
206 if (vis && vis + vsz > maxvis) {
207 putchar('\n');
208 for (j = 0; j < p->offset; j++)
209 putchar(' ');
210 vis = 0;
211 } else if (vis + vsz > maxvis)
212 errx(1, "word breaks right margin");
213 } else if (vis + vsz > p->maxrmargin)
214 errx(1, "word breaks right margin");
215
216 /*
217 * Write out the word and a trailing space. Omit the
218 * space if we're the last word in the line.
219 */
220
221 for ( ; i < p->col; i++) {
222 if (xisspace(p->buf[i]))
223 break;
224 putchar(p->buf[i]);
225 }
226 vis += vsz;
227 if (i < p->col) {
228 putchar(' ');
229 vis++;
230 }
231 }
232
233 if ((TERMP_NOBREAK & p->flags) && vis >= maxvis) {
234 putchar('\n');
235 for (i = 0; i < p->rmargin; i++)
236 putchar(' ');
237 p->col = 0;
238 return;
239 }
240
241 /*
242 * If we're not to right-marginalise it (newline), then instead
243 * pad to the right margin and stay off.
244 */
245
246 if (p->flags & TERMP_NOBREAK) {
247 for ( ; vis < maxvis; vis++)
248 putchar(' ');
249 } else
250 putchar('\n');
251
252 p->col = 0;
253 }
254
255
256 void
257 newln(struct termp *p)
258 {
259
260 /*
261 * A newline only breaks an existing line; it won't assert
262 * vertical space.
263 */
264 p->flags |= TERMP_NOSPACE;
265 if (0 == p->col) {
266 p->flags &= ~TERMP_NOLPAD;
267 return;
268 }
269 flushln(p);
270 p->flags &= ~TERMP_NOLPAD;
271 }
272
273
274 void
275 vspace(struct termp *p)
276 {
277
278 /*
279 * Asserts a vertical space (a full, empty line-break between
280 * lines).
281 */
282 newln(p);
283 putchar('\n');
284 }
285
286
287 static void
288 stringa(struct termp *p, const char *s)
289 {
290
291 /* XXX - speed up if not passing to chara. */
292 for ( ; *s; s++)
293 chara(p, *s);
294 }
295
296
297 static void
298 chara(struct termp *p, char c)
299 {
300
301 /*
302 * Insert a single character into the line-buffer. If the
303 * buffer's space is exceeded, then allocate more space.
304 */
305 if (p->col + 1 >= p->maxcols) {
306 p->buf = realloc(p->buf, p->maxcols * 2);
307 if (NULL == p->buf)
308 err(1, "malloc");
309 p->maxcols *= 2;
310 }
311 p->buf[(p->col)++] = c;
312 }
313
314
315 static void
316 style(struct termp *p, enum termstyle esc)
317 {
318
319 if (p->col + 4 >= p->maxcols)
320 errx(1, "line overrun");
321
322 p->buf[(p->col)++] = 27;
323 p->buf[(p->col)++] = '[';
324 switch (esc) {
325 case (STYLE_CLEAR):
326 p->buf[(p->col)++] = '0';
327 break;
328 case (STYLE_BOLD):
329 p->buf[(p->col)++] = '1';
330 break;
331 case (STYLE_UNDERLINE):
332 p->buf[(p->col)++] = '4';
333 break;
334 default:
335 abort();
336 /* NOTREACHED */
337 }
338 p->buf[(p->col)++] = 'm';
339 }
340
341
342 static void
343 nescape(struct termp *p, const char *word, size_t len)
344 {
345
346 switch (len) {
347 case (1):
348 if ('q' == word[0])
349 stringa(p, TERMSYM_DQUOTE);
350 break;
351 case (2):
352 if ('r' == word[0] && 'B' == word[1])
353 stringa(p, TERMSYM_RBRACK);
354 else if ('l' == word[0] && 'B' == word[1])
355 stringa(p, TERMSYM_LBRACK);
356 else if ('l' == word[0] && 'q' == word[1])
357 stringa(p, TERMSYM_LDQUOTE);
358 else if ('r' == word[0] && 'q' == word[1])
359 stringa(p, TERMSYM_RDQUOTE);
360 else if ('o' == word[0] && 'q' == word[1])
361 stringa(p, TERMSYM_LSQUOTE);
362 else if ('a' == word[0] && 'q' == word[1])
363 stringa(p, TERMSYM_RSQUOTE);
364 else if ('<' == word[0] && '-' == word[1])
365 stringa(p, TERMSYM_LARROW);
366 else if ('-' == word[0] && '>' == word[1])
367 stringa(p, TERMSYM_RARROW);
368 else if ('b' == word[0] && 'u' == word[1])
369 stringa(p, TERMSYM_BULLET);
370 else if ('<' == word[0] && '=' == word[1])
371 stringa(p, TERMSYM_LE);
372 else if ('>' == word[0] && '=' == word[1])
373 stringa(p, TERMSYM_GE);
374 else if ('=' == word[0] && '=' == word[1])
375 stringa(p, TERMSYM_EQ);
376 else if ('+' == word[0] && '-' == word[1])
377 stringa(p, TERMSYM_PLUSMINUS);
378 else if ('u' == word[0] && 'a' == word[1])
379 stringa(p, TERMSYM_UARROW);
380 else if ('d' == word[0] && 'a' == word[1])
381 stringa(p, TERMSYM_DARROW);
382 else if ('a' == word[0] && 'a' == word[1])
383 stringa(p, TERMSYM_ACUTE);
384 else if ('g' == word[0] && 'a' == word[1])
385 stringa(p, TERMSYM_GRAVE);
386 else if ('!' == word[0] && '=' == word[1])
387 stringa(p, TERMSYM_NEQ);
388 else if ('i' == word[0] && 'f' == word[1])
389 stringa(p, TERMSYM_INF);
390 else if ('n' == word[0] && 'a' == word[1])
391 stringa(p, TERMSYM_NAN);
392 else if ('b' == word[0] && 'a' == word[1])
393 stringa(p, TERMSYM_BAR);
394
395 /* Deprecated forms. */
396 else if ('B' == word[0] && 'a' == word[1])
397 stringa(p, TERMSYM_BAR);
398 else if ('I' == word[0] && 'f' == word[1])
399 stringa(p, TERMSYM_INF2);
400 else if ('G' == word[0] && 'e' == word[1])
401 stringa(p, TERMSYM_GE);
402 else if ('G' == word[0] && 't' == word[1])
403 stringa(p, TERMSYM_GT);
404 else if ('L' == word[0] && 'e' == word[1])
405 stringa(p, TERMSYM_LE);
406 else if ('L' == word[0] && 'q' == word[1])
407 stringa(p, TERMSYM_LDQUOTE);
408 else if ('L' == word[0] && 't' == word[1])
409 stringa(p, TERMSYM_LT);
410 else if ('N' == word[0] && 'a' == word[1])
411 stringa(p, TERMSYM_NAN);
412 else if ('N' == word[0] && 'e' == word[1])
413 stringa(p, TERMSYM_NEQ);
414 else if ('P' == word[0] && 'i' == word[1])
415 stringa(p, TERMSYM_PI);
416 else if ('P' == word[0] && 'm' == word[1])
417 stringa(p, TERMSYM_PLUSMINUS);
418 else if ('R' == word[0] && 'q' == word[1])
419 stringa(p, TERMSYM_RDQUOTE);
420 break;
421 default:
422 break;
423 }
424 }
425
426
427 static void
428 pescape(struct termp *p, const char *word, size_t *i, size_t len)
429 {
430 size_t j;
431
432 (*i)++;
433 assert(*i < len);
434
435 /*
436 * Handle an escape sequence. This must manage both groff-style
437 * escapes and mdoc-style escapes.
438 */
439
440 if ('(' == word[*i]) {
441 /* Two-character escapes. */
442 (*i)++;
443 assert(*i + 1 < len);
444 nescape(p, &word[*i], 2);
445 (*i)++;
446 return;
447
448 } else if ('*' == word[*i]) {
449 (*i)++;
450 assert(*i < len);
451 switch (word[*i]) {
452 case ('('):
453 (*i)++;
454 assert(*i + 1 < len);
455 nescape(p, &word[*i], 2);
456 (*i)++;
457 return;
458 default:
459 break;
460 }
461 nescape(p, &word[*i], 1);
462 return;
463
464 } else if ('[' != word[*i]) {
465 /* One-character escapes. */
466 switch (word[*i]) {
467 case ('\\'):
468 /* FALLTHROUGH */
469 case ('\''):
470 /* FALLTHROUGH */
471 case ('`'):
472 /* FALLTHROUGH */
473 case ('-'):
474 /* FALLTHROUGH */
475 case (' '):
476 /* FALLTHROUGH */
477 case ('.'):
478 chara(p, word[*i]);
479 break;
480 case ('e'):
481 chara(p, '\\');
482 break;
483 default:
484 break;
485 }
486 return;
487 }
488
489 (*i)++;
490 for (j = 0; word[*i] && ']' != word[*i]; (*i)++, j++)
491 /* Loop... */ ;
492
493 nescape(p, &word[*i - j], j);
494 }
495
496
497 static void
498 pword(struct termp *p, const char *word, size_t len)
499 {
500 size_t i;
501
502 /*assert(len > 0);*/ /* Can be, if literal. */
503
504 /*
505 * Handle pwords, partial words, which may be either a single
506 * word or a phrase that cannot be broken down (such as a
507 * literal string). This handles word styling.
508 */
509
510 if ( ! (p->flags & TERMP_NOSPACE) &&
511 ! (p->flags & TERMP_LITERAL))
512 chara(p, ' ');
513
514 if ( ! (p->flags & TERMP_NONOSPACE))
515 p->flags &= ~TERMP_NOSPACE;
516
517 /*
518 * XXX - if literal and underlining, this will underline the
519 * spaces between literal words.
520 */
521
522 if (p->flags & TERMP_BOLD)
523 style(p, STYLE_BOLD);
524 if (p->flags & TERMP_UNDERLINE)
525 style(p, STYLE_UNDERLINE);
526
527 for (i = 0; i < len; i++) {
528 if ('\\' == word[i]) {
529 pescape(p, word, &i, len);
530 continue;
531 }
532 chara(p, word[i]);
533 }
534
535 if (p->flags & TERMP_BOLD ||
536 p->flags & TERMP_UNDERLINE)
537 style(p, STYLE_CLEAR);
538 }
539
540
541 void
542 word(struct termp *p, const char *word)
543 {
544 size_t i, j, len;
545
546 /*
547 * Break apart a word into tokens. If we're a literal word,
548 * then don't. This doesn't handle zero-length words (there
549 * should be none) and makes sure that pword doesn't get spaces
550 * or nil words unless literal.
551 */
552
553 if (p->flags & TERMP_LITERAL) {
554 pword(p, word, strlen(word));
555 return;
556 }
557
558 len = strlen(word);
559 assert(len > 0);
560
561 if (mdoc_isdelim(word)) {
562 if ( ! (p->flags & TERMP_IGNDELIM))
563 p->flags |= TERMP_NOSPACE;
564 p->flags &= ~TERMP_IGNDELIM;
565 }
566
567 /* LINTED */
568 for (j = i = 0; i < len; i++) {
569 if ( ! xisspace(word[i])) {
570 j++;
571 continue;
572 }
573
574 /* Escaped spaces don't delimit... */
575 if (i > 0 && xisspace(word[i]) && '\\' == word[i - 1]) {
576 j++;
577 continue;
578 }
579
580 if (0 == j)
581 continue;
582 assert(i >= j);
583 pword(p, &word[i - j], j);
584 j = 0;
585 }
586 if (j > 0) {
587 assert(i >= j);
588 pword(p, &word[i - j], j);
589 }
590 }
591
592
593 static void
594 body(struct termp *p, struct termpair *ppair,
595 const struct mdoc_meta *meta,
596 const struct mdoc_node *node)
597 {
598 int dochild;
599 struct termpair pair;
600
601 /*
602 * This is the main function for printing out nodes. It's
603 * constituted of PRE and POST functions, which correspond to
604 * prefix and infix processing.
605 */
606
607 /* Pre-processing. */
608
609 dochild = 1;
610 pair.ppair = ppair;
611 pair.type = 0;
612 pair.offset = pair.rmargin = 0;
613 pair.flag = 0;
614 pair.count = 0;
615
616 if (MDOC_TEXT != node->type) {
617 if (termacts[node->tok].pre)
618 if ( ! (*termacts[node->tok].pre)(p, &pair, meta, node))
619 dochild = 0;
620 } else /* MDOC_TEXT == node->type */
621 word(p, node->data.text.string);
622
623 /* Children. */
624
625 if (TERMPAIR_FLAG & pair.type)
626 p->flags |= pair.flag;
627
628 if (dochild && node->child)
629 body(p, &pair, meta, node->child);
630
631 if (TERMPAIR_FLAG & pair.type)
632 p->flags &= ~pair.flag;
633
634 /* Post-processing. */
635
636 if (MDOC_TEXT != node->type)
637 if (termacts[node->tok].post)
638 (*termacts[node->tok].post)(p, &pair, meta, node);
639
640 /* Siblings. */
641
642 if (node->next)
643 body(p, ppair, meta, node->next);
644 }
645
646
647 static void
648 footer(struct termp *p, const struct mdoc_meta *meta)
649 {
650 struct tm *tm;
651 char *buf, *os;
652
653 if (NULL == (buf = malloc(p->rmargin)))
654 err(1, "malloc");
655 if (NULL == (os = malloc(p->rmargin)))
656 err(1, "malloc");
657
658 tm = localtime(&meta->date);
659
660 #ifdef __OpenBSD__
661 if (NULL == strftime(buf, p->rmargin, "%B %d, %Y", tm))
662 #else
663 if (0 == strftime(buf, p->rmargin, "%B %d, %Y", tm))
664 #endif
665 err(1, "strftime");
666
667 (void)strlcpy(os, meta->os, p->rmargin);
668
669 /*
670 * This is /slightly/ different from regular groff output
671 * because we don't have page numbers. Print the following:
672 *
673 * OS MDOCDATE
674 */
675
676 vspace(p);
677
678 p->flags |= TERMP_NOSPACE | TERMP_NOBREAK;
679 p->rmargin = p->maxrmargin - strlen(buf);
680 p->offset = 0;
681
682 word(p, os);
683 flushln(p);
684
685 p->flags |= TERMP_NOLPAD | TERMP_NOSPACE;
686 p->offset = p->rmargin;
687 p->rmargin = p->maxrmargin;
688 p->flags &= ~TERMP_NOBREAK;
689
690 word(p, buf);
691 flushln(p);
692
693 free(buf);
694 free(os);
695 }
696
697
698 static void
699 header(struct termp *p, const struct mdoc_meta *meta)
700 {
701 char *buf, *title, *bufp, *vbuf;
702 const char *pp;
703 struct utsname uts;
704
705 p->rmargin = p->maxrmargin;
706 p->offset = 0;
707
708 if (NULL == (buf = malloc(p->rmargin)))
709 err(1, "malloc");
710 if (NULL == (title = malloc(p->rmargin)))
711 err(1, "malloc");
712 if (NULL == (vbuf = malloc(p->rmargin)))
713 err(1, "malloc");
714
715 if (NULL == (pp = mdoc_vol2a(meta->vol))) {
716 switch (meta->msec) {
717 case (MSEC_1):
718 /* FALLTHROUGH */
719 case (MSEC_6):
720 /* FALLTHROUGH */
721 case (MSEC_7):
722 pp = mdoc_vol2a(VOL_URM);
723 break;
724 case (MSEC_8):
725 pp = mdoc_vol2a(VOL_SMM);
726 break;
727 case (MSEC_2):
728 /* FALLTHROUGH */
729 case (MSEC_3):
730 /* FALLTHROUGH */
731 case (MSEC_4):
732 /* FALLTHROUGH */
733 case (MSEC_5):
734 pp = mdoc_vol2a(VOL_PRM);
735 break;
736 case (MSEC_9):
737 pp = mdoc_vol2a(VOL_KM);
738 break;
739 default:
740 break;
741 }
742 }
743 vbuf[0] = 0;
744
745 if (pp) {
746 if (-1 == uname(&uts))
747 err(1, "uname");
748 (void)strlcat(vbuf, uts.sysname, p->rmargin);
749 (void)strlcat(vbuf, " ", p->rmargin);
750 } else if (NULL == (pp = mdoc_msec2a(meta->msec)))
751 pp = mdoc_msec2a(MSEC_local);
752
753 (void)strlcat(vbuf, pp, p->rmargin);
754
755 /*
756 * The header is strange. It has three components, which are
757 * really two with the first duplicated. It goes like this:
758 *
759 * IDENTIFIER TITLE IDENTIFIER
760 *
761 * The IDENTIFIER is NAME(SECTION), which is the command-name
762 * (if given, or "unknown" if not) followed by the manual page
763 * section. These are given in `Dt'. The TITLE is a free-form
764 * string depending on the manual volume. If not specified, it
765 * switches on the manual section.
766 */
767
768 if (mdoc_arch2a(meta->arch))
769 (void)snprintf(buf, p->rmargin, "%s (%s)",
770 vbuf, mdoc_arch2a(meta->arch));
771 else
772 (void)strlcpy(buf, vbuf, p->rmargin);
773
774 pp = mdoc_msec2a(meta->msec);
775
776 (void)snprintf(title, p->rmargin, "%s(%s)",
777 meta->title, pp ? pp : "");
778
779 for (bufp = title; *bufp; bufp++)
780 *bufp = toupper(*bufp);
781
782 p->offset = 0;
783 p->rmargin = (p->maxrmargin - strlen(buf)) / 2;
784 p->flags |= TERMP_NOBREAK | TERMP_NOSPACE;
785
786 word(p, title);
787 flushln(p);
788
789 p->flags |= TERMP_NOLPAD | TERMP_NOSPACE;
790 p->offset = p->rmargin;
791 p->rmargin = p->maxrmargin - strlen(title);
792
793 word(p, buf);
794 flushln(p);
795
796 p->offset = p->rmargin;
797 p->rmargin = p->maxrmargin;
798 p->flags &= ~TERMP_NOBREAK;
799 p->flags |= TERMP_NOLPAD | TERMP_NOSPACE;
800
801 word(p, title);
802 flushln(p);
803
804 p->rmargin = p->maxrmargin;
805 p->offset = 0;
806 p->flags &= ~TERMP_NOSPACE;
807
808 free(title);
809 free(vbuf);
810 free(buf);
811 }