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