]> git.cameronkatri.com Git - mandoc.git/blob - mdocterm.c
White-space churn.
[mandoc.git] / mdocterm.c
1 /* $Id: mdocterm.c,v 1.21 2009/02/28 21:31:13 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 a word is too long and we're within a line, put it
194 * on the next line. Puke if we're being asked to write
195 * something that will exceed the right margin (i.e.,
196 * from a fresh line or when we're not allowed to break
197 * the line with TERMP_NOBREAK).
198 */
199
200 /* FIXME: allow selective right-margin breaking. */
201
202 if (vis && vis + vsz > maxvis) {
203 if (p->flags & TERMP_NOBREAK)
204 errx(1, "word breaks right margin");
205 putchar('\n');
206 for (j = 0; j < p->offset; j++)
207 putchar(' ');
208 vis = 0;
209 } else if (vis + vsz > maxvis)
210 errx(1, "word breaks right margin");
211
212 /*
213 * Write out the word and a trailing space. Omit the
214 * space if we're the last word in the line.
215 */
216
217 for ( ; i < p->col; i++) {
218 if (xisspace(p->buf[i]))
219 break;
220 putchar(p->buf[i]);
221 }
222 vis += vsz;
223 if (i < p->col) {
224 putchar(' ');
225 vis++;
226 }
227 }
228
229 /*
230 * If we're not to right-marginalise it (newline), then instead
231 * pad to the right margin and stay off.
232 */
233
234 if (p->flags & TERMP_NOBREAK) {
235 if ( ! (p->flags & TERMP_NORPAD))
236 for ( ; vis < maxvis; vis++)
237 putchar(' ');
238 } else
239 putchar('\n');
240
241 p->col = 0;
242 }
243
244
245 void
246 newln(struct termp *p)
247 {
248
249 /*
250 * A newline only breaks an existing line; it won't assert
251 * vertical space.
252 */
253 p->flags |= TERMP_NOSPACE;
254 if (0 == p->col) {
255 p->flags &= ~TERMP_NOLPAD;
256 return;
257 }
258 flushln(p);
259 p->flags &= ~TERMP_NOLPAD;
260 }
261
262
263 void
264 vspace(struct termp *p)
265 {
266
267 /*
268 * Asserts a vertical space (a full, empty line-break between
269 * lines).
270 */
271 newln(p);
272 putchar('\n');
273 }
274
275
276 static void
277 stringa(struct termp *p, const char *s)
278 {
279
280 /* XXX - speed up if not passing to chara. */
281 for ( ; *s; s++)
282 chara(p, *s);
283 }
284
285
286 static void
287 chara(struct termp *p, char c)
288 {
289
290 /*
291 * Insert a single character into the line-buffer. If the
292 * buffer's space is exceeded, then allocate more space.
293 */
294 if (p->col + 1 >= p->maxcols) {
295 p->buf = realloc(p->buf, p->maxcols * 2);
296 if (NULL == p->buf)
297 err(1, "malloc");
298 p->maxcols *= 2;
299 }
300 p->buf[(p->col)++] = c;
301 }
302
303
304 static void
305 style(struct termp *p, enum termstyle esc)
306 {
307
308 if (p->col + 4 >= p->maxcols)
309 errx(1, "line overrun");
310
311 p->buf[(p->col)++] = 27;
312 p->buf[(p->col)++] = '[';
313 switch (esc) {
314 case (STYLE_CLEAR):
315 p->buf[(p->col)++] = '0';
316 break;
317 case (STYLE_BOLD):
318 p->buf[(p->col)++] = '1';
319 break;
320 case (STYLE_UNDERLINE):
321 p->buf[(p->col)++] = '4';
322 break;
323 default:
324 abort();
325 /* NOTREACHED */
326 }
327 p->buf[(p->col)++] = 'm';
328 }
329
330
331 static void
332 nescape(struct termp *p, const char *word, size_t len)
333 {
334
335 switch (len) {
336 case (1):
337 if ('q' == word[0])
338 stringa(p, TERMSYM_DQUOTE);
339 break;
340 case (2):
341 if ('r' == word[0] && 'B' == word[1])
342 stringa(p, TERMSYM_RBRACK);
343 else if ('l' == word[0] && 'B' == word[1])
344 stringa(p, TERMSYM_LBRACK);
345 else if ('l' == word[0] && 'q' == word[1])
346 stringa(p, TERMSYM_LDQUOTE);
347 else if ('r' == word[0] && 'q' == word[1])
348 stringa(p, TERMSYM_RDQUOTE);
349 else if ('o' == word[0] && 'q' == word[1])
350 stringa(p, TERMSYM_LSQUOTE);
351 else if ('a' == word[0] && 'q' == word[1])
352 stringa(p, TERMSYM_RSQUOTE);
353 else if ('<' == word[0] && '-' == word[1])
354 stringa(p, TERMSYM_LARROW);
355 else if ('-' == word[0] && '>' == word[1])
356 stringa(p, TERMSYM_RARROW);
357 else if ('b' == word[0] && 'u' == word[1])
358 stringa(p, TERMSYM_BULLET);
359 else if ('<' == word[0] && '=' == word[1])
360 stringa(p, TERMSYM_LE);
361 else if ('>' == word[0] && '=' == word[1])
362 stringa(p, TERMSYM_GE);
363 else if ('=' == word[0] && '=' == word[1])
364 stringa(p, TERMSYM_EQ);
365 else if ('+' == word[0] && '-' == word[1])
366 stringa(p, TERMSYM_PLUSMINUS);
367 else if ('u' == word[0] && 'a' == word[1])
368 stringa(p, TERMSYM_UARROW);
369 else if ('d' == word[0] && 'a' == word[1])
370 stringa(p, TERMSYM_DARROW);
371 else if ('a' == word[0] && 'a' == word[1])
372 stringa(p, TERMSYM_ACUTE);
373 else if ('g' == word[0] && 'a' == word[1])
374 stringa(p, TERMSYM_GRAVE);
375 else if ('!' == word[0] && '=' == word[1])
376 stringa(p, TERMSYM_NEQ);
377 else if ('i' == word[0] && 'f' == word[1])
378 stringa(p, TERMSYM_INF);
379 else if ('n' == word[0] && 'a' == word[1])
380 stringa(p, TERMSYM_NAN);
381 else if ('b' == word[0] && 'a' == word[1])
382 stringa(p, TERMSYM_BAR);
383
384 /* Deprecated forms. */
385 else if ('B' == word[0] && 'a' == word[1])
386 stringa(p, TERMSYM_BAR);
387 else if ('I' == word[0] && 'f' == word[1])
388 stringa(p, TERMSYM_INF2);
389 else if ('G' == word[0] && 'e' == word[1])
390 stringa(p, TERMSYM_GE);
391 else if ('G' == word[0] && 't' == word[1])
392 stringa(p, TERMSYM_GT);
393 else if ('L' == word[0] && 'e' == word[1])
394 stringa(p, TERMSYM_LE);
395 else if ('L' == word[0] && 'q' == word[1])
396 stringa(p, TERMSYM_LDQUOTE);
397 else if ('L' == word[0] && 't' == word[1])
398 stringa(p, TERMSYM_LT);
399 else if ('N' == word[0] && 'a' == word[1])
400 stringa(p, TERMSYM_NAN);
401 else if ('N' == word[0] && 'e' == word[1])
402 stringa(p, TERMSYM_NEQ);
403 else if ('P' == word[0] && 'i' == word[1])
404 stringa(p, TERMSYM_PI);
405 else if ('P' == word[0] && 'm' == word[1])
406 stringa(p, TERMSYM_PLUSMINUS);
407 else if ('R' == word[0] && 'q' == word[1])
408 stringa(p, TERMSYM_RDQUOTE);
409 break;
410 default:
411 break;
412 }
413 }
414
415
416 static void
417 pescape(struct termp *p, const char *word, size_t *i, size_t len)
418 {
419 size_t j;
420
421 (*i)++;
422 assert(*i < len);
423
424 /*
425 * Handle an escape sequence. This must manage both groff-style
426 * escapes and mdoc-style escapes.
427 */
428
429 if ('(' == word[*i]) {
430 /* Two-character escapes. */
431 (*i)++;
432 assert(*i + 1 < len);
433 nescape(p, &word[*i], 2);
434 (*i)++;
435 return;
436
437 } else if ('*' == word[*i]) {
438 (*i)++;
439 assert(*i < len);
440 switch (word[*i]) {
441 case ('('):
442 (*i)++;
443 assert(*i + 1 < len);
444 nescape(p, &word[*i], 2);
445 (*i)++;
446 return;
447 default:
448 break;
449 }
450 nescape(p, &word[*i], 1);
451 return;
452
453 } else if ('[' != word[*i]) {
454 /* One-character escapes. */
455 switch (word[*i]) {
456 case ('\\'):
457 /* FALLTHROUGH */
458 case ('\''):
459 /* FALLTHROUGH */
460 case ('`'):
461 /* FALLTHROUGH */
462 case ('-'):
463 /* FALLTHROUGH */
464 case (' '):
465 /* FALLTHROUGH */
466 case ('.'):
467 chara(p, word[*i]);
468 break;
469 case ('e'):
470 chara(p, '\\');
471 break;
472 default:
473 break;
474 }
475 return;
476 }
477
478 (*i)++;
479 for (j = 0; word[*i] && ']' != word[*i]; (*i)++, j++)
480 /* Loop... */ ;
481
482 nescape(p, &word[*i - j], j);
483 }
484
485
486 static void
487 pword(struct termp *p, const char *word, size_t len)
488 {
489 size_t i;
490
491 /*assert(len > 0);*/ /* Can be, if literal. */
492
493 /*
494 * Handle pwords, partial words, which may be either a single
495 * word or a phrase that cannot be broken down (such as a
496 * literal string). This handles word styling.
497 */
498
499 if ( ! (p->flags & TERMP_NOSPACE) &&
500 ! (p->flags & TERMP_LITERAL))
501 chara(p, ' ');
502
503 if ( ! (p->flags & TERMP_NONOSPACE))
504 p->flags &= ~TERMP_NOSPACE;
505
506 /*
507 * XXX - if literal and underlining, this will underline the
508 * spaces between literal words.
509 */
510
511 if (p->flags & TERMP_BOLD)
512 style(p, STYLE_BOLD);
513 if (p->flags & TERMP_UNDERLINE)
514 style(p, STYLE_UNDERLINE);
515
516 for (i = 0; i < len; i++) {
517 if ('\\' == word[i]) {
518 pescape(p, word, &i, len);
519 continue;
520 }
521 chara(p, word[i]);
522 }
523
524 if (p->flags & TERMP_BOLD ||
525 p->flags & TERMP_UNDERLINE)
526 style(p, STYLE_CLEAR);
527 }
528
529
530 void
531 word(struct termp *p, const char *word)
532 {
533 size_t i, j, len;
534
535 /*
536 * Break apart a word into tokens. If we're a literal word,
537 * then don't. This doesn't handle zero-length words (there
538 * should be none) and makes sure that pword doesn't get spaces
539 * or nil words unless literal.
540 */
541
542 if (p->flags & TERMP_LITERAL) {
543 pword(p, word, strlen(word));
544 return;
545 }
546
547 len = strlen(word);
548 assert(len > 0);
549
550 if (mdoc_isdelim(word)) {
551 if ( ! (p->flags & TERMP_IGNDELIM))
552 p->flags |= TERMP_NOSPACE;
553 p->flags &= ~TERMP_IGNDELIM;
554 }
555
556 /* LINTED */
557 for (j = i = 0; i < len; i++) {
558 if ( ! xisspace(word[i])) {
559 j++;
560 continue;
561 }
562
563 /* Escaped spaces don't delimit... */
564 if (i > 0 && xisspace(word[i]) && '\\' == word[i - 1]) {
565 j++;
566 continue;
567 }
568
569 if (0 == j)
570 continue;
571 assert(i >= j);
572 pword(p, &word[i - j], j);
573 j = 0;
574 }
575 if (j > 0) {
576 assert(i >= j);
577 pword(p, &word[i - j], j);
578 }
579 }
580
581
582 static void
583 body(struct termp *p, struct termpair *ppair,
584 const struct mdoc_meta *meta,
585 const struct mdoc_node *node)
586 {
587 int dochild;
588 struct termpair pair;
589
590 /*
591 * This is the main function for printing out nodes. It's
592 * constituted of PRE and POST functions, which correspond to
593 * prefix and infix processing.
594 */
595
596 /* Pre-processing. */
597
598 dochild = 1;
599 pair.ppair = ppair;
600 pair.type = 0;
601 pair.offset = pair.rmargin = 0;
602 pair.flag = 0;
603 pair.count = 0;
604
605 if (MDOC_TEXT != node->type) {
606 if (termacts[node->tok].pre)
607 if ( ! (*termacts[node->tok].pre)(p, &pair, meta, node))
608 dochild = 0;
609 } else /* MDOC_TEXT == node->type */
610 word(p, node->data.text.string);
611
612 /* Children. */
613
614 if (TERMPAIR_FLAG & pair.type)
615 p->flags |= pair.flag;
616
617 if (dochild && node->child)
618 body(p, &pair, meta, node->child);
619
620 if (TERMPAIR_FLAG & pair.type)
621 p->flags &= ~pair.flag;
622
623 /* Post-processing. */
624
625 if (MDOC_TEXT != node->type)
626 if (termacts[node->tok].post)
627 (*termacts[node->tok].post)(p, &pair, meta, node);
628
629 /* Siblings. */
630
631 if (node->next)
632 body(p, ppair, meta, node->next);
633 }
634
635
636 static void
637 footer(struct termp *p, const struct mdoc_meta *meta)
638 {
639 struct tm *tm;
640 char *buf, *os;
641
642 if (NULL == (buf = malloc(p->rmargin)))
643 err(1, "malloc");
644 if (NULL == (os = malloc(p->rmargin)))
645 err(1, "malloc");
646
647 tm = localtime(&meta->date);
648
649 #ifdef __OpenBSD__
650 if (NULL == strftime(buf, p->rmargin, "%B %d, %Y", tm))
651 #else
652 if (0 == strftime(buf, p->rmargin, "%B %d, %Y", tm))
653 #endif
654 err(1, "strftime");
655
656 (void)strlcpy(os, meta->os, p->rmargin);
657
658 /*
659 * This is /slightly/ different from regular groff output
660 * because we don't have page numbers. Print the following:
661 *
662 * OS MDOCDATE
663 */
664
665 vspace(p);
666
667 p->flags |= TERMP_NOSPACE | TERMP_NOBREAK;
668 p->rmargin = p->maxrmargin - strlen(buf);
669 p->offset = 0;
670
671 word(p, os);
672 flushln(p);
673
674 p->flags |= TERMP_NOLPAD | TERMP_NOSPACE;
675 p->offset = p->rmargin;
676 p->rmargin = p->maxrmargin;
677 p->flags &= ~TERMP_NOBREAK;
678
679 word(p, buf);
680 flushln(p);
681
682 free(buf);
683 free(os);
684 }
685
686
687 static void
688 header(struct termp *p, const struct mdoc_meta *meta)
689 {
690 char *buf, *title, *bufp, *vbuf;
691 const char *pp;
692 struct utsname uts;
693
694 p->rmargin = p->maxrmargin;
695 p->offset = 0;
696
697 if (NULL == (buf = malloc(p->rmargin)))
698 err(1, "malloc");
699 if (NULL == (title = malloc(p->rmargin)))
700 err(1, "malloc");
701 if (NULL == (vbuf = malloc(p->rmargin)))
702 err(1, "malloc");
703
704 if (NULL == (pp = mdoc_vol2a(meta->vol))) {
705 switch (meta->msec) {
706 case (MSEC_1):
707 /* FALLTHROUGH */
708 case (MSEC_6):
709 /* FALLTHROUGH */
710 case (MSEC_7):
711 pp = mdoc_vol2a(VOL_URM);
712 break;
713 case (MSEC_8):
714 pp = mdoc_vol2a(VOL_SMM);
715 break;
716 case (MSEC_2):
717 /* FALLTHROUGH */
718 case (MSEC_3):
719 /* FALLTHROUGH */
720 case (MSEC_4):
721 /* FALLTHROUGH */
722 case (MSEC_5):
723 pp = mdoc_vol2a(VOL_PRM);
724 break;
725 case (MSEC_9):
726 pp = mdoc_vol2a(VOL_KM);
727 break;
728 default:
729 break;
730 }
731 }
732 vbuf[0] = 0;
733
734 if (pp) {
735 if (-1 == uname(&uts))
736 err(1, "uname");
737 (void)strlcat(vbuf, uts.sysname, p->rmargin);
738 (void)strlcat(vbuf, " ", p->rmargin);
739 } else if (NULL == (pp = mdoc_msec2a(meta->msec)))
740 pp = mdoc_msec2a(MSEC_local);
741
742 (void)strlcat(vbuf, pp, p->rmargin);
743
744 /*
745 * The header is strange. It has three components, which are
746 * really two with the first duplicated. It goes like this:
747 *
748 * IDENTIFIER TITLE IDENTIFIER
749 *
750 * The IDENTIFIER is NAME(SECTION), which is the command-name
751 * (if given, or "unknown" if not) followed by the manual page
752 * section. These are given in `Dt'. The TITLE is a free-form
753 * string depending on the manual volume. If not specified, it
754 * switches on the manual section.
755 */
756
757 if (mdoc_arch2a(meta->arch))
758 (void)snprintf(buf, p->rmargin, "%s (%s)",
759 vbuf, mdoc_arch2a(meta->arch));
760 else
761 (void)strlcpy(buf, vbuf, p->rmargin);
762
763 pp = mdoc_msec2a(meta->msec);
764
765 (void)snprintf(title, p->rmargin, "%s(%s)",
766 meta->title, pp ? pp : "");
767
768 for (bufp = title; *bufp; bufp++)
769 *bufp = toupper(*bufp);
770
771 p->offset = 0;
772 p->rmargin = (p->maxrmargin - strlen(buf)) / 2;
773 p->flags |= TERMP_NOBREAK | TERMP_NOSPACE;
774
775 word(p, title);
776 flushln(p);
777
778 p->flags |= TERMP_NOLPAD | TERMP_NOSPACE;
779 p->offset = p->rmargin;
780 p->rmargin = p->maxrmargin - strlen(title);
781
782 word(p, buf);
783 flushln(p);
784
785 p->offset = p->rmargin;
786 p->rmargin = p->maxrmargin;
787 p->flags &= ~TERMP_NOBREAK;
788 p->flags |= TERMP_NOLPAD | TERMP_NOSPACE;
789
790 word(p, title);
791 flushln(p);
792
793 p->rmargin = p->maxrmargin;
794 p->offset = 0;
795 p->flags &= ~TERMP_NOSPACE;
796
797 free(title);
798 free(vbuf);
799 free(buf);
800 }