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