nrelease - fix/improve livecd
[dragonfly.git] / contrib / mdocml / term.c
1 /* $Id: term.c,v 1.283 2021/08/10 12:55:04 schwarze Exp $ */
2 /*
3  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
4  * Copyright (c) 2010-2020 Ingo Schwarze <schwarze@openbsd.org>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 #include "config.h"
19
20 #include <sys/types.h>
21
22 #include <assert.h>
23 #include <ctype.h>
24 #include <stdint.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28
29 #include "mandoc.h"
30 #include "mandoc_aux.h"
31 #include "out.h"
32 #include "term.h"
33 #include "main.h"
34
35 static  size_t           cond_width(const struct termp *, int, int *);
36 static  void             adjbuf(struct termp_col *, size_t);
37 static  void             bufferc(struct termp *, char);
38 static  void             encode(struct termp *, const char *, size_t);
39 static  void             encode1(struct termp *, int);
40 static  void             endline(struct termp *);
41 static  void             term_field(struct termp *, size_t, size_t);
42 static  void             term_fill(struct termp *, size_t *, size_t *,
43                                 size_t);
44
45
46 void
47 term_setcol(struct termp *p, size_t maxtcol)
48 {
49         if (maxtcol > p->maxtcol) {
50                 p->tcols = mandoc_recallocarray(p->tcols,
51                     p->maxtcol, maxtcol, sizeof(*p->tcols));
52                 p->maxtcol = maxtcol;
53         }
54         p->lasttcol = maxtcol - 1;
55         p->tcol = p->tcols;
56 }
57
58 void
59 term_free(struct termp *p)
60 {
61         for (p->tcol = p->tcols; p->tcol < p->tcols + p->maxtcol; p->tcol++)
62                 free(p->tcol->buf);
63         free(p->tcols);
64         free(p->fontq);
65         free(p);
66 }
67
68 void
69 term_begin(struct termp *p, term_margin head,
70                 term_margin foot, const struct roff_meta *arg)
71 {
72
73         p->headf = head;
74         p->footf = foot;
75         p->argf = arg;
76         (*p->begin)(p);
77 }
78
79 void
80 term_end(struct termp *p)
81 {
82
83         (*p->end)(p);
84 }
85
86 /*
87  * Flush a chunk of text.  By default, break the output line each time
88  * the right margin is reached, and continue output on the next line
89  * at the same offset as the chunk itself.  By default, also break the
90  * output line at the end of the chunk.  There are many flags modifying
91  * this behaviour, see the comments in the body of the function.
92  */
93 void
94 term_flushln(struct termp *p)
95 {
96         size_t   vbl;      /* Number of blanks to prepend to the output. */
97         size_t   vbr;      /* Actual visual position of the end of field. */
98         size_t   vfield;   /* Desired visual field width. */
99         size_t   vtarget;  /* Desired visual position of the right margin. */
100         size_t   ic;       /* Character position in the input buffer. */
101         size_t   nbr;      /* Number of characters to print in this field. */
102
103         /*
104          * Normally, start writing at the left margin, but with the
105          * NOPAD flag, start writing at the current position instead.
106          */
107
108         vbl = (p->flags & TERMP_NOPAD) || p->tcol->offset < p->viscol ?
109             0 : p->tcol->offset - p->viscol;
110         if (p->minbl && vbl < p->minbl)
111                 vbl = p->minbl;
112
113         if ((p->flags & TERMP_MULTICOL) == 0)
114                 p->tcol->col = 0;
115
116         /* Loop over output lines. */
117
118         for (;;) {
119                 vfield = p->tcol->rmargin > p->viscol + vbl ?
120                     p->tcol->rmargin - p->viscol - vbl : 0;
121
122                 /*
123                  * Normally, break the line at the the right margin
124                  * of the field, but with the NOBREAK flag, only
125                  * break it at the max right margin of the screen,
126                  * and with the BRNEVER flag, never break it at all.
127                  */
128
129                 vtarget = (p->flags & TERMP_NOBREAK) == 0 ? vfield :
130                     p->maxrmargin > p->viscol + vbl ?
131                     p->maxrmargin - p->viscol - vbl : 0;
132
133                 /*
134                  * Figure out how much text will fit in the field.
135                  * If there is whitespace only, print nothing.
136                  */
137
138                 term_fill(p, &nbr, &vbr,
139                     p->flags & TERMP_BRNEVER ? SIZE_MAX : vtarget);
140                 if (nbr == 0)
141                         break;
142
143                 /*
144                  * With the CENTER or RIGHT flag, increase the indentation
145                  * to center the text between the left and right margins
146                  * or to adjust it to the right margin, respectively.
147                  */
148
149                 if (vbr < vtarget) {
150                         if (p->flags & TERMP_CENTER)
151                                 vbl += (vtarget - vbr) / 2;
152                         else if (p->flags & TERMP_RIGHT)
153                                 vbl += vtarget - vbr;
154                 }
155
156                 /* Finally, print the field content. */
157
158                 term_field(p, vbl, nbr);
159
160                 /*
161                  * If there is no text left in the field, exit the loop.
162                  * If the BRTRSP flag is set, consider trailing
163                  * whitespace significant when deciding whether
164                  * the field fits or not.
165                  */
166
167                 for (ic = p->tcol->col; ic < p->tcol->lastcol; ic++) {
168                         switch (p->tcol->buf[ic]) {
169                         case '\t':
170                                 if (p->flags & TERMP_BRTRSP)
171                                         vbr = term_tab_next(vbr);
172                                 continue;
173                         case ' ':
174                                 if (p->flags & TERMP_BRTRSP)
175                                         vbr += (*p->width)(p, ' ');
176                                 continue;
177                         case '\n':
178                         case ASCII_BREAK:
179                                 continue;
180                         default:
181                                 break;
182                         }
183                         break;
184                 }
185                 if (ic == p->tcol->lastcol)
186                         break;
187
188                 /*
189                  * At the location of an automtic line break, input
190                  * space characters are consumed by the line break.
191                  */
192
193                 while (p->tcol->col < p->tcol->lastcol &&
194                     p->tcol->buf[p->tcol->col] == ' ')
195                         p->tcol->col++;
196
197                 /*
198                  * In multi-column mode, leave the rest of the text
199                  * in the buffer to be handled by a subsequent
200                  * invocation, such that the other columns of the
201                  * table can be handled first.
202                  * In single-column mode, simply break the line.
203                  */
204
205                 if (p->flags & TERMP_MULTICOL)
206                         return;
207
208                 endline(p);
209                 p->viscol = 0;
210
211                 /*
212                  * Normally, start the next line at the same indentation
213                  * as this one, but with the BRIND flag, start it at the
214                  * right margin instead.  This is used together with
215                  * NOBREAK for the tags in various kinds of tagged lists.
216                  */
217
218                 vbl = p->flags & TERMP_BRIND ?
219                     p->tcol->rmargin : p->tcol->offset;
220         }
221
222         /* Reset output state in preparation for the next field. */
223
224         p->col = p->tcol->col = p->tcol->lastcol = 0;
225         p->minbl = p->trailspace;
226         p->flags &= ~(TERMP_BACKAFTER | TERMP_BACKBEFORE | TERMP_NOPAD);
227
228         if (p->flags & TERMP_MULTICOL)
229                 return;
230
231         /*
232          * The HANG flag means that the next field
233          * always follows on the same line.
234          * The NOBREAK flag means that the next field
235          * follows on the same line unless the field was overrun.
236          * Normally, break the line at the end of each field.
237          */
238
239         if ((p->flags & TERMP_HANG) == 0 &&
240             ((p->flags & TERMP_NOBREAK) == 0 ||
241              vbr + term_len(p, p->trailspace) > vfield))
242                 endline(p);
243 }
244
245 /*
246  * Store the number of input characters to print in this field in *nbr
247  * and their total visual width to print in *vbr.
248  * If there is only whitespace in the field, both remain zero.
249  * The desired visual width of the field is provided by vtarget.
250  * If the first word is longer, the field will be overrun.
251  */
252 static void
253 term_fill(struct termp *p, size_t *nbr, size_t *vbr, size_t vtarget)
254 {
255         size_t   ic;        /* Character position in the input buffer. */
256         size_t   vis;       /* Visual position of the current character. */
257         size_t   vn;        /* Visual position of the next character. */
258         int      breakline; /* Break at the end of this word. */
259         int      graph;     /* Last character was non-blank. */
260
261         *nbr = *vbr = vis = 0;
262         breakline = graph = 0;
263         for (ic = p->tcol->col; ic < p->tcol->lastcol; ic++) {
264                 switch (p->tcol->buf[ic]) {
265                 case '\b':  /* Escape \o (overstrike) or backspace markup. */
266                         assert(ic > 0);
267                         vis -= (*p->width)(p, p->tcol->buf[ic - 1]);
268                         continue;
269
270                 case '\t':  /* Normal ASCII whitespace. */
271                 case ' ':
272                 case ASCII_BREAK:  /* Escape \: (breakpoint). */
273                         switch (p->tcol->buf[ic]) {
274                         case '\t':
275                                 vn = term_tab_next(vis);
276                                 break;
277                         case ' ':
278                                 vn = vis + (*p->width)(p, ' ');
279                                 break;
280                         case ASCII_BREAK:
281                                 vn = vis;
282                                 break;
283                         default:
284                                 abort();
285                         }
286                         /* Can break at the end of a word. */
287                         if (breakline || vn > vtarget)
288                                 break;
289                         if (graph) {
290                                 *nbr = ic;
291                                 *vbr = vis;
292                                 graph = 0;
293                         }
294                         vis = vn;
295                         continue;
296
297                 case '\n':  /* Escape \p (break at the end of the word). */
298                         breakline = 1;
299                         continue;
300
301                 case ASCII_HYPH:  /* Breakable hyphen. */
302                         graph = 1;
303                         /*
304                          * We are about to decide whether to break the
305                          * line or not, so we no longer need this hyphen
306                          * to be marked as breakable.  Put back a real
307                          * hyphen such that we get the correct width.
308                          */
309                         p->tcol->buf[ic] = '-';
310                         vis += (*p->width)(p, '-');
311                         if (vis > vtarget) {
312                                 ic++;
313                                 break;
314                         }
315                         *nbr = ic + 1;
316                         *vbr = vis;
317                         continue;
318
319                 case ASCII_NBRSP:  /* Non-breakable space. */
320                         p->tcol->buf[ic] = ' ';
321                         /* FALLTHROUGH */
322                 default:  /* Printable character. */
323                         graph = 1;
324                         vis += (*p->width)(p, p->tcol->buf[ic]);
325                         if (vis > vtarget && *nbr > 0)
326                                 return;
327                         continue;
328                 }
329                 break;
330         }
331
332         /*
333          * If the last word extends to the end of the field without any
334          * trailing whitespace, the loop could not check yet whether it
335          * can remain on this line.  So do the check now.
336          */
337
338         if (graph && (vis <= vtarget || *nbr == 0)) {
339                 *nbr = ic;
340                 *vbr = vis;
341         }
342 }
343
344 /*
345  * Print the contents of one field
346  * with an indentation of        vbl      visual columns,
347  * and an input string length of nbr      characters.
348  */
349 static void
350 term_field(struct termp *p, size_t vbl, size_t nbr)
351 {
352         size_t   ic;    /* Character position in the input buffer. */
353         size_t   vis;   /* Visual position of the current character. */
354         size_t   dv;    /* Visual width of the current character. */
355         size_t   vn;    /* Visual position of the next character. */
356
357         vis = 0;
358         for (ic = p->tcol->col; ic < nbr; ic++) {
359
360                 /*
361                  * To avoid the printing of trailing whitespace,
362                  * do not print whitespace right away, only count it.
363                  */
364
365                 switch (p->tcol->buf[ic]) {
366                 case '\n':
367                 case ASCII_BREAK:
368                         continue;
369                 case '\t':
370                         vn = term_tab_next(vis);
371                         vbl += vn - vis;
372                         vis = vn;
373                         continue;
374                 case ' ':
375                 case ASCII_NBRSP:
376                         dv = (*p->width)(p, ' ');
377                         vbl += dv;
378                         vis += dv;
379                         continue;
380                 default:
381                         break;
382                 }
383
384                 /*
385                  * We found a non-blank character to print,
386                  * so write preceding white space now.
387                  */
388
389                 if (vbl > 0) {
390                         (*p->advance)(p, vbl);
391                         p->viscol += vbl;
392                         vbl = 0;
393                 }
394
395                 /* Print the character and adjust the visual position. */
396
397                 (*p->letter)(p, p->tcol->buf[ic]);
398                 if (p->tcol->buf[ic] == '\b') {
399                         dv = (*p->width)(p, p->tcol->buf[ic - 1]);
400                         p->viscol -= dv;
401                         vis -= dv;
402                 } else {
403                         dv = (*p->width)(p, p->tcol->buf[ic]);
404                         p->viscol += dv;
405                         vis += dv;
406                 }
407         }
408         p->tcol->col = nbr;
409 }
410
411 static void
412 endline(struct termp *p)
413 {
414         if ((p->flags & (TERMP_NEWMC | TERMP_ENDMC)) == TERMP_ENDMC) {
415                 p->mc = NULL;
416                 p->flags &= ~TERMP_ENDMC;
417         }
418         if (p->mc != NULL) {
419                 if (p->viscol && p->maxrmargin >= p->viscol)
420                         (*p->advance)(p, p->maxrmargin - p->viscol + 1);
421                 p->flags |= TERMP_NOBUF | TERMP_NOSPACE;
422                 term_word(p, p->mc);
423                 p->flags &= ~(TERMP_NOBUF | TERMP_NEWMC);
424         }
425         p->viscol = 0;
426         p->minbl = 0;
427         (*p->endline)(p);
428 }
429
430 /*
431  * A newline only breaks an existing line; it won't assert vertical
432  * space.  All data in the output buffer is flushed prior to the newline
433  * assertion.
434  */
435 void
436 term_newln(struct termp *p)
437 {
438
439         p->flags |= TERMP_NOSPACE;
440         if (p->tcol->lastcol || p->viscol)
441                 term_flushln(p);
442 }
443
444 /*
445  * Asserts a vertical space (a full, empty line-break between lines).
446  * Note that if used twice, this will cause two blank spaces and so on.
447  * All data in the output buffer is flushed prior to the newline
448  * assertion.
449  */
450 void
451 term_vspace(struct termp *p)
452 {
453
454         term_newln(p);
455         p->viscol = 0;
456         p->minbl = 0;
457         if (0 < p->skipvsp)
458                 p->skipvsp--;
459         else
460                 (*p->endline)(p);
461 }
462
463 /* Swap current and previous font; for \fP and .ft P */
464 void
465 term_fontlast(struct termp *p)
466 {
467         enum termfont    f;
468
469         f = p->fontl;
470         p->fontl = p->fontq[p->fonti];
471         p->fontq[p->fonti] = f;
472 }
473
474 /* Set font, save current, discard previous; for \f, .ft, .B etc. */
475 void
476 term_fontrepl(struct termp *p, enum termfont f)
477 {
478
479         p->fontl = p->fontq[p->fonti];
480         p->fontq[p->fonti] = f;
481 }
482
483 /* Set font, save previous. */
484 void
485 term_fontpush(struct termp *p, enum termfont f)
486 {
487
488         p->fontl = p->fontq[p->fonti];
489         if (++p->fonti == p->fontsz) {
490                 p->fontsz += 8;
491                 p->fontq = mandoc_reallocarray(p->fontq,
492                     p->fontsz, sizeof(*p->fontq));
493         }
494         p->fontq[p->fonti] = f;
495 }
496
497 /* Flush to make the saved pointer current again. */
498 void
499 term_fontpopq(struct termp *p, int i)
500 {
501
502         assert(i >= 0);
503         if (p->fonti > i)
504                 p->fonti = i;
505 }
506
507 /* Pop one font off the stack. */
508 void
509 term_fontpop(struct termp *p)
510 {
511
512         assert(p->fonti);
513         p->fonti--;
514 }
515
516 /*
517  * Handle pwords, partial words, which may be either a single word or a
518  * phrase that cannot be broken down (such as a literal string).  This
519  * handles word styling.
520  */
521 void
522 term_word(struct termp *p, const char *word)
523 {
524         struct roffsu    su;
525         const char       nbrsp[2] = { ASCII_NBRSP, 0 };
526         const char      *seq, *cp;
527         int              sz, uc;
528         size_t           csz, lsz, ssz;
529         enum mandoc_esc  esc;
530
531         if ((p->flags & TERMP_NOBUF) == 0) {
532                 if ((p->flags & TERMP_NOSPACE) == 0) {
533                         if ((p->flags & TERMP_KEEP) == 0) {
534                                 bufferc(p, ' ');
535                                 if (p->flags & TERMP_SENTENCE)
536                                         bufferc(p, ' ');
537                         } else
538                                 bufferc(p, ASCII_NBRSP);
539                 }
540                 if (p->flags & TERMP_PREKEEP)
541                         p->flags |= TERMP_KEEP;
542                 if (p->flags & TERMP_NONOSPACE)
543                         p->flags |= TERMP_NOSPACE;
544                 else
545                         p->flags &= ~TERMP_NOSPACE;
546                 p->flags &= ~(TERMP_SENTENCE | TERMP_NONEWLINE);
547                 p->skipvsp = 0;
548         }
549
550         while ('\0' != *word) {
551                 if ('\\' != *word) {
552                         if (TERMP_NBRWORD & p->flags) {
553                                 if (' ' == *word) {
554                                         encode(p, nbrsp, 1);
555                                         word++;
556                                         continue;
557                                 }
558                                 ssz = strcspn(word, "\\ ");
559                         } else
560                                 ssz = strcspn(word, "\\");
561                         encode(p, word, ssz);
562                         word += (int)ssz;
563                         continue;
564                 }
565
566                 word++;
567                 esc = mandoc_escape(&word, &seq, &sz);
568                 switch (esc) {
569                 case ESCAPE_UNICODE:
570                         uc = mchars_num2uc(seq + 1, sz - 1);
571                         break;
572                 case ESCAPE_NUMBERED:
573                         uc = mchars_num2char(seq, sz);
574                         if (uc < 0)
575                                 continue;
576                         break;
577                 case ESCAPE_SPECIAL:
578                         if (p->enc == TERMENC_ASCII) {
579                                 cp = mchars_spec2str(seq, sz, &ssz);
580                                 if (cp != NULL)
581                                         encode(p, cp, ssz);
582                         } else {
583                                 uc = mchars_spec2cp(seq, sz);
584                                 if (uc > 0)
585                                         encode1(p, uc);
586                         }
587                         continue;
588                 case ESCAPE_UNDEF:
589                         uc = *seq;
590                         break;
591                 case ESCAPE_FONTBOLD:
592                 case ESCAPE_FONTCB:
593                         term_fontrepl(p, TERMFONT_BOLD);
594                         continue;
595                 case ESCAPE_FONTITALIC:
596                 case ESCAPE_FONTCI:
597                         term_fontrepl(p, TERMFONT_UNDER);
598                         continue;
599                 case ESCAPE_FONTBI:
600                         term_fontrepl(p, TERMFONT_BI);
601                         continue;
602                 case ESCAPE_FONT:
603                 case ESCAPE_FONTCR:
604                 case ESCAPE_FONTROMAN:
605                         term_fontrepl(p, TERMFONT_NONE);
606                         continue;
607                 case ESCAPE_FONTPREV:
608                         term_fontlast(p);
609                         continue;
610                 case ESCAPE_BREAK:
611                         bufferc(p, '\n');
612                         continue;
613                 case ESCAPE_NOSPACE:
614                         if (p->flags & TERMP_BACKAFTER)
615                                 p->flags &= ~TERMP_BACKAFTER;
616                         else if (*word == '\0')
617                                 p->flags |= (TERMP_NOSPACE | TERMP_NONEWLINE);
618                         continue;
619                 case ESCAPE_DEVICE:
620                         if (p->type == TERMTYPE_PDF)
621                                 encode(p, "pdf", 3);
622                         else if (p->type == TERMTYPE_PS)
623                                 encode(p, "ps", 2);
624                         else if (p->enc == TERMENC_ASCII)
625                                 encode(p, "ascii", 5);
626                         else
627                                 encode(p, "utf8", 4);
628                         continue;
629                 case ESCAPE_HORIZ:
630                         if (*seq == '|') {
631                                 seq++;
632                                 uc = -p->col;
633                         } else
634                                 uc = 0;
635                         if (a2roffsu(seq, &su, SCALE_EM) == NULL)
636                                 continue;
637                         uc += term_hen(p, &su);
638                         if (uc > 0)
639                                 while (uc-- > 0)
640                                         bufferc(p, ASCII_NBRSP);
641                         else if (p->col > (size_t)(-uc))
642                                 p->col += uc;
643                         else {
644                                 uc += p->col;
645                                 p->col = 0;
646                                 if (p->tcol->offset > (size_t)(-uc)) {
647                                         p->ti += uc;
648                                         p->tcol->offset += uc;
649                                 } else {
650                                         p->ti -= p->tcol->offset;
651                                         p->tcol->offset = 0;
652                                 }
653                         }
654                         continue;
655                 case ESCAPE_HLINE:
656                         if ((cp = a2roffsu(seq, &su, SCALE_EM)) == NULL)
657                                 continue;
658                         uc = term_hen(p, &su);
659                         if (uc <= 0) {
660                                 if (p->tcol->rmargin <= p->tcol->offset)
661                                         continue;
662                                 lsz = p->tcol->rmargin - p->tcol->offset;
663                         } else
664                                 lsz = uc;
665                         if (*cp == seq[-1])
666                                 uc = -1;
667                         else if (*cp == '\\') {
668                                 seq = cp + 1;
669                                 esc = mandoc_escape(&seq, &cp, &sz);
670                                 switch (esc) {
671                                 case ESCAPE_UNICODE:
672                                         uc = mchars_num2uc(cp + 1, sz - 1);
673                                         break;
674                                 case ESCAPE_NUMBERED:
675                                         uc = mchars_num2char(cp, sz);
676                                         break;
677                                 case ESCAPE_SPECIAL:
678                                         uc = mchars_spec2cp(cp, sz);
679                                         break;
680                                 case ESCAPE_UNDEF:
681                                         uc = *seq;
682                                         break;
683                                 default:
684                                         uc = -1;
685                                         break;
686                                 }
687                         } else
688                                 uc = *cp;
689                         if (uc < 0x20 || (uc > 0x7E && uc < 0xA0))
690                                 uc = '_';
691                         if (p->enc == TERMENC_ASCII) {
692                                 cp = ascii_uc2str(uc);
693                                 csz = term_strlen(p, cp);
694                                 ssz = strlen(cp);
695                         } else
696                                 csz = (*p->width)(p, uc);
697                         while (lsz >= csz) {
698                                 if (p->enc == TERMENC_ASCII)
699                                         encode(p, cp, ssz);
700                                 else
701                                         encode1(p, uc);
702                                 lsz -= csz;
703                         }
704                         continue;
705                 case ESCAPE_SKIPCHAR:
706                         p->flags |= TERMP_BACKAFTER;
707                         continue;
708                 case ESCAPE_OVERSTRIKE:
709                         cp = seq + sz;
710                         while (seq < cp) {
711                                 if (*seq == '\\') {
712                                         mandoc_escape(&seq, NULL, NULL);
713                                         continue;
714                                 }
715                                 encode1(p, *seq++);
716                                 if (seq < cp) {
717                                         if (p->flags & TERMP_BACKBEFORE)
718                                                 p->flags |= TERMP_BACKAFTER;
719                                         else
720                                                 p->flags |= TERMP_BACKBEFORE;
721                                 }
722                         }
723                         /* Trim trailing backspace/blank pair. */
724                         if (p->tcol->lastcol > 2 &&
725                             (p->tcol->buf[p->tcol->lastcol - 1] == ' ' ||
726                              p->tcol->buf[p->tcol->lastcol - 1] == '\t'))
727                                 p->tcol->lastcol -= 2;
728                         if (p->col > p->tcol->lastcol)
729                                 p->col = p->tcol->lastcol;
730                         continue;
731                 default:
732                         continue;
733                 }
734
735                 /*
736                  * Common handling for Unicode and numbered
737                  * character escape sequences.
738                  */
739
740                 if (p->enc == TERMENC_ASCII) {
741                         cp = ascii_uc2str(uc);
742                         encode(p, cp, strlen(cp));
743                 } else {
744                         if ((uc < 0x20 && uc != 0x09) ||
745                             (uc > 0x7E && uc < 0xA0))
746                                 uc = 0xFFFD;
747                         encode1(p, uc);
748                 }
749         }
750         p->flags &= ~TERMP_NBRWORD;
751 }
752
753 static void
754 adjbuf(struct termp_col *c, size_t sz)
755 {
756         if (c->maxcols == 0)
757                 c->maxcols = 1024;
758         while (c->maxcols <= sz)
759                 c->maxcols <<= 2;
760         c->buf = mandoc_reallocarray(c->buf, c->maxcols, sizeof(*c->buf));
761 }
762
763 static void
764 bufferc(struct termp *p, char c)
765 {
766         if (p->flags & TERMP_NOBUF) {
767                 (*p->letter)(p, c);
768                 return;
769         }
770         if (p->col + 1 >= p->tcol->maxcols)
771                 adjbuf(p->tcol, p->col + 1);
772         if (p->tcol->lastcol <= p->col || (c != ' ' && c != ASCII_NBRSP))
773                 p->tcol->buf[p->col] = c;
774         if (p->tcol->lastcol < ++p->col)
775                 p->tcol->lastcol = p->col;
776 }
777
778 /*
779  * See encode().
780  * Do this for a single (probably unicode) value.
781  * Does not check for non-decorated glyphs.
782  */
783 static void
784 encode1(struct termp *p, int c)
785 {
786         enum termfont     f;
787
788         if (p->flags & TERMP_NOBUF) {
789                 (*p->letter)(p, c);
790                 return;
791         }
792
793         if (p->col + 7 >= p->tcol->maxcols)
794                 adjbuf(p->tcol, p->col + 7);
795
796         f = (c == ASCII_HYPH || c > 127 || isgraph(c)) ?
797             p->fontq[p->fonti] : TERMFONT_NONE;
798
799         if (p->flags & TERMP_BACKBEFORE) {
800                 if (p->tcol->buf[p->col - 1] == ' ' ||
801                     p->tcol->buf[p->col - 1] == '\t')
802                         p->col--;
803                 else
804                         p->tcol->buf[p->col++] = '\b';
805                 p->flags &= ~TERMP_BACKBEFORE;
806         }
807         if (f == TERMFONT_UNDER || f == TERMFONT_BI) {
808                 p->tcol->buf[p->col++] = '_';
809                 p->tcol->buf[p->col++] = '\b';
810         }
811         if (f == TERMFONT_BOLD || f == TERMFONT_BI) {
812                 if (c == ASCII_HYPH)
813                         p->tcol->buf[p->col++] = '-';
814                 else
815                         p->tcol->buf[p->col++] = c;
816                 p->tcol->buf[p->col++] = '\b';
817         }
818         if (p->tcol->lastcol <= p->col || (c != ' ' && c != ASCII_NBRSP))
819                 p->tcol->buf[p->col] = c;
820         if (p->tcol->lastcol < ++p->col)
821                 p->tcol->lastcol = p->col;
822         if (p->flags & TERMP_BACKAFTER) {
823                 p->flags |= TERMP_BACKBEFORE;
824                 p->flags &= ~TERMP_BACKAFTER;
825         }
826 }
827
828 static void
829 encode(struct termp *p, const char *word, size_t sz)
830 {
831         size_t            i;
832
833         if (p->flags & TERMP_NOBUF) {
834                 for (i = 0; i < sz; i++)
835                         (*p->letter)(p, word[i]);
836                 return;
837         }
838
839         if (p->col + 2 + (sz * 5) >= p->tcol->maxcols)
840                 adjbuf(p->tcol, p->col + 2 + (sz * 5));
841
842         for (i = 0; i < sz; i++) {
843                 if (ASCII_HYPH == word[i] ||
844                     isgraph((unsigned char)word[i]))
845                         encode1(p, word[i]);
846                 else {
847                         if (p->tcol->lastcol <= p->col ||
848                             (word[i] != ' ' && word[i] != ASCII_NBRSP))
849                                 p->tcol->buf[p->col] = word[i];
850                         p->col++;
851
852                         /*
853                          * Postpone the effect of \z while handling
854                          * an overstrike sequence from ascii_uc2str().
855                          */
856
857                         if (word[i] == '\b' &&
858                             (p->flags & TERMP_BACKBEFORE)) {
859                                 p->flags &= ~TERMP_BACKBEFORE;
860                                 p->flags |= TERMP_BACKAFTER;
861                         }
862                 }
863         }
864         if (p->tcol->lastcol < p->col)
865                 p->tcol->lastcol = p->col;
866 }
867
868 void
869 term_setwidth(struct termp *p, const char *wstr)
870 {
871         struct roffsu    su;
872         int              iop, width;
873
874         iop = 0;
875         width = 0;
876         if (NULL != wstr) {
877                 switch (*wstr) {
878                 case '+':
879                         iop = 1;
880                         wstr++;
881                         break;
882                 case '-':
883                         iop = -1;
884                         wstr++;
885                         break;
886                 default:
887                         break;
888                 }
889                 if (a2roffsu(wstr, &su, SCALE_MAX) != NULL)
890                         width = term_hspan(p, &su);
891                 else
892                         iop = 0;
893         }
894         (*p->setwidth)(p, iop, width);
895 }
896
897 size_t
898 term_len(const struct termp *p, size_t sz)
899 {
900
901         return (*p->width)(p, ' ') * sz;
902 }
903
904 static size_t
905 cond_width(const struct termp *p, int c, int *skip)
906 {
907
908         if (*skip) {
909                 (*skip) = 0;
910                 return 0;
911         } else
912                 return (*p->width)(p, c);
913 }
914
915 size_t
916 term_strlen(const struct termp *p, const char *cp)
917 {
918         size_t           sz, rsz, i;
919         int              ssz, skip, uc;
920         const char      *seq, *rhs;
921         enum mandoc_esc  esc;
922         static const char rej[] = { '\\', ASCII_NBRSP, ASCII_HYPH,
923                         ASCII_BREAK, '\0' };
924
925         /*
926          * Account for escaped sequences within string length
927          * calculations.  This follows the logic in term_word() as we
928          * must calculate the width of produced strings.
929          */
930
931         sz = 0;
932         skip = 0;
933         while ('\0' != *cp) {
934                 rsz = strcspn(cp, rej);
935                 for (i = 0; i < rsz; i++)
936                         sz += cond_width(p, *cp++, &skip);
937
938                 switch (*cp) {
939                 case '\\':
940                         cp++;
941                         rhs = NULL;
942                         esc = mandoc_escape(&cp, &seq, &ssz);
943                         switch (esc) {
944                         case ESCAPE_UNICODE:
945                                 uc = mchars_num2uc(seq + 1, ssz - 1);
946                                 break;
947                         case ESCAPE_NUMBERED:
948                                 uc = mchars_num2char(seq, ssz);
949                                 if (uc < 0)
950                                         continue;
951                                 break;
952                         case ESCAPE_SPECIAL:
953                                 if (p->enc == TERMENC_ASCII) {
954                                         rhs = mchars_spec2str(seq, ssz, &rsz);
955                                         if (rhs != NULL)
956                                                 break;
957                                 } else {
958                                         uc = mchars_spec2cp(seq, ssz);
959                                         if (uc > 0)
960                                                 sz += cond_width(p, uc, &skip);
961                                 }
962                                 continue;
963                         case ESCAPE_UNDEF:
964                                 uc = *seq;
965                                 break;
966                         case ESCAPE_DEVICE:
967                                 if (p->type == TERMTYPE_PDF) {
968                                         rhs = "pdf";
969                                         rsz = 3;
970                                 } else if (p->type == TERMTYPE_PS) {
971                                         rhs = "ps";
972                                         rsz = 2;
973                                 } else if (p->enc == TERMENC_ASCII) {
974                                         rhs = "ascii";
975                                         rsz = 5;
976                                 } else {
977                                         rhs = "utf8";
978                                         rsz = 4;
979                                 }
980                                 break;
981                         case ESCAPE_SKIPCHAR:
982                                 skip = 1;
983                                 continue;
984                         case ESCAPE_OVERSTRIKE:
985                                 rsz = 0;
986                                 rhs = seq + ssz;
987                                 while (seq < rhs) {
988                                         if (*seq == '\\') {
989                                                 mandoc_escape(&seq, NULL, NULL);
990                                                 continue;
991                                         }
992                                         i = (*p->width)(p, *seq++);
993                                         if (rsz < i)
994                                                 rsz = i;
995                                 }
996                                 sz += rsz;
997                                 continue;
998                         default:
999                                 continue;
1000                         }
1001
1002                         /*
1003                          * Common handling for Unicode and numbered
1004                          * character escape sequences.
1005                          */
1006
1007                         if (rhs == NULL) {
1008                                 if (p->enc == TERMENC_ASCII) {
1009                                         rhs = ascii_uc2str(uc);
1010                                         rsz = strlen(rhs);
1011                                 } else {
1012                                         if ((uc < 0x20 && uc != 0x09) ||
1013                                             (uc > 0x7E && uc < 0xA0))
1014                                                 uc = 0xFFFD;
1015                                         sz += cond_width(p, uc, &skip);
1016                                         continue;
1017                                 }
1018                         }
1019
1020                         if (skip) {
1021                                 skip = 0;
1022                                 break;
1023                         }
1024
1025                         /*
1026                          * Common handling for all escape sequences
1027                          * printing more than one character.
1028                          */
1029
1030                         for (i = 0; i < rsz; i++)
1031                                 sz += (*p->width)(p, *rhs++);
1032                         break;
1033                 case ASCII_NBRSP:
1034                         sz += cond_width(p, ' ', &skip);
1035                         cp++;
1036                         break;
1037                 case ASCII_HYPH:
1038                         sz += cond_width(p, '-', &skip);
1039                         cp++;
1040                         break;
1041                 default:
1042                         break;
1043                 }
1044         }
1045
1046         return sz;
1047 }
1048
1049 int
1050 term_vspan(const struct termp *p, const struct roffsu *su)
1051 {
1052         double           r;
1053         int              ri;
1054
1055         switch (su->unit) {
1056         case SCALE_BU:
1057                 r = su->scale / 40.0;
1058                 break;
1059         case SCALE_CM:
1060                 r = su->scale * 6.0 / 2.54;
1061                 break;
1062         case SCALE_FS:
1063                 r = su->scale * 65536.0 / 40.0;
1064                 break;
1065         case SCALE_IN:
1066                 r = su->scale * 6.0;
1067                 break;
1068         case SCALE_MM:
1069                 r = su->scale * 0.006;
1070                 break;
1071         case SCALE_PC:
1072                 r = su->scale;
1073                 break;
1074         case SCALE_PT:
1075                 r = su->scale / 12.0;
1076                 break;
1077         case SCALE_EN:
1078         case SCALE_EM:
1079                 r = su->scale * 0.6;
1080                 break;
1081         case SCALE_VS:
1082                 r = su->scale;
1083                 break;
1084         default:
1085                 abort();
1086         }
1087         ri = r > 0.0 ? r + 0.4995 : r - 0.4995;
1088         return ri < 66 ? ri : 1;
1089 }
1090
1091 /*
1092  * Convert a scaling width to basic units, rounding towards 0.
1093  */
1094 int
1095 term_hspan(const struct termp *p, const struct roffsu *su)
1096 {
1097
1098         return (*p->hspan)(p, su);
1099 }
1100
1101 /*
1102  * Convert a scaling width to basic units, rounding to closest.
1103  */
1104 int
1105 term_hen(const struct termp *p, const struct roffsu *su)
1106 {
1107         int bu;
1108
1109         if ((bu = (*p->hspan)(p, su)) >= 0)
1110                 return (bu + 11) / 24;
1111         else
1112                 return -((-bu + 11) / 24);
1113 }