diff --git a/contrib/mandoc/Makefile b/contrib/mandoc/Makefile index 7ec34a560504..0830c9f289a3 100644 --- a/contrib/mandoc/Makefile +++ b/contrib/mandoc/Makefile @@ -1,618 +1,618 @@ # $Id: Makefile,v 1.543 2023/10/19 11:45:42 schwarze Exp $ # # Copyright (c) 2011, 2013-2022 Ingo Schwarze # Copyright (c) 2010, 2011, 2012 Kristaps Dzonsons # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -VERSION = 1.14.6s20250613 +VERSION = 1.14.6s20250727 # === LIST OF FILES ==================================================== TESTSRCS = test-attribute.c \ test-be32toh.c \ test-cmsg.c \ test-dirent-namlen.c \ test-EFTYPE.c \ test-err.c \ test-fts.c \ test-getline.c \ test-getsubopt.c \ test-isblank.c \ test-mkdtemp.c \ test-mkstemps.c \ test-nanosleep.c \ test-noop.c \ test-ntohl.c \ test-O_DIRECTORY.c \ test-ohash.c \ test-PATH_MAX.c \ test-pledge.c \ test-progname.c \ test-reallocarray.c \ test-recallocarray.c \ test-recvmsg.c \ test-rewb-bsd.c \ test-rewb-sysv.c \ test-sandbox_init.c \ test-strcasestr.c \ test-stringlist.c \ test-strlcat.c \ test-strlcpy.c \ test-strndup.c \ test-strptime.c \ test-strsep.c \ test-strtonum.c \ test-vasprintf.c \ test-wchar.c SRCS = arch.c \ att.c \ catman.c \ cgi.c \ chars.c \ compat_err.c \ compat_fts.c \ compat_getline.c \ compat_getsubopt.c \ compat_isblank.c \ compat_mkdtemp.c \ compat_mkstemps.c \ compat_ohash.c \ compat_progname.c \ compat_reallocarray.c \ compat_recallocarray.c \ compat_strcasestr.c \ compat_stringlist.c \ compat_strlcat.c \ compat_strlcpy.c \ compat_strndup.c \ compat_strsep.c \ compat_strtonum.c \ compat_vasprintf.c \ dba.c \ dba_array.c \ dba_read.c \ dba_write.c \ dbm.c \ dbm_map.c \ demandoc.c \ eqn.c \ eqn_html.c \ eqn_term.c \ html.c \ lib.c \ main.c \ man.c \ man_html.c \ man_macro.c \ man_term.c \ man_validate.c \ mandoc.c \ mandoc_aux.c \ mandoc_dbg.c \ mandoc_msg.c \ mandoc_ohash.c \ mandoc_xr.c \ mandocd.c \ mandocdb.c \ manpath.c \ mansearch.c \ mdoc.c \ mdoc_argv.c \ mdoc_html.c \ mdoc_macro.c \ mdoc_man.c \ mdoc_markdown.c \ mdoc_state.c \ mdoc_term.c \ mdoc_validate.c \ msec.c \ out.c \ preconv.c \ read.c \ roff.c \ roff_escape.c \ roff_html.c \ roff_term.c \ roff_validate.c \ soelim.c \ st.c \ tag.c \ tbl.c \ tbl_data.c \ tbl_html.c \ tbl_layout.c \ tbl_opts.c \ tbl_term.c \ term.c \ term_ascii.c \ term_ps.c \ term_tab.c \ term_tag.c \ tree.c DISTFILES = INSTALL \ LICENSE \ Makefile \ Makefile.depend \ NEWS \ TODO \ apropos.1 \ catman.8 \ cgi.h.example \ compat_fts.h \ compat_ohash.h \ compat_stringlist.h \ configure \ configure.local.example \ dba.h \ dba_array.h \ dba_write.h \ dbm.h \ dbm_map.h \ demandoc.1 \ eqn.7 \ eqn.h \ eqn_parse.h \ gmdiff \ html.h \ lib.in \ libman.h \ libmandoc.h \ libmdoc.h \ main.h \ makewhatis.8 \ man.1 \ man.7 \ man.cgi.3 \ man.cgi.8 \ man.conf.5 \ man.h \ man.options.1 \ manconf.h \ mandoc.1 \ mandoc.3 \ mandoc.css \ mandoc.db.5 \ mandoc.h \ mandoc_aux.h \ mandoc_char.7 \ mandoc_dbg.h \ mandoc_dbg_init.3 \ mandoc_escape.3 \ mandoc_headers.3 \ mandoc_html.3 \ mandoc_malloc.3 \ mandoc_ohash.h \ mandoc_parse.h \ mandoc_xr.h \ mandocd.8 \ mansearch.3 \ mansearch.h \ mchars_alloc.3 \ mdoc.7 \ mdoc.h \ msec.in \ out.h \ predefs.in \ roff.7 \ roff.h \ roff_int.h \ soelim.1 \ tag.h \ tbl.3 \ tbl.7 \ tbl.h \ tbl_int.h \ tbl_parse.h \ term.h \ term_tag.h \ $(SRCS) \ $(TESTSRCS) LIBMAN_OBJS = man.o \ man_macro.o \ man_validate.o LIBMDOC_OBJS = att.o \ lib.o \ mdoc.o \ mdoc_argv.o \ mdoc_macro.o \ mdoc_state.o \ mdoc_validate.o \ st.o LIBROFF_OBJS = eqn.o \ roff.o \ roff_escape.o \ roff_validate.o \ tbl.o \ tbl_data.o \ tbl_layout.o \ tbl_opts.o LIBMANDOC_OBJS = $(LIBMAN_OBJS) \ $(LIBMDOC_OBJS) \ $(LIBROFF_OBJS) \ $(DEBUG_OBJS) \ arch.o \ chars.o \ mandoc.o \ mandoc_aux.o \ mandoc_msg.o \ mandoc_ohash.o \ mandoc_xr.o \ msec.o \ preconv.o \ read.o \ tag.o ALL_COBJS = compat_err.o \ compat_fts.o \ compat_getline.o \ compat_getsubopt.o \ compat_isblank.o \ compat_mkdtemp.o \ compat_mkstemps.o \ compat_ohash.o \ compat_progname.o \ compat_reallocarray.o \ compat_recallocarray.o \ compat_strcasestr.o \ compat_stringlist.o \ compat_strlcat.o \ compat_strlcpy.o \ compat_strndup.o \ compat_strsep.o \ compat_strtonum.o \ compat_vasprintf.o MANDOC_HTML_OBJS = eqn_html.o \ html.o \ man_html.o \ mdoc_html.o \ roff_html.o \ tbl_html.o MANDOC_TERM_OBJS = eqn_term.o \ man_term.o \ mdoc_term.o \ roff_term.o \ term.o \ term_ascii.o \ term_ps.o \ term_tab.o \ term_tag.o \ tbl_term.o DBM_OBJS = dbm.o \ dbm_map.o \ mansearch.o DBA_OBJS = dba.o \ dba_array.o \ dba_read.o \ dba_write.o \ mandocdb.o MAIN_OBJS = $(MANDOC_HTML_OBJS) \ $(MANDOC_MAN_OBJS) \ $(MANDOC_TERM_OBJS) \ $(DBM_OBJS) \ $(DBA_OBJS) \ main.o \ manpath.o \ mdoc_man.o \ mdoc_markdown.o \ out.o \ tree.o CGI_OBJS = $(MANDOC_HTML_OBJS) \ $(DBM_OBJS) \ cgi.o \ out.o MANDOCD_OBJS = $(MANDOC_HTML_OBJS) \ $(MANDOC_TERM_OBJS) \ mandocd.o \ out.o DEMANDOC_OBJS = demandoc.o WWW_MANS = apropos.1.html \ demandoc.1.html \ man.1.html \ man.options.1.html \ mandoc.1.html \ soelim.1.html \ man.cgi.3.html \ mandoc.3.html \ mandoc_dbg_init.3.html \ mandoc_escape.3.html \ mandoc_headers.3.html \ mandoc_html.3.html \ mandoc_malloc.3.html \ mansearch.3.html \ mchars_alloc.3.html \ tbl.3.html \ man.conf.5.html \ mandoc.db.5.html \ eqn.7.html \ man.7.html \ mandoc_char.7.html \ mdoc.7.html \ roff.7.html \ tbl.7.html \ catman.8.html \ makewhatis.8.html \ man.cgi.8.html \ mandocd.8.html WWW_INCS = eqn.h.html \ html.h.html \ man.h.html \ manconf.h.html \ mandoc.h.html \ mandoc_aux.h.html \ mandoc_parse.h.html \ mansearch.h.html \ mdoc.h.html \ roff.h.html \ tbl.h.html \ tbl_int.h.html \ tbl_parse.h.html # === USER CONFIGURATION =============================================== include Makefile.local # === DEPENDENCY HANDLING ============================================== all: mandoc man demandoc soelim $(BUILD_TARGETS) Makefile.local install: base-install $(INSTALL_TARGETS) www: $(WWW_MANS) $(WWW_INCS) $(WWW_MANS) $(WWW_INCS): mandoc .PHONY: base-install cgi-install install www-install .PHONY: clean distclean depend include Makefile.depend # === TARGETS CONTAINING SHELL COMMANDS ================================ distclean: clean rm -f Makefile.local config.h config.h.old config.log config.log.old clean: rm -f libmandoc.a $(LIBMANDOC_OBJS) $(ALL_COBJS) rm -f mandoc man $(MAIN_OBJS) rm -f man.cgi $(CGI_OBJS) rm -f mandocd catman catman.o $(MANDOCD_OBJS) rm -f demandoc $(DEMANDOC_OBJS) rm -f soelim soelim.o rm -f $(WWW_MANS) $(WWW_INCS) mandoc*.tar.gz mandoc*.sha256 rm -f Makefile.tmp1 Makefile.tmp2 rm -rf *.dSYM base-install: mandoc demandoc soelim mkdir -p $(DESTDIR)$(BINDIR) mkdir -p $(DESTDIR)$(SBINDIR) mkdir -p $(DESTDIR)$(MANDIR)/man1 mkdir -p $(DESTDIR)$(MANDIR)/man5 mkdir -p $(DESTDIR)$(MANDIR)/man7 mkdir -p $(DESTDIR)$(MANDIR)/man8 mkdir -p $(DESTDIR)$(MISCDIR) $(INSTALL_PROGRAM) mandoc demandoc $(DESTDIR)$(BINDIR) $(INSTALL_PROGRAM) soelim $(DESTDIR)$(BINDIR)/$(BINM_SOELIM) cd $(DESTDIR)$(BINDIR) && $(LN) mandoc $(BINM_MAN) cd $(DESTDIR)$(BINDIR) && $(LN) mandoc $(BINM_APROPOS) cd $(DESTDIR)$(BINDIR) && $(LN) mandoc $(BINM_WHATIS) cd $(DESTDIR)$(SBINDIR) && \ $(LN) ${BIN_FROM_SBIN}/mandoc $(BINM_MAKEWHATIS) $(INSTALL_MAN) mandoc.1 demandoc.1 $(DESTDIR)$(MANDIR)/man1 $(INSTALL_MAN) soelim.1 $(DESTDIR)$(MANDIR)/man1/$(BINM_SOELIM).1 $(INSTALL_MAN) man.1 $(DESTDIR)$(MANDIR)/man1/$(BINM_MAN).1 $(INSTALL_MAN) apropos.1 $(DESTDIR)$(MANDIR)/man1/$(BINM_APROPOS).1 cd $(DESTDIR)$(MANDIR)/man1 && $(LN) $(BINM_APROPOS).1 $(BINM_WHATIS).1 $(INSTALL_MAN) man.conf.5 $(DESTDIR)$(MANDIR)/man5/$(MANM_MANCONF).5 $(INSTALL_MAN) mandoc.db.5 $(DESTDIR)$(MANDIR)/man5 $(INSTALL_MAN) man.7 $(DESTDIR)$(MANDIR)/man7/$(MANM_MAN).7 $(INSTALL_MAN) mdoc.7 $(DESTDIR)$(MANDIR)/man7/$(MANM_MDOC).7 $(INSTALL_MAN) roff.7 $(DESTDIR)$(MANDIR)/man7/$(MANM_ROFF).7 $(INSTALL_MAN) eqn.7 $(DESTDIR)$(MANDIR)/man7/$(MANM_EQN).7 $(INSTALL_MAN) tbl.7 $(DESTDIR)$(MANDIR)/man7/$(MANM_TBL).7 $(INSTALL_MAN) mandoc_char.7 $(DESTDIR)$(MANDIR)/man7 $(INSTALL_MAN) makewhatis.8 \ $(DESTDIR)$(MANDIR)/man8/$(BINM_MAKEWHATIS).8 $(INSTALL_DATA) mandoc.css $(DESTDIR)$(MISCDIR) lib-install: libmandoc.a mkdir -p $(DESTDIR)$(LIBDIR) mkdir -p $(DESTDIR)$(INCLUDEDIR) mkdir -p $(DESTDIR)$(MANDIR)/man3 $(INSTALL_LIB) libmandoc.a $(DESTDIR)$(LIBDIR) $(INSTALL_LIB) eqn.h man.h mandoc.h mandoc_aux.h mandoc_parse.h \ mdoc.h roff.h tbl.h $(DESTDIR)$(INCLUDEDIR) $(INSTALL_MAN) mandoc.3 mandoc_escape.3 mandoc_malloc.3 \ mansearch.3 mchars_alloc.3 tbl.3 $(DESTDIR)$(MANDIR)/man3 cgi-install: man.cgi mkdir -p $(DESTDIR)$(CGIBINDIR) mkdir -p $(DESTDIR)$(HTDOCDIR) $(INSTALL_PROGRAM) man.cgi $(DESTDIR)$(CGIBINDIR) $(INSTALL_DATA) mandoc.css $(DESTDIR)$(HTDOCDIR) catman-install: mandocd catman mkdir -p $(DESTDIR)$(SBINDIR) mkdir -p $(DESTDIR)$(MANDIR)/man8 $(INSTALL_PROGRAM) mandocd $(DESTDIR)$(SBINDIR) $(INSTALL_PROGRAM) catman $(DESTDIR)$(SBINDIR)/$(BINM_CATMAN) $(INSTALL_MAN) mandocd.8 $(DESTDIR)$(MANDIR)/man8 $(INSTALL_MAN) catman.8 $(DESTDIR)$(MANDIR)/man8/$(BINM_CATMAN).8 uninstall: rm -f $(DESTDIR)$(BINDIR)/mandoc rm -f $(DESTDIR)$(BINDIR)/demandoc rm -f $(DESTDIR)$(BINDIR)/$(BINM_SOELIM) rm -f $(DESTDIR)$(BINDIR)/$(BINM_MAN) rm -f $(DESTDIR)$(BINDIR)/$(BINM_APROPOS) rm -f $(DESTDIR)$(BINDIR)/$(BINM_WHATIS) rm -f $(DESTDIR)$(SBINDIR)/$(BINM_MAKEWHATIS) rm -f $(DESTDIR)$(MANDIR)/man1/mandoc.1 rm -f $(DESTDIR)$(MANDIR)/man1/demandoc.1 rm -f $(DESTDIR)$(MANDIR)/man1/$(BINM_SOELIM).1 rm -f $(DESTDIR)$(MANDIR)/man1/$(BINM_MAN).1 rm -f $(DESTDIR)$(MANDIR)/man1/$(BINM_APROPOS).1 rm -f $(DESTDIR)$(MANDIR)/man1/$(BINM_WHATIS).1 rm -f $(DESTDIR)$(MANDIR)/man5/$(MANM_MANCONF).5 rm -f $(DESTDIR)$(MANDIR)/man5/mandoc.db.5 rm -f $(DESTDIR)$(MANDIR)/man7/$(MANM_MAN).7 rm -f $(DESTDIR)$(MANDIR)/man7/$(MANM_MDOC).7 rm -f $(DESTDIR)$(MANDIR)/man7/$(MANM_ROFF).7 rm -f $(DESTDIR)$(MANDIR)/man7/$(MANM_EQN).7 rm -f $(DESTDIR)$(MANDIR)/man7/$(MANM_TBL).7 rm -f $(DESTDIR)$(MANDIR)/man7/mandoc_char.7 rm -f $(DESTDIR)$(MANDIR)/man8/$(BINM_MAKEWHATIS).8 rm -f $(DESTDIR)$(CGIBINDIR)/man.cgi rm -f $(DESTDIR)$(HTDOCDIR)/mandoc.css rm -f $(DESTDIR)$(SBINDIR)/mandocd rm -f $(DESTDIR)$(SBINDIR)/$(BINM_CATMAN) rm -f $(DESTDIR)$(MANDIR)/man8/mandocd.8 rm -f $(DESTDIR)$(MANDIR)/man8/$(BINM_CATMAN).8 rm -f $(DESTDIR)$(LIBDIR)/libmandoc.a rm -f $(DESTDIR)$(MANDIR)/man3/mandoc.3 rm -f $(DESTDIR)$(MANDIR)/man3/mandoc_escape.3 rm -f $(DESTDIR)$(MANDIR)/man3/mandoc_malloc.3 rm -f $(DESTDIR)$(MANDIR)/man3/mansearch.3 rm -f $(DESTDIR)$(MANDIR)/man3/mchars_alloc.3 rm -f $(DESTDIR)$(MANDIR)/man3/tbl.3 rm -f $(DESTDIR)$(INCLUDEDIR)/eqn.h rm -f $(DESTDIR)$(INCLUDEDIR)/man.h rm -f $(DESTDIR)$(INCLUDEDIR)/mandoc.h rm -f $(DESTDIR)$(INCLUDEDIR)/mandoc_aux.h rm -f $(DESTDIR)$(INCLUDEDIR)/mandoc_parse.h rm -f $(DESTDIR)$(INCLUDEDIR)/mdoc.h rm -f $(DESTDIR)$(INCLUDEDIR)/roff.h rm -f $(DESTDIR)$(INCLUDEDIR)/tbl.h [ ! -e $(DESTDIR)$(INCLUDEDIR) ] || rmdir $(DESTDIR)$(INCLUDEDIR) regress: all cd regress && ./regress.pl regress-clean: cd regress && ./regress.pl . clean Makefile.local config.h: configure $(TESTSRCS) @echo "$@ is out of date; please run ./configure" @exit 1 libmandoc.a: $(MANDOC_COBJS) $(LIBMANDOC_OBJS) $(AR) rs $@ $(MANDOC_COBJS) $(LIBMANDOC_OBJS) mandoc: $(MAIN_OBJS) libmandoc.a $(CC) -o $@ $(LDFLAGS) $(MAIN_OBJS) libmandoc.a $(LDADD) man: mandoc $(LN) mandoc man man.cgi: $(CGI_OBJS) libmandoc.a $(CC) $(STATIC) -o $@ $(LDFLAGS) $(CGI_OBJS) libmandoc.a $(LDADD) mandocd: $(MANDOCD_OBJS) libmandoc.a $(CC) -o $@ $(LDFLAGS) $(MANDOCD_OBJS) libmandoc.a $(LDADD) catman: catman.o libmandoc.a $(CC) -o $@ $(LDFLAGS) catman.o libmandoc.a $(LDADD) demandoc: $(DEMANDOC_OBJS) libmandoc.a $(CC) -o $@ $(LDFLAGS) $(DEMANDOC_OBJS) libmandoc.a $(LDADD) soelim: $(SOELIM_COBJS) soelim.o $(CC) -o $@ $(LDFLAGS) $(SOELIM_COBJS) soelim.o # --- maintainer targets --- www-install: www $(INSTALL_DATA) mandoc.css $(HTDOCDIR) $(INSTALL_DATA) $(WWW_MANS) $(HTDOCDIR)/man $(INSTALL_DATA) $(WWW_INCS) $(HTDOCDIR)/includes depend: config.h ./configure -depend mkdep -f Makefile.tmp1 $(CFLAGS) $(SRCS) perl -e 'undef $$/; $$_ = <>; s|/usr/include/\S+||g; \ s|\\\n||g; s| +| |g; s| $$||mg; print;' \ Makefile.tmp1 > Makefile.tmp2 rm Makefile.tmp1 mv Makefile.tmp2 Makefile.depend regress-distclean: @find regress \ -name '.#*' -o \ -name '*.orig' -o \ -name '*.rej' -o \ -name '*.core' \ -exec rm -i {} \; regress-distcheck: @find regress ! -type d ! -type f @find regress -type f \ ! -path '*/CVS/*' \ ! -name Makefile \ ! -name Makefile.inc \ ! -name '*.in' \ ! -name '*.out_ascii' \ ! -name '*.out_utf8' \ ! -name '*.out_html' \ ! -name '*.out_markdown' \ ! -name '*.out_lint' \ ! -path regress/regress.pl \ ! -path regress/regress.pl.1 dist: mandoc-$(VERSION).sha256 mandoc-$(VERSION).sha256: mandoc-$(VERSION).tar.gz sha256 mandoc-$(VERSION).tar.gz > $@ mandoc-$(VERSION).tar.gz: $(DISTFILES) ls regress/*/*/*.mandoc_* && exit 1 || true mkdir -p .dist/mandoc-$(VERSION)/ $(INSTALL) -m 0644 $(DISTFILES) .dist/mandoc-$(VERSION) cp -pR regress .dist/mandoc-$(VERSION) find .dist/mandoc-$(VERSION)/regress \ -type d -name CVS -print0 | xargs -0 rm -rf chmod 755 .dist/mandoc-$(VERSION)/configure ( cd .dist/ && tar zcf ../$@ mandoc-$(VERSION) ) rm -rf .dist/ dist-install: dist $(INSTALL_DATA) mandoc-$(VERSION).tar.gz mandoc-$(VERSION).sha256 \ $(HTDOCDIR)/snapshots # === SUFFIX RULES ===================================================== .SUFFIXES: .1 .3 .5 .7 .8 .h .SUFFIXES: .1.html .3.html .5.html .7.html .8.html .h.html .h.h.html: highlight -I $< > $@ .1.1.html .3.3.html .5.5.html .7.7.html .8.8.html: ./mandoc -Thtml -Wwarning,stop \ -O 'style=/mandoc.css,man=/man/%N.%S.html;https://man.openbsd.org/%N.%S,includes=/includes/%I.html' \ $< > $@ diff --git a/contrib/mandoc/TODO b/contrib/mandoc/TODO index 3f5a449af68f..5d582b85b154 100644 --- a/contrib/mandoc/TODO +++ b/contrib/mandoc/TODO @@ -1,809 +1,818 @@ ************************************************************************ * Official mandoc TODO. -* $Id: TODO,v 1.337 2025/04/08 21:53:14 schwarze Exp $ +* $Id: TODO,v 1.338 2025/07/22 13:36:54 schwarze Exp $ ************************************************************************ Many issues are annotated for difficulty as follows: - loc = locality of the issue * single file issue, affects file only, or very few ** single module issue, affects several files of one module *** cross-module issue, significantly impacts multiple modules and may require substantial changes to internal interfaces - exist = difficulty of the existing code in this area * affected code is straightforward and easy to read and change ** affected code is somewhat complex, but once you understand the design, not particularly difficult to understand *** affected code uses a special, exceptionally tricky design - algo = difficulty of the new algorithm to be written * the required logic and code is straightforward ** the required logic is somewhat complex and needs a careful design *** the required logic is exceptionally tricky, maybe an approach to solve that is not even known yet - size = the amount of code to be written or changed * a small number of lines (at most 100, usually much less) ** a considerable amount of code (several dozen to a few hundred) *** a large amount of code (many hundreds, maybe thousands) - imp = importance of the issue * mostly for completeness ** would be nice to have *** issue causes considerable inconvenience Obviously, as the issues have not been solved yet, these annotations are mere guesses, and some may be wrong. ************************************************************************ * assertion failures ************************************************************************ - .if n .ce in the middle of .TS data afl case f1/id:000103,sig:06,src:009024+009105,op:splice,rep:2 (jes@) While roff_parseln() prevents .ce and similar requests in the middle of a tbl, the guard is no longer effective when the .ce is wrapped in a roff block, for example a conditional. The resulting assertion has never been seen in any real-world manual page. This is too dangerous to fix before release because it requires reorganizing the very delicate internals of roff_parseln(), which risks causing more severe bugs. loc * exist *** algo *** size * imp * ************************************************************************ * bugs: invalid output ************************************************************************ - wrong number of layout columns in tbl(7) code generated by -T man https://savannah.gnu.org/bugs/?57720 The reason likely is that tbl(7) does not support the -Bl -column feature of not explicitly specifying the last table column. loc ** exist * algo ** size * imp *** - eqn(7) delimiters cause conditional lines to misbehave nabijaczleweli 8 Sep 2021 15:24:48 +0200 loc * exist *** algo *** size * imp * - roff.c, roff_expand() should not remove blanks before comments to Oliver Corff, Sep 7, 2021 loc * exist * algo * size * imp * but watch out for regressions in the high-level parsers maybe it should not even remove comments? - consider T{\" - In the body of conditional requests, escape sequence expansion must not be performed if the condition is false. This implies the first part of a request line must be expanded before request parsing (like it is now), but expansion in the second part must be delayed. to Nab 8 Aug 2023 20:05:32 +0200 Subject: if/ie d condition always true loc ** exist *** algo *** size ** imp * ************************************************************************ * missing features ************************************************************************ --- missing roff features ---------------------------------------------- - .ad (adjust margins) .ad l -- adjust left margin only (flush left) .ad r -- adjust right margin only (flush right) .ad c -- center text on line .ad b -- adjust both margins (alias: .ad n) .na -- temporarily disable adjustment without changing the mode .ad -- re-enable adjustment without changing the mode Adjustment mode is ignored while in no-fill mode (.nf). loc *** exist *** algo ** size ** imp ** (parser reorg would help) - .fc (field control) found by naddy@ in xloadimage(1) loc ** exist *** algo * size * imp * - .ns (no-space mode) occurs in xine-config(1) when implementing this, also let .TH set it reported by brad@ Sat, 15 Jan 2011 15:45:23 -0500 loc *** exist *** algo *** size ** imp * - \w'' improve width measurements would not be very useful without an expression parser, see below needed for Tcl_NewStringObj(3) via wiz@ Wed, 5 Mar 2014 22:27:43 +0100 loc ** exist *** algo *** size * imp *** - .als only works for macros in mandoc, not for user-defined strings. Also, the "val" field in struct roffkv would have to be replaced with a pointer to a reference-counted wrapper, and an alias would have to point to the same wrapper as the original. .als to undefined does nothing; the alias is not created. .rm'ing the original leaves the alias to point to the old value. .de .als .de changes both, but .de .als .rm .de only changes the new value, not the alias. Found in groffer(1) version 1.19 Jan Stary 20 Apr 2019 20:16:54 +0200 loc * exist ** algo ** size ** imp * - roff string condition comparisons fail when vars contain quotes: .ds s ' .if '\*s'' \&... hard to fix because of the basic architecture (string replacement happens before roff(7) syntax parsing) Found in groffer(1) version 1.19 Jan Stary 20 Apr 2019 20:16:54 +0200 loc * exist *** algo *** size ** imp * - mandoc replaces all ASCII control characters except tab and line feed with '?' during input. It would be better to replace them with Unicode escapes in preconv_encode() or somewhere in the vicinity, such that the already existing better replacement strings show up in the output. Emulating groff is not desirable: groff replaces 0x00, 0x0b, and 0x0d to 0x1f with the empty string (bad because that's easy to overlook for the document author), 0x01 with '.' (very confusing), and passes through 0x02 to 0x08, 0x0c, and 0x7f raw (bad because that is insecure output). Remember that 0x07 may need special handling because it is sometimes used for certain delimiters, so it may need handling *after* roff.c rather than before. reminded by John Gardner 16 Jun 2020 14:26:28 +1000 Actually, more ASCII control characters than just 0x07 may need later handling because they can for example be used in macro names. So they may need handling after roff(7) processing. pointed out by John Gardner 23 Jun 2020 18:28:08 +1000 more info from John Gardner 29 Jun 2020 19:54:04 +1000 loc ** exist ** algo ** size ** imp * - many missing features used in old groff_char(7), some can possibly be supported kamil at netbsd 12 Nov 2020 17:27:09 +0100 + reply - \s with arbitrary arg delimiters as already supported for other escapes found following jmc@'s mail 28 Apr 2021 18:31:41 +0100 loc * exist * algo * size * imp * --- missing mdoc features ---------------------------------------------- - support mixed case for section names also, first section is not "NAME" should not appear more than once per page Alejandro Colomar 28 Apr 2023 16:57:49 +0200 loc * exist * algo * size * imp *** - .Sh and .Ss should be parsed and partially callable, see groff_mdoc(7) reed at reedmedia dot net Sat, 21 Dec 2019 17:13:07 -0600 loc ** exist ** algo ** size ** imp * - .Bl -column .Xo support is missing ultimate goal: restore .Xr and .Dv to lib/libc/compat-43/sigvec.3 lib/libc/gen/signal.3 lib/libc/sys/sigaction.2 loc * exist *** algo *** size * imp ** - edge case: decide how to deal with blk_full bad nesting, e.g. .Sh .Nm .Bk .Nm .Ek .Sh found by jmc@ in ssh-keygen(1) from jmc@ Wed, 14 Jul 2010 18:10:32 +0100 loc * exist *** algo *** size ** imp ** - .Bd -filled should not be the same as .Bd -ragged, but align both the left and right margin. In groff, it is implemented in terms of .ad b, which we don't have either. Found in cksum(1). loc *** exist *** algo ** size ** imp ** (parser reorg would help) - implement blank `Bl -column', such as .Bl -column .It foo Ta bar .El loc * exist *** algo *** size * imp * - explicitly disallow nested `Bl -column', which would clobber internal flags defined for struct mdoc_macro loc * exist * algo * size * imp ** - In .Bl -column .It, the end of the line probably has to be regarded as an implicit .Ta, if there could be one, see the following mildly ugly code from login.conf(5): .Bl -column minpasswordlen program xetcxmotd .It path Ta path Ta value of Dv _PATH_DEFPATH .br Default search path. reported by Michal Mazurek via jmc@ Thu, 7 Apr 2011 16:00:53 +0059 loc * exist *** algo ** size * imp ** - inside `.Bl -column' phrases, punctuation is handled like normal text, e.g. `.Bl -column .It Fl x . Ta ...' should give "-x -." - inside `.Bl -column' phrases, TERMP_IGNDELIM handling by `Pf' is not safe, e.g. `.Bl -column .It Pf a b .' gives "ab." but should give "ab ." - prohibit `Nm' from having non-text HEAD children (e.g., NetBSD mDNSShared/dns-sd.1) (mdoc_html.c and mdoc_term.c `Nm' handlers can be slightly simplified) - support translated section names e.g. x11/scrotwm scrotwm_es.1:21:2: error: NAME section must be first that one uses NOMBRE because it is spanish... deraadt tends to think that section-dependent macro behaviour is a bad idea in the first place, so this may be irrelevant loc ** exist ** algo ** size * imp ** - When there is free text in the SYNOPSIS and that free text contains the .Nm macro, groff somehow understands to treat the .Nm as an in-line macro, while mandoc treats it as a block macro and breaks the line. No idea how the logic for distinguishing in-line and block instances should be, needs investigation. uqs@ Thu, 2 Jun 2011 11:03:51 +0200 uqs@ Thu, 2 Jun 2011 11:33:35 +0200 loc * exist ** algo *** size * imp ** --- missing man features ----------------------------------------------- - When calling less(1), specify -P to print "name(sec) lines ..." in the bottom line instead of "/tmp/man..." Jan Engelhardt (SuSE) via Matej Cepl 06 Apr 2025 14:42:52 +0200 loc * exist * algo * size * imp ** - MANWIDTH Markus Waldeck 9 Jun 2015 05:49:56 +0200 Laura Morales 26 Apr 2018 08:15:55 +0200 Kamil Rytarowski 13 Nov 2020 00:19:36 +0100 patch from Kamil 13 Nov 2020 22:37:07 +0100 loc * exist * algo * size * imp * - groff_www(7) .MTO and .URL These macros were used by the GNU grep(1) man page. The groff_www(7) manual page itself uses them, too. We should probably *not* add them to mandoc. Just mentioning this here to keep track of the abuse. Laura Morales 20 Apr 2018 07:33:02 +0200 loc ** exist * algo * size ** imp * --- missing tbl features ----------------------------------------------- - vertical centering in cells vertically spanned with ^ pali dot rohar at gmail dot com 16 Jul 2018 13:03:35 +0200 loc * exist *** algo *** size ** imp * - support mdoc(7) and man(7) macros inside tbl(7) code; probably requires the parser reorg and letting tbl(7) use roff_node such that macro sets can mix; informed by bapt@ that FreeBSD needs this: 3 Jan 2015 23:32:23 +0100 loc *** exist ** algo *** size ** imp *** - look at the POSIX manuals in the books/man-pages-posix port, they use some unsupported tbl(7) features, mostly macros in tbl(7). loc * exist ** algo ** size ** imp *** - look what Joerg Schilling manual pages use Thu, 19 Mar 2015 18:31:48 +0100 --- missing eqn features ----------------------------------------------- - In a matrix, break the output line after each matrix line. Found in the discussion at CDBUG 2015. Suggested by Avi Weinstock. This may not be the ideal solution after all: eqn(7) matrices are lists of columns, so Avi's proposal would show each *column* on its own *line*, which is likely to cause confusion. A better solution, but much harder to implement, would be to actually show the coordinates of column vectors on different terminal output lines, using the clumnated output facilities developed for .Bl -tag, .Bl -column, and also used for tbl(7). loc * exist * algo ** size ** imp ** - The "size" keyword is parsed, but ignored by the formatter. loc * exist * algo * size * imp * - The spacing characters `~', `^', and tab are currently ignored, see User's Guide (Second Edition) page 2 section 4. loc * exist * algo ** size * imp ** - Mark and lineup are parsed and ignored, see User's Guide (Second Edition) page 5 section 15. loc ** exist ** algo ** size ** imp ** - GNU eqn converts some operators to special characters, for example, input HYPHEN-MINUS becomes output \(mi, unless it is part of a quoted word. mandoc(1) only does this when the operator is surrounded by blanks, not when it is part of an unquoted word. Also, check whether there are more such cases (e.g., +?). reported by bentley@ 20 Jun 2017 02:04:29 -0600 loc * exist ** algo ** size * imp * - Primes, opprime, and ' bentley@ Thu, 13 Jul 2017 23:14:20 -0600 --- missing misc features ---------------------------------------------- - use the default volume headers for sections with suffixes certainly affects man(7); possibly mdoc(7)?; and also groff(1) Alejandro Colomar 21 Aug 2022 - consider whether man(1) fallback code in main.c/fs_*() can find files like man3c/fopen.3c (illumos, Solaris) and man3p/fopen.3p (POSIX) discussed with Robert Mustacchi 21 Sep 2021 10:39:40 -0700 loc * exist * algo ** size * imp ** - let makewhatis(8) follow symbolic links to dirs below READ_ALLOWED_PATH this may be feasible using fts_set(FTS_FOLLOW) mail to sternenseemann 19 Aug 2021 19:11:50 +0200 loc * exist ** algo ** size * imp ** - handle Unicode letters in tags in both HTML and terminal output thread "section headers with diacritics" starting with Mario Blaettermann 24 Mar 2022 18:13:23 +0100 loc ** exist * algo * size * imp ** - -T man does not handle eqn(7) and tbl(7) Stephen Gregoratto 16 Feb 2020 01:28:07 +1100 also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=901636 loc ** exist ** algo ** size *** imp ** - man -ks 1,8 route; kn@ Jul 13, 2018 orally - italic correction (\/) in PostScript mode Werner LEMBERG on groff at gnu dot org Sun, 10 Nov 2013 12:47:46 loc ** exist ** algo * size * imp * - change the default PAGER to more -Es and use the pager even for apropos title line output; req by bapt@ loc * exist * algo * size * imp *** - clean up escape sequence handling, creating three classes: (1) fully implemented, or parsed and ignored without loss of content (2) unimplemented, potentially causing loss of content or serious mangling of formatting (e.g. \n) -> ERROR see textproc/mgdiff(1) for nice examples (3) undefined, just output the character -> perhaps WARNING loc *** exist ** algo ** size ** imp *** (parser reorg helps) - man.conf(5) alias aliasname dirname or just -Mb -Mx -Mp mail to jmc@ Mar 23, 2015 03:53:14PM +0100 loc * exist * algo * size * imp ** - kettenis wants base roff, ms, and me Fri, 1 Jan 2010 22:13:15 +0100 (CET) loc ** exist ** algo ** size *** imp * --- compatibility checks ----------------------------------------------- - is .Bk implemented correctly in modern groff? sobrado@ Tue, 19 Apr 2011 22:12:55 +0200 - compare output to Heirloom roff, Solaris roff, and http://repo.or.cz/w/neatroff.git http://litcave.rudi.ir/ - look at AT&T DWB http://www2.research.att.com/sw/download Carsten Kunze has patches Mon, 4 Aug 2014 17:01:28 +0200 ported version: https://github.com/n-t-roff/DWB3.3 Carsten Kunze Wed, 22 Apr 2015 11:21:43 +0200 - look at pages generated from reStructeredText, e.g. devel/mercurial hg(1) These are a weird mixture of man(7) and custom autogenerated low-level roff stuff. Figure out to what extent we can cope. For details, see http://docutils.sourceforge.net/rst.html noted by stsp@ Sat, 24 Apr 2010 09:17:55 +0200 reminded by nicm@ Mon, 3 May 2010 09:52:41 +0100 - look at pages generated from ronn(1) github.com/rtomayko/ronn (based on markdown) - look at pages generated from Texinfo source by yat2m, e.g. security/gnupg First impression is not that bad. - look at pages generated by pandoc; see https://github.com/jgm/pandoc/blob/master/src/Text/Pandoc/Writers/Man.hs porting planned by kili@ Thu, 19 Jun 2014 19:46:28 +0200 - check compatibility with Plan9: http://swtch.com/usr/local/plan9/tmac/tmac.an http://swtch.com/plan9port/man/man7/man.html "Anthony J. Bentley" 28 Dec 2010 21:58:40 -0700 - check compatibility with COHERENT troff: http://www.nesssoftware.com/home/mwc/source.php - check compatibility with the man(7) formatter https://raw.githubusercontent.com/rofl0r/hardcore-utils/master/man.c - check compatibility with http://ikiwiki.info/plugins/contrib/mandoc/ https://github.com/schmonz/ikiwiki/compare/mandoc Amitai Schlair Mon, 19 May 2014 14:05:53 -0400 - check compatibility with https://git.sr.ht/~sircmpwn/scdoc - check features of the Slackware man.conf(5) format Carsten Kunze Wed, 11 Mar 2015 17:57:24 +0100 - look at http://www.snake.net/software/troffcvt/ (troff to HTML) mentioned by Oliver Corff 22 Jan 2021 01:36:49 +0100 ************************************************************************ * formatting issues: ugly output ************************************************************************ - revisit empty in-line macros look at the difference between "Em x Em ." and "Sq x Em ." Carsten Kunze Fri, 12 Dec 2014 00:15:41 +0100 loc *** exist *** algo *** size * imp ** - a column list with blank `Ta' cells triggers a spurious start-with-whitespace printing of a newline - In .Bl -column, .It a"bc" shows the quotes in groff, but not in mandoc loc * exist *** algo ** size * imp ** - In .Bl -column, .It Em AuthenticationKey Length ought to render "Key Length" with emphasis, too, see OpenBSD iked.conf(5). reported again Nicolas Joly via wiz@ Wed, 12 Oct 2011 00:20:00 +0200 loc * exist *** algo *** size ** imp *** - empty phrases in .Bl column produce too few blanks try e.g. .Bl -column It Ta Ta reported by millert Fri, 02 Apr 2010 16:13:46 -0400 loc * exist *** algo *** size * imp ** - .%T can have trailing punctuation. Currently, it puts the trailing punctuation into a trailing MDOC_TEXT element inside its own scope. That element should rather be outside its scope, such that the punctuation does not get underlines. This is not trivial to implement because .%T then needs some features of in_line_eoln() - slurp all arguments into one single text element - and one feature of in_line() - put trailing punctuation out of scope. Found in mount_nfs(8) and exports(5), search for "Appendix". loc ** exist ** algo *** size * imp ** - Trailing punctuation after .%T triggers EOS spacing, at least outside .Rs (eek!). Simply setting ARGSFL_DELIM for .%T is not the right solution, it sends mandoc into an endless loop. reported by Nicolas Joly Sat, 17 Nov 2012 11:49:54 +0100 loc * exist ** algo ** size * imp ** - global variables in the SYNOPSIS of section 3 pages .Vt vs .Vt/.Va vs .Ft/.Va vs .Ft/.Fa ... from kristaps@ Tue, 08 Jun 2010 11:13:32 +0200 - implicit whitespace around inline equations example code: where '$times$' denotes matrix multiplication must not have an HTML line break, nor a blank, before partial solution: html.c {"math", HTML_NLINSIDE | HTML_INDENT}, bentley@ Thu, 13 Jul 2017 19:00:59 -0600 - in enclosures, mandoc sometimes fancies a bogus end of sentence reminded by jmc@ Thu, 23 Sep 2010 18:13:39 +0059 loc * exist ** algo *** size * imp *** - the man(7) single-font macros (e.g. .B) use .itc, so ".B foo\c" followed by "bar" prints "bar" in bold gbranden@ Sun, 5 Jun 2022 18:08:46 -0500 - a line starting with "\fB something" counts as starting with whitespace and triggers a line break; found in audio/normalize-mp3(1) This will become easier once escape sequences are represented by syntax tree nodes. loc ** exist * algo ** size * imp ** - formatting /usr/local/man/man1/latex2man.1 with groff and mandoc reveals lots of bugs both in groff and mandoc... reported by bentley@ Wed, 22 May 2013 23:49:30 -0600 - Make an underlined blank an underscore rather than a blank in both groff and mandoc terminal output (likely tricky, needs investigation) job@ 21 Jan 2025 18:03:52 +0000 --- PostScript and PDF issues ------------------------------------------ - PDF output doesn't use a monospaced font for .Bd -literal Example: "mandoc -Tpdf afterboot.8 > output.pdf && pdfviewer output.pdf". Search the text "Routing tables". Also check what PostScript mode does when fixing this. reported by juanfra@ Wed, 04 Jun 2014 21:44:58 +0200 instructions from juanfra@ Wed, 11 Jun 2014 02:21:01 +0200 add a new <> block to the PDF files with /BaseFont /Courier and change the /Name from /F0 to the new font (/F5 (?)). re-reported by tb@ Mon, 16 Mar 2015 16:47:21 +0100 loc ** exist ** algo ** size * imp ** +- Check for bad line breaks caused by PostScript and PDF using variable- + width fonts, for example in .Bl -width "string". The difficulty line + below describes a naive solution by simply scaling up widths internally + or adding default spacing (like in terminal output). If fixes are + needed in width measurements, it might be a bit harder, but likely + not unreasonably so. + reported by Jan Stary 20 May 2024 10:19:01 +0200 + loc * exist * algo ** size * imp ** + --- HTML issues -------------------------------------------------------- - support the idiom .TP .IP .TP for multi-paragraph list item bodies to: Alejandro Colomar Thu, 19 Oct 2023 16:45:21 +0200 loc ** exist ** algo ** size ** imp ** - .Nm without an argument and .Bx cause premature Nab Sun, 5 Jun 2022 18:30:09 +0200 - .Aq Mt could set and reset "white-space: nowrap"; Check whether other enclosure macros could profit from similar handling, or whether that is covered by Unicode line-breaking classes WJ, ZW, GL, ZWJ. John Gardner 25 Mar 2022 04:44:27 +1100 - make the HTML scaffolding customizable with -O skip=... mail to Oliver Corff 3 Jun 2021 17:28:02 +0200 more feedback from Oliver 3 Jun 2021 18:27:56 +0200 more feedback from Oliver 3 Jun 2021 23:37:18 +0200 would also be useful for https://github.com/gbdev/rgbds-www/blob/master/ maintainer/support/man_postproc.awk - .Bd -unfilled should not use monospaced font anton@ 4 Mar 2021 08:19:35 +0100 loc ** exist * algo * size * imp ** - HTML formatting of .nf should avoid
, even when input lines start with whitespace, and not close and re-open
 on .P
   my mail to ports@ 27 Jun 2021 16:09:20 +0200
   reported again by Mohamed Akram 25 Jun 2022 16:28:18 +0000
   loc **  exist **  algo *  size *  imp **
 
 - tbl(7) HTML output does not implement column width specifications
   reported by Ted Bullock 11 Jan 2022 16:00:44 -0700
   loc *  exist *  algo ?  size ?  imp *
 
 - link from flags in the SYNOPSIS to their descriptions
   https://github.com/gbdev/rgbds-www/blob/master/
   maintainer/support/man_postproc.awk
   loc *  exist *  algo **  size *  imp *
 
 - get rid of the last handful of style= attributes such that
   Content-Security-Policy: can be enabled without unsafe-inline
   suggested by bentley@  Nov 10, 2019 at 06:02:49AM -0700
   loc *  exist *  algo *  size *  imp **
 
 - .Bf at the beginning of a paragraph inserts a bogus 1ex horizontal
   space, see for example random(3).  Introduced in
   http://mdocml.bsd.lv/cgi-bin/cvsweb/mdoc_html.c.diff?r1=1.91&r2=1.92
   reported by deraadt@ Mon, 28 Sep 2015 20:14:13 -0600 (MDT)
   loc **  exist **  algo **  size *  imp *
 
 - jsg on icb, Nov 3, 2014:
   try to guess Xr in man(7) for hyperlinking
   and render them with 
   https://github.com/Debian/debiman/issues/15
   loc *  exist *  algo **  size **  imp **
 
 - space characters can end up in href= attributes, for example coming
   from the first .Xr argument (where they make no sense, but still);
   does this affect other characters, other source macros...?
   Jackson Pauls  29 Aug 2017 16:56:27 +0100
 
 - generate  tags in HTML
   idea from florian@  Tue, 7 Apr 2015 00:26:28 +0000
   may be possible to implement with .Lk img://something.png alt_text
 
 - check https://github.com/trentm/mdocml
 
 --- CSS issues ---------------------------------------------------------
 
 - use flexbox for .Bl-tag instead of the fragile float/clear mechanism
   John Gardner 25 Mar 2022 04:44:27 +1100
 
 
 ************************************************************************
 * formatting issues: gratuitous differences
 ************************************************************************
 
 - .Fn reopens a new scope after punctuation in mandoc,
   but closes its scope for good in groff.
   Do we want to change mandoc or groff?
   Steffen Nurpmeso  Sat, 08 Nov 2014 13:34:59 +0100
   loc *  exist **  algo **  size *  imp **
 
 - Multiple issues with .In below SYNOPSIS; groff behaviour is:
   text line + .In -> no line break before #include
   called .In -> no line break before angle bracket
   .In + .In -> second one gets #include, too
   two arguments -> line break before second
   child macro -> line break before child
   .In + text line -> line break before the text line
   Evan Silberman  Fri, 20 Sep 2024 16:48:19 -0700
   loc **  exist **  algo *  size *  imp *
 
 - In .Bl -enum -width 0n, groff continues on the same line after
   the number, mandoc breaks the line.
   mail to kristaps@  Mon, 20 Jul 2009 02:21:39 +0200
   loc *  exist **  algo **  size *  imp **
 
 - .Pp between two .It in .Bl -column should produce one,
   not two blank lines, see e.g. login.conf(5).
   reported by jmc@  Sun, 17 Apr 2011 14:04:58 +0059
   reported again by sthen@  Wed, 18 Jan 2012 02:09:39 +0000 (UTC)
   loc *  exist ***  algo **  size *  imp **
 
 - If the *first* line after .It is .Pp, break the line right after
   the tag, do not pad with space characters before breaking.
   See the description of the a, c, and i commands in sed(1).
   loc *  exist **  algo **  size *  imp **
 
 - If the first line after .It is .D1, do not assert a blank line
   in between, see for example tmux(1).
   reported by nicm@  13 Jan 2011 00:18:57 +0000
   loc *  exist **  algo **  size *  imp **
 
 - Trailing punctuation after .It should trigger EOS spacing.
   reported by Nicolas Joly  Sat, 17 Nov 2012 11:49:54 +0100
   Probably, this should be fixed somewhere in termp_it_pre(), not sure.
   loc *  exist **  algo **  size *  imp **
 
 - When the -width string contains macros, the macros must be rendered
   before measuring the width, for example
     .Bl -tag -width ".Dv message"
   in magic(5), located in src/usr.bin/file, is the same
   as -width 7n, not -width 11n.
   The same applies to .Bl -column column widths;
   reported again by Nicolas Joly Thu, 1 Mar 2012 13:41:26 +0100 via wiz@ 5 Mar
   reported again by Franco Fichtner Fri, 27 Sep 2013 21:02:28 +0200
   reported again by Bruce Evans Fri, 17 Feb 2017 21:22:44 +0100 via bapt@
   https://reviews.freebsd.org/D35245
   even groff_mdoc(7) uses this: Nab Sun, 5 Jun 2022 22:16:37 +0200
   When implementing this, try to avoid breaking existing manuals,
   or at least fix them: Jan Stary Sun, 5 Jun 2022 22:48:05 +0200
   loc ***  exist ***  algo ***  size **  imp ***
   An easy partial fix has been implemented as skip_leading_dot_word().
 
 - The \& zero-width character counts as output.
   That is, when it is alone on a line between two .Pp,
   we want three blank lines, not two as in mandoc.
   loc **  exist **  algo **  size *  imp **
 
 - Sequences of multiple man(7) paragraphs (.PP, .IP) interspersed
   with .ps and .nf/.fi produce execessive blank lines, see libJudy
   and graphics/dcmtk.  The parser reorg may help with this.
 
 - The man(7) .UR macro produces UTF-8 angle brackets in -Tutf8 output mode
   with groff, but ASCII <> with mandoc
   Alejandro Colomar Mon, 7 Aug 2023 17:13:29 +0200 Subject: hostname
 
 - trailing whitespace must be ignored even when followed by a font escape,
   see for example
     makes
     \fBdig \fR
     operate in batch mode
   in dig(1).
   loc **  exist **  algo **  size *  imp **
 
 ************************************************************************
 * warning issues
 ************************************************************************
 
 - shorten/simplify error messages for usage errors
   To: deraadt@ 25 Oct 2020 23:37:01 +0100
   loc **  exist *  algo *  size **  imp ***
 
 - warn about \\ and \. in interpretation mode
   gbranden@, groff issue #62776, 10 Nov 2023 01:57:32 -0500
 
 - warn about output lines exceeding 80 characters
   Alejandro Colomar Aug 22, 2022
   not trivial because -T lint does not call any formatter
   loc ***  exist *  algo **  size **  imp **
 
 - warn about duplicate .Sh/.Ss heads
   gre(4): Rename duplicate sections 20 Apr 2018 15:27:33 +0200
   loc *  exist *  algo *  size *  imp **
 
 - style message about macros inside .Bd -literal and .Dl, in particular
   font changing macros like .Cm, .Ar, .Fa (from the mdoclint TODO)
 
 - style message about mismatches between the section number in the
   file name (if it is known) and the section number in .Dt
   (from the mdoclint TODO)
 
 - style message about NULL without .Dv (from the mdoclint TODO)
 
 - style message about error constants without .Er (from the mdoclint TODO)
 
 - warn when .Sh or .Ss contain other macros
   Steffen Nurpmeso, savannah.gnu.org/bugs/index.php?45034
   loc *  exist *  algo *  size *  imp **
 
 - style message about violations of the convention
   .An name Aq Mt localpart@domain in AUTHORS (from the mdoclint TODO)
 
 - warn about attempts to call non-callable macros
   Steffen Nurpmeso  Tue, 11 Nov 2014 22:55:16 +0100
   Note that formatting is inconsistent in groff.
   .Fn Po prints "Po()", .Ar Sh prints "file ..." and no "Sh".
   Relatively hard because the relevant code is scattered
   all over mdoc_macro.c and all subtly different.
   loc **  exist **  algo **  size **  imp **
 
 - warn about punctuation - e.g. ',' and ';' - at the beginning
   of a text line, if it is likely intended to follow the preceding
   output without intervening whitespace, in particular after a
   macro line (from the mdoclint TODO)
 
 - report double .TH in man(7) as an ERROR and let the first win
   kristaps@  28 Mar 2021 13:30:41 +0200
   loc *  exist *  algo *  size *  imp *
 
 - makewhatis -p complains about language subdirectories:
   /usr/local/man//ru: Unknown directory part
 
 
 ************************************************************************
 * documentation issues
 ************************************************************************
 
 - mark macros as: page structure domain, manual domain, general text domain
   is this useful?
 
 - mention /usr/share/misc/mdoc.template in mdoc(7)?
 
 - Is all the content from http://www.std.com/obi/BSD/doc/usd/28.tbl/tbl
   covered in tbl(7)?
 
 ************************************************************************
 * performance issues
 ************************************************************************
 
 - the PDF file is HUGE: this can be reduced by using relative offsets
 
 ************************************************************************
 * structural issues
 ************************************************************************
 
 - POSIX says in the documentation of sysconf(3) that PATH_MAX
   is allowed to be so large that it is a bad idea to use it
   for sizing static buffers.  So use dynamic buffers throughout.
   See the file test-PATH_MAX.c for details.
   Found by Aaron M. Ucko in the GNU Hurd via Bdale Garbee,
   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=829624
 
 - Is it possible to further simplify ENDBODY_SPACE?
 
 - Find better ways to prevent endless loops
   in roff(7) macro and string expansion.
 
 - make buffers for parsing functions const
   christos@ via wiz@  Fri, 18 Dec 2015 17:10:01 +0100
 
 - struct mparse refactoring
   Steffen Nurpmeso  Thu, 04 Sep 2014 12:50:00 +0200
 
 ************************************************************************
 * CGI issues
 ************************************************************************
 
  - Inspect httpd(8) logs on man.openbsd.org and consider
    whether logging can be improved, where bad syntax comes from,
    and what needs to be done to get rid of COMPAT_OLDURI.
  - Enable HTTP compression by detecting gzip encoding and filtering
    output through libz.
  - Privilege separation (see OpenSSH).
  - Enable caching support via HTTP 304 and If-Modified-Since.
 
 ************************************************************************
 * to improve in the groff_mdoc(7) macros
 ************************************************************************
 
 - delete OS release verification from .Dx, .Fx, .Nx, .Ox etc.
   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=629161
   also Branden Robinson 18 Dec 2019 00:59:52 +1100
 
 - Can the distinction between .Vt and .Va be made stricter,
   recommending .Vt extern char * Ns Va optarg ; ?
   What about the block macro properties of .Vt in the SYNOPSIS?
   zeurkous 25 Dec 2019 08:48:36 +0100
 
 - .Cd # arch1, arch2 in section 4 pages:
   find better way to indicate multiple architectures, maybe:
   allow .Dt vgafb 4 "macppc sparc64"
   already shown as "Device Drivers Manual (macppc sparc64)"
   for apropos, make that "vgafb(4) - macppc # sparc64" instead of "- all"
   groff can be made to show multiple arches, too, but it is
   tedious to do the string parsing in roff code...
   jmc@ 23 Apr 2018 07:24:52 +0100 [man for vgafb(4)...]
   loc **  exist **  algo *  size *  imp ***
 
 - use uname(1) to set doc-default-operating-system at install time
   tobimensch  Mon, 1 Dec 2014 00:25:07 +0100
 
 - apostrophe (39), circumflex (94), grave (96), tilde (126)
   in manuals: \(aq, \(ha, \`, \(ti
   Re: [Groff] ASCII Minus Sign in man Pages.
   bentley@ 26 Apr 2017 10:02:06 -0600
   Do we need to fix existing manuals?
   Do we need to fix the definition of the mdoc(7) language?
diff --git a/contrib/mandoc/catman.8 b/contrib/mandoc/catman.8
index 903fa1fa82c9..c0f14872afc6 100644
--- a/contrib/mandoc/catman.8
+++ b/contrib/mandoc/catman.8
@@ -1,186 +1,374 @@
-.\"	$Id: catman.8,v 1.8 2017/03/18 19:56:01 schwarze Exp $
+.\" $Id: catman.8,v 1.15 2025/07/13 14:15:26 schwarze Exp $
 .\"
-.\" Copyright (c) 2017 Ingo Schwarze 
+.\" Copyright (c) 2017, 2025 Ingo Schwarze 
 .\"
 .\" Permission to use, copy, modify, and distribute this software for any
 .\" purpose with or without fee is hereby granted, provided that the above
 .\" copyright notice and this permission notice appear in all copies.
 .\"
 .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\"
-.Dd $Mdocdate: March 18 2017 $
+.Dd $Mdocdate: July 13 2025 $
 .Dt CATMAN 8
 .Os
 .Sh NAME
 .Nm catman
 .Nd format all manual pages below a directory
 .Sh SYNOPSIS
 .Nm catman
 .Op Fl I Cm os Ns = Ns Ar name
 .Op Fl T Ar output
 .Ar srcdir dstdir
 .Sh DESCRIPTION
 The
 .Nm
 utility assumes that all files below
 .Ar srcdir
 are manual pages in
 .Xr mdoc 7
 and
 .Xr man 7
 format and formats all of them, storing the formatted versions in
 the same relative paths below
 .Ar dstdir .
-Subdirectories of
+Unless they already exist,
 .Ar dstdir
-are created as needed.
+itself and any required subdirectories are created.
 Existing files are not explicitly deleted, but possibly overwritten.
 .Pp
 The options are as follows:
 .Bl -tag -width Ds
 .It Fl I Cm os Ns = Ns Ar name
 Override the default operating system
 .Ar name
 for the
 .Xr mdoc 7
 .Ic \&Os
 and for the
 .Xr man 7
 .Ic TH
 macro.
 .It Fl T Ar output
 Output format.
 The
 .Ar output
 argument can be
 .Cm ascii ,
 .Cm utf8 ,
 or
 .Cm html ;
 see
 .Xr mandoc 1 .
 In
 .Cm html
 output mode, the
 .Cm fragment
 output option is implied.
 Other output options are not supported.
+.It Fl v
+Verbose mode, printing additional information to standard error output.
+Specifying this once prints a summary about the number of files
+and directories processed at the end of the iteration.
+Specifying it twice additionally prints debugging information
+about the backchannel from
+.Xr mandocd 8
+to
+.Nm
+that is used to limit the number of files in flight at any given time.
+For details, see
+.Sx DIAGNOSTICS .
 .El
 .Sh IMPLEMENTATION NOTES
 Since this version avoids
 .Xr fork 2
 and
 .Xr exec 3
 overhead and uses the much faster
 .Sy mandoc
 parsers and formatters rather than
 .Sy groff ,
 it may be about one order of magnitude faster than other
 .Nm
 implementations.
 .Sh EXIT STATUS
 .Ex -std
 .Pp
-Possible errors include:
-.Bl -bullet
-.It
-missing, invalid, or excessive command line arguments
-.It
-failure to change the current working directory to
+Failures while trying to open individual manual pages for reading,
+to save individual formatted files to the file system,
+or even to read or create subdirectories do not cause
+.Nm
+to return an error exit status.
+In such cases,
+.Nm
+simply continues with the next file or subdirectory.
+.Sh DIAGNOSTICS
+Some fatal errors cause
+.Nm
+to exit before the iteration over input files is even started:
+.Bl -tag -width Ds -offset indent
+.It unknown option \-\- Ar option
+An invalid option was passed on the command line.
+.It missing arguments: srcdir and dstdir
+No argument was provided.
+Both
 .Ar srcdir
-.It
-failure to open
+and
 .Ar dstdir
-.It
-communication failure with
+are mandatory.
+.It missing argument: dstdir
+Only one argument was provided.
+The second argument,
+.Ar dstdir ,
+is mandatory, too.
+.It too many arguments: Ar third argument
+Three or more arguments were provided, but only two are supported.
+.It Sy socketpair : Ar reason
+The sockets needed for communication with
+.Xr mandocd 8
+could not be created, for example due to file descriptor or memory exhaustion.
+.It Sy fork : Ar reason
+The new process needed to run
 .Xr mandocd 8
-.It
-resource exhaustion, for example file descriptor, process table,
-or memory exhaustion
+could not be created, for example due to process table exhaustion
+or system resource limits.
+.It Sy exec Ns Po Sy mandocd Pc : Ar reason
+The
+.Xr mandocd 8
+child program could not be started, for example because it is not in the
+.Ev PATH
+or has no execute permission.
+.It Sy mkdir No destination Ar dstdir : reason
+The
+.Ar dstdir
+does not exist and could not be created, for example because
+the parent directory does not exist or permission is denied.
+.It Sy open No destination Ar dstdir : reason
+The
+.Ar dstdir
+could not be opened for reading, for example because
+it is not a directory or permission is denied.
+.It Sy chdir No to source Ar srcdir : reason
+The current working directory could not be changed to
+.Ar srcdir ,
+for example because it does not exist, it is not a directory,
+or permission is denied.
+.It Sy fts_open : Ar reason
+Starting the iteration was attempted but failed,
+for example due to memory exhaustion.
 .El
 .Pp
-Except for memory exhaustion and similar system-level failures,
-failures while trying to open, read, parse, or format individual
-manual pages, to save individual formatted files to the file system,
-or even to create directories do not cause
+Some fatal errors cause the iteration over input files to be aborted
+prematurely:
+.Bl -tag -width Ds -offset indent
+.It FATAL: Sy fts_read : Ar reason
+A call to
+.Xr fts_read 3
+returned
+.Dv NULL ,
+meaning that the iteration failed before being complete.
+.It FATAL: mandocd child died: got Ar SIGNAME
+This message appears if
 .Nm
-to return an error exit status.
-In such cases,
+gets the
+.Dv SIGCHLD
+or
+.Dv SIGPIPE
+signal, most likely due to a fatal bug in
+.Xr mandocd 8 .
+.It FATAL: Sy sendmsg : Ar reason
+The file descriptors needed to process one of the manual pages
+could not be sent to
+.Xr mandocd 8 ,
+for example because
+.Xr mandocd 8
+could not be started or died unexpectedly.
+.It FATAL: Sy recv : Ar reason
+Trying to read a reply message from
+.Xr mandocd 8
+failed, most likely because
+.Xr mandocd 8
+unexpectedly died or closed the socket.
+.It FATAL: signal Ar SIGNAME
+This message appears if
+.Nm
+gets a
+.Dv SIGHUP ,
+.Dv SIGINT ,
+or
+.Dv SIGTERM
+signal, for example because the user deliberately killed it.
+.El
+.Pp
+Some non-fatal errors cause a single subdirectory to be skipped.
+The iteration is not aborted but continues with the next subdirectory,
+and the exit status is unaffected:
+.Bl -tag -width Ds -offset indent
+.It directory Ar subdirectory No unreadable : Ar reason
+A directory below
+.Ar srcdir
+could not be read and is skipped.
+.It directory Ar subdirectory No causes cycle
+A directory below
+.Ar srcdir
+is skipped because it would cause cyclic processing.
+.It Sy mkdirat Ar subdirectory : reason
+A required directory below
+.Ar dstdir
+does not exist and could not be created.
+The corresponding subdirectory below
+.Ar srcdir
+is skipped.
+.El
+.Pp
+Some non-fatal errors cause a single source file to be skipped.
+The iteration is not aborted but continues with the next file,
+and the exit status is unaffected:
+.Bl -tag -width Ds -offset indent
+.It file Ar filename : reason
+The function
+.Xr fts_read 3
+reported a non-fatal error with respect to
+.Ar filename .
+.It file Ar filename : No not a regular file
+For example, it might be a symbolic link or a device file.
+.It Sy open Ar filename No for reading : Ar reason
+A file below
+.Ar srcdir
+could not be read, for example due to permission problems.
+.It Sy openat Ar filename No for writing : Ar reason
+A file below
+.Ar dstdir
+could not be created or truncated, for example due to permission problems.
+.El
+.Pp
+If errors occur, the applicable summary messages appear
+after the end of the iteration:
+.Pp
+.Bl -tag -width Ds -offset indent -compact
+.It skipped Ar number No directories due to errors
+.It skipped Ar number No files due to errors
+.It processing aborted due to fatal error
+.El
+.Pp
+If the
+.Fl v
+flag is specified, the following summary message also appears:
+.Bl -tag -width Ds -offset indent
+.It processed Ar nfiles No files in Ar ndirs No directories
+A file is counted if it could be opened for reading and the
+corresponding output file could be opened for writing;
+this does not necessarily mean that it is a useful manual page.
+A directory is counted if it could be opened for reading and the
+corresponding output directory existed or could be created;
+this does not necessarily mean that any files could be
+processed inside.
+.El
+.Pp
+If the
+.Fl v
+flag is specified twice, the following messages also appear:
+.Bl -tag -width Ds -offset indent
+.It allowing up to Ar number No files in flight
+This is printed at the beginning of the iteration,
+showing the maximum number of files that
+.Nm
+allows to be in flight at any given time.
+.It files in flight: Ar old No \- Ar decrement No = Ar new
+This message is printed when
+.Nm
+learns about
+.Xr mandocd 8
+accepting more than one file at the same time.
+The three numbers printed are the old number of files in flight,
+the amount this number is being reduced, and the resulting
+new number of files in flight.
+.It waiting for Ar number No files in flight
+This message is printed at the end of the iteration, after
+.Nm
+has submitted all files to
+.Xr mandocd 8
+that it intends to.
+THe message informs about the number of files still in flight
+at this point.
+The
 .Nm
-will simply continue with the next file or subdirectory.
+program then waits until
+.Xr mandocd 8
+has accepted them all or until an error occurs.
+.El
 .Sh SEE ALSO
 .Xr mandoc 1 ,
 .Xr mandocd 8
 .Sh HISTORY
 A
 .Nm
 utility first appeared in
 .Fx 1.0 .
 Other, incompatible implementations appeared in
 .Nx 1.0
 and in
 .Sy man-db No 2.2 .
 .Pp
 This version appeared in version 1.14.1 of the
 .Sy mandoc
 toolkit.
 .Sh AUTHORS
 .An -nosplit
 The first
 .Nm
 implementation was a short shell script by
 .An Christoph Robitschko
 in July 1993.
 .Pp
 The
 .Nx
 implementations were written by
 .An J. T. Conklin Aq Mt jtc@netbsd.org
 in 1993,
 .An Christian E. Hopps Aq Mt chopps@netbsd.org
 in 1994,
 and
 .An Dante Profeta Aq Mt dante@netbsd.org
 in 1999; the
 .Sy man-db
 implementation by
 .An Graeme W. Wilford
 in 1994; and the
 .Fx
 implementations by
 .An Wolfram Schneider Aq Mt wosch@freebsd.org
 in 1995 and
 .An John Rochester Aq Mt john@jrochester.org
 in 2002.
 .Pp
 The concept of the present version was designed and implemented by
 .An Michael Stapelberg Aq Mt stapelberg@debian.org
 in 2017.
 Option and argument handling and directory iteration was added by
 .An Ingo Schwarze Aq Mt schwarze@openbsd.org .
 .Sh CAVEATS
 All versions of
 .Nm
 are incompatible with each other because each caters to the needs
 of a specific operating system, for example regarding directory
 structures and file naming conventions.
 .Pp
 This version is more flexible than the others in so far as it does
 not assume any particular directory structure or naming convention.
 That flexibility comes at the price of not being able to change the
 names and relative paths of the source files when reusing them to
 store the formatted files, of not supporting any configuration file
 formats or environment variables, and of being unable to scan for
 and remove junk files in
 .Ar dstdir .
 .Pp
 Currently,
 .Nm
 always reformats each page, even if the formatted version is newer
 than the source version.
diff --git a/contrib/mandoc/catman.c b/contrib/mandoc/catman.c
index e46613eb0e8c..c9eda18bf71c 100644
--- a/contrib/mandoc/catman.c
+++ b/contrib/mandoc/catman.c
@@ -1,260 +1,439 @@
-/*	$Id: catman.c,v 1.23 2021/10/15 15:04:02 schwarze Exp $ */
+/* $Id: catman.c,v 1.30 2025/07/13 14:15:26 schwarze Exp $ */
 /*
+ * Copyright (c) 2017, 2025 Ingo Schwarze 
  * Copyright (c) 2017 Michael Stapelberg 
- * Copyright (c) 2017 Ingo Schwarze 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #if NEED_XPG4_2
 #define _XPG4_2
 #endif
 
 #include 
 #include 
 #include 
 
+#include 
 #if HAVE_ERR
 #include 
 #endif
 #include 
 #include 
 #if HAVE_FTS
 #include 
 #else
 #include "compat_fts.h"
 #endif
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
+int		verbose_flag = 0;
+sig_atomic_t	got_signal = 0;
+
 int	 process_manpage(int, int, const char *);
 int	 process_tree(int, int);
 void	 run_mandocd(int, const char *, const char *)
 		__attribute__((__noreturn__));
+void	 signal_handler(int);
 ssize_t	 sock_fd_write(int, int, int, int);
 void	 usage(void) __attribute__((__noreturn__));
 
 
+void
+signal_handler(int signum)
+{
+	got_signal = signum;
+}
+
 void
 run_mandocd(int sockfd, const char *outtype, const char* defos)
 {
 	char	 sockfdstr[10];
+	int	 len;
 
-	if (snprintf(sockfdstr, sizeof(sockfdstr), "%d", sockfd) == -1)
+	len = snprintf(sockfdstr, sizeof(sockfdstr), "%d", sockfd);
+	if (len >= (int)sizeof(sockfdstr)) {
+		errno = EOVERFLOW;
+		len = -1;
+	}
+	if (len < 0)
 		err(1, "snprintf");
 	if (defos == NULL)
 		execlp("mandocd", "mandocd", "-T", outtype,
 		    sockfdstr, (char *)NULL);
 	else
 		execlp("mandocd", "mandocd", "-T", outtype,
 		    "-I", defos, sockfdstr, (char *)NULL);
 	err(1, "exec(mandocd)");
 }
 
 ssize_t
 sock_fd_write(int fd, int fd0, int fd1, int fd2)
 {
 	const struct timespec timeout = { 0, 10000000 };  /* 0.01 s */
 	struct msghdr	 msg;
 	struct iovec	 iov;
 	union {
 		struct cmsghdr	 cmsghdr;
 		char		 control[CMSG_SPACE(3 * sizeof(int))];
 	} cmsgu;
 	struct cmsghdr	*cmsg;
 	int		*walk;
 	ssize_t		 sz;
 	unsigned char	 dummy[1] = {'\0'};
 
 	iov.iov_base = dummy;
 	iov.iov_len = sizeof(dummy);
 
 	msg.msg_name = NULL;
 	msg.msg_namelen = 0;
 	msg.msg_iov = &iov;
 	msg.msg_iovlen = 1;
 
 	msg.msg_control = cmsgu.control;
 	msg.msg_controllen = sizeof(cmsgu.control);
 
 	cmsg = CMSG_FIRSTHDR(&msg);
 	cmsg->cmsg_len = CMSG_LEN(3 * sizeof(int));
 	cmsg->cmsg_level = SOL_SOCKET;
 	cmsg->cmsg_type = SCM_RIGHTS;
 
 	walk = (int *)CMSG_DATA(cmsg);
 	*(walk++) = fd0;
 	*(walk++) = fd1;
 	*(walk++) = fd2;
 
 	/*
 	 * It appears that on some systems, sendmsg(3)
 	 * may return EAGAIN even in blocking mode.
 	 * Seen for example on Oracle Solaris 11.2.
 	 * The sleeping time was chosen by experimentation,
 	 * to neither cause more than a handful of retries
 	 * in normal operation nor unnecessary delays.
 	 */
-	for (;;) {
-		if ((sz = sendmsg(fd, &msg, 0)) != -1 ||
-		    errno != EAGAIN)
+	while ((sz = sendmsg(fd, &msg, 0)) == -1) {
+		if (errno != EAGAIN) {
+			warn("FATAL: sendmsg");
 			break;
+		}
 		nanosleep(&timeout, NULL);
 	}
 	return sz;
 }
 
 int
 process_manpage(int srv_fd, int dstdir_fd, const char *path)
 {
 	int	 in_fd, out_fd;
 	int	 irc;
 
 	if ((in_fd = open(path, O_RDONLY)) == -1) {
-		warn("open(%s)", path);
+		warn("open %s for reading", path);
+		fflush(stderr);
 		return 0;
 	}
 
 	if ((out_fd = openat(dstdir_fd, path,
 	    O_WRONLY | O_NOFOLLOW | O_CREAT | O_TRUNC,
 	    S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) {
-		warn("openat(%s)", path);
+		warn("openat %s for writing", path);
+		fflush(stderr);
 		close(in_fd);
 		return 0;
 	}
 
 	irc = sock_fd_write(srv_fd, in_fd, out_fd, STDERR_FILENO);
 
 	close(in_fd);
 	close(out_fd);
 
-	if (irc < 0) {
-		warn("sendmsg");
-		return -1;
-	}
-	return 0;
+	return irc;
 }
 
 int
 process_tree(int srv_fd, int dstdir_fd)
 {
+	const struct timespec timeout = { 0, 10000000 };  /* 0.01 s */
+	const int	 max_inflight = 16;
+
 	FTS		*ftsp;
 	FTSENT		*entry;
 	const char	*argv[2];
 	const char	*path;
+	int		 inflight, irc, decr, fatal;
+	int		 gooddirs, baddirs, goodfiles, badfiles;
+	char		 dummy[1];
 
 	argv[0] = ".";
 	argv[1] = (char *)NULL;
 
 	if ((ftsp = fts_open((char * const *)argv,
 	    FTS_PHYSICAL | FTS_NOCHDIR, NULL)) == NULL) {
 		warn("fts_open");
 		return -1;
 	}
 
-	while ((entry = fts_read(ftsp)) != NULL) {
+	if (verbose_flag >= 2) {
+		warnx("allowing up to %d files in flight", max_inflight);
+		fflush(stderr);
+	}
+	inflight = fatal = gooddirs = baddirs = goodfiles = badfiles = 0;
+	while (fatal == 0 && got_signal == 0 &&
+	    (entry = fts_read(ftsp)) != NULL) {
+		if (inflight >= max_inflight) {
+			while (recv(srv_fd, dummy, sizeof(dummy), 0) == -1) {
+				if (errno != EAGAIN) {
+					warn("FATAL: recv");
+					fatal = errno;
+					break;
+				}
+				nanosleep(&timeout, NULL);
+			}
+			if (fatal != 0)
+				break;
+			decr = 1;
+			while ((irc = recv(srv_fd, dummy, sizeof(dummy),
+			    MSG_DONTWAIT)) > 0)
+				decr++;
+			assert(inflight >= decr);
+			if (verbose_flag >= 2 && decr > 1) {
+				warnx("files in flight: %d - %d = %d",
+				    inflight, decr, inflight - decr);
+				fflush(stderr);
+			}
+			inflight -= decr;
+			if (irc == 0) {
+				errno = ECONNRESET;
+				inflight = -1;
+			}
+			if (errno != EAGAIN) {
+				warn("FATAL: recv");
+				fatal = errno;
+				break;
+			}
+		}
 		path = entry->fts_path + 2;
 		switch (entry->fts_info) {
 		case FTS_F:
-			if (process_manpage(srv_fd, dstdir_fd, path) == -1) {
-				fts_close(ftsp);
-				return -1;
+			switch (process_manpage(srv_fd, dstdir_fd, path)) {
+			case -1:
+				fatal = errno;
+				break;
+			case 0:
+				badfiles++;
+				break;
+			default:
+				goodfiles++;
+				inflight++;
+				break;
 			}
 			break;
 		case FTS_D:
 			if (*path != '\0' &&
 			    mkdirat(dstdir_fd, path, S_IRWXU | S_IRGRP |
 			      S_IXGRP | S_IROTH | S_IXOTH) == -1 &&
 			    errno != EEXIST) {
-				warn("mkdirat(%s)", path);
+				warn("mkdirat %s", path);
+				fflush(stderr);
 				(void)fts_set(ftsp, entry, FTS_SKIP);
-			}
+				baddirs++;
+			} else
+				gooddirs++;
 			break;
 		case FTS_DP:
 			break;
+		case FTS_DNR:
+			warnx("directory %s unreadable: %s",
+			    path, strerror(entry->fts_errno));
+			fflush(stderr);
+			baddirs++;
+			break;
+		case FTS_DC:
+			warnx("directory %s causes cycle", path);
+			fflush(stderr);
+			baddirs++;
+			break;
+		case FTS_ERR:
+		case FTS_NS:
+			warnx("file %s: %s",
+			    path, strerror(entry->fts_errno));
+			fflush(stderr);
+			badfiles++;
+			break;
 		default:
-			warnx("%s: not a regular file", path);
+			warnx("file %s: not a regular file", path);
+			fflush(stderr);
+			badfiles++;
 			break;
 		}
 	}
+	if (got_signal != 0) {
+		switch (got_signal) {
+		case SIGCHLD:
+			warnx("FATAL: mandocd child died: got SIGCHLD");
+			break;
+		case SIGPIPE:
+			warnx("FATAL: mandocd child died: got SIGPIPE");
+			break;
+		default:
+			warnx("FATAL: signal SIG%s", sys_signame[got_signal]);
+			break;
+		}
+		inflight = -1;
+		fatal = 1;
+	} else if (fatal == 0 && (fatal = errno) != 0)
+		warn("FATAL: fts_read");
 
 	fts_close(ftsp);
-	return 0;
+	if (verbose_flag >= 2 && inflight > 0) {
+		warnx("waiting for %d files in flight", inflight);
+		fflush(stderr);
+	}
+	while (inflight > 0) {
+		irc = recv(srv_fd, dummy, sizeof(dummy), 0);
+		if (irc > 0)
+			inflight--;
+		else if (irc == -1 && errno == EAGAIN)
+			nanosleep(&timeout, NULL);
+		else {
+			if (irc == 0)
+				errno = ECONNRESET;
+			warn("recv");
+			inflight = -1;
+		}
+	}
+	if (verbose_flag)
+		warnx("processed %d files in %d directories",
+		    goodfiles, gooddirs);
+	if (baddirs > 0)
+		warnx("skipped %d %s due to errors", baddirs,
+		    baddirs == 1 ? "directory" : "directories");
+	if (badfiles > 0)
+		warnx("skipped %d %s due to errors", badfiles,
+		    badfiles == 1 ? "file" : "files");
+	if (fatal != 0) {
+		warnx("processing aborted due to fatal error, "
+		    "results are probably incomplete");
+		inflight = -1;
+	}
+	return inflight;
 }
 
 int
 main(int argc, char **argv)
 {
+	struct sigaction sa;
 	const char	*defos, *outtype;
 	int		 srv_fds[2];
 	int		 dstdir_fd;
 	int		 opt;
 	pid_t		 pid;
 
 	defos = NULL;
 	outtype = "ascii";
-	while ((opt = getopt(argc, argv, "I:T:")) != -1) {
+	while ((opt = getopt(argc, argv, "I:T:v")) != -1) {
 		switch (opt) {
 		case 'I':
 			defos = optarg;
 			break;
 		case 'T':
 			outtype = optarg;
 			break;
+		case 'v':
+			verbose_flag += 1;
+			break;
 		default:
 			usage();
 		}
 	}
 
 	if (argc > 0) {
 		argc -= optind;
 		argv += optind;
 	}
-	if (argc != 2)
+	if (argc != 2) {
+		switch (argc) {
+		case 0:
+			warnx("missing arguments: srcdir and dstdir");
+			break;
+		case 1:
+			warnx("missing argument: dstdir");
+			break;
+		default:
+			warnx("too many arguments: %s", argv[2]);
+			break;
+		}
 		usage();
+	}
+
+	memset(&sa, 0, sizeof(sa));
+	sa.sa_handler = &signal_handler;
+	sa.sa_flags = SA_NOCLDWAIT;
+	if (sigfillset(&sa.sa_mask) == -1)
+		err(1, "sigfillset");
+	if (sigaction(SIGHUP, &sa, NULL) == -1)
+		err(1, "sigaction(SIGHUP)");
+	if (sigaction(SIGINT, &sa, NULL) == -1)
+		err(1, "sigaction(SIGINT)");
+	if (sigaction(SIGPIPE, &sa, NULL) == -1)
+		err(1, "sigaction(SIGPIPE)");
+	if (sigaction(SIGTERM, &sa, NULL) == -1)
+		err(1, "sigaction(SIGTERM)");
+	if (sigaction(SIGCHLD, &sa, NULL) == -1)
+		err(1, "sigaction(SIGCHLD)");
 
 	if (socketpair(AF_LOCAL, SOCK_STREAM, AF_UNSPEC, srv_fds) == -1)
 		err(1, "socketpair");
 
 	pid = fork();
 	switch (pid) {
 	case -1:
 		err(1, "fork");
 	case 0:
 		close(srv_fds[0]);
 		run_mandocd(srv_fds[1], outtype, defos);
 	default:
 		break;
 	}
 	close(srv_fds[1]);
 
-	if ((dstdir_fd = open(argv[1], O_RDONLY | O_DIRECTORY)) == -1)
-		err(1, "open(%s)", argv[1]);
+	if ((dstdir_fd = open(argv[1], O_RDONLY | O_DIRECTORY)) == -1) {
+		if (errno != ENOENT)
+			err(1, "open destination %s", argv[1]);
+		if (mkdir(argv[1], S_IRWXU |
+		    S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) == -1) 
+			err(1, "mkdir destination %s", argv[1]);
+		if ((dstdir_fd = open(argv[1], O_RDONLY | O_DIRECTORY)) == -1)
+			err(1, "open destination %s", argv[1]);
+	}
 
 	if (chdir(argv[0]) == -1)
-		err(1, "chdir(%s)", argv[0]);
+		err(1, "chdir to source %s", argv[0]);
 
 	return process_tree(srv_fds[0], dstdir_fd) == -1 ? 1 : 0;
 }
 
 void
 usage(void)
 {
 	fprintf(stderr, "usage: %s [-I os=name] [-T output] "
 	    "srcdir dstdir\n", BINM_CATMAN);
 	exit(1);
 }
diff --git a/contrib/mandoc/gmdiff b/contrib/mandoc/gmdiff
index 69431f703aaf..54025e4cd450 100644
--- a/contrib/mandoc/gmdiff
+++ b/contrib/mandoc/gmdiff
@@ -1,57 +1,57 @@
 #!/bin/sh
 # Copyright (c) 2013,2014,2015,2017,2018 Ingo Schwarze 
 #
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
 # copyright notice and this permission notice appear in all copies.
 #
 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 if [ `id -u` -eq 0 ]; then
   echo "$0: do not run me as root"
   exit 1
 fi
 
 if [ $# -eq 0 ]; then
   echo "usage: $0 [-h|-u] manual_source_file ..."
   exit 1
 fi
 
 if [ "X$1" = "X-h" ]; then
   shift
   export PATH="/usr/local/heirloom-doctools/bin:$PATH"
   EQN="neqn"
   ROFF="nroff"
   MOPT="-Ios=BSD -Tascii $MOPT"
   COLPIPE="col -b"
 elif [ "X$1" = "X-u" ]; then
   shift
   ROFF="groff -ket -ww -Tutf8 -P -c"
   MOPT="-Ios=OpenBSD -Wall -Tutf8 $MOPT"
   COLPIPE="cat"
 else
   ROFF="groff -ket -ww -mtty-char -Tascii -P -c"
   MOPT="-Ios=OpenBSD -Wall -Tascii $MOPT"
   COLPIPE="cat"
 fi
 
 while [ -n "$1" ]; do
   file=$1
   shift
   echo " ========== $file ========== "
-  $ROFF -mandoc $file | $COLPIPE 2> /tmp/roff.err > /tmp/roff.out
-  ${MANDOC:=mandoc} $MOPT $file | $COLPIPE \
+  ($ROFF -mandoc $file | $COLPIPE) 2> /tmp/roff.err > /tmp/roff.out
+  (${MANDOC:=mandoc} $MOPT $file | $COLPIPE) \
     2> /tmp/mandoc.err > /tmp/mandoc.out
   for i in roff mandoc; do
     [ -s /tmp/$i.err ] && echo "$i errors:" && cat /tmp/$i.err
   done
   diff -au $DIFFOPT /tmp/roff.out /tmp/mandoc.out 2>&1
 done
 
 exit 0
diff --git a/contrib/mandoc/man.7 b/contrib/mandoc/man.7
index 4d27c76ba110..91eafbb35f70 100644
--- a/contrib/mandoc/man.7
+++ b/contrib/mandoc/man.7
@@ -1,645 +1,672 @@
-.\" $Id: man.7,v 1.150 2023/10/23 22:57:54 schwarze Exp $
+.\" $Id: man.7,v 1.154 2025/08/05 21:16:20 schwarze Exp $
 .\"
+.\" Copyright (c) 2011-2015, 2017-2020, 2023, 2025
+.\"               Ingo Schwarze 
 .\" Copyright (c) 2009, 2010, 2011, 2012 Kristaps Dzonsons 
-.\" Copyright (c) 2011-2015,2017-2020,2023 Ingo Schwarze 
 .\" Copyright (c) 2017 Anthony Bentley 
 .\" Copyright (c) 2010 Joerg Sonnenberger 
 .\"
 .\" Permission to use, copy, modify, and distribute this software for any
 .\" purpose with or without fee is hereby granted, provided that the above
 .\" copyright notice and this permission notice appear in all copies.
 .\"
 .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\"
-.Dd $Mdocdate: October 23 2023 $
+.Dd $Mdocdate: August 5 2025 $
 .Dt MAN 7
 .Os
 .Sh NAME
 .Nm man
 .Nd legacy formatting language for manual pages
 .Sh DESCRIPTION
 The
 .Nm man
 language was the standard formatting language for
 .At
 manual pages from 1979 to 1989.
 Do not use it to write new manual pages: it is a purely presentational
 language and lacks support for semantic markup.
 Use the
 .Xr mdoc 7
 language, instead.
 .Pp
 In a
 .Nm
 document, lines beginning with the control character
 .Sq \&.
 are called
 .Dq macro lines .
 The first word is the macro name.
 It usually consists of two capital letters.
 For a list of portable macros, see
 .Sx MACRO OVERVIEW .
 The words following the macro name are arguments to the macro.
 .Pp
 Lines not beginning with the control character are called
 .Dq text lines .
 They provide free-form text to be printed; the formatting of the text
 depends on the respective processing context:
 .Bd -literal -offset indent
 \&.SH Macro lines change control state.
 Text lines are interpreted within the current state.
 .Ed
 .Pp
 Many aspects of the basic syntax of the
 .Nm
 language are based on the
 .Xr roff 7
 language; see the
 .Em LANGUAGE SYNTAX
 and
 .Em MACRO SYNTAX
 sections in the
 .Xr roff 7
 manual for details, in particular regarding
 comments, escape sequences, whitespace, and quoting.
 .Pp
 Each
 .Nm
 document starts with the
 .Ic TH
 macro specifying the document's name and section, followed by the
 .Sx NAME
 section formatted as follows:
 .Bd -literal -offset indent
 \&.TH PROGNAME 1 1979-01-10
 \&.SH NAME
 \efBprogname\efR \e(en one line about what it does
 .Ed
 .Sh MACRO OVERVIEW
 This overview is sorted such that macros of similar purpose are listed
 together.
 Deprecated and non-portable macros are not included in the overview,
 but can be found in the alphabetical reference below.
 .Ss Page header and footer meta-data
 .Bl -column "RS, RE" description
 .It Ic TH Ta set the title: Ar name section date Op Ar source Op Ar volume
-.It Ic AT Ta display AT&T UNIX version in the page footer (<= 1 argument)
+.It Ic AT Ta display AT&T UNIX version in the page footer (<= 2 arguments)
 .It Ic UC Ta display BSD version in the page footer (<= 1 argument)
 .El
 .Ss Sections and paragraphs
 .Bl -column "RS, RE" description
 .It Ic SH Ta section header (one line)
 .It Ic SS Ta subsection header (one line)
 .It Ic PP Ta start an undecorated paragraph (no arguments)
 .It Ic IP Ta indented paragraph: Op Ar head Op Ar width
 .It Ic TP Ta tagged paragraph: Op Ar width
+.It Ic HP Ta hanged paragraph: Op Ar width
 .It Ic PD Ta set vertical paragraph distance: Op Ar height
 .It Ic EX , EE Ta display an example (no arguments)
 .It Ic RS , RE Ta reset the left margin: Op Ar width
 .It Ic in Ta additional indent: Op Ar width
 .El
 .Ss Physical markup
 .Bl -column "RS, RE" description
 .It Ic B Ta boldface font
 .It Ic I Ta italic font
 .It Ic SB Ta small boldface font
 .It Ic SM Ta small roman font
 .It Ic BI Ta alternate between boldface and italic fonts
 .It Ic BR Ta alternate between boldface and roman fonts
 .It Ic IB Ta alternate between italic and boldface fonts
 .It Ic IR Ta alternate between italic and roman fonts
 .It Ic RB Ta alternate between roman and boldface fonts
 .It Ic RI Ta alternate between roman and italic fonts
 .El
 .Sh MACRO REFERENCE
 This section is a canonical reference to all macros, arranged
 alphabetically.
 For the scoping of individual macros, see
 .Sx MACRO SYNTAX .
 .Bl -tag -width 3n
 .It Ic AT
 Sets the volume for the footer for compatibility with man pages from
 .At
 releases.
 The optional arguments specify which release it is from.
 This macro is an extension that first appeared in
 .Bx 4.3 .
 .It Ic B
 Text is rendered in bold face.
 .It Ic BI
 Text is rendered alternately in bold face and italic.
 Thus,
 .Sq .BI this word and that
 causes
 .Sq this
 and
 .Sq and
 to render in bold face, while
 .Sq word
 and
 .Sq that
 render in italics.
 Whitespace between arguments is omitted in output.
 .Pp
 Example:
 .Pp
 .Dl \&.BI bold italic bold italic
 .It Ic BR
 Text is rendered alternately in bold face and roman (the default font).
 Whitespace between arguments is omitted in output.
 See also
 .Ic BI .
 .It Ic DT
 Restore the default tabulator positions.
 They are at intervals of 0.5 inches.
 This has no effect unless the tabulator positions were changed with the
 .Xr roff 7
 .Ic ta
 request.
 .It Ic EE
 End an example block started with
 .Ic EX .
 This is a Version 9
 .At
 extension later adopted by GNU.
 In
 .Xr mandoc 1 ,
 it does the same as the
 .Xr roff 7
 .Ic fi
 request (switch to fill mode).
 .It Ic EX
 Begin a block to display an example.
 This is a Version 9
 .At
 extension later adopted by GNU.
 In
 .Xr mandoc 1 ,
 it does the same as the
 .Xr roff 7
 .Ic nf
 request (switch to no-fill mode).
 .It Ic HP
 Begin a paragraph whose initial output line is left-justified, but
 subsequent output lines are indented, with the following syntax:
 .Pp
 .D1 Pf . Ic HP Op Ar width
 .Pp
 The
 .Ar width
 argument is a
 .Xr roff 7
 scaling width.
 If specified, it's saved for later paragraph left margins;
 if unspecified, the saved or default width is used.
-.Pp
-This macro is portable, but deprecated
-because it has no good representation in HTML output,
-usually ending up indistinguishable from
-.Ic PP .
 .It Ic I
 Text is rendered in italics.
 .It Ic IB
 Text is rendered alternately in italics and bold face.
 Whitespace between arguments is omitted in output.
 See also
 .Ic BI .
 .It Ic IP
 Begin an indented paragraph with the following syntax:
 .Pp
 .D1 Pf . Ic IP Op Ar head Op Ar width
 .Pp
 The
 .Ar width
 argument is a
 .Xr roff 7
 scaling width defining the left margin.
 It's saved for later paragraph left-margins; if unspecified, the saved or
 default width is used.
 .Pp
 The
 .Ar head
 argument is used as a leading term, flushed to the left margin.
 This is useful for bulleted paragraphs and so on.
 .It Ic IR
 Text is rendered alternately in italics and roman (the default font).
 Whitespace between arguments is omitted in output.
 See also
 .Ic BI .
 .It Ic LP
 A synonym for
 .Ic PP .
 .It Ic ME
 End a mailto block started with
 .Ic MT .
 This is a GNU extension.
+.It Ic MR
+Reference another manual page.
+This is a Plan 9 extension also supported by GNU.
+It has the following syntax:
+.Pp
+.D1 Pf . Ic MR Ar name section Op Ar suffix
+.Pp
+The optional, single
+.Ar suffix
+argument is appended without preceding whitespace
+and typically used for trailing punctuation.
 .It Ic MT
 Begin a mailto block.
 This is a GNU extension.
 It has the following syntax:
 .Bd -unfilled -offset indent
 .Pf . Ic MT Ar address
 link description to be shown
 .Pf . Ic ME
 .Ed
 .It Ic OP
 Optional command-line argument.
-This is a rarely used DWB extension.
-It has the following syntax:
+This is a deprecated GNU extension.
+The name and purpose of the macro match an earlier DWB extension,
+but both the syntax and semantics are incompatible.
+In GNU and
+.Xr mandoc 1 ,
+it has the following syntax:
 .Pp
 .D1 Pf . Ic OP Ar key Op Ar value
 .Pp
 The
 .Ar key
 is usually a command-line flag and
 .Ar value
 its argument.
 .It Ic P
 This synonym for
 .Ic PP
 is an
 .At III
 extension later adopted by
 .Bx 4.3 .
 .It Ic PD
 Specify the vertical space to be inserted before each new paragraph.
 .br
 The syntax is as follows:
 .Pp
 .D1 Pf . Ic PD Op Ar height
 .Pp
 The
 .Ar height
 argument is a
 .Xr roff 7
 scaling width.
 It defaults to
 .Cm 1v .
 If the unit is omitted,
 .Cm v
 is assumed.
 .Pp
 This macro affects the spacing before any subsequent instances of
 .Ic HP ,
 .Ic IP ,
 .Ic LP ,
 .Ic P ,
 .Ic PP ,
 .Ic SH ,
 .Ic SS ,
 .Ic SY ,
 and
 .Ic TP .
 .It Ic PP
 Begin an undecorated paragraph.
 The scope of a paragraph is closed by a subsequent paragraph,
 sub-section, section, or end of file.
 The saved paragraph left-margin width is reset to the default.
 .It Ic RB
 Text is rendered alternately in roman (the default font) and bold face.
 Whitespace between arguments is omitted in output.
 See also
 .Ic BI .
 .It Ic RE
 Explicitly close out the scope of a prior
 .Ic RS .
 The default left margin is restored to the state before that
 .Ic RS
 invocation.
 .Pp
 The syntax is as follows:
 .Pp
 .D1 Pf . Ic RE Op Ar level
 .Pp
 Without an argument, the most recent
 .Ic RS
 block is closed out.
 If
 .Ar level
 is 1, all open
 .Ic RS
 blocks are closed out.
 Otherwise,
 .Ar level No \(mi 1
 nested
 .Ic RS
 blocks remain open.
 .It Ic RI
 Text is rendered alternately in roman (the default font) and italics.
 Whitespace between arguments is omitted in output.
 See also
 .Ic BI .
 .It Ic RS
 Temporarily reset the default left margin.
 This has the following syntax:
 .Pp
 .D1 Pf . Ic RS Op Ar width
 .Pp
 The
 .Ar width
 argument is a
 .Xr roff 7
 scaling width.
 If not specified, the saved or default width is used.
 .Pp
 See also
 .Ic RE .
 .It Ic SB
 Text is rendered in small size (one point smaller than the default font)
 bold face.
 This macro is an extension that probably first appeared in SunOS 4.0
 and was later adopted by GNU and by
 .Bx 4.4 .
 .It Ic SH
 Begin a section.
 The scope of a section is only closed by another section or the end of
 file.
 The paragraph left-margin width is reset to the default.
 .It Ic SM
 Text is rendered in small size (one point smaller than the default
 font).
 .It Ic SS
 Begin a sub-section.
 The scope of a sub-section is closed by a subsequent sub-section,
 section, or end of file.
 The paragraph left-margin width is reset to the default.
 .It Ic SY
 Begin a synopsis block with the following syntax:
 .Bd -unfilled -offset indent
 .Pf . Ic SY Ar command
 .Ar arguments
 .Pf . Ic YS
 .Ed
 .Pp
 This is a GNU extension and rarely used even in GNU manual pages.
 Formatting is similar to
 .Ic IP .
 .It Ic TH
 Set the name of the manual page for use in the page header
 and footer with the following syntax:
 .Pp
 .D1 Pf . Ic TH Ar name section date Op Ar source Op Ar volume
 .Pp
 Conventionally, the document
 .Ar name
 is given in all caps.
 The
 .Ar section
 is usually a single digit, in a few cases followed by a letter.
 The recommended
 .Ar date
 format is
 .Sy YYYY-MM-DD
 as specified in the ISO-8601 standard;
 if the argument does not conform, it is printed verbatim.
 If the
 .Ar date
 is empty or not specified, the current date is used.
 The optional
 .Ar source
 string specifies the organisation providing the utility.
 When unspecified,
 .Xr mandoc 1
 uses its
 .Fl Ios
 argument.
 The
 .Ar volume
 string replaces the default volume title of the
 .Ar section .
 .Pp
 Examples:
 .Pp
 .Dl \&.TH CVS 5 "1992-02-12" GNU
 .It Ic TP
 Begin a paragraph where the head, if exceeding the indentation width, is
 followed by a newline; if not, the body follows on the same line after
 advancing to the indentation width.
 Subsequent output lines are indented.
 The syntax is as follows:
 .Bd -unfilled -offset indent
 .Pf . Ic TP Op Ar width
 .Ar head No \e" one line
 .Ar body
 .Ed
 .Pp
 The
 .Ar width
 argument is a
 .Xr roff 7
 scaling width.
 If specified, it's saved for later paragraph left-margins; if
 unspecified, the saved or default width is used.
 .It Ic TQ
 Like
 .Ic TP ,
 except that no vertical spacing is inserted before the paragraph.
 This is a GNU extension.
 .It Ic UC
 Sets the volume for the footer for compatibility with man pages from
 .Bx
 releases.
 The optional first argument specifies which release it is from.
 This macro is an extension that first appeared in
 .Bx 3 .
 .It Ic UE
 End a uniform resource identifier block started with
 .Ic UR .
 This is a GNU extension.
 .It Ic UR
 Begin a uniform resource identifier block.
 This is a GNU extension.
 It has the following syntax:
 .Bd -unfilled -offset indent
 .Pf . Ic UR Ar uri
 link description to be shown
 .Pf . Ic UE
 .Ed
 .It Ic YS
 End a synopsis block started with
 .Ic SY .
 This is a GNU extension.
 .It Ic in
 Indent relative to the current indentation:
 .Pp
 .D1 Pf . Ic in Op Ar width
 .Pp
 If
 .Ar width
 is signed, the new offset is relative.
 Otherwise, it is absolute.
 This value is reset upon the next paragraph, section, or sub-section.
 .El
 .Sh MACRO SYNTAX
 The
 .Nm
 macros are classified by scope: line scope or block scope.
 Line macros are only scoped to the current line (and, in some
 situations, the subsequent line).
 Block macros are scoped to the current line and subsequent lines until
 closed by another block macro.
 .Ss Line Macros
 Line macros are generally scoped to the current line, with the body
 consisting of zero or more arguments.
 If a macro is scoped to the next line and the line arguments are empty,
 the next line, which must be text, is used instead.
 Thus:
 .Bd -literal -offset indent
 \&.I
 foo
 .Ed
 .Pp
 is equivalent to
 .Sq .I foo .
 If next-line macros are invoked consecutively, only the last is used.
 If a next-line macro is followed by a non-next-line macro, an error is
 raised.
 .Pp
 The syntax is as follows:
 .Bd -literal -offset indent
-\&.YO \(lBbody...\(rB
-\(lBbody...\(rB
+\&.\e" current-line syntax
+\&.YO \(lBbody ...\(rB
+
+\&.\e" next-line syntax
+\&.YO
+body ...
 .Ed
-.Bl -column "MacroX" "ArgumentsX" "ScopeXXXXX" "CompatX" -offset indent
-.It Em Macro Ta Em Arguments Ta Em Scope     Ta Em Notes
-.It Ic AT  Ta    <=1       Ta    current   Ta    \&
-.It Ic B   Ta    n         Ta    next-line Ta    \&
-.It Ic BI  Ta    n         Ta    current   Ta    \&
-.It Ic BR  Ta    n         Ta    current   Ta    \&
-.It Ic DT  Ta    0         Ta    current   Ta    \&
-.It Ic EE  Ta    0         Ta    current   Ta    Version 9 At
-.It Ic EX  Ta    0         Ta    current   Ta    Version 9 At
-.It Ic I   Ta    n         Ta    next-line Ta    \&
-.It Ic IB  Ta    n         Ta    current   Ta    \&
-.It Ic IR  Ta    n         Ta    current   Ta    \&
-.It Ic OP  Ta    >=1       Ta    current   Ta    DWB
-.It Ic PD  Ta    1         Ta    current   Ta    \&
-.It Ic RB  Ta    n         Ta    current   Ta    \&
-.It Ic RI  Ta    n         Ta    current   Ta    \&
-.It Ic SB  Ta    n         Ta    next-line Ta    \&
-.It Ic SM  Ta    n         Ta    next-line Ta    \&
-.It Ic TH  Ta    >1, <6    Ta    current   Ta    \&
-.It Ic UC  Ta    <=1       Ta    current   Ta    \&
-.It Ic in  Ta    1         Ta    current   Ta    Xr roff 7
+.Bl -column -offset indent\
+      "Macro"     "Arguments"     "curr and next"     "Version 9 AT&T UNIX"
+.It Em Macro Ta Em Arguments Ta Em Line Scope    Ta Em Notes
+.It Ic AT    Ta    0 to 2    Ta    current       Ta    \&
+.It Ic B     Ta    1 or more Ta    curr or next  Ta    \&
+.It Ic BI    Ta    2 or more Ta    current       Ta    \&
+.It Ic BR    Ta    2 or more Ta    current       Ta    \&
+.It Ic DT    Ta    0         Ta    none          Ta    \&
+.It Ic EE    Ta    0         Ta    none          Ta    Version 9 At
+.It Ic EX    Ta    0         Ta    none          Ta    Version 9 At
+.It Ic I     Ta    1 or more Ta    curr or next  Ta    \&
+.It Ic IB    Ta    2 or more Ta    current       Ta    \&
+.It Ic IR    Ta    2 or more Ta    current       Ta    \&
+.It Ic MR    Ta    2 or 3    Ta    current       Ta    Plan 9
+.It Ic OP    Ta    1 or 2    Ta    current       Ta    GNU
+.It Ic PD    Ta    0 or 1    Ta    current       Ta    \&
+.It Ic RB    Ta    2 or more Ta    current       Ta    \&
+.It Ic RI    Ta    2 or more Ta    current       Ta    \&
+.It Ic SB    Ta    1 or more Ta    curr or next  Ta    \&
+.It Ic SM    Ta    1 or more Ta    curr or next  Ta    \&
+.It Ic TH    Ta    3 to 5    Ta    current       Ta    \&
+.It Ic UC    Ta    0 or 1    Ta    current       Ta    \&
+.It Ic in    Ta    0 or 1    Ta    current       Ta    Xr roff 7
 .El
 .Ss Block Macros
 Block macros comprise a head and body.
-As with in-line macros, the head is scoped to the current line and, in
-one circumstance, the next line (the next-line stipulations as in
+As with in-line macros, the head is scoped to the current line or,
+for some macros, to the next line (the next-line stipulations as in
 .Sx Line Macros
 apply here as well).
 .Pp
 The syntax is as follows:
 .Bd -literal -offset indent
-\&.YO \(lBhead...\(rB
-\(lBhead...\(rB
-\(lBbody...\(rB
+\&.\e" current-line syntax
+\&.YO \(lBhead ...\(rB
+body ...
+\&...
+
+\&.\e" next-line syntax
+\&.YO \(lBhead\(rB
+head ...
+body ...
+\&...
 .Ed
 .Pp
 The closure of body scope may be to the section, where a macro is closed
 by
 .Ic SH ;
 sub-section, closed by a section or
 .Ic SS ;
-or paragraph, closed by a section, sub-section,
+paragraph, closed by a section, sub-section,
 .Ic HP ,
 .Ic IP ,
 .Ic LP ,
 .Ic P ,
 .Ic PP ,
-.Ic RE ,
+.Ic RS ,
 .Ic SY ,
+.Ic TP ,
 or
-.Ic TP .
-No closure refers to an explicit block closing macro.
+.Ic TQ ;
+or to an explicit block closing macro.
 .Pp
 As a rule, block macros may not be nested; thus, calling a block macro
 while another block macro scope is open, and the open scope is not
 implicitly closed, is syntactically incorrect.
-.Bl -column "MacroX" "ArgumentsX" "Head ScopeX" "sub-sectionX" "compatX" -offset indent
-.It Em Macro Ta Em Arguments Ta Em Head Scope Ta Em Body Scope  Ta Em Notes
-.It Ic HP  Ta    <2        Ta    current    Ta    paragraph   Ta    \&
-.It Ic IP  Ta    <3        Ta    current    Ta    paragraph   Ta    \&
-.It Ic LP  Ta    0         Ta    current    Ta    paragraph   Ta    \&
-.It Ic ME  Ta    0         Ta    none       Ta    none        Ta    GNU
-.It Ic MT  Ta    1         Ta    current    Ta    to \&ME     Ta    GNU
-.It Ic P   Ta    0         Ta    current    Ta    paragraph   Ta    \&
-.It Ic PP  Ta    0         Ta    current    Ta    paragraph   Ta    \&
-.It Ic RE  Ta    <=1       Ta    current    Ta    none        Ta    \&
-.It Ic RS  Ta    1         Ta    current    Ta    to \&RE     Ta    \&
-.It Ic SH  Ta    >0        Ta    next-line  Ta    section     Ta    \&
-.It Ic SS  Ta    >0        Ta    next-line  Ta    sub-section Ta    \&
-.It Ic SY  Ta    1         Ta    current    Ta    to \&YS     Ta    GNU
-.It Ic TP  Ta    n         Ta    next-line  Ta    paragraph   Ta    \&
-.It Ic TQ  Ta    n         Ta    next-line  Ta    paragraph   Ta    GNU
-.It Ic UE  Ta    0         Ta    current    Ta    none        Ta    GNU
-.It Ic UR  Ta    1         Ta    current    Ta    part        Ta    GNU
-.It Ic YS  Ta    0         Ta    none       Ta    none        Ta    GNU
+.Bl -column -offset indent\
+      "Macro"     "Arguments"     "curr and next"     "sub-section"     "Notes"
+.It Em Macro Ta Em Arguments Ta Em Head Scope    Ta Em Body Scope  Ta Em Notes
+.It Ic HP    Ta    0 or 1    Ta    current       Ta    paragraph   Ta    \&
+.It Ic IP    Ta    0 to 2    Ta    current       Ta    paragraph   Ta    \&
+.It Ic LP    Ta    0         Ta    none          Ta    paragraph   Ta    \&
+.It Ic ME    Ta    0 or 1    Ta    current       Ta    none        Ta    GNU
+.It Ic MT    Ta    1         Ta    current       Ta    to \&ME     Ta    GNU
+.It Ic P     Ta    0         Ta    none          Ta    paragraph   Ta    \&
+.It Ic PP    Ta    0         Ta    none          Ta    paragraph   Ta    \&
+.It Ic RE    Ta    0 or 1    Ta    current       Ta    none        Ta    \&
+.It Ic RS    Ta    0 or 1    Ta    current       Ta    to \&RE     Ta    \&
+.It Ic SH    Ta    1 or more Ta    curr or next  Ta    section     Ta    \&
+.It Ic SS    Ta    1 or more Ta    curr or next  Ta    sub-section Ta    \&
+.It Ic SY    Ta    1         Ta    current       Ta    to \&YS     Ta    GNU
+.It Ic TP    Ta    0 or 1    Ta    curr and next Ta    paragraph   Ta    \&
+.It Ic TQ    Ta    0 or 1    Ta    curr and next Ta    paragraph   Ta    GNU
+.It Ic UE    Ta    0 or 1    Ta    current       Ta    none        Ta    GNU
+.It Ic UR    Ta    1         Ta    current       Ta    to \&UE     Ta    GNU
+.It Ic YS    Ta    0         Ta    none          Ta    none        Ta    GNU
 .El
 .Pp
 If a block macro is next-line scoped, it may only be followed by in-line
 macros for decorating text.
 .Ss Font handling
 In
 .Nm
 documents, both
 .Sx Physical markup
 macros and
 .Xr roff 7
 .Ql \ef
 font escape sequences can be used to choose fonts.
 In text lines, the effect of manual font selection by escape sequences
 only lasts until the next macro invocation; in macro lines, it only lasts
 until the end of the macro scope.
 Note that macros like
 .Ic BR
 open and close a font scope for each argument.
 .Sh SEE ALSO
 .Xr man 1 ,
 .Xr mandoc 1 ,
 .Xr eqn 7 ,
 .Xr mandoc_char 7 ,
 .Xr mdoc 7 ,
 .Xr roff 7 ,
 .Xr tbl 7
 .Sh HISTORY
 The
 .Nm
 language first appeared as a macro package for the roff typesetting
 system in
 .At v7 .
 .Pp
 The stand-alone implementation that is part of the
 .Xr mandoc 1
 utility first appeared in
 .Ox 4.6 .
 .Sh AUTHORS
 .An -nosplit
 .An Douglas McIlroy Aq Mt m.douglas.mcilroy@dartmouth.edu
 designed and implemented the original version of these macros,
 wrote the original version of this manual page,
 and was the first to use them when he edited volume 1 of the
 .At v7
 manual pages.
 .Pp
 .An James Clark
 later rewrote the macros for groff.
 .An Eric S. Raymond Aq Mt esr@thyrsus.com
 and
 .An Werner Lemberg Aq Mt wl@gnu.org
 added the extended
 .Nm
 macros to groff in 2007.
 .Pp
 The
 .Xr mandoc 1
 program and this
 .Nm
 reference were written by
 .An Kristaps Dzonsons Aq Mt kristaps@bsd.lv .
diff --git a/contrib/mandoc/man.options.1 b/contrib/mandoc/man.options.1
index d8c790f4fa04..be65ad98fddc 100644
--- a/contrib/mandoc/man.options.1
+++ b/contrib/mandoc/man.options.1
@@ -1,1332 +1,1333 @@
-.\"	$Id: man.options.1,v 1.7 2017/07/04 23:40:01 schwarze Exp $
+.\" $Id: man.options.1,v 1.8 2025/06/30 00:11:06 schwarze Exp $
 .\"
-.\" Copyright (c) 2017 Ingo Schwarze 
+.\" Copyright (c) 2017, 2025 Ingo Schwarze 
 .\"
 .\" Permission to use, copy, modify, and distribute this software for any
 .\" purpose with or without fee is hereby granted, provided that the above
 .\" copyright notice and this permission notice appear in all copies.
 .\"
 .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\"
-.Dd $Mdocdate: July 4 2017 $
+.Dd $Mdocdate: June 30 2025 $
 .Dt MAN.OPTIONS 1
 .Os
 .Sh NAME
 .Nm man.options
 .Nd assignment of option letters in manual page utilities
 .\"
 .\" Sources that occur repeatedly.
 .\" Only use if the precise implementation time is unknown.
 .\"
 .de PWB
 .No PWB/UNIX 1.0 Pq July 1, 1977 \\$1
 ..
 .de At7
 .At v7 Pq January 1979 \\$1
 ..
 .de At3
 .At III Pq June 1980 \\$1
 ..
 .de Bx4
 .Bx 4 Pq November 16, 1980 \\$1
 ..
 .de At5
 .At V Pq January 1983 \\$1
 ..
 .de Bx43
 .Bx 4.3 Pq June 1986 \\$1
 ..
 .\" option was present in groff-1.01 as contained in 4.3BSD-Net/2
 .\" and no mention of it could be found in the ChangeLog,
 .\" so it's probably older than groff-0.4, where the log started
 .de g04
 .No probably before groff-0.4 Pq before July 14, 1990 \\$1
 ..
 .de Eaton
 .No Eaton Pq before July 7, 1993; 1990/91? \\$1
 ..
 .\" man-1.5e was released on July 11, 1998.
 .de man15e
 .No man-1.5e Pq not before 1993, not after 1998 \\$1
 ..
 .\" man-1.5g was released on April 7, 1999.
 .de man15g
 .No man-1.5g Pq not before 1993, not after 1999 \\$1
 ..
 .\" code first seen in the initial import of man-db into CVS ,
 .\" which was more or less debian man-db-2.3.17
 .\" Colin Watson's first release was 2.3.18 on May 14, 2001
 .\" no clue about it found in ChangeLog-2013,
 .\" so it was probably already present before man-db-2.2a4
 .de dbI
 .No man-db probably before 2.2a4 Pq before Nov 8, 1994 \\$1
 ..
 .\"
 .\" --------------------------------------------------------------------
 .\"
 .Sh DESCRIPTION
 This manual page lists option letters used in many different versions
 of the
 .Nm man ,
 .Nm apropos ,
 .Nm whatis ,
 .Nm mandoc ,
 .Nm makewhatis ,
 .Nm mandb ,
 .Nm makemandb ,
 .Nm catman ,
 and
 .Nm manpath
 utilities.
 Option letters used by
 .Nm groff ,
 .Nm nroff ,
 .Nm troff ,
 and
 .Nm roff
 are also included because beginning with
 .At v7 ,
 many versions of
 .Xr man 1
 pass on unrecognized options to these programs.
 .Pp
 For each option letter, information is first grouped into paragraphs,
 each paragraph describing similar functionality and starting with
 one line briefly summarizing that functionality.
 .Pp
 For each program using the letter for that functionality, one line
 is provided, giving the name of the program, a colon, the system
 where this letter first appeared for this functionality in this
 program, optionally a comma and a list of other system versions
 introducing the same, a semicolon, and a list of current systems
 supporting it.
 If a system appears before the semicolon, it is not repeated
 afterwards.
 .Pp
 Entries are sorted by historical precedence, except that obsolete
 options are moved to the end.
 Dates are commit dates where known, and release dates otherwise.
 .Bl -tag -width 3n
 .It Fl a
 display all matching manual pages
 .br
 .Nm man :
 .Bx 4.3 Tahoe Pq June 1988 ,
 .Eaton ;
 .Ox , Fx , Nx , No man-db , man-1.6 , illumos , Solaris 9-11
 .br
 .Nm apropos , whatis , mandoc :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 only display items that match all keywords
 .br
 .Nm apropos :
 .No man-db Pq Aug 29, 2007
 .Pp
 use all directories and files for
 .Xr mandoc.db 5
 .br
 .Nm makewhatis :
 .Ox 5.6 Pq April 18, 2014
 .Pp
 .Bq superseded by Fl T Cm ascii
 ASCII output mode
 .br
 .Nm troff :
 .At7
 .br
 .Nm groff :
 .g04
 .It Fl B
 use specified browser
 .br
 .Nm man :
 .No man-1.6 Pq June 24, 2005
 .It Fl b
 print a backtrace with each warning or error message
 .br
 .Nm groff :
 .g04
 .Pp
 .Bq obsolete hardware
 report whether the phototypesetter is busy
 .br
 .Nm troff :
 .At7
 .It Fl C
 alternate configuration file
 .br
 .Nm apropos , whatis :
 .Bx 4.4 Lite1 Pq April 22, 1994 ,
 .No man-db Pq Feb 22, 2003 ;
 .Ox , Nx
 .br
 .Nm man :
 .Nx 1.0 Pq Oct 26, 1994 ,
 .man15e ;
 .Ox
 .br
 .Nm mandb , catman , manpath :
 .No man-db Pq Feb 22, 2003
 .br
 .Nm makemandb :
 .Nx Pq Feb 7, 2012
 .br
 .Nm makewhatis :
 .Ox 5.6 Pq April 18, 2014
 .br
 .Nm mandoc :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 .Bq obsolete
 enable compatibility mode
 .br
 .Nm groff :
 .No before groff-0.5 Pq before August 3, 1990
 .It Fl c
 do not use a pager
 .br
 .Nm man :
 .Bx 4.3 Reno Pq June 1990 ;
 .Ox , Nx
 .br
 .Nm apropos , whatis , mandoc :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 process given catpath
 .br
 .Nm makewhatis :
 .Pq not before 1992, not after 1995
 .Pp
 recreate databases from scratch
 .br
 .Nm mandb :
 .dbI
 .Pp
 produce a catpath as opposed to a manpath
 .br
 .Nm manpath :
 .dbI
 .Pp
 internal option for use by
 .Xr catman 1
 .br
 .Nm man :
 .dbI
 .Pp
 reformat source page even if cat page exists
 .br
 .Nm man :
 .man15e
 .Pp
 disable terminal color output in
 .Xr grotty 1
 .br
 .Nm groff :
 .No groff-1.18.0 Pq Oct 4, 2001
 .Pp
 recreate nroff versions from SGML sources
 .br
 .Nm catman :
 .No Solaris 9-11
 .Pp
 .Bq obsolete
 postprocess with
 .Xr col 1
 .br
 .Nm man :
 .At3 ,
 .At5
 .It Fl D
 reset whatever was set with
 .Ev MANOPT
 .br
 .Nm man :
 .dbI
 .Pp
 print debugging info in addition to manual page
 .br
 .Nm man :
 .man15e
 .Pp
 set default input encoding for
 .Xr preconv 1
 .br
 .Nm groff :
 .No groff-1.20 Pq August 20, 2008
 .Pp
 display all files added to
 .Xr mandoc.db 5
 .br
 .Nm makewhatis :
 .Ox 5.6 Pq April 18, 2014
 .It Fl d
 define a user-defined string
 .br
 .Nm groff :
 .g04
 .Pp
 print debugging information
 .br
 .Nm man :
 .Eaton ;
 .Fx , No man-db , man-1.6 , illumos , Solaris 9-11
 .br
 .Nm manpath :
 .Eaton ;
 .Fx , No man-db
 .br
 .Nm apropos , whatis :
 .dbI ;
 .Fx
 .br
 .Nm mandb , catman :
 .dbI
 .Pp
 remove and re-add a file to
 .Xr mandoc.db 5
 .br
 .Nm makewhatis :
 .Ox 2.7 Pq Feb 3, 2000
 .Pp
 .Bq superseded by Fl l
 interpret arguments as file names
 .br
 .Nm man :
 .At3 ,
 .At5
 .It Fl E
 inhibit all error messages
 .br
 .Nm groff :
 .g04
 .Pp
 select output encoding
 .br
 .Nm man :
 .No man-db Pq Dec 23, 2001
 .It Fl e
 preprocess with
 .Xr eqn 7
 .br
 .Nm man :
 .At7
 .br
 .Nm groff :
 .g04
 .Pp
 adjust text to left and right margins
 .br
 .Nm nroff :
 .At7
 .Pp
 use exact matching
 .br
 .Nm apropos , whatis :
 .dbI
 .Pp
 restrict search by section extension
 .br
 .Nm man :
 .No man-db-2.3.5 Pq April 21, 1995
 .It Fl F
 use alternate font directory
 .br
 .Nm troff :
 .Bx 4.2 Pq September 1983
 .br
 .Nm groff :
 .g04
 .Pp
 preformat only, do not display
 .br
 .Nm man :
 .No man-1.5g Pq April 7, 1999
 .Pp
 force searching dirs, do not use index (default)
 .br
 .Nm man :
 .No illumos , Solaris 9-11
 .It Fl f
 .Xr whatis 1
 mode
 .br
 .Nm man :
 .Bx4 ,
 .Eaton ;
 .Ox , Fx , No man-db , man-1.6
 .br
 .Nm apropos , whatis :
 .No man-db Pq Dec 2, 2010 ,
 .Ox 5.7 Pq August 27, 2014
 .br
 .Nm mandoc :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 set the default font family
 .br
 .Nm groff :
 .g04
 .Pp
 force formatting even if cat page is newer
 .br
 .Nm catman :
 .Fx Pq March 15, 1995
 .Pp
 update only the entries for the given file
 .br
 .Nm mandb :
 .No man-db Pq Feb 21, 2003
 .Pp
 force rebuilding the database from scratch
 .br
 .Nm makemandb :
 .Nx Pq Feb 7, 2012
 .Pp
 locate manual page related to given file name
 .br
 .Nm man :
 .No illumos , Solaris 9-11
 .Pp
 .Bq obsolete hardware
 do not feed out paper nor stop phototypesetter
 .br
 .Nm troff :
 .At7
 .It Fl G
 preprocess with
 .Xr grap 1
 .br
 .Nm groff :
 .No groff-1.16 Pq May 1, 2000
 .It Fl g
 produce a global manpath
 .br
 .Nm manpath :
 .No man-db-2.2a7 Pq Nov 16, 1994
 .Pp
 preprocess with
 .Xr grn 1
 .br
 .Nm groff :
 .No groff-1.16 Pq Feb 20, 2000
 .Pp
 .Bq obsolete hardware
 output to a GCOS phototypesetter
 .br
 .Nm troff :
 .At7
 .Pp
 .Bq obsolete hardware
 output to a DASI 300 terminal in 12-pitch mode
 .br
 .Nm man :
 .PWB
 .It Fl H
 read hyphenation patterns from the given file
 .br
 .Nm groff :
 .g04
 .Pp
 produce HTML output
 .br
 .Nm man :
 .No man-db-1.3.12 to 1.3.17 Pq not before 1996, not after 2001
 .Pp
 use program to render HTML files as text
 .br
 .Nm man :
 .No man-1.6 Pq June 24, 2005
 .It Fl h
 print a help message and exit
 .br
 .Nm groff :
 .g04
 .br
 .Nm man :
 .Eaton ;
 .Fx , No man-db , man-1.6
 .br
 .Nm manpath :
 .Eaton ;
 .Fx , No man-db
 .br
 .Nm apropos , whatis , mandb , catman :
 .dbI
 .Pp
 display the SYNOPSIS lines only
 .br
 .Nm man :
 .Bx 4.3 Net/2 Pq August 20, 1991 ;
 .Ox , Nx
 .br
 .Nm apropos , whatis , mandoc :
 .Ox 5.7 Pq Sep 3, 2014
 .Pp
 turn on HTML formatting
 .br
 .Nm apropos :
 .Nx Pq Apr 2, 2013
 .Pp
 .Bq obsolete
 replace spaces by tabs in the output
 .br
 .Nm roff , nroff :
 .At7
 .It Fl I
 input file search path for
 .Xr soelim 1
 .br
 .Nm groff :
 .No groff-1.12 Pq Sep 11, 1999
 .Pp
 respect case when matching manual page names
 .br
 .Nm man , catman :
 .No man-db Pq Apr 21, 2002
 .Pp
 input options, in particular default operating system name
 .br
 .Nm mandoc :
 .Ox 5.2 Pq May 24, 2012
 .br
 .Nm man , apropos , whatis :
 .Ox 5.7 Pq August 27, 2014
 .It Fl i
 read standard input after the input files are exhausted
 .br
 .Nm nroff , troff :
 .At7
 .br
 .Nm groff :
 .g04
 .Pp
 ignore case when matching manual page names
 .br
 .Nm man , catman :
 .No man-db Pq Apr 21, 2002
 .Pp
 turn on terminal escape code formatting
 .br
 .Nm apropos :
 .Nx Pq March 29, 2013
 .It Fl J
 preprocess with
 .Xr gideal 1
 .br
 .Nm groff :
 .No groff-1.22.3 Pq June 17, 2014
 .It Fl j
 preprocess with
 .Xr chem 1
 .br
 .Nm groff :
 .No groff-1.22 Pq Jan 22, 2011
 .It Fl K
 source code full text search
 .br
 .Nm man :
 .man15e ,
 .No man-db Pq June 28, 2009 ;
 .No Solaris 11
 .Pp
 input encoding
 .br
 .Nm groff :
 .No groff-1.20 Pq Dec 31, 2005
 .br
 .Nm man , apropos , whatis , mandoc :
 .Ox 5.7 Pq Oct 30, 2014
 .It Fl k
 .Xr apropos 1
 mode
 .br
 .Nm man :
 .Bx4 ,
 .Eaton ;
 .No POSIX , Ox , Fx , Nx , No man-db , man-1.6 , illumos , Solaris 9-11
 .br
 .Nm apropos , whatis , mandoc :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 ignore formatting errors
 .br
 .Nm catman :
 .Nx Pq April 26, 1994
 .Pp
 preprocess with
 .Xr preconv 1
 .br
 .Nm groff :
 .No groff-1.20 Pq Dec 31, 2005
 .Pp
 .Bq obsolete hardware
 display on a Tektronix 4014 terminal
 .br
 .Nm man :
 .At7
 .It Fl L
 pass argument to the spooler
 .br
 .Nm groff :
 .No groff-0.6 Pq Sep 14, 1990
 .Pp
 use alternate
 .Xr locale 1
 .br
 .Nm man , apropos , whatis :
 .No before man-db-2.2a13 Pq before Dec 15, 1994
 .Pp
 print list of locales
 .br
 .Nm manpath :
 .Fx Pq Nov 23, 1999
 .Pp
 use
 .Xr locale 1
 specified in the environment
 .br
 .Nm catman :
 .Fx Pq May 18, 2002
 .It Fl l
 spool the output
 .br
 .Nm groff :
 .g04
 .Pp
 interpret arguments as file names
 .br
 .Nm man :
 .No before man-2.2a7 Pq before Nov 16, 1994 ,
 .Ox 5.7 Pq Aug 30, 2014
 .br
 .Nm apropos , whatis , mandoc :
 .Ox 5.7 Pq Aug 30, 2014
 .Pp
 do not trim output to the terminal width
 .br
 .Nm apropos , whatis :
 .No man-db Pq Aug 19, 2007
 .Pp
 only parse NAME sections
 .br
 .Nm makemandb :
 .Nx Pq Feb 7, 2012
 .Pp
 legacy mode: search Nm,Nd, no context or formatting
 .br
 .Nm apropos :
 .Nx Pq March 29, 2013
 .Pp
 list all manual pages matching name within the search path
 .br
 .Nm man :
 .No illumos , Solaris 9-11
 .It Fl M
 override manual page search path
 .br
 .Nm man :
 .Bx43 ,
 .Eaton ;
 .Ox , Fx , Nx , No man-db , man-1.6 , illumos , Solaris 9-11
 .br
 .Nm apropos , whatis :
 .Bx43 ,
 .No before man-db-2.2a14 Pq before Dec 16, 1994 ;
 .Ox , No illumos
 .br
 .Nm catman :
 .dbI ;
 .Nx Pq July 27, 1993 ,
 .No Solaris 9-11
 .br
 .Nm mandoc :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 prepend to macro file search path
 .br
 .Nm groff :
 .g04
 .Pp
 do not show the context of the match
 .br
 .Nm apropos :
 .Nx Pq May 22, 2016
 .It Fl m
 specify input macro language
 .br
 .Nm nroff , troff :
 .At7
 .br
 .Nm groff :
 .g04
 .br
 .Nm mandoc :
 .Ox 4.8 Pq April 6, 2009
 .Pp
 augment manual page search path
 .br
 .Nm man , apropos , whatis :
 .Bx 4.3 Reno Pq June 1990 ;
 .Ox , Nx
 .br
 .Nm catman :
 .Nx Pq Apr 4, 1999
 .br
 .Nm mandoc :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 override operating system
 .br
 .Nm man :
 .Eaton ;
 .No man-db , man-1.6
 .br
 .Nm apropos , whatis , manpath :
 .dbI
 .Pp
 override architecture
 .br
 .Nm man :
 .Fx Pq Jan 11, 2002
 .Pp
 show the context of the match
 .br
 .Nm apropos :
 .Nx Pq May 22, 2016
 .It Fl N
 do not allow newlines between
 .Xr eqn 7
 delimiters
 .br
 .Nm groff :
 .No groff-1.01 Pq Feb 21, 1991
 .It Fl n
 specify a page number for the first page
 .br
 .Nm troff :
 .At7
 .br
 .Nm groff :
 .g04
 .Pp
 .Xr nroff 1
 output mode
 .br
 .Nm man :
 .At7
 .Pp
 do not create the
 .Xr whatis 1
 database
 .br
 .Nm catman :
 .Nx Pq July 27, 1993
 .Pp
 print commands instead of executing them
 .br
 .Nm catman :
 .Fx Pq May 18, 2002 ,
 .No Solaris 9-11
 .Pp
 limit the number of results
 .br
 .Nm apropos :
 .Nx Pq Feb 7, 2012
 .Pp
 dry run simulating
 .Xr mandoc.db 5
 creation
 .br
 .Nm makewhatis :
 .Ox 5.6 Pq April 18, 2014
 .It Fl O
 output options
 .br
 .Nm mandoc :
 .Ox 4.8 Pq Oct 27, 2009
 .br
 .Nm man , apropos , whatis :
 .Ox 5.7 Pq August 27, 2014
 .It Fl o
 select pages by numbers
 .br
 .Nm nroff , troff :
 .At7
 .br
 .Nm groff :
 .g04
 .Pp
 force use of non-localized manual pages
 .br
 .Nm man :
 .Fx Pq June 7, 1999
 .Pp
 optimize index for speed and disk space
 .br
 .Nm makemandb :
 .Nx Pq Feb 7, 2012
 .It Fl P
 pass argument to postprocessor
 .br
 .Nm groff :
 .No groff-0.6 Pq Sep 14, 1990
 .Pp
 use specified pager
 .br
 .Nm man :
 .Eaton ;
 .Fx , No man-db , man-1.6
 .Pp
 turn on pager formatting
 .br
 .Nm apropos :
 .Nx Pq Apr 2, 2013
 .It Fl p
 preprocess with
 .Xr pic 1
 .br
 .Nm groff :
 .g04
 .Pp
 use the given list of preprocessors
 .br
 .Nm man :
 .Eaton ;
 .Fx , No man-db , man-1.6
 .Pp
 dry run, display commands instead of executing them
 .br
 .Nm catman :
 .Nx Pq July 27, 1993 ,
 .Fx Pq March 15, 1995 to May 18, 2002 ,
 .No Solaris 9-11
 .Pp
 print warnings when building
 .Xr mandoc.db 5
 .br
 .Nm makewhatis :
 .Ox 2.7 Pq April 23, 2000
 .Pp
 do not look for deleted manual pages
 .br
 .Nm mandb :
 .No man-db Pq June 28, 2001
 .Pp
 print the search path for manual pages
 .br
 .Nm man :
 .Nx Pq June 14 , 2011
 .Pp
 turn on pager formatting and pipe through pager
 .br
 .Nm apropos :
 .Nx Pq Feb 7, 2012
 .Pp
 .Bq obsolete hardware
 set phototypesetter point size
 .br
 .Nm troff :
 .At7
 .It Fl Q
 print only fatal error messages
 .br
 .Nm makemandb :
 .Nx Pq Aug 29, 2012
 .Pp
 quick mode of
 .Xr mandoc.db 5
 creation
 .br
 .Nm makewhatis :
 .Ox 5.6 Pq April 18, 2014
 .It Fl q
 invoke the simultaneous input-output mode of the .rd request
 .br
 .Nm nroff , troff :
 .At7
 .Pp
 issue no warnings
 .br
 .Nm manpath :
 .Eaton ;
 .Fx , No man-db
 .br
 .Nm mandb :
 .dbI
 .Pp
 print only warnings and errors, no status updates
 .br
 .Nm makemandb :
 .Nx Pq Aug 29, 2012
 .It Fl R
 postprocess with
 .Xr refer 1
 .br
 .Nm groff :
 .No groff-1.02 Pq June 2, 1991
 .Pp
 recode to the specified encoding
 .br
 .Nm man :
 .No man-db Pq Dec 31, 2007
 .It Fl r
 set number register
 .br
 .Nm nroff , troff :
 .At7
 .br
 .Nm groff :
 .g04
 .Pp
 scan for and remove junk files
 .br
 .Nm catman :
 .Fx Pq March 31, 1995
 .Pp
 set
 .Xr less 1
 prompt
 .br
 .Nm man :
 .No man-db-2.3.5 Pq April 21, 1995
 .Pp
 use regular expression matching
 .br
 .Nm apropos , whatis :
 .No man-db-2.3.5 Pq April 21, 1995
 .Pp
 turn off formatting
 .br
 .Nm apropos :
 .Nx Pq Feb 10, 2013
 .Pp
 check for formatting errors, do not display
 .br
 .Nm man :
 .No illumos , Solaris 9-11
 .It Fl S
 manual section search list
 .br
 .Nm man :
 .Eaton ;
 .Fx , No man-db , man-1.6
 .Pp
 safer mode
 .br
 .Nm groff :
 .No groff-1.10 Pq May 17, 1994
 .Pp
 restrict architecture
 .br
 .Nm man :
 .Ox 2.3 Pq March 9, 1998 ,
 .Nx Pq May 27, 2000
 .br
 .Nm apropos :
 .Ox 4.5 Pq Dec 24, 2008 ,
 .Nx Pq May 8, 2009
 .br
 .Nm whatis :
 .Ox 5.6 Pq April 18, 2014
 .br
 .Nm mandoc :
 .Ox 5.7 Pq August 27, 2014
 .It Fl s
 preprocess with
 .Xr soelim 1
 .br
 .Nm groff :
 .g04
 .Pp
 silent mode, do not echo commands
 .br
 .Nm catman :
 .Nx Pq April 26, 1994
 .Pp
 restrict section
 .br
 .Nm makewhatis :
 .man15g
 .br
 .Nm man :
 .Ox 2.3 Pq March 9, 1998 ,
 .Nx Pq June 12, 2000 ;
 .No illumos , Solaris 9-11
 .br
 .Nm apropos :
 .No man-db Pq Nov 16, 2003 ,
 .Ox 4.5 Pq Dec 24, 2008 ,
 .Nx Pq May 8, 2009 ;
 .No illumos
 .br
 .Nm whatis :
 .No man-db Pq Nov 16, 2003 ,
 .Ox 5.6 Pq April 18, 2014 ;
 .No illumos
 .br
 .Nm mandoc :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 do not look for stray cats
 .br
 .Nm mandb :
 .dbI
 .Pp
 .Bq SysV compat, recommends Fl S
 manual section search list
 .br
 .Nm man :
 .No man-db Pq Jan 1, 2008
 .Pp
 .Bq superseded by Fl h
 display the SYNOPSIS lines only
 .br
 .Nm man :
 .PWB
 .Pp
 .Bq obsolete hardware
 pause before each page for paper manipulation
 .br
 .Nm roff :
 .At7
 .Pp
 .Bq obsolete hardware
 .Xr troff 1
 output mode, small format
 .br
 .Nm man :
 .At3 ,
 .At5
 .It Fl T
 select terminal output format
 .br
 .Nm nroff :
 .At7
 .br
 .Nm man :
 .At3 ,
 .At5 ,
 .dbI ,
 .Ox 5.7 Pq August 27, 2014
 .br
 .Nm groff :
 .g04
 .br
 .Nm mandoc :
 .Ox 4.8 Pq April 6, 2009
 .br
 .Nm apropos , whatis :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 use UTF-8 for
 .Xr mandoc.db 5
 .br
 .Nm makewhatis :
 .Ox 5.6 Pq April 18, 2014
 .Pp
 .Bq superseded by Fl m
 use other macro package
 .br
 .Nm man , catman :
 .No Solaris 9-11
 .It Fl t
 .Xr troff 1
 output mode
 .br
 .Nm man :
 .PWB ,
 .At7 ,
 .Bx 2 Pq May 10, 1979 ,
 .At3 ,
 .At5 ,
 .Eaton ;
 .Fx , No man-db , man-1.6 , illumos , Solaris 9-11
 .br
 .Nm catman :
 .No Solaris 9-11
 .Pp
 preprocess with
 .Xr tbl 7
 .br
 .Nm groff :
 .g04
 .Pp
 check manual pages in the hierarchy
 .br
 .Nm mandb :
 .No man-db-1.3.12 to 1.3.17 Pq not before 1996, not after 2001
 .Pp
 check files for problems related to
 .Xr mandoc.db 5
 .br
 .Nm makewhatis :
 .Ox 2.7 Pq April 23, 2000
 .It Fl U
 unsafe mode
 .br
 .Nm groff :
 .No groff-1.12 Pq Dec 13, 1999
 .It Fl u
 update database
 .br
 .Nm makewhatis :
 .Pq not before 1992, not after 1995
 .Pp
 create user databases only
 .br
 .Nm mandb :
 .dbI
 .Pp
 update database cache (requires suid)
 .br
 .Nm man :
 .No before man-db-2.2a10 Pq before Dec 6, 1994
 .Pp
 remove files from
 .Xr mandoc.db 5
 .br
 .Nm makewhatis :
 .Ox 3.4 Pq July 9, 2003
 .It Fl V
 print the pipeline on stdout instead of executing it
 .br
 .Nm groff :
 .No groff-0.6 Pq Sep 2, 1990
 .Pp
 print version information
 .br
 .Nm man , apropos , whatis , mandb , catman , manpath :
 .dbI
 .It Fl v
 print version number
 .br
 .Nm groff :
 .g04
 .Pp
 verbose mode
 .br
 .Nm catman :
-.Fx Pq March 15, 1995
+.Fx Pq March 15, 1995 ,
+.No mandoc Pq June 30, 2025
 .br
 .Nm makewhatis :
 .man15g
 .br
 .Nm apropos , whatis :
 .No man-db Pq Dec 29, 2002
 .Pp
 print the name of every parsed file
 .br
 .Nm makemandb :
 .Nx Pq Feb 7, 2012
 .Pp
 .Bq obsolete hardware
 produce output on the Versatec printer
 .br
 .Nm man :
 .PWB
 .It Fl W
 disable the named warning
 .br
 .Nm groff :
 .No groff-0.5 Pq August 14, 1990
 .Pp
 list pathnames without additional information
 .br
 .Nm man :
 .man15e
 .Pp
 list pathnames of cat files
 .br
 .Nm man :
 .No man-db Pq Aug 13, 2002
 .Pp
 minimum message level to display
 .br
 .Nm mandoc :
 .Ox 4.8 Pq April 6, 2009
 .br
 .Nm man , apropos , whatis :
 .Ox 5.7 Pq August 27, 2014
 .It Fl w
 list pathnames
 .br
 .Nm man :
 .At7 ,
 .At3 ,
 .At5 ,
 .Eaton ;
 .Ox , Fx , Nx , No man-db , man-1.6
 .br
 .Nm apropos , whatis , mandoc :
 .Ox 5.7 Pq August 27, 2014
 .Pp
 enable the named warning
 .br
 .Nm groff :
 .No groff-0.5 Pq August 14, 1990
 .Pp
 only create the
 .Xr whatis 1
 database
 .br
 .Nm catman :
 .Nx Pq July 27, 1993 ,
 .No Solaris 9-11
 .Pp
 use wildcard matching
 .br
 .Nm apropos , whatis :
 .No man-db-2.3.5 Pq April 21, 1995
 .Pp
 use manpath obtained from man --path
 .br
 .Nm makewhatis :
 .man15g
 .Pp
 update the
 .Xr whatis 1
 database
 .br
 .Nm man :
 .No illumos
 .Pp
 .Bq obsolete hardware
 wait until the phototypesetter is available
 .br
 .Nm troff :
 .At7
 .It Fl X
 display with
 .Xr gxditview 1
 .br
 .Nm groff :
 .No groff-1.06 Pq Sep 1, 1992
 .br
 .Nm man :
 .dbI
 .It Fl y
 use the non-compacted version of the macros
 .br
 .Nm man :
 .At3 ,
 .At5
 .It Fl Z
 do not run preprocessors
 .br
 .Nm groff :
 .g04
 .br
 .Nm man :
 .No man-db-2.2a5 Pq Nov 10, 1994
 .It Fl z
 suppress formatted output from
 .Xr troff 1 ,
 print only error messages
 .br
 .Nm groff :
 .g04
 .It Fl 7
 ASCII output mode
 .br
 .Nm man :
 .No man-db-2.3.5 Pq April 21, 1995
 .It Fl \&?
 print a help message and exit
 .br
 .Nm groff :
 .g04
 .br
 .Nm man , manpath :
 .Eaton ;
 .Fx , No man-db
 .br
 .Nm apropos , whatis , mandb , catman :
 .dbI
 .El
 .Pp
 Multi-letter options:
 .Bl -tag -width Ds
 .It Fl hp
 .Bq obsolete hardware
 output to a Hewlett Packard terminal
 .br
 .Nm man :
 .PWB
 .It Fl 12
 .Bq obsolete hardware
 use 12-pitch for certain terminals
 .br
 .Nm man :
 .At3 ,
 .At5
 .It Fl 450
 .Bq obsolete hardware
 output to a DASI 450 terminal
 .br
 .Nm man :
 .PWB
 .El
 .Pp
 In
 .At v3 ,
 .Xr man 1
 had no options.
 .br
 The syntax was:
 .Sy man Ar name Op Ar section
 .Pp
 In
 .At v4 ,
 .br
 the syntax changed to:
 .Sy man Oo Ar section Oc Op Ar name ...
 .Sh AUTHORS
 This information was assembled by
 .An Ingo Schwarze Aq Mt schwarze@openbsd.org
 using
 .Bl -bullet -compact
 .It
 the Unix Archive of the Unix Heritage Society
 .It
 the CSRG Archive CD-ROMs
 .It
 the
 .Fx
 SVN repository
 .It
 the
 .Ox
 CVS repository
 .It
 the
 .Nx
 CVS repository
 .It
 the GNU roff (groff) git repository
 .It
 the 4.3BSD-Net/2 groff CHANGES file (Oct 1990 to March 1991)
 .It
 the 4.3BSD-Net/2 groff ChangeLog file (July 1990 to March 1991)
 .It
 the man-db CVS and git repositories (since April 2001)
 .It
 the man-db NEWS file (April 1995 to Dec 2016)
 .It
 the man-db ChangeLog-2013 file (Nov 1994 to Dec 2013)
 .It
 release tarballs man-1.5g (July 1998) to man-1.5p (Jan 2005),
 man-1.6 (June 2005), and man-1.6a to man-1.6g (Dec 2010)
 .It
 a makewhatis release tarball without version number from 1995
 .It
 the illumos manual pages on the WWW
 .It
 and Solaris 11, SunOS 5.10, and SunOS 5.9 machines at opencsw.org.
 .El
diff --git a/contrib/mandoc/man_html.c b/contrib/mandoc/man_html.c
index 6784171af1e6..fc593be1112c 100644
--- a/contrib/mandoc/man_html.c
+++ b/contrib/mandoc/man_html.c
@@ -1,686 +1,695 @@
-/* $Id: man_html.c,v 1.187 2023/10/24 20:53:12 schwarze Exp $ */
+/* $Id: man_html.c,v 1.188 2025/06/26 17:06:34 schwarze Exp $ */
 /*
- * Copyright (c) 2013-15,2017-20,2022-23 Ingo Schwarze 
+ * Copyright (c) 2013-2020,2022-2023,2025 Ingo Schwarze 
  * Copyright (c) 2008-2012, 2014 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
  * HTML formatter for man(7) used by mandoc(1).
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "roff.h"
 #include "man.h"
 #include "out.h"
 #include "html.h"
 #include "main.h"
 
 #define	MAN_ARGS	  const struct roff_meta *man, \
 			  struct roff_node *n, \
 			  struct html *h
 
 struct	man_html_act {
 	int		(*pre)(MAN_ARGS);
 	int		(*post)(MAN_ARGS);
 };
 
 static	void		  print_man_head(const struct roff_meta *,
 				struct html *);
 static	void		  print_man_nodelist(MAN_ARGS);
 static	void		  print_man_node(MAN_ARGS);
 static	char		  list_continues(const struct roff_node *,
 				const struct roff_node *);
 static	int		  man_B_pre(MAN_ARGS);
 static	int		  man_IP_pre(MAN_ARGS);
 static	int		  man_I_pre(MAN_ARGS);
 static	int		  man_MR_pre(MAN_ARGS);
 static	int		  man_OP_pre(MAN_ARGS);
 static	int		  man_PP_pre(MAN_ARGS);
 static	int		  man_RS_pre(MAN_ARGS);
 static	int		  man_SH_pre(MAN_ARGS);
 static	int		  man_SM_pre(MAN_ARGS);
 static	int		  man_SY_pre(MAN_ARGS);
 static	int		  man_UR_pre(MAN_ARGS);
 static	int		  man_alt_pre(MAN_ARGS);
 static	int		  man_ign_pre(MAN_ARGS);
 static	int		  man_in_pre(MAN_ARGS);
 static	void		  man_root_post(const struct roff_meta *,
 				struct html *);
 static	void		  man_root_pre(const struct roff_meta *,
 				struct html *);
 
 static	const struct man_html_act man_html_acts[MAN_MAX - MAN_TH] = {
 	{ NULL, NULL }, /* TH */
 	{ man_SH_pre, NULL }, /* SH */
 	{ man_SH_pre, NULL }, /* SS */
 	{ man_IP_pre, NULL }, /* TP */
 	{ man_IP_pre, NULL }, /* TQ */
 	{ man_PP_pre, NULL }, /* LP */
 	{ man_PP_pre, NULL }, /* PP */
 	{ man_PP_pre, NULL }, /* P */
 	{ man_IP_pre, NULL }, /* IP */
 	{ man_PP_pre, NULL }, /* HP */
 	{ man_SM_pre, NULL }, /* SM */
 	{ man_SM_pre, NULL }, /* SB */
 	{ man_alt_pre, NULL }, /* BI */
 	{ man_alt_pre, NULL }, /* IB */
 	{ man_alt_pre, NULL }, /* BR */
 	{ man_alt_pre, NULL }, /* RB */
 	{ NULL, NULL }, /* R */
 	{ man_B_pre, NULL }, /* B */
 	{ man_I_pre, NULL }, /* I */
 	{ man_alt_pre, NULL }, /* IR */
 	{ man_alt_pre, NULL }, /* RI */
 	{ NULL, NULL }, /* RE */
 	{ man_RS_pre, NULL }, /* RS */
 	{ man_ign_pre, NULL }, /* DT */
 	{ man_ign_pre, NULL }, /* UC */
 	{ man_ign_pre, NULL }, /* PD */
 	{ man_ign_pre, NULL }, /* AT */
 	{ man_in_pre, NULL }, /* in */
 	{ man_SY_pre, NULL }, /* SY */
 	{ NULL, NULL }, /* YS */
 	{ man_OP_pre, NULL }, /* OP */
 	{ NULL, NULL }, /* EX */
 	{ NULL, NULL }, /* EE */
 	{ man_UR_pre, NULL }, /* UR */
 	{ NULL, NULL }, /* UE */
 	{ man_UR_pre, NULL }, /* MT */
 	{ NULL, NULL }, /* ME */
 	{ man_MR_pre, NULL }, /* MR */
 };
 
 
 void
 html_man(void *arg, const struct roff_meta *man)
 {
 	struct html		*h;
 	struct roff_node	*n;
 	struct tag		*t;
 
 	h = (struct html *)arg;
 	n = man->first->child;
 
 	if ((h->oflags & HTML_FRAGMENT) == 0) {
 		print_gen_decls(h);
 		print_otag(h, TAG_HTML, "");
 		t = print_otag(h, TAG_HEAD, "");
 		print_man_head(man, h);
 		print_tagq(h, t);
 		if (n != NULL && n->type == ROFFT_COMMENT)
 			print_gen_comment(h, n);
 		print_otag(h, TAG_BODY, "");
 	}
 
 	man_root_pre(man, h);
 	t = print_otag(h, TAG_MAIN, "c", "manual-text");
 	print_man_nodelist(man, n, h);
 	print_tagq(h, t);
 	man_root_post(man, h);
 	print_tagq(h, NULL);
 }
 
 static void
 print_man_head(const struct roff_meta *man, struct html *h)
 {
 	char	*cp;
 
 	print_gen_head(h);
 	mandoc_asprintf(&cp, "%s(%s)", man->title, man->msec);
 	print_otag(h, TAG_TITLE, "");
 	print_text(h, cp);
 	free(cp);
 }
 
 static void
 print_man_nodelist(MAN_ARGS)
 {
 	while (n != NULL) {
 		print_man_node(man, n, h);
 		n = n->next;
 	}
 }
 
 static void
 print_man_node(MAN_ARGS)
 {
 	struct tag	*t;
 	int		 child;
 
 	if (n->type == ROFFT_COMMENT || n->flags & NODE_NOPRT)
 		return;
 
 	if ((n->flags & NODE_NOFILL) == 0)
 		html_fillmode(h, ROFF_fi);
 	else if (html_fillmode(h, ROFF_nf) == ROFF_nf &&
 	    n->tok != ROFF_fi && n->flags & NODE_LINE &&
 	    (n->prev == NULL || n->prev->tok != MAN_YS))
 		print_endline(h);
 
 	child = 1;
 	switch (n->type) {
 	case ROFFT_TEXT:
 		if (*n->string == '\0') {
 			print_endline(h);
 			return;
 		}
 		if (*n->string == ' ' && n->flags & NODE_LINE &&
 		    (h->flags & HTML_NONEWLINE) == 0)
 			print_otag(h, TAG_BR, "");
 		else if (n->flags & NODE_DELIMC)
 			h->flags |= HTML_NOSPACE;
 		t = h->tag;
 		t->refcnt++;
 		print_text(h, n->string);
 		break;
 	case ROFFT_EQN:
 		t = h->tag;
 		t->refcnt++;
 		print_eqn(h, n->eqn);
 		break;
 	case ROFFT_TBL:
 		/*
 		 * This will take care of initialising all of the table
 		 * state data for the first table, then tearing it down
 		 * for the last one.
 		 */
 		print_tbl(h, n->span);
 		return;
 	default:
 		/*
 		 * Close out scope of font prior to opening a macro
 		 * scope.
 		 */
 		if (h->metac != ESCAPE_FONTROMAN) {
 			h->metal = h->metac;
 			h->metac = ESCAPE_FONTROMAN;
 		}
 
 		/*
 		 * Close out the current table, if it's open, and unset
 		 * the "meta" table state.  This will be reopened on the
 		 * next table element.
 		 */
 		if (h->tblt != NULL)
 			print_tblclose(h);
 		t = h->tag;
 		t->refcnt++;
 		if (n->tok < ROFF_MAX) {
 			roff_html_pre(h, n);
 			t->refcnt--;
 			print_stagq(h, t);
 			return;
 		}
 		assert(n->tok >= MAN_TH && n->tok < MAN_MAX);
 		if (man_html_acts[n->tok - MAN_TH].pre != NULL)
 			child = (*man_html_acts[n->tok - MAN_TH].pre)(man,
 			    n, h);
 		break;
 	}
 
 	if (child && n->child != NULL)
 		print_man_nodelist(man, n->child, h);
 
 	/* This will automatically close out any font scope. */
 	t->refcnt--;
 	if (n->type == ROFFT_BLOCK &&
 	    (n->tok == MAN_IP || n->tok == MAN_TP || n->tok == MAN_TQ)) {
 		t = h->tag;
 		while (t->tag != TAG_DL && t->tag != TAG_UL)
 			t = t->next;
 		/*
 		 * Close the list if no further item of the same type
 		 * follows; otherwise, close the item only.
 		 */
 		if (list_continues(n, roff_node_next(n)) == '\0') {
 			print_tagq(h, t);
 			t = NULL;
 		}
 	}
 	if (t != NULL)
 		print_stagq(h, t);
 }
 
 static void
 man_root_pre(const struct roff_meta *man, struct html *h)
 {
 	struct tag	*t;
 	char		*title;
 
 	assert(man->title);
 	assert(man->msec);
 	mandoc_asprintf(&title, "%s(%s)", man->title, man->msec);
 
 	t = print_otag(h, TAG_DIV, "cr?", "head", "doc-pageheader",
 	    "aria-label", "Manual header line");
 
 	print_otag(h, TAG_SPAN, "c", "head-ltitle");
 	print_text(h, title);
 	print_stagq(h, t);
 
 	print_otag(h, TAG_SPAN, "c", "head-vol");
 	if (man->vol != NULL)
 		print_text(h, man->vol);
 	print_stagq(h, t);
 
 	print_otag(h, TAG_SPAN, "c", "head-rtitle");
 	print_text(h, title);
 	print_tagq(h, t);
 	free(title);
 }
 
 static void
 man_root_post(const struct roff_meta *man, struct html *h)
 {
 	struct tag	*t;
+	char		*title;
+
+	assert(man->title != NULL);
+	if (man->msec == NULL)
+		title = mandoc_strdup(man->title);
+	else
+		mandoc_asprintf(&title, "%s(%s)", man->title, man->msec);
 
 	t = print_otag(h, TAG_DIV, "cr?", "foot", "doc-pagefooter",
 	    "aria-label", "Manual footer line");
 
 	print_otag(h, TAG_SPAN, "c", "foot-left");
+	if (man->os != NULL)
+		print_text(h, man->os);
 	print_stagq(h, t);
 
 	print_otag(h, TAG_SPAN, "c", "foot-date");
 	print_text(h, man->date);
 	print_stagq(h, t);
 
-	print_otag(h, TAG_SPAN, "c", "foot-os");
-	if (man->os != NULL)
-		print_text(h, man->os);
+	print_otag(h, TAG_SPAN, "c", "foot-right");
+	print_text(h, title);
 	print_tagq(h, t);
+	free(title);
 }
 
 static int
 man_SH_pre(MAN_ARGS)
 {
 	const char	*class;
 	enum htmltag	 tag;
 
 	if (n->tok == MAN_SH) {
 		tag = TAG_H2;
 		class = "Sh";
 	} else {
 		tag = TAG_H3;
 		class = "Ss";
 	}
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		print_otag(h, TAG_SECTION, "c", class);
 		break;
 	case ROFFT_HEAD:
 		print_otag_id(h, tag, class, n);
 		break;
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static int
 man_alt_pre(MAN_ARGS)
 {
 	const struct roff_node	*nn;
 	struct tag	*t;
 	int		 i;
 	enum htmltag	 fp;
 
 	for (i = 0, nn = n->child; nn != NULL; nn = nn->next, i++) {
 		switch (n->tok) {
 		case MAN_BI:
 			fp = i % 2 ? TAG_I : TAG_B;
 			break;
 		case MAN_IB:
 			fp = i % 2 ? TAG_B : TAG_I;
 			break;
 		case MAN_RI:
 			fp = i % 2 ? TAG_I : TAG_MAX;
 			break;
 		case MAN_IR:
 			fp = i % 2 ? TAG_MAX : TAG_I;
 			break;
 		case MAN_BR:
 			fp = i % 2 ? TAG_MAX : TAG_B;
 			break;
 		case MAN_RB:
 			fp = i % 2 ? TAG_B : TAG_MAX;
 			break;
 		default:
 			abort();
 		}
 
 		if (i)
 			h->flags |= HTML_NOSPACE;
 
 		if (fp != TAG_MAX)
 			t = print_otag(h, fp, "");
 
 		print_text(h, nn->string);
 
 		if (fp != TAG_MAX)
 			print_tagq(h, t);
 	}
 	return 0;
 }
 
 static int
 man_SM_pre(MAN_ARGS)
 {
 	print_otag(h, TAG_SMALL, "");
 	if (n->tok == MAN_SB)
 		print_otag(h, TAG_B, "");
 	return 1;
 }
 
 static int
 man_PP_pre(MAN_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		break;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		if (n->child != NULL &&
 		    (n->child->flags & NODE_NOFILL) == 0)
 			print_otag(h, TAG_P, "c",
 			    n->tok == MAN_HP ? "Pp HP" : "Pp");
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static char
 list_continues(const struct roff_node *n1, const struct roff_node *n2)
 {
 	const char *s1, *s2;
 	char c1, c2;
 
 	if (n1 == NULL || n1->type != ROFFT_BLOCK ||
 	    n2 == NULL || n2->type != ROFFT_BLOCK)
 		return '\0';
 	if ((n1->tok == MAN_TP || n1->tok == MAN_TQ) &&
 	    (n2->tok == MAN_TP || n2->tok == MAN_TQ))
 		return ' ';
 	if (n1->tok != MAN_IP || n2->tok != MAN_IP)
 		return '\0';
 	n1 = n1->head->child;
 	n2 = n2->head->child;
 	s1 = n1 == NULL ? "" : n1->string;
 	s2 = n2 == NULL ? "" : n2->string;
 	c1 = strcmp(s1, "*") == 0 ? '*' :
 	     strcmp(s1, "\\-") == 0 ? '-' :
 	     strcmp(s1, "\\(bu") == 0 ? 'b' :
 	     strcmp(s1, "\\[bu]") == 0 ? 'b' : ' ';
 	c2 = strcmp(s2, "*") == 0 ? '*' :
 	     strcmp(s2, "\\-") == 0 ? '-' :
 	     strcmp(s2, "\\(bu") == 0 ? 'b' :
 	     strcmp(s2, "\\[bu]") == 0 ? 'b' : ' ';
 	return c1 != c2 ? '\0' : c1 == 'b' ? '*' : c1;
 }
 
 static int
 man_IP_pre(MAN_ARGS)
 {
 	struct roff_node	*nn;
 	const char		*list_class;
 	enum htmltag		 list_elem, body_elem;
 	char			 list_type;
 
 	nn = n->type == ROFFT_BLOCK ? n : n->parent;
 	list_type = list_continues(roff_node_prev(nn), nn);
 	if (list_type == '\0') {
 		/* Start a new list. */
 		list_type = list_continues(nn, roff_node_next(nn));
 		if (list_type == '\0')
 			list_type = ' ';
 		switch (list_type) {
 		case ' ':
 			list_class = "Bl-tag";
 			list_elem = TAG_DL;
 			break;
 		case '*':
 			list_class = "Bl-bullet";
 			list_elem = TAG_UL;
 			break;
 		case '-':
 			list_class = "Bl-dash";
 			list_elem = TAG_UL;
 			break;
 		default:
 			abort();
 		}
 	} else {
 		/* Continue a list that was started earlier. */
 		list_class = NULL;
 		list_elem = TAG_MAX;
 	}
 	body_elem = list_type == ' ' ? TAG_DD : TAG_LI;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		if (list_elem != TAG_MAX)
 			print_otag(h, list_elem, "c", list_class);
 		return 1;
 	case ROFFT_HEAD:
 		if (body_elem == TAG_LI)
 			return 0;
 		print_otag_id(h, TAG_DT, NULL, n);
 		break;
 	case ROFFT_BODY:
 		print_otag(h, body_elem, "");
 		return 1;
 	default:
 		abort();
 	}
 	switch(n->tok) {
 	case MAN_IP:  /* Only print the first header element. */
 		if (n->child != NULL)
 			print_man_node(man, n->child, h);
 		break;
 	case MAN_TP:  /* Only print next-line header elements. */
 	case MAN_TQ:
 		nn = n->child;
 		while (nn != NULL && (NODE_LINE & nn->flags) == 0)
 			nn = nn->next;
 		while (nn != NULL) {
 			print_man_node(man, nn, h);
 			nn = nn->next;
 		}
 		break;
 	default:
 		abort();
 	}
 	return 0;
 }
 
 static int
 man_MR_pre(MAN_ARGS)
 {
 	struct tag	*t;
 	const char	*name, *section, *suffix;
 	char		*label;
 
 	html_setfont(h, ESCAPE_FONTROMAN);
 	name = section = suffix = label = NULL;
 	if (n->child != NULL) {
 		name = n->child->string;
 		if (n->child->next != NULL) {
 			section = n->child->next->string;
 			mandoc_asprintf(&label,
 			    "%s, section %s", name, section);
 			if (n->child->next->next != NULL)
 				suffix = n->child->next->next->string;
 		}
 	}
 
 	if (name != NULL && section != NULL && h->base_man1 != NULL)
 		t = print_otag(h, TAG_A, "chM?", "Xr",
 		    name, section, "aria-label", label);
 	else
 		t = print_otag(h, TAG_A, "c?", "Xr", "aria-label", label);
 
 	free(label);
 	if (name != NULL) {
 		print_text(h, name);
 		h->flags |= HTML_NOSPACE;
 	}
 	print_text(h, "(");
 	h->flags |= HTML_NOSPACE;
 	if (section != NULL) {
 		print_text(h, section);
 		h->flags |= HTML_NOSPACE;
 	}
 	print_text(h, ")");
 	print_tagq(h, t);
 	if (suffix != NULL) {
 		h->flags |= HTML_NOSPACE;
 		print_text(h, suffix);
 	}
 	return 0;
 }
 
 static int
 man_OP_pre(MAN_ARGS)
 {
 	struct tag	*tt;
 
 	print_text(h, "[");
 	h->flags |= HTML_NOSPACE;
 	tt = print_otag(h, TAG_SPAN, "c", "Op");
 
 	if ((n = n->child) != NULL) {
 		print_otag(h, TAG_B, "");
 		print_text(h, n->string);
 	}
 
 	print_stagq(h, tt);
 
 	if (n != NULL && n->next != NULL) {
 		print_otag(h, TAG_I, "");
 		print_text(h, n->next->string);
 	}
 
 	print_stagq(h, tt);
 	h->flags |= HTML_NOSPACE;
 	print_text(h, "]");
 	return 0;
 }
 
 static int
 man_B_pre(MAN_ARGS)
 {
 	print_otag(h, TAG_B, "");
 	return 1;
 }
 
 static int
 man_I_pre(MAN_ARGS)
 {
 	print_otag(h, TAG_I, "");
 	return 1;
 }
 
 static int
 man_in_pre(MAN_ARGS)
 {
 	print_otag(h, TAG_BR, "");
 	return 0;
 }
 
 static int
 man_ign_pre(MAN_ARGS)
 {
 	return 0;
 }
 
 static int
 man_RS_pre(MAN_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		break;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		print_otag(h, TAG_DIV, "c", "Bd-indent");
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static int
 man_SY_pre(MAN_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		print_otag(h, TAG_TABLE, "c", "Nm");
 		print_otag(h, TAG_TR, "");
 		break;
 	case ROFFT_HEAD:
 		print_otag(h, TAG_TD, "");
 		print_otag(h, TAG_CODE, "c", "Nm");
 		break;
 	case ROFFT_BODY:
 		print_otag(h, TAG_TD, "");
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static int
 man_UR_pre(MAN_ARGS)
 {
 	char *cp;
 
 	n = n->child;
 	assert(n->type == ROFFT_HEAD);
 	if (n->child != NULL) {
 		assert(n->child->type == ROFFT_TEXT);
 		if (n->tok == MAN_MT) {
 			mandoc_asprintf(&cp, "mailto:%s", n->child->string);
 			print_otag(h, TAG_A, "ch", "Mt", cp);
 			free(cp);
 		} else
 			print_otag(h, TAG_A, "ch", "Lk", n->child->string);
 	}
 
 	assert(n->next->type == ROFFT_BODY);
 	if (n->next->child != NULL)
 		n = n->next;
 
 	print_man_nodelist(man, n->child, h);
 	return 0;
 }
diff --git a/contrib/mandoc/man_term.c b/contrib/mandoc/man_term.c
index 706fab8cd4d1..ac75c2c5ef40 100644
--- a/contrib/mandoc/man_term.c
+++ b/contrib/mandoc/man_term.c
@@ -1,1155 +1,1142 @@
-/* $Id: man_term.c,v 1.244 2023/11/13 19:13:01 schwarze Exp $ */
+/* $Id: man_term.c,v 1.248 2025/07/27 15:27:28 schwarze Exp $ */
 /*
- * Copyright (c) 2010-15,2017-20,2022-23 Ingo Schwarze 
+ * Copyright (c) 2010-2020,2022-23,2025 Ingo Schwarze 
  * Copyright (c) 2008-2012 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
  * Plain text formatter for man(7), used by mandoc(1)
  * for ASCII, UTF-8, PostScript, and PDF output.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "roff.h"
 #include "man.h"
 #include "out.h"
 #include "term.h"
 #include "term_tag.h"
 #include "main.h"
 
-#define	MAXMARGINS	  64 /* maximum number of indented scopes */
+#define	MAXMARGINS	  64 /* Maximum number of indented scopes. */
 
 struct	mtermp {
-	int		  lmargin[MAXMARGINS]; /* margins (incl. vis. page) */
-	int		  lmargincur; /* index of current margin */
-	int		  lmarginsz; /* actual number of nested margins */
-	size_t		  offset; /* default offset to visible page */
-	int		  pardist; /* vert. space before par., unit: [v] */
+	int		  lmargin[MAXMARGINS]; /* Margins in basic units. */
+	int		  lmargincur; /* Index of current margin. */
+	int		  lmarginsz; /* Actual number of nested margins. */
+	size_t		  offset; /* Default offset in basic units. */
+	int		  pardist; /* Vert. space before par., unit: [v]. */
 };
 
 #define	DECL_ARGS	  struct termp *p, \
 			  struct mtermp *mt, \
 			  struct roff_node *n, \
 			  const struct roff_meta *meta
 
 struct	man_term_act {
 	int		(*pre)(DECL_ARGS);
 	void		(*post)(DECL_ARGS);
 	int		  flags;
 #define	MAN_NOTEXT	 (1 << 0) /* Never has text children. */
 };
 
 static	void		  print_man_nodelist(DECL_ARGS);
 static	void		  print_man_node(DECL_ARGS);
 static	void		  print_man_head(struct termp *,
 				const struct roff_meta *);
 static	void		  print_man_foot(struct termp *,
 				const struct roff_meta *);
 static	void		  print_bvspace(struct termp *,
 				struct roff_node *, int);
 
 static	int		  pre_B(DECL_ARGS);
 static	int		  pre_DT(DECL_ARGS);
 static	int		  pre_HP(DECL_ARGS);
 static	int		  pre_I(DECL_ARGS);
 static	int		  pre_IP(DECL_ARGS);
 static	int		  pre_MR(DECL_ARGS);
 static	int		  pre_OP(DECL_ARGS);
 static	int		  pre_PD(DECL_ARGS);
 static	int		  pre_PP(DECL_ARGS);
 static	int		  pre_RS(DECL_ARGS);
 static	int		  pre_SH(DECL_ARGS);
 static	int		  pre_SS(DECL_ARGS);
 static	int		  pre_SY(DECL_ARGS);
 static	int		  pre_TP(DECL_ARGS);
 static	int		  pre_UR(DECL_ARGS);
 static	int		  pre_alternate(DECL_ARGS);
 static	int		  pre_ign(DECL_ARGS);
 static	int		  pre_in(DECL_ARGS);
 static	int		  pre_literal(DECL_ARGS);
 
 static	void		  post_IP(DECL_ARGS);
 static	void		  post_HP(DECL_ARGS);
 static	void		  post_RS(DECL_ARGS);
 static	void		  post_SH(DECL_ARGS);
 static	void		  post_SY(DECL_ARGS);
 static	void		  post_TP(DECL_ARGS);
 static	void		  post_UR(DECL_ARGS);
 
 static const struct man_term_act man_term_acts[MAN_MAX - MAN_TH] = {
 	{ NULL, NULL, 0 }, /* TH */
 	{ pre_SH, post_SH, 0 }, /* SH */
 	{ pre_SS, post_SH, 0 }, /* SS */
 	{ pre_TP, post_TP, 0 }, /* TP */
 	{ pre_TP, post_TP, 0 }, /* TQ */
 	{ pre_PP, NULL, 0 }, /* LP */
 	{ pre_PP, NULL, 0 }, /* PP */
 	{ pre_PP, NULL, 0 }, /* P */
 	{ pre_IP, post_IP, 0 }, /* IP */
 	{ pre_HP, post_HP, 0 }, /* HP */
 	{ NULL, NULL, 0 }, /* SM */
 	{ pre_B, NULL, 0 }, /* SB */
 	{ pre_alternate, NULL, 0 }, /* BI */
 	{ pre_alternate, NULL, 0 }, /* IB */
 	{ pre_alternate, NULL, 0 }, /* BR */
 	{ pre_alternate, NULL, 0 }, /* RB */
 	{ NULL, NULL, 0 }, /* R */
 	{ pre_B, NULL, 0 }, /* B */
 	{ pre_I, NULL, 0 }, /* I */
 	{ pre_alternate, NULL, 0 }, /* IR */
 	{ pre_alternate, NULL, 0 }, /* RI */
 	{ NULL, NULL, 0 }, /* RE */
 	{ pre_RS, post_RS, 0 }, /* RS */
 	{ pre_DT, NULL, MAN_NOTEXT }, /* DT */
 	{ pre_ign, NULL, MAN_NOTEXT }, /* UC */
 	{ pre_PD, NULL, MAN_NOTEXT }, /* PD */
 	{ pre_ign, NULL, MAN_NOTEXT }, /* AT */
 	{ pre_in, NULL, MAN_NOTEXT }, /* in */
 	{ pre_SY, post_SY, 0 }, /* SY */
 	{ NULL, NULL, 0 }, /* YS */
 	{ pre_OP, NULL, 0 }, /* OP */
 	{ pre_literal, NULL, 0 }, /* EX */
 	{ pre_literal, NULL, 0 }, /* EE */
 	{ pre_UR, post_UR, 0 }, /* UR */
 	{ NULL, NULL, 0 }, /* UE */
 	{ pre_UR, post_UR, 0 }, /* MT */
 	{ NULL, NULL, 0 }, /* ME */
 	{ pre_MR, NULL, 0 }, /* MR */
 };
 static const struct man_term_act *man_term_act(enum roff_tok);
 
 
 static const struct man_term_act *
 man_term_act(enum roff_tok tok)
 {
 	assert(tok >= MAN_TH && tok <= MAN_MAX);
 	return man_term_acts + (tok - MAN_TH);
 }
 
 void
 terminal_man(void *arg, const struct roff_meta *man)
 {
 	struct mtermp		 mt;
 	struct termp		*p;
 	struct roff_node	*n, *nc, *nn;
 
 	p = (struct termp *)arg;
 	p->tcol->rmargin = p->maxrmargin = p->defrmargin;
 	term_tab_set(p, NULL);
 	term_tab_set(p, "T");
 	term_tab_set(p, ".5i");
 
 	memset(&mt, 0, sizeof(mt));
 	mt.lmargin[mt.lmargincur] = term_len(p, 7);
 	mt.offset = term_len(p, p->defindent);
 	mt.pardist = 1;
 
 	n = man->first->child;
 	if (p->synopsisonly) {
 		for (nn = NULL; n != NULL; n = n->next) {
 			if (n->tok != MAN_SH)
 				continue;
 			nc = n->child->child;
 			if (nc->type != ROFFT_TEXT)
 				continue;
 			if (strcmp(nc->string, "SYNOPSIS") == 0)
 				break;
 			if (nn == NULL && strcmp(nc->string, "NAME") == 0)
 				nn = n;
 		}
 		if (n == NULL)
 			n = nn;
 		p->flags |= TERMP_NOSPACE;
 		if (n != NULL && (n = n->child->next->child) != NULL)
 			print_man_nodelist(p, &mt, n, man);
 		term_newln(p);
 	} else {
 		term_begin(p, print_man_head, print_man_foot, man);
 		p->flags |= TERMP_NOSPACE;
 		if (n != NULL)
 			print_man_nodelist(p, &mt, n, man);
 		term_end(p);
 	}
 }
 
 /*
- * Printing leading vertical space before a block.
- * This is used for the paragraph macros.
- * The rules are pretty simple, since there's very little nesting going
- * on here.  Basically, if we're the first within another block (SS/SH),
- * then don't emit vertical space.  If we are (RS), then do.  If not the
- * first, print it.
+ * Print leading vertical space before a paragraph, unless
+ * it is the first paragraph in a section or subsection.
+ * If it is the first paragraph in an .RS block, consider
+ * that .RS block instead of the paragraph, recursively.
  */
 static void
 print_bvspace(struct termp *p, struct roff_node *n, int pardist)
 {
 	struct roff_node	*nch;
 	int			 i;
 
 	term_newln(p);
 
 	if (n->body != NULL &&
 	    (nch = roff_node_child(n->body)) != NULL &&
 	    nch->type == ROFFT_TBL)
 		return;
 
-	if (n->parent->tok != MAN_RS && roff_node_prev(n) == NULL)
-		return;
-
+	while (roff_node_prev(n) == NULL) {
+		n = n->parent;
+		if (n->tok != MAN_RS)
+			return;
+		if (n->type == ROFFT_BODY)
+			n = n->parent;
+	}
 	for (i = 0; i < pardist; i++)
 		term_vspace(p);
 }
 
 static int
 pre_ign(DECL_ARGS)
 {
 	return 0;
 }
 
 static int
 pre_I(DECL_ARGS)
 {
 	term_fontrepl(p, TERMFONT_UNDER);
 	return 1;
 }
 
 static int
 pre_literal(DECL_ARGS)
 {
 	term_newln(p);
 
 	/*
 	 * Unlike .IP and .TP, .HP does not have a HEAD.
 	 * So in case a second call to term_flushln() is needed,
 	 * indentation has to be set up explicitly.
 	 */
 	if (n->parent->tok == MAN_HP && p->tcol->rmargin < p->maxrmargin) {
 		p->tcol->offset = p->tcol->rmargin;
 		p->tcol->rmargin = p->maxrmargin;
 		p->trailspace = 0;
 		p->flags &= ~(TERMP_NOBREAK | TERMP_BRIND);
 		p->flags |= TERMP_NOSPACE;
 	}
 	return 0;
 }
 
 static int
 pre_PD(DECL_ARGS)
 {
 	struct roffsu	 su;
 
 	n = n->child;
 	if (n == NULL) {
 		mt->pardist = 1;
 		return 0;
 	}
 	assert(n->type == ROFFT_TEXT);
 	if (a2roffsu(n->string, &su, SCALE_VS) != NULL)
 		mt->pardist = term_vspan(p, &su);
 	return 0;
 }
 
 static int
 pre_alternate(DECL_ARGS)
 {
 	enum termfont		 font[2];
 	struct roff_node	*nn;
 	int			 i;
 
 	switch (n->tok) {
 	case MAN_RB:
 		font[0] = TERMFONT_NONE;
 		font[1] = TERMFONT_BOLD;
 		break;
 	case MAN_RI:
 		font[0] = TERMFONT_NONE;
 		font[1] = TERMFONT_UNDER;
 		break;
 	case MAN_BR:
 		font[0] = TERMFONT_BOLD;
 		font[1] = TERMFONT_NONE;
 		break;
 	case MAN_BI:
 		font[0] = TERMFONT_BOLD;
 		font[1] = TERMFONT_UNDER;
 		break;
 	case MAN_IR:
 		font[0] = TERMFONT_UNDER;
 		font[1] = TERMFONT_NONE;
 		break;
 	case MAN_IB:
 		font[0] = TERMFONT_UNDER;
 		font[1] = TERMFONT_BOLD;
 		break;
 	default:
 		abort();
 	}
 	for (i = 0, nn = n->child; nn != NULL; nn = nn->next, i = 1 - i) {
 		term_fontrepl(p, font[i]);
 		assert(nn->type == ROFFT_TEXT);
 		term_word(p, nn->string);
 		if (nn->flags & NODE_EOS)
 			p->flags |= TERMP_SENTENCE;
 		if (nn->next != NULL)
 			p->flags |= TERMP_NOSPACE;
 	}
 	return 0;
 }
 
 static int
 pre_B(DECL_ARGS)
 {
 	term_fontrepl(p, TERMFONT_BOLD);
 	return 1;
 }
 
 static int
 pre_MR(DECL_ARGS)
 {
 	term_fontrepl(p, TERMFONT_NONE);
 	n = n->child;
 	if (n != NULL) {
 		term_word(p, n->string);   /* name */
 		p->flags |= TERMP_NOSPACE;
 	}
 	term_word(p, "(");
 	p->flags |= TERMP_NOSPACE;
 	if (n != NULL && (n = n->next) != NULL) {
 		term_word(p, n->string);   /* section */
 		p->flags |= TERMP_NOSPACE;
 	}
 	term_word(p, ")");
 	if (n != NULL && (n = n->next) != NULL) {
 		p->flags |= TERMP_NOSPACE;
 		term_word(p, n->string);   /* suffix */
 	}
 	return 0;
 }
 
 static int
 pre_OP(DECL_ARGS)
 {
 	term_word(p, "[");
 	p->flags |= TERMP_KEEP | TERMP_NOSPACE;
 
 	if ((n = n->child) != NULL) {
 		term_fontrepl(p, TERMFONT_BOLD);
 		term_word(p, n->string);
 	}
 	if (n != NULL && n->next != NULL) {
 		term_fontrepl(p, TERMFONT_UNDER);
 		term_word(p, n->next->string);
 	}
 	term_fontrepl(p, TERMFONT_NONE);
 	p->flags &= ~TERMP_KEEP;
 	p->flags |= TERMP_NOSPACE;
 	term_word(p, "]");
 	return 0;
 }
 
 static int
 pre_in(DECL_ARGS)
 {
 	struct roffsu	 su;
-	const char	*cp;
-	size_t		 v;
+	const char	*cp;	/* Request argument. */
+	size_t		 v;	/* Indentation in basic units. */
 	int		 less;
 
 	term_newln(p);
 
 	if (n->child == NULL) {
 		p->tcol->offset = mt->offset;
 		return 0;
 	}
 
 	cp = n->child->string;
 	less = 0;
 
-	if (*cp == '-')
+	if (*cp == '-') {
 		less = -1;
-	else if (*cp == '+')
+		cp++;
+	} else if (*cp == '+') {
 		less = 1;
-	else
-		cp--;
+		cp++;
+	}
 
-	if (a2roffsu(++cp, &su, SCALE_EN) == NULL)
+	if (a2roffsu(cp, &su, SCALE_EN) == NULL)
 		return 0;
 
-	v = term_hen(p, &su);
+	v = term_hspan(p, &su);
 
 	if (less < 0)
 		p->tcol->offset -= p->tcol->offset > v ? v : p->tcol->offset;
 	else if (less > 0)
 		p->tcol->offset += v;
 	else
 		p->tcol->offset = v;
 	if (p->tcol->offset > SHRT_MAX)
 		p->tcol->offset = term_len(p, p->defindent);
 
 	return 0;
 }
 
 static int
 pre_DT(DECL_ARGS)
 {
 	term_tab_set(p, NULL);
 	term_tab_set(p, "T");
 	term_tab_set(p, ".5i");
 	return 0;
 }
 
 static int
 pre_HP(DECL_ARGS)
 {
 	struct roffsu		 su;
 	const struct roff_node	*nn;
-	int			 len;
+	int			 len;	/* Indentation in basic units. */
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		print_bvspace(p, n, mt->pardist);
 		return 1;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 
 	if (n->child == NULL)
 		return 0;
 
 	if ((n->child->flags & NODE_NOFILL) == 0) {
 		p->flags |= TERMP_NOBREAK | TERMP_BRIND;
 		p->trailspace = 2;
 	}
 
 	/* Calculate offset. */
 
 	if ((nn = n->parent->head->child) != NULL &&
 	    a2roffsu(nn->string, &su, SCALE_EN) != NULL) {
-		len = term_hen(p, &su);
+		len = term_hspan(p, &su);
 		if (len < 0 && (size_t)(-len) > mt->offset)
 			len = -mt->offset;
 		else if (len > SHRT_MAX)
 			len = term_len(p, p->defindent);
 		mt->lmargin[mt->lmargincur] = len;
 	} else
 		len = mt->lmargin[mt->lmargincur];
 
 	p->tcol->offset = mt->offset;
 	p->tcol->rmargin = mt->offset + len;
 	return 1;
 }
 
 static void
 post_HP(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 	case ROFFT_HEAD:
 		break;
 	case ROFFT_BODY:
 		term_newln(p);
 
 		/*
 		 * Compatibility with a groff bug.
 		 * The .HP macro uses the undocumented .tag request
 		 * which causes a line break and cancels no-space
 		 * mode even if there isn't any output.
 		 */
 
 		if (n->child == NULL)
 			term_vspace(p);
 
 		p->flags &= ~(TERMP_NOBREAK | TERMP_BRIND);
 		p->trailspace = 0;
 		p->tcol->offset = mt->offset;
 		p->tcol->rmargin = p->maxrmargin;
 		break;
 	default:
 		abort();
 	}
 }
 
 static int
 pre_PP(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		mt->lmargin[mt->lmargincur] = term_len(p, 7);
 		print_bvspace(p, n, mt->pardist);
 		break;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		p->tcol->offset = mt->offset;
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static int
 pre_IP(DECL_ARGS)
 {
 	struct roffsu		 su;
 	const struct roff_node	*nn;
-	int			 len;
+	int			 len;	/* Indentation in basic units. */
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		print_bvspace(p, n, mt->pardist);
 		return 1;
 	case ROFFT_HEAD:
 		p->flags |= TERMP_NOBREAK;
 		p->trailspace = 1;
 		break;
 	case ROFFT_BODY:
 		p->flags |= TERMP_NOSPACE | TERMP_NONEWLINE;
 		break;
 	default:
 		abort();
 	}
 
 	/* Calculate the offset from the optional second argument. */
 	if ((nn = n->parent->head->child) != NULL &&
 	    (nn = nn->next) != NULL &&
 	    a2roffsu(nn->string, &su, SCALE_EN) != NULL) {
-		len = term_hen(p, &su);
+		len = term_hspan(p, &su);
 		if (len < 0 && (size_t)(-len) > mt->offset)
 			len = -mt->offset;
 		else if (len > SHRT_MAX)
 			len = term_len(p, p->defindent);
 		mt->lmargin[mt->lmargincur] = len;
 	} else
 		len = mt->lmargin[mt->lmargincur];
 
 	switch (n->type) {
 	case ROFFT_HEAD:
 		p->tcol->offset = mt->offset;
 		p->tcol->rmargin = mt->offset + len;
 		if (n->child != NULL)
 			print_man_node(p, mt, n->child, meta);
 		return 0;
 	case ROFFT_BODY:
 		p->tcol->offset = mt->offset + len;
 		p->tcol->rmargin = p->maxrmargin;
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static void
 post_IP(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		break;
 	case ROFFT_HEAD:
 		term_flushln(p);
 		p->flags &= ~TERMP_NOBREAK;
 		p->trailspace = 0;
 		p->tcol->rmargin = p->maxrmargin;
 		break;
 	case ROFFT_BODY:
 		term_newln(p);
 		p->tcol->offset = mt->offset;
 		break;
 	default:
 		abort();
 	}
 }
 
 static int
 pre_TP(DECL_ARGS)
 {
 	struct roffsu		 su;
 	struct roff_node	*nn;
-	int			 len;
+	int			 len;	/* Indentation in basic units. */
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		if (n->tok == MAN_TP)
 			print_bvspace(p, n, mt->pardist);
 		return 1;
 	case ROFFT_HEAD:
 		p->flags |= TERMP_NOBREAK | TERMP_BRTRSP;
 		p->trailspace = 1;
 		break;
 	case ROFFT_BODY:
 		p->flags |= TERMP_NOSPACE | TERMP_NONEWLINE;
 		break;
 	default:
 		abort();
 	}
 
 	/* Calculate offset. */
 
 	if ((nn = n->parent->head->child) != NULL &&
 	    nn->string != NULL && ! (NODE_LINE & nn->flags) &&
 	    a2roffsu(nn->string, &su, SCALE_EN) != NULL) {
-		len = term_hen(p, &su);
+		len = term_hspan(p, &su);
 		if (len < 0 && (size_t)(-len) > mt->offset)
 			len = -mt->offset;
 		else if (len > SHRT_MAX)
 			len = term_len(p, p->defindent);
 		mt->lmargin[mt->lmargincur] = len;
 	} else
 		len = mt->lmargin[mt->lmargincur];
 
 	switch (n->type) {
 	case ROFFT_HEAD:
 		p->tcol->offset = mt->offset;
 		p->tcol->rmargin = mt->offset + len;
 
 		/* Don't print same-line elements. */
 		nn = n->child;
 		while (nn != NULL && (nn->flags & NODE_LINE) == 0)
 			nn = nn->next;
 
 		while (nn != NULL) {
 			print_man_node(p, mt, nn, meta);
 			nn = nn->next;
 		}
 		return 0;
 	case ROFFT_BODY:
 		p->tcol->offset = mt->offset + len;
 		p->tcol->rmargin = p->maxrmargin;
 		p->trailspace = 0;
 		p->flags &= ~(TERMP_NOBREAK | TERMP_BRTRSP);
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static void
 post_TP(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		break;
 	case ROFFT_HEAD:
 		term_flushln(p);
 		break;
 	case ROFFT_BODY:
 		term_newln(p);
 		p->tcol->offset = mt->offset;
 		break;
 	default:
 		abort();
 	}
 }
 
 static int
 pre_SS(DECL_ARGS)
 {
 	int	 i;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		mt->lmargin[mt->lmargincur] = term_len(p, 7);
 		mt->offset = term_len(p, p->defindent);
 
 		/*
 		 * No vertical space before the first subsection
 		 * and after an empty subsection.
 		 */
 
 		if ((n = roff_node_prev(n)) == NULL ||
 		    (n->tok == MAN_SS && roff_node_child(n->body) == NULL))
 			break;
 
 		for (i = 0; i < mt->pardist; i++)
 			term_vspace(p);
 		break;
 	case ROFFT_HEAD:
+		p->fontibi = 1;
 		term_fontrepl(p, TERMFONT_BOLD);
-		p->tcol->offset = term_len(p, 3);
+		p->tcol->offset = term_len(p, p->defindent) / 2 + 1;
 		p->tcol->rmargin = mt->offset;
-		p->trailspace = mt->offset;
+		p->trailspace = mt->offset / term_len(p, 1);
 		p->flags |= TERMP_NOBREAK | TERMP_BRIND;
 		break;
 	case ROFFT_BODY:
 		p->tcol->offset = mt->offset;
 		p->tcol->rmargin = p->maxrmargin;
 		p->trailspace = 0;
 		p->flags &= ~(TERMP_NOBREAK | TERMP_BRIND);
 		break;
 	default:
 		break;
 	}
 	return 1;
 }
 
 static int
 pre_SH(DECL_ARGS)
 {
 	int	 i;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		mt->lmargin[mt->lmargincur] = term_len(p, 7);
 		mt->offset = term_len(p, p->defindent);
 
 		/*
 		 * No vertical space before the first section
 		 * and after an empty section.
 		 */
 
 		if ((n = roff_node_prev(n)) == NULL ||
 		    (n->tok == MAN_SH && roff_node_child(n->body) == NULL))
 			break;
 
 		for (i = 0; i < mt->pardist; i++)
 			term_vspace(p);
 		break;
 	case ROFFT_HEAD:
+		p->fontibi = 1;
 		term_fontrepl(p, TERMFONT_BOLD);
 		p->tcol->offset = 0;
 		p->tcol->rmargin = mt->offset;
-		p->trailspace = mt->offset;
+		p->trailspace = mt->offset / term_len(p, 1);
 		p->flags |= TERMP_NOBREAK | TERMP_BRIND;
 		break;
 	case ROFFT_BODY:
 		p->tcol->offset = mt->offset;
 		p->tcol->rmargin = p->maxrmargin;
 		p->trailspace = 0;
 		p->flags &= ~(TERMP_NOBREAK | TERMP_BRIND);
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static void
 post_SH(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		break;
 	case ROFFT_HEAD:
+		p->fontibi = 0;
+		/* FALLTHROUGH */
 	case ROFFT_BODY:
 		term_newln(p);
 		break;
 	default:
 		abort();
 	}
 }
 
 static int
 pre_RS(DECL_ARGS)
 {
 	struct roffsu	 su;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		term_newln(p);
 		return 1;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 
 	n = n->parent->head;
 	n->aux = SHRT_MAX + 1;
 	if (n->child == NULL)
 		n->aux = mt->lmargin[mt->lmargincur];
 	else if (a2roffsu(n->child->string, &su, SCALE_EN) != NULL)
-		n->aux = term_hen(p, &su);
+		n->aux = term_hspan(p, &su);
 	if (n->aux < 0 && (size_t)(-n->aux) > mt->offset)
 		n->aux = -mt->offset;
 	else if (n->aux > SHRT_MAX)
 		n->aux = term_len(p, p->defindent);
 
 	mt->offset += n->aux;
 	p->tcol->offset = mt->offset;
 	p->tcol->rmargin = p->maxrmargin;
 
 	if (++mt->lmarginsz < MAXMARGINS)
 		mt->lmargincur = mt->lmarginsz;
 
 	mt->lmargin[mt->lmargincur] = term_len(p, 7);
 	return 1;
 }
 
 static void
 post_RS(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 	case ROFFT_HEAD:
 		return;
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 	term_newln(p);
 	mt->offset -= n->parent->head->aux;
 	p->tcol->offset = mt->offset;
 	if (--mt->lmarginsz < MAXMARGINS)
 		mt->lmargincur = mt->lmarginsz;
 }
 
 static int
 pre_SY(DECL_ARGS)
 {
 	const struct roff_node	*nn;
-	int			 len;
+	int			 len;	/* Indentation in basic units. */
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		if ((nn = roff_node_prev(n)) == NULL || nn->tok != MAN_SY)
 			print_bvspace(p, n, mt->pardist);
 		return 1;
 	case ROFFT_HEAD:
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 
 	nn = n->parent->head->child;
-	len = nn == NULL ? 1 : term_strlen(p, nn->string) + 1;
+	len = term_len(p, 1);
+	if (nn != NULL)
+		len += term_strlen(p, nn->string);
 
 	switch (n->type) {
 	case ROFFT_HEAD:
 		p->tcol->offset = mt->offset;
 		p->tcol->rmargin = mt->offset + len;
 		if (n->next->child == NULL ||
 		    (n->next->child->flags & NODE_NOFILL) == 0)
 			p->flags |= TERMP_NOBREAK;
 		term_fontrepl(p, TERMFONT_BOLD);
 		break;
 	case ROFFT_BODY:
 		mt->lmargin[mt->lmargincur] = len;
 		p->tcol->offset = mt->offset + len;
 		p->tcol->rmargin = p->maxrmargin;
 		p->flags |= TERMP_NOSPACE;
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static void
 post_SY(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		break;
 	case ROFFT_HEAD:
 		term_flushln(p);
 		p->flags &= ~TERMP_NOBREAK;
 		break;
 	case ROFFT_BODY:
 		term_newln(p);
 		p->tcol->offset = mt->offset;
 		break;
 	default:
 		abort();
 	}
 }
 
 static int
 pre_UR(DECL_ARGS)
 {
 	return n->type != ROFFT_HEAD;
 }
 
 static void
 post_UR(DECL_ARGS)
 {
 	if (n->type != ROFFT_BLOCK)
 		return;
 
 	term_word(p, "<");
 	p->flags |= TERMP_NOSPACE;
 
 	if (n->child->child != NULL)
 		print_man_node(p, mt, n->child->child, meta);
 
 	p->flags |= TERMP_NOSPACE;
 	term_word(p, ">");
 }
 
 static void
 print_man_node(DECL_ARGS)
 {
 	const struct man_term_act *act;
 	int c;
 
 	/*
 	 * In no-fill mode, break the output line at the beginning
 	 * of new input lines except after \c, and nowhere else.
 	 */
 
 	if (n->flags & NODE_NOFILL) {
 		if (n->flags & NODE_LINE &&
 		    (p->flags & TERMP_NONEWLINE) == 0)
 			term_newln(p);
 		p->flags |= TERMP_BRNEVER;
 	} else {
 		if (n->flags & NODE_LINE)
 			term_tab_ref(p);
 		p->flags &= ~TERMP_BRNEVER;
 	}
 
 	if (n->flags & NODE_ID)
 		term_tag_write(n, p->line);
 
 	switch (n->type) {
 	case ROFFT_TEXT:
 		/*
 		 * If we have a blank line, output a vertical space.
 		 * If we have a space as the first character, break
 		 * before printing the line's data.
 		 */
 		if (*n->string == '\0') {
 			if (p->flags & TERMP_NONEWLINE)
 				term_newln(p);
 			else
 				term_vspace(p);
 			return;
 		} else if (*n->string == ' ' && n->flags & NODE_LINE &&
 		    (p->flags & TERMP_NONEWLINE) == 0)
 			term_newln(p);
 		else if (n->flags & NODE_DELIMC)
 			p->flags |= TERMP_NOSPACE;
 
 		term_word(p, n->string);
 		goto out;
 	case ROFFT_COMMENT:
 		return;
 	case ROFFT_EQN:
 		if ( ! (n->flags & NODE_LINE))
 			p->flags |= TERMP_NOSPACE;
 		term_eqn(p, n->eqn);
 		if (n->next != NULL && ! (n->next->flags & NODE_LINE))
 			p->flags |= TERMP_NOSPACE;
 		return;
 	case ROFFT_TBL:
 		if (p->tbl.cols == NULL)
 			term_newln(p);
 		term_tbl(p, n->span);
 		return;
 	default:
 		break;
 	}
 
 	if (n->tok < ROFF_MAX) {
 		roff_term_pre(p, n);
 		return;
 	}
 
 	act = man_term_act(n->tok);
 	if ((act->flags & MAN_NOTEXT) == 0 && n->tok != MAN_SM)
 		term_fontrepl(p, TERMFONT_NONE);
 
 	c = 1;
 	if (act->pre != NULL)
 		c = (*act->pre)(p, mt, n, meta);
 
 	if (c && n->child != NULL)
 		print_man_nodelist(p, mt, n->child, meta);
 
 	if (act->post != NULL)
 		(*act->post)(p, mt, n, meta);
 	if ((act->flags & MAN_NOTEXT) == 0 && n->tok != MAN_SM)
 		term_fontrepl(p, TERMFONT_NONE);
 
 out:
 	if (n->parent->tok == MAN_HP && n->parent->type == ROFFT_BODY &&
 	    n->prev == NULL && n->flags & NODE_NOFILL) {
 		term_newln(p);
 		p->tcol->offset = p->tcol->rmargin;
 		p->tcol->rmargin = p->maxrmargin;
 	}
 	if (n->flags & NODE_EOS)
 		p->flags |= TERMP_SENTENCE;
 }
 
 static void
 print_man_nodelist(DECL_ARGS)
 {
 	while (n != NULL) {
 		print_man_node(p, mt, n, meta);
 		n = n->next;
 	}
 }
 
 static void
 print_man_foot(struct termp *p, const struct roff_meta *meta)
 {
 	char			*title;
-	size_t			 datelen, titlen;
+	size_t			 datelen, titlen;  /* In basic units. */
 
-	assert(meta->title);
-	assert(meta->msec);
-	assert(meta->date);
+	assert(meta->title != NULL);
+	assert(meta->msec != NULL);
 
 	term_fontrepl(p, TERMFONT_NONE);
-
 	if (meta->hasbody)
 		term_vspace(p);
 
-	/*
-	 * Temporary, undocumented option to imitate mdoc(7) output.
-	 * In the bottom right corner, use the operating system
-	 * instead of the title.
-	 */
-
-	if ( ! p->mdocstyle) {
-		mandoc_asprintf(&title, "%s(%s)",
-		    meta->title, meta->msec);
-	} else if (meta->os != NULL) {
-		title = mandoc_strdup(meta->os);
-	} else {
-		title = mandoc_strdup("");
-	}
 	datelen = term_strlen(p, meta->date);
+	mandoc_asprintf(&title, "%s(%s)", meta->title, meta->msec);
+	titlen = term_strlen(p, title);
 
 	/* Bottom left corner: operating system. */
 
-	p->flags |= TERMP_NOSPACE | TERMP_NOBREAK;
-	p->trailspace = 1;
 	p->tcol->offset = 0;
 	p->tcol->rmargin = p->maxrmargin > datelen ?
 	    (p->maxrmargin + term_len(p, 1) - datelen) / 2 : 0;
+	p->trailspace = 1;
+	p->flags |= TERMP_NOSPACE | TERMP_NOBREAK;
 
 	if (meta->os)
 		term_word(p, meta->os);
 	term_flushln(p);
 
 	/* At the bottom in the middle: manual date. */
 
 	p->tcol->offset = p->tcol->rmargin;
-	titlen = term_strlen(p, title);
 	p->tcol->rmargin = p->maxrmargin > titlen ?
 	    p->maxrmargin - titlen : 0;
 	p->flags |= TERMP_NOSPACE;
 
 	term_word(p, meta->date);
 	term_flushln(p);
 
 	/* Bottom right corner: manual title and section. */
 
-	p->flags &= ~TERMP_NOBREAK;
-	p->flags |= TERMP_NOSPACE;
-	p->trailspace = 0;
 	p->tcol->offset = p->tcol->rmargin;
 	p->tcol->rmargin = p->maxrmargin;
+	p->trailspace = 0;
+	p->flags &= ~TERMP_NOBREAK;
+	p->flags |= TERMP_NOSPACE;
 
 	term_word(p, title);
 	term_flushln(p);
 
 	/*
 	 * Reset the terminal state for more output after the footer:
 	 * Some output modes, in particular PostScript and PDF, print
 	 * the header and the footer into a buffer such that it can be
 	 * reused for multiple output pages, then go on to format the
 	 * main text.
 	 */
 
         p->tcol->offset = 0;
         p->flags = 0;
-
 	free(title);
 }
 
 static void
 print_man_head(struct termp *p, const struct roff_meta *meta)
 {
 	const char		*volume;
 	char			*title;
-	size_t			 vollen, titlen;
+	size_t			 vollen, titlen;  /* In basic units. */
 
 	assert(meta->title);
 	assert(meta->msec);
 
 	volume = NULL == meta->vol ? "" : meta->vol;
 	vollen = term_strlen(p, volume);
 
 	/* Top left corner: manual title and section. */
 
 	mandoc_asprintf(&title, "%s(%s)", meta->title, meta->msec);
 	titlen = term_strlen(p, title);
 
 	p->flags |= TERMP_NOBREAK | TERMP_NOSPACE;
 	p->trailspace = 1;
 	p->tcol->offset = 0;
-	p->tcol->rmargin = 2 * (titlen+1) + vollen < p->maxrmargin ?
+	p->tcol->rmargin =
+	    titlen * 2 + term_len(p, 2) + vollen < p->maxrmargin ?
 	    (p->maxrmargin - vollen + term_len(p, 1)) / 2 :
 	    vollen < p->maxrmargin ? p->maxrmargin - vollen : 0;
 
 	term_word(p, title);
 	term_flushln(p);
 
 	/* At the top in the middle: manual volume. */
 
 	p->flags |= TERMP_NOSPACE;
 	p->tcol->offset = p->tcol->rmargin;
 	p->tcol->rmargin = p->tcol->offset + vollen + titlen <
-	    p->maxrmargin ?  p->maxrmargin - titlen : p->maxrmargin;
+	    p->maxrmargin ? p->maxrmargin - titlen : p->maxrmargin;
 
 	term_word(p, volume);
 	term_flushln(p);
 
 	/* Top right corner: title and section, again. */
 
 	p->flags &= ~TERMP_NOBREAK;
 	p->trailspace = 0;
 	if (p->tcol->rmargin + titlen <= p->maxrmargin) {
 		p->flags |= TERMP_NOSPACE;
 		p->tcol->offset = p->tcol->rmargin;
 		p->tcol->rmargin = p->maxrmargin;
 		term_word(p, title);
 		term_flushln(p);
 	}
 
 	p->flags &= ~TERMP_NOSPACE;
 	p->tcol->offset = 0;
 	p->tcol->rmargin = p->maxrmargin;
-
-	/*
-	 * Groff prints three blank lines before the content.
-	 * Do the same, except in the temporary, undocumented
-	 * mode imitating mdoc(7) output.
-	 */
-
 	term_vspace(p);
 	free(title);
 }
diff --git a/contrib/mandoc/man_validate.c b/contrib/mandoc/man_validate.c
index 857adba2798f..57ac9327afd4 100644
--- a/contrib/mandoc/man_validate.c
+++ b/contrib/mandoc/man_validate.c
@@ -1,668 +1,676 @@
-/* $Id: man_validate.c,v 1.159 2023/10/24 20:53:12 schwarze Exp $ */
+/* $Id: man_validate.c,v 1.161 2025/07/09 12:51:06 schwarze Exp $ */
 /*
- * Copyright (c) 2010, 2012-2020, 2023 Ingo Schwarze 
+ * Copyright (c) 2010-2020, 2023, 2025 Ingo Schwarze 
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
  * Validation module for man(7) syntax trees used by mandoc(1).
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "mandoc_xr.h"
 #include "roff.h"
 #include "man.h"
 #include "libmandoc.h"
 #include "roff_int.h"
 #include "libman.h"
 #include "tag.h"
 
 #define	CHKARGS	  struct roff_man *man, struct roff_node *n
 
 typedef	void	(*v_check)(CHKARGS);
 
 static	void	  check_par(CHKARGS);
 static	void	  check_part(CHKARGS);
 static	void	  check_root(CHKARGS);
 static	void	  check_tag(struct roff_node *, struct roff_node *);
 static	void	  check_text(CHKARGS);
 
 static	void	  post_AT(CHKARGS);
 static	void	  post_EE(CHKARGS);
 static	void	  post_EX(CHKARGS);
 static	void	  post_IP(CHKARGS);
 static	void	  post_MR(CHKARGS);
 static	void	  post_OP(CHKARGS);
 static	void	  post_SH(CHKARGS);
 static	void	  post_TH(CHKARGS);
 static	void	  post_TP(CHKARGS);
 static	void	  post_UC(CHKARGS);
 static	void	  post_UR(CHKARGS);
 static	void	  post_in(CHKARGS);
 
 static	const v_check man_valids[MAN_MAX - MAN_TH] = {
 	post_TH,    /* TH */
 	post_SH,    /* SH */
 	post_SH,    /* SS */
 	post_TP,    /* TP */
 	post_TP,    /* TQ */
 	check_par,  /* LP */
 	check_par,  /* PP */
 	check_par,  /* P */
 	post_IP,    /* IP */
 	NULL,       /* HP */
 	NULL,       /* SM */
 	NULL,       /* SB */
 	NULL,       /* BI */
 	NULL,       /* IB */
 	NULL,       /* BR */
 	NULL,       /* RB */
 	NULL,       /* R */
 	NULL,       /* B */
 	NULL,       /* I */
 	NULL,       /* IR */
 	NULL,       /* RI */
 	NULL,       /* RE */
 	check_part, /* RS */
 	NULL,       /* DT */
 	post_UC,    /* UC */
 	NULL,       /* PD */
 	post_AT,    /* AT */
 	post_in,    /* in */
 	NULL,       /* SY */
 	NULL,       /* YS */
 	post_OP,    /* OP */
 	post_EX,    /* EX */
 	post_EE,    /* EE */
 	post_UR,    /* UR */
 	NULL,       /* UE */
 	post_UR,    /* MT */
 	NULL,       /* ME */
 	post_MR,    /* MR */
 };
 
 
 /* Validate the subtree rooted at man->last. */
 void
 man_validate(struct roff_man *man)
 {
 	struct roff_node *n;
 	const v_check	 *cp;
 
 	/*
 	 * Iterate over all children, recursing into each one
 	 * in turn, depth-first.
 	 */
 
 	n = man->last;
 	man->last = man->last->child;
 	while (man->last != NULL) {
 		man_validate(man);
 		if (man->last == n)
 			man->last = man->last->child;
 		else
 			man->last = man->last->next;
 	}
 
 	/* Finally validate the macro itself. */
 
 	man->last = n;
 	man->next = ROFF_NEXT_SIBLING;
 	switch (n->type) {
 	case ROFFT_TEXT:
 		check_text(man, n);
 		break;
 	case ROFFT_ROOT:
 		check_root(man, n);
 		break;
 	case ROFFT_COMMENT:
 	case ROFFT_EQN:
 	case ROFFT_TBL:
 		break;
 	default:
 		if (n->tok < ROFF_MAX) {
 			roff_validate(man);
 			break;
 		}
 		assert(n->tok >= MAN_TH && n->tok < MAN_MAX);
 		cp = man_valids + (n->tok - MAN_TH);
 		if (*cp)
 			(*cp)(man, n);
 		if (man->last == n)
 			n->flags |= NODE_VALID;
 		break;
 	}
 }
 
 static void
 check_root(CHKARGS)
 {
 	assert((man->flags & (MAN_BLINE | MAN_ELINE)) == 0);
 
 	if (n->last == NULL || n->last->type == ROFFT_COMMENT)
 		mandoc_msg(MANDOCERR_DOC_EMPTY, n->line, n->pos, NULL);
 	else
 		man->meta.hasbody = 1;
 
 	if (NULL == man->meta.title) {
 		mandoc_msg(MANDOCERR_TH_NOTITLE, n->line, n->pos, NULL);
 
 		/*
 		 * If a title hasn't been set, do so now (by
 		 * implication, date and section also aren't set).
 		 */
 
 		man->meta.title = mandoc_strdup("");
 		man->meta.msec = mandoc_strdup("");
 		man->meta.date = mandoc_normdate(NULL, NULL);
 	}
 
 	if (man->meta.os_e &&
 	    (man->meta.rcsids & (1 << man->meta.os_e)) == 0)
 		mandoc_msg(MANDOCERR_RCS_MISSING, 0, 0,
 		    man->meta.os_e == MANDOC_OS_OPENBSD ?
 		    "(OpenBSD)" : "(NetBSD)");
 }
 
 /*
  * Skip leading whitespace, dashes, backslashes, and font escapes,
  * then create a tag if the first following byte is a letter.
  * Priority is high unless whitespace is present.
  */
 static void
 check_tag(struct roff_node *n, struct roff_node *nt)
 {
 	const char	*cp, *arg;
 	int		 prio, sz;
 
 	if (nt == NULL || nt->type != ROFFT_TEXT)
 		return;
 
 	cp = nt->string;
 	prio = TAG_STRONG;
 	for (;;) {
 		switch (*cp) {
 		case ' ':
 		case '\t':
 			prio = TAG_WEAK;
 			/* FALLTHROUGH */
 		case '-':
 			cp++;
 			break;
 		case '\\':
 			cp++;
 			switch (mandoc_escape(&cp, &arg, &sz)) {
 			case ESCAPE_FONT:
 			case ESCAPE_FONTBOLD:
 			case ESCAPE_FONTITALIC:
 			case ESCAPE_FONTBI:
 			case ESCAPE_FONTROMAN:
 			case ESCAPE_FONTCR:
 			case ESCAPE_FONTCB:
 			case ESCAPE_FONTCI:
 			case ESCAPE_FONTPREV:
 			case ESCAPE_IGNORE:
 				break;
 			case ESCAPE_SPECIAL:
 				if (sz != 1)
 					return;
 				switch (*arg) {
 				case '-':
 				case 'e':
 					break;
 				default:
 					return;
 				}
 				break;
 			default:
 				return;
 			}
 			break;
 		default:
 			if (isalpha((unsigned char)*cp))
 				tag_put(cp, prio, n);
 			return;
 		}
 	}
 }
 
 static void
 check_text(CHKARGS)
 {
 	char		*cp, *p;
 
 	if (n->flags & NODE_NOFILL)
 		return;
 
 	cp = n->string;
 	for (p = cp; NULL != (p = strchr(p, '\t')); p++)
 		mandoc_msg(MANDOCERR_FI_TAB,
 		    n->line, n->pos + (int)(p - cp), NULL);
 }
 
 static void
 post_EE(CHKARGS)
 {
 	if ((n->flags & NODE_NOFILL) == 0)
 		mandoc_msg(MANDOCERR_FI_SKIP, n->line, n->pos, "EE");
 }
 
 static void
 post_EX(CHKARGS)
 {
 	if (n->flags & NODE_NOFILL)
 		mandoc_msg(MANDOCERR_NF_SKIP, n->line, n->pos, "EX");
 }
 
 static void
 post_OP(CHKARGS)
 {
 
 	if (n->child == NULL)
 		mandoc_msg(MANDOCERR_OP_EMPTY, n->line, n->pos, "OP");
 	else if (n->child->next != NULL && n->child->next->next != NULL) {
 		n = n->child->next->next;
 		mandoc_msg(MANDOCERR_ARG_EXCESS,
 		    n->line, n->pos, "OP ... %s", n->string);
 	}
 }
 
 static void
 post_SH(CHKARGS)
 {
 	struct roff_node	*nc;
 	char			*cp, *tag;
 
 	nc = n->child;
 	switch (n->type) {
+	case ROFFT_BLOCK:
+		if ((nc = n->prev) != NULL && nc->tok == ROFF_br) {
+			mandoc_msg(MANDOCERR_PAR_SKIP, nc->line, nc->pos,
+			    "%s before first %s", roff_name[nc->tok],
+			    roff_name[n->tok]);
+			roff_node_delete(man, nc);
+		}
+		return;
 	case ROFFT_HEAD:
 		tag = NULL;
 		deroff(&tag, n);
 		if (tag != NULL) {
 			for (cp = tag; *cp != '\0'; cp++)
 				if (*cp == ' ')
 					*cp = '_';
 			if (nc != NULL && nc->type == ROFFT_TEXT &&
 			    strcmp(nc->string, tag) == 0)
 				tag_put(NULL, TAG_STRONG, n);
 			else
 				tag_put(tag, TAG_FALLBACK, n);
 			free(tag);
 		}
 		return;
 	case ROFFT_BODY:
 		if (nc != NULL)
 			break;
 		return;
 	default:
 		return;
 	}
 
 	if ((nc->tok == MAN_LP || nc->tok == MAN_PP || nc->tok == MAN_P) &&
 	    nc->body->child != NULL) {
 		while (nc->body->last != NULL) {
 			man->next = ROFF_NEXT_CHILD;
 			roff_node_relink(man, nc->body->last);
 			man->last = n;
 		}
 	}
 
 	if (nc->tok == MAN_LP || nc->tok == MAN_PP || nc->tok == MAN_P ||
 	    nc->tok == ROFF_sp || nc->tok == ROFF_br) {
 		mandoc_msg(MANDOCERR_PAR_SKIP, nc->line, nc->pos,
 		    "%s after %s", roff_name[nc->tok], roff_name[n->tok]);
 		roff_node_delete(man, nc);
 	}
 
 	/*
 	 * Trailing PP is empty, so it is deleted by check_par().
 	 * Trailing sp is significant.
 	 */
 
 	if ((nc = n->last) != NULL && nc->tok == ROFF_br) {
 		mandoc_msg(MANDOCERR_PAR_SKIP,
 		    nc->line, nc->pos, "%s at the end of %s",
 		    roff_name[nc->tok], roff_name[n->tok]);
 		roff_node_delete(man, nc);
 	}
 }
 
 static void
 post_UR(CHKARGS)
 {
 	if (n->type == ROFFT_HEAD && n->child == NULL)
 		mandoc_msg(MANDOCERR_UR_NOHEAD, n->line, n->pos,
 		    "%s", roff_name[n->tok]);
 }
 
 static void
 check_part(CHKARGS)
 {
 	if (n->type == ROFFT_BODY && n->child == NULL)
 		mandoc_msg(MANDOCERR_BLK_EMPTY, n->line, n->pos,
 		    "%s", roff_name[n->tok]);
 }
 
 static void
 check_par(CHKARGS)
 {
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		if (n->body->child == NULL)
 			roff_node_delete(man, n);
 		break;
 	case ROFFT_BODY:
 		if (n->child != NULL &&
 		    (n->child->tok == ROFF_sp || n->child->tok == ROFF_br)) {
 			mandoc_msg(MANDOCERR_PAR_SKIP,
 			    n->child->line, n->child->pos,
 			    "%s after %s", roff_name[n->child->tok],
 			    roff_name[n->tok]);
 			roff_node_delete(man, n->child);
 		}
 		if (n->child == NULL)
 			mandoc_msg(MANDOCERR_PAR_SKIP, n->line, n->pos,
 			    "%s empty", roff_name[n->tok]);
 		break;
 	case ROFFT_HEAD:
 		if (n->child != NULL)
 			mandoc_msg(MANDOCERR_ARG_SKIP,
 			    n->line, n->pos, "%s %s%s",
 			    roff_name[n->tok], n->child->string,
 			    n->child->next != NULL ? " ..." : "");
 		break;
 	default:
 		break;
 	}
 }
 
 static void
 post_IP(CHKARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		if (n->head->child == NULL && n->body->child == NULL)
 			roff_node_delete(man, n);
 		break;
 	case ROFFT_HEAD:
 		check_tag(n, n->child);
 		break;
 	case ROFFT_BODY:
 		if (n->parent->head->child == NULL && n->child == NULL)
 			mandoc_msg(MANDOCERR_PAR_SKIP, n->line, n->pos,
 			    "%s empty", roff_name[n->tok]);
 		break;
 	default:
 		break;
 	}
 }
 
 /*
  * The first next-line element in the head is the tag.
  * If that's a font macro, use its first child instead.
  */
 static void
 post_TP(CHKARGS)
 {
 	struct roff_node *nt;
 
 	if (n->type != ROFFT_HEAD || (nt = n->child) == NULL)
 		return;
 
 	while ((nt->flags & NODE_LINE) == 0)
 		if ((nt = nt->next) == NULL)
 			return;
 
 	switch (nt->tok) {
 	case MAN_B:
 	case MAN_BI:
 	case MAN_BR:
 	case MAN_I:
 	case MAN_IB:
 	case MAN_IR:
 		nt = nt->child;
 		break;
 	default:
 		break;
 	}
 	check_tag(n, nt);
 }
 
 static void
 post_TH(CHKARGS)
 {
 	struct roff_node *nb;
 	const char	*p;
 
 	free(man->meta.title);
 	free(man->meta.vol);
 	free(man->meta.os);
 	free(man->meta.msec);
 	free(man->meta.date);
 
 	man->meta.title = man->meta.vol = man->meta.date =
 	    man->meta.msec = man->meta.os = NULL;
 
 	nb = n;
 
 	/* ->TITLE<- MSEC DATE OS VOL */
 
 	n = n->child;
-	if (n != NULL && n->string != NULL) {
+	if (n != NULL && n->string != NULL && *n->string != '\0') {
 		for (p = n->string; *p != '\0'; p++) {
 			/* Only warn about this once... */
 			if (isalpha((unsigned char)*p) &&
 			    ! isupper((unsigned char)*p)) {
 				mandoc_msg(MANDOCERR_TITLE_CASE, n->line,
 				    n->pos + (int)(p - n->string),
 				    "TH %s", n->string);
 				break;
 			}
 		}
 		man->meta.title = mandoc_strdup(n->string);
 	} else {
-		man->meta.title = mandoc_strdup("");
-		mandoc_msg(MANDOCERR_TH_NOTITLE, nb->line, nb->pos, "TH");
+		man->meta.title = mandoc_strdup("UNTITLED");
+		mandoc_msg(MANDOCERR_DT_NOTITLE, nb->line, nb->pos, "TH");
 	}
 
 	/* TITLE ->MSEC<- DATE OS VOL */
 
 	if (n != NULL)
 		n = n->next;
 	if (n != NULL && n->string != NULL) {
 		man->meta.msec = mandoc_strdup(n->string);
 		if (man->filesec != '\0' &&
 		    man->filesec != *n->string &&
 		    *n->string >= '1' && *n->string <= '9')
 			mandoc_msg(MANDOCERR_MSEC_FILE, n->line, n->pos,
 			    "*.%c vs TH ... %c", man->filesec, *n->string);
 	} else {
 		man->meta.msec = mandoc_strdup("");
 		mandoc_msg(MANDOCERR_MSEC_MISSING,
 		    nb->line, nb->pos, "TH %s", man->meta.title);
 	}
 
 	/* TITLE MSEC ->DATE<- OS VOL */
 
 	if (n != NULL)
 		n = n->next;
 	if (man->quick && n != NULL)
 		man->meta.date = mandoc_strdup("");
 	else
 		man->meta.date = mandoc_normdate(n, nb);
 
 	/* TITLE MSEC DATE ->OS<- VOL */
 
 	if (n && (n = n->next))
 		man->meta.os = mandoc_strdup(n->string);
 	else if (man->os_s != NULL)
 		man->meta.os = mandoc_strdup(man->os_s);
 	if (man->meta.os_e == MANDOC_OS_OTHER && man->meta.os != NULL) {
 		if (strstr(man->meta.os, "OpenBSD") != NULL)
 			man->meta.os_e = MANDOC_OS_OPENBSD;
 		else if (strstr(man->meta.os, "NetBSD") != NULL)
 			man->meta.os_e = MANDOC_OS_NETBSD;
 	}
 
 	/* TITLE MSEC DATE OS ->VOL<- */
 	/* If missing, use the default VOL name for MSEC. */
 
 	if (n && (n = n->next))
 		man->meta.vol = mandoc_strdup(n->string);
 	else if ('\0' != man->meta.msec[0] &&
 	    (NULL != (p = mandoc_a2msec(man->meta.msec))))
 		man->meta.vol = mandoc_strdup(p);
 
 	if (n != NULL && (n = n->next) != NULL)
 		mandoc_msg(MANDOCERR_ARG_EXCESS,
 		    n->line, n->pos, "TH ... %s", n->string);
 
 	/*
 	 * Remove the `TH' node after we've processed it for our
 	 * meta-data.
 	 */
 	roff_node_delete(man, man->last);
 }
 
 static void
 post_MR(CHKARGS)
 {
 	struct roff_node *nch;
 
 	if ((nch = n->child) == NULL) {
 		mandoc_msg(MANDOCERR_NM_NONAME, n->line, n->pos, "MR");
 		return;
 	}
 	if (nch->next == NULL) {
 		mandoc_msg(MANDOCERR_XR_NOSEC,
 		    n->line, n->pos, "MR %s", nch->string);
 		return;
 	}
 	if (mandoc_xr_add(nch->next->string, nch->string, nch->line, nch->pos))
 		mandoc_msg(MANDOCERR_XR_SELF, nch->line, nch->pos,
 		    "MR %s %s", nch->string, nch->next->string);
 	if ((nch = nch->next->next) == NULL || nch->next == NULL)
 		return;
 
 	mandoc_msg(MANDOCERR_ARG_EXCESS, nch->next->line, nch->next->pos,
 	    "MR ... %s", nch->next->string);
 	while (nch->next != NULL)
 		roff_node_delete(man, nch->next);
 }
 
 static void
 post_UC(CHKARGS)
 {
 	static const char * const bsd_versions[] = {
 	    "3rd Berkeley Distribution",
 	    "4th Berkeley Distribution",
 	    "4.2 Berkeley Distribution",
 	    "4.3 Berkeley Distribution",
 	    "4.4 Berkeley Distribution",
 	};
 
 	const char	*p, *s;
 
 	n = n->child;
 
 	if (n == NULL || n->type != ROFFT_TEXT)
 		p = bsd_versions[0];
 	else {
 		s = n->string;
 		if (0 == strcmp(s, "3"))
 			p = bsd_versions[0];
 		else if (0 == strcmp(s, "4"))
 			p = bsd_versions[1];
 		else if (0 == strcmp(s, "5"))
 			p = bsd_versions[2];
 		else if (0 == strcmp(s, "6"))
 			p = bsd_versions[3];
 		else if (0 == strcmp(s, "7"))
 			p = bsd_versions[4];
 		else
 			p = bsd_versions[0];
 	}
 
 	free(man->meta.os);
 	man->meta.os = mandoc_strdup(p);
 }
 
 static void
 post_AT(CHKARGS)
 {
 	static const char * const unix_versions[] = {
 	    "7th Edition",
 	    "System III",
 	    "System V",
 	    "System V Release 2",
 	};
 
 	struct roff_node *nn;
 	const char	*p, *s;
 
 	n = n->child;
 
 	if (n == NULL || n->type != ROFFT_TEXT)
 		p = unix_versions[0];
 	else {
 		s = n->string;
 		if (0 == strcmp(s, "3"))
 			p = unix_versions[0];
 		else if (0 == strcmp(s, "4"))
 			p = unix_versions[1];
 		else if (0 == strcmp(s, "5")) {
 			nn = n->next;
 			if (nn != NULL &&
 			    nn->type == ROFFT_TEXT &&
 			    nn->string[0] != '\0')
 				p = unix_versions[3];
 			else
 				p = unix_versions[2];
 		} else
 			p = unix_versions[0];
 	}
 
 	free(man->meta.os);
 	man->meta.os = mandoc_strdup(p);
 }
 
 static void
 post_in(CHKARGS)
 {
 	char	*s;
 
 	if (n->parent->tok != MAN_TP ||
 	    n->parent->type != ROFFT_HEAD ||
 	    n->child == NULL ||
 	    *n->child->string == '+' ||
 	    *n->child->string == '-')
 		return;
 	mandoc_asprintf(&s, "+%s", n->child->string);
 	free(n->child->string);
 	n->child->string = s;
 }
diff --git a/contrib/mandoc/mandoc.1 b/contrib/mandoc/mandoc.1
index 32a3e2811513..8b6fe7d19b1e 100644
--- a/contrib/mandoc/mandoc.1
+++ b/contrib/mandoc/mandoc.1
@@ -1,2512 +1,2503 @@
-.\" $Id: mandoc.1,v 1.270 2025/03/03 14:07:51 schwarze Exp $
+.\" $Id: mandoc.1,v 1.272 2025/07/09 13:46:05 schwarze Exp $
 .\"
 .\" Copyright (c) 2012, 2014-2023, 2025 Ingo Schwarze 
 .\" Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons 
 .\"
 .\" Permission to use, copy, modify, and distribute this software for any
 .\" purpose with or without fee is hereby granted, provided that the above
 .\" copyright notice and this permission notice appear in all copies.
 .\"
 .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\"
-.Dd $Mdocdate: March 3 2025 $
+.Dd $Mdocdate: July 9 2025 $
 .Dt MANDOC 1
 .Os
 .Sh NAME
 .Nm mandoc
 .Nd format manual pages
 .Sh SYNOPSIS
 .Nm mandoc
 .Op Fl ac
 .Op Fl I Cm os Ns = Ns Ar name
 .Op Fl K Ar encoding
 .Op Fl mdoc | man
 .Op Fl O Ar options
 .Op Fl T Ar output
 .Op Fl W Ar level
 .Op Ar
 .Sh DESCRIPTION
 The
 .Nm
 utility formats manual pages for display.
 .Pp
 By default,
 .Nm
 reads
 .Xr mdoc 7
 or
 .Xr man 7
 text from stdin and produces
 .Fl T Cm locale
 output.
 .Pp
 The options are as follows:
 .Bl -tag -width Ds
 .It Fl a
 If the standard output is a terminal device and
 .Fl c
 is not specified, use
 .Xr less 1
 to paginate the output, just like
 .Xr man 1
 would.
 .It Fl c
 Copy the formatted manual pages to the standard output without using
 .Xr less 1
 to paginate them.
 This is the default.
 It can be specified to override
 .Fl a .
 .It Fl I Cm os Ns = Ns Ar name
 Override the default operating system
 .Ar name
 for the
 .Xr mdoc 7
 .Ic \&Os
 and for the
 .Xr man 7
 .Ic \&TH
 macro.
 .It Fl K Ar encoding
 Specify the input encoding.
 The supported
 .Ar encoding
 arguments are
 .Cm us-ascii ,
 .Cm iso-8859-1 ,
 and
 .Cm utf-8 .
 If not specified, autodetection uses the first match in the following
 list:
 .Bl -enum
 .It
 If the first three bytes of the input file are the UTF-8 byte order
 mark (BOM, 0xefbbbf), input is interpreted as
 .Cm utf-8 .
 .It
 If the first or second line of the input file matches the
 .Sy emacs
 mode line format
 .Pp
 .D1 .\e" -*- Oo ...; Oc coding: Ar encoding ; No -*-
 .Pp
 then input is interpreted according to
 .Ar encoding .
 .It
 If the first non-ASCII byte in the file introduces a valid UTF-8
 sequence, input is interpreted as
 .Cm utf-8 .
 .It
 Otherwise, input is interpreted as
 .Cm iso-8859-1 .
 .El
 .It Fl mdoc | man
 With
 .Fl mdoc ,
 all input files are interpreted as
 .Xr mdoc 7 .
 With
 .Fl man ,
 all input files are interpreted as
 .Xr man 7 .
 By default, the input language is automatically detected for each file:
 if the first macro is
 .Ic \&Dd
 or
 .Ic \&Dt ,
 the
 .Xr mdoc 7
 parser is used; otherwise, the
 .Xr man 7
 parser is used.
 With other arguments,
 .Fl m
 is silently ignored.
 .It Fl O Ar options
 Comma-separated output options.
 See the descriptions of the individual output formats for supported
 .Ar options .
 .It Fl T Ar output
 Select the output format.
 Supported values for the
 .Ar output
 argument are
 .Cm ascii ,
 .Cm html ,
 the default of
 .Cm locale ,
 .Cm man ,
 .Cm markdown ,
 .Cm pdf ,
 .Cm ps ,
 .Cm tree ,
 and
 .Cm utf8 .
 .Pp
 The special
 .Fl T Cm lint
 mode only parses the input and produces no output.
 It implies
 .Fl W Cm all
 and redirects parser messages, which usually appear on standard
 error output, to standard output.
 .It Fl W Ar level
 Specify the minimum message
 .Ar level
 to be reported on the standard error output and to affect the exit status.
 The
 .Ar level
 can be
 .Cm base ,
 .Cm style ,
 .Cm warning ,
 .Cm error ,
 or
 .Cm unsupp .
 The
 .Cm base
 level automatically derives the operating system from the contents of the
 .Ic \&Os
 macro, from the
 .Fl Ios
 command line option, or from the
 .Xr uname 3
 return value.
 The levels
 .Cm openbsd
 and
 .Cm netbsd
 are variants of
 .Cm base
 that bypass autodetection and request validation of base system
 conventions for a particular operating system.
 The level
 .Cm all
 is an alias for
 .Cm base .
 By default,
 .Nm
 is silent.
 See
 .Sx EXIT STATUS
 and
 .Sx DIAGNOSTICS
 for details.
 .Pp
 The special option
 .Fl W Cm stop
 tells
 .Nm
 to exit after parsing a file that causes warnings or errors of at least
 the requested level.
 No formatted output will be produced from that file.
 If both a
 .Ar level
 and
 .Cm stop
 are requested, they can be joined with a comma, for example
 .Fl W Cm error , Ns Cm stop .
 .It Ar file
 Read from the given input file.
 If multiple files are specified, they are processed in the given order.
 If unspecified,
 .Nm
 reads from standard input.
 .El
 .Pp
 The options
 .Fl fhklw
 are also supported and are documented in
 .Xr man 1 .
 In
 .Fl f
 and
 .Fl k
 mode,
 .Nm
 also supports the options
 .Fl CMmOSs
 described in the
 .Xr apropos 1
 manual.
 The options
 .Fl fkl
 are mutually exclusive and override each other.
 .Ss ASCII Output
 Use
 .Fl T Cm ascii
 to force text output in 7-bit ASCII character encoding documented in the
 .Xr ascii 7
 manual page, ignoring the
 .Xr locale 1
 set in the environment.
 .Pp
 Font styles are applied by using back-spaced encoding such that an
 underlined character
 .Sq c
 is rendered as
 .Sq _ Ns \e[bs] Ns c ,
 where
 .Sq \e[bs]
 is the back-space character number 8.
 Emboldened characters are rendered as
 .Sq c Ns \e[bs] Ns c .
 This markup is typically converted to appropriate terminal sequences by
 the pager or
 .Xr ul 1 .
 To remove the markup, pipe the output to
 .Xr col 1
 .Fl b
 instead.
 .Pp
 The special characters documented in
 .Xr mandoc_char 7
 are rendered best-effort in an ASCII equivalent.
 In particular, opening and closing
 .Sq single quotes
 are represented as characters number 0x60 and 0x27, respectively,
 which agrees with all ASCII standards from 1965 to the latest
 revision (2012) and which matches the traditional way in which
 .Xr roff 7
 formatters represent single quotes in ASCII output.
 This correct ASCII rendering may look strange with modern
 Unicode-compatible fonts because contrary to ASCII, Unicode uses
 the code point U+0060 for the grave accent only, never for an opening
 quote.
 .Pp
 The following
 .Fl O
 arguments are accepted:
 .Bl -tag -width Ds
 .It Cm indent Ns = Ns Ar indent
 The left margin for normal text is set to
 .Ar indent
 blank characters instead of the default of five.
 Increasing this is not recommended; it may result in degraded formatting,
 for example overfull lines or ugly line breaks.
 When output is to a pager on a terminal that is less than 66 columns
 wide, the default is reduced to three columns.
-.It Cm mdoc
-Format
-.Xr man 7
-input files in
-.Xr mdoc 7
-output style.
-This prints the operating system name rather than the page title
-on the right side of the footer line.
-One useful application is for checking that
-.Fl T Cm man
-output formats in the same way as the
-.Xr mdoc 7
-source it was generated from.
 .It Cm tag Ns Op = Ns Ar term
 If the formatted manual page is opened in a pager,
 go to the definition of the
 .Ar term
 rather than showing the manual page from the beginning.
 If no
 .Ar term
 is specified, reuse the first command line argument that is not a
 .Ar section
 number.
 If that argument is in
 .Xr apropos 1
 .Ar key Ns = Ns Ar val
 format, only the
 .Ar val
 is used rather than the argument as a whole.
 This is useful for commands like
 .Ql man -akO tag Ic=ulimit
 to search for a keyword and jump right to its definition
 in the matching manual pages.
 .It Cm width Ns = Ns Ar width
 The output width is set to
 .Ar width
 instead of the default of 78.
 When output is to a pager on a terminal that is less than 79 columns
 wide, the default is reduced to one less than the terminal width.
 In any case, lines that are output in literal mode are never wrapped
 and may exceed the output width.
 .El
 .Ss HTML Output
 Output produced by
 .Fl T Cm html
 conforms to HTML5 using optional self-closing tags.
 Equations rendered from
 .Xr eqn 7
 blocks use MathML.
 Non-ASCII characters are rendered
 as hexadecimal Unicode character references.
 .Pp
 The following
 .Fl O
 arguments are accepted:
 .Bl -tag -width Ds
 .It Cm fragment
 Omit the  declaration and the , , and 
 elements and only emit the subtree below the  element.
 The
 .Cm style
 argument will be ignored.
 This is useful when embedding manual content within existing documents.
 .It Cm includes Ns = Ns Ar fmt
 The string
 .Ar fmt ,
 for example,
 .Ar ../src/%I.html ,
 is used as a template for linked header files (usually via the
 .Ic \&In
 macro).
 Instances of
 .Sq \&%I
 are replaced with the include filename.
 The default is not to present a
 hyperlink.
 .It Cm man Ns = Ns Ar fmt Ns Op ; Ns Ar fmt
 The string
 .Ar fmt ,
 for example,
 .Ar ../html%S/%N.%S.html ,
 is used as a template for linked manuals (usually via the
 .Ic \&Xr
 macro).
 Instances of
 .Sq \&%N
 and
 .Sq %S
 are replaced with the linked manual's name and section, respectively.
 If no section is included, section 1 is assumed.
 The default is not to
 present a hyperlink.
 If two formats are given and a file
 .Ar %N.%S
 exists in the current directory, the first format is used;
 otherwise, the second format is used.
 .It Cm style Ns = Ns Ar style.css
 The file
 .Ar style.css
 is used as an external stylesheet.
 This must be a valid absolute or
 relative URI.
 .Pp
 Using the file
 .Pa mandoc.css
 that is distributed with
 .Nm
 is recommended.
 It provides an appearance similar to terminal output with some additional
 features specific to
 .Nm
 HTML output, in particular making anchor locations that support
 deep linking stand out visually by putting a dotted line under them,
 providing tooltips showing the semantic function of elements (macro
 names), providing some simple aspects of responsive web design, and
 providing simple support for users who prefer a dark color scheme.
 .Pp
 Using a custom CSS file is possible, but writing it requires
 proficiency in all of the languages HTML 5, CSS 4, and
 .Xr mdoc 7
 and familiarity with the
 .Nm Ns -specific
 classes used in
 .Pa mandoc.css .
 Besides, while the file
 .Pa mandoc.css
 is always adapted to the HTML output generated by the
 .Nm
 version it is distributed with, maintaining a custom CSS file usually
 requires adaptations each time
 .Nm
 is upgraded to a new version.
 .Pp
 If a stylesheet is not specified with
 .Fl O Cm style ,
 .Fl T Cm html
 embeds a minimal stylesheet into the HTML output, mostly to select
 adequate font-style and font-weight attributes for various macros.
 The result is readable in any graphical or text-based web browser,
 but does not aim for looking similar to terminal output.
 Instead, formatting is mostly left to browser defaults
 and to user settings in the browser configuration.
 .It Cm tag Ns Op = Ns Ar term
 Same syntax and semantics as for
 .Sx ASCII Output .
 This is implemented by passing a
 .Ic file://
 URI ending in a fragment identifier to the pager
 rather than passing merely a file name.
 When using this argument, use a pager supporting such URIs, for example
 .Bd -literal -offset 3n
 MANPAGER='lynx -force_html' man -T html -O tag=MANPAGER man
 MANPAGER='w3m -T text/html' man -T html -O tag=toc mandoc
 .Ed
 .Pp
 Consequently, for HTML output, this argument does not work with
 .Xr more 1
 or
 .Xr less 1 .
 For example,
 .Ql MANPAGER=less man -T html -O tag=toc mandoc
 does not work because
 .Xr less 1
 does not support
 .Ic file://
 URIs.
 .It Cm toc
 If an input file contains at least two non-standard sections,
 print a table of contents near the beginning of the output.
 .El
 .Ss Locale Output
 By default,
 .Nm
 automatically selects UTF-8 or ASCII output according to the current
 .Xr locale 1 .
 If any of the environment variables
 .Ev LC_ALL ,
 .Ev LC_CTYPE ,
 or
 .Ev LANG
 are set and the first one that is set
 selects the UTF-8 character encoding, it produces
 .Sx UTF-8 Output ;
 otherwise, it falls back to
 .Sx ASCII Output .
 This output mode can also be selected explicitly with
 .Fl T Cm locale .
 .Ss Man Output
 Use
 .Fl T Cm man
 to translate
 .Xr mdoc 7
 input into
 .Xr man 7
 output format.
 This is useful for distributing manual sources to legacy systems
 lacking
 .Xr mdoc 7
 formatters.
 Embedded
 .Xr eqn 7
 and
 .Xr tbl 7
 code is not supported.
 .Pp
 If the input format of a file is
 .Xr man 7 ,
 the input is copied to the output.
 The parser is also run, and as usual, the
 .Fl W
 level controls which
 .Sx DIAGNOSTICS
 are displayed before copying the input to the output.
 .Ss Markdown Output
 Use
 .Fl T Cm markdown
 to translate
 .Xr mdoc 7
 input to the markdown format conforming to
 .Lk https://daringfireball.net/projects/markdown/syntax.text\
  "John Gruber's 2004 specification" .
 The output also almost conforms to the
 .Lk https://commonmark.org/ CommonMark
 specification.
 .Pp
 The character set used for the markdown output is ASCII.
 Non-ASCII characters are encoded as HTML entities.
 Since that is not possible in literal font contexts, because these
 are rendered as code spans and code blocks in the markdown output,
 non-ASCII characters are transliterated to ASCII approximations in
 these contexts.
 .Pp
 Markdown is a very weak markup language, so all semantic markup is
 lost, and even part of the presentational markup may be lost.
 Do not use this as an intermediate step in converting to HTML;
 instead, use
 .Fl T Cm html
 directly.
 .Pp
 The
 .Xr man 7 ,
 .Xr tbl 7 ,
 and
 .Xr eqn 7
 input languages are not supported by
 .Fl T Cm markdown
 output mode.
 .Ss PDF Output
 PDF-1.1 output may be generated by
 .Fl T Cm pdf .
 See
 .Sx PostScript Output
 for
 .Fl O
 arguments and defaults.
 .Ss PostScript Output
 PostScript
 .Qq Adobe-3.0
 Level-2 pages may be generated by
 .Fl T Cm ps .
 Output pages default to letter sized and are rendered in the Times font
 family, 11-point.
 Margins are calculated as 1/9 the page length and width.
 Line-height is 1.4m.
 .Pp
 Special characters are rendered as in
 .Sx ASCII Output .
 .Pp
 The following
 .Fl O
 arguments are accepted:
 .Bl -tag -width Ds
 .It Cm paper Ns = Ns Ar name
 The paper size
 .Ar name
 may be one of
 .Ar a3 ,
 .Ar a4 ,
 .Ar a5 ,
 .Ar legal ,
 or
 .Ar letter .
 You may also manually specify dimensions as
 .Ar NNxNN ,
 width by height in millimetres.
 If an unknown value is encountered,
 .Ar letter
 is used.
 .El
 .Ss UTF-8 Output
 Use
 .Fl T Cm utf8
 to force text output in UTF-8 multi-byte character encoding,
 ignoring the
 .Xr locale 1
 settings in the environment.
 See
 .Sx ASCII Output
 regarding font styles and
 .Fl O
 arguments.
 .Pp
 On operating systems lacking locale or wide character support, and
 on those where the internal character representation is not UCS-4,
 .Nm
 always falls back to
 .Sx ASCII Output .
 .Ss Syntax tree output
 Use
 .Fl T Cm tree
 to show a human readable representation of the syntax tree.
 It is useful for debugging the source code of manual pages.
 The exact format is subject to change, so don't write parsers for it.
 .Pp
 The first paragraph shows meta data found in the
 .Xr mdoc 7
 prologue, on the
 .Xr man 7
 .Ic \&TH
 line, or the fallbacks used.
 .Pp
 In the tree dump, each output line shows one syntax tree node.
 Child nodes are indented with respect to their parent node.
 The columns are:
 .Pp
 .Bl -enum -compact
 .It
 For macro nodes, the macro name; for text and
 .Xr tbl 7
 nodes, the content.
 There is a special format for
 .Xr eqn 7
 nodes.
 .It
 Node type (text, elem, block, head, body, body-end, tail, tbl, eqn).
 .It
 Flags:
 .Bl -dash -compact
 .It
 An opening parenthesis if the node is an opening delimiter.
 .It
 An asterisk if the node starts a new input line.
 .It
 The input line number (starting at one).
 .It
 A colon.
 .It
 The input column number (starting at one).
 .It
 A closing parenthesis if the node is a closing delimiter.
 .It
 A full stop if the node ends a sentence.
 .It
 BROKEN if the node is a block broken by another block.
 .It
 NOSRC if the node is not in the input file,
 but automatically generated from macros.
 .It
 NOPRT if the node is not supposed to generate output
 for any output format.
 .El
 .El
 .Pp
 The following
 .Fl O
 argument is accepted:
 .Bl -tag -width Ds
 .It Cm noval
 Skip validation and show the unvalidated syntax tree.
 This can help to find out whether a given behaviour is caused by
 the parser or by the validator.
 Meta data is not available in this case.
 .El
 .Sh ENVIRONMENT
 .Bl -tag -width MANPAGER
 .It Ev LC_CTYPE
 The character encoding
 .Xr locale 1 .
 When
 .Sx Locale Output
 is selected, it decides whether to use ASCII or UTF-8 output format.
 It never affects the interpretation of input files.
 .It Ev MANPAGER
 Any non-empty value of the environment variable
 .Ev MANPAGER
 is used instead of the standard pagination program,
 .Xr less 1 ;
 see
 .Xr man 1
 for details.
 Only used if
 .Fl a
 or
 .Fl l
 is specified.
 .It Ev PAGER
 Specifies the pagination program to use when
 .Ev MANPAGER
 is not defined.
 If neither PAGER nor MANPAGER is defined,
 .Xr less 1
 is used.
 Only used if
 .Fl a
 or
 .Fl l
 is specified.
 .El
 .Sh EXIT STATUS
 The
 .Nm
 utility exits with one of the following values, controlled by the message
 .Ar level
 associated with the
 .Fl W
 option:
 .Pp
 .Bl -tag -width Ds -compact
 .It 0
 No base system convention violations, style suggestions, warnings,
 or errors occurred, or those that did were ignored because they
 were lower than the requested
 .Ar level .
 .It 1
 At least one base system convention violation or style suggestion
 occurred, but no warning or error, and
 .Fl W Cm base
 or
 .Fl W Cm style
 was specified.
 .It 2
 At least one warning occurred, but no error, and
 .Fl W Cm warning
 or a lower
 .Ar level
 was requested.
 .It 3
 At least one parsing error occurred,
 but no unsupported feature was encountered, and
 .Fl W Cm error
 or a lower
 .Ar level
 was requested.
 .It 4
 At least one unsupported feature was encountered, and
 .Fl W Cm unsupp
 or a lower
 .Ar level
 was requested.
 .It 5
 Invalid command line arguments were specified.
 No input files have been read.
 .It 6
 An operating system error occurred, for example exhaustion
 of memory, file descriptors, or process table entries.
 Such errors may cause
 .Nm
 to exit at once, possibly in the middle of parsing or formatting a file.
 .El
 .Pp
 Note that selecting
 .Fl T Cm lint
 output mode implies
 .Fl W Cm all .
 .Sh EXAMPLES
 To page manuals to the terminal:
 .Pp
 .Dl $ mandoc -a mandoc.1 man.1 apropos.1 makewhatis.8
 .Pp
 To produce HTML manuals with
 .Pa /usr/share/misc/mandoc.css
 as the stylesheet:
 .Pp
 .Dl $ mandoc \-T html -O style=/usr/share/misc/mandoc.css mdoc.7 > mdoc.7.html
 .Pp
 To check over a large set of manuals:
 .Pp
 .Dl $ mandoc \-T lint \(gafind /usr/src -name \e*\e.[1-9]\(ga
 .Pp
 To produce a series of PostScript manuals for A4 paper:
 .Pp
 .Dl $ mandoc \-T ps \-O paper=a4 mdoc.7 man.7 > manuals.ps
 .Pp
 Convert a modern
 .Xr mdoc 7
 manual to the older
 .Xr man 7
 format, for use on systems lacking an
 .Xr mdoc 7
 parser:
 .Pp
 .Dl $ mandoc \-T man foo.mdoc > foo.man
 .Sh DIAGNOSTICS
 Messages displayed by
 .Nm
 follow this format:
 .Bd -ragged -offset indent
 .Nm :
 .Ar file : Ns Ar line : Ns Ar column : level : message : macro argument ...
 .Pq Ar os
 .Ed
 .Pp
 The first three fields identify the
 .Ar file
 name,
 .Ar line
 number, and
 .Ar column
 number of the input file where the message was triggered.
 The line and column numbers start at 1.
 Both are omitted for messages referring to an input file as a whole.
 All
 .Ar level
 and
 .Ar message
 strings are explained below.
 The name of the
 .Ar macro
 triggering the message and its arguments are omitted where meaningless.
 The
 .Ar os
 operating system specifier is omitted for messages that are relevant
 for all operating systems.
 Fatal messages about invalid command line arguments
 or operating system errors, for example when memory is exhausted,
 may also omit the
 .Ar file
 and
 .Ar level
 fields.
 .Pp
 Message levels have the following meanings:
 .Bl -tag -width "warning"
 .It Cm syserr
 An operating system error occurred.
 There isn't necessarily anything wrong with the input files.
 Output may all the same be missing or incomplete.
 .It Cm badarg
 Invalid command line arguments were specified.
 No input files have been read and no output is produced.
 .It Cm unsupp
 An input file uses unsupported low-level
 .Xr roff 7
 features.
 The output may be incomplete and/or misformatted,
 so using GNU troff instead of
 .Nm
 to process the file may be preferable.
 .It Cm error
 Indicates a risk of information loss or severe misformatting,
 in most cases caused by serious syntax errors.
 .It Cm warning
 Indicates a risk that the information shown or its formatting
 may mismatch the author's intent in minor ways.
 Additionally, syntax errors are classified at least as warnings,
 even if they do not usually cause misformatting.
 .It Cm style
 An input file uses dubious or discouraged style.
 This is not a complaint about the syntax, and probably neither
 formatting nor portability are in danger.
 While great care is taken to avoid false positives on the higher
 message levels, the
 .Cm style
 level tries to reduce the probability that issues go unnoticed,
 so it may occasionally issue bogus suggestions.
 Use your judgement to decide whether any particular
 .Cm style
 suggestion really justifies a change to the input file.
 .It Cm base
 A convention used in the base system of a specific operating system
 is not adhered to.
 These are not markup mistakes, and neither the quality of formatting
 nor portability are in danger.
 Messages of the
 .Cm base
 level are printed with the more intuitive
 .Cm style
 .Ar level
 tag.
 .El
 .Pp
 Messages of the
 .Cm base ,
 .Cm style ,
 .Cm warning ,
 .Cm error ,
 and
 .Cm unsupp
 levels are hidden unless their level, or a lower level, is requested using a
 .Fl W
 option or
 .Fl T Cm lint
 output mode.
 .Pp
 As indicated below, all
 .Cm base
 and some
 .Cm style
 checks are only performed if a specific operating system name occurs
 in the arguments of the
 .Fl W
 command line option, of the
 .Ic \&Os
 macro, of the
 .Fl Ios
 command line option, or, if neither are present, in the return value
 of the
 .Xr uname 3
 function.
 .Ss Conventions for base system manuals
 .Bl -ohang
 .It Sy "Mdocdate found"
 .Pq mdoc , Nx
 The
 .Ic \&Dd
 macro uses CVS
 .Ic Mdocdate
 keyword substitution, which is not supported by the
 .Nx
 base system.
 Consider using the conventional
 .Dq "Month dd, yyyy"
 format instead.
 .It Sy "Mdocdate missing"
 .Pq mdoc , Ox
 The
 .Ic \&Dd
 macro does not use CVS
 .Ic Mdocdate
 keyword substitution, but using it is conventionally expected in the
 .Ox
 base system.
 .It Sy "unknown architecture"
 .Pq mdoc , Ox , Nx
 The third argument of the
 .Ic \&Dt
 macro does not match any of the architectures this operating system
 is running on.
 .It Sy "operating system explicitly specified"
 .Pq mdoc , Ox , Nx
 The
 .Ic \&Os
 macro has an argument.
 In the base system, it is conventionally left blank.
 .It Sy "RCS id missing"
 .Pq Ox , Nx
 The manual page lacks the comment line with the RCS identifier
 generated by CVS
 .Ic OpenBSD
 or
 .Ic NetBSD
 keyword substitution as conventionally used in these operating systems.
 .El
 .Ss Style suggestions
 .Bl -ohang
 .It Sy "legacy man(7) date format"
 .Pq mdoc
 The
 .Ic \&Dd
 macro uses the legacy
 .Xr man 7
 date format
 .Dq yyyy-mm-dd .
 Consider using the conventional
 .Xr mdoc 7
 date format
 .Dq "Month dd, yyyy"
 instead.
 .It Sy "normalizing date format to" : No ...
 .Pq mdoc , man
 The
 .Ic \&Dd
 or
 .Ic \&TH
 macro provides an abbreviated month name or a day number with a
 leading zero.
 In the formatted output, the month name is written out in full
 and the leading zero is omitted.
 .It Sy "lower case character in document title"
 .Pq mdoc , man
 The title is still used as given in the
 .Ic \&Dt
 or
 .Ic \&TH
 macro.
 .It Sy "duplicate RCS id"
 A single manual page contains two copies of the RCS identifier for
 the same operating system.
 Consider deleting the later instance and moving the first one up
 to the top of the page.
 .It Sy "possible typo in section name"
 .Pq mdoc
 Fuzzy string matching revealed that the argument of an
 .Ic \&Sh
 macro is similar, but not identical to a standard section name.
 .It Sy "unterminated quoted argument"
 .Pq roff
 Macro arguments can be enclosed in double quote characters
 such that space characters and macro names contained in the quoted
 argument need not be escaped.
 The closing quote of the last argument of a macro can be omitted.
 However, omitting it is not recommended because it makes the code
 harder to read.
 .It Sy "useless macro"
 .Pq mdoc
 A
 .Ic \&Bt ,
 .Ic \&Tn ,
 or
 .Ic \&Ud
 macro was found.
 Simply delete it: it serves no useful purpose.
 .It Sy "consider using OS macro"
 .Pq mdoc
 A string was found in plain text or in a
 .Ic \&Bx
 macro that could be represented using
 .Ic \&Ox ,
 .Ic \&Nx ,
 .Ic \&Fx ,
 or
 .Ic \&Dx .
 .It Sy "errnos out of order"
 .Pq mdoc, Nx
 The
 .Ic \&Er
 items in a
 .Ic \&Bl
 list are not in alphabetical order.
 .It Sy "duplicate errno"
 .Pq mdoc, Nx
 A
 .Ic \&Bl
 list contains two consecutive
 .Ic \&It
 entries describing the same
 .Ic \&Er
 number.
 .It Sy "referenced manual not found"
 .Pq mdoc
 An
 .Ic \&Xr
 macro references a manual page that was not found.
 When running with
 .Fl W Cm base ,
 the search is restricted to the base system, by default to
 .Pa /usr/share/man : Ns Pa /usr/X11R6/man .
 This path can be configured at compile time using the
 .Dv MANPATH_BASE
 preprocessor macro.
 When running with
 .Fl W Cm style ,
 the search is done along the full search path as described in the
 .Xr man 1
 manual page, respecting the
 .Fl m
 and
 .Fl M
 command line options, the
 .Ev MANPATH
 environment variable, the
 .Xr man.conf 5
 file and falling back to the default of
 .Pa /usr/share/man : Ns Pa /usr/X11R6/man : Ns Pa /usr/local/man ,
 also configurable at compile time using the
 .Dv MANPATH_DEFAULT
 preprocessor macro.
 .It Sy "trailing delimiter"
 .Pq mdoc
 The last argument of an
 .Ic \&Ex , \&Fo , \&Nd , \&Nm , \&Os , \&Sh , \&Ss , \&St ,
 or
 .Ic \&Sx
 macro ends with a trailing delimiter.
 This is usually bad style and often indicates typos.
 Most likely, the delimiter can be removed.
 .It Sy "no blank before trailing delimiter"
 .Pq mdoc
 The last argument of a macro that supports trailing delimiter
 arguments is longer than one byte and ends with a trailing delimiter.
 Consider inserting a blank such that the delimiter becomes a separate
 argument, thus moving it out of the scope of the macro.
 .It Sy "fill mode already enabled, skipping"
 .Pq man
 A
 .Ic \&fi
 request occurs even though the document is still in fill mode,
 or already switched back to fill mode.
 It has no effect.
 .It Sy "fill mode already disabled, skipping"
 .Pq man
 An
 .Ic \&nf
 request occurs even though the document already switched to no-fill mode
 and did not switch back to fill mode yet.
 It has no effect.
 .It Sy "input text line longer than 80 bytes"
 Consider breaking the input text line
 at one of the blank characters before column 80.
 .It Sy "verbatim \(dq--\(dq, maybe consider using \e(em"
 .Pq mdoc
 Even though the ASCII output device renders an em-dash as
 .Qq \-\- ,
 that is not a good way to write it in an input file
 because it renders poorly on all other output devices.
 .It Sy "function name without markup"
 .Pq mdoc
 A word followed by an empty pair of parentheses occurs on a text line.
 Consider using an
 .Ic \&Fn
 or
 .Ic \&Xr
 macro.
 .It Sy "whitespace at end of input line"
 .Pq mdoc , man , roff
 Whitespace at the end of input lines is almost never semantically
 significant \(em but in the odd case where it might be, it is
 extremely confusing when reviewing and maintaining documents.
 .It Sy "bad comment style"
 .Pq roff
 Comment lines start with a dot, a backslash, and a double-quote character.
 The
 .Nm
 utility treats the line as a comment line even without the backslash,
 but leaving out the backslash might not be portable.
 .El
 .Ss Warnings related to the document prologue
 .Bl -ohang
 .It Sy "missing manual title, using UNTITLED"
-.Pq mdoc
+.Pq mdoc , man
 A
 .Ic \&Dt
-macro has no arguments, or there is no
+or
+.Ic \&TH
+macro has no arguments, its first argument is an empty string, or there is no
 .Ic \&Dt
-macro before the first non-prologue macro.
+macro before the first non-prologue
+.Xr mdoc 7
+macro.
 .It Sy "missing manual title, using \(dq\(dq"
 .Pq man
-There is no
+An input document does not contain any
 .Ic \&TH
-macro, or it has no arguments.
+macro.
 .It Sy "missing manual section, using \(dq\(dq"
 .Pq mdoc , man
 A
 .Ic \&Dt
 or
 .Ic \&TH
 macro lacks the mandatory section argument.
 .It Sy "unknown manual section"
 .Pq mdoc
 The section number in a
 .Ic \&Dt
 line is invalid, but still used.
 .It Sy "filename/section mismatch"
 .Pq mdoc , man
 The name of the input file being processed is known and its file
 name extension starts with a non-zero digit, but the
 .Ic \&Dt
 or
 .Ic \&TH
 macro contains a
 .Ar section
 argument that starts with a different non-zero digit.
 The
 .Ar section
 argument is used as provided anyway.
 Consider checking whether the file name or the argument need a correction.
 .It Sy "missing date, using \(dq\(dq"
 .Pq mdoc, man
 The document was parsed as
 .Xr mdoc 7
 and it has no
 .Ic \&Dd
 macro, or the
 .Ic \&Dd
 macro has no arguments or only empty arguments;
 or the document was parsed as
 .Xr man 7
 and it has no
 .Ic \&TH
 macro, or the
 .Ic \&TH
 macro has less than three arguments or its third argument is empty.
 .It Sy "cannot parse date, using it verbatim"
 .Pq mdoc , man
 The date given in a
 .Ic \&Dd
 or
 .Ic \&TH
 macro does not follow the conventional format.
 .It Sy "date in the future, using it anyway"
 .Pq mdoc , man
 The date given in a
 .Ic \&Dd
 or
 .Ic \&TH
 macro is more than a day ahead of the current system
 .Xr time 3 .
 .It Sy "missing Os macro, using \(dq\(dq"
 .Pq mdoc
 The default or current system is not shown in this case.
 .It Sy "late prologue macro"
 .Pq mdoc
 A
 .Ic \&Dd
 or
 .Ic \&Os
 macro occurs after some non-prologue macro, but still takes effect.
 .It Sy "prologue macros out of order"
 .Pq mdoc
 The prologue macros are not given in the conventional order
 .Ic \&Dd ,
 .Ic \&Dt ,
 .Ic \&Os .
 All three macros are used even when given in another order.
 .El
 .Ss Warnings regarding document structure
 .Bl -ohang
 .It Sy ".so is fragile, better use ln(1)"
 .Pq roff
 Including files only works when the parser program runs with the correct
 current working directory.
 .It Sy "no document body"
 .Pq mdoc , man
 The document body contains neither text nor macros.
 An empty document is shown, consisting only of a header and a footer line.
 .It Sy "content before first section header"
 .Pq mdoc , man
 Some macros or text precede the first
 .Ic \&Sh
 or
 .Ic \&SH
 section header.
 The offending macros and text are parsed and added to the top level
 of the syntax tree, outside any section block.
 .It Sy "first section is not NAME"
 .Pq mdoc
 The argument of the first
 .Ic \&Sh
 macro is not
 .Sq NAME .
 This may confuse
 .Xr makewhatis 8
 and
 .Xr apropos 1 .
 .It Sy "NAME section without Nm before Nd"
 .Pq mdoc
 The NAME section does not contain any
 .Ic \&Nm
 child macro before the first
 .Ic \&Nd
 macro.
 .It Sy "NAME section without description"
 .Pq mdoc
 The NAME section lacks the mandatory
 .Ic \&Nd
 child macro.
 .It Sy "description not at the end of NAME"
 .Pq mdoc
 The NAME section does contain an
 .Ic \&Nd
 child macro, but other content follows it.
 .It Sy "bad NAME section content"
 .Pq mdoc
 The NAME section contains plain text or macros other than
 .Ic \&Nm
 and
 .Ic \&Nd .
 .It Sy "missing comma before name"
 .Pq mdoc
 The NAME section contains an
 .Ic \&Nm
 macro that is neither the first one nor preceded by a comma.
 .It Sy "missing description line, using \(dq\(dq"
 .Pq mdoc
 The
 .Ic \&Nd
 macro lacks the required argument.
 The title line of the manual will end after the dash.
 .It Sy "description line outside NAME section"
 .Pq mdoc
 An
 .Ic \&Nd
 macro appears outside the NAME section.
 The arguments are printed anyway and the following text is used for
 .Xr apropos 1 ,
 but none of that behaviour is portable.
 .It Sy "sections out of conventional order"
 .Pq mdoc
 A standard section occurs after another section it usually precedes.
 All section titles are used as given,
 and the order of sections is not changed.
 .It Sy "duplicate section title"
 .Pq mdoc
 The same standard section title occurs more than once.
 .It Sy "unexpected section"
 .Pq mdoc
 A standard section header occurs in a section of the manual
 where it normally isn't useful.
 .It Sy "cross reference to self"
 .Pq mdoc , man
 An
 .Ic \&Xr
 or
 .Ic \&MR
 macro refers to a name and section matching the section of the present
 manual page and a name mentioned in an
 .Ic \&Nm
 macro in the NAME or SYNOPSIS section, or in an
 .Ic \&Fn
 or
 .Ic \&Fo
 macro in the SYNOPSIS.
 Consider using
 .Ic \&Nm
 or
 .Ic \&Fn
 instead of
 .Ic \&Xr .
 .It Sy "unusual Xr order"
 .Pq mdoc
 In the SEE ALSO section, an
 .Ic \&Xr
 macro with a lower section number follows one with a higher number,
 or two
 .Ic \&Xr
 macros referring to the same section are out of alphabetical order.
 .It Sy "unusual Xr punctuation"
 .Pq mdoc
 In the SEE ALSO section, punctuation between two
 .Ic \&Xr
 macros differs from a single comma, or there is trailing punctuation
 after the last
 .Ic \&Xr
 macro.
 .It Sy "AUTHORS section without An macro"
 .Pq mdoc
 An AUTHORS sections contains no
 .Ic \&An
 macros, or only empty ones.
 Probably, there are author names lacking markup.
 .El
 .Ss "Warnings related to macros and nesting"
 .Bl -ohang
 .It Sy "obsolete macro"
 .Pq mdoc
 See the
 .Xr mdoc 7
 manual for replacements.
 .It Sy "macro neither callable nor escaped"
 .Pq mdoc
 The name of a macro that is not callable appears on a macro line.
 It is printed verbatim.
 If the intention is to call it, move it to its own input line;
 otherwise, escape it by prepending
 .Sq \e& .
 .It Sy "skipping paragraph macro"
 In
 .Xr mdoc 7
 documents, this happens
 .Bl -dash -compact
 .It
 at the beginning and end of sections and subsections
 .It
 right before non-compact lists and displays
 .It
 at the end of items in non-column, non-compact lists
 .It
 and for multiple consecutive paragraph macros.
 .El
 In
 .Xr man 7
 documents, it happens
 .Bl -dash -compact
 .It
 for empty
 .Ic \&P ,
 .Ic \&PP ,
 and
 .Ic \&LP
 macros
 .It
 for
 .Ic \&IP
 macros having neither head nor body arguments
 .It
 for
 .Ic \&br
 or
 .Ic \&sp
 right after
 .Ic \&SH
 or
 .Ic \&SS
 .El
 .It Sy "moving paragraph macro out of list"
 .Pq mdoc
 A list item in a
 .Ic \&Bl
 list contains a trailing paragraph macro.
 The paragraph macro is moved after the end of the list.
 .It Sy "skipping no-space macro"
 .Pq mdoc
 An input line begins with an
 .Ic \&Ns
 macro, or the next argument after an
 .Ic \&Ns
 macro is an isolated closing delimiter.
 The macro is ignored.
 .It Sy "blocks badly nested"
 .Pq mdoc
 If two blocks intersect, one should completely contain the other.
 Otherwise, rendered output is likely to look strange in any output
 format, and rendering in SGML-based output formats is likely to be
 outright wrong because such languages do not support badly nested
 blocks at all.
 Typical examples of badly nested blocks are
 .Qq Ic \&Ao \&Bo \&Ac \&Bc
 and
 .Qq Ic \&Ao \&Bq \&Ac .
 In these examples,
 .Ic \&Ac
 breaks
 .Ic \&Bo
 and
 .Ic \&Bq ,
 respectively.
 .It Sy "nested displays are not portable"
 .Pq mdoc
 A
 .Ic \&Bd ,
 .Ic \&D1 ,
 or
 .Ic \&Dl
 display occurs nested inside another
 .Ic \&Bd
 display.
 This works with
 .Nm ,
 but fails with most other implementations.
 .It Sy "moving content out of list"
 .Pq mdoc
 A
 .Ic \&Bl
 list block contains text or macros before the first
 .Ic \&It
 macro.
 The offending children are moved before the beginning of the list.
 .It Sy "first macro on line"
 Inside a
 .Ic \&Bl Fl column
 list, a
 .Ic \&Ta
 macro occurs as the first macro on a line, which is not portable.
 .It Sy "line scope broken"
 .Pq man
 While parsing the next-line scope of the previous macro,
 another macro is found that prematurely terminates the previous one.
 The previous, interrupted macro is deleted from the parse tree.
 .El
 .Ss "Warnings related to missing arguments"
 .Bl -ohang
 .It Sy "skipping empty request"
 .Pq roff , eqn
 The macro name is missing from a macro definition request,
 or an
 .Xr eqn 7
 control statement or operation keyword lacks its required argument.
 .It Sy "conditional request controls empty scope"
 .Pq roff
 A conditional request is only useful if any of the following
 follows it on the same logical input line:
 .Bl -dash -compact
 .It
 The
 .Sq \e{
 keyword to open a multi-line scope.
 .It
 A request or macro or some text, resulting in a single-line scope.
 .It
 The immediate end of the logical line without any intervening whitespace,
 resulting in next-line scope.
 .El
 Here, a conditional request is followed by trailing whitespace only,
 and there is no other content on its logical input line.
 Note that it doesn't matter whether the logical input line is split
 across multiple physical input lines using
 .Sq \e
 line continuation characters.
 This is one of the rare cases
 where trailing whitespace is syntactically significant.
 The conditional request controls a scope containing whitespace only,
 so it is unlikely to have a significant effect,
 except that it may control a following
 .Ic \&el
 clause.
 .It Sy "skipping empty macro"
 .Pq mdoc
 The indicated macro has no arguments and hence no effect.
 .It Sy "empty block"
 .Pq mdoc , man
 A
 .Ic \&Bd ,
 .Ic \&Bk ,
 .Ic \&Bl ,
 .Ic \&D1 ,
 .Ic \&Dl ,
 or
 .Ic \&RS
 block contains nothing in its body and will produce no output.
 .It Sy "empty argument, using 0n"
 .Pq mdoc
 The required width is missing after
 .Ic \&Bd
 or
 .Ic \&Bl
 .Fl offset
 or
 .Fl width .
 .It Sy "missing display type, using -ragged"
 .Pq mdoc
 The
 .Ic \&Bd
 macro is invoked without the required display type.
 .It Sy "list type is not the first argument"
 .Pq mdoc
 In a
 .Ic \&Bl
 macro, at least one other argument precedes the type argument.
 The
 .Nm
 utility copes with any argument order, but some other
 .Xr mdoc 7
 implementations do not.
 .It Sy "missing -width in -tag list, using 8n"
 .Pq mdoc
 Every
 .Ic \&Bl
 macro having the
 .Fl tag
 argument requires
 .Fl width ,
 too.
 .It Sy "missing utility name, using \(dq\(dq"
 .Pq mdoc
 The
 .Ic \&Ex Fl std
 macro is called without an argument before
 .Ic \&Nm
 has first been called with an argument.
 .It Sy "missing function name, using \(dq\(dq"
 .Pq mdoc
 The
 .Ic \&Fo
 macro is called without an argument.
 No function name is printed.
 .It Sy "empty head in list item"
 .Pq mdoc
 In a
 .Ic \&Bl
 .Fl diag ,
 .Fl hang ,
 .Fl inset ,
 .Fl ohang ,
 or
 .Fl tag
 list, an
 .Ic \&It
 macro lacks the required argument.
 The item head is left empty.
 .It Sy "empty list item"
 .Pq mdoc
 In a
 .Ic \&Bl
 .Fl bullet ,
 .Fl dash ,
 .Fl enum ,
 or
 .Fl hyphen
 list, an
 .Ic \&It
 block is empty.
 An empty list item is shown.
 .It Sy "missing argument, using next line"
 .Pq mdoc
 An
 .Ic \&It
 macro in a
 .Ic \&Bd Fl column
 list has no arguments.
 While
 .Nm
 uses the text or macros of the following line, if any, for the cell,
 other formatters may misformat the list.
 .It Sy "missing font type, using \efR"
 .Pq mdoc
 A
 .Ic \&Bf
 macro has no argument.
 It switches to the default font.
 .It Sy "unknown font type, using \efR"
 .Pq mdoc
 The
 .Ic \&Bf
 argument is invalid.
 The default font is used instead.
 .It Sy "nothing follows prefix"
 .Pq mdoc
 A
 .Ic \&Pf
 macro has no argument, or only one argument and no macro follows
 on the same input line.
 This defeats its purpose; in particular, spacing is not suppressed
 before the text or macros following on the next input line.
 .It Sy "empty reference block"
 .Pq mdoc
 An
 .Ic \&Rs
 macro is immediately followed by an
 .Ic \&Re
 macro on the next input line.
 Such an empty block does not produce any output.
 .It Sy "missing section argument"
 .Pq mdoc , man
 An
 .Ic \&Xr
 or
 .Ic \&MR
 macro lacks its second, section number argument.
 The first argument, i.e. the name, is printed, but without a section number.
 In the case of
 .Ic \&Xr ,
 the parentheses are also omitted.
 .It Sy "missing -std argument, adding it"
 .Pq mdoc
 An
 .Ic \&Ex
 or
 .Ic \&Rv
 macro lacks the required
 .Fl std
 argument.
 The
 .Nm
 utility assumes
 .Fl std
 even when it is not specified, but other implementations may not.
 .It Sy "missing option string, using \(dq\(dq"
 .Pq man
 The
 .Ic \&OP
 macro is invoked without any argument.
 An empty pair of square brackets is shown.
 .It Sy "missing resource identifier, using \(dq\(dq"
 .Pq man
 The
 .Ic \&MT
 or
 .Ic \&UR
 macro is invoked without any argument.
 An empty pair of angle brackets is shown.
 .It Sy "missing eqn box, using \(dq\(dq"
 .Pq eqn
 A diacritic mark or a binary operator is found,
 but there is nothing to the left of it.
 An empty box is inserted.
 .El
 .Ss "Warnings related to bad macro arguments"
 .Bl -ohang
 .It Sy "duplicate argument"
 .Pq mdoc
 A
 .Ic \&Bd
 or
 .Ic \&Bl
 macro has more than one
 .Fl compact ,
 more than one
 .Fl offset ,
 or more than one
 .Fl width
 argument.
 All but the last instances of these arguments are ignored.
 .It Sy "skipping duplicate argument"
 .Pq mdoc
 An
 .Ic \&An
 macro has more than one
 .Fl split
 or
 .Fl nosplit
 argument.
 All but the first of these arguments are ignored.
 .It Sy "skipping duplicate display type"
 .Pq mdoc
 A
 .Ic \&Bd
 macro has more than one type argument; the first one is used.
 .It Sy "skipping duplicate list type"
 .Pq mdoc
 A
 .Ic \&Bl
 macro has more than one type argument; the first one is used.
 .It Sy "skipping -width argument"
 .Pq mdoc
 A
 .Ic \&Bl
 .Fl column ,
 .Fl diag ,
 .Fl ohang ,
 .Fl inset ,
 or
 .Fl item
 list has a
 .Fl width
 argument.
 That has no effect.
 .It Sy "wrong number of cells"
 In a line of a
 .Ic \&Bl Fl column
 list, the number of tabs or
 .Ic \&Ta
 macros is less than the number expected from the list header line
 or exceeds the expected number by more than one.
 Missing cells remain empty, and all cells exceeding the number of
 columns are joined into one single cell.
 .It Sy "unknown AT&T UNIX version"
 .Pq mdoc
 An
 .Ic \&At
 macro has an invalid argument.
 It is used verbatim, with
 .Qq "AT&T UNIX "
 prefixed to it.
 .It Sy "comma in function argument"
 .Pq mdoc
 An argument of an
 .Ic \&Fa
 or
 .Ic \&Fn
 macro contains a comma; it should probably be split into two arguments.
 .It Sy "parenthesis in function name"
 .Pq mdoc
 The first argument of an
 .Ic \&Fc
 or
 .Ic \&Fn
 macro contains an opening or closing parenthesis; that's probably wrong,
 parentheses are added automatically.
 .It Sy "unknown library name"
 .Pq mdoc, not on Ox
 An
 .Ic \&Lb
 macro has an unknown name argument and will be rendered as
 .Qq library Dq Ar name .
 .It Sy "invalid content in Rs block"
 .Pq mdoc
 An
 .Ic \&Rs
 block contains plain text or non-% macros.
 The bogus content is left in the syntax tree.
 Formatting may be poor.
 .It Sy "invalid Boolean argument"
 .Pq mdoc
 An
 .Ic \&Sm
 macro has an argument other than
 .Cm on
 or
 .Cm off .
 The invalid argument is moved out of the macro, which leaves the macro
 empty, causing it to toggle the spacing mode.
 .It Sy "argument contains two font escapes"
 .Pq roff
 The second argument of a
 .Ic char
 request contains more than one font escape sequence.
 A wrong font may remain active after using the character.
 .It Sy "unknown font, skipping request"
 .Pq man , tbl
 A
 .Xr roff 7
 .Ic \&ft
 request or a
 .Xr tbl 7
 .Ic \&f
 layout modifier has an unknown
 .Ar font
 argument.
 .It Sy "ignoring distance argument"
 .Pq roff
 In addition to the margin character, an
 .Ic \&mc
 request has a second argument supposed to represent a distance, but the
 .Nm
 implementation of
 .Ic \&mc
 always ignores the second argument.
 .It Sy "odd number of characters in request"
 .Pq roff
 A
 .Ic \&tr
 request contains an odd number of characters.
 The last character is mapped to the blank character.
 .El
 .Ss "Warnings related to plain text"
 .Bl -ohang
 .It Sy "blank line in fill mode, using .sp"
 .Pq mdoc
 The meaning of blank input lines is only well-defined in non-fill mode:
 In fill mode, line breaks of text input lines are not supposed to be
 significant.
 However, for compatibility with groff, blank lines in fill mode
 are formatted like
 .Ic \&sp
 requests.
 To request a paragraph break, use
 .Ic \&Pp
 instead of a blank line.
 .It Sy "tab in filled text"
 .Pq mdoc , man
 The meaning of tab characters is only well-defined in non-fill mode:
 In fill mode, whitespace is not supposed to be significant
 on text input lines.
 As an implementation dependent choice, tab characters on text lines
 are passed through to the formatters in any case.
 Given that the text before the tab character will be filled,
 it is hard to predict which tab stop position the tab will advance to.
 .It Sy "new sentence, new line"
 .Pq mdoc
 A new sentence starts in the middle of a text line.
 Start it on a new input line to help formatters produce correct spacing.
 .It Sy "invalid escape sequence argument"
 .Pq roff
 The argument of an escape sequence is of an invalid form.
 Invalid escape sequences are ignored.
 .It Sy "undefined escape, printing literally"
 .Pq roff
 In an escape sequence, the first character
 right after the leading backslash is invalid.
 That character is printed literally,
 which is equivalent to ignoring the backslash.
 .It Sy "undefined string, using \(dq\(dq"
 .Pq roff
 If a string is used without being defined before,
 its value is implicitly set to the empty string.
 However, defining strings explicitly before use
 keeps the code more readable.
 .El
 .Ss "Warnings related to tables"
 .Bl -ohang
 .It Sy "tbl line starts with span"
 .Pq tbl
 The first cell in a table layout line is a horizontal span
 .Pq Sq Cm s .
 Data provided for this cell is ignored, and nothing is printed in the cell.
 .It Sy "tbl column starts with span"
 .Pq tbl
 The first line of a table layout specification
 requests a vertical span
 .Pq Sq Cm ^ .
 Data provided for this cell is ignored, and nothing is printed in the cell.
 .It Sy "skipping vertical bar in tbl layout"
 .Pq tbl
 A table layout specification contains more than two consecutive vertical bars.
 A double bar is printed, all additional bars are discarded.
 .El
 .Ss "Errors related to tables"
 .Bl -ohang
 .It Sy "non-alphabetic character in tbl options"
 .Pq tbl
 The table options line contains a character other than a letter,
 blank, or comma where the beginning of an option name is expected.
 The character is ignored.
 .It Sy "skipping unknown tbl option"
 .Pq tbl
 The table options line contains a string of letters that does not
 match any known option name.
 The word is ignored.
 .It Sy "missing tbl option argument"
 .Pq tbl
 A table option that requires an argument is not followed by an
 opening parenthesis, or the opening parenthesis is immediately
 followed by a closing parenthesis.
 The option is ignored.
 .It Sy "wrong tbl option argument size"
 .Pq tbl
 A table option argument contains an invalid number of characters.
 Both the option and the argument are ignored.
 .It Sy "empty tbl layout"
 .Pq tbl
 A table layout specification is completely empty,
 specifying zero lines and zero columns.
 As a fallback, a single left-justified column is used.
 .It Sy "invalid character in tbl layout"
 .Pq tbl
 A table layout specification contains a character that can neither
 be interpreted as a layout key character nor as a layout modifier,
 or a modifier precedes the first key.
 The invalid character is discarded.
 .It Sy "unmatched parenthesis in tbl layout"
 .Pq tbl
 A table layout specification contains an opening parenthesis,
 but no matching closing parenthesis.
 The rest of the input line, starting from the parenthesis, has no effect.
 .It Sy "ignoring invalid column width in tbl layout"
 .Pq tbl
 A column width specifier in a table layout is empty, zero, or not a valid
 numerical expression.
 The width specifier is ignored and the column is made wide enough
 to accommodate all its data cells.
 .It Sy "ignoring excessive spacing in tbl layout"
 .Pq tbl
 A spacing modifier in a table layout is unreasonably large.
 The default spacing of 3n is used instead.
 .It Sy "tbl without any data cells"
 .Pq tbl
 A table does not contain any data cells.
 It will probably produce no output.
 .It Sy "ignoring data in spanned tbl cell"
 .Pq tbl
 A table cell is marked as a horizontal span
 .Pq Sq Cm s
 or vertical span
 .Pq Sq Cm ^
 in the table layout, but it contains data.
 The data is ignored.
 .It Sy "ignoring extra tbl data cells"
 .Pq tbl
 A data line contains more cells than the corresponding layout line.
 The data in the extra cells is ignored.
 .It Sy "data block open at end of tbl"
 .Pq tbl
 A data block is opened with
 .Cm T{ ,
 but never closed with a matching
 .Cm T} .
 The remaining data lines of the table are all put into one cell,
 and any remaining cells stay empty.
 .El
 .Ss "Errors related to roff, mdoc, and man code"
 .Bl -ohang
 .It Sy "duplicate prologue macro"
 .Pq mdoc
 One of the prologue macros occurs more than once.
 The last instance overrides all previous ones.
 .It Sy "skipping late title macro"
 .Pq mdoc
 The
 .Ic \&Dt
 macro appears after the first non-prologue macro.
 Traditional formatters cannot handle this because
 they write the page header before parsing the document body.
 Even though this technical restriction does not apply to
 .Nm ,
 traditional semantics is preserved.
 The late macro is discarded including its arguments.
 .It Sy "input stack limit exceeded, infinite loop?"
 .Pq roff
 Explicit recursion limits are implemented for the following features,
 in order to prevent infinite loops:
 .Bl -dash -compact
 .It
 expansion of nested escape sequences
 including expansion of strings and number registers,
 .It
 expansion of nested user-defined macros,
 .It
 and
 .Ic \&so
 file inclusion.
 .El
 When a limit is hit, the output is incorrect, typically losing
 some content, but the parser can continue.
 .It Sy "skipping bad character"
 .Pq mdoc , man , roff
 The input file contains a byte that is not a printable
 .Xr ascii 7
 character.
 The message mentions the character number.
 The offending byte is replaced with a question mark
 .Pq Sq \&? .
 Consider editing the input file to replace the byte with an ASCII
 transliteration of the intended character.
 .It Sy "skipping unknown macro"
 .Pq mdoc , man , roff
 The first identifier on a request or macro line is neither recognized as a
 .Xr roff 7
 request, nor as a user-defined macro, nor, respectively, as an
 .Xr mdoc 7
 or
 .Xr man 7
 macro.
 It may be mistyped or unsupported.
 The request or macro is discarded including its arguments.
 .It Sy "skipping request outside macro"
 .Pq roff
 A
 .Ic shift
 or
 .Ic return
 request occurs outside any macro definition and has no effect.
 .It Sy "skipping insecure request"
 .Pq roff
 An input file attempted to run a shell command
 or to read or write an external file.
 Such attempts are denied for security reasons.
 .It Sy "skipping item outside list"
 .Pq mdoc , eqn
 An
 .Ic \&It
 macro occurs outside any
 .Ic \&Bl
 list, or an
 .Xr eqn 7
 .Ic above
 delimiter occurs outside any pile.
 It is discarded including its arguments.
 .It Sy "skipping column outside column list"
 .Pq mdoc
 A
 .Ic \&Ta
 macro occurs outside any
 .Ic \&Bl Fl column
 block.
 It is discarded including its arguments.
 .It Sy "skipping end of block that is not open"
 .Pq mdoc , man , eqn , tbl , roff
 Various syntax elements can only be used to explicitly close blocks
 that have previously been opened.
 An
 .Xr mdoc 7
 block closing macro, a
 .Xr man 7
 .Ic \&ME , \&RE
 or
 .Ic \&UE
 macro, an
 .Xr eqn 7
 right delimiter or closing brace, or the end of an equation, table, or
 .Xr roff 7
 conditional request is encountered but no matching block is open.
 The offending request or macro is discarded.
 .It Sy "fewer RS blocks open, skipping"
 .Pq man
 The
 .Ic \&RE
 macro is invoked with an argument, but less than the specified number of
 .Ic \&RS
 blocks is open.
 The
 .Ic \&RE
 macro is discarded.
 .It Sy "inserting missing end of block"
 .Pq mdoc , tbl
 Various
 .Xr mdoc 7
 macros as well as tables require explicit closing by dedicated macros.
 A block that doesn't support bad nesting
 ends before all of its children are properly closed.
 The open child nodes are closed implicitly.
 .It Sy "appending missing end of block"
 .Pq mdoc , man , eqn , tbl , roff
 At the end of the document, an explicit
 .Xr mdoc 7
 block, a
 .Xr man 7
 next-line scope or
 .Ic \&MT , \&RS
 or
 .Ic \&UR
 block, an equation, table, or
 .Xr roff 7
 conditional or ignore block is still open.
 The open block is closed implicitly.
 .It Sy "escaped character not allowed in a name"
 .Pq roff
 Macro, string and register identifiers consist of printable,
 non-whitespace ASCII characters.
 Escape sequences and characters and strings expressed in terms of them
 cannot form part of a name.
 The first argument of an
 .Ic \&am ,
 .Ic \&as ,
 .Ic \&de ,
 .Ic \&ds ,
 .Ic \&nr ,
 or
 .Ic \&rr
 request, or any argument of an
 .Ic \&rm
 request, or the name of a request or user defined macro being called,
 is terminated by an escape sequence.
 In the cases of
 .Ic \&as ,
 .Ic \&ds ,
 and
 .Ic \&nr ,
 the request has no effect at all.
 In the cases of
 .Ic \&am ,
 .Ic \&de ,
 .Ic \&rr ,
 and
 .Ic \&rm ,
 what was parsed up to this point is used as the arguments to the request,
 and the rest of the input line is discarded including the escape sequence.
 When parsing for a request or a user-defined macro name to be called,
 only the escape sequence is discarded.
 The characters preceding it are used as the request or macro name,
 the characters following it are used as the arguments to the request or macro.
 .It Sy "using macro argument outside macro"
 .Pq roff
 The escape sequence \e$ occurs outside any macro definition
 and expands to the empty string.
 .It Sy "argument number is not numeric"
 .Pq roff
 The argument of the escape sequence \e$ is not a digit;
 the escape sequence expands to the empty string.
 .It Sy "negative argument, using 0"
 .Pq roff
 A
 .Ic \&shift
 request has a negative argument
 or an argument that is negative due to integer overflow.
 Macro argument numbering remains unchanged.
 .It Sy "NOT IMPLEMENTED: Bd -file"
 .Pq mdoc
 For security reasons, the
 .Ic \&Bd
 macro does not support the
 .Fl file
 argument.
 By requesting the inclusion of a sensitive file, a malicious document
 might otherwise trick a privileged user into inadvertently displaying
 the file on the screen, revealing the file content to bystanders.
 The argument is ignored including the file name following it.
 .It Sy "skipping display without arguments"
 .Pq mdoc
 A
 .Ic \&Bd
 block macro does not have any arguments.
 The block is discarded, and the block content is displayed in
 whatever mode was active before the block.
 .It Sy "missing list type, using -item"
 .Pq mdoc
 A
 .Ic \&Bl
 macro fails to specify the list type.
 .It Sy "argument is not numeric, using 1"
 .Pq roff
 The argument of a
 .Ic \&ce
 request is not a number.
 .It Sy "argument is not a character"
 .Pq roff
 The first argument of a
 .Ic char
 request is neither a single ASCII character
 nor a single character escape sequence.
 The request is ignored including all its arguments.
 .It Sy "skipping unusable escape sequence"
 .Pq roff
 The first argument of an
 .Ic mc
 request is neither a single ASCII character
 nor a single character escape sequence.
 All arguments are ignored and printing of a margin character is disabled.
 .It Sy "missing manual name, using \(dq\(dq"
 .Pq mdoc , man
 The first call to
 .Ic \&Nm ,
 or any call in the NAME section, lacks the required argument, or
 .Ic \&MR
 is called without any argument.
 .It Sy "uname(3) system call failed, using UNKNOWN"
 .Pq mdoc
 The
 .Ic \&Os
 macro is called without arguments, and the
 .Xr uname 3
 system call failed.
 As a workaround,
 .Nm
 can be compiled with
 .Sm off
 .Fl D Cm OSNAME=\(dq\e\(dq Ar string Cm \e\(dq\(dq .
 .Sm on
 .It Sy "unknown standard specifier"
 .Pq mdoc
 An
 .Ic \&St
 macro has an unknown argument and is discarded.
 .It Sy "skipping request without numeric argument"
 .Pq roff , eqn
 An
 .Ic \&it
 request or an
 .Xr eqn 7
 .Ic \&size
 or
 .Ic \&gsize
 statement has a non-numeric or negative argument or no argument at all.
 The invalid request or statement is ignored.
 .It Sy "excessive shift"
 .Pq roff
 The argument of a
 .Ic shift
 request is larger than the number of arguments of the macro that is
 currently being executed.
 All macro arguments are deleted and \en(.$ is set to zero.
 .It Sy "NOT IMPLEMENTED: .so with absolute path or \(dq..\(dq"
 .Pq roff
 For security reasons,
 .Nm
 allows
 .Ic \&so
 file inclusion requests only with relative paths
 and only without ascending to any parent directory.
 By requesting the inclusion of a sensitive file, a malicious document
 might otherwise trick a privileged user into inadvertently displaying
 the file on the screen, revealing the file content to bystanders.
 .Nm
 only shows the path as it appears behind
 .Ic \&so .
 .It Sy ".so request failed"
 .Pq roff
 Servicing a
 .Ic \&so
 request requires reading an external file, but the file could not be
 opened.
 .Nm
 only shows the path as it appears behind
 .Ic \&so .
 .It Sy "skipping all arguments"
 .Pq mdoc , man , eqn , roff
 An
 .Xr mdoc 7
 .Ic \&Bt ,
 .Ic \&Ed ,
 .Ic \&Ef ,
 .Ic \&Ek ,
 .Ic \&El ,
 .Ic \&Lp ,
 .Ic \&Pp ,
 .Ic \&Re ,
 .Ic \&Rs ,
 or
 .Ic \&Ud
 macro, an
 .Ic \&It
 macro in a list that don't support item heads, a
 .Xr man 7
 .Ic \&LP ,
 .Ic \&P ,
 or
 .Ic \&PP
 macro, an
 .Xr eqn 7
 .Ic \&EQ
 or
 .Ic \&EN
 macro, or a
 .Xr roff 7
 .Ic \&br ,
 .Ic \&fi ,
 or
 .Ic \&nf
 request or
 .Sq \&..
 block closing request is invoked with at least one argument.
 All arguments are ignored.
 .It Sy "skipping excess arguments"
 .Pq mdoc , man , roff
 A macro or request is invoked with too many arguments:
 .Bl -dash -offset 2n -width 2n -compact
 .It
 .Ic \&Fo ,
 .Ic \&MT ,
 .Ic \&PD ,
 .Ic \&RS ,
 .Ic \&UR ,
 .Ic \&ft ,
 or
 .Ic \&sp
 with more than one argument
 .It
 .Ic \&An
 with another argument after
 .Fl split
 or
 .Fl nosplit
 .It
 .Ic \&RE
 with more than one argument or with a non-integer argument
 .It
 .Ic \&OP
 or a request of the
 .Ic \&de
 family with more than two arguments
 .It
 .Ic \&Dt
 or
 .Ic \&MR
 with more than three arguments
 .It
 .Ic \&TH
 with more than five arguments
 .It
 .Ic \&Bd ,
 .Ic \&Bk ,
 or
 .Ic \&Bl
 with invalid arguments
 .El
 The excess arguments are ignored.
 .El
 .Ss "Errors related to escape sequences"
 .Bl -ohang
 .It Sy "incomplete escape sequence"
 .Pq roff
 The end of the input line is encountered
 while parsing the argument of an escape sequence.
 In this case,
 .Ic \e*
 and
 .Ic \en
 expand to an empty string,
 .Ic \eB
 to the digit
 .Sq 0 ,
 and
 .Ic \ew
 to the length of the incomplete argument.
 All other incomplete escape sequences are ignored.
 .It Sy "invalid special character"
 .Pq roff
 A special character escape sequence is invalid,
 for example a Unicode sequence pointing to a surrogate
 or beyond the Unicode range, a \e[char...] escape sequence
 representing a control character or pointing beyond the
 .Vt unsigned char
 range, or an invalid variable-length form
 of a single-byte character escape sequence, for example writing
 .Qq \e[e]
 or
 .Qq \e[~]
 instead of
 .Qq \ee
 or
 .Qq \e~ ,
 respectively.
 The escape sequence is ignored.
 .It Sy "unknown special character"
 .Pq roff
 The name given in a special character escape sequence is not known to
 .Nm .
 The escape sequence is ignored.
 .It Sy "invalid escape argument delimiter"
 .Pq roff
 An escape sequence that expects a numerical argument
 attempts to employ one of the characters
 .Qq " %&()*+-./0123456789:<=>"
 as an argument delimiter.
 The escape sequence is ignored including the invalid opening delimiter
 and the rest of the argument may appear as output text.
 While various characters can be used as argument delimiters,
 using the apostrophe-quote character
 .Pq Sq \(aq
 is recommended for readability and robustness.
 .El
 .Ss Unsupported features
 .Bl -ohang
 .It Sy "input too large"
 .Pq mdoc , man
 Currently,
 .Nm
 cannot handle input files larger than its arbitrary size limit
 of 2^31 bytes (2 Gigabytes).
 Since useful manuals are always small, this is not a problem in practice.
 Parsing is aborted as soon as the condition is detected.
 .It Sy "unsupported control character"
 .Pq roff
 An ASCII control character supported by other
 .Xr roff 7
 implementations but not by
 .Nm
 was found in an input file.
 It is replaced by a question mark.
 .It Sy "unsupported escape sequence"
 .Pq roff
 An input file contains an escape sequence supported by GNU troff
 or Heirloom troff but not by
 .Nm ,
 and it is likely that this will cause information loss
 or considerable misformatting.
 .It Sy "unsupported roff request"
 .Pq roff
 An input file contains a
 .Xr roff 7
 request supported by GNU troff or Heirloom troff but not by
 .Nm ,
 and it is likely that this will cause information loss
 or considerable misformatting.
 .It Sy "eqn delim option in tbl"
 .Pq eqn , tbl
 The options line of a table defines equation delimiters.
 Any equation source code contained in the table will be printed unformatted.
 .It Sy "unsupported table layout modifier"
 .Pq tbl
 A table layout specification contains an
 .Sq Cm m
 modifier.
 The modifier is discarded.
 .It Sy "ignoring macro in table"
 .Pq tbl , mdoc , man
 A table contains an invocation of an
 .Xr mdoc 7
 or
 .Xr man 7
 macro or of an undefined macro.
 The macro is ignored, and its arguments are handled
 as if they were a text line.
 .It Sy "skipping tbl in -Tman mode"
 .Pq mdoc , tbl
 An input file contains the
 .Ic \&TS
 macro.
 This message is only generated in
 .Fl T Cm man
 output mode, where
 .Xr tbl 7
 input is not supported.
 .It Sy "skipping eqn in -Tman mode"
 .Pq mdoc , eqn
 An input file contains the
 .Ic \&EQ
 macro.
 This message is only generated in
 .Fl T Cm man
 output mode, where
 .Xr eqn 7
 input is not supported.
 .El
 .Ss Bad command line arguments
 .Bl -ohang
 .It Sy "bad command line argument"
 The argument following one of the
 .Fl IKMmOTW
 command line options is invalid, or a
 .Ar file
 given as a command line argument cannot be opened.
 .It Sy "duplicate command line argument"
 The
 .Fl I
 command line option was specified twice.
 .It Sy "option has a superfluous value"
 An argument to the
 .Fl O
 option has a value but does not accept one.
 .It Sy "missing option value"
 An argument to the
 .Fl O
 option has no argument but requires one.
 .It Sy "bad option value"
 An argument to the
 .Fl O
 .Cm indent
 or
 .Cm width
 option has an invalid value.
 .It Sy "duplicate option value"
 The same
 .Fl O
 option is specified more than once.
 .It Sy "no such tag"
 The
 .Fl O Cm tag
 option was specified but the tag was not found in any of the displayed
 manual pages.
 .It Sy "\-Tmarkdown unsupported for man(7) input"
 .Pq man
 The
 .Fl T Cm markdown
 option was specified but an input file uses the
 .Xr man 7
 language.
 No output is produced for that input file.
 .El
 .Sh SEE ALSO
 .Xr apropos 1 ,
 .Xr man 1 ,
 .Xr eqn 7 ,
 .Xr man 7 ,
 .Xr mandoc_char 7 ,
 .Xr mdoc 7 ,
 .Xr roff 7 ,
 .Xr tbl 7
 .Sh HISTORY
 The
 .Nm
 utility first appeared in
 .Ox 4.8 .
 The option
 .Fl I
 appeared in
 .Ox 5.2 ,
 and
 .Fl aCcfhKklMSsw
 in
 .Ox 5.7 .
 .Sh AUTHORS
 .An -nosplit
 The
 .Nm
 utility was written by
 .An Kristaps Dzonsons Aq Mt kristaps@bsd.lv
 and is maintained by
 .An Ingo Schwarze Aq Mt schwarze@openbsd.org .
diff --git a/contrib/mandoc/mandoc.css b/contrib/mandoc/mandoc.css
index 88432b9322b7..46e03a386ae0 100644
--- a/contrib/mandoc/mandoc.css
+++ b/contrib/mandoc/mandoc.css
@@ -1,369 +1,369 @@
-/* $Id: mandoc.css,v 1.54 2025/01/25 03:18:55 schwarze Exp $ */
+/* $Id: mandoc.css,v 1.55 2025/06/26 17:06:34 schwarze Exp $ */
 /*
  * Standard style sheet for mandoc(1) -Thtml and man.cgi(8).
  *
  * Written by Ingo Schwarze .
  * I place this file into the public domain.
  * Permission to use, copy, modify, and distribute it for any purpose
  * with or without fee is hereby granted, without any conditions.
  */
 
 /* Global defaults. */
 
 html {		max-width: 65em;
 		--bg: #FFFFFF;
 		--fg: #000000; }
 body {		background: var(--bg);
 		color: var(--fg);
 		font-family: Helvetica,Arial,sans-serif; }
 h1, h2 {	font-size: 110%; }
 table {		margin-top: 0em;
 		margin-bottom: 0em;
 		border-collapse: collapse; }
 /* Some browsers set border-color in a browser style for tbody,
  * but not for table, resulting in inconsistent border styling. */
 tbody {		border-color: inherit; }
 tr {		border-color: inherit; }
 td {		vertical-align: top;
 		padding-left: 0.2em;
 		padding-right: 0.2em;
 		border-color: inherit; }
 ul, ol, dl {	margin-top: 0em;
 		margin-bottom: 0em; }
 li, dt {	margin-top: 1em; }
 pre {		font-family: inherit; }
 
 .permalink {	border-bottom: thin dotted;
 		color: inherit;
 		font: inherit;
 		text-decoration: inherit; }
 * {		clear: both }
 
 /* Search form and search results. */
 
 fieldset {	border: thin solid silver;
 		border-radius: 1em;
 		text-align: center; }
 input[name=expr] {
 		width: 25%; }
 
 table.results {	margin-top: 1em;
 		margin-left: 2em;
 		font-size: smaller; }
 
 /* Header and footer lines. */
 
 div[role=doc-pageheader] {
 		display: flex;
 		border-bottom: 1px dotted #808080;
 		margin-bottom: 1em;
 		font-size: smaller; }
 .head-ltitle {	flex: 1; }
 .head-vol {	flex: 0 1 auto;
 		text-align: center; }
 .head-rtitle {	flex: 1;
 		text-align: right; }
 
 div[role=doc-pagefooter] {
 		display: flex;
 		justify-content: space-between;
 		border-top: 1px dotted #808080;
 		margin-top: 1em;
 		font-size: smaller; }
 .foot-left {	flex: 1; }
 .foot-date {	flex: 0 1 auto;
 		text-align: center; }
-.foot-os {	flex: 1;
+.foot-right {	flex: 1;
 		text-align: right; }
 
 /* Sections and paragraphs. */
 
 main {		margin-left: 3.8em; }
 .Nd { }
 section.Sh { }
 h2.Sh {		margin-top: 1.2em;
 		margin-bottom: 0.6em;
 		margin-left: -3.2em; }
 section.Ss { }
 h3.Ss {		margin-top: 1.2em;
 		margin-bottom: 0.6em;
 		margin-left: -1.2em;
 		font-size: 105%; }
 .Pp {		margin: 0.6em 0em; }
 .Sx { }
 .Xr { }
 
 /* Displays and lists. */
 
 .Bd { }
 .Bd-indent {	margin-left: 3.8em; }
 
 .Bl-bullet {	list-style-type: disc;
 		padding-left: 1em; }
 .Bl-bullet > li { }
 .Bl-dash {	list-style-type: none;
 		padding-left: 0em; }
 .Bl-dash > li:before {
 		content: "\2014  "; }
 .Bl-item {	list-style-type: none;
 		padding-left: 0em; }
 .Bl-item > li { }
 .Bl-compact > li {
 		margin-top: 0em; }
 
 .Bl-enum {	padding-left: 2em; }
 .Bl-enum > li { }
 .Bl-compact > li {
 		margin-top: 0em; }
 
 .Bl-diag { }
 .Bl-diag > dt {
 		font-style: normal;
 		font-weight: bold; }
 .Bl-diag > dd {
 		margin-left: 0em; }
 .Bl-hang { }
 .Bl-hang > dt { }
 .Bl-hang > dd {
 		margin-left: 5.5em; }
 .Bl-inset { }
 .Bl-inset > dt { }
 .Bl-inset > dd {
 		margin-left: 0em; }
 .Bl-ohang { }
 .Bl-ohang > dt { }
 .Bl-ohang > dd {
 		margin-left: 0em; }
 .Bl-tag {	margin-top: 0.6em;
 		margin-left: 5.5em; }
 .Bl-tag > dt {
 		float: left;
 		margin-top: 0em;
 		margin-left: -5.5em;
 		padding-right: 0.5em;
 		vertical-align: top; }
 .Bl-tag > dd {
 		clear: right;
 		column-count: 1;  /* Force block formatting context. */
 		width: 100%;
 		margin-top: 0em;
 		margin-left: 0em;
 		margin-bottom: 0.6em;
 		vertical-align: top; }
 .Bl-compact {	margin-top: 0em; }
 .Bl-compact > dd {
 		margin-bottom: 0em; }
 .Bl-compact > dt {
 		margin-top: 0em; }
 
 .Bl-column { }
 .Bl-column > tbody > tr { }
 .Bl-column > tbody > tr > td {
 		margin-top: 1em; }
 .Bl-compact > tbody > tr > td {
 		margin-top: 0em; }
 
 .Rs {		font-style: normal;
 		font-weight: normal; }
 .RsA { }
 .RsB {		font-style: italic;
 		font-weight: normal; }
 .RsC { }
 .RsD { }
 .RsI {		font-style: italic;
 		font-weight: normal; }
 .RsJ {		font-style: italic;
 		font-weight: normal; }
 .RsN { }
 .RsO { }
 .RsP { }
 .RsQ { }
 .RsR { }
 .RsT {		font-style: normal;
 		font-weight: normal; }
 .RsU { }
 .RsV { }
 
 .eqn { }
 .tbl td {	vertical-align: middle; }
 
 .HP {		margin-left: 3.8em;
 		text-indent: -3.8em; }
 
 /* Semantic markup for command line utilities. */
 
 table.Nm { }
 code.Nm {	font-style: normal;
 		font-weight: bold;
 		font-family: inherit; }
 .Fl {		font-style: normal;
 		font-weight: bold;
 		font-family: inherit; }
 .Cm {		font-style: normal;
 		font-weight: bold;
 		font-family: inherit; }
 .Ar {		font-style: italic;
 		font-weight: normal; }
 .Op {		display: inline flow; }
 .Ic {		font-style: normal;
 		font-weight: bold;
 		font-family: inherit; }
 .Ev {		font-style: normal;
 		font-weight: normal;
 		font-family: monospace; }
 .Pa {		font-style: italic;
 		font-weight: normal; }
 
 /* Semantic markup for function libraries. */
 
 .Lb { }
 code.In {	font-style: normal;
 		font-weight: bold;
 		font-family: inherit; }
 a.In { }
 .Fd {		font-style: normal;
 		font-weight: bold;
 		font-family: inherit; }
 .Ft {		font-style: italic;
 		font-weight: normal; }
 .Fn {		font-style: normal;
 		font-weight: bold;
 		font-family: inherit; }
 .Fa {		font-style: italic;
 		font-weight: normal; }
 .Vt {		font-style: italic;
 		font-weight: normal; }
 .Va {		font-style: italic;
 		font-weight: normal; }
 .Dv {		font-style: normal;
 		font-weight: normal;
 		font-family: monospace; }
 .Er {		font-style: normal;
 		font-weight: normal;
 		font-family: monospace; }
 
 /* Various semantic markup. */
 
 .An { }
 .Lk { }
 .Mt { }
 .Cd {		font-style: normal;
 		font-weight: bold;
 		font-family: inherit; }
 .Ad {		font-style: italic;
 		font-weight: normal; }
 .Ms {		font-style: normal;
 		font-weight: bold; }
 .St { }
 .Ux { }
 
 /* Physical markup. */
 
 .Bf {		display: inline flow; }
 .No {		font-style: normal;
 		font-weight: normal; }
 .Em {		font-style: italic;
 		font-weight: normal; }
 .Sy {		font-style: normal;
 		font-weight: bold; }
 .Li {		font-style: normal;
 		font-weight: normal;
 		font-family: monospace; }
 
 /* Tooltip support. */
 
 h2.Sh, h3.Ss {	position: relative; }
 .An, .Ar, .Cd, .Cm, .Dv, .Em, .Er, .Ev, .Fa, .Fd, .Fl, .Fn, .Ft,
 .Ic, code.In, .Lb, .Lk, .Ms, .Mt, .Nd, code.Nm, .Pa, .Rs,
 .St, .Sx, .Sy, .Va, .Vt, .Xr {
 		display: inline flow;
 		position: relative; }
 
 .An::before {	content: "An"; }
 .Ar::before {	content: "Ar"; }
 .Cd::before {	content: "Cd"; }
 .Cm::before {	content: "Cm"; }
 .Dv::before {	content: "Dv"; }
 .Em::before {	content: "Em"; }
 .Er::before {	content: "Er"; }
 .Ev::before {	content: "Ev"; }
 .Fa::before {	content: "Fa"; }
 .Fd::before {	content: "Fd"; }
 .Fl::before {	content: "Fl"; }
 .Fn::before {	content: "Fn"; }
 .Ft::before {	content: "Ft"; }
 .Ic::before {	content: "Ic"; }
 code.In::before { content: "In"; }
 .Lb::before {	content: "Lb"; }
 .Lk::before {	content: "Lk"; }
 .Ms::before {	content: "Ms"; }
 .Mt::before {	content: "Mt"; }
 .Nd::before {	content: "Nd"; }
 code.Nm::before { content: "Nm"; }
 .Pa::before {	content: "Pa"; }
 .Rs::before {	content: "Rs"; }
 h2.Sh::before {	content: "Sh"; }
 h3.Ss::before {	content: "Ss"; }
 .St::before {	content: "St"; }
 .Sx::before {	content: "Sx"; }
 .Sy::before {	content: "Sy"; }
 .Va::before {	content: "Va"; }
 .Vt::before {	content: "Vt"; }
 .Xr::before {	content: "Xr"; }
 
 .An::before, .Ar::before, .Cd::before, .Cm::before,
 .Dv::before, .Em::before, .Er::before, .Ev::before,
 .Fa::before, .Fd::before, .Fl::before, .Fn::before, .Ft::before,
 .Ic::before, code.In::before, .Lb::before, .Lk::before,
 .Ms::before, .Mt::before, .Nd::before, code.Nm::before,
 .Pa::before, .Rs::before,
 h2.Sh::before, h3.Ss::before, .St::before, .Sx::before, .Sy::before,
 .Va::before, .Vt::before, .Xr::before {
 		opacity: 0;
 		transition: .15s ease opacity;
 		pointer-events: none;
 		position: absolute;
 		bottom: 100%;
 		box-shadow: 0 0 .35em var(--fg);
 		padding: .15em .25em;
 		white-space: nowrap;
 		font-family: Helvetica,Arial,sans-serif;
 		font-style: normal;
 		font-weight: bold;
 		background: var(--bg);
 		color: var(--fg); }
 .An:hover::before, .Ar:hover::before, .Cd:hover::before, .Cm:hover::before,
 .Dv:hover::before, .Em:hover::before, .Er:hover::before, .Ev:hover::before,
 .Fa:hover::before, .Fd:hover::before, .Fl:hover::before, .Fn:hover::before,
 .Ft:hover::before, .Ic:hover::before, code.In:hover::before,
 .Lb:hover::before, .Lk:hover::before, .Ms:hover::before, .Mt:hover::before,
 .Nd:hover::before, code.Nm:hover::before, .Pa:hover::before,
 .Rs:hover::before, h2.Sh:hover::before, h3.Ss:hover::before, .St:hover::before,
 .Sx:hover::before, .Sy:hover::before, .Va:hover::before, .Vt:hover::before,
 .Xr:hover::before {
 		opacity: 1;
 		pointer-events: inherit; }
 
 /* Overrides to avoid excessive margins on small devices. */
 
 @media (max-width: 37.5em) {
 main {		margin-left: 0.5em; }
 h2.Sh, h3.Ss {	margin-left: 0em; }
 .Bd-indent {	margin-left: 2em; }
 .Bl-hang > dd {
 		margin-left: 2em; }
 .Bl-tag {	margin-left: 2em; }
 .Bl-tag > dt {
 		margin-left: -2em; }
 .HP {		margin-left: 2em;
 		text-indent: -2em; }
 }
 
 /* Overrides for a dark color scheme for accessibility. */
 
 @media (prefers-color-scheme: dark) {
 html {		--bg: #1E1F21;
 		--fg: #EEEFF1; }
 :link {		color: #BAD7FF; }
 :visited {	color: #F6BAFF; }
 }
diff --git a/contrib/mandoc/mandocd.8 b/contrib/mandoc/mandocd.8
index d679deb1b9e4..aaf4e3dede70 100644
--- a/contrib/mandoc/mandocd.8
+++ b/contrib/mandoc/mandocd.8
@@ -1,198 +1,212 @@
-.\"	$Id: mandocd.8,v 1.3 2021/09/28 15:41:41 schwarze Exp $
+.\" $Id: mandocd.8,v 1.5 2025/06/30 15:07:38 schwarze Exp $
 .\"
-.\" Copyright (c) 2017 Ingo Schwarze 
+.\" Copyright (c) 2017, 2025 Ingo Schwarze 
 .\"
 .\" Permission to use, copy, modify, and distribute this software for any
 .\" purpose with or without fee is hereby granted, provided that the above
 .\" copyright notice and this permission notice appear in all copies.
 .\"
 .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\"
-.Dd $Mdocdate: September 28 2021 $
+.Dd $Mdocdate: June 30 2025 $
 .Dt MANDOCD 8
 .Os
 .Sh NAME
 .Nm mandocd
 .Nd server process to format manual pages in batch mode
 .Sh SYNOPSIS
 .Nm mandocd
 .Op Fl I Cm os Ns = Ns Ar name
 .Op Fl T Ar output
 .Ar socket_fd
 .Sh DESCRIPTION
 The
 .Nm
 utility formats many manual pages without requiring
 .Xr fork 2
 and
 .Xr exec 3
 overhead in between.
 It does not require listing all the manuals to be formatted on the
 command line, and it supports writing each formatted manual to its
 own file descriptor.
 .Pp
 This server requires that a connected UNIX domain
 .Xr socket 2
 is already present at
 .Xr exec 3
 time.
 Consequently, it cannot be started from the
 .Xr sh 1
 command line because the shell cannot supply such a socket.
 Typically, the socket is created by the parent process using
 .Xr socketpair 2
 before calling
 .Xr fork 2
 and
 .Xr exec 3
 on
 .Nm .
 The parent process will pass the file descriptor number as an argument to
 .Xr exec 3 ,
 formatted as a decimal ASCII-encoded integer.
 See
 .Xr catman 8
 for a typical implementation of a parent process.
 .Pp
 .Nm
 loops reading one-byte messages with
 .Xr recvmsg 2
 from the file descriptor number
 .Ar socket_fd .
 It ignores the byte read and only uses the out-of-band auxiliary
 .Vt struct cmsghdr
 control data, typically supplied by the calling process using
 .Xr CMSG_FIRSTHDR 3 .
 The parent process is expected to pass three file descriptors
 with each dummy byte.
 The first one is used for
 .Xr mdoc 7
 or
 .Xr man 7
 input, the second one for formatted output, and the third one
 for error output.
 .Pp
+After accepting each message,
+.Nm
+replies with a one-byte message of its own,
+such that the parent process can keep track of how many messages
+.Nm
+has already accepted and how many file descriptors
+consequently are still in flight, such that the parent process
+can limit the number of file descriptors in flight at any given time
+in order to prevent
+.Er EMFILE
+failure of
+.Xr sendmsg 2 .
+.Pp
 The options are as follows:
 .Bl -tag -width Ds
 .It Fl I Cm os Ns = Ns Ar name
 Override the default operating system
 .Ar name
 for the
 .Xr mdoc 7
 .Ic \&Os
 and for the
 .Xr man 7
 .Ic TH
 macro.
 .It Fl T Ar output
 Output format.
 The
 .Ar output
 argument can be
 .Cm ascii ,
 .Cm utf8 ,
 or
 .Cm html ;
 see
 .Xr mandoc 1 .
 In
 .Cm html
 output mode, the
 .Cm fragment
 output option is implied.
 Other output options are not supported.
 .El
 .Pp
 After exhausting one input file descriptor, all three file descriptors
 are closed before reading the next dummy byte and control message.
 .Pp
-When a zero-byte message is read, when the
+When a zero-byte message or a misformatted message is read, when the
 .Ar socket_fd
 is closed by the parent process,
 or when an error occurs,
 .Nm
 exits.
 .Sh EXIT STATUS
 .Ex -std
 .Pp
 A zero-byte message or a closed
 .Ar socket_fd
 is considered success.
 Possible errors include:
 .Bl -bullet
 .It
 missing, invalid, or excessive
 .Xr exec 3
 arguments
 .It
+communication failure with the parent, for example failure in
 .Xr recvmsg 2
-failure, for example due to
-.Er EMSGSIZE
+or
+.Xr send 2
 .It
 missing or unexpected control data, in particular a
 .Fa cmsg_level
 in the
 .Vt struct cmsghdr
 that differs from
 .Dv SOL_SOCKET ,
 a
 .Fa cmsg_type
 that differs from
 .Dv SCM_RIGHTS ,
 or a
 .Fa cmsg_len
 that is not three times the size of an
 .Vt int
 .It
 invalid file descriptors passed in the
 .Xr CMSG_DATA 3
 .It
 resource exhaustion, in particular
 .Xr dup 2
 or
 .Xr malloc 3
 failure
 .El
 .Pp
 Except for memory exhaustion and similar system-level failures,
 parsing and formatting errors do not cause
 .Nm
 to return an error exit status.
 Even after severe parsing errors,
 .Nm
 will simply accept and process the next input file descriptor.
 .Sh SEE ALSO
 .Xr mandoc 1 ,
 .Xr mandoc 3 ,
 .Xr catman 8
 .Sh HISTORY
 The
 .Nm
 utility appeared in version 1.14.1 of the
 .Sy mandoc
 toolkit.
 .Sh AUTHORS
 .An -nosplit
 The concept was designed and implemented by
 .An Michael Stapelberg Aq Mt stapelberg@debian.org .
 The
 .Xr mandoc 3
 glue needed to make it a stand-alone process was added by
 .An Ingo Schwarze Aq Mt schwarze@openbsd.org .
 .Sh CAVEATS
 If the parsed manual pages contain
 .Xr roff 7
 .Pf . Ic so
 requests,
 .Nm
 needs to be started with the current working directory set to the
 root of the manual page tree.
 Avoid starting it in directories that contain secret files in any
 subdirectories, in particular if the user starting it has read
 access to these secret files.
diff --git a/contrib/mandoc/mandocd.c b/contrib/mandoc/mandocd.c
index ccc846bd0310..52ba0cc613fa 100644
--- a/contrib/mandoc/mandocd.c
+++ b/contrib/mandoc/mandocd.c
@@ -1,293 +1,319 @@
-/* $Id: mandocd.c,v 1.13 2022/04/14 16:43:44 schwarze Exp $ */
+/* $Id: mandocd.c,v 1.15 2025/06/30 15:04:57 schwarze Exp $ */
 /*
+ * Copyright (c) 2017-2019, 2022, 2025 Ingo Schwarze 
  * Copyright (c) 2017 Michael Stapelberg 
- * Copyright (c) 2017, 2019, 2021 Ingo Schwarze 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #if NEED_XPG4_2
 #define _XPG4_2
 #endif
 
 #include 
 #include 
 
 #if HAVE_ERR
 #include 
 #endif
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include "mandoc.h"
 #if DEBUG_MEMORY
 #define DEBUG_NODEF 1
 #include "mandoc_dbg.h"
 #endif
 #include "roff.h"
 #include "mdoc.h"
 #include "man.h"
 #include "mandoc_parse.h"
 #include "main.h"
 #include "manconf.h"
 
 enum	outt {
 	OUTT_ASCII = 0,
 	OUTT_UTF8,
 	OUTT_HTML
 };
 
 static	void	  process(struct mparse *, enum outt, void *);
 static	int	  read_fds(int, int *);
 static	void	  usage(void) __attribute__((__noreturn__));
 
 
 #define NUM_FDS 3
 static int
 read_fds(int clientfd, int *fds)
 {
+	const struct timespec timeout = { 0, 10000000 };  /* 0.01 s */
 	struct msghdr	 msg;
 	struct iovec	 iov[1];
 	unsigned char	 dummy[1];
 	struct cmsghdr	*cmsg;
 	int		*walk;
 	int		 cnt;
 
 	/* Union used for alignment. */
 	union {
 		uint8_t controlbuf[CMSG_SPACE(NUM_FDS * sizeof(int))];
 		struct cmsghdr align;
 	} u;
 
 	memset(&msg, '\0', sizeof(msg));
 	msg.msg_control = u.controlbuf;
 	msg.msg_controllen = sizeof(u.controlbuf);
 
 	/*
 	 * Read a dummy byte - sendmsg cannot send an empty message,
 	 * even if we are only interested in the OOB data.
 	 */
 
 	iov[0].iov_base = dummy;
 	iov[0].iov_len = sizeof(dummy);
 	msg.msg_iov = iov;
 	msg.msg_iovlen = 1;
 
 	switch (recvmsg(clientfd, &msg, 0)) {
 	case -1:
 		warn("recvmsg");
 		return -1;
 	case 0:
 		return 0;
 	default:
 		break;
 	}
 
+	*dummy = '\0';
+	while (send(clientfd, dummy, sizeof(dummy), 0) == -1) {
+		if (errno != EAGAIN) {
+			warn("send");
+			return -1;
+		}
+		nanosleep(&timeout, NULL);
+	}
+
 	if ((cmsg = CMSG_FIRSTHDR(&msg)) == NULL) {
 		warnx("CMSG_FIRSTHDR: missing control message");
 		return -1;
 	}
 
 	if (cmsg->cmsg_level != SOL_SOCKET ||
 	    cmsg->cmsg_type != SCM_RIGHTS ||
 	    cmsg->cmsg_len != CMSG_LEN(NUM_FDS * sizeof(int))) {
 		warnx("CMSG_FIRSTHDR: invalid control message");
 		return -1;
 	}
 
 	walk = (int *)CMSG_DATA(cmsg);
 	for (cnt = 0; cnt < NUM_FDS; cnt++)
 		fds[cnt] = *walk++;
 
 	return 1;
 }
 
 int
 main(int argc, char *argv[])
 {
+	struct sigaction	 sa;
 	struct manoutput	 options;
 	struct mparse		*parser;
 	void			*formatter;
 	const char		*defos;
 	const char		*errstr;
 	int			 clientfd;
 	int			 old_stdin;
 	int			 old_stdout;
 	int			 old_stderr;
 	int			 fds[3];
 	int			 state, opt;
 	enum outt		 outtype;
 
 #if DEBUG_MEMORY
 	mandoc_dbg_init(argc, argv);
 #endif
 
 	defos = NULL;
 	outtype = OUTT_ASCII;
 	while ((opt = getopt(argc, argv, "I:T:")) != -1) {
 		switch (opt) {
 		case 'I':
 			if (strncmp(optarg, "os=", 3) == 0)
 				defos = optarg + 3;
 			else {
 				warnx("-I %s: Bad argument", optarg);
 				usage();
 			}
 			break;
 		case 'T':
 			if (strcmp(optarg, "ascii") == 0)
 				outtype = OUTT_ASCII;
 			else if (strcmp(optarg, "utf8") == 0)
 				outtype = OUTT_UTF8;
 			else if (strcmp(optarg, "html") == 0)
 				outtype = OUTT_HTML;
 			else {
 				warnx("-T %s: Bad argument", optarg);
 				usage();
 			}
 			break;
 		default:
 			usage();
 		}
 	}
 
 	if (argc > 0) {
 		argc -= optind;
 		argv += optind;
 	}
-	if (argc != 1)
+	if (argc != 1) {
+		if (argc == 0)
+			warnx("missing argument: socket_fd");
+		else
+			warnx("too many arguments: %s", argv[1]);
 		usage();
+	}
 
 	errstr = NULL;
 	clientfd = strtonum(argv[0], 3, INT_MAX, &errstr);
 	if (errstr)
-		errx(1, "file descriptor %s %s", argv[1], errstr);
+		errx(1, "file descriptor %s is %s", argv[0], errstr);
+
+	memset(&sa, 0, sizeof(sa));
+	sa.sa_handler = SIG_IGN;
+	if (sigfillset(&sa.sa_mask) == -1)
+		err(1, "sigfillset");
+	if (sigaction(SIGPIPE, &sa, NULL) == -1)
+		err(1, "sigaction(SIGPIPE)");
 
 	mchars_alloc();
 	parser = mparse_alloc(MPARSE_SO | MPARSE_UTF8 | MPARSE_LATIN1 |
 	    MPARSE_VALIDATE, MANDOC_OS_OTHER, defos);
 
 	memset(&options, 0, sizeof(options));
 	switch (outtype) {
 	case OUTT_ASCII:
 		formatter = ascii_alloc(&options);
 		break;
 	case OUTT_UTF8:
 		formatter = utf8_alloc(&options);
 		break;
 	case OUTT_HTML:
 		options.fragment = 1;
 		formatter = html_alloc(&options);
 		break;
 	}
 
 	state = 1;  /* work to do */
 	fflush(stdout);
 	fflush(stderr);
 	if ((old_stdin = dup(STDIN_FILENO)) == -1 ||
 	    (old_stdout = dup(STDOUT_FILENO)) == -1 ||
 	    (old_stderr = dup(STDERR_FILENO)) == -1) {
 		warn("dup");
 		state = -1;  /* error */
 	}
 
 	while (state == 1 && (state = read_fds(clientfd, fds)) == 1) {
 		if (dup2(fds[0], STDIN_FILENO) == -1 ||
 		    dup2(fds[1], STDOUT_FILENO) == -1 ||
 		    dup2(fds[2], STDERR_FILENO) == -1) {
 			warn("dup2");
 			state = -1;
 			break;
 		}
 
 		close(fds[0]);
 		close(fds[1]);
 		close(fds[2]);
 
 		process(parser, outtype, formatter);
 		mparse_reset(parser);
 		if (outtype == OUTT_HTML)
 			html_reset(formatter);
 
 		fflush(stdout);
 		fflush(stderr);
 		/* Close file descriptors by restoring the old ones. */
 		if (dup2(old_stderr, STDERR_FILENO) == -1 ||
 		    dup2(old_stdout, STDOUT_FILENO) == -1 ||
 		    dup2(old_stdin, STDIN_FILENO) == -1) {
 			warn("dup2");
 			state = -1;
 			break;
 		}
 	}
 
 	close(clientfd);
 	switch (outtype) {
 	case OUTT_ASCII:
 	case OUTT_UTF8:
 		ascii_free(formatter);
 		break;
 	case OUTT_HTML:
 		html_free(formatter);
 		break;
 	}
 	mparse_free(parser);
 	mchars_free();
 #if DEBUG_MEMORY
 	mandoc_dbg_finish();
 #endif
 	return state == -1 ? 1 : 0;
 }
 
 static void
 process(struct mparse *parser, enum outt outtype, void *formatter)
 {
 	struct roff_meta *meta;
 
 	mparse_readfd(parser, STDIN_FILENO, "");
 	meta = mparse_result(parser);
 	if (meta->macroset == MACROSET_MDOC) {
 		switch (outtype) {
 		case OUTT_ASCII:
 		case OUTT_UTF8:
 			terminal_mdoc(formatter, meta);
 			break;
 		case OUTT_HTML:
 			html_mdoc(formatter, meta);
 			break;
 		}
 	}
 	if (meta->macroset == MACROSET_MAN) {
 		switch (outtype) {
 		case OUTT_ASCII:
 		case OUTT_UTF8:
 			terminal_man(formatter, meta);
 			break;
 		case OUTT_HTML:
 			html_man(formatter, meta);
 			break;
 		}
 	}
 }
 
 void
 usage(void)
 {
 	fprintf(stderr, "usage: mandocd [-I os=name] [-T output] socket_fd\n");
 	exit(1);
 }
diff --git a/contrib/mandoc/manpath.c b/contrib/mandoc/manpath.c
index 3760e2293c3a..f744368b5a38 100644
--- a/contrib/mandoc/manpath.c
+++ b/contrib/mandoc/manpath.c
@@ -1,352 +1,349 @@
-/*	$Id: manpath.c,v 1.44 2021/11/05 18:03:08 schwarze Exp $ */
+/* $Id: manpath.c,v 1.45 2025/06/26 17:26:23 schwarze Exp $ */
 /*
- * Copyright (c) 2011,2014,2015,2017-2019 Ingo Schwarze 
+ * Copyright (c) 2011,2014,2015,2017-2021 Ingo Schwarze 
  * Copyright (c) 2011 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #include 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "manconf.h"
 
 static	void	 manconf_file(struct manconf *, const char *, int);
 static	void	 manpath_add(struct manpaths *, const char *, char);
 static	void	 manpath_parseline(struct manpaths *, char *, char);
 
 
 void
 manconf_parse(struct manconf *conf, const char *file, char *pend, char *pbeg)
 {
 	int use_path_from_file = 1;
 
 	/* Always prepend -m. */
 	manpath_parseline(&conf->manpath, pbeg, 'm');
 
 	if (pend != NULL && *pend != '\0') {
 		/* If -M is given, it overrides everything else. */
 		manpath_parseline(&conf->manpath, pend, 'M');
 		use_path_from_file = 0;
 		pbeg = pend = NULL;
 	} else if ((pbeg = getenv("MANPATH")) == NULL || *pbeg == '\0') {
 		/* No MANPATH; use man.conf(5) only. */
 		pbeg = pend = NULL;
 	} else if (*pbeg == ':') {
 		/* Prepend man.conf(5) to MANPATH. */
 		pend = pbeg + 1;
 		pbeg = NULL;
 	} else if ((pend = strstr(pbeg, "::")) != NULL) {
 		/* Insert man.conf(5) into MANPATH. */
 		*pend = '\0';
 		pend += 2;
 	} else if (pbeg[strlen(pbeg) - 1] == ':') {
 		/* Append man.conf(5) to MANPATH. */
 		pend = NULL;
 	} else {
 		/* MANPATH overrides man.conf(5) completely. */
 		use_path_from_file = 0;
 		pend = NULL;
 	}
 
 	manpath_parseline(&conf->manpath, pbeg, '\0');
 
 	if (file == NULL)
 		file = MAN_CONF_FILE;
 	manconf_file(conf, file, use_path_from_file);
 
 	manpath_parseline(&conf->manpath, pend, '\0');
 }
 
 void
 manpath_base(struct manpaths *dirs)
 {
 	char path_base[] = MANPATH_BASE;
 	manpath_parseline(dirs, path_base, '\0');
 }
 
 /*
  * Parse a FULL pathname from a colon-separated list of arrays.
  */
 static void
 manpath_parseline(struct manpaths *dirs, char *path, char option)
 {
 	char	*dir;
 
 	if (NULL == path)
 		return;
 
 	for (dir = strtok(path, ":"); dir; dir = strtok(NULL, ":"))
 		manpath_add(dirs, dir, option);
 }
 
 /*
  * Add a directory to the array, ignoring bad directories.
  * Grow the array one-by-one for simplicity's sake.
  */
 static void
 manpath_add(struct manpaths *dirs, const char *dir, char option)
 {
 	char		 buf[PATH_MAX];
 	struct stat	 sb;
 	char		*cp;
 	size_t		 i;
 
 	if ((cp = realpath(dir, buf)) == NULL)
 		goto fail;
 
 	for (i = 0; i < dirs->sz; i++)
 		if (strcmp(dirs->paths[i], dir) == 0)
 			return;
 
 	if (stat(cp, &sb) == -1)
 		goto fail;
 
 	dirs->paths = mandoc_reallocarray(dirs->paths,
 	    dirs->sz + 1, sizeof(*dirs->paths));
 	dirs->paths[dirs->sz++] = mandoc_strdup(cp);
 	return;
 
 fail:
 	if (option != '\0')
 		mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0,
 		    "-%c %s: %s", option, dir, strerror(errno));
 }
 
 void
 manconf_free(struct manconf *conf)
 {
 	size_t		 i;
 
 	for (i = 0; i < conf->manpath.sz; i++)
 		free(conf->manpath.paths[i]);
 
 	free(conf->manpath.paths);
 	free(conf->output.includes);
 	free(conf->output.man);
 	free(conf->output.paper);
 	free(conf->output.style);
 }
 
 static void
 manconf_file(struct manconf *conf, const char *file, int use_path_from_file)
 {
 	const char *const toks[] = { "manpath", "output" };
 	char manpath_default[] = MANPATH_DEFAULT;
 
 	FILE		*stream;
 	char		*line, *cp, *ep;
 	size_t		 linesz, tok, toklen;
 	ssize_t		 linelen;
 
 	if ((stream = fopen(file, "r")) == NULL)
 		goto out;
 
 	line = NULL;
 	linesz = 0;
 
 	while ((linelen = getline(&line, &linesz, stream)) != -1) {
 		cp = line;
 		ep = cp + linelen - 1;
 		while (ep > cp && isspace((unsigned char)*ep))
 			*ep-- = '\0';
 		while (isspace((unsigned char)*cp))
 			cp++;
 		if (cp == ep || *cp == '#')
 			continue;
 
 		for (tok = 0; tok < sizeof(toks)/sizeof(toks[0]); tok++) {
 			toklen = strlen(toks[tok]);
 			if (cp + toklen < ep &&
 			    isspace((unsigned char)cp[toklen]) &&
 			    strncmp(cp, toks[tok], toklen) == 0) {
 				cp += toklen;
 				while (isspace((unsigned char)*cp))
 					cp++;
 				break;
 			}
 		}
 
 		switch (tok) {
 		case 0:  /* manpath */
 			if (use_path_from_file)
 				manpath_add(&conf->manpath, cp, '\0');
 			*manpath_default = '\0';
 			break;
 		case 1:  /* output */
 			manconf_output(&conf->output, cp, 1);
 			break;
 		default:
 			break;
 		}
 	}
 	free(line);
 	fclose(stream);
 
 out:
 	if (use_path_from_file && *manpath_default != '\0')
 		manpath_parseline(&conf->manpath, manpath_default, '\0');
 }
 
 int
 manconf_output(struct manoutput *conf, const char *cp, int fromfile)
 {
 	const char *const toks[] = {
 	    /* Tokens requiring an argument. */
 	    "includes", "man", "paper", "style", "indent", "width",
 	    "outfilename", "tagfilename",
 	    /* Token taking an optional argument. */
 	    "tag",
 	    /* Tokens not taking arguments. */
-	    "fragment", "mdoc", "noval", "toc"
+	    "fragment", "noval", "toc"
 	};
 	const size_t ntoks = sizeof(toks) / sizeof(toks[0]);
 
 	const char	*errstr;
 	char		*oldval;
 	size_t		 len, tok;
 
 	for (tok = 0; tok < ntoks; tok++) {
 		len = strlen(toks[tok]);
 		if (strncmp(cp, toks[tok], len) == 0 &&
 		    strchr(" =	", cp[len]) != NULL) {
 			cp += len;
 			if (*cp == '=')
 				cp++;
 			while (isspace((unsigned char)*cp))
 				cp++;
 			break;
 		}
 	}
 
 	if (tok < 8 && *cp == '\0') {
 		mandoc_msg(MANDOCERR_BADVAL_MISS, 0, 0, "-O %s=?", toks[tok]);
 		return -1;
 	}
 	if (tok > 8 && tok < ntoks && *cp != '\0') {
 		mandoc_msg(MANDOCERR_BADVAL, 0, 0, "-O %s=%s", toks[tok], cp);
 		return -1;
 	}
 
 	switch (tok) {
 	case 0:
 		if (conf->includes != NULL) {
 			oldval = mandoc_strdup(conf->includes);
 			break;
 		}
 		conf->includes = mandoc_strdup(cp);
 		return 0;
 	case 1:
 		if (conf->man != NULL) {
 			oldval = mandoc_strdup(conf->man);
 			break;
 		}
 		conf->man = mandoc_strdup(cp);
 		return 0;
 	case 2:
 		if (conf->paper != NULL) {
 			oldval = mandoc_strdup(conf->paper);
 			break;
 		}
 		conf->paper = mandoc_strdup(cp);
 		return 0;
 	case 3:
 		if (conf->style != NULL) {
 			oldval = mandoc_strdup(conf->style);
 			break;
 		}
 		conf->style = mandoc_strdup(cp);
 		return 0;
 	case 4:
 		if (conf->indent) {
 			mandoc_asprintf(&oldval, "%zu", conf->indent);
 			break;
 		}
 		conf->indent = strtonum(cp, 0, 1000, &errstr);
 		if (errstr == NULL)
 			return 0;
 		mandoc_msg(MANDOCERR_BADVAL_BAD, 0, 0,
 		    "-O indent=%s is %s", cp, errstr);
 		return -1;
 	case 5:
 		if (conf->width) {
 			mandoc_asprintf(&oldval, "%zu", conf->width);
 			break;
 		}
 		conf->width = strtonum(cp, 1, 1000, &errstr);
 		if (errstr == NULL)
 			return 0;
 		mandoc_msg(MANDOCERR_BADVAL_BAD, 0, 0,
 		    "-O width=%s is %s", cp, errstr);
 		return -1;
 	case 6:
 		if (conf->outfilename != NULL) {
 			oldval = mandoc_strdup(conf->outfilename);
 			break;
 		}
 		conf->outfilename = mandoc_strdup(cp);
 		return 0;
 	case 7:
 		if (conf->tagfilename != NULL) {
 			oldval = mandoc_strdup(conf->tagfilename);
 			break;
 		}
 		conf->tagfilename = mandoc_strdup(cp);
 		return 0;
 	/*
 	 * If the index of the following token changes,
 	 * do not forget to adjust the range check above the switch.
 	 */
 	case 8:
 		if (conf->tag != NULL) {
 			oldval = mandoc_strdup(conf->tag);
 			break;
 		}
 		conf->tag = mandoc_strdup(cp);
 		return 0;
 	case 9:
 		conf->fragment = 1;
 		return 0;
 	case 10:
-		conf->mdoc = 1;
-		return 0;
-	case 11:
 		conf->noval = 1;
 		return 0;
-	case 12:
+	case 11:
 		conf->toc = 1;
 		return 0;
 	default:
 		mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0, "-O %s", cp);
 		return -1;
 	}
 	if (fromfile) {
 		free(oldval);
 		return 0;
 	} else {
 		mandoc_msg(MANDOCERR_BADVAL_DUPE, 0, 0,
 		    "-O %s=%s: already set to %s", toks[tok], cp, oldval);
 		free(oldval);
 		return -1;
 	}
 }
diff --git a/contrib/mandoc/mdoc_html.c b/contrib/mandoc/mdoc_html.c
index b67eac4be233..8ac3884c7225 100644
--- a/contrib/mandoc/mdoc_html.c
+++ b/contrib/mandoc/mdoc_html.c
@@ -1,1806 +1,1815 @@
-/* $Id: mdoc_html.c,v 1.353 2025/01/25 00:22:28 schwarze Exp $ */
+/* $Id: mdoc_html.c,v 1.354 2025/06/26 17:06:34 schwarze Exp $ */
 /*
  * Copyright (c) 2014-2022, 2025 Ingo Schwarze 
  * Copyright (c) 2008-2011, 2014 Kristaps Dzonsons 
  * Copyright (c) 2022 Anna Vyalkova 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
  * HTML formatter for mdoc(7) used by mandoc(1).
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "roff.h"
 #include "mdoc.h"
 #include "out.h"
 #include "html.h"
 #include "main.h"
 
 #define	MDOC_ARGS	  const struct roff_meta *meta, \
 			  struct roff_node *n, \
 			  struct html *h
 
 #ifndef MIN
 #define	MIN(a,b)	((/*CONSTCOND*/(a)<(b))?(a):(b))
 #endif
 
 struct	mdoc_html_act {
 	int		(*pre)(MDOC_ARGS);
 	void		(*post)(MDOC_ARGS);
 };
 
 static	void		  print_mdoc_head(const struct roff_meta *,
 				struct html *);
 static	void		  print_mdoc_node(MDOC_ARGS);
 static	void		  print_mdoc_nodelist(MDOC_ARGS);
 static	void		  synopsis_pre(struct html *, struct roff_node *);
 
 static	void		  mdoc_root_post(const struct roff_meta *,
 				struct html *);
 static	int		  mdoc_root_pre(const struct roff_meta *,
 				struct html *);
 
 static	void		  mdoc__x_post(MDOC_ARGS);
 static	int		  mdoc__x_pre(MDOC_ARGS);
 static	int		  mdoc_abort_pre(MDOC_ARGS);
 static	int		  mdoc_ad_pre(MDOC_ARGS);
 static	int		  mdoc_an_pre(MDOC_ARGS);
 static	int		  mdoc_ap_pre(MDOC_ARGS);
 static	int		  mdoc_ar_pre(MDOC_ARGS);
 static	int		  mdoc_bd_pre(MDOC_ARGS);
 static	int		  mdoc_bf_pre(MDOC_ARGS);
 static	void		  mdoc_bk_post(MDOC_ARGS);
 static	int		  mdoc_bk_pre(MDOC_ARGS);
 static	int		  mdoc_bl_pre(MDOC_ARGS);
 static	int		  mdoc_cd_pre(MDOC_ARGS);
 static	int		  mdoc_code_pre(MDOC_ARGS);
 static	int		  mdoc_d1_pre(MDOC_ARGS);
 static	int		  mdoc_fa_pre(MDOC_ARGS);
 static	int		  mdoc_fd_pre(MDOC_ARGS);
 static	int		  mdoc_fl_pre(MDOC_ARGS);
 static	int		  mdoc_fn_pre(MDOC_ARGS);
 static	int		  mdoc_ft_pre(MDOC_ARGS);
 static	int		  mdoc_em_pre(MDOC_ARGS);
 static	void		  mdoc_eo_post(MDOC_ARGS);
 static	int		  mdoc_eo_pre(MDOC_ARGS);
 static	int		  mdoc_ex_pre(MDOC_ARGS);
 static	void		  mdoc_fo_post(MDOC_ARGS);
 static	int		  mdoc_fo_pre(MDOC_ARGS);
 static	int		  mdoc_igndelim_pre(MDOC_ARGS);
 static	int		  mdoc_in_pre(MDOC_ARGS);
 static	int		  mdoc_it_pre(MDOC_ARGS);
 static	int		  mdoc_lb_pre(MDOC_ARGS);
 static	int		  mdoc_lk_pre(MDOC_ARGS);
 static	int		  mdoc_mt_pre(MDOC_ARGS);
 static	int		  mdoc_nd_pre(MDOC_ARGS);
 static	int		  mdoc_nm_pre(MDOC_ARGS);
 static	int		  mdoc_no_pre(MDOC_ARGS);
 static	int		  mdoc_ns_pre(MDOC_ARGS);
 static	int		  mdoc_pa_pre(MDOC_ARGS);
 static	void		  mdoc_pf_post(MDOC_ARGS);
 static	int		  mdoc_pp_pre(MDOC_ARGS);
 static	void		  mdoc_quote_post(MDOC_ARGS);
 static	int		  mdoc_quote_pre(MDOC_ARGS);
 static	int		  mdoc_rs_pre(MDOC_ARGS);
 static	int		  mdoc_sh_pre(MDOC_ARGS);
 static	int		  mdoc_skip_pre(MDOC_ARGS);
 static	int		  mdoc_sm_pre(MDOC_ARGS);
 static	int		  mdoc_ss_pre(MDOC_ARGS);
 static	int		  mdoc_st_pre(MDOC_ARGS);
 static	int		  mdoc_sx_pre(MDOC_ARGS);
 static	int		  mdoc_sy_pre(MDOC_ARGS);
 static	int		  mdoc_tg_pre(MDOC_ARGS);
 static	int		  mdoc_va_pre(MDOC_ARGS);
 static	int		  mdoc_vt_pre(MDOC_ARGS);
 static	int		  mdoc_xr_pre(MDOC_ARGS);
 static	int		  mdoc_xx_pre(MDOC_ARGS);
 
 static const struct mdoc_html_act mdoc_html_acts[MDOC_MAX - MDOC_Dd] = {
 	{NULL, NULL}, /* Dd */
 	{NULL, NULL}, /* Dt */
 	{NULL, NULL}, /* Os */
 	{mdoc_sh_pre, NULL }, /* Sh */
 	{mdoc_ss_pre, NULL }, /* Ss */
 	{mdoc_pp_pre, NULL}, /* Pp */
 	{mdoc_d1_pre, NULL}, /* D1 */
 	{mdoc_d1_pre, NULL}, /* Dl */
 	{mdoc_bd_pre, NULL}, /* Bd */
 	{NULL, NULL}, /* Ed */
 	{mdoc_bl_pre, NULL}, /* Bl */
 	{NULL, NULL}, /* El */
 	{mdoc_it_pre, NULL}, /* It */
 	{mdoc_ad_pre, NULL}, /* Ad */
 	{mdoc_an_pre, NULL}, /* An */
 	{mdoc_ap_pre, NULL}, /* Ap */
 	{mdoc_ar_pre, NULL}, /* Ar */
 	{mdoc_cd_pre, NULL}, /* Cd */
 	{mdoc_code_pre, NULL}, /* Cm */
 	{mdoc_code_pre, NULL}, /* Dv */
 	{mdoc_code_pre, NULL}, /* Er */
 	{mdoc_code_pre, NULL}, /* Ev */
 	{mdoc_ex_pre, NULL}, /* Ex */
 	{mdoc_fa_pre, NULL}, /* Fa */
 	{mdoc_fd_pre, NULL}, /* Fd */
 	{mdoc_fl_pre, NULL}, /* Fl */
 	{mdoc_fn_pre, NULL}, /* Fn */
 	{mdoc_ft_pre, NULL}, /* Ft */
 	{mdoc_code_pre, NULL}, /* Ic */
 	{mdoc_in_pre, NULL}, /* In */
 	{mdoc_code_pre, NULL}, /* Li */
 	{mdoc_nd_pre, NULL}, /* Nd */
 	{mdoc_nm_pre, NULL}, /* Nm */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Op */
 	{mdoc_abort_pre, NULL}, /* Ot */
 	{mdoc_pa_pre, NULL}, /* Pa */
 	{mdoc_ex_pre, NULL}, /* Rv */
 	{mdoc_st_pre, NULL}, /* St */
 	{mdoc_va_pre, NULL}, /* Va */
 	{mdoc_vt_pre, NULL}, /* Vt */
 	{mdoc_xr_pre, NULL}, /* Xr */
 	{mdoc__x_pre, mdoc__x_post}, /* %A */
 	{mdoc__x_pre, mdoc__x_post}, /* %B */
 	{mdoc__x_pre, mdoc__x_post}, /* %D */
 	{mdoc__x_pre, mdoc__x_post}, /* %I */
 	{mdoc__x_pre, mdoc__x_post}, /* %J */
 	{mdoc__x_pre, mdoc__x_post}, /* %N */
 	{mdoc__x_pre, mdoc__x_post}, /* %O */
 	{mdoc__x_pre, mdoc__x_post}, /* %P */
 	{mdoc__x_pre, mdoc__x_post}, /* %R */
 	{mdoc__x_pre, mdoc__x_post}, /* %T */
 	{mdoc__x_pre, mdoc__x_post}, /* %V */
 	{NULL, NULL}, /* Ac */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Ao */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Aq */
 	{mdoc_xx_pre, NULL}, /* At */
 	{NULL, NULL}, /* Bc */
 	{mdoc_bf_pre, NULL}, /* Bf */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Bo */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Bq */
 	{mdoc_xx_pre, NULL}, /* Bsx */
 	{mdoc_xx_pre, NULL}, /* Bx */
 	{mdoc_skip_pre, NULL}, /* Db */
 	{NULL, NULL}, /* Dc */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Do */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Dq */
 	{NULL, NULL}, /* Ec */ /* FIXME: no space */
 	{NULL, NULL}, /* Ef */
 	{mdoc_em_pre, NULL}, /* Em */
 	{mdoc_eo_pre, mdoc_eo_post}, /* Eo */
 	{mdoc_xx_pre, NULL}, /* Fx */
 	{mdoc_no_pre, NULL}, /* Ms */
 	{mdoc_no_pre, NULL}, /* No */
 	{mdoc_ns_pre, NULL}, /* Ns */
 	{mdoc_xx_pre, NULL}, /* Nx */
 	{mdoc_xx_pre, NULL}, /* Ox */
 	{NULL, NULL}, /* Pc */
 	{mdoc_igndelim_pre, mdoc_pf_post}, /* Pf */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Po */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Pq */
 	{NULL, NULL}, /* Qc */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Ql */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Qo */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Qq */
 	{NULL, NULL}, /* Re */
 	{mdoc_rs_pre, NULL}, /* Rs */
 	{NULL, NULL}, /* Sc */
 	{mdoc_quote_pre, mdoc_quote_post}, /* So */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Sq */
 	{mdoc_sm_pre, NULL}, /* Sm */
 	{mdoc_sx_pre, NULL}, /* Sx */
 	{mdoc_sy_pre, NULL}, /* Sy */
 	{NULL, NULL}, /* Tn */
 	{mdoc_xx_pre, NULL}, /* Ux */
 	{NULL, NULL}, /* Xc */
 	{NULL, NULL}, /* Xo */
 	{mdoc_fo_pre, mdoc_fo_post}, /* Fo */
 	{NULL, NULL}, /* Fc */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Oo */
 	{NULL, NULL}, /* Oc */
 	{mdoc_bk_pre, mdoc_bk_post}, /* Bk */
 	{NULL, NULL}, /* Ek */
 	{NULL, NULL}, /* Bt */
 	{NULL, NULL}, /* Hf */
 	{mdoc_em_pre, NULL}, /* Fr */
 	{NULL, NULL}, /* Ud */
 	{mdoc_lb_pre, NULL}, /* Lb */
 	{mdoc_abort_pre, NULL}, /* Lp */
 	{mdoc_lk_pre, NULL}, /* Lk */
 	{mdoc_mt_pre, NULL}, /* Mt */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Brq */
 	{mdoc_quote_pre, mdoc_quote_post}, /* Bro */
 	{NULL, NULL}, /* Brc */
 	{mdoc__x_pre, mdoc__x_post}, /* %C */
 	{mdoc_skip_pre, NULL}, /* Es */
 	{mdoc_quote_pre, mdoc_quote_post}, /* En */
 	{mdoc_xx_pre, NULL}, /* Dx */
 	{mdoc__x_pre, mdoc__x_post}, /* %Q */
 	{mdoc__x_pre, mdoc__x_post}, /* %U */
 	{NULL, NULL}, /* Ta */
 	{mdoc_tg_pre, NULL}, /* Tg */
 };
 
 
 /*
  * See the same function in mdoc_term.c for documentation.
  */
 static void
 synopsis_pre(struct html *h, struct roff_node *n)
 {
 	struct roff_node *np;
 
 	if ((n->flags & NODE_SYNPRETTY) == 0 ||
 	    (np = roff_node_prev(n)) == NULL)
 		return;
 
 	if (np->tok == n->tok &&
 	    MDOC_Fo != n->tok &&
 	    MDOC_Ft != n->tok &&
 	    MDOC_Fn != n->tok) {
 		print_otag(h, TAG_BR, "");
 		return;
 	}
 
 	switch (np->tok) {
 	case MDOC_Fd:
 	case MDOC_Fn:
 	case MDOC_Fo:
 	case MDOC_In:
 	case MDOC_Vt:
 		break;
 	case MDOC_Ft:
 		if (n->tok != MDOC_Fn && n->tok != MDOC_Fo)
 			break;
 		/* FALLTHROUGH */
 	default:
 		print_otag(h, TAG_BR, "");
 		return;
 	}
 	html_close_paragraph(h);
 	print_otag(h, TAG_P, "c", "Pp");
 }
 
 void
 html_mdoc(void *arg, const struct roff_meta *mdoc)
 {
 	struct html		*h;
 	struct roff_node	*n;
 	struct tag		*t;
 
 	h = (struct html *)arg;
 	n = mdoc->first->child;
 
 	if ((h->oflags & HTML_FRAGMENT) == 0) {
 		print_gen_decls(h);
 		print_otag(h, TAG_HTML, "");
 		t = print_otag(h, TAG_HEAD, "");
 		print_mdoc_head(mdoc, h);
 		print_tagq(h, t);
 		if (n != NULL && n->type == ROFFT_COMMENT)
 			print_gen_comment(h, n);
 		print_otag(h, TAG_BODY, "");
 	}
 
 	mdoc_root_pre(mdoc, h);
 	t = print_otag(h, TAG_MAIN, "c", "manual-text");
 	print_mdoc_nodelist(mdoc, n, h);
 	print_tagq(h, t);
 	mdoc_root_post(mdoc, h);
 	print_tagq(h, NULL);
 }
 
 static void
 print_mdoc_head(const struct roff_meta *meta, struct html *h)
 {
 	char	*cp;
 
 	print_gen_head(h);
 
 	if (meta->arch != NULL && meta->msec != NULL)
 		mandoc_asprintf(&cp, "%s(%s) (%s)", meta->title,
 		    meta->msec, meta->arch);
 	else if (meta->msec != NULL)
 		mandoc_asprintf(&cp, "%s(%s)", meta->title, meta->msec);
 	else if (meta->arch != NULL)
 		mandoc_asprintf(&cp, "%s (%s)", meta->title, meta->arch);
 	else
 		cp = mandoc_strdup(meta->title);
 
 	print_otag(h, TAG_TITLE, "");
 	print_text(h, cp);
 	free(cp);
 }
 
 static void
 print_mdoc_nodelist(MDOC_ARGS)
 {
 
 	while (n != NULL) {
 		print_mdoc_node(meta, n, h);
 		n = n->next;
 	}
 }
 
 static void
 print_mdoc_node(MDOC_ARGS)
 {
 	struct tag	*t;
 	int		 child;
 
 	if (n->type == ROFFT_COMMENT || n->flags & NODE_NOPRT)
 		return;
 
 	if ((n->flags & NODE_NOFILL) == 0)
 		html_fillmode(h, ROFF_fi);
 	else if (html_fillmode(h, ROFF_nf) == ROFF_nf &&
 	    n->tok != ROFF_fi && n->flags & NODE_LINE)
 		print_endline(h);
 
 	child = 1;
 	n->flags &= ~NODE_ENDED;
 	switch (n->type) {
 	case ROFFT_TEXT:
 		if (n->flags & NODE_LINE) {
 			switch (*n->string) {
 			case '\0':
 				h->col = 1;
 				print_endline(h);
 				return;
 			case ' ':
 				if ((h->flags & HTML_NONEWLINE) == 0 &&
 				    (n->flags & NODE_NOFILL) == 0)
 					print_otag(h, TAG_BR, "");
 				break;
 			default:
 				break;
 			}
 		}
 		t = h->tag;
 		t->refcnt++;
 		if (n->flags & NODE_DELIMC)
 			h->flags |= HTML_NOSPACE;
 		if (n->flags & NODE_HREF)
 			print_tagged_text(h, n->string, n);
 		else
 			print_text(h, n->string);
 		if (n->flags & NODE_DELIMO)
 			h->flags |= HTML_NOSPACE;
 		break;
 	case ROFFT_EQN:
 		t = h->tag;
 		t->refcnt++;
 		print_eqn(h, n->eqn);
 		break;
 	case ROFFT_TBL:
 		/*
 		 * This will take care of initialising all of the table
 		 * state data for the first table, then tearing it down
 		 * for the last one.
 		 */
 		print_tbl(h, n->span);
 		return;
 	default:
 		/*
 		 * Close out the current table, if it's open, and unset
 		 * the "meta" table state.  This will be reopened on the
 		 * next table element.
 		 */
 		if (h->tblt != NULL)
 			print_tblclose(h);
 		assert(h->tblt == NULL);
 		t = h->tag;
 		t->refcnt++;
 		if (n->tok < ROFF_MAX) {
 			roff_html_pre(h, n);
 			t->refcnt--;
 			print_stagq(h, t);
 			return;
 		}
 		assert(n->tok >= MDOC_Dd && n->tok < MDOC_MAX);
 		if (mdoc_html_acts[n->tok - MDOC_Dd].pre != NULL &&
 		    (n->end == ENDBODY_NOT || n->child != NULL))
 			child = (*mdoc_html_acts[n->tok - MDOC_Dd].pre)(meta,
 			    n, h);
 		break;
 	}
 
 	if (h->flags & HTML_KEEP && n->flags & NODE_LINE) {
 		h->flags &= ~HTML_KEEP;
 		h->flags |= HTML_PREKEEP;
 	}
 
 	if (child && n->child != NULL)
 		print_mdoc_nodelist(meta, n->child, h);
 
 	t->refcnt--;
 	print_stagq(h, t);
 
 	switch (n->type) {
 	case ROFFT_TEXT:
 	case ROFFT_EQN:
 		break;
 	default:
 		if (mdoc_html_acts[n->tok - MDOC_Dd].post == NULL ||
 		    n->flags & NODE_ENDED)
 			break;
 		(*mdoc_html_acts[n->tok - MDOC_Dd].post)(meta, n, h);
 		if (n->end != ENDBODY_NOT)
 			n->body->flags |= NODE_ENDED;
 		break;
 	}
 }
 
 static void
 mdoc_root_post(const struct roff_meta *meta, struct html *h)
 {
 	struct tag	*t;
+	char		*title;
+
+	assert(meta->title != NULL);
+	if (meta->msec == NULL)
+		title = mandoc_strdup(meta->title);
+	else
+		mandoc_asprintf(&title, "%s(%s)", meta->title, meta->msec);
 
 	t = print_otag(h, TAG_DIV, "cr?", "foot", "doc-pagefooter",
 	    "aria-label", "Manual footer line");
 
 	print_otag(h, TAG_SPAN, "c", "foot-left");
+	print_text(h, meta->os);
 	print_stagq(h, t);
 
 	print_otag(h, TAG_SPAN, "c", "foot-date");
 	print_text(h, meta->date);
 	print_stagq(h, t);
 
-	print_otag(h, TAG_SPAN, "c", "foot-os");
-	print_text(h, meta->os);
+	print_otag(h, TAG_SPAN, "c", "foot-right");
+	print_text(h, title);
 	print_tagq(h, t);
+	free(title);
 }
 
 static int
 mdoc_root_pre(const struct roff_meta *meta, struct html *h)
 {
 	struct tag	*t;
 	char		*volume, *title;
 
 	if (NULL == meta->arch)
 		volume = mandoc_strdup(meta->vol);
 	else
 		mandoc_asprintf(&volume, "%s (%s)",
 		    meta->vol, meta->arch);
 
 	if (NULL == meta->msec)
 		title = mandoc_strdup(meta->title);
 	else
 		mandoc_asprintf(&title, "%s(%s)",
 		    meta->title, meta->msec);
 
 	t = print_otag(h, TAG_DIV, "cr?", "head", "doc-pageheader",
 	    "aria-label", "Manual header line");
 
 	print_otag(h, TAG_SPAN, "c", "head-ltitle");
 	print_text(h, title);
 	print_stagq(h, t);
 
 	print_otag(h, TAG_SPAN, "c", "head-vol");
 	print_text(h, volume);
 	print_stagq(h, t);
 
 	print_otag(h, TAG_SPAN, "c", "head-rtitle");
 	print_text(h, title);
 	print_tagq(h, t);
 
 	free(title);
 	free(volume);
 	return 1;
 }
 
 static int
 mdoc_code_pre(MDOC_ARGS)
 {
 	print_otag_id(h, TAG_CODE, roff_name[n->tok], n);
 	return 1;
 }
 
 static int
 mdoc_sh_pre(MDOC_ARGS)
 {
 	struct roff_node	*sn, *subn;
 	struct tag		*t, *tnav, *tsec, *tsub;
 	char			*id;
 	int			 sc;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		if ((h->oflags & HTML_TOC) == 0 ||
 		    h->flags & HTML_TOCDONE ||
 		    n->sec <= SEC_SYNOPSIS) {
 			print_otag(h, TAG_SECTION, "c", "Sh");
 			break;
 		}
 		h->flags |= HTML_TOCDONE;
 		sc = 0;
 		for (sn = n->next; sn != NULL; sn = sn->next)
 			if (sn->sec == SEC_CUSTOM)
 				if (++sc == 2)
 					break;
 		if (sc < 2)
 			break;
 		tnav = print_otag(h, TAG_NAV, "r", "doc-toc");
 		t = print_otag(h, TAG_H2, "c", "Sh");
 		print_text(h, "TABLE OF CONTENTS");
 		print_tagq(h, t);
 		t = print_otag(h, TAG_UL, "c", "Bl-compact");
 		for (sn = n; sn != NULL; sn = sn->next) {
 			tsec = print_otag(h, TAG_LI, "");
 			id = html_make_id(sn->head, 0);
 			tsub = print_otag(h, TAG_A, "hR", id);
 			free(id);
 			print_mdoc_nodelist(meta, sn->head->child, h);
 			print_tagq(h, tsub);
 			tsub = NULL;
 			for (subn = sn->body->child; subn != NULL;
 			    subn = subn->next) {
 				if (subn->tok != MDOC_Ss)
 					continue;
 				id = html_make_id(subn->head, 0);
 				if (id == NULL)
 					continue;
 				if (tsub == NULL)
 					print_otag(h, TAG_UL,
 					    "c", "Bl-compact");
 				tsub = print_otag(h, TAG_LI, "");
 				print_otag(h, TAG_A, "hR", id);
 				free(id);
 				print_mdoc_nodelist(meta,
 				    subn->head->child, h);
 				print_tagq(h, tsub);
 			}
 			print_tagq(h, tsec);
 		}
 		print_tagq(h, tnav);
 		print_otag(h, TAG_SECTION, "c", "Sh");
 		break;
 	case ROFFT_HEAD:
 		print_otag_id(h, TAG_H2, "Sh", n);
 		break;
 	case ROFFT_BODY:
 		if (n->sec == SEC_AUTHORS)
 			h->flags &= ~(HTML_SPLIT|HTML_NOSPLIT);
 		break;
 	default:
 		break;
 	}
 	return 1;
 }
 
 static int
 mdoc_ss_pre(MDOC_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		print_otag(h, TAG_SECTION, "c", "Ss");
 		break;
 	case ROFFT_HEAD:
 		print_otag_id(h, TAG_H3, "Ss", n);
 		break;
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static int
 mdoc_fl_pre(MDOC_ARGS)
 {
 	struct roff_node	*nn;
 
 	print_otag_id(h, TAG_CODE, "Fl", n);
 	print_text(h, "\\-");
 	if (n->child != NULL ||
 	    ((nn = roff_node_next(n)) != NULL &&
 	     nn->type != ROFFT_TEXT &&
 	     (nn->flags & NODE_LINE) == 0))
 		h->flags |= HTML_NOSPACE;
 
 	return 1;
 }
 
 static int
 mdoc_nd_pre(MDOC_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		return 1;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 	print_text(h, "\\(em");
 	print_otag(h, TAG_SPAN, "cr", "Nd", "doc-subtitle");
 	return 1;
 }
 
 static int
 mdoc_nm_pre(MDOC_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		break;
 	case ROFFT_HEAD:
 		print_otag(h, TAG_TD, "");
 		/* FALLTHROUGH */
 	case ROFFT_ELEM:
 		print_otag(h, TAG_CODE, "c", "Nm");
 		return 1;
 	case ROFFT_BODY:
 		print_otag(h, TAG_TD, "");
 		return 1;
 	default:
 		abort();
 	}
 	html_close_paragraph(h);
 	synopsis_pre(h, n);
 	print_otag(h, TAG_TABLE, "c", "Nm");
 	print_otag(h, TAG_TR, "");
 	return 1;
 }
 
 static int
 mdoc_xr_pre(MDOC_ARGS)
 {
 	char	*name, *section, *label;
 
 	if (n->child == NULL)
 		return 0;
 
 	name = n->child->string;
 	if (n->child->next != NULL) {
 		section = n->child->next->string;
 		mandoc_asprintf(&label, "%s, section %s", name, section);
 	} else
 		section = label = NULL;
 
 	if (h->base_man1)
 		print_otag(h, TAG_A, "chM?", "Xr",
 		    name, section, "aria-label", label);
 	else
 		print_otag(h, TAG_A, "c?", "Xr", "aria-label", label);
 
 	free(label);
 	print_text(h, name);
 
 	if (section == NULL)
 		return 0;
 
 	h->flags |= HTML_NOSPACE;
 	print_text(h, "(");
 	h->flags |= HTML_NOSPACE;
 	print_text(h, section);
 	h->flags |= HTML_NOSPACE;
 	print_text(h, ")");
 	return 0;
 }
 
 static int
 mdoc_tg_pre(MDOC_ARGS)
 {
 	char	*id;
 
 	if ((id = html_make_id(n, 1)) != NULL) {
 		print_tagq(h, print_otag(h, TAG_MARK, "i", id));
 		free(id);
 	}
 	return 0;
 }
 
 static int
 mdoc_ns_pre(MDOC_ARGS)
 {
 
 	if ( ! (NODE_LINE & n->flags))
 		h->flags |= HTML_NOSPACE;
 	return 1;
 }
 
 static int
 mdoc_ar_pre(MDOC_ARGS)
 {
 	print_otag(h, TAG_VAR, "c", "Ar");
 	return 1;
 }
 
 static int
 mdoc_xx_pre(MDOC_ARGS)
 {
 	print_otag(h, TAG_SPAN, "c", "Ux");
 	return 1;
 }
 
 static int
 mdoc_it_pre(MDOC_ARGS)
 {
 	const struct roff_node	*bl;
 	enum mdoc_list		 type;
 
 	bl = n->parent;
 	while (bl->tok != MDOC_Bl)
 		bl = bl->parent;
 	type = bl->norm->Bl.type;
 
 	switch (type) {
 	case LIST_bullet:
 	case LIST_dash:
 	case LIST_hyphen:
 	case LIST_item:
 	case LIST_enum:
 		switch (n->type) {
 		case ROFFT_HEAD:
 			return 0;
 		case ROFFT_BODY:
 			print_otag_id(h, TAG_LI, NULL, n);
 			break;
 		default:
 			break;
 		}
 		break;
 	case LIST_diag:
 	case LIST_hang:
 	case LIST_inset:
 	case LIST_ohang:
 		switch (n->type) {
 		case ROFFT_HEAD:
 			print_otag_id(h, TAG_DT, NULL, n);
 			break;
 		case ROFFT_BODY:
 			print_otag(h, TAG_DD, "");
 			break;
 		default:
 			break;
 		}
 		break;
 	case LIST_tag:
 		switch (n->type) {
 		case ROFFT_HEAD:
 			print_otag_id(h, TAG_DT, NULL, n);
 			break;
 		case ROFFT_BODY:
 			if (n->child == NULL) {
 				print_otag(h, TAG_DD, "s", "width", "auto");
 				print_text(h, "\\ ");
 			} else
 				print_otag(h, TAG_DD, "");
 			break;
 		default:
 			break;
 		}
 		break;
 	case LIST_column:
 		switch (n->type) {
 		case ROFFT_HEAD:
 			break;
 		case ROFFT_BODY:
 			print_otag(h, TAG_TD, "");
 			break;
 		default:
 			print_otag_id(h, TAG_TR, NULL, n);
 		}
 	default:
 		break;
 	}
 
 	return 1;
 }
 
 static int
 mdoc_bl_pre(MDOC_ARGS)
 {
 	char		 cattr[32];
 	struct mdoc_bl	*bl;
 	enum htmltag	 elemtype;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		break;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		return 1;
 	default:
 		abort();
 	}
 
 	bl = &n->norm->Bl;
 	switch (bl->type) {
 	case LIST_bullet:
 		elemtype = TAG_UL;
 		(void)strlcpy(cattr, "Bl-bullet", sizeof(cattr));
 		break;
 	case LIST_dash:
 	case LIST_hyphen:
 		elemtype = TAG_UL;
 		(void)strlcpy(cattr, "Bl-dash", sizeof(cattr));
 		break;
 	case LIST_item:
 		elemtype = TAG_UL;
 		(void)strlcpy(cattr, "Bl-item", sizeof(cattr));
 		break;
 	case LIST_enum:
 		elemtype = TAG_OL;
 		(void)strlcpy(cattr, "Bl-enum", sizeof(cattr));
 		break;
 	case LIST_diag:
 		elemtype = TAG_DL;
 		(void)strlcpy(cattr, "Bl-diag", sizeof(cattr));
 		break;
 	case LIST_hang:
 		elemtype = TAG_DL;
 		(void)strlcpy(cattr, "Bl-hang", sizeof(cattr));
 		break;
 	case LIST_inset:
 		elemtype = TAG_DL;
 		(void)strlcpy(cattr, "Bl-inset", sizeof(cattr));
 		break;
 	case LIST_ohang:
 		elemtype = TAG_DL;
 		(void)strlcpy(cattr, "Bl-ohang", sizeof(cattr));
 		break;
 	case LIST_tag:
 		if (bl->offs)
 			print_otag(h, TAG_DIV, "c", "Bd-indent");
 		print_otag_id(h, TAG_DL,
 		    bl->comp ? "Bl-tag Bl-compact" : "Bl-tag", n->body);
 		return 1;
 	case LIST_column:
 		elemtype = TAG_TABLE;
 		(void)strlcpy(cattr, "Bl-column", sizeof(cattr));
 		break;
 	default:
 		abort();
 	}
 	if (bl->offs != NULL)
 		(void)strlcat(cattr, " Bd-indent", sizeof(cattr));
 	if (bl->comp)
 		(void)strlcat(cattr, " Bl-compact", sizeof(cattr));
 	print_otag_id(h, elemtype, cattr, n->body);
 	return 1;
 }
 
 static int
 mdoc_ex_pre(MDOC_ARGS)
 {
 	if (roff_node_prev(n) != NULL)
 		print_otag(h, TAG_BR, "");
 	return 1;
 }
 
 static int
 mdoc_st_pre(MDOC_ARGS)
 {
 	print_otag(h, TAG_SPAN, "c", "St");
 	return 1;
 }
 
 static int
 mdoc_em_pre(MDOC_ARGS)
 {
 	print_otag_id(h, TAG_I, "Em", n);
 	return 1;
 }
 
 static int
 mdoc_d1_pre(MDOC_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		return 1;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 	print_otag_id(h, TAG_DIV, "Bd Bd-indent", n);
 	if (n->tok == MDOC_Dl)
 		print_otag(h, TAG_CODE, "c", "Li");
 	return 1;
 }
 
 static int
 mdoc_sx_pre(MDOC_ARGS)
 {
 	char	*id;
 
 	id = html_make_id(n, 0);
 	print_otag(h, TAG_A, "chR", "Sx", id);
 	free(id);
 	return 1;
 }
 
 static int
 mdoc_bd_pre(MDOC_ARGS)
 {
 	char			 buf[20];
 	struct roff_node	*nn;
 	int			 comp;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		return 1;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 
 	/* Handle preceding whitespace. */
 
 	comp = n->norm->Bd.comp;
 	for (nn = n; nn != NULL && comp == 0; nn = nn->parent) {
 		if (nn->type != ROFFT_BLOCK)
 			continue;
 		if (nn->tok == MDOC_Sh || nn->tok == MDOC_Ss)
 			comp = 1;
 		if (roff_node_prev(nn) != NULL)
 			break;
 	}
 	(void)strlcpy(buf, "Bd", sizeof(buf));
 	if (comp == 0)
 		(void)strlcat(buf, " Pp", sizeof(buf));
 
 	/* Handle the -offset argument. */
 
 	if (n->norm->Bd.offs != NULL &&
 	    strcmp(n->norm->Bd.offs, "left") != 0)
 		(void)strlcat(buf, " Bd-indent", sizeof(buf));
 
 	if (n->norm->Bd.type == DISP_literal)
 		(void)strlcat(buf, " Li", sizeof(buf));
 
 	print_otag_id(h, TAG_DIV, buf, n);
 	return 1;
 }
 
 static int
 mdoc_pa_pre(MDOC_ARGS)
 {
 	print_otag(h, TAG_SPAN, "c", "Pa");
 	return 1;
 }
 
 static int
 mdoc_ad_pre(MDOC_ARGS)
 {
 	print_otag(h, TAG_SPAN, "c", "Ad");
 	return 1;
 }
 
 static int
 mdoc_an_pre(MDOC_ARGS)
 {
 	if (n->norm->An.auth == AUTH_split) {
 		h->flags &= ~HTML_NOSPLIT;
 		h->flags |= HTML_SPLIT;
 		return 0;
 	}
 	if (n->norm->An.auth == AUTH_nosplit) {
 		h->flags &= ~HTML_SPLIT;
 		h->flags |= HTML_NOSPLIT;
 		return 0;
 	}
 
 	if (h->flags & HTML_SPLIT)
 		print_otag(h, TAG_BR, "");
 
 	if (n->sec == SEC_AUTHORS && ! (h->flags & HTML_NOSPLIT))
 		h->flags |= HTML_SPLIT;
 
 	print_otag(h, TAG_SPAN, "c", "An");
 	return 1;
 }
 
 static int
 mdoc_cd_pre(MDOC_ARGS)
 {
 	synopsis_pre(h, n);
 	print_otag(h, TAG_CODE, "c", "Cd");
 	return 1;
 }
 
 static int
 mdoc_fa_pre(MDOC_ARGS)
 {
 	const struct roff_node	*nn;
 	struct tag		*t;
 
 	if (n->parent->tok != MDOC_Fo) {
 		print_otag(h, TAG_VAR, "c", "Fa");
 		return 1;
 	}
 	for (nn = n->child; nn != NULL; nn = nn->next) {
 		t = print_otag(h, TAG_VAR, "c", "Fa");
 		print_text(h, nn->string);
 		print_tagq(h, t);
 		if (nn->next != NULL) {
 			h->flags |= HTML_NOSPACE;
 			print_text(h, ",");
 		}
 	}
 	if (n->child != NULL &&
 	    (nn = roff_node_next(n)) != NULL &&
 	    nn->tok == MDOC_Fa) {
 		h->flags |= HTML_NOSPACE;
 		print_text(h, ",");
 	}
 	return 0;
 }
 
 static int
 mdoc_fd_pre(MDOC_ARGS)
 {
 	struct tag	*t;
 	char		*buf, *cp;
 
 	synopsis_pre(h, n);
 
 	if (NULL == (n = n->child))
 		return 0;
 
 	assert(n->type == ROFFT_TEXT);
 
 	if (strcmp(n->string, "#include")) {
 		print_otag(h, TAG_CODE, "c", "Fd");
 		return 1;
 	}
 
 	print_otag(h, TAG_CODE, "c", "In");
 	print_text(h, n->string);
 
 	if (NULL != (n = n->next)) {
 		assert(n->type == ROFFT_TEXT);
 
 		if (h->base_includes) {
 			cp = n->string;
 			if (*cp == '<' || *cp == '"')
 				cp++;
 			buf = mandoc_strdup(cp);
 			cp = strchr(buf, '\0') - 1;
 			if (cp >= buf && (*cp == '>' || *cp == '"'))
 				*cp = '\0';
 			t = print_otag(h, TAG_A, "chI", "In", buf);
 			free(buf);
 		} else
 			t = print_otag(h, TAG_A, "c", "In");
 
 		print_text(h, n->string);
 		print_tagq(h, t);
 
 		n = n->next;
 	}
 
 	for ( ; n; n = n->next) {
 		assert(n->type == ROFFT_TEXT);
 		print_text(h, n->string);
 	}
 
 	return 0;
 }
 
 static int
 mdoc_vt_pre(MDOC_ARGS)
 {
 	if (n->type == ROFFT_BLOCK) {
 		synopsis_pre(h, n);
 		return 1;
 	} else if (n->type == ROFFT_ELEM) {
 		synopsis_pre(h, n);
 	} else if (n->type == ROFFT_HEAD)
 		return 0;
 
 	print_otag(h, TAG_VAR, "c", "Vt");
 	return 1;
 }
 
 static int
 mdoc_ft_pre(MDOC_ARGS)
 {
 	synopsis_pre(h, n);
 	print_otag(h, TAG_VAR, "c", "Ft");
 	return 1;
 }
 
 static int
 mdoc_fn_pre(MDOC_ARGS)
 {
 	struct tag	*t;
 	char		 nbuf[BUFSIZ];
 	const char	*sp, *ep;
 	int		 sz, pretty;
 
 	pretty = NODE_SYNPRETTY & n->flags;
 	synopsis_pre(h, n);
 
 	/* Split apart into type and name. */
 	assert(n->child->string);
 	sp = n->child->string;
 
 	ep = strchr(sp, ' ');
 	if (NULL != ep) {
 		t = print_otag(h, TAG_VAR, "c", "Ft");
 
 		while (ep) {
 			sz = MIN((int)(ep - sp), BUFSIZ - 1);
 			(void)memcpy(nbuf, sp, (size_t)sz);
 			nbuf[sz] = '\0';
 			print_text(h, nbuf);
 			sp = ++ep;
 			ep = strchr(sp, ' ');
 		}
 		print_tagq(h, t);
 	}
 
 	t = print_otag_id(h, TAG_CODE, "Fn", n);
 
 	if (sp)
 		print_text(h, sp);
 
 	print_tagq(h, t);
 
 	h->flags |= HTML_NOSPACE;
 	print_text(h, "(");
 	h->flags |= HTML_NOSPACE;
 
 	for (n = n->child->next; n; n = n->next) {
 		if (NODE_SYNPRETTY & n->flags)
 			t = print_otag(h, TAG_VAR, "cs", "Fa",
 			    "white-space", "nowrap");
 		else
 			t = print_otag(h, TAG_VAR, "c", "Fa");
 		print_text(h, n->string);
 		print_tagq(h, t);
 		if (n->next) {
 			h->flags |= HTML_NOSPACE;
 			print_text(h, ",");
 		}
 	}
 
 	h->flags |= HTML_NOSPACE;
 	print_text(h, ")");
 
 	if (pretty) {
 		h->flags |= HTML_NOSPACE;
 		print_text(h, ";");
 	}
 
 	return 0;
 }
 
 static int
 mdoc_sm_pre(MDOC_ARGS)
 {
 
 	if (NULL == n->child)
 		h->flags ^= HTML_NONOSPACE;
 	else if (0 == strcmp("on", n->child->string))
 		h->flags &= ~HTML_NONOSPACE;
 	else
 		h->flags |= HTML_NONOSPACE;
 
 	if ( ! (HTML_NONOSPACE & h->flags))
 		h->flags &= ~HTML_NOSPACE;
 
 	return 0;
 }
 
 static int
 mdoc_skip_pre(MDOC_ARGS)
 {
 
 	return 0;
 }
 
 static int
 mdoc_pp_pre(MDOC_ARGS)
 {
 	char	*id;
 
 	if (n->flags & NODE_NOFILL) {
 		print_endline(h);
 		if (n->flags & NODE_ID)
 			mdoc_tg_pre(meta, n, h);
 		else {
 			h->col = 1;
 			print_endline(h);
 		}
 	} else {
 		html_close_paragraph(h);
 		id = n->flags & NODE_ID ? html_make_id(n, 1) : NULL;
 		print_otag(h, TAG_P, "ci", "Pp", id);
 		free(id);
 	}
 	return 0;
 }
 
 static int
 mdoc_lk_pre(MDOC_ARGS)
 {
 	const struct roff_node *link, *descr, *punct;
 	struct tag	*t;
 
 	if ((link = n->child) == NULL)
 		return 0;
 
 	/* Find beginning of trailing punctuation. */
 	punct = n->last;
 	while (punct != link && punct->flags & NODE_DELIMC)
 		punct = punct->prev;
 	punct = punct->next;
 
 	/* Link target and link text. */
 	descr = link->next;
 	if (descr == punct)
 		descr = link;  /* no text */
 	t = print_otag(h, TAG_A, "ch", "Lk", link->string);
 	do {
 		if (descr->flags & (NODE_DELIMC | NODE_DELIMO))
 			h->flags |= HTML_NOSPACE;
 		print_text(h, descr->string);
 		descr = descr->next;
 	} while (descr != punct);
 	print_tagq(h, t);
 
 	/* Trailing punctuation. */
 	while (punct != NULL) {
 		h->flags |= HTML_NOSPACE;
 		print_text(h, punct->string);
 		punct = punct->next;
 	}
 	return 0;
 }
 
 static int
 mdoc_mt_pre(MDOC_ARGS)
 {
 	struct tag	*t;
 	char		*cp;
 
 	for (n = n->child; n; n = n->next) {
 		assert(n->type == ROFFT_TEXT);
 		mandoc_asprintf(&cp, "mailto:%s", n->string);
 		t = print_otag(h, TAG_A, "ch", "Mt", cp);
 		print_text(h, n->string);
 		print_tagq(h, t);
 		free(cp);
 	}
 	return 0;
 }
 
 static int
 mdoc_fo_pre(MDOC_ARGS)
 {
 	struct tag	*t;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		synopsis_pre(h, n);
 		return 1;
 	case ROFFT_HEAD:
 		if (n->child != NULL) {
 			t = print_otag_id(h, TAG_CODE, "Fn", n);
 			print_text(h, n->child->string);
 			print_tagq(h, t);
 		}
 		return 0;
 	case ROFFT_BODY:
 		h->flags |= HTML_NOSPACE;
 		print_text(h, "(");
 		h->flags |= HTML_NOSPACE;
 		return 1;
 	default:
 		abort();
 	}
 }
 
 static void
 mdoc_fo_post(MDOC_ARGS)
 {
 	if (n->type != ROFFT_BODY)
 		return;
 	h->flags |= HTML_NOSPACE;
 	print_text(h, ")");
 	h->flags |= HTML_NOSPACE;
 	print_text(h, ";");
 }
 
 static int
 mdoc_in_pre(MDOC_ARGS)
 {
 	struct tag	*t;
 
 	synopsis_pre(h, n);
 	print_otag(h, TAG_CODE, "c", "In");
 
 	/*
 	 * The first argument of the `In' gets special treatment as
 	 * being a linked value.  Subsequent values are printed
 	 * afterward.  groff does similarly.  This also handles the case
 	 * of no children.
 	 */
 
 	if (NODE_SYNPRETTY & n->flags && NODE_LINE & n->flags)
 		print_text(h, "#include");
 
 	print_text(h, "<");
 	h->flags |= HTML_NOSPACE;
 
 	if (NULL != (n = n->child)) {
 		assert(n->type == ROFFT_TEXT);
 
 		if (h->base_includes)
 			t = print_otag(h, TAG_A, "chI", "In", n->string);
 		else
 			t = print_otag(h, TAG_A, "c", "In");
 		print_text(h, n->string);
 		print_tagq(h, t);
 
 		n = n->next;
 	}
 
 	h->flags |= HTML_NOSPACE;
 	print_text(h, ">");
 
 	for ( ; n; n = n->next) {
 		assert(n->type == ROFFT_TEXT);
 		print_text(h, n->string);
 	}
 	return 0;
 }
 
 static int
 mdoc_va_pre(MDOC_ARGS)
 {
 	print_otag(h, TAG_VAR, "c", "Va");
 	return 1;
 }
 
 static int
 mdoc_ap_pre(MDOC_ARGS)
 {
 	h->flags |= HTML_NOSPACE;
 	print_text(h, "\\(aq");
 	h->flags |= HTML_NOSPACE;
 	return 1;
 }
 
 static int
 mdoc_bf_pre(MDOC_ARGS)
 {
 	const char	*cattr;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		html_close_paragraph(h);
 		return 1;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		break;
 	default:
 		abort();
 	}
 
 	if (FONT_Em == n->norm->Bf.font)
 		cattr = "Bf Em";
 	else if (FONT_Sy == n->norm->Bf.font)
 		cattr = "Bf Sy";
 	else if (FONT_Li == n->norm->Bf.font)
 		cattr = "Bf Li";
 	else
 		cattr = "Bf No";
 
 	/* Cannot use TAG_SPAN because it may contain blocks. */
 	print_otag(h, TAG_DIV, "c", cattr);
 	return 1;
 }
 
 static int
 mdoc_igndelim_pre(MDOC_ARGS)
 {
 	h->flags |= HTML_IGNDELIM;
 	return 1;
 }
 
 static void
 mdoc_pf_post(MDOC_ARGS)
 {
 	if ( ! (n->next == NULL || n->next->flags & NODE_LINE))
 		h->flags |= HTML_NOSPACE;
 }
 
 static int
 mdoc_rs_pre(MDOC_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		if (n->sec == SEC_SEE_ALSO)
 			html_close_paragraph(h);
 		break;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		if (n->sec == SEC_SEE_ALSO)
 			print_otag(h, TAG_P, "c", "Pp");
 		print_otag(h, TAG_SPAN, "c", "Rs");
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static int
 mdoc_no_pre(MDOC_ARGS)
 {
 	print_otag_id(h, TAG_SPAN, roff_name[n->tok], n);
 	return 1;
 }
 
 static int
 mdoc_sy_pre(MDOC_ARGS)
 {
 	print_otag_id(h, TAG_B, "Sy", n);
 	return 1;
 }
 
 static int
 mdoc_lb_pre(MDOC_ARGS)
 {
 	if (n->sec == SEC_LIBRARY &&
 	    n->flags & NODE_LINE &&
 	    roff_node_prev(n) != NULL)
 		print_otag(h, TAG_BR, "");
 
 	print_otag(h, TAG_SPAN, "c", "Lb");
 	return 1;
 }
 
 static int
 mdoc__x_pre(MDOC_ARGS)
 {
 	struct roff_node	*nn;
 	const unsigned char	*cp;
 	const char		*cattr, *arg;
 	char			*url;
 	enum htmltag		 t;
 
 	t = TAG_SPAN;
 	arg = n->child->string;
 
 	switch (n->tok) {
 	case MDOC__A:
 		cattr = "RsA";
 		if ((nn = roff_node_prev(n)) != NULL && nn->tok == MDOC__A &&
 		    ((nn = roff_node_next(n)) == NULL || nn->tok != MDOC__A))
 			print_text(h, "and");
 		break;
 	case MDOC__B:
 		t = TAG_CITE;
 		cattr = "RsB";
 		break;
 	case MDOC__C:
 		cattr = "RsC";
 		break;
 	case MDOC__D:
 		cattr = "RsD";
 		break;
 	case MDOC__I:
 		t = TAG_I;
 		cattr = "RsI";
 		break;
 	case MDOC__J:
 		t = TAG_I;
 		cattr = "RsJ";
 		break;
 	case MDOC__N:
 		cattr = "RsN";
 		break;
 	case MDOC__O:
 		cattr = "RsO";
 		break;
 	case MDOC__P:
 		cattr = "RsP";
 		break;
 	case MDOC__Q:
 		cattr = "RsQ";
 		break;
 	case MDOC__R:
 		if (strncmp(arg, "RFC ", 4) == 0) {
 			cp = arg += 4;
 			while (isdigit(*cp))
 				cp++;
 			if (*cp == '\0') {
 				mandoc_asprintf(&url, "https://www.rfc-"
 				    "editor.org/rfc/rfc%s.html", arg);
 				print_otag(h, TAG_A, "ch", "RsR", url);
 				free(url);
 				return 1;
 			}
 		}
 		cattr = "RsR";
 		break;
 	case MDOC__T:
 		t = TAG_CITE;
 		if (n->parent != NULL && n->parent->tok == MDOC_Rs &&
 		    n->parent->norm->Rs.quote_T) {
 			print_text(h, "\\(lq");
 			h->flags |= HTML_NOSPACE;
 			cattr = "RsT";
 		} else
 			cattr = "RsB";
 		break;
 	case MDOC__U:
 		print_otag(h, TAG_A, "ch", "RsU", arg);
 		return 1;
 	case MDOC__V:
 		cattr = "RsV";
 		break;
 	default:
 		abort();
 	}
 
 	print_otag(h, t, "c", cattr);
 	return 1;
 }
 
 static void
 mdoc__x_post(MDOC_ARGS)
 {
 	struct roff_node *nn;
 
 	switch (n->tok) {
 	case MDOC__A:
 		if ((nn = roff_node_next(n)) != NULL && nn->tok == MDOC__A &&
 		    ((nn = roff_node_next(nn)) == NULL || nn->tok != MDOC__A) &&
 		    ((nn = roff_node_prev(n)) == NULL || nn->tok != MDOC__A))
 			return;
 		break;
 	case MDOC__T:
 		if (n->parent != NULL && n->parent->tok == MDOC_Rs &&
 		    n->parent->norm->Rs.quote_T) {
 			h->flags |= HTML_NOSPACE;
 			print_text(h, "\\(rq");
 		}
 		break;
 	default:
 		break;
 	}
 	if (n->parent == NULL || n->parent->tok != MDOC_Rs)
 		return;
 
 	h->flags |= HTML_NOSPACE;
 	print_text(h, roff_node_next(n) ? "," : ".");
 }
 
 static int
 mdoc_bk_pre(MDOC_ARGS)
 {
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		break;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		if (n->parent->args != NULL || n->prev->child == NULL)
 			h->flags |= HTML_PREKEEP;
 		break;
 	default:
 		abort();
 	}
 
 	return 1;
 }
 
 static void
 mdoc_bk_post(MDOC_ARGS)
 {
 
 	if (n->type == ROFFT_BODY)
 		h->flags &= ~(HTML_KEEP | HTML_PREKEEP);
 }
 
 static int
 mdoc_quote_pre(MDOC_ARGS)
 {
 	if (n->type != ROFFT_BODY)
 		return 1;
 
 	switch (n->tok) {
 	case MDOC_Ao:
 	case MDOC_Aq:
 		print_text(h, n->child != NULL && n->child->next == NULL &&
 		    n->child->tok == MDOC_Mt ?  "<" : "\\(la");
 		break;
 	case MDOC_Bro:
 	case MDOC_Brq:
 		print_text(h, "\\(lC");
 		break;
 	case MDOC_Bo:
 	case MDOC_Bq:
 		print_text(h, "\\(lB");
 		break;
 	case MDOC_Oo:
 	case MDOC_Op:
 		print_text(h, "\\(lB");
 		/*
 		 * Give up on semantic markup for now.
 		 * We cannot use TAG_SPAN because .Oo may contain blocks.
 		 * We cannot use TAG_DIV because we might be in a
 		 * phrasing context (like .Dl or .Pp); we cannot
 		 * close out a .Pp at this point either because
 		 * that would break the line.
 		 */
 		/* XXX print_otag(h, TAG_???, "c", "Op"); */
 		break;
 	case MDOC_En:
 		if (NULL == n->norm->Es ||
 		    NULL == n->norm->Es->child)
 			return 1;
 		print_text(h, n->norm->Es->child->string);
 		break;
 	case MDOC_Do:
 	case MDOC_Dq:
 		print_text(h, "\\(lq");
 		break;
 	case MDOC_Qo:
 	case MDOC_Qq:
 		print_text(h, "\"");
 		break;
 	case MDOC_Po:
 	case MDOC_Pq:
 		print_text(h, "(");
 		break;
 	case MDOC_Ql:
 		print_text(h, "\\(oq");
 		h->flags |= HTML_NOSPACE;
 		print_otag(h, TAG_CODE, "c", "Li");
 		break;
 	case MDOC_So:
 	case MDOC_Sq:
 		print_text(h, "\\(oq");
 		break;
 	default:
 		abort();
 	}
 
 	h->flags |= HTML_NOSPACE;
 	return 1;
 }
 
 static void
 mdoc_quote_post(MDOC_ARGS)
 {
 
 	if (n->type != ROFFT_BODY && n->type != ROFFT_ELEM)
 		return;
 
 	h->flags |= HTML_NOSPACE;
 
 	switch (n->tok) {
 	case MDOC_Ao:
 	case MDOC_Aq:
 		print_text(h, n->child != NULL && n->child->next == NULL &&
 		    n->child->tok == MDOC_Mt ?  ">" : "\\(ra");
 		break;
 	case MDOC_Bro:
 	case MDOC_Brq:
 		print_text(h, "\\(rC");
 		break;
 	case MDOC_Oo:
 	case MDOC_Op:
 	case MDOC_Bo:
 	case MDOC_Bq:
 		print_text(h, "\\(rB");
 		break;
 	case MDOC_En:
 		if (n->norm->Es == NULL ||
 		    n->norm->Es->child == NULL ||
 		    n->norm->Es->child->next == NULL)
 			h->flags &= ~HTML_NOSPACE;
 		else
 			print_text(h, n->norm->Es->child->next->string);
 		break;
 	case MDOC_Do:
 	case MDOC_Dq:
 		print_text(h, "\\(rq");
 		break;
 	case MDOC_Qo:
 	case MDOC_Qq:
 		print_text(h, "\"");
 		break;
 	case MDOC_Po:
 	case MDOC_Pq:
 		print_text(h, ")");
 		break;
 	case MDOC_Ql:
 	case MDOC_So:
 	case MDOC_Sq:
 		print_text(h, "\\(cq");
 		break;
 	default:
 		abort();
 	}
 }
 
 static int
 mdoc_eo_pre(MDOC_ARGS)
 {
 
 	if (n->type != ROFFT_BODY)
 		return 1;
 
 	if (n->end == ENDBODY_NOT &&
 	    n->parent->head->child == NULL &&
 	    n->child != NULL &&
 	    n->child->end != ENDBODY_NOT)
 		print_text(h, "\\&");
 	else if (n->end != ENDBODY_NOT ? n->child != NULL :
 	    n->parent->head->child != NULL && (n->child != NULL ||
 	    (n->parent->tail != NULL && n->parent->tail->child != NULL)))
 		h->flags |= HTML_NOSPACE;
 	return 1;
 }
 
 static void
 mdoc_eo_post(MDOC_ARGS)
 {
 	int	 body, tail;
 
 	if (n->type != ROFFT_BODY)
 		return;
 
 	if (n->end != ENDBODY_NOT) {
 		h->flags &= ~HTML_NOSPACE;
 		return;
 	}
 
 	body = n->child != NULL || n->parent->head->child != NULL;
 	tail = n->parent->tail != NULL && n->parent->tail->child != NULL;
 
 	if (body && tail)
 		h->flags |= HTML_NOSPACE;
 	else if ( ! tail)
 		h->flags &= ~HTML_NOSPACE;
 }
 
 static int
 mdoc_abort_pre(MDOC_ARGS)
 {
 	abort();
 }
diff --git a/contrib/mandoc/mdoc_man.c b/contrib/mandoc/mdoc_man.c
index 5438b2ba5941..99693b5d81dd 100644
--- a/contrib/mandoc/mdoc_man.c
+++ b/contrib/mandoc/mdoc_man.c
@@ -1,1853 +1,1852 @@
-/* $Id: mdoc_man.c,v 1.139 2025/01/24 22:37:24 schwarze Exp $ */
+/* $Id: mdoc_man.c,v 1.141 2025/07/02 19:57:48 schwarze Exp $ */
 /*
  * Copyright (c) 2011-2021, 2025 Ingo Schwarze 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "roff.h"
 #include "mdoc.h"
 #include "man.h"
 #include "out.h"
 #include "main.h"
 
 #define	DECL_ARGS const struct roff_meta *meta, struct roff_node *n
 
 typedef	int	(*int_fp)(DECL_ARGS);
 typedef	void	(*void_fp)(DECL_ARGS);
 
 struct	mdoc_man_act {
 	int_fp		  cond; /* DON'T run actions */
 	int_fp		  pre; /* pre-node action */
 	void_fp		  post; /* post-node action */
 	const char	 *prefix; /* pre-node string constant */
 	const char	 *suffix; /* post-node string constant */
 };
 
 static	int	  cond_body(DECL_ARGS);
 static	int	  cond_head(DECL_ARGS);
 static  void	  font_push(char);
 static	void	  font_pop(void);
 static	int	  man_strlen(const char *);
 static	void	  mid_it(void);
 static	void	  post__t(DECL_ARGS);
 static	void	  post_aq(DECL_ARGS);
 static	void	  post_bd(DECL_ARGS);
 static	void	  post_bf(DECL_ARGS);
 static	void	  post_bk(DECL_ARGS);
 static	void	  post_bl(DECL_ARGS);
 static	void	  post_dl(DECL_ARGS);
 static	void	  post_en(DECL_ARGS);
 static	void	  post_enc(DECL_ARGS);
 static	void	  post_eo(DECL_ARGS);
 static	void	  post_fa(DECL_ARGS);
 static	void	  post_fd(DECL_ARGS);
 static	void	  post_fl(DECL_ARGS);
 static	void	  post_fn(DECL_ARGS);
 static	void	  post_fo(DECL_ARGS);
 static	void	  post_font(DECL_ARGS);
 static	void	  post_in(DECL_ARGS);
 static	void	  post_it(DECL_ARGS);
 static	void	  post_lb(DECL_ARGS);
 static	void	  post_nm(DECL_ARGS);
 static	void	  post_percent(DECL_ARGS);
 static	void	  post_pf(DECL_ARGS);
 static	void	  post_sect(DECL_ARGS);
 static	void	  post_vt(DECL_ARGS);
 static	int	  pre__t(DECL_ARGS);
 static	int	  pre_abort(DECL_ARGS);
 static	int	  pre_an(DECL_ARGS);
 static	int	  pre_ap(DECL_ARGS);
 static	int	  pre_aq(DECL_ARGS);
 static	int	  pre_bd(DECL_ARGS);
 static	int	  pre_bf(DECL_ARGS);
 static	int	  pre_bk(DECL_ARGS);
 static	int	  pre_bl(DECL_ARGS);
 static	void	  pre_br(DECL_ARGS);
 static	int	  pre_dl(DECL_ARGS);
 static	int	  pre_en(DECL_ARGS);
 static	int	  pre_enc(DECL_ARGS);
 static	int	  pre_em(DECL_ARGS);
 static	int	  pre_skip(DECL_ARGS);
 static	int	  pre_eo(DECL_ARGS);
 static	int	  pre_ex(DECL_ARGS);
 static	int	  pre_fa(DECL_ARGS);
 static	int	  pre_fd(DECL_ARGS);
 static	int	  pre_fl(DECL_ARGS);
 static	int	  pre_fn(DECL_ARGS);
 static	int	  pre_fo(DECL_ARGS);
 static	void	  pre_ft(DECL_ARGS);
 static	int	  pre_Ft(DECL_ARGS);
 static	int	  pre_in(DECL_ARGS);
 static	int	  pre_it(DECL_ARGS);
 static	int	  pre_lk(DECL_ARGS);
 static	int	  pre_li(DECL_ARGS);
 static	int	  pre_nm(DECL_ARGS);
 static	int	  pre_no(DECL_ARGS);
 static	void	  pre_noarg(DECL_ARGS);
 static	int	  pre_ns(DECL_ARGS);
 static	void	  pre_onearg(DECL_ARGS);
 static	int	  pre_pp(DECL_ARGS);
 static	int	  pre_rs(DECL_ARGS);
 static	int	  pre_sm(DECL_ARGS);
 static	void	  pre_sp(DECL_ARGS);
 static	int	  pre_sect(DECL_ARGS);
 static	int	  pre_sy(DECL_ARGS);
 static	void	  pre_syn(struct roff_node *);
 static	void	  pre_ta(DECL_ARGS);
 static	int	  pre_vt(DECL_ARGS);
 static	int	  pre_xr(DECL_ARGS);
 static	void	  print_word(const char *);
 static	void	  print_line(const char *, int);
 static	void	  print_block(const char *, int);
 static	void	  print_offs(const char *, int);
 static	void	  print_width(const struct mdoc_bl *,
 			const struct roff_node *);
 static	void	  print_count(int *);
 static	void	  print_node(DECL_ARGS);
 
 static const void_fp roff_man_acts[ROFF_MAX] = {
 	pre_br,		/* br */
 	pre_onearg,	/* ce */
 	pre_noarg,	/* fi */
 	pre_ft,		/* ft */
 	pre_onearg,	/* ll */
 	pre_onearg,	/* mc */
 	pre_noarg,	/* nf */
 	pre_onearg,	/* po */
 	pre_onearg,	/* rj */
 	pre_sp,		/* sp */
 	pre_ta,		/* ta */
 	pre_onearg,	/* ti */
 };
 
 static const struct mdoc_man_act mdoc_man_acts[MDOC_MAX - MDOC_Dd] = {
 	{ NULL, NULL, NULL, NULL, NULL }, /* Dd */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Dt */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Os */
 	{ NULL, pre_sect, post_sect, ".SH", NULL }, /* Sh */
 	{ NULL, pre_sect, post_sect, ".SS", NULL }, /* Ss */
 	{ NULL, pre_pp, NULL, NULL, NULL }, /* Pp */
 	{ cond_body, pre_dl, post_dl, NULL, NULL }, /* D1 */
 	{ cond_body, pre_dl, post_dl, NULL, NULL }, /* Dl */
 	{ cond_body, pre_bd, post_bd, NULL, NULL }, /* Bd */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ed */
 	{ cond_body, pre_bl, post_bl, NULL, NULL }, /* Bl */
 	{ NULL, NULL, NULL, NULL, NULL }, /* El */
 	{ NULL, pre_it, post_it, NULL, NULL }, /* It */
 	{ NULL, pre_em, post_font, NULL, NULL }, /* Ad */
 	{ NULL, pre_an, NULL, NULL, NULL }, /* An */
 	{ NULL, pre_ap, NULL, NULL, NULL }, /* Ap */
 	{ NULL, pre_em, post_font, NULL, NULL }, /* Ar */
 	{ NULL, pre_sy, post_font, NULL, NULL }, /* Cd */
 	{ NULL, pre_sy, post_font, NULL, NULL }, /* Cm */
 	{ NULL, pre_li, post_font, NULL, NULL }, /* Dv */
 	{ NULL, pre_li, post_font, NULL, NULL }, /* Er */
 	{ NULL, pre_li, post_font, NULL, NULL }, /* Ev */
 	{ NULL, pre_ex, NULL, NULL, NULL }, /* Ex */
 	{ NULL, pre_fa, post_fa, NULL, NULL }, /* Fa */
 	{ NULL, pre_fd, post_fd, NULL, NULL }, /* Fd */
 	{ NULL, pre_fl, post_fl, NULL, NULL }, /* Fl */
 	{ NULL, pre_fn, post_fn, NULL, NULL }, /* Fn */
 	{ NULL, pre_Ft, post_font, NULL, NULL }, /* Ft */
 	{ NULL, pre_sy, post_font, NULL, NULL }, /* Ic */
 	{ NULL, pre_in, post_in, NULL, NULL }, /* In */
 	{ NULL, pre_li, post_font, NULL, NULL }, /* Li */
 	{ cond_head, pre_enc, NULL, "\\- ", NULL }, /* Nd */
 	{ NULL, pre_nm, post_nm, NULL, NULL }, /* Nm */
 	{ cond_body, pre_enc, post_enc, "[", "]" }, /* Op */
 	{ NULL, pre_abort, NULL, NULL, NULL }, /* Ot */
 	{ NULL, pre_em, post_font, NULL, NULL }, /* Pa */
 	{ NULL, pre_ex, NULL, NULL, NULL }, /* Rv */
 	{ NULL, NULL, NULL, NULL, NULL }, /* St */
 	{ NULL, pre_em, post_font, NULL, NULL }, /* Va */
 	{ NULL, pre_vt, post_vt, NULL, NULL }, /* Vt */
 	{ NULL, pre_xr, NULL, NULL, NULL }, /* Xr */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %A */
 	{ NULL, pre_em, post_percent, NULL, NULL }, /* %B */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %D */
 	{ NULL, pre_em, post_percent, NULL, NULL }, /* %I */
 	{ NULL, pre_em, post_percent, NULL, NULL }, /* %J */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %N */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %O */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %P */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %R */
 	{ NULL, pre__t, post__t, NULL, NULL }, /* %T */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %V */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ac */
 	{ cond_body, pre_aq, post_aq, NULL, NULL }, /* Ao */
 	{ cond_body, pre_aq, post_aq, NULL, NULL }, /* Aq */
 	{ NULL, NULL, NULL, NULL, NULL }, /* At */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Bc */
 	{ NULL, pre_bf, post_bf, NULL, NULL }, /* Bf */
 	{ cond_body, pre_enc, post_enc, "[", "]" }, /* Bo */
 	{ cond_body, pre_enc, post_enc, "[", "]" }, /* Bq */
 	{ NULL, pre_bk, post_bk, NULL, NULL }, /* Bsx */
 	{ NULL, pre_bk, post_bk, NULL, NULL }, /* Bx */
 	{ NULL, pre_skip, NULL, NULL, NULL }, /* Db */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Dc */
 	{ cond_body, pre_enc, post_enc, "\\(lq", "\\(rq" }, /* Do */
 	{ cond_body, pre_enc, post_enc, "\\(lq", "\\(rq" }, /* Dq */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ec */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ef */
 	{ NULL, pre_em, post_font, NULL, NULL }, /* Em */
 	{ cond_body, pre_eo, post_eo, NULL, NULL }, /* Eo */
 	{ NULL, pre_bk, post_bk, NULL, NULL }, /* Fx */
 	{ NULL, pre_sy, post_font, NULL, NULL }, /* Ms */
 	{ NULL, pre_no, NULL, NULL, NULL }, /* No */
 	{ NULL, pre_ns, NULL, NULL, NULL }, /* Ns */
 	{ NULL, pre_bk, post_bk, NULL, NULL }, /* Nx */
 	{ NULL, pre_bk, post_bk, NULL, NULL }, /* Ox */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Pc */
 	{ NULL, NULL, post_pf, NULL, NULL }, /* Pf */
 	{ cond_body, pre_enc, post_enc, "(", ")" }, /* Po */
 	{ cond_body, pre_enc, post_enc, "(", ")" }, /* Pq */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Qc */
 	{ cond_body, pre_enc, post_enc, "\\(oq", "\\(cq" }, /* Ql */
 	{ cond_body, pre_enc, post_enc, "\"", "\"" }, /* Qo */
 	{ cond_body, pre_enc, post_enc, "\"", "\"" }, /* Qq */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Re */
 	{ cond_body, pre_rs, NULL, NULL, NULL }, /* Rs */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Sc */
 	{ cond_body, pre_enc, post_enc, "\\(oq", "\\(cq" }, /* So */
 	{ cond_body, pre_enc, post_enc, "\\(oq", "\\(cq" }, /* Sq */
 	{ NULL, pre_sm, NULL, NULL, NULL }, /* Sm */
 	{ NULL, pre_em, post_font, NULL, NULL }, /* Sx */
 	{ NULL, pre_sy, post_font, NULL, NULL }, /* Sy */
 	{ NULL, pre_li, post_font, NULL, NULL }, /* Tn */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ux */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Xc */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Xo */
 	{ NULL, pre_fo, post_fo, NULL, NULL }, /* Fo */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Fc */
 	{ cond_body, pre_enc, post_enc, "[", "]" }, /* Oo */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Oc */
 	{ NULL, pre_bk, post_bk, NULL, NULL }, /* Bk */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ek */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Bt */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Hf */
 	{ NULL, pre_em, post_font, NULL, NULL }, /* Fr */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ud */
 	{ NULL, NULL, post_lb, NULL, NULL }, /* Lb */
 	{ NULL, pre_abort, NULL, NULL, NULL }, /* Lp */
 	{ NULL, pre_lk, NULL, NULL, NULL }, /* Lk */
 	{ NULL, pre_em, post_font, NULL, NULL }, /* Mt */
 	{ cond_body, pre_enc, post_enc, "{", "}" }, /* Brq */
 	{ cond_body, pre_enc, post_enc, "{", "}" }, /* Bro */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Brc */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %C */
 	{ NULL, pre_skip, NULL, NULL, NULL }, /* Es */
 	{ cond_body, pre_en, post_en, NULL, NULL }, /* En */
 	{ NULL, pre_bk, post_bk, NULL, NULL }, /* Dx */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %Q */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %U */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ta */
 	{ NULL, pre_skip, NULL, NULL, NULL }, /* Tg */
 };
 static const struct mdoc_man_act *mdoc_man_act(enum roff_tok);
 
 static	int		outflags;
 #define	MMAN_spc	(1 << 0)  /* blank character before next word */
 #define	MMAN_spc_force	(1 << 1)  /* even before trailing punctuation */
 #define	MMAN_nl		(1 << 2)  /* break man(7) code line */
 #define	MMAN_br		(1 << 3)  /* break output line */
 #define	MMAN_sp		(1 << 4)  /* insert a blank output line */
 #define	MMAN_PP		(1 << 5)  /* reset indentation etc. */
 #define	MMAN_Sm		(1 << 6)  /* horizontal spacing mode */
 #define	MMAN_Bk		(1 << 7)  /* word keep mode */
 #define	MMAN_Bk_susp	(1 << 8)  /* suspend this (after a macro) */
 #define	MMAN_An_split	(1 << 9)  /* author mode is "split" */
 #define	MMAN_An_nosplit	(1 << 10) /* author mode is "nosplit" */
 #define	MMAN_PD		(1 << 11) /* inter-paragraph spacing disabled */
 #define	MMAN_nbrword	(1 << 12) /* do not break the next word */
 
 #define	BL_STACK_MAX	32
 
 static	int		Bl_stack[BL_STACK_MAX];  /* offsets [chars] */
 static	int		Bl_stack_post[BL_STACK_MAX];  /* add final .RE */
 static	int		Bl_stack_len;  /* number of nested Bl blocks */
 static	int		TPremain;  /* characters before tag is full */
 
 static	struct {
 	char	*head;
 	char	*tail;
 	size_t	 size;
 }	fontqueue;
 
 
 static const struct mdoc_man_act *
 mdoc_man_act(enum roff_tok tok)
 {
 	assert(tok >= MDOC_Dd && tok <= MDOC_MAX);
 	return mdoc_man_acts + (tok - MDOC_Dd);
 }
 
 static int
 man_strlen(const char *cp)
 {
 	size_t	 rsz;
 	int	 skip, sz;
 
 	sz = 0;
 	skip = 0;
 	for (;;) {
 		rsz = strcspn(cp, "\\");
 		if (rsz) {
 			cp += rsz;
 			if (skip) {
 				skip = 0;
 				rsz--;
 			}
 			sz += rsz;
 		}
 		if ('\0' == *cp)
 			break;
 		cp++;
 		switch (mandoc_escape(&cp, NULL, NULL)) {
 		case ESCAPE_ERROR:
 			return sz;
 		case ESCAPE_UNICODE:
 		case ESCAPE_NUMBERED:
 		case ESCAPE_SPECIAL:
 		case ESCAPE_UNDEF:
 		case ESCAPE_OVERSTRIKE:
 			if (skip)
 				skip = 0;
 			else
 				sz++;
 			break;
 		case ESCAPE_SKIPCHAR:
 			skip = 1;
 			break;
 		default:
 			break;
 		}
 	}
 	return sz;
 }
 
 static void
 font_push(char newfont)
 {
 
 	if (fontqueue.head + fontqueue.size <= ++fontqueue.tail) {
 		fontqueue.size += 8;
 		fontqueue.head = mandoc_realloc(fontqueue.head,
 		    fontqueue.size);
 	}
 	*fontqueue.tail = newfont;
 	print_word("");
 	printf("\\f");
 	putchar(newfont);
 	outflags &= ~MMAN_spc;
 }
 
 static void
 font_pop(void)
 {
 
 	if (fontqueue.tail > fontqueue.head)
 		fontqueue.tail--;
 	outflags &= ~MMAN_spc;
 	print_word("");
 	printf("\\f");
 	putchar(*fontqueue.tail);
 }
 
 static void
 print_word(const char *s)
 {
 
 	if ((MMAN_PP | MMAN_sp | MMAN_br | MMAN_nl) & outflags) {
 		/*
 		 * If we need a newline, print it now and start afresh.
 		 */
 		if (MMAN_PP & outflags) {
 			if (MMAN_sp & outflags) {
 				if (MMAN_PD & outflags) {
 					printf("\n.PD");
 					outflags &= ~MMAN_PD;
 				}
 			} else if ( ! (MMAN_PD & outflags)) {
 				printf("\n.PD 0");
 				outflags |= MMAN_PD;
 			}
 			printf("\n.PP\n");
 		} else if (MMAN_sp & outflags)
 			printf("\n.sp\n");
 		else if (MMAN_br & outflags)
 			printf("\n.br\n");
 		else if (MMAN_nl & outflags)
 			putchar('\n');
 		outflags &= ~(MMAN_PP|MMAN_sp|MMAN_br|MMAN_nl|MMAN_spc);
 		if (1 == TPremain)
 			printf(".br\n");
 		TPremain = 0;
 	} else if (MMAN_spc & outflags) {
 		/*
 		 * If we need a space, only print it if
 		 * (1) it is forced by `No' or
 		 * (2) what follows is not terminating punctuation or
 		 * (3) what follows is longer than one character.
 		 */
 		if (MMAN_spc_force & outflags || '\0' == s[0] ||
 		    NULL == strchr(".,:;)]?!", s[0]) || '\0' != s[1]) {
 			if (MMAN_Bk & outflags &&
 			    ! (MMAN_Bk_susp & outflags))
 				putchar('\\');
 			putchar(' ');
 			if (TPremain)
 				TPremain--;
 		}
 	}
 
 	/*
 	 * Reassign needing space if we're not following opening
 	 * punctuation.
 	 */
 	if (MMAN_Sm & outflags && ('\0' == s[0] ||
 	    (('(' != s[0] && '[' != s[0]) || '\0' != s[1])))
 		outflags |= MMAN_spc;
 	else
 		outflags &= ~MMAN_spc;
 	outflags &= ~(MMAN_spc_force | MMAN_Bk_susp);
 
 	for ( ; *s; s++) {
 		switch (*s) {
 		case ASCII_NBRSP:
 			printf("\\ ");
 			break;
 		case ASCII_HYPH:
 			putchar('-');
 			break;
 		case ASCII_BREAK:
 			printf("\\:");
 			break;
 		case ' ':
 			if (MMAN_nbrword & outflags) {
 				printf("\\ ");
 				break;
 			}
 			/* FALLTHROUGH */
 		default:
 			putchar((unsigned char)*s);
 			break;
 		}
 		if (TPremain)
 			TPremain--;
 	}
 	outflags &= ~MMAN_nbrword;
 }
 
 static void
 print_line(const char *s, int newflags)
 {
 
 	outflags |= MMAN_nl;
 	print_word(s);
 	outflags |= newflags;
 }
 
 static void
 print_block(const char *s, int newflags)
 {
 
 	outflags &= ~MMAN_PP;
 	if (MMAN_sp & outflags) {
 		outflags &= ~(MMAN_sp | MMAN_br);
 		if (MMAN_PD & outflags) {
 			print_line(".PD", 0);
 			outflags &= ~MMAN_PD;
 		}
 	} else if (! (MMAN_PD & outflags))
 		print_line(".PD 0", MMAN_PD);
 	outflags |= MMAN_nl;
 	print_word(s);
 	outflags |= MMAN_Bk_susp | newflags;
 }
 
 static void
 print_offs(const char *v, int keywords)
 {
 	char		  buf[24];
 	struct roffsu	  su;
 	const char	 *end;
 	int		  sz;
 
+	outflags &= ~MMAN_PP;
 	print_line(".RS", MMAN_Bk_susp);
 
 	/* Convert v into a number (of characters). */
 	if (NULL == v || '\0' == *v || (keywords && !strcmp(v, "left")))
 		sz = 0;
 	else if (keywords && !strcmp(v, "indent"))
 		sz = 6;
 	else if (keywords && !strcmp(v, "indent-two"))
 		sz = 12;
 	else {
 		end = a2roffsu(v, &su, SCALE_EN);
 		if (end == NULL || *end != '\0')
 			sz = man_strlen(v);
 		else if (SCALE_EN == su.unit)
 			sz = su.scale;
 		else {
 			/*
 			 * XXX
 			 * If we are inside an enclosing list,
 			 * there is no easy way to add the two
 			 * indentations because they are provided
 			 * in terms of different units.
 			 */
 			print_word(v);
 			outflags |= MMAN_nl;
 			return;
 		}
 	}
 
 	/*
 	 * We are inside an enclosing list.
 	 * Add the two indentations.
 	 */
 	if (Bl_stack_len)
 		sz += Bl_stack[Bl_stack_len - 1];
 
 	(void)snprintf(buf, sizeof(buf), "%dn", sz);
 	print_word(buf);
 	outflags |= MMAN_nl;
 }
 
 /*
  * Set up the indentation for a list item; used from pre_it().
  */
 static void
 print_width(const struct mdoc_bl *bl, const struct roff_node *child)
 {
 	char		  buf[24];
 	struct roffsu	  su;
 	const char	 *end;
 	int		  numeric, remain, sz, chsz;
 
 	numeric = 1;
 	remain = 0;
 
 	/* Convert the width into a number (of characters). */
 	if (bl->width == NULL)
 		sz = (bl->type == LIST_hang) ? 6 : 0;
 	else {
 		end = a2roffsu(bl->width, &su, SCALE_MAX);
 		if (end == NULL || *end != '\0')
 			sz = man_strlen(bl->width);
 		else if (SCALE_EN == su.unit)
 			sz = su.scale;
 		else {
 			sz = 0;
 			numeric = 0;
 		}
 	}
 
 	/* XXX Rough estimation, might have multiple parts. */
 	if (bl->type == LIST_enum)
 		chsz = (bl->count > 8) + 1;
 	else if (child != NULL && child->type == ROFFT_TEXT)
 		chsz = man_strlen(child->string);
 	else
 		chsz = 0;
 
 	/* Maybe we are inside an enclosing list? */
 	mid_it();
 
 	/*
 	 * Save our own indentation,
 	 * such that child lists can use it.
 	 */
 	Bl_stack[Bl_stack_len++] = sz + 2;
 
 	/* Set up the current list. */
 	if (chsz > sz && bl->type != LIST_tag)
 		print_block(".HP", MMAN_spc);
 	else {
 		print_block(".TP", MMAN_spc);
 		remain = sz + 2;
 	}
 	if (numeric) {
 		(void)snprintf(buf, sizeof(buf), "%dn", sz + 2);
 		print_word(buf);
 	} else
 		print_word(bl->width);
 	TPremain = remain;
 }
 
 static void
 print_count(int *count)
 {
 	char		  buf[24];
 
 	(void)snprintf(buf, sizeof(buf), "%d.\\&", ++*count);
 	print_word(buf);
 }
 
 void
 man_mdoc(void *arg, const struct roff_meta *mdoc)
 {
 	struct roff_node *n;
 
 	printf(".\\\" Automatically generated from an mdoc input file."
 	    "  Do not edit.\n");
 	for (n = mdoc->first->child; n != NULL; n = n->next) {
 		if (n->type != ROFFT_COMMENT)
 			break;
 		printf(".\\\"%s\n", n->string);
 	}
 
 	printf(".TH \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"\n",
 	    mdoc->title, (mdoc->msec == NULL ? "" : mdoc->msec),
 	    mdoc->date, mdoc->os, mdoc->vol);
 
 	/* Disable hyphenation and if nroff, disable justification. */
 	printf(".nh\n.if n .ad l");
 
 	outflags = MMAN_nl | MMAN_Sm;
 	if (0 == fontqueue.size) {
 		fontqueue.size = 8;
 		fontqueue.head = fontqueue.tail = mandoc_malloc(8);
 		*fontqueue.tail = 'R';
 	}
 	for (; n != NULL; n = n->next)
 		print_node(mdoc, n);
 	putchar('\n');
 }
 
 static void
 print_node(DECL_ARGS)
 {
 	const struct mdoc_man_act	*act;
 	struct roff_node		*sub;
 	int				 cond, do_sub;
 
 	if (n->flags & NODE_NOPRT)
 		return;
 
 	/*
 	 * Break the line if we were parsed subsequent the current node.
 	 * This makes the page structure be more consistent.
 	 */
 	if (outflags & MMAN_spc &&
 	    n->flags & NODE_LINE &&
 	    !roff_node_transparent(n))
 		outflags |= MMAN_nl;
 
 	act = NULL;
 	cond = 0;
 	do_sub = 1;
 	n->flags &= ~NODE_ENDED;
 
 	switch (n->type) {
 	case ROFFT_EQN:
 	case ROFFT_TBL:
 		mandoc_msg(n->type == ROFFT_EQN ? MANDOCERR_EQN_TMAN :
 		    MANDOCERR_TBL_TMAN, n->line, n->pos, NULL);
 		outflags |= MMAN_PP | MMAN_sp | MMAN_nl;
 		print_word("The");
 		print_line(".B \\-T man", MMAN_nl);
 		print_word("output mode does not support");
 		print_word(n->type == ROFFT_EQN ? "eqn(7)" : "tbl(7)");
 		print_word("input.");
 		outflags |= MMAN_PP | MMAN_sp | MMAN_nl;
 		return;
 	case ROFFT_TEXT:
 		/*
 		 * Make sure that we don't happen to start with a
 		 * control character at the start of a line.
 		 */
 		if (MMAN_nl & outflags &&
 		    ('.' == *n->string || '\'' == *n->string)) {
 			print_word("");
 			printf("\\&");
 			outflags &= ~MMAN_spc;
 		}
 		if (n->flags & NODE_DELIMC)
 			outflags &= ~(MMAN_spc | MMAN_spc_force);
 		else if (outflags & MMAN_Sm)
 			outflags |= MMAN_spc_force;
 		print_word(n->string);
 		if (n->flags & NODE_DELIMO)
 			outflags &= ~(MMAN_spc | MMAN_spc_force);
 		else if (outflags & MMAN_Sm)
 			outflags |= MMAN_spc;
 		break;
 	default:
 		if (n->tok < ROFF_MAX) {
 			(*roff_man_acts[n->tok])(meta, n);
 			return;
 		}
 		act = mdoc_man_act(n->tok);
 		cond = act->cond == NULL || (*act->cond)(meta, n);
 		if (cond && act->pre != NULL &&
 		    (n->end == ENDBODY_NOT || n->child != NULL))
 			do_sub = (*act->pre)(meta, n);
 		break;
 	}
 
 	/*
 	 * Conditionally run all child nodes.
 	 * Note that this iterates over children instead of using
 	 * recursion.  This prevents unnecessary depth in the stack.
 	 */
 	if (do_sub)
 		for (sub = n->child; sub; sub = sub->next)
 			print_node(meta, sub);
 
 	/*
 	 * Lastly, conditionally run the post-node handler.
 	 */
 	if (NODE_ENDED & n->flags)
 		return;
 
 	if (cond && act->post)
 		(*act->post)(meta, n);
 
 	if (ENDBODY_NOT != n->end)
 		n->body->flags |= NODE_ENDED;
 }
 
 static int
 cond_head(DECL_ARGS)
 {
 
 	return n->type == ROFFT_HEAD;
 }
 
 static int
 cond_body(DECL_ARGS)
 {
 
 	return n->type == ROFFT_BODY;
 }
 
 static int
 pre_abort(DECL_ARGS)
 {
 	abort();
 }
 
 static int
 pre_enc(DECL_ARGS)
 {
 	const char	*prefix;
 
 	prefix = mdoc_man_act(n->tok)->prefix;
 	if (NULL == prefix)
 		return 1;
 	print_word(prefix);
 	outflags &= ~MMAN_spc;
 	return 1;
 }
 
 static void
 post_enc(DECL_ARGS)
 {
 	const char *suffix;
 
 	suffix = mdoc_man_act(n->tok)->suffix;
 	if (NULL == suffix)
 		return;
 	outflags &= ~(MMAN_spc | MMAN_nl);
 	print_word(suffix);
 }
 
 static int
 pre_ex(DECL_ARGS)
 {
 	outflags |= MMAN_br | MMAN_nl;
 	return 1;
 }
 
 static void
 post_font(DECL_ARGS)
 {
 
 	font_pop();
 }
 
 static void
 post_percent(DECL_ARGS)
 {
 	struct roff_node *np, *nn, *nnn;
 
 	if (mdoc_man_act(n->tok)->pre == pre_em)
 		font_pop();
 
 	if (n->parent == NULL || n->parent->tok != MDOC_Rs)
 		return;
 
 	if ((nn = roff_node_next(n)) != NULL) {
 		np = roff_node_prev(n);
 		nnn = nn == NULL ? NULL : roff_node_next(nn);
 		if (nn->tok != n->tok ||
 		    (np != NULL && np->tok == n->tok) ||
 		    (nnn != NULL && nnn->tok == n->tok))
 			print_word(",");
 		if (nn->tok == n->tok &&
 		    (nnn == NULL || nnn->tok != n->tok))
 			print_word("and");
 	} else {
 		print_word(".");
 		outflags |= MMAN_nl;
 	}
 }
 
 static int
 pre__t(DECL_ARGS)
 {
 
 	if (n->parent->tok == MDOC_Rs && n->parent->norm->Rs.quote_T) {
 		print_word("\\(lq");
 		outflags &= ~MMAN_spc;
 	} else
 		font_push('I');
 	return 1;
 }
 
 static void
 post__t(DECL_ARGS)
 {
 
 	if (n->parent->tok  == MDOC_Rs && n->parent->norm->Rs.quote_T) {
 		outflags &= ~MMAN_spc;
 		print_word("\\(rq");
 	} else
 		font_pop();
 	post_percent(meta, n);
 }
 
 /*
  * Print before a section header.
  */
 static int
 pre_sect(DECL_ARGS)
 {
 
 	if (n->type == ROFFT_HEAD) {
 		outflags |= MMAN_sp;
 		print_block(mdoc_man_act(n->tok)->prefix, 0);
 		print_word("");
 		putchar('\"');
 		outflags &= ~MMAN_spc;
 	}
 	return 1;
 }
 
 /*
  * Print subsequent a section header.
  */
 static void
 post_sect(DECL_ARGS)
 {
 
 	if (n->type != ROFFT_HEAD)
 		return;
 	outflags &= ~MMAN_spc;
 	print_word("");
 	putchar('\"');
 	outflags |= MMAN_nl;
 	if (MDOC_Sh == n->tok && SEC_AUTHORS == n->sec)
 		outflags &= ~(MMAN_An_split | MMAN_An_nosplit);
 }
 
 /* See mdoc_term.c, synopsis_pre() for comments. */
 static void
 pre_syn(struct roff_node *n)
 {
 	struct roff_node *np;
 
 	if ((n->flags & NODE_SYNPRETTY) == 0 ||
 	    (np = roff_node_prev(n)) == NULL)
 		return;
 
 	if (np->tok == n->tok &&
 	    MDOC_Ft != n->tok &&
 	    MDOC_Fo != n->tok &&
 	    MDOC_Fn != n->tok) {
 		outflags |= MMAN_br;
 		return;
 	}
 
 	switch (np->tok) {
 	case MDOC_Fd:
 	case MDOC_Fn:
 	case MDOC_Fo:
 	case MDOC_In:
 	case MDOC_Vt:
 		outflags |= MMAN_sp;
 		break;
 	case MDOC_Ft:
 		if (MDOC_Fn != n->tok && MDOC_Fo != n->tok) {
 			outflags |= MMAN_sp;
 			break;
 		}
 		/* FALLTHROUGH */
 	default:
 		outflags |= MMAN_br;
 		break;
 	}
 }
 
 static int
 pre_an(DECL_ARGS)
 {
 
 	switch (n->norm->An.auth) {
 	case AUTH_split:
 		outflags &= ~MMAN_An_nosplit;
 		outflags |= MMAN_An_split;
 		return 0;
 	case AUTH_nosplit:
 		outflags &= ~MMAN_An_split;
 		outflags |= MMAN_An_nosplit;
 		return 0;
 	default:
 		if (MMAN_An_split & outflags)
 			outflags |= MMAN_br;
 		else if (SEC_AUTHORS == n->sec &&
 		    ! (MMAN_An_nosplit & outflags))
 			outflags |= MMAN_An_split;
 		return 1;
 	}
 }
 
 static int
 pre_ap(DECL_ARGS)
 {
 
 	outflags &= ~MMAN_spc;
 	print_word("'");
 	outflags &= ~MMAN_spc;
 	return 0;
 }
 
 static int
 pre_aq(DECL_ARGS)
 {
 
 	print_word(n->child != NULL && n->child->next == NULL &&
 	    n->child->tok == MDOC_Mt ?  "<" : "\\(la");
 	outflags &= ~MMAN_spc;
 	return 1;
 }
 
 static void
 post_aq(DECL_ARGS)
 {
 
 	outflags &= ~(MMAN_spc | MMAN_nl);
 	print_word(n->child != NULL && n->child->next == NULL &&
 	    n->child->tok == MDOC_Mt ?  ">" : "\\(ra");
 }
 
 static int
 pre_bd(DECL_ARGS)
 {
 	outflags &= ~(MMAN_PP | MMAN_sp | MMAN_br);
 	if (n->norm->Bd.type == DISP_unfilled ||
 	    n->norm->Bd.type == DISP_literal)
 		print_line(".nf", 0);
 	if (n->norm->Bd.comp == 0 && roff_node_prev(n->parent) != NULL)
 		outflags |= MMAN_sp;
 	print_offs(n->norm->Bd.offs, 1);
 	return 1;
 }
 
 static void
 post_bd(DECL_ARGS)
 {
 	enum roff_tok	 bef, now;
 
 	/* Close out this display. */
 	print_line(".RE", MMAN_nl);
 	bef = n->flags & NODE_NOFILL ? ROFF_nf : ROFF_fi;
 	if (n->last == NULL)
 		now = n->norm->Bd.type == DISP_unfilled ||
 		    n->norm->Bd.type == DISP_literal ? ROFF_nf : ROFF_fi;
 	else if (n->last->tok == ROFF_nf)
 		now = ROFF_nf;
 	else if (n->last->tok == ROFF_fi)
 		now = ROFF_fi;
 	else
 		now = n->last->flags & NODE_NOFILL ? ROFF_nf : ROFF_fi;
 	if (bef != now) {
 		outflags |= MMAN_nl;
 		print_word(".");
 		outflags &= ~MMAN_spc;
 		print_word(roff_name[bef]);
 		outflags |= MMAN_nl;
 	}
 
 	/* Maybe we are inside an enclosing list? */
 	if (roff_node_next(n->parent) != NULL)
 		mid_it();
 }
 
 static int
 pre_bf(DECL_ARGS)
 {
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		return 1;
 	case ROFFT_BODY:
 		break;
 	default:
 		return 0;
 	}
 	switch (n->norm->Bf.font) {
 	case FONT_Em:
 		font_push('I');
 		break;
 	case FONT_Sy:
 		font_push('B');
 		break;
 	default:
 		font_push('R');
 		break;
 	}
 	return 1;
 }
 
 static void
 post_bf(DECL_ARGS)
 {
 
 	if (n->type == ROFFT_BODY)
 		font_pop();
 }
 
 static int
 pre_bk(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		return 1;
 	case ROFFT_BODY:
 	case ROFFT_ELEM:
 		outflags |= MMAN_Bk;
 		return 1;
 	default:
 		return 0;
 	}
 }
 
 static void
 post_bk(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_ELEM:
 		while ((n = n->parent) != NULL)
 			 if (n->tok == MDOC_Bk)
 				return;
 		/* FALLTHROUGH */
 	case ROFFT_BODY:
 		outflags &= ~MMAN_Bk;
 		break;
 	default:
 		break;
 	}
 }
 
 static int
 pre_bl(DECL_ARGS)
 {
 	size_t		 icol;
 
 	/*
 	 * print_offs() will increase the -offset to account for
 	 * a possible enclosing .It, but any enclosed .It blocks
 	 * just nest and do not add up their indentation.
 	 */
 	if (n->norm->Bl.offs) {
 		print_offs(n->norm->Bl.offs, 0);
 		Bl_stack[Bl_stack_len++] = 0;
 	}
 
 	switch (n->norm->Bl.type) {
 	case LIST_enum:
 		n->norm->Bl.count = 0;
 		return 1;
 	case LIST_column:
 		break;
 	default:
 		return 1;
 	}
 
 	if (n->child != NULL) {
 		print_line(".TS", MMAN_nl);
 		for (icol = 0; icol < n->norm->Bl.ncols; icol++)
 			print_word("l");
 		print_word(".");
 	}
 	outflags |= MMAN_nl;
 	return 1;
 }
 
 static void
 post_bl(DECL_ARGS)
 {
 
 	switch (n->norm->Bl.type) {
 	case LIST_column:
 		if (n->child != NULL)
 			print_line(".TE", 0);
 		break;
 	case LIST_enum:
 		n->norm->Bl.count = 0;
 		break;
 	default:
 		break;
 	}
 
 	if (n->norm->Bl.offs) {
 		print_line(".RE", MMAN_nl);
 		assert(Bl_stack_len);
 		Bl_stack_len--;
 		assert(Bl_stack[Bl_stack_len] == 0);
 	} else {
 		outflags |= MMAN_PP | MMAN_nl;
 		outflags &= ~(MMAN_sp | MMAN_br);
 	}
 
 	/* Maybe we are inside an enclosing list? */
 	if (roff_node_next(n->parent) != NULL)
 		mid_it();
 }
 
 static void
 pre_br(DECL_ARGS)
 {
 	outflags |= MMAN_br;
 }
 
 static int
 pre_dl(DECL_ARGS)
 {
 	print_offs("6n", 0);
 	return 1;
 }
 
 static void
 post_dl(DECL_ARGS)
 {
 	print_line(".RE", MMAN_nl);
 
 	/* Maybe we are inside an enclosing list? */
 	if (roff_node_next(n->parent) != NULL)
 		mid_it();
 }
 
 static int
 pre_em(DECL_ARGS)
 {
 
 	font_push('I');
 	return 1;
 }
 
 static int
 pre_en(DECL_ARGS)
 {
 
 	if (NULL == n->norm->Es ||
 	    NULL == n->norm->Es->child)
 		return 1;
 
 	print_word(n->norm->Es->child->string);
 	outflags &= ~MMAN_spc;
 	return 1;
 }
 
 static void
 post_en(DECL_ARGS)
 {
 
 	if (NULL == n->norm->Es ||
 	    NULL == n->norm->Es->child ||
 	    NULL == n->norm->Es->child->next)
 		return;
 
 	outflags &= ~MMAN_spc;
 	print_word(n->norm->Es->child->next->string);
 	return;
 }
 
 static int
 pre_eo(DECL_ARGS)
 {
 
 	if (n->end == ENDBODY_NOT &&
 	    n->parent->head->child == NULL &&
 	    n->child != NULL &&
 	    n->child->end != ENDBODY_NOT)
 		print_word("\\&");
 	else if (n->end != ENDBODY_NOT ? n->child != NULL :
 	    n->parent->head->child != NULL && (n->child != NULL ||
 	    (n->parent->tail != NULL && n->parent->tail->child != NULL)))
 		outflags &= ~(MMAN_spc | MMAN_nl);
 	return 1;
 }
 
 static void
 post_eo(DECL_ARGS)
 {
 	int	 body, tail;
 
 	if (n->end != ENDBODY_NOT) {
 		outflags |= MMAN_spc;
 		return;
 	}
 
 	body = n->child != NULL || n->parent->head->child != NULL;
 	tail = n->parent->tail != NULL && n->parent->tail->child != NULL;
 
 	if (body && tail)
 		outflags &= ~MMAN_spc;
 	else if ( ! (body || tail))
 		print_word("\\&");
 	else if ( ! tail)
 		outflags |= MMAN_spc;
 }
 
 static int
 pre_fa(DECL_ARGS)
 {
 	int	 am_Fa;
 
 	am_Fa = MDOC_Fa == n->tok;
 
 	if (am_Fa)
 		n = n->child;
 
 	while (NULL != n) {
 		font_push('I');
 		if (am_Fa || NODE_SYNPRETTY & n->flags)
 			outflags |= MMAN_nbrword;
 		print_node(meta, n);
 		font_pop();
 		if (NULL != (n = n->next))
 			print_word(",");
 	}
 	return 0;
 }
 
 static void
 post_fa(DECL_ARGS)
 {
 	struct roff_node *nn;
 
 	if ((nn = roff_node_next(n)) != NULL && nn->tok == MDOC_Fa)
 		print_word(",");
 }
 
 static int
 pre_fd(DECL_ARGS)
 {
 	pre_syn(n);
 	font_push('B');
 	return 1;
 }
 
 static void
 post_fd(DECL_ARGS)
 {
 	font_pop();
 	outflags |= MMAN_br;
 }
 
 static int
 pre_fl(DECL_ARGS)
 {
 	font_push('B');
 	print_word("\\-");
 	if (n->child != NULL)
 		outflags &= ~MMAN_spc;
 	return 1;
 }
 
 static void
 post_fl(DECL_ARGS)
 {
 	struct roff_node *nn;
 
 	font_pop();
 	if (n->child == NULL &&
 	    ((nn = roff_node_next(n)) != NULL &&
 	    nn->type != ROFFT_TEXT &&
 	    (nn->flags & NODE_LINE) == 0))
 		outflags &= ~MMAN_spc;
 }
 
 static int
 pre_fn(DECL_ARGS)
 {
 
 	pre_syn(n);
 
 	n = n->child;
 	if (NULL == n)
 		return 0;
 
 	if (NODE_SYNPRETTY & n->flags)
 		print_block(".HP 4n", MMAN_nl);
 
 	font_push('B');
 	print_node(meta, n);
 	font_pop();
 	outflags &= ~MMAN_spc;
 	print_word("(");
 	outflags &= ~MMAN_spc;
 
 	n = n->next;
 	if (NULL != n)
 		pre_fa(meta, n);
 	return 0;
 }
 
 static void
 post_fn(DECL_ARGS)
 {
 
 	print_word(")");
 	if (NODE_SYNPRETTY & n->flags) {
 		print_word(";");
 		outflags |= MMAN_PP;
 	}
 }
 
 static int
 pre_fo(DECL_ARGS)
 {
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		pre_syn(n);
 		break;
 	case ROFFT_HEAD:
 		if (n->child == NULL)
 			return 0;
 		if (NODE_SYNPRETTY & n->flags)
 			print_block(".HP 4n", MMAN_nl);
 		font_push('B');
 		break;
 	case ROFFT_BODY:
 		outflags &= ~(MMAN_spc | MMAN_nl);
 		print_word("(");
 		outflags &= ~MMAN_spc;
 		break;
 	default:
 		break;
 	}
 	return 1;
 }
 
 static void
 post_fo(DECL_ARGS)
 {
 
 	switch (n->type) {
 	case ROFFT_HEAD:
 		if (n->child != NULL)
 			font_pop();
 		break;
 	case ROFFT_BODY:
 		post_fn(meta, n);
 		break;
 	default:
 		break;
 	}
 }
 
 static int
 pre_Ft(DECL_ARGS)
 {
 
 	pre_syn(n);
 	font_push('I');
 	return 1;
 }
 
 static void
 pre_ft(DECL_ARGS)
 {
 	print_line(".ft", 0);
 	print_word(n->child->string);
 	outflags |= MMAN_nl;
 }
 
 static int
 pre_in(DECL_ARGS)
 {
 
 	if (NODE_SYNPRETTY & n->flags) {
 		pre_syn(n);
 		font_push('B');
 		print_word("#include <");
 		outflags &= ~MMAN_spc;
 	} else {
 		print_word("<");
 		outflags &= ~MMAN_spc;
 		font_push('I');
 	}
 	return 1;
 }
 
 static void
 post_in(DECL_ARGS)
 {
 
 	if (NODE_SYNPRETTY & n->flags) {
 		outflags &= ~MMAN_spc;
 		print_word(">");
 		font_pop();
 		outflags |= MMAN_br;
 	} else {
 		font_pop();
 		outflags &= ~MMAN_spc;
 		print_word(">");
 	}
 }
 
 static int
 pre_it(DECL_ARGS)
 {
 	const struct roff_node *bln;
 
 	switch (n->type) {
 	case ROFFT_HEAD:
 		outflags |= MMAN_PP | MMAN_nl;
 		bln = n->parent->parent;
 		if (bln->norm->Bl.comp == 0 ||
 		    (n->parent->prev == NULL &&
 		     roff_node_prev(bln->parent) == NULL))
 			outflags |= MMAN_sp;
 		outflags &= ~MMAN_br;
 		switch (bln->norm->Bl.type) {
 		case LIST_item:
 			return 0;
 		case LIST_inset:
 		case LIST_diag:
 		case LIST_ohang:
 			if (bln->norm->Bl.type == LIST_diag)
 				print_line(".B \"", 0);
 			else
 				print_line(".BR \\& \"", 0);
 			outflags &= ~MMAN_spc;
 			return 1;
 		case LIST_bullet:
 		case LIST_dash:
 		case LIST_hyphen:
 			print_width(&bln->norm->Bl, NULL);
 			TPremain = 0;
 			outflags |= MMAN_nl;
 			font_push('B');
 			if (LIST_bullet == bln->norm->Bl.type)
 				print_word("\\(bu");
 			else
 				print_word("-");
 			font_pop();
 			outflags |= MMAN_nl;
 			return 0;
 		case LIST_enum:
 			print_width(&bln->norm->Bl, NULL);
 			TPremain = 0;
 			outflags |= MMAN_nl;
 			print_count(&bln->norm->Bl.count);
 			outflags |= MMAN_nl;
 			return 0;
 		case LIST_hang:
 			print_width(&bln->norm->Bl, n->child);
 			TPremain = 0;
 			outflags |= MMAN_nl;
 			return 1;
 		case LIST_tag:
 			print_width(&bln->norm->Bl, n->child);
 			putchar('\n');
 			outflags &= ~MMAN_spc;
 			return 1;
 		default:
 			return 1;
 		}
 	default:
 		break;
 	}
 	return 1;
 }
 
 /*
  * This function is called after closing out an indented block.
  * If we are inside an enclosing list, restore its indentation.
  */
 static void
 mid_it(void)
 {
 	char		 buf[24];
 
 	/* Nothing to do outside a list. */
 	if (0 == Bl_stack_len || 0 == Bl_stack[Bl_stack_len - 1])
 		return;
 
 	/* The indentation has already been set up. */
 	if (Bl_stack_post[Bl_stack_len - 1])
 		return;
 
 	/* Restore the indentation of the enclosing list. */
 	print_line(".RS", MMAN_Bk_susp);
 	(void)snprintf(buf, sizeof(buf), "%dn",
 	    Bl_stack[Bl_stack_len - 1]);
 	print_word(buf);
 
 	/* Remember to close out this .RS block later. */
 	Bl_stack_post[Bl_stack_len - 1] = 1;
 }
 
 static void
 post_it(DECL_ARGS)
 {
 	const struct roff_node *bln;
 
 	bln = n->parent->parent;
 
 	switch (n->type) {
 	case ROFFT_HEAD:
 		switch (bln->norm->Bl.type) {
 		case LIST_diag:
 			outflags &= ~MMAN_spc;
 			print_word("\\ ");
 			break;
 		case LIST_ohang:
 			outflags |= MMAN_br;
 			break;
 		default:
 			break;
 		}
 		break;
 	case ROFFT_BODY:
 		switch (bln->norm->Bl.type) {
 		case LIST_bullet:
 		case LIST_dash:
 		case LIST_hyphen:
 		case LIST_enum:
 		case LIST_hang:
 		case LIST_tag:
 			assert(Bl_stack_len);
 			Bl_stack[--Bl_stack_len] = 0;
 
 			/*
 			 * Our indentation had to be restored
 			 * after a child display or child list.
 			 * Close out that indentation block now.
 			 */
 			if (Bl_stack_post[Bl_stack_len]) {
 				print_line(".RE", MMAN_nl);
 				Bl_stack_post[Bl_stack_len] = 0;
 			}
 			break;
 		case LIST_column:
 			if (NULL != n->next) {
 				putchar('\t');
 				outflags &= ~MMAN_spc;
 			}
 			break;
 		default:
 			break;
 		}
 		break;
 	default:
 		break;
 	}
 }
 
 static void
 post_lb(DECL_ARGS)
 {
 
 	if (SEC_LIBRARY == n->sec)
 		outflags |= MMAN_br;
 }
 
 static int
 pre_lk(DECL_ARGS)
 {
 	const struct roff_node *link, *descr, *punct;
 
 	if ((link = n->child) == NULL)
 		return 0;
 
 	/* Find beginning of trailing punctuation. */
 	punct = n->last;
 	while (punct != link && punct->flags & NODE_DELIMC)
 		punct = punct->prev;
 	punct = punct->next;
 
 	/* Link text. */
 	if ((descr = link->next) != NULL && descr != punct) {
 		font_push('I');
 		while (descr != punct) {
 			print_word(descr->string);
 			descr = descr->next;
 		}
 		font_pop();
 		print_word(":");
 	}
 
 	/* Link target. */
-	font_push('B');
 	print_word(link->string);
-	font_pop();
 
 	/* Trailing punctuation. */
 	while (punct != NULL) {
 		print_word(punct->string);
 		punct = punct->next;
 	}
 	return 0;
 }
 
 static void
 pre_onearg(DECL_ARGS)
 {
 	outflags |= MMAN_nl;
 	print_word(".");
 	outflags &= ~MMAN_spc;
 	print_word(roff_name[n->tok]);
 	if (n->child != NULL)
 		print_word(n->child->string);
 	outflags |= MMAN_nl;
 	if (n->tok == ROFF_ce)
 		for (n = n->child->next; n != NULL; n = n->next)
 			print_node(meta, n);
 }
 
 static int
 pre_li(DECL_ARGS)
 {
 	font_push('R');
 	return 1;
 }
 
 static int
 pre_nm(DECL_ARGS)
 {
 	char	*name;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		outflags |= MMAN_Bk;
 		pre_syn(n);
 		return 1;
 	case ROFFT_HEAD:
 	case ROFFT_ELEM:
 		break;
 	default:
 		return 1;
 	}
 	name = n->child == NULL ? NULL : n->child->string;
 	if (name == NULL)
 		return 0;
 	if (n->type == ROFFT_HEAD) {
 		if (roff_node_prev(n->parent) == NULL)
 			outflags |= MMAN_sp;
 		print_block(".HP", 0);
 		printf(" %dn", man_strlen(name) + 1);
 		outflags |= MMAN_nl;
 	}
 	font_push('B');
 	return 1;
 }
 
 static void
 post_nm(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		outflags &= ~MMAN_Bk;
 		break;
 	case ROFFT_HEAD:
 	case ROFFT_ELEM:
 		if (n->child != NULL && n->child->string != NULL)
 			font_pop();
 		break;
 	default:
 		break;
 	}
 }
 
 static int
 pre_no(DECL_ARGS)
 {
 	outflags |= MMAN_spc_force;
 	return 1;
 }
 
 static void
 pre_noarg(DECL_ARGS)
 {
 	outflags |= MMAN_nl;
 	print_word(".");
 	outflags &= ~MMAN_spc;
 	print_word(roff_name[n->tok]);
 	outflags |= MMAN_nl;
 }
 
 static int
 pre_ns(DECL_ARGS)
 {
 	outflags &= ~MMAN_spc;
 	return 0;
 }
 
 static void
 post_pf(DECL_ARGS)
 {
 
 	if ( ! (n->next == NULL || n->next->flags & NODE_LINE))
 		outflags &= ~MMAN_spc;
 }
 
 static int
 pre_pp(DECL_ARGS)
 {
 
 	if (MDOC_It != n->parent->tok)
 		outflags |= MMAN_PP;
 	outflags |= MMAN_sp | MMAN_nl;
 	outflags &= ~MMAN_br;
 	return 0;
 }
 
 static int
 pre_rs(DECL_ARGS)
 {
 
 	if (SEC_SEE_ALSO == n->sec) {
 		outflags |= MMAN_PP | MMAN_sp | MMAN_nl;
 		outflags &= ~MMAN_br;
 	}
 	return 1;
 }
 
 static int
 pre_skip(DECL_ARGS)
 {
 
 	return 0;
 }
 
 static int
 pre_sm(DECL_ARGS)
 {
 
 	if (NULL == n->child)
 		outflags ^= MMAN_Sm;
 	else if (0 == strcmp("on", n->child->string))
 		outflags |= MMAN_Sm;
 	else
 		outflags &= ~MMAN_Sm;
 
 	if (MMAN_Sm & outflags)
 		outflags |= MMAN_spc;
 
 	return 0;
 }
 
 static void
 pre_sp(DECL_ARGS)
 {
 	if (outflags & MMAN_PP) {
 		outflags &= ~MMAN_PP;
 		print_line(".PP", 0);
 	} else {
 		print_line(".sp", 0);
 		if (n->child != NULL)
 			print_word(n->child->string);
 	}
 	outflags |= MMAN_nl;
 }
 
 static int
 pre_sy(DECL_ARGS)
 {
 
 	font_push('B');
 	return 1;
 }
 
 static void
 pre_ta(DECL_ARGS)
 {
 	print_line(".ta", 0);
 	for (n = n->child; n != NULL; n = n->next)
 		print_word(n->string);
 	outflags |= MMAN_nl;
 }
 
 static int
 pre_vt(DECL_ARGS)
 {
 
 	if (NODE_SYNPRETTY & n->flags) {
 		switch (n->type) {
 		case ROFFT_BLOCK:
 			pre_syn(n);
 			return 1;
 		case ROFFT_BODY:
 			break;
 		default:
 			return 0;
 		}
 	}
 	font_push('I');
 	return 1;
 }
 
 static void
 post_vt(DECL_ARGS)
 {
 
 	if (n->flags & NODE_SYNPRETTY && n->type != ROFFT_BODY)
 		return;
 	font_pop();
 }
 
 static int
 pre_xr(DECL_ARGS)
 {
 
 	n = n->child;
 	if (NULL == n)
 		return 0;
 	print_node(meta, n);
 	n = n->next;
 	if (NULL == n)
 		return 0;
 	outflags &= ~MMAN_spc;
 	print_word("(");
 	print_node(meta, n);
 	print_word(")");
 	return 0;
 }
diff --git a/contrib/mandoc/mdoc_markdown.c b/contrib/mandoc/mdoc_markdown.c
index 06ca839a58b8..eaa22626c99c 100644
--- a/contrib/mandoc/mdoc_markdown.c
+++ b/contrib/mandoc/mdoc_markdown.c
@@ -1,1639 +1,1647 @@
-/* $Id: mdoc_markdown.c,v 1.39 2025/01/20 07:01:17 schwarze Exp $ */
+/* $Id: mdoc_markdown.c,v 1.40 2025/06/26 17:06:34 schwarze Exp $ */
 /*
  * Copyright (c) 2017, 2018, 2020, 2025 Ingo Schwarze 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
  * Markdown formatter for mdoc(7) used by mandoc(1).
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "roff.h"
 #include "mdoc.h"
 #include "main.h"
 
 struct	md_act {
 	int		(*cond)(struct roff_node *);
 	int		(*pre)(struct roff_node *);
 	void		(*post)(struct roff_node *);
 	const char	 *prefix; /* pre-node string constant */
 	const char	 *suffix; /* post-node string constant */
 };
 
 static	void	 md_nodelist(struct roff_node *);
 static	void	 md_node(struct roff_node *);
 static	const char *md_stack(char);
 static	void	 md_preword(void);
 static	void	 md_rawword(const char *);
 static	void	 md_word(const char *);
 static	void	 md_named(const char *);
 static	void	 md_char(unsigned char);
 static	void	 md_uri(const char *);
 
 static	int	 md_cond_head(struct roff_node *);
 static	int	 md_cond_body(struct roff_node *);
 
 static	int	 md_pre_abort(struct roff_node *);
 static	int	 md_pre_raw(struct roff_node *);
 static	int	 md_pre_word(struct roff_node *);
 static	int	 md_pre_skip(struct roff_node *);
 static	void	 md_pre_syn(struct roff_node *);
 static	int	 md_pre_An(struct roff_node *);
 static	int	 md_pre_Ap(struct roff_node *);
 static	int	 md_pre_Bd(struct roff_node *);
 static	int	 md_pre_Bk(struct roff_node *);
 static	int	 md_pre_Bl(struct roff_node *);
 static	int	 md_pre_D1(struct roff_node *);
 static	int	 md_pre_Dl(struct roff_node *);
 static	int	 md_pre_En(struct roff_node *);
 static	int	 md_pre_Eo(struct roff_node *);
 static	int	 md_pre_Fa(struct roff_node *);
 static	int	 md_pre_Fd(struct roff_node *);
 static	int	 md_pre_Fn(struct roff_node *);
 static	int	 md_pre_Fo(struct roff_node *);
 static	int	 md_pre_In(struct roff_node *);
 static	int	 md_pre_It(struct roff_node *);
 static	int	 md_pre_Lk(struct roff_node *);
 static	int	 md_pre_Mt(struct roff_node *);
 static	int	 md_pre_Nd(struct roff_node *);
 static	int	 md_pre_Nm(struct roff_node *);
 static	int	 md_pre_No(struct roff_node *);
 static	int	 md_pre_Ns(struct roff_node *);
 static	int	 md_pre_Pp(struct roff_node *);
 static	int	 md_pre_Rs(struct roff_node *);
 static	int	 md_pre_Sh(struct roff_node *);
 static	int	 md_pre_Sm(struct roff_node *);
 static	int	 md_pre_Vt(struct roff_node *);
 static	int	 md_pre_Xr(struct roff_node *);
 static	int	 md_pre__R(struct roff_node *);
 static	int	 md_pre__T(struct roff_node *);
 static	int	 md_pre_br(struct roff_node *);
 
 static	void	 md_post_raw(struct roff_node *);
 static	void	 md_post_word(struct roff_node *);
 static	void	 md_post_pc(struct roff_node *);
 static	void	 md_post_Bk(struct roff_node *);
 static	void	 md_post_Bl(struct roff_node *);
 static	void	 md_post_D1(struct roff_node *);
 static	void	 md_post_En(struct roff_node *);
 static	void	 md_post_Eo(struct roff_node *);
 static	void	 md_post_Fa(struct roff_node *);
 static	void	 md_post_Fd(struct roff_node *);
 static	void	 md_post_Fl(struct roff_node *);
 static	void	 md_post_Fn(struct roff_node *);
 static	void	 md_post_Fo(struct roff_node *);
 static	void	 md_post_In(struct roff_node *);
 static	void	 md_post_It(struct roff_node *);
 static	void	 md_post_Lb(struct roff_node *);
 static	void	 md_post_Nm(struct roff_node *);
 static	void	 md_post_Pf(struct roff_node *);
 static	void	 md_post_Vt(struct roff_node *);
 static	void	 md_post__T(struct roff_node *);
 
 static	const struct md_act md_acts[MDOC_MAX - MDOC_Dd] = {
 	{ NULL, NULL, NULL, NULL, NULL }, /* Dd */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Dt */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Os */
 	{ NULL, md_pre_Sh, NULL, NULL, NULL }, /* Sh */
 	{ NULL, md_pre_Sh, NULL, NULL, NULL }, /* Ss */
 	{ NULL, md_pre_Pp, NULL, NULL, NULL }, /* Pp */
 	{ md_cond_body, md_pre_D1, md_post_D1, NULL, NULL }, /* D1 */
 	{ md_cond_body, md_pre_Dl, md_post_D1, NULL, NULL }, /* Dl */
 	{ md_cond_body, md_pre_Bd, md_post_D1, NULL, NULL }, /* Bd */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ed */
 	{ md_cond_body, md_pre_Bl, md_post_Bl, NULL, NULL }, /* Bl */
 	{ NULL, NULL, NULL, NULL, NULL }, /* El */
 	{ NULL, md_pre_It, md_post_It, NULL, NULL }, /* It */
 	{ NULL, md_pre_raw, md_post_raw, "*", "*" }, /* Ad */
 	{ NULL, md_pre_An, NULL, NULL, NULL }, /* An */
 	{ NULL, md_pre_Ap, NULL, NULL, NULL }, /* Ap */
 	{ NULL, md_pre_raw, md_post_raw, "*", "*" }, /* Ar */
 	{ NULL, md_pre_raw, md_post_raw, "**", "**" }, /* Cd */
 	{ NULL, md_pre_raw, md_post_raw, "**", "**" }, /* Cm */
 	{ NULL, md_pre_raw, md_post_raw, "`", "`" }, /* Dv */
 	{ NULL, md_pre_raw, md_post_raw, "`", "`" }, /* Er */
 	{ NULL, md_pre_raw, md_post_raw, "`", "`" }, /* Ev */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ex */
 	{ NULL, md_pre_Fa, md_post_Fa, NULL, NULL }, /* Fa */
 	{ NULL, md_pre_Fd, md_post_Fd, "**", "**" }, /* Fd */
 	{ NULL, md_pre_raw, md_post_Fl, "**-", "**" }, /* Fl */
 	{ NULL, md_pre_Fn, md_post_Fn, NULL, NULL }, /* Fn */
 	{ NULL, md_pre_Fd, md_post_raw, "*", "*" }, /* Ft */
 	{ NULL, md_pre_raw, md_post_raw, "**", "**" }, /* Ic */
 	{ NULL, md_pre_In, md_post_In, NULL, NULL }, /* In */
 	{ NULL, md_pre_raw, md_post_raw, "`", "`" }, /* Li */
 	{ md_cond_head, md_pre_Nd, NULL, NULL, NULL }, /* Nd */
 	{ NULL, md_pre_Nm, md_post_Nm, "**", "**" }, /* Nm */
 	{ md_cond_body, md_pre_word, md_post_word, "[", "]" }, /* Op */
 	{ NULL, md_pre_abort, NULL, NULL, NULL }, /* Ot */
 	{ NULL, md_pre_raw, md_post_raw, "*", "*" }, /* Pa */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Rv */
 	{ NULL, NULL, NULL, NULL, NULL }, /* St */
 	{ NULL, md_pre_raw, md_post_raw, "*", "*" }, /* Va */
 	{ NULL, md_pre_Vt, md_post_Vt, "*", "*" }, /* Vt */
 	{ NULL, md_pre_Xr, NULL, NULL, NULL }, /* Xr */
 	{ NULL, NULL, md_post_pc, NULL, NULL }, /* %A */
 	{ NULL, md_pre_raw, md_post_pc, "*", "*" }, /* %B */
 	{ NULL, NULL, md_post_pc, NULL, NULL }, /* %D */
 	{ NULL, md_pre_raw, md_post_pc, "*", "*" }, /* %I */
 	{ NULL, md_pre_raw, md_post_pc, "*", "*" }, /* %J */
 	{ NULL, NULL, md_post_pc, NULL, NULL }, /* %N */
 	{ NULL, NULL, md_post_pc, NULL, NULL }, /* %O */
 	{ NULL, NULL, md_post_pc, NULL, NULL }, /* %P */
 	{ NULL, md_pre__R, md_post_pc, NULL, NULL }, /* %R */
 	{ NULL, md_pre__T, md_post__T, NULL, NULL }, /* %T */
 	{ NULL, NULL, md_post_pc, NULL, NULL }, /* %V */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ac */
 	{ md_cond_body, md_pre_word, md_post_word, "<", ">" }, /* Ao */
 	{ md_cond_body, md_pre_word, md_post_word, "<", ">" }, /* Aq */
 	{ NULL, NULL, NULL, NULL, NULL }, /* At */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Bc */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Bf XXX not implemented */
 	{ md_cond_body, md_pre_word, md_post_word, "[", "]" }, /* Bo */
 	{ md_cond_body, md_pre_word, md_post_word, "[", "]" }, /* Bq */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Bsx */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Bx */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Db */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Dc */
 	{ md_cond_body, md_pre_word, md_post_word, "\"", "\"" }, /* Do */
 	{ md_cond_body, md_pre_word, md_post_word, "\"", "\"" }, /* Dq */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ec */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ef */
 	{ NULL, md_pre_raw, md_post_raw, "*", "*" }, /* Em */
 	{ md_cond_body, md_pre_Eo, md_post_Eo, NULL, NULL }, /* Eo */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Fx */
 	{ NULL, md_pre_raw, md_post_raw, "**", "**" }, /* Ms */
 	{ NULL, md_pre_No, NULL, NULL, NULL }, /* No */
 	{ NULL, md_pre_Ns, NULL, NULL, NULL }, /* Ns */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Nx */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ox */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Pc */
 	{ NULL, NULL, md_post_Pf, NULL, NULL }, /* Pf */
 	{ md_cond_body, md_pre_word, md_post_word, "(", ")" }, /* Po */
 	{ md_cond_body, md_pre_word, md_post_word, "(", ")" }, /* Pq */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Qc */
 	{ md_cond_body, md_pre_raw, md_post_raw, "'`", "`'" }, /* Ql */
 	{ md_cond_body, md_pre_word, md_post_word, "\"", "\"" }, /* Qo */
 	{ md_cond_body, md_pre_word, md_post_word, "\"", "\"" }, /* Qq */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Re */
 	{ md_cond_body, md_pre_Rs, NULL, NULL, NULL }, /* Rs */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Sc */
 	{ md_cond_body, md_pre_word, md_post_word, "'", "'" }, /* So */
 	{ md_cond_body, md_pre_word, md_post_word, "'", "'" }, /* Sq */
 	{ NULL, md_pre_Sm, NULL, NULL, NULL }, /* Sm */
 	{ NULL, md_pre_raw, md_post_raw, "*", "*" }, /* Sx */
 	{ NULL, md_pre_raw, md_post_raw, "**", "**" }, /* Sy */
 	{ NULL, md_pre_raw, md_post_raw, "`", "`" }, /* Tn */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ux */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Xc */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Xo */
 	{ NULL, md_pre_Fo, md_post_Fo, "**", "**" }, /* Fo */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Fc */
 	{ md_cond_body, md_pre_word, md_post_word, "[", "]" }, /* Oo */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Oc */
 	{ NULL, md_pre_Bk, md_post_Bk, NULL, NULL }, /* Bk */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ek */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Bt */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Hf */
 	{ NULL, md_pre_raw, md_post_raw, "*", "*" }, /* Fr */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ud */
 	{ NULL, NULL, md_post_Lb, NULL, NULL }, /* Lb */
 	{ NULL, md_pre_abort, NULL, NULL, NULL }, /* Lp */
 	{ NULL, md_pre_Lk, NULL, NULL, NULL }, /* Lk */
 	{ NULL, md_pre_Mt, NULL, NULL, NULL }, /* Mt */
 	{ md_cond_body, md_pre_word, md_post_word, "{", "}" }, /* Brq */
 	{ md_cond_body, md_pre_word, md_post_word, "{", "}" }, /* Bro */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Brc */
 	{ NULL, NULL, md_post_pc, NULL, NULL }, /* %C */
 	{ NULL, md_pre_skip, NULL, NULL, NULL }, /* Es */
 	{ md_cond_body, md_pre_En, md_post_En, NULL, NULL }, /* En */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Dx */
 	{ NULL, NULL, md_post_pc, NULL, NULL }, /* %Q */
 	{ NULL, md_pre_Lk, md_post_pc, NULL, NULL }, /* %U */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ta */
 	{ NULL, md_pre_skip, NULL, NULL, NULL }, /* Tg */
 };
 static const struct md_act *md_act(enum roff_tok);
 
 static	int	 outflags;
 #define	MD_spc		 (1 << 0)  /* Blank character before next word. */
 #define	MD_spc_force	 (1 << 1)  /* Even before trailing punctuation. */
 #define	MD_nonl		 (1 << 2)  /* Prevent linebreak in markdown code. */
 #define	MD_nl		 (1 << 3)  /* Break markdown code line. */
 #define	MD_br		 (1 << 4)  /* Insert an output line break. */
 #define	MD_sp		 (1 << 5)  /* Insert a paragraph break. */
 #define	MD_Sm		 (1 << 6)  /* Horizontal spacing mode. */
 #define	MD_Bk		 (1 << 7)  /* Word keep mode. */
 #define	MD_An_split	 (1 << 8)  /* Author mode is "split". */
 #define	MD_An_nosplit	 (1 << 9)  /* Author mode is "nosplit". */
 
 static	int	 escflags; /* Escape in generated markdown code: */
 #define	ESC_BOL	 (1 << 0)  /* "#*+-" near the beginning of a line. */
 #define	ESC_NUM	 (1 << 1)  /* "." after a leading number. */
 #define	ESC_HYP	 (1 << 2)  /* "(" immediately after "]". */
 #define	ESC_SQU	 (1 << 4)  /* "]" when "[" is open. */
 #define	ESC_FON	 (1 << 5)  /* "*" immediately after unrelated "*". */
 #define	ESC_EOL	 (1 << 6)  /* " " at the and of a line. */
 
 static	int	 code_blocks, quote_blocks, list_blocks;
 static	int	 outcount;
 
 
 static const struct md_act *
 md_act(enum roff_tok tok)
 {
 	assert(tok >= MDOC_Dd && tok <= MDOC_MAX);
 	return md_acts + (tok - MDOC_Dd);
 }
 
 void
 markdown_mdoc(void *arg, const struct roff_meta *mdoc)
 {
 	outflags = MD_Sm;
 	md_word(mdoc->title);
 	if (mdoc->msec != NULL) {
 		outflags &= ~MD_spc;
 		md_word("(");
 		md_word(mdoc->msec);
 		md_word(")");
 	}
 	md_word("-");
 	md_word(mdoc->vol);
 	if (mdoc->arch != NULL) {
 		md_word("(");
 		md_word(mdoc->arch);
 		md_word(")");
 	}
 	outflags |= MD_sp;
 
 	md_nodelist(mdoc->first->child);
 
 	outflags |= MD_sp;
 	md_word(mdoc->os);
 	md_word("-");
 	md_word(mdoc->date);
+	md_word("-");
+	md_word(mdoc->title);
+	if (mdoc->msec != NULL) {
+		outflags &= ~MD_spc;
+		md_word("(");
+		md_word(mdoc->msec);
+		md_word(")");
+	}
 	putchar('\n');
 }
 
 static void
 md_nodelist(struct roff_node *n)
 {
 	while (n != NULL) {
 		md_node(n);
 		n = n->next;
 	}
 }
 
 static void
 md_node(struct roff_node *n)
 {
 	const struct md_act	*act;
 	int			 cond, process_children;
 
 	if (n->type == ROFFT_COMMENT || n->flags & NODE_NOPRT)
 		return;
 
 	if (outflags & MD_nonl)
 		outflags &= ~(MD_nl | MD_sp);
 	else if (outflags & MD_spc &&
 	     n->flags & NODE_LINE &&
 	     !roff_node_transparent(n))
 		outflags |= MD_nl;
 
 	act = NULL;
 	cond = 0;
 	process_children = 1;
 	n->flags &= ~NODE_ENDED;
 
 	if (n->type == ROFFT_TEXT) {
 		if (n->flags & NODE_DELIMC)
 			outflags &= ~(MD_spc | MD_spc_force);
 		else if (outflags & MD_Sm)
 			outflags |= MD_spc_force;
 		md_word(n->string);
 		if (n->flags & NODE_DELIMO)
 			outflags &= ~(MD_spc | MD_spc_force);
 		else if (outflags & MD_Sm)
 			outflags |= MD_spc;
 	} else if (n->tok < ROFF_MAX) {
 		switch (n->tok) {
 		case ROFF_br:
 			process_children = md_pre_br(n);
 			break;
 		case ROFF_sp:
 			process_children = md_pre_Pp(n);
 			break;
 		default:
 			process_children = 0;
 			break;
 		}
 	} else {
 		act = md_act(n->tok);
 		cond = act->cond == NULL || (*act->cond)(n);
 		if (cond && act->pre != NULL &&
 		    (n->end == ENDBODY_NOT || n->child != NULL))
 			process_children = (*act->pre)(n);
 	}
 
 	if (process_children && n->child != NULL)
 		md_nodelist(n->child);
 
 	if (n->flags & NODE_ENDED)
 		return;
 
 	if (cond && act->post != NULL)
 		(*act->post)(n);
 
 	if (n->end != ENDBODY_NOT)
 		n->body->flags |= NODE_ENDED;
 }
 
 static const char *
 md_stack(char c)
 {
 	static char	*stack;
 	static size_t	 sz;
 	static size_t	 cur;
 
 	switch (c) {
 	case '\0':
 		break;
 	case (char)-1:
 		assert(cur);
 		stack[--cur] = '\0';
 		break;
 	default:
 		if (cur + 1 >= sz) {
 			sz += 8;
 			stack = mandoc_realloc(stack, sz);
 		}
 		stack[cur] = c;
 		stack[++cur] = '\0';
 		break;
 	}
 	return stack == NULL ? "" : stack;
 }
 
 /*
  * Handle vertical and horizontal spacing.
  */
 static void
 md_preword(void)
 {
 	const char	*cp;
 
 	/*
 	 * If a list block is nested inside a code block or a blockquote,
 	 * blank lines for paragraph breaks no longer work; instead,
 	 * they terminate the list.  Work around this markdown issue
 	 * by using mere line breaks instead.
 	 */
 
 	if (list_blocks && outflags & MD_sp) {
 		outflags &= ~MD_sp;
 		outflags |= MD_br;
 	}
 
 	/*
 	 * End the old line if requested.
 	 * Escape whitespace at the end of the markdown line
 	 * such that it won't look like an output line break.
 	 */
 
 	if (outflags & MD_sp)
 		putchar('\n');
 	else if (outflags & MD_br) {
 		putchar(' ');
 		putchar(' ');
 	} else if (outflags & MD_nl && escflags & ESC_EOL)
 		md_named("zwnj");
 
 	/* Start a new line if necessary. */
 
 	if (outflags & (MD_nl | MD_br | MD_sp)) {
 		putchar('\n');
 		for (cp = md_stack('\0'); *cp != '\0'; cp++) {
 			putchar(*cp);
 			if (*cp == '>')
 				putchar(' ');
 		}
 		outflags &= ~(MD_nl | MD_br | MD_sp);
 		escflags = ESC_BOL;
 		outcount = 0;
 
 	/* Handle horizontal spacing. */
 
 	} else if (outflags & MD_spc) {
 		if (outflags & MD_Bk)
 			fputs(" ", stdout);
 		else
 			putchar(' ');
 		escflags &= ~ESC_FON;
 		outcount++;
 	}
 
 	outflags &= ~(MD_spc_force | MD_nonl);
 	if (outflags & MD_Sm)
 		outflags |= MD_spc;
 	else
 		outflags &= ~MD_spc;
 }
 
 /*
  * Print markdown syntax elements.
  * Can also be used for constant strings when neither escaping
  * nor delimiter handling is required.
  */
 static void
 md_rawword(const char *s)
 {
 	md_preword();
 
 	if (*s == '\0')
 		return;
 
 	if (escflags & ESC_FON) {
 		escflags &= ~ESC_FON;
 		if (*s == '*' && !code_blocks)
 			fputs("‌", stdout);
 	}
 
 	while (*s != '\0') {
 		switch(*s) {
 		case '*':
 			if (s[1] == '\0')
 				escflags |= ESC_FON;
 			break;
 		case '[':
 			escflags |= ESC_SQU;
 			break;
 		case ']':
 			escflags |= ESC_HYP;
 			escflags &= ~ESC_SQU;
 			break;
 		default:
 			break;
 		}
 		md_char(*s++);
 	}
 	if (s[-1] == ' ')
 		escflags |= ESC_EOL;
 	else
 		escflags &= ~ESC_EOL;
 }
 
 /*
  * Print text and mdoc(7) syntax elements.
  */
 static void
 md_word(const char *s)
 {
 	const char	*seq, *prevfont, *currfont, *nextfont;
 	char		 c;
 	int		 bs, sz, uc, breakline;
 
 	/* No spacing before closing delimiters. */
 	if (s[0] != '\0' && s[1] == '\0' &&
 	    strchr("!),.:;?]", s[0]) != NULL &&
 	    (outflags & MD_spc_force) == 0)
 		outflags &= ~MD_spc;
 
 	md_preword();
 
 	if (*s == '\0')
 		return;
 
 	/* No spacing after opening delimiters. */
 	if ((s[0] == '(' || s[0] == '[') && s[1] == '\0')
 		outflags &= ~MD_spc;
 
 	breakline = 0;
 	prevfont = currfont = "";
 	while ((c = *s++) != '\0') {
 		bs = 0;
 		switch(c) {
 		case ASCII_NBRSP:
 			if (code_blocks)
 				c = ' ';
 			else {
 				md_named("nbsp");
 				c = '\0';
 			}
 			break;
 		case ASCII_HYPH:
 			bs = escflags & ESC_BOL && !code_blocks;
 			c = '-';
 			break;
 		case ASCII_BREAK:
 			continue;
 		case '#':
 		case '+':
 		case '-':
 			bs = escflags & ESC_BOL && !code_blocks;
 			break;
 		case '(':
 			bs = escflags & ESC_HYP && !code_blocks;
 			break;
 		case ')':
 			bs = escflags & ESC_NUM && !code_blocks;
 			break;
 		case '*':
 		case '[':
 		case '_':
 		case '`':
 			bs = !code_blocks;
 			break;
 		case '.':
 			bs = escflags & ESC_NUM && !code_blocks;
 			break;
 		case '<':
 			if (code_blocks == 0) {
 				md_named("lt");
 				c = '\0';
 			}
 			break;
 		case '=':
 			if (escflags & ESC_BOL && !code_blocks) {
 				md_named("equals");
 				c = '\0';
 			}
 			break;
 		case '>':
 			if (code_blocks == 0) {
 				md_named("gt");
 				c = '\0';
 			}
 			break;
 		case '\\':
 			uc = 0;
 			nextfont = NULL;
 			switch (mandoc_escape(&s, &seq, &sz)) {
 			case ESCAPE_UNICODE:
 				uc = mchars_num2uc(seq + 1, sz - 1);
 				break;
 			case ESCAPE_NUMBERED:
 				uc = mchars_num2char(seq, sz);
 				break;
 			case ESCAPE_SPECIAL:
 				uc = mchars_spec2cp(seq, sz);
 				break;
 			case ESCAPE_UNDEF:
 				uc = *seq;
 				break;
 			case ESCAPE_DEVICE:
 				md_rawword("markdown");
 				continue;
 			case ESCAPE_FONTBOLD:
 			case ESCAPE_FONTCB:
 				nextfont = "**";
 				break;
 			case ESCAPE_FONTITALIC:
 			case ESCAPE_FONTCI:
 				nextfont = "*";
 				break;
 			case ESCAPE_FONTBI:
 				nextfont = "***";
 				break;
 			case ESCAPE_FONT:
 			case ESCAPE_FONTCR:
 			case ESCAPE_FONTROMAN:
 				nextfont = "";
 				break;
 			case ESCAPE_FONTPREV:
 				nextfont = prevfont;
 				break;
 			case ESCAPE_BREAK:
 				breakline = 1;
 				break;
 			case ESCAPE_NOSPACE:
 			case ESCAPE_SKIPCHAR:
 			case ESCAPE_OVERSTRIKE:
 				/* XXX not implemented */
 				/* FALLTHROUGH */
 			case ESCAPE_ERROR:
 			default:
 				break;
 			}
 			if (nextfont != NULL && !code_blocks) {
 				if (*currfont != '\0') {
 					outflags &= ~MD_spc;
 					md_rawword(currfont);
 				}
 				prevfont = currfont;
 				currfont = nextfont;
 				if (*currfont != '\0') {
 					outflags &= ~MD_spc;
 					md_rawword(currfont);
 				}
 			}
 			if (uc) {
 				if ((uc < 0x20 && uc != 0x09) ||
 				    (uc > 0x7E && uc < 0xA0))
 					uc = 0xFFFD;
 				if (code_blocks) {
 					seq = mchars_uc2str(uc);
 					fputs(seq, stdout);
 					outcount += strlen(seq);
 				} else {
 					printf("&#%d;", uc);
 					outcount++;
 				}
 				escflags &= ~ESC_FON;
 			}
 			c = '\0';
 			break;
 		case ']':
 			bs = escflags & ESC_SQU && !code_blocks;
 			escflags |= ESC_HYP;
 			break;
 		default:
 			break;
 		}
 		if (bs)
 			putchar('\\');
 		md_char(c);
 		if (breakline &&
 		    (*s == '\0' || *s == ' ' || *s == ASCII_NBRSP)) {
 			printf("  \n");
 			breakline = 0;
 			while (*s == ' ' || *s == ASCII_NBRSP)
 				s++;
 		}
 	}
 	if (*currfont != '\0') {
 		outflags &= ~MD_spc;
 		md_rawword(currfont);
 	} else if (s[-2] == ' ')
 		escflags |= ESC_EOL;
 	else
 		escflags &= ~ESC_EOL;
 }
 
 /*
  * Print a single HTML named character reference.
  */
 static void
 md_named(const char *s)
 {
 	printf("&%s;", s);
 	escflags &= ~(ESC_FON | ESC_EOL);
 	outcount++;
 }
 
 /*
  * Print a single raw character and maintain certain escape flags.
  */
 static void
 md_char(unsigned char c)
 {
 	if (c != '\0') {
 		putchar(c);
 		if (c == '*')
 			escflags |= ESC_FON;
 		else
 			escflags &= ~ESC_FON;
 		outcount++;
 	}
 	if (c != ']')
 		escflags &= ~ESC_HYP;
 	if (c == ' ' || c == '\t' || c == '>')
 		return;
 	if (isdigit(c) == 0)
 		escflags &= ~ESC_NUM;
 	else if (escflags & ESC_BOL)
 		escflags |= ESC_NUM;
 	escflags &= ~ESC_BOL;
 }
 
 static int
 md_cond_head(struct roff_node *n)
 {
 	return n->type == ROFFT_HEAD;
 }
 
 static int
 md_cond_body(struct roff_node *n)
 {
 	return n->type == ROFFT_BODY;
 }
 
 static int
 md_pre_abort(struct roff_node *n)
 {
 	abort();
 }
 
 static int
 md_pre_raw(struct roff_node *n)
 {
 	const char	*prefix;
 
 	if ((prefix = md_act(n->tok)->prefix) != NULL) {
 		md_rawword(prefix);
 		outflags &= ~MD_spc;
 		if (strchr(prefix, '`') != NULL)
 			code_blocks++;
 	}
 	return 1;
 }
 
 static void
 md_post_raw(struct roff_node *n)
 {
 	const char	*suffix;
 
 	if ((suffix = md_act(n->tok)->suffix) != NULL) {
 		outflags &= ~(MD_spc | MD_nl);
 		md_rawword(suffix);
 		if (strchr(suffix, '`') != NULL)
 			code_blocks--;
 	}
 }
 
 static int
 md_pre_word(struct roff_node *n)
 {
 	const char	*prefix;
 
 	if ((prefix = md_act(n->tok)->prefix) != NULL) {
 		md_word(prefix);
 		outflags &= ~MD_spc;
 	}
 	return 1;
 }
 
 static void
 md_post_word(struct roff_node *n)
 {
 	const char	*suffix;
 
 	if ((suffix = md_act(n->tok)->suffix) != NULL) {
 		outflags &= ~(MD_spc | MD_nl);
 		md_word(suffix);
 	}
 }
 
 static void
 md_post_pc(struct roff_node *n)
 {
 	struct roff_node *nn;
 
 	md_post_raw(n);
 	if (n->parent->tok != MDOC_Rs)
 		return;
 
 	if ((nn = roff_node_next(n)) != NULL) {
 		md_word(",");
 		if (nn->tok == n->tok &&
 		    (nn = roff_node_prev(n)) != NULL &&
 		    nn->tok == n->tok)
 			md_word("and");
 	} else {
 		md_word(".");
 		outflags |= MD_nl;
 	}
 }
 
 static int
 md_pre_skip(struct roff_node *n)
 {
 	return 0;
 }
 
 static void
 md_pre_syn(struct roff_node *n)
 {
 	struct roff_node *np;
 
 	if ((n->flags & NODE_SYNPRETTY) == 0 ||
 	    (np = roff_node_prev(n)) == NULL)
 		return;
 
 	if (np->tok == n->tok &&
 	    n->tok != MDOC_Ft &&
 	    n->tok != MDOC_Fo &&
 	    n->tok != MDOC_Fn) {
 		outflags |= MD_br;
 		return;
 	}
 
 	switch (np->tok) {
 	case MDOC_Fd:
 	case MDOC_Fn:
 	case MDOC_Fo:
 	case MDOC_In:
 	case MDOC_Vt:
 		outflags |= MD_sp;
 		break;
 	case MDOC_Ft:
 		if (n->tok != MDOC_Fn && n->tok != MDOC_Fo) {
 			outflags |= MD_sp;
 			break;
 		}
 		/* FALLTHROUGH */
 	default:
 		outflags |= MD_br;
 		break;
 	}
 }
 
 static int
 md_pre_An(struct roff_node *n)
 {
 	switch (n->norm->An.auth) {
 	case AUTH_split:
 		outflags &= ~MD_An_nosplit;
 		outflags |= MD_An_split;
 		return 0;
 	case AUTH_nosplit:
 		outflags &= ~MD_An_split;
 		outflags |= MD_An_nosplit;
 		return 0;
 	default:
 		if (outflags & MD_An_split)
 			outflags |= MD_br;
 		else if (n->sec == SEC_AUTHORS &&
 		    ! (outflags & MD_An_nosplit))
 			outflags |= MD_An_split;
 		return 1;
 	}
 }
 
 static int
 md_pre_Ap(struct roff_node *n)
 {
 	outflags &= ~MD_spc;
 	md_word("'");
 	outflags &= ~MD_spc;
 	return 0;
 }
 
 static int
 md_pre_Bd(struct roff_node *n)
 {
 	switch (n->norm->Bd.type) {
 	case DISP_unfilled:
 	case DISP_literal:
 		return md_pre_Dl(n);
 	default:
 		return md_pre_D1(n);
 	}
 }
 
 static int
 md_pre_Bk(struct roff_node *n)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		return 1;
 	case ROFFT_BODY:
 		outflags |= MD_Bk;
 		return 1;
 	default:
 		return 0;
 	}
 }
 
 static void
 md_post_Bk(struct roff_node *n)
 {
 	if (n->type == ROFFT_BODY)
 		outflags &= ~MD_Bk;
 }
 
 static int
 md_pre_Bl(struct roff_node *n)
 {
 	n->norm->Bl.count = 0;
 	if (n->norm->Bl.type == LIST_column)
 		md_pre_Dl(n);
 	outflags |= MD_sp;
 	return 1;
 }
 
 static void
 md_post_Bl(struct roff_node *n)
 {
 	n->norm->Bl.count = 0;
 	if (n->norm->Bl.type == LIST_column)
 		md_post_D1(n);
 	outflags |= MD_sp;
 }
 
 static int
 md_pre_D1(struct roff_node *n)
 {
 	/*
 	 * Markdown blockquote syntax does not work inside code blocks.
 	 * The best we can do is fall back to another nested code block.
 	 */
 	if (code_blocks) {
 		md_stack('\t');
 		code_blocks++;
 	} else {
 		md_stack('>');
 		quote_blocks++;
 	}
 	outflags |= MD_sp;
 	return 1;
 }
 
 static void
 md_post_D1(struct roff_node *n)
 {
 	md_stack((char)-1);
 	if (code_blocks)
 		code_blocks--;
 	else
 		quote_blocks--;
 	outflags |= MD_sp;
 }
 
 static int
 md_pre_Dl(struct roff_node *n)
 {
 	/*
 	 * Markdown code block syntax does not work inside blockquotes.
 	 * The best we can do is fall back to another nested blockquote.
 	 */
 	if (quote_blocks) {
 		md_stack('>');
 		quote_blocks++;
 	} else {
 		md_stack('\t');
 		code_blocks++;
 	}
 	outflags |= MD_sp;
 	return 1;
 }
 
 static int
 md_pre_En(struct roff_node *n)
 {
 	if (n->norm->Es == NULL ||
 	    n->norm->Es->child == NULL)
 		return 1;
 
 	md_word(n->norm->Es->child->string);
 	outflags &= ~MD_spc;
 	return 1;
 }
 
 static void
 md_post_En(struct roff_node *n)
 {
 	if (n->norm->Es == NULL ||
 	    n->norm->Es->child == NULL ||
 	    n->norm->Es->child->next == NULL)
 		return;
 
 	outflags &= ~MD_spc;
 	md_word(n->norm->Es->child->next->string);
 }
 
 static int
 md_pre_Eo(struct roff_node *n)
 {
 	if (n->end == ENDBODY_NOT &&
 	    n->parent->head->child == NULL &&
 	    n->child != NULL &&
 	    n->child->end != ENDBODY_NOT)
 		md_preword();
 	else if (n->end != ENDBODY_NOT ? n->child != NULL :
 	    n->parent->head->child != NULL && (n->child != NULL ||
 	    (n->parent->tail != NULL && n->parent->tail->child != NULL)))
 		outflags &= ~(MD_spc | MD_nl);
 	return 1;
 }
 
 static void
 md_post_Eo(struct roff_node *n)
 {
 	if (n->end != ENDBODY_NOT) {
 		outflags |= MD_spc;
 		return;
 	}
 
 	if (n->child == NULL && n->parent->head->child == NULL)
 		return;
 
 	if (n->parent->tail != NULL && n->parent->tail->child != NULL)
 		outflags &= ~MD_spc;
         else
 		outflags |= MD_spc;
 }
 
 static int
 md_pre_Fa(struct roff_node *n)
 {
 	int	 am_Fa;
 
 	am_Fa = n->tok == MDOC_Fa;
 
 	if (am_Fa)
 		n = n->child;
 
 	while (n != NULL) {
 		md_rawword("*");
 		outflags &= ~MD_spc;
 		md_node(n);
 		outflags &= ~MD_spc;
 		md_rawword("*");
 		if ((n = n->next) != NULL)
 			md_word(",");
 	}
 	return 0;
 }
 
 static void
 md_post_Fa(struct roff_node *n)
 {
 	struct roff_node *nn;
 
 	if ((nn = roff_node_next(n)) != NULL && nn->tok == MDOC_Fa)
 		md_word(",");
 }
 
 static int
 md_pre_Fd(struct roff_node *n)
 {
 	md_pre_syn(n);
 	md_pre_raw(n);
 	return 1;
 }
 
 static void
 md_post_Fd(struct roff_node *n)
 {
 	md_post_raw(n);
 	outflags |= MD_br;
 }
 
 static void
 md_post_Fl(struct roff_node *n)
 {
 	struct roff_node *nn;
 
 	md_post_raw(n);
 	if (n->child == NULL && (nn = roff_node_next(n)) != NULL &&
 	    nn->type != ROFFT_TEXT && (nn->flags & NODE_LINE) == 0)
 		outflags &= ~MD_spc;
 }
 
 static int
 md_pre_Fn(struct roff_node *n)
 {
 	md_pre_syn(n);
 
 	if ((n = n->child) == NULL)
 		return 0;
 
 	md_rawword("**");
 	outflags &= ~MD_spc;
 	md_node(n);
 	outflags &= ~MD_spc;
 	md_rawword("**");
 	outflags &= ~MD_spc;
 	md_word("(");
 
 	if ((n = n->next) != NULL)
 		md_pre_Fa(n);
 	return 0;
 }
 
 static void
 md_post_Fn(struct roff_node *n)
 {
 	md_word(")");
 	if (n->flags & NODE_SYNPRETTY) {
 		md_word(";");
 		outflags |= MD_sp;
 	}
 }
 
 static int
 md_pre_Fo(struct roff_node *n)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		md_pre_syn(n);
 		break;
 	case ROFFT_HEAD:
 		if (n->child == NULL)
 			return 0;
 		md_pre_raw(n);
 		break;
 	case ROFFT_BODY:
 		outflags &= ~(MD_spc | MD_nl);
 		md_word("(");
 		break;
 	default:
 		break;
 	}
 	return 1;
 }
 
 static void
 md_post_Fo(struct roff_node *n)
 {
 	switch (n->type) {
 	case ROFFT_HEAD:
 		if (n->child != NULL)
 			md_post_raw(n);
 		break;
 	case ROFFT_BODY:
 		md_post_Fn(n);
 		break;
 	default:
 		break;
 	}
 }
 
 static int
 md_pre_In(struct roff_node *n)
 {
 	if (n->flags & NODE_SYNPRETTY) {
 		md_pre_syn(n);
 		md_rawword("**");
 		outflags &= ~MD_spc;
 		md_word("#include <");
 	} else {
 		md_word("<");
 		outflags &= ~MD_spc;
 		md_rawword("*");
 	}
 	outflags &= ~MD_spc;
 	return 1;
 }
 
 static void
 md_post_In(struct roff_node *n)
 {
 	if (n->flags & NODE_SYNPRETTY) {
 		outflags &= ~MD_spc;
 		md_rawword(">**");
 		outflags |= MD_nl;
 	} else {
 		outflags &= ~MD_spc;
 		md_rawword("*>");
 	}
 }
 
 static int
 md_pre_It(struct roff_node *n)
 {
 	struct roff_node	*bln;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		return 1;
 
 	case ROFFT_HEAD:
 		bln = n->parent->parent;
 		if (bln->norm->Bl.comp == 0 &&
 		    bln->norm->Bl.type != LIST_column)
 			outflags |= MD_sp;
 		outflags |= MD_nl;
 
 		switch (bln->norm->Bl.type) {
 		case LIST_item:
 			outflags |= MD_br;
 			return 0;
 		case LIST_inset:
 		case LIST_diag:
 		case LIST_ohang:
 			outflags |= MD_br;
 			return 1;
 		case LIST_tag:
 		case LIST_hang:
 			outflags |= MD_sp;
 			return 1;
 		case LIST_bullet:
 			md_rawword("*\t");
 			break;
 		case LIST_dash:
 		case LIST_hyphen:
 			md_rawword("-\t");
 			break;
 		case LIST_enum:
 			md_preword();
 			if (bln->norm->Bl.count < 99)
 				bln->norm->Bl.count++;
 			printf("%d.\t", bln->norm->Bl.count);
 			escflags &= ~ESC_FON;
 			break;
 		case LIST_column:
 			outflags |= MD_br;
 			return 0;
 		default:
 			return 0;
 		}
 		outflags &= ~MD_spc;
 		outflags |= MD_nonl;
 		outcount = 0;
 		md_stack('\t');
 		if (code_blocks || quote_blocks)
 			list_blocks++;
 		return 0;
 
 	case ROFFT_BODY:
 		bln = n->parent->parent;
 		switch (bln->norm->Bl.type) {
 		case LIST_ohang:
 			outflags |= MD_br;
 			break;
 		case LIST_tag:
 		case LIST_hang:
 			md_pre_D1(n);
 			break;
 		default:
 			break;
 		}
 		return 1;
 
 	default:
 		return 0;
 	}
 }
 
 static void
 md_post_It(struct roff_node *n)
 {
 	struct roff_node	*bln;
 	int			 i, nc;
 
 	if (n->type != ROFFT_BODY)
 		return;
 
 	bln = n->parent->parent;
 	switch (bln->norm->Bl.type) {
 	case LIST_bullet:
 	case LIST_dash:
 	case LIST_hyphen:
 	case LIST_enum:
 		md_stack((char)-1);
 		if (code_blocks || quote_blocks)
 			list_blocks--;
 		break;
 	case LIST_tag:
 	case LIST_hang:
 		md_post_D1(n);
 		break;
 
 	case LIST_column:
 		if (n->next == NULL)
 			break;
 
 		/* Calculate the array index of the current column. */
 
 		i = 0;
 		while ((n = n->prev) != NULL && n->type != ROFFT_HEAD)
 			i++;
 
 		/*
 		 * If a width was specified for this column,
 		 * subtract what printed, and
 		 * add the same spacing as in mdoc_term.c.
 		 */
 
 		nc = bln->norm->Bl.ncols;
 		i = i < nc ? strlen(bln->norm->Bl.cols[i]) - outcount +
 		    (nc < 5 ? 4 : nc == 5 ? 3 : 1) : 1;
 		if (i < 1)
 			i = 1;
 		while (i-- > 0)
 			putchar(' ');
 
 		outflags &= ~MD_spc;
 		escflags &= ~ESC_FON;
 		outcount = 0;
 		break;
 
 	default:
 		break;
 	}
 }
 
 static void
 md_post_Lb(struct roff_node *n)
 {
 	if (n->sec == SEC_LIBRARY)
 		outflags |= MD_br;
 }
 
 static void
 md_uri(const char *s)
 {
 	while (*s != '\0') {
 		if (strchr("%()<>", *s) != NULL) {
 			printf("%%%2.2hhX", *s);
 			outcount += 3;
 		} else {
 			putchar(*s);
 			outcount++;
 		}
 		s++;
 	}
 }
 
 static int
 md_pre_Lk(struct roff_node *n)
 {
 	const struct roff_node *link, *descr, *punct;
 
 	if ((link = n->child) == NULL)
 		return 0;
 
 	/* Find beginning of trailing punctuation. */
 	punct = n->last;
 	while (punct != link && punct->flags & NODE_DELIMC)
 		punct = punct->prev;
 	punct = punct->next;
 
 	/* Link text. */
 	descr = link->next;
 	if (descr == punct)
 		descr = link;  /* no text */
 	md_rawword("[");
 	outflags &= ~MD_spc;
 	do {
 		md_word(descr->string);
 		descr = descr->next;
 	} while (descr != punct);
 	outflags &= ~MD_spc;
 
 	/* Link target. */
 	md_rawword("](");
 	md_uri(link->string);
 	outflags &= ~MD_spc;
 	md_rawword(")");
 
 	/* Trailing punctuation. */
 	while (punct != NULL) {
 		md_word(punct->string);
 		punct = punct->next;
 	}
 	return 0;
 }
 
 static int
 md_pre_Mt(struct roff_node *n)
 {
 	const struct roff_node *nch;
 
 	md_rawword("[");
 	outflags &= ~MD_spc;
 	for (nch = n->child; nch != NULL; nch = nch->next)
 		md_word(nch->string);
 	outflags &= ~MD_spc;
 	md_rawword("](mailto:");
 	for (nch = n->child; nch != NULL; nch = nch->next) {
 		md_uri(nch->string);
 		if (nch->next != NULL) {
 			putchar(' ');
 			outcount++;
 		}
 	}
 	outflags &= ~MD_spc;
 	md_rawword(")");
 	return 0;
 }
 
 static int
 md_pre_Nd(struct roff_node *n)
 {
 	outflags &= ~MD_nl;
 	outflags |= MD_spc;
 	md_word("-");
 	return 1;
 }
 
 static int
 md_pre_Nm(struct roff_node *n)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		outflags |= MD_Bk;
 		md_pre_syn(n);
 		break;
 	case ROFFT_HEAD:
 	case ROFFT_ELEM:
 		md_pre_raw(n);
 		break;
 	default:
 		break;
 	}
 	return 1;
 }
 
 static void
 md_post_Nm(struct roff_node *n)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		outflags &= ~MD_Bk;
 		break;
 	case ROFFT_HEAD:
 	case ROFFT_ELEM:
 		md_post_raw(n);
 		break;
 	default:
 		break;
 	}
 }
 
 static int
 md_pre_No(struct roff_node *n)
 {
 	outflags |= MD_spc_force;
 	return 1;
 }
 
 static int
 md_pre_Ns(struct roff_node *n)
 {
 	outflags &= ~MD_spc;
 	return 0;
 }
 
 static void
 md_post_Pf(struct roff_node *n)
 {
 	if (n->next != NULL && (n->next->flags & NODE_LINE) == 0)
 		outflags &= ~MD_spc;
 }
 
 static int
 md_pre_Pp(struct roff_node *n)
 {
 	outflags |= MD_sp;
 	return 0;
 }
 
 static int
 md_pre_Rs(struct roff_node *n)
 {
 	if (n->sec == SEC_SEE_ALSO)
 		outflags |= MD_sp;
 	return 1;
 }
 
 static int
 md_pre_Sh(struct roff_node *n)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		if (n->sec == SEC_AUTHORS)
 			outflags &= ~(MD_An_split | MD_An_nosplit);
 		break;
 	case ROFFT_HEAD:
 		outflags |= MD_sp;
 		md_rawword(n->tok == MDOC_Sh ? "#" : "##");
 		break;
 	case ROFFT_BODY:
 		outflags |= MD_sp;
 		break;
 	default:
 		break;
 	}
 	return 1;
 }
 
 static int
 md_pre_Sm(struct roff_node *n)
 {
 	if (n->child == NULL)
 		outflags ^= MD_Sm;
 	else if (strcmp("on", n->child->string) == 0)
 		outflags |= MD_Sm;
 	else
 		outflags &= ~MD_Sm;
 
 	if (outflags & MD_Sm)
 		outflags |= MD_spc;
 
 	return 0;
 }
 
 static int
 md_pre_Vt(struct roff_node *n)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		md_pre_syn(n);
 		return 1;
 	case ROFFT_BODY:
 	case ROFFT_ELEM:
 		md_pre_raw(n);
 		return 1;
 	default:
 		return 0;
 	}
 }
 
 static void
 md_post_Vt(struct roff_node *n)
 {
 	switch (n->type) {
 	case ROFFT_BODY:
 	case ROFFT_ELEM:
 		md_post_raw(n);
 		break;
 	default:
 		break;
 	}
 }
 
 static int
 md_pre_Xr(struct roff_node *n)
 {
 	n = n->child;
 	if (n == NULL)
 		return 0;
 	md_node(n);
 	n = n->next;
 	if (n == NULL)
 		return 0;
 	outflags &= ~MD_spc;
 	md_word("(");
 	md_node(n);
 	md_word(")");
 	return 0;
 }
 
 static int
 md_pre__R(struct roff_node *n)
 {
 	const unsigned char	*cp;
 	const char		*arg;
 
 	arg = n->child->string;
 
 	if (strncmp(arg, "RFC ", 4) != 0)
 		return 1;
 	cp = arg += 4;
 	while (isdigit(*cp))
 		cp++;
 	if (*cp != '\0')
 		return 1;
 
 	md_rawword("[RFC ");
 	outflags &= ~MD_spc;
 	md_rawword(arg);
 	outflags &= ~MD_spc;
 	md_rawword("](http://www.rfc-editor.org/rfc/rfc");
 	outflags &= ~MD_spc;
 	md_rawword(arg);
 	outflags &= ~MD_spc;
 	md_rawword(".html)");
 	return 0;
 }
 
 static int
 md_pre__T(struct roff_node *n)
 {
 	if (n->parent->tok == MDOC_Rs && n->parent->norm->Rs.quote_T)
 		md_word("\"");
 	else
 		md_rawword("*");
 	outflags &= ~MD_spc;
 	return 1;
 }
 
 static void
 md_post__T(struct roff_node *n)
 {
 	outflags &= ~MD_spc;
 	if (n->parent->tok == MDOC_Rs && n->parent->norm->Rs.quote_T)
 		md_word("\"");
 	else
 		md_rawword("*");
 	md_post_pc(n);
 }
 
 static int
 md_pre_br(struct roff_node *n)
 {
 	outflags |= MD_br;
 	return 0;
 }
diff --git a/contrib/mandoc/mdoc_term.c b/contrib/mandoc/mdoc_term.c
index 931bc384a002..b0544de0304e 100644
--- a/contrib/mandoc/mdoc_term.c
+++ b/contrib/mandoc/mdoc_term.c
@@ -1,1963 +1,1977 @@
-/* $Id: mdoc_term.c,v 1.383 2023/11/13 19:13:01 schwarze Exp $ */
+/* $Id: mdoc_term.c,v 1.387 2025/07/27 15:27:28 schwarze Exp $ */
 /*
- * Copyright (c) 2010, 2012-2020, 2022 Ingo Schwarze 
+ * Copyright (c) 2010,2012-2020,2022,2025 Ingo Schwarze 
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons 
  * Copyright (c) 2013 Franco Fichtner 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
  * Plain text formatter for mdoc(7), used by mandoc(1)
  * for ASCII, UTF-8, PostScript, and PDF output.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "roff.h"
 #include "mdoc.h"
 #include "out.h"
 #include "term.h"
 #include "term_tag.h"
 #include "main.h"
 
 struct	termpair {
 	struct termpair	 *ppair;
 	int		  count;
 };
 
 #define	DECL_ARGS struct termp *p, \
 		  struct termpair *pair, \
 		  const struct roff_meta *meta, \
 		  struct roff_node *n
 
 struct	mdoc_term_act {
 	int	(*pre)(DECL_ARGS);
 	void	(*post)(DECL_ARGS);
 };
 
 static	int	  a2width(const struct termp *, const char *);
 
 static	void	  print_bvspace(struct termp *,
 			struct roff_node *, struct roff_node *);
 static	void	  print_mdoc_node(DECL_ARGS);
 static	void	  print_mdoc_nodelist(DECL_ARGS);
 static	void	  print_mdoc_head(struct termp *, const struct roff_meta *);
 static	void	  print_mdoc_foot(struct termp *, const struct roff_meta *);
 static	void	  synopsis_pre(struct termp *, struct roff_node *);
 
 static	void	  termp____post(DECL_ARGS);
 static	void	  termp__t_post(DECL_ARGS);
 static	void	  termp_bd_post(DECL_ARGS);
 static	void	  termp_bk_post(DECL_ARGS);
 static	void	  termp_bl_post(DECL_ARGS);
 static	void	  termp_eo_post(DECL_ARGS);
 static	void	  termp_fd_post(DECL_ARGS);
 static	void	  termp_fo_post(DECL_ARGS);
 static	void	  termp_in_post(DECL_ARGS);
 static	void	  termp_it_post(DECL_ARGS);
 static	void	  termp_lb_post(DECL_ARGS);
 static	void	  termp_nm_post(DECL_ARGS);
 static	void	  termp_pf_post(DECL_ARGS);
 static	void	  termp_quote_post(DECL_ARGS);
 static	void	  termp_sh_post(DECL_ARGS);
 static	void	  termp_ss_post(DECL_ARGS);
 static	void	  termp_xx_post(DECL_ARGS);
 
 static	int	  termp__a_pre(DECL_ARGS);
 static	int	  termp__t_pre(DECL_ARGS);
 static	int	  termp_abort_pre(DECL_ARGS);
 static	int	  termp_an_pre(DECL_ARGS);
 static	int	  termp_ap_pre(DECL_ARGS);
 static	int	  termp_bd_pre(DECL_ARGS);
 static	int	  termp_bf_pre(DECL_ARGS);
 static	int	  termp_bk_pre(DECL_ARGS);
 static	int	  termp_bl_pre(DECL_ARGS);
 static	int	  termp_bold_pre(DECL_ARGS);
 static	int	  termp_d1_pre(DECL_ARGS);
 static	int	  termp_eo_pre(DECL_ARGS);
 static	int	  termp_ex_pre(DECL_ARGS);
 static	int	  termp_fa_pre(DECL_ARGS);
 static	int	  termp_fd_pre(DECL_ARGS);
 static	int	  termp_fl_pre(DECL_ARGS);
 static	int	  termp_fn_pre(DECL_ARGS);
 static	int	  termp_fo_pre(DECL_ARGS);
 static	int	  termp_ft_pre(DECL_ARGS);
 static	int	  termp_in_pre(DECL_ARGS);
 static	int	  termp_it_pre(DECL_ARGS);
 static	int	  termp_li_pre(DECL_ARGS);
 static	int	  termp_lk_pre(DECL_ARGS);
 static	int	  termp_nd_pre(DECL_ARGS);
 static	int	  termp_nm_pre(DECL_ARGS);
 static	int	  termp_ns_pre(DECL_ARGS);
 static	int	  termp_quote_pre(DECL_ARGS);
 static	int	  termp_rs_pre(DECL_ARGS);
 static	int	  termp_sh_pre(DECL_ARGS);
 static	int	  termp_skip_pre(DECL_ARGS);
 static	int	  termp_sm_pre(DECL_ARGS);
 static	int	  termp_pp_pre(DECL_ARGS);
 static	int	  termp_ss_pre(DECL_ARGS);
 static	int	  termp_under_pre(DECL_ARGS);
 static	int	  termp_vt_pre(DECL_ARGS);
 static	int	  termp_xr_pre(DECL_ARGS);
 static	int	  termp_xx_pre(DECL_ARGS);
 
 static const struct mdoc_term_act mdoc_term_acts[MDOC_MAX - MDOC_Dd] = {
 	{ NULL, NULL }, /* Dd */
 	{ NULL, NULL }, /* Dt */
 	{ NULL, NULL }, /* Os */
 	{ termp_sh_pre, termp_sh_post }, /* Sh */
 	{ termp_ss_pre, termp_ss_post }, /* Ss */
 	{ termp_pp_pre, NULL }, /* Pp */
 	{ termp_d1_pre, termp_bl_post }, /* D1 */
 	{ termp_d1_pre, termp_bl_post }, /* Dl */
 	{ termp_bd_pre, termp_bd_post }, /* Bd */
 	{ NULL, NULL }, /* Ed */
 	{ termp_bl_pre, termp_bl_post }, /* Bl */
 	{ NULL, NULL }, /* El */
 	{ termp_it_pre, termp_it_post }, /* It */
 	{ termp_under_pre, NULL }, /* Ad */
 	{ termp_an_pre, NULL }, /* An */
 	{ termp_ap_pre, NULL }, /* Ap */
 	{ termp_under_pre, NULL }, /* Ar */
 	{ termp_fd_pre, NULL }, /* Cd */
 	{ termp_bold_pre, NULL }, /* Cm */
 	{ termp_li_pre, NULL }, /* Dv */
 	{ NULL, NULL }, /* Er */
 	{ NULL, NULL }, /* Ev */
 	{ termp_ex_pre, NULL }, /* Ex */
 	{ termp_fa_pre, NULL }, /* Fa */
 	{ termp_fd_pre, termp_fd_post }, /* Fd */
 	{ termp_fl_pre, NULL }, /* Fl */
 	{ termp_fn_pre, NULL }, /* Fn */
 	{ termp_ft_pre, NULL }, /* Ft */
 	{ termp_bold_pre, NULL }, /* Ic */
 	{ termp_in_pre, termp_in_post }, /* In */
 	{ termp_li_pre, NULL }, /* Li */
 	{ termp_nd_pre, NULL }, /* Nd */
 	{ termp_nm_pre, termp_nm_post }, /* Nm */
 	{ termp_quote_pre, termp_quote_post }, /* Op */
 	{ termp_abort_pre, NULL }, /* Ot */
 	{ termp_under_pre, NULL }, /* Pa */
 	{ termp_ex_pre, NULL }, /* Rv */
 	{ NULL, NULL }, /* St */
 	{ termp_under_pre, NULL }, /* Va */
 	{ termp_vt_pre, NULL }, /* Vt */
 	{ termp_xr_pre, NULL }, /* Xr */
 	{ termp__a_pre, termp____post }, /* %A */
 	{ termp_under_pre, termp____post }, /* %B */
 	{ NULL, termp____post }, /* %D */
 	{ termp_under_pre, termp____post }, /* %I */
 	{ termp_under_pre, termp____post }, /* %J */
 	{ NULL, termp____post }, /* %N */
 	{ NULL, termp____post }, /* %O */
 	{ NULL, termp____post }, /* %P */
 	{ NULL, termp____post }, /* %R */
 	{ termp__t_pre, termp__t_post }, /* %T */
 	{ NULL, termp____post }, /* %V */
 	{ NULL, NULL }, /* Ac */
 	{ termp_quote_pre, termp_quote_post }, /* Ao */
 	{ termp_quote_pre, termp_quote_post }, /* Aq */
 	{ NULL, NULL }, /* At */
 	{ NULL, NULL }, /* Bc */
 	{ termp_bf_pre, NULL }, /* Bf */
 	{ termp_quote_pre, termp_quote_post }, /* Bo */
 	{ termp_quote_pre, termp_quote_post }, /* Bq */
 	{ termp_xx_pre, termp_xx_post }, /* Bsx */
 	{ NULL, NULL }, /* Bx */
 	{ termp_skip_pre, NULL }, /* Db */
 	{ NULL, NULL }, /* Dc */
 	{ termp_quote_pre, termp_quote_post }, /* Do */
 	{ termp_quote_pre, termp_quote_post }, /* Dq */
 	{ NULL, NULL }, /* Ec */ /* FIXME: no space */
 	{ NULL, NULL }, /* Ef */
 	{ termp_under_pre, NULL }, /* Em */
 	{ termp_eo_pre, termp_eo_post }, /* Eo */
 	{ termp_xx_pre, termp_xx_post }, /* Fx */
 	{ termp_bold_pre, NULL }, /* Ms */
 	{ termp_li_pre, NULL }, /* No */
 	{ termp_ns_pre, NULL }, /* Ns */
 	{ termp_xx_pre, termp_xx_post }, /* Nx */
 	{ termp_xx_pre, termp_xx_post }, /* Ox */
 	{ NULL, NULL }, /* Pc */
 	{ NULL, termp_pf_post }, /* Pf */
 	{ termp_quote_pre, termp_quote_post }, /* Po */
 	{ termp_quote_pre, termp_quote_post }, /* Pq */
 	{ NULL, NULL }, /* Qc */
 	{ termp_quote_pre, termp_quote_post }, /* Ql */
 	{ termp_quote_pre, termp_quote_post }, /* Qo */
 	{ termp_quote_pre, termp_quote_post }, /* Qq */
 	{ NULL, NULL }, /* Re */
 	{ termp_rs_pre, NULL }, /* Rs */
 	{ NULL, NULL }, /* Sc */
 	{ termp_quote_pre, termp_quote_post }, /* So */
 	{ termp_quote_pre, termp_quote_post }, /* Sq */
 	{ termp_sm_pre, NULL }, /* Sm */
 	{ termp_under_pre, NULL }, /* Sx */
 	{ termp_bold_pre, NULL }, /* Sy */
 	{ NULL, NULL }, /* Tn */
 	{ termp_xx_pre, termp_xx_post }, /* Ux */
 	{ NULL, NULL }, /* Xc */
 	{ NULL, NULL }, /* Xo */
 	{ termp_fo_pre, termp_fo_post }, /* Fo */
 	{ NULL, NULL }, /* Fc */
 	{ termp_quote_pre, termp_quote_post }, /* Oo */
 	{ NULL, NULL }, /* Oc */
 	{ termp_bk_pre, termp_bk_post }, /* Bk */
 	{ NULL, NULL }, /* Ek */
 	{ NULL, NULL }, /* Bt */
 	{ NULL, NULL }, /* Hf */
 	{ termp_under_pre, NULL }, /* Fr */
 	{ NULL, NULL }, /* Ud */
 	{ NULL, termp_lb_post }, /* Lb */
 	{ termp_abort_pre, NULL }, /* Lp */
 	{ termp_lk_pre, NULL }, /* Lk */
 	{ termp_under_pre, NULL }, /* Mt */
 	{ termp_quote_pre, termp_quote_post }, /* Brq */
 	{ termp_quote_pre, termp_quote_post }, /* Bro */
 	{ NULL, NULL }, /* Brc */
 	{ NULL, termp____post }, /* %C */
 	{ termp_skip_pre, NULL }, /* Es */
 	{ termp_quote_pre, termp_quote_post }, /* En */
 	{ termp_xx_pre, termp_xx_post }, /* Dx */
 	{ NULL, termp____post }, /* %Q */
 	{ NULL, termp____post }, /* %U */
 	{ NULL, NULL }, /* Ta */
 	{ termp_skip_pre, NULL }, /* Tg */
 };
 
 
 void
 terminal_mdoc(void *arg, const struct roff_meta *mdoc)
 {
 	struct roff_node	*n, *nn;
 	struct termp		*p;
 
 	p = (struct termp *)arg;
 	p->tcol->rmargin = p->maxrmargin = p->defrmargin;
 	term_tab_set(p, NULL);
 	term_tab_set(p, "T");
 	term_tab_set(p, ".5i");
 
 	n = mdoc->first->child;
 	if (p->synopsisonly) {
 		for (nn = NULL; n != NULL; n = n->next) {
 			if (n->tok != MDOC_Sh)
 				continue;
 			if (n->sec == SEC_SYNOPSIS)
 				break;
 			if (nn == NULL && n->sec == SEC_NAME)
 				nn = n;
 		}
 		if (n == NULL)
 			n = nn;
 		p->flags |= TERMP_NOSPACE;
 		if (n != NULL && (n = n->child->next->child) != NULL)
 			print_mdoc_nodelist(p, NULL, mdoc, n);
 		term_newln(p);
 	} else {
 		term_begin(p, print_mdoc_head, print_mdoc_foot, mdoc);
 		while (n != NULL &&
 		    (n->type == ROFFT_COMMENT ||
 		     n->flags & NODE_NOPRT))
 			n = n->next;
 		if (n != NULL) {
 			if (n->tok != MDOC_Sh)
 				term_vspace(p);
 			print_mdoc_nodelist(p, NULL, mdoc, n);
 		}
 		term_end(p);
 	}
 }
 
 static void
 print_mdoc_nodelist(DECL_ARGS)
 {
 	while (n != NULL) {
 		print_mdoc_node(p, pair, meta, n);
 		n = n->next;
 	}
 }
 
 static void
 print_mdoc_node(DECL_ARGS)
 {
 	const struct mdoc_term_act *act;
 	struct termpair	 npair;
-	size_t		 offset, rmargin;
+	size_t		 offset, rmargin;  /* In basic units. */
 	int		 chld;
 
 	/*
 	 * In no-fill mode, break the output line at the beginning
 	 * of new input lines except after \c, and nowhere else.
 	 */
 
 	if (n->flags & NODE_NOFILL) {
 		if (n->flags & NODE_LINE &&
 		    (p->flags & TERMP_NONEWLINE) == 0)
 			term_newln(p);
 		p->flags |= TERMP_BRNEVER;
 	} else {
 		if (n->flags & NODE_LINE)
 			term_tab_ref(p);
 		p->flags &= ~TERMP_BRNEVER;
 	}
 
 	if (n->type == ROFFT_COMMENT || n->flags & NODE_NOPRT)
 		return;
 
 	chld = 1;
 	offset = p->tcol->offset;
 	rmargin = p->tcol->rmargin;
 	n->flags &= ~NODE_ENDED;
 	n->prev_font = p->fonti;
 
 	memset(&npair, 0, sizeof(struct termpair));
 	npair.ppair = pair;
 
 	if (n->flags & NODE_ID && n->tok != MDOC_Pp &&
 	    (n->tok != MDOC_It || n->type != ROFFT_BLOCK))
 		term_tag_write(n, p->line);
 
 	/*
 	 * Keeps only work until the end of a line.  If a keep was
 	 * invoked in a prior line, revert it to PREKEEP.
 	 */
 
 	if (p->flags & TERMP_KEEP && n->flags & NODE_LINE) {
 		p->flags &= ~TERMP_KEEP;
 		p->flags |= TERMP_PREKEEP;
 	}
 
 	/*
 	 * After the keep flags have been set up, we may now
 	 * produce output.  Note that some pre-handlers do so.
 	 */
 
 	act = NULL;
 	switch (n->type) {
 	case ROFFT_TEXT:
 		if (n->flags & NODE_LINE) {
 			switch (*n->string) {
 			case '\0':
 				if (p->flags & TERMP_NONEWLINE)
 					term_newln(p);
 				else
 					term_vspace(p);
 				return;
 			case ' ':
 				if ((p->flags & TERMP_NONEWLINE) == 0)
 					term_newln(p);
 				break;
 			default:
 				break;
 			}
 		}
 		if (NODE_DELIMC & n->flags)
 			p->flags |= TERMP_NOSPACE;
 		term_word(p, n->string);
 		if (NODE_DELIMO & n->flags)
 			p->flags |= TERMP_NOSPACE;
 		break;
 	case ROFFT_EQN:
 		if ( ! (n->flags & NODE_LINE))
 			p->flags |= TERMP_NOSPACE;
 		term_eqn(p, n->eqn);
 		if (n->next != NULL && ! (n->next->flags & NODE_LINE))
 			p->flags |= TERMP_NOSPACE;
 		break;
 	case ROFFT_TBL:
 		if (p->tbl.cols == NULL)
 			term_newln(p);
 		term_tbl(p, n->span);
 		break;
 	default:
 		if (n->tok < ROFF_MAX) {
 			roff_term_pre(p, n);
 			return;
 		}
 		assert(n->tok >= MDOC_Dd && n->tok < MDOC_MAX);
 		act = mdoc_term_acts + (n->tok - MDOC_Dd);
 		if (act->pre != NULL &&
 		    (n->end == ENDBODY_NOT || n->child != NULL))
 			chld = (*act->pre)(p, &npair, meta, n);
 		break;
 	}
 
 	if (chld && n->child)
 		print_mdoc_nodelist(p, &npair, meta, n->child);
 
 	term_fontpopq(p,
 	    (ENDBODY_NOT == n->end ? n : n->body)->prev_font);
 
 	switch (n->type) {
 	case ROFFT_TEXT:
 		break;
 	case ROFFT_TBL:
 		break;
 	case ROFFT_EQN:
 		break;
 	default:
 		if (act->post == NULL || n->flags & NODE_ENDED)
 			break;
 		(void)(*act->post)(p, &npair, meta, n);
 
 		/*
 		 * Explicit end tokens not only call the post
 		 * handler, but also tell the respective block
 		 * that it must not call the post handler again.
 		 */
 		if (ENDBODY_NOT != n->end)
 			n->body->flags |= NODE_ENDED;
 		break;
 	}
 
 	if (NODE_EOS & n->flags)
 		p->flags |= TERMP_SENTENCE;
 
 	if (n->type != ROFFT_TEXT)
 		p->tcol->offset = offset;
 	p->tcol->rmargin = rmargin;
 }
 
 static void
 print_mdoc_foot(struct termp *p, const struct roff_meta *meta)
 {
-	size_t sz;
+	char	*title;
+	size_t	 datelen, titlen;  /* In basic units. */
 
-	term_fontrepl(p, TERMFONT_NONE);
-
-	/*
-	 * Output the footer in new-groff style, that is, three columns
-	 * with the middle being the manual date and flanking columns
-	 * being the operating system:
-	 *
-	 * SYSTEM                  DATE                    SYSTEM
-	 */
+	assert(meta->title != NULL);
+	datelen = term_strlen(p, meta->date);
+	if (meta->msec == NULL)
+		title = mandoc_strdup(meta->title);
+	else
+		mandoc_asprintf(&title, "%s(%s)", meta->title, meta->msec);
+	titlen = term_strlen(p, title);
 
+	term_fontrepl(p, TERMFONT_NONE);
 	term_vspace(p);
 
+	/* Bottom left corner: operating system. */
+
 	p->tcol->offset = 0;
-	sz = term_strlen(p, meta->date);
-	p->tcol->rmargin = p->maxrmargin > sz ?
-	    (p->maxrmargin + term_len(p, 1) - sz) / 2 : 0;
+	p->tcol->rmargin = p->maxrmargin > datelen ?
+	    (p->maxrmargin + term_len(p, 1) - datelen) / 2 : 0;
 	p->trailspace = 1;
 	p->flags |= TERMP_NOSPACE | TERMP_NOBREAK;
 
 	term_word(p, meta->os);
 	term_flushln(p);
 
+	/* At the bottom in the middle: manual date. */
+
 	p->tcol->offset = p->tcol->rmargin;
-	sz = term_strlen(p, meta->os);
-	p->tcol->rmargin = p->maxrmargin > sz ? p->maxrmargin - sz : 0;
+	p->tcol->rmargin = p->maxrmargin > titlen ?
+	    p->maxrmargin - titlen : 0;
 	p->flags |= TERMP_NOSPACE;
 
 	term_word(p, meta->date);
 	term_flushln(p);
 
+	/* Bottom right corner: manual title and section. */
+
 	p->tcol->offset = p->tcol->rmargin;
 	p->tcol->rmargin = p->maxrmargin;
 	p->trailspace = 0;
 	p->flags &= ~TERMP_NOBREAK;
 	p->flags |= TERMP_NOSPACE;
 
-	term_word(p, meta->os);
+	term_word(p, title);
 	term_flushln(p);
 
 	p->tcol->offset = 0;
-	p->tcol->rmargin = p->maxrmargin;
 	p->flags = 0;
+	free(title);
 }
 
 static void
 print_mdoc_head(struct termp *p, const struct roff_meta *meta)
 {
 	char			*volume, *title;
-	size_t			 vollen, titlen;
-
-	/*
-	 * The header is strange.  It has three components, which are
-	 * really two with the first duplicated.  It goes like this:
-	 *
-	 * IDENTIFIER              TITLE                   IDENTIFIER
-	 *
-	 * The IDENTIFIER is NAME(SECTION), which is the command-name
-	 * (if given, or "unknown" if not) followed by the manual page
-	 * section.  These are given in `Dt'.  The TITLE is a free-form
-	 * string depending on the manual volume.  If not specified, it
-	 * switches on the manual section.
-	 */
+	size_t			 vollen, titlen;  /* In basic units. */
 
 	assert(meta->vol);
 	if (NULL == meta->arch)
 		volume = mandoc_strdup(meta->vol);
 	else
 		mandoc_asprintf(&volume, "%s (%s)",
 		    meta->vol, meta->arch);
 	vollen = term_strlen(p, volume);
 
+	/* Top left corner: manual title and section. */
+
 	if (NULL == meta->msec)
 		title = mandoc_strdup(meta->title);
 	else
 		mandoc_asprintf(&title, "%s(%s)",
 		    meta->title, meta->msec);
 	titlen = term_strlen(p, title);
 
 	p->flags |= TERMP_NOBREAK | TERMP_NOSPACE;
 	p->trailspace = 1;
 	p->tcol->offset = 0;
-	p->tcol->rmargin = 2 * (titlen+1) + vollen < p->maxrmargin ?
+	p->tcol->rmargin =
+	    titlen * 2 + term_len(p, 2) + vollen < p->maxrmargin ?
 	    (p->maxrmargin - vollen + term_len(p, 1)) / 2 :
-	    vollen < p->maxrmargin ?  p->maxrmargin - vollen : 0;
+	    vollen < p->maxrmargin ? p->maxrmargin - vollen : 0;
 
 	term_word(p, title);
 	term_flushln(p);
 
+	/* At the top in the middle: manual volume. */
+
 	p->flags |= TERMP_NOSPACE;
 	p->tcol->offset = p->tcol->rmargin;
 	p->tcol->rmargin = p->tcol->offset + vollen + titlen <
 	    p->maxrmargin ? p->maxrmargin - titlen : p->maxrmargin;
 
 	term_word(p, volume);
 	term_flushln(p);
 
+	/* Top right corner: title and section, again. */
+
 	p->flags &= ~TERMP_NOBREAK;
 	p->trailspace = 0;
 	if (p->tcol->rmargin + titlen <= p->maxrmargin) {
 		p->flags |= TERMP_NOSPACE;
 		p->tcol->offset = p->tcol->rmargin;
 		p->tcol->rmargin = p->maxrmargin;
 		term_word(p, title);
 		term_flushln(p);
 	}
 
 	p->flags &= ~TERMP_NOSPACE;
 	p->tcol->offset = 0;
 	p->tcol->rmargin = p->maxrmargin;
 	free(title);
 	free(volume);
 }
 
+/*
+ * Interpret the string v as a scaled width or, if the syntax is invalid,
+ * measure how much width it takes up when printed.  In both cases,
+ * return the width in basic units.
+ */
 static int
 a2width(const struct termp *p, const char *v)
 {
 	struct roffsu	 su;
 	const char	*end;
 
 	end = a2roffsu(v, &su, SCALE_MAX);
 	if (end == NULL || *end != '\0') {
-		su.unit = SCALE_EN;
-		su.scale = term_strlen(p, v) / term_strlen(p, "0");
+		su.unit = SCALE_BU;
+		su.scale = term_strlen(p, v);
 	}
-	return term_hen(p, &su);
+	return term_hspan(p, &su);
 }
 
 /*
  * Determine how much space to print out before block elements of `It'
  * (and thus `Bl') and `Bd'.  And then go ahead and print that space,
  * too.
  */
 static void
 print_bvspace(struct termp *p, struct roff_node *bl, struct roff_node *n)
 {
 	struct roff_node *nn;
 
 	term_newln(p);
 
 	if ((bl->tok == MDOC_Bd && bl->norm->Bd.comp) ||
 	    (bl->tok == MDOC_Bl && bl->norm->Bl.comp))
 		return;
 
 	/* Do not vspace directly after Ss/Sh. */
 
 	nn = n;
 	while (roff_node_prev(nn) == NULL) {
 		do {
 			nn = nn->parent;
 			if (nn->type == ROFFT_ROOT)
 				return;
 		} while (nn->type != ROFFT_BLOCK);
 		if (nn->tok == MDOC_Sh || nn->tok == MDOC_Ss)
 			return;
 		if (nn->tok == MDOC_It &&
 		    nn->parent->parent->norm->Bl.type != LIST_item)
 			break;
 	}
 
 	/*
 	 * No vertical space after:
 	 * items in .Bl -column
 	 * items without a body in .Bl -diag
 	 */
 
 	if (bl->tok != MDOC_Bl ||
 	    n->prev == NULL || n->prev->tok != MDOC_It ||
 	    (bl->norm->Bl.type != LIST_column &&
 	     (bl->norm->Bl.type != LIST_diag ||
 	      n->prev->body->child != NULL)))
 		term_vspace(p);
 }
 
 
 static int
 termp_it_pre(DECL_ARGS)
 {
 	struct roffsu		su;
 	char			buf[24];
 	const struct roff_node *bl, *nn;
-	size_t			ncols, dcol;
-	int			i, offset, width;
+	size_t			ncols;	/* Number of columns in .Bl -column. */
+	size_t			dcol;	/* Column spacing in basic units. */
+	int			i;	/* Zero-based column index. */
+	int			offset;	/* Start of column in basic units. */
+	int			width;	/* Column width in basic units. */
 	enum mdoc_list		type;
 
 	if (n->type == ROFFT_BLOCK) {
 		print_bvspace(p, n->parent->parent, n);
 		if (n->flags & NODE_ID)
 			term_tag_write(n, p->line);
 		return 1;
 	}
 
 	bl = n->parent->parent->parent;
 	type = bl->norm->Bl.type;
 
 	/*
 	 * Defaults for specific list types.
 	 */
 
 	switch (type) {
 	case LIST_bullet:
 	case LIST_dash:
 	case LIST_hyphen:
 	case LIST_enum:
 		width = term_len(p, 2);
 		break;
 	case LIST_hang:
 	case LIST_tag:
 		width = term_len(p, 8);
 		break;
 	case LIST_column:
 		width = term_len(p, 10);
 		break;
 	default:
 		width = 0;
 		break;
 	}
 	offset = 0;
 
 	/*
 	 * First calculate width and offset.  This is pretty easy unless
 	 * we're a -column list, in which case all prior columns must
 	 * be accounted for.
 	 */
 
 	if (bl->norm->Bl.offs != NULL) {
 		offset = a2width(p, bl->norm->Bl.offs);
 		if (offset < 0 && (size_t)(-offset) > p->tcol->offset)
 			offset = -p->tcol->offset;
 		else if (offset > SHRT_MAX)
 			offset = 0;
 	}
 
 	switch (type) {
 	case LIST_column:
 		if (n->type == ROFFT_HEAD)
 			break;
 
 		/*
 		 * Imitate groff's column handling:
 		 * - For each earlier column, add its width.
 		 * - For less than 5 columns, add four more blanks per
 		 *   column.
 		 * - For exactly 5 columns, add three more blank per
 		 *   column.
 		 * - For more than 5 columns, add only one column.
 		 */
 		ncols = bl->norm->Bl.ncols;
 		dcol = ncols < 5 ? term_len(p, 4) :
 		    ncols == 5 ? term_len(p, 3) : term_len(p, 1);
 
 		/*
 		 * Calculate the offset by applying all prior ROFFT_BODY,
 		 * so we stop at the ROFFT_HEAD (nn->prev == NULL).
 		 */
 
 		for (i = 0, nn = n->prev;
 		    nn->prev && i < (int)ncols;
 		    nn = nn->prev, i++) {
-			su.unit = SCALE_EN;
-			su.scale = term_strlen(p, bl->norm->Bl.cols[i]) /
-			    term_strlen(p, "0");
-			offset += term_hen(p, &su) + dcol;
+			su.unit = SCALE_BU;
+			su.scale = term_strlen(p, bl->norm->Bl.cols[i]);
+			offset += term_hspan(p, &su) + dcol;
 		}
 
 		/*
 		 * When exceeding the declared number of columns, leave
 		 * the remaining widths at 0.  This will later be
 		 * adjusted to the default width of 10, or, for the last
 		 * column, stretched to the right margin.
 		 */
 		if (i >= (int)ncols)
 			break;
 
 		/*
 		 * Use the declared column widths, extended as explained
 		 * in the preceding paragraph.
 		 */
-		su.unit = SCALE_EN;
-		su.scale = term_strlen(p, bl->norm->Bl.cols[i]) /
-		    term_strlen(p, "0");
-		width = term_hen(p, &su) + dcol;
+		su.unit = SCALE_BU;
+		su.scale = term_strlen(p, bl->norm->Bl.cols[i]);
+		width = term_hspan(p, &su) + dcol;
 		break;
 	default:
 		if (NULL == bl->norm->Bl.width)
 			break;
 
 		/*
 		 * Note: buffer the width by 2, which is groff's magic
 		 * number for buffering single arguments.  See the above
 		 * handling for column for how this changes.
 		 */
 		width = a2width(p, bl->norm->Bl.width) + term_len(p, 2);
 		if (width < 0 && (size_t)(-width) > p->tcol->offset)
 			width = -p->tcol->offset;
 		else if (width > SHRT_MAX)
 			width = 0;
 		break;
 	}
 
 	/*
 	 * Whitespace control.  Inset bodies need an initial space,
 	 * while diagonal bodies need two.
 	 */
 
 	p->flags |= TERMP_NOSPACE;
 
 	switch (type) {
 	case LIST_diag:
 		if (n->type == ROFFT_BODY)
 			term_word(p, "\\ \\ ");
 		break;
 	case LIST_inset:
 		if (n->type == ROFFT_BODY && n->parent->head->child != NULL)
 			term_word(p, "\\ ");
 		break;
 	default:
 		break;
 	}
 
 	p->flags |= TERMP_NOSPACE;
 
 	switch (type) {
 	case LIST_diag:
 		if (n->type == ROFFT_HEAD)
 			term_fontpush(p, TERMFONT_BOLD);
 		break;
 	default:
 		break;
 	}
 
 	/*
 	 * Pad and break control.  This is the tricky part.  These flags
 	 * are documented in term_flushln() in term.c.  Note that we're
 	 * going to unset all of these flags in termp_it_post() when we
 	 * exit.
 	 */
 
 	switch (type) {
 	case LIST_enum:
 	case LIST_bullet:
 	case LIST_dash:
 	case LIST_hyphen:
 		if (n->type == ROFFT_HEAD) {
 			p->flags |= TERMP_NOBREAK | TERMP_HANG;
 			p->trailspace = 1;
 		} else if (width <= (int)term_len(p, 2))
 			p->flags |= TERMP_NOPAD;
 		break;
 	case LIST_hang:
 		if (n->type != ROFFT_HEAD)
 			break;
 		p->flags |= TERMP_NOBREAK | TERMP_BRIND | TERMP_HANG;
 		p->trailspace = 1;
 		break;
 	case LIST_tag:
 		if (n->type != ROFFT_HEAD)
 			break;
 
 		p->flags |= TERMP_NOBREAK | TERMP_BRTRSP | TERMP_BRIND;
 		p->trailspace = 2;
 
 		if (NULL == n->next || NULL == n->next->child)
 			p->flags |= TERMP_HANG;
 		break;
 	case LIST_column:
 		if (n->type == ROFFT_HEAD)
 			break;
 
 		if (NULL == n->next) {
 			p->flags &= ~TERMP_NOBREAK;
 			p->trailspace = 0;
 		} else {
 			p->flags |= TERMP_NOBREAK;
 			p->trailspace = 1;
 		}
 
 		break;
 	case LIST_diag:
 		if (n->type != ROFFT_HEAD)
 			break;
 		p->flags |= TERMP_NOBREAK | TERMP_BRIND;
 		p->trailspace = 1;
 		break;
 	default:
 		break;
 	}
 
 	/*
 	 * Margin control.  Set-head-width lists have their right
 	 * margins shortened.  The body for these lists has the offset
 	 * necessarily lengthened.  Everybody gets the offset.
 	 */
 
 	p->tcol->offset += offset;
 
 	switch (type) {
 	case LIST_bullet:
 	case LIST_dash:
 	case LIST_enum:
 	case LIST_hyphen:
 	case LIST_hang:
 	case LIST_tag:
 		if (n->type == ROFFT_HEAD)
 			p->tcol->rmargin = p->tcol->offset + width;
 		else
 			p->tcol->offset += width;
 		break;
 	case LIST_column:
 		assert(width);
 		p->tcol->rmargin = p->tcol->offset + width;
 		/*
 		 * XXX - this behaviour is not documented: the
 		 * right-most column is filled to the right margin.
 		 */
 		if (n->type == ROFFT_HEAD)
 			break;
 		if (n->next == NULL && p->tcol->rmargin < p->maxrmargin)
 			p->tcol->rmargin = p->maxrmargin;
 		break;
 	default:
 		break;
 	}
 
 	/*
 	 * The dash, hyphen, bullet and enum lists all have a special
 	 * HEAD character (temporarily bold, in some cases).
 	 */
 
 	if (n->type == ROFFT_HEAD)
 		switch (type) {
 		case LIST_bullet:
 			term_fontpush(p, TERMFONT_BOLD);
 			term_word(p, "\\[bu]");
 			term_fontpop(p);
 			break;
 		case LIST_dash:
 		case LIST_hyphen:
 			term_fontpush(p, TERMFONT_BOLD);
 			term_word(p, "-");
 			term_fontpop(p);
 			break;
 		case LIST_enum:
 			(pair->ppair->ppair->count)++;
 			(void)snprintf(buf, sizeof(buf), "%d.",
 			    pair->ppair->ppair->count);
 			term_word(p, buf);
 			break;
 		default:
 			break;
 		}
 
 	/*
 	 * If we're not going to process our children, indicate so here.
 	 */
 
 	switch (type) {
 	case LIST_bullet:
 	case LIST_item:
 	case LIST_dash:
 	case LIST_hyphen:
 	case LIST_enum:
 		if (n->type == ROFFT_HEAD)
 			return 0;
 		break;
 	case LIST_column:
 		if (n->type == ROFFT_HEAD)
 			return 0;
 		p->minbl = 0;
 		break;
 	default:
 		break;
 	}
 
 	return 1;
 }
 
 static void
 termp_it_post(DECL_ARGS)
 {
 	enum mdoc_list	   type;
 
 	if (n->type == ROFFT_BLOCK)
 		return;
 
 	type = n->parent->parent->parent->norm->Bl.type;
 
 	switch (type) {
 	case LIST_item:
 	case LIST_diag:
 	case LIST_inset:
 		if (n->type == ROFFT_BODY)
 			term_newln(p);
 		break;
 	case LIST_column:
 		if (n->type == ROFFT_BODY)
 			term_flushln(p);
 		break;
 	default:
 		term_newln(p);
 		break;
 	}
 
 	/*
 	 * Now that our output is flushed, we can reset our tags.  Since
 	 * only `It' sets these flags, we're free to assume that nobody
 	 * has munged them in the meanwhile.
 	 */
 
 	p->flags &= ~(TERMP_NOBREAK | TERMP_BRTRSP | TERMP_BRIND | TERMP_HANG);
 	p->trailspace = 0;
 }
 
 static int
 termp_nm_pre(DECL_ARGS)
 {
 	const char	*cp;
 
 	if (n->type == ROFFT_BLOCK) {
 		p->flags |= TERMP_PREKEEP;
 		return 1;
 	}
 
 	if (n->type == ROFFT_BODY) {
 		if (n->child == NULL)
 			return 0;
 		p->flags |= TERMP_NOSPACE;
 		cp = NULL;
 		if (n->prev->child != NULL)
 		    cp = n->prev->child->string;
 		if (cp == NULL)
 			cp = meta->name;
 		if (cp == NULL)
 			p->tcol->offset += term_len(p, 6);
 		else
 			p->tcol->offset += term_len(p, 1) +
 			    term_strlen(p, cp);
 		return 1;
 	}
 
 	if (n->child == NULL)
 		return 0;
 
 	if (n->type == ROFFT_HEAD)
 		synopsis_pre(p, n->parent);
 
 	if (n->type == ROFFT_HEAD &&
 	    n->next != NULL && n->next->child != NULL) {
 		p->flags |= TERMP_NOSPACE | TERMP_NOBREAK | TERMP_BRIND;
 		p->trailspace = 1;
 		p->tcol->rmargin = p->tcol->offset + term_len(p, 1);
 		if (n->child == NULL)
 			p->tcol->rmargin += term_strlen(p, meta->name);
 		else if (n->child->type == ROFFT_TEXT) {
 			p->tcol->rmargin += term_strlen(p, n->child->string);
 			if (n->child->next != NULL)
 				p->flags |= TERMP_HANG;
 		} else {
 			p->tcol->rmargin += term_len(p, 5);
 			p->flags |= TERMP_HANG;
 		}
 	}
 	return termp_bold_pre(p, pair, meta, n);
 }
 
 static void
 termp_nm_post(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		p->flags &= ~(TERMP_KEEP | TERMP_PREKEEP);
 		break;
 	case ROFFT_HEAD:
 		if (n->next == NULL || n->next->child == NULL)
 			break;
 		term_flushln(p);
 		p->flags &= ~(TERMP_NOBREAK | TERMP_BRIND | TERMP_HANG);
 		p->trailspace = 0;
 		break;
 	case ROFFT_BODY:
 		if (n->child != NULL)
 			term_flushln(p);
 		break;
 	default:
 		break;
 	}
 }
 
 static int
 termp_fl_pre(DECL_ARGS)
 {
 	struct roff_node *nn;
 
 	term_fontpush(p, TERMFONT_BOLD);
 	term_word(p, "\\-");
 
 	if (n->child != NULL ||
 	    ((nn = roff_node_next(n)) != NULL &&
 	     nn->type != ROFFT_TEXT &&
 	     (nn->flags & NODE_LINE) == 0))
 		p->flags |= TERMP_NOSPACE;
 
 	return 1;
 }
 
 static int
 termp__a_pre(DECL_ARGS)
 {
 	struct roff_node *nn;
 
 	if ((nn = roff_node_prev(n)) != NULL && nn->tok == MDOC__A &&
 	    ((nn = roff_node_next(n)) == NULL || nn->tok != MDOC__A))
 		term_word(p, "and");
 
 	return 1;
 }
 
 static int
 termp_an_pre(DECL_ARGS)
 {
 
 	if (n->norm->An.auth == AUTH_split) {
 		p->flags &= ~TERMP_NOSPLIT;
 		p->flags |= TERMP_SPLIT;
 		return 0;
 	}
 	if (n->norm->An.auth == AUTH_nosplit) {
 		p->flags &= ~TERMP_SPLIT;
 		p->flags |= TERMP_NOSPLIT;
 		return 0;
 	}
 
 	if (p->flags & TERMP_SPLIT)
 		term_newln(p);
 
 	if (n->sec == SEC_AUTHORS && ! (p->flags & TERMP_NOSPLIT))
 		p->flags |= TERMP_SPLIT;
 
 	return 1;
 }
 
 static int
 termp_ns_pre(DECL_ARGS)
 {
 
 	if ( ! (NODE_LINE & n->flags))
 		p->flags |= TERMP_NOSPACE;
 	return 1;
 }
 
 static int
 termp_rs_pre(DECL_ARGS)
 {
 	if (SEC_SEE_ALSO != n->sec)
 		return 1;
 	if (n->type == ROFFT_BLOCK && roff_node_prev(n) != NULL)
 		term_vspace(p);
 	return 1;
 }
 
 static int
 termp_ex_pre(DECL_ARGS)
 {
 	term_newln(p);
 	return 1;
 }
 
 static int
 termp_nd_pre(DECL_ARGS)
 {
 	if (n->type == ROFFT_BODY)
 		term_word(p, "\\(en");
 	return 1;
 }
 
 static int
 termp_bl_pre(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		term_newln(p);
 		return 1;
 	case ROFFT_HEAD:
 		return 0;
 	default:
 		return 1;
 	}
 }
 
 static void
 termp_bl_post(DECL_ARGS)
 {
 	if (n->type != ROFFT_BLOCK)
 		return;
 	term_newln(p);
 	if (n->tok != MDOC_Bl || n->norm->Bl.type != LIST_column)
 		return;
 	term_tab_set(p, NULL);
 	term_tab_set(p, "T");
 	term_tab_set(p, ".5i");
 }
 
 static int
 termp_xr_pre(DECL_ARGS)
 {
 	if (NULL == (n = n->child))
 		return 0;
 
 	assert(n->type == ROFFT_TEXT);
 	term_word(p, n->string);
 
 	if (NULL == (n = n->next))
 		return 0;
 
 	p->flags |= TERMP_NOSPACE;
 	term_word(p, "(");
 	p->flags |= TERMP_NOSPACE;
 
 	assert(n->type == ROFFT_TEXT);
 	term_word(p, n->string);
 
 	p->flags |= TERMP_NOSPACE;
 	term_word(p, ")");
 
 	return 0;
 }
 
 /*
  * This decides how to assert whitespace before any of the SYNOPSIS set
  * of macros (which, as in the case of Ft/Fo and Ft/Fn, may contain
  * macro combos).
  */
 static void
 synopsis_pre(struct termp *p, struct roff_node *n)
 {
 	struct roff_node	*np;
 
 	if ((n->flags & NODE_SYNPRETTY) == 0 ||
 	    (np = roff_node_prev(n)) == NULL)
 		return;
 
 	/*
 	 * If we're the second in a pair of like elements, emit our
 	 * newline and return.  UNLESS we're `Fo', `Fn', `Fn', in which
 	 * case we soldier on.
 	 */
 	if (np->tok == n->tok &&
 	    MDOC_Ft != n->tok &&
 	    MDOC_Fo != n->tok &&
 	    MDOC_Fn != n->tok) {
 		term_newln(p);
 		return;
 	}
 
 	/*
 	 * If we're one of the SYNOPSIS set and non-like pair-wise after
 	 * another (or Fn/Fo, which we've let slip through) then assert
 	 * vertical space, else only newline and move on.
 	 */
 	switch (np->tok) {
 	case MDOC_Fd:
 	case MDOC_Fn:
 	case MDOC_Fo:
 	case MDOC_In:
 	case MDOC_Vt:
 		term_vspace(p);
 		break;
 	case MDOC_Ft:
 		if (n->tok != MDOC_Fn && n->tok != MDOC_Fo) {
 			term_vspace(p);
 			break;
 		}
 		/* FALLTHROUGH */
 	default:
 		term_newln(p);
 		break;
 	}
 }
 
 static int
 termp_vt_pre(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_ELEM:
 		return termp_ft_pre(p, pair, meta, n);
 	case ROFFT_BLOCK:
 		synopsis_pre(p, n);
 		return 1;
 	case ROFFT_HEAD:
 		return 0;
 	default:
 		return termp_under_pre(p, pair, meta, n);
 	}
 }
 
 static int
 termp_bold_pre(DECL_ARGS)
 {
 	term_fontpush(p, TERMFONT_BOLD);
 	return 1;
 }
 
 static int
 termp_fd_pre(DECL_ARGS)
 {
 	synopsis_pre(p, n);
 	return termp_bold_pre(p, pair, meta, n);
 }
 
 static void
 termp_fd_post(DECL_ARGS)
 {
 	term_newln(p);
 }
 
 static int
 termp_sh_pre(DECL_ARGS)
 {
 	struct roff_node	*np;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		/*
 		 * Vertical space before sections, except
 		 * when the previous section was empty.
 		 */
 		if ((np = roff_node_prev(n)) == NULL ||
 		    np->tok != MDOC_Sh ||
 		    (np->body != NULL && np->body->child != NULL))
 			term_vspace(p);
 		break;
 	case ROFFT_HEAD:
+		p->fontibi = 1;
 		return termp_bold_pre(p, pair, meta, n);
 	case ROFFT_BODY:
 		p->tcol->offset = term_len(p, p->defindent);
 		term_tab_set(p, NULL);
 		term_tab_set(p, "T");
 		term_tab_set(p, ".5i");
 		if (n->sec == SEC_AUTHORS)
 			p->flags &= ~(TERMP_SPLIT|TERMP_NOSPLIT);
 		break;
 	default:
 		break;
 	}
 	return 1;
 }
 
 static void
 termp_sh_post(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_HEAD:
+		p->fontibi = 0;
 		term_newln(p);
 		break;
 	case ROFFT_BODY:
 		term_newln(p);
 		p->tcol->offset = 0;
 		break;
 	default:
 		break;
 	}
 }
 
 static void
 termp_lb_post(DECL_ARGS)
 {
 	if (n->sec == SEC_LIBRARY && n->flags & NODE_LINE)
 		term_newln(p);
 }
 
 static int
 termp_d1_pre(DECL_ARGS)
 {
 	if (n->type != ROFFT_BLOCK)
 		return 1;
 	term_newln(p);
 	p->tcol->offset += term_len(p, p->defindent + 1);
 	term_tab_set(p, NULL);
 	term_tab_set(p, "T");
 	term_tab_set(p, ".5i");
 	return 1;
 }
 
 static int
 termp_ft_pre(DECL_ARGS)
 {
 	synopsis_pre(p, n);
 	return termp_under_pre(p, pair, meta, n);
 }
 
 static int
 termp_fn_pre(DECL_ARGS)
 {
 	size_t		 rmargin = 0;
 	int		 pretty;
 
 	synopsis_pre(p, n);
 	pretty = n->flags & NODE_SYNPRETTY;
 	if ((n = n->child) == NULL)
 		return 0;
 
 	if (pretty) {
 		rmargin = p->tcol->rmargin;
 		p->tcol->rmargin = p->tcol->offset + term_len(p, 4);
 		p->flags |= TERMP_NOBREAK | TERMP_BRIND | TERMP_HANG;
 	}
 
 	assert(n->type == ROFFT_TEXT);
 	term_fontpush(p, TERMFONT_BOLD);
 	term_word(p, n->string);
 	term_fontpop(p);
 
 	if (pretty) {
 		term_flushln(p);
 		p->flags &= ~(TERMP_NOBREAK | TERMP_BRIND | TERMP_HANG);
 		p->flags |= TERMP_NOPAD;
 		p->tcol->offset = p->tcol->rmargin;
 		p->tcol->rmargin = rmargin;
 	}
 
 	p->flags |= TERMP_NOSPACE;
 	term_word(p, "(");
 	p->flags |= TERMP_NOSPACE;
 
 	for (n = n->next; n; n = n->next) {
 		assert(n->type == ROFFT_TEXT);
 		term_fontpush(p, TERMFONT_UNDER);
 		if (pretty)
 			p->flags |= TERMP_NBRWORD;
 		term_word(p, n->string);
 		term_fontpop(p);
 
 		if (n->next) {
 			p->flags |= TERMP_NOSPACE;
 			term_word(p, ",");
 		}
 	}
 
 	p->flags |= TERMP_NOSPACE;
 	term_word(p, ")");
 
 	if (pretty) {
 		p->flags |= TERMP_NOSPACE;
 		term_word(p, ";");
 		term_flushln(p);
 	}
 	return 0;
 }
 
 static int
 termp_fa_pre(DECL_ARGS)
 {
 	const struct roff_node	*nn;
 
 	if (n->parent->tok != MDOC_Fo)
 		return termp_under_pre(p, pair, meta, n);
 
 	for (nn = n->child; nn != NULL; nn = nn->next) {
 		term_fontpush(p, TERMFONT_UNDER);
 		p->flags |= TERMP_NBRWORD;
 		term_word(p, nn->string);
 		term_fontpop(p);
 		if (nn->next != NULL) {
 			p->flags |= TERMP_NOSPACE;
 			term_word(p, ",");
 		}
 	}
 	if (n->child != NULL &&
 	    (nn = roff_node_next(n)) != NULL &&
 	    nn->tok == MDOC_Fa) {
 		p->flags |= TERMP_NOSPACE;
 		term_word(p, ",");
 	}
 	return 0;
 }
 
 static int
 termp_bd_pre(DECL_ARGS)
 {
-	int			 offset;
+	int			 offset;  /* In basic units. */
 
 	if (n->type == ROFFT_BLOCK) {
 		print_bvspace(p, n, n);
 		return 1;
 	} else if (n->type == ROFFT_HEAD)
 		return 0;
 
 	/* Handle the -offset argument. */
 
 	if (n->norm->Bd.offs == NULL ||
 	    ! strcmp(n->norm->Bd.offs, "left"))
 		/* nothing */;
 	else if ( ! strcmp(n->norm->Bd.offs, "indent"))
 		p->tcol->offset += term_len(p, p->defindent + 1);
 	else if ( ! strcmp(n->norm->Bd.offs, "indent-two"))
 		p->tcol->offset += term_len(p, (p->defindent + 1) * 2);
 	else {
 		offset = a2width(p, n->norm->Bd.offs);
 		if (offset < 0 && (size_t)(-offset) > p->tcol->offset)
 			p->tcol->offset = 0;
 		else if (offset < SHRT_MAX)
 			p->tcol->offset += offset;
 	}
 
 	switch (n->norm->Bd.type) {
 	case DISP_literal:
 		term_tab_set(p, NULL);
 		term_tab_set(p, "T");
 		term_tab_set(p, "8n");
 		break;
 	case DISP_centered:
 		p->flags |= TERMP_CENTER;
 		break;
 	default:
 		break;
 	}
 	return 1;
 }
 
 static void
 termp_bd_post(DECL_ARGS)
 {
 	if (n->type != ROFFT_BODY)
 		return;
 	if (n->norm->Bd.type == DISP_unfilled ||
 	    n->norm->Bd.type == DISP_literal)
 		p->flags |= TERMP_BRNEVER;
 	p->flags |= TERMP_NOSPACE;
 	term_newln(p);
 	p->flags &= ~TERMP_BRNEVER;
 	if (n->norm->Bd.type == DISP_centered)
 		p->flags &= ~TERMP_CENTER;
 }
 
 static int
 termp_xx_pre(DECL_ARGS)
 {
 	if ((n->aux = p->flags & TERMP_PREKEEP) == 0)
 		p->flags |= TERMP_PREKEEP;
 	return 1;
 }
 
 static void
 termp_xx_post(DECL_ARGS)
 {
 	if (n->aux == 0)
 		p->flags &= ~(TERMP_KEEP | TERMP_PREKEEP);
 }
 
 static void
 termp_pf_post(DECL_ARGS)
 {
 	if (n->next != NULL && (n->next->flags & NODE_LINE) == 0)
 		p->flags |= TERMP_NOSPACE;
 }
 
 static int
 termp_ss_pre(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		if (roff_node_prev(n) == NULL)
 			term_newln(p);
 		else
 			term_vspace(p);
 		break;
 	case ROFFT_HEAD:
-		p->tcol->offset = term_len(p, (p->defindent+1)/2);
+		p->tcol->offset = term_len(p, p->defindent) / 2 + 1;
+		p->fontibi = 1;
 		return termp_bold_pre(p, pair, meta, n);
 	case ROFFT_BODY:
 		p->tcol->offset = term_len(p, p->defindent);
 		term_tab_set(p, NULL);
 		term_tab_set(p, "T");
 		term_tab_set(p, ".5i");
 		break;
 	default:
 		break;
 	}
 	return 1;
 }
 
 static void
 termp_ss_post(DECL_ARGS)
 {
-	if (n->type == ROFFT_HEAD || n->type == ROFFT_BODY)
+	switch (n->type) {
+	case ROFFT_HEAD:
+		p->fontibi = 0;
+		/* FALLTHROUGH */
+	case ROFFT_BODY:
 		term_newln(p);
+		break;
+	default:
+		break;
+	}
 }
 
 static int
 termp_in_pre(DECL_ARGS)
 {
 	synopsis_pre(p, n);
 	if (n->flags & NODE_SYNPRETTY && n->flags & NODE_LINE) {
 		term_fontpush(p, TERMFONT_BOLD);
 		term_word(p, "#include");
 		term_word(p, "<");
 	} else {
 		term_word(p, "<");
 		term_fontpush(p, TERMFONT_UNDER);
 	}
 	p->flags |= TERMP_NOSPACE;
 	return 1;
 }
 
 static void
 termp_in_post(DECL_ARGS)
 {
 	if (n->flags & NODE_SYNPRETTY)
 		term_fontpush(p, TERMFONT_BOLD);
 	p->flags |= TERMP_NOSPACE;
 	term_word(p, ">");
 	if (n->flags & NODE_SYNPRETTY)
 		term_fontpop(p);
 }
 
 static int
 termp_pp_pre(DECL_ARGS)
 {
 	term_vspace(p);
 	if (n->flags & NODE_ID)
 		term_tag_write(n, p->line);
 	return 0;
 }
 
 static int
 termp_skip_pre(DECL_ARGS)
 {
 	return 0;
 }
 
 static int
 termp_quote_pre(DECL_ARGS)
 {
 	if (n->type != ROFFT_BODY && n->type != ROFFT_ELEM)
 		return 1;
 
 	switch (n->tok) {
 	case MDOC_Ao:
 	case MDOC_Aq:
 		term_word(p, n->child != NULL && n->child->next == NULL &&
 		    n->child->tok == MDOC_Mt ? "<" : "\\(la");
 		break;
 	case MDOC_Bro:
 	case MDOC_Brq:
 		term_word(p, "{");
 		break;
 	case MDOC_Oo:
 	case MDOC_Op:
 	case MDOC_Bo:
 	case MDOC_Bq:
 		term_word(p, "[");
 		break;
 	case MDOC__T:
 		/* FALLTHROUGH */
 	case MDOC_Do:
 	case MDOC_Dq:
 		term_word(p, "\\(lq");
 		break;
 	case MDOC_En:
 		if (NULL == n->norm->Es ||
 		    NULL == n->norm->Es->child)
 			return 1;
 		term_word(p, n->norm->Es->child->string);
 		break;
 	case MDOC_Po:
 	case MDOC_Pq:
 		term_word(p, "(");
 		break;
 	case MDOC_Qo:
 	case MDOC_Qq:
 		term_word(p, "\"");
 		break;
 	case MDOC_Ql:
 	case MDOC_So:
 	case MDOC_Sq:
 		term_word(p, "\\(oq");
 		break;
 	default:
 		abort();
 	}
 
 	p->flags |= TERMP_NOSPACE;
 	return 1;
 }
 
 static void
 termp_quote_post(DECL_ARGS)
 {
 
 	if (n->type != ROFFT_BODY && n->type != ROFFT_ELEM)
 		return;
 
 	p->flags |= TERMP_NOSPACE;
 
 	switch (n->tok) {
 	case MDOC_Ao:
 	case MDOC_Aq:
 		term_word(p, n->child != NULL && n->child->next == NULL &&
 		    n->child->tok == MDOC_Mt ? ">" : "\\(ra");
 		break;
 	case MDOC_Bro:
 	case MDOC_Brq:
 		term_word(p, "}");
 		break;
 	case MDOC_Oo:
 	case MDOC_Op:
 	case MDOC_Bo:
 	case MDOC_Bq:
 		term_word(p, "]");
 		break;
 	case MDOC__T:
 		/* FALLTHROUGH */
 	case MDOC_Do:
 	case MDOC_Dq:
 		term_word(p, "\\(rq");
 		break;
 	case MDOC_En:
 		if (n->norm->Es == NULL ||
 		    n->norm->Es->child == NULL ||
 		    n->norm->Es->child->next == NULL)
 			p->flags &= ~TERMP_NOSPACE;
 		else
 			term_word(p, n->norm->Es->child->next->string);
 		break;
 	case MDOC_Po:
 	case MDOC_Pq:
 		term_word(p, ")");
 		break;
 	case MDOC_Qo:
 	case MDOC_Qq:
 		term_word(p, "\"");
 		break;
 	case MDOC_Ql:
 	case MDOC_So:
 	case MDOC_Sq:
 		term_word(p, "\\(cq");
 		break;
 	default:
 		abort();
 	}
 }
 
 static int
 termp_eo_pre(DECL_ARGS)
 {
 
 	if (n->type != ROFFT_BODY)
 		return 1;
 
 	if (n->end == ENDBODY_NOT &&
 	    n->parent->head->child == NULL &&
 	    n->child != NULL &&
 	    n->child->end != ENDBODY_NOT)
 		term_word(p, "\\&");
 	else if (n->end != ENDBODY_NOT ? n->child != NULL :
 	     n->parent->head->child != NULL && (n->child != NULL ||
 	     (n->parent->tail != NULL && n->parent->tail->child != NULL)))
 		p->flags |= TERMP_NOSPACE;
 
 	return 1;
 }
 
 static void
 termp_eo_post(DECL_ARGS)
 {
 	int	 body, tail;
 
 	if (n->type != ROFFT_BODY)
 		return;
 
 	if (n->end != ENDBODY_NOT) {
 		p->flags &= ~TERMP_NOSPACE;
 		return;
 	}
 
 	body = n->child != NULL || n->parent->head->child != NULL;
 	tail = n->parent->tail != NULL && n->parent->tail->child != NULL;
 
 	if (body && tail)
 		p->flags |= TERMP_NOSPACE;
 	else if ( ! (body || tail))
 		term_word(p, "\\&");
 	else if ( ! tail)
 		p->flags &= ~TERMP_NOSPACE;
 }
 
 static int
 termp_fo_pre(DECL_ARGS)
 {
 	size_t rmargin;
 
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		synopsis_pre(p, n);
 		return 1;
 	case ROFFT_BODY:
 		rmargin = p->tcol->rmargin;
 		if (n->flags & NODE_SYNPRETTY) {
 			p->tcol->rmargin = p->tcol->offset + term_len(p, 4);
 			p->flags |= TERMP_NOBREAK | TERMP_BRIND |
 					TERMP_HANG;
 		}
 		p->flags |= TERMP_NOSPACE;
 		term_word(p, "(");
 		p->flags |= TERMP_NOSPACE;
 		if (n->flags & NODE_SYNPRETTY) {
 			term_flushln(p);
 			p->flags &= ~(TERMP_NOBREAK | TERMP_BRIND |
 					TERMP_HANG);
 			p->flags |= TERMP_NOPAD;
 			p->tcol->offset = p->tcol->rmargin;
 			p->tcol->rmargin = rmargin;
 		}
 		return 1;
 	default:
 		return termp_bold_pre(p, pair, meta, n);
 	}
 }
 
 static void
 termp_fo_post(DECL_ARGS)
 {
 	if (n->type != ROFFT_BODY)
 		return;
 
 	p->flags |= TERMP_NOSPACE;
 	term_word(p, ")");
 
 	if (n->flags & NODE_SYNPRETTY) {
 		p->flags |= TERMP_NOSPACE;
 		term_word(p, ";");
 		term_flushln(p);
 	}
 }
 
 static int
 termp_bf_pre(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		break;
 	default:
 		return 1;
 	}
 	switch (n->norm->Bf.font) {
 	case FONT_Em:
 		return termp_under_pre(p, pair, meta, n);
 	case FONT_Sy:
 		return termp_bold_pre(p, pair, meta, n);
 	default:
 		return termp_li_pre(p, pair, meta, n);
 	}
 }
 
 static int
 termp_sm_pre(DECL_ARGS)
 {
 	if (n->child == NULL)
 		p->flags ^= TERMP_NONOSPACE;
 	else if (strcmp(n->child->string, "on") == 0)
 		p->flags &= ~TERMP_NONOSPACE;
 	else
 		p->flags |= TERMP_NONOSPACE;
 
 	if (p->col && ! (TERMP_NONOSPACE & p->flags))
 		p->flags &= ~TERMP_NOSPACE;
 
 	return 0;
 }
 
 static int
 termp_ap_pre(DECL_ARGS)
 {
 	p->flags |= TERMP_NOSPACE;
 	term_word(p, "'");
 	p->flags |= TERMP_NOSPACE;
 	return 1;
 }
 
 static void
 termp____post(DECL_ARGS)
 {
 	struct roff_node *nn;
 
 	/*
 	 * Handle lists of authors.  In general, print each followed by
 	 * a comma.  Don't print the comma if there are only two
 	 * authors.
 	 */
 	if (n->tok == MDOC__A &&
 	    (nn = roff_node_next(n)) != NULL && nn->tok == MDOC__A &&
 	    ((nn = roff_node_next(nn)) == NULL || nn->tok != MDOC__A) &&
 	    ((nn = roff_node_prev(n)) == NULL || nn->tok != MDOC__A))
 		return;
 
 	/* TODO: %U. */
 
 	if (n->parent == NULL || n->parent->tok != MDOC_Rs)
 		return;
 
 	p->flags |= TERMP_NOSPACE;
 	if (roff_node_next(n) == NULL) {
 		term_word(p, ".");
 		p->flags |= TERMP_SENTENCE;
 	} else
 		term_word(p, ",");
 }
 
 static int
 termp_li_pre(DECL_ARGS)
 {
 	term_fontpush(p, TERMFONT_NONE);
 	return 1;
 }
 
 static int
 termp_lk_pre(DECL_ARGS)
 {
 	const struct roff_node *link, *descr, *punct;
 
 	if ((link = n->child) == NULL)
 		return 0;
 
 	/* Find beginning of trailing punctuation. */
 	punct = n->last;
 	while (punct != link && punct->flags & NODE_DELIMC)
 		punct = punct->prev;
 	punct = punct->next;
 
 	/* Link text. */
 	if ((descr = link->next) != NULL && descr != punct) {
 		term_fontpush(p, TERMFONT_UNDER);
 		while (descr != punct) {
 			if (descr->flags & (NODE_DELIMC | NODE_DELIMO))
 				p->flags |= TERMP_NOSPACE;
 			term_word(p, descr->string);
 			descr = descr->next;
 		}
 		term_fontpop(p);
 		p->flags |= TERMP_NOSPACE;
 		term_word(p, ":");
 	}
 
 	/* Link target. */
-	term_fontpush(p, TERMFONT_BOLD);
 	term_word(p, link->string);
-	term_fontpop(p);
 
 	/* Trailing punctuation. */
 	while (punct != NULL) {
 		p->flags |= TERMP_NOSPACE;
 		term_word(p, punct->string);
 		punct = punct->next;
 	}
 	return 0;
 }
 
 static int
 termp_bk_pre(DECL_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		break;
 	case ROFFT_HEAD:
 		return 0;
 	case ROFFT_BODY:
 		if (n->parent->args != NULL || n->prev->child == NULL)
 			p->flags |= TERMP_PREKEEP;
 		break;
 	default:
 		abort();
 	}
 	return 1;
 }
 
 static void
 termp_bk_post(DECL_ARGS)
 {
 	if (n->type == ROFFT_BODY)
 		p->flags &= ~(TERMP_KEEP | TERMP_PREKEEP);
 }
 
 /*
  * If we are in an `Rs' and there is a journal present,
  * then quote us instead of underlining us (for disambiguation).
  */
 static void
 termp__t_post(DECL_ARGS)
 {
 	if (n->parent != NULL && n->parent->tok == MDOC_Rs &&
 	    n->parent->norm->Rs.quote_T)
 		termp_quote_post(p, pair, meta, n);
 	termp____post(p, pair, meta, n);
 }
 
 static int
 termp__t_pre(DECL_ARGS)
 {
 	if (n->parent != NULL && n->parent->tok == MDOC_Rs &&
 	    n->parent->norm->Rs.quote_T)
 		return termp_quote_pre(p, pair, meta, n);
 	else
 		return termp_under_pre(p, pair, meta, n);
 }
 
 static int
 termp_under_pre(DECL_ARGS)
 {
 	term_fontpush(p, TERMFONT_UNDER);
 	return 1;
 }
 
 static int
 termp_abort_pre(DECL_ARGS)
 {
 	abort();
 }
diff --git a/contrib/mandoc/mdoc_validate.c b/contrib/mandoc/mdoc_validate.c
index 4ca1253e4b70..ac265b88f484 100644
--- a/contrib/mandoc/mdoc_validate.c
+++ b/contrib/mandoc/mdoc_validate.c
@@ -1,3127 +1,3125 @@
-/* $Id: mdoc_validate.c,v 1.393 2025/06/05 12:38:26 schwarze Exp $ */
+/* $Id: mdoc_validate.c,v 1.396 2025/07/26 12:23:16 schwarze Exp $ */
 /*
  * Copyright (c) 2010-2022, 2025 Ingo Schwarze 
  * Copyright (c) 2008-2012 Kristaps Dzonsons 
  * Copyright (c) 2010 Joerg Sonnenberger 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
  * Validation module for mdoc(7) syntax trees used by mandoc(1).
  */
 #include "config.h"
 
 #include 
 #ifndef OSNAME
 #include 
 #endif
 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "mandoc_xr.h"
 #include "roff.h"
 #include "mdoc.h"
 #include "libmandoc.h"
 #include "roff_int.h"
 #include "libmdoc.h"
 #include "tag.h"
 
 /* FIXME: .Bl -diag can't have non-text children in HEAD. */
 
 #define	POST_ARGS struct roff_man *mdoc
 
 enum	check_ineq {
 	CHECK_LT,
 	CHECK_GT,
 	CHECK_EQ
 };
 
 typedef	void	(*v_post)(POST_ARGS);
 
 static	int	 build_list(struct roff_man *, int);
 static	void	 check_argv(struct roff_man *,
 			struct roff_node *, struct mdoc_argv *);
 static	void	 check_args(struct roff_man *, struct roff_node *);
 static	void	 check_text(struct roff_man *, int, int, char *);
 static	void	 check_text_em(struct roff_man *, int, int, char *);
 static	void	 check_toptext(struct roff_man *, int, int, const char *);
 static	int	 child_an(const struct roff_node *);
 static	size_t		macro2len(enum roff_tok);
 static	void	 rewrite_macro2len(struct roff_man *, char **);
 static	int	 similar(const char *, const char *);
 
 static	void	 post_abort(POST_ARGS) __attribute__((__noreturn__));
 static	void	 post_an(POST_ARGS);
 static	void	 post_an_norm(POST_ARGS);
 static	void	 post_at(POST_ARGS);
 static	void	 post_bd(POST_ARGS);
 static	void	 post_bf(POST_ARGS);
 static	void	 post_bk(POST_ARGS);
 static	void	 post_bl(POST_ARGS);
 static	void	 post_bl_block(POST_ARGS);
 static	void	 post_bl_head(POST_ARGS);
 static	void	 post_bl_norm(POST_ARGS);
 static	void	 post_bx(POST_ARGS);
 static	void	 post_defaults(POST_ARGS);
 static	void	 post_display(POST_ARGS);
 static	void	 post_dd(POST_ARGS);
 static	void	 post_delim(POST_ARGS);
 static	void	 post_delim_nb(POST_ARGS);
 static	void	 post_dt(POST_ARGS);
 static	void	 post_em(POST_ARGS);
 static	void	 post_en(POST_ARGS);
 static	void	 post_er(POST_ARGS);
 static	void	 post_es(POST_ARGS);
 static	void	 post_eoln(POST_ARGS);
 static	void	 post_ex(POST_ARGS);
 static	void	 post_fa(POST_ARGS);
 static	void	 post_fl(POST_ARGS);
 static	void	 post_fn(POST_ARGS);
 static	void	 post_fname(POST_ARGS);
 static	void	 post_fo(POST_ARGS);
 static	void	 post_hyph(POST_ARGS);
 static	void	 post_it(POST_ARGS);
 static	void	 post_lb(POST_ARGS);
 static	void	 post_nd(POST_ARGS);
 static	void	 post_nm(POST_ARGS);
 static	void	 post_ns(POST_ARGS);
 static	void	 post_obsolete(POST_ARGS);
 static	void	 post_os(POST_ARGS);
 static	void	 post_par(POST_ARGS);
 static	void	 post_prevpar(POST_ARGS);
 static	void	 post_root(POST_ARGS);
 static	void	 post_rs(POST_ARGS);
 static	void	 post_rv(POST_ARGS);
 static	void	 post_section(POST_ARGS);
 static	void	 post_sh(POST_ARGS);
 static	void	 post_sh_head(POST_ARGS);
 static	void	 post_sh_name(POST_ARGS);
 static	void	 post_sh_see_also(POST_ARGS);
 static	void	 post_sh_authors(POST_ARGS);
 static	void	 post_sm(POST_ARGS);
 static	void	 post_st(POST_ARGS);
 static	void	 post_std(POST_ARGS);
 static	void	 post_sx(POST_ARGS);
 static	void	 post_tag(POST_ARGS);
 static	void	 post_tg(POST_ARGS);
 static	void	 post_useless(POST_ARGS);
 static	void	 post_xr(POST_ARGS);
 static	void	 post_xx(POST_ARGS);
 
 static	const v_post mdoc_valids[MDOC_MAX - MDOC_Dd] = {
 	post_dd,	/* Dd */
 	post_dt,	/* Dt */
 	post_os,	/* Os */
 	post_sh,	/* Sh */
 	post_section,	/* Ss */
 	post_par,	/* Pp */
 	post_display,	/* D1 */
 	post_display,	/* Dl */
 	post_display,	/* Bd */
 	NULL,		/* Ed */
 	post_bl,	/* Bl */
 	NULL,		/* El */
 	post_it,	/* It */
 	post_delim_nb,	/* Ad */
 	post_an,	/* An */
 	NULL,		/* Ap */
 	post_defaults,	/* Ar */
 	NULL,		/* Cd */
 	post_tag,	/* Cm */
 	post_tag,	/* Dv */
 	post_er,	/* Er */
 	post_tag,	/* Ev */
 	post_ex,	/* Ex */
 	post_fa,	/* Fa */
 	NULL,		/* Fd */
 	post_fl,	/* Fl */
 	post_fn,	/* Fn */
 	post_delim_nb,	/* Ft */
 	post_tag,	/* Ic */
 	post_delim_nb,	/* In */
 	post_tag,	/* Li */
 	post_nd,	/* Nd */
 	post_nm,	/* Nm */
 	post_delim_nb,	/* Op */
 	post_abort,	/* Ot */
 	post_defaults,	/* Pa */
 	post_rv,	/* Rv */
 	post_st,	/* St */
 	post_tag,	/* Va */
 	post_delim_nb,	/* Vt */
 	post_xr,	/* Xr */
 	NULL,		/* %A */
 	post_hyph,	/* %B */ /* FIXME: can be used outside Rs/Re. */
 	NULL,		/* %D */
 	NULL,		/* %I */
 	NULL,		/* %J */
 	post_hyph,	/* %N */
 	post_hyph,	/* %O */
 	NULL,		/* %P */
 	post_hyph,	/* %R */
 	post_hyph,	/* %T */ /* FIXME: can be used outside Rs/Re. */
 	NULL,		/* %V */
 	NULL,		/* Ac */
 	NULL,		/* Ao */
 	post_delim_nb,	/* Aq */
 	post_at,	/* At */
 	NULL,		/* Bc */
 	post_bf,	/* Bf */
 	NULL,		/* Bo */
 	NULL,		/* Bq */
 	post_xx,	/* Bsx */
 	post_bx,	/* Bx */
 	post_obsolete,	/* Db */
 	NULL,		/* Dc */
 	NULL,		/* Do */
 	NULL,		/* Dq */
 	NULL,		/* Ec */
 	NULL,		/* Ef */
 	post_em,	/* Em */
 	NULL,		/* Eo */
 	post_xx,	/* Fx */
 	post_tag,	/* Ms */
 	post_tag,	/* No */
 	post_ns,	/* Ns */
 	post_xx,	/* Nx */
 	post_xx,	/* Ox */
 	NULL,		/* Pc */
 	NULL,		/* Pf */
 	NULL,		/* Po */
 	post_delim_nb,	/* Pq */
 	NULL,		/* Qc */
 	post_delim_nb,	/* Ql */
 	NULL,		/* Qo */
 	post_delim_nb,	/* Qq */
 	NULL,		/* Re */
 	post_rs,	/* Rs */
 	NULL,		/* Sc */
 	NULL,		/* So */
 	post_delim_nb,	/* Sq */
 	post_sm,	/* Sm */
 	post_sx,	/* Sx */
 	post_em,	/* Sy */
 	post_useless,	/* Tn */
 	post_xx,	/* Ux */
 	NULL,		/* Xc */
 	NULL,		/* Xo */
 	post_fo,	/* Fo */
 	NULL,		/* Fc */
 	NULL,		/* Oo */
 	NULL,		/* Oc */
 	post_bk,	/* Bk */
 	NULL,		/* Ek */
 	post_eoln,	/* Bt */
 	post_obsolete,	/* Hf */
 	post_obsolete,	/* Fr */
 	post_eoln,	/* Ud */
 	post_lb,	/* Lb */
 	post_abort,	/* Lp */
 	post_delim_nb,	/* Lk */
 	post_defaults,	/* Mt */
 	post_delim_nb,	/* Brq */
 	NULL,		/* Bro */
 	NULL,		/* Brc */
 	NULL,		/* %C */
 	post_es,	/* Es */
 	post_en,	/* En */
 	post_xx,	/* Dx */
 	NULL,		/* %Q */
 	NULL,		/* %U */
 	NULL,		/* Ta */
 	post_tg,	/* Tg */
 };
 
 #define	RSORD_MAX 14 /* Number of `Rs' blocks. */
 
 static	const enum roff_tok rsord[RSORD_MAX] = {
 	MDOC__A,
 	MDOC__T,
 	MDOC__B,
 	MDOC__I,
 	MDOC__J,
 	MDOC__R,
 	MDOC__N,
 	MDOC__V,
 	MDOC__U,
 	MDOC__P,
 	MDOC__Q,
 	MDOC__C,
 	MDOC__D,
 	MDOC__O
 };
 
 static	const char * const secnames[SEC__MAX] = {
 	NULL,
 	"NAME",
 	"LIBRARY",
 	"SYNOPSIS",
 	"DESCRIPTION",
 	"CONTEXT",
 	"IMPLEMENTATION NOTES",
 	"RETURN VALUES",
 	"ENVIRONMENT",
 	"FILES",
 	"EXIT STATUS",
 	"EXAMPLES",
 	"DIAGNOSTICS",
 	"COMPATIBILITY",
 	"ERRORS",
 	"SEE ALSO",
 	"STANDARDS",
 	"HISTORY",
 	"AUTHORS",
 	"CAVEATS",
 	"BUGS",
 	"SECURITY CONSIDERATIONS",
 	NULL
 };
 
 static	int	  fn_prio = TAG_STRONG;
 
 
 /* Validate the subtree rooted at mdoc->last. */
 void
 mdoc_validate(struct roff_man *mdoc)
 {
 	struct roff_node *n, *np;
 	const v_post *p;
 
 	/*
 	 * Translate obsolete macros to modern macros first
 	 * such that later code does not need to look
 	 * for the obsolete versions.
 	 */
 
 	n = mdoc->last;
 	switch (n->tok) {
 	case MDOC_Lp:
 		n->tok = MDOC_Pp;
 		break;
 	case MDOC_Ot:
 		post_obsolete(mdoc);
 		n->tok = MDOC_Ft;
 		break;
 	default:
 		break;
 	}
 
 	/*
 	 * Iterate over all children, recursing into each one
 	 * in turn, depth-first.
 	 */
 
 	mdoc->last = mdoc->last->child;
 	while (mdoc->last != NULL) {
 		mdoc_validate(mdoc);
 		if (mdoc->last == n)
 			mdoc->last = mdoc->last->child;
 		else
 			mdoc->last = mdoc->last->next;
 	}
 
 	/* Finally validate the macro itself. */
 
 	mdoc->last = n;
 	mdoc->next = ROFF_NEXT_SIBLING;
 	switch (n->type) {
 	case ROFFT_TEXT:
 		np = n->parent;
 		if (n->sec != SEC_SYNOPSIS ||
 		    (np->tok != MDOC_Cd && np->tok != MDOC_Fd))
 			check_text(mdoc, n->line, n->pos, n->string);
 		if ((n->flags & NODE_NOFILL) == 0 &&
 		    (np->tok != MDOC_It || np->type != ROFFT_HEAD ||
 		     np->parent->parent->norm->Bl.type != LIST_diag))
 			check_text_em(mdoc, n->line, n->pos, n->string);
 		if (np->tok == MDOC_It || (np->type == ROFFT_BODY &&
 		    (np->tok == MDOC_Sh || np->tok == MDOC_Ss)))
 			check_toptext(mdoc, n->line, n->pos, n->string);
 		break;
 	case ROFFT_COMMENT:
 	case ROFFT_EQN:
 	case ROFFT_TBL:
 		break;
 	case ROFFT_ROOT:
 		post_root(mdoc);
 		break;
 	default:
 		check_args(mdoc, mdoc->last);
 
 		/*
 		 * Closing delimiters are not special at the
 		 * beginning of a block, opening delimiters
 		 * are not special at the end.
 		 */
 
 		if (n->child != NULL)
 			n->child->flags &= ~NODE_DELIMC;
 		if (n->last != NULL)
 			n->last->flags &= ~NODE_DELIMO;
 
 		/* Call the macro's postprocessor. */
 
 		if (n->tok < ROFF_MAX) {
 			roff_validate(mdoc);
 			break;
 		}
 
 		assert(n->tok >= MDOC_Dd && n->tok < MDOC_MAX);
 		p = mdoc_valids + (n->tok - MDOC_Dd);
 		if (*p)
 			(*p)(mdoc);
 		if (mdoc->last == n)
 			mdoc_state(mdoc, n);
 		break;
 	}
 }
 
 static void
 check_args(struct roff_man *mdoc, struct roff_node *n)
 {
 	int		 i;
 
 	if (NULL == n->args)
 		return;
 
 	assert(n->args->argc);
 	for (i = 0; i < (int)n->args->argc; i++)
 		check_argv(mdoc, n, &n->args->argv[i]);
 }
 
 static void
 check_argv(struct roff_man *mdoc, struct roff_node *n, struct mdoc_argv *v)
 {
 	int		 i;
 
 	for (i = 0; i < (int)v->sz; i++)
 		check_text(mdoc, v->line, v->pos, v->value[i]);
 }
 
 static void
 check_text(struct roff_man *mdoc, int ln, int pos, char *p)
 {
 	char		*cp;
 
 	if (mdoc->last->flags & NODE_NOFILL)
 		return;
 
 	for (cp = p; NULL != (p = strchr(p, '\t')); p++)
 		mandoc_msg(MANDOCERR_FI_TAB, ln, pos + (int)(p - cp), NULL);
 }
 
 static void
 check_text_em(struct roff_man *mdoc, int ln, int pos, char *p)
 {
 	const struct roff_node	*np, *nn;
 	char			*cp;
 
 	np = mdoc->last->prev;
 	nn = mdoc->last->next;
 
 	/* Look for em-dashes wrongly encoded as "--". */
 
 	for (cp = p; *cp != '\0'; cp++) {
 		if (cp[0] != '-' || cp[1] != '-')
 			continue;
 		cp++;
 
 		/* Skip input sequences of more than two '-'. */
 
 		if (cp[1] == '-') {
 			while (cp[1] == '-')
 				cp++;
 			continue;
 		}
 
 		/* Skip "--" directly attached to something else. */
 
 		if ((cp - p > 1 && cp[-2] != ' ') ||
 		    (cp[1] != '\0' && cp[1] != ' '))
 			continue;
 
 		/* Require a letter right before or right afterwards. */
 
 		if ((cp - p > 2 ?
 		     isalpha((unsigned char)cp[-3]) :
 		     np != NULL &&
 		     np->type == ROFFT_TEXT &&
 		     *np->string != '\0' &&
 		     isalpha((unsigned char)np->string[
 		       strlen(np->string) - 1])) ||
 		    (cp[1] != '\0' && cp[2] != '\0' ?
 		     isalpha((unsigned char)cp[2]) :
 		     nn != NULL &&
 		     nn->type == ROFFT_TEXT &&
 		     isalpha((unsigned char)*nn->string))) {
 			mandoc_msg(MANDOCERR_DASHDASH,
 			    ln, pos + (int)(cp - p) - 1, NULL);
 			break;
 		}
 	}
 }
 
 static void
 check_toptext(struct roff_man *mdoc, int ln, int pos, const char *p)
 {
 	const char	*cp, *cpr;
 
 	if (*p == '\0')
 		return;
 
 	if ((cp = strstr(p, "OpenBSD")) != NULL)
 		mandoc_msg(MANDOCERR_BX, ln, pos + (int)(cp - p), "Ox");
 	if ((cp = strstr(p, "NetBSD")) != NULL)
 		mandoc_msg(MANDOCERR_BX, ln, pos + (int)(cp - p), "Nx");
 	if ((cp = strstr(p, "FreeBSD")) != NULL)
 		mandoc_msg(MANDOCERR_BX, ln, pos + (int)(cp - p), "Fx");
 	if ((cp = strstr(p, "DragonFly")) != NULL)
 		mandoc_msg(MANDOCERR_BX, ln, pos + (int)(cp - p), "Dx");
 
 	cp = p;
 	while ((cp = strstr(cp + 1, "()")) != NULL) {
 		for (cpr = cp - 1; cpr >= p; cpr--)
 			if (*cpr != '_' && !isalnum((unsigned char)*cpr))
 				break;
 		if ((cpr < p || *cpr == ' ') && cpr + 1 < cp) {
 			cpr++;
 			mandoc_msg(MANDOCERR_FUNC, ln, pos + (int)(cpr - p),
 			    "%.*s()", (int)(cp - cpr), cpr);
 		}
 	}
 }
 
 static void
 post_abort(POST_ARGS)
 {
 	abort();
 }
 
 static void
 post_delim(POST_ARGS)
 {
 	const struct roff_node	*nch;
 	const char		*lc;
 	enum mdelim		 delim;
 	enum roff_tok		 tok;
 
 	tok = mdoc->last->tok;
 	nch = mdoc->last->last;
 	if (nch == NULL || nch->type != ROFFT_TEXT)
 		return;
 	lc = strchr(nch->string, '\0') - 1;
 	if (lc < nch->string)
 		return;
 	delim = mdoc_isdelim(lc);
 	if (delim == DELIM_NONE || delim == DELIM_OPEN)
 		return;
 	if (*lc == ')' && (tok == MDOC_Nd || tok == MDOC_Sh ||
 	    tok == MDOC_Ss || tok == MDOC_Fo))
 		return;
 
 	mandoc_msg(MANDOCERR_DELIM, nch->line,
 	    nch->pos + (int)(lc - nch->string), "%s%s %s", roff_name[tok],
 	    nch == mdoc->last->child ? "" : " ...", nch->string);
 }
 
 static void
 post_delim_nb(POST_ARGS)
 {
 	const struct roff_node	*nch;
 	const char		*lc, *cp;
 	int			 nw;
 	enum mdelim		 delim;
 	enum roff_tok		 tok;
 
 	/*
 	 * Find candidates: at least two bytes,
 	 * the last one a closing or middle delimiter.
 	 */
 
 	tok = mdoc->last->tok;
 	nch = mdoc->last->last;
 	if (nch == NULL || nch->type != ROFFT_TEXT)
 		return;
 	lc = strchr(nch->string, '\0') - 1;
 	if (lc <= nch->string)
 		return;
 	delim = mdoc_isdelim(lc);
 	if (delim == DELIM_NONE || delim == DELIM_OPEN)
 		return;
 
 	/*
 	 * Reduce false positives by allowing various cases.
 	 */
 
 	/* Escaped delimiters. */
 	if (lc > nch->string + 1 && lc[-2] == '\\' &&
 	    (lc[-1] == '&' || lc[-1] == 'e'))
 		return;
 
 	/* Specific byte sequences. */
 	switch (*lc) {
 	case ')':
 		for (cp = lc; cp >= nch->string; cp--)
 			if (*cp == '(')
 				return;
 		break;
 	case '.':
 		if (lc > nch->string + 1 && lc[-2] == '.' && lc[-1] == '.')
 			return;
 		if (lc[-1] == '.')
 			return;
 		break;
 	case ';':
 		if (tok == MDOC_Vt)
 			return;
 		break;
 	case '?':
 		if (lc[-1] == '?')
 			return;
 		break;
 	case ']':
 		for (cp = lc; cp >= nch->string; cp--)
 			if (*cp == '[')
 				return;
 		break;
 	case '|':
 		if (lc == nch->string + 1 && lc[-1] == '|')
 			return;
 	default:
 		break;
 	}
 
 	/* Exactly two non-alphanumeric bytes. */
 	if (lc == nch->string + 1 && !isalnum((unsigned char)lc[-1]))
 		return;
 
 	/* At least three alphabetic words with a sentence ending. */
 	if (strchr("!.:?", *lc) != NULL && (tok == MDOC_Em ||
 	    tok == MDOC_Li || tok == MDOC_Pq || tok == MDOC_Sy)) {
 		nw = 0;
 		for (cp = lc - 1; cp >= nch->string; cp--) {
 			if (*cp == ' ') {
 				nw++;
 				if (cp > nch->string && cp[-1] == ',')
 					cp--;
 			} else if (isalpha((unsigned int)*cp)) {
 				if (nw > 1)
 					return;
 			} else
 				break;
 		}
 	}
 
 	mandoc_msg(MANDOCERR_DELIM_NB, nch->line,
 	    nch->pos + (int)(lc - nch->string), "%s%s %s", roff_name[tok],
 	    nch == mdoc->last->child ? "" : " ...", nch->string);
 }
 
 static void
 post_bl_norm(POST_ARGS)
 {
 	struct roff_node *n;
 	struct mdoc_argv *argv, *wa;
 	int		  i;
 	enum mdocargt	  mdoclt;
 	enum mdoc_list	  lt;
 
 	n = mdoc->last->parent;
 	n->norm->Bl.type = LIST__NONE;
 
 	/*
 	 * First figure out which kind of list to use: bind ourselves to
 	 * the first mentioned list type and warn about any remaining
 	 * ones.  If we find no list type, we default to LIST_item.
 	 */
 
 	wa = (n->args == NULL) ? NULL : n->args->argv;
 	mdoclt = MDOC_ARG_MAX;
 	for (i = 0; n->args && i < (int)n->args->argc; i++) {
 		argv = n->args->argv + i;
 		lt = LIST__NONE;
 		switch (argv->arg) {
 		/* Set list types. */
 		case MDOC_Bullet:
 			lt = LIST_bullet;
 			break;
 		case MDOC_Dash:
 			lt = LIST_dash;
 			break;
 		case MDOC_Enum:
 			lt = LIST_enum;
 			break;
 		case MDOC_Hyphen:
 			lt = LIST_hyphen;
 			break;
 		case MDOC_Item:
 			lt = LIST_item;
 			break;
 		case MDOC_Tag:
 			lt = LIST_tag;
 			break;
 		case MDOC_Diag:
 			lt = LIST_diag;
 			break;
 		case MDOC_Hang:
 			lt = LIST_hang;
 			break;
 		case MDOC_Ohang:
 			lt = LIST_ohang;
 			break;
 		case MDOC_Inset:
 			lt = LIST_inset;
 			break;
 		case MDOC_Column:
 			lt = LIST_column;
 			break;
 		/* Set list arguments. */
 		case MDOC_Compact:
 			if (n->norm->Bl.comp)
 				mandoc_msg(MANDOCERR_ARG_REP,
 				    argv->line, argv->pos, "Bl -compact");
 			n->norm->Bl.comp = 1;
 			break;
 		case MDOC_Width:
 			wa = argv;
 			if (0 == argv->sz) {
 				mandoc_msg(MANDOCERR_ARG_EMPTY,
 				    argv->line, argv->pos, "Bl -width");
 				n->norm->Bl.width = "0n";
 				break;
 			}
 			if (NULL != n->norm->Bl.width)
 				mandoc_msg(MANDOCERR_ARG_REP,
 				    argv->line, argv->pos,
 				    "Bl -width %s", argv->value[0]);
 			rewrite_macro2len(mdoc, argv->value);
 			n->norm->Bl.width = argv->value[0];
 			break;
 		case MDOC_Offset:
 			if (0 == argv->sz) {
 				mandoc_msg(MANDOCERR_ARG_EMPTY,
 				    argv->line, argv->pos, "Bl -offset");
 				break;
 			}
 			if (NULL != n->norm->Bl.offs)
 				mandoc_msg(MANDOCERR_ARG_REP,
 				    argv->line, argv->pos,
 				    "Bl -offset %s", argv->value[0]);
 			rewrite_macro2len(mdoc, argv->value);
 			n->norm->Bl.offs = argv->value[0];
 			break;
 		default:
 			continue;
 		}
 		if (LIST__NONE == lt)
 			continue;
 		mdoclt = argv->arg;
 
 		/* Check: multiple list types. */
 
 		if (LIST__NONE != n->norm->Bl.type) {
 			mandoc_msg(MANDOCERR_BL_REP, n->line, n->pos,
 			    "Bl -%s", mdoc_argnames[argv->arg]);
 			continue;
 		}
 
 		/* The list type should come first. */
 
 		if (n->norm->Bl.width ||
 		    n->norm->Bl.offs ||
 		    n->norm->Bl.comp)
 			mandoc_msg(MANDOCERR_BL_LATETYPE,
 			    n->line, n->pos, "Bl -%s",
 			    mdoc_argnames[n->args->argv[0].arg]);
 
 		n->norm->Bl.type = lt;
 		if (LIST_column == lt) {
 			n->norm->Bl.ncols = argv->sz;
 			n->norm->Bl.cols = (void *)argv->value;
 		}
 	}
 
 	/* Allow lists to default to LIST_item. */
 
 	if (LIST__NONE == n->norm->Bl.type) {
 		mandoc_msg(MANDOCERR_BL_NOTYPE, n->line, n->pos, "Bl");
 		n->norm->Bl.type = LIST_item;
 		mdoclt = MDOC_Item;
 	}
 
 	/*
 	 * Validate the width field.  Some list types don't need width
 	 * types and should be warned about them.  Others should have it
 	 * and must also be warned.  Yet others have a default and need
 	 * no warning.
 	 */
 
 	switch (n->norm->Bl.type) {
 	case LIST_tag:
 		if (n->norm->Bl.width == NULL)
 			mandoc_msg(MANDOCERR_BL_NOWIDTH,
 			    n->line, n->pos, "Bl -tag");
 		break;
 	case LIST_column:
 	case LIST_diag:
 	case LIST_ohang:
 	case LIST_inset:
 	case LIST_item:
 		if (n->norm->Bl.width != NULL)
 			mandoc_msg(MANDOCERR_BL_SKIPW, wa->line, wa->pos,
 			    "Bl -%s", mdoc_argnames[mdoclt]);
 		n->norm->Bl.width = NULL;
 		break;
 	case LIST_bullet:
 	case LIST_dash:
 	case LIST_hyphen:
 		if (n->norm->Bl.width == NULL)
 			n->norm->Bl.width = "2n";
 		break;
 	case LIST_enum:
 		if (n->norm->Bl.width == NULL)
 			n->norm->Bl.width = "3n";
 		break;
 	default:
 		break;
 	}
 }
 
 static void
 post_bd(POST_ARGS)
 {
 	struct roff_node *n;
 	struct mdoc_argv *argv;
 	int		  i;
 	enum mdoc_disp	  dt;
 
 	n = mdoc->last;
 	for (i = 0; n->args && i < (int)n->args->argc; i++) {
 		argv = n->args->argv + i;
 		dt = DISP__NONE;
 
 		switch (argv->arg) {
 		case MDOC_Centred:
 			dt = DISP_centered;
 			break;
 		case MDOC_Ragged:
 			dt = DISP_ragged;
 			break;
 		case MDOC_Unfilled:
 			dt = DISP_unfilled;
 			break;
 		case MDOC_Filled:
 			dt = DISP_filled;
 			break;
 		case MDOC_Literal:
 			dt = DISP_literal;
 			break;
 		case MDOC_File:
 			mandoc_msg(MANDOCERR_BD_FILE, n->line, n->pos, NULL);
 			break;
 		case MDOC_Offset:
 			if (0 == argv->sz) {
 				mandoc_msg(MANDOCERR_ARG_EMPTY,
 				    argv->line, argv->pos, "Bd -offset");
 				break;
 			}
 			if (NULL != n->norm->Bd.offs)
 				mandoc_msg(MANDOCERR_ARG_REP,
 				    argv->line, argv->pos,
 				    "Bd -offset %s", argv->value[0]);
 			rewrite_macro2len(mdoc, argv->value);
 			n->norm->Bd.offs = argv->value[0];
 			break;
 		case MDOC_Compact:
 			if (n->norm->Bd.comp)
 				mandoc_msg(MANDOCERR_ARG_REP,
 				    argv->line, argv->pos, "Bd -compact");
 			n->norm->Bd.comp = 1;
 			break;
 		default:
 			abort();
 		}
 		if (DISP__NONE == dt)
 			continue;
 
 		if (DISP__NONE == n->norm->Bd.type)
 			n->norm->Bd.type = dt;
 		else
 			mandoc_msg(MANDOCERR_BD_REP, n->line, n->pos,
 			    "Bd -%s", mdoc_argnames[argv->arg]);
 	}
 
 	if (DISP__NONE == n->norm->Bd.type) {
 		mandoc_msg(MANDOCERR_BD_NOTYPE, n->line, n->pos, "Bd");
 		n->norm->Bd.type = DISP_ragged;
 	}
 }
 
 /*
  * Stand-alone line macros.
  */
 
 static void
 post_an_norm(POST_ARGS)
 {
 	struct roff_node *n;
 	struct mdoc_argv *argv;
 	size_t	 i;
 
 	n = mdoc->last;
 	if (n->args == NULL)
 		return;
 
 	for (i = 1; i < n->args->argc; i++) {
 		argv = n->args->argv + i;
 		mandoc_msg(MANDOCERR_AN_REP, argv->line, argv->pos,
 		    "An -%s", mdoc_argnames[argv->arg]);
 	}
 
 	argv = n->args->argv;
 	if (argv->arg == MDOC_Split)
 		n->norm->An.auth = AUTH_split;
 	else if (argv->arg == MDOC_Nosplit)
 		n->norm->An.auth = AUTH_nosplit;
 	else
 		abort();
 }
 
 static void
 post_eoln(POST_ARGS)
 {
 	struct roff_node	*n;
 
 	post_useless(mdoc);
 	n = mdoc->last;
 	if (n->child != NULL)
 		mandoc_msg(MANDOCERR_ARG_SKIP, n->line,
 		    n->pos, "%s %s", roff_name[n->tok], n->child->string);
 
 	while (n->child != NULL)
 		roff_node_delete(mdoc, n->child);
 
 	roff_word_alloc(mdoc, n->line, n->pos, n->tok == MDOC_Bt ?
 	    "is currently in beta test." : "currently under development.");
 	mdoc->last->flags |= NODE_EOS | NODE_NOSRC;
 	mdoc->last = n;
 }
 
 static int
 build_list(struct roff_man *mdoc, int tok)
 {
 	struct roff_node	*n;
 	int			 ic;
 
 	n = mdoc->last->next;
 	for (ic = 1;; ic++) {
 		roff_elem_alloc(mdoc, n->line, n->pos, tok);
 		mdoc->last->flags |= NODE_NOSRC;
 		roff_node_relink(mdoc, n);
 		n = mdoc->last = mdoc->last->parent;
 		mdoc->next = ROFF_NEXT_SIBLING;
 		if (n->next == NULL)
 			return ic;
 		if (ic > 1 || n->next->next != NULL) {
 			roff_word_alloc(mdoc, n->line, n->pos, ",");
 			mdoc->last->flags |= NODE_DELIMC | NODE_NOSRC;
 		}
 		n = mdoc->last->next;
 		if (n->next == NULL) {
 			roff_word_alloc(mdoc, n->line, n->pos, "and");
 			mdoc->last->flags |= NODE_NOSRC;
 		}
 	}
 }
 
 static void
 post_ex(POST_ARGS)
 {
 	struct roff_node	*n;
 	int			 ic;
 
 	post_std(mdoc);
 
 	n = mdoc->last;
 	mdoc->next = ROFF_NEXT_CHILD;
 	roff_word_alloc(mdoc, n->line, n->pos, "The");
 	mdoc->last->flags |= NODE_NOSRC;
 
 	if (mdoc->last->next != NULL)
 		ic = build_list(mdoc, MDOC_Nm);
 	else if (mdoc->meta.name != NULL) {
 		roff_elem_alloc(mdoc, n->line, n->pos, MDOC_Nm);
 		mdoc->last->flags |= NODE_NOSRC;
 		roff_word_alloc(mdoc, n->line, n->pos, mdoc->meta.name);
 		mdoc->last->flags |= NODE_NOSRC;
 		mdoc->last = mdoc->last->parent;
 		mdoc->next = ROFF_NEXT_SIBLING;
 		ic = 1;
 	} else {
 		mandoc_msg(MANDOCERR_EX_NONAME, n->line, n->pos, "Ex");
 		ic = 0;
 	}
 
 	roff_word_alloc(mdoc, n->line, n->pos,
 	    ic > 1 ? "utilities exit\\~0" : "utility exits\\~0");
 	mdoc->last->flags |= NODE_NOSRC;
 	roff_word_alloc(mdoc, n->line, n->pos,
 	    "on success, and\\~>0 if an error occurs.");
 	mdoc->last->flags |= NODE_EOS | NODE_NOSRC;
 	mdoc->last = n;
 }
 
 static void
 post_lb(POST_ARGS)
 {
 	struct roff_node	*n, *nch;
 	const char		*ccp;
 	char			*cp;
 
 	post_delim_nb(mdoc);
 
 	n = mdoc->last;
 	nch = n->child;
 	assert(nch->type == ROFFT_TEXT);
 	mdoc->next = ROFF_NEXT_CHILD;
 
 	if (n->sec == SEC_SYNOPSIS) {
 		roff_word_alloc(mdoc, n->line, n->pos, "/*");
 		mdoc->last->flags = NODE_NOSRC;
 		while (nch != NULL) {
 			roff_word_alloc(mdoc, n->line, n->pos, "-l");
 			mdoc->last->flags = NODE_DELIMO | NODE_NOSRC;
 			mdoc->last = nch;
 			assert(nch->type == ROFFT_TEXT);
 			cp = nch->string;
  			if (strncmp(cp, "lib", 3) == 0)
 				memmove(cp, cp + 3, strlen(cp) - 3 + 1);
 			nch = nch->next;
 		}
 		roff_word_alloc(mdoc, n->line, n->pos, "*/");
 		mdoc->last->flags = NODE_NOSRC;
 		mdoc->last = n;
 		return;
 	}
 
 	if ((ccp = mdoc_a2lib(n->child->string)) != NULL) {
 		n->child->flags |= NODE_NOPRT;
 		roff_word_alloc(mdoc, n->line, n->pos, ccp);
 		mdoc->last->flags = NODE_NOSRC;
 		mdoc->last = n;
 		return;
 	}
 
 	mandoc_msg(MANDOCERR_LB_BAD, n->child->line,
 	    n->child->pos, "Lb %s", n->child->string);
 
 	roff_word_alloc(mdoc, n->line, n->pos, "library");
 	mdoc->last->flags = NODE_NOSRC;
 	roff_word_alloc(mdoc, n->line, n->pos, "\\(lq");
 	mdoc->last->flags = NODE_DELIMO | NODE_NOSRC;
 	mdoc->last = mdoc->last->next;
 	roff_word_alloc(mdoc, n->line, n->pos, "\\(rq");
 	mdoc->last->flags = NODE_DELIMC | NODE_NOSRC;
 	mdoc->last = n;
 }
 
 static void
 post_rv(POST_ARGS)
 {
 	struct roff_node	*n;
 	int			 ic;
 
 	post_std(mdoc);
 
 	n = mdoc->last;
 	mdoc->next = ROFF_NEXT_CHILD;
 	if (n->child != NULL) {
 		roff_word_alloc(mdoc, n->line, n->pos, "The");
 		mdoc->last->flags |= NODE_NOSRC;
 		ic = build_list(mdoc, MDOC_Fn);
 		roff_word_alloc(mdoc, n->line, n->pos,
 		    ic > 1 ? "functions return" : "function returns");
 		mdoc->last->flags |= NODE_NOSRC;
 		roff_word_alloc(mdoc, n->line, n->pos,
 		    "the value\\~0 if successful;");
 	} else
 		roff_word_alloc(mdoc, n->line, n->pos, "Upon successful "
 		    "completion, the value\\~0 is returned;");
 	mdoc->last->flags |= NODE_NOSRC;
 
 	roff_word_alloc(mdoc, n->line, n->pos, "otherwise "
 	    "the value\\~\\-1 is returned and the global variable");
 	mdoc->last->flags |= NODE_NOSRC;
 	roff_elem_alloc(mdoc, n->line, n->pos, MDOC_Va);
 	mdoc->last->flags |= NODE_NOSRC;
 	roff_word_alloc(mdoc, n->line, n->pos, "errno");
 	mdoc->last->flags |= NODE_NOSRC;
 	mdoc->last = mdoc->last->parent;
 	mdoc->next = ROFF_NEXT_SIBLING;
 	roff_word_alloc(mdoc, n->line, n->pos,
 	    "is set to indicate the error.");
 	mdoc->last->flags |= NODE_EOS | NODE_NOSRC;
 	mdoc->last = n;
 }
 
 static void
 post_std(POST_ARGS)
 {
 	struct roff_node *n;
 
 	post_delim(mdoc);
 
 	n = mdoc->last;
 	if (n->args && n->args->argc == 1)
 		if (n->args->argv[0].arg == MDOC_Std)
 			return;
 
 	mandoc_msg(MANDOCERR_ARG_STD, n->line, n->pos,
 	    "%s", roff_name[n->tok]);
 }
 
 static void
 post_st(POST_ARGS)
 {
 	struct roff_node	 *n, *nch;
 	const char		 *p;
 
 	n = mdoc->last;
 	nch = n->child;
 	assert(nch->type == ROFFT_TEXT);
 
 	if ((p = mdoc_a2st(nch->string)) == NULL) {
 		mandoc_msg(MANDOCERR_ST_BAD,
 		    nch->line, nch->pos, "St %s", nch->string);
 		roff_node_delete(mdoc, n);
 		return;
 	}
 
 	nch->flags |= NODE_NOPRT;
 	mdoc->next = ROFF_NEXT_CHILD;
 	roff_word_alloc(mdoc, nch->line, nch->pos, p);
 	mdoc->last->flags |= NODE_NOSRC;
 	mdoc->last= n;
 }
 
 static void
 post_tg(POST_ARGS)
 {
 	struct roff_node *n;	/* The .Tg node. */
 	struct roff_node *nch;	/* The first child of the .Tg node. */
 	struct roff_node *nn;   /* The next node after the .Tg node. */
 	struct roff_node *np;	/* The parent of the next node. */
 	struct roff_node *nt;	/* The TEXT node containing the tag. */
 	size_t		  len;	/* The number of bytes in the tag. */
 
 	/* Find the next node. */
 	n = mdoc->last;
 	for (nn = n; nn != NULL; nn = nn->parent) {
 		if (nn->type != ROFFT_HEAD && nn->type != ROFFT_BODY &&
 		    nn->type != ROFFT_TAIL && nn->next != NULL) {
 			nn = nn->next;
 			break;
 		}
 	}
 
 	/* Find the tag. */
 	nt = nch = n->child;
 	if (nch == NULL && nn != NULL && nn->child != NULL &&
 	    nn->child->type == ROFFT_TEXT)
 		nt = nn->child;
 
 	/* Validate the tag. */
 	if (nt == NULL || *nt->string == '\0')
 		mandoc_msg(MANDOCERR_MACRO_EMPTY, n->line, n->pos, "Tg");
 	if (nt == NULL) {
 		roff_node_delete(mdoc, n);
 		return;
 	}
 	len = strcspn(nt->string, " \t\\");
 	if (nt->string[len] != '\0')
 		mandoc_msg(MANDOCERR_TG_SPC, nt->line,
 		    nt->pos + len, "Tg %s", nt->string);
 
 	/* Keep only the first argument. */
 	if (nch != NULL && nch->next != NULL) {
 		mandoc_msg(MANDOCERR_ARG_EXCESS, nch->next->line,
 		    nch->next->pos, "Tg ... %s", nch->next->string);
 		while (nch->next != NULL)
 			roff_node_delete(mdoc, nch->next);
 	}
 
 	/* Drop the macro if the first argument is invalid. */
 	if (len == 0 || nt->string[len] != '\0') {
 		roff_node_delete(mdoc, n);
 		return;
 	}
 
 	/* By default, tag the .Tg node itself. */
 	if (nn == NULL || nn->flags & NODE_ID)
 		nn = n;
 
 	/* Explicit tagging of specific macros. */
 	switch (nn->tok) {
 	case MDOC_Sh:
 	case MDOC_Ss:
 	case MDOC_Fo:
 		nn = nn->head->child == NULL ? n : nn->head;
 		break;
 	case MDOC_It:
 		np = nn->parent;
 		while (np->tok != MDOC_Bl)
 			np = np->parent;
 		switch (np->norm->Bl.type) {
 		case LIST_column:
 			break;
 		case LIST_diag:
 		case LIST_hang:
 		case LIST_inset:
 		case LIST_ohang:
 		case LIST_tag:
 			nn = nn->head;
 			break;
 		case LIST_bullet:
 		case LIST_dash:
 		case LIST_enum:
 		case LIST_hyphen:
 		case LIST_item:
 			nn = nn->body->child == NULL ? n : nn->body;
 			break;
 		default:
 			abort();
 		}
 		break;
 	case MDOC_Bd:
 	case MDOC_Bl:
 	case MDOC_D1:
 	case MDOC_Dl:
 		nn = nn->body->child == NULL ? n : nn->body;
 		break;
 	case MDOC_Pp:
 		break;
 	case MDOC_Cm:
 	case MDOC_Dv:
 	case MDOC_Em:
 	case MDOC_Er:
 	case MDOC_Ev:
 	case MDOC_Fl:
 	case MDOC_Fn:
 	case MDOC_Ic:
 	case MDOC_Li:
 	case MDOC_Ms:
 	case MDOC_No:
 	case MDOC_Sy:
 		if (nn->child == NULL)
 			nn = n;
 		break;
 	default:
 		nn = n;
 		break;
 	}
 	tag_put(nt->string, TAG_MANUAL, nn);
 	if (nn != n)
 		n->flags |= NODE_NOPRT;
 }
 
 static void
 post_obsolete(POST_ARGS)
 {
 	struct roff_node *n;
 
 	n = mdoc->last;
 	if (n->type == ROFFT_ELEM || n->type == ROFFT_BLOCK)
 		mandoc_msg(MANDOCERR_MACRO_OBS, n->line, n->pos,
 		    "%s", roff_name[n->tok]);
 }
 
 static void
 post_useless(POST_ARGS)
 {
 	struct roff_node *n;
 
 	n = mdoc->last;
 	mandoc_msg(MANDOCERR_MACRO_USELESS, n->line, n->pos,
 	    "%s", roff_name[n->tok]);
 }
 
 /*
  * Block macros.
  */
 
 static void
 post_bf(POST_ARGS)
 {
 	struct roff_node *np, *nch;
 
 	/*
 	 * Unlike other data pointers, these are "housed" by the HEAD
 	 * element, which contains the goods.
 	 */
 
 	np = mdoc->last;
 	if (np->type != ROFFT_HEAD)
 		return;
 
 	assert(np->parent->type == ROFFT_BLOCK);
 	assert(np->parent->tok == MDOC_Bf);
 
 	/* Check the number of arguments. */
 
 	nch = np->child;
 	if (np->parent->args == NULL) {
 		if (nch == NULL) {
 			mandoc_msg(MANDOCERR_BF_NOFONT,
 			    np->line, np->pos, "Bf");
 			return;
 		}
 		nch = nch->next;
 	}
 	if (nch != NULL)
 		mandoc_msg(MANDOCERR_ARG_EXCESS,
 		    nch->line, nch->pos, "Bf ... %s", nch->string);
 
 	/* Extract argument into data. */
 
 	if (np->parent->args != NULL) {
 		switch (np->parent->args->argv[0].arg) {
 		case MDOC_Emphasis:
 			np->norm->Bf.font = FONT_Em;
 			break;
 		case MDOC_Literal:
 			np->norm->Bf.font = FONT_Li;
 			break;
 		case MDOC_Symbolic:
 			np->norm->Bf.font = FONT_Sy;
 			break;
 		default:
 			abort();
 		}
 		return;
 	}
 
 	/* Extract parameter into data. */
 
 	if ( ! strcmp(np->child->string, "Em"))
 		np->norm->Bf.font = FONT_Em;
 	else if ( ! strcmp(np->child->string, "Li"))
 		np->norm->Bf.font = FONT_Li;
 	else if ( ! strcmp(np->child->string, "Sy"))
 		np->norm->Bf.font = FONT_Sy;
 	else
 		mandoc_msg(MANDOCERR_BF_BADFONT, np->child->line,
 		    np->child->pos, "Bf %s", np->child->string);
 }
 
 static void
 post_fname(POST_ARGS)
 {
 	struct roff_node	*n, *nch;
 	const char		*cp;
 	size_t			 pos;
 
 	n = mdoc->last;
 	nch = n->child;
 	cp = nch->string;
 	if (*cp == '(') {
 		if (cp[strlen(cp + 1)] == ')')
 			return;
 		pos = 0;
 	} else {
 		pos = strcspn(cp, "()");
 		if (cp[pos] == '\0') {
 			if (n->sec == SEC_DESCRIPTION ||
 			    n->sec == SEC_CUSTOM)
 				tag_put(NULL, fn_prio++, n);
 			return;
 		}
 	}
 	mandoc_msg(MANDOCERR_FN_PAREN, nch->line, nch->pos + pos, "%s", cp);
 }
 
 static void
 post_fn(POST_ARGS)
 {
 	post_fname(mdoc);
 	post_fa(mdoc);
 }
 
 static void
 post_fo(POST_ARGS)
 {
 	const struct roff_node	*n;
 
 	n = mdoc->last;
 
 	if (n->type != ROFFT_HEAD)
 		return;
 
 	if (n->child == NULL) {
 		mandoc_msg(MANDOCERR_FO_NOHEAD, n->line, n->pos, "Fo");
 		return;
 	}
 	if (n->child != n->last) {
 		mandoc_msg(MANDOCERR_ARG_EXCESS,
 		    n->child->next->line, n->child->next->pos,
 		    "Fo ... %s", n->child->next->string);
 		while (n->child != n->last)
 			roff_node_delete(mdoc, n->last);
 	} else
 		post_delim(mdoc);
 
 	post_fname(mdoc);
 }
 
 static void
 post_fa(POST_ARGS)
 {
 	const struct roff_node *n;
 	const char *cp;
 
 	for (n = mdoc->last->child; n != NULL; n = n->next) {
 		for (cp = n->string; *cp != '\0'; cp++) {
 			/* Ignore callbacks and alterations. */
 			if (*cp == '(' || *cp == '{')
 				break;
 			if (*cp != ',')
 				continue;
 			mandoc_msg(MANDOCERR_FA_COMMA, n->line,
 			    n->pos + (int)(cp - n->string), "%s", n->string);
 			break;
 		}
 	}
 	post_delim_nb(mdoc);
 }
 
 static void
 post_nm(POST_ARGS)
 {
 	struct roff_node	*n;
 
 	n = mdoc->last;
 
 	if (n->sec == SEC_NAME && n->child != NULL &&
 	    n->child->type == ROFFT_TEXT && mdoc->meta.msec != NULL)
 		mandoc_xr_add(mdoc->meta.msec, n->child->string, -1, -1);
 
 	if (n->last != NULL && n->last->tok == MDOC_Pp)
 		roff_node_relink(mdoc, n->last);
 
 	if (mdoc->meta.name == NULL)
 		deroff(&mdoc->meta.name, n);
 
 	if (mdoc->meta.name == NULL ||
 	    (mdoc->lastsec == SEC_NAME && n->child == NULL))
 		mandoc_msg(MANDOCERR_NM_NONAME, n->line, n->pos, "Nm");
 
 	switch (n->type) {
 	case ROFFT_ELEM:
 		post_delim_nb(mdoc);
 		break;
 	case ROFFT_HEAD:
 		post_delim(mdoc);
 		break;
 	default:
 		return;
 	}
 
 	if ((n->child != NULL && n->child->type == ROFFT_TEXT) ||
 	    mdoc->meta.name == NULL)
 		return;
 
 	mdoc->next = ROFF_NEXT_CHILD;
 	roff_word_alloc(mdoc, n->line, n->pos, mdoc->meta.name);
 	mdoc->last->flags |= NODE_NOSRC;
 	mdoc->last = n;
 }
 
 static void
 post_nd(POST_ARGS)
 {
 	struct roff_node	*n;
 
 	n = mdoc->last;
 
 	if (n->type != ROFFT_BODY)
 		return;
 
 	if (n->sec != SEC_NAME)
 		mandoc_msg(MANDOCERR_ND_LATE, n->line, n->pos, "Nd");
 
 	if (n->child == NULL)
 		mandoc_msg(MANDOCERR_ND_EMPTY, n->line, n->pos, "Nd");
 	else
 		post_delim(mdoc);
 
 	post_hyph(mdoc);
 }
 
 static void
 post_display(POST_ARGS)
 {
 	struct roff_node *n, *np;
 
 	n = mdoc->last;
 	switch (n->type) {
 	case ROFFT_BODY:
 		if (n->end != ENDBODY_NOT) {
 			if (n->tok == MDOC_Bd &&
 			    n->body->parent->args == NULL)
 				roff_node_delete(mdoc, n);
 		} else if (n->child == NULL)
 			mandoc_msg(MANDOCERR_BLK_EMPTY, n->line, n->pos,
 			    "%s", roff_name[n->tok]);
 		else if (n->tok == MDOC_D1)
 			post_hyph(mdoc);
 		break;
 	case ROFFT_BLOCK:
 		if (n->tok == MDOC_Bd) {
 			if (n->args == NULL) {
 				mandoc_msg(MANDOCERR_BD_NOARG,
 				    n->line, n->pos, "Bd");
 				mdoc->next = ROFF_NEXT_SIBLING;
 				while (n->body->child != NULL)
 					roff_node_relink(mdoc,
 					    n->body->child);
 				roff_node_delete(mdoc, n);
 				break;
 			}
 			post_bd(mdoc);
 			post_prevpar(mdoc);
 		}
 		for (np = n->parent; np != NULL; np = np->parent) {
 			if (np->type == ROFFT_BLOCK && np->tok == MDOC_Bd) {
 				mandoc_msg(MANDOCERR_BD_NEST, n->line,
 				    n->pos, "%s in Bd", roff_name[n->tok]);
 				break;
 			}
 		}
 		break;
 	default:
 		break;
 	}
 }
 
 static void
 post_defaults(POST_ARGS)
 {
 	struct roff_node *n;
 
 	n = mdoc->last;
 	if (n->child != NULL) {
 		post_delim_nb(mdoc);
 		return;
 	}
 	mdoc->next = ROFF_NEXT_CHILD;
 	switch (n->tok) {
 	case MDOC_Ar:
 		roff_word_alloc(mdoc, n->line, n->pos, "file");
 		mdoc->last->flags |= NODE_NOSRC;
 		roff_word_alloc(mdoc, n->line, n->pos, "...");
 		break;
 	case MDOC_Pa:
 	case MDOC_Mt:
 		roff_word_alloc(mdoc, n->line, n->pos, "~");
 		break;
 	default:
 		abort();
 	}
 	mdoc->last->flags |= NODE_NOSRC;
 	mdoc->last = n;
 }
 
 static void
 post_at(POST_ARGS)
 {
 	struct roff_node	*n, *nch;
 	const char		*att;
 
 	n = mdoc->last;
 	nch = n->child;
 
 	/*
 	 * If we have a child, look it up in the standard keys.  If a
 	 * key exist, use that instead of the child; if it doesn't,
 	 * prefix "AT&T UNIX " to the existing data.
 	 */
 
 	att = NULL;
 	if (nch != NULL && ((att = mdoc_a2att(nch->string)) == NULL))
 		mandoc_msg(MANDOCERR_AT_BAD,
 		    nch->line, nch->pos, "At %s", nch->string);
 
 	mdoc->next = ROFF_NEXT_CHILD;
 	if (att != NULL) {
 		roff_word_alloc(mdoc, nch->line, nch->pos, att);
 		nch->flags |= NODE_NOPRT;
 	} else
 		roff_word_alloc(mdoc, n->line, n->pos, "AT&T UNIX");
 	mdoc->last->flags |= NODE_NOSRC;
 	mdoc->last = n;
 }
 
 static void
 post_an(POST_ARGS)
 {
 	struct roff_node *np, *nch;
 
 	post_an_norm(mdoc);
 
 	np = mdoc->last;
 	nch = np->child;
 	if (np->norm->An.auth == AUTH__NONE) {
 		if (nch == NULL)
 			mandoc_msg(MANDOCERR_MACRO_EMPTY,
 			    np->line, np->pos, "An");
 		else
 			post_delim_nb(mdoc);
 	} else if (nch != NULL)
 		mandoc_msg(MANDOCERR_ARG_EXCESS,
 		    nch->line, nch->pos, "An ... %s", nch->string);
 }
 
 static void
 post_em(POST_ARGS)
 {
 	post_tag(mdoc);
 	tag_put(NULL, TAG_FALLBACK, mdoc->last);
 }
 
 static void
 post_en(POST_ARGS)
 {
 	post_obsolete(mdoc);
 	if (mdoc->last->type == ROFFT_BLOCK)
 		mdoc->last->norm->Es = mdoc->last_es;
 }
 
 static void
 post_er(POST_ARGS)
 {
 	struct roff_node *n;
 
 	n = mdoc->last;
 	if (n->sec == SEC_ERRORS &&
 	    (n->parent->tok == MDOC_It ||
 	     (n->parent->tok == MDOC_Bq &&
 	      n->parent->parent->parent->tok == MDOC_It)))
 		tag_put(NULL, TAG_STRONG, n);
 	post_delim_nb(mdoc);
 }
 
 static void
 post_tag(POST_ARGS)
 {
 	struct roff_node *n;
 
 	n = mdoc->last;
 	if ((n->prev == NULL ||
 	     (n->prev->type == ROFFT_TEXT &&
 	      strcmp(n->prev->string, "|") == 0)) &&
 	    (n->parent->tok == MDOC_It ||
 	     (n->parent->tok == MDOC_Xo &&
 	      n->parent->parent->prev == NULL &&
 	      n->parent->parent->parent->tok == MDOC_It)))
 		tag_put(NULL, TAG_STRONG, n);
 	post_delim_nb(mdoc);
 }
 
 static void
 post_es(POST_ARGS)
 {
 	post_obsolete(mdoc);
 	mdoc->last_es = mdoc->last;
 }
 
 static void
 post_fl(POST_ARGS)
 {
 	struct roff_node	*n;
 	char			*cp;
 
 	/*
 	 * Transform ".Fl Fl long" to ".Fl \-long",
 	 * resulting for example in better HTML output.
 	 */
 
 	n = mdoc->last;
 	if (n->prev != NULL && n->prev->tok == MDOC_Fl &&
 	    n->prev->child == NULL && n->child != NULL &&
 	    (n->flags & NODE_LINE) == 0) {
 		mandoc_asprintf(&cp, "\\-%s", n->child->string);
 		free(n->child->string);
 		n->child->string = cp;
 		roff_node_delete(mdoc, n->prev);
 	}
 	post_tag(mdoc);
 }
 
 static void
 post_xx(POST_ARGS)
 {
 	struct roff_node	*n;
 	const char		*os;
 	char			*v;
 
 	post_delim_nb(mdoc);
 
 	n = mdoc->last;
 	switch (n->tok) {
 	case MDOC_Bsx:
 		os = "BSD/OS";
 		break;
 	case MDOC_Dx:
 		os = "DragonFly";
 		break;
 	case MDOC_Fx:
 		os = "FreeBSD";
 		break;
 	case MDOC_Nx:
 		os = "NetBSD";
 		if (n->child == NULL)
 			break;
 		v = n->child->string;
 		if ((v[0] != '0' && v[0] != '1') || v[1] != '.' ||
 		    v[2] < '0' || v[2] > '9' ||
 		    v[3] < 'a' || v[3] > 'z' || v[4] != '\0')
 			break;
 		n->child->flags |= NODE_NOPRT;
 		mdoc->next = ROFF_NEXT_CHILD;
 		roff_word_alloc(mdoc, n->child->line, n->child->pos, v);
 		v = mdoc->last->string;
 		v[3] = toupper((unsigned char)v[3]);
 		mdoc->last->flags |= NODE_NOSRC;
 		mdoc->last = n;
 		break;
 	case MDOC_Ox:
 		os = "OpenBSD";
 		break;
 	case MDOC_Ux:
-		os = "UNIX";
+		os = "Unix";
 		break;
 	default:
 		abort();
 	}
 	mdoc->next = ROFF_NEXT_CHILD;
 	roff_word_alloc(mdoc, n->line, n->pos, os);
 	mdoc->last->flags |= NODE_NOSRC;
 	mdoc->last = n;
 }
 
 static void
 post_it(POST_ARGS)
 {
 	struct roff_node *nbl, *nit, *nch;
 	int		  i, cols;
 	enum mdoc_list	  lt;
 
 	post_prevpar(mdoc);
 
 	nit = mdoc->last;
 	if (nit->type != ROFFT_BLOCK)
 		return;
 
 	nbl = nit->parent->parent;
 	lt = nbl->norm->Bl.type;
 
 	switch (lt) {
 	case LIST_tag:
 	case LIST_hang:
 	case LIST_ohang:
 	case LIST_inset:
 	case LIST_diag:
 		if (nit->head->child == NULL)
 			mandoc_msg(MANDOCERR_IT_NOHEAD,
 			    nit->line, nit->pos, "Bl -%s It",
 			    mdoc_argnames[nbl->args->argv[0].arg]);
 		break;
 	case LIST_bullet:
 	case LIST_dash:
 	case LIST_enum:
 	case LIST_hyphen:
 		if (nit->body == NULL || nit->body->child == NULL)
 			mandoc_msg(MANDOCERR_IT_NOBODY,
 			    nit->line, nit->pos, "Bl -%s It",
 			    mdoc_argnames[nbl->args->argv[0].arg]);
 		/* FALLTHROUGH */
 	case LIST_item:
 		if ((nch = nit->head->child) != NULL)
 			mandoc_msg(MANDOCERR_ARG_SKIP,
 			    nit->line, nit->pos, "It %s",
 			    nch->type == ROFFT_TEXT ? nch->string :
 			    roff_name[nch->tok]);
 		break;
 	case LIST_column:
 		cols = (int)nbl->norm->Bl.ncols;
 
 		assert(nit->head->child == NULL);
 
 		if (nit->head->next->child == NULL &&
 		    nit->head->next->next == NULL) {
 			mandoc_msg(MANDOCERR_MACRO_EMPTY,
 			    nit->line, nit->pos, "It");
 			roff_node_delete(mdoc, nit);
 			break;
 		}
 
 		i = 0;
 		for (nch = nit->child; nch != NULL; nch = nch->next) {
 			if (nch->type != ROFFT_BODY)
 				continue;
 			if (i++ && nch->flags & NODE_LINE)
 				mandoc_msg(MANDOCERR_TA_LINE,
 				    nch->line, nch->pos, "Ta");
 		}
 		if (i < cols || i > cols + 1)
 			mandoc_msg(MANDOCERR_BL_COL, nit->line, nit->pos,
 			    "%d columns, %d cells", cols, i);
 		else if (nit->head->next->child != NULL &&
 		    nit->head->next->child->flags & NODE_LINE)
 			mandoc_msg(MANDOCERR_IT_NOARG,
 			    nit->line, nit->pos, "Bl -column It");
 		break;
 	default:
 		abort();
 	}
 }
 
 static void
 post_bl_block(POST_ARGS)
 {
 	struct roff_node *n, *ni, *nc;
 
 	post_prevpar(mdoc);
 
 	n = mdoc->last;
 	for (ni = n->body->child; ni != NULL; ni = ni->next) {
 		if (ni->body == NULL)
 			continue;
 		nc = ni->body->last;
 		while (nc != NULL) {
 			switch (nc->tok) {
 			case MDOC_Pp:
 			case ROFF_br:
 				break;
 			default:
 				nc = NULL;
 				continue;
 			}
 			if (ni->next == NULL) {
 				mandoc_msg(MANDOCERR_PAR_MOVE, nc->line,
 				    nc->pos, "%s", roff_name[nc->tok]);
 				roff_node_relink(mdoc, nc);
 			} else if (n->norm->Bl.comp == 0 &&
 			    n->norm->Bl.type != LIST_column) {
 				mandoc_msg(MANDOCERR_PAR_SKIP,
 				    nc->line, nc->pos,
 				    "%s before It", roff_name[nc->tok]);
 				roff_node_delete(mdoc, nc);
 			} else
 				break;
 			nc = ni->body->last;
 		}
 	}
 }
 
 /*
  * If "in" begins with a dot, a word, and whitespace, return a dynamically
  * allocated copy of "in" that skips all of those.  Otherwise, return NULL.
  *
  * This is a partial workaround for the TODO list item beginning with:
  * - When the -width string contains macros, the macros must be rendered
  */
 static char *
 skip_leading_dot_word(const char *in)
 {
 	const char *iter = in;
 	const char *space;
 
 	if (*iter != '.')
 		return NULL;
 	iter++;
 
 	while (*iter != '\0' && !isspace(*iter))
 		iter++;
 	/*
 	 * If the dot was followed by space or NUL,
 	 * do not skip anything.
 	 */
 	if (iter == in + 1)
 		return NULL;
 
 	space = iter;
 	while (isspace(*iter))
 		iter++;
 	/*
 	 * If the word was not followed by space,
 	 * do not skip anything.
 	 */
 	if (iter == space)
 		return NULL;
 
 	return strdup(iter);
 }
 
 /*
  * If the argument of -offset or -width is a macro,
  * replace it with the associated default width.
  */
 static void
 rewrite_macro2len(struct roff_man *mdoc, char **arg)
 {
 	size_t		  width;
 	enum roff_tok	  tok;
 	char		 *newarg;
 
 	newarg = NULL;
 	if (*arg == NULL)
 		return;
 	else if ( ! strcmp(*arg, "Ds"))
 		width = 6;
 	else if ((tok = roffhash_find(mdoc->mdocmac, *arg, 0)) != TOKEN_NONE)
 		width = macro2len(tok);
 	else if ((newarg = skip_leading_dot_word(*arg)) == NULL)
 		return;
 
 	free(*arg);
 	if (newarg != NULL)
 		*arg = newarg;
 	else
 		mandoc_asprintf(arg, "%zun", width);
 }
 
 static void
 post_bl_head(POST_ARGS)
 {
 	struct roff_node *nbl, *nh, *nch, *nnext;
 	struct mdoc_argv *argv;
 	int		  i, j;
 
 	post_bl_norm(mdoc);
 
 	nh = mdoc->last;
 	if (nh->norm->Bl.type != LIST_column) {
 		if ((nch = nh->child) == NULL)
 			return;
 		mandoc_msg(MANDOCERR_ARG_EXCESS,
 		    nch->line, nch->pos, "Bl ... %s", nch->string);
 		while (nch != NULL) {
 			roff_node_delete(mdoc, nch);
 			nch = nh->child;
 		}
 		return;
 	}
 
 	/*
 	 * Append old-style lists, where the column width specifiers
 	 * trail as macro parameters, to the new-style ("normal-form")
 	 * lists where they're argument values following -column.
 	 */
 
 	if (nh->child == NULL)
 		return;
 
 	nbl = nh->parent;
 	for (j = 0; j < (int)nbl->args->argc; j++)
 		if (nbl->args->argv[j].arg == MDOC_Column)
 			break;
 
 	assert(j < (int)nbl->args->argc);
 
 	/*
 	 * Accommodate for new-style groff column syntax.  Shuffle the
 	 * child nodes, all of which must be TEXT, as arguments for the
 	 * column field.  Then, delete the head children.
 	 */
 
 	argv = nbl->args->argv + j;
 	i = argv->sz;
 	for (nch = nh->child; nch != NULL; nch = nch->next)
 		argv->sz++;
 	argv->value = mandoc_reallocarray(argv->value,
 	    argv->sz, sizeof(char *));
 
 	nh->norm->Bl.ncols = argv->sz;
 	nh->norm->Bl.cols = (void *)argv->value;
 
 	for (nch = nh->child; nch != NULL; nch = nnext) {
 		argv->value[i++] = nch->string;
 		nch->string = NULL;
 		nnext = nch->next;
 		roff_node_delete(NULL, nch);
 	}
 	nh->child = NULL;
 }
 
 static void
 post_bl(POST_ARGS)
 {
 	struct roff_node	*nbody;           /* of the Bl */
 	struct roff_node	*nchild, *nnext;  /* of the Bl body */
 	const char		*prev_Er;
 	int			 order;
 
 	nbody = mdoc->last;
 	switch (nbody->type) {
 	case ROFFT_BLOCK:
 		post_bl_block(mdoc);
 		return;
 	case ROFFT_HEAD:
 		post_bl_head(mdoc);
 		return;
 	case ROFFT_BODY:
 		break;
 	default:
 		return;
 	}
 	if (nbody->end != ENDBODY_NOT)
 		return;
 
 	/*
 	 * Up to the first item, move nodes before the list,
 	 * but leave transparent nodes where they are
 	 * if they precede an item.
 	 * The next non-transparent node is kept in nchild.
 	 * It only needs to be updated after a non-transparent
 	 * node was moved out, and at the very beginning
 	 * when no node at all was moved yet.
 	 */
 
 	nchild = mdoc->last;
 	for (;;) {
 		if (nchild == mdoc->last)
 			nchild = roff_node_child(nbody);
 		if (nchild == NULL) {
 			mdoc->last = nbody;
 			mandoc_msg(MANDOCERR_BLK_EMPTY,
 			    nbody->line, nbody->pos, "Bl");
 			return;
 		}
 		if (nchild->tok == MDOC_It) {
 			mdoc->last = nbody;
 			break;
 		}
 		mandoc_msg(MANDOCERR_BL_MOVE, nbody->child->line,
 		    nbody->child->pos, "%s", roff_name[nbody->child->tok]);
 		if (nbody->parent->prev == NULL) {
 			mdoc->last = nbody->parent->parent;
 			mdoc->next = ROFF_NEXT_CHILD;
 		} else {
 			mdoc->last = nbody->parent->prev;
 			mdoc->next = ROFF_NEXT_SIBLING;
 		}
 		roff_node_relink(mdoc, nbody->child);
 	}
 
 	/*
 	 * We have reached the first item,
 	 * so moving nodes out is no longer possible.
 	 * But in .Bl -column, the first rows may be implicit,
 	 * that is, they may not start with .It macros.
 	 * Such rows may be followed by nodes generated on the
 	 * roff level, for example .TS.
 	 * Wrap such roff nodes into an implicit row.
 	 */
 
 	while (nchild != NULL) {
 		if (nchild->tok == MDOC_It) {
 			nchild = roff_node_next(nchild);
 			continue;
 		}
 		nnext = nchild->next;
 		mdoc->last = nchild->prev;
 		mdoc->next = ROFF_NEXT_SIBLING;
 		roff_block_alloc(mdoc, nchild->line, nchild->pos, MDOC_It);
 		roff_head_alloc(mdoc, nchild->line, nchild->pos, MDOC_It);
 		mdoc->next = ROFF_NEXT_SIBLING;
 		roff_body_alloc(mdoc, nchild->line, nchild->pos, MDOC_It);
 		while (nchild->tok != MDOC_It) {
 			roff_node_relink(mdoc, nchild);
 			if (nnext == NULL)
 				break;
 			nchild = nnext;
 			nnext = nchild->next;
 			mdoc->next = ROFF_NEXT_SIBLING;
 		}
 		mdoc->last = nbody;
 	}
 
 	if (mdoc->meta.os_e != MANDOC_OS_NETBSD)
 		return;
 
 	prev_Er = NULL;
 	for (nchild = nbody->child; nchild != NULL; nchild = nchild->next) {
 		if (nchild->tok != MDOC_It)
 			continue;
 		if ((nnext = nchild->head->child) == NULL)
 			continue;
 		if (nnext->type == ROFFT_BLOCK)
 			nnext = nnext->body->child;
 		if (nnext == NULL || nnext->tok != MDOC_Er)
 			continue;
 		nnext = nnext->child;
 		if (prev_Er != NULL) {
 			order = strcmp(prev_Er, nnext->string);
 			if (order > 0)
 				mandoc_msg(MANDOCERR_ER_ORDER,
 				    nnext->line, nnext->pos,
 				    "Er %s %s (NetBSD)",
 				    prev_Er, nnext->string);
 			else if (order == 0)
 				mandoc_msg(MANDOCERR_ER_REP,
 				    nnext->line, nnext->pos,
 				    "Er %s (NetBSD)", prev_Er);
 		}
 		prev_Er = nnext->string;
 	}
 }
 
 static void
 post_bk(POST_ARGS)
 {
 	struct roff_node	*n;
 
 	n = mdoc->last;
 
 	if (n->type == ROFFT_BLOCK && n->body->child == NULL) {
 		mandoc_msg(MANDOCERR_BLK_EMPTY, n->line, n->pos, "Bk");
 		roff_node_delete(mdoc, n);
 	}
 }
 
 static void
 post_sm(POST_ARGS)
 {
 	struct roff_node	*nch;
 
 	nch = mdoc->last->child;
 
 	if (nch == NULL) {
 		mdoc->flags ^= MDOC_SMOFF;
 		return;
 	}
 
 	assert(nch->type == ROFFT_TEXT);
 
 	if ( ! strcmp(nch->string, "on")) {
 		mdoc->flags &= ~MDOC_SMOFF;
 		return;
 	}
 	if ( ! strcmp(nch->string, "off")) {
 		mdoc->flags |= MDOC_SMOFF;
 		return;
 	}
 
 	mandoc_msg(MANDOCERR_SM_BAD, nch->line, nch->pos,
 	    "%s %s", roff_name[mdoc->last->tok], nch->string);
 	roff_node_relink(mdoc, nch);
 	return;
 }
 
 static void
 post_root(POST_ARGS)
 {
 	struct roff_node *n;
 
 	/* Add missing prologue data. */
 
 	if (mdoc->meta.date == NULL)
 		mdoc->meta.date = mandoc_normdate(NULL, NULL);
 
 	if (mdoc->meta.title == NULL) {
 		mandoc_msg(MANDOCERR_DT_NOTITLE, 0, 0, "EOF");
 		mdoc->meta.title = mandoc_strdup("UNTITLED");
 	}
 
 	if (mdoc->meta.vol == NULL)
 		mdoc->meta.vol = mandoc_strdup("LOCAL");
 
 	if (mdoc->meta.os == NULL) {
 		mandoc_msg(MANDOCERR_OS_MISSING, 0, 0, NULL);
 		mdoc->meta.os = mandoc_strdup("");
 	} else if (mdoc->meta.os_e &&
 	    (mdoc->meta.rcsids & (1 << mdoc->meta.os_e)) == 0)
 		mandoc_msg(MANDOCERR_RCS_MISSING, 0, 0,
 		    mdoc->meta.os_e == MANDOC_OS_OPENBSD ?
 		    "(OpenBSD)" : "(NetBSD)");
 
 	if (mdoc->meta.arch != NULL &&
 	    arch_valid(mdoc->meta.arch, mdoc->meta.os_e) == 0) {
 		n = mdoc->meta.first->child;
 		while (n->tok != MDOC_Dt ||
 		    n->child == NULL ||
 		    n->child->next == NULL ||
 		    n->child->next->next == NULL)
 			n = n->next;
 		n = n->child->next->next;
 		mandoc_msg(MANDOCERR_ARCH_BAD, n->line, n->pos,
 		    "Dt ... %s %s", mdoc->meta.arch,
 		    mdoc->meta.os_e == MANDOC_OS_OPENBSD ?
 		    "(OpenBSD)" : "(NetBSD)");
 	}
 
 	/* Check that we begin with a proper `Sh'. */
 
 	n = mdoc->meta.first->child;
 	while (n != NULL &&
 	    (n->type == ROFFT_COMMENT ||
 	     (n->tok >= MDOC_Dd &&
 	      mdoc_macro(n->tok)->flags & MDOC_PROLOGUE)))
 		n = n->next;
 
 	if (n == NULL)
 		mandoc_msg(MANDOCERR_DOC_EMPTY, 0, 0, NULL);
 	else if (n->tok != MDOC_Sh)
 		mandoc_msg(MANDOCERR_SEC_BEFORE, n->line, n->pos,
 		    "%s", roff_name[n->tok]);
 }
 
 static void
 post_rs(POST_ARGS)
 {
 	struct roff_node *np, *nch, *next, *prev;
 	int		  i, j;
 
 	np = mdoc->last;
 
 	if (np->type != ROFFT_BODY)
 		return;
 
 	if (np->child == NULL) {
 		mandoc_msg(MANDOCERR_RS_EMPTY, np->line, np->pos, "Rs");
 		return;
 	}
 
 	/*
 	 * The full `Rs' block needs special handling to order the
 	 * sub-elements according to `rsord'.  Pick through each element
 	 * and correctly order it.  This is an insertion sort.
 	 */
 
 	next = NULL;
 	for (nch = np->child->next; nch != NULL; nch = next) {
 		/* Determine order number of this child. */
 		for (i = 0; i < RSORD_MAX; i++)
 			if (rsord[i] == nch->tok)
 				break;
 
 		if (i == RSORD_MAX) {
 			mandoc_msg(MANDOCERR_RS_BAD, nch->line, nch->pos,
 			    "%s", roff_name[nch->tok]);
 			i = -1;
 		} else if (nch->tok == MDOC__J || nch->tok == MDOC__B)
 			np->norm->Rs.quote_T++;
 
 		/*
 		 * Remove this child from the chain.  This somewhat
 		 * repeats roff_node_unlink(), but since we're
 		 * just re-ordering, there's no need for the
 		 * full unlink process.
 		 */
 
 		if ((next = nch->next) != NULL)
 			next->prev = nch->prev;
 
 		if ((prev = nch->prev) != NULL)
 			prev->next = nch->next;
 
 		nch->prev = nch->next = NULL;
 
 		/*
 		 * Scan back until we reach a node that's
 		 * to be ordered before this child.
 		 */
 
 		for ( ; prev ; prev = prev->prev) {
 			/* Determine order of `prev'. */
 			for (j = 0; j < RSORD_MAX; j++)
 				if (rsord[j] == prev->tok)
 					break;
 			if (j == RSORD_MAX)
 				j = -1;
 
 			if (j <= i)
 				break;
 		}
 
 		/*
 		 * Set this child back into its correct place
 		 * in front of the `prev' node.
 		 */
 
 		nch->prev = prev;
 
 		if (prev == NULL) {
 			np->child->prev = nch;
 			nch->next = np->child;
 			np->child = nch;
 		} else {
 			if (prev->next)
 				prev->next->prev = nch;
 			nch->next = prev->next;
 			prev->next = nch;
 		}
 	}
 }
 
 /*
  * For some arguments of some macros,
  * convert all breakable hyphens into ASCII_HYPH.
  */
 static void
 post_hyph(POST_ARGS)
 {
 	struct roff_node	*n, *nch;
 	char			*cp;
 
 	n = mdoc->last;
 	for (nch = n->child; nch != NULL; nch = nch->next) {
 		if (nch->type != ROFFT_TEXT)
 			continue;
 		cp = nch->string;
 		if (*cp == '\0')
 			continue;
 		while (*(++cp) != '\0')
 			if (*cp == '-' &&
 			    isalpha((unsigned char)cp[-1]) &&
 			    isalpha((unsigned char)cp[1])) {
 				if (n->tag == NULL && n->flags & NODE_ID)
 					n->tag = mandoc_strdup(nch->string);
 				*cp = ASCII_HYPH;
 			}
 	}
 }
 
 static void
 post_ns(POST_ARGS)
 {
 	struct roff_node	*n;
 
 	n = mdoc->last;
 	if (n->flags & NODE_LINE ||
 	    (n->next != NULL && n->next->flags & NODE_DELIMC))
 		mandoc_msg(MANDOCERR_NS_SKIP, n->line, n->pos, NULL);
 }
 
 static void
 post_sx(POST_ARGS)
 {
 	post_delim(mdoc);
 	post_hyph(mdoc);
 }
 
 static void
 post_sh(POST_ARGS)
 {
 	post_section(mdoc);
 
 	switch (mdoc->last->type) {
 	case ROFFT_HEAD:
 		post_sh_head(mdoc);
 		break;
 	case ROFFT_BODY:
 		switch (mdoc->lastsec)  {
 		case SEC_NAME:
 			post_sh_name(mdoc);
 			break;
 		case SEC_SEE_ALSO:
 			post_sh_see_also(mdoc);
 			break;
 		case SEC_AUTHORS:
 			post_sh_authors(mdoc);
 			break;
 		default:
 			break;
 		}
 		break;
 	default:
 		break;
 	}
 }
 
 static void
 post_sh_name(POST_ARGS)
 {
 	struct roff_node *n;
 	int hasnm, hasnd;
 
 	hasnm = hasnd = 0;
 
 	for (n = mdoc->last->child; n != NULL; n = n->next) {
 		switch (n->tok) {
 		case MDOC_Nm:
 			if (hasnm && n->child != NULL)
 				mandoc_msg(MANDOCERR_NAMESEC_PUNCT,
 				    n->line, n->pos,
 				    "Nm %s", n->child->string);
 			hasnm = 1;
 			continue;
 		case MDOC_Nd:
 			hasnd = 1;
 			if (n->next != NULL)
 				mandoc_msg(MANDOCERR_NAMESEC_ND,
 				    n->line, n->pos, NULL);
 			break;
 		case TOKEN_NONE:
 			if (n->type == ROFFT_TEXT &&
 			    n->string[0] == ',' && n->string[1] == '\0' &&
 			    n->next != NULL && n->next->tok == MDOC_Nm) {
 				n = n->next;
 				continue;
 			}
 			/* FALLTHROUGH */
 		default:
 			mandoc_msg(MANDOCERR_NAMESEC_BAD,
 			    n->line, n->pos, "%s", roff_name[n->tok]);
 			continue;
 		}
 		break;
 	}
 
 	if ( ! hasnm)
 		mandoc_msg(MANDOCERR_NAMESEC_NONM,
 		    mdoc->last->line, mdoc->last->pos, NULL);
 	if ( ! hasnd)
 		mandoc_msg(MANDOCERR_NAMESEC_NOND,
 		    mdoc->last->line, mdoc->last->pos, NULL);
 }
 
 static void
 post_sh_see_also(POST_ARGS)
 {
 	const struct roff_node	*n;
 	const char		*name, *sec;
 	const char		*lastname, *lastsec, *lastpunct;
 	int			 cmp;
 
 	n = mdoc->last->child;
 	lastname = lastsec = lastpunct = NULL;
 	while (n != NULL) {
 		if (n->tok != MDOC_Xr ||
 		    n->child == NULL ||
 		    n->child->next == NULL)
 			break;
 
 		/* Process one .Xr node. */
 
 		name = n->child->string;
 		sec = n->child->next->string;
 		if (lastsec != NULL) {
 			if (lastpunct[0] != ',' || lastpunct[1] != '\0')
 				mandoc_msg(MANDOCERR_XR_PUNCT, n->line,
 				    n->pos, "%s before %s(%s)",
 				    lastpunct, name, sec);
 			cmp = strcmp(lastsec, sec);
 			if (cmp > 0)
 				mandoc_msg(MANDOCERR_XR_ORDER, n->line,
 				    n->pos, "%s(%s) after %s(%s)",
 				    name, sec, lastname, lastsec);
 			else if (cmp == 0 &&
 			    strcasecmp(lastname, name) > 0)
 				mandoc_msg(MANDOCERR_XR_ORDER, n->line,
 				    n->pos, "%s after %s", name, lastname);
 		}
 		lastname = name;
 		lastsec = sec;
 
 		/* Process the following node. */
 
 		n = n->next;
 		if (n == NULL)
 			break;
 		if (n->tok == MDOC_Xr) {
 			lastpunct = "none";
 			continue;
 		}
 		if (n->type != ROFFT_TEXT)
 			break;
 		for (name = n->string; *name != '\0'; name++)
 			if (isalpha((const unsigned char)*name))
 				return;
 		lastpunct = n->string;
 		if (n->next == NULL || n->next->tok == MDOC_Rs)
 			mandoc_msg(MANDOCERR_XR_PUNCT, n->line,
 			    n->pos, "%s after %s(%s)",
 			    lastpunct, lastname, lastsec);
 		n = n->next;
 	}
 }
 
 static int
 child_an(const struct roff_node *n)
 {
 
 	for (n = n->child; n != NULL; n = n->next)
 		if ((n->tok == MDOC_An && n->child != NULL) || child_an(n))
 			return 1;
 	return 0;
 }
 
 static void
 post_sh_authors(POST_ARGS)
 {
 
 	if ( ! child_an(mdoc->last))
 		mandoc_msg(MANDOCERR_AN_MISSING,
 		    mdoc->last->line, mdoc->last->pos, NULL);
 }
 
 /*
  * Return an upper bound for the string distance (allowing
  * transpositions).  Not a full Levenshtein implementation
  * because Levenshtein is quadratic in the string length
  * and this function is called for every standard name,
  * so the check for each custom name would be cubic.
  * The following crude heuristics is linear, resulting
  * in quadratic behaviour for checking one custom name,
  * which does not cause measurable slowdown.
  */
 static int
 similar(const char *s1, const char *s2)
 {
 	const int	maxdist = 3;
 	int		dist = 0;
 
 	while (s1[0] != '\0' && s2[0] != '\0') {
 		if (s1[0] == s2[0]) {
 			s1++;
 			s2++;
 			continue;
 		}
 		if (++dist > maxdist)
 			return INT_MAX;
 		if (s1[1] == s2[1]) {  /* replacement */
 			s1++;
 			s2++;
 		} else if (s1[0] == s2[1] && s1[1] == s2[0]) {
 			s1 += 2;	/* transposition */
 			s2 += 2;
 		} else if (s1[0] == s2[1])  /* insertion */
 			s2++;
 		else if (s1[1] == s2[0])  /* deletion */
 			s1++;
 		else
 			return INT_MAX;
 	}
 	dist += strlen(s1) + strlen(s2);
 	return dist > maxdist ? INT_MAX : dist;
 }
 
 static void
 post_sh_head(POST_ARGS)
 {
 	struct roff_node	*nch;
 	const char		*goodsec;
 	const char *const	*testsec;
 	int			 dist, mindist;
 	enum roff_sec		 sec;
 
 	/*
 	 * Process a new section.  Sections are either "named" or
 	 * "custom".  Custom sections are user-defined, while named ones
 	 * follow a conventional order and may only appear in certain
 	 * manual sections.
 	 */
 
 	sec = mdoc->last->sec;
 
 	/* The NAME should be first. */
 
 	if (sec != SEC_NAME && mdoc->lastnamed == SEC_NONE)
 		mandoc_msg(MANDOCERR_NAMESEC_FIRST,
 		    mdoc->last->line, mdoc->last->pos, "Sh %s",
 		    sec != SEC_CUSTOM ? secnames[sec] :
 		    (nch = mdoc->last->child) == NULL ? "" :
 		    nch->type == ROFFT_TEXT ? nch->string :
 		    roff_name[nch->tok]);
 
 	/* The SYNOPSIS gets special attention in other areas. */
 
 	if (sec == SEC_SYNOPSIS) {
 		roff_setreg(mdoc->roff, "nS", 1, '=');
 		mdoc->flags |= MDOC_SYNOPSIS;
 	} else {
 		roff_setreg(mdoc->roff, "nS", 0, '=');
 		mdoc->flags &= ~MDOC_SYNOPSIS;
 	}
 	if (sec == SEC_DESCRIPTION)
 		fn_prio = TAG_STRONG;
 
 	/* Mark our last section. */
 
 	mdoc->lastsec = sec;
 
 	/* We don't care about custom sections after this. */
 
 	if (sec == SEC_CUSTOM) {
 		if ((nch = mdoc->last->child) == NULL ||
 		    nch->type != ROFFT_TEXT || nch->next != NULL)
 			return;
 		goodsec = NULL;
 		mindist = INT_MAX;
 		for (testsec = secnames + 1; *testsec != NULL; testsec++) {
 			dist = similar(nch->string, *testsec);
 			if (dist < mindist) {
 				goodsec = *testsec;
 				mindist = dist;
 			}
 		}
 		if (goodsec != NULL)
 			mandoc_msg(MANDOCERR_SEC_TYPO, nch->line, nch->pos,
 			    "Sh %s instead of %s", nch->string, goodsec);
 		return;
 	}
 
 	/*
 	 * Check whether our non-custom section is being repeated or is
 	 * out of order.
 	 */
 
 	if (sec == mdoc->lastnamed)
 		mandoc_msg(MANDOCERR_SEC_REP, mdoc->last->line,
 		    mdoc->last->pos, "Sh %s", secnames[sec]);
 
 	if (sec < mdoc->lastnamed)
 		mandoc_msg(MANDOCERR_SEC_ORDER, mdoc->last->line,
 		    mdoc->last->pos, "Sh %s", secnames[sec]);
 
 	/* Mark the last named section. */
 
 	mdoc->lastnamed = sec;
 
 	/* Check particular section/manual conventions. */
 
 	if (mdoc->meta.msec == NULL)
 		return;
 
 	goodsec = NULL;
 	switch (sec) {
 	case SEC_ERRORS:
 		if (*mdoc->meta.msec == '4')
 			break;
 		goodsec = "2, 3, 4, 9";
 		/* FALLTHROUGH */
 	case SEC_RETURN_VALUES:
 	case SEC_LIBRARY:
 		if (*mdoc->meta.msec == '2')
 			break;
 		if (*mdoc->meta.msec == '3')
 			break;
 		if (NULL == goodsec)
 			goodsec = "2, 3, 9";
 		/* FALLTHROUGH */
 	case SEC_CONTEXT:
 		if (*mdoc->meta.msec == '9')
 			break;
 		if (NULL == goodsec)
 			goodsec = "9";
 		mandoc_msg(MANDOCERR_SEC_MSEC,
 		    mdoc->last->line, mdoc->last->pos,
 		    "Sh %s for %s only", secnames[sec], goodsec);
 		break;
 	default:
 		break;
 	}
 }
 
 static void
 post_xr(POST_ARGS)
 {
 	struct roff_node *n, *nch;
 
 	n = mdoc->last;
 	nch = n->child;
 	if (nch->next == NULL) {
 		mandoc_msg(MANDOCERR_XR_NOSEC,
 		    n->line, n->pos, "Xr %s", nch->string);
 	} else {
 		assert(nch->next == n->last);
 		if(mandoc_xr_add(nch->next->string, nch->string,
 		    nch->line, nch->pos))
 			mandoc_msg(MANDOCERR_XR_SELF,
 			    nch->line, nch->pos, "Xr %s %s",
 			    nch->string, nch->next->string);
 	}
 	post_delim_nb(mdoc);
 }
 
 static void
 post_section(POST_ARGS)
 {
 	struct roff_node *n, *nch;
 	char		 *cp, *tag;
 
 	n = mdoc->last;
 	switch (n->type) {
 	case ROFFT_BLOCK:
 		post_prevpar(mdoc);
 		return;
 	case ROFFT_HEAD:
 		tag = NULL;
 		deroff(&tag, n);
 		if (tag != NULL) {
 			for (cp = tag; *cp != '\0'; cp++)
 				if (*cp == ' ')
 					*cp = '_';
 			if ((nch = n->child) != NULL &&
 			    nch->type == ROFFT_TEXT &&
 			    strcmp(nch->string, tag) == 0)
 				tag_put(NULL, TAG_STRONG, n);
 			else
 				tag_put(tag, TAG_FALLBACK, n);
 			free(tag);
 		}
 		post_delim(mdoc);
 		post_hyph(mdoc);
 		return;
 	case ROFFT_BODY:
 		break;
 	default:
 		return;
 	}
 	if ((nch = n->child) != NULL &&
 	    (nch->tok == MDOC_Pp || nch->tok == ROFF_br ||
 	     nch->tok == ROFF_sp)) {
 		mandoc_msg(MANDOCERR_PAR_SKIP, nch->line, nch->pos,
 		    "%s after %s", roff_name[nch->tok],
 		    roff_name[n->tok]);
 		roff_node_delete(mdoc, nch);
 	}
 	if ((nch = n->last) != NULL &&
 	    (nch->tok == MDOC_Pp || nch->tok == ROFF_br)) {
 		mandoc_msg(MANDOCERR_PAR_SKIP, nch->line, nch->pos,
 		    "%s at the end of %s", roff_name[nch->tok],
 		    roff_name[n->tok]);
 		roff_node_delete(mdoc, nch);
 	}
 }
 
 static void
 post_prevpar(POST_ARGS)
 {
 	struct roff_node *n, *np;
 
 	n = mdoc->last;
 	if (n->type != ROFFT_ELEM && n->type != ROFFT_BLOCK)
 		return;
 	if ((np = roff_node_prev(n)) == NULL)
 		return;
 
 	/*
 	 * Don't allow `Pp' prior to a paragraph-type
 	 * block: `Pp' or non-compact `Bd' or `Bl'.
 	 */
 
 	if (np->tok != MDOC_Pp && np->tok != ROFF_br)
 		return;
 	if (n->tok == MDOC_Bl && n->norm->Bl.comp)
 		return;
 	if (n->tok == MDOC_Bd && n->norm->Bd.comp)
 		return;
 	if (n->tok == MDOC_It && n->parent->norm->Bl.comp)
 		return;
 
 	mandoc_msg(MANDOCERR_PAR_SKIP, np->line, np->pos,
 	    "%s before %s", roff_name[np->tok], roff_name[n->tok]);
 	roff_node_delete(mdoc, np);
 }
 
 static void
 post_par(POST_ARGS)
 {
 	struct roff_node *np;
 
 	fn_prio = TAG_STRONG;
 	post_prevpar(mdoc);
 
 	np = mdoc->last;
 	if (np->child != NULL)
 		mandoc_msg(MANDOCERR_ARG_SKIP, np->line, np->pos,
 		    "%s %s", roff_name[np->tok], np->child->string);
 }
 
 static void
 post_dd(POST_ARGS)
 {
 	struct roff_node *n;
 
 	n = mdoc->last;
 	n->flags |= NODE_NOPRT;
 
 	if (mdoc->meta.date != NULL) {
 		mandoc_msg(MANDOCERR_PROLOG_REP, n->line, n->pos, "Dd");
 		free(mdoc->meta.date);
 	} else if (mdoc->flags & MDOC_PBODY)
 		mandoc_msg(MANDOCERR_PROLOG_LATE, n->line, n->pos, "Dd");
 	else if (mdoc->meta.title != NULL)
 		mandoc_msg(MANDOCERR_PROLOG_ORDER,
 		    n->line, n->pos, "Dd after Dt");
 	else if (mdoc->meta.os != NULL)
 		mandoc_msg(MANDOCERR_PROLOG_ORDER,
 		    n->line, n->pos, "Dd after Os");
 
-	if (mdoc->quick && n != NULL)
+	if (mdoc->quick)
 		mdoc->meta.date = mandoc_strdup("");
 	else
 		mdoc->meta.date = mandoc_normdate(n->child, n);
 }
 
 static void
 post_dt(POST_ARGS)
 {
 	struct roff_node *nn, *n;
 	const char	 *cp;
 	char		 *p;
 
 	n = mdoc->last;
 	n->flags |= NODE_NOPRT;
 
 	if (mdoc->flags & MDOC_PBODY) {
 		mandoc_msg(MANDOCERR_DT_LATE, n->line, n->pos, "Dt");
 		return;
 	}
 
 	if (mdoc->meta.title != NULL)
 		mandoc_msg(MANDOCERR_PROLOG_REP, n->line, n->pos, "Dt");
 	else if (mdoc->meta.os != NULL)
 		mandoc_msg(MANDOCERR_PROLOG_ORDER,
 		    n->line, n->pos, "Dt after Os");
 
 	free(mdoc->meta.title);
 	free(mdoc->meta.msec);
 	free(mdoc->meta.vol);
 	free(mdoc->meta.arch);
 
 	mdoc->meta.title = NULL;
 	mdoc->meta.msec = NULL;
 	mdoc->meta.vol = NULL;
 	mdoc->meta.arch = NULL;
 
 	/* Mandatory first argument: title. */
 
 	nn = n->child;
 	if (nn == NULL || *nn->string == '\0') {
 		mandoc_msg(MANDOCERR_DT_NOTITLE, n->line, n->pos, "Dt");
 		mdoc->meta.title = mandoc_strdup("UNTITLED");
 	} else {
 		mdoc->meta.title = mandoc_strdup(nn->string);
 
 		/* Check that all characters are uppercase. */
 
 		for (p = nn->string; *p != '\0'; p++)
 			if (islower((unsigned char)*p)) {
 				mandoc_msg(MANDOCERR_TITLE_CASE, nn->line,
 				    nn->pos + (int)(p - nn->string),
 				    "Dt %s", nn->string);
 				break;
 			}
 	}
 
 	/* Mandatory second argument: section. */
 
 	if (nn != NULL)
 		nn = nn->next;
 
 	if (nn == NULL) {
 		mandoc_msg(MANDOCERR_MSEC_MISSING, n->line, n->pos,
 		    "Dt %s", mdoc->meta.title);
-		mdoc->meta.vol = mandoc_strdup("LOCAL");
-		return;  /* msec and arch remain NULL. */
+		return;  /* msec, vol, and arch remain NULL. */
 	}
 
 	mdoc->meta.msec = mandoc_strdup(nn->string);
 
 	/* Infer volume title from section number. */
 
 	cp = mandoc_a2msec(nn->string);
 	if (cp == NULL) {
 		mandoc_msg(MANDOCERR_MSEC_BAD,
 		    nn->line, nn->pos, "Dt ... %s", nn->string);
-		mdoc->meta.vol = mandoc_strdup(nn->string);
 	} else {
 		mdoc->meta.vol = mandoc_strdup(cp);
 		if (mdoc->filesec != '\0' &&
 		    mdoc->filesec != *nn->string &&
 		    *nn->string >= '1' && *nn->string <= '9')
 			mandoc_msg(MANDOCERR_MSEC_FILE, nn->line, nn->pos,
 			    "*.%c vs Dt ... %c", mdoc->filesec, *nn->string);
 	}
 
 	/* Optional third argument: architecture. */
 
 	if ((nn = nn->next) == NULL)
 		return;
 
 	for (p = nn->string; *p != '\0'; p++)
 		*p = tolower((unsigned char)*p);
 	mdoc->meta.arch = mandoc_strdup(nn->string);
 
 	/* Ignore fourth and later arguments. */
 
 	if ((nn = nn->next) != NULL)
 		mandoc_msg(MANDOCERR_ARG_EXCESS,
 		    nn->line, nn->pos, "Dt ... %s", nn->string);
 }
 
 static void
 post_bx(POST_ARGS)
 {
 	struct roff_node	*n, *nch;
 	const char		*macro;
 
 	post_delim_nb(mdoc);
 
 	n = mdoc->last;
 	nch = n->child;
 
 	if (nch != NULL) {
 		macro = !strcmp(nch->string, "Open") ? "Ox" :
 		    !strcmp(nch->string, "Net") ? "Nx" :
 		    !strcmp(nch->string, "Free") ? "Fx" :
 		    !strcmp(nch->string, "DragonFly") ? "Dx" : NULL;
 		if (macro != NULL)
 			mandoc_msg(MANDOCERR_BX,
 			    n->line, n->pos, "%s", macro);
 		mdoc->last = nch;
 		nch = nch->next;
 		mdoc->next = ROFF_NEXT_SIBLING;
 		roff_elem_alloc(mdoc, n->line, n->pos, MDOC_Ns);
 		mdoc->last->flags |= NODE_NOSRC;
 		mdoc->next = ROFF_NEXT_SIBLING;
 	} else
 		mdoc->next = ROFF_NEXT_CHILD;
 	roff_word_alloc(mdoc, n->line, n->pos, "BSD");
 	mdoc->last->flags |= NODE_NOSRC;
 
 	if (nch == NULL) {
 		mdoc->last = n;
 		return;
 	}
 
 	roff_elem_alloc(mdoc, n->line, n->pos, MDOC_Ns);
 	mdoc->last->flags |= NODE_NOSRC;
 	mdoc->next = ROFF_NEXT_SIBLING;
 	roff_word_alloc(mdoc, n->line, n->pos, "-");
 	mdoc->last->flags |= NODE_NOSRC;
 	roff_elem_alloc(mdoc, n->line, n->pos, MDOC_Ns);
 	mdoc->last->flags |= NODE_NOSRC;
 	mdoc->last = n;
 
 	/*
 	 * Make `Bx's second argument always start with an uppercase
 	 * letter.  Groff checks if it's an "accepted" term, but we just
 	 * uppercase blindly.
 	 */
 
 	*nch->string = (char)toupper((unsigned char)*nch->string);
 }
 
 static void
 post_os(POST_ARGS)
 {
 #ifndef OSNAME
 	struct utsname	  utsname;
 #endif
 	struct roff_node *n;
 
 	n = mdoc->last;
 	n->flags |= NODE_NOPRT;
 
 	if (mdoc->meta.os != NULL)
 		mandoc_msg(MANDOCERR_PROLOG_REP, n->line, n->pos, "Os");
 	else if (mdoc->flags & MDOC_PBODY)
 		mandoc_msg(MANDOCERR_PROLOG_LATE, n->line, n->pos, "Os");
 
 	post_delim(mdoc);
 
 	/*
 	 * Set the operating system by way of the `Os' macro.
 	 * The order of precedence is:
 	 * 1. the argument of the `Os' macro, unless empty
 	 * 2. the -Ios=foo command line argument, if provided
 	 * 3. -DOSNAME="\"foo\"", if provided during compilation
 	 * 4. "sysname release" from uname(3)
 	 */
 
 	free(mdoc->meta.os);
 	mdoc->meta.os = NULL;
 	deroff(&mdoc->meta.os, n);
 	if (mdoc->meta.os)
 		goto out;
 
 	if (mdoc->os_s != NULL) {
 		mdoc->meta.os = mandoc_strdup(mdoc->os_s);
 		goto out;
 	}
 
 #ifdef OSNAME
 	mdoc->meta.os = mandoc_strdup(OSNAME);
 #else /*!OSNAME */
 	if (mdoc->os_r == NULL) {
 		if (uname(&utsname) == -1) {
 			mandoc_msg(MANDOCERR_OS_UNAME, n->line, n->pos, "Os");
 			mdoc->os_r = mandoc_strdup("UNKNOWN");
 		} else
 			mandoc_asprintf(&mdoc->os_r, "%s %s",
 			    utsname.sysname, utsname.release);
 	}
 	mdoc->meta.os = mandoc_strdup(mdoc->os_r);
 #endif /*!OSNAME*/
 
 out:
 	if (mdoc->meta.os_e == MANDOC_OS_OTHER) {
 		if (strstr(mdoc->meta.os, "OpenBSD") != NULL)
 			mdoc->meta.os_e = MANDOC_OS_OPENBSD;
 		else if (strstr(mdoc->meta.os, "NetBSD") != NULL)
 			mdoc->meta.os_e = MANDOC_OS_NETBSD;
 	}
 
 	/*
 	 * This is the earliest point where we can check
 	 * Mdocdate conventions because we don't know
 	 * the operating system earlier.
 	 */
 
 	if (n->child != NULL)
 		mandoc_msg(MANDOCERR_OS_ARG, n->child->line, n->child->pos,
 		    "Os %s (%s)", n->child->string,
 		    mdoc->meta.os_e == MANDOC_OS_OPENBSD ?
 		    "OpenBSD" : "NetBSD");
 
 	while (n->tok != MDOC_Dd)
 		if ((n = n->prev) == NULL)
 			return;
 	if ((n = n->child) == NULL)
 		return;
 	if (strncmp(n->string, "$" "Mdocdate", 9)) {
 		if (mdoc->meta.os_e == MANDOC_OS_OPENBSD)
 			mandoc_msg(MANDOCERR_MDOCDATE_MISSING, n->line,
 			    n->pos, "Dd %s (OpenBSD)", n->string);
 	} else {
 		if (mdoc->meta.os_e == MANDOC_OS_NETBSD)
 			mandoc_msg(MANDOCERR_MDOCDATE, n->line,
 			    n->pos, "Dd %s (NetBSD)", n->string);
 	}
 }
 
 enum roff_sec
 mdoc_a2sec(const char *p)
 {
 	int		 i;
 
 	for (i = 0; i < (int)SEC__MAX; i++)
 		if (secnames[i] && 0 == strcmp(p, secnames[i]))
 			return (enum roff_sec)i;
 
 	return SEC_CUSTOM;
 }
 
 static size_t
 macro2len(enum roff_tok macro)
 {
 
 	switch (macro) {
 	case MDOC_Ad:
 		return 12;
 	case MDOC_Ao:
 		return 12;
 	case MDOC_An:
 		return 12;
 	case MDOC_Aq:
 		return 12;
 	case MDOC_Ar:
 		return 12;
 	case MDOC_Bo:
 		return 12;
 	case MDOC_Bq:
 		return 12;
 	case MDOC_Cd:
 		return 12;
 	case MDOC_Cm:
 		return 10;
 	case MDOC_Do:
 		return 10;
 	case MDOC_Dq:
 		return 12;
 	case MDOC_Dv:
 		return 12;
 	case MDOC_Eo:
 		return 12;
 	case MDOC_Em:
 		return 10;
 	case MDOC_Er:
 		return 17;
 	case MDOC_Ev:
 		return 15;
 	case MDOC_Fa:
 		return 12;
 	case MDOC_Fl:
 		return 10;
 	case MDOC_Fo:
 		return 16;
 	case MDOC_Fn:
 		return 16;
 	case MDOC_Ic:
 		return 10;
 	case MDOC_Li:
 		return 16;
 	case MDOC_Ms:
 		return 6;
 	case MDOC_Nm:
 		return 10;
 	case MDOC_No:
 		return 12;
 	case MDOC_Oo:
 		return 10;
 	case MDOC_Op:
 		return 14;
 	case MDOC_Pa:
 		return 32;
 	case MDOC_Pf:
 		return 12;
 	case MDOC_Po:
 		return 12;
 	case MDOC_Pq:
 		return 12;
 	case MDOC_Ql:
 		return 16;
 	case MDOC_Qo:
 		return 12;
 	case MDOC_So:
 		return 12;
 	case MDOC_Sq:
 		return 12;
 	case MDOC_Sy:
 		return 6;
 	case MDOC_Sx:
 		return 16;
 	case MDOC_Tn:
 		return 10;
 	case MDOC_Va:
 		return 12;
 	case MDOC_Vt:
 		return 12;
 	case MDOC_Xr:
 		return 10;
 	default:
 		break;
 	}
 	return 0;
 }
diff --git a/contrib/mandoc/out.c b/contrib/mandoc/out.c
index f6f5859a1629..21c282b2141b 100644
--- a/contrib/mandoc/out.c
+++ b/contrib/mandoc/out.c
@@ -1,549 +1,570 @@
-/*	$Id: out.c,v 1.86 2025/01/05 18:14:39 schwarze Exp $ */
+/* $Id: out.c,v 1.87 2025/07/16 14:33:08 schwarze Exp $ */
 /*
- * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons 
- * Copyright (c) 2011, 2014, 2015, 2017, 2018, 2019, 2021
+ * Copyright (c) 2011, 2014, 2015, 2017, 2018, 2019, 2021, 2025
  *               Ingo Schwarze 
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "tbl.h"
 #include "out.h"
 
 struct	tbl_colgroup {
 	struct tbl_colgroup	*next;
 	size_t			 wanted;
 	int			 startcol;
 	int			 endcol;
 };
 
 static	size_t	tblcalc_data(struct rofftbl *, struct roffcol *,
 			const struct tbl_opts *, const struct tbl_dat *,
 			size_t);
 static	size_t	tblcalc_literal(struct rofftbl *, struct roffcol *,
 			const struct tbl_dat *, size_t);
 static	size_t	tblcalc_number(struct rofftbl *, struct roffcol *,
 			const struct tbl_opts *, const struct tbl_dat *);
 
 
 /*
  * Parse the *src string and store a scaling unit into *dst.
  * If the string doesn't specify the unit, use the default.
  * If no default is specified, fail.
  * Return a pointer to the byte after the last byte used,
  * or NULL on total failure.
  */
 const char *
 a2roffsu(const char *src, struct roffsu *dst, enum roffscale def)
 {
 	char		*endptr;
 
 	dst->unit = def == SCALE_MAX ? SCALE_BU : def;
 	dst->scale = strtod(src, &endptr);
 	if (endptr == src)
 		return NULL;
 
 	switch (*endptr++) {
 	case 'c':
 		dst->unit = SCALE_CM;
 		break;
 	case 'i':
 		dst->unit = SCALE_IN;
 		break;
 	case 'f':
 		dst->unit = SCALE_FS;
 		break;
 	case 'M':
 		dst->unit = SCALE_MM;
 		break;
 	case 'm':
 		dst->unit = SCALE_EM;
 		break;
 	case 'n':
 		dst->unit = SCALE_EN;
 		break;
 	case 'P':
 		dst->unit = SCALE_PC;
 		break;
 	case 'p':
 		dst->unit = SCALE_PT;
 		break;
 	case 'u':
 		dst->unit = SCALE_BU;
 		break;
 	case 'v':
 		dst->unit = SCALE_VS;
 		break;
 	default:
 		endptr--;
 		if (SCALE_MAX == def)
 			return NULL;
 		dst->unit = def;
 		break;
 	}
 	return endptr;
 }
 
 /*
  * Calculate the abstract widths and decimal positions of columns in a
  * table.  This routine allocates the columns structures then runs over
  * all rows and cells in the table.  The function pointers in "tbl" are
  * used for the actual width calculations.
  */
 void
 tblcalc(struct rofftbl *tbl, const struct tbl_span *sp_first,
     size_t offset, size_t rmargin)
 {
 	const struct tbl_opts	*opts;
 	const struct tbl_span	*sp;
 	const struct tbl_dat	*dp;
 	struct roffcol		*col;
 	struct tbl_colgroup	*first_group, **gp, *g;
-	size_t			*colwidth;
-	size_t			 ewidth, min1, min2, wanted, width, xwidth;
-	int			 done, icol, maxcol, necol, nxcol, quirkcol;
+
+	/* Widths in basic units. */
+	size_t	*colwidth; /* Widths of all columns. */
+	size_t	 min1;     /* Width of the narrowest column. */
+	size_t	 min2;     /* Width of the second narrowest column. */
+	size_t	 wanted;   /* For any of the narrowest columns. */
+	size_t	 xwidth;   /* Total width of columns not to expand. */
+	size_t	 ewidth;   /* Width of widest column to equalize. */
+	size_t	 width;    /* Width of the data in basic units. */
+	size_t	 enw;      /* Width of one EN unit. */
+
+	int	 icol;     /* Column number, starting at zero. */
+	int	 maxcol;   /* Number of last column. */
+	int	 necol;    /* Number of columns to equalize. */
+	int	 nxcol;    /* Number of columns to expand. */
+	int	 done;	   /* Boolean: this group is wide enough. */
+	int	 quirkcol;
 
 	/*
 	 * Allocate the master column specifiers.  These will hold the
 	 * widths and decimal positions for all cells in the column.  It
 	 * must be freed and nullified by the caller.
 	 */
 
 	assert(tbl->cols == NULL);
 	tbl->cols = mandoc_calloc((size_t)sp_first->opts->cols,
 	    sizeof(struct roffcol));
 	opts = sp_first->opts;
 
 	maxcol = -1;
 	first_group = NULL;
+	enw = (*tbl->len)(1, tbl->arg);
 	for (sp = sp_first; sp != NULL; sp = sp->next) {
 		if (sp->pos != TBL_SPAN_DATA)
 			continue;
 
 		/*
 		 * Account for the data cells in the layout, matching it
 		 * to data cells in the data section.
 		 */
 
 		for (dp = sp->first; dp != NULL; dp = dp->next) {
 			icol = dp->layout->col;
 			while (maxcol < icol + dp->hspans)
 				tbl->cols[++maxcol].spacing = SIZE_MAX;
 			col = tbl->cols + icol;
 			col->flags |= dp->layout->flags;
 			if (dp->layout->flags & TBL_CELL_WIGN)
 				continue;
 
 			/* Handle explicit width specifications. */
 			if (col->width < dp->layout->width)
 				col->width = dp->layout->width;
 			if (dp->layout->spacing != SIZE_MAX &&
 			    (col->spacing == SIZE_MAX ||
 			     col->spacing < dp->layout->spacing))
 				col->spacing = dp->layout->spacing;
 
 			/*
 			 * Calculate an automatic width.
 			 * Except for spanning cells, apply it.
 			 */
 
 			width = tblcalc_data(tbl,
 			    dp->hspans == 0 ? col : NULL,
 			    opts, dp,
 			    dp->block == 0 ? 0 :
 			    dp->layout->width ? dp->layout->width :
-			    rmargin ? (rmargin + sp->opts->cols / 2)
-			    / (sp->opts->cols + 1) : 0);
+			    rmargin ? (rmargin / enw + sp->opts->cols / 2) /
+			    (sp->opts->cols + 1) * enw : 0);
 			if (dp->hspans == 0)
 				continue;
 
 			/*
 			 * Build a singly linked list
 			 * of all groups of columns joined by spans,
 			 * recording the minimum width for each group.
 			 */
 
 			gp = &first_group;
 			while (*gp != NULL && ((*gp)->startcol != icol ||
 			    (*gp)->endcol != icol + dp->hspans))
 				gp = &(*gp)->next;
 			if (*gp == NULL) {
 				g = mandoc_malloc(sizeof(*g));
 				g->next = *gp;
 				g->wanted = width;
 				g->startcol = icol;
 				g->endcol = icol + dp->hspans;
 				*gp = g;
 			} else if ((*gp)->wanted < width)
 				(*gp)->wanted = width;
 		}
 	}
 
 	/*
 	 * The minimum width of columns explicitly specified
 	 * in the layout is 1n.
 	 */
 
 	if (maxcol < sp_first->opts->cols - 1)
 		maxcol = sp_first->opts->cols - 1;
 	for (icol = 0; icol <= maxcol; icol++) {
 		col = tbl->cols + icol;
-		if (col->width < 1)
-			col->width = 1;
+		if (col->width < enw)
+			col->width = enw;
 
 		/*
 		 * Column spacings are needed for span width
 		 * calculations, so set the default values now.
 		 */
 
 		if (col->spacing == SIZE_MAX || icol == maxcol)
 			col->spacing = 3;
 	}
 
 	/*
 	 * Replace the minimum widths with the missing widths,
 	 * and dismiss groups that are already wide enough.
 	 */
 
 	gp = &first_group;
 	while ((g = *gp) != NULL) {
 		done = 0;
 		for (icol = g->startcol; icol <= g->endcol; icol++) {
 			width = tbl->cols[icol].width;
 			if (icol < g->endcol)
-				width += tbl->cols[icol].spacing;
+				width += (*tbl->len)(tbl->cols[icol].spacing,
+				    tbl->arg);
 			if (g->wanted <= width) {
 				done = 1;
 				break;
 			} else
 				g->wanted -= width;
 		}
 		if (done) {
 			*gp = g->next;
 			free(g);
 		} else
 			gp = &g->next;
 	}
 
 	colwidth = mandoc_reallocarray(NULL, maxcol + 1, sizeof(*colwidth));
 	while (first_group != NULL) {
 
 		/*
 		 * Rebuild the array of the widths of all columns
 		 * participating in spans that require expansion.
 		 */
 
 		for (icol = 0; icol <= maxcol; icol++)
 			colwidth[icol] = SIZE_MAX;
 		for (g = first_group; g != NULL; g = g->next)
 			for (icol = g->startcol; icol <= g->endcol; icol++)
 				colwidth[icol] = tbl->cols[icol].width;
 
 		/*
 		 * Find the smallest and second smallest column width
 		 * among the columns which may need expamsion.
 		 */
 
 		min1 = min2 = SIZE_MAX;
 		for (icol = 0; icol <= maxcol; icol++) {
 			width = colwidth[icol];
 			if (min1 > width) {
 				min2 = min1;
 				min1 = width;
 			} else if (min1 < width && min2 > width)
 				min2 = width;
 		}
 
 		/*
 		 * Find the minimum wanted width
 		 * for any one of the narrowest columns,
 		 * and mark the columns wanting that width.
 		 */
 
 		wanted = min2;
 		for (g = first_group; g != NULL; g = g->next) {
 			necol = 0;
 			for (icol = g->startcol; icol <= g->endcol; icol++)
 				if (colwidth[icol] == min1)
 					necol++;
 			if (necol == 0)
 				continue;
 			width = min1 + (g->wanted - 1) / necol + 1;
 			if (width > min2)
 				width = min2;
 			if (wanted > width)
 				wanted = width;
 		}
 
 		/* Record the effect of the widening. */
 
 		gp = &first_group;
 		while ((g = *gp) != NULL) {
 			done = 0;
 			for (icol = g->startcol; icol <= g->endcol; icol++) {
 				if (colwidth[icol] != min1)
 					continue;
 				if (g->wanted <= wanted - min1) {
 					tbl->cols[icol].width += g->wanted;
 					done = 1;
 					break;
 				}
 				tbl->cols[icol].width = wanted;
 				g->wanted -= wanted - min1;
 			}
 			if (done) {
 				*gp = g->next;
 				free(g);
 			} else
 				gp = &g->next;
 		}
 	}
 	free(colwidth);
 
 	/*
 	 * Align numbers with text.
 	 * Count columns to equalize and columns to maximize.
 	 * Find maximum width of the columns to equalize.
 	 * Find total width of the columns *not* to maximize.
 	 */
 
 	necol = nxcol = 0;
 	ewidth = xwidth = 0;
 	for (icol = 0; icol <= maxcol; icol++) {
 		col = tbl->cols + icol;
 		if (col->width > col->nwidth)
 			col->decimal += (col->width - col->nwidth) / 2;
 		if (col->flags & TBL_CELL_EQUAL) {
 			necol++;
 			if (ewidth < col->width)
 				ewidth = col->width;
 		}
 		if (col->flags & TBL_CELL_WMAX)
 			nxcol++;
 		else
 			xwidth += col->width;
 	}
 
 	/*
 	 * Equalize columns, if requested for any of them.
 	 * Update total width of the columns not to maximize.
 	 */
 
 	if (necol) {
 		for (icol = 0; icol <= maxcol; icol++) {
 			col = tbl->cols + icol;
 			if ( ! (col->flags & TBL_CELL_EQUAL))
 				continue;
 			if (col->width == ewidth)
 				continue;
 			if (nxcol && rmargin)
 				xwidth += ewidth - col->width;
 			col->width = ewidth;
 		}
 	}
 
 	/*
 	 * If there are any columns to maximize, find the total
 	 * available width, deducting 3n margins between columns.
 	 * Distribute the available width evenly.
 	 */
 
 	if (nxcol && rmargin) {
-		xwidth += 3*maxcol +
+		xwidth += (*tbl->len)(3 * maxcol +
 		    (opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX) ?
-		     2 : !!opts->lvert + !!opts->rvert);
+		     2 : !!opts->lvert + !!opts->rvert), tbl->arg);
 		if (rmargin <= offset + xwidth)
 			return;
 		xwidth = rmargin - offset - xwidth;
 
 		/*
 		 * Emulate a bug in GNU tbl width calculation that
 		 * manifests itself for large numbers of x-columns.
 		 * Emulating it for 5 x-columns gives identical
 		 * behaviour for up to 6 x-columns.
 		 */
 
 		if (nxcol == 5) {
-			quirkcol = xwidth % nxcol + 2;
+			quirkcol = xwidth / enw % nxcol + 2;
 			if (quirkcol != 3 && quirkcol != 4)
 				quirkcol = -1;
 		} else
 			quirkcol = -1;
 
 		necol = 0;
 		ewidth = 0;
 		for (icol = 0; icol <= maxcol; icol++) {
 			col = tbl->cols + icol;
 			if ( ! (col->flags & TBL_CELL_WMAX))
 				continue;
 			col->width = (double)xwidth * ++necol / nxcol
 			    - ewidth + 0.4995;
 			if (necol == quirkcol)
-				col->width--;
+				col->width -= enw;
 			ewidth += col->width;
 		}
 	}
 }
 
 static size_t
 tblcalc_data(struct rofftbl *tbl, struct roffcol *col,
     const struct tbl_opts *opts, const struct tbl_dat *dp, size_t mw)
 {
 	size_t		 sz;
 
 	/* Branch down into data sub-types. */
 
 	switch (dp->layout->pos) {
 	case TBL_CELL_HORIZ:
 	case TBL_CELL_DHORIZ:
 		sz = (*tbl->len)(1, tbl->arg);
 		if (col != NULL && col->width < sz)
 			col->width = sz;
 		return sz;
 	case TBL_CELL_LONG:
 	case TBL_CELL_CENTRE:
 	case TBL_CELL_LEFT:
 	case TBL_CELL_RIGHT:
 		return tblcalc_literal(tbl, col, dp, mw);
 	case TBL_CELL_NUMBER:
 		return tblcalc_number(tbl, col, opts, dp);
 	case TBL_CELL_DOWN:
 		return 0;
 	default:
 		abort();
 	}
 }
 
 static size_t
 tblcalc_literal(struct rofftbl *tbl, struct roffcol *col,
     const struct tbl_dat *dp, size_t mw)
 {
 	const char	*str;	/* Beginning of the first line. */
 	const char	*beg;	/* Beginning of the current line. */
 	char		*end;	/* End of the current line. */
-	size_t		 lsz;	/* Length of the current line. */
-	size_t		 wsz;	/* Length of the current word. */
-	size_t		 msz;   /* Length of the longest line. */
+
+	/* Widths in basic units. */
+	size_t		 lsz;	/* Of the current line. */
+	size_t		 wsz;	/* Of the current word. */
+	size_t		 msz;   /* Of the longest line. */
+	size_t		 enw;	/* Of one EN unit. */
 
 	if (dp->string == NULL || *dp->string == '\0')
 		return 0;
 	str = mw ? mandoc_strdup(dp->string) : dp->string;
 	msz = lsz = 0;
 	for (beg = str; beg != NULL && *beg != '\0'; beg = end) {
 		end = mw ? strchr(beg, ' ') : NULL;
 		if (end != NULL) {
 			*end++ = '\0';
 			while (*end == ' ')
 				end++;
 		}
 		wsz = (*tbl->slen)(beg, tbl->arg);
-		if (mw && lsz && lsz + 1 + wsz <= mw)
-			lsz += 1 + wsz;
+		enw = (*tbl->len)(1, tbl->arg);
+		if (mw && lsz && lsz + enw + wsz <= mw)
+			lsz += enw + wsz;
 		else
 			lsz = wsz;
 		if (msz < lsz)
 			msz = lsz;
 	}
 	if (mw)
 		free((void *)str);
 	if (col != NULL && col->width < msz)
 		col->width = msz;
 	return msz;
 }
 
 static size_t
 tblcalc_number(struct rofftbl *tbl, struct roffcol *col,
 		const struct tbl_opts *opts, const struct tbl_dat *dp)
 {
 	const char	*cp, *lastdigit, *lastpoint;
-	size_t		 intsz, totsz;
+	size_t		 totsz;	/* Total width of the number in basic units. */
+	size_t		 intsz; /* Width of the integer part in basic units. */
 	char		 buf[2];
 
 	if (dp->string == NULL || *dp->string == '\0')
 		return 0;
 
 	totsz = (*tbl->slen)(dp->string, tbl->arg);
 	if (col == NULL)
 		return totsz;
 
 	/*
 	 * Find the last digit and
 	 * the last decimal point that is adjacent to a digit.
 	 * The alignment indicator "\&" overrides everything.
 	 */
 
 	lastdigit = lastpoint = NULL;
 	for (cp = dp->string; cp[0] != '\0'; cp++) {
 		if (cp[0] == '\\' && cp[1] == '&') {
 			lastdigit = lastpoint = cp;
 			break;
 		} else if (cp[0] == opts->decimal &&
 		    (isdigit((unsigned char)cp[1]) ||
 		     (cp > dp->string && isdigit((unsigned char)cp[-1]))))
 			lastpoint = cp;
 		else if (isdigit((unsigned char)cp[0]))
 			lastdigit = cp;
 	}
 
 	/* Not a number, treat as a literal string. */
 
 	if (lastdigit == NULL) {
 		if (col != NULL && col->width < totsz)
 			col->width = totsz;
 		return totsz;
 	}
 
 	/* Measure the width of the integer part. */
 
 	if (lastpoint == NULL)
 		lastpoint = lastdigit + 1;
 	intsz = 0;
 	buf[1] = '\0';
 	for (cp = dp->string; cp < lastpoint; cp++) {
 		buf[0] = cp[0];
 		intsz += (*tbl->slen)(buf, tbl->arg);
 	}
 
 	/*
          * If this number has more integer digits than all numbers
          * seen on earlier lines, shift them all to the right.
 	 * If it has fewer, shift this number to the right.
 	 */
 
 	if (intsz > col->decimal) {
 		col->nwidth += intsz - col->decimal;
 		col->decimal = intsz;
 	} else
 		totsz += col->decimal - intsz;
 
 	/* Update the maximum total width seen so far. */
 
 	if (totsz > col->nwidth)
 		col->nwidth = totsz;
 	if (col->nwidth > col->width)
 		col->width = col->nwidth;
 	return totsz;
 }
diff --git a/contrib/mandoc/out.h b/contrib/mandoc/out.h
index f746e4486958..a3b49b70460d 100644
--- a/contrib/mandoc/out.h
+++ b/contrib/mandoc/out.h
@@ -1,65 +1,63 @@
-/* $Id: out.h,v 1.35 2022/09/11 09:13:48 schwarze Exp $ */
+/* $Id: out.h,v 1.36 2025/07/16 14:33:08 schwarze Exp $ */
 /*
+ * Copyright (c) 2011,2014,2017,2018,2025 Ingo Schwarze 
  * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons 
- * Copyright (c) 2014, 2017, 2018 Ingo Schwarze 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
  * Utilities for use by multiple mandoc(1) formatters.
  */
 
 enum	roffscale {
 	SCALE_CM, /* centimeters (c) */
 	SCALE_IN, /* inches (i) */
 	SCALE_PC, /* pica (P) */
 	SCALE_PT, /* points (p) */
 	SCALE_EM, /* ems (m) */
 	SCALE_MM, /* mini-ems (M) */
 	SCALE_EN, /* ens (n) */
 	SCALE_BU, /* default horizontal (u) */
 	SCALE_VS, /* default vertical (v) */
 	SCALE_FS, /* syn. for u (f) */
 	SCALE_MAX
 };
 
 struct	roffcol {
-	size_t		 width; /* width of cell */
-	size_t		 nwidth; /* max. width of number in cell */
-	size_t		 decimal; /* decimal position in cell */
-	size_t		 spacing; /* spacing after the column */
-	int		 flags; /* layout flags, see tbl_cell */
+	size_t		 width;    /* Width of cell [BU]. */
+	size_t		 nwidth;   /* Maximum width of number [BU]. */
+	size_t		 decimal;  /* Decimal position [BU]. */
+	size_t		 spacing;  /* Spacing after the column [EN]. */
+	int		 flags;    /* Layout flags, see tbl_cell. */
 };
 
 struct	roffsu {
 	enum roffscale	  unit;
 	double		  scale;
 };
 
-typedef	size_t	(*tbl_sulen)(const struct roffsu *, void *);
 typedef	size_t	(*tbl_strlen)(const char *, void *);
 typedef	size_t	(*tbl_len)(size_t, void *);
 
 struct	rofftbl {
-	tbl_sulen	 sulen; /* calculate scaling unit length */
-	tbl_strlen	 slen; /* calculate string length */
-	tbl_len		 len; /* produce width of empty space */
-	struct roffcol	*cols; /* master column specifiers */
-	void		*arg; /* passed to sulen, slen, and len */
+	tbl_strlen	 slen;	/* Calculate string length [BU]. */
+	tbl_len		 len;	/* Produce width of empty space [BU]. */
+	struct roffcol	*cols;	/* Master column specifiers. */
+	void		*arg;	/* Passed to slen() and len(). */
 };
 
 
 struct	tbl_span;
 
 const char	 *a2roffsu(const char *, struct roffsu *, enum roffscale);
 void		  tblcalc(struct rofftbl *,
 			const struct tbl_span *, size_t, size_t);
diff --git a/contrib/mandoc/roff.7 b/contrib/mandoc/roff.7
index 27f83853e75b..adb5852e069b 100644
--- a/contrib/mandoc/roff.7
+++ b/contrib/mandoc/roff.7
@@ -1,2363 +1,2464 @@
-.\" $Id: roff.7,v 1.121 2023/10/23 20:25:02 schwarze Exp $
+.\" $Id: roff.7,v 1.123 2025/08/04 23:12:08 schwarze Exp $
 .\"
-.\" Copyright (c) 2010-2019, 2022-2023 Ingo Schwarze 
+.\" Copyright (c) 2010-2019,2022-2023,2025 Ingo Schwarze 
 .\" Copyright (c) 2010, 2011, 2012 Kristaps Dzonsons 
 .\"
 .\" Permission to use, copy, modify, and distribute this software for any
 .\" purpose with or without fee is hereby granted, provided that the above
 .\" copyright notice and this permission notice appear in all copies.
 .\"
 .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\"
-.Dd $Mdocdate: October 23 2023 $
+.Dd $Mdocdate: August 4 2025 $
 .Dt ROFF 7
 .Os
 .Sh NAME
 .Nm roff
 .Nd roff language reference for mandoc
 .Sh DESCRIPTION
 The
 .Nm roff
 language is a general purpose text formatting language.
 Since traditional implementations of the
 .Xr mdoc 7
 and
 .Xr man 7
 manual formatting languages are based on it,
 many real-world manuals use small numbers of
 .Nm
 requests and escape sequences intermixed with their
 .Xr mdoc 7
 or
 .Xr man 7
 code.
 To properly format such manuals, the
 .Xr mandoc 1
 utility supports a subset of
 .Nm
 requests and escapes.
 Even though this manual page lists all
 .Nm
 requests and escape sequences, it only contains partial information
 about requests not supported by
 .Xr mandoc 1
 and about language features that do not matter for manual pages.
 For complete
 .Nm
 manuals, consult the
 .Sx SEE ALSO
 section.
 .Pp
 Input lines beginning with the control character
 .Sq \&.
 are parsed for requests and macros.
 Such lines are called
 .Dq request lines
 or
 .Dq macro lines ,
 respectively.
 Requests change the processing state and manipulate the formatting;
 some macros also define the document structure and produce formatted
 output.
 The single quote
 .Pq Qq \(aq
 is accepted as an alternative control character,
 treated by
 .Xr mandoc 1
 just like
 .Ql \&.
 .Pp
 Lines not beginning with control characters are called
 .Dq text lines .
 They provide free-form text to be printed; the formatting of the text
 depends on the respective processing context.
 .Sh LANGUAGE SYNTAX
 .Nm
-documents may contain only graphable 7-bit ASCII characters, the space
-character, and, in certain circumstances, the tab character.
+documents are text files containing only printable
+.Xr ascii 7
+characters, the space character,
+and, in certain circumstances, the tab character.
 The backslash character
 .Sq \e
 indicates the start of an escape sequence, used for example for
 .Sx Comments
 and
 .Sx Special Characters .
 For a complete listing of escape sequences, consult the
 .Sx ESCAPE SEQUENCE REFERENCE
 below.
 .Ss Comments
 Text following an escaped double-quote
 .Sq \e\(dq ,
 whether in a request, macro, or text line, is ignored to the end of the line.
 A request line beginning with a control character and comment escape
 .Sq \&.\e\(dq
 is also ignored.
 Furthermore, request lines with only a control character and optional
 trailing whitespace are stripped from input.
 .Pp
 Examples:
 .Bd -literal -offset indent -compact
 \&.\e\(dq This is a comment line.
 \&.\e\(dq The next line is ignored:
 \&.
 \&.Sh EXAMPLES \e\(dq This is a comment, too.
 \&example text \e\(dq And so is this.
 .Ed
 .Ss Special Characters
 Special characters are used to encode special glyphs and are rendered
 differently across output media.
 They may occur in request, macro, and text lines.
 Sequences begin with the escape character
 .Sq \e
 followed by either an open-parenthesis
 .Sq \&(
 for two-character sequences; an open-bracket
 .Sq \&[
 for n-character sequences (terminated at a close-bracket
 .Sq \&] ) ;
 or a single one character sequence.
 .Pp
 Examples:
 .Bl -tag -width Ds -offset indent -compact
 .It Li \e(em
 Two-letter em dash escape.
 .It Li \ee
 One-letter backslash escape.
 .El
 .Pp
 See
 .Xr mandoc_char 7
 for a complete list.
 .Ss Font Selection
 In
 .Xr mdoc 7
 and
 .Xr man 7
 documents, fonts are usually selected with macros.
 The
 .Ic \ef
 escape sequence and the
 .Ic \&ft
 request can be used to manually change the font,
 but this is not recommended in
 .Xr mdoc 7
 documents.
 Such manual font changes are overridden by many subsequent macros.
 .Pp
 The following fonts are supported:
 .Pp
 .Bl -tag -width CW -offset indent -compact
 .It Cm B
 Bold font.
 .It Cm BI
 A font that is both bold and italic.
 .It Cm CB
 Bold constant width font.
 Same as
 .Cm B
 in terminal output.
 .It Cm CI
 Italic constant width font.
 Same as
 .Cm I
 in terminal output.
 .It Cm CR
 Regular constant width font.
 Same as
 .Cm R
 in terminal output.
 .It Cm CW
 An alias for
 .Cm CR .
 .It Cm I
 Italic font.
 .It Cm P
 Return to the previous font.
 If a macro caused a font change since the last
 .Ic \ef
-eascape sequence or
+escape sequence or
 .Ic \&ft
 request, this returns to the font before the last font change in
 the macro rather than to the font before the last manual font change.
 .It Cm R
 Roman font.
 This is the default font.
 .It Cm 1
 An alias for
 .Cm R .
 .It Cm 2
 An alias for
 .Cm I .
 .It Cm 3
 An alias for
 .Cm B .
 .It Cm 4
 An alias for
 .Cm BI .
 .El
 .Pp
 Examples:
 .Bl -tag -width Ds -offset indent -compact
 .It Li \efBbold\efR
 Write in \fBbold\fP, then switch to regular font mode.
 .It Li \efIitalic\efP
 Write in \fIitalic\fP, then return to previous font mode.
 .It Li \ef(BIbold italic\efP
 Write in \f(BIbold italic\fP, then return to previous font mode.
 .El
 .Ss Whitespace
 Whitespace consists of the space character.
 In text lines, whitespace is preserved within a line.
 In request and macro lines, whitespace delimits arguments and is discarded.
 .Pp
 Unescaped trailing spaces are stripped from text line input unless in a
 literal context.
 In general, trailing whitespace on any input line is discouraged for
 reasons of portability.
 In the rare case that a space character is needed at the end of an
 input line, it may be forced by
 .Sq \e\ \e& .
 .Pp
 Literal space characters can be produced in the output
 using escape sequences.
 In macro lines, they can also be included in arguments using quotation; see
 .Sx MACRO SYNTAX
 for details.
 .Pp
 Blank text lines, which may include whitespace, are only permitted
 within literal contexts.
 If the first character of a text line is a space, that line is printed
 with a leading newline.
 .Ss Scaling Widths
 Many requests and macros support scaled widths for their arguments.
 The syntax for a scaled width is
 .Sq Li [+-]?[0-9]*.[0-9]*[:unit:] ,
 where a decimal must be preceded or followed by at least one digit.
 .Pp
 The following scaling units are accepted:
 .Pp
 .Bl -tag -width Ds -offset indent -compact
 .It c
 centimetre
 .It i
 inch
 .It P
 pica (1/6 inch)
 .It p
 point (1/72 inch)
 .It f
 scale
 .Sq u
 by 65536
 .It v
 default vertical span
 .It m
 width of rendered
 .Sq m
 .Pq em
 character
 .It n
 width of rendered
 .Sq n
 .Pq en
 character
 .It u
-default horizontal span for the terminal
+device-dependent basic units
 .It M
 mini-em (1/100 em)
 .El
 .Pp
 Using anything other than
 .Sq m ,
 .Sq n ,
 or
 .Sq v
 is necessarily non-portable across output media.
 See
 .Sx COMPATIBILITY .
 .Pp
 If a scaling unit is not provided, the numerical value is interpreted
 under the default rules of
 .Sq v
 for vertical spaces and
 .Sq u
 for horizontal ones.
 .Pp
 Examples:
-.Bl -tag -width ".Bl -tag -width 2i" -offset indent -compact
+.Bl -tag -width "xBl -tag -width 2i" -offset indent -compact
 .It Li \&.Bl -tag -width 2i
 two-inch tagged list indentation in
 .Xr mdoc 7
 .It Li \&.HP 2i
 two-inch tagged list indentation in
 .Xr man 7
 .It Li \&.sp 2v
 two vertical spaces
 .El
 .Ss Sentence Spacing
 Each sentence should terminate at the end of an input line.
 By doing this, a formatter will be able to apply the proper amount of
 spacing after the end of sentence (unescaped) period, exclamation mark,
 or question mark followed by zero or more non-sentence closing
 delimiters
 .Po
 .Sq \&) ,
 .Sq \&] ,
 .Sq \&' ,
 .Sq \&"
 .Pc .
 .Pp
 The proper spacing is also intelligently preserved if a sentence ends at
 the boundary of a macro line.
 .Pp
 If an input line happens to end with a period, exclamation or question
 mark that isn't the end of a sentence, append a zero-width space
 .Pq Sq \e& .
 .Pp
 Examples:
 .Bd -literal -offset indent -compact
 Do not end sentences mid-line like this.  Instead,
 end a sentence like this.
 A macro would end like this:
 \&.Xr mandoc 1 \&.
 An abbreviation at the end of an input line needs escaping, e.g.\e&
 like this.
 .Ed
 .Sh REQUEST SYNTAX
 A request or macro line consists of:
 .Pp
 .Bl -enum -compact
 .It
 the control character
 .Sq \&.
 or
 .Sq \(aq
 at the beginning of the line,
 .It
 optionally an arbitrary amount of whitespace,
 .It
 the name of the request or the macro, which is one word of arbitrary
 length, terminated by whitespace,
 .It
 and zero or more arguments delimited by whitespace.
 .El
 .Pp
 Thus, the following request lines are all equivalent:
 .Bd -literal -offset indent
 \&.ig end
 \&.ig    end
 \&.   ig end
 .Ed
 .Sh MACRO SYNTAX
 Macros are provided by the
 .Xr mdoc 7
 and
 .Xr man 7
 languages and can be defined by the
 .Ic \&de
 request.
 When called, they follow the same syntax as requests, except that
 macro arguments may optionally be quoted by enclosing them
 in double quote characters
 .Pq Sq \(dq .
 Quoted text, even if it contains whitespace or would cause
 a macro invocation when unquoted, is always considered literal text.
 Inside quoted text, pairs of double quote characters
 .Pq Sq Qq
 resolve to single double quote characters.
 .Pp
 To be recognised as the beginning of a quoted argument, the opening
 quote character must be preceded by a space character.
 A quoted argument extends to the next double quote character that is not
 part of a pair, or to the end of the input line, whichever comes earlier.
 Leaving out the terminating double quote character at the end of the line
 is discouraged.
 For clarity, if more arguments follow on the same input line,
 it is recommended to follow the terminating double quote character
 by a space character; in case the next character after the terminating
 double quote character is anything else, it is regarded as the beginning
 of the next, unquoted argument.
 .Pp
 Both in quoted and unquoted arguments, pairs of backslashes
 .Pq Sq \e\e
 resolve to single backslashes.
 In unquoted arguments, space characters can alternatively be included
 by preceding them with a backslash
 .Pq Sq \e\~ ,
 but quoting is usually better for clarity.
 .Pp
 Examples:
 .Bl -tag -width Ds -offset indent -compact
 .It Li .Fn strlen \(dqconst char *s\(dq
 Group arguments
 .Qq const char *s
 into one function argument.
 If unspecified,
 .Qq const ,
 .Qq char ,
 and
 .Qq *s
 would be considered separate arguments.
 .It Li .Op \(dqFl a\(dq
 Consider
 .Qq \&Fl a
 as literal text instead of a flag macro.
 .El
 .Sh REQUEST REFERENCE
 The
 .Xr mandoc 1
 .Nm
 parser recognises the following requests.
 For requests marked as "ignored" or "unsupported", any arguments are
 ignored, and the number of arguments is not checked.
 .Bl -tag -width Ds
 .It Ic \&ab Op Ar message
 Abort processing.
 Currently unsupported.
 .It Ic \&ad Op Cm b | c | l | n | r
 Set line adjustment mode for subsequent text.
 Currently ignored.
 .It Ic \&af Ar registername format
 Assign an output format to a number register.
 Currently ignored.
 .It Ic \&aln Ar newname oldname
 Create an alias for a number register.
 Currently unsupported.
 .It Ic \&als Ar newname oldname
 Create an alias for a request, string, macro, or diversion.
 .It Ic \&am Ar macroname Op Ar endmacro
 Append to a macro definition.
 The syntax of this request is the same as that of
 .Ic \&de .
 .It Ic \&am1 Ar macroname Op Ar endmacro
 Append to a macro definition, switching roff compatibility mode off
 during macro execution (groff extension).
 The syntax of this request is the same as that of
 .Ic \&de1 .
 Since
 .Xr mandoc 1
 does not implement
 .Nm
 compatibility mode at all, it handles this request as an alias for
 .Ic \&am .
 .It Ic \&ami Ar macrostring Op Ar endstring
 Append to a macro definition, specifying the macro name indirectly
 (groff extension).
 The syntax of this request is the same as that of
 .Ic \&dei .
 .It Ic \&ami1 Ar macrostring Op Ar endstring
 Append to a macro definition, specifying the macro name indirectly
 and switching roff compatibility mode off during macro execution
 (groff extension).
 The syntax of this request is the same as that of
 .Ic \&dei1 .
 Since
 .Xr mandoc 1
 does not implement
 .Nm
 compatibility mode at all, it handles this request as an alias for
 .Ic \&ami .
 .It Ic \&as Ar stringname Op Ar string
 Append to a user-defined string.
 The syntax of this request is the same as that of
 .Ic \&ds .
 If a user-defined string with the specified name does not yet exist,
 it is set to the empty string before appending.
 .It Ic \&as1 Ar stringname Op Ar string
 Append to a user-defined string, switching roff compatibility mode off
 during macro execution (groff extension).
 The syntax of this request is the same as that of
 .Ic \&ds1 .
 Since
 .Xr mandoc 1
 does not implement
 .Nm
 compatibility mode at all, it handles this request as an alias for
 .Ic \&as .
 .It Ic \&asciify Ar divname
 Fully unformat a diversion.
 Currently unsupported.
 .It Ic \&backtrace
 Print a backtrace of the input stack.
 This is a groff extension and currently ignored.
 .It Ic \&bd Ar font Oo Ar curfont Oc Op Ar offset
 Artificially embolden by repeated printing with small shifts.
 Currently ignored.
 .It Ic \&bleedat Ar left top width height
 Set the BleedBox page parameter for PDF generation.
 This is a Heirloom extension and currently ignored.
 .It Ic \&blm Ar macroname
 Set a blank line trap.
 Currently unsupported.
 .It Ic \&box Ar divname
 Begin a diversion without including a partially filled line.
 Currently unsupported.
 .It Ic \&boxa Ar divname
 Add to a diversion without including a partially filled line.
 Currently unsupported.
 .It Ic \&bp Oo Cm + Ns | Ns Cm - Oc Ns Ar pagenumber
 Begin a new page.
 Currently ignored.
 .It Ic \&BP Ar source height width position offset flags label
 Define a frame and place a picture in it.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&br
 Break the output line.
 .It Ic \&break
 Break out of the innermost
 .Ic \&while
 loop.
 .It Ic \&breakchar Ar char ...
 Optional line break characters.
 This is a Heirloom extension and currently ignored.
 .It Ic \&brnl Ar N
 Break output line after the next
 .Ar N
 input lines.
 This is a Heirloom extension and currently ignored.
 .It Ic \&brp
 Break and spread output line.
 Currently, this is implemented as an alias for
 .Ic \&br .
 .It Ic \&brpnl Ar N
 Break and spread output line after the next
 .Ar N
 input lines.
 This is a Heirloom extension and currently ignored.
 .It Ic \&c2 Op Ar char
 Change the no-break control character.
 Currently unsupported.
 .It Ic \&cc Op Ar char
 Change the control character.
 If
 .Ar char
 is not specified, the control character is reset to
 .Sq \&. .
 Trailing characters are ignored.
 .It Ic \&ce Op Ar N
 Center the next
 .Ar N
 input lines without filling.
 .Ar N
 defaults to 1.
 An argument of 0 or less ends centering.
 Currently, high level macros abort centering.
 .It Ic \&cf Ar filename
 Output the contents of a file.
 Ignored because insecure.
 .It Ic \&cflags Ar flags char ...
 Set character flags.
 This is a groff extension and currently ignored.
 .It Ic \&ch Ar macroname Op Ar dist
 Change a trap location.
 Currently ignored.
 .It Ic \&char Ar glyph Op Ar string
 Define or redefine the ASCII character or character escape sequence
 .Ar glyph
 to be rendered as
 .Ar string ,
 which can be empty.
 Only partially supported in
 .Xr mandoc 1 ;
 may interact incorrectly with
 .Ic \&tr .
 .It Ic \&chop Ar stringname
 Remove the last character from a macro, string, or diversion.
 Currently unsupported.
 .It Ic \&class Ar classname char ...
 Define a character class.
 This is a groff extension and currently ignored.
 .It Ic \&close Ar streamname
 Close an open file.
 Ignored because insecure.
 .It Ic \&CL Ar color text
 Print text in color.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&color Op Cm 1 | 0
 Activate or deactivate colors.
 This is a groff extension and currently ignored.
 .It Ic \&composite Ar from to
 Define a name component for composite glyph names.
 This is a groff extension and currently unsupported.
 .It Ic \&continue
 Immediately start the next iteration of a
 .Ic \&while
 loop.
 Currently unsupported.
 .It Ic \&cp Op Cm 1 | 0
 Switch
 .Nm
 compatibility mode on or off.
 Currently ignored.
 .It Ic \&cropat Ar left top width height
 Set the CropBox page parameter for PDF generation.
 This is a Heirloom extension and currently ignored.
 .It Ic \&cs Ar font Op Ar width Op Ar emsize
 Constant character spacing mode.
 Currently ignored.
 .It Ic \&cu Op Ar N
 Underline next
 .Ar N
 input lines including whitespace.
 Currently ignored.
 .It Ic \&da Ar divname
 Append to a diversion.
 Currently unsupported.
 .It Ic \&dch Ar macroname Op Ar dist
 Change a trap location in the current diversion.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&de Ar macroname Op Ar endmacro
 Define a
 .Nm
 macro.
 Its syntax can be either
 .Bd -literal -offset indent
 .Pf . Ic \&de Ar macroname
 .Ar definition
 \&..
 .Ed
 .Pp
 or
 .Bd -literal -offset indent
 .Pf . Ic \&de Ar macroname endmacro
 .Ar definition
 .Pf . Ar endmacro
 .Ed
 .Pp
 Both forms define or redefine the macro
 .Ar macroname
 to represent the
 .Ar definition ,
 which may consist of one or more input lines, including the newline
 characters terminating each line, optionally containing calls to
 .Nm
 requests,
 .Nm
 macros or high-level macros like
 .Xr man 7
 or
 .Xr mdoc 7
 macros, whichever applies to the document in question.
 .Pp
 Specifying a custom
 .Ar endmacro
 works in the same way as for
 .Ic \&ig ;
 namely, the call to
 .Sq Pf . Ar endmacro
 first ends the
 .Ar definition ,
 and after that, it is also evaluated as a
 .Nm
 request or
 .Nm
 macro, but not as a high-level macro.
 .Pp
 The macro can be invoked later using the syntax
 .Pp
 .D1 Pf . Ar macroname Op Ar argument Op Ar argument ...
 .Pp
 Regarding argument parsing, see
 .Sx MACRO SYNTAX
 above.
 .Pp
 The line invoking the macro will be replaced
 in the input stream by the
 .Ar definition ,
 replacing all occurrences of
 .No \e\e$ Ns Ar N ,
 where
 .Ar N
 is a digit, by the
 .Ar N Ns th Ar argument .
 For example,
 .Bd -literal -offset indent
 \&.de ZN
 \efI\e^\e\e$1\e^\efP\e\e$2
 \&..
 \&.ZN XtFree .
 .Ed
 .Pp
 produces
 .Pp
 .D1 \efI\e^XtFree\e^\efP.
 .Pp
 in the input stream, and thus in the output: \fI\^XtFree\^\fP.
 Each occurrence of \e\e$* is replaced with all the arguments,
 joined together with single space characters.
 The variant \e\e$@ is similar, except that each argument is
 individually quoted.
 .Pp
 Since macros and user-defined strings share a common string table,
 defining a macro
 .Ar macroname
 clobbers the user-defined string
 .Ar macroname ,
 and the
 .Ar definition
 can also be printed using the
 .Sq \e*
 string interpolation syntax described below
 .Ic ds ,
 but this is rarely useful because every macro definition contains at least
 one explicit newline character.
 .Pp
 In order to prevent endless recursion, both groff and
 .Xr mandoc 1
 limit the stack depth for expanding macros and strings
 to a large, but finite number, and
 .Xr mandoc 1
 also limits the length of the expanded input line.
 Do not rely on the exact values of these limits.
 .It Ic \&de1 Ar macroname Op Ar endmacro
 Define a
 .Nm
 macro that will be executed with
 .Nm
 compatibility mode switched off during macro execution.
 This is a groff extension.
 Since
 .Xr mandoc 1
 does not implement
 .Nm
 compatibility mode at all, it handles this request as an alias for
 .Ic \&de .
 .It Ic \&defcolor Ar newname scheme component ...
 Define a color name.
 This is a groff extension and currently ignored.
 .It Ic \&dei Ar macrostring Op Ar endstring
 Define a
 .Nm
 macro, specifying the macro name indirectly (groff extension).
 The syntax of this request is the same as that of
 .Ic \&de .
 The effect is the same as:
 .Pp
 .D1 Pf . Cm \&de No \e* Ns Bo Ar macrostring Bc Op \e* Ns Bq Ar endstring
 .It Ic \&dei1 Ar macrostring Op Ar endstring
 Define a
 .Nm
 macro that will be executed with
 .Nm
 compatibility mode switched off during macro execution,
 specifying the macro name indirectly (groff extension).
 Since
 .Xr mandoc 1
 does not implement
 .Nm
 compatibility mode at all, it handles this request as an alias for
 .Ic \&dei .
 .It Ic \&device Ar string ...
 .It Ic \&devicem Ar stringname
 These two requests only make sense with the groff-specific intermediate
 output format and are unsupported.
 .It Ic \&di Ar divname
 Begin a diversion.
 Currently unsupported.
 .It Ic \&do Ar command Op Ar argument ...
 Execute
 .Nm
 request or macro line with compatibility mode disabled.
 Currently unsupported.
 .It Ic \&ds Ar stringname Op Oo \(dq Oc Ns Ar string
 Define a user-defined string.
 The
 .Ar stringname
 and
 .Ar string
 arguments are space-separated.
 If the
 .Ar string
 begins with a double-quote character, that character will not be part
 of the string.
 All remaining characters on the input line form the
 .Ar string ,
 including whitespace and double-quote characters, even trailing ones.
 .Pp
 The
 .Ar string
 can be interpolated into subsequent text by using
 .No \e* Ns Bq Ar stringname
 for a
 .Ar stringname
 of arbitrary length, or \e*(NN or \e*N if the length of
 .Ar stringname
 is two or one characters, respectively.
 Interpolation can be prevented by escaping the leading backslash;
 that is, an asterisk preceded by an even number of backslashes
 does not trigger string interpolation.
 .Pp
 Since user-defined strings and macros share a common string table,
 defining a string
 .Ar stringname
 clobbers the macro
 .Ar stringname ,
 and the
 .Ar stringname
 used for defining a string can also be invoked as a macro,
 in which case the following input line will be appended to the
 .Ar string ,
 forming a new input line passed to the
 .Nm
 parser.
 For example,
 .Bd -literal -offset indent
 \&.ds badidea .S
 \&.badidea
 H SYNOPSIS
 .Ed
 .Pp
 invokes the
 .Ic SH
 macro when used in a
 .Xr man 7
 document.
 Such abuse is of course strongly discouraged.
 .It Ic \&ds1 Ar stringname Op Oo \(dq Oc Ns Ar string
 Define a user-defined string that will be expanded with
 .Nm
 compatibility mode switched off during string expansion.
 This is a groff extension.
 Since
 .Xr mandoc 1
 does not implement
 .Nm
 compatibility mode at all, it handles this request as an alias for
 .Ic \&ds .
 .It Ic \&dwh Ar dist macroname
 Set a location trap in the current diversion.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&dt Op Ar dist macroname
 Set a trap within a diversion.
 Currently unsupported.
 .It Ic \&ec Op Ar char
 Enable the escape mechanism and change the escape character.
 The
 .Ar char
 argument defaults to the backslash
 .Pq Sq \e .
 .It Ic \&ecr
 Restore the escape character.
 Currently unsupported.
 .It Ic \&ecs
 Save the escape character.
 Currently unsupported.
 .It Ic \&el Ar body
 The
 .Dq else
 half of an if/else conditional.
 Pops a result off the stack of conditional evaluations pushed by
 .Ic \&ie
 and uses it as its conditional.
 If no stack entries are present (e.g., due to no prior
 .Ic \&ie
 calls)
 then false is assumed.
 The syntax of this request is similar to
 .Ic \&if
 except that the conditional is missing.
 .It Ic \&em Ar macroname
 Set a trap at the end of input.
 Currently unsupported.
 .It Ic \&EN
 End an equation block.
 See
 .Ic \&EQ .
 .It Ic \&eo
 Disable the escape mechanism completely.
 .It Ic \&EP
 End a picture started by
 .Ic \&BP .
 This is a Heirloom extension and currently unsupported.
 .It Ic \&EQ
 Begin an equation block.
 See
 .Xr eqn 7
 for a description of the equation language.
 .It Ic \&errprint Ar message
 Print a string like an error message.
 This is a Heirloom extension and currently ignored.
 .It Ic \&ev Op Ar envname
 Switch to another environment.
 Currently unsupported.
 .It Ic \&evc Op Ar envname
 Copy an environment into the current environment.
 Currently unsupported.
 .It Ic \&ex
 Abort processing and exit.
 Currently unsupported.
 .It Ic \&fallback Ar curfont font ...
 Select the fallback sequence for a font.
 This is a Heirloom extension and currently ignored.
 .It Ic \&fam Op Ar familyname
 Change the font family.
 This is a groff extension and currently ignored.
 .It Ic \&fc Op Ar delimchar Op Ar padchar
 Define a delimiting and a padding character for fields.
 Currently unsupported.
 .It Ic \&fchar Ar glyphname Op Ar string
 Define a fallback glyph.
 Currently unsupported.
 .It Ic \&fcolor Ar colorname
 Set the fill color for \eD objects.
 This is a groff extension and currently ignored.
 .It Ic \&fdeferlig Ar font string ...
 Defer ligature building.
 This is a Heirloom extension and currently ignored.
 .It Ic \&feature Cm + Ns | Ns Cm - Ns Ar name
 Enable or disable an OpenType feature.
 This is a Heirloom extension and currently ignored.
 .It Ic \&fi
 Break the output line and switch to fill mode,
 which is active by default but can be ended with the
 .Ic \&nf
 request.
 In fill mode, input from subsequent input lines is added to
 the same output line until the next word no longer fits,
 at which point the output line is broken.
 This request is implied by the
 .Xr mdoc 7
 .Ic \&Sh
 macro and by the
 .Xr man 7
 .Ic \&SH ,
 .Ic \&SS ,
 and
 .Ic \&EE
 macros.
 .It Ic \&fkern Ar font minkern
 Control the use of kerning tables for a font.
 This is a Heirloom extension and currently ignored.
 .It Ic \&fl
 Flush output.
 Currently ignored.
 .It Ic \&flig Ar font string char ...
 Define ligatures.
 This is a Heirloom extension and currently ignored.
 .It Ic \&fp Ar position font Op Ar filename
 Assign font position.
 Currently ignored.
 .It Ic \&fps Ar mapname ...
 Mount a font with a special character map.
 This is a Heirloom extension and currently ignored.
 .It Ic \&fschar Ar font glyphname Op Ar string
 Define a font-specific fallback glyph.
 This is a groff extension and currently unsupported.
 .It Ic \&fspacewidth Ar font Op Ar afmunits
 Set a font-specific width for the space character.
 This is a Heirloom extension and currently ignored.
 .It Ic \&fspecial Ar curfont Op Ar font ...
 Conditionally define a special font.
 This is a groff extension and currently ignored.
 .It Ic \&ft Op Ar font
 Change the font; see
 .Sx Font Selection .
 The
 .Ar font
 argument defaults to
 .Cm P .
 .It Ic \&ftr Ar newname Op Ar oldname
 Translate font name.
 This is a groff extension and currently ignored.
 .It Ic \&fzoom Ar font Op Ar permille
 Zoom font size.
 Currently ignored.
 .It Ic \&gcolor Op Ar colorname
 Set glyph color.
 This is a groff extension and currently ignored.
 .It Ic \&hc Op Ar char
 Set the hyphenation character.
 Currently ignored.
 .It Ic \&hcode Ar char code ...
 Set hyphenation codes of characters.
 Currently ignored.
 .It Ic \&hidechar Ar font char ...
 Hide characters in a font.
 This is a Heirloom extension and currently ignored.
 .It Ic \&hla Ar language
 Set hyphenation language.
 This is a groff extension and currently ignored.
 .It Ic \&hlm Op Ar number
 Set maximum number of consecutive hyphenated lines.
 Currently ignored.
 .It Ic \&hpf Ar filename
 Load hyphenation pattern file.
 This is a groff extension and currently ignored.
 .It Ic \&hpfa Ar filename
 Load hyphenation pattern file, appending to the current patterns.
 This is a groff extension and currently ignored.
 .It Ic \&hpfcode Ar code code ...
 Define mapping values for character codes in hyphenation patterns.
 This is a groff extension and currently ignored.
 .It Ic \&hw Ar word ...
 Specify hyphenation points in words.
 Currently ignored.
 .It Ic \&hy Op Ar mode
 Set automatic hyphenation mode.
 Currently ignored.
 .It Ic \&hylang Ar language
 Set hyphenation language.
 This is a Heirloom extension and currently ignored.
 .It Ic \&hylen Ar nchar
 Minimum word length for hyphenation.
 This is a Heirloom extension and currently ignored.
 .It Ic \&hym Op Ar length
 Set hyphenation margin.
 This is a groff extension and currently ignored.
 .It Ic \&hypp Ar penalty ...
 Define hyphenation penalties.
 This is a Heirloom extension and currently ignored.
 .It Ic \&hys Op Ar length
 Set hyphenation space.
 This is a groff extension and currently ignored.
 .It Ic \&ie Ar condition body
 The
 .Dq if
 half of an if/else conditional.
 The result of the conditional is pushed into a stack used by subsequent
 invocations of
 .Ic \&el ,
 which may be separated by any intervening input (or not exist at all).
 Its syntax is equivalent to
 .Ic \&if .
 .It Ic \&if Ar condition body
 Begin a conditional.
 This request can also be written as follows:
 .Bd -unfilled -offset indent
 .Pf . Ic \&if Ar condition No \e{ Ns Ar body
 .Ar body ... Ns \e}
 .Ed
 .Bd -unfilled -offset indent
 .Pf . Ic \&if Ar condition No \e{\e
 .Ar body ...
 .Pf . No \e}
 .Ed
 .Pp
 The
 .Ar condition
 is a boolean expression.
 Currently,
 .Xr mandoc 1
 supports the following subset of roff conditionals:
 .Bl -bullet
 .It
 If
 .Sq \&!
 is prefixed to
 .Ar condition ,
 it is logically inverted.
 .It
 If the first character of
 .Ar condition
 is
 .Sq n
 .Pq nroff mode
 or
 .Sq o
 .Pq odd page ,
 it evaluates to true, and the
 .Ar body
 starts with the next character.
 .It
 If the first character of
 .Ar condition
 is
 .Sq e
 .Pq even page ,
 .Sq t
 .Pq troff mode ,
 or
 .Sq v
 .Pq vroff mode ,
 it evaluates to false, and the
 .Ar body
 starts with the next character.
 .It
 If the first character of
 .Ar condition
 is
 .Sq c
 .Pq character available ,
 it evaluates to true if the following character is an ASCII character
 or a valid character escape sequence, or to false otherwise.
 The
 .Ar body
 starts with the character following that next character.
 .It
 If the first character of
 .Ar condition
 is
 .Sq d ,
 it evaluates to true if the rest of
 .Ar condition
 is the name of an existing user defined macro or string;
 otherwise, it evaluates to false.
 .It
 If the first character of
 .Ar condition
 is
 .Sq r ,
 it evaluates to true if the rest of
 .Ar condition
 is the name of an existing number register;
 otherwise, it evaluates to false.
 .It
 If the
 .Ar condition
 starts with a parenthesis or with an optionally signed
 integer number, it is evaluated according to the rules of
 .Sx Numerical expressions
 explained below.
 It evaluates to true if the result is positive,
 or to false if the result is zero or negative.
 .It
 Otherwise, the first character of
 .Ar condition
 is regarded as a delimiter and it evaluates to true if the string
 extending from its first to its second occurrence is equal to the
 string extending from its second to its third occurrence.
 .It
 If
 .Ar condition
 cannot be parsed, it evaluates to false.
 .El
 .Pp
 If a conditional is false, its children are not processed, but are
 syntactically interpreted to preserve the integrity of the input
 document.
 Thus,
 .Pp
 .D1 \&.if t .ig
 .Pp
 will discard the
 .Sq \&.ig ,
 which may lead to interesting results, but
 .Pp
 .D1 \&.if t .if t \e{\e
 .Pp
 will continue to syntactically interpret to the block close of the final
 conditional.
 Sub-conditionals, in this case, obviously inherit the truth value of
 the parent.
 .Pp
 If the
 .Ar body
 section is begun by an escaped brace
 .Sq \e{ ,
 scope continues until the end of the input line containing the
 matching closing-brace escape sequence
 .Sq \e} .
 If the
 .Ar body
 is not enclosed in braces, scope continues until the end of the line.
 If the
 .Ar condition
 is followed by a
 .Ar body
 on the same line, whether after a brace or not, then requests and macros
 .Em must
 begin with a control character.
 It is generally more intuitive, in this case, to write
 .Bd -unfilled -offset indent
 .Pf . Ic \&if Ar condition No \e{\e
 .Pf . Ar request
 .Pf . No \e}
 .Ed
 .Pp
 than having the request or macro follow as
 .Pp
 .D1 Pf . Ic \&if Ar condition Pf \e{. Ar request
 .Pp
 The scope of a conditional is always parsed, but only executed if the
 conditional evaluates to true.
 .Pp
 Note that the
 .Sq \e}
 is converted into a zero-width escape sequence if not passed as a
 standalone macro
 .Sq \&.\e} .
 For example,
 .Pp
 .D1 \&.Fl a \e} b
 .Pp
 will result in
 .Sq \e}
 being considered an argument of the
 .Sq \&Fl
 macro.
 .It Ic \&ig Op Ar endmacro
 Ignore input.
 Its syntax can be either
 .Bd -literal -offset indent
 .Pf . Cm \&ig
 .Ar ignored text
 \&..
 .Ed
 .Pp
 or
 .Bd -literal -offset indent
 .Pf . Cm \&ig Ar endmacro
 .Ar ignored text
 .Pf . Ar endmacro
 .Ed
 .Pp
 In the first case, input is ignored until a
 .Sq \&..
 request is encountered on its own line.
 In the second case, input is ignored until the specified
 .Sq Pf . Ar endmacro
 is encountered.
 Do not use the escape character
 .Sq \e
 anywhere in the definition of
 .Ar endmacro ;
 it would cause very strange behaviour.
 .Pp
 When the
 .Ar endmacro
 is a roff request or a roff macro, like in
 .Pp
 .D1 \&.ig if
 .Pp
 the subsequent invocation of
 .Ic \&if
 will first terminate the
 .Ar ignored text ,
 then be invoked as usual.
 Otherwise, it only terminates the
 .Ar ignored text ,
 and arguments following it or the
 .Sq \&..
 request are discarded.
 .It Ic \&in Op Oo Cm + Ns | Ns Cm - Oc Ns Ar width
 Change indentation.
 See
 .Xr man 7 .
 Ignored in
 .Xr mdoc 7 .
 .It Ic \&index Ar register stringname substring
 Find a substring in a string.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&it Ar expression macro
 Set an input line trap.
 The named
 .Ar macro
 will be invoked after processing the number of input text lines
 specified by the numerical
 .Ar expression .
 While evaluating the
 .Ar expression ,
 the unit suffixes described below
 .Sx Scaling Widths
 are ignored.
 .It Ic \&itc Ar expression macro
 Set an input line trap, not counting lines ending with \ec.
 Currently unsupported.
 .It Ic \&IX Ar class keystring
 To support the generation of a table of contents,
 .Xr pod2man 1
 emits this user-defined macro, usually without defining it.
 To avoid reporting large numbers of spurious errors,
 .Xr mandoc 1
 ignores it.
 .It Ic \&kern Op Cm 1 | 0
 Switch kerning on or off.
 Currently ignored.
 .It Ic \&kernafter Ar font char ... afmunits ...
 Increase kerning after some characters.
 This is a Heirloom extension and currently ignored.
 .It Ic \&kernbefore Ar font char ... afmunits ...
 Increase kerning before some characters.
 This is a Heirloom extension and currently ignored.
 .It Ic \&kernpair Ar font char ... font char ... afmunits
 Add a kerning pair to the kerning table.
 This is a Heirloom extension and currently ignored.
 .It Ic \&lc Op Ar glyph
 Define a leader repetition character.
 Currently unsupported.
 .It Ic \&lc_ctype Ar localename
 Set the
 .Dv LC_CTYPE
 locale.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&lds Ar macroname string
 Define a local string.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&length Ar register string
 Count the number of input characters in a string.
 Currently unsupported.
 .It Ic \&letadj Ar lspmin lshmin letss lspmax lshmax
 Dynamic letter spacing and reshaping.
 This is a Heirloom extension and currently ignored.
 .It Ic \&lf Ar lineno Op Ar filename
 Change the line number for error messages.
 Ignored because insecure.
 .It Ic \&lg Op Cm 1 | 0
 Switch the ligature mechanism on or off.
 Currently ignored.
 .It Ic \&lhang Ar font char ... afmunits
 Hang characters at left margin.
 This is a Heirloom extension and currently ignored.
 .It Ic \&linetabs Op Cm 1 | 0
 Enable or disable line-tabs mode.
 This is a groff extension and currently unsupported.
 .It Ic \&ll Op Oo Cm + Ns | Ns Cm - Oc Ns Ar width
 Change the output line length.
 If the
 .Ar width
 argument is omitted, the line length is reset to its previous value.
 The default setting for terminal output is 78n.
 If a sign is given, the line length is added to or subtracted from;
 otherwise, it is set to the provided value.
 Using this request in new manuals is discouraged for several reasons,
 among others because it overrides the
 .Xr mandoc 1
 .Fl O Cm width
 command line option.
-.It Ic \&lnr Ar register Oo Cm + Ns | Ns Cm - Oc Ns Ar value Op Ar increment
+.It Ic \&lnr Ar registername Xo
+.Oo Cm + Ns | Ns Cm \- Oc Ns Ar value
+.Op Ar increment
+.Xc
 Set local number register.
 This is a Heirloom extension and currently unsupported.
-.It Ic \&lnrf Ar register Oo Cm + Ns | Ns Cm - Oc Ns Ar value Op Ar increment
+.It Ic \&lnrf Ar registername Xo
+.Oo Cm + Ns | Ns Cm \- Oc Ns Ar value
+.Op Ar increment
+.Xc
 Set local floating-point register.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&lpfx Ar string
 Set a line prefix.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&ls Op Ar factor
 Set line spacing.
 It takes one integer argument specifying the vertical distance of
 subsequent output text lines measured in v units.
 Currently ignored.
 .It Ic \&lsm Ar macroname
 Set a leading spaces trap.
 This is a groff extension and currently unsupported.
 .It Ic \< Op Oo Cm + Ns | Ns Cm - Oc Ns Ar width
 Set title line length.
 Currently ignored.
 .It Ic \&mc Ar glyph Op Ar dist
 Print margin character in the right margin.
 The
 .Ar dist
 is currently ignored; instead, 1n is used.
 .It Ic \&mediasize Ar media
 Set the device media size.
 This is a Heirloom extension and currently ignored.
 .It Ic \&minss Ar width
 Set minimum word space.
 This is a Heirloom extension and currently ignored.
 .It Ic \&mk Op Ar register
 Mark vertical position.
 Currently ignored.
 .It Ic \&mso Ar filename
 Load a macro file using the search path.
 Ignored because insecure.
 .It Ic \&na
 Disable adjusting without changing the adjustment mode.
 Currently ignored.
 .It Ic \&ne Op Ar height
 Declare the need for the specified minimum vertical space
 before the next trap or the bottom of the page.
 Currently ignored.
 .It Ic \&nf
 Break the output line and switch to no-fill mode.
 Subsequent input lines are kept together on the same output line
 even when exceeding the right margin,
 and line breaks in subsequent input cause output line breaks.
 This request is implied by the
 .Xr mdoc 7
 .Ic \&Bd Fl unfilled
 and
 .Ic \&Bd Fl literal
 macros and by the
 .Xr man 7
 .Ic \&EX
 macro.
 The
 .Ic \&fi
 request switches back to the default fill mode.
 .It Ic \&nh
 Turn off automatic hyphenation mode.
 Currently ignored.
 .It Ic \&nhychar Ar char ...
 Define hyphenation-inhibiting characters.
 This is a Heirloom extension and currently ignored.
 .It Ic \&nm Op Ar start Op Ar inc Op Ar space Op Ar indent
 Print line numbers.
 Currently unsupported.
 .It Ic \&nn Op Ar number
 Temporarily turn off line numbering.
 Currently unsupported.
 .It Ic \&nop Ar body
 Execute the rest of the input line as a request, macro, or text line,
 skipping the
 .Ic \&nop
 request and any space characters immediately following it.
 This is mostly used to indent text lines inside macro definitions.
-.It Ic \&nr Ar register Oo Cm + Ns | Ns Cm - Oc Ns Ar expression Op Ar stepsize
-Define or change a register.
-A register is an arbitrary string value that defines some sort of state,
-which influences parsing and/or formatting.
+.It Ic \&nr Ar registername Xo
+.Oo Cm + Ns | Ns Cm \- Oc Ns Ar expression
+.Op Ar stepsize
+.Xc
+Define or change the number register with the given
+.Ar registername .
+A register can store an integer number.
 For the syntax of
 .Ar expression ,
 see
 .Sx Numerical expressions
 below.
 If it is prefixed by a sign, the register will be
 incremented or decremented instead of assigned to.
 .Pp
+Once set, the value of a number register can be interpolated using the
+.Ic \en
+escape sequence.
 The
 .Ar stepsize
 is used by the
 .Ic \en+
 auto-increment feature.
 It remains unchanged when omitted while changing an existing register,
 and it defaults to 0 when defining a new register.
 .Pp
-The following
-.Ar register
-is handled specially:
-.Bl -tag -width Ds
-.It Cm nS
-If set to a positive integer value, certain
-.Xr mdoc 7
-macros will behave in the same way as in the
-.Em SYNOPSIS
-section.
-If set to 0, these macros will behave in the same way as outside the
-.Em SYNOPSIS
-section, even when called within the
-.Em SYNOPSIS
-section itself.
-Note that starting a new
-.Xr mdoc 7
-section with the
-.Ic \&Sh
-macro will reset this register.
-.El
+Some number registers can be read to inspect parser state,
+and some can be changed to influence formatting.
+For details about individual registers, see the
+.Sx NUMBER REGISTER REFERENCE
+below.
 .It Xo
-.Ic \&nrf Ar register Oo Cm + Ns | Ns Cm - Oc Ns Ar expression
+.Ic \&nrf Ar registername Oo Cm + Ns | Ns Cm \- Oc Ns Ar expression
 .Op Ar increment
 .Xc
 Define or change a floating-point register.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&nroff
 Force nroff mode.
 This is a groff extension and currently ignored.
 .It Ic \&ns
 Turn on no-space mode.
 Currently ignored.
 .It Ic \&nx Op Ar filename
 Abort processing of the current input file and process another one.
 Ignored because insecure.
 .It Ic \&open Ar stream file
 Open a file for writing.
 Ignored because insecure.
 .It Ic \&opena Ar stream file
 Open a file for appending.
 Ignored because insecure.
 .It Ic \&os
 Output saved vertical space.
 Currently ignored.
 .It Ic \&output Ar string
 Output directly to intermediate output.
 Not supported.
 .It Ic \&padj Op Cm 1 | 0
 Globally control paragraph-at-once adjustment.
 This is a Heirloom extension and currently ignored.
 .It Ic \&papersize Ar media
 Set the paper size.
 This is a Heirloom extension and currently ignored.
 .It Ic \&pc Op Ar char
 Change the page number character.
 Currently ignored.
 .It Ic \&pev
 Print environments.
 This is a groff extension and currently ignored.
 .It Ic \&pi Ar command
 Pipe output to a shell command.
 Ignored because insecure.
 .It Ic \&PI
 Low-level request used by
 .Ic \&BP .
 This is a Heirloom extension and currently unsupported.
 .It Ic \&pl Op Oo Cm + Ns | Ns Cm - Oc Ns Ar height
 Change page length.
 Currently ignored.
 .It Ic \&pm
 Print names and sizes of macros, strings, and diversions
 to standard error output.
 Currently ignored.
 .It Ic \&pn Oo Cm + Ns | Ns Cm - Oc Ns Ar number
 Change the page number of the next page.
 Currently ignored.
 .It Ic \&pnr
 Print all number registers on standard error output.
 Currently ignored.
 .It Ic \&po Op Oo Cm + Ns | Ns Cm - Oc Ns Ar offset
 Set a horizontal page offset.
 If no argument is specified, the page offset is reverted to its
 previous value.
 If a sign is specified, the new page offset is calculated relative
 to the current one; otherwise, it is absolute.
 The argument follows the syntax of
 .Sx Scaling Widths
 and the default scaling unit is
 .Cm m .
 .It Ic \&ps Op Oo Cm + Ns | Ns Cm - Oc Ns size
 Change point size.
 Currently ignored.
 .It Ic \&psbb Ar filename
 Retrieve the bounding box of a PostScript file.
 Currently unsupported.
 .It Ic \&pshape Ar indent length ...
 Set a special shape for the current paragraph.
 This is a Heirloom extension and currently unsupported.
 .It Ic \&pso Ar command
 Include output of a shell command.
 Ignored because insecure.
 .It Ic \&ptr
 Print the names and positions of all traps on standard error output.
 This is a groff extension and currently ignored.
 .It Ic \&pvs Op Oo Cm + Ns | Ns Cm - Oc Ns Ar height
 Change post-vertical spacing.
 This is a groff extension and currently ignored.
 .It Ic \&rchar Ar glyph ...
 Remove glyph definitions.
 Currently unsupported.
 .It Ic \&rd Op Ar prompt Op Ar argument ...
 Read from standard input.
 Currently ignored.
 .It Ic \&recursionlimit Ar maxrec maxtail
 Set the maximum stack depth for recursive macros.
 This is a Heirloom extension and currently ignored.
 .It Ic \&return Op Ar twice
 Exit the presently executed macro and return to the caller.
 The argument is currently ignored.
 .It Ic \&rfschar Ar font glyph ...
 Remove font-specific fallback glyph definitions.
 Currently unsupported.
 .It Ic \&rhang Ar font char ... afmunits
 Hang characters at right margin.
 This is a Heirloom extension and currently ignored.
 .It Ic \&rj Op Ar N
 Justify the next
 .Ar N
 input lines to the right margin without filling.
 .Ar N
 defaults to 1.
 An argument of 0 or less ends right adjustment.
 .It Ic \&rm Ar macroname
 Remove a request, macro or string.
 .It Ic \&rn Ar oldname newname
 Rename a request, macro, diversion, or string.
 In
 .Xr mandoc 1 ,
 user-defined macros,
 .Xr mdoc 7
 and
 .Xr man 7
 macros, and user-defined strings can be renamed, but renaming of
 predefined strings and of
 .Nm
 requests is not supported, and diversions are not implemented at all.
 .It Ic \&rnn Ar oldname newname
 Rename a number register.
 Currently unsupported.
 .It Ic \&rr Ar register
-Remove a register.
+Remove a number register.
 .It Ic \&rs
 End no-space mode.
 Currently ignored.
 .It Ic \&rt Op Ar dist
 Return to marked vertical position.
 Currently ignored.
 .It Ic \&schar Ar glyph Op Ar string
 Define global fallback glyph.
 This is a groff extension and currently unsupported.
 .It Ic \&sentchar Ar char ...
 Define sentence-ending characters.
 This is a Heirloom extension and currently ignored.
 .It Ic \&shc Op Ar glyph
 Change the soft hyphen character.
 Currently ignored.
 .It Ic \&shift Op Ar number
 Shift macro arguments
 .Ar number
 times, by default once: \e\e$i becomes what \e\e$i+number was.
 Also decrement \en(.$ by
 .Ar number .
 .It Ic \&sizes Ar size ...
 Define permissible point sizes.
 This is a groff extension and currently ignored.
 .It Ic \&so Ar filename
 Include a source file.
 The file is read and its contents processed as input in place of the
 .Ic \&so
 request line.
 To avoid inadvertent inclusion of unrelated files,
 .Xr mandoc 1
 only accepts relative paths not containing the strings
 .Qq ../
 and
 .Qq /.. .
 .Pp
 This request requires
 .Xr man 1
 to change to the right directory before calling
 .Xr mandoc 1 ,
 per convention to the root of the manual tree.
 Typical usage looks like:
 .Pp
 .Dl \&.so man3/Xcursor.3
 .Pp
 As the whole concept is rather fragile, the use of
 .Ic \&so
 is discouraged.
 Use
 .Xr ln 1
 instead.
 .It Ic \&sp Op Ar height
 Break the output line and emit vertical space.
 The argument follows the syntax of
 .Sx Scaling Widths
 and defaults to one blank line
 .Pq Li 1v .
 .It Ic \&spacewidth Op Cm 1 | 0
 Set the space width from the font metrics file.
 This is a Heirloom extension and currently ignored.
 .It Ic \&special Op Ar font ...
 Define a special font.
 This is a groff extension and currently ignored.
 .It Ic \&spreadwarn Op Ar width
 Warn about wide spacing between words.
 Currently ignored.
 .It Ic \&ss Ar wordspace Op Ar sentencespace
 Set space character size.
 Currently ignored.
 .It Ic \&sty Ar position style
 Associate style with a font position.
 This is a groff extension and currently ignored.
 .It Ic \&substring Ar stringname startpos Op Ar endpos
 Replace a user-defined string with a substring.
 Currently unsupported.
 .It Ic \&sv Op Ar height
 Save vertical space.
 Currently ignored.
 .It Ic \&sy Ar command
 Execute shell command.
 Ignored because insecure.
 .It Ic \&T&
 Re-start a table layout, retaining the options of the prior table
 invocation.
 See
 .Ic \&TS .
 .It Ic \&ta Op Ar width ... Op Cm T Ar width ...
 Set tab stops.
 Each
 .Ar width
 argument follows the syntax of
 .Sx Scaling Widths .
 If prefixed by a plus sign, it is relative to the previous tab stop.
 The arguments after the
 .Cm T
 marker are used repeatedly as often as needed; for each reuse,
 they are taken relative to the last previously established tab stop.
 When
 .Ic \&ta
 is called without arguments, all tab stops are cleared.
 .It Ic \&tc Op Ar glyph
 Change tab repetition character.
 Currently unsupported.
 .It Ic \&TE
 End a table context.
 See
 .Ic \&TS .
 .It Ic \&ti Oo Cm + Ns | Ns Cm - Oc Ns Ar width
 Break the output line and indent the next output line by
 .Ar width .
 If a sign is specified, the temporary indentation is calculated
 relative to the current indentation; otherwise, it is absolute.
 The argument follows the syntax of
 .Sx Scaling Widths
 and the default scaling unit is
 .Cm m .
 .It Ic \&tkf Ar font minps width1 maxps width2
 Enable track kerning for a font.
 Currently ignored.
 .It Ic \&tl No \& Ap Ar left Ap Ar center Ap Ar right Ap
 Print a title line.
 Currently unsupported.
 .It Ic \&tm Ar string
 Print to standard error output.
 Currently ignored.
 .It Ic \&tm1 Ar string
 Print to standard error output, allowing leading blanks.
 This is a groff extension and currently ignored.
 .It Ic \&tmc Ar string
 Print to standard error output without a trailing newline.
 This is a groff extension and currently ignored.
 .It Ic \&tr Ar glyph glyph ...
 Output character translation.
 The first glyph in each pair is replaced by the second one.
 Character escapes can be used; for example,
 .Pp
 .Dl tr \e(xx\e(yy
 .Pp
 replaces all invocations of \e(xx with \e(yy.
 .It Ic \&track Ar font minps width1 maxps width2
 Static letter space tracking.
 This is a Heirloom extension and currently ignored.
 .It Ic \&transchar Ar char ...
 Define transparent characters for sentence-ending.
 This is a Heirloom extension and currently ignored.
 .It Ic \&trf Ar filename
 Output the contents of a file, disallowing invalid characters.
 This is a groff extension and ignored because insecure.
 .It Ic \&trimat Ar left top width height
 Set the TrimBox page parameter for PDF generation.
 This is a Heirloom extension and currently ignored.
 .It Ic \&trin Ar glyph glyph ...
 Output character translation, ignored by
 .Ic \&asciify .
 Currently unsupported.
 .It Ic \&trnt Ar glyph glyph ...
 Output character translation, ignored by \e!.
 Currently unsupported.
 .It Ic \&troff
 Force troff mode.
 This is a groff extension and currently ignored.
 .It Ic \&TS
 Begin a table, which formats input in aligned rows and columns.
 See
 .Xr tbl 7
 for a description of the tbl language.
 .It Ic \&uf Ar font
 Globally set the underline font.
 Currently ignored.
 .It Ic \&ul Op Ar N
 Underline next
 .Ar N
 input lines.
 Currently ignored.
 .It Ic \&unformat Ar divname
 Unformat spaces and tabs in a diversion.
 Currently unsupported.
 .It Ic \&unwatch Ar macroname
 Disable notification for string or macro.
 This is a Heirloom extension and currently ignored.
 .It Ic \&unwatchn Ar register
 Disable notification for register.
 This is a Heirloom extension and currently ignored.
 .It Ic \&vpt Op Cm 1 | 0
 Enable or disable vertical position traps.
 This is a groff extension and currently ignored.
 .It Ic \&vs Op Oo Cm + Ns | Ns Cm - Oc Ns Ar height
 Change vertical spacing.
 Currently ignored.
 .It Ic \&warn Ar flags
 Set warning level.
 Currently ignored.
 .It Ic \&warnscale Ar si
 Set the scaling indicator used in warnings.
 This is a groff extension and currently ignored.
 .It Ic \&watch Ar macroname
 Notify on change of string or macro.
 This is a Heirloom extension and currently ignored.
 .It Ic \&watchlength Ar maxlength
 On change, report the contents of macros and strings
 up to the specified length.
 This is a Heirloom extension and currently ignored.
 .It Ic \&watchn Ar register
 Notify on change of register.
 This is a Heirloom extension and currently ignored.
 .It Ic \&wh Ar dist Op Ar macroname
 Set a page location trap.
 Currently unsupported.
 .It Ic \&while Ar condition body
 Repeated execution while a
 .Ar condition
 is true, with syntax similar to
 .Ic \&if .
 Currently implemented with two restrictions: cannot nest,
 and each loop must start and end in the same scope.
 .It Ic \&write Oo \(dq Oc Ns Ar string
 Write to an open file.
 Ignored because insecure.
 .It Ic \&writec Oo \(dq Oc Ns Ar string
 Write to an open file without appending a newline.
 Ignored because insecure.
 .It Ic \&writem Ar macroname
 Write macro or string to an open file.
 Ignored because insecure.
 .It Ic \&xflag Ar level
 Set the extension level.
 This is a Heirloom extension and currently ignored.
 .El
 .Ss Numerical expressions
 The
 .Ic \&nr ,
 .Ic \&if ,
 and
 .Ic \&ie
 requests accept integer numerical expressions as arguments.
 These are always evaluated using the C
 .Vt int
 type; integer overflow works the same way as in the C language.
 Numbers consist of an arbitrary number of digits
 .Sq 0
 to
 .Sq 9
 prefixed by an optional sign
 .Sq +
 or
 .Sq - .
 Each number may be followed by one optional scaling unit described below
 .Sx Scaling Widths .
 The following equations hold:
 .Bd -literal -offset indent
 1i = 6v = 6P = 10m = 10n = 72p = 1000M = 240u = 240
 254c = 100i = 24000u = 24000
 1f = 65536u = 65536
 .Ed
 .Pp
 The following binary operators are implemented.
 Unless otherwise stated, they behave as in the C language:
 .Pp
 .Bl -tag -width 2n -compact
 .It Ic +
 addition
 .It Ic -
 subtraction
 .It Ic *
 multiplication
 .It Ic /
 division
 .It Ic %
 remainder of division
 .It Ic <
 less than
 .It Ic >
 greater than
 .It Ic ==
 equal to
 .It Ic =
 equal to, same effect as
 .Ic ==
 (this differs from C)
 .It Ic <=
 less than or equal to
 .It Ic >=
 greater than or equal to
 .It Ic <>
 not equal to (corresponds to C
 .Ic != ;
 this one is of limited portability, it is supported by Heirloom roff,
 but not by groff)
 .It Ic &
 logical and (corresponds to C
 .Ic && )
 .It Ic \&:
 logical or (corresponds to C
 .Ic || )
 .It Ic ?
 maximum (not available in C)
 .El
 .Pp
 There is no concept of precedence; evaluation proceeds from left to right,
 except when subexpressions are enclosed in parentheses.
 Inside parentheses, whitespace is ignored.
 .Sh ESCAPE SEQUENCE REFERENCE
 The
 .Xr mandoc 1
 .Nm
 parser recognises the following escape sequences.
 In
 .Xr mdoc 7
 and
 .Xr man 7
 documents, using escape sequences is discouraged except for those
 described in the
 .Sx LANGUAGE SYNTAX
 section above.
 .Pp
 A backslash followed by any character not listed here
 simply prints that character itself.
 .Bl -tag -width Ds
 .It Ic \e
 A backslash at the end of an input line can be used to continue the
 logical input line on the next physical input line, joining the text
 on both lines together as if it were on a single input line.
 .It Ic \e
 The escape sequence backslash-space
 .Pq Sq \e\ \&
 is an unpaddable space-sized non-breaking space character; see
 .Sx Whitespace
 and
 .Xr mandoc_char 7 .
 .It Ic \e!
 Embed text up to and including the end of the input line into the
 current diversion or into intermediate output without interpreting
 requests, macros, and escapes.
 Currently unsupported.
 .It Ic \e\(dq
 The rest of the input line is treated as
 .Sx Comments .
 .It Ic \e#
 Line continuation with comment.
 Discard the rest of the physical input line and continue the logical
 input line on the next physical input line, joining the text on
 both lines together as if it were on a single input line.
 This is a groff extension.
 .It Ic \e$ Ns Ar arg
 Macro argument expansion, see
 .Ic \&de .
 .It Ic \e%
 Hyphenation allowed at this point of the word; ignored by
 .Xr mandoc 1 .
 .It Ic \e&
 Non-printing zero-width character,
 often used for various kinds of escaping; see
 .Sx Whitespace ,
 .Xr mandoc_char 7 ,
 and the
 .Dq MACRO SYNTAX
 and
 .Dq Delimiters
 sections in
 .Xr mdoc 7 .
 .It Ic \e\(aq
 Acute accent special character; use
 .Ic \e(aa
 instead.
 .It Ic \e( Ns Ar cc
 .Sx Special Characters
 with two-letter names, see
 .Xr mandoc_char 7 .
 .It Ic \e)
 Zero-width space transparent to end-of-sentence detection;
 ignored by
 .Xr mandoc 1 .
 .It Ic \e*[ Ns Ar name Ns Ic \&]
 Interpolate the string with the
 .Ar name .
 For short names, there are variants
 .Ic \e* Ns Ar c
 and
 .Ic \e*( Ns Ar cc .
 .Pp
 One string is predefined on the
 .Nm
 language level:
 .Ic \e*(.T
 expands to the name of the output device,
 for example ascii, utf8, ps, pdf, html, or markdown.
 .Pp
 Macro sets traditionally predefine additional strings which are not
 portable and differ across implementations.
 Those supported by
 .Xr mandoc 1
 are listed in
 .Xr mandoc_char 7 .
 .Pp
 Strings can be defined, changed, and deleted with the
 .Ic \&ds ,
 .Ic \&as ,
 and
 .Ic \&rm
 requests.
 .It Ic \e,
 Left italic correction (groff extension); ignored by
 .Xr mandoc 1 .
 .It Ic \e-
 Special character
 .Dq mathematical minus sign ;
 see
 .Xr mandoc_char 7
 for details.
 .It Ic \e/
 Right italic correction (groff extension); ignored by
 .Xr mandoc 1 .
 .It Ic \e:
 Breaking the line is allowed at this point of the word
 without inserting a hyphen.
 .It Ic \e?
 Embed the text up to the next
 .Ic \e?
 into the current diversion without interpreting requests, macros,
 and escapes.
 This is a groff extension and currently unsupported.
 .It Ic \e[ Ns Ar name Ns Ic \&]
 .Sx Special Characters
 with names of arbitrary length, see
 .Xr mandoc_char 7 .
 .It Ic \e^
 One-twelfth em half-narrow space character, effectively zero-width in
 .Xr mandoc 1 .
 .It Ic \e_
 Underline special character; use
 .Ic \e(ul
 instead.
 .It Ic \e`
 Grave accent special character; use
 .Ic \e(ga
 instead.
 .It Ic \e{
 Begin conditional input; see
 .Ic \&if .
 .It Ic \e\(ba
 One-sixth em narrow space character, effectively zero-width in
 .Xr mandoc 1 .
 .It Ic \e}
 End conditional input; see
 .Ic \&if .
 .It Ic \e~
 Paddable non-breaking space character.
 .It Ic \e0
 Digit width space character.
 .It Ic \eA\(aq Ns Ar name Ns Ic \(aq
 Interpolate
 .Sq 1
 if
 .Ar name
 is a syntactically valid identifier that can be used
 as a name for a macro or user-defined string, or
 .Sq 0
 otherwise.
 This is a thoroughly non-portable groff extension.
 Heirloom troff uses the same escape sequence with the same syntax
 for a completely different purpose,
 defining a hyperlink target position, also called an
 .Dq anchor ,
 with the given
 .Ar name .
 The Heirloom semantics is not supported by
 .Xr mandoc 1 .
 .It Ic \ea
 Leader character; ignored by
 .Xr mandoc 1 .
 .It Ic \eB\(aq Ns Ar string Ns Ic \(aq
 Interpolate
 .Sq 1
 if
 .Ar string
 conforms to the syntax of
 .Sx Numerical expressions
 explained above or
 .Sq 0
 otherwise.
 .It Ic \eb\(aq Ns Ar string Ns Ic \(aq
 Bracket building function; ignored by
 .Xr mandoc 1 .
 .It Ic \eC\(aq Ns Ar name Ns Ic \(aq
 .Sx Special Characters
 with names of arbitrary length.
 .It Ic \ec
 When encountered at the end of an input text line,
 the next input text line is considered to continue that line,
 even if there are request or macro lines in between.
 No whitespace is inserted.
 .It Ic \eD\(aq Ns Ar string Ns Ic \(aq
 Draw graphics function; ignored by
 .Xr mandoc 1 .
 .It Ic \ed
 Move down by half a line; ignored by
 .Xr mandoc 1 .
 .It Ic \eE
 Escape character intended to not be interpreted in copy mode.
 In
 .Xr mandoc 1 ,
 it currently does the same as
 .Ic \e
 itself.
 .It Ic \ee
 Backslash special character.
 .It Ic \eF[ Ns Ar name Ns Ic \&]
 Switch font family (groff extension); ignored by
 .Xr mandoc 1 .
 For short names, there are variants
 .Ic \eF Ns Ar c
 and
 .Ic \eF( Ns Ar cc .
 .It Ic \ef[ Ns Ar name Ns Ic \&]
 Switch to the font
 .Ar name ,
 see
 .Sx Font Selection .
 For short names, there are variants
 .Ic \ef Ns Ar c
 and
 .Ic \ef( Ns Ar cc .
 An empty name
 .Ic \ef[]
 defaults to
 .Ic \efP .
 .It Ic \eg[ Ns Ar name Ns Ic \&]
 Interpolate the format of a number register; ignored by
 .Xr mandoc 1 ,
 which interpolates an empty string instead.
 For short names, there are variants
 .Ic \eg Ns Ar c
 and
 .Ic \eg( Ns Ar cc .
 .It Ic \eH\(aq Ns Oo +|- Oc Ns Ar number Ns Ic \(aq
 Set the height of the current font; ignored by
 .Xr mandoc 1 .
 .It Ic \eh\(aq Ns Oo Cm \&| Oc Ns Ar width Ns Ic \(aq
 Horizontal motion.
 If the vertical bar is given, the motion is relative to the current
 indentation.
 Otherwise, it is relative to the current position.
 The default scaling unit is
 .Cm m .
 .It Ic \ek[ Ns Ar name Ns Ic \&]
 Mark horizontal input place in register; ignored by
 .Xr mandoc 1 .
 For short names, there are variants
 .Ic \ek Ns Ar c
 and
 .Ic \ek( Ns Ar cc .
 .It Ic \eL\(aq Ns Ar number Ns Oo Ar c Oc Ns Ic \(aq
 Vertical line drawing function; ignored by
 .Xr mandoc 1 .
 .It Ic \el\(aq Ns Ar width Ns Oo Ar c Oc Ns Ic \(aq
 Draw a horizontal line of
 .Ar width
 using the glyph
 .Ar c .
 .It Ic \eM[ Ns Ar name Ns Ic \&]
 Set fill (background) color (groff extension); ignored by
 .Xr mandoc 1 .
 For short names, there are variants
 .Ic \eM Ns Ar c
 and
 .Ic \eM( Ns Ar cc .
 .It Ic \em[ Ns Ar name Ns Ic \&]
 Set glyph drawing color (groff extension); ignored by
 .Xr mandoc 1 .
 For short names, there are variants
 .Ic \em Ns Ar c
 and
 .Ic \em( Ns Ar cc .
 .It Ic \eN\(aq Ns Ar number Ns Ic \(aq
 Character
 .Ar number
 on the current font.
 .It Ic \en Ns Oo +|- Oc Ns Ic \&[ Ns Ar name Ns Ic \&]
 Interpolate the number register
 .Ar name .
+If the register is not yet defined,
+it is automatically initialised to zero before interpolation.
 For short names, there are variants
 .Ic \en Ns Ar c
 and
 .Ic \en( Ns Ar cc .
 If the optional sign is specified,
 the register is first incremented or decremented by the
 .Ar stepsize
 that was specified in the relevant
 .Ic \&nr
 request, and the changed value is interpolated.
+For the names of predefined registers, see the
+.Sx NUMBER REGISTER REFERENCE
+below.
 .It Ic \eO Ns Ar digit , Ic \eO[5 Ns arguments Ns Ic \&]
 Suppress output.
 This is a groff extension and currently unsupported.
 With an argument of
 .Ic 1 , 2 , 3 ,
 or
 .Ic 4 ,
 it is ignored.
 .It Ic \eo\(aq Ns Ar string Ns Ic \(aq
 Overstrike, writing all the characters contained in the
 .Ar string
 to the same output position.
 In terminal and HTML output modes,
 only the last one of the characters is visible.
 .It Ic \ep
 Break the output line at the end of the current word.
 .It Ic \eR\(aq Ns Ar name Oo +|- Oc Ns Ar number Ns Ic \(aq
 Set number register; ignored by
 .Xr mandoc 1 .
 .It Ic \er
 Reverse line feed: move up by one output line.
 Currently unsupported.
 .It Ic \eS\(aq Ns Ar number Ns Ic \(aq
 Slant output; ignored by
 .Xr mandoc 1 .
 .It Ic \es\(aq Ns Oo +|- Oc Ns Ar number Ns Ic \(aq
 Change point size; ignored by
 .Xr mandoc 1 .
 Alternative forms
 .Ic \es Ns Oo +|- Oc Ns Ar n ,
 .Ic \es Ns Oo +|- Oc Ns Ic \(aq Ns Ar number Ns Ic \(aq ,
 .Ic \es[ Ns Oo +|- Oc Ns Ar number Ns Ic \&] ,
 and
 .Ic \es Ns Oo +|- Oc Ns Ic \&[ Ns Ar number Ns Ic \&]
 are also parsed and ignored.
 .It Ic \et
 Horizontal tab; ignored by
 .Xr mandoc 1 .
 .It Ic \eu
 Move up by half a line; ignored by
 .Xr mandoc 1 .
 .It Ic \eV[ Ns Ar name Ns Ic \&]
 Interpolate an environment variable.
 For short names, there are variants
 .Ic \eV Ns Ar c
 and
 .Ic \eV( Ns Ar cc .
 This escape sequence is intentionally unsupported;
 .Xr mandoc 1
 prints the string
 .Dq Pf $ Brq Ar name
 instead of inspecting the environment.
 .It Ic \ev\(aq Ns Ar number Ns Ic \(aq
 Vertical motion; ignored by
 .Xr mandoc 1 .
 .It Ic \ew\(aq Ns Ar string Ns Ic \(aq
 Interpolate the width of the
 .Ar string .
 The
 .Xr mandoc 1
 implementation assumes that after expansion of user-defined strings, the
 .Ar string
 only contains normal characters, characters expressed as escape sequences,
 and zero-width escape sequences, and that each
 character has a width of 24 basic units.
 .It Ic \eX\(aq Ns Ar string Ns Ic \(aq
 Output
 .Ar string
 as device control function; ignored in nroff mode and by
 .Xr mandoc 1 .
 .It Ic \ex\(aq Ns Ar number Ns Ic \(aq
 Extra line space function; ignored by
 .Xr mandoc 1 .
 .It Ic \eY[ Ns Ar name Ns Ic \&]
 Output a string as a device control function; ignored in nroff mode and by
 .Xr mandoc 1 .
 For short names, there are variants
 .Ic \eY Ns Ar c
 and
 .Ic \eY( Ns Ar cc .
 .It Ic \eZ\(aq Ns Ar string Ns Ic \(aq
 Print
 .Ar string
 with zero width and height; ignored by
 .Xr mandoc 1 .
 .It Ic \ez
 Output the next character without advancing the cursor position.
 .El
+.Sh NUMBER REGISTER REFERENCE
+In
+.Xr mdoc 7
+and
+.Xr man 7
+documents, using registers is discouraged.
+For compatibility with legacy documents, the
+.Xr mandoc 1
+.Nm
+parser recognises the following names of read-only registers:
+.Bl -tag -width Ds
+.It Cm .$
+The number of arguments of the innermost user-defined macro
+currently being called, or 0 by default.
+The
+.Ic shift
+request decrements the value of this register.
+.It Cm .A
+Whether ASCII approximation mode is on;
+.Xr mandoc 1
+always returns 0, meaning off.
+.It Cm .g
+Whether the formatter claims groff compatibility;
+.Xr mandoc 1
+always returns 1, meaning yes.
+.It Cm .H
+The minimum horizontal movement in basic units;
+.Xr mandoc 1
+always returns 24, corresponding to one character position.
+.It Cm .j
+The current line adjustment mode;
+.Xr mandoc 1
+always returns 0, meaning flush left.
+.It Cm .l
+The line length in basic units;
+.Xr mandoc 1
+always returns 78 * 24, corresponding to 78 characters per line.
+.It Cm \&.T
+Whether an output device has been selected;
+.Xr mandoc 1
+always returns 1, meaning yes.
+.It Cm .V
+The minimum vertical movement in basic units;
+.Xr mandoc 1
+always returns 40, corresponding to one line height.
+.El
+.Pp
+The
+.Cm nS
+register is handled specially.
+If set to a positive integer value, certain
+.Xr mdoc 7
+macros behave in the same way as in the
+.Em SYNOPSIS
+section.
+If set to 0, these macros behave in the same way as outside the
+.Em SYNOPSIS
+section, even when called within the
+.Em SYNOPSIS
+section itself.
+Starting a new
+.Xr mdoc 7
+section with the
+.Ic \&Sh
+macro resets this register.
+.Pp
+Full
+.Nm
+implementations support large numbers of additional predefined registers.
+While the
+.Ic \&nr
+request supports setting and the
+.Ic \en
+escape sequence supports inspecting arbitrary registers,
+.Xr mandoc 1
+only defines the few registers listed above by default.
+All other registers are undefined by default and yield 0 when interpolated.
 .Sh COMPATIBILITY
 The
 .Xr mandoc 1
 implementation of the
 .Nm
 language is incomplete.
 Major unimplemented features include:
 .Pp
 .Bl -dash -compact
 .It
 For security reasons,
 .Xr mandoc 1
 never reads or writes external files except via
 .Ic \&so
 requests with safe relative paths.
 .It
-There is no automatic hyphenation, no adjustment to the right margin,
-and very limited support for centering; the output is always set flush-left.
-.It
-Support for setting tabulator and leader characters is missing,
-and support for manually changing indentation is limited.
+There is no automatic hyphenation and no support for the
+.Ic \&ad
+line adjustment request.
+Except when the
+.Ic \&ce
+or
+.Ic \&rj
+requests or the
+.Xr tbl 7
+cell specifications
+.Cm c ,
+.Cm n ,
+or
+.Cm r
+or the table option
+.Cm center
+are used, output is always set flush-left.
 .It
-The
-.Sq u
-scaling unit is the default terminal unit.
-In traditional troff systems, this unit changes depending on the
-output media.
+Support for setting tabulator and leader characters is missing, and the
+.Ic \&in
+indentation request is not supported in
+.Xr mdoc 7
+input files.
 .It
 Width measurements are implemented in a crude way
 and often yield wrong results.
 Support for explicit movement requests and escapes is limited.
 .It
 There is no concept of output pages, no support for floats,
 graphics drawing, and picture inclusion;
 terminal output is always continuous.
 .It
 Requests regarding color, font families, font sizes,
 and glyph manipulation are ignored.
 Font support is very limited.
 Kerning is not implemented, and no ligatures are produced.
 .It
 The
 .Qq \(aq
 macro control character does not suppress output line breaks.
 .It
 Diversions and environments are not implemented,
 and support for traps is very incomplete.
 .It
 Use of macros is not supported inside
 .Xr tbl 7
 code.
 .El
 .Pp
 The special semantics of the
 .Cm nS
 number register is an idiosyncrasy of
 .Ox
 manuals and not supported by other
 .Xr mdoc 7
 implementations.
 .Sh SEE ALSO
 .Xr mandoc 1 ,
 .Xr eqn 7 ,
 .Xr man 7 ,
 .Xr mandoc_char 7 ,
 .Xr mdoc 7 ,
 .Xr tbl 7
 .Rs
 .%A Joseph F. Ossanna
 .%A Brian W. Kernighan
 .%I AT&T Bell Laboratories
 .%T Troff User's Manual
 .%R Computing Science Technical Report
 .%N 54
 .%C Murray Hill, New Jersey
 .%D 1976 and 1992
 .%U http://www.kohala.com/start/troff/cstr54.ps
 .Re
 .Rs
 .%A Joseph F. Ossanna
 .%A Brian W. Kernighan
 .%A Gunnar Ritter
 .%T Heirloom Documentation Tools Nroff/Troff User's Manual
 .%D September 17, 2007
 .%U http://heirloom.sourceforge.net/doctools/troff.pdf
 .Re
+.Rs
+.%A James Clark
+.%A Werner Lemberg
+.%A G. Branden Robinson
+.%I Free Software Foundation, Inc.
+.%T The GNU Troff Manual
+.%D 1999\(en2023
+.%U https://www.gnu.org/software/groff/manual/
+.Re
 .Sh HISTORY
 The RUNOFF typesetting system, whose input forms the basis for
 .Nm ,
 was written in MAD and FAP for the CTSS operating system by Jerome E.
 Saltzer in 1964.
 Doug McIlroy rewrote it in BCPL in 1969, renaming it
 .Nm .
 Dennis M. Ritchie rewrote McIlroy's
 .Nm
 in PDP-11 assembly for
 .At v1 ,
 Joseph F. Ossanna improved roff and renamed it nroff
 for
 .At v2 ,
 then ported nroff to C as troff, which Brian W. Kernighan released with
 .At v7 .
 In 1989, James Clark re-implemented troff in C++, naming it groff.
 .Sh AUTHORS
 .An -nosplit
 This
 .Nm
 reference was written by
 .An Kristaps Dzonsons Aq Mt kristaps@bsd.lv
 and
 .An Ingo Schwarze Aq Mt schwarze@openbsd.org .
diff --git a/contrib/mandoc/roff_term.c b/contrib/mandoc/roff_term.c
index f696898ebd5a..8f95aa920790 100644
--- a/contrib/mandoc/roff_term.c
+++ b/contrib/mandoc/roff_term.c
@@ -1,266 +1,275 @@
-/* $Id: roff_term.c,v 1.25 2023/04/28 19:11:04 schwarze Exp $ */
+/* $Id: roff_term.c,v 1.26 2025/07/16 14:33:08 schwarze Exp $ */
 /*
- * Copyright (c) 2010,2014,2015,2017-2020 Ingo Schwarze 
+ * Copyright (c) 2010, 2014, 2015, 2017-2021, 2025
+ *               Ingo Schwarze 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 
 #include "mandoc.h"
 #include "roff.h"
 #include "out.h"
 #include "term.h"
 
 #define	ROFF_TERM_ARGS struct termp *p, const struct roff_node *n
 
 typedef	void	(*roff_term_pre_fp)(ROFF_TERM_ARGS);
 
 static	void	  roff_term_pre_br(ROFF_TERM_ARGS);
 static	void	  roff_term_pre_ce(ROFF_TERM_ARGS);
 static	void	  roff_term_pre_ft(ROFF_TERM_ARGS);
 static	void	  roff_term_pre_ll(ROFF_TERM_ARGS);
 static	void	  roff_term_pre_mc(ROFF_TERM_ARGS);
 static	void	  roff_term_pre_po(ROFF_TERM_ARGS);
 static	void	  roff_term_pre_sp(ROFF_TERM_ARGS);
 static	void	  roff_term_pre_ta(ROFF_TERM_ARGS);
 static	void	  roff_term_pre_ti(ROFF_TERM_ARGS);
 
 static	const roff_term_pre_fp roff_term_pre_acts[ROFF_MAX] = {
 	roff_term_pre_br,  /* br */
 	roff_term_pre_ce,  /* ce */
 	roff_term_pre_br,  /* fi */
 	roff_term_pre_ft,  /* ft */
 	roff_term_pre_ll,  /* ll */
 	roff_term_pre_mc,  /* mc */
 	roff_term_pre_br,  /* nf */
 	roff_term_pre_po,  /* po */
 	roff_term_pre_ce,  /* rj */
 	roff_term_pre_sp,  /* sp */
 	roff_term_pre_ta,  /* ta */
 	roff_term_pre_ti,  /* ti */
 };
 
 
 void
 roff_term_pre(struct termp *p, const struct roff_node *n)
 {
 	assert(n->tok < ROFF_MAX);
 	(*roff_term_pre_acts[n->tok])(p, n);
 }
 
 static void
 roff_term_pre_br(ROFF_TERM_ARGS)
 {
 	term_newln(p);
 	if (p->flags & TERMP_BRIND) {
 		p->tcol->offset = p->tcol->rmargin;
 		p->tcol->rmargin = p->maxrmargin;
 		p->trailspace = 0;
 		p->flags &= ~(TERMP_NOBREAK | TERMP_BRIND);
 		p->flags |= TERMP_NOSPACE;
 	}
 }
 
 static void
 roff_term_pre_ce(ROFF_TERM_ARGS)
 {
 	const struct roff_node	*nc1, *nc2;
 
 	roff_term_pre_br(p, n);
 	p->flags |= n->tok == ROFF_ce ? TERMP_CENTER : TERMP_RIGHT;
 	nc1 = n->child->next;
 	while (nc1 != NULL) {
 		nc2 = nc1;
 		do {
 			nc2 = nc2->next;
 		} while (nc2 != NULL && (nc2->type != ROFFT_TEXT ||
 		    (nc2->flags & NODE_LINE) == 0));
 		while (nc1 != nc2) {
 			if (nc1->type == ROFFT_TEXT)
 				term_word(p, nc1->string);
 			else
 				roff_term_pre(p, nc1);
 			nc1 = nc1->next;
 		}
 		p->flags |= TERMP_NOSPACE;
 		term_flushln(p);
 	}
 	p->flags &= ~(TERMP_CENTER | TERMP_RIGHT);
 }
 
 static void
 roff_term_pre_ft(ROFF_TERM_ARGS)
 {
 	const char	*cp;
 
 	cp = n->child->string;
 	switch (mandoc_font(cp, (int)strlen(cp))) {
 	case ESCAPE_FONTBOLD:
 	case ESCAPE_FONTCB:
 		term_fontrepl(p, TERMFONT_BOLD);
 		break;
 	case ESCAPE_FONTITALIC:
 	case ESCAPE_FONTCI:
 		term_fontrepl(p, TERMFONT_UNDER);
 		break;
 	case ESCAPE_FONTBI:
 		term_fontrepl(p, TERMFONT_BI);
 		break;
 	case ESCAPE_FONTPREV:
 		term_fontlast(p);
 		break;
 	case ESCAPE_FONTROMAN:
 	case ESCAPE_FONTCR:
 		term_fontrepl(p, TERMFONT_NONE);
 		break;
 	default:
 		break;
 	}
 }
 
 static void
 roff_term_pre_ll(ROFF_TERM_ARGS)
 {
 	term_setwidth(p, n->child != NULL ? n->child->string : NULL);
 }
 
 static void
 roff_term_pre_mc(ROFF_TERM_ARGS)
 {
 	if (p->col) {
 		p->flags |= TERMP_NOBREAK;
 		term_flushln(p);
 		p->flags &= ~(TERMP_NOBREAK | TERMP_NOSPACE);
 	}
 	if (n->child != NULL) {
 		p->mc = n->child->string;
 		p->flags |= TERMP_NEWMC;
 	} else
 		p->flags |= TERMP_ENDMC;
 }
 
 static void
 roff_term_pre_po(ROFF_TERM_ARGS)
 {
 	struct roffsu	 su;
-	static int	 po, pouse, polast;
-	int		 ponew;
+
+	/* Page offsets in basic units. */
+	static int	 polast;  /* Previously requested. */
+	static int	 po;      /* Currently requested. */
+	static int	 pouse;   /* Currently used. */
+	int		 pomax;   /* Maximum to be used. */
+	int		 ponew;   /* Newly requested. */
 
 	/* Revert the currently active page offset. */
 	p->tcol->offset -= pouse;
 
 	/* Determine the requested page offset. */
 	if (n->child != NULL &&
 	    a2roffsu(n->child->string, &su, SCALE_EM) != NULL) {
-		ponew = term_hen(p, &su);
+		ponew = term_hspan(p, &su);
 		if (*n->child->string == '+' ||
 		    *n->child->string == '-')
 			ponew += po;
 	} else
 		ponew = polast;
 
 	/* Remember both the previous and the newly requested offset. */
 	polast = po;
 	po = ponew;
 
 	/* Truncate to the range [-offset, 60], remember, and apply it. */
-	pouse = po >= 60 ? 60 :
-	    po < -(int)p->tcol->offset ? -(int)p->tcol->offset : po;
+	pomax = term_len(p, 60);
+	pouse = po >= pomax ? pomax :
+	    po < -(int)p->tcol->offset ? -p->tcol->offset : po;
 	p->tcol->offset += pouse;
 }
 
 static void
 roff_term_pre_sp(ROFF_TERM_ARGS)
 {
 	struct roffsu	 su;
 	int		 len;
 
 	if (n->child != NULL) {
 		if (a2roffsu(n->child->string, &su, SCALE_VS) == NULL)
 			su.scale = 1.0;
 		len = term_vspan(p, &su);
 	} else
 		len = 1;
 
 	if (len < 0)
 		p->skipvsp -= len;
 	else
 		while (len--)
 			term_vspace(p);
 
 	roff_term_pre_br(p, n);
 }
 
 static void
 roff_term_pre_ta(ROFF_TERM_ARGS)
 {
 	term_tab_set(p, NULL);
 	for (n = n->child; n != NULL; n = n->next)
 		term_tab_set(p, n->string);
 }
 
 static void
 roff_term_pre_ti(ROFF_TERM_ARGS)
 {
 	struct roffsu	 su;
-	const char	*cp;
-	const size_t	 maxoff = 72;
-	int		 len, sign;
+	const char	*cp;      /* Request argument. */
+	size_t		 maxoff;  /* Maximum indentation in basic units. */
+	int		 len;	  /* Request argument in basic units. */
+	int		 sign;
 
 	roff_term_pre_br(p, n);
 
 	if (n->child == NULL)
 		return;
 	cp = n->child->string;
 	if (*cp == '+') {
 		sign = 1;
 		cp++;
 	} else if (*cp == '-') {
 		sign = -1;
 		cp++;
 	} else
 		sign = 0;
 
 	if (a2roffsu(cp, &su, SCALE_EM) == NULL)
 		return;
-	len = term_hen(p, &su);
+	len = term_hspan(p, &su);
+	maxoff = term_len(p, 72);
 
 	switch (sign) {
 	case 1:
 		if (p->tcol->offset + len <= maxoff)
 			p->ti = len;
 		else if (p->tcol->offset < maxoff)
 			p->ti = maxoff - p->tcol->offset;
 		else
 			p->ti = 0;
 		break;
 	case -1:
 		if ((size_t)len < p->tcol->offset)
 			p->ti = -len;
 		else
 			p->ti = -p->tcol->offset;
 		break;
 	default:
 		if ((size_t)len > maxoff)
 			len = maxoff;
 		p->ti = len - p->tcol->offset;
 		break;
 	}
 	p->tcol->offset += p->ti;
 }
diff --git a/contrib/mandoc/tbl.h b/contrib/mandoc/tbl.h
index 5e98735d6f97..c7566c110f42 100644
--- a/contrib/mandoc/tbl.h
+++ b/contrib/mandoc/tbl.h
@@ -1,120 +1,120 @@
-/*	$Id: tbl.h,v 1.3 2025/01/05 18:14:39 schwarze Exp $ */
+/* $Id: tbl.h,v 1.4 2025/07/16 14:33:08 schwarze Exp $ */
 /*
+ * Copyright (c) 2014-2018, 2021, 2025 Ingo Schwarze 
  * Copyright (c) 2010, 2011 Kristaps Dzonsons 
- * Copyright (c) 2014,2015,2017,2018,2021 Ingo Schwarze 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
 struct	tbl_opts {
 	int		  opts;
 #define	TBL_OPT_ALLBOX	 (1 << 0)  /* Option "allbox". */
 #define	TBL_OPT_BOX	 (1 << 1)  /* Option "box". */
 #define	TBL_OPT_CENTRE	 (1 << 2)  /* Option "center". */
 #define	TBL_OPT_DBOX	 (1 << 3)  /* Option "doublebox". */
 #define	TBL_OPT_EXPAND	 (1 << 4)  /* Option "expand". */
 #define	TBL_OPT_NOKEEP	 (1 << 5)  /* Option "nokeep". */
 #define	TBL_OPT_NOSPACE	 (1 << 6)  /* Option "nospaces". */
 #define	TBL_OPT_NOWARN	 (1 << 7)  /* Option "nowarn". */
 	int		  cols;    /* Number of columns. */
-	int		  lvert;   /* Width of left vertical line. */
-	int		  rvert;   /* Width of right vertical line. */
+	int		  lvert;   /* Width of left vertical line in EN. */
+	int		  rvert;   /* Width of right vertical line in EN. */
 	char		  tab;     /* Option "tab": cell separator. */
 	char		  decimal; /* Option "decimalpoint". */
 };
 
 enum	tbl_cellt {
 	TBL_CELL_CENTRE,  /* c, C */
 	TBL_CELL_RIGHT,   /* r, R */
 	TBL_CELL_LEFT,    /* l, L */
 	TBL_CELL_NUMBER,  /* n, N */
 	TBL_CELL_SPAN,    /* s, S */
 	TBL_CELL_LONG,    /* a, A */
 	TBL_CELL_DOWN,    /* ^    */
 	TBL_CELL_HORIZ,   /* _, - */
 	TBL_CELL_DHORIZ,  /* =    */
 	TBL_CELL_MAX
 };
 
 /*
  * A cell in a layout row.
  */
 struct	tbl_cell {
 	struct tbl_cell	 *next;     /* Layout cell to the right. */
-	size_t		  width;    /* Minimum column width. */
-	size_t		  spacing;  /* To the right of the column. */
-	int		  vert;     /* Width of subsequent vertical line. */
+	size_t		  width;    /* Minimum column width in basic units. */
+	size_t		  spacing;  /* To the right of the column in EN. */
+	int		  vert;     /* Width of subseq. vertical line in EN. */
 	int		  col;      /* Column number, starting from 0. */
 	int		  flags;
 #define	TBL_CELL_TALIGN	 (1 << 2)   /* t, T */
 #define	TBL_CELL_UP	 (1 << 3)   /* u, U */
 #define	TBL_CELL_BALIGN	 (1 << 4)   /* d, D */
 #define	TBL_CELL_WIGN	 (1 << 5)   /* z, Z */
 #define	TBL_CELL_EQUAL	 (1 << 6)   /* e, E */
 #define	TBL_CELL_WMAX	 (1 << 7)   /* x, X */
 	enum mandoc_esc	  font;
 	enum tbl_cellt	  pos;
 };
 
 /*
  * A layout row.
  */
 struct	tbl_row {
 	struct tbl_row	 *next;   /* Layout row below. */
 	struct tbl_cell	 *first;  /* Leftmost layout cell. */
 	struct tbl_cell	 *last;   /* Rightmost layout cell. */
-	int		  vert;   /* Width of left vertical line. */
+	int		  vert;   /* Width of left vertical line in EN. */
 };
 
 enum	tbl_datt {
 	TBL_DATA_NONE,    /* Uninitialized row. */
 	TBL_DATA_DATA,    /* Contains data rather than a line. */
 	TBL_DATA_HORIZ,   /* _: connecting horizontal line. */
 	TBL_DATA_DHORIZ,  /* =: connecting double horizontal line. */
 	TBL_DATA_NHORIZ,  /* \_: isolated horizontal line. */
 	TBL_DATA_NDHORIZ  /* \=: isolated double horizontal line. */
 };
 
 /*
  * A cell within a row of data.  The "string" field contains the
  * actual string value that's in the cell.  The rest is layout.
  */
 struct	tbl_dat {
 	struct tbl_dat	 *next;    /* Data cell to the right. */
 	struct tbl_cell	 *layout;  /* Associated layout cell. */
 	char		 *string;  /* Data, or NULL if not TBL_DATA_DATA. */
 	int		  hspans;  /* How many horizontal spans follow. */
 	int		  vspans;  /* How many vertical spans follow. */
 	int		  block;   /* T{ text block T} */
 	enum tbl_datt	  pos;
 };
 
 enum	tbl_spant {
 	TBL_SPAN_DATA,   /* Contains data rather than a line. */
 	TBL_SPAN_HORIZ,  /* _: horizontal line. */
 	TBL_SPAN_DHORIZ  /* =: double horizontal line. */
 };
 
 /*
  * A row of data in a table.
  */
 struct	tbl_span {
 	struct tbl_opts	 *opts;    /* Options for the table as a whole. */
 	struct tbl_span	 *prev;    /* Data row above. */
 	struct tbl_span	 *next;    /* Data row below. */
 	struct tbl_row	 *layout;  /* Associated layout row. */
 	struct tbl_dat	 *first;   /* Leftmost data cell. */
 	struct tbl_dat	 *last;    /* Rightmost data cell. */
 	int		  line;    /* Input file line number. */
 	enum tbl_spant	  pos;
 };
diff --git a/contrib/mandoc/tbl_html.c b/contrib/mandoc/tbl_html.c
index 57d90c4c2d67..56ea3c08eef4 100644
--- a/contrib/mandoc/tbl_html.c
+++ b/contrib/mandoc/tbl_html.c
@@ -1,300 +1,268 @@
-/* $Id: tbl_html.c,v 1.41 2022/04/23 14:02:17 schwarze Exp $ */
+/* $Id: tbl_html.c,v 1.42 2025/07/16 14:33:08 schwarze Exp $ */
 /*
  * Copyright (c) 2014, 2015, 2017, 2018, 2021, 2022
  *               Ingo Schwarze 
  * Copyright (c) 2011 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 
 #if DEBUG_MEMORY
 #include "mandoc_dbg.h"
 #endif
 #include "mandoc.h"
 #include "roff.h"
 #include "tbl.h"
 #include "out.h"
 #include "html.h"
 
 static	void	 html_tblopen(struct html *, const struct tbl_span *);
 static	size_t	 html_tbl_len(size_t, void *);
 static	size_t	 html_tbl_strlen(const char *, void *);
-static	size_t	 html_tbl_sulen(const struct roffsu *, void *);
 
 
 static size_t
 html_tbl_len(size_t sz, void *arg)
 {
 	return sz;
 }
 
 static size_t
 html_tbl_strlen(const char *p, void *arg)
 {
 	return strlen(p);
 }
 
-static size_t
-html_tbl_sulen(const struct roffsu *su, void *arg)
-{
-	if (su->scale < 0.0)
-		return 0;
-
-	switch (su->unit) {
-	case SCALE_FS:  /* 2^16 basic units */
-		return su->scale * 65536.0 / 24.0;
-	case SCALE_IN:  /* 10 characters per inch */
-		return su->scale * 10.0;
-	case SCALE_CM:  /* 2.54 cm per inch */
-		return su->scale * 10.0 / 2.54;
-	case SCALE_PC:  /* 6 pica per inch */
-	case SCALE_VS:
-		return su->scale * 10.0 / 6.0;
-	case SCALE_EN:
-	case SCALE_EM:
-		return su->scale;
-	case SCALE_PT:  /* 12 points per pica */
-		return su->scale * 10.0 / 6.0 / 12.0;
-	case SCALE_BU:  /* 24 basic units per character */
-		return su->scale / 24.0;
-	case SCALE_MM:  /* 1/1000 inch */
-		return su->scale / 100.0;
-	default:
-		abort();
-	}
-}
-
 static void
 html_tblopen(struct html *h, const struct tbl_span *sp)
 {
 	html_close_paragraph(h);
 	if (h->tbl.cols == NULL) {
 		h->tbl.len = html_tbl_len;
 		h->tbl.slen = html_tbl_strlen;
-		h->tbl.sulen = html_tbl_sulen;
 		tblcalc(&h->tbl, sp, 0, 0);
 	}
 	assert(NULL == h->tblt);
 	h->tblt = print_otag(h, TAG_TABLE, "c?ss", "tbl",
 	    "border",
 		sp->opts->opts & TBL_OPT_ALLBOX ? "1" : NULL,
 	    "border-style",
 		sp->opts->opts & TBL_OPT_DBOX ? "double" :
 		sp->opts->opts & TBL_OPT_BOX ? "solid" : NULL,
 	    "border-top-style",
 		sp->pos == TBL_SPAN_DHORIZ ? "double" :
 		sp->pos == TBL_SPAN_HORIZ ? "solid" : NULL);
 }
 
 void
 print_tblclose(struct html *h)
 {
 
 	assert(h->tblt);
 	print_tagq(h, h->tblt);
 	h->tblt = NULL;
 }
 
 void
 print_tbl(struct html *h, const struct tbl_span *sp)
 {
 	const struct tbl_dat	*dp;
 	const struct tbl_cell	*cp;
 	const struct tbl_span	*psp;
 	const struct roffcol	*col;
 	struct tag		*tt;
 	const char		*hspans, *vspans, *halign, *valign;
 	const char		*bborder, *lborder, *rborder;
 	const char		*ccp;
 	char			 hbuf[4], vbuf[4];
 	size_t			 sz;
 	enum mandoc_esc		 save_font;
 	int			 i;
 
 	if (h->tblt == NULL)
 		html_tblopen(h, sp);
 
 	/*
 	 * Horizontal lines spanning the whole table
 	 * are handled by previous or following table rows.
 	 */
 
 	if (sp->pos != TBL_SPAN_DATA)
 		goto out;
 
 	/* Inhibit printing of spaces: we do padding ourselves. */
 
 	h->flags |= HTML_NONOSPACE;
 	h->flags |= HTML_NOSPACE;
 
 	/* Draw a vertical line left of this row? */
 
 	switch (sp->layout->vert) {
 	case 2:
 		lborder = "double";
 		break;
 	case 1:
 		lborder = "solid";
 		break;
 	default:
 		lborder = NULL;
 		break;
 	}
 
 	/* Draw a horizontal line below this row? */
 
 	bborder = NULL;
 	if ((psp = sp->next) != NULL) {
 		switch (psp->pos) {
 		case TBL_SPAN_DHORIZ:
 			bborder = "double";
 			break;
 		case TBL_SPAN_HORIZ:
 			bborder = "solid";
 			break;
 		default:
 			break;
 		}
 	}
 
 	tt = print_otag(h, TAG_TR, "ss",
 	    "border-left-style", lborder,
 	    "border-bottom-style", bborder);
 
 	for (dp = sp->first; dp != NULL; dp = dp->next) {
 		print_stagq(h, tt);
 
 		/*
 		 * Do not generate  elements for continuations
 		 * of spanned cells.  Larger  elements covering
 		 * this space were already generated earlier.
 		 */
 
 		cp = dp->layout;
 		if (cp->pos == TBL_CELL_SPAN || cp->pos == TBL_CELL_DOWN ||
 		    (dp->string != NULL && strcmp(dp->string, "\\^") == 0))
 			continue;
 
 		/* Determine the attribute values. */
 
 		if (dp->hspans > 0) {
 			(void)snprintf(hbuf, sizeof(hbuf),
 			    "%d", dp->hspans + 1);
 			hspans = hbuf;
 		} else
 			hspans = NULL;
 		if (dp->vspans > 0) {
 			(void)snprintf(vbuf, sizeof(vbuf),
 			    "%d", dp->vspans + 1);
 			vspans = vbuf;
 		} else
 			vspans = NULL;
 
 		switch (cp->pos) {
 		case TBL_CELL_CENTRE:
 			halign = "center";
 			break;
 		case TBL_CELL_RIGHT:
 		case TBL_CELL_NUMBER:
 			halign = "right";
 			break;
 		default:
 			halign = NULL;
 			break;
 		}
 		if (cp->flags & TBL_CELL_TALIGN)
 			valign = "top";
 		else if (cp->flags & TBL_CELL_BALIGN)
 			valign = "bottom";
 		else
 			valign = NULL;
 
 		for (i = dp->hspans; i > 0; i--)
 			cp = cp->next;
 		switch (cp->vert) {
 		case 2:
 			rborder = "double";
 			break;
 		case 1:
 			rborder = "solid";
 			break;
 		default:
 			rborder = NULL;
 			break;
 		}
 
 		/* Print the element and the attributes. */
 
 		print_otag(h, TAG_TD, "??sss",
 		    "colspan", hspans, "rowspan", vspans,
 		    "vertical-align", valign,
 		    "text-align", halign,
 		    "border-right-style", rborder);
 		if (dp->layout->pos == TBL_CELL_HORIZ ||
 		    dp->layout->pos == TBL_CELL_DHORIZ ||
 		    dp->pos == TBL_DATA_HORIZ ||
 		    dp->pos == TBL_DATA_NHORIZ ||
 		    dp->pos == TBL_DATA_DHORIZ ||
 		    dp->pos == TBL_DATA_NDHORIZ)
 			print_otag(h, TAG_HR, "");
 		else if (dp->string != NULL) {
 			save_font = h->metac;
 			html_setfont(h, dp->layout->font);
 			if (dp->layout->pos == TBL_CELL_LONG)
 				print_text(h, "\\[u2003]");  /* em space */
 			print_text(h, dp->string);
 			if (dp->layout->pos == TBL_CELL_NUMBER) {
 				col = h->tbl.cols + dp->layout->col;
 				if (col->decimal < col->nwidth) {
 					if ((ccp = strrchr(dp->string,
 					    sp->opts->decimal)) == NULL) {
 						/* Punctuation space. */
 						print_text(h, "\\[u2008]");
 						ccp = strchr(dp->string, '\0');
 					} else
 						ccp++;
 					sz = col->nwidth - col->decimal;
 					while (--sz > 0) {
 						if (*ccp == '\0')
 							/* Figure space. */
 							print_text(h,
 							    "\\[u2007]");
 						else
 							ccp++;
 					}
 				}
 			}
 			html_setfont(h, save_font);
 		}
 	}
 
 	print_tagq(h, tt);
 
 	h->flags &= ~HTML_NONOSPACE;
 
 out:
 	if (sp->next == NULL) {
 		assert(h->tbl.cols);
 		free(h->tbl.cols);
 		h->tbl.cols = NULL;
 		print_tblclose(h);
 	}
 }
diff --git a/contrib/mandoc/tbl_layout.c b/contrib/mandoc/tbl_layout.c
index 3b7e64580fd5..aa054dee9411 100644
--- a/contrib/mandoc/tbl_layout.c
+++ b/contrib/mandoc/tbl_layout.c
@@ -1,382 +1,382 @@
-/*	$Id: tbl_layout.c,v 1.51 2025/01/05 18:14:39 schwarze Exp $ */
+/* $Id: tbl_layout.c,v 1.52 2025/07/16 14:33:08 schwarze Exp $ */
 /*
  * Copyright (c) 2012, 2014, 2015, 2017, 2020, 2021, 2025
  *               Ingo Schwarze 
  * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc_aux.h"
 #include "mandoc.h"
 #include "tbl.h"
 #include "libmandoc.h"
 #include "tbl_int.h"
 
 struct	tbl_phrase {
 	char		 name;
 	enum tbl_cellt	 key;
 };
 
 static	const struct tbl_phrase keys[] = {
 	{ 'c',		 TBL_CELL_CENTRE },
 	{ 'r',		 TBL_CELL_RIGHT },
 	{ 'l',		 TBL_CELL_LEFT },
 	{ 'n',		 TBL_CELL_NUMBER },
 	{ 's',		 TBL_CELL_SPAN },
 	{ 'a',		 TBL_CELL_LONG },
 	{ '^',		 TBL_CELL_DOWN },
 	{ '-',		 TBL_CELL_HORIZ },
 	{ '_',		 TBL_CELL_HORIZ },
 	{ '=',		 TBL_CELL_DHORIZ }
 };
 
 #define KEYS_MAX ((int)(sizeof(keys)/sizeof(keys[0])))
 
 static	void		 mods(struct tbl_node *, struct tbl_cell *,
 				int, const char *, int *);
 static	void		 cell(struct tbl_node *, struct tbl_row *,
 				int, const char *, int *);
 static	struct tbl_cell *cell_alloc(struct tbl_node *, struct tbl_row *,
 				enum tbl_cellt);
 
 
 static void
 mods(struct tbl_node *tbl, struct tbl_cell *cp,
 		int ln, const char *p, int *pos)
 {
 	char		*endptr;
-	unsigned long	 spacing;
-	int		 isz;
+	unsigned long	 spacing;  /* Column spacing in EN units. */
+	int		 isz;      /* Width in basic units. */
 	enum mandoc_esc	 fontesc;
 
 mod:
 	while (p[*pos] == ' ' || p[*pos] == '\t')
 		(*pos)++;
 
 	/* Row delimiters and cell specifiers end modifier lists. */
 
 	if (strchr(".,-=^_ACLNRSaclnrs", p[*pos]) != NULL)
 		return;
 
 	/* Throw away parenthesised expression. */
 
 	if ('(' == p[*pos]) {
 		(*pos)++;
 		while (p[*pos] && ')' != p[*pos])
 			(*pos)++;
 		if (')' == p[*pos]) {
 			(*pos)++;
 			goto mod;
 		}
 		mandoc_msg(MANDOCERR_TBLLAYOUT_PAR, ln, *pos, NULL);
 		return;
 	}
 
 	/* Parse numerical spacing from modifier string. */
 
 	if (isdigit((unsigned char)p[*pos])) {
 		if ((spacing = strtoul(p + *pos, &endptr, 10)) > 9)
 			mandoc_msg(MANDOCERR_TBLLAYOUT_SPC, ln, *pos,
 			    "%lu", spacing);
 		else
 			cp->spacing = spacing;
 		*pos = endptr - p;
 		goto mod;
 	}
 
 	switch (tolower((unsigned char)p[(*pos)++])) {
 	case 'b':
 		cp->font = ESCAPE_FONTBOLD;
 		goto mod;
 	case 'd':
 		cp->flags |= TBL_CELL_BALIGN;
 		goto mod;
 	case 'e':
 		cp->flags |= TBL_CELL_EQUAL;
 		goto mod;
 	case 'f':
 		break;
 	case 'i':
 		cp->font = ESCAPE_FONTITALIC;
 		goto mod;
 	case 'm':
 		mandoc_msg(MANDOCERR_TBLLAYOUT_MOD, ln, *pos, "m");
 		goto mod;
 	case 'p':
 	case 'v':
 		if (p[*pos] == '-' || p[*pos] == '+')
 			(*pos)++;
 		while (isdigit((unsigned char)p[*pos]))
 			(*pos)++;
 		goto mod;
 	case 't':
 		cp->flags |= TBL_CELL_TALIGN;
 		goto mod;
 	case 'u':
 		cp->flags |= TBL_CELL_UP;
 		goto mod;
 	case 'w':
 		if (p[*pos] == '(') {
 			(*pos)++;
 			isz = 0;
 			if (roff_evalnum(ln, p, pos, &isz, 'n', 1) == 0 ||
 			    p[*pos] != ')')
 				mandoc_msg(MANDOCERR_TBLLAYOUT_WIDTH,
 				    ln, *pos, "%s", p + *pos);
 			else {
-				/* Convert from BU to EN and round. */
-				cp->width = (isz + 11) /24;
+				cp->width = isz;
 				(*pos)++;
 			}
 		} else {
 			cp->width = 0;
 			while (isdigit((unsigned char)p[*pos])) {
 				cp->width *= 10;
 				cp->width += p[(*pos)++] - '0';
 			}
+			cp->width *= 24;
 			if (cp->width == 0)
 				mandoc_msg(MANDOCERR_TBLLAYOUT_WIDTH,
 				    ln, *pos, "%s", p + *pos);
 		}
 		goto mod;
 	case 'x':
 		cp->flags |= TBL_CELL_WMAX;
 		goto mod;
 	case 'z':
 		cp->flags |= TBL_CELL_WIGN;
 		goto mod;
 	case '|':
 		if (cp->vert < 2)
 			cp->vert++;
 		else
 			mandoc_msg(MANDOCERR_TBLLAYOUT_VERT,
 			    ln, *pos - 1, NULL);
 		goto mod;
 	default:
 		mandoc_msg(MANDOCERR_TBLLAYOUT_CHAR,
 		    ln, *pos - 1, "%c", p[*pos - 1]);
 		goto mod;
 	}
 
 	while (p[*pos] == ' ' || p[*pos] == '\t')
 		(*pos)++;
 
 	/* Ignore parenthised font names for now. */
 
 	if (p[*pos] == '(')
 		goto mod;
 
 	isz = 0;
 	if (p[*pos] != '\0')
 		isz++;
 	if (strchr(" \t.", p[*pos + isz]) == NULL)
 		isz++;
 	
 	fontesc = mandoc_font(p + *pos, isz);
 
 	switch (fontesc) {
 	case ESCAPE_FONTPREV:
 	case ESCAPE_ERROR:
 		mandoc_msg(MANDOCERR_FT_BAD,
 		    ln, *pos, "TS %s", p + *pos - 1);
 		break;
 	default:
 		cp->font = fontesc;
 		break;
 	}
 	*pos += isz;
 	goto mod;
 }
 
 static void
 cell(struct tbl_node *tbl, struct tbl_row *rp,
 		int ln, const char *p, int *pos)
 {
 	int		 i;
 	enum tbl_cellt	 c;
 
 	/* Handle leading vertical lines */
 
 	while (p[*pos] == ' ' || p[*pos] == '\t' || p[*pos] == '|') {
 		if (p[*pos] == '|') {
 			if (rp->vert < 2)
 				rp->vert++;
 			else
 				mandoc_msg(MANDOCERR_TBLLAYOUT_VERT,
 				    ln, *pos, NULL);
 		}
 		(*pos)++;
 	}
 
 again:
 	while (p[*pos] == ' ' || p[*pos] == '\t')
 		(*pos)++;
 
 	if (p[*pos] == '.' || p[*pos] == '\0')
 		return;
 
 	/* Parse the column position (`c', `l', `r', ...). */
 
 	for (i = 0; i < KEYS_MAX; i++)
 		if (tolower((unsigned char)p[*pos]) == keys[i].name)
 			break;
 
 	if (i == KEYS_MAX) {
 		mandoc_msg(MANDOCERR_TBLLAYOUT_CHAR,
 		    ln, *pos, "%c", p[*pos]);
 		(*pos)++;
 		goto again;
 	}
 	c = keys[i].key;
 
 	/* Special cases of spanners. */
 
 	if (c == TBL_CELL_SPAN) {
 		if (rp->last == NULL)
 			mandoc_msg(MANDOCERR_TBLLAYOUT_SPAN, ln, *pos, NULL);
 		else if (rp->last->pos == TBL_CELL_HORIZ ||
 		    rp->last->pos == TBL_CELL_DHORIZ)
 			c = rp->last->pos;
 	} else if (c == TBL_CELL_DOWN && rp == tbl->first_row)
 		mandoc_msg(MANDOCERR_TBLLAYOUT_DOWN, ln, *pos, NULL);
 
 	(*pos)++;
 
 	/* Allocate cell then parse its modifiers. */
 
 	mods(tbl, cell_alloc(tbl, rp, c), ln, p, pos);
 }
 
 void
 tbl_layout(struct tbl_node *tbl, int ln, const char *p, int pos)
 {
 	struct tbl_row	*rp;
 
 	rp = NULL;
 	for (;;) {
 		/* Skip whitespace before and after each cell. */
 
 		while (p[pos] == ' ' || p[pos] == '\t')
 			pos++;
 
 		switch (p[pos]) {
 		case ',':  /* Next row on this input line. */
 			pos++;
 			rp = NULL;
 			continue;
 		case '\0':  /* Next row on next input line. */
 			return;
 		case '.':  /* End of layout. */
 			pos++;
 			tbl->part = TBL_PART_DATA;
 
 			/*
 			 * When the layout is completely empty,
 			 * default to one left-justified column.
 			 */
 
 			if (tbl->first_row == NULL) {
 				tbl->first_row = tbl->last_row =
 				    mandoc_calloc(1, sizeof(*rp));
 			}
 			if (tbl->first_row->first == NULL) {
 				mandoc_msg(MANDOCERR_TBLLAYOUT_NONE,
 				    ln, pos, NULL);
 				cell_alloc(tbl, tbl->first_row,
 				    TBL_CELL_LEFT);
 				if (tbl->opts.lvert < tbl->first_row->vert)
 					tbl->opts.lvert = tbl->first_row->vert;
 				return;
 			}
 
 			/*
 			 * Search for the widest line
 			 * along the left and right margins.
 			 */
 
 			for (rp = tbl->first_row; rp; rp = rp->next) {
 				if (tbl->opts.lvert < rp->vert)
 					tbl->opts.lvert = rp->vert;
 				if (rp->last != NULL &&
 				    rp->last->col + 1 == tbl->opts.cols &&
 				    tbl->opts.rvert < rp->last->vert)
 					tbl->opts.rvert = rp->last->vert;
 
 				/* If the last line is empty, drop it. */
 
 				if (rp->next != NULL &&
 				    rp->next->first == NULL) {
 					free(rp->next);
 					rp->next = NULL;
 					tbl->last_row = rp;
 				}
 			}
 			return;
 		default:  /* Cell. */
 			break;
 		}
 
 		/*
 		 * If the last line had at least one cell,
 		 * start a new one; otherwise, continue it.
 		 */
 
 		if (rp == NULL) {
 			if (tbl->last_row == NULL ||
 			    tbl->last_row->first != NULL) {
 				rp = mandoc_calloc(1, sizeof(*rp));
 				if (tbl->last_row)
 					tbl->last_row->next = rp;
 				else
 					tbl->first_row = rp;
 				tbl->last_row = rp;
 			} else
 				rp = tbl->last_row;
 		}
 		cell(tbl, rp, ln, p, &pos);
 	}
 }
 
 static struct tbl_cell *
 cell_alloc(struct tbl_node *tbl, struct tbl_row *rp, enum tbl_cellt pos)
 {
 	struct tbl_cell	*p, *pp;
 
 	p = mandoc_calloc(1, sizeof(*p));
 	p->spacing = SIZE_MAX;
 	p->font = ESCAPE_FONTROMAN;
 	p->pos = pos;
 
 	if ((pp = rp->last) != NULL) {
 		pp->next = p;
 		p->col = pp->col + 1;
 	} else
 		rp->first = p;
 	rp->last = p;
 
 	if (tbl->opts.cols <= p->col)
 		tbl->opts.cols = p->col + 1;
 
 	return p;
 }
diff --git a/contrib/mandoc/tbl_term.c b/contrib/mandoc/tbl_term.c
index e92349514d9f..a7f057084266 100644
--- a/contrib/mandoc/tbl_term.c
+++ b/contrib/mandoc/tbl_term.c
@@ -1,951 +1,994 @@
-/* $Id: tbl_term.c,v 1.79 2022/08/28 10:58:31 schwarze Exp $ */
+/* $Id: tbl_term.c,v 1.81 2025/07/24 17:54:48 schwarze Exp $ */
 /*
- * Copyright (c) 2011-2022 Ingo Schwarze 
+ * Copyright (c) 2011-2022, 2025 Ingo Schwarze 
  * Copyright (c) 2009, 2011 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #if DEBUG_MEMORY
 #include "mandoc_dbg.h"
 #endif
 #include "mandoc.h"
 #include "tbl.h"
 #include "out.h"
 #include "term.h"
 
 #define	IS_HORIZ(cp)	((cp)->pos == TBL_CELL_HORIZ || \
 			 (cp)->pos == TBL_CELL_DHORIZ)
 
 
 static	size_t	term_tbl_len(size_t, void *);
 static	size_t	term_tbl_strlen(const char *, void *);
-static	size_t	term_tbl_sulen(const struct roffsu *, void *);
 static	void	tbl_data(struct termp *, const struct tbl_opts *,
 			const struct tbl_cell *,
 			const struct tbl_dat *,
-			const struct roffcol *);
+			const struct roffcol *, size_t *);
 static	void	tbl_direct_border(struct termp *, int, size_t);
-static	void	tbl_fill_border(struct termp *, int, size_t);
-static	void	tbl_fill_char(struct termp *, char, size_t);
-static	void	tbl_fill_string(struct termp *, const char *, size_t);
+static	void	tbl_fill_border(struct termp *, int, size_t *, size_t);
+static	void	tbl_fill_char(struct termp *, char, size_t *, size_t);
+static	void	tbl_fill_string(struct termp *, const char *,
+			size_t *, size_t);
 static	void	tbl_hrule(struct termp *, const struct tbl_span *,
 			const struct tbl_span *, const struct tbl_span *,
 			int);
 static	void	tbl_literal(struct termp *, const struct tbl_dat *,
-			const struct roffcol *);
+			const struct roffcol *, size_t *);
 static	void	tbl_number(struct termp *, const struct tbl_opts *,
 			const struct tbl_dat *,
-			const struct roffcol *);
+			const struct roffcol *, size_t *);
 static	void	tbl_word(struct termp *, const struct tbl_dat *);
 
 
 /*
  * The following border-character tables are indexed
  * by ternary (3-based) numbers, as opposed to binary or decimal.
  * Each ternary digit describes the line width in one direction:
  * 0 means no line, 1 single or light line, 2 double or heavy line.
  */
 
 /* Positional values of the four directions. */
 #define	BRIGHT	1
 #define	BDOWN	3
 #define	BLEFT	(3 * 3)
 #define	BUP	(3 * 3 * 3)
 #define	BHORIZ	(BLEFT + BRIGHT)
 
 /* Code points to use for each combination of widths. */
 static  const int borders_utf8[81] = {
 	0x0020, 0x2576, 0x257a,  /* 000 right */
 	0x2577, 0x250c, 0x250d,  /* 001 down */
 	0x257b, 0x250e, 0x250f,  /* 002 */
 	0x2574, 0x2500, 0x257c,  /* 010 left */
 	0x2510, 0x252c, 0x252e,  /* 011 left down */
 	0x2512, 0x2530, 0x2532,  /* 012 */
 	0x2578, 0x257e, 0x2501,  /* 020 left */
 	0x2511, 0x252d, 0x252f,  /* 021 left down */
 	0x2513, 0x2531, 0x2533,  /* 022 */
 	0x2575, 0x2514, 0x2515,  /* 100 up */
 	0x2502, 0x251c, 0x251d,  /* 101 up down */
 	0x257d, 0x251f, 0x2522,  /* 102 */
 	0x2518, 0x2534, 0x2536,  /* 110 up left */
 	0x2524, 0x253c, 0x253e,  /* 111 all */
 	0x2527, 0x2541, 0x2546,  /* 112 */
 	0x2519, 0x2535, 0x2537,  /* 120 up left */
 	0x2525, 0x253d, 0x253f,  /* 121 all */
 	0x252a, 0x2545, 0x2548,  /* 122 */
 	0x2579, 0x2516, 0x2517,  /* 200 up */
 	0x257f, 0x251e, 0x2521,  /* 201 up down */
 	0x2503, 0x2520, 0x2523,  /* 202 */
 	0x251a, 0x2538, 0x253a,  /* 210 up left */
 	0x2526, 0x2540, 0x2544,  /* 211 all */
 	0x2528, 0x2542, 0x254a,  /* 212 */
 	0x251b, 0x2539, 0x253b,  /* 220 up left */
 	0x2529, 0x2543, 0x2547,  /* 221 all */
 	0x252b, 0x2549, 0x254b,  /* 222 */
 };
 
 /* ASCII approximations for these code points, compatible with groff. */
 static  const int borders_ascii[81] = {
 	' ', '-', '=',  /* 000 right */
 	'|', '+', '+',  /* 001 down */
 	'|', '+', '+',  /* 002 */
 	'-', '-', '=',  /* 010 left */
 	'+', '+', '+',  /* 011 left down */
 	'+', '+', '+',  /* 012 */
 	'=', '=', '=',  /* 020 left */
 	'+', '+', '+',  /* 021 left down */
 	'+', '+', '+',  /* 022 */
 	'|', '+', '+',  /* 100 up */
 	'|', '+', '+',  /* 101 up down */
 	'|', '+', '+',  /* 102 */
 	'+', '+', '+',  /* 110 up left */
 	'+', '+', '+',  /* 111 all */
 	'+', '+', '+',  /* 112 */
 	'+', '+', '+',  /* 120 up left */
 	'+', '+', '+',  /* 121 all */
 	'+', '+', '+',  /* 122 */
 	'|', '+', '+',  /* 200 up */
 	'|', '+', '+',  /* 201 up down */
 	'|', '+', '+',  /* 202 */
 	'+', '+', '+',  /* 210 up left */
 	'+', '+', '+',  /* 211 all */
 	'+', '+', '+',  /* 212 */
 	'+', '+', '+',  /* 220 up left */
 	'+', '+', '+',  /* 221 all */
 	'+', '+', '+',  /* 222 */
 };
 
 /* Either of the above according to the selected output encoding. */
 static	const int *borders_locale;
 
 
-static size_t
-term_tbl_sulen(const struct roffsu *su, void *arg)
-{
-	int	 i;
-
-	i = term_hen((const struct termp *)arg, su);
-	return i > 0 ? i : 0;
-}
-
 static size_t
 term_tbl_strlen(const char *p, void *arg)
 {
 	return term_strlen((const struct termp *)arg, p);
 }
 
 static size_t
 term_tbl_len(size_t sz, void *arg)
 {
 	return term_len((const struct termp *)arg, sz);
 }
 
 
 void
 term_tbl(struct termp *tp, const struct tbl_span *sp)
 {
 	const struct tbl_cell	*cp, *cpn, *cpp, *cps;
 	const struct tbl_dat	*dp;
-	static size_t		 offset;
-	size_t			 save_offset;
-	size_t			 coloff, tsz;
-	int			 hspans, ic, more;
-	int			 dvert, fc, horiz, lhori, rhori, uvert;
+
+	/* Positions and widths in basic units. */
+	static size_t	 offset;	/* Of the table as a whole. */
+	size_t		 save_offset;	/* Of the surrounding text. */
+	size_t		 coloff;	/* Of this cell. */
+	size_t		 tsz;		/* Total width of the table. */
+	size_t		 enw; 		/* Width of one EN unit. */
+
+	int	 ic;	 /* Column number. */
+	int	 hspans; /* Number of spans following this cell. */
+	int	 horiz;	 /* Boolean: this row only contains a line. */
+	int	 lhori;	 /* Number of horizontal lines pointing left. */
+	int	 rhori;	 /* Number of horizontal lines pointing right. */
+	int	 dvert;	 /* Number of vertical lines pointing down. */
+	int	 uvert;	 /* Number of vertical lines pointing up. */
+	int	 fc;	 /* Frame character index in borders_locale[]. */
+	int	 more;	 /* Boolean: there are more columns to print. */
 
 	/* Inhibit printing of spaces: we do padding ourselves. */
 
 	tp->flags |= TERMP_NOSPACE | TERMP_NONOSPACE;
 	save_offset = tp->tcol->offset;
+	enw = term_len(tp, 1);
 
 	/*
 	 * The first time we're invoked for a given table block,
 	 * calculate the table widths and decimal positions.
 	 */
 
 	if (tp->tbl.cols == NULL) {
 		borders_locale = tp->enc == TERMENC_UTF8 ?
 		    borders_utf8 : borders_ascii;
 
 		tp->tbl.len = term_tbl_len;
 		tp->tbl.slen = term_tbl_strlen;
-		tp->tbl.sulen = term_tbl_sulen;
 		tp->tbl.arg = tp;
 
 		tblcalc(&tp->tbl, sp, tp->tcol->offset, tp->tcol->rmargin);
 
 		/* Center the table as a whole. */
 
 		offset = tp->tcol->offset;
 		if (sp->opts->opts & TBL_OPT_CENTRE) {
-			tsz = sp->opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX)
-			    ? 2 : !!sp->opts->lvert + !!sp->opts->rvert;
+
+			/*
+			 * Vertical lines on the edges of the table make the
+			 * table wider; take that into account for centering.
+			 * The following assignment essentially says that a
+			 * line on the right side occupies two columns (which
+			 * matches reality) and a line on the left side three
+			 * columns (which does not match reality; in fact,
+			 * it only occupies two columns).  But this is how
+			 * groff does centering, so for compatibility, use
+			 * the same numbers as groff.
+			 */
+
+			tsz = term_len(tp,
+			    sp->opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX) ?
+			    5 : 3 * !!sp->opts->lvert + 2 * !!sp->opts->rvert);
+
+			/* Column widths and column spacing. */
+
 			for (ic = 0; ic + 1 < sp->opts->cols; ic++)
 				tsz += tp->tbl.cols[ic].width +
-				    tp->tbl.cols[ic].spacing;
+				    term_len(tp, tp->tbl.cols[ic].spacing);
 			if (sp->opts->cols)
 				tsz += tp->tbl.cols[sp->opts->cols - 1].width;
+
 			if (offset + tsz > tp->tcol->rmargin)
-				tsz -= 1;
+				tsz -= enw;
 			offset = offset + tp->tcol->rmargin > tsz ?
-			    (offset + tp->tcol->rmargin - tsz) / 2 : 0;
+			    ((offset + tp->tcol->rmargin - tsz) / enw / 2) *
+			    enw : 0;
 			tp->tcol->offset = offset;
 		}
 
 		/* Horizontal frame at the start of boxed tables. */
 
 		if (tp->enc == TERMENC_ASCII &&
 		    sp->opts->opts & TBL_OPT_DBOX)
 			tbl_hrule(tp, NULL, sp, sp, TBL_OPT_DBOX);
 		if (sp->opts->opts & (TBL_OPT_DBOX | TBL_OPT_BOX))
 			tbl_hrule(tp, NULL, sp, sp, TBL_OPT_BOX);
 	}
 
 	/* Set up the columns. */
 
 	tp->flags |= TERMP_MULTICOL;
 	tp->tcol->offset = offset;
 	horiz = 0;
 	switch (sp->pos) {
 	case TBL_SPAN_HORIZ:
 	case TBL_SPAN_DHORIZ:
 		horiz = 1;
 		term_setcol(tp, 1);
 		break;
 	case TBL_SPAN_DATA:
 		term_setcol(tp, sp->opts->cols + 2);
 		coloff = tp->tcol->offset;
 
 		/* Set up a column for a left vertical frame. */
 
 		if (sp->opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX) ||
 		    sp->opts->lvert)
-			coloff++;
+			coloff += enw * 2;
 		tp->tcol->rmargin = coloff;
 
 		/* Set up the data columns. */
 
 		dp = sp->first;
 		hspans = 0;
 		for (ic = 0; ic < sp->opts->cols; ic++) {
 			if (hspans == 0) {
 				tp->tcol++;
 				tp->tcol->offset = coloff;
 			}
 			coloff += tp->tbl.cols[ic].width;
 			tp->tcol->rmargin = coloff;
 			if (ic + 1 < sp->opts->cols)
-				coloff += tp->tbl.cols[ic].spacing;
+				coloff += term_len(tp,
+				    tp->tbl.cols[ic].spacing);
 			if (hspans) {
 				hspans--;
 				continue;
 			}
 			if (dp != NULL &&
 			    (ic || sp->layout->first->pos != TBL_CELL_SPAN)) {
 				hspans = dp->hspans;
 				dp = dp->next;
 			}
 		}
 
 		/* Set up a column for a right vertical frame. */
 
 		tp->tcol++;
-		tp->tcol->offset = coloff + 1;
+		tp->tcol->offset = coloff + enw;
 		tp->tcol->rmargin = tp->maxrmargin;
 
 		/* Spans may have reduced the number of columns. */
 
 		tp->lasttcol = tp->tcol - tp->tcols;
 
 		/* Fill the buffers for all data columns. */
 
 		tp->tcol = tp->tcols;
+		coloff = tp->tcols[1].offset;
 		cp = cpn = sp->layout->first;
 		dp = sp->first;
 		hspans = 0;
 		for (ic = 0; ic < sp->opts->cols; ic++) {
 			if (cpn != NULL) {
 				cp = cpn;
 				cpn = cpn->next;
 			}
 			if (hspans) {
 				hspans--;
 				continue;
 			}
 			tp->tcol++;
 			tp->col = 0;
 			tp->flags &= ~(TERMP_BACKAFTER | TERMP_BACKBEFORE);
-			tbl_data(tp, sp->opts, cp, dp, tp->tbl.cols + ic);
+			tbl_data(tp, sp->opts, cp, dp, tp->tbl.cols + ic,
+			    &coloff);
+			coloff += term_len(tp, tp->tbl.cols[ic].spacing);
 			if (dp != NULL &&
 			    (ic || sp->layout->first->pos != TBL_CELL_SPAN)) {
 				hspans = dp->hspans;
 				dp = dp->next;
 			}
 		}
 		break;
 	}
 
 	do {
 		/* Print the vertical frame at the start of each row. */
 
 		tp->tcol = tp->tcols;
 		uvert = dvert = sp->opts->opts & TBL_OPT_DBOX ? 2 :
 		    sp->opts->opts & TBL_OPT_BOX ? 1 : 0;
 		if (sp->pos == TBL_SPAN_DATA && uvert < sp->layout->vert)
 			uvert = dvert = sp->layout->vert;
 		if (sp->next != NULL && sp->next->pos == TBL_SPAN_DATA &&
 		    dvert < sp->next->layout->vert)
 			dvert = sp->next->layout->vert;
 		if (sp->prev != NULL && uvert < sp->prev->layout->vert &&
 		    (horiz || (IS_HORIZ(sp->layout->first) &&
 		      !IS_HORIZ(sp->prev->layout->first))))
 			uvert = sp->prev->layout->vert;
 		rhori = sp->pos == TBL_SPAN_DHORIZ ||
 		    (sp->first != NULL && sp->first->pos == TBL_DATA_DHORIZ) ||
 		    sp->layout->first->pos == TBL_CELL_DHORIZ ? 2 :
 		    sp->pos == TBL_SPAN_HORIZ ||
 		    (sp->first != NULL && sp->first->pos == TBL_DATA_HORIZ) ||
 		    sp->layout->first->pos == TBL_CELL_HORIZ ? 1 : 0;
 		fc = BUP * uvert + BDOWN * dvert + BRIGHT * rhori;
 		if (uvert > 0 || dvert > 0 || (horiz && sp->opts->lvert)) {
 			(*tp->advance)(tp, tp->tcols->offset);
-			tp->viscol = tp->tcol->offset;
-			tbl_direct_border(tp, fc, 1);
+			tbl_direct_border(tp, fc, enw);
+			tbl_direct_border(tp, BHORIZ * rhori, enw);
 		}
 
 		/* Print the data cells. */
 
 		more = 0;
 		if (horiz)
 			tbl_hrule(tp, sp->prev, sp, sp->next, 0);
 		else {
 			cp = sp->layout->first;
 			cpn = sp->next == NULL ? NULL :
 			    sp->next->layout->first;
 			cpp = sp->prev == NULL ? NULL :
 			    sp->prev->layout->first;
 			dp = sp->first;
 			hspans = 0;
 			for (ic = 0; ic < sp->opts->cols; ic++) {
 
 				/*
+				 * Handle horizontal alignment.
 				 * Figure out whether to print a
 				 * vertical line after this cell
 				 * and advance to next layout cell.
 				 */
 
 				uvert = dvert = fc = 0;
 				if (cp != NULL) {
 					cps = cp;
 					while (cps->next != NULL &&
 					    cps->next->pos == TBL_CELL_SPAN)
 						cps = cps->next;
 					if (sp->pos == TBL_SPAN_DATA)
 						uvert = dvert = cps->vert;
 					switch (cp->pos) {
+					case TBL_CELL_CENTRE:
+						tp->flags |= TERMP_CENTER;
+						break;
+					case TBL_CELL_RIGHT:
+						tp->flags |= TERMP_RIGHT;
+						break;
+					case TBL_CELL_LONG:
+						if (hspans == 0)
+							tp->tcol->offset += enw;
+						break;
 					case TBL_CELL_HORIZ:
 						fc = BHORIZ;
 						break;
 					case TBL_CELL_DHORIZ:
 						fc = BHORIZ * 2;
 						break;
 					default:
 						break;
 					}
 				}
 				if (cpp != NULL) {
 					if (uvert < cpp->vert &&
 					    cp != NULL &&
 					    ((IS_HORIZ(cp) &&
 					      !IS_HORIZ(cpp)) ||
 					     (cp->next != NULL &&
 					      cpp->next != NULL &&
 					      IS_HORIZ(cp->next) &&
 					      !IS_HORIZ(cpp->next))))
 						uvert = cpp->vert;
 					cpp = cpp->next;
 				}
 				if (sp->opts->opts & TBL_OPT_ALLBOX) {
 					if (uvert == 0)
 						uvert = 1;
 					if (dvert == 0)
 						dvert = 1;
 				}
 				if (cpn != NULL) {
 					if (dvert == 0 ||
 					    (dvert < cpn->vert &&
 					     tp->enc == TERMENC_UTF8))
 						dvert = cpn->vert;
 					cpn = cpn->next;
 				}
 
 				lhori = (cp != NULL &&
 				     cp->pos == TBL_CELL_DHORIZ) ||
 				    (dp != NULL &&
 				     dp->pos == TBL_DATA_DHORIZ) ? 2 :
 				    (cp != NULL &&
 				     cp->pos == TBL_CELL_HORIZ) ||
 				    (dp != NULL &&
 				     dp->pos == TBL_DATA_HORIZ) ? 1 : 0;
 
 				/*
 				 * Skip later cells in a span,
 				 * figure out whether to start a span,
 				 * and advance to next data cell.
 				 */
 
 				if (hspans) {
 					hspans--;
 					cp = cp->next;
 					continue;
 				}
 				if (dp != NULL && (ic ||
 				    sp->layout->first->pos != TBL_CELL_SPAN)) {
 					hspans = dp->hspans;
 					dp = dp->next;
 				}
 
 				/*
 				 * Print one line of text in the cell
 				 * and remember whether there is more.
 				 */
 
 				tp->tcol++;
 				if (tp->tcol->col < tp->tcol->lastcol)
 					term_flushln(tp);
+				tp->flags &= ~(TERMP_CENTER | TERMP_RIGHT);
+				if (cp != NULL && cp->pos == TBL_CELL_LONG)
+					tp->tcol->offset -= enw;
 				if (tp->tcol->col < tp->tcol->lastcol)
 					more = 1;
 
 				/*
 				 * Vertical frames between data cells,
 				 * but not after the last column.
 				 */
 
 				if (fc == 0 &&
 				    ((uvert == 0 && dvert == 0 &&
 				      cp != NULL && (cp->next == NULL ||
 				      !IS_HORIZ(cp->next))) ||
 				     tp->tcol + 1 ==
 				      tp->tcols + tp->lasttcol)) {
 					if (cp != NULL)
 						cp = cp->next;
 					continue;
 				}
 
-				if (tp->viscol < tp->tcol->rmargin) {
-					(*tp->advance)(tp, tp->tcol->rmargin
-					   - tp->viscol);
-					tp->viscol = tp->tcol->rmargin;
-				}
+				if (tp->viscol < tp->tcol->rmargin)
+					(*tp->advance)(tp,
+					   tp->tcol->rmargin - tp->viscol);
 				while (tp->viscol < tp->tcol->rmargin +
-				    tp->tbl.cols[ic].spacing / 2)
+				    term_len(tp, tp->tbl.cols[ic].spacing / 2))
 					tbl_direct_border(tp,
-					    BHORIZ * lhori, 1);
+					    BHORIZ * lhori, enw);
 
 				if (tp->tcol + 1 == tp->tcols + tp->lasttcol)
 					continue;
 
 				if (cp != NULL)
 					cp = cp->next;
 
 				rhori = (cp != NULL &&
 				     cp->pos == TBL_CELL_DHORIZ) ||
 				    (dp != NULL &&
 				     dp->pos == TBL_DATA_DHORIZ) ? 2 :
 				    (cp != NULL &&
 				     cp->pos == TBL_CELL_HORIZ) ||
 				    (dp != NULL &&
 				     dp->pos == TBL_DATA_HORIZ) ? 1 : 0;
 
 				if (tp->tbl.cols[ic].spacing)
 					tbl_direct_border(tp,
 					    BLEFT * lhori + BRIGHT * rhori +
-					    BUP * uvert + BDOWN * dvert, 1);
+					    BUP * uvert + BDOWN * dvert, enw);
 
 				if (tp->enc == TERMENC_UTF8)
 					uvert = dvert = 0;
 
 				if (tp->tbl.cols[ic].spacing > 2 &&
 				    (uvert > 1 || dvert > 1 || rhori))
 					tbl_direct_border(tp,
 					    BHORIZ * rhori +
 					    BUP * (uvert > 1) +
-					    BDOWN * (dvert > 1), 1);
+					    BDOWN * (dvert > 1), enw);
 			}
 		}
 
 		/* Print the vertical frame at the end of each row. */
 
 		uvert = dvert = sp->opts->opts & TBL_OPT_DBOX ? 2 :
 		    sp->opts->opts & TBL_OPT_BOX ? 1 : 0;
 		if (sp->pos == TBL_SPAN_DATA &&
 		    uvert < sp->layout->last->vert &&
 		    sp->layout->last->col + 1 == sp->opts->cols)
 			uvert = dvert = sp->layout->last->vert;
 		if (sp->next != NULL &&
 		    dvert < sp->next->layout->last->vert &&
 		    sp->next->layout->last->col + 1 == sp->opts->cols)
 			dvert = sp->next->layout->last->vert;
 		if (sp->prev != NULL &&
 		    uvert < sp->prev->layout->last->vert &&
 		    sp->prev->layout->last->col + 1 == sp->opts->cols &&
 		    (horiz || (IS_HORIZ(sp->layout->last) &&
 		     !IS_HORIZ(sp->prev->layout->last))))
 			uvert = sp->prev->layout->last->vert;
 		lhori = sp->pos == TBL_SPAN_DHORIZ ||
 		    (sp->last != NULL &&
 		     sp->last->pos == TBL_DATA_DHORIZ &&
 		     sp->last->layout->col + 1 == sp->opts->cols) ||
 		    (sp->layout->last->pos == TBL_CELL_DHORIZ &&
 		     sp->layout->last->col + 1 == sp->opts->cols) ? 2 :
 		    sp->pos == TBL_SPAN_HORIZ ||
 		    (sp->last != NULL &&
 		     sp->last->pos == TBL_DATA_HORIZ &&
 		     sp->last->layout->col + 1 == sp->opts->cols) ||
 		    (sp->layout->last->pos == TBL_CELL_HORIZ &&
 		     sp->layout->last->col + 1 == sp->opts->cols) ? 1 : 0;
 		fc = BUP * uvert + BDOWN * dvert + BLEFT * lhori;
 		if (uvert > 0 || dvert > 0 || (horiz && sp->opts->rvert)) {
 			if (horiz == 0 && (IS_HORIZ(sp->layout->last) == 0 ||
 			    sp->layout->last->col + 1 < sp->opts->cols)) {
 				tp->tcol++;
-				do {
+				if (tp->tcol->offset > tp->viscol)
 					tbl_direct_border(tp,
-					    BHORIZ * lhori, 1);
-				} while (tp->viscol < tp->tcol->offset);
+					    BHORIZ * lhori,
+					    tp->tcol->offset - tp->viscol);
 			}
-			tbl_direct_border(tp, fc, 1);
+			tbl_direct_border(tp, fc, enw);
 		}
 		(*tp->endline)(tp);
-		tp->viscol = 0;
 	} while (more);
 
 	/*
 	 * Clean up after this row.  If it is the last line
 	 * of the table, print the box line and clean up
 	 * column data; otherwise, print the allbox line.
 	 */
 
 	term_setcol(tp, 1);
 	tp->flags &= ~TERMP_MULTICOL;
 	tp->tcol->rmargin = tp->maxrmargin;
 	if (sp->next == NULL) {
 		if (sp->opts->opts & (TBL_OPT_DBOX | TBL_OPT_BOX))
 			tbl_hrule(tp, sp, sp, NULL, TBL_OPT_BOX);
 		if (tp->enc == TERMENC_ASCII &&
 		    sp->opts->opts & TBL_OPT_DBOX)
 			tbl_hrule(tp, sp, sp, NULL, TBL_OPT_DBOX);
 		assert(tp->tbl.cols);
 		free(tp->tbl.cols);
 		tp->tbl.cols = NULL;
 	} else if (horiz == 0 && sp->opts->opts & TBL_OPT_ALLBOX &&
 	    (sp->next == NULL || sp->next->pos == TBL_SPAN_DATA ||
 	     sp->next->next != NULL))
 		tbl_hrule(tp, sp, sp, sp->next, TBL_OPT_ALLBOX);
 
 	tp->tcol->offset = save_offset;
 	tp->flags &= ~TERMP_NONOSPACE;
 }
 
 static void
 tbl_hrule(struct termp *tp, const struct tbl_span *spp,
     const struct tbl_span *sp, const struct tbl_span *spn, int flags)
 {
 	const struct tbl_cell	*cpp;    /* Layout cell above this line. */
 	const struct tbl_cell	*cp;     /* Layout cell in this line. */
 	const struct tbl_cell	*cpn;    /* Layout cell below this line. */
 	const struct tbl_dat	*dpn;	 /* Data cell below this line. */
 	const struct roffcol	*col;    /* Contains width and spacing. */
+	size_t			 enw;	 /* Width of one EN unit. */
 	int			 opts;   /* For the table as a whole. */
 	int			 bw;	 /* Box line width. */
 	int			 hw;     /* Horizontal line width. */
 	int			 lw, rw; /* Left and right line widths. */
 	int			 uw, dw; /* Vertical line widths. */
 
 	cpp = spp == NULL ? NULL : spp->layout->first;
 	cp  = sp  == NULL ? NULL : sp->layout->first;
 	cpn = spn == NULL ? NULL : spn->layout->first;
 	dpn = NULL;
 	if (spn != NULL) {
 		if (spn->pos == TBL_SPAN_DATA)
 			dpn = spn->first;
 		else if (spn->next != NULL)
 			dpn = spn->next->first;
 	}
 	opts = sp->opts->opts;
 	bw = opts & TBL_OPT_DBOX ? (tp->enc == TERMENC_UTF8 ? 2 : 1) :
 	    opts & (TBL_OPT_BOX | TBL_OPT_ALLBOX) ? 1 : 0;
 	hw = flags == TBL_OPT_DBOX || flags == TBL_OPT_BOX ? bw :
 	    sp->pos == TBL_SPAN_DHORIZ ? 2 : 1;
 
 	/* Print the left end of the line. */
 
-	if (tp->viscol == 0) {
+	enw = term_len(tp, 1);
+	if (tp->viscol == 0)
 		(*tp->advance)(tp, tp->tcols->offset);
-		tp->viscol = tp->tcols->offset;
-	}
-	if (flags != 0)
+	if (flags != 0) {
 		tbl_direct_border(tp,
 		    (spp == NULL ? 0 : BUP * bw) +
 		    (spn == NULL ? 0 : BDOWN * bw) +
 		    (spp == NULL || cpn == NULL ||
-		     cpn->pos != TBL_CELL_DOWN ? BRIGHT * hw : 0), 1);
+		     cpn->pos != TBL_CELL_DOWN ? BRIGHT * hw : 0), enw);
+		tbl_direct_border(tp,
+		    (spp == NULL || cpn == NULL ||
+		     cpn->pos != TBL_CELL_DOWN ? BHORIZ * hw : 0), enw);
+	}
 
 	col = tp->tbl.cols;
 	for (;;) {
 		if (cp == NULL)
 			col++;
 		else
 			col = tp->tbl.cols + cp->col;
 
 		/* Print the horizontal line inside this column. */
 
 		lw = cpp == NULL || cpn == NULL ||
 		    (cpn->pos != TBL_CELL_DOWN &&
 		     (dpn == NULL || dpn->string == NULL ||
 		      strcmp(dpn->string, "\\^") != 0))
 		    ? hw : 0;
 		tbl_direct_border(tp, BHORIZ * lw,
-		    col->width + col->spacing / 2);
+		    col->width + term_len(tp, col->spacing / 2));
 
 		/*
 		 * Figure out whether a vertical line is crossing
 		 * at the end of this column,
 		 * and advance to the next column.
 		 */
 
 		uw = dw = 0;
 		if (cpp != NULL) {
 			if (flags != TBL_OPT_DBOX) {
 				uw = cpp->vert;
 				if (uw == 0 && opts & TBL_OPT_ALLBOX)
 					uw = 1;
 			}
 			cpp = cpp->next;
 		} else if (spp != NULL && opts & TBL_OPT_ALLBOX)
 			uw = 1;
 		if (cp != NULL)
 			cp = cp->next;
 		if (cpn != NULL) {
 			if (flags != TBL_OPT_DBOX) {
 				dw = cpn->vert;
 				if (dw == 0 && opts & TBL_OPT_ALLBOX)
 					dw = 1;
 			}
 			cpn = cpn->next;
 			while (dpn != NULL && dpn->layout != cpn)
 				dpn = dpn->next;
 		} else if (spn != NULL && opts & TBL_OPT_ALLBOX)
 			dw = 1;
 		if (col + 1 == tp->tbl.cols + sp->opts->cols)
 			break;
 
 		/* Vertical lines do not cross spanned cells. */
 
 		if (cpp != NULL && cpp->pos == TBL_CELL_SPAN)
 			uw = 0;
 		if (cpn != NULL && cpn->pos == TBL_CELL_SPAN)
 			dw = 0;
 
 		/* The horizontal line inside the next column. */
 
 		rw = cpp == NULL || cpn == NULL ||
 		    (cpn->pos != TBL_CELL_DOWN &&
 		     (dpn == NULL || dpn->string == NULL ||
 		      strcmp(dpn->string, "\\^") != 0))
 		    ? hw : 0;
 
 		/* The line crossing at the end of this column. */
 
 		if (col->spacing)
 			tbl_direct_border(tp, BLEFT * lw +
-			    BRIGHT * rw + BUP * uw + BDOWN * dw, 1);
+			    BRIGHT * rw + BUP * uw + BDOWN * dw, enw);
 
 		/*
 		 * In ASCII output, a crossing may print two characters.
 		 */
 
 		if (tp->enc != TERMENC_ASCII || (uw < 2 && dw < 2))
 			uw = dw = 0;
 		if (col->spacing > 2)
 			tbl_direct_border(tp,
-                            BHORIZ * rw + BUP * uw + BDOWN * dw, 1);
+                            BHORIZ * rw + BUP * uw + BDOWN * dw, enw);
 
 		/* Padding before the start of the next column. */
 
 		if (col->spacing > 4)
 			tbl_direct_border(tp,
-			    BHORIZ * rw, (col->spacing - 3) / 2);
+			    BHORIZ * rw,
+			    term_len(tp, (col->spacing - 3) / 2));
 	}
 
 	/* Print the right end of the line. */
 
 	if (flags != 0) {
 		tbl_direct_border(tp,
 		    (spp == NULL ? 0 : BUP * bw) +
 		    (spn == NULL ? 0 : BDOWN * bw) +
 		    (spp == NULL || spn == NULL ||
 		     spn->layout->last->pos != TBL_CELL_DOWN ?
-		     BLEFT * hw : 0), 1);
+		     BLEFT * hw : 0), enw);
 		(*tp->endline)(tp);
-		tp->viscol = 0;
 	}
 }
 
 static void
 tbl_data(struct termp *tp, const struct tbl_opts *opts,
     const struct tbl_cell *cp, const struct tbl_dat *dp,
-    const struct roffcol *col)
+    const struct roffcol *col, size_t *coloff)
 {
 	switch (cp->pos) {
 	case TBL_CELL_HORIZ:
-		tbl_fill_border(tp, BHORIZ, col->width);
+		tbl_fill_border(tp, BHORIZ, coloff, col->width);
 		return;
 	case TBL_CELL_DHORIZ:
-		tbl_fill_border(tp, BHORIZ * 2, col->width);
+		tbl_fill_border(tp, BHORIZ * 2, coloff, col->width);
 		return;
 	default:
 		break;
 	}
 
 	if (dp == NULL)
 		return;
 
 	switch (dp->pos) {
 	case TBL_DATA_NONE:
 		return;
 	case TBL_DATA_HORIZ:
 	case TBL_DATA_NHORIZ:
-		tbl_fill_border(tp, BHORIZ, col->width);
+		tbl_fill_border(tp, BHORIZ, coloff, col->width);
 		return;
 	case TBL_DATA_NDHORIZ:
 	case TBL_DATA_DHORIZ:
-		tbl_fill_border(tp, BHORIZ * 2, col->width);
+		tbl_fill_border(tp, BHORIZ * 2, coloff, col->width);
 		return;
 	default:
 		break;
 	}
 
 	switch (cp->pos) {
 	case TBL_CELL_LONG:
 	case TBL_CELL_CENTRE:
 	case TBL_CELL_LEFT:
 	case TBL_CELL_RIGHT:
-		tbl_literal(tp, dp, col);
+		tbl_literal(tp, dp, col, coloff);
 		break;
 	case TBL_CELL_NUMBER:
-		tbl_number(tp, opts, dp, col);
+		tbl_number(tp, opts, dp, col, coloff);
 		break;
 	case TBL_CELL_DOWN:
 	case TBL_CELL_SPAN:
 		break;
 	default:
 		abort();
 	}
 }
 
+/*
+ * Print multiple copies of the string cp to advance to
+ * len basic units from the left edge of the current column.
+ */
 static void
-tbl_fill_string(struct termp *tp, const char *cp, size_t len)
+tbl_fill_string(struct termp *tp, const char *cp, size_t *coloff, size_t len)
 {
-	size_t	 i, sz;
+	size_t	 sz;      /* Width of the string cp in basic units. */
+	size_t	 target;  /* Distance from the left margin in basic units. */
 
+	if (len == 0)
+		return;
 	sz = term_strlen(tp, cp);
-	for (i = 0; i < len; i += sz)
+	target = tp->tcol->offset + len;
+	while (*coloff < target) {
 		term_word(tp, cp);
+		*coloff += sz;
+	}
 }
 
+/*
+ * Print multiple copies of the ASCII character c to advance to
+ * len basic units from the left edge of the current column.
+ */
 static void
-tbl_fill_char(struct termp *tp, char c, size_t len)
+tbl_fill_char(struct termp *tp, char c, size_t *coloff, size_t len)
 {
 	char	 cp[2];
 
 	cp[0] = c;
 	cp[1] = '\0';
-	tbl_fill_string(tp, cp, len);
+	tbl_fill_string(tp, cp, coloff, len);
 }
 
+/*
+ * Print multiple copies of the border c to fill len basic units.
+ * Used for horizontal lines inside table cells.
+ */
 static void
-tbl_fill_border(struct termp *tp, int c, size_t len)
+tbl_fill_border(struct termp *tp, int c, size_t *coloff, size_t len)
 {
 	char	 buf[12];
 
 	if ((c = borders_locale[c]) > 127) {
 		(void)snprintf(buf, sizeof(buf), "\\[u%04x]", c);
-		tbl_fill_string(tp, buf, len);
+		tbl_fill_string(tp, buf, coloff, len);
 	} else
-		tbl_fill_char(tp, c, len);
+		tbl_fill_char(tp, c, coloff, len);
 }
 
+/*
+ * The same, but bypassing term_flushln().
+ * Used for horizontal and vertical lines at the edges of table cells.
+ */
 static void
 tbl_direct_border(struct termp *tp, int c, size_t len)
 {
-	size_t	 i, sz;
+	size_t	 sz;      /* Width of the character in basic units. */
+	size_t	 enw2;    /* Width of half an EN in basic units. */
+	size_t	 target;  /* Distance from the left margin in basic units. */
 
 	c = borders_locale[c];
-	sz = (*tp->width)(tp, c);
-	for (i = 0; i < len; i += sz) {
+	sz = (*tp->getwidth)(tp, c);
+	enw2 = (*tp->getwidth)(tp, ' ') / 2;
+	target = tp->viscol + len;
+	while (tp->viscol + enw2 < target) {
 		(*tp->letter)(tp, c);
 		tp->viscol += sz;
 	}
 }
 
 static void
 tbl_literal(struct termp *tp, const struct tbl_dat *dp,
-		const struct roffcol *col)
+		const struct roffcol *col, size_t *coloff)
 {
-	size_t		 len, padl, padr, width;
-	int		 ic, hspans;
+	size_t	 width;	 /* Of the cell including following spans [BU]. */
+	int	 ic;	 /* Column number of the cell. */
+	int	 hspans; /* Number of horizontal spans that follow. */
 
-	assert(dp->string);
-	len = term_strlen(tp, dp->string);
 	width = col->width;
 	ic = dp->layout->col;
 	hspans = dp->hspans;
 	while (hspans--) {
-		width += tp->tbl.cols[ic].spacing;
+		width += term_len(tp, tp->tbl.cols[ic].spacing);
 		ic++;
 		width += tp->tbl.cols[ic].width;
 	}
-
-	padr = width > len ? width - len : 0;
-	padl = 0;
-
-	switch (dp->layout->pos) {
-	case TBL_CELL_LONG:
-		padl = term_len(tp, 1);
-		padr = padr > padl ? padr - padl : 0;
-		break;
-	case TBL_CELL_CENTRE:
-		if (2 > padr)
-			break;
-		padl = padr / 2;
-		padr -= padl;
-		break;
-	case TBL_CELL_RIGHT:
-		padl = padr;
-		padr = 0;
-		break;
-	default:
-		break;
-	}
-
-	tbl_fill_char(tp, ASCII_NBRSP, padl);
 	tbl_word(tp, dp);
-	tbl_fill_char(tp, ASCII_NBRSP, padr);
+	*coloff += width;
 }
 
 static void
 tbl_number(struct termp *tp, const struct tbl_opts *opts,
 		const struct tbl_dat *dp,
-		const struct roffcol *col)
+		const struct roffcol *col, size_t *coloff)
 {
 	const char	*cp, *lastdigit, *lastpoint;
-	size_t		 intsz, padl, totsz;
+
+	/* Widths in basic units. */
+	size_t		 pad;	/* Padding before the number. */
+	size_t		 totsz;	/* Of the number to be printed. */
+	size_t		 intsz;	/* Of the integer part. */
+
 	char		 buf[2];
 
 	/*
 	 * Almost the same code as in tblcalc_number():
 	 * First find the position of the decimal point.
 	 */
 
 	assert(dp->string);
 	lastdigit = lastpoint = NULL;
 	for (cp = dp->string; cp[0] != '\0'; cp++) {
 		if (cp[0] == '\\' && cp[1] == '&') {
 			lastdigit = lastpoint = cp;
 			break;
 		} else if (cp[0] == opts->decimal &&
 		    (isdigit((unsigned char)cp[1]) ||
 		     (cp > dp->string && isdigit((unsigned char)cp[-1]))))
 			lastpoint = cp;
 		else if (isdigit((unsigned char)cp[0]))
 			lastdigit = cp;
 	}
 
 	/* Then measure both widths. */
 
-	padl = 0;
+	pad = 0;
 	totsz = term_strlen(tp, dp->string);
 	if (lastdigit != NULL) {
 		if (lastpoint == NULL)
 			lastpoint = lastdigit + 1;
 		intsz = 0;
 		buf[1] = '\0';
 		for (cp = dp->string; cp < lastpoint; cp++) {
 			buf[0] = cp[0];
 			intsz += term_strlen(tp, buf);
 		}
 
 		/*
 		 * Pad left to match the decimal position,
 		 * but avoid exceeding the total column width.
 		 */
 
 		if (col->decimal > intsz && col->width > totsz) {
-			padl = col->decimal - intsz;
-			if (padl + totsz > col->width)
-				padl = col->width - totsz;
+			pad = col->decimal - intsz;
+			if (pad + totsz > col->width)
+				pad = col->width - totsz;
 		}
 
 	/* If it is not a number, simply center the string. */
 
 	} else if (col->width > totsz)
-		padl = (col->width - totsz) / 2;
+		pad = (col->width - totsz) / 2;
 
-	tbl_fill_char(tp, ASCII_NBRSP, padl);
+	tbl_fill_char(tp, ASCII_NBRSP, coloff, pad);
 	tbl_word(tp, dp);
-
-	/* Pad right to fill the column.  */
-
-	if (col->width > padl + totsz)
-		tbl_fill_char(tp, ASCII_NBRSP, col->width - padl - totsz);
+	*coloff += col->width;
 }
 
 static void
 tbl_word(struct termp *tp, const struct tbl_dat *dp)
 {
 	int		 prev_font;
 
 	prev_font = tp->fonti;
 	switch (dp->layout->font) {
 		case ESCAPE_FONTBI:
 			term_fontpush(tp, TERMFONT_BI);
 			break;
 		case ESCAPE_FONTBOLD:
 		case ESCAPE_FONTCB:
 			term_fontpush(tp, TERMFONT_BOLD);
 			break;
 		case ESCAPE_FONTITALIC:
 		case ESCAPE_FONTCI:
 			term_fontpush(tp, TERMFONT_UNDER);
 			break;
 		case ESCAPE_FONTROMAN:
 		case ESCAPE_FONTCR:
 			break;
 		default:
 			abort();
 	}
 
 	term_word(tp, dp->string);
 
 	term_fontpopq(tp, prev_font);
 }
diff --git a/contrib/mandoc/term.c b/contrib/mandoc/term.c
index 58d9d9bf9240..4dde60d1e45c 100644
--- a/contrib/mandoc/term.c
+++ b/contrib/mandoc/term.c
@@ -1,1170 +1,1185 @@
-/* $Id: term.c,v 1.291 2023/04/28 19:11:04 schwarze Exp $ */
+/* $Id: term.c,v 1.294 2025/08/01 14:59:39 schwarze Exp $ */
 /*
- * Copyright (c) 2010-2022 Ingo Schwarze 
+ * Copyright (c) 2010-2022, 2025 Ingo Schwarze 
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
 #include "mandoc.h"
 #include "mandoc_aux.h"
 #include "out.h"
 #include "term.h"
 #include "main.h"
 
 static	size_t		 cond_width(const struct termp *, int, int *);
 static	void		 adjbuf(struct termp_col *, size_t);
 static	void		 bufferc(struct termp *, char);
 static	void		 encode(struct termp *, const char *, size_t);
 static	void		 encode1(struct termp *, int);
 static	void		 endline(struct termp *);
 static	void		 term_field(struct termp *, size_t, size_t);
 static	void		 term_fill(struct termp *, size_t *, size_t *,
 				size_t);
 
 
 void
 term_setcol(struct termp *p, size_t maxtcol)
 {
 	if (maxtcol > p->maxtcol) {
 		p->tcols = mandoc_recallocarray(p->tcols,
 		    p->maxtcol, maxtcol, sizeof(*p->tcols));
 		p->maxtcol = maxtcol;
 	}
 	p->lasttcol = maxtcol - 1;
 	p->tcol = p->tcols;
 }
 
 void
 term_free(struct termp *p)
 {
 	term_tab_free();
 	for (p->tcol = p->tcols; p->tcol < p->tcols + p->maxtcol; p->tcol++)
 		free(p->tcol->buf);
 	free(p->tcols);
 	free(p->fontq);
 	free(p);
 }
 
 void
 term_begin(struct termp *p, term_margin head,
 		term_margin foot, const struct roff_meta *arg)
 {
 
 	p->headf = head;
 	p->footf = foot;
 	p->argf = arg;
 	(*p->begin)(p);
 }
 
 void
 term_end(struct termp *p)
 {
 
 	(*p->end)(p);
 }
 
 /*
  * Flush a chunk of text.  By default, break the output line each time
  * the right margin is reached, and continue output on the next line
  * at the same offset as the chunk itself.  By default, also break the
  * output line at the end of the chunk.  There are many flags modifying
  * this behaviour, see the comments in the body of the function.
  */
 void
 term_flushln(struct termp *p)
 {
-	size_t	 vbl;      /* Number of blanks to prepend to the output. */
+	/* Widths in basic units. */
+	size_t	 vbl;      /* Whitespace to prepend to the output. */
 	size_t	 vbr;      /* Actual visual position of the end of field. */
 	size_t	 vfield;   /* Desired visual field width. */
 	size_t	 vtarget;  /* Desired visual position of the right margin. */
-	size_t	 ic;       /* Character position in the input buffer. */
-	size_t	 nbr;      /* Number of characters to print in this field. */
+
+	/* Bytes. */
+	size_t	 ic;       /* Byte index in the input buffer. */
+	size_t	 nbr;      /* Number of bytes to print in this field. */
 
 	/*
 	 * Normally, start writing at the left margin, but with the
 	 * NOPAD flag, start writing at the current position instead.
 	 */
 
 	vbl = (p->flags & TERMP_NOPAD) || p->tcol->offset < p->viscol ?
 	    0 : p->tcol->offset - p->viscol;
-	if (p->minbl && vbl < p->minbl)
-		vbl = p->minbl;
+	if (p->minbl > 0 && vbl < term_len(p, p->minbl))
+		vbl = term_len(p, p->minbl);
 
 	if ((p->flags & TERMP_MULTICOL) == 0)
 		p->tcol->col = 0;
 
 	/* Loop over output lines. */
 
 	for (;;) {
 		vfield = p->tcol->rmargin > p->viscol + vbl ?
 		    p->tcol->rmargin - p->viscol - vbl : 0;
 
 		/*
 		 * Normally, break the line at the the right margin
 		 * of the field, but with the NOBREAK flag, only
 		 * break it at the max right margin of the screen,
 		 * and with the BRNEVER flag, never break it at all.
 		 */
 
 		vtarget = (p->flags & TERMP_NOBREAK) == 0 ? vfield :
 		    p->maxrmargin > p->viscol + vbl ?
 		    p->maxrmargin - p->viscol - vbl : 0;
 
 		/*
 		 * Figure out how much text will fit in the field.
 		 * If there is whitespace only, print nothing.
 		 */
 
 		term_fill(p, &nbr, &vbr,
-		    p->flags & TERMP_BRNEVER ? SIZE_MAX : vtarget);
+		    p->flags & TERMP_BRNEVER ? SIZE_MAX / 2 : vtarget);
 		if (nbr == 0)
 			break;
 
 		/*
 		 * With the CENTER or RIGHT flag, increase the indentation
 		 * to center the text between the left and right margins
 		 * or to adjust it to the right margin, respectively.
 		 */
 
 		if (vbr < vtarget) {
 			if (p->flags & TERMP_CENTER)
 				vbl += (vtarget - vbr) / 2;
 			else if (p->flags & TERMP_RIGHT)
 				vbl += vtarget - vbr;
 		}
 
 		/* Finally, print the field content. */
 
 		term_field(p, vbl, nbr);
 		if (vbr < vtarget)
 			p->tcol->taboff += vbr;
 		else
 			p->tcol->taboff += vtarget;
-		p->tcol->taboff += (*p->width)(p, ' ');
+		p->tcol->taboff += term_len(p, 1);
 
 		/*
 		 * If there is no text left in the field, exit the loop.
 		 * If the BRTRSP flag is set, consider trailing
 		 * whitespace significant when deciding whether
 		 * the field fits or not.
 		 */
 
 		for (ic = p->tcol->col; ic < p->tcol->lastcol; ic++) {
 			switch (p->tcol->buf[ic]) {
 			case '\t':
 				if (p->flags & TERMP_BRTRSP)
 					vbr = term_tab_next(vbr);
 				continue;
 			case ' ':
 				if (p->flags & TERMP_BRTRSP)
-					vbr += (*p->width)(p, ' ');
+					vbr += term_len(p, 1);
 				continue;
 			case '\n':
 			case ASCII_NBRZW:
 			case ASCII_BREAK:
 			case ASCII_TABREF:
 				continue;
 			default:
 				break;
 			}
 			break;
 		}
 		if (ic == p->tcol->lastcol)
 			break;
 
 		/*
 		 * At the location of an automatic line break, input
 		 * space characters are consumed by the line break.
 		 */
 
 		while (p->tcol->col < p->tcol->lastcol &&
 		    p->tcol->buf[p->tcol->col] == ' ')
 			p->tcol->col++;
 
 		/*
 		 * In multi-column mode, leave the rest of the text
 		 * in the buffer to be handled by a subsequent
 		 * invocation, such that the other columns of the
 		 * table can be handled first.
 		 * In single-column mode, simply break the line.
 		 */
 
 		if (p->flags & TERMP_MULTICOL)
 			return;
 
 		endline(p);
 
 		/*
 		 * Normally, start the next line at the same indentation
 		 * as this one, but with the BRIND flag, start it at the
 		 * right margin instead.  This is used together with
 		 * NOBREAK for the tags in various kinds of tagged lists.
 		 */
 
 		vbl = p->flags & TERMP_BRIND ?
 		    p->tcol->rmargin : p->tcol->offset;
 	}
 
 	/* Reset output state in preparation for the next field. */
 
 	p->col = p->tcol->col = p->tcol->lastcol = 0;
 	p->minbl = p->trailspace;
 	p->flags &= ~(TERMP_BACKAFTER | TERMP_BACKBEFORE | TERMP_NOPAD);
 
 	if (p->flags & TERMP_MULTICOL)
 		return;
 
 	/*
 	 * The HANG flag means that the next field
 	 * always follows on the same line.
 	 * The NOBREAK flag means that the next field
 	 * follows on the same line unless the field was overrun.
 	 * Normally, break the line at the end of each field.
 	 */
 
 	if ((p->flags & TERMP_HANG) == 0 &&
 	    ((p->flags & TERMP_NOBREAK) == 0 ||
-	     vbr + term_len(p, p->trailspace) > vfield))
+	     vbr + term_len(p, p->trailspace) > vfield + term_len(p, 1) / 2))
 		endline(p);
 }
 
 /*
- * Store the number of input characters to print in this field in *nbr
- * and their total visual width to print in *vbr.
+ * Store the number of input bytes to print in this field in *nbr
+ * and their total visual width in basic units in *vbr.
  * If there is only whitespace in the field, both remain zero.
  * The desired visual width of the field is provided by vtarget.
  * If the first word is longer, the field will be overrun.
  */
 static void
 term_fill(struct termp *p, size_t *nbr, size_t *vbr, size_t vtarget)
 {
-	size_t	 ic;        /* Character position in the input buffer. */
+	/* Widths in basic units. */
 	size_t	 vis;       /* Visual position of the current character. */
 	size_t	 vn;        /* Visual position of the next character. */
+	size_t	 enw;       /* Width of an EN unit. */
+	int	 taboff;    /* Temporary offset for literal tabs. */
+
+	size_t	 ic;        /* Byte index in the input buffer. */
 	int	 breakline; /* Break at the end of this word. */
 	int	 graph;     /* Last character was non-blank. */
-	int	 taboff;    /* Temporary offset for literal tabs. */
 
 	*nbr = *vbr = vis = 0;
 	breakline = graph = 0;
 	taboff = p->tcol->taboff;
+	enw = (*p->getwidth)(p, ' ');
+	vtarget += enw / 2;
 	for (ic = p->tcol->col; ic < p->tcol->lastcol; ic++) {
 		switch (p->tcol->buf[ic]) {
 		case '\b':  /* Escape \o (overstrike) or backspace markup. */
 			assert(ic > 0);
-			vis -= (*p->width)(p, p->tcol->buf[ic - 1]);
+			vis -= (*p->getwidth)(p, p->tcol->buf[ic - 1]);
 			continue;
 
 		case ' ':
 		case ASCII_BREAK:  /* Escape \: (breakpoint). */
 			vn = vis;
 			if (p->tcol->buf[ic] == ' ')
-				vn += (*p->width)(p, ' ');
+				vn += enw;
 			/* Can break at the end of a word. */
 			if (breakline || vn > vtarget)
 				break;
 			if (graph) {
 				*nbr = ic;
 				*vbr = vis;
 				graph = 0;
 			}
 			vis = vn;
 			continue;
 
 		case '\n':  /* Escape \p (break at the end of the word). */
 			breakline = 1;
 			continue;
 
 		case ASCII_HYPH:  /* Breakable hyphen. */
 			graph = 1;
 			/*
 			 * We are about to decide whether to break the
 			 * line or not, so we no longer need this hyphen
 			 * to be marked as breakable.  Put back a real
 			 * hyphen such that we get the correct width.
 			 */
 			p->tcol->buf[ic] = '-';
-			vis += (*p->width)(p, '-');
+			vis += (*p->getwidth)(p, '-');
 			if (vis > vtarget) {
 				ic++;
 				break;
 			}
 			*nbr = ic + 1;
 			*vbr = vis;
 			continue;
 
 		case ASCII_TABREF:
-			taboff = -vis - (*p->width)(p, ' ');
+			taboff = -vis - enw;
 			continue;
 
 		default:
 			switch (p->tcol->buf[ic]) {
 			case '\t':
 				if (taboff < 0 && (size_t)-taboff > vis)
 					vis = 0;
 				else
 					vis += taboff;
 				vis = term_tab_next(vis);
 				vis -= taboff;
 				break;
 			case ASCII_NBRZW:  /* Non-breakable zero-width. */
 				break;
 			case ASCII_NBRSP:  /* Non-breakable space. */
 				p->tcol->buf[ic] = ' ';
 				/* FALLTHROUGH */
 			default:  /* Printable character. */
-				vis += (*p->width)(p, p->tcol->buf[ic]);
+				vis += (*p->getwidth)(p, p->tcol->buf[ic]);
 				break;
 			}
 			graph = 1;
 			if (vis > vtarget && *nbr > 0)
 				return;
 			continue;
 		}
 		break;
 	}
 
 	/*
 	 * If the last word extends to the end of the field without any
 	 * trailing whitespace, the loop could not check yet whether it
 	 * can remain on this line.  So do the check now.
 	 */
 
 	if (graph && (vis <= vtarget || *nbr == 0)) {
 		*nbr = ic;
 		*vbr = vis;
 	}
 }
 
 /*
  * Print the contents of one field
- * with an indentation of	 vbl	  visual columns,
- * and an input string length of nbr	  characters.
+ * with an indentation        of  vbl  basic units
+ * and an input string length of  nbr  bytes.
  */
 static void
 term_field(struct termp *p, size_t vbl, size_t nbr)
 {
-	size_t	 ic;	/* Character position in the input buffer. */
+	/* Widths in basic units. */
 	size_t	 vis;	/* Visual position of the current character. */
 	size_t	 vt;	/* Visual position including tab offset. */
 	size_t	 dv;	/* Visual width of the current character. */
 	int	 taboff; /* Temporary offset for literal tabs. */
 
+	size_t	 ic;	/* Byte position in the input buffer. */
+
 	vis = 0;
 	taboff = p->tcol->taboff;
 	for (ic = p->tcol->col; ic < nbr; ic++) {
 
 		/*
 		 * To avoid the printing of trailing whitespace,
 		 * do not print whitespace right away, only count it.
 		 */
 
 		switch (p->tcol->buf[ic]) {
 		case '\n':
 		case ASCII_BREAK:
 		case ASCII_NBRZW:
 			continue;
 		case ASCII_TABREF:
-			taboff = -vis - (*p->width)(p, ' ');
+			taboff = -vis - (*p->getwidth)(p, ' ');
 			continue;
 		case '\t':
 		case ' ':
 		case ASCII_NBRSP:
 			if (p->tcol->buf[ic] == '\t') {
 				if (taboff < 0 && (size_t)-taboff > vis)
 					vt = 0;
 				else
 					vt = vis + taboff;
 				dv = term_tab_next(vt) - vt;
 			} else
-				dv = (*p->width)(p, ' ');
+				dv = (*p->getwidth)(p, ' ');
 			vbl += dv;
 			vis += dv;
 			continue;
 		default:
 			break;
 		}
 
 		/*
 		 * We found a non-blank character to print,
 		 * so write preceding white space now.
 		 */
 
 		if (vbl > 0) {
 			(*p->advance)(p, vbl);
-			p->viscol += vbl;
 			vbl = 0;
 		}
 
 		/* Print the character and adjust the visual position. */
 
 		(*p->letter)(p, p->tcol->buf[ic]);
 		if (p->tcol->buf[ic] == '\b') {
-			dv = (*p->width)(p, p->tcol->buf[ic - 1]);
+			dv = (*p->getwidth)(p, p->tcol->buf[ic - 1]);
 			p->viscol -= dv;
 			vis -= dv;
 		} else {
-			dv = (*p->width)(p, p->tcol->buf[ic]);
+			dv = (*p->getwidth)(p, p->tcol->buf[ic]);
 			p->viscol += dv;
 			vis += dv;
 		}
 	}
 	p->tcol->col = nbr;
 }
 
+/*
+ * Print the margin character, if one is configured,
+ * and end the output line.
+ */
 static void
 endline(struct termp *p)
 {
 	if ((p->flags & (TERMP_NEWMC | TERMP_ENDMC)) == TERMP_ENDMC) {
 		p->mc = NULL;
 		p->flags &= ~TERMP_ENDMC;
 	}
 	if (p->mc != NULL) {
-		if (p->viscol && p->maxrmargin >= p->viscol)
-			(*p->advance)(p, p->maxrmargin - p->viscol + 1);
+		if (p->viscol > 0 && p->viscol <= p->maxrmargin)
+			(*p->advance)(p,
+			    p->maxrmargin - p->viscol + term_len(p, 1));
 		p->flags |= TERMP_NOBUF | TERMP_NOSPACE;
 		term_word(p, p->mc);
 		p->flags &= ~(TERMP_NOBUF | TERMP_NEWMC);
 	}
-	p->viscol = 0;
-	p->minbl = 0;
 	(*p->endline)(p);
 }
 
 /*
  * A newline only breaks an existing line; it won't assert vertical
  * space.  All data in the output buffer is flushed prior to the newline
  * assertion.
  */
 void
 term_newln(struct termp *p)
 {
 	p->flags |= TERMP_NOSPACE;
 	if (p->tcol->lastcol || p->viscol)
 		term_flushln(p);
 	p->tcol->taboff = 0;
 }
 
 /*
  * Asserts a vertical space (a full, empty line-break between lines).
  * Note that if used twice, this will cause two blank spaces and so on.
  * All data in the output buffer is flushed prior to the newline
  * assertion.
  */
 void
 term_vspace(struct termp *p)
 {
 
 	term_newln(p);
-	p->viscol = 0;
-	p->minbl = 0;
 	if (0 < p->skipvsp)
 		p->skipvsp--;
 	else
 		(*p->endline)(p);
 }
 
 /* Swap current and previous font; for \fP and .ft P */
 void
 term_fontlast(struct termp *p)
 {
 	enum termfont	 f;
 
 	f = p->fontl;
 	p->fontl = p->fontq[p->fonti];
 	p->fontq[p->fonti] = f;
 }
 
-/* Set font, save current, discard previous; for \f, .ft, .B etc. */
+/* Set font, save current, discard previous; for \f, .ft, and man(7). */
 void
 term_fontrepl(struct termp *p, enum termfont f)
 {
-
 	p->fontl = p->fontq[p->fonti];
+	if (p->fontibi && f == TERMFONT_UNDER)
+		f = TERMFONT_BI;
 	p->fontq[p->fonti] = f;
 }
 
-/* Set font, save previous. */
+/* Set font, save previous; for mdoc(7), eqn(7), and tbl(7). */
 void
 term_fontpush(struct termp *p, enum termfont f)
 {
+	enum termfont	 fl;
 
-	p->fontl = p->fontq[p->fonti];
+	fl = p->fontq[p->fonti];
 	if (++p->fonti == p->fontsz) {
 		p->fontsz += 8;
 		p->fontq = mandoc_reallocarray(p->fontq,
 		    p->fontsz, sizeof(*p->fontq));
 	}
-	p->fontq[p->fonti] = f;
+	p->fontq[p->fonti] = fl;
+	term_fontrepl(p, f);
 }
 
 /* Flush to make the saved pointer current again. */
 void
 term_fontpopq(struct termp *p, int i)
 {
-
 	assert(i >= 0);
 	if (p->fonti > i)
 		p->fonti = i;
 }
 
 /* Pop one font off the stack. */
 void
 term_fontpop(struct termp *p)
 {
-
-	assert(p->fonti);
+	assert(p->fonti > 0);
 	p->fonti--;
 }
 
 /*
  * Handle pwords, partial words, which may be either a single word or a
  * phrase that cannot be broken down (such as a literal string).  This
  * handles word styling.
  */
 void
 term_word(struct termp *p, const char *word)
 {
 	struct roffsu	 su;
 	const char	 nbrsp[2] = { ASCII_NBRSP, 0 };
-	const char	*seq, *cp;
-	int		 sz, uc;
-	size_t		 csz, lsz, ssz;
+	const char	*seq;		/* Escape sequence argument. */
+	const char	*cp;		/* String to be printed. */
+	size_t		 csz;		/* String length in basic units. */
+	size_t		 lsz;		/* Line width in basic units. */
+	size_t		 ssz;		/* Substring length in bytes. */
+	int		 sz;		/* Argument length in bytes. */
+	int		 uc;		/* Unicode codepoint number. */
+	int		 bu;		/* Width in basic units. */
 	enum mandoc_esc	 esc;
 
 	if ((p->flags & TERMP_NOBUF) == 0) {
 		if ((p->flags & TERMP_NOSPACE) == 0) {
 			if ((p->flags & TERMP_KEEP) == 0) {
 				bufferc(p, ' ');
 				if (p->flags & TERMP_SENTENCE)
 					bufferc(p, ' ');
 			} else
 				bufferc(p, ASCII_NBRSP);
 		}
 		if (p->flags & TERMP_PREKEEP)
 			p->flags |= TERMP_KEEP;
 		if (p->flags & TERMP_NONOSPACE)
 			p->flags |= TERMP_NOSPACE;
 		else
 			p->flags &= ~TERMP_NOSPACE;
 		p->flags &= ~(TERMP_SENTENCE | TERMP_NONEWLINE);
 		p->skipvsp = 0;
 	}
 
 	while ('\0' != *word) {
 		if ('\\' != *word) {
 			if (TERMP_NBRWORD & p->flags) {
 				if (' ' == *word) {
 					encode(p, nbrsp, 1);
 					word++;
 					continue;
 				}
 				ssz = strcspn(word, "\\ ");
 			} else
 				ssz = strcspn(word, "\\");
 			encode(p, word, ssz);
 			word += (int)ssz;
 			continue;
 		}
 
 		word++;
 		esc = mandoc_escape(&word, &seq, &sz);
 		switch (esc) {
 		case ESCAPE_UNICODE:
 			uc = mchars_num2uc(seq + 1, sz - 1);
 			break;
 		case ESCAPE_NUMBERED:
 			uc = mchars_num2char(seq, sz);
 			if (uc >= 0)
 				break;
 			bufferc(p, ASCII_NBRZW);
 			continue;
 		case ESCAPE_SPECIAL:
 			if (p->enc == TERMENC_ASCII) {
 				cp = mchars_spec2str(seq, sz, &ssz);
 				if (cp != NULL)
 					encode(p, cp, ssz);
 				else
 					bufferc(p, ASCII_NBRZW);
 			} else {
 				uc = mchars_spec2cp(seq, sz);
 				if (uc > 0)
 					encode1(p, uc);
 				else
 					bufferc(p, ASCII_NBRZW);
 			}
 			continue;
 		case ESCAPE_UNDEF:
 			uc = *seq;
 			break;
 		case ESCAPE_FONTBOLD:
 		case ESCAPE_FONTCB:
 			term_fontrepl(p, TERMFONT_BOLD);
 			continue;
 		case ESCAPE_FONTITALIC:
 		case ESCAPE_FONTCI:
 			term_fontrepl(p, TERMFONT_UNDER);
 			continue;
 		case ESCAPE_FONTBI:
 			term_fontrepl(p, TERMFONT_BI);
 			continue;
 		case ESCAPE_FONT:
 		case ESCAPE_FONTCR:
 		case ESCAPE_FONTROMAN:
 			term_fontrepl(p, TERMFONT_NONE);
 			continue;
 		case ESCAPE_FONTPREV:
 			term_fontlast(p);
 			continue;
 		case ESCAPE_BREAK:
 			bufferc(p, '\n');
 			continue;
 		case ESCAPE_NOSPACE:
 			if (p->flags & TERMP_BACKAFTER)
 				p->flags &= ~TERMP_BACKAFTER;
 			else if (*word == '\0')
 				p->flags |= (TERMP_NOSPACE | TERMP_NONEWLINE);
 			continue;
 		case ESCAPE_DEVICE:
 			if (p->type == TERMTYPE_PDF)
 				encode(p, "pdf", 3);
 			else if (p->type == TERMTYPE_PS)
 				encode(p, "ps", 2);
 			else if (p->enc == TERMENC_ASCII)
 				encode(p, "ascii", 5);
 			else
 				encode(p, "utf8", 4);
 			continue;
 		case ESCAPE_HORIZ:
 			if (p->flags & TERMP_BACKAFTER) {
 				p->flags &= ~TERMP_BACKAFTER;
 				continue;
 			}
 			if (*seq == '|') {
 				seq++;
-				uc = -p->col;
+				bu = -term_len(p, p->col);
 			} else
-				uc = 0;
+				bu = 0;
 			if (a2roffsu(seq, &su, SCALE_EM) == NULL)
 				continue;
-			uc += term_hen(p, &su);
-			if (uc >= 0) {
-				while (uc > 0) {
-					uc -= term_len(p, 1);
+			bu += term_hspan(p, &su);
+			if (bu >= 0) {
+				while (bu > 0) {
+					bu -= term_len(p, 1);
 					if (p->flags & TERMP_BACKBEFORE)
 						p->flags &= ~TERMP_BACKBEFORE;
 					else
 						bufferc(p, ASCII_NBRSP);
 				}
 				continue;
 			}
 			if (p->flags & TERMP_BACKBEFORE) {
 				p->flags &= ~TERMP_BACKBEFORE;
-				assert(p->col > 0);
+				assert(p->col > 1);
 				p->col--;
 			}
-			if (p->col >= (size_t)(-uc)) {
-				p->col += uc;
+			if (term_len(p, p->col) >= (size_t)(-bu)) {
+				p->col -= -bu / term_len(p, 1);
 			} else {
-				uc += p->col;
+				bu += term_len(p, p->col);
 				p->col = 0;
-				if (p->tcol->offset > (size_t)(-uc)) {
-					p->ti += uc;
-					p->tcol->offset += uc;
+				if (p->tcol->offset > (size_t)(-bu)) {
+					p->ti += bu;
+					p->tcol->offset += bu;
 				} else {
 					p->ti -= p->tcol->offset;
 					p->tcol->offset = 0;
 				}
 			}
 			continue;
 		case ESCAPE_HLINE:
 			if ((cp = a2roffsu(seq, &su, SCALE_EM)) == NULL)
 				continue;
-			uc = term_hen(p, &su);
-			if (uc <= 0) {
+			bu = term_hspan(p, &su);
+			if (bu <= 0) {
 				if (p->tcol->rmargin <= p->tcol->offset)
 					continue;
 				lsz = p->tcol->rmargin - p->tcol->offset;
 			} else
-				lsz = uc;
+				lsz = bu;
 			if (*cp == seq[-1])
 				uc = -1;
 			else if (*cp == '\\') {
 				seq = cp + 1;
 				esc = mandoc_escape(&seq, &cp, &sz);
 				switch (esc) {
 				case ESCAPE_UNICODE:
 					uc = mchars_num2uc(cp + 1, sz - 1);
 					break;
 				case ESCAPE_NUMBERED:
 					uc = mchars_num2char(cp, sz);
 					break;
 				case ESCAPE_SPECIAL:
 					uc = mchars_spec2cp(cp, sz);
 					break;
 				case ESCAPE_UNDEF:
 					uc = *seq;
 					break;
 				default:
 					uc = -1;
 					break;
 				}
 			} else
 				uc = *cp;
 			if (uc < 0x20 || (uc > 0x7E && uc < 0xA0))
 				uc = '_';
 			if (p->enc == TERMENC_ASCII) {
 				cp = ascii_uc2str(uc);
 				csz = term_strlen(p, cp);
 				ssz = strlen(cp);
 			} else
-				csz = (*p->width)(p, uc);
-			while (lsz >= csz) {
+				csz = (*p->getwidth)(p, uc);
+			while (lsz > 0) {
 				if (p->enc == TERMENC_ASCII)
 					encode(p, cp, ssz);
 				else
 					encode1(p, uc);
-				lsz -= csz;
+				if (lsz > csz)
+					lsz -= csz;
+				else
+					lsz = 0;
 			}
 			continue;
 		case ESCAPE_SKIPCHAR:
 			p->flags |= TERMP_BACKAFTER;
 			continue;
 		case ESCAPE_OVERSTRIKE:
 			cp = seq + sz;
 			while (seq < cp) {
 				if (*seq == '\\') {
 					mandoc_escape(&seq, NULL, NULL);
 					continue;
 				}
 				encode1(p, *seq++);
 				if (seq < cp) {
 					if (p->flags & TERMP_BACKBEFORE)
 						p->flags |= TERMP_BACKAFTER;
 					else
 						p->flags |= TERMP_BACKBEFORE;
 				}
 			}
 			/* Trim trailing backspace/blank pair. */
 			if (p->tcol->lastcol > 2 &&
 			    (p->tcol->buf[p->tcol->lastcol - 1] == ' ' ||
 			     p->tcol->buf[p->tcol->lastcol - 1] == '\t'))
 				p->tcol->lastcol -= 2;
 			if (p->col > p->tcol->lastcol)
 				p->col = p->tcol->lastcol;
 			continue;
 		case ESCAPE_IGNORE:
 			bufferc(p, ASCII_NBRZW);
 			continue;
 		default:
 			continue;
 		}
 
 		/*
 		 * Common handling for Unicode and numbered
 		 * character escape sequences.
 		 */
 
 		if (p->enc == TERMENC_ASCII) {
 			cp = ascii_uc2str(uc);
 			encode(p, cp, strlen(cp));
 		} else {
 			if ((uc < 0x20 && uc != 0x09) ||
 			    (uc > 0x7E && uc < 0xA0))
 				uc = 0xFFFD;
 			encode1(p, uc);
 		}
 	}
 	p->flags &= ~TERMP_NBRWORD;
 }
 
 static void
 adjbuf(struct termp_col *c, size_t sz)
 {
 	if (c->maxcols == 0)
 		c->maxcols = 1024;
 	while (c->maxcols <= sz)
 		c->maxcols <<= 2;
 	c->buf = mandoc_reallocarray(c->buf, c->maxcols, sizeof(*c->buf));
 }
 
 static void
 bufferc(struct termp *p, char c)
 {
 	if (p->flags & TERMP_NOBUF) {
 		(*p->letter)(p, c);
 		return;
 	}
 	if (p->col + 1 >= p->tcol->maxcols)
 		adjbuf(p->tcol, p->col + 1);
 	if (p->tcol->lastcol <= p->col || (c != ' ' && c != ASCII_NBRSP))
 		p->tcol->buf[p->col] = c;
 	if (p->tcol->lastcol < ++p->col)
 		p->tcol->lastcol = p->col;
 }
 
 void
 term_tab_ref(struct termp *p)
 {
 	if (p->tcol->lastcol && p->tcol->lastcol <= p->col &&
 	    (p->flags & TERMP_NOBUF) == 0)
 		bufferc(p, ASCII_TABREF);
 }
 
 /*
  * See encode().
  * Do this for a single (probably unicode) value.
  * Does not check for non-decorated glyphs.
  */
 static void
 encode1(struct termp *p, int c)
 {
 	enum termfont	  f;
 
 	if (p->flags & TERMP_NOBUF) {
 		(*p->letter)(p, c);
 		return;
 	}
 
 	if (p->col + 7 >= p->tcol->maxcols)
 		adjbuf(p->tcol, p->col + 7);
 
 	f = (c == ASCII_HYPH || c > 127 || isgraph(c)) ?
 	    p->fontq[p->fonti] : TERMFONT_NONE;
 
 	if (p->flags & TERMP_BACKBEFORE) {
 		if (p->tcol->buf[p->col - 1] == ' ' ||
 		    p->tcol->buf[p->col - 1] == '\t')
 			p->col--;
 		else
 			p->tcol->buf[p->col++] = '\b';
 		p->flags &= ~TERMP_BACKBEFORE;
 	}
 	if (f == TERMFONT_UNDER || f == TERMFONT_BI) {
 		p->tcol->buf[p->col++] = '_';
 		p->tcol->buf[p->col++] = '\b';
 	}
 	if (f == TERMFONT_BOLD || f == TERMFONT_BI) {
 		if (c == ASCII_HYPH)
 			p->tcol->buf[p->col++] = '-';
 		else
 			p->tcol->buf[p->col++] = c;
 		p->tcol->buf[p->col++] = '\b';
 	}
 	if (p->tcol->lastcol <= p->col || (c != ' ' && c != ASCII_NBRSP))
 		p->tcol->buf[p->col] = c;
 	if (p->tcol->lastcol < ++p->col)
 		p->tcol->lastcol = p->col;
 	if (p->flags & TERMP_BACKAFTER) {
 		p->flags |= TERMP_BACKBEFORE;
 		p->flags &= ~TERMP_BACKAFTER;
 	}
 }
 
 static void
 encode(struct termp *p, const char *word, size_t sz)
 {
 	size_t		  i;
 
 	if (p->flags & TERMP_NOBUF) {
 		for (i = 0; i < sz; i++)
 			(*p->letter)(p, word[i]);
 		return;
 	}
 
 	if (p->col + 2 + (sz * 5) >= p->tcol->maxcols)
 		adjbuf(p->tcol, p->col + 2 + (sz * 5));
 
 	for (i = 0; i < sz; i++) {
 		if (ASCII_HYPH == word[i] ||
 		    isgraph((unsigned char)word[i]))
 			encode1(p, word[i]);
 		else {
 			if (p->tcol->lastcol <= p->col ||
 			    (word[i] != ' ' && word[i] != ASCII_NBRSP))
 				p->tcol->buf[p->col] = word[i];
 			p->col++;
 
 			/*
 			 * Postpone the effect of \z while handling
 			 * an overstrike sequence from ascii_uc2str().
 			 */
 
 			if (word[i] == '\b' &&
 			    (p->flags & TERMP_BACKBEFORE)) {
 				p->flags &= ~TERMP_BACKBEFORE;
 				p->flags |= TERMP_BACKAFTER;
 			}
 		}
 	}
 	if (p->tcol->lastcol < p->col)
 		p->tcol->lastcol = p->col;
 }
 
 void
 term_setwidth(struct termp *p, const char *wstr)
 {
 	struct roffsu	 su;
 	int		 iop, width;
 
 	iop = 0;
 	width = 0;
 	if (NULL != wstr) {
 		switch (*wstr) {
 		case '+':
 			iop = 1;
 			wstr++;
 			break;
 		case '-':
 			iop = -1;
 			wstr++;
 			break;
 		default:
 			break;
 		}
 		if (a2roffsu(wstr, &su, SCALE_MAX) != NULL)
 			width = term_hspan(p, &su);
 		else
 			iop = 0;
 	}
 	(*p->setwidth)(p, iop, width);
 }
 
 size_t
 term_len(const struct termp *p, size_t sz)
 {
-
-	return (*p->width)(p, ' ') * sz;
+	return (*p->getwidth)(p, ' ') * sz;
 }
 
 static size_t
 cond_width(const struct termp *p, int c, int *skip)
 {
-
 	if (*skip) {
 		(*skip) = 0;
 		return 0;
 	} else
-		return (*p->width)(p, c);
+		return (*p->getwidth)(p, c);
 }
 
 size_t
 term_strlen(const struct termp *p, const char *cp)
 {
-	size_t		 sz, rsz, i;
-	int		 ssz, skip, uc;
-	const char	*seq, *rhs;
+	const char	*seq;		/* Escape sequence argument. */
+	const char	*rhs;		/* String to be printed. */
+
+	/* Widths in basic units. */
+	size_t		 sz;		/* Return value. */
+	size_t		 this_sz;	/* Individual char for overstrike. */
+	size_t		 max_sz;	/* Result of overstrike. */
+
+	/* Numbers of bytes. */
+	size_t		 rsz;		/* Substring length in bytes. */
+	size_t		 i;		/* Byte index in substring. */
+	int		 ssz;		/* Argument length in bytes. */
+	int		 skip;		/* Number of bytes to skip. */
+
+	int		 uc;		/* Unicode codepoint number. */
 	enum mandoc_esc	 esc;
+
 	static const char rej[] = { '\\', ASCII_NBRSP, ASCII_NBRZW,
 		ASCII_BREAK, ASCII_HYPH, ASCII_TABREF, '\0' };
 
 	/*
 	 * Account for escaped sequences within string length
 	 * calculations.  This follows the logic in term_word() as we
 	 * must calculate the width of produced strings.
 	 */
 
 	sz = 0;
 	skip = 0;
 	while ('\0' != *cp) {
 		rsz = strcspn(cp, rej);
 		for (i = 0; i < rsz; i++)
 			sz += cond_width(p, *cp++, &skip);
 
 		switch (*cp) {
 		case '\\':
 			cp++;
 			rhs = NULL;
 			esc = mandoc_escape(&cp, &seq, &ssz);
 			switch (esc) {
 			case ESCAPE_UNICODE:
 				uc = mchars_num2uc(seq + 1, ssz - 1);
 				break;
 			case ESCAPE_NUMBERED:
 				uc = mchars_num2char(seq, ssz);
 				if (uc < 0)
 					continue;
 				break;
 			case ESCAPE_SPECIAL:
 				if (p->enc == TERMENC_ASCII) {
 					rhs = mchars_spec2str(seq, ssz, &rsz);
 					if (rhs != NULL)
 						break;
 				} else {
 					uc = mchars_spec2cp(seq, ssz);
 					if (uc > 0)
 						sz += cond_width(p, uc, &skip);
 				}
 				continue;
 			case ESCAPE_UNDEF:
 				uc = *seq;
 				break;
 			case ESCAPE_DEVICE:
 				if (p->type == TERMTYPE_PDF) {
 					rhs = "pdf";
 					rsz = 3;
 				} else if (p->type == TERMTYPE_PS) {
 					rhs = "ps";
 					rsz = 2;
 				} else if (p->enc == TERMENC_ASCII) {
 					rhs = "ascii";
 					rsz = 5;
 				} else {
 					rhs = "utf8";
 					rsz = 4;
 				}
 				break;
 			case ESCAPE_SKIPCHAR:
 				skip = 1;
 				continue;
 			case ESCAPE_OVERSTRIKE:
-				rsz = 0;
+				max_sz = 0;
 				rhs = seq + ssz;
 				while (seq < rhs) {
 					if (*seq == '\\') {
 						mandoc_escape(&seq, NULL, NULL);
 						continue;
 					}
-					i = (*p->width)(p, *seq++);
-					if (rsz < i)
-						rsz = i;
+					this_sz = (*p->getwidth)(p, *seq++);
+					if (max_sz < this_sz)
+						max_sz = this_sz;
 				}
-				sz += rsz;
+				sz += max_sz;
 				continue;
 			default:
 				continue;
 			}
 
 			/*
 			 * Common handling for Unicode and numbered
 			 * character escape sequences.
 			 */
 
 			if (rhs == NULL) {
 				if (p->enc == TERMENC_ASCII) {
 					rhs = ascii_uc2str(uc);
 					rsz = strlen(rhs);
 				} else {
 					if ((uc < 0x20 && uc != 0x09) ||
 					    (uc > 0x7E && uc < 0xA0))
 						uc = 0xFFFD;
 					sz += cond_width(p, uc, &skip);
 					continue;
 				}
 			}
 
 			if (skip) {
 				skip = 0;
 				break;
 			}
 
 			/*
 			 * Common handling for all escape sequences
 			 * printing more than one character.
 			 */
 
 			for (i = 0; i < rsz; i++)
-				sz += (*p->width)(p, *rhs++);
+				sz += (*p->getwidth)(p, *rhs++);
 			break;
 		case ASCII_NBRSP:
 			sz += cond_width(p, ' ', &skip);
 			cp++;
 			break;
 		case ASCII_HYPH:
 			sz += cond_width(p, '-', &skip);
 			cp++;
 			break;
 		default:
 			break;
 		}
 	}
 
 	return sz;
 }
 
 int
 term_vspan(const struct termp *p, const struct roffsu *su)
 {
 	double		 r;
 	int		 ri;
 
 	switch (su->unit) {
 	case SCALE_BU:
 		r = su->scale / 40.0;
 		break;
 	case SCALE_CM:
 		r = su->scale * 6.0 / 2.54;
 		break;
 	case SCALE_FS:
 		r = su->scale * 65536.0 / 40.0;
 		break;
 	case SCALE_IN:
 		r = su->scale * 6.0;
 		break;
 	case SCALE_MM:
 		r = su->scale * 0.006;
 		break;
 	case SCALE_PC:
 		r = su->scale;
 		break;
 	case SCALE_PT:
 		r = su->scale / 12.0;
 		break;
 	case SCALE_EN:
 	case SCALE_EM:
 		r = su->scale * 0.6;
 		break;
 	case SCALE_VS:
 		r = su->scale;
 		break;
 	default:
 		abort();
 	}
 	ri = r > 0.0 ? r + 0.4995 : r - 0.4995;
 	return ri < 66 ? ri : 1;
 }
 
 /*
- * Convert a scaling width to basic units, rounding towards 0.
+ * Convert a scaling width to basic units.
  */
 int
 term_hspan(const struct termp *p, const struct roffsu *su)
 {
-
 	return (*p->hspan)(p, su);
 }
-
-/*
- * Convert a scaling width to basic units, rounding to closest.
- */
-int
-term_hen(const struct termp *p, const struct roffsu *su)
-{
-	int bu;
-
-	if ((bu = (*p->hspan)(p, su)) >= 0)
-		return (bu + 11) / 24;
-	else
-		return -((-bu + 11) / 24);
-}
diff --git a/contrib/mandoc/term.h b/contrib/mandoc/term.h
index 3b3a79527eeb..1e4659734fc5 100644
--- a/contrib/mandoc/term.h
+++ b/contrib/mandoc/term.h
@@ -1,161 +1,155 @@
-/* $Id: term.h,v 1.134 2022/08/16 17:45:55 schwarze Exp $ */
+/* $Id: term.h,v 1.138 2025/07/27 15:27:28 schwarze Exp $ */
 /*
- * Copyright (c) 2011-2015,2017,2019,2022 Ingo Schwarze 
+ * Copyright (c) 2011-2015, 2017, 2019, 2021, 2022, 2025
+ *               Ingo Schwarze 
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
 enum	termenc {
 	TERMENC_ASCII,
 	TERMENC_LOCALE,
 	TERMENC_UTF8
 };
 
 enum	termtype {
 	TERMTYPE_CHAR,
 	TERMTYPE_PS,
 	TERMTYPE_PDF
 };
 
 enum	termfont {
 	TERMFONT_NONE = 0,
 	TERMFONT_BOLD,
 	TERMFONT_UNDER,
 	TERMFONT_BI,
 	TERMFONT__MAX
 };
 
 struct	eqn_box;
 struct	roff_meta;
 struct	roff_node;
 struct	tbl_span;
 struct	termp;
 
 typedef void	(*term_margin)(struct termp *, const struct roff_meta *);
 
-struct	termp_tbl {
-	int		  width;	/* width in fixed chars */
-	int		  decimal;	/* decimal point position */
-};
-
 struct	termp_col {
 	int		 *buf;		/* Output buffer. */
 	size_t		  maxcols;	/* Allocated bytes in buf. */
 	size_t		  lastcol;	/* Last byte in buf. */
 	size_t		  col;		/* Byte in buf to be written. */
-	size_t		  rmargin;	/* Current right margin. */
-	size_t		  offset;	/* Current left margin. */
-	size_t		  taboff;	/* Offset for literal tabs. */
+	size_t		  rmargin;	/* Current right margin [BU]. */
+	size_t		  offset;	/* Current left margin [BU]. */
+	size_t		  taboff;	/* Offset for literal tabs [BU]. */
 };
 
 struct	termp {
 	struct rofftbl	  tbl;		/* Table configuration. */
 	struct termp_col *tcols;	/* Array of table columns. */
 	struct termp_col *tcol;		/* Current table column. */
 	size_t		  maxtcol;	/* Allocated table columns. */
 	size_t		  lasttcol;	/* Last column currently used. */
 	size_t		  line;		/* Current output line number. */
-	size_t		  defindent;	/* Default indent for text. */
-	size_t		  defrmargin;	/* Right margin of the device. */
-	size_t		  lastrmargin;	/* Right margin before the last ll. */
-	size_t		  maxrmargin;	/* Max right margin. */
+	size_t		  defindent;	/* Default indent for text [EN]. */
+	size_t		  defrmargin;	/* Right margin of the device [BU]. */
+	size_t		  lastrmargin;	/* Right margin before last ll [BU]. */
+	size_t		  maxrmargin;	/* Maximum right margin [BU]. */
 	size_t		  col;		/* Byte position in buf. */
-	size_t		  viscol;	/* Chars on current line. */
-	size_t		  trailspace;	/* See term_flushln(). */
-	size_t		  minbl;	/* Minimum blanks before next field. */
+	size_t		  viscol;	/* Width of the current line [BU]. */
+	size_t		  trailspace;	/* Whitespace after field [EN]. */
+	size_t		  minbl;	/* Whitespace before field [EN]. */
 	int		  synopsisonly; /* Print the synopsis only. */
-	int		  mdocstyle;	/* Imitate mdoc(7) output. */
-	int		  ti;		/* Temporary indent for one line. */
+	int		  ti;		/* Temporary indent for line [BU]. */
 	int		  skipvsp;	/* Vertical space to skip. */
 	int		  flags;
 #define	TERMP_SENTENCE	 (1 << 0)	/* Space before a sentence. */
 #define	TERMP_NOSPACE	 (1 << 1)	/* No space before words. */
 #define	TERMP_NONOSPACE	 (1 << 2)	/* No space (no autounset). */
 #define	TERMP_NBRWORD	 (1 << 3)	/* Make next word nonbreaking. */
 #define	TERMP_KEEP	 (1 << 4)	/* Keep words together. */
 #define	TERMP_PREKEEP	 (1 << 5)	/* ...starting with the next one. */
 #define	TERMP_BACKAFTER	 (1 << 6)	/* Back up after next character. */
 #define	TERMP_BACKBEFORE (1 << 7)	/* Back up before next character. */
 #define	TERMP_NOBREAK	 (1 << 8)	/* See term_flushln(). */
 #define	TERMP_BRTRSP	 (1 << 9)	/* See term_flushln(). */
 #define	TERMP_BRIND	 (1 << 10)	/* See term_flushln(). */
 #define	TERMP_HANG	 (1 << 11)	/* See term_flushln(). */
 #define	TERMP_NOPAD	 (1 << 12)	/* See term_flushln(). */
 #define	TERMP_NOSPLIT	 (1 << 13)	/* Do not break line before .An. */
 #define	TERMP_SPLIT	 (1 << 14)	/* Break line before .An. */
 #define	TERMP_NONEWLINE	 (1 << 15)	/* No line break in nofill mode. */
 #define	TERMP_BRNEVER	 (1 << 16)	/* Don't even break at maxrmargin. */
 #define	TERMP_NOBUF	 (1 << 17)	/* Bypass output buffer. */
 #define	TERMP_NEWMC	 (1 << 18)	/* No .mc printed yet. */
 #define	TERMP_ENDMC	 (1 << 19)	/* Next break ends .mc mode. */
 #define	TERMP_MULTICOL	 (1 << 20)	/* Multiple column mode. */
 #define	TERMP_CENTER	 (1 << 21)	/* Center output lines. */
 #define	TERMP_RIGHT	 (1 << 22)	/* Adjust to the right margin. */
 	enum termtype	  type;		/* Terminal, PS, or PDF. */
 	enum termenc	  enc;		/* Type of encoding. */
 	enum termfont	  fontl;	/* Last font set. */
 	enum termfont	 *fontq;	/* Symmetric fonts. */
 	int		  fontsz;	/* Allocated size of font stack */
 	int		  fonti;	/* Index of font stack. */
+	int		  fontibi;	/* Map font I to BI. */
 	term_margin	  headf;	/* invoked to print head */
 	term_margin	  footf;	/* invoked to print foot */
 	void		(*letter)(struct termp *, int);
 	void		(*begin)(struct termp *);
 	void		(*end)(struct termp *);
 	void		(*endline)(struct termp *);
 	void		(*advance)(struct termp *, size_t);
 	void		(*setwidth)(struct termp *, int, int);
-	size_t		(*width)(const struct termp *, int);
+	size_t		(*getwidth)(const struct termp *, int);
 	int		(*hspan)(const struct termp *,
 				const struct roffsu *);
 	const void	 *argf;		/* arg for headf/footf */
 	const char	 *mc;		/* Margin character. */
 	struct termp_ps	 *ps;
 };
 
 
 const char	 *ascii_uc2str(int);
 
 void		  roff_term_pre(struct termp *, const struct roff_node *);
 
 void		  term_eqn(struct termp *, const struct eqn_box *);
 void		  term_tbl(struct termp *, const struct tbl_span *);
 void		  term_free(struct termp *);
 void		  term_setcol(struct termp *, size_t);
 void		  term_newln(struct termp *);
 void		  term_vspace(struct termp *);
 void		  term_word(struct termp *, const char *);
 void		  term_flushln(struct termp *);
 void		  term_begin(struct termp *, term_margin,
 			term_margin, const struct roff_meta *);
 void		  term_end(struct termp *);
 
 void		  term_setwidth(struct termp *, const char *);
 int		  term_hspan(const struct termp *, const struct roffsu *);
-int		  term_hen(const struct termp *, const struct roffsu *);
 int		  term_vspan(const struct termp *, const struct roffsu *);
 size_t		  term_strlen(const struct termp *, const char *);
 size_t		  term_len(const struct termp *, size_t);
 
 void		  term_tab_set(const struct termp *, const char *);
-void		  term_tab_iset(size_t);
 void		  term_tab_ref(struct termp *);
 size_t		  term_tab_next(size_t);
 void		  term_tab_free(void);
 
 void		  term_fontpush(struct termp *, enum termfont);
 void		  term_fontpop(struct termp *);
 void		  term_fontpopq(struct termp *, int);
 void		  term_fontrepl(struct termp *, enum termfont);
 void		  term_fontlast(struct termp *);
diff --git a/contrib/mandoc/term_ascii.c b/contrib/mandoc/term_ascii.c
index 3942dc757953..990833c8a021 100644
--- a/contrib/mandoc/term_ascii.c
+++ b/contrib/mandoc/term_ascii.c
@@ -1,423 +1,434 @@
-/* $Id: term_ascii.c,v 1.69 2023/11/13 19:13:01 schwarze Exp $ */
+/* $Id: term_ascii.c,v 1.71 2025/07/16 14:33:08 schwarze Exp $ */
 /*
+ * Copyright (c) 2014,2015,2017-2020,2025 Ingo Schwarze 
  * Copyright (c) 2010, 2011 Kristaps Dzonsons 
- * Copyright (c) 2014,2015,2017,2018,2020 Ingo Schwarze 
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 #include "config.h"
 
 #include 
 
 #include 
 #if HAVE_WCHAR
 #include 
 #include 
 #endif
 #include 
 #include 
 #include 
 #include 
 #include 
 #if HAVE_WCHAR
 #include 
 #endif
 
 #include "mandoc.h"
 #include "mandoc_aux.h"
 #include "out.h"
 #include "term.h"
 #include "manconf.h"
 #include "main.h"
 
 static	struct termp	 *ascii_init(enum termenc, const struct manoutput *);
 static	int		  ascii_hspan(const struct termp *,
 				const struct roffsu *);
-static	size_t		  ascii_width(const struct termp *, int);
+static	size_t		  ascii_getwidth(const struct termp *, int);
 static	void		  ascii_advance(struct termp *, size_t);
 static	void		  ascii_begin(struct termp *);
 static	void		  ascii_end(struct termp *);
 static	void		  ascii_endline(struct termp *);
 static	void		  ascii_letter(struct termp *, int);
 static	void		  ascii_setwidth(struct termp *, int, int);
 
 #if HAVE_WCHAR
 static	void		  locale_advance(struct termp *, size_t);
 static	void		  locale_endline(struct termp *);
 static	void		  locale_letter(struct termp *, int);
-static	size_t		  locale_width(const struct termp *, int);
+static	size_t		  locale_getwidth(const struct termp *, int);
 #endif
 
 
 static struct termp *
 ascii_init(enum termenc enc, const struct manoutput *outopts)
 {
 #if HAVE_WCHAR
 	char		*v;
 #endif
 	struct termp	*p;
 
 	p = mandoc_calloc(1, sizeof(*p));
 	p->tcol = p->tcols = mandoc_calloc(1, sizeof(*p->tcol));
 	p->maxtcol = 1;
 
 	p->line = 1;
 	p->defindent = 5;
-	p->defrmargin = p->lastrmargin = 78;
 	p->fontq = mandoc_reallocarray(NULL,
 	     (p->fontsz = 8), sizeof(*p->fontq));
 	p->fontq[0] = p->fontl = TERMFONT_NONE;
 
 	p->begin = ascii_begin;
 	p->end = ascii_end;
 	p->hspan = ascii_hspan;
 	p->type = TERMTYPE_CHAR;
-
 	p->enc = TERMENC_ASCII;
 	p->advance = ascii_advance;
 	p->endline = ascii_endline;
 	p->letter = ascii_letter;
 	p->setwidth = ascii_setwidth;
-	p->width = ascii_width;
+	p->getwidth = ascii_getwidth;
 
 #if HAVE_WCHAR
 	if (enc != TERMENC_ASCII) {
 
 		/*
 		 * Do not change any of this to LC_ALL.  It might break
 		 * the formatting by subtly changing the behaviour of
 		 * various functions, for example strftime(3).  As a
 		 * worst case, it might even cause buffer overflows.
 		 */
 
 		v = enc == TERMENC_LOCALE ?
 		    setlocale(LC_CTYPE, "") :
 		    setlocale(LC_CTYPE, UTF8_LOCALE);
 
 		/*
 		 * We only support UTF-8,
 		 * so revert to ASCII for anything else.
 		 */
 
 		if (v != NULL &&
 		    strcmp(nl_langinfo(CODESET), "UTF-8") != 0)
 			v = setlocale(LC_CTYPE, "C");
 
 		if (v != NULL && MB_CUR_MAX > 1) {
 			p->enc = TERMENC_UTF8;
 			p->advance = locale_advance;
 			p->endline = locale_endline;
 			p->letter = locale_letter;
-			p->width = locale_width;
+			p->getwidth = locale_getwidth;
 		}
 	}
 #endif
+	p->defrmargin = term_len(p, outopts->width ? outopts->width : 78);
+	p->lastrmargin = p->defrmargin;
 
-	if (outopts->mdoc)
-		p->mdocstyle = 1;
 	if (outopts->indent)
 		p->defindent = outopts->indent;
-	if (outopts->width)
-		p->defrmargin = outopts->width;
 	if (outopts->synopsisonly)
 		p->synopsisonly = 1;
 
 	assert(p->defindent < UINT16_MAX);
 	assert(p->defrmargin < UINT16_MAX);
 	return p;
 }
 
 void *
 ascii_alloc(const struct manoutput *outopts)
 {
-
 	return ascii_init(TERMENC_ASCII, outopts);
 }
 
 void *
 utf8_alloc(const struct manoutput *outopts)
 {
-
 	return ascii_init(TERMENC_UTF8, outopts);
 }
 
 void *
 locale_alloc(const struct manoutput *outopts)
 {
-
 	return ascii_init(TERMENC_LOCALE, outopts);
 }
 
 static void
 ascii_setwidth(struct termp *p, int iop, int width)
 {
-
-	width /= 24;
 	p->tcol->rmargin = p->defrmargin;
 	if (iop > 0)
 		p->defrmargin += width;
 	else if (iop == 0)
 		p->defrmargin = width ? (size_t)width : p->lastrmargin;
 	else if (p->defrmargin > (size_t)width)
 		p->defrmargin -= width;
 	else
 		p->defrmargin = 0;
-	if (p->defrmargin > 1000)
-		p->defrmargin = 1000;
+	if (p->defrmargin > term_len(p, 1000))
+		p->defrmargin = term_len(p, 1000);
 	p->lastrmargin = p->tcol->rmargin;
 	p->tcol->rmargin = p->maxrmargin = p->defrmargin;
 }
 
 void
 terminal_sepline(void *arg)
 {
 	struct termp	*p;
-	size_t		 i;
+	size_t		 i;	/* Printed width in basic units. */
+	size_t		 sz;	/* Width of a dash in basic units. */
 
 	p = (struct termp *)arg;
 	(*p->endline)(p);
-	for (i = 0; i < p->defrmargin; i++)
+	sz = (*p->getwidth)(p, '-');
+	for (i = 0; i < p->defrmargin; i += sz)
 		(*p->letter)(p, '-');
 	(*p->endline)(p);
 	(*p->endline)(p);
 }
 
 static size_t
-ascii_width(const struct termp *p, int c)
+ascii_getwidth(const struct termp *p, int c)
 {
-	return c != ASCII_BREAK && c != ASCII_NBRZW && c != ASCII_TABREF;
+	switch (c) {
+	case ASCII_BREAK:
+	case ASCII_NBRZW:
+	case ASCII_TABREF:
+		return 0;
+	default:
+		return 24;
+	}
 }
 
 void
 ascii_free(void *arg)
 {
-
 	term_free((struct termp *)arg);
 }
 
 static void
 ascii_letter(struct termp *p, int c)
 {
-
 	putchar(c);
 }
 
 static void
 ascii_begin(struct termp *p)
 {
-
 	(*p->headf)(p, p->argf);
 }
 
 static void
 ascii_end(struct termp *p)
 {
-
 	(*p->footf)(p, p->argf);
 }
 
 static void
 ascii_endline(struct termp *p)
 {
-
 	p->line++;
 	if ((int)p->tcol->offset > p->ti)
 		p->tcol->offset -= p->ti;
 	else
 		p->tcol->offset = 0;
 	p->ti = 0;
+	p->minbl = 0;
+	p->viscol = 0;
 	putchar('\n');
 }
 
 static void
 ascii_advance(struct termp *p, size_t len)
 {
-	size_t		i;
+	size_t		 dst;	/* Destination column in basic units. */
+	size_t		 sz;	/* Width of a space in basic units. */
+
+	sz = (*p->getwidth)(p, ' ');
 
 	/*
 	 * XXX We used to have "assert(len < UINT16_MAX)" here.
 	 * that is not quite right because the input document
 	 * can trigger that by merely providing large input.
 	 * For now, simply truncate.
 	 */
-	if (len > 256)
-		len = 256;
-	for (i = 0; i < len; i++)
+	if (len > 256 * sz)
+		len = 256 * sz;
+
+	dst = p->viscol + len;
+	while (p->viscol + sz / 2 < dst) {
 		putchar(' ');
+		p->viscol += sz;
+	}
 }
 
 static int
 ascii_hspan(const struct termp *p, const struct roffsu *su)
 {
 	double		 r;
 
 	switch (su->unit) {
 	case SCALE_BU:
 		r = su->scale;
 		break;
 	case SCALE_CM:
 		r = su->scale * 240.0 / 2.54;
 		break;
 	case SCALE_FS:
 		r = su->scale * 65536.0;
 		break;
 	case SCALE_IN:
 		r = su->scale * 240.0;
 		break;
 	case SCALE_MM:
 		r = su->scale * 0.24;
 		break;
 	case SCALE_VS:
 	case SCALE_PC:
 		r = su->scale * 40.0;
 		break;
 	case SCALE_PT:
 		r = su->scale * 10.0 / 3.0;
 		break;
 	case SCALE_EN:
 	case SCALE_EM:
 		r = su->scale * 24.0;
 		break;
 	default:
 		abort();
 	}
 	return r > 0.0 ? r + 0.01 : r - 0.01;
 }
 
 const char *
 ascii_uc2str(int uc)
 {
 	static const char nbrsp[2] = { ASCII_NBRSP, '\0' };
 	static const char *tab[] = {
 	"","","","","","","","",
 	"",	"\t",	"",	"",	"",	"",	"",	"",
 	"","","","","","","","",
 	"","",	"","","",	"",	"",	"",
 	" ",	"!",	"\"",	"#",	"$",	"%",	"&",	"'",
 	"(",	")",	"*",	"+",	",",	"-",	".",	"/",
 	"0",	"1",	"2",	"3",	"4",	"5",	"6",	"7",
 	"8",	"9",	":",	";",	"<",	"=",	">",	"?",
 	"@",	"A",	"B",	"C",	"D",	"E",	"F",	"G",
 	"H",	"I",	"J",	"K",	"L",	"M",	"N",	"O",
 	"P",	"Q",	"R",	"S",	"T",	"U",	"V",	"W",
 	"X",	"Y",	"Z",	"[",	"\\",	"]",	"^",	"_",
 	"`",	"a",	"b",	"c",	"d",	"e",	"f",	"g",
 	"h",	"i",	"j",	"k",	"l",	"m",	"n",	"o",
 	"p",	"q",	"r",	"s",	"t",	"u",	"v",	"w",
 	"x",	"y",	"z",	"{",	"|",	"}",	"~",	"",
 	"<80>",	"<81>",	"<82>",	"<83>",	"<84>",	"<85>",	"<86>",	"<87>",
 	"<88>",	"<89>",	"<8A>",	"<8B>",	"<8C>",	"<8D>",	"<8E>",	"<8F>",
 	"<90>",	"<91>",	"<92>",	"<93>",	"<94>",	"<95>",	"<96>",	"<97>",
 	"<98>",	"<99>",	"<9A>",	"<9B>",	"<9C>",	"<9D>",	"<9E>",	"<9F>",
 	nbrsp,	"!",	"/\bc",	"-\bL",	"o\bx",	"=\bY",	"|",	"
", "\"", "(C)", "_\ba", "<<", "~", "", "(R)", "-", "","+-","^2", "^3", "'","","",".", ",", "^1", "_\bo", ">>", "1/4", "1/2", "3/4", "?", "`\bA", "'\bA", "^\bA", "~\bA", "\"\bA","o\bA", "AE", ",\bC", "`\bE", "'\bE", "^\bE", "\"\bE","`\bI", "'\bI", "^\bI", "\"\bI", "Dh", "~\bN", "`\bO", "'\bO", "^\bO", "~\bO", "\"\bO","x", "/\bO", "`\bU", "'\bU", "^\bU", "\"\bU","'\bY", "Th", "ss", "`\ba", "'\ba", "^\ba", "~\ba", "\"\ba","o\ba", "ae", ",\bc", "`\be", "'\be", "^\be", "\"\be","`\bi", "'\bi", "^\bi", "\"\bi", "dh", "~\bn", "`\bo", "'\bo", "^\bo", "~\bo", "\"\bo","/", "/\bo", "`\bu", "'\bu", "^\bu", "\"\bu","'\by", "th", "\"\by", "A", "a", "A", "a", "A", "a", "'\bC", "'\bc", "^\bC", "^\bc", "C", "c", "C", "c", "D", "d", "/\bD", "/\bd", "E", "e", "E", "e", "E", "e", "E", "e", "E", "e", "^\bG", "^\bg", "G", "g", "G", "g", ",\bG", ",\bg", "^\bH", "^\bh", "/\bH", "/\bh", "~\bI", "~\bi", "I", "i", "I", "i", "I", "i", "I", "i", "IJ", "ij", "^\bJ", "^\bj", ",\bK", ",\bk", "q", "'\bL", "'\bl", ",\bL", ",\bl", "L", "l", "L", "l", "/\bL", "/\bl", "'\bN", "'\bn", ",\bN", ",\bn", "N", "n", "'n", "Ng", "ng", "O", "o", "O", "o", "O", "o", "OE", "oe", "'\bR", "'\br", ",\bR", ",\br", "R", "r", "'\bS", "'\bs", "^\bS", "^\bs", ",\bS", ",\bs", "S", "s", ",\bT", ",\bt", "T", "t", "/\bT", "/\bt", "~\bU", "~\bu", "U", "u", "U", "u", "U", "u", "U", "u", "U", "u", "^\bW", "^\bw", "^\bY", "^\by", "\"\bY","'\bZ", "'\bz", "Z", "z", "Z", "z", "s", "b", "B", "B", "b", "6", "6", "O", "C", "c", "D", "D", "D", "d", "d", "3", "@", "E", "F", ",\bf", "G", "G", "hv", "I", "/\bI", "K", "k", "/\bl", "l", "W", "N", "n", "~\bO", "O", "o", "OI", "oi", "P", "p", "YR", "2", "2", "SH", "sh", "t", "T", "t", "T", "U", "u", "Y", "V", "Y", "y", "/\bZ", "/\bz", "ZH", "ZH", "zh", "zh", "/\b2", "5", "5", "ts", "w", "|", "||", "|=", "!", "DZ", "Dz", "dz", "LJ", "Lj", "lj", "NJ", "Nj", "nj", "A", "a", "I", "i", "O", "o", "U", "u", "U", "u", "U", "u", "U", "u", "U", "u", "@", "A", "a", "A", "a", "AE", "ae", "/\bG", "/\bg", "G", "g", "K", "k", "O", "o", "O", "o", "ZH", "zh", "j", "DZ", "Dz", "dz", "'\bG", "'\bg", "HV", "W", "`\bN", "`\bn", "A", "a", "'\bAE","'\bae","O", "o"}; assert(uc >= 0); if ((size_t)uc < sizeof(tab)/sizeof(tab[0])) return tab[uc]; return mchars_uc2str(uc); } #if HAVE_WCHAR static size_t -locale_width(const struct termp *p, int c) +locale_getwidth(const struct termp *p, int c) { int rc; if (c == ASCII_NBRSP) c = ' '; rc = wcwidth(c); if (rc < 0) rc = 0; - return rc; + return rc * 24; } static void locale_advance(struct termp *p, size_t len) { - size_t i; + size_t dst; /* Destination column in basic units. */ + size_t sz; /* Width of a space in basic units. */ + + sz = (*p->getwidth)(p, ' '); /* * XXX We used to have "assert(len < UINT16_MAX)" here. * that is not quite right because the input document * can trigger that by merely providing large input. * For now, simply truncate. */ - if (len > 256) - len = 256; - for (i = 0; i < len; i++) + if (len > 256 * sz) + len = 256 * sz; + + dst = p->viscol + len; + while (p->viscol + sz / 2 < dst) { putwchar(L' '); + p->viscol += sz; + } } static void locale_endline(struct termp *p) { - p->line++; if ((int)p->tcol->offset > p->ti) p->tcol->offset -= p->ti; else p->tcol->offset = 0; p->ti = 0; + p->minbl = 0; + p->viscol = 0; putwchar(L'\n'); } static void locale_letter(struct termp *p, int c) { - putwchar(c); } #endif diff --git a/contrib/mandoc/term_ps.c b/contrib/mandoc/term_ps.c index 374d3d9a6abd..4c6368ca1d1f 100644 --- a/contrib/mandoc/term_ps.c +++ b/contrib/mandoc/term_ps.c @@ -1,1362 +1,1364 @@ -/* $Id: term_ps.c,v 1.92 2020/09/06 14:45:22 schwarze Exp $ */ +/* $Id: term_ps.c,v 1.94 2025/07/18 15:47:18 schwarze Exp $ */ /* + * Copyright (c) 2014-2017, 2020, 2025 Ingo Schwarze * Copyright (c) 2010, 2011 Kristaps Dzonsons - * Copyright (c) 2014,2015,2016,2017,2020 Ingo Schwarze * Copyright (c) 2017 Marc Espie * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "config.h" #include #include #if HAVE_ERR #include #endif #include #include #include #include #include #include #include "mandoc_aux.h" #include "out.h" #include "term.h" #include "manconf.h" #include "main.h" /* These work the buffer used by the header and footer. */ #define PS_BUFSLOP 128 /* Convert PostScript point "x" to an AFM unit. */ #define PNT2AFM(p, x) \ (size_t)((double)(x) * (1000.0 / (double)(p)->ps->scale)) /* Convert an AFM unit "x" to a PostScript points */ #define AFM2PNT(p, x) \ ((double)(x) / (1000.0 / (double)(p)->ps->scale)) struct glyph { unsigned short wx; /* WX in AFM */ }; struct font { const char *name; /* FontName in AFM */ #define MAXCHAR 95 /* total characters we can handle */ struct glyph gly[MAXCHAR]; /* glyph metrics */ }; struct termp_ps { int flags; #define PS_INLINE (1 << 0) /* we're in a word */ #define PS_MARGINS (1 << 1) /* we're in the margins */ #define PS_NEWPAGE (1 << 2) /* new page, no words yet */ #define PS_BACKSP (1 << 3) /* last character was backspace */ size_t pscol; /* visible column (AFM units) */ size_t pscolnext; /* used for overstrike */ size_t psrow; /* visible row (AFM units) */ size_t lastrow; /* psrow of the previous word */ char *psmarg; /* margin buf */ size_t psmargsz; /* margin buf size */ size_t psmargcur; /* cur index in margin buf */ char last; /* last non-backspace seen */ enum termfont lastf; /* last set font */ enum termfont nextf; /* building next font here */ size_t scale; /* font scaling factor */ size_t pages; /* number of pages shown */ size_t lineheight; /* line height (AFM units) */ size_t top; /* body top (AFM units) */ size_t bottom; /* body bottom (AFM units) */ const char *medianame; /* for DocumentMedia and PageSize */ size_t height; /* page height (AFM units */ size_t width; /* page width (AFM units) */ size_t lastwidth; /* page width before last ll */ size_t left; /* body left (AFM units) */ size_t header; /* header pos (AFM units) */ size_t footer; /* footer pos (AFM units) */ size_t pdfbytes; /* current output byte */ size_t pdflastpg; /* byte of last page mark */ size_t pdfbody; /* start of body object */ size_t *pdfobjs; /* table of object offsets */ size_t pdfobjsz; /* size of pdfobjs */ }; static int ps_hspan(const struct termp *, const struct roffsu *); -static size_t ps_width(const struct termp *, int); +static size_t ps_getwidth(const struct termp *, int); static void ps_advance(struct termp *, size_t); static void ps_begin(struct termp *); static void ps_closepage(struct termp *); static void ps_end(struct termp *); static void ps_endline(struct termp *); static void ps_growbuf(struct termp *, size_t); static void ps_letter(struct termp *, int); static void ps_pclose(struct termp *); static void ps_plast(struct termp *); static void ps_pletter(struct termp *, int); static void ps_printf(struct termp *, const char *, ...) __attribute__((__format__ (__printf__, 2, 3))); static void ps_putchar(struct termp *, char); static void ps_setfont(struct termp *, enum termfont); static void ps_setwidth(struct termp *, int, int); static struct termp *pspdf_alloc(const struct manoutput *, enum termtype); static void pdf_obj(struct termp *, size_t); /* * We define, for the time being, three fonts: bold, oblique/italic, and * normal (roman). The following table hard-codes the font metrics for * ASCII, i.e., 32--127. */ static const struct font fonts[TERMFONT__MAX] = { { "Times-Roman", { { 250 }, { 333 }, { 408 }, { 500 }, { 500 }, { 833 }, { 778 }, { 333 }, { 333 }, { 333 }, { 500 }, { 564 }, { 250 }, { 333 }, { 250 }, { 278 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 278 }, { 278 }, { 564 }, { 564 }, { 564 }, { 444 }, { 921 }, { 722 }, { 667 }, { 667 }, { 722 }, { 611 }, { 556 }, { 722 }, { 722 }, { 333 }, { 389 }, { 722 }, { 611 }, { 889 }, { 722 }, { 722 }, { 556 }, { 722 }, { 667 }, { 556 }, { 611 }, { 722 }, { 722 }, { 944 }, { 722 }, { 722 }, { 611 }, { 333 }, { 278 }, { 333 }, { 469 }, { 500 }, { 333 }, { 444 }, { 500 }, { 444 }, { 500}, { 444}, { 333}, { 500}, { 500}, { 278}, { 278}, { 500}, { 278}, { 778}, { 500}, { 500}, { 500}, { 500}, { 333}, { 389}, { 278}, { 500}, { 500}, { 722}, { 500}, { 500}, { 444}, { 480}, { 200}, { 480}, { 541}, } }, { "Times-Bold", { { 250 }, { 333 }, { 555 }, { 500 }, { 500 }, { 1000 }, { 833 }, { 333 }, { 333 }, { 333 }, { 500 }, { 570 }, { 250 }, { 333 }, { 250 }, { 278 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 333 }, { 333 }, { 570 }, { 570 }, { 570 }, { 500 }, { 930 }, { 722 }, { 667 }, { 722 }, { 722 }, { 667 }, { 611 }, { 778 }, { 778 }, { 389 }, { 500 }, { 778 }, { 667 }, { 944 }, { 722 }, { 778 }, { 611 }, { 778 }, { 722 }, { 556 }, { 667 }, { 722 }, { 722 }, { 1000 }, { 722 }, { 722 }, { 667 }, { 333 }, { 278 }, { 333 }, { 581 }, { 500 }, { 333 }, { 500 }, { 556 }, { 444 }, { 556 }, { 444 }, { 333 }, { 500 }, { 556 }, { 278 }, { 333 }, { 556 }, { 278 }, { 833 }, { 556 }, { 500 }, { 556 }, { 556 }, { 444 }, { 389 }, { 333 }, { 556 }, { 500 }, { 722 }, { 500 }, { 500 }, { 444 }, { 394 }, { 220 }, { 394 }, { 520 }, } }, { "Times-Italic", { { 250 }, { 333 }, { 420 }, { 500 }, { 500 }, { 833 }, { 778 }, { 333 }, { 333 }, { 333 }, { 500 }, { 675 }, { 250 }, { 333 }, { 250 }, { 278 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 333 }, { 333 }, { 675 }, { 675 }, { 675 }, { 500 }, { 920 }, { 611 }, { 611 }, { 667 }, { 722 }, { 611 }, { 611 }, { 722 }, { 722 }, { 333 }, { 444 }, { 667 }, { 556 }, { 833 }, { 667 }, { 722 }, { 611 }, { 722 }, { 611 }, { 500 }, { 556 }, { 722 }, { 611 }, { 833 }, { 611 }, { 556 }, { 556 }, { 389 }, { 278 }, { 389 }, { 422 }, { 500 }, { 333 }, { 500 }, { 500 }, { 444 }, { 500 }, { 444 }, { 278 }, { 500 }, { 500 }, { 278 }, { 278 }, { 444 }, { 278 }, { 722 }, { 500 }, { 500 }, { 500 }, { 500 }, { 389 }, { 389 }, { 278 }, { 500 }, { 444 }, { 667 }, { 444 }, { 444 }, { 389 }, { 400 }, { 275 }, { 400 }, { 541 }, } }, { "Times-BoldItalic", { { 250 }, { 389 }, { 555 }, { 500 }, { 500 }, { 833 }, { 778 }, { 333 }, { 333 }, { 333 }, { 500 }, { 570 }, { 250 }, { 333 }, { 250 }, { 278 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 500 }, { 333 }, { 333 }, { 570 }, { 570 }, { 570 }, { 500 }, { 832 }, { 667 }, { 667 }, { 667 }, { 722 }, { 667 }, { 667 }, { 722 }, { 778 }, { 389 }, { 500 }, { 667 }, { 611 }, { 889 }, { 722 }, { 722 }, { 611 }, { 722 }, { 667 }, { 556 }, { 611 }, { 722 }, { 667 }, { 889 }, { 667 }, { 611 }, { 611 }, { 333 }, { 278 }, { 333 }, { 570 }, { 500 }, { 333 }, { 500 }, { 500 }, { 444 }, { 500 }, { 444 }, { 333 }, { 500 }, { 556 }, { 278 }, { 278 }, { 500 }, { 278 }, { 778 }, { 556 }, { 500 }, { 500 }, { 500 }, { 389 }, { 389 }, { 278 }, { 556 }, { 444 }, { 667 }, { 500 }, { 444 }, { 389 }, { 348 }, { 220 }, { 348 }, { 570 }, } }, }; void * pdf_alloc(const struct manoutput *outopts) { return pspdf_alloc(outopts, TERMTYPE_PDF); } void * ps_alloc(const struct manoutput *outopts) { return pspdf_alloc(outopts, TERMTYPE_PS); } static struct termp * pspdf_alloc(const struct manoutput *outopts, enum termtype type) { struct termp *p; unsigned int pagex, pagey; size_t marginx, marginy, lineheight; const char *pp; p = mandoc_calloc(1, sizeof(*p)); p->tcol = p->tcols = mandoc_calloc(1, sizeof(*p->tcol)); p->maxtcol = 1; p->type = type; p->enc = TERMENC_ASCII; p->fontq = mandoc_reallocarray(NULL, (p->fontsz = 8), sizeof(*p->fontq)); p->fontq[0] = p->fontl = TERMFONT_NONE; p->ps = mandoc_calloc(1, sizeof(*p->ps)); p->advance = ps_advance; p->begin = ps_begin; p->end = ps_end; p->endline = ps_endline; p->hspan = ps_hspan; p->letter = ps_letter; p->setwidth = ps_setwidth; - p->width = ps_width; + p->getwidth = ps_getwidth; /* Default to US letter (millimetres). */ p->ps->medianame = "Letter"; pagex = 216; pagey = 279; /* * The ISO-269 paper sizes can be calculated automatically, but * it would require bringing in -lm for pow() and I'd rather not * do that. So just do it the easy way for now. Since this * only happens once, I'm not terribly concerned. */ pp = outopts->paper; if (pp != NULL && strcasecmp(pp, "letter") != 0) { if (strcasecmp(pp, "a3") == 0) { p->ps->medianame = "A3"; pagex = 297; pagey = 420; } else if (strcasecmp(pp, "a4") == 0) { p->ps->medianame = "A4"; pagex = 210; pagey = 297; } else if (strcasecmp(pp, "a5") == 0) { p->ps->medianame = "A5"; pagex = 148; pagey = 210; } else if (strcasecmp(pp, "legal") == 0) { p->ps->medianame = "Legal"; pagex = 216; pagey = 356; } else if (sscanf(pp, "%ux%u", &pagex, &pagey) == 2) p->ps->medianame = "CustomSize"; else warnx("%s: Unknown paper", pp); } /* * This MUST be defined before any PNT2AFM or AFM2PNT * calculations occur. */ p->ps->scale = 11; /* Remember millimetres -> AFM units. */ pagex = PNT2AFM(p, ((double)pagex * 72.0 / 25.4)); pagey = PNT2AFM(p, ((double)pagey * 72.0 / 25.4)); /* Margins are 1/9 the page x and y. */ marginx = (size_t)((double)pagex / 9.0); marginy = (size_t)((double)pagey / 9.0); /* Line-height is 1.4em. */ lineheight = PNT2AFM(p, ((double)p->ps->scale * 1.4)); p->ps->width = p->ps->lastwidth = (size_t)pagex; p->ps->height = (size_t)pagey; p->ps->header = pagey - (marginy / 2) - (lineheight / 2); p->ps->top = pagey - marginy; p->ps->footer = (marginy / 2) - (lineheight / 2); p->ps->bottom = marginy; p->ps->left = marginx; p->ps->lineheight = lineheight; p->defrmargin = pagex - (marginx * 2); return p; } static void ps_setwidth(struct termp *p, int iop, int width) { size_t lastwidth; lastwidth = p->ps->width; if (iop > 0) p->ps->width += width; else if (iop == 0) p->ps->width = width ? (size_t)width : p->ps->lastwidth; else if (p->ps->width > (size_t)width) p->ps->width -= width; else p->ps->width = 0; p->ps->lastwidth = lastwidth; } void pspdf_free(void *arg) { struct termp *p; p = (struct termp *)arg; free(p->ps->psmarg); free(p->ps->pdfobjs); free(p->ps); term_free(p); } static void ps_printf(struct termp *p, const char *fmt, ...) { va_list ap; int pos, len; va_start(ap, fmt); /* * If we're running in regular mode, then pipe directly into * vprintf(). If we're processing margins, then push the data * into our growable margin buffer. */ if ( ! (PS_MARGINS & p->ps->flags)) { len = vprintf(fmt, ap); va_end(ap); p->ps->pdfbytes += len < 0 ? 0 : (size_t)len; return; } /* * XXX: I assume that the in-margin print won't exceed * PS_BUFSLOP (128 bytes), which is reasonable but still an * assumption that will cause pukeage if it's not the case. */ ps_growbuf(p, PS_BUFSLOP); pos = (int)p->ps->psmargcur; vsnprintf(&p->ps->psmarg[pos], PS_BUFSLOP, fmt, ap); va_end(ap); p->ps->psmargcur = strlen(p->ps->psmarg); } static void ps_putchar(struct termp *p, char c) { int pos; /* See ps_printf(). */ if ( ! (PS_MARGINS & p->ps->flags)) { putchar(c); p->ps->pdfbytes++; return; } ps_growbuf(p, 2); pos = (int)p->ps->psmargcur++; p->ps->psmarg[pos++] = c; p->ps->psmarg[pos] = '\0'; } static void pdf_obj(struct termp *p, size_t obj) { assert(obj > 0); if ((obj - 1) >= p->ps->pdfobjsz) { p->ps->pdfobjsz = obj + 128; p->ps->pdfobjs = mandoc_reallocarray(p->ps->pdfobjs, p->ps->pdfobjsz, sizeof(size_t)); } p->ps->pdfobjs[(int)obj - 1] = p->ps->pdfbytes; ps_printf(p, "%zu 0 obj\n", obj); } static void ps_closepage(struct termp *p) { int i; size_t len, base; /* * Close out a page that we've already flushed to output. In * PostScript, we simply note that the page must be shown. In * PDF, we must now create the Length, Resource, and Page node * for the page contents. */ assert(p->ps->psmarg && p->ps->psmarg[0]); ps_printf(p, "%s", p->ps->psmarg); if (TERMTYPE_PS != p->type) { len = p->ps->pdfbytes - p->ps->pdflastpg; base = p->ps->pages * 4 + p->ps->pdfbody; ps_printf(p, "endstream\nendobj\n"); /* Length of content. */ pdf_obj(p, base + 1); ps_printf(p, "%zu\nendobj\n", len); /* Resource for content. */ pdf_obj(p, base + 2); ps_printf(p, "<<\n/ProcSet [/PDF /Text]\n"); ps_printf(p, "/Font <<\n"); for (i = 0; i < (int)TERMFONT__MAX; i++) ps_printf(p, "/F%d %d 0 R\n", i, 3 + i); ps_printf(p, ">>\n>>\nendobj\n"); /* Page node. */ pdf_obj(p, base + 3); ps_printf(p, "<<\n"); ps_printf(p, "/Type /Page\n"); ps_printf(p, "/Parent 2 0 R\n"); ps_printf(p, "/Resources %zu 0 R\n", base + 2); ps_printf(p, "/Contents %zu 0 R\n", base); ps_printf(p, ">>\nendobj\n"); } else ps_printf(p, "showpage\n"); p->ps->pages++; p->ps->psrow = p->ps->top; assert( ! (PS_NEWPAGE & p->ps->flags)); p->ps->flags |= PS_NEWPAGE; } static void ps_end(struct termp *p) { size_t i, xref, base; ps_plast(p); ps_pclose(p); /* * At the end of the file, do one last showpage. This is the * same behaviour as groff(1) and works for multiple pages as * well as just one. */ if ( ! (PS_NEWPAGE & p->ps->flags)) { assert(0 == p->ps->flags); assert('\0' == p->ps->last); ps_closepage(p); } if (TERMTYPE_PS == p->type) { ps_printf(p, "%%%%Trailer\n"); ps_printf(p, "%%%%Pages: %zu\n", p->ps->pages); ps_printf(p, "%%%%EOF\n"); return; } pdf_obj(p, 2); ps_printf(p, "<<\n/Type /Pages\n"); ps_printf(p, "/MediaBox [0 0 %zu %zu]\n", (size_t)AFM2PNT(p, p->ps->width), (size_t)AFM2PNT(p, p->ps->height)); ps_printf(p, "/Count %zu\n", p->ps->pages); ps_printf(p, "/Kids ["); for (i = 0; i < p->ps->pages; i++) ps_printf(p, " %zu 0 R", i * 4 + p->ps->pdfbody + 3); base = (p->ps->pages - 1) * 4 + p->ps->pdfbody + 4; ps_printf(p, "]\n>>\nendobj\n"); pdf_obj(p, base); ps_printf(p, "<<\n"); ps_printf(p, "/Type /Catalog\n"); ps_printf(p, "/Pages 2 0 R\n"); ps_printf(p, ">>\nendobj\n"); xref = p->ps->pdfbytes; ps_printf(p, "xref\n"); ps_printf(p, "0 %zu\n", base + 1); ps_printf(p, "0000000000 65535 f \n"); for (i = 0; i < base; i++) ps_printf(p, "%.10zu 00000 n \n", p->ps->pdfobjs[(int)i]); ps_printf(p, "trailer\n"); ps_printf(p, "<<\n"); ps_printf(p, "/Size %zu\n", base + 1); ps_printf(p, "/Root %zu 0 R\n", base); ps_printf(p, "/Info 1 0 R\n"); ps_printf(p, ">>\n"); ps_printf(p, "startxref\n"); ps_printf(p, "%zu\n", xref); ps_printf(p, "%%%%EOF\n"); } static void ps_begin(struct termp *p) { size_t width, height; int i; /* * Print margins into margin buffer. Nothing gets output to the * screen yet, so we don't need to initialise the primary state. */ if (p->ps->psmarg) { assert(p->ps->psmargsz); p->ps->psmarg[0] = '\0'; } /*p->ps->pdfbytes = 0;*/ p->ps->psmargcur = 0; p->ps->flags = PS_MARGINS; p->ps->pscol = p->ps->left; p->ps->psrow = p->ps->header; p->ps->lastrow = 0; /* impossible row */ ps_setfont(p, TERMFONT_NONE); (*p->headf)(p, p->argf); (*p->endline)(p); p->ps->pscol = p->ps->left; p->ps->psrow = p->ps->footer; (*p->footf)(p, p->argf); (*p->endline)(p); p->ps->flags &= ~PS_MARGINS; assert(0 == p->ps->flags); assert(p->ps->psmarg); assert('\0' != p->ps->psmarg[0]); /* * Print header and initialise page state. Following this, * stuff gets printed to the screen, so make sure we're sane. */ if (TERMTYPE_PS == p->type) { width = AFM2PNT(p, p->ps->width); height = AFM2PNT(p, p->ps->height); ps_printf(p, "%%!PS-Adobe-3.0\n"); ps_printf(p, "%%%%DocumentData: Clean7Bit\n"); ps_printf(p, "%%%%Orientation: Portrait\n"); ps_printf(p, "%%%%Pages: (atend)\n"); ps_printf(p, "%%%%PageOrder: Ascend\n"); ps_printf(p, "%%%%DocumentMedia: man-%s %zu %zu 0 () ()\n", p->ps->medianame, width, height); ps_printf(p, "%%%%DocumentNeededResources: font"); for (i = 0; i < (int)TERMFONT__MAX; i++) ps_printf(p, " %s", fonts[i].name); ps_printf(p, "\n%%%%DocumentSuppliedResources: " "procset MandocProcs 1.0 0\n"); ps_printf(p, "%%%%EndComments\n"); ps_printf(p, "%%%%BeginProlog\n"); ps_printf(p, "%%%%BeginResource: procset MandocProcs " "10170 10170\n"); /* The font size is effectively hard-coded for now. */ ps_printf(p, "/fs %zu def\n", p->ps->scale); for (i = 0; i < (int)TERMFONT__MAX; i++) ps_printf(p, "/f%d { /%s fs selectfont } def\n", i, fonts[i].name); ps_printf(p, "/s { 3 1 roll moveto show } bind def\n"); ps_printf(p, "/c { exch currentpoint exch pop " "moveto show } bind def\n"); ps_printf(p, "%%%%EndResource\n"); ps_printf(p, "%%%%EndProlog\n"); ps_printf(p, "%%%%BeginSetup\n"); ps_printf(p, "%%%%BeginFeature: *PageSize %s\n", p->ps->medianame); ps_printf(p, "<>setpagedevice\n", width, height); ps_printf(p, "%%%%EndFeature\n"); ps_printf(p, "%%%%EndSetup\n"); } else { ps_printf(p, "%%PDF-1.1\n"); pdf_obj(p, 1); ps_printf(p, "<<\n"); ps_printf(p, ">>\n"); ps_printf(p, "endobj\n"); for (i = 0; i < (int)TERMFONT__MAX; i++) { pdf_obj(p, (size_t)i + 3); ps_printf(p, "<<\n"); ps_printf(p, "/Type /Font\n"); ps_printf(p, "/Subtype /Type1\n"); ps_printf(p, "/Name /F%d\n", i); ps_printf(p, "/BaseFont /%s\n", fonts[i].name); ps_printf(p, ">>\nendobj\n"); } } p->ps->pdfbody = (size_t)TERMFONT__MAX + 3; p->ps->pscol = p->ps->left; p->ps->psrow = p->ps->top; p->ps->flags |= PS_NEWPAGE; ps_setfont(p, TERMFONT_NONE); } static void ps_pletter(struct termp *p, int c) { int f; /* * If we haven't opened a page context, then output that we're * in a new page and make sure the font is correctly set. */ if (PS_NEWPAGE & p->ps->flags) { if (TERMTYPE_PS == p->type) { ps_printf(p, "%%%%Page: %zu %zu\n", p->ps->pages + 1, p->ps->pages + 1); ps_printf(p, "f%d\n", (int)p->ps->lastf); } else { pdf_obj(p, p->ps->pdfbody + p->ps->pages * 4); ps_printf(p, "<<\n"); ps_printf(p, "/Length %zu 0 R\n", p->ps->pdfbody + 1 + p->ps->pages * 4); ps_printf(p, ">>\nstream\n"); } p->ps->pdflastpg = p->ps->pdfbytes; p->ps->flags &= ~PS_NEWPAGE; } /* * If we're not in a PostScript "word" context, then open one * now at the current cursor. */ if ( ! (PS_INLINE & p->ps->flags)) { if (TERMTYPE_PS != p->type) { ps_printf(p, "BT\n/F%d %zu Tf\n", (int)p->ps->lastf, p->ps->scale); ps_printf(p, "%.3f %.3f Td\n(", AFM2PNT(p, p->ps->pscol), AFM2PNT(p, p->ps->psrow)); } else { ps_printf(p, "%.3f", AFM2PNT(p, p->ps->pscol)); if (p->ps->psrow != p->ps->lastrow) ps_printf(p, " %.3f", AFM2PNT(p, p->ps->psrow)); ps_printf(p, "("); } p->ps->flags |= PS_INLINE; } assert( ! (PS_NEWPAGE & p->ps->flags)); /* * We need to escape these characters as per the PostScript * specification. We would also escape non-graphable characters * (like tabs), but none of them would get to this point and * it's superfluous to abort() on them. */ switch (c) { case '(': case ')': case '\\': ps_putchar(p, '\\'); break; default: break; } /* Write the character and adjust where we are on the page. */ f = (int)p->ps->lastf; if (c <= 32 || c - 32 >= MAXCHAR) c = 32; ps_putchar(p, (char)c); c -= 32; p->ps->pscol += (size_t)fonts[f].gly[c].wx; } static void ps_pclose(struct termp *p) { /* * Spit out that we're exiting a word context (this is a * "partial close" because we don't check the last-char buffer * or anything). */ if ( ! (PS_INLINE & p->ps->flags)) return; if (TERMTYPE_PS != p->type) ps_printf(p, ") Tj\nET\n"); else if (p->ps->psrow == p->ps->lastrow) ps_printf(p, ")c\n"); else { ps_printf(p, ")s\n"); p->ps->lastrow = p->ps->psrow; } p->ps->flags &= ~PS_INLINE; } /* If we have a `last' char that wasn't printed yet, print it now. */ static void ps_plast(struct termp *p) { size_t wx; if (p->ps->last == '\0') return; /* Check the font mode; open a new scope if it doesn't match. */ if (p->ps->nextf != p->ps->lastf) { ps_pclose(p); ps_setfont(p, p->ps->nextf); } p->ps->nextf = TERMFONT_NONE; /* * For an overstrike, if a previous character * was wider, advance to center the new one. */ if (p->ps->pscolnext) { wx = fonts[p->ps->lastf].gly[(int)p->ps->last-32].wx; if (p->ps->pscol + wx < p->ps->pscolnext) p->ps->pscol = (p->ps->pscol + p->ps->pscolnext - wx) / 2; } ps_pletter(p, p->ps->last); p->ps->last = '\0'; /* * For an overstrike, if a previous character * was wider, advance to the end of the old one. */ if (p->ps->pscol < p->ps->pscolnext) { ps_pclose(p); p->ps->pscol = p->ps->pscolnext; } } static void ps_letter(struct termp *p, int arg) { size_t savecol; char c; c = arg >= 128 || arg <= 0 ? '?' : arg; /* * When receiving a backspace, merely flag it. * We don't know yet whether it is * a font instruction or an overstrike. */ if (c == '\b') { assert(p->ps->last != '\0'); assert( ! (p->ps->flags & PS_BACKSP)); p->ps->flags |= PS_BACKSP; return; } /* * Decode font instructions. */ if (p->ps->flags & PS_BACKSP) { if (p->ps->last == '_') { switch (p->ps->nextf) { case TERMFONT_BI: break; case TERMFONT_BOLD: p->ps->nextf = TERMFONT_BI; break; default: p->ps->nextf = TERMFONT_UNDER; } p->ps->last = c; p->ps->flags &= ~PS_BACKSP; return; } if (p->ps->last == c) { switch (p->ps->nextf) { case TERMFONT_BI: break; case TERMFONT_UNDER: p->ps->nextf = TERMFONT_BI; break; default: p->ps->nextf = TERMFONT_BOLD; } p->ps->flags &= ~PS_BACKSP; return; } /* * This is not a font instruction, but rather * the next character. Prepare for overstrike. */ savecol = p->ps->pscol; } else savecol = SIZE_MAX; /* * We found the next character, so the font instructions * for the previous one are complete. * Use them and print it. */ ps_plast(p); /* * Do not print the current character yet because font * instructions might follow; only remember the character. * It will get printed later from ps_plast(). */ p->ps->last = c; /* * For an overstrike, back up to the previous position. * If the previous character is wider than any it overstrikes, * remember the current position, because it might also be * wider than all that will overstrike it. */ if (savecol != SIZE_MAX) { if (p->ps->pscolnext < p->ps->pscol) p->ps->pscolnext = p->ps->pscol; ps_pclose(p); p->ps->pscol = savecol; p->ps->flags &= ~PS_BACKSP; } else p->ps->pscolnext = 0; } static void ps_advance(struct termp *p, size_t len) { /* * Advance some spaces. This can probably be made smarter, * i.e., to have multiple space-separated words in the same * scope, but this is easier: just close out the current scope * and readjust our column settings. */ ps_plast(p); ps_pclose(p); p->ps->pscol += len; + p->viscol += len; } static void ps_endline(struct termp *p) { /* Close out any scopes we have open: we're at eoln. */ ps_plast(p); ps_pclose(p); /* * If we're in the margin, don't try to recalculate our current * row. XXX: if the column tries to be fancy with multiple * lines, we'll do nasty stuff. */ if (PS_MARGINS & p->ps->flags) return; /* Left-justify. */ p->ps->pscol = p->ps->left; + p->viscol = 0; + p->minbl = 0; /* If we haven't printed anything, return. */ if (PS_NEWPAGE & p->ps->flags) return; /* * Put us down a line. If we're at the page bottom, spit out a * showpage and restart our row. */ if (p->ps->psrow >= p->ps->lineheight + p->ps->bottom) { p->ps->psrow -= p->ps->lineheight; return; } ps_closepage(p); if ((int)p->tcol->offset > p->ti) p->tcol->offset -= p->ti; else p->tcol->offset = 0; p->ti = 0; } static void ps_setfont(struct termp *p, enum termfont f) { assert(f < TERMFONT__MAX); p->ps->lastf = f; /* * If we're still at the top of the page, let the font-setting * be delayed until we actually have stuff to print. */ if (PS_NEWPAGE & p->ps->flags) return; if (TERMTYPE_PS == p->type) ps_printf(p, "f%d\n", (int)f); else ps_printf(p, "/F%d %zu Tf\n", (int)f, p->ps->scale); } static size_t -ps_width(const struct termp *p, int c) +ps_getwidth(const struct termp *p, int c) { if (c <= 32 || c - 32 >= MAXCHAR) c = 0; else c -= 32; return (size_t)fonts[(int)TERMFONT_NONE].gly[c].wx; } static int ps_hspan(const struct termp *p, const struct roffsu *su) { double r; /* * All of these measurements are derived by converting from the * native measurement to AFM units. */ switch (su->unit) { case SCALE_BU: /* * Traditionally, the default unit is fixed to the * output media. So this would refer to the point. In * mandoc(1), however, we stick to the default terminal * scaling unit so that output is the same regardless * the media. */ - r = PNT2AFM(p, su->scale * 72.0 / 240.0); + r = PNT2AFM(p, su->scale * 72.0 / 10.0); break; case SCALE_CM: r = PNT2AFM(p, su->scale * 72.0 / 2.54); break; case SCALE_EM: r = su->scale * fonts[(int)TERMFONT_NONE].gly[109 - 32].wx; break; case SCALE_EN: r = su->scale * fonts[(int)TERMFONT_NONE].gly[110 - 32].wx; break; case SCALE_IN: r = PNT2AFM(p, su->scale * 72.0); break; case SCALE_MM: r = su->scale * fonts[(int)TERMFONT_NONE].gly[109 - 32].wx / 100.0; break; case SCALE_PC: r = PNT2AFM(p, su->scale * 12.0); break; case SCALE_PT: r = PNT2AFM(p, su->scale * 1.0); break; case SCALE_VS: r = su->scale * p->ps->lineheight; break; default: r = su->scale; break; } - - return r * 24.0; + return r; } static void ps_growbuf(struct termp *p, size_t sz) { if (p->ps->psmargcur + sz <= p->ps->psmargsz) return; if (sz < PS_BUFSLOP) sz = PS_BUFSLOP; p->ps->psmargsz += sz; p->ps->psmarg = mandoc_realloc(p->ps->psmarg, p->ps->psmargsz); } diff --git a/contrib/mandoc/term_tab.c b/contrib/mandoc/term_tab.c index a2d1074159b9..dd1b6bcdc696 100644 --- a/contrib/mandoc/term_tab.c +++ b/contrib/mandoc/term_tab.c @@ -1,140 +1,125 @@ -/* $Id: term_tab.c,v 1.7 2021/10/04 18:56:31 schwarze Exp $ */ +/* $Id: term_tab.c,v 1.9 2025/07/16 14:33:08 schwarze Exp $ */ /* - * Copyright (c) 2017, 2021 Ingo Schwarze + * Copyright (c) 2017, 2021, 2025 Ingo Schwarze * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "config.h" #include #include #include #include #include "mandoc_aux.h" #include "out.h" #include "term.h" struct tablist { - size_t *t; /* Allocated array of tab positions. */ + size_t *t; /* Allocated array of tab positions [BU]. */ size_t s; /* Allocated number of positions. */ size_t n; /* Currently used number of positions. */ }; static struct { struct tablist a; /* All tab positions for lookup. */ struct tablist p; /* Periodic tab positions to add. */ struct tablist *r; /* Tablist currently being recorded. */ - size_t d; /* Default tab width in units of n. */ + size_t d; /* Default tab width in basic units. */ } tabs; void term_tab_set(const struct termp *p, const char *arg) { struct roffsu su; struct tablist *tl; size_t pos; int add; /* Special arguments: clear all tabs or switch lists. */ if (arg == NULL) { tabs.a.n = tabs.p.n = 0; tabs.r = &tabs.a; if (tabs.d == 0) { a2roffsu(".8i", &su, SCALE_IN); - tabs.d = term_hen(p, &su); + tabs.d = term_hspan(p, &su); } return; } if (arg[0] == 'T' && arg[1] == '\0') { tabs.r = &tabs.p; return; } /* Parse the sign, the number, and the unit. */ if (*arg == '+') { add = 1; arg++; } else add = 0; if (a2roffsu(arg, &su, SCALE_EM) == NULL) return; /* Select the list, and extend it if it is full. */ tl = tabs.r; if (tl->n >= tl->s) { tl->s += 8; tl->t = mandoc_reallocarray(tl->t, tl->s, sizeof(*tl->t)); } /* Append the new position. */ - pos = term_hen(p, &su); + pos = term_hspan(p, &su); tl->t[tl->n] = pos; if (add && tl->n) tl->t[tl->n] += tl->t[tl->n - 1]; tl->n++; } -/* - * Simplified version without a parser, - * never incremental, never periodic, for use by tbl(7). - */ -void -term_tab_iset(size_t inc) -{ - if (tabs.a.n >= tabs.a.s) { - tabs.a.s += 8; - tabs.a.t = mandoc_reallocarray(tabs.a.t, tabs.a.s, - sizeof(*tabs.a.t)); - } - tabs.a.t[tabs.a.n++] = inc; -} - size_t term_tab_next(size_t prev) { size_t i, j; for (i = 0;; i++) { if (i == tabs.a.n) { if (tabs.p.n == 0) return prev; tabs.a.n += tabs.p.n; if (tabs.a.s < tabs.a.n) { tabs.a.s = tabs.a.n; tabs.a.t = mandoc_reallocarray(tabs.a.t, tabs.a.s, sizeof(*tabs.a.t)); } for (j = 0; j < tabs.p.n; j++) tabs.a.t[i + j] = tabs.p.t[j] + (i ? tabs.a.t[i - 1] : 0); } if (prev < tabs.a.t[i]) return tabs.a.t[i]; } } void term_tab_free(void) { free(tabs.a.t); free(tabs.p.t); memset(&tabs, 0, sizeof(tabs)); tabs.r = &tabs.a; }