1 /* $Id: term.c,v 1.274 2017/07/28 14:25:48 schwarze Exp $ */
3 * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
4 * Copyright (c) 2010-2017 Ingo Schwarze <schwarze@openbsd.org>
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.
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.
20 #include <sys/types.h>
29 #include "mandoc_aux.h"
34 static size_t cond_width(const struct termp
*, int, int *);
35 static void adjbuf(struct termp_col
*, size_t);
36 static void bufferc(struct termp
*, char);
37 static void encode(struct termp
*, const char *, size_t);
38 static void encode1(struct termp
*, int);
39 static void endline(struct termp
*);
43 term_setcol(struct termp
*p
, size_t maxtcol
)
45 if (maxtcol
> p
->maxtcol
) {
46 p
->tcols
= mandoc_recallocarray(p
->tcols
,
47 p
->maxtcol
, maxtcol
, sizeof(*p
->tcols
));
50 p
->lasttcol
= maxtcol
- 1;
55 term_free(struct termp
*p
)
57 for (p
->tcol
= p
->tcols
; p
->tcol
< p
->tcols
+ p
->maxtcol
; p
->tcol
++)
65 term_begin(struct termp
*p
, term_margin head
,
66 term_margin foot
, const struct roff_meta
*arg
)
76 term_end(struct termp
*p
)
83 * Flush a chunk of text. By default, break the output line each time
84 * the right margin is reached, and continue output on the next line
85 * at the same offset as the chunk itself. By default, also break the
86 * output line at the end of the chunk.
87 * The following flags may be specified:
89 * - TERMP_NOBREAK: Do not break the output line at the right margin,
90 * but only at the max right margin. Also, do not break the output
91 * line at the end of the chunk, such that the next call can pad to
92 * the next column. However, if less than p->trailspace blanks,
93 * which can be 0, 1, or 2, remain to the right margin, the line
95 * - TERMP_BRTRSP: Consider trailing whitespace significant
96 * when deciding whether the chunk fits or not.
97 * - TERMP_BRIND: If the chunk does not fit and the output line has
98 * to be broken, start the next line at the right margin instead
99 * of at the offset. Used together with TERMP_NOBREAK for the tags
100 * in various kinds of tagged lists.
101 * - TERMP_HANG: Do not break the output line at the right margin,
102 * append the next chunk after it even if this one is too long.
103 * To be used together with TERMP_NOBREAK.
104 * - TERMP_NOPAD: Start writing at the current position,
105 * do not pad with blank characters up to the offset.
108 term_flushln(struct termp
*p
)
110 size_t vis
; /* current visual position on output */
111 size_t vbl
; /* number of blanks to prepend to output */
112 size_t vend
; /* end of word visual position on output */
113 size_t bp
; /* visual right border position */
114 size_t dv
; /* temporary for visual pos calculations */
115 size_t j
; /* temporary loop index for p->tcol->buf */
116 size_t jhy
; /* last hyph before overflow w/r/t j */
117 size_t maxvis
; /* output position of visible boundary */
118 int ntab
; /* number of tabs to prepend */
119 int breakline
; /* after this word */
121 vbl
= (p
->flags
& TERMP_NOPAD
) || p
->tcol
->offset
< p
->viscol
?
122 0 : p
->tcol
->offset
- p
->viscol
;
123 if (p
->minbl
&& vbl
< p
->minbl
)
125 maxvis
= p
->tcol
->rmargin
> p
->viscol
+ vbl
?
126 p
->tcol
->rmargin
- p
->viscol
- vbl
: 0;
127 bp
= !(p
->flags
& TERMP_NOBREAK
) ? maxvis
:
128 p
->maxrmargin
> p
->viscol
+ vbl
?
129 p
->maxrmargin
- p
->viscol
- vbl
: 0;
132 if ((p
->flags
& TERMP_MULTICOL
) == 0)
134 while (p
->tcol
->col
< p
->tcol
->lastcol
) {
137 * Handle literal tab characters: collapse all
138 * subsequent tabs into a single huge set of spaces.
142 while (p
->tcol
->col
< p
->tcol
->lastcol
&&
143 p
->tcol
->buf
[p
->tcol
->col
] == '\t') {
144 vend
= term_tab_next(vis
);
152 * Count up visible word characters. Control sequences
153 * (starting with the CSI) aren't counted. A space
154 * generates a non-printing word, which is valid (the
155 * space is printed according to regular spacing rules).
160 for (j
= p
->tcol
->col
; j
< p
->tcol
->lastcol
; j
++) {
161 if (p
->tcol
->buf
[j
] == '\n') {
162 if ((p
->flags
& TERMP_BRIND
) == 0)
166 if (p
->tcol
->buf
[j
] == ' ' || p
->tcol
->buf
[j
] == '\t')
169 /* Back over the last printed character. */
170 if (p
->tcol
->buf
[j
] == '\b') {
172 vend
-= (*p
->width
)(p
, p
->tcol
->buf
[j
- 1]);
177 /* Break at the hyphen point if we overrun. */
178 if (vend
> vis
&& vend
< bp
&&
179 (p
->tcol
->buf
[j
] == ASCII_HYPH
||
180 p
->tcol
->buf
[j
] == ASCII_BREAK
))
184 * Hyphenation now decided, put back a real
185 * hyphen such that we get the correct width.
187 if (p
->tcol
->buf
[j
] == ASCII_HYPH
)
188 p
->tcol
->buf
[j
] = '-';
190 vend
+= (*p
->width
)(p
, p
->tcol
->buf
[j
]);
194 * Find out whether we would exceed the right margin.
195 * If so, break to the next line.
198 if (vend
> bp
&& jhy
== 0 && vis
> 0 &&
199 (p
->flags
& TERMP_BRNEVER
) == 0) {
200 if (p
->flags
& TERMP_MULTICOL
)
206 /* Use pending tabs on the new line. */
210 vbl
= term_tab_next(vbl
);
212 /* Re-establish indentation. */
214 if (p
->flags
& TERMP_BRIND
)
215 vbl
+= p
->tcol
->rmargin
;
217 vbl
+= p
->tcol
->offset
;
218 maxvis
= p
->tcol
->rmargin
> vbl
?
219 p
->tcol
->rmargin
- vbl
: 0;
220 bp
= !(p
->flags
& TERMP_NOBREAK
) ? maxvis
:
221 p
->maxrmargin
> vbl
? p
->maxrmargin
- vbl
: 0;
225 * Write out the rest of the word.
228 for ( ; p
->tcol
->col
< p
->tcol
->lastcol
; p
->tcol
->col
++) {
229 if (vend
> bp
&& jhy
> 0 && p
->tcol
->col
> jhy
)
231 if (p
->tcol
->buf
[p
->tcol
->col
] == '\n')
233 if (p
->tcol
->buf
[p
->tcol
->col
] == '\t')
235 if (p
->tcol
->buf
[p
->tcol
->col
] == ' ') {
237 while (p
->tcol
->col
< p
->tcol
->lastcol
&&
238 p
->tcol
->buf
[p
->tcol
->col
] == ' ')
240 dv
= (p
->tcol
->col
- j
) * (*p
->width
)(p
, ' ');
245 if (p
->tcol
->buf
[p
->tcol
->col
] == ASCII_NBRSP
) {
246 vbl
+= (*p
->width
)(p
, ' ');
249 if (p
->tcol
->buf
[p
->tcol
->col
] == ASCII_BREAK
)
253 * Now we definitely know there will be
254 * printable characters to output,
255 * so write preceding white space now.
258 (*p
->advance
)(p
, vbl
);
263 (*p
->letter
)(p
, p
->tcol
->buf
[p
->tcol
->col
]);
264 if (p
->tcol
->buf
[p
->tcol
->col
] == '\b')
265 p
->viscol
-= (*p
->width
)(p
,
266 p
->tcol
->buf
[p
->tcol
->col
- 1]);
268 p
->viscol
+= (*p
->width
)(p
,
269 p
->tcol
->buf
[p
->tcol
->col
]);
276 /* Explicitly requested output line break. */
278 if (p
->flags
& TERMP_MULTICOL
)
285 /* Re-establish indentation. */
287 vbl
= p
->tcol
->offset
;
288 maxvis
= p
->tcol
->rmargin
> vbl
?
289 p
->tcol
->rmargin
- vbl
: 0;
290 bp
= !(p
->flags
& TERMP_NOBREAK
) ? maxvis
:
291 p
->maxrmargin
> vbl
? p
->maxrmargin
- vbl
: 0;
295 * If there was trailing white space, it was not printed;
296 * so reset the cursor position accordingly.
304 p
->col
= p
->tcol
->col
= p
->tcol
->lastcol
= 0;
305 p
->minbl
= p
->trailspace
;
306 p
->flags
&= ~(TERMP_BACKAFTER
| TERMP_BACKBEFORE
| TERMP_NOPAD
);
308 if (p
->flags
& TERMP_MULTICOL
)
311 /* Trailing whitespace is significant in some columns. */
313 if (vis
&& vbl
&& (TERMP_BRTRSP
& p
->flags
))
316 /* If the column was overrun, break the line. */
317 if ((p
->flags
& TERMP_NOBREAK
) == 0 ||
318 ((p
->flags
& TERMP_HANG
) == 0 &&
319 vis
+ p
->trailspace
* (*p
->width
)(p
, ' ') > maxvis
))
324 endline(struct termp
*p
)
326 if ((p
->flags
& (TERMP_NEWMC
| TERMP_ENDMC
)) == TERMP_ENDMC
) {
328 p
->flags
&= ~TERMP_ENDMC
;
331 if (p
->viscol
&& p
->maxrmargin
>= p
->viscol
)
332 (*p
->advance
)(p
, p
->maxrmargin
- p
->viscol
+ 1);
333 p
->flags
|= TERMP_NOBUF
| TERMP_NOSPACE
;
335 p
->flags
&= ~(TERMP_NOBUF
| TERMP_NEWMC
);
343 * A newline only breaks an existing line; it won't assert vertical
344 * space. All data in the output buffer is flushed prior to the newline
348 term_newln(struct termp
*p
)
351 p
->flags
|= TERMP_NOSPACE
;
352 if (p
->tcol
->lastcol
|| p
->viscol
)
357 * Asserts a vertical space (a full, empty line-break between lines).
358 * Note that if used twice, this will cause two blank spaces and so on.
359 * All data in the output buffer is flushed prior to the newline
363 term_vspace(struct termp
*p
)
375 /* Swap current and previous font; for \fP and .ft P */
377 term_fontlast(struct termp
*p
)
382 p
->fontl
= p
->fontq
[p
->fonti
];
383 p
->fontq
[p
->fonti
] = f
;
386 /* Set font, save current, discard previous; for \f, .ft, .B etc. */
388 term_fontrepl(struct termp
*p
, enum termfont f
)
391 p
->fontl
= p
->fontq
[p
->fonti
];
392 p
->fontq
[p
->fonti
] = f
;
395 /* Set font, save previous. */
397 term_fontpush(struct termp
*p
, enum termfont f
)
400 p
->fontl
= p
->fontq
[p
->fonti
];
401 if (++p
->fonti
== p
->fontsz
) {
403 p
->fontq
= mandoc_reallocarray(p
->fontq
,
404 p
->fontsz
, sizeof(*p
->fontq
));
406 p
->fontq
[p
->fonti
] = f
;
409 /* Flush to make the saved pointer current again. */
411 term_fontpopq(struct termp
*p
, int i
)
419 /* Pop one font off the stack. */
421 term_fontpop(struct termp
*p
)
429 * Handle pwords, partial words, which may be either a single word or a
430 * phrase that cannot be broken down (such as a literal string). This
431 * handles word styling.
434 term_word(struct termp
*p
, const char *word
)
437 const char nbrsp
[2] = { ASCII_NBRSP
, 0 };
438 const char *seq
, *cp
;
440 size_t csz
, lsz
, ssz
;
443 if ((p
->flags
& TERMP_NOBUF
) == 0) {
444 if ((p
->flags
& TERMP_NOSPACE
) == 0) {
445 if ((p
->flags
& TERMP_KEEP
) == 0) {
447 if (p
->flags
& TERMP_SENTENCE
)
450 bufferc(p
, ASCII_NBRSP
);
452 if (p
->flags
& TERMP_PREKEEP
)
453 p
->flags
|= TERMP_KEEP
;
454 if (p
->flags
& TERMP_NONOSPACE
)
455 p
->flags
|= TERMP_NOSPACE
;
457 p
->flags
&= ~TERMP_NOSPACE
;
458 p
->flags
&= ~(TERMP_SENTENCE
| TERMP_NONEWLINE
);
462 while ('\0' != *word
) {
464 if (TERMP_NBRWORD
& p
->flags
) {
470 ssz
= strcspn(word
, "\\ ");
472 ssz
= strcspn(word
, "\\");
473 encode(p
, word
, ssz
);
479 esc
= mandoc_escape(&word
, &seq
, &sz
);
480 if (ESCAPE_ERROR
== esc
)
485 uc
= mchars_num2uc(seq
+ 1, sz
- 1);
487 case ESCAPE_NUMBERED
:
488 uc
= mchars_num2char(seq
, sz
);
493 if (p
->enc
== TERMENC_ASCII
) {
494 cp
= mchars_spec2str(seq
, sz
, &ssz
);
498 uc
= mchars_spec2cp(seq
, sz
);
503 case ESCAPE_FONTBOLD
:
504 term_fontrepl(p
, TERMFONT_BOLD
);
506 case ESCAPE_FONTITALIC
:
507 term_fontrepl(p
, TERMFONT_UNDER
);
510 term_fontrepl(p
, TERMFONT_BI
);
513 case ESCAPE_FONTROMAN
:
514 term_fontrepl(p
, TERMFONT_NONE
);
516 case ESCAPE_FONTPREV
:
523 if (p
->flags
& TERMP_BACKAFTER
)
524 p
->flags
&= ~TERMP_BACKAFTER
;
525 else if (*word
== '\0')
526 p
->flags
|= (TERMP_NOSPACE
| TERMP_NONEWLINE
);
534 if (a2roffsu(seq
, &su
, SCALE_EM
) == NULL
)
536 uc
+= term_hen(p
, &su
);
539 bufferc(p
, ASCII_NBRSP
);
540 else if (p
->col
> (size_t)(-uc
))
545 if (p
->tcol
->offset
> (size_t)(-uc
)) {
547 p
->tcol
->offset
+= uc
;
549 p
->ti
-= p
->tcol
->offset
;
555 if ((cp
= a2roffsu(seq
, &su
, SCALE_EM
)) == NULL
)
557 uc
= term_hen(p
, &su
);
559 if (p
->tcol
->rmargin
<= p
->tcol
->offset
)
561 lsz
= p
->tcol
->rmargin
- p
->tcol
->offset
;
566 else if (*cp
== '\\') {
568 esc
= mandoc_escape(&seq
, &cp
, &sz
);
571 uc
= mchars_num2uc(cp
+ 1, sz
- 1);
573 case ESCAPE_NUMBERED
:
574 uc
= mchars_num2char(cp
, sz
);
577 uc
= mchars_spec2cp(cp
, sz
);
585 if (uc
< 0x20 || (uc
> 0x7E && uc
< 0xA0))
587 if (p
->enc
== TERMENC_ASCII
) {
588 cp
= ascii_uc2str(uc
);
589 csz
= term_strlen(p
, cp
);
592 csz
= (*p
->width
)(p
, uc
);
594 if (p
->enc
== TERMENC_ASCII
)
601 case ESCAPE_SKIPCHAR
:
602 p
->flags
|= TERMP_BACKAFTER
;
604 case ESCAPE_OVERSTRIKE
:
608 mandoc_escape(&seq
, NULL
, NULL
);
613 if (p
->flags
& TERMP_BACKBEFORE
)
614 p
->flags
|= TERMP_BACKAFTER
;
616 p
->flags
|= TERMP_BACKBEFORE
;
619 /* Trim trailing backspace/blank pair. */
620 if (p
->tcol
->lastcol
> 2 &&
621 (p
->tcol
->buf
[p
->tcol
->lastcol
- 1] == ' ' ||
622 p
->tcol
->buf
[p
->tcol
->lastcol
- 1] == '\t'))
623 p
->tcol
->lastcol
-= 2;
624 if (p
->col
> p
->tcol
->lastcol
)
625 p
->col
= p
->tcol
->lastcol
;
632 * Common handling for Unicode and numbered
633 * character escape sequences.
636 if (p
->enc
== TERMENC_ASCII
) {
637 cp
= ascii_uc2str(uc
);
638 encode(p
, cp
, strlen(cp
));
640 if ((uc
< 0x20 && uc
!= 0x09) ||
641 (uc
> 0x7E && uc
< 0xA0))
646 p
->flags
&= ~TERMP_NBRWORD
;
650 adjbuf(struct termp_col
*c
, size_t sz
)
654 while (c
->maxcols
<= sz
)
656 c
->buf
= mandoc_reallocarray(c
->buf
, c
->maxcols
, sizeof(*c
->buf
));
660 bufferc(struct termp
*p
, char c
)
662 if (p
->flags
& TERMP_NOBUF
) {
666 if (p
->col
+ 1 >= p
->tcol
->maxcols
)
667 adjbuf(p
->tcol
, p
->col
+ 1);
668 if (p
->tcol
->lastcol
<= p
->col
|| (c
!= ' ' && c
!= ASCII_NBRSP
))
669 p
->tcol
->buf
[p
->col
] = c
;
670 if (p
->tcol
->lastcol
< ++p
->col
)
671 p
->tcol
->lastcol
= p
->col
;
676 * Do this for a single (probably unicode) value.
677 * Does not check for non-decorated glyphs.
680 encode1(struct termp
*p
, int c
)
684 if (p
->flags
& TERMP_NOBUF
) {
689 if (p
->col
+ 7 >= p
->tcol
->maxcols
)
690 adjbuf(p
->tcol
, p
->col
+ 7);
692 f
= (c
== ASCII_HYPH
|| c
> 127 || isgraph(c
)) ?
693 p
->fontq
[p
->fonti
] : TERMFONT_NONE
;
695 if (p
->flags
& TERMP_BACKBEFORE
) {
696 if (p
->tcol
->buf
[p
->col
- 1] == ' ' ||
697 p
->tcol
->buf
[p
->col
- 1] == '\t')
700 p
->tcol
->buf
[p
->col
++] = '\b';
701 p
->flags
&= ~TERMP_BACKBEFORE
;
703 if (f
== TERMFONT_UNDER
|| f
== TERMFONT_BI
) {
704 p
->tcol
->buf
[p
->col
++] = '_';
705 p
->tcol
->buf
[p
->col
++] = '\b';
707 if (f
== TERMFONT_BOLD
|| f
== TERMFONT_BI
) {
709 p
->tcol
->buf
[p
->col
++] = '-';
711 p
->tcol
->buf
[p
->col
++] = c
;
712 p
->tcol
->buf
[p
->col
++] = '\b';
714 if (p
->tcol
->lastcol
<= p
->col
|| (c
!= ' ' && c
!= ASCII_NBRSP
))
715 p
->tcol
->buf
[p
->col
] = c
;
716 if (p
->tcol
->lastcol
< ++p
->col
)
717 p
->tcol
->lastcol
= p
->col
;
718 if (p
->flags
& TERMP_BACKAFTER
) {
719 p
->flags
|= TERMP_BACKBEFORE
;
720 p
->flags
&= ~TERMP_BACKAFTER
;
725 encode(struct termp
*p
, const char *word
, size_t sz
)
729 if (p
->flags
& TERMP_NOBUF
) {
730 for (i
= 0; i
< sz
; i
++)
731 (*p
->letter
)(p
, word
[i
]);
735 if (p
->col
+ 2 + (sz
* 5) >= p
->tcol
->maxcols
)
736 adjbuf(p
->tcol
, p
->col
+ 2 + (sz
* 5));
738 for (i
= 0; i
< sz
; i
++) {
739 if (ASCII_HYPH
== word
[i
] ||
740 isgraph((unsigned char)word
[i
]))
743 if (p
->tcol
->lastcol
<= p
->col
||
744 (word
[i
] != ' ' && word
[i
] != ASCII_NBRSP
))
745 p
->tcol
->buf
[p
->col
] = word
[i
];
749 * Postpone the effect of \z while handling
750 * an overstrike sequence from ascii_uc2str().
753 if (word
[i
] == '\b' &&
754 (p
->flags
& TERMP_BACKBEFORE
)) {
755 p
->flags
&= ~TERMP_BACKBEFORE
;
756 p
->flags
|= TERMP_BACKAFTER
;
760 if (p
->tcol
->lastcol
< p
->col
)
761 p
->tcol
->lastcol
= p
->col
;
765 term_setwidth(struct termp
*p
, const char *wstr
)
785 if (a2roffsu(wstr
, &su
, SCALE_MAX
) != NULL
)
786 width
= term_hspan(p
, &su
);
790 (*p
->setwidth
)(p
, iop
, width
);
794 term_len(const struct termp
*p
, size_t sz
)
797 return (*p
->width
)(p
, ' ') * sz
;
801 cond_width(const struct termp
*p
, int c
, int *skip
)
808 return (*p
->width
)(p
, c
);
812 term_strlen(const struct termp
*p
, const char *cp
)
816 const char *seq
, *rhs
;
818 static const char rej
[] = { '\\', ASCII_NBRSP
, ASCII_HYPH
,
822 * Account for escaped sequences within string length
823 * calculations. This follows the logic in term_word() as we
824 * must calculate the width of produced strings.
829 while ('\0' != *cp
) {
830 rsz
= strcspn(cp
, rej
);
831 for (i
= 0; i
< rsz
; i
++)
832 sz
+= cond_width(p
, *cp
++, &skip
);
837 esc
= mandoc_escape(&cp
, &seq
, &ssz
);
838 if (ESCAPE_ERROR
== esc
)
845 uc
= mchars_num2uc(seq
+ 1, ssz
- 1);
847 case ESCAPE_NUMBERED
:
848 uc
= mchars_num2char(seq
, ssz
);
853 if (p
->enc
== TERMENC_ASCII
) {
854 rhs
= mchars_spec2str(seq
, ssz
, &rsz
);
858 uc
= mchars_spec2cp(seq
, ssz
);
860 sz
+= cond_width(p
, uc
, &skip
);
863 case ESCAPE_SKIPCHAR
:
866 case ESCAPE_OVERSTRIKE
:
871 mandoc_escape(&seq
, NULL
, NULL
);
874 i
= (*p
->width
)(p
, *seq
++);
885 * Common handling for Unicode and numbered
886 * character escape sequences.
890 if (p
->enc
== TERMENC_ASCII
) {
891 rhs
= ascii_uc2str(uc
);
894 if ((uc
< 0x20 && uc
!= 0x09) ||
895 (uc
> 0x7E && uc
< 0xA0))
897 sz
+= cond_width(p
, uc
, &skip
);
908 * Common handling for all escape sequences
909 * printing more than one character.
912 for (i
= 0; i
< rsz
; i
++)
913 sz
+= (*p
->width
)(p
, *rhs
++);
916 sz
+= cond_width(p
, ' ', &skip
);
920 sz
+= cond_width(p
, '-', &skip
);
932 term_vspan(const struct termp
*p
, const struct roffsu
*su
)
939 r
= su
->scale
/ 40.0;
942 r
= su
->scale
* 6.0 / 2.54;
945 r
= su
->scale
* 65536.0 / 40.0;
951 r
= su
->scale
* 0.006;
957 r
= su
->scale
/ 12.0;
969 ri
= r
> 0.0 ? r
+ 0.4995 : r
- 0.4995;
970 return ri
< 66 ? ri
: 1;
974 * Convert a scaling width to basic units, rounding towards 0.
977 term_hspan(const struct termp
*p
, const struct roffsu
*su
)
980 return (*p
->hspan
)(p
, su
);
984 * Convert a scaling width to basic units, rounding to closest.
987 term_hen(const struct termp
*p
, const struct roffsu
*su
)
991 if ((bu
= (*p
->hspan
)(p
, su
)) >= 0)
992 return (bu
+ 11) / 24;
994 return -((-bu
+ 11) / 24);