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