xref: /freebsd/contrib/mandoc/term.c (revision 2008043f386721d58158e37e0d7e50df8095942d)
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 }
1114