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