From 5d69c6a2661bba0a22f3ecfd517e2e9767a38346 Mon Sep 17 00:00:00 2001 From: Cem Keylan Date: Fri, 16 Oct 2020 17:47:01 +0300 Subject: add tools --- usr.bin/mandoc/CVS/Entries | 101 + usr.bin/mandoc/CVS/Repository | 1 + usr.bin/mandoc/CVS/Root | 1 + usr.bin/mandoc/Makefile | 78 + usr.bin/mandoc/apropos.1 | 510 +++++ usr.bin/mandoc/arch.c | 52 + usr.bin/mandoc/att.c | 47 + usr.bin/mandoc/cgi.c | 1255 ++++++++++++ usr.bin/mandoc/cgi.h.example | 7 + usr.bin/mandoc/chars.c | 506 +++++ usr.bin/mandoc/dba.c | 501 +++++ usr.bin/mandoc/dba.h | 50 + usr.bin/mandoc/dba_array.c | 188 ++ usr.bin/mandoc/dba_array.h | 47 + usr.bin/mandoc/dba_read.c | 72 + usr.bin/mandoc/dba_write.c | 117 ++ usr.bin/mandoc/dba_write.h | 30 + usr.bin/mandoc/dbm.c | 474 +++++ usr.bin/mandoc/dbm.h | 68 + usr.bin/mandoc/dbm_map.c | 188 ++ usr.bin/mandoc/dbm_map.h | 29 + usr.bin/mandoc/eqn.c | 1130 +++++++++++ usr.bin/mandoc/eqn.h | 72 + usr.bin/mandoc/eqn_html.c | 244 +++ usr.bin/mandoc/eqn_parse.h | 48 + usr.bin/mandoc/eqn_term.c | 172 ++ usr.bin/mandoc/html.c | 1085 ++++++++++ usr.bin/mandoc/html.h | 141 ++ usr.bin/mandoc/libman.h | 42 + usr.bin/mandoc/libmandoc.h | 85 + usr.bin/mandoc/libmdoc.h | 86 + usr.bin/mandoc/main.c | 1255 ++++++++++++ usr.bin/mandoc/main.h | 53 + usr.bin/mandoc/makewhatis.8 | 228 +++ usr.bin/mandoc/man.1 | 431 ++++ usr.bin/mandoc/man.c | 343 ++++ usr.bin/mandoc/man.cgi.8 | 426 ++++ usr.bin/mandoc/man.conf.5 | 133 ++ usr.bin/mandoc/man.h | 21 + usr.bin/mandoc/man_html.c | 640 ++++++ usr.bin/mandoc/man_macro.c | 466 +++++ usr.bin/mandoc/man_term.c | 1149 +++++++++++ usr.bin/mandoc/man_validate.c | 656 ++++++ usr.bin/mandoc/manconf.h | 56 + usr.bin/mandoc/mandoc.1 | 2336 +++++++++++++++++++++ usr.bin/mandoc/mandoc.c | 657 ++++++ usr.bin/mandoc/mandoc.css | 360 ++++ usr.bin/mandoc/mandoc.h | 322 +++ usr.bin/mandoc/mandoc_aux.c | 113 ++ usr.bin/mandoc/mandoc_aux.h | 27 + usr.bin/mandoc/mandoc_msg.c | 368 ++++ usr.bin/mandoc/mandoc_ohash.c | 64 + usr.bin/mandoc/mandoc_ohash.h | 19 + usr.bin/mandoc/mandoc_parse.h | 44 + usr.bin/mandoc/mandoc_xr.c | 122 ++ usr.bin/mandoc/mandoc_xr.h | 31 + usr.bin/mandoc/mandocdb.c | 2386 ++++++++++++++++++++++ usr.bin/mandoc/manpath.c | 342 ++++ usr.bin/mandoc/mansearch.c | 842 ++++++++ usr.bin/mandoc/mansearch.h | 118 ++ usr.bin/mandoc/mdoc.c | 431 ++++ usr.bin/mandoc/mdoc.h | 158 ++ usr.bin/mandoc/mdoc_argv.c | 680 +++++++ usr.bin/mandoc/mdoc_html.c | 1758 ++++++++++++++++ usr.bin/mandoc/mdoc_macro.c | 1598 +++++++++++++++ usr.bin/mandoc/mdoc_man.c | 1836 +++++++++++++++++ usr.bin/mandoc/mdoc_markdown.c | 1606 +++++++++++++++ usr.bin/mandoc/mdoc_state.c | 254 +++ usr.bin/mandoc/mdoc_term.c | 1962 ++++++++++++++++++ usr.bin/mandoc/mdoc_validate.c | 3047 ++++++++++++++++++++++++++++ usr.bin/mandoc/msec.c | 35 + usr.bin/mandoc/msec.in | 34 + usr.bin/mandoc/out.c | 563 ++++++ usr.bin/mandoc/out.h | 70 + usr.bin/mandoc/preconv.c | 177 ++ usr.bin/mandoc/predefs.in | 65 + usr.bin/mandoc/read.c | 727 +++++++ usr.bin/mandoc/roff.c | 4372 ++++++++++++++++++++++++++++++++++++++++ usr.bin/mandoc/roff.h | 561 ++++++ usr.bin/mandoc/roff_html.c | 117 ++ usr.bin/mandoc/roff_int.h | 94 + usr.bin/mandoc/roff_term.c | 244 +++ usr.bin/mandoc/roff_validate.c | 149 ++ usr.bin/mandoc/st.c | 80 + usr.bin/mandoc/tag.c | 326 +++ usr.bin/mandoc/tag.h | 35 + usr.bin/mandoc/tbl.c | 181 ++ usr.bin/mandoc/tbl.h | 122 ++ usr.bin/mandoc/tbl_data.c | 300 +++ usr.bin/mandoc/tbl_html.c | 255 +++ usr.bin/mandoc/tbl_int.h | 47 + usr.bin/mandoc/tbl_layout.c | 371 ++++ usr.bin/mandoc/tbl_opts.c | 171 ++ usr.bin/mandoc/tbl_parse.h | 30 + usr.bin/mandoc/tbl_term.c | 943 +++++++++ usr.bin/mandoc/term.c | 1112 ++++++++++ usr.bin/mandoc/term.h | 158 ++ usr.bin/mandoc/term_ascii.c | 392 ++++ usr.bin/mandoc/term_ps.c | 1355 +++++++++++++ usr.bin/mandoc/term_tab.c | 128 ++ usr.bin/mandoc/term_tag.c | 199 ++ usr.bin/mandoc/term_tag.h | 34 + usr.bin/mandoc/tree.c | 514 +++++ 103 files changed, 48726 insertions(+) create mode 100644 usr.bin/mandoc/CVS/Entries create mode 100644 usr.bin/mandoc/CVS/Repository create mode 100644 usr.bin/mandoc/CVS/Root create mode 100644 usr.bin/mandoc/Makefile create mode 100644 usr.bin/mandoc/apropos.1 create mode 100644 usr.bin/mandoc/arch.c create mode 100644 usr.bin/mandoc/att.c create mode 100644 usr.bin/mandoc/cgi.c create mode 100644 usr.bin/mandoc/cgi.h.example create mode 100644 usr.bin/mandoc/chars.c create mode 100644 usr.bin/mandoc/dba.c create mode 100644 usr.bin/mandoc/dba.h create mode 100644 usr.bin/mandoc/dba_array.c create mode 100644 usr.bin/mandoc/dba_array.h create mode 100644 usr.bin/mandoc/dba_read.c create mode 100644 usr.bin/mandoc/dba_write.c create mode 100644 usr.bin/mandoc/dba_write.h create mode 100644 usr.bin/mandoc/dbm.c create mode 100644 usr.bin/mandoc/dbm.h create mode 100644 usr.bin/mandoc/dbm_map.c create mode 100644 usr.bin/mandoc/dbm_map.h create mode 100644 usr.bin/mandoc/eqn.c create mode 100644 usr.bin/mandoc/eqn.h create mode 100644 usr.bin/mandoc/eqn_html.c create mode 100644 usr.bin/mandoc/eqn_parse.h create mode 100644 usr.bin/mandoc/eqn_term.c create mode 100644 usr.bin/mandoc/html.c create mode 100644 usr.bin/mandoc/html.h create mode 100644 usr.bin/mandoc/libman.h create mode 100644 usr.bin/mandoc/libmandoc.h create mode 100644 usr.bin/mandoc/libmdoc.h create mode 100644 usr.bin/mandoc/main.c create mode 100644 usr.bin/mandoc/main.h create mode 100644 usr.bin/mandoc/makewhatis.8 create mode 100644 usr.bin/mandoc/man.1 create mode 100644 usr.bin/mandoc/man.c create mode 100644 usr.bin/mandoc/man.cgi.8 create mode 100644 usr.bin/mandoc/man.conf.5 create mode 100644 usr.bin/mandoc/man.h create mode 100644 usr.bin/mandoc/man_html.c create mode 100644 usr.bin/mandoc/man_macro.c create mode 100644 usr.bin/mandoc/man_term.c create mode 100644 usr.bin/mandoc/man_validate.c create mode 100644 usr.bin/mandoc/manconf.h create mode 100644 usr.bin/mandoc/mandoc.1 create mode 100644 usr.bin/mandoc/mandoc.c create mode 100644 usr.bin/mandoc/mandoc.css create mode 100644 usr.bin/mandoc/mandoc.h create mode 100644 usr.bin/mandoc/mandoc_aux.c create mode 100644 usr.bin/mandoc/mandoc_aux.h create mode 100644 usr.bin/mandoc/mandoc_msg.c create mode 100644 usr.bin/mandoc/mandoc_ohash.c create mode 100644 usr.bin/mandoc/mandoc_ohash.h create mode 100644 usr.bin/mandoc/mandoc_parse.h create mode 100644 usr.bin/mandoc/mandoc_xr.c create mode 100644 usr.bin/mandoc/mandoc_xr.h create mode 100644 usr.bin/mandoc/mandocdb.c create mode 100644 usr.bin/mandoc/manpath.c create mode 100644 usr.bin/mandoc/mansearch.c create mode 100644 usr.bin/mandoc/mansearch.h create mode 100644 usr.bin/mandoc/mdoc.c create mode 100644 usr.bin/mandoc/mdoc.h create mode 100644 usr.bin/mandoc/mdoc_argv.c create mode 100644 usr.bin/mandoc/mdoc_html.c create mode 100644 usr.bin/mandoc/mdoc_macro.c create mode 100644 usr.bin/mandoc/mdoc_man.c create mode 100644 usr.bin/mandoc/mdoc_markdown.c create mode 100644 usr.bin/mandoc/mdoc_state.c create mode 100644 usr.bin/mandoc/mdoc_term.c create mode 100644 usr.bin/mandoc/mdoc_validate.c create mode 100644 usr.bin/mandoc/msec.c create mode 100644 usr.bin/mandoc/msec.in create mode 100644 usr.bin/mandoc/out.c create mode 100644 usr.bin/mandoc/out.h create mode 100644 usr.bin/mandoc/preconv.c create mode 100644 usr.bin/mandoc/predefs.in create mode 100644 usr.bin/mandoc/read.c create mode 100644 usr.bin/mandoc/roff.c create mode 100644 usr.bin/mandoc/roff.h create mode 100644 usr.bin/mandoc/roff_html.c create mode 100644 usr.bin/mandoc/roff_int.h create mode 100644 usr.bin/mandoc/roff_term.c create mode 100644 usr.bin/mandoc/roff_validate.c create mode 100644 usr.bin/mandoc/st.c create mode 100644 usr.bin/mandoc/tag.c create mode 100644 usr.bin/mandoc/tag.h create mode 100644 usr.bin/mandoc/tbl.c create mode 100644 usr.bin/mandoc/tbl.h create mode 100644 usr.bin/mandoc/tbl_data.c create mode 100644 usr.bin/mandoc/tbl_html.c create mode 100644 usr.bin/mandoc/tbl_int.h create mode 100644 usr.bin/mandoc/tbl_layout.c create mode 100644 usr.bin/mandoc/tbl_opts.c create mode 100644 usr.bin/mandoc/tbl_parse.h create mode 100644 usr.bin/mandoc/tbl_term.c create mode 100644 usr.bin/mandoc/term.c create mode 100644 usr.bin/mandoc/term.h create mode 100644 usr.bin/mandoc/term_ascii.c create mode 100644 usr.bin/mandoc/term_ps.c create mode 100644 usr.bin/mandoc/term_tab.c create mode 100644 usr.bin/mandoc/term_tag.c create mode 100644 usr.bin/mandoc/term_tag.h create mode 100644 usr.bin/mandoc/tree.c (limited to 'usr.bin/mandoc') diff --git a/usr.bin/mandoc/CVS/Entries b/usr.bin/mandoc/CVS/Entries new file mode 100644 index 0000000..30e23be --- /dev/null +++ b/usr.bin/mandoc/CVS/Entries @@ -0,0 +1,101 @@ +/Makefile/1.118/Fri Mar 13 00:31:04 2020// +/apropos.1/1.41/Thu Nov 22 12:32:10 2018// +/arch.c/1.11/Sat May 11 07:18:17 2019// +/att.c/1.14/Thu Dec 13 11:55:14 2018// +/cgi.c/1.110/Fri Apr 3 11:34:19 2020// +/cgi.h.example/1.6/Sat Mar 18 16:48:07 2017// +/chars.c/1.49/Thu Feb 13 16:16:03 2020// +/dba.c/1.7/Thu Feb 9 18:26:17 2017// +/dba.h/1.2/Wed Aug 17 20:46:06 2016// +/dba_array.c/1.1/Mon Aug 1 10:32:39 2016// +/dba_array.h/1.1/Mon Aug 1 10:32:39 2016// +/dba_read.c/1.4/Wed Aug 17 20:46:06 2016// +/dba_write.c/1.1/Mon Aug 1 10:32:39 2016// +/dba_write.h/1.1/Mon Aug 1 10:32:39 2016// +/dbm.c/1.5/Mon Jul 1 22:43:03 2019// +/dbm.h/1.1/Mon Aug 1 10:32:39 2016// +/dbm_map.c/1.6/Thu Feb 9 18:26:17 2017// +/dbm_map.h/1.2/Mon Jul 1 22:43:03 2019// +/eqn.c/1.47/Wed Jan 8 12:09:14 2020// +/eqn.h/1.1/Thu Dec 13 05:13:15 2018// +/eqn_html.c/1.15/Sun Mar 17 18:20:07 2019// +/eqn_parse.h/1.3/Fri Dec 14 06:33:03 2018// +/eqn_term.c/1.15/Thu Dec 13 05:13:15 2018// +/html.c/1.141/Mon Apr 20 12:59:24 2020// +/html.h/1.70/Sat Apr 18 20:28:46 2020// +/libman.h/1.61/Mon Dec 31 10:03:38 2018// +/libmandoc.h/1.64/Fri Apr 3 11:34:19 2020// +/libmdoc.h/1.88/Mon Dec 31 04:55:42 2018// +/main.c/1.251/Thu Apr 2 22:10:27 2020// +/main.h/1.25/Sun Mar 3 13:01:47 2019// +/makewhatis.8/1.14/Wed May 17 22:26:52 2017// +/man.1/1.36/Mon Feb 10 13:49:04 2020// +/man.c/1.135/Sat Jan 5 00:36:46 2019// +/man.cgi.8/1.22/Sun May 20 21:48:23 2018// +/man.conf.5/1.8/Mon Feb 10 14:42:03 2020// +/man.h/1.59/Thu Aug 23 19:32:03 2018// +/man_html.c/1.131/Sat Apr 4 20:23:06 2020// +/man_macro.c/1.106/Sat Jan 5 18:59:37 2019// +/man_term.c/1.188/Fri Mar 13 00:31:05 2020// +/man_validate.c/1.124/Fri Apr 24 11:58:02 2020// +/manconf.h/1.8/Thu Apr 2 22:10:27 2020// +/mandoc.1/1.167/Fri Apr 24 11:58:02 2020// +/mandoc.c/1.85/Sun Jan 19 16:16:32 2020// +/mandoc.css/1.33/Sun Jun 2 16:50:46 2019// +/mandoc.h/1.210/Fri Apr 24 11:58:02 2020// +/mandoc_aux.c/1.9/Wed Feb 7 20:04:33 2018// +/mandoc_aux.h/1.9/Mon Jun 12 18:55:42 2017// +/mandoc_msg.c/1.9/Fri Apr 24 11:58:02 2020// +/mandoc_ohash.c/1.2/Mon Oct 19 18:58:20 2015// +/mandoc_ohash.h/1.2/Sat Nov 7 13:57:55 2015// +/mandoc_parse.h/1.4/Sat Nov 9 14:39:42 2019// +/mandoc_xr.c/1.3/Sun Jul 2 21:17:12 2017// +/mandoc_xr.h/1.3/Sun Jul 2 21:17:12 2017// +/mandocdb.c/1.216/Fri Apr 3 11:34:19 2020// +/manpath.c/1.28/Mon Feb 10 14:42:03 2020// +/mansearch.c/1.65/Mon Jul 1 22:43:03 2019// +/mansearch.h/1.24/Tue Apr 30 18:48:26 2019// +/mdoc.c/1.164/Mon Apr 6 09:55:49 2020// +/mdoc.h/1.71/Sun Dec 30 00:48:47 2018// +/mdoc_argv.c/1.76/Thu Jul 11 16:56:52 2019// +/mdoc_html.c/1.215/Sun Apr 19 15:15:54 2020// +/mdoc_macro.c/1.191/Sun Jan 19 17:59:01 2020// +/mdoc_man.c/1.134/Thu Feb 27 01:25:57 2020// +/mdoc_markdown.c/1.35/Fri Apr 3 11:34:19 2020// +/mdoc_state.c/1.16/Sun Jan 19 17:59:01 2020// +/mdoc_term.c/1.279/Mon Apr 6 09:55:49 2020// +/mdoc_validate.c/1.302/Sun Apr 26 21:29:45 2020// +/msec.c/1.13/Fri Dec 14 01:17:46 2018// +/msec.in/1.6/Sat Jun 24 17:36:50 2017// +/out.c/1.51/Tue Dec 31 22:49:17 2019// +/out.h/1.25/Fri Apr 3 11:34:19 2020// +/preconv.c/1.9/Thu Dec 13 11:55:14 2018// +/predefs.in/1.4/Fri Nov 28 19:25:03 2014// +/read.c/1.190/Fri Apr 24 11:58:02 2020// +/roff.c/1.246/Wed Apr 8 11:54:14 2020// +/roff.h/1.56/Wed Apr 8 11:54:14 2020// +/roff_html.c/1.20/Tue Apr 30 15:52:42 2019// +/roff_int.h/1.17/Fri Apr 24 11:58:02 2020// +/roff_term.c/1.19/Fri Jan 4 03:24:30 2019// +/roff_validate.c/1.19/Thu Feb 27 01:25:58 2020// +/st.c/1.13/Fri Dec 14 01:17:46 2018// +/tag.c/1.36/Sun Apr 19 16:26:11 2020// +/tag.h/1.14/Sat Apr 18 20:28:46 2020// +/tbl.c/1.27/Fri Dec 14 06:33:03 2018// +/tbl.h/1.5/Wed Dec 12 21:54:30 2018// +/tbl_data.c/1.40/Sat Jan 11 20:48:13 2020// +/tbl_html.c/1.28/Sun Mar 17 18:20:07 2019// +/tbl_int.h/1.2/Fri Dec 14 06:33:03 2018// +/tbl_layout.c/1.35/Fri Dec 14 05:17:45 2018// +/tbl_opts.c/1.16/Fri Dec 14 05:17:45 2018// +/tbl_parse.h/1.2/Fri Dec 14 06:33:03 2018// +/tbl_term.c/1.61/Sat Jan 11 16:24:33 2020// +/term.c/1.141/Mon Jun 3 20:23:39 2019// +/term.h/1.75/Fri Jan 4 03:20:44 2019// +/term_ascii.c/1.50/Fri Jul 19 21:45:37 2019// +/term_ps.c/1.55/Fri Nov 10 14:16:28 2017// +/term_tab.c/1.4/Sat Jun 17 14:55:02 2017// +/term_tag.c/1.4/Sat Apr 18 20:28:46 2020// +/term_tag.h/1.2/Thu Apr 2 22:10:27 2020// +/tree.c/1.56/Wed Apr 8 11:54:14 2020// +D diff --git a/usr.bin/mandoc/CVS/Repository b/usr.bin/mandoc/CVS/Repository new file mode 100644 index 0000000..3f4a4d0 --- /dev/null +++ b/usr.bin/mandoc/CVS/Repository @@ -0,0 +1 @@ +src/usr.bin/mandoc diff --git a/usr.bin/mandoc/CVS/Root b/usr.bin/mandoc/CVS/Root new file mode 100644 index 0000000..3811072 --- /dev/null +++ b/usr.bin/mandoc/CVS/Root @@ -0,0 +1 @@ +/cvs diff --git a/usr.bin/mandoc/Makefile b/usr.bin/mandoc/Makefile new file mode 100644 index 0000000..8d6fec5 --- /dev/null +++ b/usr.bin/mandoc/Makefile @@ -0,0 +1,78 @@ +# $OpenBSD: Makefile,v 1.118 2020/03/13 00:31:04 schwarze Exp $ + +.include + +CFLAGS += -W -Wall -Wstrict-prototypes -Wno-unused-parameter +DPADD += ${LIBUTIL} +LDADD += -lutil -lz + +SRCS= mandoc_aux.c mandoc_ohash.c mandoc.c mandoc_msg.c mandoc_xr.c \ + arch.c chars.c msec.c preconv.c read.c tag.c +SRCS+= roff.c roff_validate.c tbl.c tbl_opts.c tbl_layout.c tbl_data.c eqn.c +SRCS+= mdoc.c mdoc_argv.c mdoc_macro.c mdoc_state.c mdoc_validate.c \ + att.c st.c +SRCS+= man_macro.c man.c man_validate.c +SRCS+= main.c out.c tree.c +SRCS+= term.c term_ascii.c term_ps.c term_tab.c term_tag.c +SRCS+= roff_term.c mdoc_term.c man_term.c eqn_term.c tbl_term.c +SRCS+= mdoc_man.c +SRCS+= html.c roff_html.c mdoc_html.c man_html.c eqn_html.c tbl_html.c +SRCS+= mdoc_markdown.c +SRCS+= dbm_map.c dbm.c dba_write.c dba_array.c dba.c dba_read.c +SRCS+= manpath.c mandocdb.c mansearch.c + +PROG= mandoc + +LINKS = ${BINDIR}/mandoc ${BINDIR}/apropos \ + ${BINDIR}/mandoc ${BINDIR}/help \ + ${BINDIR}/mandoc ${BINDIR}/man \ + ${BINDIR}/mandoc ${BINDIR}/whatis \ + ${BINDIR}/mandoc /usr/sbin/makewhatis \ + ${BINDIR}/mandoc /usr/libexec/makewhatis + +MAN = apropos.1 man.1 mandoc.1 man.conf.5 makewhatis.8 + +CLEANFILES += man.cgi cgi.o + +afterinstall: + install -o ${BINOWN} -g ${BINGRP} -m 444 \ + ${.CURDIR}/mandoc.css ${DESTDIR}/usr/share/misc + + +# ---------------------------------------------------------------------- +# Variables and targets to build and install man.cgi(8), +# not used during make build and make release. + +# To configure, run: cp cgi.h.example cgi.h; vi cgi.h +# To build, run: make man.cgi +# To install, run: sudo make installcgi +# After that, read: man man.cgi.8 + +LIBMDOC_OBJS = mdoc_argv.o mdoc_macro.o mdoc_state.o \ + mdoc_validate.o mdoc.o att.o st.o +LIBMAN_OBJS = man.o man_macro.o man_validate.o +LIBROFF_OBJS = roff.o roff_validate.o eqn.o \ + tbl.o tbl_data.o tbl_layout.o tbl_opts.o +LIBMANDOC_OBJS = ${LIBMDOC_OBJS} ${LIBMAN_OBJS} ${LIBROFF_OBJS} \ + arch.o mandoc.o mandoc_aux.o mandoc_msg.o mandoc_ohash.o \ + mandoc_xr.o chars.o msec.o preconv.o read.o tag.o +HTML_OBJS = html.o roff_html.o mdoc_html.o man_html.o \ + tbl_html.o eqn_html.o out.o +CGI_OBJS = ${LIBMANDOC_OBJS} ${HTML_OBJS} \ + dbm_map.o dbm.o mansearch.o cgi.o + +cgi.o: cgi.h main.h manconf.h mandoc.h mandoc_aux.h mandoc_parse.h \ + mansearch.h man.h mdoc.h roff.h + +man.cgi: ${CGI_OBJS} + ${CC} ${LDFLAGS} ${STATIC} -o ${.TARGET} ${CGI_OBJS} ${LDADD} + +installcgi: man.cgi + ${INSTALL} -d -o root -g wheel -m 755 ${DESTDIR}/var/www/cgi-bin + ${INSTALL} ${INSTALL_COPY} ${INSTALL_STRIP} \ + -o ${BINOWN} -g ${BINGRP} -m ${BINMODE} \ + man.cgi ${DESTDIR}/var/www/cgi-bin/man.cgi + ${INSTALL} ${INSTALL_COPY} -o root -g wheel -m 644 \ + ${.CURDIR}/mandoc.css ${DESTDIR}/var/www/htdocs/ + +.include diff --git a/usr.bin/mandoc/apropos.1 b/usr.bin/mandoc/apropos.1 new file mode 100644 index 0000000..401976f --- /dev/null +++ b/usr.bin/mandoc/apropos.1 @@ -0,0 +1,510 @@ +.\" $OpenBSD: apropos.1,v 1.41 2018/11/22 12:32:10 schwarze Exp $ +.\" +.\" Copyright (c) 2011, 2012 Kristaps Dzonsons +.\" Copyright (c) 2011,2012,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. +.\" +.Dd $Mdocdate: November 22 2018 $ +.Dt APROPOS 1 +.Os +.Sh NAME +.Nm apropos , +.Nm whatis +.Nd search manual page databases +.Sh SYNOPSIS +.Nm +.Op Fl afk +.Op Fl C Ar file +.Op Fl M Ar path +.Op Fl m Ar path +.Op Fl O Ar outkey +.Op Fl S Ar arch +.Op Fl s Ar section +.Ar expression ... +.Sh DESCRIPTION +The +.Nm apropos +and +.Nm whatis +utilities query manual page databases generated by +.Xr makewhatis 8 , +evaluating +.Ar expression +for each file in each database. +By default, they display the names, section numbers, and description lines +of all matching manuals. +.Pp +By default, +.Nm +searches for +.Xr makewhatis 8 +databases in the default paths stipulated by +.Xr man 1 +and uses case-insensitive extended regular expression matching +over manual names and descriptions +.Pq the Li \&Nm No and Li \&Nd No macro keys . +Multiple terms imply pairwise +.Fl o . +.Pp +.Nm whatis +is a synonym for +.Nm +.Fl f . +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl a +Instead of showing only the title lines, show the complete manual pages, +just like +.Xr man 1 +.Fl a +would. +If the standard output is a terminal device and +.Fl c +is not specified, use +.Xr more 1 +to paginate them. +In +.Fl a +mode, the options +.Fl IKOTW +described in the +.Xr mandoc 1 +manual are also available. +.It Fl C Ar file +Specify an alternative configuration +.Ar file +in +.Xr man.conf 5 +format. +.It Fl f +Search for all words in +.Ar expression +in manual page names only. +The search is case-insensitive and matches whole words only. +In this mode, macro keys, comparison operators, and logical operators +are not available. +.It Fl k +Support the full +.Ar expression +syntax. +It is the default for +.Nm . +.It Fl M Ar path +Use the colon-separated path instead of the default list of paths +searched for +.Xr makewhatis 8 +databases. +Invalid paths, or paths without manual databases, are ignored. +.It Fl m Ar path +Prepend the colon-separated paths to the list of paths searched +for +.Xr makewhatis 8 +databases. +Invalid paths, or paths without manual databases, are ignored. +.It Fl O Ar outkey +Show the values associated with the key +.Ar outkey +instead of the manual descriptions. +.It Fl S Ar arch +Restrict the search to pages for the specified +.Xr machine 1 +architecture. +.Ar arch +is case-insensitive. +By default, pages for all architectures are shown. +.It Fl s Ar section +Restrict the search to the specified section of the manual. +By default, pages from all sections are shown. +See +.Xr man 1 +for a listing of sections. +.El +.Pp +The options +.Fl chlw +are also supported and are documented in +.Xr man 1 . +The options +.Fl fkl +are mutually exclusive and override each other. +.Pp +An +.Ar expression +consists of search terms joined by logical operators +.Fl a +.Pq and +and +.Fl o +.Pq or . +The +.Fl a +operator has precedence over +.Fl o +and both are evaluated left-to-right. +.Bl -tag -width Ds +.It \&( Ar expr No \&) +True if the subexpression +.Ar expr +is true. +.It Ar expr1 Fl a Ar expr2 +True if both +.Ar expr1 +and +.Ar expr2 +are true (logical +.Sq and ) . +.It Ar expr1 Oo Fl o Oc Ar expr2 +True if +.Ar expr1 +and/or +.Ar expr2 +evaluate to true (logical +.Sq or ) . +.It Ar term +True if +.Ar term +is satisfied. +This has syntax +.Sm off +.Oo +.Op Ar key Op , Ar key ... +.Pq Cm = | \(ti +.Oc +.Ar val , +.Sm on +where +.Ar key +is an +.Xr mdoc 7 +macro to query and +.Ar val +is its value. +See +.Sx Macro Keys +for a list of available keys. +Operator +.Cm = +evaluates a substring, while +.Cm \(ti +evaluates a case-sensitive extended regular expression. +.It Fl i Ar term +If +.Ar term +is a regular expression, it +is evaluated case-insensitively. +Has no effect on substring terms. +.El +.Pp +Results are sorted first according to the section number in ascending +numerical order, then by the page name in ascending +.Xr ascii 7 +alphabetical order, case-insensitive. +.Pp +Each output line is formatted as +.Pp +.D1 name[, name...](sec) \- description +.Pp +Where +.Dq name +is the manual's name, +.Dq sec +is the manual section, and +.Dq description +is the manual's short description. +If an architecture is specified for the manual, it is displayed as +.Pp +.D1 name(sec/arch) \- description +.Pp +Resulting manuals may be accessed as +.Pp +.Dl $ man \-s sec name +.Pp +If an architecture is specified in the output, use +.Pp +.Dl $ man \-s sec \-S arch name +.Ss Macro Keys +Queries evaluate over a subset of +.Xr mdoc 7 +macros indexed by +.Xr makewhatis 8 . +In addition to the macro keys listed below, the special key +.Cm any +may be used to match any available macro key. +.Pp +Names and description: +.Bl -column "xLix" description -offset indent -compact +.It Li \&Nm Ta manual name +.It Li \&Nd Ta one-line manual description +.It Li arch Ta machine architecture (case-insensitive) +.It Li sec Ta manual section number +.El +.Pp +Sections and cross references: +.Bl -column "xLix" description -offset indent -compact +.It Li \&Sh Ta section header (excluding standard sections) +.It Li \&Ss Ta subsection header +.It Li \&Xr Ta cross reference to another manual page +.It Li \&Rs Ta bibliographic reference +.El +.Pp +Semantic markup for command line utilities: +.Bl -column "xLix" description -offset indent -compact +.It Li \&Fl Ta command line options (flags) +.It Li \&Cm Ta command modifier +.It Li \&Ar Ta command argument +.It Li \&Ic Ta internal or interactive command +.It Li \&Ev Ta environmental variable +.It Li \&Pa Ta file system path +.El +.Pp +Semantic markup for function libraries: +.Bl -column "xLix" description -offset indent -compact +.It Li \&Lb Ta function library name +.It Li \&In Ta include file +.It Li \&Ft Ta function return type +.It Li \&Fn Ta function name +.It Li \&Fa Ta function argument type and name +.It Li \&Vt Ta variable type +.It Li \&Va Ta variable name +.It Li \&Dv Ta defined variable or preprocessor constant +.It Li \&Er Ta error constant +.It Li \&Ev Ta environmental variable +.El +.Pp +Various semantic markup: +.Bl -column "xLix" description -offset indent -compact +.It Li \&An Ta author name +.It Li \&Lk Ta hyperlink +.It Li \&Mt Ta Do mailto Dc hyperlink +.It Li \&Cd Ta kernel configuration declaration +.It Li \&Ms Ta mathematical symbol +.It Li \&Tn Ta tradename +.El +.Pp +Physical markup: +.Bl -column "xLix" description -offset indent -compact +.It Li \&Em Ta italic font or underline +.It Li \&Sy Ta boldface font +.It Li \&Li Ta typewriter font +.El +.Pp +Text production: +.Bl -column "xLix" description -offset indent -compact +.It Li \&St Ta reference to a standards document +.It Li \&At Ta At No version reference +.It Li \&Bx Ta Bx No version reference +.It Li \&Bsx Ta Bsx No version reference +.It Li \&Nx Ta Nx No version reference +.It Li \&Fx Ta Fx No version reference +.It Li \&Ox Ta Ox No version reference +.It Li \&Dx Ta Dx No version reference +.El +.Pp +In general, macro keys are supposed to yield complete results without +expecting the user to consider actual macro usage. +For example, results include: +.Pp +.Bl -tag -width 3n -offset 3n -compact +.It Li \&Fa +function arguments appearing on +.Ic \&Fn +lines +.It Li \&Fn +function names marked up with +.Ic \&Fo +macros +.It Li \&In +include file names marked up with +.Ic \&Fd +macros +.It Li \&Vt +types appearing as function return types and +.It \& +types appearing in function arguments in the SYNOPSIS +.El +.Sh ENVIRONMENT +.Bl -tag -width MANPAGER +.It Ev MANPAGER +Any non-empty value of the environment variable +.Ev MANPAGER +is used instead of the standard pagination program, +.Xr more 1 ; +see +.Xr man 1 +for details. +Only used if +.Fl a +or +.Fl l +is specified. +.It Ev MANPATH +A colon-separated list of directories to search for manual pages; see +.Xr man 1 +for details. +Overridden by +.Fl M , +ignored if +.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 more 1 +.Fl s +is used. +Only used if +.Fl a +or +.Fl l +is specified. +.El +.Sh FILES +.Bl -tag -width "/etc/man.conf" -compact +.It Pa mandoc.db +name of the +.Xr makewhatis 8 +keyword database +.It Pa /etc/man.conf +default +.Xr man 1 +configuration file +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +Search for +.Qq .cf +as a substring of manual names and descriptions: +.Pp +.Dl $ apropos =.cf +.Pp +Include matches for +.Qq .cnf +and +.Qq .conf +as well: +.Pp +.Dl $ apropos =.cf =.cnf =.conf +.Pp +Search in names and descriptions using a case-sensitive regular expression: +.Pp +.Dl $ apropos \(aq\(tiset.?[ug]id\(aq +.Pp +Search for manuals in the library section mentioning both the +.Qq optind +and the +.Qq optarg +variables: +.Pp +.Dl $ apropos \-s 3 Va=optind \-a Va=optarg +.Pp +Do exactly the same as calling +.Nm whatis +with the argument +.Qq ssh : +.Pp +.Dl $ apropos \-\- \-i \(aqNm\(ti[[:<:]]ssh[[:>:]]\(aq +.Pp +The following two invocations are equivalent: +.Pp +.D1 Li $ apropos -S Ar arch Li -s Ar section expression +.Bd -ragged -offset indent +.Li $ apropos \e( Ar expression Li \e) +.Li -a arch\(ti^( Ns Ar arch Ns Li |any)$ +.Li -a sec\(ti^ Ns Ar section Ns Li $ +.Ed +.Sh SEE ALSO +.Xr man 1 , +.Xr re_format 7 , +.Xr makewhatis 8 +.Sh STANDARDS +The +.Nm +utility is compliant with the +.St -p1003.1-2008 +specification of +.Xr man 1 +.Fl k . +.Pp +All options, the +.Nm whatis +command, support for logical operators, macro keys, +substring matching, sorting of results, the environment variables +.Ev MANPAGER +and +.Ev MANPATH , +the database format, and the configuration file +are extensions to that specification. +.Sh HISTORY +Part of the functionality of +.Nm whatis +was already provided by the former +.Nm manwhere +utility in +.Bx 1 . +The +.Nm +and +.Nm whatis +utilities first appeared in +.Bx 2 . +They were rewritten from scratch for +.Ox 5.6 . +.Pp +The +.Fl M +option and the +.Ev MANPATH +variable first appeared in +.Bx 4.3 ; +.Fl m +in +.Bx 4.3 Reno ; +.Fl C +in +.Bx 4.4 Lite1 ; +and +.Fl S +and +.Fl s +in +.Ox 4.5 +for +.Nm +and in +.Ox 5.6 +for +.Nm whatis . +The options +.Fl acfhIKklOTWw +appeared in +.Ox 5.7 . +.Sh AUTHORS +.An -nosplit +.An Bill Joy +wrote +.Nm manwhere +in 1977 and the original +.Bx +.Nm +and +.Nm whatis +in February 1979. +The current version was written by +.An Kristaps Dzonsons Aq Mt kristaps@bsd.lv +and +.An Ingo Schwarze Aq Mt schwarze@openbsd.org . diff --git a/usr.bin/mandoc/arch.c b/usr.bin/mandoc/arch.c new file mode 100644 index 0000000..68a20bb --- /dev/null +++ b/usr.bin/mandoc/arch.c @@ -0,0 +1,52 @@ +/* $OpenBSD: arch.c,v 1.11 2019/05/11 07:18:17 deraadt Exp $ */ +/* + * Copyright (c) 2017, 2019 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 + +#include "roff.h" + +int +arch_valid(const char *arch, enum mandoc_os os) +{ + const char *openbsd_arch[] = { + "alpha", "amd64", "arm64", "armv7", "hppa", "i386", + "landisk", "loongson", "luna88k", "macppc", "mips64", + "octeon", "sgi", "sparc64", NULL + }; + const char *netbsd_arch[] = { + "acorn26", "acorn32", "algor", "alpha", "amiga", + "arc", "atari", + "bebox", "cats", "cesfic", "cobalt", "dreamcast", + "emips", "evbarm", "evbmips", "evbppc", "evbsh3", "evbsh5", + "hp300", "hpcarm", "hpcmips", "hpcsh", "hppa", + "i386", "ibmnws", "luna68k", + "mac68k", "macppc", "mipsco", "mmeye", "mvme68k", "mvmeppc", + "netwinder", "news68k", "newsmips", "next68k", + "pc532", "playstation2", "pmax", "pmppc", "prep", + "sandpoint", "sbmips", "sgimips", "shark", + "sparc", "sparc64", "sun2", "sun3", + "vax", "walnut", "x68k", "x86", "x86_64", "xen", NULL + }; + const char **arches[] = { NULL, netbsd_arch, openbsd_arch }; + const char **arch_p; + + if ((arch_p = arches[os]) == NULL) + return 1; + for (; *arch_p != NULL; arch_p++) + if (strcmp(*arch_p, arch) == 0) + return 1; + return 0; +} diff --git a/usr.bin/mandoc/att.c b/usr.bin/mandoc/att.c new file mode 100644 index 0000000..85c184e --- /dev/null +++ b/usr.bin/mandoc/att.c @@ -0,0 +1,47 @@ +/* $OpenBSD: att.c,v 1.14 2018/12/13 11:55:14 schwarze Exp $ */ +/* + * Copyright (c) 2009 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 +#include + +#include "roff.h" +#include "libmdoc.h" + +#define LINE(x, y) \ + if (0 == strcmp(p, x)) return(y) + + +const char * +mdoc_a2att(const char *p) +{ + + LINE("v1", "Version\\~1 AT&T UNIX"); + LINE("v2", "Version\\~2 AT&T UNIX"); + LINE("v3", "Version\\~3 AT&T UNIX"); + LINE("v4", "Version\\~4 AT&T UNIX"); + LINE("v5", "Version\\~5 AT&T UNIX"); + LINE("v6", "Version\\~6 AT&T UNIX"); + LINE("v7", "Version\\~7 AT&T UNIX"); + LINE("32v", "Version\\~32V AT&T UNIX"); + LINE("III", "AT&T System\\~III UNIX"); + LINE("V", "AT&T System\\~V UNIX"); + LINE("V.1", "AT&T System\\~V Release\\~1 UNIX"); + LINE("V.2", "AT&T System\\~V Release\\~2 UNIX"); + LINE("V.3", "AT&T System\\~V Release\\~3 UNIX"); + LINE("V.4", "AT&T System\\~V Release\\~4 UNIX"); + + return NULL; +} diff --git a/usr.bin/mandoc/cgi.c b/usr.bin/mandoc/cgi.c new file mode 100644 index 0000000..766ac06 --- /dev/null +++ b/usr.bin/mandoc/cgi.c @@ -0,0 +1,1255 @@ +/* $OpenBSD: cgi.c,v 1.110 2020/04/03 11:34:19 schwarze Exp $ */ +/* + * Copyright (c) 2014-2019 Ingo Schwarze + * Copyright (c) 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 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. + * + * Implementation of the man.cgi(8) program. + */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mandoc_aux.h" +#include "mandoc.h" +#include "roff.h" +#include "mdoc.h" +#include "man.h" +#include "mandoc_parse.h" +#include "main.h" +#include "manconf.h" +#include "mansearch.h" +#include "cgi.h" + +/* + * A query as passed to the search function. + */ +struct query { + char *manpath; /* desired manual directory */ + char *arch; /* architecture */ + char *sec; /* manual section */ + char *query; /* unparsed query expression */ + int equal; /* match whole names, not substrings */ +}; + +struct req { + struct query q; + char **p; /* array of available manpaths */ + size_t psz; /* number of available manpaths */ + int isquery; /* QUERY_STRING used, not PATH_INFO */ +}; + +enum focus { + FOCUS_NONE = 0, + FOCUS_QUERY +}; + +static void html_print(const char *); +static void html_putchar(char); +static int http_decode(char *); +static void http_encode(const char *); +static void parse_manpath_conf(struct req *); +static void parse_path_info(struct req *, const char *); +static void parse_query_string(struct req *, const char *); +static void pg_error_badrequest(const char *); +static void pg_error_internal(void); +static void pg_index(const struct req *); +static void pg_noresult(const struct req *, int, const char *, + const char *); +static void pg_redirect(const struct req *, const char *); +static void pg_search(const struct req *); +static void pg_searchres(const struct req *, + struct manpage *, size_t); +static void pg_show(struct req *, const char *); +static void resp_begin_html(int, const char *, const char *); +static void resp_begin_http(int, const char *); +static void resp_catman(const struct req *, const char *); +static void resp_copy(const char *); +static void resp_end_html(void); +static void resp_format(const struct req *, const char *); +static void resp_searchform(const struct req *, enum focus); +static void resp_show(const struct req *, const char *); +static void set_query_attr(char **, char **); +static int validate_arch(const char *); +static int validate_filename(const char *); +static int validate_manpath(const struct req *, const char *); +static int validate_urifrag(const char *); + +static const char *scriptname = SCRIPT_NAME; + +static const int sec_prios[] = {1, 4, 5, 8, 6, 3, 7, 2, 9}; +static const char *const sec_numbers[] = { + "0", "1", "2", "3", "3p", "4", "5", "6", "7", "8", "9" +}; +static const char *const sec_names[] = { + "All Sections", + "1 - General Commands", + "2 - System Calls", + "3 - Library Functions", + "3p - Perl Library", + "4 - Device Drivers", + "5 - File Formats", + "6 - Games", + "7 - Miscellaneous Information", + "8 - System Manager\'s Manual", + "9 - Kernel Developer\'s Manual" +}; +static const int sec_MAX = sizeof(sec_names) / sizeof(char *); + +static const char *const arch_names[] = { + "amd64", "alpha", "armv7", "arm64", + "hppa", "i386", "landisk", + "loongson", "luna88k", "macppc", "mips64", + "octeon", "sgi", "socppc", "sparc64", + "amiga", "arc", "armish", "arm32", + "atari", "aviion", "beagle", "cats", + "hppa64", "hp300", + "ia64", "mac68k", "mvme68k", "mvme88k", + "mvmeppc", "palm", "pc532", "pegasos", + "pmax", "powerpc", "solbourne", "sparc", + "sun3", "vax", "wgrisc", "x68k", + "zaurus" +}; +static const int arch_MAX = sizeof(arch_names) / sizeof(char *); + +/* + * Print a character, escaping HTML along the way. + * This will pass non-ASCII straight to output: be warned! + */ +static void +html_putchar(char c) +{ + + switch (c) { + case '"': + printf("""); + break; + case '&': + printf("&"); + break; + case '>': + printf(">"); + break; + case '<': + printf("<"); + break; + default: + putchar((unsigned char)c); + break; + } +} + +/* + * Call through to html_putchar(). + * Accepts NULL strings. + */ +static void +html_print(const char *p) +{ + + if (NULL == p) + return; + while ('\0' != *p) + html_putchar(*p++); +} + +/* + * Transfer the responsibility for the allocated string *val + * to the query structure. + */ +static void +set_query_attr(char **attr, char **val) +{ + + free(*attr); + if (**val == '\0') { + *attr = NULL; + free(*val); + } else + *attr = *val; + *val = NULL; +} + +/* + * Parse the QUERY_STRING for key-value pairs + * and store the values into the query structure. + */ +static void +parse_query_string(struct req *req, const char *qs) +{ + char *key, *val; + size_t keysz, valsz; + + req->isquery = 1; + req->q.manpath = NULL; + req->q.arch = NULL; + req->q.sec = NULL; + req->q.query = NULL; + req->q.equal = 1; + + key = val = NULL; + while (*qs != '\0') { + + /* Parse one key. */ + + keysz = strcspn(qs, "=;&"); + key = mandoc_strndup(qs, keysz); + qs += keysz; + if (*qs != '=') + goto next; + + /* Parse one value. */ + + valsz = strcspn(++qs, ";&"); + val = mandoc_strndup(qs, valsz); + qs += valsz; + + /* Decode and catch encoding errors. */ + + if ( ! (http_decode(key) && http_decode(val))) + goto next; + + /* Handle key-value pairs. */ + + if ( ! strcmp(key, "query")) + set_query_attr(&req->q.query, &val); + + else if ( ! strcmp(key, "apropos")) + req->q.equal = !strcmp(val, "0"); + + else if ( ! strcmp(key, "manpath")) { +#ifdef COMPAT_OLDURI + if ( ! strncmp(val, "OpenBSD ", 8)) { + val[7] = '-'; + if ('C' == val[8]) + val[8] = 'c'; + } +#endif + set_query_attr(&req->q.manpath, &val); + } + + else if ( ! (strcmp(key, "sec") +#ifdef COMPAT_OLDURI + && strcmp(key, "sektion") +#endif + )) { + if ( ! strcmp(val, "0")) + *val = '\0'; + set_query_attr(&req->q.sec, &val); + } + + else if ( ! strcmp(key, "arch")) { + if ( ! strcmp(val, "default")) + *val = '\0'; + set_query_attr(&req->q.arch, &val); + } + + /* + * The key must be freed in any case. + * The val may have been handed over to the query + * structure, in which case it is now NULL. + */ +next: + free(key); + key = NULL; + free(val); + val = NULL; + + if (*qs != '\0') + qs++; + } +} + +/* + * HTTP-decode a string. The standard explanation is that this turns + * "%4e+foo" into "n foo" in the regular way. This is done in-place + * over the allocated string. + */ +static int +http_decode(char *p) +{ + char hex[3]; + char *q; + int c; + + hex[2] = '\0'; + + q = p; + for ( ; '\0' != *p; p++, q++) { + if ('%' == *p) { + if ('\0' == (hex[0] = *(p + 1))) + return 0; + if ('\0' == (hex[1] = *(p + 2))) + return 0; + if (1 != sscanf(hex, "%x", &c)) + return 0; + if ('\0' == c) + return 0; + + *q = (char)c; + p += 2; + } else + *q = '+' == *p ? ' ' : *p; + } + + *q = '\0'; + return 1; +} + +static void +http_encode(const char *p) +{ + for (; *p != '\0'; p++) { + if (isalnum((unsigned char)*p) == 0 && + strchr("-._~", *p) == NULL) + printf("%%%2.2X", (unsigned char)*p); + else + putchar(*p); + } +} + +static void +resp_begin_http(int code, const char *msg) +{ + + if (200 != code) + printf("Status: %d %s\r\n", code, msg); + + printf("Content-Type: text/html; charset=utf-8\r\n" + "Cache-Control: no-cache\r\n" + "Content-Security-Policy: default-src 'none'; " + "style-src 'self' 'unsafe-inline'\r\n" + "Pragma: no-cache\r\n" + "\r\n"); + + fflush(stdout); +} + +static void +resp_copy(const char *filename) +{ + char buf[4096]; + ssize_t sz; + int fd; + + if ((fd = open(filename, O_RDONLY)) != -1) { + fflush(stdout); + while ((sz = read(fd, buf, sizeof(buf))) > 0) + write(STDOUT_FILENO, buf, sz); + close(fd); + } +} + +static void +resp_begin_html(int code, const char *msg, const char *file) +{ + char *cp; + + resp_begin_http(code, msg); + + printf("\n" + "\n" + "\n" + " \n" + " \n" + " \n" + " ", + CSS_DIR); + if (file != NULL) { + if ((cp = strrchr(file, '/')) != NULL) + file = cp + 1; + if ((cp = strrchr(file, '.')) != NULL) { + printf("%.*s(%s) - ", (int)(cp - file), file, cp + 1); + } else + printf("%s - ", file); + } + printf("%s\n" + "\n" + "\n", + CUSTOMIZE_TITLE); + + resp_copy(MAN_DIR "/header.html"); +} + +static void +resp_end_html(void) +{ + + resp_copy(MAN_DIR "/footer.html"); + + puts("\n" + ""); +} + +static void +resp_searchform(const struct req *req, enum focus focus) +{ + int i; + + printf("
\n" + "
\n" + " Manual Page Search Parameters\n", + scriptname); + + /* Write query input box. */ + + printf(" q.query != NULL) + html_print(req->q.query); + printf( "\" size=\"40\""); + if (focus == FOCUS_QUERY) + printf(" autofocus"); + puts(">"); + + /* Write submission buttons. */ + + printf( " \n" + " \n" + "
\n"); + + /* Write section selector. */ + + puts(" "); + + /* Write architecture selector. */ + + printf( " "); + + /* Write manpath selector. */ + + if (req->psz > 1) { + puts(" "); + } + + puts("
\n" + "
"); +} + +static int +validate_urifrag(const char *frag) +{ + + while ('\0' != *frag) { + if ( ! (isalnum((unsigned char)*frag) || + '-' == *frag || '.' == *frag || + '/' == *frag || '_' == *frag)) + return 0; + frag++; + } + return 1; +} + +static int +validate_manpath(const struct req *req, const char* manpath) +{ + size_t i; + + for (i = 0; i < req->psz; i++) + if ( ! strcmp(manpath, req->p[i])) + return 1; + + return 0; +} + +static int +validate_arch(const char *arch) +{ + int i; + + for (i = 0; i < arch_MAX; i++) + if (strcmp(arch, arch_names[i]) == 0) + return 1; + + return 0; +} + +static int +validate_filename(const char *file) +{ + + if ('.' == file[0] && '/' == file[1]) + file += 2; + + return ! (strstr(file, "../") || strstr(file, "/..") || + (strncmp(file, "man", 3) && strncmp(file, "cat", 3))); +} + +static void +pg_index(const struct req *req) +{ + + resp_begin_html(200, NULL, NULL); + resp_searchform(req, FOCUS_QUERY); + printf("

\n" + "This web interface is documented in the\n" + "man.cgi(8)\n" + "manual, and the\n" + "apropos(1)\n" + "manual explains the query syntax.\n" + "

\n", + scriptname, *scriptname == '\0' ? "" : "/", + scriptname, *scriptname == '\0' ? "" : "/"); + resp_end_html(); +} + +static void +pg_noresult(const struct req *req, int code, const char *http_msg, + const char *user_msg) +{ + resp_begin_html(code, http_msg, NULL); + resp_searchform(req, FOCUS_QUERY); + puts("

"); + puts(user_msg); + puts("

"); + resp_end_html(); +} + +static void +pg_error_badrequest(const char *msg) +{ + + resp_begin_html(400, "Bad Request", NULL); + puts("

Bad Request

\n" + "

\n"); + puts(msg); + printf("Try again from the\n" + "main page.\n" + "

", scriptname); + resp_end_html(); +} + +static void +pg_error_internal(void) +{ + resp_begin_html(500, "Internal Server Error", NULL); + puts("

Internal Server Error

"); + resp_end_html(); +} + +static void +pg_redirect(const struct req *req, const char *name) +{ + printf("Status: 303 See Other\r\n" + "Location: /"); + if (*scriptname != '\0') + printf("%s/", scriptname); + if (strcmp(req->q.manpath, req->p[0])) + printf("%s/", req->q.manpath); + if (req->q.arch != NULL) + printf("%s/", req->q.arch); + http_encode(name); + if (req->q.sec != NULL) { + putchar('.'); + http_encode(req->q.sec); + } + printf("\r\nContent-Type: text/html; charset=utf-8\r\n\r\n"); +} + +static void +pg_searchres(const struct req *req, struct manpage *r, size_t sz) +{ + char *arch, *archend; + const char *sec; + size_t i, iuse; + int archprio, archpriouse; + int prio, priouse; + + for (i = 0; i < sz; i++) { + if (validate_filename(r[i].file)) + continue; + warnx("invalid filename %s in %s database", + r[i].file, req->q.manpath); + pg_error_internal(); + return; + } + + if (req->isquery && sz == 1) { + /* + * If we have just one result, then jump there now + * without any delay. + */ + printf("Status: 303 See Other\r\n" + "Location: /"); + if (*scriptname != '\0') + printf("%s/", scriptname); + if (strcmp(req->q.manpath, req->p[0])) + printf("%s/", req->q.manpath); + printf("%s\r\n" + "Content-Type: text/html; charset=utf-8\r\n\r\n", + r[0].file); + return; + } + + /* + * In man(1) mode, show one of the pages + * even if more than one is found. + */ + + iuse = 0; + if (req->q.equal || sz == 1) { + priouse = 20; + archpriouse = 3; + for (i = 0; i < sz; i++) { + sec = r[i].file; + sec += strcspn(sec, "123456789"); + if (sec[0] == '\0') + continue; + prio = sec_prios[sec[0] - '1']; + if (sec[1] != '/') + prio += 10; + if (req->q.arch == NULL) { + archprio = + ((arch = strchr(sec + 1, '/')) + == NULL) ? 3 : + ((archend = strchr(arch + 1, '/')) + == NULL) ? 0 : + strncmp(arch, "amd64/", + archend - arch) ? 2 : 1; + if (archprio < archpriouse) { + archpriouse = archprio; + priouse = prio; + iuse = i; + continue; + } + if (archprio > archpriouse) + continue; + } + if (prio >= priouse) + continue; + priouse = prio; + iuse = i; + } + resp_begin_html(200, NULL, r[iuse].file); + } else + resp_begin_html(200, NULL, NULL); + + resp_searchform(req, + req->q.equal || sz == 1 ? FOCUS_NONE : FOCUS_QUERY); + + if (sz > 1) { + puts(""); + for (i = 0; i < sz; i++) { + printf(" \n" + " \n" + " \n" + " "); + } + puts("
" + "q.manpath, req->p[0])) + printf("%s/", req->q.manpath); + printf("%s\">", r[i].file); + html_print(r[i].names); + printf(""); + html_print(r[i].output); + puts("
"); + } + + if (req->q.equal || sz == 1) { + puts("
"); + resp_show(req, r[iuse].file); + } + + resp_end_html(); +} + +static void +resp_catman(const struct req *req, const char *file) +{ + FILE *f; + char *p; + size_t sz; + ssize_t len; + int i; + int italic, bold; + + if ((f = fopen(file, "r")) == NULL) { + puts("

You specified an invalid manual file.

"); + return; + } + + puts("
\n" + "
");
+
+	p = NULL;
+	sz = 0;
+
+	while ((len = getline(&p, &sz, f)) != -1) {
+		bold = italic = 0;
+		for (i = 0; i < len - 1; i++) {
+			/*
+			 * This means that the catpage is out of state.
+			 * Ignore it and keep going (although the
+			 * catpage is bogus).
+			 */
+
+			if ('\b' == p[i] || '\n' == p[i])
+				continue;
+
+			/*
+			 * Print a regular character.
+			 * Close out any bold/italic scopes.
+			 * If we're in back-space mode, make sure we'll
+			 * have something to enter when we backspace.
+			 */
+
+			if ('\b' != p[i + 1]) {
+				if (italic)
+					printf("");
+				if (bold)
+					printf("");
+				italic = bold = 0;
+				html_putchar(p[i]);
+				continue;
+			} else if (i + 2 >= len)
+				continue;
+
+			/* Italic mode. */
+
+			if ('_' == p[i]) {
+				if (bold)
+					printf("");
+				if ( ! italic)
+					printf("");
+				bold = 0;
+				italic = 1;
+				i += 2;
+				html_putchar(p[i]);
+				continue;
+			}
+
+			/*
+			 * Handle funny behaviour troff-isms.
+			 * These grok'd from the original man2html.c.
+			 */
+
+			if (('+' == p[i] && 'o' == p[i + 2]) ||
+					('o' == p[i] && '+' == p[i + 2]) ||
+					('|' == p[i] && '=' == p[i + 2]) ||
+					('=' == p[i] && '|' == p[i + 2]) ||
+					('*' == p[i] && '=' == p[i + 2]) ||
+					('=' == p[i] && '*' == p[i + 2]) ||
+					('*' == p[i] && '|' == p[i + 2]) ||
+					('|' == p[i] && '*' == p[i + 2]))  {
+				if (italic)
+					printf("");
+				if (bold)
+					printf("");
+				italic = bold = 0;
+				putchar('*');
+				i += 2;
+				continue;
+			} else if (('|' == p[i] && '-' == p[i + 2]) ||
+					('-' == p[i] && '|' == p[i + 1]) ||
+					('+' == p[i] && '-' == p[i + 1]) ||
+					('-' == p[i] && '+' == p[i + 1]) ||
+					('+' == p[i] && '|' == p[i + 1]) ||
+					('|' == p[i] && '+' == p[i + 1]))  {
+				if (italic)
+					printf("");
+				if (bold)
+					printf("");
+				italic = bold = 0;
+				putchar('+');
+				i += 2;
+				continue;
+			}
+
+			/* Bold mode. */
+
+			if (italic)
+				printf("");
+			if ( ! bold)
+				printf("");
+			bold = 1;
+			italic = 0;
+			i += 2;
+			html_putchar(p[i]);
+		}
+
+		/*
+		 * Clean up the last character.
+		 * We can get to a newline; don't print that.
+		 */
+
+		if (italic)
+			printf("");
+		if (bold)
+			printf("");
+
+		if (i == len - 1 && p[i] != '\n')
+			html_putchar(p[i]);
+
+		putchar('\n');
+	}
+	free(p);
+
+	puts("
\n" + "
"); + + fclose(f); +} + +static void +resp_format(const struct req *req, const char *file) +{ + struct manoutput conf; + struct mparse *mp; + struct roff_meta *meta; + void *vp; + int fd; + int usepath; + + if (-1 == (fd = open(file, O_RDONLY, 0))) { + puts("

You specified an invalid manual file.

"); + return; + } + + mchars_alloc(); + mp = mparse_alloc(MPARSE_SO | MPARSE_UTF8 | MPARSE_LATIN1 | + MPARSE_VALIDATE, MANDOC_OS_OTHER, req->q.manpath); + mparse_readfd(mp, fd, file); + close(fd); + meta = mparse_result(mp); + + memset(&conf, 0, sizeof(conf)); + conf.fragment = 1; + conf.style = mandoc_strdup(CSS_DIR "/mandoc.css"); + usepath = strcmp(req->q.manpath, req->p[0]); + mandoc_asprintf(&conf.man, "/%s%s%s%s%%N.%%S", + scriptname, *scriptname == '\0' ? "" : "/", + usepath ? req->q.manpath : "", usepath ? "/" : ""); + + vp = html_alloc(&conf); + if (meta->macroset == MACROSET_MDOC) + html_mdoc(vp, meta); + else + html_man(vp, meta); + + html_free(vp); + mparse_free(mp); + mchars_free(); + free(conf.man); + free(conf.style); +} + +static void +resp_show(const struct req *req, const char *file) +{ + + if ('.' == file[0] && '/' == file[1]) + file += 2; + + if ('c' == *file) + resp_catman(req, file); + else + resp_format(req, file); +} + +static void +pg_show(struct req *req, const char *fullpath) +{ + char *manpath; + const char *file; + + if ((file = strchr(fullpath, '/')) == NULL) { + pg_error_badrequest( + "You did not specify a page to show."); + return; + } + manpath = mandoc_strndup(fullpath, file - fullpath); + file++; + + if ( ! validate_manpath(req, manpath)) { + pg_error_badrequest( + "You specified an invalid manpath."); + free(manpath); + return; + } + + /* + * Begin by chdir()ing into the manpath. + * This way we can pick up the database files, which are + * relative to the manpath root. + */ + + if (chdir(manpath) == -1) { + warn("chdir %s", manpath); + pg_error_internal(); + free(manpath); + return; + } + free(manpath); + + if ( ! validate_filename(file)) { + pg_error_badrequest( + "You specified an invalid manual file."); + return; + } + + resp_begin_html(200, NULL, file); + resp_searchform(req, FOCUS_NONE); + resp_show(req, file); + resp_end_html(); +} + +static void +pg_search(const struct req *req) +{ + struct mansearch search; + struct manpaths paths; + struct manpage *res; + char **argv; + char *query, *rp, *wp; + size_t ressz; + int argc; + + /* + * Begin by chdir()ing into the root of the manpath. + * This way we can pick up the database files, which are + * relative to the manpath root. + */ + + if (chdir(req->q.manpath) == -1) { + warn("chdir %s", req->q.manpath); + pg_error_internal(); + return; + } + + search.arch = req->q.arch; + search.sec = req->q.sec; + search.outkey = "Nd"; + search.argmode = req->q.equal ? ARG_NAME : ARG_EXPR; + search.firstmatch = 1; + + paths.sz = 1; + paths.paths = mandoc_malloc(sizeof(char *)); + paths.paths[0] = mandoc_strdup("."); + + /* + * Break apart at spaces with backslash-escaping. + */ + + argc = 0; + argv = NULL; + rp = query = mandoc_strdup(req->q.query); + for (;;) { + while (isspace((unsigned char)*rp)) + rp++; + if (*rp == '\0') + break; + argv = mandoc_reallocarray(argv, argc + 1, sizeof(char *)); + argv[argc++] = wp = rp; + for (;;) { + if (isspace((unsigned char)*rp)) { + *wp = '\0'; + rp++; + break; + } + if (rp[0] == '\\' && rp[1] != '\0') + rp++; + if (wp != rp) + *wp = *rp; + if (*rp == '\0') + break; + wp++; + rp++; + } + } + + res = NULL; + ressz = 0; + if (req->isquery && req->q.equal && argc == 1) + pg_redirect(req, argv[0]); + else if (mansearch(&search, &paths, argc, argv, &res, &ressz) == 0) + pg_noresult(req, 400, "Bad Request", + "You entered an invalid query."); + else if (ressz == 0) + pg_noresult(req, 404, "Not Found", "No results found."); + else + pg_searchres(req, res, ressz); + + free(query); + mansearch_free(res, ressz); + free(paths.paths[0]); + free(paths.paths); +} + +int +main(void) +{ + struct req req; + struct itimerval itimer; + const char *path; + const char *querystring; + int i; + + /* + * The "rpath" pledge could be revoked after mparse_readfd() + * if the file desciptor to "/footer.html" would be opened + * up front, but it's probably not worth the complication + * of the code it would cause: it would require scattering + * pledge() calls in multiple low-level resp_*() functions. + */ + + if (pledge("stdio rpath", NULL) == -1) { + warn("pledge"); + pg_error_internal(); + return EXIT_FAILURE; + } + + /* Poor man's ReDoS mitigation. */ + + itimer.it_value.tv_sec = 2; + itimer.it_value.tv_usec = 0; + itimer.it_interval.tv_sec = 2; + itimer.it_interval.tv_usec = 0; + if (setitimer(ITIMER_VIRTUAL, &itimer, NULL) == -1) { + warn("setitimer"); + pg_error_internal(); + return EXIT_FAILURE; + } + + /* + * First we change directory into the MAN_DIR so that + * subsequent scanning for manpath directories is rooted + * relative to the same position. + */ + + if (chdir(MAN_DIR) == -1) { + warn("MAN_DIR: %s", MAN_DIR); + pg_error_internal(); + return EXIT_FAILURE; + } + + memset(&req, 0, sizeof(struct req)); + req.q.equal = 1; + parse_manpath_conf(&req); + + /* Parse the path info and the query string. */ + + if ((path = getenv("PATH_INFO")) == NULL) + path = ""; + else if (*path == '/') + path++; + + if (*path != '\0') { + parse_path_info(&req, path); + if (req.q.manpath == NULL || req.q.sec == NULL || + *req.q.query == '\0' || access(path, F_OK) == -1) + path = ""; + } else if ((querystring = getenv("QUERY_STRING")) != NULL) + parse_query_string(&req, querystring); + + /* Validate parsed data and add defaults. */ + + if (req.q.manpath == NULL) + req.q.manpath = mandoc_strdup(req.p[0]); + else if ( ! validate_manpath(&req, req.q.manpath)) { + pg_error_badrequest( + "You specified an invalid manpath."); + return EXIT_FAILURE; + } + + if (req.q.arch != NULL && validate_arch(req.q.arch) == 0) { + pg_error_badrequest( + "You specified an invalid architecture."); + return EXIT_FAILURE; + } + + /* Dispatch to the three different pages. */ + + if ('\0' != *path) + pg_show(&req, path); + else if (NULL != req.q.query) + pg_search(&req); + else + pg_index(&req); + + free(req.q.manpath); + free(req.q.arch); + free(req.q.sec); + free(req.q.query); + for (i = 0; i < (int)req.psz; i++) + free(req.p[i]); + free(req.p); + return EXIT_SUCCESS; +} + +/* + * Translate PATH_INFO to a query. + */ +static void +parse_path_info(struct req *req, const char *path) +{ + const char *name, *sec, *end; + + req->isquery = 0; + req->q.equal = 1; + req->q.manpath = NULL; + req->q.arch = NULL; + + /* Mandatory manual page name. */ + if ((name = strrchr(path, '/')) == NULL) + name = path; + else + name++; + + /* Optional trailing section. */ + sec = strrchr(name, '.'); + if (sec != NULL && isdigit((unsigned char)*++sec)) { + req->q.query = mandoc_strndup(name, sec - name - 1); + req->q.sec = mandoc_strdup(sec); + } else { + req->q.query = mandoc_strdup(name); + req->q.sec = NULL; + } + + /* Handle the case of name[.section] only. */ + if (name == path) + return; + + /* Optional manpath. */ + end = strchr(path, '/'); + req->q.manpath = mandoc_strndup(path, end - path); + if (validate_manpath(req, req->q.manpath)) { + path = end + 1; + if (name == path) + return; + } else { + free(req->q.manpath); + req->q.manpath = NULL; + } + + /* Optional section. */ + if (strncmp(path, "man", 3) == 0 || strncmp(path, "cat", 3) == 0) { + path += 3; + end = strchr(path, '/'); + free(req->q.sec); + req->q.sec = mandoc_strndup(path, end - path); + path = end + 1; + if (name == path) + return; + } + + /* Optional architecture. */ + end = strchr(path, '/'); + if (end + 1 != name) { + pg_error_badrequest( + "You specified too many directory components."); + exit(EXIT_FAILURE); + } + req->q.arch = mandoc_strndup(path, end - path); + if (validate_arch(req->q.arch) == 0) { + pg_error_badrequest( + "You specified an invalid directory component."); + exit(EXIT_FAILURE); + } +} + +/* + * Scan for indexable paths. + */ +static void +parse_manpath_conf(struct req *req) +{ + FILE *fp; + char *dp; + size_t dpsz; + ssize_t len; + + if ((fp = fopen("manpath.conf", "r")) == NULL) { + warn("%s/manpath.conf", MAN_DIR); + pg_error_internal(); + exit(EXIT_FAILURE); + } + + dp = NULL; + dpsz = 0; + + while ((len = getline(&dp, &dpsz, fp)) != -1) { + if (dp[len - 1] == '\n') + dp[--len] = '\0'; + req->p = mandoc_realloc(req->p, + (req->psz + 1) * sizeof(char *)); + if ( ! validate_urifrag(dp)) { + warnx("%s/manpath.conf contains " + "unsafe path \"%s\"", MAN_DIR, dp); + pg_error_internal(); + exit(EXIT_FAILURE); + } + if (strchr(dp, '/') != NULL) { + warnx("%s/manpath.conf contains " + "path with slash \"%s\"", MAN_DIR, dp); + pg_error_internal(); + exit(EXIT_FAILURE); + } + req->p[req->psz++] = dp; + dp = NULL; + dpsz = 0; + } + free(dp); + + if (req->p == NULL) { + warnx("%s/manpath.conf is empty", MAN_DIR); + pg_error_internal(); + exit(EXIT_FAILURE); + } +} diff --git a/usr.bin/mandoc/cgi.h.example b/usr.bin/mandoc/cgi.h.example new file mode 100644 index 0000000..2ccbe25 --- /dev/null +++ b/usr.bin/mandoc/cgi.h.example @@ -0,0 +1,7 @@ +/* Example compile-time configuration file for man.cgi(8). */ + +#define SCRIPT_NAME "cgi-bin/man.cgi" +#define MAN_DIR "/man" +#define CSS_DIR "" +#define CUSTOMIZE_TITLE "Manual pages with mandoc" +#define COMPAT_OLDURI Yes diff --git a/usr.bin/mandoc/chars.c b/usr.bin/mandoc/chars.c new file mode 100644 index 0000000..d091ab2 --- /dev/null +++ b/usr.bin/mandoc/chars.c @@ -0,0 +1,506 @@ +/* $OpenBSD: chars.c,v 1.49 2020/02/13 16:16:03 schwarze Exp $ */ +/* + * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons + * Copyright (c) 2011, 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 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 +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "mandoc.h" +#include "mandoc_aux.h" +#include "mandoc_ohash.h" +#include "libmandoc.h" + +struct ln { + const char roffcode[16]; + const char *ascii; + int unicode; +}; + +/* Special break control characters. */ +static const char ascii_nbrsp[2] = { ASCII_NBRSP, '\0' }; +static const char ascii_break[2] = { ASCII_BREAK, '\0' }; + +static struct ln lines[] = { + + /* Spacing. */ + { " ", ascii_nbrsp, 0x00a0 }, + { "~", ascii_nbrsp, 0x00a0 }, + { "0", ascii_nbrsp, 0x00a0 }, + { ":", ascii_break, 0 }, + + /* Lines. */ + { "ba", "|", 0x007c }, + { "br", "|", 0x2502 }, + { "ul", "_", 0x005f }, + { "_", "_", 0x005f }, + { "ru", "_", 0x005f }, + { "rn", "-", 0x203e }, + { "bb", "|", 0x00a6 }, + { "sl", "/", 0x002f }, + { "rs", "\\", 0x005c }, + + /* Text markers. */ + { "ci", "O", 0x25cb }, + { "bu", "+\bo", 0x2022 }, + { "dd", "<**>", 0x2021 }, + { "dg", "<*>", 0x2020 }, + { "lz", "<>", 0x25ca }, + { "sq", "[]", 0x25a1 }, + { "ps", "", 0x00b6 }, + { "sc", "
", 0x00a7 }, + { "lh", "<=", 0x261c }, + { "rh", "=>", 0x261e }, + { "at", "@", 0x0040 }, + { "sh", "#", 0x0023 }, + { "CR", "", 0x21b5 }, + { "OK", "\\/", 0x2713 }, + { "CL", "C", 0x2663 }, + { "SP", "S", 0x2660 }, + { "HE", "H", 0x2665 }, + { "DI", "D", 0x2666 }, + + /* Legal symbols. */ + { "co", "(C)", 0x00a9 }, + { "rg", "(R)", 0x00ae }, + { "tm", "tm", 0x2122 }, + + /* Punctuation. */ + { "em", "--", 0x2014 }, + { "en", "-", 0x2013 }, + { "hy", "-", 0x2010 }, + { "e", "\\", 0x005c }, + { ".", ".", 0x002e }, + { "r!", "!", 0x00a1 }, + { "r?", "?", 0x00bf }, + + /* Quotes. */ + { "Bq", ",,", 0x201e }, + { "bq", ",", 0x201a }, + { "lq", "\"", 0x201c }, + { "rq", "\"", 0x201d }, + { "Lq", "\"", 0x201c }, + { "Rq", "\"", 0x201d }, + { "oq", "`", 0x2018 }, + { "cq", "\'", 0x2019 }, + { "aq", "\'", 0x0027 }, + { "dq", "\"", 0x0022 }, + { "Fo", "<<", 0x00ab }, + { "Fc", ">>", 0x00bb }, + { "fo", "<", 0x2039 }, + { "fc", ">", 0x203a }, + + /* Brackets. */ + { "lB", "[", 0x005b }, + { "rB", "]", 0x005d }, + { "lC", "{", 0x007b }, + { "rC", "}", 0x007d }, + { "la", "<", 0x27e8 }, + { "ra", ">", 0x27e9 }, + { "bv", "|", 0x23aa }, + { "braceex", "|", 0x23aa }, + { "bracketlefttp", "|", 0x23a1 }, + { "bracketleftbt", "|", 0x23a3 }, + { "bracketleftex", "|", 0x23a2 }, + { "bracketrighttp", "|", 0x23a4 }, + { "bracketrightbt", "|", 0x23a6 }, + { "bracketrightex", "|", 0x23a5 }, + { "lt", ",-", 0x23a7 }, + { "bracelefttp", ",-", 0x23a7 }, + { "lk", "{", 0x23a8 }, + { "braceleftmid", "{", 0x23a8 }, + { "lb", "`-", 0x23a9 }, + { "braceleftbt", "`-", 0x23a9 }, + { "braceleftex", "|", 0x23aa }, + { "rt", "-.", 0x23ab }, + { "bracerighttp", "-.", 0x23ab }, + { "rk", "}", 0x23ac }, + { "bracerightmid", "}", 0x23ac }, + { "rb", "-\'", 0x23ad }, + { "bracerightbt", "-\'", 0x23ad }, + { "bracerightex", "|", 0x23aa }, + { "parenlefttp", "/", 0x239b }, + { "parenleftbt", "\\", 0x239d }, + { "parenleftex", "|", 0x239c }, + { "parenrighttp", "\\", 0x239e }, + { "parenrightbt", "/", 0x23a0 }, + { "parenrightex", "|", 0x239f }, + + /* Arrows and lines. */ + { "<-", "<-", 0x2190 }, + { "->", "->", 0x2192 }, + { "<>", "<->", 0x2194 }, + { "da", "|\bv", 0x2193 }, + { "ua", "|\b^", 0x2191 }, + { "va", "^v", 0x2195 }, + { "lA", "<=", 0x21d0 }, + { "rA", "=>", 0x21d2 }, + { "hA", "<=>", 0x21d4 }, + { "uA", "=\b^", 0x21d1 }, + { "dA", "=\bv", 0x21d3 }, + { "vA", "^=v", 0x21d5 }, + { "an", "-", 0x23af }, + + /* Logic. */ + { "AN", "^", 0x2227 }, + { "OR", "v", 0x2228 }, + { "no", "~", 0x00ac }, + { "tno", "~", 0x00ac }, + { "te", "", 0x2203 }, + { "fa", "", 0x2200 }, + { "st", "", 0x220b }, + { "tf", "", 0x2234 }, + { "3d", "", 0x2234 }, + { "or", "|", 0x007c }, + + /* Mathematicals. */ + { "pl", "+", 0x002b }, + { "mi", "-", 0x2212 }, + { "-", "-", 0x002d }, + { "-+", "-+", 0x2213 }, + { "+-", "+-", 0x00b1 }, + { "t+-", "+-", 0x00b1 }, + { "pc", ".", 0x00b7 }, + { "md", ".", 0x22c5 }, + { "mu", "x", 0x00d7 }, + { "tmu", "x", 0x00d7 }, + { "c*", "O\bx", 0x2297 }, + { "c+", "O\b+", 0x2295 }, + { "di", "/", 0x00f7 }, + { "tdi", "/", 0x00f7 }, + { "f/", "/", 0x2044 }, + { "**", "*", 0x2217 }, + { "<=", "<=", 0x2264 }, + { ">=", ">=", 0x2265 }, + { "<<", "<<", 0x226a }, + { ">>", ">>", 0x226b }, + { "eq", "=", 0x003d }, + { "!=", "!=", 0x2260 }, + { "==", "==", 0x2261 }, + { "ne", "!==", 0x2262 }, + { "ap", "~", 0x223c }, + { "|=", "-~", 0x2243 }, + { "=~", "=~", 0x2245 }, + { "~~", "~~", 0x2248 }, + { "~=", "~=", 0x2248 }, + { "pt", "", 0x221d }, + { "es", "{}", 0x2205 }, + { "mo", "", 0x2208 }, + { "nm", "", 0x2209 }, + { "sb", "", 0x2282 }, + { "nb", "", 0x2284 }, + { "sp", "", 0x2283 }, + { "nc", "", 0x2285 }, + { "ib", "", 0x2286 }, + { "ip", "", 0x2287 }, + { "ca", "", 0x2229 }, + { "cu", "", 0x222a }, + { "/_", "", 0x2220 }, + { "pp", "", 0x22a5 }, + { "is", "", 0x222b }, + { "integral", "", 0x222b }, + { "sum", "", 0x2211 }, + { "product", "", 0x220f }, + { "coproduct", "", 0x2210 }, + { "gr", "", 0x2207 }, + { "sr", "", 0x221a }, + { "sqrt", "", 0x221a }, + { "lc", "|~", 0x2308 }, + { "rc", "~|", 0x2309 }, + { "lf", "|_", 0x230a }, + { "rf", "_|", 0x230b }, + { "if", "", 0x221e }, + { "Ah", "", 0x2135 }, + { "Im", "", 0x2111 }, + { "Re", "", 0x211c }, + { "wp", "p", 0x2118 }, + { "pd", "", 0x2202 }, + { "-h", "/h", 0x210f }, + { "hbar", "/h", 0x210f }, + { "12", "1/2", 0x00bd }, + { "14", "1/4", 0x00bc }, + { "34", "3/4", 0x00be }, + { "18", "1/8", 0x215B }, + { "38", "3/8", 0x215C }, + { "58", "5/8", 0x215D }, + { "78", "7/8", 0x215E }, + { "S1", "^1", 0x00B9 }, + { "S2", "^2", 0x00B2 }, + { "S3", "^3", 0x00B3 }, + + /* Ligatures. */ + { "ff", "ff", 0xfb00 }, + { "fi", "fi", 0xfb01 }, + { "fl", "fl", 0xfb02 }, + { "Fi", "ffi", 0xfb03 }, + { "Fl", "ffl", 0xfb04 }, + { "AE", "AE", 0x00c6 }, + { "ae", "ae", 0x00e6 }, + { "OE", "OE", 0x0152 }, + { "oe", "oe", 0x0153 }, + { "ss", "ss", 0x00df }, + { "IJ", "IJ", 0x0132 }, + { "ij", "ij", 0x0133 }, + + /* Accents. */ + { "a\"", "\"", 0x02dd }, + { "a-", "-", 0x00af }, + { "a.", ".", 0x02d9 }, + { "a^", "^", 0x005e }, + { "aa", "\'", 0x00b4 }, + { "\'", "\'", 0x00b4 }, + { "ga", "`", 0x0060 }, + { "`", "`", 0x0060 }, + { "ab", "'\b`", 0x02d8 }, + { "ac", ",", 0x00b8 }, + { "ad", "\"", 0x00a8 }, + { "ah", "v", 0x02c7 }, + { "ao", "o", 0x02da }, + { "a~", "~", 0x007e }, + { "ho", ",", 0x02db }, + { "ha", "^", 0x005e }, + { "ti", "~", 0x007e }, + { "u02DC", "~", 0x02dc }, + + /* Accented letters. */ + { "'A", "'\bA", 0x00c1 }, + { "'E", "'\bE", 0x00c9 }, + { "'I", "'\bI", 0x00cd }, + { "'O", "'\bO", 0x00d3 }, + { "'U", "'\bU", 0x00da }, + { "'Y", "'\bY", 0x00dd }, + { "'a", "'\ba", 0x00e1 }, + { "'e", "'\be", 0x00e9 }, + { "'i", "'\bi", 0x00ed }, + { "'o", "'\bo", 0x00f3 }, + { "'u", "'\bu", 0x00fa }, + { "'y", "'\by", 0x00fd }, + { "`A", "`\bA", 0x00c0 }, + { "`E", "`\bE", 0x00c8 }, + { "`I", "`\bI", 0x00cc }, + { "`O", "`\bO", 0x00d2 }, + { "`U", "`\bU", 0x00d9 }, + { "`a", "`\ba", 0x00e0 }, + { "`e", "`\be", 0x00e8 }, + { "`i", "`\bi", 0x00ec }, + { "`o", "`\bo", 0x00f2 }, + { "`u", "`\bu", 0x00f9 }, + { "~A", "~\bA", 0x00c3 }, + { "~N", "~\bN", 0x00d1 }, + { "~O", "~\bO", 0x00d5 }, + { "~a", "~\ba", 0x00e3 }, + { "~n", "~\bn", 0x00f1 }, + { "~o", "~\bo", 0x00f5 }, + { ":A", "\"\bA", 0x00c4 }, + { ":E", "\"\bE", 0x00cb }, + { ":I", "\"\bI", 0x00cf }, + { ":O", "\"\bO", 0x00d6 }, + { ":U", "\"\bU", 0x00dc }, + { ":a", "\"\ba", 0x00e4 }, + { ":e", "\"\be", 0x00eb }, + { ":i", "\"\bi", 0x00ef }, + { ":o", "\"\bo", 0x00f6 }, + { ":u", "\"\bu", 0x00fc }, + { ":y", "\"\by", 0x00ff }, + { "^A", "^\bA", 0x00c2 }, + { "^E", "^\bE", 0x00ca }, + { "^I", "^\bI", 0x00ce }, + { "^O", "^\bO", 0x00d4 }, + { "^U", "^\bU", 0x00db }, + { "^a", "^\ba", 0x00e2 }, + { "^e", "^\be", 0x00ea }, + { "^i", "^\bi", 0x00ee }, + { "^o", "^\bo", 0x00f4 }, + { "^u", "^\bu", 0x00fb }, + { ",C", ",\bC", 0x00c7 }, + { ",c", ",\bc", 0x00e7 }, + { "/L", "/\bL", 0x0141 }, + { "/l", "/\bl", 0x0142 }, + { "/O", "/\bO", 0x00d8 }, + { "/o", "/\bo", 0x00f8 }, + { "oA", "o\bA", 0x00c5 }, + { "oa", "o\ba", 0x00e5 }, + + /* Special letters. */ + { "-D", "Dh", 0x00d0 }, + { "Sd", "dh", 0x00f0 }, + { "TP", "Th", 0x00de }, + { "Tp", "th", 0x00fe }, + { ".i", "i", 0x0131 }, + { ".j", "j", 0x0237 }, + + /* Currency. */ + { "Do", "$", 0x0024 }, + { "ct", "/\bc", 0x00a2 }, + { "Eu", "EUR", 0x20ac }, + { "eu", "EUR", 0x20ac }, + { "Ye", "=\bY", 0x00a5 }, + { "Po", "-\bL", 0x00a3 }, + { "Cs", "o\bx", 0x00a4 }, + { "Fn", ",\bf", 0x0192 }, + + /* Units. */ + { "de", "", 0x00b0 }, + { "%0", "", 0x2030 }, + { "fm", "\'", 0x2032 }, + { "sd", "''", 0x2033 }, + { "mc", "", 0x00b5 }, + { "Of", "_\ba", 0x00aa }, + { "Om", "_\bo", 0x00ba }, + + /* Greek characters. */ + { "*A", "A", 0x0391 }, + { "*B", "B", 0x0392 }, + { "*G", "", 0x0393 }, + { "*D", "", 0x0394 }, + { "*E", "E", 0x0395 }, + { "*Z", "Z", 0x0396 }, + { "*Y", "H", 0x0397 }, + { "*H", "", 0x0398 }, + { "*I", "I", 0x0399 }, + { "*K", "K", 0x039a }, + { "*L", "", 0x039b }, + { "*M", "M", 0x039c }, + { "*N", "N", 0x039d }, + { "*C", "", 0x039e }, + { "*O", "O", 0x039f }, + { "*P", "", 0x03a0 }, + { "*R", "P", 0x03a1 }, + { "*S", "", 0x03a3 }, + { "*T", "T", 0x03a4 }, + { "*U", "Y", 0x03a5 }, + { "*F", "", 0x03a6 }, + { "*X", "X", 0x03a7 }, + { "*Q", "", 0x03a8 }, + { "*W", "", 0x03a9 }, + { "*a", "", 0x03b1 }, + { "*b", "", 0x03b2 }, + { "*g", "", 0x03b3 }, + { "*d", "", 0x03b4 }, + { "*e", "", 0x03b5 }, + { "*z", "", 0x03b6 }, + { "*y", "", 0x03b7 }, + { "*h", "", 0x03b8 }, + { "*i", "", 0x03b9 }, + { "*k", "", 0x03ba }, + { "*l", "", 0x03bb }, + { "*m", "", 0x03bc }, + { "*n", "", 0x03bd }, + { "*c", "", 0x03be }, + { "*o", "o", 0x03bf }, + { "*p", "", 0x03c0 }, + { "*r", "", 0x03c1 }, + { "*s", "", 0x03c3 }, + { "*t", "", 0x03c4 }, + { "*u", "", 0x03c5 }, + { "*f", "", 0x03d5 }, + { "*x", "", 0x03c7 }, + { "*q", "", 0x03c8 }, + { "*w", "", 0x03c9 }, + { "+h", "", 0x03d1 }, + { "+f", "", 0x03c6 }, + { "+p", "", 0x03d6 }, + { "+e", "", 0x03f5 }, + { "ts", "", 0x03c2 }, +}; + +static struct ohash mchars; + + +void +mchars_free(void) +{ + + ohash_delete(&mchars); +} + +void +mchars_alloc(void) +{ + size_t i; + unsigned int slot; + + mandoc_ohash_init(&mchars, 9, offsetof(struct ln, roffcode)); + for (i = 0; i < sizeof(lines)/sizeof(lines[0]); i++) { + slot = ohash_qlookup(&mchars, lines[i].roffcode); + assert(ohash_find(&mchars, slot) == NULL); + ohash_insert(&mchars, slot, lines + i); + } +} + +int +mchars_spec2cp(const char *p, size_t sz) +{ + const struct ln *ln; + const char *end; + + end = p + sz; + ln = ohash_find(&mchars, ohash_qlookupi(&mchars, p, &end)); + return ln != NULL ? ln->unicode : -1; +} + +int +mchars_num2char(const char *p, size_t sz) +{ + int i; + + i = mandoc_strntoi(p, sz, 10); + return i >= 0 && i < 256 ? i : -1; +} + +int +mchars_num2uc(const char *p, size_t sz) +{ + int i; + + i = mandoc_strntoi(p, sz, 16); + assert(i >= 0 && i <= 0x10FFFF); + return i; +} + +const char * +mchars_spec2str(const char *p, size_t sz, size_t *rsz) +{ + const struct ln *ln; + const char *end; + + end = p + sz; + ln = ohash_find(&mchars, ohash_qlookupi(&mchars, p, &end)); + if (ln == NULL) + return NULL; + + *rsz = strlen(ln->ascii); + return ln->ascii; +} + +const char * +mchars_uc2str(int uc) +{ + size_t i; + + for (i = 0; i < sizeof(lines)/sizeof(lines[0]); i++) + if (uc == lines[i].unicode) + return lines[i].ascii; + return ""; +} diff --git a/usr.bin/mandoc/dba.c b/usr.bin/mandoc/dba.c new file mode 100644 index 0000000..36d4bb7 --- /dev/null +++ b/usr.bin/mandoc/dba.c @@ -0,0 +1,501 @@ +/* $OpenBSD: dba.c,v 1.7 2017/02/09 18:26:17 schwarze Exp $ */ +/* + * Copyright (c) 2016, 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 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. + * + * Allocation-based version of the mandoc database, for read-write access. + * The interface is defined in "dba.h". + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mandoc_aux.h" +#include "mandoc_ohash.h" +#include "mansearch.h" +#include "dba_write.h" +#include "dba_array.h" +#include "dba.h" + +struct macro_entry { + struct dba_array *pages; + char value[]; +}; + +static void *prepend(const char *, char); +static void dba_pages_write(struct dba_array *); +static int compare_names(const void *, const void *); +static int compare_strings(const void *, const void *); + +static struct macro_entry + *get_macro_entry(struct ohash *, const char *, int32_t); +static void dba_macros_write(struct dba_array *); +static void dba_macro_write(struct ohash *); +static int compare_entries(const void *, const void *); + + +/*** top-level functions **********************************************/ + +struct dba * +dba_new(int32_t npages) +{ + struct dba *dba; + struct ohash *macro; + int32_t im; + + dba = mandoc_malloc(sizeof(*dba)); + dba->pages = dba_array_new(npages, DBA_GROW); + dba->macros = dba_array_new(MACRO_MAX, 0); + for (im = 0; im < MACRO_MAX; im++) { + macro = mandoc_malloc(sizeof(*macro)); + mandoc_ohash_init(macro, 4, + offsetof(struct macro_entry, value)); + dba_array_set(dba->macros, im, macro); + } + return dba; +} + +void +dba_free(struct dba *dba) +{ + struct dba_array *page; + struct ohash *macro; + struct macro_entry *entry; + unsigned int slot; + + dba_array_FOREACH(dba->macros, macro) { + for (entry = ohash_first(macro, &slot); entry != NULL; + entry = ohash_next(macro, &slot)) { + dba_array_free(entry->pages); + free(entry); + } + ohash_delete(macro); + free(macro); + } + dba_array_free(dba->macros); + + dba_array_undel(dba->pages); + dba_array_FOREACH(dba->pages, page) { + dba_array_free(dba_array_get(page, DBP_NAME)); + dba_array_free(dba_array_get(page, DBP_SECT)); + dba_array_free(dba_array_get(page, DBP_ARCH)); + free(dba_array_get(page, DBP_DESC)); + dba_array_free(dba_array_get(page, DBP_FILE)); + dba_array_free(page); + } + dba_array_free(dba->pages); + + free(dba); +} + +/* + * Write the complete mandoc database to disk; the format is: + * - One integer each for magic and version. + * - One pointer each to the macros table and to the final magic. + * - The pages table. + * - The macros table. + * - And at the very end, the magic integer again. + */ +int +dba_write(const char *fname, struct dba *dba) +{ + int save_errno; + int32_t pos_end, pos_macros, pos_macros_ptr; + + if (dba_open(fname) == -1) + return -1; + dba_int_write(MANDOCDB_MAGIC); + dba_int_write(MANDOCDB_VERSION); + pos_macros_ptr = dba_skip(1, 2); + dba_pages_write(dba->pages); + pos_macros = dba_tell(); + dba_macros_write(dba->macros); + pos_end = dba_tell(); + dba_int_write(MANDOCDB_MAGIC); + dba_seek(pos_macros_ptr); + dba_int_write(pos_macros); + dba_int_write(pos_end); + if (dba_close() == -1) { + save_errno = errno; + unlink(fname); + errno = save_errno; + return -1; + } + return 0; +} + + +/*** functions for handling pages *************************************/ + +/* + * Create a new page and append it to the pages table. + */ +struct dba_array * +dba_page_new(struct dba_array *pages, const char *arch, + const char *desc, const char *file, enum form form) +{ + struct dba_array *page, *entry; + + page = dba_array_new(DBP_MAX, 0); + entry = dba_array_new(1, DBA_STR | DBA_GROW); + dba_array_add(page, entry); + entry = dba_array_new(1, DBA_STR | DBA_GROW); + dba_array_add(page, entry); + if (arch != NULL && *arch != '\0') { + entry = dba_array_new(1, DBA_STR | DBA_GROW); + dba_array_add(entry, (void *)arch); + } else + entry = NULL; + dba_array_add(page, entry); + dba_array_add(page, mandoc_strdup(desc)); + entry = dba_array_new(1, DBA_STR | DBA_GROW); + dba_array_add(entry, prepend(file, form)); + dba_array_add(page, entry); + dba_array_add(pages, page); + return page; +} + +/* + * Add a section, architecture, or file name to an existing page. + * Passing the NULL pointer for the architecture makes the page MI. + * In that case, any earlier or later architectures are ignored. + */ +void +dba_page_add(struct dba_array *page, int32_t ie, const char *str) +{ + struct dba_array *entries; + char *entry; + + entries = dba_array_get(page, ie); + if (ie == DBP_ARCH) { + if (entries == NULL) + return; + if (str == NULL || *str == '\0') { + dba_array_free(entries); + dba_array_set(page, DBP_ARCH, NULL); + return; + } + } + if (*str == '\0') + return; + dba_array_FOREACH(entries, entry) { + if (ie == DBP_FILE && *entry < ' ') + entry++; + if (strcmp(entry, str) == 0) + return; + } + dba_array_add(entries, (void *)str); +} + +/* + * Add an additional name to an existing page. + */ +void +dba_page_alias(struct dba_array *page, const char *name, uint64_t mask) +{ + struct dba_array *entries; + char *entry; + char maskbyte; + + if (*name == '\0') + return; + maskbyte = mask & NAME_MASK; + entries = dba_array_get(page, DBP_NAME); + dba_array_FOREACH(entries, entry) { + if (strcmp(entry + 1, name) == 0) { + *entry |= maskbyte; + return; + } + } + dba_array_add(entries, prepend(name, maskbyte)); +} + +/* + * Return a pointer to a temporary copy of instr with inbyte prepended. + */ +static void * +prepend(const char *instr, char inbyte) +{ + static char *outstr = NULL; + static size_t outlen = 0; + size_t newlen; + + newlen = strlen(instr) + 1; + if (newlen > outlen) { + outstr = mandoc_realloc(outstr, newlen + 1); + outlen = newlen; + } + *outstr = inbyte; + memcpy(outstr + 1, instr, newlen); + return outstr; +} + +/* + * Write the pages table to disk; the format is: + * - One integer containing the number of pages. + * - For each page, five pointers to the names, sections, + * architectures, description, and file names of the page. + * MI pages write 0 instead of the architecture pointer. + * - One list each for names, sections, architectures, descriptions and + * file names. The description for each page ends with a NUL byte. + * For all the other lists, each string ends with a NUL byte, + * and the last string for a page ends with two NUL bytes. + * - To assure alignment of following integers, + * the end is padded with NUL bytes up to a multiple of four bytes. + */ +static void +dba_pages_write(struct dba_array *pages) +{ + struct dba_array *page, *entry; + int32_t pos_pages, pos_end; + + pos_pages = dba_array_writelen(pages, 5); + dba_array_FOREACH(pages, page) { + dba_array_setpos(page, DBP_NAME, dba_tell()); + entry = dba_array_get(page, DBP_NAME); + dba_array_sort(entry, compare_names); + dba_array_writelst(entry); + } + dba_array_FOREACH(pages, page) { + dba_array_setpos(page, DBP_SECT, dba_tell()); + entry = dba_array_get(page, DBP_SECT); + dba_array_sort(entry, compare_strings); + dba_array_writelst(entry); + } + dba_array_FOREACH(pages, page) { + if ((entry = dba_array_get(page, DBP_ARCH)) != NULL) { + dba_array_setpos(page, DBP_ARCH, dba_tell()); + dba_array_sort(entry, compare_strings); + dba_array_writelst(entry); + } else + dba_array_setpos(page, DBP_ARCH, 0); + } + dba_array_FOREACH(pages, page) { + dba_array_setpos(page, DBP_DESC, dba_tell()); + dba_str_write(dba_array_get(page, DBP_DESC)); + } + dba_array_FOREACH(pages, page) { + dba_array_setpos(page, DBP_FILE, dba_tell()); + dba_array_writelst(dba_array_get(page, DBP_FILE)); + } + pos_end = dba_align(); + dba_seek(pos_pages); + dba_array_FOREACH(pages, page) + dba_array_writepos(page); + dba_seek(pos_end); +} + +static int +compare_names(const void *vp1, const void *vp2) +{ + const char *cp1, *cp2; + int diff; + + cp1 = *(const char * const *)vp1; + cp2 = *(const char * const *)vp2; + return (diff = *cp2 - *cp1) ? diff : + strcasecmp(cp1 + 1, cp2 + 1); +} + +static int +compare_strings(const void *vp1, const void *vp2) +{ + const char *cp1, *cp2; + + cp1 = *(const char * const *)vp1; + cp2 = *(const char * const *)vp2; + return strcmp(cp1, cp2); +} + +/*** functions for handling macros ************************************/ + +/* + * In the hash table for a single macro, look up an entry by + * the macro value or add an empty one if it doesn't exist yet. + */ +static struct macro_entry * +get_macro_entry(struct ohash *macro, const char *value, int32_t np) +{ + struct macro_entry *entry; + size_t len; + unsigned int slot; + + slot = ohash_qlookup(macro, value); + if ((entry = ohash_find(macro, slot)) == NULL) { + len = strlen(value) + 1; + entry = mandoc_malloc(sizeof(*entry) + len); + memcpy(&entry->value, value, len); + entry->pages = dba_array_new(np, DBA_GROW); + ohash_insert(macro, slot, entry); + } + return entry; +} + +/* + * In addition to get_macro_entry(), add multiple page references, + * converting them from the on-disk format (byte offsets in the file) + * to page pointers in memory. + */ +void +dba_macro_new(struct dba *dba, int32_t im, const char *value, + const int32_t *pp) +{ + struct macro_entry *entry; + const int32_t *ip; + int32_t np; + + np = 0; + for (ip = pp; *ip; ip++) + np++; + + entry = get_macro_entry(dba_array_get(dba->macros, im), value, np); + for (ip = pp; *ip; ip++) + dba_array_add(entry->pages, dba_array_get(dba->pages, + be32toh(*ip) / 5 / sizeof(*ip) - 1)); +} + +/* + * In addition to get_macro_entry(), add one page reference, + * directly taking the in-memory page pointer as an argument. + */ +void +dba_macro_add(struct dba_array *macros, int32_t im, const char *value, + struct dba_array *page) +{ + struct macro_entry *entry; + + if (*value == '\0') + return; + entry = get_macro_entry(dba_array_get(macros, im), value, 1); + dba_array_add(entry->pages, page); +} + +/* + * Write the macros table to disk; the format is: + * - The number of macro tables (actually, MACRO_MAX). + * - That number of pointers to the individual macro tables. + * - The individual macro tables. + */ +static void +dba_macros_write(struct dba_array *macros) +{ + struct ohash *macro; + int32_t im, pos_macros, pos_end; + + pos_macros = dba_array_writelen(macros, 1); + im = 0; + dba_array_FOREACH(macros, macro) { + dba_array_setpos(macros, im++, dba_tell()); + dba_macro_write(macro); + } + pos_end = dba_tell(); + dba_seek(pos_macros); + dba_array_writepos(macros); + dba_seek(pos_end); +} + +/* + * Write one individual macro table to disk; the format is: + * - The number of entries in the table. + * - For each entry, two pointers, the first one to the value + * and the second one to the list of pages. + * - A list of values, each ending in a NUL byte. + * - To assure alignment of following integers, + * padding with NUL bytes up to a multiple of four bytes. + * - A list of pointers to pages, each list ending in a 0 integer. + */ +static void +dba_macro_write(struct ohash *macro) +{ + struct macro_entry **entries, *entry; + struct dba_array *page; + int32_t *kpos, *dpos; + unsigned int ie, ne, slot; + int use; + int32_t addr, pos_macro, pos_end; + + /* Temporary storage for filtering and sorting. */ + + ne = ohash_entries(macro); + entries = mandoc_reallocarray(NULL, ne, sizeof(*entries)); + kpos = mandoc_reallocarray(NULL, ne, sizeof(*kpos)); + dpos = mandoc_reallocarray(NULL, ne, sizeof(*dpos)); + + /* Build a list of non-empty entries and sort it. */ + + ne = 0; + for (entry = ohash_first(macro, &slot); entry != NULL; + entry = ohash_next(macro, &slot)) { + use = 0; + dba_array_FOREACH(entry->pages, page) + if (dba_array_getpos(page)) + use = 1; + if (use) + entries[ne++] = entry; + } + qsort(entries, ne, sizeof(*entries), compare_entries); + + /* Number of entries, and space for the pointer pairs. */ + + dba_int_write(ne); + pos_macro = dba_skip(2, ne); + + /* String table. */ + + for (ie = 0; ie < ne; ie++) { + kpos[ie] = dba_tell(); + dba_str_write(entries[ie]->value); + } + dba_align(); + + /* Pages table. */ + + for (ie = 0; ie < ne; ie++) { + dpos[ie] = dba_tell(); + dba_array_FOREACH(entries[ie]->pages, page) + if ((addr = dba_array_getpos(page))) + dba_int_write(addr); + dba_int_write(0); + } + pos_end = dba_tell(); + + /* Fill in the pointer pairs. */ + + dba_seek(pos_macro); + for (ie = 0; ie < ne; ie++) { + dba_int_write(kpos[ie]); + dba_int_write(dpos[ie]); + } + dba_seek(pos_end); + + free(entries); + free(kpos); + free(dpos); +} + +static int +compare_entries(const void *vp1, const void *vp2) +{ + const struct macro_entry *ep1, *ep2; + + ep1 = *(const struct macro_entry * const *)vp1; + ep2 = *(const struct macro_entry * const *)vp2; + return strcmp(ep1->value, ep2->value); +} diff --git a/usr.bin/mandoc/dba.h b/usr.bin/mandoc/dba.h new file mode 100644 index 0000000..7787958 --- /dev/null +++ b/usr.bin/mandoc/dba.h @@ -0,0 +1,50 @@ +/* $OpenBSD: dba.h,v 1.2 2016/08/17 20:46:06 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Public interface of the allocation-based version + * of the mandoc database, for read-write access. + * To be used by dba.c, dba_read.c, and makewhatis(8). + */ + +#define DBP_NAME 0 +#define DBP_SECT 1 +#define DBP_ARCH 2 +#define DBP_DESC 3 +#define DBP_FILE 4 +#define DBP_MAX 5 + +struct dba_array; + +struct dba { + struct dba_array *pages; + struct dba_array *macros; +}; + + +struct dba *dba_new(int32_t); +void dba_free(struct dba *); +struct dba *dba_read(const char *); +int dba_write(const char *, struct dba *); + +struct dba_array *dba_page_new(struct dba_array *, const char *, + const char *, const char *, enum form); +void dba_page_add(struct dba_array *, int32_t, const char *); +void dba_page_alias(struct dba_array *, const char *, uint64_t); + +void dba_macro_new(struct dba *, int32_t, + const char *, const int32_t *); +void dba_macro_add(struct dba_array *, int32_t, + const char *, struct dba_array *); diff --git a/usr.bin/mandoc/dba_array.c b/usr.bin/mandoc/dba_array.c new file mode 100644 index 0000000..dd08a32 --- /dev/null +++ b/usr.bin/mandoc/dba_array.c @@ -0,0 +1,188 @@ +/* $OpenBSD: dba_array.c,v 1.1 2016/08/01 10:32:39 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Allocation-based arrays for the mandoc database, for read-write access. + * The interface is defined in "dba_array.h". + */ +#include +#include +#include +#include + +#include "mandoc_aux.h" +#include "dba_write.h" +#include "dba_array.h" + +struct dba_array { + void **ep; /* Array of entries. */ + int32_t *em; /* Array of map positions. */ + int flags; + int32_t ea; /* Entries allocated. */ + int32_t eu; /* Entries used (including deleted). */ + int32_t ed; /* Entries deleted. */ + int32_t ec; /* Currently active entry. */ + int32_t pos; /* Map position of this array. */ +}; + + +struct dba_array * +dba_array_new(int32_t ea, int flags) +{ + struct dba_array *array; + + assert(ea > 0); + array = mandoc_malloc(sizeof(*array)); + array->ep = mandoc_reallocarray(NULL, ea, sizeof(*array->ep)); + array->em = mandoc_reallocarray(NULL, ea, sizeof(*array->em)); + array->ea = ea; + array->eu = 0; + array->ed = 0; + array->ec = 0; + array->flags = flags; + array->pos = 0; + return array; +} + +void +dba_array_free(struct dba_array *array) +{ + int32_t ie; + + if (array == NULL) + return; + if (array->flags & DBA_STR) + for (ie = 0; ie < array->eu; ie++) + free(array->ep[ie]); + free(array->ep); + free(array->em); + free(array); +} + +void +dba_array_set(struct dba_array *array, int32_t ie, void *entry) +{ + assert(ie >= 0); + assert(ie < array->ea); + assert(ie <= array->eu); + if (ie == array->eu) + array->eu++; + if (array->flags & DBA_STR) + entry = mandoc_strdup(entry); + array->ep[ie] = entry; + array->em[ie] = 0; +} + +void +dba_array_add(struct dba_array *array, void *entry) +{ + if (array->eu == array->ea) { + assert(array->flags & DBA_GROW); + array->ep = mandoc_reallocarray(array->ep, + 2, sizeof(*array->ep) * array->ea); + array->em = mandoc_reallocarray(array->em, + 2, sizeof(*array->em) * array->ea); + array->ea *= 2; + } + dba_array_set(array, array->eu, entry); +} + +void * +dba_array_get(struct dba_array *array, int32_t ie) +{ + if (ie < 0 || ie >= array->eu || array->em[ie] == -1) + return NULL; + return array->ep[ie]; +} + +void +dba_array_start(struct dba_array *array) +{ + array->ec = array->eu; +} + +void * +dba_array_next(struct dba_array *array) +{ + if (array->ec < array->eu) + array->ec++; + else + array->ec = 0; + while (array->ec < array->eu && array->em[array->ec] == -1) + array->ec++; + return array->ec < array->eu ? array->ep[array->ec] : NULL; +} + +void +dba_array_del(struct dba_array *array) +{ + if (array->ec < array->eu && array->em[array->ec] != -1) { + array->em[array->ec] = -1; + array->ed++; + } +} + +void +dba_array_undel(struct dba_array *array) +{ + memset(array->em, 0, sizeof(*array->em) * array->eu); +} + +void +dba_array_setpos(struct dba_array *array, int32_t ie, int32_t pos) +{ + array->em[ie] = pos; +} + +int32_t +dba_array_getpos(struct dba_array *array) +{ + return array->pos; +} + +void +dba_array_sort(struct dba_array *array, dba_compare_func func) +{ + assert(array->ed == 0); + qsort(array->ep, array->eu, sizeof(*array->ep), func); +} + +int32_t +dba_array_writelen(struct dba_array *array, int32_t nmemb) +{ + dba_int_write(array->eu - array->ed); + return dba_skip(nmemb, array->eu - array->ed); +} + +void +dba_array_writepos(struct dba_array *array) +{ + int32_t ie; + + array->pos = dba_tell(); + for (ie = 0; ie < array->eu; ie++) + if (array->em[ie] != -1) + dba_int_write(array->em[ie]); +} + +void +dba_array_writelst(struct dba_array *array) +{ + const char *str; + + dba_array_FOREACH(array, str) + dba_str_write(str); + dba_char_write('\0'); +} diff --git a/usr.bin/mandoc/dba_array.h b/usr.bin/mandoc/dba_array.h new file mode 100644 index 0000000..167f68f --- /dev/null +++ b/usr.bin/mandoc/dba_array.h @@ -0,0 +1,47 @@ +/* $OpenBSD: dba_array.h,v 1.1 2016/08/01 10:32:39 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Public interface for allocation-based arrays + * for the mandoc database, for read-write access. + * To be used by dba*.c and by makewhatis(8). + */ + +struct dba_array; + +#define DBA_STR 0x01 /* Map contains strings, not pointers. */ +#define DBA_GROW 0x02 /* Allow the array to grow. */ + +#define dba_array_FOREACH(a, e) \ + dba_array_start(a); \ + while (((e) = dba_array_next(a)) != NULL) + +typedef int dba_compare_func(const void *, const void *); + +struct dba_array *dba_array_new(int32_t, int); +void dba_array_free(struct dba_array *); +void dba_array_set(struct dba_array *, int32_t, void *); +void dba_array_add(struct dba_array *, void *); +void *dba_array_get(struct dba_array *, int32_t); +void dba_array_start(struct dba_array *); +void *dba_array_next(struct dba_array *); +void dba_array_del(struct dba_array *); +void dba_array_undel(struct dba_array *); +void dba_array_setpos(struct dba_array *, int32_t, int32_t); +int32_t dba_array_getpos(struct dba_array *); +void dba_array_sort(struct dba_array *, dba_compare_func); +int32_t dba_array_writelen(struct dba_array *, int32_t); +void dba_array_writepos(struct dba_array *); +void dba_array_writelst(struct dba_array *); diff --git a/usr.bin/mandoc/dba_read.c b/usr.bin/mandoc/dba_read.c new file mode 100644 index 0000000..3ab4a39 --- /dev/null +++ b/usr.bin/mandoc/dba_read.c @@ -0,0 +1,72 @@ +/* $OpenBSD: dba_read.c,v 1.4 2016/08/17 20:46:06 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Function to read the mandoc database from disk into RAM, + * such that data can be added or removed. + * The interface is defined in "dba.h". + * This file is seperate from dba.c because this also uses "dbm.h". + */ +#include +#include +#include +#include +#include + +#include "mandoc_aux.h" +#include "mansearch.h" +#include "dba_array.h" +#include "dba.h" +#include "dbm.h" + + +struct dba * +dba_read(const char *fname) +{ + struct dba *dba; + struct dba_array *page; + struct dbm_page *pdata; + struct dbm_macro *mdata; + const char *cp; + int32_t im, ip, iv, npages; + + if (dbm_open(fname) == -1) + return NULL; + npages = dbm_page_count(); + dba = dba_new(npages < 128 ? 128 : npages); + for (ip = 0; ip < npages; ip++) { + pdata = dbm_page_get(ip); + page = dba_page_new(dba->pages, pdata->arch, + pdata->desc, pdata->file + 1, *pdata->file); + for (cp = pdata->name; *cp != '\0'; cp = strchr(cp, '\0') + 1) + dba_page_add(page, DBP_NAME, cp); + for (cp = pdata->sect; *cp != '\0'; cp = strchr(cp, '\0') + 1) + dba_page_add(page, DBP_SECT, cp); + if ((cp = pdata->arch) != NULL) + while (*(cp = strchr(cp, '\0') + 1) != '\0') + dba_page_add(page, DBP_ARCH, cp); + cp = pdata->file; + while (*(cp = strchr(cp, '\0') + 1) != '\0') + dba_page_add(page, DBP_FILE, cp); + } + for (im = 0; im < MACRO_MAX; im++) { + for (iv = 0; iv < dbm_macro_count(im); iv++) { + mdata = dbm_macro_get(im, iv); + dba_macro_new(dba, im, mdata->value, mdata->pp); + } + } + dbm_close(); + return dba; +} diff --git a/usr.bin/mandoc/dba_write.c b/usr.bin/mandoc/dba_write.c new file mode 100644 index 0000000..ef15dbe --- /dev/null +++ b/usr.bin/mandoc/dba_write.c @@ -0,0 +1,117 @@ +/* $OpenBSD: dba_write.c,v 1.1 2016/08/01 10:32:39 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Low-level functions for serializing allocation-based data to disk. + * The interface is defined in "dba_write.h". + */ +#include +#include +#include +#include +#include +#include +#include + +#include "dba_write.h" + +static FILE *ofp; + + +int +dba_open(const char *fname) +{ + ofp = fopen(fname, "w"); + return ofp == NULL ? -1 : 0; +} + +int +dba_close(void) +{ + return fclose(ofp) == EOF ? -1 : 0; +} + +int32_t +dba_tell(void) +{ + long pos; + + if ((pos = ftell(ofp)) == -1) + err(1, "ftell"); + if (pos >= INT32_MAX) { + errno = EOVERFLOW; + err(1, "ftell = %ld", pos); + } + return pos; +} + +void +dba_seek(int32_t pos) +{ + if (fseek(ofp, pos, SEEK_SET) == -1) + err(1, "fseek(%d)", pos); +} + +int32_t +dba_align(void) +{ + int32_t pos; + + pos = dba_tell(); + while (pos & 3) { + dba_char_write('\0'); + pos++; + } + return pos; +} + +int32_t +dba_skip(int32_t nmemb, int32_t sz) +{ + const int32_t out[5] = {0, 0, 0, 0, 0}; + int32_t i, pos; + + assert(sz >= 0); + assert(nmemb > 0); + assert(nmemb <= 5); + pos = dba_tell(); + for (i = 0; i < sz; i++) + if (nmemb - fwrite(&out, sizeof(out[0]), nmemb, ofp)) + err(1, "fwrite"); + return pos; +} + +void +dba_char_write(int c) +{ + if (putc(c, ofp) == EOF) + err(1, "fputc"); +} + +void +dba_str_write(const char *str) +{ + if (fputs(str, ofp) == EOF) + err(1, "fputs"); + dba_char_write('\0'); +} + +void +dba_int_write(int32_t i) +{ + i = htobe32(i); + if (fwrite(&i, sizeof(i), 1, ofp) != 1) + err(1, "fwrite"); +} diff --git a/usr.bin/mandoc/dba_write.h b/usr.bin/mandoc/dba_write.h new file mode 100644 index 0000000..bbbaa5e --- /dev/null +++ b/usr.bin/mandoc/dba_write.h @@ -0,0 +1,30 @@ +/* $OpenBSD: dba_write.h,v 1.1 2016/08/01 10:32:39 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Internal interface to low-level functions + * for serializing allocation-based data to disk. + * For use by dba_array.c and dba.c only. + */ + +int dba_open(const char *); +int dba_close(void); +int32_t dba_tell(void); +void dba_seek(int32_t); +int32_t dba_align(void); +int32_t dba_skip(int32_t, int32_t); +void dba_char_write(int); +void dba_str_write(const char *); +void dba_int_write(int32_t); diff --git a/usr.bin/mandoc/dbm.c b/usr.bin/mandoc/dbm.c new file mode 100644 index 0000000..261321e --- /dev/null +++ b/usr.bin/mandoc/dbm.c @@ -0,0 +1,474 @@ +/* $OpenBSD: dbm.c,v 1.5 2019/07/01 22:43:03 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Map-based version of the mandoc database, for read-only access. + * The interface is defined in "dbm.h". + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mansearch.h" +#include "dbm_map.h" +#include "dbm.h" + +#ifndef EFTYPE +#define EFTYPE 79 +#endif + +struct macro { + int32_t value; + int32_t pages; +}; + +struct page { + int32_t name; + int32_t sect; + int32_t arch; + int32_t desc; + int32_t file; +}; + +enum iter { + ITER_NONE = 0, + ITER_NAME, + ITER_SECT, + ITER_ARCH, + ITER_DESC, + ITER_MACRO +}; + +static struct macro *macros[MACRO_MAX]; +static int32_t nvals[MACRO_MAX]; +static struct page *pages; +static int32_t npages; +static enum iter iteration; + +static struct dbm_res page_bytitle(enum iter, const struct dbm_match *); +static struct dbm_res page_byarch(const struct dbm_match *); +static struct dbm_res page_bymacro(int32_t, const struct dbm_match *); +static char *macro_bypage(int32_t, int32_t); + + +/*** top level functions **********************************************/ + +/* + * Open a disk-based mandoc database for read-only access. + * Map the pages and macros[] arrays. + * Return 0 on success. Return -1 and set errno on failure. + */ +int +dbm_open(const char *fname) +{ + const int32_t *mp, *ep; + int32_t im; + + if (dbm_map(fname) == -1) + return -1; + + if ((npages = be32toh(*dbm_getint(4))) < 0) { + warnx("dbm_open(%s): Invalid number of pages: %d", + fname, npages); + goto fail; + } + pages = (struct page *)dbm_getint(5); + + if ((mp = dbm_get(*dbm_getint(2))) == NULL) { + warnx("dbm_open(%s): Invalid offset of macros array", fname); + goto fail; + } + if (be32toh(*mp) != MACRO_MAX) { + warnx("dbm_open(%s): Invalid number of macros: %d", + fname, be32toh(*mp)); + goto fail; + } + for (im = 0; im < MACRO_MAX; im++) { + if ((ep = dbm_get(*++mp)) == NULL) { + warnx("dbm_open(%s): Invalid offset of macro %d", + fname, im); + goto fail; + } + nvals[im] = be32toh(*ep); + macros[im] = (struct macro *)++ep; + } + return 0; + +fail: + dbm_unmap(); + errno = EFTYPE; + return -1; +} + +void +dbm_close(void) +{ + dbm_unmap(); +} + + +/*** functions for handling pages *************************************/ + +int32_t +dbm_page_count(void) +{ + return npages; +} + +/* + * Give the caller pointers to the data for one manual page. + */ +struct dbm_page * +dbm_page_get(int32_t ip) +{ + static struct dbm_page res; + + assert(ip >= 0); + assert(ip < npages); + res.name = dbm_get(pages[ip].name); + if (res.name == NULL) + res.name = "(NULL)\0"; + res.sect = dbm_get(pages[ip].sect); + if (res.sect == NULL) + res.sect = "(NULL)\0"; + res.arch = pages[ip].arch ? dbm_get(pages[ip].arch) : NULL; + res.desc = dbm_get(pages[ip].desc); + if (res.desc == NULL) + res.desc = "(NULL)"; + res.file = dbm_get(pages[ip].file); + if (res.file == NULL) + res.file = " (NULL)\0"; + res.addr = dbm_addr(pages + ip); + return &res; +} + +/* + * Functions to start filtered iterations over manual pages. + */ +void +dbm_page_byname(const struct dbm_match *match) +{ + assert(match != NULL); + page_bytitle(ITER_NAME, match); +} + +void +dbm_page_bysect(const struct dbm_match *match) +{ + assert(match != NULL); + page_bytitle(ITER_SECT, match); +} + +void +dbm_page_byarch(const struct dbm_match *match) +{ + assert(match != NULL); + page_byarch(match); +} + +void +dbm_page_bydesc(const struct dbm_match *match) +{ + assert(match != NULL); + page_bytitle(ITER_DESC, match); +} + +void +dbm_page_bymacro(int32_t im, const struct dbm_match *match) +{ + assert(im >= 0); + assert(im < MACRO_MAX); + assert(match != NULL); + page_bymacro(im, match); +} + +/* + * Return the number of the next manual page in the current iteration. + */ +struct dbm_res +dbm_page_next(void) +{ + struct dbm_res res = {-1, 0}; + + switch(iteration) { + case ITER_NONE: + return res; + case ITER_ARCH: + return page_byarch(NULL); + case ITER_MACRO: + return page_bymacro(0, NULL); + default: + return page_bytitle(iteration, NULL); + } +} + +/* + * Functions implementing the iteration over manual pages. + */ +static struct dbm_res +page_bytitle(enum iter arg_iter, const struct dbm_match *arg_match) +{ + static const struct dbm_match *match; + static const char *cp; + static int32_t ip; + struct dbm_res res = {-1, 0}; + + assert(arg_iter == ITER_NAME || arg_iter == ITER_DESC || + arg_iter == ITER_SECT); + + /* Initialize for a new iteration. */ + + if (arg_match != NULL) { + iteration = arg_iter; + match = arg_match; + switch (iteration) { + case ITER_NAME: + cp = dbm_get(pages[0].name); + break; + case ITER_SECT: + cp = dbm_get(pages[0].sect); + break; + case ITER_DESC: + cp = dbm_get(pages[0].desc); + break; + default: + abort(); + } + if (cp == NULL) { + iteration = ITER_NONE; + match = NULL; + cp = NULL; + ip = npages; + } else + ip = 0; + return res; + } + + /* Search for a name. */ + + while (ip < npages) { + if (iteration == ITER_NAME) + cp++; + if (dbm_match(match, cp)) + break; + cp = strchr(cp, '\0') + 1; + if (iteration == ITER_DESC) + ip++; + else if (*cp == '\0') { + cp++; + ip++; + } + } + + /* Reached the end without a match. */ + + if (ip == npages) { + iteration = ITER_NONE; + match = NULL; + cp = NULL; + return res; + } + + /* Found a match; save the quality for later retrieval. */ + + res.page = ip; + res.bits = iteration == ITER_NAME ? cp[-1] : 0; + + /* Skip the remaining names of this page. */ + + if (++ip < npages) { + do { + cp++; + } while (cp[-1] != '\0' || + (iteration != ITER_DESC && cp[-2] != '\0')); + } + return res; +} + +static struct dbm_res +page_byarch(const struct dbm_match *arg_match) +{ + static const struct dbm_match *match; + struct dbm_res res = {-1, 0}; + static int32_t ip; + const char *cp; + + /* Initialize for a new iteration. */ + + if (arg_match != NULL) { + iteration = ITER_ARCH; + match = arg_match; + ip = 0; + return res; + } + + /* Search for an architecture. */ + + for ( ; ip < npages; ip++) + if (pages[ip].arch) + for (cp = dbm_get(pages[ip].arch); + *cp != '\0'; + cp = strchr(cp, '\0') + 1) + if (dbm_match(match, cp)) { + res.page = ip++; + return res; + } + + /* Reached the end without a match. */ + + iteration = ITER_NONE; + match = NULL; + return res; +} + +static struct dbm_res +page_bymacro(int32_t arg_im, const struct dbm_match *arg_match) +{ + static const struct dbm_match *match; + static const int32_t *pp; + static const char *cp; + static int32_t im, iv; + struct dbm_res res = {-1, 0}; + + assert(im >= 0); + assert(im < MACRO_MAX); + + /* Initialize for a new iteration. */ + + if (arg_match != NULL) { + iteration = ITER_MACRO; + match = arg_match; + im = arg_im; + cp = nvals[im] ? dbm_get(macros[im]->value) : NULL; + pp = NULL; + iv = -1; + return res; + } + if (iteration != ITER_MACRO) + return res; + + /* Find the next matching macro value. */ + + while (pp == NULL || *pp == 0) { + if (++iv == nvals[im]) { + iteration = ITER_NONE; + return res; + } + if (iv) + cp = strchr(cp, '\0') + 1; + if (dbm_match(match, cp)) + pp = dbm_get(macros[im][iv].pages); + } + + /* Found a matching page. */ + + res.page = (struct page *)dbm_get(*pp++) - pages; + return res; +} + + +/*** functions for handling macros ************************************/ + +int32_t +dbm_macro_count(int32_t im) +{ + assert(im >= 0); + assert(im < MACRO_MAX); + return nvals[im]; +} + +struct dbm_macro * +dbm_macro_get(int32_t im, int32_t iv) +{ + static struct dbm_macro macro; + + assert(im >= 0); + assert(im < MACRO_MAX); + assert(iv >= 0); + assert(iv < nvals[im]); + macro.value = dbm_get(macros[im][iv].value); + macro.pp = dbm_get(macros[im][iv].pages); + return ¯o; +} + +/* + * Filtered iteration over macro entries. + */ +void +dbm_macro_bypage(int32_t im, int32_t ip) +{ + assert(im >= 0); + assert(im < MACRO_MAX); + assert(ip != 0); + macro_bypage(im, ip); +} + +char * +dbm_macro_next(void) +{ + return macro_bypage(MACRO_MAX, 0); +} + +static char * +macro_bypage(int32_t arg_im, int32_t arg_ip) +{ + static const int32_t *pp; + static int32_t im, ip, iv; + + /* Initialize for a new iteration. */ + + if (arg_im < MACRO_MAX && arg_ip != 0) { + im = arg_im; + ip = arg_ip; + pp = dbm_get(macros[im]->pages); + iv = 0; + return NULL; + } + if (im >= MACRO_MAX) + return NULL; + + /* Search for the next value. */ + + while (iv < nvals[im]) { + if (*pp == ip) + break; + if (*pp == 0) + iv++; + pp++; + } + + /* Reached the end without a match. */ + + if (iv == nvals[im]) { + im = MACRO_MAX; + ip = 0; + pp = NULL; + return NULL; + } + + /* Found a match; skip the remaining pages of this entry. */ + + if (++iv < nvals[im]) + while (*pp++ != 0) + continue; + + return dbm_get(macros[im][iv - 1].value); +} diff --git a/usr.bin/mandoc/dbm.h b/usr.bin/mandoc/dbm.h new file mode 100644 index 0000000..0f12ee1 --- /dev/null +++ b/usr.bin/mandoc/dbm.h @@ -0,0 +1,68 @@ +/* $OpenBSD: dbm.h,v 1.1 2016/08/01 10:32:39 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Public interface for the map-based version + * of the mandoc database, for read-only access. + * To be used by dbm*.c, dba_read.c, and man(1) and apropos(1). + */ + +enum dbm_mtype { + DBM_EXACT = 0, + DBM_SUB, + DBM_REGEX +}; + +struct dbm_match { + regex_t *re; + const char *str; + enum dbm_mtype type; +}; + +struct dbm_res { + int32_t page; + int32_t bits; +}; + +struct dbm_page { + const char *name; + const char *sect; + const char *arch; + const char *desc; + const char *file; + int32_t addr; +}; + +struct dbm_macro { + const char *value; + const int32_t *pp; +}; + +int dbm_open(const char *); +void dbm_close(void); + +int32_t dbm_page_count(void); +struct dbm_page *dbm_page_get(int32_t); +void dbm_page_byname(const struct dbm_match *); +void dbm_page_bysect(const struct dbm_match *); +void dbm_page_byarch(const struct dbm_match *); +void dbm_page_bydesc(const struct dbm_match *); +void dbm_page_bymacro(int32_t, const struct dbm_match *); +struct dbm_res dbm_page_next(void); + +int32_t dbm_macro_count(int32_t); +struct dbm_macro *dbm_macro_get(int32_t, int32_t); +void dbm_macro_bypage(int32_t, int32_t); +char *dbm_macro_next(void); diff --git a/usr.bin/mandoc/dbm_map.c b/usr.bin/mandoc/dbm_map.c new file mode 100644 index 0000000..72b1220 --- /dev/null +++ b/usr.bin/mandoc/dbm_map.c @@ -0,0 +1,188 @@ +/* $OpenBSD: dbm_map.c,v 1.6 2017/02/09 18:26:17 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Low-level routines for the map-based version + * of the mandoc database, for read-only access. + * The interface is defined in "dbm_map.h". + */ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mansearch.h" +#include "dbm_map.h" +#include "dbm.h" + +#ifndef EFTYPE +#define EFTYPE 79 +#endif + +static struct stat st; +static char *dbm_base; +static int ifd; +static int32_t max_offset; + +/* + * Open a disk-based database for read-only access. + * Validate the file format as far as it is not mandoc-specific. + * Return 0 on success. Return -1 and set errno on failure. + */ +int +dbm_map(const char *fname) +{ + int save_errno; + const int32_t *magic; + + if ((ifd = open(fname, O_RDONLY)) == -1) + return -1; + if (fstat(ifd, &st) == -1) + goto fail; + if (st.st_size < 5) { + warnx("dbm_map(%s): File too short", fname); + errno = EFTYPE; + goto fail; + } + if (st.st_size > INT32_MAX) { + errno = EFBIG; + goto fail; + } + if ((dbm_base = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, + ifd, 0)) == MAP_FAILED) + goto fail; + magic = dbm_getint(0); + if (be32toh(*magic) != MANDOCDB_MAGIC) { + if (strncmp(dbm_base, "SQLite format 3", 15)) + warnx("dbm_map(%s): " + "Bad initial magic %x (expected %x)", + fname, be32toh(*magic), MANDOCDB_MAGIC); + else + warnx("dbm_map(%s): " + "Obsolete format based on SQLite 3", + fname); + errno = EFTYPE; + goto fail; + } + magic = dbm_getint(1); + if (be32toh(*magic) != MANDOCDB_VERSION) { + warnx("dbm_map(%s): Bad version number %d (expected %d)", + fname, be32toh(*magic), MANDOCDB_VERSION); + errno = EFTYPE; + goto fail; + } + max_offset = be32toh(*dbm_getint(3)) + sizeof(int32_t); + if (st.st_size != max_offset) { + warnx("dbm_map(%s): Inconsistent file size %lld (expected %d)", + fname, (long long)st.st_size, max_offset); + errno = EFTYPE; + goto fail; + } + if ((magic = dbm_get(*dbm_getint(3))) == NULL) { + errno = EFTYPE; + goto fail; + } + if (be32toh(*magic) != MANDOCDB_MAGIC) { + warnx("dbm_map(%s): Bad final magic %x (expected %x)", + fname, be32toh(*magic), MANDOCDB_MAGIC); + errno = EFTYPE; + goto fail; + } + return 0; + +fail: + save_errno = errno; + close(ifd); + errno = save_errno; + return -1; +} + +void +dbm_unmap(void) +{ + if (munmap(dbm_base, st.st_size) == -1) + warn("dbm_unmap: munmap"); + if (close(ifd) == -1) + warn("dbm_unmap: close"); + dbm_base = (char *)-1; +} + +/* + * Take a raw integer as it was read from the database. + * Interpret it as an offset into the database file + * and return a pointer to that place in the file. + */ +void * +dbm_get(int32_t offset) +{ + offset = be32toh(offset); + if (offset < 0) { + warnx("dbm_get: Database corrupt: offset %d", offset); + return NULL; + } + if (offset >= max_offset) { + warnx("dbm_get: Database corrupt: offset %d > %d", + offset, max_offset); + return NULL; + } + return dbm_base + offset; +} + +/* + * Assume the database starts with some integers. + * Assume they are numbered starting from 0, increasing. + * Get a pointer to one with the number "offset". + */ +int32_t * +dbm_getint(int32_t offset) +{ + return (int32_t *)dbm_base + offset; +} + +/* + * The reverse of dbm_get(). + * Take pointer into the database file + * and convert it to the raw integer + * that would be used to refer to that place in the file. + */ +int32_t +dbm_addr(const void *p) +{ + return htobe32((const char *)p - dbm_base); +} + +int +dbm_match(const struct dbm_match *match, const char *str) +{ + switch (match->type) { + case DBM_EXACT: + return strcmp(str, match->str) == 0; + case DBM_SUB: + return strcasestr(str, match->str) != NULL; + case DBM_REGEX: + return regexec(match->re, str, 0, NULL, 0) == 0; + default: + abort(); + } +} diff --git a/usr.bin/mandoc/dbm_map.h b/usr.bin/mandoc/dbm_map.h new file mode 100644 index 0000000..94ee82f --- /dev/null +++ b/usr.bin/mandoc/dbm_map.h @@ -0,0 +1,29 @@ +/* $OpenBSD: dbm_map.h,v 1.2 2019/07/01 22:43:03 schwarze Exp $ */ +/* + * Copyright (c) 2016 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. + * + * Private interface for low-level routines for the map-based version + * of the mandoc database, for read-only access. + * To be used by dbm*.c only. + */ + +struct dbm_match; + +int dbm_map(const char *); +void dbm_unmap(void); +void *dbm_get(int32_t); +int32_t *dbm_getint(int32_t); +int32_t dbm_addr(const void *); +int dbm_match(const struct dbm_match *, const char *); diff --git a/usr.bin/mandoc/eqn.c b/usr.bin/mandoc/eqn.c new file mode 100644 index 0000000..ad32067 --- /dev/null +++ b/usr.bin/mandoc/eqn.c @@ -0,0 +1,1130 @@ +/* $OpenBSD: eqn.c,v 1.47 2020/01/08 12:09:14 schwarze Exp $ */ +/* + * Copyright (c) 2011, 2014 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 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 + +#include +#include +#include +#include +#include +#include +#include + +#include "mandoc_aux.h" +#include "mandoc.h" +#include "roff.h" +#include "eqn.h" +#include "libmandoc.h" +#include "eqn_parse.h" + +#define EQN_NEST_MAX 128 /* maximum nesting of defines */ +#define STRNEQ(p1, sz1, p2, sz2) \ + ((sz1) == (sz2) && 0 == strncmp((p1), (p2), (sz1))) + +enum eqn_tok { + EQN_TOK_DYAD = 0, + EQN_TOK_VEC, + EQN_TOK_UNDER, + EQN_TOK_BAR, + EQN_TOK_TILDE, + EQN_TOK_HAT, + EQN_TOK_DOT, + EQN_TOK_DOTDOT, + EQN_TOK_FWD, + EQN_TOK_BACK, + EQN_TOK_DOWN, + EQN_TOK_UP, + EQN_TOK_FAT, + EQN_TOK_ROMAN, + EQN_TOK_ITALIC, + EQN_TOK_BOLD, + EQN_TOK_SIZE, + EQN_TOK_SUB, + EQN_TOK_SUP, + EQN_TOK_SQRT, + EQN_TOK_OVER, + EQN_TOK_FROM, + EQN_TOK_TO, + EQN_TOK_BRACE_OPEN, + EQN_TOK_BRACE_CLOSE, + EQN_TOK_GSIZE, + EQN_TOK_GFONT, + EQN_TOK_MARK, + EQN_TOK_LINEUP, + EQN_TOK_LEFT, + EQN_TOK_RIGHT, + EQN_TOK_PILE, + EQN_TOK_LPILE, + EQN_TOK_RPILE, + EQN_TOK_CPILE, + EQN_TOK_MATRIX, + EQN_TOK_CCOL, + EQN_TOK_LCOL, + EQN_TOK_RCOL, + EQN_TOK_DELIM, + EQN_TOK_DEFINE, + EQN_TOK_TDEFINE, + EQN_TOK_NDEFINE, + EQN_TOK_UNDEF, + EQN_TOK_ABOVE, + EQN_TOK__MAX, + EQN_TOK_FUNC, + EQN_TOK_QUOTED, + EQN_TOK_SYM, + EQN_TOK_EOF +}; + +static const char *eqn_toks[EQN_TOK__MAX] = { + "dyad", /* EQN_TOK_DYAD */ + "vec", /* EQN_TOK_VEC */ + "under", /* EQN_TOK_UNDER */ + "bar", /* EQN_TOK_BAR */ + "tilde", /* EQN_TOK_TILDE */ + "hat", /* EQN_TOK_HAT */ + "dot", /* EQN_TOK_DOT */ + "dotdot", /* EQN_TOK_DOTDOT */ + "fwd", /* EQN_TOK_FWD * */ + "back", /* EQN_TOK_BACK */ + "down", /* EQN_TOK_DOWN */ + "up", /* EQN_TOK_UP */ + "fat", /* EQN_TOK_FAT */ + "roman", /* EQN_TOK_ROMAN */ + "italic", /* EQN_TOK_ITALIC */ + "bold", /* EQN_TOK_BOLD */ + "size", /* EQN_TOK_SIZE */ + "sub", /* EQN_TOK_SUB */ + "sup", /* EQN_TOK_SUP */ + "sqrt", /* EQN_TOK_SQRT */ + "over", /* EQN_TOK_OVER */ + "from", /* EQN_TOK_FROM */ + "to", /* EQN_TOK_TO */ + "{", /* EQN_TOK_BRACE_OPEN */ + "}", /* EQN_TOK_BRACE_CLOSE */ + "gsize", /* EQN_TOK_GSIZE */ + "gfont", /* EQN_TOK_GFONT */ + "mark", /* EQN_TOK_MARK */ + "lineup", /* EQN_TOK_LINEUP */ + "left", /* EQN_TOK_LEFT */ + "right", /* EQN_TOK_RIGHT */ + "pile", /* EQN_TOK_PILE */ + "lpile", /* EQN_TOK_LPILE */ + "rpile", /* EQN_TOK_RPILE */ + "cpile", /* EQN_TOK_CPILE */ + "matrix", /* EQN_TOK_MATRIX */ + "ccol", /* EQN_TOK_CCOL */ + "lcol", /* EQN_TOK_LCOL */ + "rcol", /* EQN_TOK_RCOL */ + "delim", /* EQN_TOK_DELIM */ + "define", /* EQN_TOK_DEFINE */ + "tdefine", /* EQN_TOK_TDEFINE */ + "ndefine", /* EQN_TOK_NDEFINE */ + "undef", /* EQN_TOK_UNDEF */ + "above", /* EQN_TOK_ABOVE */ +}; + +static const char *const eqn_func[] = { + "acos", "acsc", "and", "arc", "asec", "asin", "atan", + "cos", "cosh", "coth", "csc", "det", "exp", "for", + "if", "lim", "ln", "log", "max", "min", + "sec", "sin", "sinh", "tan", "tanh", "Im", "Re", +}; + +enum eqn_symt { + EQNSYM_alpha = 0, + EQNSYM_beta, + EQNSYM_chi, + EQNSYM_delta, + EQNSYM_epsilon, + EQNSYM_eta, + EQNSYM_gamma, + EQNSYM_iota, + EQNSYM_kappa, + EQNSYM_lambda, + EQNSYM_mu, + EQNSYM_nu, + EQNSYM_omega, + EQNSYM_omicron, + EQNSYM_phi, + EQNSYM_pi, + EQNSYM_ps, + EQNSYM_rho, + EQNSYM_sigma, + EQNSYM_tau, + EQNSYM_theta, + EQNSYM_upsilon, + EQNSYM_xi, + EQNSYM_zeta, + EQNSYM_DELTA, + EQNSYM_GAMMA, + EQNSYM_LAMBDA, + EQNSYM_OMEGA, + EQNSYM_PHI, + EQNSYM_PI, + EQNSYM_PSI, + EQNSYM_SIGMA, + EQNSYM_THETA, + EQNSYM_UPSILON, + EQNSYM_XI, + EQNSYM_inter, + EQNSYM_union, + EQNSYM_prod, + EQNSYM_int, + EQNSYM_sum, + EQNSYM_grad, + EQNSYM_del, + EQNSYM_times, + EQNSYM_cdot, + EQNSYM_nothing, + EQNSYM_approx, + EQNSYM_prime, + EQNSYM_half, + EQNSYM_partial, + EQNSYM_inf, + EQNSYM_muchgreat, + EQNSYM_muchless, + EQNSYM_larrow, + EQNSYM_rarrow, + EQNSYM_pm, + EQNSYM_nequal, + EQNSYM_equiv, + EQNSYM_lessequal, + EQNSYM_moreequal, + EQNSYM_minus, + EQNSYM__MAX +}; + +struct eqnsym { + const char *str; + const char *sym; +}; + +static const struct eqnsym eqnsyms[EQNSYM__MAX] = { + { "alpha", "*a" }, /* EQNSYM_alpha */ + { "beta", "*b" }, /* EQNSYM_beta */ + { "chi", "*x" }, /* EQNSYM_chi */ + { "delta", "*d" }, /* EQNSYM_delta */ + { "epsilon", "*e" }, /* EQNSYM_epsilon */ + { "eta", "*y" }, /* EQNSYM_eta */ + { "gamma", "*g" }, /* EQNSYM_gamma */ + { "iota", "*i" }, /* EQNSYM_iota */ + { "kappa", "*k" }, /* EQNSYM_kappa */ + { "lambda", "*l" }, /* EQNSYM_lambda */ + { "mu", "*m" }, /* EQNSYM_mu */ + { "nu", "*n" }, /* EQNSYM_nu */ + { "omega", "*w" }, /* EQNSYM_omega */ + { "omicron", "*o" }, /* EQNSYM_omicron */ + { "phi", "*f" }, /* EQNSYM_phi */ + { "pi", "*p" }, /* EQNSYM_pi */ + { "psi", "*q" }, /* EQNSYM_psi */ + { "rho", "*r" }, /* EQNSYM_rho */ + { "sigma", "*s" }, /* EQNSYM_sigma */ + { "tau", "*t" }, /* EQNSYM_tau */ + { "theta", "*h" }, /* EQNSYM_theta */ + { "upsilon", "*u" }, /* EQNSYM_upsilon */ + { "xi", "*c" }, /* EQNSYM_xi */ + { "zeta", "*z" }, /* EQNSYM_zeta */ + { "DELTA", "*D" }, /* EQNSYM_DELTA */ + { "GAMMA", "*G" }, /* EQNSYM_GAMMA */ + { "LAMBDA", "*L" }, /* EQNSYM_LAMBDA */ + { "OMEGA", "*W" }, /* EQNSYM_OMEGA */ + { "PHI", "*F" }, /* EQNSYM_PHI */ + { "PI", "*P" }, /* EQNSYM_PI */ + { "PSI", "*Q" }, /* EQNSYM_PSI */ + { "SIGMA", "*S" }, /* EQNSYM_SIGMA */ + { "THETA", "*H" }, /* EQNSYM_THETA */ + { "UPSILON", "*U" }, /* EQNSYM_UPSILON */ + { "XI", "*C" }, /* EQNSYM_XI */ + { "inter", "ca" }, /* EQNSYM_inter */ + { "union", "cu" }, /* EQNSYM_union */ + { "prod", "product" }, /* EQNSYM_prod */ + { "int", "integral" }, /* EQNSYM_int */ + { "sum", "sum" }, /* EQNSYM_sum */ + { "grad", "gr" }, /* EQNSYM_grad */ + { "del", "gr" }, /* EQNSYM_del */ + { "times", "mu" }, /* EQNSYM_times */ + { "cdot", "pc" }, /* EQNSYM_cdot */ + { "nothing", "&" }, /* EQNSYM_nothing */ + { "approx", "~~" }, /* EQNSYM_approx */ + { "prime", "fm" }, /* EQNSYM_prime */ + { "half", "12" }, /* EQNSYM_half */ + { "partial", "pd" }, /* EQNSYM_partial */ + { "inf", "if" }, /* EQNSYM_inf */ + { ">>", ">>" }, /* EQNSYM_muchgreat */ + { "<<", "<<" }, /* EQNSYM_muchless */ + { "<-", "<-" }, /* EQNSYM_larrow */ + { "->", "->" }, /* EQNSYM_rarrow */ + { "+-", "+-" }, /* EQNSYM_pm */ + { "!=", "!=" }, /* EQNSYM_nequal */ + { "==", "==" }, /* EQNSYM_equiv */ + { "<=", "<=" }, /* EQNSYM_lessequal */ + { ">=", ">=" }, /* EQNSYM_moreequal */ + { "-", "mi" }, /* EQNSYM_minus */ +}; + +enum parse_mode { + MODE_QUOTED, + MODE_NOSUB, + MODE_SUB, + MODE_TOK +}; + +struct eqn_def { + char *key; + size_t keysz; + char *val; + size_t valsz; +}; + +static struct eqn_box *eqn_box_alloc(struct eqn_node *, struct eqn_box *); +static struct eqn_box *eqn_box_makebinary(struct eqn_node *, + struct eqn_box *); +static void eqn_def(struct eqn_node *); +static struct eqn_def *eqn_def_find(struct eqn_node *); +static void eqn_delim(struct eqn_node *); +static enum eqn_tok eqn_next(struct eqn_node *, enum parse_mode); +static void eqn_undef(struct eqn_node *); + + +struct eqn_node * +eqn_alloc(void) +{ + struct eqn_node *ep; + + ep = mandoc_calloc(1, sizeof(*ep)); + ep->gsize = EQN_DEFSIZE; + return ep; +} + +void +eqn_reset(struct eqn_node *ep) +{ + free(ep->data); + ep->data = ep->start = ep->end = NULL; + ep->sz = ep->toksz = 0; +} + +void +eqn_read(struct eqn_node *ep, const char *p) +{ + char *cp; + + if (ep->data == NULL) { + ep->sz = strlen(p); + ep->data = mandoc_strdup(p); + } else { + ep->sz = mandoc_asprintf(&cp, "%s %s", ep->data, p); + free(ep->data); + ep->data = cp; + } + ep->sz += 1; +} + +/* + * Find the key "key" of the give size within our eqn-defined values. + */ +static struct eqn_def * +eqn_def_find(struct eqn_node *ep) +{ + int i; + + for (i = 0; i < (int)ep->defsz; i++) + if (ep->defs[i].keysz && STRNEQ(ep->defs[i].key, + ep->defs[i].keysz, ep->start, ep->toksz)) + return &ep->defs[i]; + + return NULL; +} + +/* + * Parse a token from the input text. The modes are: + * MODE_QUOTED: Use *ep->start as the delimiter; the token ends + * before its next occurence. Do not interpret the token in any + * way and return EQN_TOK_QUOTED. All other modes behave like + * MODE_QUOTED when *ep->start is '"'. + * MODE_NOSUB: If *ep->start is a curly brace, the token ends after it; + * otherwise, it ends before the next whitespace or brace. + * Do not interpret the token and return EQN_TOK__MAX. + * MODE_SUB: Like MODE_NOSUB, but try to interpret the token as an + * alias created with define. If it is an alias, replace it with + * its string value and reparse. + * MODE_TOK: Like MODE_SUB, but also check the token against the list + * of tokens, and if there is a match, return that token. Otherwise, + * if the token matches a symbol, return EQN_TOK_SYM; if it matches + * a function name, EQN_TOK_FUNC, or else EQN_TOK__MAX. Except for + * a token match, *ep->start is set to an allocated string that the + * caller is expected to free. + * All modes skip whitespace following the end of the token. + */ +static enum eqn_tok +eqn_next(struct eqn_node *ep, enum parse_mode mode) +{ + static int last_len, lim; + + struct eqn_def *def; + size_t start; + int diff, i, quoted; + enum eqn_tok tok; + + /* + * Reset the recursion counter after advancing + * beyond the end of the previous substitution. + */ + if (ep->end - ep->data >= last_len) + lim = 0; + + ep->start = ep->end; + quoted = mode == MODE_QUOTED; + for (;;) { + switch (*ep->start) { + case '\0': + ep->toksz = 0; + return EQN_TOK_EOF; + case '"': + quoted = 1; + break; + case ' ': + case '\t': + case '~': + case '^': + if (quoted) + break; + ep->start++; + continue; + default: + break; + } + if (quoted) { + ep->end = strchr(ep->start + 1, *ep->start); + ep->start++; /* Skip opening quote. */ + if (ep->end == NULL) { + mandoc_msg(MANDOCERR_ARG_QUOTE, + ep->node->line, ep->node->pos, NULL); + ep->end = strchr(ep->start, '\0'); + } + } else { + ep->end = ep->start + 1; + if (*ep->start != '{' && *ep->start != '}') + ep->end += strcspn(ep->end, " ^~\"{}\t"); + } + ep->toksz = ep->end - ep->start; + if (quoted && *ep->end != '\0') + ep->end++; /* Skip closing quote. */ + while (*ep->end != '\0' && strchr(" \t^~", *ep->end) != NULL) + ep->end++; + if (quoted) /* Cannot return, may have to strndup. */ + break; + if (mode == MODE_NOSUB) + return EQN_TOK__MAX; + if ((def = eqn_def_find(ep)) == NULL) + break; + if (++lim > EQN_NEST_MAX) { + mandoc_msg(MANDOCERR_ROFFLOOP, + ep->node->line, ep->node->pos, NULL); + return EQN_TOK_EOF; + } + + /* Replace a defined name with its string value. */ + if ((diff = def->valsz - ep->toksz) > 0) { + start = ep->start - ep->data; + ep->sz += diff; + ep->data = mandoc_realloc(ep->data, ep->sz + 1); + ep->start = ep->data + start; + } + if (diff) + memmove(ep->start + def->valsz, ep->start + ep->toksz, + strlen(ep->start + ep->toksz) + 1); + memcpy(ep->start, def->val, def->valsz); + last_len = ep->start - ep->data + def->valsz; + } + if (mode != MODE_TOK) + return quoted ? EQN_TOK_QUOTED : EQN_TOK__MAX; + if (quoted) { + ep->start = mandoc_strndup(ep->start, ep->toksz); + return EQN_TOK_QUOTED; + } + for (tok = 0; tok < EQN_TOK__MAX; tok++) + if (STRNEQ(ep->start, ep->toksz, + eqn_toks[tok], strlen(eqn_toks[tok]))) + return tok; + + for (i = 0; i < EQNSYM__MAX; i++) { + if (STRNEQ(ep->start, ep->toksz, + eqnsyms[i].str, strlen(eqnsyms[i].str))) { + mandoc_asprintf(&ep->start, + "\\[%s]", eqnsyms[i].sym); + return EQN_TOK_SYM; + } + } + ep->start = mandoc_strndup(ep->start, ep->toksz); + for (i = 0; i < (int)(sizeof(eqn_func)/sizeof(*eqn_func)); i++) + if (STRNEQ(ep->start, ep->toksz, + eqn_func[i], strlen(eqn_func[i]))) + return EQN_TOK_FUNC; + return EQN_TOK__MAX; +} + +void +eqn_box_free(struct eqn_box *bp) +{ + if (bp == NULL) + return; + + if (bp->first) + eqn_box_free(bp->first); + if (bp->next) + eqn_box_free(bp->next); + + free(bp->text); + free(bp->left); + free(bp->right); + free(bp->top); + free(bp->bottom); + free(bp); +} + +struct eqn_box * +eqn_box_new(void) +{ + struct eqn_box *bp; + + bp = mandoc_calloc(1, sizeof(*bp)); + bp->expectargs = UINT_MAX; + return bp; +} + +/* + * Allocate a box as the last child of the parent node. + */ +static struct eqn_box * +eqn_box_alloc(struct eqn_node *ep, struct eqn_box *parent) +{ + struct eqn_box *bp; + + bp = eqn_box_new(); + bp->parent = parent; + bp->parent->args++; + bp->font = bp->parent->font; + bp->size = ep->gsize; + + if (NULL != parent->first) { + parent->last->next = bp; + bp->prev = parent->last; + } else + parent->first = bp; + + parent->last = bp; + return bp; +} + +/* + * Reparent the current last node (of the current parent) under a new + * EQN_SUBEXPR as the first element. + * Then return the new parent. + * The new EQN_SUBEXPR will have a two-child limit. + */ +static struct eqn_box * +eqn_box_makebinary(struct eqn_node *ep, struct eqn_box *parent) +{ + struct eqn_box *b, *newb; + + assert(NULL != parent->last); + b = parent->last; + if (parent->last == parent->first) + parent->first = NULL; + parent->args--; + parent->last = b->prev; + b->prev = NULL; + newb = eqn_box_alloc(ep, parent); + newb->type = EQN_SUBEXPR; + newb->expectargs = 2; + newb->args = 1; + newb->first = newb->last = b; + newb->first->next = NULL; + b->parent = newb; + return newb; +} + +/* + * Parse the "delim" control statement. + */ +static void +eqn_delim(struct eqn_node *ep) +{ + if (ep->end[0] == '\0' || ep->end[1] == '\0') { + mandoc_msg(MANDOCERR_REQ_EMPTY, + ep->node->line, ep->node->pos, "delim"); + if (ep->end[0] != '\0') + ep->end++; + } else if (strncmp(ep->end, "off", 3) == 0) { + ep->delim = 0; + ep->end += 3; + } else if (strncmp(ep->end, "on", 2) == 0) { + if (ep->odelim && ep->cdelim) + ep->delim = 1; + ep->end += 2; + } else { + ep->odelim = *ep->end++; + ep->cdelim = *ep->end++; + ep->delim = 1; + } +} + +/* + * Undefine a previously-defined string. + */ +static void +eqn_undef(struct eqn_node *ep) +{ + struct eqn_def *def; + + if (eqn_next(ep, MODE_NOSUB) == EQN_TOK_EOF) { + mandoc_msg(MANDOCERR_REQ_EMPTY, + ep->node->line, ep->node->pos, "undef"); + return; + } + if ((def = eqn_def_find(ep)) == NULL) + return; + free(def->key); + free(def->val); + def->key = def->val = NULL; + def->keysz = def->valsz = 0; +} + +static void +eqn_def(struct eqn_node *ep) +{ + struct eqn_def *def; + int i; + + if (eqn_next(ep, MODE_NOSUB) == EQN_TOK_EOF) { + mandoc_msg(MANDOCERR_REQ_EMPTY, + ep->node->line, ep->node->pos, "define"); + return; + } + + /* + * Search for a key that already exists. + * Create a new key if none is found. + */ + if ((def = eqn_def_find(ep)) == NULL) { + /* Find holes in string array. */ + for (i = 0; i < (int)ep->defsz; i++) + if (0 == ep->defs[i].keysz) + break; + + if (i == (int)ep->defsz) { + ep->defsz++; + ep->defs = mandoc_reallocarray(ep->defs, + ep->defsz, sizeof(struct eqn_def)); + ep->defs[i].key = ep->defs[i].val = NULL; + } + + def = ep->defs + i; + free(def->key); + def->key = mandoc_strndup(ep->start, ep->toksz); + def->keysz = ep->toksz; + } + + if (eqn_next(ep, MODE_QUOTED) == EQN_TOK_EOF) { + mandoc_msg(MANDOCERR_REQ_EMPTY, + ep->node->line, ep->node->pos, "define %s", def->key); + free(def->key); + free(def->val); + def->key = def->val = NULL; + def->keysz = def->valsz = 0; + return; + } + free(def->val); + def->val = mandoc_strndup(ep->start, ep->toksz); + def->valsz = ep->toksz; +} + +void +eqn_parse(struct eqn_node *ep) +{ + struct eqn_box *cur, *nbox, *parent, *split; + const char *cp, *cpn; + char *p; + enum eqn_tok tok; + enum { CCL_LET, CCL_DIG, CCL_PUN } ccl, ccln; + int size; + + parent = ep->node->eqn; + assert(parent != NULL); + + /* + * Empty equation. + * Do not add it to the high-level syntax tree. + */ + + if (ep->data == NULL) + return; + + ep->start = ep->end = ep->data; + +next_tok: + tok = eqn_next(ep, MODE_TOK); + switch (tok) { + case EQN_TOK_UNDEF: + eqn_undef(ep); + break; + case EQN_TOK_NDEFINE: + case EQN_TOK_DEFINE: + eqn_def(ep); + break; + case EQN_TOK_TDEFINE: + if (eqn_next(ep, MODE_NOSUB) == EQN_TOK_EOF || + eqn_next(ep, MODE_QUOTED) == EQN_TOK_EOF) + mandoc_msg(MANDOCERR_REQ_EMPTY, + ep->node->line, ep->node->pos, "tdefine"); + break; + case EQN_TOK_DELIM: + eqn_delim(ep); + break; + case EQN_TOK_GFONT: + if (eqn_next(ep, MODE_SUB) == EQN_TOK_EOF) + mandoc_msg(MANDOCERR_REQ_EMPTY, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + break; + case EQN_TOK_MARK: + case EQN_TOK_LINEUP: + /* Ignore these. */ + break; + case EQN_TOK_DYAD: + case EQN_TOK_VEC: + case EQN_TOK_UNDER: + case EQN_TOK_BAR: + case EQN_TOK_TILDE: + case EQN_TOK_HAT: + case EQN_TOK_DOT: + case EQN_TOK_DOTDOT: + if (parent->last == NULL) { + mandoc_msg(MANDOCERR_EQN_NOBOX, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + cur = eqn_box_alloc(ep, parent); + cur->type = EQN_TEXT; + cur->text = mandoc_strdup(""); + } + parent = eqn_box_makebinary(ep, parent); + parent->type = EQN_LIST; + parent->expectargs = 1; + parent->font = EQNFONT_ROMAN; + switch (tok) { + case EQN_TOK_DOTDOT: + parent->top = mandoc_strdup("\\[ad]"); + break; + case EQN_TOK_VEC: + parent->top = mandoc_strdup("\\[->]"); + break; + case EQN_TOK_DYAD: + parent->top = mandoc_strdup("\\[<>]"); + break; + case EQN_TOK_TILDE: + parent->top = mandoc_strdup("\\[a~]"); + break; + case EQN_TOK_UNDER: + parent->bottom = mandoc_strdup("\\[ul]"); + break; + case EQN_TOK_BAR: + parent->top = mandoc_strdup("\\[rn]"); + break; + case EQN_TOK_DOT: + parent->top = mandoc_strdup("\\[a.]"); + break; + case EQN_TOK_HAT: + parent->top = mandoc_strdup("\\[ha]"); + break; + default: + abort(); + } + parent = parent->parent; + break; + case EQN_TOK_FWD: + case EQN_TOK_BACK: + case EQN_TOK_DOWN: + case EQN_TOK_UP: + if (eqn_next(ep, MODE_SUB) == EQN_TOK_EOF) + mandoc_msg(MANDOCERR_REQ_EMPTY, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + break; + case EQN_TOK_FAT: + case EQN_TOK_ROMAN: + case EQN_TOK_ITALIC: + case EQN_TOK_BOLD: + while (parent->args == parent->expectargs) + parent = parent->parent; + /* + * These values apply to the next word or sequence of + * words; thus, we mark that we'll have a child with + * exactly one of those. + */ + parent = eqn_box_alloc(ep, parent); + parent->type = EQN_LIST; + parent->expectargs = 1; + switch (tok) { + case EQN_TOK_FAT: + parent->font = EQNFONT_FAT; + break; + case EQN_TOK_ROMAN: + parent->font = EQNFONT_ROMAN; + break; + case EQN_TOK_ITALIC: + parent->font = EQNFONT_ITALIC; + break; + case EQN_TOK_BOLD: + parent->font = EQNFONT_BOLD; + break; + default: + abort(); + } + break; + case EQN_TOK_SIZE: + case EQN_TOK_GSIZE: + /* Accept two values: integral size and a single. */ + if (eqn_next(ep, MODE_SUB) == EQN_TOK_EOF) { + mandoc_msg(MANDOCERR_REQ_EMPTY, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + break; + } + size = mandoc_strntoi(ep->start, ep->toksz, 10); + if (-1 == size) { + mandoc_msg(MANDOCERR_IT_NONUM, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + break; + } + if (EQN_TOK_GSIZE == tok) { + ep->gsize = size; + break; + } + while (parent->args == parent->expectargs) + parent = parent->parent; + parent = eqn_box_alloc(ep, parent); + parent->type = EQN_LIST; + parent->expectargs = 1; + parent->size = size; + break; + case EQN_TOK_FROM: + case EQN_TOK_TO: + case EQN_TOK_SUB: + case EQN_TOK_SUP: + /* + * We have a left-right-associative expression. + * Repivot under a positional node, open a child scope + * and keep on reading. + */ + if (parent->last == NULL) { + mandoc_msg(MANDOCERR_EQN_NOBOX, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + cur = eqn_box_alloc(ep, parent); + cur->type = EQN_TEXT; + cur->text = mandoc_strdup(""); + } + while (parent->expectargs == 1 && parent->args == 1) + parent = parent->parent; + if (tok == EQN_TOK_FROM || tok == EQN_TOK_TO) { + for (cur = parent; cur != NULL; cur = cur->parent) + if (cur->pos == EQNPOS_SUB || + cur->pos == EQNPOS_SUP || + cur->pos == EQNPOS_SUBSUP || + cur->pos == EQNPOS_SQRT || + cur->pos == EQNPOS_OVER) + break; + if (cur != NULL) + parent = cur->parent; + } + if (tok == EQN_TOK_SUP && parent->pos == EQNPOS_SUB) { + parent->expectargs = 3; + parent->pos = EQNPOS_SUBSUP; + break; + } + if (tok == EQN_TOK_TO && parent->pos == EQNPOS_FROM) { + parent->expectargs = 3; + parent->pos = EQNPOS_FROMTO; + break; + } + parent = eqn_box_makebinary(ep, parent); + switch (tok) { + case EQN_TOK_FROM: + parent->pos = EQNPOS_FROM; + break; + case EQN_TOK_TO: + parent->pos = EQNPOS_TO; + break; + case EQN_TOK_SUP: + parent->pos = EQNPOS_SUP; + break; + case EQN_TOK_SUB: + parent->pos = EQNPOS_SUB; + break; + default: + abort(); + } + break; + case EQN_TOK_SQRT: + while (parent->args == parent->expectargs) + parent = parent->parent; + /* + * Accept a left-right-associative set of arguments just + * like sub and sup and friends but without rebalancing + * under a pivot. + */ + parent = eqn_box_alloc(ep, parent); + parent->type = EQN_SUBEXPR; + parent->pos = EQNPOS_SQRT; + parent->expectargs = 1; + break; + case EQN_TOK_OVER: + /* + * We have a right-left-associative fraction. + * Close out anything that's currently open, then + * rebalance and continue reading. + */ + if (parent->last == NULL) { + mandoc_msg(MANDOCERR_EQN_NOBOX, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + cur = eqn_box_alloc(ep, parent); + cur->type = EQN_TEXT; + cur->text = mandoc_strdup(""); + } + while (parent->args == parent->expectargs) + parent = parent->parent; + while (EQN_SUBEXPR == parent->type) + parent = parent->parent; + parent = eqn_box_makebinary(ep, parent); + parent->pos = EQNPOS_OVER; + break; + case EQN_TOK_RIGHT: + case EQN_TOK_BRACE_CLOSE: + /* + * Close out the existing brace. + * FIXME: this is a shitty sentinel: we should really + * have a native EQN_BRACE type or whatnot. + */ + for (cur = parent; cur != NULL; cur = cur->parent) + if (cur->type == EQN_LIST && + cur->expectargs > 1 && + (tok == EQN_TOK_BRACE_CLOSE || + cur->left != NULL)) + break; + if (cur == NULL) { + mandoc_msg(MANDOCERR_BLK_NOTOPEN, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + break; + } + parent = cur; + if (EQN_TOK_RIGHT == tok) { + if (eqn_next(ep, MODE_SUB) == EQN_TOK_EOF) { + mandoc_msg(MANDOCERR_REQ_EMPTY, + ep->node->line, ep->node->pos, + "%s", eqn_toks[tok]); + break; + } + /* Handling depends on right/left. */ + if (STRNEQ(ep->start, ep->toksz, "ceiling", 7)) + parent->right = mandoc_strdup("\\[rc]"); + else if (STRNEQ(ep->start, ep->toksz, "floor", 5)) + parent->right = mandoc_strdup("\\[rf]"); + else + parent->right = + mandoc_strndup(ep->start, ep->toksz); + } + parent = parent->parent; + if (tok == EQN_TOK_BRACE_CLOSE && + (parent->type == EQN_PILE || + parent->type == EQN_MATRIX)) + parent = parent->parent; + /* Close out any "singleton" lists. */ + while (parent->type == EQN_LIST && + parent->expectargs == 1 && + parent->args == 1) + parent = parent->parent; + break; + case EQN_TOK_BRACE_OPEN: + case EQN_TOK_LEFT: + /* + * If we already have something in the stack and we're + * in an expression, then rewind til we're not any more + * (just like with the text node). + */ + while (parent->args == parent->expectargs) + parent = parent->parent; + if (EQN_TOK_LEFT == tok && + eqn_next(ep, MODE_SUB) == EQN_TOK_EOF) { + mandoc_msg(MANDOCERR_REQ_EMPTY, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + break; + } + parent = eqn_box_alloc(ep, parent); + parent->type = EQN_LIST; + if (EQN_TOK_LEFT == tok) { + if (STRNEQ(ep->start, ep->toksz, "ceiling", 7)) + parent->left = mandoc_strdup("\\[lc]"); + else if (STRNEQ(ep->start, ep->toksz, "floor", 5)) + parent->left = mandoc_strdup("\\[lf]"); + else + parent->left = + mandoc_strndup(ep->start, ep->toksz); + } + break; + case EQN_TOK_PILE: + case EQN_TOK_LPILE: + case EQN_TOK_RPILE: + case EQN_TOK_CPILE: + case EQN_TOK_CCOL: + case EQN_TOK_LCOL: + case EQN_TOK_RCOL: + while (parent->args == parent->expectargs) + parent = parent->parent; + parent = eqn_box_alloc(ep, parent); + parent->type = EQN_PILE; + parent->expectargs = 1; + break; + case EQN_TOK_ABOVE: + for (cur = parent; cur != NULL; cur = cur->parent) + if (cur->type == EQN_PILE) + break; + if (cur == NULL) { + mandoc_msg(MANDOCERR_IT_STRAY, ep->node->line, + ep->node->pos, "%s", eqn_toks[tok]); + break; + } + parent = eqn_box_alloc(ep, cur); + parent->type = EQN_LIST; + break; + case EQN_TOK_MATRIX: + while (parent->args == parent->expectargs) + parent = parent->parent; + parent = eqn_box_alloc(ep, parent); + parent->type = EQN_MATRIX; + parent->expectargs = 1; + break; + case EQN_TOK_EOF: + return; + case EQN_TOK__MAX: + case EQN_TOK_FUNC: + case EQN_TOK_QUOTED: + case EQN_TOK_SYM: + p = ep->start; + assert(p != NULL); + /* + * If we already have something in the stack and we're + * in an expression, then rewind til we're not any more. + */ + while (parent->args == parent->expectargs) + parent = parent->parent; + cur = eqn_box_alloc(ep, parent); + cur->type = EQN_TEXT; + cur->text = p; + switch (tok) { + case EQN_TOK_FUNC: + cur->font = EQNFONT_ROMAN; + break; + case EQN_TOK_QUOTED: + if (cur->font == EQNFONT_NONE) + cur->font = EQNFONT_ITALIC; + break; + case EQN_TOK_SYM: + break; + default: + if (cur->font != EQNFONT_NONE || *p == '\0') + break; + cpn = p - 1; + ccln = CCL_LET; + split = NULL; + for (;;) { + /* Advance to next character. */ + cp = cpn++; + ccl = ccln; + ccln = isalpha((unsigned char)*cpn) ? CCL_LET : + isdigit((unsigned char)*cpn) || + (*cpn == '.' && (ccl == CCL_DIG || + isdigit((unsigned char)cpn[1]))) ? + CCL_DIG : CCL_PUN; + /* No boundary before first character. */ + if (cp < p) + continue; + cur->font = ccl == CCL_LET ? + EQNFONT_ITALIC : EQNFONT_ROMAN; + if (*cp == '\\') + mandoc_escape(&cpn, NULL, NULL); + /* No boundary after last character. */ + if (*cpn == '\0') + break; + if (ccln == ccl && *cp != ',' && *cpn != ',') + continue; + /* Boundary found, split the text. */ + if (parent->args == parent->expectargs) { + /* Remove the text from the tree. */ + if (cur->prev == NULL) + parent->first = cur->next; + else + cur->prev->next = NULL; + parent->last = cur->prev; + parent->args--; + /* Set up a list instead. */ + split = eqn_box_alloc(ep, parent); + split->type = EQN_LIST; + /* Insert the word into the list. */ + split->first = split->last = cur; + cur->parent = split; + cur->prev = NULL; + parent = split; + } + /* Append a new text box. */ + nbox = eqn_box_alloc(ep, parent); + nbox->type = EQN_TEXT; + nbox->text = mandoc_strdup(cpn); + /* Truncate the old box. */ + p = mandoc_strndup(cur->text, + cpn - cur->text); + free(cur->text); + cur->text = p; + /* Setup to process the new box. */ + cur = nbox; + p = nbox->text; + cpn = p - 1; + ccln = CCL_LET; + } + if (split != NULL) + parent = split->parent; + break; + } + break; + default: + abort(); + } + goto next_tok; +} + +void +eqn_free(struct eqn_node *p) +{ + int i; + + if (p == NULL) + return; + + for (i = 0; i < (int)p->defsz; i++) { + free(p->defs[i].key); + free(p->defs[i].val); + } + + free(p->data); + free(p->defs); + free(p); +} diff --git a/usr.bin/mandoc/eqn.h b/usr.bin/mandoc/eqn.h new file mode 100644 index 0000000..0eff4a4 --- /dev/null +++ b/usr.bin/mandoc/eqn.h @@ -0,0 +1,72 @@ +/* $OpenBSD: eqn.h,v 1.1 2018/12/13 05:13:15 schwarze Exp $ */ +/* + * Copyright (c) 2011, 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. + * + * Public data types for eqn(7) syntax trees. + */ + +enum eqn_boxt { + EQN_TEXT, /* Text, e.g. number, variable, operator, ... */ + EQN_SUBEXPR, /* Nested eqn(7) subexpression. */ + EQN_LIST, /* List, for example in braces. */ + EQN_PILE, /* Vertical pile. */ + EQN_MATRIX /* List of columns. */ +}; + +enum eqn_fontt { + EQNFONT_NONE = 0, + EQNFONT_ROMAN, + EQNFONT_BOLD, + EQNFONT_FAT, + EQNFONT_ITALIC, + EQNFONT__MAX +}; + +enum eqn_post { + EQNPOS_NONE = 0, + EQNPOS_SUP, + EQNPOS_SUBSUP, + EQNPOS_SUB, + EQNPOS_TO, + EQNPOS_FROM, + EQNPOS_FROMTO, + EQNPOS_OVER, + EQNPOS_SQRT, + EQNPOS__MAX +}; + + /* + * A "box" is a parsed mathematical expression as defined by the eqn.7 + * grammar. + */ +struct eqn_box { + struct eqn_box *parent; + struct eqn_box *prev; + struct eqn_box *next; + struct eqn_box *first; /* First child node. */ + struct eqn_box *last; /* Last child node. */ + char *text; /* Text (or NULL). */ + char *left; /* Left-hand fence. */ + char *right; /* Right-hand fence. */ + char *top; /* Symbol above. */ + char *bottom; /* Symbol below. */ + size_t expectargs; /* Maximal number of arguments. */ + size_t args; /* Actual number of arguments. */ + int size; /* Font size. */ +#define EQN_DEFSIZE INT_MIN + enum eqn_boxt type; /* Type of node. */ + enum eqn_fontt font; /* Font in this box. */ + enum eqn_post pos; /* Position of the next box. */ +}; diff --git a/usr.bin/mandoc/eqn_html.c b/usr.bin/mandoc/eqn_html.c new file mode 100644 index 0000000..049bbf9 --- /dev/null +++ b/usr.bin/mandoc/eqn_html.c @@ -0,0 +1,244 @@ +/* $OpenBSD: eqn_html.c,v 1.15 2019/03/17 18:20:07 schwarze Exp $ */ +/* + * Copyright (c) 2011, 2014 Kristaps Dzonsons + * 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 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 + +#include +#include +#include +#include +#include + +#include "mandoc.h" +#include "roff.h" +#include "eqn.h" +#include "out.h" +#include "html.h" + +static void +eqn_box(struct html *p, const struct eqn_box *bp) +{ + struct tag *post, *row, *cell, *t; + const struct eqn_box *child, *parent; + const char *cp; + size_t i, j, rows; + enum htmltag tag; + enum eqn_fontt font; + + if (NULL == bp) + return; + + post = NULL; + + /* + * Special handling for a matrix, which is presented to us in + * column order, but must be printed in row-order. + */ + if (EQN_MATRIX == bp->type) { + if (NULL == bp->first) + goto out; + if (bp->first->type != EQN_LIST || + bp->first->expectargs == 1) { + eqn_box(p, bp->first); + goto out; + } + if (NULL == (parent = bp->first->first)) + goto out; + /* Estimate the number of rows, first. */ + if (NULL == (child = parent->first)) + goto out; + for (rows = 0; NULL != child; rows++) + child = child->next; + /* Print row-by-row. */ + post = print_otag(p, TAG_MTABLE, ""); + for (i = 0; i < rows; i++) { + parent = bp->first->first; + row = print_otag(p, TAG_MTR, ""); + while (NULL != parent) { + child = parent->first; + for (j = 0; j < i; j++) { + if (NULL == child) + break; + child = child->next; + } + cell = print_otag(p, TAG_MTD, ""); + /* + * If we have no data for this + * particular cell, then print a + * placeholder and continue--don't puke. + */ + if (NULL != child) + eqn_box(p, child->first); + print_tagq(p, cell); + parent = parent->next; + } + print_tagq(p, row); + } + goto out; + } + + switch (bp->pos) { + case EQNPOS_TO: + post = print_otag(p, TAG_MOVER, ""); + break; + case EQNPOS_SUP: + post = print_otag(p, TAG_MSUP, ""); + break; + case EQNPOS_FROM: + post = print_otag(p, TAG_MUNDER, ""); + break; + case EQNPOS_SUB: + post = print_otag(p, TAG_MSUB, ""); + break; + case EQNPOS_OVER: + post = print_otag(p, TAG_MFRAC, ""); + break; + case EQNPOS_FROMTO: + post = print_otag(p, TAG_MUNDEROVER, ""); + break; + case EQNPOS_SUBSUP: + post = print_otag(p, TAG_MSUBSUP, ""); + break; + case EQNPOS_SQRT: + post = print_otag(p, TAG_MSQRT, ""); + break; + default: + break; + } + + if (bp->top || bp->bottom) { + assert(NULL == post); + if (bp->top && NULL == bp->bottom) + post = print_otag(p, TAG_MOVER, ""); + else if (bp->top && bp->bottom) + post = print_otag(p, TAG_MUNDEROVER, ""); + else if (bp->bottom) + post = print_otag(p, TAG_MUNDER, ""); + } + + if (EQN_PILE == bp->type) { + assert(NULL == post); + if (bp->first != NULL && + bp->first->type == EQN_LIST && + bp->first->expectargs > 1) + post = print_otag(p, TAG_MTABLE, ""); + } else if (bp->type == EQN_LIST && bp->expectargs > 1 && + bp->parent && bp->parent->type == EQN_PILE) { + assert(NULL == post); + post = print_otag(p, TAG_MTR, ""); + print_otag(p, TAG_MTD, ""); + } + + if (bp->text != NULL) { + assert(post == NULL); + tag = TAG_MI; + cp = bp->text; + if (isdigit((unsigned char)cp[0]) || + (cp[0] == '.' && isdigit((unsigned char)cp[1]))) { + tag = TAG_MN; + while (*++cp != '\0') { + if (*cp != '.' && + isdigit((unsigned char)*cp) == 0) { + tag = TAG_MI; + break; + } + } + } else if (*cp != '\0' && isalpha((unsigned char)*cp) == 0) { + tag = TAG_MO; + while (*cp != '\0') { + if (cp[0] == '\\' && cp[1] != '\0') { + cp++; + mandoc_escape(&cp, NULL, NULL); + } else if (isalnum((unsigned char)*cp)) { + tag = TAG_MI; + break; + } else + cp++; + } + } + font = bp->font; + if (bp->text[0] != '\0' && + (((tag == TAG_MN || tag == TAG_MO) && + font == EQNFONT_ROMAN) || + (tag == TAG_MI && font == (bp->text[1] == '\0' ? + EQNFONT_ITALIC : EQNFONT_ROMAN)))) + font = EQNFONT_NONE; + switch (font) { + case EQNFONT_NONE: + post = print_otag(p, tag, ""); + break; + case EQNFONT_ROMAN: + post = print_otag(p, tag, "?", "fontstyle", "normal"); + break; + case EQNFONT_BOLD: + case EQNFONT_FAT: + post = print_otag(p, tag, "?", "fontweight", "bold"); + break; + case EQNFONT_ITALIC: + post = print_otag(p, tag, "?", "fontstyle", "italic"); + break; + default: + abort(); + } + print_text(p, bp->text); + } else if (NULL == post) { + if (NULL != bp->left || NULL != bp->right) + post = print_otag(p, TAG_MFENCED, "??", + "open", bp->left == NULL ? "" : bp->left, + "close", bp->right == NULL ? "" : bp->right); + if (NULL == post) + post = print_otag(p, TAG_MROW, ""); + else + print_otag(p, TAG_MROW, ""); + } + + eqn_box(p, bp->first); + +out: + if (NULL != bp->bottom) { + t = print_otag(p, TAG_MO, ""); + print_text(p, bp->bottom); + print_tagq(p, t); + } + if (NULL != bp->top) { + t = print_otag(p, TAG_MO, ""); + print_text(p, bp->top); + print_tagq(p, t); + } + + if (NULL != post) + print_tagq(p, post); + + eqn_box(p, bp->next); +} + +void +print_eqn(struct html *p, const struct eqn_box *bp) +{ + struct tag *t; + + if (bp->first == NULL) + return; + + t = print_otag(p, TAG_MATH, "c", "eqn"); + + p->flags |= HTML_NONOSPACE; + eqn_box(p, bp); + p->flags &= ~HTML_NONOSPACE; + + print_tagq(p, t); +} diff --git a/usr.bin/mandoc/eqn_parse.h b/usr.bin/mandoc/eqn_parse.h new file mode 100644 index 0000000..0a8e619 --- /dev/null +++ b/usr.bin/mandoc/eqn_parse.h @@ -0,0 +1,48 @@ +/* $OpenBSD: eqn_parse.h,v 1.3 2018/12/14 06:33:03 schwarze Exp $ */ +/* + * Copyright (c) 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 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. + * + * External interface of the eqn(7) parser. + * For use in the roff(7) and eqn(7) parsers only. + */ + +struct roff_node; +struct eqn_box; +struct eqn_def; + +struct eqn_node { + struct roff_node *node; /* Syntax tree of this equation. */ + struct eqn_def *defs; /* Array of definitions. */ + char *data; /* Source code of this equation. */ + char *start; /* First byte of the current token. */ + char *end; /* First byte of the next token. */ + size_t defsz; /* Number of definitions. */ + size_t sz; /* Length of the source code. */ + size_t toksz; /* Length of the current token. */ + int gsize; /* Default point size. */ + int delim; /* In-line delimiters enabled. */ + char odelim; /* In-line opening delimiter. */ + char cdelim; /* In-line closing delimiter. */ +}; + + +struct eqn_node *eqn_alloc(void); +struct eqn_box *eqn_box_new(void); +void eqn_box_free(struct eqn_box *); +void eqn_free(struct eqn_node *); +void eqn_parse(struct eqn_node *); +void eqn_read(struct eqn_node *, const char *); +void eqn_reset(struct eqn_node *); diff --git a/usr.bin/mandoc/eqn_term.c b/usr.bin/mandoc/eqn_term.c new file mode 100644 index 0000000..7bb1aae --- /dev/null +++ b/usr.bin/mandoc/eqn_term.c @@ -0,0 +1,172 @@ +/* $OpenBSD: eqn_term.c,v 1.15 2018/12/13 05:13:15 schwarze Exp $ */ +/* + * Copyright (c) 2011 Kristaps Dzonsons + * Copyright (c) 2014, 2015, 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 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 + +#include +#include +#include +#include +#include + +#include "eqn.h" +#include "out.h" +#include "term.h" + +static const enum termfont fontmap[EQNFONT__MAX] = { + TERMFONT_NONE, /* EQNFONT_NONE */ + TERMFONT_NONE, /* EQNFONT_ROMAN */ + TERMFONT_BOLD, /* EQNFONT_BOLD */ + TERMFONT_BOLD, /* EQNFONT_FAT */ + TERMFONT_UNDER /* EQNFONT_ITALIC */ +}; + +static void eqn_box(struct termp *, const struct eqn_box *); + + +void +term_eqn(struct termp *p, const struct eqn_box *bp) +{ + + eqn_box(p, bp); + p->flags &= ~TERMP_NOSPACE; +} + +static void +eqn_box(struct termp *p, const struct eqn_box *bp) +{ + const struct eqn_box *child; + const char *cp; + int delim; + + /* Delimiters around this box? */ + + if ((bp->type == EQN_LIST && bp->expectargs > 1) || + (bp->type == EQN_PILE && (bp->prev || bp->next)) || + (bp->parent != NULL && (bp->parent->pos == EQNPOS_SQRT || + /* Diacritic followed by ^ or _. */ + ((bp->top != NULL || bp->bottom != NULL) && + bp->parent->type == EQN_SUBEXPR && + bp->parent->pos != EQNPOS_OVER && bp->next != NULL) || + /* Nested over, sub, sup, from, to. */ + (bp->type == EQN_SUBEXPR && bp->pos != EQNPOS_SQRT && + ((bp->parent->type == EQN_LIST && bp->expectargs == 1) || + (bp->parent->type == EQN_SUBEXPR && + bp->pos != EQNPOS_SQRT)))))) { + if ((bp->parent->type == EQN_SUBEXPR && bp->prev != NULL) || + (bp->type == EQN_LIST && + bp->first != NULL && + bp->first->type != EQN_PILE && + bp->first->type != EQN_MATRIX && + bp->prev != NULL && + (bp->prev->type == EQN_LIST || + (bp->prev->type == EQN_TEXT && + (*bp->prev->text == '\\' || + isalpha((unsigned char)*bp->prev->text)))))) + p->flags |= TERMP_NOSPACE; + term_word(p, bp->left != NULL ? bp->left : "("); + p->flags |= TERMP_NOSPACE; + delim = 1; + } else + delim = 0; + + /* Handle Fonts and text. */ + + if (bp->font != EQNFONT_NONE) + term_fontpush(p, fontmap[(int)bp->font]); + + if (bp->text != NULL) { + if (strchr("!\"'),.:;?]}", *bp->text) != NULL) + p->flags |= TERMP_NOSPACE; + term_word(p, bp->text); + if ((cp = strchr(bp->text, '\0')) > bp->text && + (strchr("\"'([{", cp[-1]) != NULL || + (bp->prev == NULL && (cp[-1] == '-' || + (cp >= bp->text + 5 && + strcmp(cp - 5, "\\[mi]") == 0))))) + p->flags |= TERMP_NOSPACE; + } + + /* Special box types. */ + + if (bp->pos == EQNPOS_SQRT) { + term_word(p, "\\(sr"); + if (bp->first != NULL) { + p->flags |= TERMP_NOSPACE; + eqn_box(p, bp->first); + } + } else if (bp->type == EQN_SUBEXPR) { + child = bp->first; + eqn_box(p, child); + p->flags |= TERMP_NOSPACE; + term_word(p, bp->pos == EQNPOS_OVER ? "/" : + (bp->pos == EQNPOS_SUP || + bp->pos == EQNPOS_TO) ? "^" : "_"); + child = child->next; + if (child != NULL) { + p->flags |= TERMP_NOSPACE; + eqn_box(p, child); + if (bp->pos == EQNPOS_FROMTO || + bp->pos == EQNPOS_SUBSUP) { + p->flags |= TERMP_NOSPACE; + term_word(p, "^"); + p->flags |= TERMP_NOSPACE; + child = child->next; + if (child != NULL) + eqn_box(p, child); + } + } + } else { + child = bp->first; + if (bp->type == EQN_MATRIX && + child != NULL && + child->type == EQN_LIST && + child->expectargs > 1) + child = child->first; + while (child != NULL) { + eqn_box(p, + bp->type == EQN_PILE && + child->type == EQN_LIST && + child->expectargs > 1 && + child->args == 1 ? + child->first : child); + child = child->next; + } + } + + /* Handle Fonts and diacritics. */ + + if (bp->font != EQNFONT_NONE) + term_fontpop(p); + if (bp->top != NULL) { + p->flags |= TERMP_NOSPACE; + term_word(p, bp->top); + } + if (bp->bottom != NULL) { + p->flags |= TERMP_NOSPACE; + term_word(p, "_"); + } + + /* Right delimiter after this box? */ + + if (delim) { + p->flags |= TERMP_NOSPACE; + term_word(p, bp->right != NULL ? bp->right : ")"); + if (bp->parent->type == EQN_SUBEXPR && bp->next != NULL) + p->flags |= TERMP_NOSPACE; + } +} diff --git a/usr.bin/mandoc/html.c b/usr.bin/mandoc/html.c new file mode 100644 index 0000000..0225e66 --- /dev/null +++ b/usr.bin/mandoc/html.c @@ -0,0 +1,1085 @@ +/* $OpenBSD: html.c,v 1.141 2020/04/20 12:59:24 schwarze Exp $ */ +/* + * Copyright (c) 2011-2015, 2017-2020 Ingo Schwarze + * Copyright (c) 2008-2011, 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. + * + * Common functions for mandoc(1) HTML formatters. + * For use by individual formatters and by the main program. + */ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mandoc_aux.h" +#include "mandoc_ohash.h" +#include "mandoc.h" +#include "roff.h" +#include "out.h" +#include "html.h" +#include "manconf.h" +#include "main.h" + +struct htmldata { + const char *name; + int flags; +#define HTML_INPHRASE (1 << 0) /* Can appear in phrasing context. */ +#define HTML_TOPHRASE (1 << 1) /* Establishes phrasing context. */ +#define HTML_NOSTACK (1 << 2) /* Does not have an end tag. */ +#define HTML_NLBEFORE (1 << 3) /* Output line break before opening. */ +#define HTML_NLBEGIN (1 << 4) /* Output line break after opening. */ +#define HTML_NLEND (1 << 5) /* Output line break before closing. */ +#define HTML_NLAFTER (1 << 6) /* Output line break after closing. */ +#define HTML_NLAROUND (HTML_NLBEFORE | HTML_NLAFTER) +#define HTML_NLINSIDE (HTML_NLBEGIN | HTML_NLEND) +#define HTML_NLALL (HTML_NLAROUND | HTML_NLINSIDE) +#define HTML_INDENT (1 << 7) /* Indent content by two spaces. */ +#define HTML_NOINDENT (1 << 8) /* Exception: never indent content. */ +}; + +static const struct htmldata htmltags[TAG_MAX] = { + {"html", HTML_NLALL}, + {"head", HTML_NLALL | HTML_INDENT}, + {"meta", HTML_NOSTACK | HTML_NLALL}, + {"link", HTML_NOSTACK | HTML_NLALL}, + {"style", HTML_NLALL | HTML_INDENT}, + {"title", HTML_NLAROUND}, + {"body", HTML_NLALL}, + {"div", HTML_NLAROUND}, + {"section", HTML_NLALL}, + {"table", HTML_NLALL | HTML_INDENT}, + {"tr", HTML_NLALL | HTML_INDENT}, + {"td", HTML_NLAROUND}, + {"li", HTML_NLAROUND | HTML_INDENT}, + {"ul", HTML_NLALL | HTML_INDENT}, + {"ol", HTML_NLALL | HTML_INDENT}, + {"dl", HTML_NLALL | HTML_INDENT}, + {"dt", HTML_NLAROUND}, + {"dd", HTML_NLAROUND | HTML_INDENT}, + {"h1", HTML_TOPHRASE | HTML_NLAROUND}, + {"h2", HTML_TOPHRASE | HTML_NLAROUND}, + {"p", HTML_TOPHRASE | HTML_NLAROUND | HTML_INDENT}, + {"pre", HTML_TOPHRASE | HTML_NLALL | HTML_NOINDENT}, + {"a", HTML_INPHRASE | HTML_TOPHRASE}, + {"b", HTML_INPHRASE | HTML_TOPHRASE}, + {"cite", HTML_INPHRASE | HTML_TOPHRASE}, + {"code", HTML_INPHRASE | HTML_TOPHRASE}, + {"i", HTML_INPHRASE | HTML_TOPHRASE}, + {"small", HTML_INPHRASE | HTML_TOPHRASE}, + {"span", HTML_INPHRASE | HTML_TOPHRASE}, + {"var", HTML_INPHRASE | HTML_TOPHRASE}, + {"br", HTML_INPHRASE | HTML_NOSTACK | HTML_NLALL}, + {"mark", HTML_INPHRASE }, + {"math", HTML_INPHRASE | HTML_NLALL | HTML_INDENT}, + {"mrow", 0}, + {"mi", 0}, + {"mn", 0}, + {"mo", 0}, + {"msup", 0}, + {"msub", 0}, + {"msubsup", 0}, + {"mfrac", 0}, + {"msqrt", 0}, + {"mfenced", 0}, + {"mtable", 0}, + {"mtr", 0}, + {"mtd", 0}, + {"munderover", 0}, + {"munder", 0}, + {"mover", 0}, +}; + +/* Avoid duplicate HTML id= attributes. */ + +struct id_entry { + int ord; /* Ordinal number of the latest occurrence. */ + char id[]; /* The id= attribute without any ordinal suffix. */ +}; +static struct ohash id_unique; + +static void html_reset_internal(struct html *); +static void print_byte(struct html *, char); +static void print_endword(struct html *); +static void print_indent(struct html *); +static void print_word(struct html *, const char *); + +static void print_ctag(struct html *, struct tag *); +static int print_escape(struct html *, char); +static int print_encode(struct html *, const char *, const char *, int); +static void print_href(struct html *, const char *, const char *, int); +static void print_metaf(struct html *); + + +void * +html_alloc(const struct manoutput *outopts) +{ + struct html *h; + + h = mandoc_calloc(1, sizeof(struct html)); + + h->tag = NULL; + h->style = outopts->style; + if ((h->base_man1 = outopts->man) == NULL) + h->base_man2 = NULL; + else if ((h->base_man2 = strchr(h->base_man1, ';')) != NULL) + *h->base_man2++ = '\0'; + h->base_includes = outopts->includes; + if (outopts->fragment) + h->oflags |= HTML_FRAGMENT; + if (outopts->toc) + h->oflags |= HTML_TOC; + + mandoc_ohash_init(&id_unique, 4, offsetof(struct id_entry, id)); + + return h; +} + +static void +html_reset_internal(struct html *h) +{ + struct tag *tag; + struct id_entry *entry; + unsigned int slot; + + while ((tag = h->tag) != NULL) { + h->tag = tag->next; + free(tag); + } + entry = ohash_first(&id_unique, &slot); + while (entry != NULL) { + free(entry); + entry = ohash_next(&id_unique, &slot); + } + ohash_delete(&id_unique); +} + +void +html_reset(void *p) +{ + html_reset_internal(p); + mandoc_ohash_init(&id_unique, 4, offsetof(struct id_entry, id)); +} + +void +html_free(void *p) +{ + html_reset_internal(p); + free(p); +} + +void +print_gen_head(struct html *h) +{ + struct tag *t; + + print_otag(h, TAG_META, "?", "charset", "utf-8"); + if (h->style != NULL) { + print_otag(h, TAG_LINK, "?h??", "rel", "stylesheet", + h->style, "type", "text/css", "media", "all"); + return; + } + + /* + * Print a minimal embedded style sheet. + */ + + t = print_otag(h, TAG_STYLE, ""); + print_text(h, "table.head, table.foot { width: 100%; }"); + print_endline(h); + print_text(h, "td.head-rtitle, td.foot-os { text-align: right; }"); + print_endline(h); + print_text(h, "td.head-vol { text-align: center; }"); + print_endline(h); + print_text(h, ".Nd, .Bf, .Op { display: inline; }"); + print_endline(h); + print_text(h, ".Pa, .Ad { font-style: italic; }"); + print_endline(h); + print_text(h, ".Ms { font-weight: bold; }"); + print_endline(h); + print_text(h, ".Bl-diag "); + print_byte(h, '>'); + print_text(h, " dt { font-weight: bold; }"); + print_endline(h); + print_text(h, "code.Nm, .Fl, .Cm, .Ic, code.In, .Fd, .Fn, .Cd " + "{ font-weight: bold; font-family: inherit; }"); + print_tagq(h, t); +} + +int +html_setfont(struct html *h, enum mandoc_esc font) +{ + switch (font) { + case ESCAPE_FONTPREV: + font = h->metal; + break; + case ESCAPE_FONTITALIC: + case ESCAPE_FONTBOLD: + case ESCAPE_FONTBI: + case ESCAPE_FONTCW: + case ESCAPE_FONTROMAN: + break; + case ESCAPE_FONT: + font = ESCAPE_FONTROMAN; + break; + default: + return 0; + } + h->metal = h->metac; + h->metac = font; + return 1; +} + +static void +print_metaf(struct html *h) +{ + if (h->metaf) { + print_tagq(h, h->metaf); + h->metaf = NULL; + } + switch (h->metac) { + case ESCAPE_FONTITALIC: + h->metaf = print_otag(h, TAG_I, ""); + break; + case ESCAPE_FONTBOLD: + h->metaf = print_otag(h, TAG_B, ""); + break; + case ESCAPE_FONTBI: + h->metaf = print_otag(h, TAG_B, ""); + print_otag(h, TAG_I, ""); + break; + case ESCAPE_FONTCW: + h->metaf = print_otag(h, TAG_SPAN, "c", "Li"); + break; + default: + break; + } +} + +void +html_close_paragraph(struct html *h) +{ + struct tag *this, *next; + int flags; + + this = h->tag; + for (;;) { + next = this->next; + flags = htmltags[this->tag].flags; + if (flags & (HTML_INPHRASE | HTML_TOPHRASE)) + print_ctag(h, this); + if ((flags & HTML_INPHRASE) == 0) + break; + this = next; + } +} + +/* + * ROFF_nf switches to no-fill mode, ROFF_fi to fill mode. + * TOKEN_NONE does not switch. The old mode is returned. + */ +enum roff_tok +html_fillmode(struct html *h, enum roff_tok want) +{ + struct tag *t; + enum roff_tok had; + + for (t = h->tag; t != NULL; t = t->next) + if (t->tag == TAG_PRE) + break; + + had = t == NULL ? ROFF_fi : ROFF_nf; + + if (want != had) { + switch (want) { + case ROFF_fi: + print_tagq(h, t); + break; + case ROFF_nf: + html_close_paragraph(h); + print_otag(h, TAG_PRE, ""); + break; + case TOKEN_NONE: + break; + default: + abort(); + } + } + return had; +} + +/* + * Allocate a string to be used for the "id=" attribute of an HTML + * element and/or as a segment identifier for a URI in an element. + * The function may fail and return NULL if the node lacks text data + * to create the attribute from. + * The caller is responsible for free(3)ing the returned string. + * + * If the "unique" argument is non-zero, the "id_unique" ohash table + * is used for de-duplication. If the "unique" argument is 1, + * it is the first time the function is called for this tag and + * location, so if an ordinal suffix is needed, it is incremented. + * If the "unique" argument is 2, it is the second time the function + * is called for this tag and location, so the ordinal suffix + * remains unchanged. + */ +char * +html_make_id(const struct roff_node *n, int unique) +{ + const struct roff_node *nch; + struct id_entry *entry; + char *buf, *cp; + size_t len; + unsigned int slot; + + if (n->tag != NULL) + buf = mandoc_strdup(n->tag); + else { + switch (n->tok) { + case MDOC_Sh: + case MDOC_Ss: + case MDOC_Sx: + case MAN_SH: + case MAN_SS: + for (nch = n->child; nch != NULL; nch = nch->next) + if (nch->type != ROFFT_TEXT) + return NULL; + buf = NULL; + deroff(&buf, n); + if (buf == NULL) + return NULL; + break; + default: + if (n->child == NULL || n->child->type != ROFFT_TEXT) + return NULL; + buf = mandoc_strdup(n->child->string); + break; + } + } + + /* + * In ID attributes, only use ASCII characters that are + * permitted in URL-fragment strings according to the + * explicit list at: + * https://url.spec.whatwg.org/#url-fragment-string + * In addition, reserve '~' for ordinal suffixes. + */ + + for (cp = buf; *cp != '\0'; cp++) + if (isalnum((unsigned char)*cp) == 0 && + strchr("!$&'()*+,-./:;=?@_", *cp) == NULL) + *cp = '_'; + + if (unique == 0) + return buf; + + /* Avoid duplicate HTML id= attributes. */ + + slot = ohash_qlookup(&id_unique, buf); + if ((entry = ohash_find(&id_unique, slot)) == NULL) { + len = strlen(buf) + 1; + entry = mandoc_malloc(sizeof(*entry) + len); + entry->ord = 1; + memcpy(entry->id, buf, len); + ohash_insert(&id_unique, slot, entry); + } else if (unique == 1) + entry->ord++; + + if (entry->ord > 1) { + cp = buf; + mandoc_asprintf(&buf, "%s~%d", cp, entry->ord); + free(cp); + } + return buf; +} + +static int +print_escape(struct html *h, char c) +{ + + switch (c) { + case '<': + print_word(h, "<"); + break; + case '>': + print_word(h, ">"); + break; + case '&': + print_word(h, "&"); + break; + case '"': + print_word(h, """); + break; + case ASCII_NBRSP: + print_word(h, " "); + break; + case ASCII_HYPH: + print_byte(h, '-'); + break; + case ASCII_BREAK: + break; + default: + return 0; + } + return 1; +} + +static int +print_encode(struct html *h, const char *p, const char *pend, int norecurse) +{ + char numbuf[16]; + const char *seq; + size_t sz; + int c, len, breakline, nospace; + enum mandoc_esc esc; + static const char rejs[10] = { ' ', '\\', '<', '>', '&', '"', + ASCII_NBRSP, ASCII_HYPH, ASCII_BREAK, '\0' }; + + if (pend == NULL) + pend = strchr(p, '\0'); + + breakline = 0; + nospace = 0; + + while (p < pend) { + if (HTML_SKIPCHAR & h->flags && '\\' != *p) { + h->flags &= ~HTML_SKIPCHAR; + p++; + continue; + } + + for (sz = strcspn(p, rejs); sz-- && p < pend; p++) + print_byte(h, *p); + + if (breakline && + (p >= pend || *p == ' ' || *p == ASCII_NBRSP)) { + print_otag(h, TAG_BR, ""); + breakline = 0; + while (p < pend && (*p == ' ' || *p == ASCII_NBRSP)) + p++; + continue; + } + + if (p >= pend) + break; + + if (*p == ' ') { + print_endword(h); + p++; + continue; + } + + if (print_escape(h, *p++)) + continue; + + esc = mandoc_escape(&p, &seq, &len); + switch (esc) { + case ESCAPE_FONT: + case ESCAPE_FONTPREV: + case ESCAPE_FONTBOLD: + case ESCAPE_FONTITALIC: + case ESCAPE_FONTBI: + case ESCAPE_FONTCW: + case ESCAPE_FONTROMAN: + if (0 == norecurse) { + h->flags |= HTML_NOSPACE; + if (html_setfont(h, esc)) + print_metaf(h); + h->flags &= ~HTML_NOSPACE; + } + continue; + case ESCAPE_SKIPCHAR: + h->flags |= HTML_SKIPCHAR; + continue; + case ESCAPE_ERROR: + continue; + default: + break; + } + + if (h->flags & HTML_SKIPCHAR) { + h->flags &= ~HTML_SKIPCHAR; + continue; + } + + switch (esc) { + case ESCAPE_UNICODE: + /* Skip past "u" header. */ + c = mchars_num2uc(seq + 1, len - 1); + break; + case ESCAPE_NUMBERED: + c = mchars_num2char(seq, len); + if (c < 0) + continue; + break; + case ESCAPE_SPECIAL: + c = mchars_spec2cp(seq, len); + if (c <= 0) + continue; + break; + case ESCAPE_UNDEF: + c = *seq; + break; + case ESCAPE_DEVICE: + print_word(h, "html"); + continue; + case ESCAPE_BREAK: + breakline = 1; + continue; + case ESCAPE_NOSPACE: + if ('\0' == *p) + nospace = 1; + continue; + case ESCAPE_OVERSTRIKE: + if (len == 0) + continue; + c = seq[len - 1]; + break; + default: + continue; + } + if ((c < 0x20 && c != 0x09) || + (c > 0x7E && c < 0xA0)) + c = 0xFFFD; + if (c > 0x7E) { + (void)snprintf(numbuf, sizeof(numbuf), "&#x%.4X;", c); + print_word(h, numbuf); + } else if (print_escape(h, c) == 0) + print_byte(h, c); + } + + return nospace; +} + +static void +print_href(struct html *h, const char *name, const char *sec, int man) +{ + struct stat sb; + const char *p, *pp; + char *filename; + + if (man) { + pp = h->base_man1; + if (h->base_man2 != NULL) { + mandoc_asprintf(&filename, "%s.%s", name, sec); + if (stat(filename, &sb) == -1) + pp = h->base_man2; + free(filename); + } + } else + pp = h->base_includes; + + while ((p = strchr(pp, '%')) != NULL) { + print_encode(h, pp, p, 1); + if (man && p[1] == 'S') { + if (sec == NULL) + print_byte(h, '1'); + else + print_encode(h, sec, NULL, 1); + } else if ((man && p[1] == 'N') || + (man == 0 && p[1] == 'I')) + print_encode(h, name, NULL, 1); + else + print_encode(h, p, p + 2, 1); + pp = p + 2; + } + if (*pp != '\0') + print_encode(h, pp, NULL, 1); +} + +struct tag * +print_otag(struct html *h, enum htmltag tag, const char *fmt, ...) +{ + va_list ap; + struct tag *t; + const char *attr; + char *arg1, *arg2; + int style_written, tflags; + + tflags = htmltags[tag].flags; + + /* Flow content is not allowed in phrasing context. */ + + if ((tflags & HTML_INPHRASE) == 0) { + for (t = h->tag; t != NULL; t = t->next) { + if (t->closed) + continue; + assert((htmltags[t->tag].flags & HTML_TOPHRASE) == 0); + break; + } + + /* + * Always wrap phrasing elements in a paragraph + * unless already contained in some flow container; + * never put them directly into a section. + */ + + } else if (tflags & HTML_TOPHRASE && h->tag->tag == TAG_SECTION) + print_otag(h, TAG_P, "c", "Pp"); + + /* Push this tag onto the stack of open scopes. */ + + if ((tflags & HTML_NOSTACK) == 0) { + t = mandoc_malloc(sizeof(struct tag)); + t->tag = tag; + t->next = h->tag; + t->refcnt = 0; + t->closed = 0; + h->tag = t; + } else + t = NULL; + + if (tflags & HTML_NLBEFORE) + print_endline(h); + if (h->col == 0) + print_indent(h); + else if ((h->flags & HTML_NOSPACE) == 0) { + if (h->flags & HTML_KEEP) + print_word(h, " "); + else { + if (h->flags & HTML_PREKEEP) + h->flags |= HTML_KEEP; + print_endword(h); + } + } + + if ( ! (h->flags & HTML_NONOSPACE)) + h->flags &= ~HTML_NOSPACE; + else + h->flags |= HTML_NOSPACE; + + /* Print out the tag name and attributes. */ + + print_byte(h, '<'); + print_word(h, htmltags[tag].name); + + va_start(ap, fmt); + + while (*fmt != '\0' && *fmt != 's') { + + /* Parse attributes and arguments. */ + + arg1 = va_arg(ap, char *); + arg2 = NULL; + switch (*fmt++) { + case 'c': + attr = "class"; + break; + case 'h': + attr = "href"; + break; + case 'i': + attr = "id"; + break; + case '?': + attr = arg1; + arg1 = va_arg(ap, char *); + break; + default: + abort(); + } + if (*fmt == 'M') + arg2 = va_arg(ap, char *); + if (arg1 == NULL) + continue; + + /* Print the attributes. */ + + print_byte(h, ' '); + print_word(h, attr); + print_byte(h, '='); + print_byte(h, '"'); + switch (*fmt) { + case 'I': + print_href(h, arg1, NULL, 0); + fmt++; + break; + case 'M': + print_href(h, arg1, arg2, 1); + fmt++; + break; + case 'R': + print_byte(h, '#'); + print_encode(h, arg1, NULL, 1); + fmt++; + break; + default: + print_encode(h, arg1, NULL, 1); + break; + } + print_byte(h, '"'); + } + + style_written = 0; + while (*fmt++ == 's') { + arg1 = va_arg(ap, char *); + arg2 = va_arg(ap, char *); + if (arg2 == NULL) + continue; + print_byte(h, ' '); + if (style_written == 0) { + print_word(h, "style=\""); + style_written = 1; + } + print_word(h, arg1); + print_byte(h, ':'); + print_byte(h, ' '); + print_word(h, arg2); + print_byte(h, ';'); + } + if (style_written) + print_byte(h, '"'); + + va_end(ap); + + /* Accommodate for "well-formed" singleton escaping. */ + + if (htmltags[tag].flags & HTML_NOSTACK) + print_byte(h, '/'); + + print_byte(h, '>'); + + if (tflags & HTML_NLBEGIN) + print_endline(h); + else + h->flags |= HTML_NOSPACE; + + if (tflags & HTML_INDENT) + h->indent++; + if (tflags & HTML_NOINDENT) + h->noindent++; + + return t; +} + +/* + * Print an element with an optional "id=" attribute. + * If the element has phrasing content and an "id=" attribute, + * also add a permalink: outside if it can be in phrasing context, + * inside otherwise. + */ +struct tag * +print_otag_id(struct html *h, enum htmltag elemtype, const char *cattr, + struct roff_node *n) +{ + struct roff_node *nch; + struct tag *ret, *t; + char *id, *href; + + ret = NULL; + id = href = NULL; + if (n->flags & NODE_ID) + id = html_make_id(n, 1); + if (n->flags & NODE_HREF) + href = id == NULL ? html_make_id(n, 2) : id; + if (href != NULL && htmltags[elemtype].flags & HTML_INPHRASE) + ret = print_otag(h, TAG_A, "chR", "permalink", href); + t = print_otag(h, elemtype, "ci", cattr, id); + if (ret == NULL) { + ret = t; + if (href != NULL && (nch = n->child) != NULL) { + /* man(7) is safe, it tags phrasing content only. */ + if (n->tok > MDOC_MAX || + htmltags[elemtype].flags & HTML_TOPHRASE) + nch = NULL; + else /* For mdoc(7), beware of nested blocks. */ + while (nch != NULL && nch->type == ROFFT_TEXT) + nch = nch->next; + if (nch == NULL) + print_otag(h, TAG_A, "chR", "permalink", href); + } + } + free(id); + if (id == NULL) + free(href); + return ret; +} + +static void +print_ctag(struct html *h, struct tag *tag) +{ + int tflags; + + if (tag->closed == 0) { + tag->closed = 1; + if (tag == h->metaf) + h->metaf = NULL; + if (tag == h->tblt) + h->tblt = NULL; + + tflags = htmltags[tag->tag].flags; + if (tflags & HTML_INDENT) + h->indent--; + if (tflags & HTML_NOINDENT) + h->noindent--; + if (tflags & HTML_NLEND) + print_endline(h); + print_indent(h); + print_byte(h, '<'); + print_byte(h, '/'); + print_word(h, htmltags[tag->tag].name); + print_byte(h, '>'); + if (tflags & HTML_NLAFTER) + print_endline(h); + } + if (tag->refcnt == 0) { + h->tag = tag->next; + free(tag); + } +} + +void +print_gen_decls(struct html *h) +{ + print_word(h, ""); + print_endline(h); +} + +void +print_gen_comment(struct html *h, struct roff_node *n) +{ + int wantblank; + + print_word(h, "") == NULL && + (wantblank || *n->string != '\0')) { + print_endline(h); + print_indent(h); + print_word(h, n->string); + wantblank = *n->string != '\0'; + } + n = n->next; + } + if (wantblank) + print_endline(h); + print_word(h, " -->"); + print_endline(h); + h->indent = 0; +} + +void +print_text(struct html *h, const char *word) +{ + print_tagged_text(h, word, NULL); +} + +void +print_tagged_text(struct html *h, const char *word, struct roff_node *n) +{ + struct tag *t; + char *href; + + /* + * Always wrap text in a paragraph unless already contained in + * some flow container; never put it directly into a section. + */ + + if (h->tag->tag == TAG_SECTION) + print_otag(h, TAG_P, "c", "Pp"); + + /* Output whitespace before this text? */ + + if (h->col && (h->flags & HTML_NOSPACE) == 0) { + if ( ! (HTML_KEEP & h->flags)) { + if (HTML_PREKEEP & h->flags) + h->flags |= HTML_KEEP; + print_endword(h); + } else + print_word(h, " "); + } + + /* + * Optionally switch fonts, optionally write a permalink, then + * print the text, optionally surrounded by HTML whitespace. + */ + + assert(h->metaf == NULL); + print_metaf(h); + print_indent(h); + + if (n != NULL && (href = html_make_id(n, 2)) != NULL) { + t = print_otag(h, TAG_A, "chR", "permalink", href); + free(href); + } else + t = NULL; + + if ( ! print_encode(h, word, NULL, 0)) { + if ( ! (h->flags & HTML_NONOSPACE)) + h->flags &= ~HTML_NOSPACE; + h->flags &= ~HTML_NONEWLINE; + } else + h->flags |= HTML_NOSPACE | HTML_NONEWLINE; + + if (h->metaf != NULL) { + print_tagq(h, h->metaf); + h->metaf = NULL; + } else if (t != NULL) + print_tagq(h, t); + + h->flags &= ~HTML_IGNDELIM; +} + +void +print_tagq(struct html *h, const struct tag *until) +{ + struct tag *this, *next; + + for (this = h->tag; this != NULL; this = next) { + next = this == until ? NULL : this->next; + print_ctag(h, this); + } +} + +/* + * Close out all open elements up to but excluding suntil. + * Note that a paragraph just inside stays open together with it + * because paragraphs include subsequent phrasing content. + */ +void +print_stagq(struct html *h, const struct tag *suntil) +{ + struct tag *this, *next; + + for (this = h->tag; this != NULL; this = next) { + next = this->next; + if (this == suntil || (next == suntil && + (this->tag == TAG_P || this->tag == TAG_PRE))) + break; + print_ctag(h, this); + } +} + + +/*********************************************************************** + * Low level output functions. + * They implement line breaking using a short static buffer. + ***********************************************************************/ + +/* + * Buffer one HTML output byte. + * If the buffer is full, flush and deactivate it and start a new line. + * If the buffer is inactive, print directly. + */ +static void +print_byte(struct html *h, char c) +{ + if ((h->flags & HTML_BUFFER) == 0) { + putchar(c); + h->col++; + return; + } + + if (h->col + h->bufcol < sizeof(h->buf)) { + h->buf[h->bufcol++] = c; + return; + } + + putchar('\n'); + h->col = 0; + print_indent(h); + putchar(' '); + putchar(' '); + fwrite(h->buf, h->bufcol, 1, stdout); + putchar(c); + h->col = (h->indent + 1) * 2 + h->bufcol + 1; + h->bufcol = 0; + h->flags &= ~HTML_BUFFER; +} + +/* + * If something was printed on the current output line, end it. + * Not to be called right after print_indent(). + */ +void +print_endline(struct html *h) +{ + if (h->col == 0) + return; + + if (h->bufcol) { + putchar(' '); + fwrite(h->buf, h->bufcol, 1, stdout); + h->bufcol = 0; + } + putchar('\n'); + h->col = 0; + h->flags |= HTML_NOSPACE; + h->flags &= ~HTML_BUFFER; +} + +/* + * Flush the HTML output buffer. + * If it is inactive, activate it. + */ +static void +print_endword(struct html *h) +{ + if (h->noindent) { + print_byte(h, ' '); + return; + } + + if ((h->flags & HTML_BUFFER) == 0) { + h->col++; + h->flags |= HTML_BUFFER; + } else if (h->bufcol) { + putchar(' '); + fwrite(h->buf, h->bufcol, 1, stdout); + h->col += h->bufcol + 1; + } + h->bufcol = 0; +} + +/* + * If at the beginning of a new output line, + * perform indentation and mark the line as containing output. + * Make sure to really produce some output right afterwards, + * but do not use print_otag() for producing it. + */ +static void +print_indent(struct html *h) +{ + size_t i; + + if (h->col || h->noindent) + return; + + h->col = h->indent * 2; + for (i = 0; i < h->col; i++) + putchar(' '); +} + +/* + * Print or buffer some characters + * depending on the current HTML output buffer state. + */ +static void +print_word(struct html *h, const char *cp) +{ + while (*cp != '\0') + print_byte(h, *cp++); +} diff --git a/usr.bin/mandoc/html.h b/usr.bin/mandoc/html.h new file mode 100644 index 0000000..dcce339 --- /dev/null +++ b/usr.bin/mandoc/html.h @@ -0,0 +1,141 @@ +/* $OpenBSD: html.h,v 1.70 2020/04/18 20:28:46 schwarze Exp $ */ +/* + * Copyright (c) 2017, 2018, 2019, 2020 Ingo Schwarze + * Copyright (c) 2008-2011, 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 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. + * + * Internal interfaces for mandoc(1) HTML formatters. + * For use by the individual HTML formatters only. + */ + +enum htmltag { + TAG_HTML, + TAG_HEAD, + TAG_META, + TAG_LINK, + TAG_STYLE, + TAG_TITLE, + TAG_BODY, + TAG_DIV, + TAG_SECTION, + TAG_TABLE, + TAG_TR, + TAG_TD, + TAG_LI, + TAG_UL, + TAG_OL, + TAG_DL, + TAG_DT, + TAG_DD, + TAG_H1, + TAG_H2, + TAG_P, + TAG_PRE, + TAG_A, + TAG_B, + TAG_CITE, + TAG_CODE, + TAG_I, + TAG_SMALL, + TAG_SPAN, + TAG_VAR, + TAG_BR, + TAG_MARK, + TAG_MATH, + TAG_MROW, + TAG_MI, + TAG_MN, + TAG_MO, + TAG_MSUP, + TAG_MSUB, + TAG_MSUBSUP, + TAG_MFRAC, + TAG_MSQRT, + TAG_MFENCED, + TAG_MTABLE, + TAG_MTR, + TAG_MTD, + TAG_MUNDEROVER, + TAG_MUNDER, + TAG_MOVER, + TAG_MAX +}; + +struct tag { + struct tag *next; + int refcnt; + int closed; + enum htmltag tag; +}; + +struct html { + int flags; +#define HTML_NOSPACE (1 << 0) /* suppress next space */ +#define HTML_IGNDELIM (1 << 1) +#define HTML_KEEP (1 << 2) +#define HTML_PREKEEP (1 << 3) +#define HTML_NONOSPACE (1 << 4) /* never add spaces */ +#define HTML_SKIPCHAR (1 << 6) /* skip the next character */ +#define HTML_NOSPLIT (1 << 7) /* do not break line before .An */ +#define HTML_SPLIT (1 << 8) /* break line before .An */ +#define HTML_NONEWLINE (1 << 9) /* No line break in nofill mode. */ +#define HTML_BUFFER (1 << 10) /* Collect a word to see if it fits. */ +#define HTML_TOCDONE (1 << 11) /* The TOC was already written. */ + size_t indent; /* current output indentation level */ + int noindent; /* indent disabled by
 */
+	size_t		  col; /* current output byte position */
+	size_t		  bufcol; /* current buf byte position */
+	char		  buf[80]; /* output buffer */
+	struct tag	 *tag; /* last open tag */
+	struct rofftbl	  tbl; /* current table */
+	struct tag	 *tblt; /* current open table scope */
+	char		 *base_man1; /* bases for manpage href */
+	char		 *base_man2;
+	char		 *base_includes; /* base for include href */
+	char		 *style; /* style-sheet URI */
+	struct tag	 *metaf; /* current open font scope */
+	enum mandoc_esc	  metal; /* last used font */
+	enum mandoc_esc	  metac; /* current font mode */
+	int		  oflags; /* output options */
+#define	HTML_FRAGMENT	 (1 << 0) /* don't emit HTML/HEAD/BODY */
+#define	HTML_TOC	 (1 << 1) /* emit a table of contents */
+};
+
+
+struct	roff_node;
+struct	tbl_span;
+struct	eqn_box;
+
+void		  roff_html_pre(struct html *, const struct roff_node *);
+
+void		  print_gen_comment(struct html *, struct roff_node *);
+void		  print_gen_decls(struct html *);
+void		  print_gen_head(struct html *);
+struct tag	 *print_otag(struct html *, enum htmltag, const char *, ...);
+struct tag	 *print_otag_id(struct html *, enum htmltag, const char *,
+			struct roff_node *);
+void		  print_tagq(struct html *, const struct tag *);
+void		  print_stagq(struct html *, const struct tag *);
+void		  print_tagged_text(struct html *, const char *,
+			struct roff_node *);
+void		  print_text(struct html *, const char *);
+void		  print_tblclose(struct html *);
+void		  print_tbl(struct html *, const struct tbl_span *);
+void		  print_eqn(struct html *, const struct eqn_box *);
+void		  print_endline(struct html *);
+
+void		  html_close_paragraph(struct html *);
+enum roff_tok	  html_fillmode(struct html *, enum roff_tok);
+char		 *html_make_id(const struct roff_node *, int);
+int		  html_setfont(struct html *, enum mandoc_esc);
diff --git a/usr.bin/mandoc/libman.h b/usr.bin/mandoc/libman.h
new file mode 100644
index 0000000..daffd76
--- /dev/null
+++ b/usr.bin/mandoc/libman.h
@@ -0,0 +1,42 @@
+/*	$OpenBSD: libman.h,v 1.61 2018/12/31 10:03:38 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons 
+ * Copyright (c) 2014, 2015, 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 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	roff_node;
+struct	roff_man;
+
+#define	MACRO_PROT_ARGS	  struct roff_man *man, \
+			  enum roff_tok tok, \
+			  int line, \
+			  int ppos, \
+			  int *pos, \
+			  char *buf
+
+struct	man_macro {
+	void		(*fp)(MACRO_PROT_ARGS);
+	int		  flags;
+#define	MAN_BSCOPED	 (1 << 0)  /* Optional next-line block scope. */
+#define	MAN_ESCOPED	 (1 << 1)  /* Optional next-line element scope. */
+#define	MAN_NSCOPED	 (1 << 2)  /* Allowed in next-line element scope. */
+#define	MAN_XSCOPE	 (1 << 3)  /* Exit next-line block scope. */
+#define	MAN_JOIN	 (1 << 4)  /* Join arguments together. */
+};
+
+const struct man_macro *man_macro(enum roff_tok);
+
+void		  man_descope(struct roff_man *, int, int, char *);
+void		  man_unscope(struct roff_man *, const struct roff_node *);
diff --git a/usr.bin/mandoc/libmandoc.h b/usr.bin/mandoc/libmandoc.h
new file mode 100644
index 0000000..b291631
--- /dev/null
+++ b/usr.bin/mandoc/libmandoc.h
@@ -0,0 +1,85 @@
+/* $OpenBSD: libmandoc.h,v 1.64 2020/04/03 11:34:19 schwarze Exp $ */
+/*
+ * Copyright (c) 2013-2015,2017,2018,2020 Ingo Schwarze 
+ * Copyright (c) 2009, 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 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.
+ *
+ * Internal interfaces for parser utilities needed by multiple parsers
+ * and the top-level functions to call the mdoc, man, and roff parsers.
+ */
+
+/*
+ * Return codes passed from the roff parser to the main parser.
+ */
+
+/* Main instruction: what to do with the returned line. */
+#define	ROFF_IGN	0x000	/* Don't do anything with it. */
+#define	ROFF_CONT	0x001	/* Give it to the high-level parser. */
+#define	ROFF_RERUN	0x002	/* Re-run the roff parser with an offset. */
+#define	ROFF_REPARSE	0x004	/* Recursively run the main parser on it. */
+#define	ROFF_SO		0x008	/* Include the named file. */
+#define	ROFF_MASK	0x00f	/* Only one of these bits should be set. */
+
+/* Options for further parsing, to be OR'ed with the above. */
+#define	ROFF_APPEND	0x010	/* Append the next line to this one. */
+#define	ROFF_USERCALL	0x020	/* Start execution of a new macro. */
+#define	ROFF_USERRET	0x040	/* Abort execution of the current macro. */
+#define	ROFF_WHILE	0x100	/* Start a new .while loop. */
+#define	ROFF_LOOPCONT	0x200	/* Iterate the current .while loop. */
+#define	ROFF_LOOPEXIT	0x400	/* Exit the current .while loop. */
+#define	ROFF_LOOPMASK	0xf00
+
+
+struct	buf {
+	char		*buf;
+	size_t		 sz;
+	struct buf	*next;
+};
+
+
+struct	roff;
+struct	roff_man;
+struct	roff_node;
+
+char		*mandoc_normdate(struct roff_node *, struct roff_node *);
+int		 mandoc_eos(const char *, size_t);
+int		 mandoc_strntoi(const char *, size_t, int);
+const char	*mandoc_a2msec(const char*);
+
+int		 mdoc_parseln(struct roff_man *, int, char *, int);
+void		 mdoc_endparse(struct roff_man *);
+
+int		 man_parseln(struct roff_man *, int, char *, int);
+void		 man_endparse(struct roff_man *);
+
+int		 preconv_cue(const struct buf *, size_t);
+int		 preconv_encode(const struct buf *, size_t *,
+			struct buf *, size_t *, int *);
+
+void		 roff_free(struct roff *);
+struct roff	*roff_alloc(int);
+void		 roff_reset(struct roff *);
+void		 roff_man_free(struct roff_man *);
+struct roff_man	*roff_man_alloc(struct roff *, const char *, int);
+void		 roff_man_reset(struct roff_man *);
+int		 roff_parseln(struct roff *, int, struct buf *, int *);
+void		 roff_userret(struct roff *);
+void		 roff_endparse(struct roff *);
+void		 roff_setreg(struct roff *, const char *, int, char);
+int		 roff_getreg(struct roff *, const char *);
+char		*roff_strdup(const struct roff *, const char *);
+char		*roff_getarg(struct roff *, char **, int, int *);
+int		 roff_getcontrol(const struct roff *,
+			const char *, int *);
+int		 roff_getformat(const struct roff *);
diff --git a/usr.bin/mandoc/libmdoc.h b/usr.bin/mandoc/libmdoc.h
new file mode 100644
index 0000000..4cdea31
--- /dev/null
+++ b/usr.bin/mandoc/libmdoc.h
@@ -0,0 +1,86 @@
+/*	$OpenBSD: libmdoc.h,v 1.88 2018/12/31 04:55:42 schwarze Exp $ */
+/*
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons 
+ * 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 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	roff_node;
+struct	roff_man;
+struct	mdoc_arg;
+
+#define	MACRO_PROT_ARGS	struct roff_man *mdoc, \
+			enum roff_tok tok, \
+			int line, \
+			int ppos, \
+			int *pos, \
+			char *buf
+
+struct	mdoc_macro {
+	void		(*fp)(MACRO_PROT_ARGS);
+	int		  flags;
+#define	MDOC_CALLABLE	 (1 << 0)
+#define	MDOC_PARSED	 (1 << 1)
+#define	MDOC_EXPLICIT	 (1 << 2)
+#define	MDOC_PROLOGUE	 (1 << 3)
+#define	MDOC_IGNDELIM	 (1 << 4)
+#define	MDOC_JOIN	 (1 << 5)
+};
+
+enum	margserr {
+	ARGS_ERROR,
+	ARGS_EOLN, /* end-of-line */
+	ARGS_WORD, /* normal word */
+	ARGS_ALLOC, /* normal word from roff_getarg() */
+	ARGS_PUNCT, /* series of punctuation */
+	ARGS_PHRASE /* Bl -column phrase */
+};
+
+/*
+ * A punctuation delimiter is opening, closing, or "middle mark"
+ * punctuation.  These govern spacing.
+ * Opening punctuation (e.g., the opening parenthesis) suppresses the
+ * following space; closing punctuation (e.g., the closing parenthesis)
+ * suppresses the leading space; middle punctuation (e.g., the vertical
+ * bar) can do either.  The middle punctuation delimiter bends the rules
+ * depending on usage.
+ */
+enum	mdelim {
+	DELIM_NONE = 0,
+	DELIM_OPEN,
+	DELIM_MIDDLE,
+	DELIM_CLOSE,
+	DELIM_MAX
+};
+
+const struct mdoc_macro *mdoc_macro(enum roff_tok);
+
+void		  mdoc_elem_alloc(struct roff_man *, int, int,
+			enum roff_tok, struct mdoc_arg *);
+struct roff_node *mdoc_block_alloc(struct roff_man *, int, int,
+			enum roff_tok, struct mdoc_arg *);
+void		  mdoc_tail_alloc(struct roff_man *, int, int,
+			enum roff_tok);
+struct roff_node *mdoc_endbody_alloc(struct roff_man *, int, int,
+			enum roff_tok, struct roff_node *);
+void		  mdoc_state(struct roff_man *, struct roff_node *);
+const char	 *mdoc_a2arch(const char *);
+const char	 *mdoc_a2att(const char *);
+enum roff_sec	  mdoc_a2sec(const char *);
+const char	 *mdoc_a2st(const char *);
+void		  mdoc_argv(struct roff_man *, int, enum roff_tok,
+			struct mdoc_arg **, int *, char *);
+enum margserr	  mdoc_args(struct roff_man *, int,
+			int *, char *, enum roff_tok, char **);
+enum mdelim	  mdoc_isdelim(const char *);
diff --git a/usr.bin/mandoc/main.c b/usr.bin/mandoc/main.c
new file mode 100644
index 0000000..6f2174a
--- /dev/null
+++ b/usr.bin/mandoc/main.c
@@ -0,0 +1,1255 @@
+/* $OpenBSD: main.c,v 1.251 2020/04/02 22:10:27 schwarze Exp $ */
+/*
+ * Copyright (c) 2010-2012, 2014-2020 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.
+ *
+ * Main program for mandoc(1), man(1), apropos(1), whatis(1), and help(1).
+ */
+#include 
+#include 
+#include 	/* MACHINE */
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#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 "mdoc.h"
+#include "man.h"
+#include "mandoc_parse.h"
+#include "tag.h"
+#include "term_tag.h"
+#include "main.h"
+#include "manconf.h"
+#include "mansearch.h"
+
+#define BINM_APROPOS	"apropos"
+#define BINM_MAN	"man"
+#define BINM_MAKEWHATIS	"makewhatis"
+#define BINM_WHATIS	"whatis"
+#define OSENUM		MANDOC_OS_OPENBSD
+
+enum	outmode {
+	OUTMODE_DEF = 0,
+	OUTMODE_FLN,
+	OUTMODE_LST,
+	OUTMODE_ALL,
+	OUTMODE_ONE
+};
+
+enum	outt {
+	OUTT_ASCII = 0,	/* -Tascii */
+	OUTT_LOCALE,	/* -Tlocale */
+	OUTT_UTF8,	/* -Tutf8 */
+	OUTT_TREE,	/* -Ttree */
+	OUTT_MAN,	/* -Tman */
+	OUTT_HTML,	/* -Thtml */
+	OUTT_MARKDOWN,	/* -Tmarkdown */
+	OUTT_LINT,	/* -Tlint */
+	OUTT_PS,	/* -Tps */
+	OUTT_PDF	/* -Tpdf */
+};
+
+struct	outstate {
+	struct tag_files *tag_files;	/* Tagging state variables. */
+	void		 *outdata;	/* data for output */
+	int		  use_pager;
+	int		  wstop;	/* stop after a file with a warning */
+	int		  had_output;	/* Some output was generated. */
+	enum outt	  outtype;	/* which output to use */
+};
+
+
+int			  mandocdb(int, char *[]);
+
+static	void		  check_xr(void);
+static	int		  fs_lookup(const struct manpaths *,
+				size_t ipath, const char *,
+				const char *, const char *,
+				struct manpage **, size_t *);
+static	int		  fs_search(const struct mansearch *,
+				const struct manpaths *, const char *,
+				struct manpage **, size_t *);
+static	void		  glob_esc(char **, const char *, const char *);
+static	void		  outdata_alloc(struct outstate *, struct manoutput *);
+static	void		  parse(struct mparse *, int, const char *,
+				struct outstate *, struct manoutput *);
+static	void		  passthrough(int, int);
+static	void		  process_onefile(struct mparse *, struct manpage *,
+				int, struct outstate *, struct manconf *);
+static	void		  run_pager(struct tag_files *, char *);
+static	pid_t		  spawn_pager(struct tag_files *, char *);
+static	void		  usage(enum argmode) __attribute__((__noreturn__));
+static	int		  woptions(char *, enum mandoc_os *, int *);
+
+static	const int sec_prios[] = {1, 4, 5, 8, 6, 3, 7, 2, 9};
+static	char		  help_arg[] = "help";
+static	char		 *help_argv[] = {help_arg, NULL};
+
+
+int
+main(int argc, char *argv[])
+{
+	struct manconf	 conf;		/* Manpaths and output options. */
+	struct outstate	 outst;		/* Output state. */
+	struct winsize	 ws;		/* Result of ioctl(TIOCGWINSZ). */
+	struct mansearch search;	/* Search options. */
+	struct manpage	*res;		/* Complete list of search results. */
+	struct manpage	*resn;		/* Search results for one name. */
+	struct mparse	*mp;		/* Opaque parser object. */
+	const char	*conf_file;	/* -C: alternate config file. */
+	const char	*os_s;		/* -I: Operating system for display. */
+	const char	*progname, *sec;
+	char		*defpaths;	/* -M: override manpaths. */
+	char		*auxpaths;	/* -m: additional manpaths. */
+	char		*oarg;		/* -O: output option string. */
+	char		*tagarg;	/* -O tag: default value. */
+	unsigned char	*uc;
+	size_t		 ressz;		/* Number of elements in res[]. */
+	size_t		 resnsz;	/* Number of elements in resn[]. */
+	size_t		 i, ib, ssz;
+	int		 options;	/* Parser options. */
+	int		 show_usage;	/* Invalid argument: give up. */
+	int		 prio, best_prio;
+	int		 startdir;
+	int		 c;
+	enum mandoc_os	 os_e;		/* Check base system conventions. */
+	enum outmode	 outmode;	/* According to command line. */
+
+	progname = getprogname();
+	mandoc_msg_setoutfile(stderr);
+	if (strncmp(progname, "mandocdb", 8) == 0 ||
+	    strcmp(progname, BINM_MAKEWHATIS) == 0)
+		return mandocdb(argc, argv);
+
+	if (pledge("stdio rpath tmppath tty proc exec", NULL) == -1) {
+		mandoc_msg(MANDOCERR_PLEDGE, 0, 0, "%s", strerror(errno));
+		return mandoc_msg_getrc();
+	}
+
+	/* Search options. */
+
+	memset(&conf, 0, sizeof(conf));
+	conf_file = NULL;
+	defpaths = auxpaths = NULL;
+
+	memset(&search, 0, sizeof(struct mansearch));
+	search.outkey = "Nd";
+	oarg = NULL;
+
+	if (strcmp(progname, BINM_MAN) == 0)
+		search.argmode = ARG_NAME;
+	else if (strcmp(progname, BINM_APROPOS) == 0)
+		search.argmode = ARG_EXPR;
+	else if (strcmp(progname, BINM_WHATIS) == 0)
+		search.argmode = ARG_WORD;
+	else if (strncmp(progname, "help", 4) == 0)
+		search.argmode = ARG_NAME;
+	else
+		search.argmode = ARG_FILE;
+
+	/* Parser options. */
+
+	options = MPARSE_SO | MPARSE_UTF8 | MPARSE_LATIN1;
+	os_e = MANDOC_OS_OTHER;
+	os_s = NULL;
+
+	/* Formatter options. */
+
+	memset(&outst, 0, sizeof(outst));
+	outst.tag_files = NULL;
+	outst.outtype = OUTT_LOCALE;
+	outst.use_pager = 1;
+
+	show_usage = 0;
+	outmode = OUTMODE_DEF;
+
+	while ((c = getopt(argc, argv,
+	    "aC:cfhI:iK:klM:m:O:S:s:T:VW:w")) != -1) {
+		if (c == 'i' && search.argmode == ARG_EXPR) {
+			optind--;
+			break;
+		}
+		switch (c) {
+		case 'a':
+			outmode = OUTMODE_ALL;
+			break;
+		case 'C':
+			conf_file = optarg;
+			break;
+		case 'c':
+			outst.use_pager = 0;
+			break;
+		case 'f':
+			search.argmode = ARG_WORD;
+			break;
+		case 'h':
+			conf.output.synopsisonly = 1;
+			outst.use_pager = 0;
+			outmode = OUTMODE_ALL;
+			break;
+		case 'I':
+			if (strncmp(optarg, "os=", 3) != 0) {
+				mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0,
+				    "-I %s", optarg);
+				return mandoc_msg_getrc();
+			}
+			if (os_s != NULL) {
+				mandoc_msg(MANDOCERR_BADARG_DUPE, 0, 0,
+				    "-I %s", optarg);
+				return mandoc_msg_getrc();
+			}
+			os_s = optarg + 3;
+			break;
+		case 'K':
+			options &= ~(MPARSE_UTF8 | MPARSE_LATIN1);
+			if (strcmp(optarg, "utf-8") == 0)
+				options |=  MPARSE_UTF8;
+			else if (strcmp(optarg, "iso-8859-1") == 0)
+				options |=  MPARSE_LATIN1;
+			else if (strcmp(optarg, "us-ascii") != 0) {
+				mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0,
+				    "-K %s", optarg);
+				return mandoc_msg_getrc();
+			}
+			break;
+		case 'k':
+			search.argmode = ARG_EXPR;
+			break;
+		case 'l':
+			search.argmode = ARG_FILE;
+			outmode = OUTMODE_ALL;
+			break;
+		case 'M':
+			defpaths = optarg;
+			break;
+		case 'm':
+			auxpaths = optarg;
+			break;
+		case 'O':
+			oarg = optarg;
+			break;
+		case 'S':
+			search.arch = optarg;
+			break;
+		case 's':
+			search.sec = optarg;
+			break;
+		case 'T':
+			if (strcmp(optarg, "ascii") == 0)
+				outst.outtype = OUTT_ASCII;
+			else if (strcmp(optarg, "lint") == 0) {
+				outst.outtype = OUTT_LINT;
+				mandoc_msg_setoutfile(stdout);
+				mandoc_msg_setmin(MANDOCERR_BASE);
+			} else if (strcmp(optarg, "tree") == 0)
+				outst.outtype = OUTT_TREE;
+			else if (strcmp(optarg, "man") == 0)
+				outst.outtype = OUTT_MAN;
+			else if (strcmp(optarg, "html") == 0)
+				outst.outtype = OUTT_HTML;
+			else if (strcmp(optarg, "markdown") == 0)
+				outst.outtype = OUTT_MARKDOWN;
+			else if (strcmp(optarg, "utf8") == 0)
+				outst.outtype = OUTT_UTF8;
+			else if (strcmp(optarg, "locale") == 0)
+				outst.outtype = OUTT_LOCALE;
+			else if (strcmp(optarg, "ps") == 0)
+				outst.outtype = OUTT_PS;
+			else if (strcmp(optarg, "pdf") == 0)
+				outst.outtype = OUTT_PDF;
+			else {
+				mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0,
+				    "-T %s", optarg);
+				return mandoc_msg_getrc();
+			}
+			break;
+		case 'W':
+			if (woptions(optarg, &os_e, &outst.wstop) == -1)
+				return mandoc_msg_getrc();
+			break;
+		case 'w':
+			outmode = OUTMODE_FLN;
+			break;
+		default:
+			show_usage = 1;
+			break;
+		}
+	}
+
+	if (show_usage)
+		usage(search.argmode);
+
+	/* Postprocess options. */
+
+	switch (outmode) {
+	case OUTMODE_DEF:
+		switch (search.argmode) {
+		case ARG_FILE:
+			outmode = OUTMODE_ALL;
+			outst.use_pager = 0;
+			break;
+		case ARG_NAME:
+			outmode = OUTMODE_ONE;
+			break;
+		default:
+			outmode = OUTMODE_LST;
+			break;
+		}
+		break;
+	case OUTMODE_FLN:
+		if (search.argmode == ARG_FILE)
+			outmode = OUTMODE_ALL;
+		break;
+	case OUTMODE_ALL:
+		break;
+	case OUTMODE_LST:
+	case OUTMODE_ONE:
+		abort();
+	}
+
+	if (oarg != NULL) {
+		if (outmode == OUTMODE_LST)
+			search.outkey = oarg;
+		else {
+			while (oarg != NULL) {
+				if (manconf_output(&conf.output,
+				    strsep(&oarg, ","), 0) == -1)
+					return mandoc_msg_getrc();
+			}
+		}
+	}
+
+	if (outst.outtype != OUTT_TREE || conf.output.noval == 0)
+		options |= MPARSE_VALIDATE;
+
+	if (outmode == OUTMODE_FLN ||
+	    outmode == OUTMODE_LST ||
+	    !isatty(STDOUT_FILENO))
+		outst.use_pager = 0;
+
+	if (outst.use_pager &&
+	    (conf.output.width == 0 || conf.output.indent == 0) &&
+	    ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1 &&
+	    ws.ws_col > 1) {
+		if (conf.output.width == 0 && ws.ws_col < 79)
+			conf.output.width = ws.ws_col - 1;
+		if (conf.output.indent == 0 && ws.ws_col < 66)
+			conf.output.indent = 3;
+	}
+
+	if (outst.use_pager == 0) {
+		if (pledge("stdio rpath", NULL) == -1) {
+			mandoc_msg(MANDOCERR_PLEDGE, 0, 0,
+			    "%s", strerror(errno));
+			return mandoc_msg_getrc();
+		}
+	}
+
+	/* Parse arguments. */
+
+	if (argc > 0) {
+		argc -= optind;
+		argv += optind;
+	}
+
+	/*
+	 * Quirks for help(1) and man(1),
+	 * in particular for a section argument without -s.
+	 */
+
+	if (search.argmode == ARG_NAME) {
+		if (*progname == 'h') {
+			if (argc == 0) {
+				argv = help_argv;
+				argc = 1;
+			}
+		} else if (argc > 1 &&
+		    ((uc = (unsigned char *)argv[0]) != NULL) &&
+		    ((isdigit(uc[0]) && (uc[1] == '\0' ||
+		      isalpha(uc[1]))) ||
+		     (uc[0] == 'n' && uc[1] == '\0'))) {
+			search.sec = (char *)uc;
+			argv++;
+			argc--;
+		}
+		if (search.arch == NULL)
+			search.arch = getenv("MACHINE");
+#ifdef MACHINE
+		if (search.arch == NULL)
+			search.arch = MACHINE;
+#endif
+		if (outmode == OUTMODE_ONE)
+			search.firstmatch = 1;
+	}
+
+	/*
+	 * Use the first argument for -O tag in addition to
+	 * using it as a search term for man(1) or apropos(1).
+	 */
+
+	if (conf.output.tag != NULL && *conf.output.tag == '\0') {
+		tagarg = argc > 0 && search.argmode == ARG_EXPR ?
+		    strchr(*argv, '=') : NULL;
+		conf.output.tag = tagarg == NULL ? *argv : tagarg + 1;
+	}
+
+	/* Read the configuration file. */
+
+	if (search.argmode != ARG_FILE)
+		manconf_parse(&conf, conf_file, defpaths, auxpaths);
+
+	/* man(1): Resolve each name individually. */
+
+	if (search.argmode == ARG_NAME) {
+		if (argc < 1) {
+			if (outmode != OUTMODE_FLN)
+				usage(ARG_NAME);
+			if (conf.manpath.sz == 0) {
+				warnx("The manpath is empty.");
+				mandoc_msg_setrc(MANDOCLEVEL_BADARG);
+			} else {
+				for (i = 0; i + 1 < conf.manpath.sz; i++)
+					printf("%s:", conf.manpath.paths[i]);
+				printf("%s\n", conf.manpath.paths[i]);
+			}
+			manconf_free(&conf);
+			return (int)mandoc_msg_getrc();
+		}
+		for (res = NULL, ressz = 0; argc > 0; argc--, argv++) {
+			(void)mansearch(&search, &conf.manpath,
+			    1, argv, &resn, &resnsz);
+			if (resnsz == 0)
+				(void)fs_search(&search, &conf.manpath,
+				    *argv, &resn, &resnsz);
+			if (resnsz == 0 && strchr(*argv, '/') == NULL) {
+				if (search.arch != NULL &&
+				    arch_valid(search.arch, OSENUM) == 0)
+					warnx("Unknown architecture \"%s\".",
+					    search.arch);
+				else if (search.sec != NULL)
+					warnx("No entry for %s in "
+					    "section %s of the manual.",
+					    *argv, search.sec);
+				else
+					warnx("No entry for %s in "
+					    "the manual.", *argv);
+				mandoc_msg_setrc(MANDOCLEVEL_BADARG);
+				continue;
+			}
+			if (resnsz == 0) {
+				if (access(*argv, R_OK) == -1) {
+					mandoc_msg_setinfilename(*argv);
+					mandoc_msg(MANDOCERR_BADARG_BAD,
+					    0, 0, "%s", strerror(errno));
+					mandoc_msg_setinfilename(NULL);
+					continue;
+				}
+				resnsz = 1;
+				resn = mandoc_calloc(resnsz, sizeof(*res));
+				resn->file = mandoc_strdup(*argv);
+				resn->ipath = SIZE_MAX;
+				resn->form = FORM_SRC;
+			}
+			if (outmode != OUTMODE_ONE || resnsz == 1) {
+				res = mandoc_reallocarray(res,
+				    ressz + resnsz, sizeof(*res));
+				memcpy(res + ressz, resn,
+				    sizeof(*resn) * resnsz);
+				ressz += resnsz;
+				continue;
+			}
+
+			/* Search for the best section. */
+
+			best_prio = 40;
+			for (ib = i = 0; i < resnsz; i++) {
+				sec = resn[i].file;
+				sec += strcspn(sec, "123456789");
+				if (sec[0] == '\0')
+					continue; /* No section at all. */
+				prio = sec_prios[sec[0] - '1'];
+				if (search.sec != NULL) {
+					ssz = strlen(search.sec);
+					if (strncmp(sec, search.sec, ssz) == 0)
+						sec += ssz;
+				} else
+					sec++; /* Prefer without suffix. */
+				if (*sec != '/')
+					prio += 10; /* Wrong dir name. */
+				if (search.sec != NULL &&
+				    (strlen(sec) <= ssz  + 3 ||
+				     strcmp(sec + strlen(sec) - ssz,
+				      search.sec) != 0))
+					prio += 20; /* Wrong file ext. */
+				if (prio >= best_prio)
+					continue;
+				best_prio = prio;
+				ib = i;
+			}
+			res = mandoc_reallocarray(res, ressz + 1,
+			    sizeof(*res));
+			memcpy(res + ressz++, resn + ib, sizeof(*resn));
+		}
+
+	/* apropos(1), whatis(1): Process the full search expression. */
+
+	} else if (search.argmode != ARG_FILE) {
+		if (mansearch(&search, &conf.manpath,
+		    argc, argv, &res, &ressz) == 0)
+			usage(search.argmode);
+
+		if (ressz == 0) {
+			warnx("nothing appropriate");
+			mandoc_msg_setrc(MANDOCLEVEL_BADARG);
+			goto out;
+		}
+
+	/* mandoc(1): Take command line arguments as file names. */
+
+	} else {
+		ressz = argc > 0 ? argc : 1;
+		res = mandoc_calloc(ressz, sizeof(*res));
+		for (i = 0; i < ressz; i++) {
+			if (argc > 0)
+				res[i].file = mandoc_strdup(argv[i]);
+			res[i].ipath = SIZE_MAX;
+			res[i].form = FORM_SRC;
+		}
+	}
+
+	switch (outmode) {
+	case OUTMODE_FLN:
+		for (i = 0; i < ressz; i++)
+			puts(res[i].file);
+		goto out;
+	case OUTMODE_LST:
+		for (i = 0; i < ressz; i++)
+			printf("%s - %s\n", res[i].names,
+			    res[i].output == NULL ? "" :
+			    res[i].output);
+		goto out;
+	default:
+		break;
+	}
+
+	if (search.argmode == ARG_FILE && auxpaths != NULL) {
+		if (strcmp(auxpaths, "doc") == 0)
+			options |= MPARSE_MDOC;
+		else if (strcmp(auxpaths, "an") == 0)
+			options |= MPARSE_MAN;
+	}
+
+	mchars_alloc();
+	mp = mparse_alloc(options, os_e, os_s);
+
+	/*
+	 * Remember the original working directory, if possible.
+	 * This will be needed if some names on the command line
+	 * are page names and some are relative file names.
+	 * Do not error out if the current directory is not
+	 * readable: Maybe it won't be needed after all.
+	 */
+	startdir = open(".", O_RDONLY | O_DIRECTORY);
+	for (i = 0; i < ressz; i++) {
+		process_onefile(mp, res + i, startdir, &outst, &conf);
+		if (outst.wstop && mandoc_msg_getrc() != MANDOCLEVEL_OK)
+			break;
+	}
+	if (startdir != -1) {
+		(void)fchdir(startdir);
+		close(startdir);
+	}
+	if (conf.output.tag != NULL && conf.output.tag_found == 0) {
+		mandoc_msg(MANDOCERR_TAG, 0, 0, "%s", conf.output.tag);
+		conf.output.tag = NULL;
+	}
+	if (outst.outdata != NULL) {
+		switch (outst.outtype) {
+		case OUTT_HTML:
+			html_free(outst.outdata);
+			break;
+		case OUTT_UTF8:
+		case OUTT_LOCALE:
+		case OUTT_ASCII:
+			ascii_free(outst.outdata);
+			break;
+		case OUTT_PDF:
+		case OUTT_PS:
+			pspdf_free(outst.outdata);
+			break;
+		default:
+			break;
+		}
+	}
+	mandoc_xr_free();
+	mparse_free(mp);
+	mchars_free();
+
+out:
+	mansearch_free(res, ressz);
+	if (search.argmode != ARG_FILE)
+		manconf_free(&conf);
+
+	if (outst.tag_files != NULL) {
+		if (term_tag_close() != -1)
+			run_pager(outst.tag_files, conf.output.tag);
+		term_tag_unlink();
+	} else if (outst.had_output && outst.outtype != OUTT_LINT)
+		mandoc_msg_summary();
+
+	return (int)mandoc_msg_getrc();
+}
+
+static void
+usage(enum argmode argmode)
+{
+	switch (argmode) {
+	case ARG_FILE:
+		fputs("usage: mandoc [-ac] [-I os=name] "
+		    "[-K encoding] [-mdoc | -man] [-O options]\n"
+		    "\t      [-T output] [-W level] [file ...]\n", stderr);
+		break;
+	case ARG_NAME:
+		fputs("usage: man [-acfhklw] [-C file] [-M path] "
+		    "[-m path] [-S subsection]\n"
+		    "\t   [[-s] section] name ...\n", stderr);
+		break;
+	case ARG_WORD:
+		fputs("usage: whatis [-afk] [-C file] "
+		    "[-M path] [-m path] [-O outkey] [-S arch]\n"
+		    "\t      [-s section] name ...\n", stderr);
+		break;
+	case ARG_EXPR:
+		fputs("usage: apropos [-afk] [-C file] "
+		    "[-M path] [-m path] [-O outkey] [-S arch]\n"
+		    "\t       [-s section] expression ...\n", stderr);
+		break;
+	}
+	exit((int)MANDOCLEVEL_BADARG);
+}
+
+static void
+glob_esc(char **dst, const char *src, const char *suffix)
+{
+	while (*src != '\0') {
+		if (strchr("*?[", *src) != NULL)
+			*(*dst)++ = '\\';
+		*(*dst)++ = *src++;
+	}
+	while (*suffix != '\0')
+		*(*dst)++ = *suffix++;
+}
+
+static int
+fs_lookup(const struct manpaths *paths, size_t ipath,
+	const char *sec, const char *arch, const char *name,
+	struct manpage **res, size_t *ressz)
+{
+	struct stat	 sb;
+	glob_t		 globinfo;
+	struct manpage	*page;
+	char		*file, *cp;
+	int		 globres;
+	enum form	 form;
+
+	const char *const slman = "/man";
+	const char *const slash = "/";
+	const char *const sglob = ".[01-9]*";
+
+	form = FORM_SRC;
+	mandoc_asprintf(&file, "%s/man%s/%s.%s",
+	    paths->paths[ipath], sec, name, sec);
+	if (stat(file, &sb) != -1)
+		goto found;
+	free(file);
+
+	mandoc_asprintf(&file, "%s/cat%s/%s.0",
+	    paths->paths[ipath], sec, name);
+	if (stat(file, &sb) != -1) {
+		form = FORM_CAT;
+		goto found;
+	}
+	free(file);
+
+	if (arch != NULL) {
+		mandoc_asprintf(&file, "%s/man%s/%s/%s.%s",
+		    paths->paths[ipath], sec, arch, name, sec);
+		if (stat(file, &sb) != -1)
+			goto found;
+		free(file);
+	}
+
+	cp = file = mandoc_malloc(strlen(paths->paths[ipath]) * 2 +
+	    strlen(slman) + strlen(sec) * 2 + strlen(slash) +
+	    strlen(name) * 2 + strlen(sglob) + 1);
+	glob_esc(&cp, paths->paths[ipath], slman);
+	glob_esc(&cp, sec, slash);
+	glob_esc(&cp, name, sglob);
+	*cp = '\0';
+	globres = glob(file, 0, NULL, &globinfo);
+	if (globres != 0 && globres != GLOB_NOMATCH)
+		mandoc_msg(MANDOCERR_GLOB, 0, 0,
+		    "%s: %s", file, strerror(errno));
+	free(file);
+	if (globres == 0)
+		file = mandoc_strdup(*globinfo.gl_pathv);
+	globfree(&globinfo);
+	if (globres == 0) {
+		if (stat(file, &sb) != -1)
+			goto found;
+		free(file);
+	}
+	if (res != NULL || ipath + 1 != paths->sz)
+		return -1;
+
+	mandoc_asprintf(&file, "%s.%s", name, sec);
+	globres = stat(file, &sb);
+	free(file);
+	return globres;
+
+found:
+	warnx("outdated mandoc.db lacks %s(%s) entry, run %s %s",
+	    name, sec, BINM_MAKEWHATIS, paths->paths[ipath]);
+	if (res == NULL) {
+		free(file);
+		return 0;
+	}
+	*res = mandoc_reallocarray(*res, ++*ressz, sizeof(**res));
+	page = *res + (*ressz - 1);
+	page->file = file;
+	page->names = NULL;
+	page->output = NULL;
+	page->bits = NAME_FILE & NAME_MASK;
+	page->ipath = ipath;
+	page->sec = (*sec >= '1' && *sec <= '9') ? *sec - '1' + 1 : 10;
+	page->form = form;
+	return 0;
+}
+
+static int
+fs_search(const struct mansearch *cfg, const struct manpaths *paths,
+	const char *name, struct manpage **res, size_t *ressz)
+{
+	const char *const sections[] =
+	    {"1", "8", "6", "2", "3", "5", "7", "4", "9", "3p"};
+	const size_t nsec = sizeof(sections)/sizeof(sections[0]);
+
+	size_t		 ipath, isec;
+
+	assert(cfg->argmode == ARG_NAME);
+	if (res != NULL)
+		*res = NULL;
+	*ressz = 0;
+	for (ipath = 0; ipath < paths->sz; ipath++) {
+		if (cfg->sec != NULL) {
+			if (fs_lookup(paths, ipath, cfg->sec, cfg->arch,
+			    name, res, ressz) != -1 && cfg->firstmatch)
+				return 0;
+		} else {
+			for (isec = 0; isec < nsec; isec++)
+				if (fs_lookup(paths, ipath, sections[isec],
+				    cfg->arch, name, res, ressz) != -1 &&
+				    cfg->firstmatch)
+					return 0;
+		}
+	}
+	return -1;
+}
+
+static void
+process_onefile(struct mparse *mp, struct manpage *resp, int startdir,
+    struct outstate *outst, struct manconf *conf)
+{
+	int	 fd;
+
+	/*
+	 * Changing directories is not needed in ARG_FILE mode.
+	 * Do it on a best-effort basis.  Even in case of
+	 * failure, some functionality may still work.
+	 */
+	if (resp->ipath != SIZE_MAX)
+		(void)chdir(conf->manpath.paths[resp->ipath]);
+	else if (startdir != -1)
+		(void)fchdir(startdir);
+
+	mandoc_msg_setinfilename(resp->file);
+	if (resp->file != NULL) {
+		if ((fd = mparse_open(mp, resp->file)) == -1) {
+			mandoc_msg(resp->ipath == SIZE_MAX ?
+			    MANDOCERR_BADARG_BAD : MANDOCERR_OPEN,
+			    0, 0, "%s", strerror(errno));
+			mandoc_msg_setinfilename(NULL);
+			return;
+		}
+	} else
+		fd = STDIN_FILENO;
+
+	if (outst->use_pager) {
+		outst->use_pager = 0;
+		outst->tag_files = term_tag_init();
+	}
+	if (outst->had_output && outst->outtype <= OUTT_UTF8) {
+		if (outst->outdata == NULL)
+			outdata_alloc(outst, &conf->output);
+		terminal_sepline(outst->outdata);
+	}
+
+	if (resp->form == FORM_SRC)
+		parse(mp, fd, resp->file, outst, &conf->output);
+	else {
+		passthrough(fd, conf->output.synopsisonly);
+		outst->had_output = 1;
+	}
+
+	if (ferror(stdout)) {
+		if (outst->tag_files != NULL) {
+			mandoc_msg(MANDOCERR_WRITE, 0, 0, "%s: %s",
+			    outst->tag_files->ofn, strerror(errno));
+			term_tag_unlink();
+			outst->tag_files = NULL;
+		} else
+			mandoc_msg(MANDOCERR_WRITE, 0, 0, "%s",
+			    strerror(errno));
+	}
+	mandoc_msg_setinfilename(NULL);
+}
+
+static void
+parse(struct mparse *mp, int fd, const char *file,
+    struct outstate *outst, struct manoutput *outconf)
+{
+	static int		 previous;
+	struct roff_meta	*meta;
+
+	assert(fd >= 0);
+	if (file == NULL)
+		file = "";
+
+	if (previous)
+		mparse_reset(mp);
+	else
+		previous = 1;
+
+	mparse_readfd(mp, fd, file);
+	if (fd != STDIN_FILENO)
+		close(fd);
+
+	/*
+	 * With -Wstop and warnings or errors of at least the requested
+	 * level, do not produce output.
+	 */
+
+	if (outst->wstop && mandoc_msg_getrc() != MANDOCLEVEL_OK)
+		return;
+
+	if (outst->outdata == NULL)
+		outdata_alloc(outst, outconf);
+	else if (outst->outtype == OUTT_HTML)
+		html_reset(outst);
+
+	mandoc_xr_reset();
+	meta = mparse_result(mp);
+
+	/* Execute the out device, if it exists. */
+
+	outst->had_output = 1;
+	if (meta->macroset == MACROSET_MDOC) {
+		switch (outst->outtype) {
+		case OUTT_HTML:
+			html_mdoc(outst->outdata, meta);
+			break;
+		case OUTT_TREE:
+			tree_mdoc(outst->outdata, meta);
+			break;
+		case OUTT_MAN:
+			man_mdoc(outst->outdata, meta);
+			break;
+		case OUTT_PDF:
+		case OUTT_ASCII:
+		case OUTT_UTF8:
+		case OUTT_LOCALE:
+		case OUTT_PS:
+			terminal_mdoc(outst->outdata, meta);
+			break;
+		case OUTT_MARKDOWN:
+			markdown_mdoc(outst->outdata, meta);
+			break;
+		default:
+			break;
+		}
+	}
+	if (meta->macroset == MACROSET_MAN) {
+		switch (outst->outtype) {
+		case OUTT_HTML:
+			html_man(outst->outdata, meta);
+			break;
+		case OUTT_TREE:
+			tree_man(outst->outdata, meta);
+			break;
+		case OUTT_MAN:
+			mparse_copy(mp);
+			break;
+		case OUTT_PDF:
+		case OUTT_ASCII:
+		case OUTT_UTF8:
+		case OUTT_LOCALE:
+		case OUTT_PS:
+			terminal_man(outst->outdata, meta);
+			break;
+		default:
+			break;
+		}
+	}
+	if (outconf->tag != NULL && outconf->tag_found == 0 &&
+	    tag_exists(outconf->tag))
+		outconf->tag_found = 1;
+	if (mandoc_msg_getmin() < MANDOCERR_STYLE)
+		check_xr();
+}
+
+static void
+check_xr(void)
+{
+	static struct manpaths	 paths;
+	struct mansearch	 search;
+	struct mandoc_xr	*xr;
+	size_t			 sz;
+
+	if (paths.sz == 0)
+		manpath_base(&paths);
+
+	for (xr = mandoc_xr_get(); xr != NULL; xr = xr->next) {
+		if (xr->line == -1)
+			continue;
+		search.arch = NULL;
+		search.sec = xr->sec;
+		search.outkey = NULL;
+		search.argmode = ARG_NAME;
+		search.firstmatch = 1;
+		if (mansearch(&search, &paths, 1, &xr->name, NULL, &sz))
+			continue;
+		if (fs_search(&search, &paths, xr->name, NULL, &sz) != -1)
+			continue;
+		if (xr->count == 1)
+			mandoc_msg(MANDOCERR_XR_BAD, xr->line,
+			    xr->pos + 1, "Xr %s %s", xr->name, xr->sec);
+		else
+			mandoc_msg(MANDOCERR_XR_BAD, xr->line,
+			    xr->pos + 1, "Xr %s %s (%d times)",
+			    xr->name, xr->sec, xr->count);
+	}
+}
+
+static void
+outdata_alloc(struct outstate *outst, struct manoutput *outconf)
+{
+	switch (outst->outtype) {
+	case OUTT_HTML:
+		outst->outdata = html_alloc(outconf);
+		break;
+	case OUTT_UTF8:
+		outst->outdata = utf8_alloc(outconf);
+		break;
+	case OUTT_LOCALE:
+		outst->outdata = locale_alloc(outconf);
+		break;
+	case OUTT_ASCII:
+		outst->outdata = ascii_alloc(outconf);
+		break;
+	case OUTT_PDF:
+		outst->outdata = pdf_alloc(outconf);
+		break;
+	case OUTT_PS:
+		outst->outdata = ps_alloc(outconf);
+		break;
+	default:
+		break;
+	}
+}
+
+static void
+passthrough(int fd, int synopsis_only)
+{
+	const char	 synb[] = "S\bSY\bYN\bNO\bOP\bPS\bSI\bIS\bS";
+	const char	 synr[] = "SYNOPSIS";
+
+	FILE		*stream;
+	char		*line, *cp;
+	size_t		 linesz;
+	ssize_t		 len, written;
+	int		 lno, print;
+
+	stream = NULL;
+	line = NULL;
+	linesz = 0;
+
+	if (fflush(stdout) == EOF) {
+		mandoc_msg(MANDOCERR_FFLUSH, 0, 0, "%s", strerror(errno));
+		goto done;
+	}
+	if ((stream = fdopen(fd, "r")) == NULL) {
+		close(fd);
+		mandoc_msg(MANDOCERR_FDOPEN, 0, 0, "%s", strerror(errno));
+		goto done;
+	}
+
+	lno = print = 0;
+	while ((len = getline(&line, &linesz, stream)) != -1) {
+		lno++;
+		cp = line;
+		if (synopsis_only) {
+			if (print) {
+				if ( ! isspace((unsigned char)*cp))
+					goto done;
+				while (isspace((unsigned char)*cp)) {
+					cp++;
+					len--;
+				}
+			} else {
+				if (strcmp(cp, synb) == 0 ||
+				    strcmp(cp, synr) == 0)
+					print = 1;
+				continue;
+			}
+		}
+		for (; len > 0; len -= written) {
+			if ((written = write(STDOUT_FILENO, cp, len)) == -1) {
+				mandoc_msg(MANDOCERR_WRITE, 0, 0,
+				    "%s", strerror(errno));
+				goto done;
+			}
+		}
+	}
+	if (ferror(stream))
+		mandoc_msg(MANDOCERR_GETLINE, lno, 0, "%s", strerror(errno));
+
+done:
+	free(line);
+	if (stream != NULL)
+		fclose(stream);
+}
+
+static int
+woptions(char *arg, enum mandoc_os *os_e, int *wstop)
+{
+	char		*v, *o;
+	const char	*toks[11];
+
+	toks[0] = "stop";
+	toks[1] = "all";
+	toks[2] = "base";
+	toks[3] = "style";
+	toks[4] = "warning";
+	toks[5] = "error";
+	toks[6] = "unsupp";
+	toks[7] = "fatal";
+	toks[8] = "openbsd";
+	toks[9] = "netbsd";
+	toks[10] = NULL;
+
+	while (*arg) {
+		o = arg;
+		switch (getsubopt(&arg, (char * const *)toks, &v)) {
+		case 0:
+			*wstop = 1;
+			break;
+		case 1:
+		case 2:
+			mandoc_msg_setmin(MANDOCERR_BASE);
+			break;
+		case 3:
+			mandoc_msg_setmin(MANDOCERR_STYLE);
+			break;
+		case 4:
+			mandoc_msg_setmin(MANDOCERR_WARNING);
+			break;
+		case 5:
+			mandoc_msg_setmin(MANDOCERR_ERROR);
+			break;
+		case 6:
+			mandoc_msg_setmin(MANDOCERR_UNSUPP);
+			break;
+		case 7:
+			mandoc_msg_setmin(MANDOCERR_BADARG);
+			break;
+		case 8:
+			mandoc_msg_setmin(MANDOCERR_BASE);
+			*os_e = MANDOC_OS_OPENBSD;
+			break;
+		case 9:
+			mandoc_msg_setmin(MANDOCERR_BASE);
+			*os_e = MANDOC_OS_NETBSD;
+			break;
+		default:
+			mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0, "-W %s", o);
+			return -1;
+		}
+	}
+	return 0;
+}
+
+/*
+ * Wait until moved to the foreground,
+ * then fork the pager and wait for the user to close it.
+ */
+static void
+run_pager(struct tag_files *tag_files, char *tag_target)
+{
+	int	 signum, status;
+	pid_t	 man_pgid, tc_pgid;
+	pid_t	 pager_pid, wait_pid;
+
+	man_pgid = getpgid(0);
+	tag_files->tcpgid = man_pgid == getpid() ? getpgid(getppid()) :
+	    man_pgid;
+	pager_pid = 0;
+	signum = SIGSTOP;
+
+	for (;;) {
+		/* Stop here until moved to the foreground. */
+
+		tc_pgid = tcgetpgrp(STDOUT_FILENO);
+		if (tc_pgid != man_pgid) {
+			if (tc_pgid == pager_pid) {
+				(void)tcsetpgrp(STDOUT_FILENO, man_pgid);
+				if (signum == SIGTTIN)
+					continue;
+			} else
+				tag_files->tcpgid = tc_pgid;
+			kill(0, signum);
+			continue;
+		}
+
+		/* Once in the foreground, activate the pager. */
+
+		if (pager_pid) {
+			(void)tcsetpgrp(STDOUT_FILENO, pager_pid);
+			kill(pager_pid, SIGCONT);
+		} else
+			pager_pid = spawn_pager(tag_files, tag_target);
+
+		/* Wait for the pager to stop or exit. */
+
+		while ((wait_pid = waitpid(pager_pid, &status,
+		    WUNTRACED)) == -1 && errno == EINTR)
+			continue;
+
+		if (wait_pid == -1) {
+			mandoc_msg(MANDOCERR_WAIT, 0, 0,
+			    "%s", strerror(errno));
+			break;
+		}
+		if (!WIFSTOPPED(status))
+			break;
+
+		signum = WSTOPSIG(status);
+	}
+}
+
+static pid_t
+spawn_pager(struct tag_files *tag_files, char *tag_target)
+{
+	const struct timespec timeout = { 0, 100000000 };  /* 0.1s */
+#define MAX_PAGER_ARGS 16
+	char		*argv[MAX_PAGER_ARGS];
+	const char	*pager;
+	char		*cp;
+	int		 argc, use_ofn;
+	pid_t		 pager_pid;
+
+	assert(tag_files->ofd == -1);
+	assert(tag_files->tfs == NULL);
+
+	pager = getenv("MANPAGER");
+	if (pager == NULL || *pager == '\0')
+		pager = getenv("PAGER");
+	if (pager == NULL || *pager == '\0')
+		pager = "more -s";
+	cp = mandoc_strdup(pager);
+
+	/*
+	 * Parse the pager command into words.
+	 * Intentionally do not do anything fancy here.
+	 */
+
+	argc = 0;
+	while (argc + 5 < MAX_PAGER_ARGS) {
+		argv[argc++] = cp;
+		cp = strchr(cp, ' ');
+		if (cp == NULL)
+			break;
+		*cp++ = '\0';
+		while (*cp == ' ')
+			cp++;
+		if (*cp == '\0')
+			break;
+	}
+
+	/* For more(1) and less(1), use the tag file. */
+
+	use_ofn = 1;
+	if (use_ofn)
+		argv[argc++] = tag_files->ofn;
+	argv[argc] = NULL;
+
+	switch (pager_pid = fork()) {
+	case -1:
+		mandoc_msg(MANDOCERR_FORK, 0, 0, "%s", strerror(errno));
+		exit(mandoc_msg_getrc());
+	case 0:
+		break;
+	default:
+		(void)setpgid(pager_pid, 0);
+		(void)tcsetpgrp(STDOUT_FILENO, pager_pid);
+		if (pledge("stdio rpath tmppath tty proc", NULL) == -1) {
+			mandoc_msg(MANDOCERR_PLEDGE, 0, 0,
+			    "%s", strerror(errno));
+			exit(mandoc_msg_getrc());
+		}
+		tag_files->pager_pid = pager_pid;
+		return pager_pid;
+	}
+
+	/*
+	 * The child process becomes the pager.
+	 * Do not start it before controlling the terminal.
+	 */
+
+	while (tcgetpgrp(STDOUT_FILENO) != getpid())
+		nanosleep(&timeout, NULL);
+
+	execvp(argv[0], argv);
+	mandoc_msg(MANDOCERR_EXEC, 0, 0, "%s: %s", argv[0], strerror(errno));
+	_exit(mandoc_msg_getrc());
+}
diff --git a/usr.bin/mandoc/main.h b/usr.bin/mandoc/main.h
new file mode 100644
index 0000000..ee8e10d
--- /dev/null
+++ b/usr.bin/mandoc/main.h
@@ -0,0 +1,53 @@
+/*	$OpenBSD: main.h,v 1.25 2019/03/03 13:01:47 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons 
+ * Copyright (c) 2014, 2015, 2019 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	roff_meta;
+struct	manoutput;
+
+/*
+ * Definitions for main.c-visible output device functions, e.g., -Thtml
+ * and -Tascii.  Note that ascii_alloc() is named as such in
+ * anticipation of latin1_alloc() and so on, all of which map into the
+ * terminal output routines with different character settings.
+ */
+
+void		 *html_alloc(const struct manoutput *);
+void		  html_mdoc(void *, const struct roff_meta *);
+void		  html_man(void *, const struct roff_meta *);
+void		  html_reset(void *);
+void		  html_free(void *);
+
+void		  tree_mdoc(void *, const struct roff_meta *);
+void		  tree_man(void *, const struct roff_meta *);
+
+void		  man_mdoc(void *, const struct roff_meta *);
+
+void		 *locale_alloc(const struct manoutput *);
+void		 *utf8_alloc(const struct manoutput *);
+void		 *ascii_alloc(const struct manoutput *);
+void		  ascii_free(void *);
+
+void		 *pdf_alloc(const struct manoutput *);
+void		 *ps_alloc(const struct manoutput *);
+void		  pspdf_free(void *);
+
+void		  terminal_mdoc(void *, const struct roff_meta *);
+void		  terminal_man(void *, const struct roff_meta *);
+void		  terminal_sepline(void *);
+
+void		  markdown_mdoc(void *, const struct roff_meta *);
diff --git a/usr.bin/mandoc/makewhatis.8 b/usr.bin/mandoc/makewhatis.8
new file mode 100644
index 0000000..c42bcc4
--- /dev/null
+++ b/usr.bin/mandoc/makewhatis.8
@@ -0,0 +1,228 @@
+.\"	$OpenBSD: makewhatis.8,v 1.14 2017/05/17 22:26:52 schwarze Exp $
+.\"
+.\" Copyright (c) 2011, 2012 Kristaps Dzonsons 
+.\" Copyright (c) 2011, 2012, 2014, 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 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: May 17 2017 $
+.Dt MAKEWHATIS 8
+.Os
+.Sh NAME
+.Nm makewhatis
+.Nd index UNIX manuals
+.Sh SYNOPSIS
+.Nm
+.Op Fl aDnpQ
+.Op Fl T Cm utf8
+.Op Fl C Ar file
+.Nm
+.Op Fl aDnpQ
+.Op Fl T Cm utf8
+.Ar dir ...
+.Nm
+.Op Fl DnpQ
+.Op Fl T Cm utf8
+.Fl d Ar dir
+.Op Ar
+.Nm
+.Op Fl Dnp
+.Op Fl T Cm utf8
+.Fl u Ar dir
+.Op Ar
+.Nm
+.Op Fl DQ
+.Fl t Ar
+.Sh DESCRIPTION
+The
+.Nm
+utility extracts keywords from
+.Ux
+manuals and indexes them in a database for fast retrieval by
+.Xr apropos 1 ,
+.Xr whatis 1 ,
+and
+.Xr man 1 Ns 's
+.Fl k
+option.
+.Pp
+By default,
+.Nm
+creates a database in each
+.Ar dir
+using the files
+.Sm off
+.Sy man Ar section Li /
+.Op Ar arch Li /
+.Ar title . section
+.Sm on
+and
+.Sm off
+.Sy cat Ar section Li /
+.Op Ar arch Li /
+.Ar title . Sy 0
+.Sm on
+in that directory.
+Existing databases are replaced.
+If a directory contains no manual pages, no database is created in that
+directory.
+If
+.Ar dir
+is not provided,
+.Nm
+uses the default paths stipulated by
+.Xr man.conf 5 .
+.Pp
+The arguments are as follows:
+.Bl -tag -width "-C file"
+.It Fl a
+Use all directories and files found below
+.Ar dir ... .
+.It Fl C Ar file
+Specify an alternative configuration
+.Ar file
+in
+.Xr man.conf 5
+format.
+.It Fl D
+Display all files added or removed to the index.
+With a second
+.Fl D ,
+also show all keywords added for each file.
+.It Fl d Ar dir
+Merge (remove and re-add)
+.Ar
+to the database in
+.Ar dir .
+.It Fl n
+Do not create or modify any database; scan and parse only,
+and print manual page names and descriptions to standard output.
+.It Fl p
+Print warnings about potential problems with manual pages
+to the standard error output.
+.It Fl Q
+Quickly build reduced-size databases
+by reading only the NAME sections of manuals.
+The resulting databases will usually contain names and descriptions only.
+.It Fl T Cm utf8
+Use UTF-8 encoding instead of ASCII for strings stored in the databases.
+.It Fl t Ar
+Check the given
+.Ar files
+for potential problems.
+Implies
+.Fl a ,
+.Fl n ,
+and
+.Fl p .
+All diagnostic messages are printed to the standard output;
+the standard error output is not used.
+.It Fl u Ar dir
+Remove
+.Ar
+from the database in
+.Ar dir .
+If that causes the database to become empty, also delete the database file.
+.El
+.Pp
+If fatal parse errors are encountered while parsing, the offending file
+is printed to stderr, omitted from the index, and the parse continues
+with the next input file.
+.Sh ENVIRONMENT
+.Bl -tag -width MANPATH
+.It Ev MANPATH
+A colon-separated list of directories to create databases in.
+Ignored if a
+.Ar dir
+argument or the
+.Fl t
+option is specified.
+.El
+.Sh FILES
+.Bl -tag -width Ds
+.It Pa mandoc.db
+A database of manpages relative to the directory of the file.
+This file is portable across architectures and systems, so long as the
+manpage hierarchy it indexes does not change.
+.It Pa /etc/man.conf
+The default
+.Xr man 1
+configuration file.
+.El
+.Sh EXIT STATUS
+The
+.Nm
+utility exits with one of the following values:
+.Pp
+.Bl -tag -width Ds -compact
+.It 0
+No errors occurred.
+.It 5
+Invalid command line arguments were specified.
+No input files have been read.
+.It 6
+An operating system error occurred, for example memory exhaustion or an
+error accessing input files.
+Such errors cause
+.Nm
+to exit at once, possibly in the middle of parsing or formatting a file.
+The output databases are corrupt and should be removed.
+.El
+.Sh SEE ALSO
+.Xr apropos 1 ,
+.Xr man 1 ,
+.Xr whatis 1 ,
+.Xr man.conf 5
+.Sh HISTORY
+A
+.Nm
+utility first appeared in
+.Bx 2 .
+It was rewritten in
+.Xr perl 1
+for
+.Ox 2.7
+and in C for
+.Ox 5.6 .
+.Pp
+The
+.Ar dir
+argument first appeared in
+.Nx 1.0 ;
+the options
+.Fl dpt
+in
+.Ox 2.7 ;
+the option
+.Fl u
+in
+.Ox 3.4 ;
+and the options
+.Fl aCDnQT
+in
+.Ox 5.6 .
+.Sh AUTHORS
+.An -nosplit
+.An Bill Joy
+wrote the original
+.Bx
+.Nm
+in February 1979,
+.An Marc Espie
+started the Perl version in 2000,
+and the current version of
+.Nm
+was written by
+.An Kristaps Dzonsons Aq Mt kristaps@bsd.lv
+and
+.An Ingo Schwarze Aq Mt schwarze@openbsd.org .
diff --git a/usr.bin/mandoc/man.1 b/usr.bin/mandoc/man.1
new file mode 100644
index 0000000..2d4d33a
--- /dev/null
+++ b/usr.bin/mandoc/man.1
@@ -0,0 +1,431 @@
+.\"	$OpenBSD: man.1,v 1.36 2020/02/10 13:49:04 schwarze Exp $
+.\"
+.\" Copyright (c) 1989, 1990, 1993
+.\"	The Regents of the University of California.  All rights reserved.
+.\" Copyright (c) 2003, 2007, 2008, 2014 Jason McIntyre 
+.\" Copyright (c) 2010, 2011, 2014-2020 Ingo Schwarze 
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\"    notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\"    notice, this list of conditions and the following disclaimer in the
+.\"    documentation and/or other materials provided with the distribution.
+.\" 3. Neither the name of the University nor the names of its contributors
+.\"    may be used to endorse or promote products derived from this software
+.\"    without specific prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.\"     @(#)man.1	8.2 (Berkeley) 1/2/94
+.\"
+.Dd $Mdocdate: February 10 2020 $
+.Dt MAN 1
+.Os
+.Sh NAME
+.Nm man
+.Nd display manual pages
+.Sh SYNOPSIS
+.Nm man
+.Op Fl acfhklw
+.Op Fl C Ar file
+.Op Fl M Ar path
+.Op Fl m Ar path
+.Op Fl S Ar subsection
+.Op Oo Fl s Oc Ar section
+.Ar name ...
+.Sh DESCRIPTION
+The
+.Nm
+utility
+displays the
+manual page entitled
+.Ar name .
+Pages may be selected according to
+a specific category
+.Pq Ar section
+or
+machine architecture
+.Pq Ar subsection .
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl a
+Display all matching manual pages.
+.It Fl C Ar file
+Use the specified
+.Ar file
+instead of the default configuration file.
+This permits users to configure their own manual environment.
+See
+.Xr man.conf 5
+for a description of the contents of this file.
+.It Fl c
+Copy the manual page to the standard output instead of using
+.Xr more 1
+to paginate it.
+This is done by default if the standard output is not a terminal device.
+.Pp
+When using
+.Fl c ,
+most terminal devices are unable to show the markup.
+To print the output of
+.Nm
+to the terminal with markup but without using a pager, pipe it to
+.Xr ul 1 .
+To remove the markup, pipe the output to
+.Xr col 1
+.Fl b
+instead.
+.It Fl f
+A synonym for
+.Xr whatis 1 .
+It searches for
+.Ar name
+in manual page names and displays the header lines from all matching pages.
+The search is case insensitive and matches whole words only.
+.It Fl h
+Display only the SYNOPSIS lines of the requested manual pages.
+Implies
+.Fl a
+and
+.Fl c .
+.It Fl k
+A synonym for
+.Xr apropos 1 .
+Instead of
+.Ar name ,
+an expression can be provided using the syntax described in the
+.Xr apropos 1
+manual.
+By default, it displays the header lines of all matching pages.
+.It Fl l
+A synonym for
+.Xr mandoc 1 .
+The
+.Ar name
+arguments are interpreted as filenames.
+No search is done and
+.Ar file ,
+.Ar path ,
+.Ar section ,
+.Ar subsection ,
+and
+.Fl w
+are ignored.
+This option implies
+.Fl a .
+.It Fl M Ar path
+Override the list of directories to search for manual pages.
+The supplied
+.Ar path
+must be a colon
+.Pq Ql \&:
+separated list of directories.
+This option also overrides the environment variable
+.Ev MANPATH
+and any directories specified in the
+.Xr man.conf 5
+file.
+.It Fl m Ar path
+Augment the list of directories to search for manual pages.
+The supplied
+.Ar path
+must be a colon
+.Pq Ql \&:
+separated list of directories.
+These directories will be searched before those specified using the
+.Fl M
+option, the
+.Ev MANPATH
+environment variable, the
+.Xr man.conf 5
+file, or the default directories.
+.It Fl S Ar subsection
+Only show pages for the specified
+.Xr machine 1
+architecture.
+.Ar subsection
+is case insensitive.
+.Pp
+By default manual pages for all architectures are installed.
+Therefore this option can be used to view pages for one
+architecture whilst using another.
+.Pp
+This option overrides the
+.Ev MACHINE
+environment variable.
+.It Oo Fl s Oc Ar section
+Only select manuals from the specified
+.Ar section .
+The currently available sections are:
+.Pp
+.Bl -tag -width "localXXX" -offset indent -compact
+.It 1
+General commands
+.Pq tools and utilities .
+.It 2
+System calls and error numbers.
+.It 3
+Library functions.
+.It 3p
+.Xr perl 1
+programmer's reference guide.
+.It 4
+Device drivers.
+.It 5
+File formats.
+.It 6
+Games.
+.It 7
+Miscellaneous information.
+.It 8
+System maintenance and operation commands.
+.It 9
+Kernel internals.
+.El
+.It Fl w
+List the pathnames of all matching manual pages instead of displaying
+any of them.
+If no
+.Ar name
+is given, list the directories that would be searched.
+.El
+.Pp
+The options
+.Fl IKOTW
+are also supported and are documented in
+.Xr mandoc 1 .
+The options
+.Fl fkl
+are mutually exclusive and override each other.
+.Pp
+The search starts with the
+.Fl m
+argument if provided, then continues with the
+.Fl M
+argument, the
+.Ev MANPATH
+variable, the
+.Ic manpath
+entries in the
+.Xr man.conf 5
+file, or with
+.Pa /usr/share/man : Ns Pa /usr/X11R6/man : Ns Pa /usr/local/man
+by default.
+Within each of these, directories are searched in the order provided.
+Within each directory, the search proceeds according to the following
+list of sections: 1, 8, 6, 2, 3, 5, 7, 4, 9, 3p.
+The first match found is shown.
+.Pp
+The
+.Xr mandoc.db 5
+database is used for looking up manual page entries.
+In cases where the database is absent, outdated, or corrupt,
+.Nm
+falls back to looking for files called
+.Ar name . Ns Ar section .
+If both a formatted and an unformatted version of the same manual page,
+for example
+.Pa cat1/foo.0
+and
+.Pa man1/foo.1 ,
+exist in the same directory, only the unformatted version is used.
+The database is kept up to date with
+.Xr makewhatis 8 ,
+which is run by the
+.Xr weekly 8
+maintenance script.
+.Pp
+Guidelines for writing
+man pages can be found in
+.Xr mdoc 7 .
+.Sh ENVIRONMENT
+.Bl -tag -width MANPATHX
+.It Ev MACHINE
+As some manual pages are intended only for specific architectures,
+.Nm
+searches any subdirectories,
+with the same name as the current architecture,
+in every directory which it searches.
+Machine specific areas are checked before general areas.
+The current machine type may be overridden by setting the environment
+variable
+.Ev MACHINE
+to the name of a specific architecture,
+or with the
+.Fl S
+option.
+.Ev MACHINE
+is case insensitive.
+.It Ev MANPAGER
+Any non-empty value of the environment variable
+.Ev MANPAGER
+is used instead of the standard pagination program,
+.Xr more 1 .
+If
+.Xr less 1
+is used, the interactive
+.Ic :t
+command can be used to go to the definitions of various terms, for
+example command line options, command modifiers, internal commands,
+environment variables, function names, preprocessor macros,
+.Xr errno 2
+values, and some other emphasized words.
+Some terms may have defining text at more than one place.
+In that case, the
+.Xr less 1
+interactive commands
+.Ic t
+and
+.Ic T
+can be used to move to the next and to the previous place providing
+information about the term last searched for with
+.Ic :t .
+The
+.Fl O Cm tag Ns Op = Ns Ar term
+option documented in the
+.Xr mandoc 1
+manual opens a manual page at the definition of a specific
+.Ar term
+rather than at the beginning.
+.It Ev MANPATH
+Override the standard search path which is either specified in
+.Xr man.conf 5
+or the default path.
+The format of
+.Ev MANPATH
+is a colon
+.Pq Ql \&:
+separated list of directories.
+Invalid directories are ignored.
+Overridden by
+.Fl M ,
+ignored if
+.Fl l
+is specified.
+.Pp
+If
+.Ev MANPATH
+begins with a colon, it is appended to the standard path;
+if it ends with a colon, it is prepended to the standard path;
+or if it contains two adjacent colons,
+the standard path is inserted between the colons.
+.It Ev PAGER
+Specifies the pagination program to use when
+.Ev MANPAGER
+is not defined.
+If neither PAGER nor MANPAGER is defined,
+.Xr more 1
+.Fl s
+is used.
+.El
+.Sh FILES
+.Bl -tag -width /etc/man.conf -compact
+.It Pa /etc/man.conf
+default
+.Nm
+configuration file
+.El
+.Sh EXIT STATUS
+.Ex -std man
+See
+.Xr mandoc 1
+for details.
+.Sh EXAMPLES
+Format a page for pasting extracts into an email message \(em
+avoid printing any UTF-8 characters, reduce the width to ease
+quoting in replies, and remove markup:
+.Pp
+.Dl $ man -T ascii -O width=65 pledge | col -b
+.Pp
+Read a typeset page in a PDF viewer:
+.Pp
+.Dl $ MANPAGER=mupdf man -T pdf lpd
+.Sh SEE ALSO
+.Xr apropos 1 ,
+.Xr col 1 ,
+.Xr mandoc 1 ,
+.Xr ul 1 ,
+.Xr whereis 1 ,
+.Xr man.conf 5 ,
+.Xr mdoc 7
+.Sh STANDARDS
+The
+.Nm
+utility is compliant with the
+.St -p1003.1-2008
+specification.
+.Pp
+The flags
+.Op Fl aCcfhIKlMmOSsTWw ,
+as well as the environment variables
+.Ev MACHINE ,
+.Ev MANPAGER ,
+and
+.Ev MANPATH ,
+are extensions to that specification.
+.Sh HISTORY
+A
+.Nm
+command first appeared in
+.At v2 .
+.Pp
+The
+.Fl w
+option first appeared in
+.At v7 ;
+.Fl f
+and
+.Fl k
+in
+.Bx 4 ;
+.Fl M
+in
+.Bx 4.3 ;
+.Fl a
+in
+.Bx 4.3 Tahoe ;
+.Fl c
+and
+.Fl m
+in
+.Bx 4.3 Reno ;
+.Fl h
+in
+.Bx 4.3 Net/2 ;
+.Fl C
+in
+.Nx 1.0 ;
+.Fl s
+and
+.Fl S
+in
+.Ox 2.3 ;
+and
+.Fl I ,
+.Fl K ,
+.Fl l ,
+.Fl O ,
+and
+.Fl W
+in
+.Ox 5.7 .
+The
+.Fl T
+option first appeared in
+.At III
+and was also added in
+.Ox 5.7 .
diff --git a/usr.bin/mandoc/man.c b/usr.bin/mandoc/man.c
new file mode 100644
index 0000000..934f2b3
--- /dev/null
+++ b/usr.bin/mandoc/man.c
@@ -0,0 +1,343 @@
+/*	$OpenBSD: man.c,v 1.135 2019/01/05 00:36:46 schwarze Exp $ */
+/*
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons 
+ * Copyright (c) 2013-2015, 2017-2019 Ingo Schwarze 
+ * Copyright (c) 2011 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.
+ */
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "mandoc_aux.h"
+#include "mandoc.h"
+#include "roff.h"
+#include "man.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+#include "libman.h"
+
+static	char		*man_hasc(char *);
+static	int		 man_ptext(struct roff_man *, int, char *, int);
+static	int		 man_pmacro(struct roff_man *, int, char *, int);
+
+
+int
+man_parseln(struct roff_man *man, int ln, char *buf, int offs)
+{
+
+	if (man->last->type != ROFFT_EQN || ln > man->last->line)
+		man->flags |= MAN_NEWLINE;
+
+	return roff_getcontrol(man->roff, buf, &offs) ?
+	    man_pmacro(man, ln, buf, offs) :
+	    man_ptext(man, ln, buf, offs);
+}
+
+/*
+ * If the string ends with \c, return a pointer to the backslash.
+ * Otherwise, return NULL.
+ */
+static char *
+man_hasc(char *start)
+{
+	char	*cp, *ep;
+
+	ep = strchr(start, '\0') - 2;
+	if (ep < start || ep[0] != '\\' || ep[1] != 'c')
+		return NULL;
+	for (cp = ep; cp > start; cp--)
+		if (cp[-1] != '\\')
+			break;
+	return (ep - cp) % 2 ? NULL : ep;
+}
+
+void
+man_descope(struct roff_man *man, int line, int offs, char *start)
+{
+	/* Trailing \c keeps next-line scope open. */
+
+	if (start != NULL && man_hasc(start) != NULL)
+		return;
+
+	/*
+	 * Co-ordinate what happens with having a next-line scope open:
+	 * first close out the element scopes (if applicable),
+	 * then close out the block scope (also if applicable).
+	 */
+
+	if (man->flags & MAN_ELINE) {
+		while (man->last->parent->type != ROFFT_ROOT &&
+		    man_macro(man->last->parent->tok)->flags & MAN_ESCOPED)
+			man_unscope(man, man->last->parent);
+		man->flags &= ~MAN_ELINE;
+	}
+	if ( ! (man->flags & MAN_BLINE))
+		return;
+	man_unscope(man, man->last->parent);
+	roff_body_alloc(man, line, offs, man->last->tok);
+	man->flags &= ~(MAN_BLINE | ROFF_NONOFILL);
+}
+
+static int
+man_ptext(struct roff_man *man, int line, char *buf, int offs)
+{
+	int		 i;
+	char		*ep;
+
+	/* In no-fill mode, whitespace is preserved on text lines. */
+
+	if (man->flags & ROFF_NOFILL) {
+		roff_word_alloc(man, line, offs, buf + offs);
+		man_descope(man, line, offs, buf + offs);
+		return 1;
+	}
+
+	for (i = offs; buf[i] == ' '; i++)
+		/* Skip leading whitespace. */ ;
+
+	/*
+	 * Blank lines are ignored in next line scope
+	 * and right after headings and cancel preceding \c,
+	 * but add a single vertical space elsewhere.
+	 */
+
+	if (buf[i] == '\0') {
+		if (man->flags & (MAN_ELINE | MAN_BLINE)) {
+			mandoc_msg(MANDOCERR_BLK_BLANK, line, 0, NULL);
+			return 1;
+		}
+		if (man->last->tok == MAN_SH || man->last->tok == MAN_SS)
+			return 1;
+		if (man->last->type == ROFFT_TEXT &&
+		    ((ep = man_hasc(man->last->string)) != NULL)) {
+			*ep = '\0';
+			return 1;
+		}
+		roff_elem_alloc(man, line, offs, ROFF_sp);
+		man->next = ROFF_NEXT_SIBLING;
+		return 1;
+	}
+
+	/*
+	 * Warn if the last un-escaped character is whitespace. Then
+	 * strip away the remaining spaces (tabs stay!).
+	 */
+
+	i = (int)strlen(buf);
+	assert(i);
+
+	if (' ' == buf[i - 1] || '\t' == buf[i - 1]) {
+		if (i > 1 && '\\' != buf[i - 2])
+			mandoc_msg(MANDOCERR_SPACE_EOL, line, i - 1, NULL);
+
+		for (--i; i && ' ' == buf[i]; i--)
+			/* Spin back to non-space. */ ;
+
+		/* Jump ahead of escaped whitespace. */
+		i += '\\' == buf[i] ? 2 : 1;
+
+		buf[i] = '\0';
+	}
+	roff_word_alloc(man, line, offs, buf + offs);
+
+	/*
+	 * End-of-sentence check.  If the last character is an unescaped
+	 * EOS character, then flag the node as being the end of a
+	 * sentence.  The front-end will know how to interpret this.
+	 */
+
+	assert(i);
+	if (mandoc_eos(buf, (size_t)i))
+		man->last->flags |= NODE_EOS;
+
+	man_descope(man, line, offs, buf + offs);
+	return 1;
+}
+
+static int
+man_pmacro(struct roff_man *man, int ln, char *buf, int offs)
+{
+	struct roff_node *n;
+	const char	*cp;
+	size_t		 sz;
+	enum roff_tok	 tok;
+	int		 ppos;
+	int		 bline;
+
+	/* Determine the line macro. */
+
+	ppos = offs;
+	tok = TOKEN_NONE;
+	for (sz = 0; sz < 4 && strchr(" \t\\", buf[offs]) == NULL; sz++)
+		offs++;
+	if (sz > 0 && sz < 4)
+		tok = roffhash_find(man->manmac, buf + ppos, sz);
+	if (tok == TOKEN_NONE) {
+		mandoc_msg(MANDOCERR_MACRO, ln, ppos, "%s", buf + ppos - 1);
+		return 1;
+	}
+
+	/* Skip a leading escape sequence or tab. */
+
+	switch (buf[offs]) {
+	case '\\':
+		cp = buf + offs + 1;
+		mandoc_escape(&cp, NULL, NULL);
+		offs = cp - buf;
+		break;
+	case '\t':
+		offs++;
+		break;
+	default:
+		break;
+	}
+
+	/* Jump to the next non-whitespace word. */
+
+	while (buf[offs] == ' ')
+		offs++;
+
+	/*
+	 * Trailing whitespace.  Note that tabs are allowed to be passed
+	 * into the parser as "text", so we only warn about spaces here.
+	 */
+
+	if (buf[offs] == '\0' && buf[offs - 1] == ' ')
+		mandoc_msg(MANDOCERR_SPACE_EOL, ln, offs - 1, NULL);
+
+	/*
+	 * Some macros break next-line scopes; otherwise, remember
+	 * whether we are in next-line scope for a block head.
+	 */
+
+	man_breakscope(man, tok);
+	bline = man->flags & MAN_BLINE;
+
+	/*
+	 * If the line in next-line scope ends with \c, keep the
+	 * next-line scope open for the subsequent input line.
+	 * That is not at all portable, only groff >= 1.22.4
+	 * does it, but *if* this weird idiom occurs in a manual
+	 * page, that's very likely what the author intended.
+	 */
+
+	if (bline && man_hasc(buf + offs))
+		bline = 0;
+
+	/* Call to handler... */
+
+	(*man_macro(tok)->fp)(man, tok, ln, ppos, &offs, buf);
+
+	/* In quick mode (for mandocdb), abort after the NAME section. */
+
+	if (man->quick && tok == MAN_SH) {
+		n = man->last;
+		if (n->type == ROFFT_BODY &&
+		    strcmp(n->prev->child->string, "NAME"))
+			return 2;
+	}
+
+	/*
+	 * If we are in a next-line scope for a block head,
+	 * close it out now and switch to the body,
+	 * unless the next-line scope is allowed to continue.
+	 */
+
+	if (bline == 0 ||
+	    (man->flags & MAN_BLINE) == 0 ||
+	    man->flags & MAN_ELINE ||
+	    man_macro(tok)->flags & MAN_NSCOPED)
+		return 1;
+
+	man_unscope(man, man->last->parent);
+	roff_body_alloc(man, ln, ppos, man->last->tok);
+	man->flags &= ~(MAN_BLINE | ROFF_NONOFILL);
+	return 1;
+}
+
+void
+man_breakscope(struct roff_man *man, int tok)
+{
+	struct roff_node *n;
+
+	/*
+	 * An element next line scope is open,
+	 * and the new macro is not allowed inside elements.
+	 * Delete the element that is being broken.
+	 */
+
+	if (man->flags & MAN_ELINE && (tok < MAN_TH ||
+	    (man_macro(tok)->flags & MAN_NSCOPED) == 0)) {
+		n = man->last;
+		if (n->type == ROFFT_TEXT)
+			n = n->parent;
+		if (n->tok < MAN_TH ||
+		    (man_macro(n->tok)->flags & (MAN_NSCOPED | MAN_ESCOPED))
+		     == MAN_NSCOPED)
+			n = n->parent;
+
+		mandoc_msg(MANDOCERR_BLK_LINE, n->line, n->pos,
+		    "%s breaks %s", roff_name[tok], roff_name[n->tok]);
+
+		roff_node_delete(man, n);
+		man->flags &= ~MAN_ELINE;
+	}
+
+	/*
+	 * Weird special case:
+	 * Switching fill mode closes section headers.
+	 */
+
+	if (man->flags & MAN_BLINE &&
+	    (tok == ROFF_nf || tok == ROFF_fi) &&
+	    (man->last->tok == MAN_SH || man->last->tok == MAN_SS)) {
+		n = man->last;
+		man_unscope(man, n);
+		roff_body_alloc(man, n->line, n->pos, n->tok);
+		man->flags &= ~(MAN_BLINE | ROFF_NONOFILL);
+	}
+
+	/*
+	 * A block header next line scope is open,
+	 * and the new macro is not allowed inside block headers.
+	 * Delete the block that is being broken.
+	 */
+
+	if (man->flags & MAN_BLINE && tok != ROFF_nf && tok != ROFF_fi &&
+	    (tok < MAN_TH || man_macro(tok)->flags & MAN_XSCOPE)) {
+		n = man->last;
+		if (n->type == ROFFT_TEXT)
+			n = n->parent;
+		if (n->tok < MAN_TH ||
+		    (man_macro(n->tok)->flags & MAN_XSCOPE) == 0)
+			n = n->parent;
+
+		assert(n->type == ROFFT_HEAD);
+		n = n->parent;
+		assert(n->type == ROFFT_BLOCK);
+		assert(man_macro(n->tok)->flags & MAN_BSCOPED);
+
+		mandoc_msg(MANDOCERR_BLK_LINE, n->line, n->pos,
+		    "%s breaks %s", roff_name[tok], roff_name[n->tok]);
+
+		roff_node_delete(man, n);
+		man->flags &= ~(MAN_BLINE | ROFF_NONOFILL);
+	}
+}
diff --git a/usr.bin/mandoc/man.cgi.8 b/usr.bin/mandoc/man.cgi.8
new file mode 100644
index 0000000..a524f58
--- /dev/null
+++ b/usr.bin/mandoc/man.cgi.8
@@ -0,0 +1,426 @@
+.\"	$OpenBSD: man.cgi.8,v 1.22 2018/05/20 21:48:23 schwarze Exp $
+.\"
+.\" Copyright (c) 2014, 2015, 2016 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: May 20 2018 $
+.Dt MAN.CGI 8
+.Os
+.Sh NAME
+.Nm man.cgi
+.Nd CGI program to search and display manual pages
+.Sh DESCRIPTION
+The
+.Nm
+CGI program searches for manual pages on a WWW server
+and displays them to HTTP clients,
+providing functionality equivalent to the
+.Xr man 1
+and
+.Xr apropos 1
+utilities.
+It can use multiple manual trees in parallel.
+.Ss HTML search interface
+At the top of each generated HTML page,
+.Nm
+displays a search form containing these elements:
+.Bl -enum
+.It
+An input box for search queries, expecting
+either a name of a manual page or an
+.Ar expression
+using the syntax described in the
+.Xr apropos 1
+manual; filling this in is required for each search.
+.Pp
+The expression is broken into words at whitespace.
+Whitespace characters and backslashes can be escaped
+by prepending a backslash.
+The effect of prepending a backslash to another character is undefined;
+in the current implementation, it has no effect.
+.It
+A
+.Xr man 1
+submit button.
+The string in the input box is interpreted as the name of a manual page.
+.It
+An
+.Xr apropos 1
+submit button.
+The string in the input box is interpreted as a search
+.Ar expression .
+.It
+A dropdown menu to optionally select a manual section.
+If one is provided, it has the same effect as the
+.Xr man 1
+and
+.Xr apropos 1
+.Fl s
+option.
+Otherwise, pages from all sections are shown.
+.It
+A dropdown menu to optionally select an architecture.
+If one is provided, it has the same effect as the
+.Xr man 1
+and
+.Xr apropos 1
+.Fl S
+option.
+By default, pages for all architectures are shown.
+.It
+A dropdown menu to select a manual tree.
+If the configuration file
+.Pa /var/www/man/manpath.conf
+contains only one manpath, the dropdown menu is not shown.
+By default, the first manpath given in the file is used.
+.El
+.Ss Program output
+The
+.Nm
+program generates five kinds of output pages:
+.Bl -tag -width Ds
+.It The index page.
+This is returned when calling
+.Nm
+without
+.Ev PATH_INFO
+and without a
+.Ev QUERY_STRING .
+It serves as a starting point for using the program
+and shows the search form only.
+.It A list page.
+Lists are returned when searches match more than one manual page.
+The first column shows the names and section numbers of manuals
+as clickable links.
+The second column shows the one-line descriptions of the manuals.
+For
+.Xr man 1
+style searches, the content of the first manual page follows the list.
+.It A manual page.
+This output format is used when a search matches exactly one
+manual page, or when a link on a list page or an
+.Ic \&Xr
+link on another manual page is followed.
+.It A no-result page.
+This is shown when a search request returns no results -
+either because it violates the query syntax, or because
+the search does not match any manual pages.
+.It \&An error page.
+This cannot happen by merely clicking the
+.Dq Search
+button, but only by manually entering an invalid URI.
+It does not show the search form, but only an error message
+and a link back to the index page.
+.El
+.Ss Setup
+For each manual tree, create one first-level subdirectory below
+.Pa /var/www/man .
+The name of one of these directories is called a
+.Dq manpath
+in the context of
+.Nm .
+Create a single ASCII text file
+.Pa /var/www/man/manpath.conf
+containing the names of these directories, one per line.
+The directory given first is used as the default manpath.
+.Pp
+Inside each of these directories, use the same directory and file
+structure as found below
+.Pa /usr/share/man ,
+that is, second-level subdirectories
+.Pa /var/www/man/*/man1 , /var/www/man/*/man2
+etc. containing source
+.Xr mdoc 7
+and
+.Xr man 7
+manuals with file name extensions matching the section numbers,
+second-level subdirectories
+.Pa /var/www/man/*/cat1 , /var/www/man/*/cat2
+etc. containing preformatted manuals with the file name extension
+.Sq 0 ,
+and optional third-level subdirectories for architectures.
+Use
+.Xr makewhatis 8
+to create a
+.Xr mandoc.db 5
+database inside each manpath.
+.Pp
+Configure your web server to execute CGI programs located in
+.Pa /cgi-bin .
+When using
+.Ox
+.Xr httpd 8 ,
+the
+.Xr slowcgi 8
+proxy daemon is needed to translate FastCGI requests to plain old CGI.
+.Pp
+To compile
+.Nm ,
+first copy
+.Pa cgi.h.example
+to
+.Pa cgi.h
+and edit it according to your needs.
+It contains the following compile-time definitions:
+.Bl -tag -width Ds
+.It Ev COMPAT_OLDURI
+Only useful for running on www.openbsd.org to deal with old URIs containing
+.Qq "manpath=OpenBSD "
+where the blank character has to be translated to a hyphen.
+When compiling for other sites, this definition can be deleted.
+.It Dv CSS_DIR
+An optional file system path to the directory containing the file
+.Pa mandoc.css ,
+to be specified relative to the server's document root,
+and to be specified without a trailing slash.
+When empty, the CSS file is assumed to be in the document root.
+Otherwise, a leading slash is needed.
+This is used in generated HTML code.
+.It Dv CUSTOMIZE_TITLE
+An ASCII string to be used for the HTML  element.
+.It Dv MAN_DIR
+A file system path to the
+.Nm
+data directory relative to the web server
+.Xr chroot 2
+directory, to be specified with a leading slash and without a trailing slash.
+It needs to have at least one component; the root directory cannot be used
+for this purpose.
+The files
+.Pa manpath.conf ,
+.Pa header.html ,
+and
+.Pa footer.html
+are looked up in this directory.
+It is also prepended to the manpath when opening
+.Xr mandoc.db 5
+and manual page files.
+.It Dv SCRIPT_NAME
+The initial component of URIs, to be specified without leading
+and trailing slashes.
+It can be empty.
+.El
+.Pp
+After editing
+.Pa cgi.h ,
+run
+.Pp
+.Dl make man.cgi
+.Pp
+and copy the resulting binary to the proper location,
+for example using the command:
+.Pp
+.Dl make installcgi
+.Pp
+In addition to that, make sure the default manpath contains the files
+.Pa man1/apropos.1
+and
+.Pa man8/man.cgi.8 ,
+or the documentation links at the bottom of the index page will not work.
+.Ss URI interface
+.Nm
+uniform resource identifiers are not needed for interactive use,
+but can be useful for deep linking.
+They consist of:
+.Bl -enum
+.It
+The
+.Cm http://
+or
+.Cm https://
+protocol specifier.
+.It
+The host name.
+.It
+The
+.Dv SCRIPT_NAME ,
+preceded by a slash unless empty.
+.It
+To show a single page, a slash, the manpath, another slash,
+and the name of the requested file, for example
+.Pa /OpenBSD-current/man1/mandoc.1 .
+This can be abbreviated according to the following syntax:
+.Sm off
+.Op / Ar manpath
+.Op / Cm man Ar sec
+.Op / Ar arch
+.Pf / Ar name Op \&. Ar sec
+.Sm on
+.It
+For searches, a query string starting with a question mark
+and consisting of
+.Ar key Ns = Ns Ar value
+pairs, separated by ampersands, for example
+.Pa ?manpath=OpenBSD-current&query=mandoc .
+Supported keys are
+.Cm manpath ,
+.Cm query ,
+.Cm sec ,
+.Cm arch ,
+corresponding to
+.Xr apropos 1
+.Fl M ,
+.Ar expression ,
+.Fl s ,
+.Fl S ,
+respectively, and
+.Cm apropos ,
+which is a boolean parameter to select or deselect the
+.Xr apropos 1
+query mode.
+For backward compatibility with the traditional
+.Nm ,
+.Cm sektion
+is supported as an alias for
+.Cm sec .
+.El
+.Ss Restricted character set
+For security reasons, in particular to prevent cross site scripting
+attacks, some strings used by
+.Nm
+can only contain the following characters:
+.Pp
+.Bl -dash -compact -offset indent
+.It
+lower case and upper case ASCII letters
+.It
+the ten decimal digits
+.It
+the dash
+.Pq Sq -
+.It
+the dot
+.Pq Sq \&.
+.It
+the slash
+.Pq Sq /
+.It
+the underscore
+.Pq Sq _
+.El
+.Pp
+In particular, this applies to all manpaths and architecture names.
+.Sh ENVIRONMENT
+The web server may pass the following CGI variables to
+.Nm :
+.Bl -tag -width Ds
+.It Ev SCRIPT_NAME
+The initial part of the URI passed from the client to the server,
+starting after the server's host name and ending before
+.Ev PATH_INFO .
+This is ignored by
+.Nm .
+When constructing URIs for links and redirections, the
+.Dv SCRIPT_NAME
+preprocessor constant is used instead.
+.It Ev PATH_INFO
+The final part of the URI path passed from the client to the server,
+starting after the
+.Ev SCRIPT_NAME
+and ending before the
+.Ev QUERY_STRING .
+It is used by the
+.Cm show
+page to acquire the manpath and filename it needs.
+.It Ev QUERY_STRING
+The HTTP query string passed from the client to the server.
+It is the final part of the URI, after the question mark.
+It is used by the
+.Cm search
+page to acquire the named parameters it needs.
+.El
+.Sh FILES
+.Bl -tag -width Ds
+.It Pa /var/www
+Default web server
+.Xr chroot 2
+directory.
+All the following paths are specified relative to this directory.
+.It Pa /cgi-bin/man.cgi
+The usual file system path to the
+.Nm
+program inside the web server
+.Xr chroot 2
+directory.
+A different name can be chosen, but in any case, it needs to be configured in
+.Xr httpd.conf 5 .
+.It Pa /htdocs
+The file system path to the server document root directory
+relative to the server
+.Xr chroot 2
+directory.
+This is part of the web server configuration and not specific to
+.Nm .
+.It Pa /htdocs/mandoc.css
+A style sheet for
+.Xr mandoc 1
+HTML styling, referenced from each generated HTML page.
+.It Pa /man
+Default
+.Nm
+data directory containing all the manual trees.
+Can be overridden by
+.Dv MAN_DIR .
+.It Pa /man/manpath.conf
+The list of available manpaths, one per line.
+If any of the lines in this file contains a slash
+.Pq Sq /
+or any character not contained in the
+.Sx Restricted character set ,
+.Nm
+reports an internal server error and exits without doing anything.
+.It Pa /man/header.html
+An optional file containing static HTML code to be inserted right
+after opening the <BODY> element.
+.It Pa /man/footer.html
+An optional file containing static HTML code to be inserted right
+before closing the <BODY> element.
+.It Pa /man/OpenBSD-current/man1/mandoc.1
+An example
+.Xr mdoc 7
+source file located below the
+.Dq OpenBSD-current
+manpath.
+.El
+.Sh COMPATIBILITY
+The
+.Nm
+CGI program is call-compatible with queries from the traditional
+.Pa man.cgi
+script by Wolfram Schneider.
+However, the output looks quite different.
+.Sh SEE ALSO
+.Xr apropos 1 ,
+.Xr mandoc.db 5 ,
+.Xr makewhatis 8 ,
+.Xr slowcgi 8
+.Sh HISTORY
+A version of
+.Nm
+based on
+.Xr mandoc 1
+first appeared in mdocml-1.12.1 (March 2012).
+The current
+.Xr mandoc.db 5
+database format first appeared in
+.Ox 6.1 .
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+program was written by
+.An Kristaps Dzonsons Aq Mt kristaps@bsd.lv
+and is maintained by
+.An Ingo Schwarze Aq Mt schwarze@openbsd.org ,
+who also designed and implemented the database format.
diff --git a/usr.bin/mandoc/man.conf.5 b/usr.bin/mandoc/man.conf.5
new file mode 100644
index 0000000..2ba8cde
--- /dev/null
+++ b/usr.bin/mandoc/man.conf.5
@@ -0,0 +1,133 @@
+.\"	$OpenBSD: man.conf.5,v 1.8 2020/02/10 14:42:03 schwarze Exp $
+.\"
+.\" Copyright (c) 2015, 2017 Ingo Schwarze <schwarze@openbsd.org>
+.\"
+.\" 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: February 10 2020 $
+.Dt MAN.CONF 5
+.Os
+.Sh NAME
+.Nm man.conf
+.Nd configuration file for man
+.Sh DESCRIPTION
+This is the configuration file
+for the
+.Xr man 1 ,
+.Xr apropos 1 ,
+and
+.Xr makewhatis 8
+utilities.
+Its presence, and all directives, are optional.
+.Pp
+This file is an ASCII text file.
+Leading whitespace on lines, lines starting with
+.Sq # ,
+and blank lines are ignored.
+Words are separated by whitespace.
+The first word on each line is the name of a configuration directive.
+.Pp
+The following directives are supported:
+.Bl -tag -width Ds
+.It Ic manpath Ar path
+Override the default search
+.Ar path
+for
+.Xr man 1 ,
+.Xr apropos 1 ,
+and
+.Xr makewhatis 8 .
+It can be used multiple times to specify multiple paths,
+with the order determining the manual page search order.
+.Pp
+Each path is a tree containing subdirectories
+whose names consist of the strings
+.Sq man
+and/or
+.Sq cat
+followed by the names of sections, usually single digits.
+The former are supposed to contain unformatted manual pages in
+.Xr mdoc 7
+and/or
+.Xr man 7
+format; file names should end with the name of the section
+preceded by a dot.
+The latter should contain preformatted manual pages;
+file names should end with
+.Ql .0 .
+.Pp
+Creating a
+.Xr mandoc.db 5
+database with
+.Xr makewhatis 8
+in each directory configured with
+.Ic manpath
+is recommended and necessary for
+.Xr apropos 1
+to work, and also for
+.Xr man 1
+on operating systems like
+.Ox
+that install each manual page with only one file name in the file system,
+even if it documents multiple utilities or functions.
+.It Ic output Ar option Op Ar value
+Configure the default value of an output option.
+These directives are overridden by the
+.Fl O
+command line options of the same names.
+For details, see the
+.Xr mandoc 1
+manual.
+.Pp
+.Bl -column fragment integer "ascii, utf8" -compact
+.It Ar option   Ta Ar value Ta used by Fl T Ta purpose
+.It Ta Ta Ta
+.It Ic fragment Ta none     Ta Cm html Ta print only body
+.It Ic includes Ta string   Ta Cm html Ta path to header files
+.It Ic indent   Ta integer  Ta Cm ascii , utf8 Ta left margin
+.It Ic man      Ta string   Ta Cm html Ta path for \&Xr links
+.It Ic paper    Ta string   Ta Cm ps , pdf Ta paper size
+.It Ic style    Ta string   Ta Cm html Ta CSS file
+.It Ic toc      Ta none     Ta Cm html Ta print table of contents
+.It Ic width    Ta integer  Ta Cm ascii , utf8 Ta right margin
+.El
+.El
+.Sh FILES
+.Bl -tag -width /etc/examples/man.conf -compact
+.It Pa /etc/man.conf
+.It Pa /etc/examples/man.conf
+.El
+.Sh EXAMPLES
+The following configuration file reproduces the defaults:
+installing it is equivalent to not having a
+.Nm
+file at all.
+.Bd -literal -offset indent
+manpath /usr/share/man
+manpath /usr/X11R6/man
+manpath /usr/local/man
+.Ed
+.Sh SEE ALSO
+.Xr apropos 1 ,
+.Xr man 1 ,
+.Xr makewhatis 8
+.Sh HISTORY
+A relatively complicated
+.Nm
+file format first appeared in
+.Bx 4.3 Reno .
+For
+.Ox 5.8 ,
+it was redesigned from scratch, aiming for simplicity.
+.Sh AUTHORS
+.An Ingo Schwarze Aq Mt schwarze@openbsd.org
diff --git a/usr.bin/mandoc/man.h b/usr.bin/mandoc/man.h
new file mode 100644
index 0000000..eda8c6c
--- /dev/null
+++ b/usr.bin/mandoc/man.h
@@ -0,0 +1,21 @@
+/*	$OpenBSD: man.h,v 1.59 2018/08/23 19:32:03 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2015 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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	roff_man;
+
+void			 man_validate(struct roff_man *);
diff --git a/usr.bin/mandoc/man_html.c b/usr.bin/mandoc/man_html.c
new file mode 100644
index 0000000..a2a1b4d
--- /dev/null
+++ b/usr.bin/mandoc/man_html.c
@@ -0,0 +1,640 @@
+/* $OpenBSD: man_html.c,v 1.131 2020/04/04 20:23:06 schwarze Exp $ */
+/*
+ * Copyright (c) 2013-2015, 2017-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008-2012, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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_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_abort_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_abort_pre, NULL }, /* LP */
+	{ man_PP_pre, NULL }, /* PP */
+	{ man_abort_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 */
+};
+
+
+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, "");
+		if (n != NULL && n->type == ROFFT_COMMENT)
+			print_gen_comment(h, n);
+		t = print_otag(h, TAG_HEAD, "");
+		print_man_head(man, h);
+		print_tagq(h, t);
+		print_otag(h, TAG_BODY, "");
+	}
+
+	man_root_pre(man, h);
+	t = print_otag(h, TAG_DIV, "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;
+
+	html_fillmode(h, n->flags & NODE_NOFILL ? ROFF_nf : ROFF_fi);
+
+	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);
+
+	if (n->flags & NODE_NOFILL && n->tok != MAN_YS &&
+	    (n->next != NULL && n->next->flags & NODE_LINE)) {
+		/* In .nf = <pre>, print even empty lines. */
+		h->col++;
+		print_endline(h);
+	}
+}
+
+static void
+man_root_pre(const struct roff_meta *man, struct html *h)
+{
+	struct tag	*t, *tt;
+	char		*title;
+
+	assert(man->title);
+	assert(man->msec);
+	mandoc_asprintf(&title, "%s(%s)", man->title, man->msec);
+
+	t = print_otag(h, TAG_TABLE, "c", "head");
+	tt = print_otag(h, TAG_TR, "");
+
+	print_otag(h, TAG_TD, "c", "head-ltitle");
+	print_text(h, title);
+	print_stagq(h, tt);
+
+	print_otag(h, TAG_TD, "c", "head-vol");
+	if (man->vol != NULL)
+		print_text(h, man->vol);
+	print_stagq(h, tt);
+
+	print_otag(h, TAG_TD, "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, *tt;
+
+	t = print_otag(h, TAG_TABLE, "c", "foot");
+	tt = print_otag(h, TAG_TR, "");
+
+	print_otag(h, TAG_TD, "c", "foot-date");
+	print_text(h, man->date);
+	print_stagq(h, tt);
+
+	print_otag(h, TAG_TD, "c", "foot-os");
+	if (man->os != NULL)
+		print_text(h, man->os);
+	print_tagq(h, t);
+}
+
+static int
+man_SH_pre(MAN_ARGS)
+{
+	const char	*class;
+	enum htmltag	 tag;
+
+	if (n->tok == MAN_SH) {
+		tag = TAG_H1;
+		class = "Sh";
+	} else {
+		tag = TAG_H2;
+		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_PP ? "Pp" : "Pp HP");
+		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' : ' ';
+	c2 = strcmp(s2, "*") == 0 ? '*' :
+	     strcmp(s2, "\\-") == 0 ? '-' :
+	     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_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;
+}
+
+static int
+man_abort_pre(MAN_ARGS)
+{
+	abort();
+}
diff --git a/usr.bin/mandoc/man_macro.c b/usr.bin/mandoc/man_macro.c
new file mode 100644
index 0000000..dc0bcfd
--- /dev/null
+++ b/usr.bin/mandoc/man_macro.c
@@ -0,0 +1,466 @@
+/*	$OpenBSD: man_macro.c,v 1.106 2019/01/05 18:59:37 schwarze Exp $ */
+/*
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2012-2015, 2017-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2013 Franco Fichtner <franco@lastsummer.de>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "man.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+#include "libman.h"
+
+static	void		 blk_close(MACRO_PROT_ARGS);
+static	void		 blk_exp(MACRO_PROT_ARGS);
+static	void		 blk_imp(MACRO_PROT_ARGS);
+static	void		 in_line_eoln(MACRO_PROT_ARGS);
+static	int		 man_args(struct roff_man *, int,
+				int *, char *, char **);
+static	void		 rew_scope(struct roff_man *, enum roff_tok);
+
+static const struct man_macro man_macros[MAN_MAX - MAN_TH] = {
+	{ in_line_eoln, MAN_XSCOPE }, /* TH */
+	{ blk_imp, MAN_XSCOPE | MAN_BSCOPED }, /* SH */
+	{ blk_imp, MAN_XSCOPE | MAN_BSCOPED }, /* SS */
+	{ blk_imp, MAN_XSCOPE | MAN_BSCOPED }, /* TP */
+	{ blk_imp, MAN_XSCOPE | MAN_BSCOPED }, /* TQ */
+	{ blk_imp, MAN_XSCOPE }, /* LP */
+	{ blk_imp, MAN_XSCOPE }, /* PP */
+	{ blk_imp, MAN_XSCOPE }, /* P */
+	{ blk_imp, MAN_XSCOPE }, /* IP */
+	{ blk_imp, MAN_XSCOPE }, /* HP */
+	{ in_line_eoln, MAN_NSCOPED | MAN_ESCOPED | MAN_JOIN }, /* SM */
+	{ in_line_eoln, MAN_NSCOPED | MAN_ESCOPED | MAN_JOIN }, /* SB */
+	{ in_line_eoln, 0 }, /* BI */
+	{ in_line_eoln, 0 }, /* IB */
+	{ in_line_eoln, 0 }, /* BR */
+	{ in_line_eoln, 0 }, /* RB */
+	{ in_line_eoln, MAN_NSCOPED | MAN_ESCOPED | MAN_JOIN }, /* R */
+	{ in_line_eoln, MAN_NSCOPED | MAN_ESCOPED | MAN_JOIN }, /* B */
+	{ in_line_eoln, MAN_NSCOPED | MAN_ESCOPED | MAN_JOIN }, /* I */
+	{ in_line_eoln, 0 }, /* IR */
+	{ in_line_eoln, 0 }, /* RI */
+	{ blk_close, MAN_XSCOPE }, /* RE */
+	{ blk_exp, MAN_XSCOPE }, /* RS */
+	{ in_line_eoln, 0 }, /* DT */
+	{ in_line_eoln, 0 }, /* UC */
+	{ in_line_eoln, MAN_NSCOPED }, /* PD */
+	{ in_line_eoln, 0 }, /* AT */
+	{ in_line_eoln, MAN_NSCOPED }, /* in */
+	{ blk_imp, MAN_XSCOPE }, /* SY */
+	{ blk_close, MAN_XSCOPE }, /* YS */
+	{ in_line_eoln, 0 }, /* OP */
+	{ in_line_eoln, MAN_XSCOPE }, /* EX */
+	{ in_line_eoln, MAN_XSCOPE }, /* EE */
+	{ blk_exp, MAN_XSCOPE }, /* UR */
+	{ blk_close, MAN_XSCOPE }, /* UE */
+	{ blk_exp, MAN_XSCOPE }, /* MT */
+	{ blk_close, MAN_XSCOPE }, /* ME */
+};
+
+
+const struct man_macro *
+man_macro(enum roff_tok tok)
+{
+	assert(tok >= MAN_TH && tok <= MAN_MAX);
+	return man_macros + (tok - MAN_TH);
+}
+
+void
+man_unscope(struct roff_man *man, const struct roff_node *to)
+{
+	struct roff_node *n;
+
+	to = to->parent;
+	n = man->last;
+	while (n != to) {
+
+		/* Reached the end of the document? */
+
+		if (to == NULL && ! (n->flags & NODE_VALID)) {
+			if (man->flags & (MAN_BLINE | MAN_ELINE) &&
+			    man_macro(n->tok)->flags &
+			     (MAN_BSCOPED | MAN_NSCOPED)) {
+				mandoc_msg(MANDOCERR_BLK_LINE,
+				    n->line, n->pos,
+				    "EOF breaks %s", roff_name[n->tok]);
+				if (man->flags & MAN_ELINE)
+					man->flags &= ~MAN_ELINE;
+				else {
+					assert(n->type == ROFFT_HEAD);
+					n = n->parent;
+					man->flags &= ~MAN_BLINE;
+				}
+				man->last = n;
+				n = n->parent;
+				roff_node_delete(man, man->last);
+				continue;
+			}
+			if (n->type == ROFFT_BLOCK &&
+			    man_macro(n->tok)->fp == blk_exp)
+				mandoc_msg(MANDOCERR_BLK_NOEND,
+				    n->line, n->pos, "%s",
+				    roff_name[n->tok]);
+		}
+
+		/*
+		 * We might delete the man->last node
+		 * in the post-validation phase.
+		 * Save a pointer to the parent such that
+		 * we know where to continue the iteration.
+		 */
+
+		man->last = n;
+		n = n->parent;
+		man->last->flags |= NODE_VALID;
+	}
+
+	/*
+	 * If we ended up at the parent of the node we were
+	 * supposed to rewind to, that means the target node
+	 * got deleted, so add the next node we parse as a child
+	 * of the parent instead of as a sibling of the target.
+	 */
+
+	man->next = (man->last == to) ?
+	    ROFF_NEXT_CHILD : ROFF_NEXT_SIBLING;
+}
+
+/*
+ * Rewinding entails ascending the parse tree until a coherent point,
+ * for example, the `SH' macro will close out any intervening `SS'
+ * scopes.  When a scope is closed, it must be validated and actioned.
+ */
+static void
+rew_scope(struct roff_man *man, enum roff_tok tok)
+{
+	struct roff_node *n;
+
+	/* Preserve empty paragraphs before RS. */
+
+	n = man->last;
+	if (tok == MAN_RS && n->child == NULL &&
+	    (n->tok == MAN_P || n->tok == MAN_PP || n->tok == MAN_LP))
+		return;
+
+	for (;;) {
+		if (n->type == ROFFT_ROOT)
+			return;
+		if (n->flags & NODE_VALID) {
+			n = n->parent;
+			continue;
+		}
+		if (n->type != ROFFT_BLOCK) {
+			if (n->parent->type == ROFFT_ROOT) {
+				man_unscope(man, n);
+				return;
+			} else {
+				n = n->parent;
+				continue;
+			}
+		}
+		if (tok != MAN_SH && (n->tok == MAN_SH ||
+		    (tok != MAN_SS && (n->tok == MAN_SS ||
+		     man_macro(n->tok)->fp == blk_exp))))
+			return;
+		man_unscope(man, n);
+		n = man->last;
+	}
+}
+
+
+/*
+ * Close out a generic explicit macro.
+ */
+void
+blk_close(MACRO_PROT_ARGS)
+{
+	enum roff_tok		 ctok, ntok;
+	const struct roff_node	*nn;
+	char			*p, *ep;
+	int			 cline, cpos, la, nrew, target;
+
+	nrew = 1;
+	switch (tok) {
+	case MAN_RE:
+		ntok = MAN_RS;
+		la = *pos;
+		if ( ! man_args(man, line, pos, buf, &p))
+			break;
+		for (nn = man->last->parent; nn; nn = nn->parent)
+			if (nn->tok == ntok && nn->type == ROFFT_BLOCK)
+				nrew++;
+		target = strtol(p, &ep, 10);
+		if (*ep != '\0')
+			mandoc_msg(MANDOCERR_ARG_EXCESS, line,
+			    la + (buf[la] == '"') + (int)(ep - p),
+			    "RE ... %s", ep);
+		free(p);
+		if (target == 0)
+			target = 1;
+		nrew -= target;
+		if (nrew < 1) {
+			mandoc_msg(MANDOCERR_RE_NOTOPEN,
+			    line, ppos, "RE %d", target);
+			return;
+		}
+		break;
+	case MAN_YS:
+		ntok = MAN_SY;
+		break;
+	case MAN_UE:
+		ntok = MAN_UR;
+		break;
+	case MAN_ME:
+		ntok = MAN_MT;
+		break;
+	default:
+		abort();
+	}
+
+	for (nn = man->last->parent; nn; nn = nn->parent)
+		if (nn->tok == ntok && nn->type == ROFFT_BLOCK && ! --nrew)
+			break;
+
+	if (nn == NULL) {
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN,
+		    line, ppos, "%s", roff_name[tok]);
+		rew_scope(man, MAN_PP);
+		if (tok == MAN_RE) {
+			roff_elem_alloc(man, line, ppos, ROFF_br);
+			man->last->flags |= NODE_LINE |
+			    NODE_VALID | NODE_ENDED;
+			man->next = ROFF_NEXT_SIBLING;
+		}
+		return;
+	}
+
+	cline = man->last->line;
+	cpos = man->last->pos;
+	ctok = man->last->tok;
+	man_unscope(man, nn);
+
+	if (tok == MAN_RE && nn->head->aux > 0)
+		roff_setreg(man->roff, "an-margin", nn->head->aux, '-');
+
+	/* Trailing text. */
+
+	if (buf[*pos] != '\0') {
+		roff_word_alloc(man, line, ppos, buf + *pos);
+		man->last->flags |= NODE_DELIMC;
+		if (mandoc_eos(man->last->string, strlen(man->last->string)))
+			man->last->flags |= NODE_EOS;
+	}
+
+	/* Move a trailing paragraph behind the block. */
+
+	if (ctok == MAN_LP || ctok == MAN_PP || ctok == MAN_P) {
+		*pos = strlen(buf);
+		blk_imp(man, ctok, cline, cpos, pos, buf);
+	}
+
+	/* Synopsis blocks need an explicit end marker for spacing. */
+
+	if (tok == MAN_YS && man->last == nn) {
+		roff_elem_alloc(man, line, ppos, tok);
+		man_unscope(man, man->last);
+	}
+}
+
+void
+blk_exp(MACRO_PROT_ARGS)
+{
+	struct roff_node *head;
+	char		*p;
+	int		 la;
+
+	if (tok == MAN_RS) {
+		rew_scope(man, tok);
+		man->flags |= ROFF_NONOFILL;
+	}
+	roff_block_alloc(man, line, ppos, tok);
+	head = roff_head_alloc(man, line, ppos, tok);
+
+	la = *pos;
+	if (man_args(man, line, pos, buf, &p)) {
+		roff_word_alloc(man, line, la, p);
+		if (tok == MAN_RS) {
+			if (roff_getreg(man->roff, "an-margin") == 0)
+				roff_setreg(man->roff, "an-margin",
+				    7 * 24, '=');
+			if ((head->aux = strtod(p, NULL) * 24.0) > 0)
+				roff_setreg(man->roff, "an-margin",
+				    head->aux, '+');
+		}
+		free(p);
+	}
+
+	if (buf[*pos] != '\0')
+		mandoc_msg(MANDOCERR_ARG_EXCESS, line, *pos,
+		    "%s ... %s", roff_name[tok], buf + *pos);
+
+	man_unscope(man, head);
+	roff_body_alloc(man, line, ppos, tok);
+	man->flags &= ~ROFF_NONOFILL;
+}
+
+/*
+ * Parse an implicit-block macro.  These contain a ROFFT_HEAD and a
+ * ROFFT_BODY contained within a ROFFT_BLOCK.  Rules for closing out other
+ * scopes, such as `SH' closing out an `SS', are defined in the rew
+ * routines.
+ */
+void
+blk_imp(MACRO_PROT_ARGS)
+{
+	int		 la;
+	char		*p;
+	struct roff_node *n;
+
+	rew_scope(man, tok);
+	man->flags |= ROFF_NONOFILL;
+	if (tok == MAN_SH || tok == MAN_SS)
+		man->flags &= ~ROFF_NOFILL;
+	roff_block_alloc(man, line, ppos, tok);
+	n = roff_head_alloc(man, line, ppos, tok);
+
+	/* Add line arguments. */
+
+	for (;;) {
+		la = *pos;
+		if ( ! man_args(man, line, pos, buf, &p))
+			break;
+		roff_word_alloc(man, line, la, p);
+		free(p);
+	}
+
+	/*
+	 * For macros having optional next-line scope,
+	 * keep the head open if there were no arguments.
+	 * For `TP' and `TQ', always keep the head open.
+	 */
+
+	if (man_macro(tok)->flags & MAN_BSCOPED &&
+	    (tok == MAN_TP || tok == MAN_TQ || n == man->last)) {
+		man->flags |= MAN_BLINE;
+		return;
+	}
+
+	/* Close out the head and open the body. */
+
+	man_unscope(man, n);
+	roff_body_alloc(man, line, ppos, tok);
+	man->flags &= ~ROFF_NONOFILL;
+}
+
+void
+in_line_eoln(MACRO_PROT_ARGS)
+{
+	int		 la;
+	char		*p;
+	struct roff_node *n;
+
+	roff_elem_alloc(man, line, ppos, tok);
+	n = man->last;
+
+	if (tok == MAN_EX)
+		man->flags |= ROFF_NOFILL;
+	else if (tok == MAN_EE)
+		man->flags &= ~ROFF_NOFILL;
+
+	for (;;) {
+		if (buf[*pos] != '\0' && man->last != n && tok == MAN_PD) {
+			mandoc_msg(MANDOCERR_ARG_EXCESS, line, *pos,
+			    "%s ... %s", roff_name[tok], buf + *pos);
+			break;
+		}
+		la = *pos;
+		if ( ! man_args(man, line, pos, buf, &p))
+			break;
+		if (man_macro(tok)->flags & MAN_JOIN &&
+		    man->last->type == ROFFT_TEXT)
+			roff_word_append(man, p);
+		else
+			roff_word_alloc(man, line, la, p);
+		free(p);
+	}
+
+	/*
+	 * Append NODE_EOS in case the last snipped argument
+	 * ends with a dot, e.g. `.IR syslog (3).'
+	 */
+
+	if (n != man->last &&
+	    mandoc_eos(man->last->string, strlen(man->last->string)))
+		man->last->flags |= NODE_EOS;
+
+	/*
+	 * If no arguments are specified and this is MAN_ESCOPED (i.e.,
+	 * next-line scoped), then set our mode to indicate that we're
+	 * waiting for terms to load into our context.
+	 */
+
+	if (n == man->last && man_macro(tok)->flags & MAN_ESCOPED) {
+		man->flags |= MAN_ELINE;
+		return;
+	}
+
+	assert(man->last->type != ROFFT_ROOT);
+	man->next = ROFF_NEXT_SIBLING;
+
+	/* Rewind our element scope. */
+
+	for ( ; man->last; man->last = man->last->parent) {
+		man->last->flags |= NODE_VALID;
+		if (man->last == n)
+			break;
+	}
+
+	/* Rewind next-line scoped ancestors, if any. */
+
+	if (man_macro(tok)->flags & MAN_ESCOPED)
+		man_descope(man, line, ppos, NULL);
+}
+
+void
+man_endparse(struct roff_man *man)
+{
+	man_unscope(man, man->meta.first);
+}
+
+static int
+man_args(struct roff_man *man, int line, int *pos, char *buf, char **v)
+{
+	char	 *start;
+
+	assert(*pos);
+	*v = start = buf + *pos;
+	assert(' ' != *start);
+
+	if ('\0' == *start)
+		return 0;
+
+	*v = roff_getarg(man->roff, v, line, pos);
+	return 1;
+}
diff --git a/usr.bin/mandoc/man_term.c b/usr.bin/mandoc/man_term.c
new file mode 100644
index 0000000..3bf25ad
--- /dev/null
+++ b/usr.bin/mandoc/man_term.c
@@ -0,0 +1,1149 @@
+/* $OpenBSD: man_term.c,v 1.188 2020/03/13 00:31:05 schwarze Exp $ */
+/*
+ * Copyright (c) 2010-2015, 2017-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 */
+
+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] */
+};
+
+#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_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_abort(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_abort, NULL, 0 }, /* LP */
+	{ pre_PP, NULL, 0 }, /* PP */
+	{ pre_abort, 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, 0 }, /* DT */
+	{ pre_ign, NULL, MAN_NOTEXT }, /* UC */
+	{ pre_PD, NULL, MAN_NOTEXT }, /* PD */
+	{ pre_ign, NULL, 0 }, /* 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 */
+};
+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;
+	size_t			 save_defindent;
+
+	p = (struct termp *)arg;
+	save_defindent = p->defindent;
+	if (p->synopsisonly == 0 && p->defindent == 0)
+		p->defindent = 7;
+	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, p->defindent);
+	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);
+	}
+	p->defindent = save_defindent;
+}
+
+/*
+ * 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.
+ */
+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;
+
+	for (i = 0; i < pardist; i++)
+		term_vspace(p);
+}
+
+
+static int
+pre_abort(DECL_ARGS)
+{
+	abort();
+}
+
+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_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;
+	int		 less;
+
+	term_newln(p);
+
+	if (n->child == NULL) {
+		p->tcol->offset = mt->offset;
+		return 0;
+	}
+
+	cp = n->child->string;
+	less = 0;
+
+	if (*cp == '-')
+		less = -1;
+	else if (*cp == '+')
+		less = 1;
+	else
+		cp--;
+
+	if (a2roffsu(++cp, &su, SCALE_EN) == NULL)
+		return 0;
+
+	v = term_hen(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;
+
+	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);
+		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, p->defindent);
+		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;
+
+	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;
+		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);
+		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;
+
+	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;
+		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);
+		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, p->defindent);
+		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:
+		term_fontrepl(p, TERMFONT_BOLD);
+		p->tcol->offset = term_len(p, 3);
+		p->tcol->rmargin = mt->offset;
+		p->trailspace = mt->offset;
+		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, p->defindent);
+		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:
+		term_fontrepl(p, TERMFONT_BOLD);
+		p->tcol->offset = 0;
+		p->tcol->rmargin = mt->offset;
+		p->trailspace = mt->offset;
+		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:
+	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);
+	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, p->defindent);
+	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;
+
+	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;
+
+	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;
+
+	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_vspace(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 we're in a literal context, make sure that words
+	 * together on the same line stay together.  This is a
+	 * POST-printing call, so we check the NEXT word.  Since
+	 * -man doesn't have nested macros, we don't need to be
+	 * more specific than this.
+	 */
+	if (n->flags & NODE_NOFILL &&
+	    ! (p->flags & (TERMP_NOBREAK | TERMP_NONEWLINE)) &&
+	    (n->next == NULL || n->next->flags & NODE_LINE)) {
+		p->flags |= TERMP_BRNEVER | TERMP_NOSPACE;
+		if (n->string != NULL && *n->string != '\0')
+			term_flushln(p);
+		else
+			term_newln(p);
+		p->flags &= ~TERMP_BRNEVER;
+		if (p->tcol->rmargin < p->maxrmargin &&
+		    n->parent->tok == MAN_HP) {
+			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;
+
+	assert(meta->title);
+	assert(meta->msec);
+	assert(meta->date);
+
+	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) {
+		if (meta->hasbody) {
+			term_vspace(p);
+			term_vspace(p);
+		}
+		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);
+
+	/* 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;
+
+	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;
+
+	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;
+
+	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->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;
+
+	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);
+	if ( ! p->mdocstyle) {
+		term_vspace(p);
+		term_vspace(p);
+	}
+	free(title);
+}
diff --git a/usr.bin/mandoc/man_validate.c b/usr.bin/mandoc/man_validate.c
new file mode 100644
index 0000000..49aa390
--- /dev/null
+++ b/usr.bin/mandoc/man_validate.c
@@ -0,0 +1,656 @@
+/* $OpenBSD: man_validate.c,v 1.124 2020/04/24 11:58:02 schwarze Exp $ */
+/*
+ * Copyright (c) 2010, 2012-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "mandoc_aux.h"
+#include "mandoc.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_abort(CHKARGS) __attribute__((__noreturn__));
+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_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_abort,/* LP */
+	check_par,  /* PP */
+	check_abort,/* 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 */
+};
+
+
+/* Validate the subtree rooted at man->last. */
+void
+man_validate(struct roff_man *man)
+{
+	struct roff_node *n;
+	const v_check	 *cp;
+
+	/*
+	 * Translate obsolete macros such that later code
+	 * does not need to look for them.
+	 */
+
+	n = man->last;
+	switch (n->tok) {
+	case MAN_LP:
+	case MAN_P:
+		n->tok = MAN_PP;
+		break;
+	default:
+		break;
+	}
+
+	/*
+	 * Iterate over all children, recursing into each one
+	 * in turn, depth-first.
+	 */
+
+	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)");
+}
+
+static void
+check_abort(CHKARGS)
+{
+	abort();
+}
+
+/*
+ * 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_FONTCW:
+			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_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_WEAK, 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_PP && 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_PP || 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]);
+	check_part(man, n);
+}
+
+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) {
+		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");
+	}
+
+	/* 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_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/usr.bin/mandoc/manconf.h b/usr.bin/mandoc/manconf.h
new file mode 100644
index 0000000..4cc623f
--- /dev/null
+++ b/usr.bin/mandoc/manconf.h
@@ -0,0 +1,56 @@
+/* $OpenBSD: manconf.h,v 1.8 2020/04/02 22:10:27 schwarze Exp $ */
+/*
+ * Copyright (c) 2011,2015,2017,2018,2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ *
+ * Public interface to man(1) configuration management.
+ * For use by the main program and by the formatters.
+ */
+
+/* List of unique, absolute paths to manual trees. */
+
+struct	manpaths {
+	char	**paths;
+	size_t	  sz;
+};
+
+/* Data from -O options and man.conf(5) output directives. */
+
+struct	manoutput {
+	char	 *includes;
+	char	 *man;
+	char	 *paper;
+	char	 *style;
+	char	 *tag;
+	size_t	  indent;
+	size_t	  width;
+	int	  fragment;
+	int	  mdoc;
+	int	  noval;
+	int	  synopsisonly;
+	int	  tag_found;
+	int	  toc;
+};
+
+struct	manconf {
+	struct manoutput	  output;
+	struct manpaths		  manpath;
+};
+
+
+void	 manconf_parse(struct manconf *, const char *, char *, char *);
+int	 manconf_output(struct manoutput *, const char *, int);
+void	 manconf_free(struct manconf *);
+void	 manpath_base(struct manpaths *);
diff --git a/usr.bin/mandoc/mandoc.1 b/usr.bin/mandoc/mandoc.1
new file mode 100644
index 0000000..a5ea382
--- /dev/null
+++ b/usr.bin/mandoc/mandoc.1
@@ -0,0 +1,2336 @@
+.\" $OpenBSD: mandoc.1,v 1.167 2020/04/24 11:58:02 schwarze Exp $
+.\"
+.\" Copyright (c) 2012, 2014-2020 Ingo Schwarze <schwarze@openbsd.org>
+.\" Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+.\"
+.\" 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: April 24 2020 $
+.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 more 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 more 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 for
+.Xr mdoc 7
+and seven for
+.Xr man 7 .
+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.
+Specifically, this suppresses the two additional blank lines near the
+top and the bottom of each page, and it implies
+.Fl O Cm indent Ns =5 .
+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.
+Default styles use only CSS1.
+Equations rendered from
+.Xr eqn 7
+blocks use MathML.
+.Pp
+The file
+.Pa /usr/share/misc/mandoc.css
+documents style-sheet classes available for customising output.
+If a style-sheet is not specified with
+.Fl O Cm style ,
+.Fl T Cm html
+defaults to simple output (via an embedded style-sheet)
+readable in any graphical or text-based web
+browser.
+.Pp
+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 <!DOCTYPE> declaration and the <html>, <head>, and <body>
+elements and only emit the subtree below the <body> 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 for an external style-sheet.
+This must be a valid absolute or
+relative URI.
+.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, expanding any
+.Xr roff 7
+.Ic so
+requests.
+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 http://daringfireball.net/projects/markdown/syntax.text\
+ "John Gruber's 2004 specification" .
+The output also almost conforms to the
+.Lk http://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 more 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 more 1
+.Fl s
+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 -l mandoc.1 man.1 apropos.1 makewhatis.8
+.Pp
+To produce HTML manuals with
+.Pa /usr/share/misc/mandoc.css
+as the style-sheet:
+.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 arguments
+.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
+.Ar 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.
+Please use your good 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.
+.It Sy "referenced manual not found"
+.Pq mdoc
+An
+.Ic \&Xr
+macro references a manual page that is not found in the base system.
+The path to look for base system manuals is configurable at compile
+time and defaults to
+.Pa /usr/share/man : /usr/X11R6/man .
+.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-dd-mm .
+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 "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 "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
+A
+.Ic \&Dt
+macro has no arguments, or there is no
+.Ic \&Dt
+macro before the first non-prologue macro.
+.It Sy "missing manual title, using \(dq\(dq"
+.Pq man
+There is no
+.Ic \&TH
+macro, or it has no arguments.
+.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
+An
+.Ic \&Xr
+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 ,
+.Ic \&MT ,
+.Ic \&RS ,
+or
+.Ic \&UR
+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
+An
+.Ic \&Xr
+macro lacks its second, section number argument.
+The first argument, i.e. the name, is printed, but without subsequent
+parentheses.
+.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 "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"
+.Pq roff
+An escape sequence has an invalid opening argument delimiter, lacks the
+closing argument delimiter, the argument is of an invalid form, or it is
+a character escape sequence with an invalid name.
+If the argument is incomplete,
+.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 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 "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 "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 "missing manual name, using \(dq\(dq"
+.Pq mdoc
+The first call to
+.Ic \&Nm ,
+or any call in the NAME section, lacks the required 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
+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 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.
+.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.
+.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/usr.bin/mandoc/mandoc.c b/usr.bin/mandoc/mandoc.c
new file mode 100644
index 0000000..0b2d301
--- /dev/null
+++ b/usr.bin/mandoc/mandoc.c
@@ -0,0 +1,657 @@
+/*	$OpenBSD: mandoc.c,v 1.85 2020/01/19 16:16:32 schwarze Exp $ */
+/*
+ * Copyright (c) 2008-2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2011-2015, 2017-2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#include "mandoc_aux.h"
+#include "mandoc.h"
+#include "roff.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+
+static	int	 a2time(time_t *, const char *, const char *);
+static	char	*time2a(time_t);
+
+
+enum mandoc_esc
+mandoc_font(const char *cp, int sz)
+{
+	switch (sz) {
+	case 0:
+		return ESCAPE_FONTPREV;
+	case 1:
+		switch (cp[0]) {
+		case 'B':
+		case '3':
+			return ESCAPE_FONTBOLD;
+		case 'I':
+		case '2':
+			return ESCAPE_FONTITALIC;
+		case 'P':
+			return ESCAPE_FONTPREV;
+		case 'R':
+		case '1':
+			return ESCAPE_FONTROMAN;
+		case '4':
+			return ESCAPE_FONTBI;
+		default:
+			return ESCAPE_ERROR;
+		}
+	case 2:
+		switch (cp[0]) {
+		case 'B':
+			switch (cp[1]) {
+			case 'I':
+				return ESCAPE_FONTBI;
+			default:
+				return ESCAPE_ERROR;
+			}
+		case 'C':
+			switch (cp[1]) {
+			case 'B':
+				return ESCAPE_FONTBOLD;
+			case 'I':
+				return ESCAPE_FONTITALIC;
+			case 'R':
+			case 'W':
+				return ESCAPE_FONTCW;
+			default:
+				return ESCAPE_ERROR;
+			}
+		default:
+			return ESCAPE_ERROR;
+		}
+	default:
+		return ESCAPE_ERROR;
+	}
+}
+
+enum mandoc_esc
+mandoc_escape(const char **end, const char **start, int *sz)
+{
+	const char	*local_start;
+	int		 local_sz, c, i;
+	char		 term;
+	enum mandoc_esc	 gly;
+
+	/*
+	 * When the caller doesn't provide return storage,
+	 * use local storage.
+	 */
+
+	if (NULL == start)
+		start = &local_start;
+	if (NULL == sz)
+		sz = &local_sz;
+
+	/*
+	 * Treat "\E" just like "\";
+	 * it only makes a difference in copy mode.
+	 */
+
+	if (**end == 'E')
+		++*end;
+
+	/*
+	 * Beyond the backslash, at least one input character
+	 * is part of the escape sequence.  With one exception
+	 * (see below), that character won't be returned.
+	 */
+
+	gly = ESCAPE_ERROR;
+	*start = ++*end;
+	*sz = 0;
+	term = '\0';
+
+	switch ((*start)[-1]) {
+	/*
+	 * First the glyphs.  There are several different forms of
+	 * these, but each eventually returns a substring of the glyph
+	 * name.
+	 */
+	case '(':
+		gly = ESCAPE_SPECIAL;
+		*sz = 2;
+		break;
+	case '[':
+		if (**start == ' ') {
+			++*end;
+			return ESCAPE_ERROR;
+		}
+		gly = ESCAPE_SPECIAL;
+		term = ']';
+		break;
+	case 'C':
+		if ('\'' != **start)
+			return ESCAPE_ERROR;
+		*start = ++*end;
+		gly = ESCAPE_SPECIAL;
+		term = '\'';
+		break;
+
+	/*
+	 * Escapes taking no arguments at all.
+	 */
+	case '!':
+	case '?':
+		return ESCAPE_UNSUPP;
+	case '%':
+	case '&':
+	case ')':
+	case ',':
+	case '/':
+	case '^':
+	case 'a':
+	case 'd':
+	case 'r':
+	case 't':
+	case 'u':
+	case '{':
+	case '|':
+	case '}':
+		return ESCAPE_IGNORE;
+	case 'c':
+		return ESCAPE_NOSPACE;
+	case 'p':
+		return ESCAPE_BREAK;
+
+	/*
+	 * The \z escape is supposed to output the following
+	 * character without advancing the cursor position.
+	 * Since we are mostly dealing with terminal mode,
+	 * let us just skip the next character.
+	 */
+	case 'z':
+		return ESCAPE_SKIPCHAR;
+
+	/*
+	 * Handle all triggers matching \X(xy, \Xx, and \X[xxxx], where
+	 * 'X' is the trigger.  These have opaque sub-strings.
+	 */
+	case 'F':
+	case 'f':
+	case 'g':
+	case 'k':
+	case 'M':
+	case 'm':
+	case 'n':
+	case 'O':
+	case 'V':
+	case 'Y':
+		gly = (*start)[-1] == 'f' ? ESCAPE_FONT : ESCAPE_IGNORE;
+		switch (**start) {
+		case '(':
+			if ((*start)[-1] == 'O')
+				gly = ESCAPE_ERROR;
+			*start = ++*end;
+			*sz = 2;
+			break;
+		case '[':
+			if ((*start)[-1] == 'O')
+				gly = (*start)[1] == '5' ?
+				    ESCAPE_UNSUPP : ESCAPE_ERROR;
+			*start = ++*end;
+			term = ']';
+			break;
+		default:
+			if ((*start)[-1] == 'O') {
+				switch (**start) {
+				case '0':
+					gly = ESCAPE_UNSUPP;
+					break;
+				case '1':
+				case '2':
+				case '3':
+				case '4':
+					break;
+				default:
+					gly = ESCAPE_ERROR;
+					break;
+				}
+			}
+			*sz = 1;
+			break;
+		}
+		break;
+	case '*':
+		if (strncmp(*start, "(.T", 3) != 0)
+			abort();
+		gly = ESCAPE_DEVICE;
+		*start = ++*end;
+		*sz = 2;
+		break;
+
+	/*
+	 * These escapes are of the form \X'Y', where 'X' is the trigger
+	 * and 'Y' is any string.  These have opaque sub-strings.
+	 * The \B and \w escapes are handled in roff.c, roff_res().
+	 */
+	case 'A':
+	case 'b':
+	case 'D':
+	case 'R':
+	case 'X':
+	case 'Z':
+		gly = ESCAPE_IGNORE;
+		/* FALLTHROUGH */
+	case 'o':
+		if (**start == '\0')
+			return ESCAPE_ERROR;
+		if (gly == ESCAPE_ERROR)
+			gly = ESCAPE_OVERSTRIKE;
+		term = **start;
+		*start = ++*end;
+		break;
+
+	/*
+	 * These escapes are of the form \X'N', where 'X' is the trigger
+	 * and 'N' resolves to a numerical expression.
+	 */
+	case 'h':
+	case 'H':
+	case 'L':
+	case 'l':
+	case 'S':
+	case 'v':
+	case 'x':
+		if (strchr(" %&()*+-./0123456789:<=>", **start)) {
+			if ('\0' != **start)
+				++*end;
+			return ESCAPE_ERROR;
+		}
+		switch ((*start)[-1]) {
+		case 'h':
+			gly = ESCAPE_HORIZ;
+			break;
+		case 'l':
+			gly = ESCAPE_HLINE;
+			break;
+		default:
+			gly = ESCAPE_IGNORE;
+			break;
+		}
+		term = **start;
+		*start = ++*end;
+		break;
+
+	/*
+	 * Special handling for the numbered character escape.
+	 * XXX Do any other escapes need similar handling?
+	 */
+	case 'N':
+		if ('\0' == **start)
+			return ESCAPE_ERROR;
+		(*end)++;
+		if (isdigit((unsigned char)**start)) {
+			*sz = 1;
+			return ESCAPE_IGNORE;
+		}
+		(*start)++;
+		while (isdigit((unsigned char)**end))
+			(*end)++;
+		*sz = *end - *start;
+		if ('\0' != **end)
+			(*end)++;
+		return ESCAPE_NUMBERED;
+
+	/*
+	 * Sizes get a special category of their own.
+	 */
+	case 's':
+		gly = ESCAPE_IGNORE;
+
+		/* See +/- counts as a sign. */
+		if ('+' == **end || '-' == **end || ASCII_HYPH == **end)
+			*start = ++*end;
+
+		switch (**end) {
+		case '(':
+			*start = ++*end;
+			*sz = 2;
+			break;
+		case '[':
+			*start = ++*end;
+			term = ']';
+			break;
+		case '\'':
+			*start = ++*end;
+			term = '\'';
+			break;
+		case '3':
+		case '2':
+		case '1':
+			*sz = (*end)[-1] == 's' &&
+			    isdigit((unsigned char)(*end)[1]) ? 2 : 1;
+			break;
+		default:
+			*sz = 1;
+			break;
+		}
+
+		break;
+
+	/*
+	 * Several special characters can be encoded as
+	 * one-byte escape sequences without using \[].
+	 */
+	case ' ':
+	case '\'':
+	case '-':
+	case '.':
+	case '0':
+	case ':':
+	case '_':
+	case '`':
+	case 'e':
+	case '~':
+		gly = ESCAPE_SPECIAL;
+		/* FALLTHROUGH */
+	default:
+		if (gly == ESCAPE_ERROR)
+			gly = ESCAPE_UNDEF;
+		*start = --*end;
+		*sz = 1;
+		break;
+	}
+
+	/*
+	 * Read up to the terminating character,
+	 * paying attention to nested escapes.
+	 */
+
+	if ('\0' != term) {
+		while (**end != term) {
+			switch (**end) {
+			case '\0':
+				return ESCAPE_ERROR;
+			case '\\':
+				(*end)++;
+				if (ESCAPE_ERROR ==
+				    mandoc_escape(end, NULL, NULL))
+					return ESCAPE_ERROR;
+				break;
+			default:
+				(*end)++;
+				break;
+			}
+		}
+		*sz = (*end)++ - *start;
+
+		/*
+		 * The file chars.c only provides one common list
+		 * of character names, but \[-] == \- is the only
+		 * one of the characters with one-byte names that
+		 * allows enclosing the name in brackets.
+		 */
+		if (gly == ESCAPE_SPECIAL && *sz == 1 && **start != '-')
+			return ESCAPE_ERROR;
+	} else {
+		assert(*sz > 0);
+		if ((size_t)*sz > strlen(*start))
+			return ESCAPE_ERROR;
+		*end += *sz;
+	}
+
+	/* Run post-processors. */
+
+	switch (gly) {
+	case ESCAPE_FONT:
+		gly = mandoc_font(*start, *sz);
+		break;
+	case ESCAPE_SPECIAL:
+		if (**start == 'c') {
+			if (*sz < 6 || *sz > 7 ||
+			    strncmp(*start, "char", 4) != 0 ||
+			    (int)strspn(*start + 4, "0123456789") + 4 < *sz)
+				break;
+			c = 0;
+			for (i = 4; i < *sz; i++)
+				c = 10 * c + ((*start)[i] - '0');
+			if (c < 0x21 || (c > 0x7e && c < 0xa0) || c > 0xff)
+				break;
+			*start += 4;
+			*sz -= 4;
+			gly = ESCAPE_NUMBERED;
+			break;
+		}
+
+		/*
+		 * Unicode escapes are defined in groff as \[u0000]
+		 * to \[u10FFFF], where the contained value must be
+		 * a valid Unicode codepoint.  Here, however, only
+		 * check the length and range.
+		 */
+		if (**start != 'u' || *sz < 5 || *sz > 7)
+			break;
+		if (*sz == 7 && ((*start)[1] != '1' || (*start)[2] != '0'))
+			break;
+		if (*sz == 6 && (*start)[1] == '0')
+			break;
+		if (*sz == 5 && (*start)[1] == 'D' &&
+		    strchr("89ABCDEF", (*start)[2]) != NULL)
+			break;
+		if ((int)strspn(*start + 1, "0123456789ABCDEFabcdef")
+		    + 1 == *sz)
+			gly = ESCAPE_UNICODE;
+		break;
+	default:
+		break;
+	}
+
+	return gly;
+}
+
+static int
+a2time(time_t *t, const char *fmt, const char *p)
+{
+	struct tm	 tm;
+	char		*pp;
+
+	memset(&tm, 0, sizeof(struct tm));
+
+	pp = strptime(p, fmt, &tm);
+	if (NULL != pp && '\0' == *pp) {
+		*t = mktime(&tm);
+		return 1;
+	}
+
+	return 0;
+}
+
+static char *
+time2a(time_t t)
+{
+	struct tm	*tm;
+	char		*buf, *p;
+	size_t		 ssz;
+	int		 isz;
+
+	buf = NULL;
+	tm = localtime(&t);
+	if (tm == NULL)
+		goto fail;
+
+	/*
+	 * Reserve space:
+	 * up to 9 characters for the month (September) + blank
+	 * up to 2 characters for the day + comma + blank
+	 * 4 characters for the year and a terminating '\0'
+	 */
+
+	p = buf = mandoc_malloc(10 + 4 + 4 + 1);
+
+	if ((ssz = strftime(p, 10 + 1, "%B ", tm)) == 0)
+		goto fail;
+	p += (int)ssz;
+
+	/*
+	 * The output format is just "%d" here, not "%2d" or "%02d".
+	 * That's also the reason why we can't just format the
+	 * date as a whole with "%B %e, %Y" or "%B %d, %Y".
+	 * Besides, the present approach is less prone to buffer
+	 * overflows, in case anybody should ever introduce the bug
+	 * of looking at LC_TIME.
+	 */
+
+	isz = snprintf(p, 4 + 1, "%d, ", tm->tm_mday);
+	if (isz < 0 || isz > 4)
+		goto fail;
+	p += isz;
+
+	if (strftime(p, 4 + 1, "%Y", tm) == 0)
+		goto fail;
+	return buf;
+
+fail:
+	free(buf);
+	return mandoc_strdup("");
+}
+
+char *
+mandoc_normdate(struct roff_node *nch, struct roff_node *nbl)
+{
+	char		*cp;
+	time_t		 t;
+
+	/* No date specified. */
+
+	if (nch == NULL) {
+		if (nbl == NULL)
+			mandoc_msg(MANDOCERR_DATE_MISSING, 0, 0, NULL);
+		else
+			mandoc_msg(MANDOCERR_DATE_MISSING, nbl->line,
+			    nbl->pos, "%s", roff_name[nbl->tok]);
+		return mandoc_strdup("");
+	}
+	if (*nch->string == '\0') {
+		mandoc_msg(MANDOCERR_DATE_MISSING, nch->line,
+		    nch->pos, "%s", roff_name[nbl->tok]);
+		return mandoc_strdup("");
+	}
+	if (strcmp(nch->string, "$" "Mdocdate$") == 0)
+		return time2a(time(NULL));
+
+	/* Valid mdoc(7) date format. */
+
+	if (a2time(&t, "$" "Mdocdate: %b %d %Y $", nch->string) ||
+	    a2time(&t, "%b %d, %Y", nch->string)) {
+		cp = time2a(t);
+		if (t > time(NULL) + 86400)
+			mandoc_msg(MANDOCERR_DATE_FUTURE, nch->line,
+			    nch->pos, "%s %s", roff_name[nbl->tok], cp);
+		else if (*nch->string != '$' &&
+		    strcmp(nch->string, cp) != 0)
+			mandoc_msg(MANDOCERR_DATE_NORM, nch->line,
+			    nch->pos, "%s %s", roff_name[nbl->tok], cp);
+		return cp;
+	}
+
+	/* In man(7), do not warn about the legacy format. */
+
+	if (a2time(&t, "%Y-%m-%d", nch->string) == 0)
+		mandoc_msg(MANDOCERR_DATE_BAD, nch->line, nch->pos,
+		    "%s %s", roff_name[nbl->tok], nch->string);
+	else if (t > time(NULL) + 86400)
+		mandoc_msg(MANDOCERR_DATE_FUTURE, nch->line, nch->pos,
+		    "%s %s", roff_name[nbl->tok], nch->string);
+	else if (nbl->tok == MDOC_Dd)
+		mandoc_msg(MANDOCERR_DATE_LEGACY, nch->line, nch->pos,
+		    "Dd %s", nch->string);
+
+	/* Use any non-mdoc(7) date verbatim. */
+
+	return mandoc_strdup(nch->string);
+}
+
+int
+mandoc_eos(const char *p, size_t sz)
+{
+	const char	*q;
+	int		 enclosed, found;
+
+	if (0 == sz)
+		return 0;
+
+	/*
+	 * End-of-sentence recognition must include situations where
+	 * some symbols, such as `)', allow prior EOS punctuation to
+	 * propagate outward.
+	 */
+
+	enclosed = found = 0;
+	for (q = p + (int)sz - 1; q >= p; q--) {
+		switch (*q) {
+		case '\"':
+		case '\'':
+		case ']':
+		case ')':
+			if (0 == found)
+				enclosed = 1;
+			break;
+		case '.':
+		case '!':
+		case '?':
+			found = 1;
+			break;
+		default:
+			return found &&
+			    (!enclosed || isalnum((unsigned char)*q));
+		}
+	}
+
+	return found && !enclosed;
+}
+
+/*
+ * Convert a string to a long that may not be <0.
+ * If the string is invalid, or is less than 0, return -1.
+ */
+int
+mandoc_strntoi(const char *p, size_t sz, int base)
+{
+	char		 buf[32];
+	char		*ep;
+	long		 v;
+
+	if (sz > 31)
+		return -1;
+
+	memcpy(buf, p, sz);
+	buf[(int)sz] = '\0';
+
+	errno = 0;
+	v = strtol(buf, &ep, base);
+
+	if (buf[0] == '\0' || *ep != '\0')
+		return -1;
+
+	if (v > INT_MAX)
+		v = INT_MAX;
+	if (v < INT_MIN)
+		v = INT_MIN;
+
+	return (int)v;
+}
diff --git a/usr.bin/mandoc/mandoc.css b/usr.bin/mandoc/mandoc.css
new file mode 100644
index 0000000..3c4ea18
--- /dev/null
+++ b/usr.bin/mandoc/mandoc.css
@@ -0,0 +1,360 @@
+/* $OpenBSD: mandoc.css,v 1.33 2019/06/02 16:50:46 schwarze Exp $ */
+/*
+ * Standard style sheet for mandoc(1) -Thtml and man.cgi(8).
+ *
+ * Written by Ingo Schwarze <schwarze@openbsd.org>.
+ * 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 {		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; }
+
+.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. */
+
+table.head {	width: 100%;
+		border-bottom: 1px dotted #808080;
+		margin-bottom: 1em;
+		font-size: smaller; }
+td.head-vol {	text-align: center; }
+td.head-rtitle {
+		text-align: right; }
+
+table.foot {	width: 100%;
+		border-top: 1px dotted #808080;
+		margin-top: 1em;
+		font-size: smaller; }
+td.foot-os {	text-align: right; }
+
+/* Sections and paragraphs. */
+
+.manual-text {
+		margin-left: 3.8em; }
+.Nd { }
+section.Sh { }
+h1.Sh {		margin-top: 1.2em;
+		margin-bottom: 0.6em;
+		margin-left: -3.2em; }
+section.Ss { }
+h2.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;
+		width: 100%;
+		margin-top: 0em;
+		margin-left: 0em;
+		margin-bottom: 0.6em;
+		vertical-align: top;
+		overflow: auto; }
+.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 {		text-decoration: underline; }
+.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; }
+.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; }
+.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. */
+
+h1.Sh, h2.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-block;
+		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"; }
+h1.Sh::before {	content: "Sh"; }
+h2.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,
+h1.Sh::before, h2.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, h1.Sh:hover::before, h2.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) {
+.manual-text {
+		margin-left: 0.5em; }
+h1.Sh, h2.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/usr.bin/mandoc/mandoc.h b/usr.bin/mandoc/mandoc.h
new file mode 100644
index 0000000..3a48c89
--- /dev/null
+++ b/usr.bin/mandoc/mandoc.h
@@ -0,0 +1,322 @@
+/* $OpenBSD: mandoc.h,v 1.210 2020/04/24 11:58:02 schwarze Exp $ */
+/*
+ * Copyright (c) 2012-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2010, 2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ *
+ * Error handling, escape sequence, and character utilities.
+ * Can be used by all code in the mandoc package.
+ */
+
+#define ASCII_NBRSP	 31  /* non-breaking space */
+#define	ASCII_HYPH	 30  /* breakable hyphen */
+#define	ASCII_BREAK	 29  /* breakable zero-width space */
+
+/*
+ * Status level.  This refers to both internal status (i.e., whilst
+ * running, when warnings/errors are reported) and an indicator of a
+ * threshold of when to halt (when said internal state exceeds the
+ * threshold).
+ */
+enum	mandoclevel {
+	MANDOCLEVEL_OK = 0,
+	MANDOCLEVEL_STYLE, /* style suggestions */
+	MANDOCLEVEL_WARNING, /* warnings: syntax, whitespace, etc. */
+	MANDOCLEVEL_ERROR, /* input has been thrown away */
+	MANDOCLEVEL_UNSUPP, /* input needs unimplemented features */
+	MANDOCLEVEL_BADARG, /* bad argument in invocation */
+	MANDOCLEVEL_SYSERR, /* system error */
+	MANDOCLEVEL_MAX
+};
+
+/*
+ * All possible things that can go wrong within a parse, be it libroff,
+ * libmdoc, or libman.
+ */
+enum	mandocerr {
+	MANDOCERR_OK,
+
+	MANDOCERR_BASE, /* ===== start of base system conventions ===== */
+
+	MANDOCERR_MDOCDATE, /* Mdocdate found: Dd ... */
+	MANDOCERR_MDOCDATE_MISSING, /* Mdocdate missing: Dd ... */
+	MANDOCERR_ARCH_BAD,  /* unknown architecture: Dt ... arch */
+	MANDOCERR_OS_ARG,  /* operating system explicitly specified: Os ... */
+	MANDOCERR_RCS_MISSING, /* RCS id missing */
+	MANDOCERR_XR_BAD,  /* referenced manual not found: Xr name sec */
+
+	MANDOCERR_STYLE, /* ===== start of style suggestions ===== */
+
+	MANDOCERR_DATE_LEGACY, /* legacy man(7) date format: Dd ... */
+	MANDOCERR_DATE_NORM, /* normalizing date format to: ... */
+	MANDOCERR_TITLE_CASE, /* lower case character in document title */
+	MANDOCERR_RCS_REP, /* duplicate RCS id: ... */
+	MANDOCERR_SEC_TYPO,  /* possible typo in section name: Sh ... */
+	MANDOCERR_ARG_QUOTE, /* unterminated quoted argument */
+	MANDOCERR_MACRO_USELESS, /* useless macro: macro */
+	MANDOCERR_BX, /* consider using OS macro: macro */
+	MANDOCERR_ER_ORDER, /* errnos out of order: Er ... */
+	MANDOCERR_ER_REP, /* duplicate errno: Er ... */
+	MANDOCERR_DELIM, /* trailing delimiter: macro ... */
+	MANDOCERR_DELIM_NB, /* no blank before trailing delimiter: macro ... */
+	MANDOCERR_FI_SKIP, /* fill mode already enabled, skipping: fi */
+	MANDOCERR_NF_SKIP, /* fill mode already disabled, skipping: nf */
+	MANDOCERR_DASHDASH, /* verbatim "--", maybe consider using \(em */
+	MANDOCERR_FUNC, /* function name without markup: name() */
+	MANDOCERR_SPACE_EOL, /* whitespace at end of input line */
+	MANDOCERR_COMMENT_BAD, /* bad comment style */
+
+	MANDOCERR_WARNING, /* ===== start of warnings ===== */
+
+	/* related to the prologue */
+	MANDOCERR_DT_NOTITLE, /* missing manual title, using UNTITLED: line */
+	MANDOCERR_TH_NOTITLE, /* missing manual title, using "": [macro] */
+	MANDOCERR_MSEC_MISSING, /* missing manual section, using "": macro */
+	MANDOCERR_MSEC_BAD, /* unknown manual section: Dt ... section */
+	MANDOCERR_MSEC_FILE, /* filename/section mismatch: ... */
+	MANDOCERR_DATE_MISSING, /* missing date, using "": [macro] */
+	MANDOCERR_DATE_BAD, /* cannot parse date, using it verbatim: date */
+	MANDOCERR_DATE_FUTURE, /* date in the future, using it anyway: date */
+	MANDOCERR_OS_MISSING, /* missing Os macro, using "" */
+	MANDOCERR_PROLOG_LATE, /* late prologue macro: macro */
+	MANDOCERR_PROLOG_ORDER, /* prologue macros out of order: macros */
+
+	/* related to document structure */
+	MANDOCERR_SO, /* .so is fragile, better use ln(1): so path */
+	MANDOCERR_DOC_EMPTY, /* no document body */
+	MANDOCERR_SEC_BEFORE, /* content before first section header: macro */
+	MANDOCERR_NAMESEC_FIRST, /* first section is not NAME: Sh title */
+	MANDOCERR_NAMESEC_NONM, /* NAME section without Nm before Nd */
+	MANDOCERR_NAMESEC_NOND, /* NAME section without description */
+	MANDOCERR_NAMESEC_ND, /* description not at the end of NAME */
+	MANDOCERR_NAMESEC_BAD, /* bad NAME section content: macro */
+	MANDOCERR_NAMESEC_PUNCT, /* missing comma before name: Nm name */
+	MANDOCERR_ND_EMPTY, /* missing description line, using "" */
+	MANDOCERR_ND_LATE, /* description line outside NAME section */
+	MANDOCERR_SEC_ORDER, /* sections out of conventional order: Sh title */
+	MANDOCERR_SEC_REP, /* duplicate section title: Sh title */
+	MANDOCERR_SEC_MSEC, /* unexpected section: Sh title for ... only */
+	MANDOCERR_XR_SELF,  /* cross reference to self: Xr name sec */
+	MANDOCERR_XR_ORDER, /* unusual Xr order: ... after ... */
+	MANDOCERR_XR_PUNCT, /* unusual Xr punctuation: ... after ... */
+	MANDOCERR_AN_MISSING, /* AUTHORS section without An macro */
+
+	/* related to macros and nesting */
+	MANDOCERR_MACRO_OBS, /* obsolete macro: macro */
+	MANDOCERR_MACRO_CALL, /* macro neither callable nor escaped: macro */
+	MANDOCERR_PAR_SKIP, /* skipping paragraph macro: macro ... */
+	MANDOCERR_PAR_MOVE, /* moving paragraph macro out of list: macro */
+	MANDOCERR_NS_SKIP, /* skipping no-space macro */
+	MANDOCERR_BLK_NEST, /* blocks badly nested: macro ... */
+	MANDOCERR_BD_NEST, /* nested displays are not portable: macro ... */
+	MANDOCERR_BL_MOVE, /* moving content out of list: macro */
+	MANDOCERR_TA_LINE, /* first macro on line: Ta */
+	MANDOCERR_BLK_LINE, /* line scope broken: macro breaks macro */
+	MANDOCERR_BLK_BLANK, /* skipping blank line in line scope */
+
+	/* related to missing arguments */
+	MANDOCERR_REQ_EMPTY, /* skipping empty request: request */
+	MANDOCERR_COND_EMPTY, /* conditional request controls empty scope */
+	MANDOCERR_MACRO_EMPTY, /* skipping empty macro: macro */
+	MANDOCERR_BLK_EMPTY, /* empty block: macro */
+	MANDOCERR_ARG_EMPTY, /* empty argument, using 0n: macro arg */
+	MANDOCERR_BD_NOTYPE, /* missing display type, using -ragged: Bd */
+	MANDOCERR_BL_LATETYPE, /* list type is not the first argument: Bl arg */
+	MANDOCERR_BL_NOWIDTH, /* missing -width in -tag list, using 6n */
+	MANDOCERR_EX_NONAME, /* missing utility name, using "": Ex */
+	MANDOCERR_FO_NOHEAD, /* missing function name, using "": Fo */
+	MANDOCERR_IT_NOHEAD, /* empty head in list item: Bl -type It */
+	MANDOCERR_IT_NOBODY, /* empty list item: Bl -type It */
+	MANDOCERR_IT_NOARG, /* missing argument, using next line: Bl -c It */
+	MANDOCERR_BF_NOFONT, /* missing font type, using \fR: Bf */
+	MANDOCERR_BF_BADFONT, /* unknown font type, using \fR: Bf font */
+	MANDOCERR_PF_SKIP, /* nothing follows prefix: Pf arg */
+	MANDOCERR_RS_EMPTY, /* empty reference block: Rs */
+	MANDOCERR_XR_NOSEC, /* missing section argument: Xr arg */
+	MANDOCERR_ARG_STD, /* missing -std argument, adding it: macro */
+	MANDOCERR_OP_EMPTY, /* missing option string, using "": OP */
+	MANDOCERR_UR_NOHEAD, /* missing resource identifier, using "": UR */
+	MANDOCERR_EQN_NOBOX, /* missing eqn box, using "": op */
+
+	/* related to bad arguments */
+	MANDOCERR_ARG_REP, /* duplicate argument: macro arg */
+	MANDOCERR_AN_REP, /* skipping duplicate argument: An -arg */
+	MANDOCERR_BD_REP, /* skipping duplicate display type: Bd -type */
+	MANDOCERR_BL_REP, /* skipping duplicate list type: Bl -type */
+	MANDOCERR_BL_SKIPW, /* skipping -width argument: Bl -type */
+	MANDOCERR_BL_COL, /* wrong number of cells */
+	MANDOCERR_AT_BAD, /* unknown AT&T UNIX version: At version */
+	MANDOCERR_FA_COMMA, /* comma in function argument: arg */
+	MANDOCERR_FN_PAREN, /* parenthesis in function name: arg */
+	MANDOCERR_LB_BAD, /* unknown library name: Lb ... */
+	MANDOCERR_RS_BAD, /* invalid content in Rs block: macro */
+	MANDOCERR_SM_BAD, /* invalid Boolean argument: macro arg */
+	MANDOCERR_CHAR_FONT, /* argument contains two font escapes */
+	MANDOCERR_FT_BAD, /* unknown font, skipping request: ft font */
+	MANDOCERR_TR_ODD, /* odd number of characters in request: tr char */
+
+	/* related to plain text */
+	MANDOCERR_FI_BLANK, /* blank line in fill mode, using .sp */
+	MANDOCERR_FI_TAB, /* tab in filled text */
+	MANDOCERR_EOS, /* new sentence, new line */
+	MANDOCERR_ESC_BAD, /* invalid escape sequence: esc */
+	MANDOCERR_ESC_UNDEF, /* undefined escape, printing literally: char */
+	MANDOCERR_STR_UNDEF, /* undefined string, using "": name */
+
+	/* related to tables */
+	MANDOCERR_TBLLAYOUT_SPAN, /* tbl line starts with span */
+	MANDOCERR_TBLLAYOUT_DOWN, /* tbl column starts with span */
+	MANDOCERR_TBLLAYOUT_VERT, /* skipping vertical bar in tbl layout */
+
+	MANDOCERR_ERROR, /* ===== start of errors ===== */
+
+	/* related to tables */
+	MANDOCERR_TBLOPT_ALPHA, /* non-alphabetic character in tbl options */
+	MANDOCERR_TBLOPT_BAD, /* skipping unknown tbl option: option */
+	MANDOCERR_TBLOPT_NOARG, /* missing tbl option argument: option */
+	MANDOCERR_TBLOPT_ARGSZ, /* wrong tbl option argument size: option */
+	MANDOCERR_TBLLAYOUT_NONE, /* empty tbl layout */
+	MANDOCERR_TBLLAYOUT_CHAR, /* invalid character in tbl layout: char */
+	MANDOCERR_TBLLAYOUT_PAR, /* unmatched parenthesis in tbl layout */
+	MANDOCERR_TBLDATA_NONE, /* tbl without any data cells */
+	MANDOCERR_TBLDATA_SPAN, /* ignoring data in spanned tbl cell: data */
+	MANDOCERR_TBLDATA_EXTRA, /* ignoring extra tbl data cells: data */
+	MANDOCERR_TBLDATA_BLK, /* data block open at end of tbl: macro */
+
+	/* related to document structure and macros */
+	MANDOCERR_PROLOG_REP, /* duplicate prologue macro: macro */
+	MANDOCERR_DT_LATE, /* skipping late title macro: Dt args */
+	MANDOCERR_ROFFLOOP, /* input stack limit exceeded, infinite loop? */
+	MANDOCERR_CHAR_BAD, /* skipping bad character: number */
+	MANDOCERR_MACRO, /* skipping unknown macro: macro */
+	MANDOCERR_REQ_NOMAC, /* skipping request outside macro: ... */
+	MANDOCERR_REQ_INSEC, /* skipping insecure request: request */
+	MANDOCERR_IT_STRAY, /* skipping item outside list: It ... */
+	MANDOCERR_TA_STRAY, /* skipping column outside column list: Ta */
+	MANDOCERR_BLK_NOTOPEN, /* skipping end of block that is not open */
+	MANDOCERR_RE_NOTOPEN, /* fewer RS blocks open, skipping: RE arg */
+	MANDOCERR_BLK_BROKEN, /* inserting missing end of block: macro ... */
+	MANDOCERR_BLK_NOEND, /* appending missing end of block: macro */
+
+	/* related to request and macro arguments */
+	MANDOCERR_NAMESC, /* escaped character not allowed in a name: name */
+	MANDOCERR_ARG_UNDEF, /* using macro argument outside macro */
+	MANDOCERR_ARG_NONUM, /* argument number is not numeric */
+	MANDOCERR_BD_FILE, /* NOT IMPLEMENTED: Bd -file */
+	MANDOCERR_BD_NOARG, /* skipping display without arguments: Bd */
+	MANDOCERR_BL_NOTYPE, /* missing list type, using -item: Bl */
+	MANDOCERR_CE_NONUM, /* argument is not numeric, using 1: ce ... */
+	MANDOCERR_CHAR_ARG, /* argument is not a character: char ... */
+	MANDOCERR_NM_NONAME, /* missing manual name, using "": Nm */
+	MANDOCERR_OS_UNAME, /* uname(3) system call failed, using UNKNOWN */
+	MANDOCERR_ST_BAD, /* unknown standard specifier: St standard */
+	MANDOCERR_IT_NONUM, /* skipping request without numeric argument */
+	MANDOCERR_SHIFT, /* excessive shift: ..., but max is ... */
+	MANDOCERR_SO_PATH, /* NOT IMPLEMENTED: .so with absolute path or ".." */
+	MANDOCERR_SO_FAIL, /* .so request failed */
+	MANDOCERR_TG_SPC, /* skipping tag containing whitespace: tag */
+	MANDOCERR_ARG_SKIP, /* skipping all arguments: macro args */
+	MANDOCERR_ARG_EXCESS, /* skipping excess arguments: macro ... args */
+	MANDOCERR_DIVZERO, /* divide by zero */
+
+	MANDOCERR_UNSUPP, /* ===== start of unsupported features ===== */
+
+	MANDOCERR_TOOLARGE, /* input too large */
+	MANDOCERR_CHAR_UNSUPP, /* unsupported control character: number */
+	MANDOCERR_ESC_UNSUPP, /* unsupported escape sequence: escape */
+	MANDOCERR_REQ_UNSUPP, /* unsupported roff request: request */
+	MANDOCERR_WHILE_NEST, /* nested .while loops */
+	MANDOCERR_WHILE_OUTOF, /* end of scope with open .while loop */
+	MANDOCERR_WHILE_INTO, /* end of .while loop in inner scope */
+	MANDOCERR_WHILE_FAIL, /* cannot continue this .while loop */
+	MANDOCERR_TBLOPT_EQN, /* eqn delim option in tbl: arg */
+	MANDOCERR_TBLLAYOUT_MOD, /* unsupported tbl layout modifier: m */
+	MANDOCERR_TBLMACRO, /* ignoring macro in table: macro */
+
+	MANDOCERR_BADARG, /* ===== start of bad invocations ===== */
+
+	MANDOCERR_BADARG_BAD, /* bad argument */
+	MANDOCERR_BADARG_DUPE, /* duplicate argument */
+	MANDOCERR_BADVAL, /* does not take a value */
+	MANDOCERR_BADVAL_MISS, /* missing argument value */
+	MANDOCERR_BADVAL_BAD, /* bad argument value */
+	MANDOCERR_BADVAL_DUPE, /* duplicate argument value */
+	MANDOCERR_TAG, /* no such tag */
+
+	MANDOCERR_SYSERR, /* ===== start of system errors ===== */
+
+	MANDOCERR_DUP,
+	MANDOCERR_EXEC,
+	MANDOCERR_FDOPEN,
+	MANDOCERR_FFLUSH,
+	MANDOCERR_FORK,
+	MANDOCERR_FSTAT,
+	MANDOCERR_GETLINE,
+	MANDOCERR_GLOB,
+	MANDOCERR_GZCLOSE,
+	MANDOCERR_GZDOPEN,
+	MANDOCERR_MKSTEMP,
+	MANDOCERR_OPEN,
+	MANDOCERR_PLEDGE,
+	MANDOCERR_READ,
+	MANDOCERR_WAIT,
+	MANDOCERR_WRITE,
+
+	MANDOCERR_MAX
+};
+
+enum	mandoc_esc {
+	ESCAPE_ERROR = 0, /* bail! unparsable escape */
+	ESCAPE_UNSUPP, /* unsupported escape; ignore it */
+	ESCAPE_IGNORE, /* escape to be ignored */
+	ESCAPE_UNDEF, /* undefined escape; print literal character */
+	ESCAPE_SPECIAL, /* a regular special character */
+	ESCAPE_FONT, /* a generic font mode */
+	ESCAPE_FONTBOLD, /* bold font mode */
+	ESCAPE_FONTITALIC, /* italic font mode */
+	ESCAPE_FONTBI, /* bold italic font mode */
+	ESCAPE_FONTROMAN, /* roman font mode */
+	ESCAPE_FONTCW, /* constant width font mode */
+	ESCAPE_FONTPREV, /* previous font mode */
+	ESCAPE_NUMBERED, /* a numbered glyph */
+	ESCAPE_UNICODE, /* a unicode codepoint */
+	ESCAPE_DEVICE, /* print the output device name */
+	ESCAPE_BREAK, /* break the output line */
+	ESCAPE_NOSPACE, /* suppress space if the last on a line */
+	ESCAPE_HORIZ, /* horizontal movement */
+	ESCAPE_HLINE, /* horizontal line drawing */
+	ESCAPE_SKIPCHAR, /* skip the next character */
+	ESCAPE_OVERSTRIKE /* overstrike all chars in the argument */
+};
+
+
+enum mandoc_esc	  mandoc_font(const char *, int);
+enum mandoc_esc	  mandoc_escape(const char **, const char **, int *);
+void		  mandoc_msg_setoutfile(FILE *);
+const char	 *mandoc_msg_getinfilename(void);
+void		  mandoc_msg_setinfilename(const char *);
+enum mandocerr	  mandoc_msg_getmin(void);
+void		  mandoc_msg_setmin(enum mandocerr);
+enum mandoclevel  mandoc_msg_getrc(void);
+void		  mandoc_msg_setrc(enum mandoclevel);
+void		  mandoc_msg(enum mandocerr, int, int, const char *, ...)
+			__attribute__((__format__ (__printf__, 4, 5)));
+void		  mandoc_msg_summary(void);
+void		  mchars_alloc(void);
+void		  mchars_free(void);
+int		  mchars_num2char(const char *, size_t);
+const char	 *mchars_uc2str(int);
+int		  mchars_num2uc(const char *, size_t);
+int		  mchars_spec2cp(const char *, size_t);
+const char	 *mchars_spec2str(const char *, size_t, size_t *);
diff --git a/usr.bin/mandoc/mandoc_aux.c b/usr.bin/mandoc/mandoc_aux.c
new file mode 100644
index 0000000..7c23ecf
--- /dev/null
+++ b/usr.bin/mandoc/mandoc_aux.c
@@ -0,0 +1,113 @@
+/*	$OpenBSD: mandoc_aux.c,v 1.9 2018/02/07 20:04:33 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2015, 2017 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <err.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "mandoc.h"
+#include "mandoc_aux.h"
+
+int
+mandoc_asprintf(char **dest, const char *fmt, ...)
+{
+	va_list	 ap;
+	int	 ret;
+
+	va_start(ap, fmt);
+	ret = vasprintf(dest, fmt, ap);
+	va_end(ap);
+
+	if (ret == -1)
+		err((int)MANDOCLEVEL_SYSERR, NULL);
+	return ret;
+}
+
+void *
+mandoc_calloc(size_t num, size_t size)
+{
+	void	*ptr;
+
+	ptr = calloc(num, size);
+	if (ptr == NULL)
+		err((int)MANDOCLEVEL_SYSERR, NULL);
+	return ptr;
+}
+
+void *
+mandoc_malloc(size_t size)
+{
+	void	*ptr;
+
+	ptr = malloc(size);
+	if (ptr == NULL)
+		err((int)MANDOCLEVEL_SYSERR, NULL);
+	return ptr;
+}
+
+void *
+mandoc_realloc(void *ptr, size_t size)
+{
+	ptr = realloc(ptr, size);
+	if (ptr == NULL)
+		err((int)MANDOCLEVEL_SYSERR, NULL);
+	return ptr;
+}
+
+void *
+mandoc_reallocarray(void *ptr, size_t num, size_t size)
+{
+	ptr = reallocarray(ptr, num, size);
+	if (ptr == NULL)
+		err((int)MANDOCLEVEL_SYSERR, NULL);
+	return ptr;
+}
+
+void *
+mandoc_recallocarray(void *ptr, size_t oldnum, size_t num, size_t size)
+{
+	ptr = recallocarray(ptr, oldnum, num, size);
+	if (ptr == NULL)
+		err((int)MANDOCLEVEL_SYSERR, NULL);
+	return ptr;
+}
+
+char *
+mandoc_strdup(const char *ptr)
+{
+	char	*p;
+
+	p = strdup(ptr);
+	if (p == NULL)
+		err((int)MANDOCLEVEL_SYSERR, NULL);
+	return p;
+}
+
+char *
+mandoc_strndup(const char *ptr, size_t sz)
+{
+	char	*p;
+
+	p = strndup(ptr, sz);
+	if (p == NULL)
+		err((int)MANDOCLEVEL_SYSERR, NULL);
+	return p;
+}
diff --git a/usr.bin/mandoc/mandoc_aux.h b/usr.bin/mandoc/mandoc_aux.h
new file mode 100644
index 0000000..f535d85
--- /dev/null
+++ b/usr.bin/mandoc/mandoc_aux.h
@@ -0,0 +1,27 @@
+/*	$OpenBSD: mandoc_aux.h,v 1.9 2017/06/12 18:55:42 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2017 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ */
+
+int		  mandoc_asprintf(char **, const char *, ...)
+			__attribute__((__format__ (__printf__, 2, 3)));
+void		 *mandoc_calloc(size_t, size_t);
+void		 *mandoc_malloc(size_t);
+void		 *mandoc_realloc(void *, size_t);
+void		 *mandoc_reallocarray(void *, size_t, size_t);
+void		 *mandoc_recallocarray(void *, size_t, size_t, size_t);
+char		 *mandoc_strdup(const char *);
+char		 *mandoc_strndup(const char *, size_t);
diff --git a/usr.bin/mandoc/mandoc_msg.c b/usr.bin/mandoc/mandoc_msg.c
new file mode 100644
index 0000000..a9334ae
--- /dev/null
+++ b/usr.bin/mandoc/mandoc_msg.c
@@ -0,0 +1,368 @@
+/* $OpenBSD: mandoc_msg.c,v 1.9 2020/04/24 11:58:02 schwarze Exp $ */
+/*
+ * Copyright (c) 2014-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ *
+ * Implementation of warning and error messages for mandoc(1).
+ */
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "mandoc.h"
+
+static	const enum mandocerr lowest_type[MANDOCLEVEL_MAX] = {
+	MANDOCERR_OK,
+	MANDOCERR_OK,
+	MANDOCERR_WARNING,
+	MANDOCERR_ERROR,
+	MANDOCERR_UNSUPP,
+	MANDOCERR_BADARG,
+	MANDOCERR_SYSERR
+};
+
+static	const char *const level_name[MANDOCLEVEL_MAX] = {
+	"SUCCESS",
+	"STYLE",
+	"WARNING",
+	"ERROR",
+	"UNSUPP",
+	"BADARG",
+	"SYSERR"
+};
+
+static	const char *const type_message[MANDOCERR_MAX] = {
+	"ok",
+
+	"base system convention",
+
+	"Mdocdate found",
+	"Mdocdate missing",
+	"unknown architecture",
+	"operating system explicitly specified",
+	"RCS id missing",
+	"referenced manual not found",
+
+	"generic style suggestion",
+
+	"legacy man(7) date format",
+	"normalizing date format to",
+	"lower case character in document title",
+	"duplicate RCS id",
+	"possible typo in section name",
+	"unterminated quoted argument",
+	"useless macro",
+	"consider using OS macro",
+	"errnos out of order",
+	"duplicate errno",
+	"trailing delimiter",
+	"no blank before trailing delimiter",
+	"fill mode already enabled, skipping",
+	"fill mode already disabled, skipping",
+	"verbatim \"--\", maybe consider using \\(em",
+	"function name without markup",
+	"whitespace at end of input line",
+	"bad comment style",
+
+	"generic warning",
+
+	/* related to the prologue */
+	"missing manual title, using UNTITLED",
+	"missing manual title, using \"\"",
+	"missing manual section, using \"\"",
+	"unknown manual section",
+	"filename/section mismatch",
+	"missing date, using \"\"",
+	"cannot parse date, using it verbatim",
+	"date in the future, using it anyway",
+	"missing Os macro, using \"\"",
+	"late prologue macro",
+	"prologue macros out of order",
+
+	/* related to document structure */
+	".so is fragile, better use ln(1)",
+	"no document body",
+	"content before first section header",
+	"first section is not \"NAME\"",
+	"NAME section without Nm before Nd",
+	"NAME section without description",
+	"description not at the end of NAME",
+	"bad NAME section content",
+	"missing comma before name",
+	"missing description line, using \"\"",
+	"description line outside NAME section",
+	"sections out of conventional order",
+	"duplicate section title",
+	"unexpected section",
+	"cross reference to self",
+	"unusual Xr order",
+	"unusual Xr punctuation",
+	"AUTHORS section without An macro",
+
+	/* related to macros and nesting */
+	"obsolete macro",
+	"macro neither callable nor escaped",
+	"skipping paragraph macro",
+	"moving paragraph macro out of list",
+	"skipping no-space macro",
+	"blocks badly nested",
+	"nested displays are not portable",
+	"moving content out of list",
+	"first macro on line",
+	"line scope broken",
+	"skipping blank line in line scope",
+
+	/* related to missing macro arguments */
+	"skipping empty request",
+	"conditional request controls empty scope",
+	"skipping empty macro",
+	"empty block",
+	"empty argument, using 0n",
+	"missing display type, using -ragged",
+	"list type is not the first argument",
+	"missing -width in -tag list, using 6n",
+	"missing utility name, using \"\"",
+	"missing function name, using \"\"",
+	"empty head in list item",
+	"empty list item",
+	"missing argument, using next line",
+	"missing font type, using \\fR",
+	"unknown font type, using \\fR",
+	"nothing follows prefix",
+	"empty reference block",
+	"missing section argument",
+	"missing -std argument, adding it",
+	"missing option string, using \"\"",
+	"missing resource identifier, using \"\"",
+	"missing eqn box, using \"\"",
+
+	/* related to bad macro arguments */
+	"duplicate argument",
+	"skipping duplicate argument",
+	"skipping duplicate display type",
+	"skipping duplicate list type",
+	"skipping -width argument",
+	"wrong number of cells",
+	"unknown AT&T UNIX version",
+	"comma in function argument",
+	"parenthesis in function name",
+	"unknown library name",
+	"invalid content in Rs block",
+	"invalid Boolean argument",
+	"argument contains two font escapes",
+	"unknown font, skipping request",
+	"odd number of characters in request",
+
+	/* related to plain text */
+	"blank line in fill mode, using .sp",
+	"tab in filled text",
+	"new sentence, new line",
+	"invalid escape sequence",
+	"undefined escape, printing literally",
+	"undefined string, using \"\"",
+
+	/* related to tables */
+	"tbl line starts with span",
+	"tbl column starts with span",
+	"skipping vertical bar in tbl layout",
+
+	"generic error",
+
+	/* related to tables */
+	"non-alphabetic character in tbl options",
+	"skipping unknown tbl option",
+	"missing tbl option argument",
+	"wrong tbl option argument size",
+	"empty tbl layout",
+	"invalid character in tbl layout",
+	"unmatched parenthesis in tbl layout",
+	"tbl without any data cells",
+	"ignoring data in spanned tbl cell",
+	"ignoring extra tbl data cells",
+	"data block open at end of tbl",
+
+	/* related to document structure and macros */
+	"duplicate prologue macro",
+	"skipping late title macro",
+	"input stack limit exceeded, infinite loop?",
+	"skipping bad character",
+	"skipping unknown macro",
+	"ignoring request outside macro",
+	"skipping insecure request",
+	"skipping item outside list",
+	"skipping column outside column list",
+	"skipping end of block that is not open",
+	"fewer RS blocks open, skipping",
+	"inserting missing end of block",
+	"appending missing end of block",
+
+	/* related to request and macro arguments */
+	"escaped character not allowed in a name",
+	"using macro argument outside macro",
+	"argument number is not numeric",
+	"NOT IMPLEMENTED: Bd -file",
+	"skipping display without arguments",
+	"missing list type, using -item",
+	"argument is not numeric, using 1",
+	"argument is not a character",
+	"missing manual name, using \"\"",
+	"uname(3) system call failed, using UNKNOWN",
+	"unknown standard specifier",
+	"skipping request without numeric argument",
+	"excessive shift",
+	"NOT IMPLEMENTED: .so with absolute path or \"..\"",
+	".so request failed",
+	"skipping tag containing whitespace",
+	"skipping all arguments",
+	"skipping excess arguments",
+	"divide by zero",
+
+	"unsupported feature",
+	"input too large",
+	"unsupported control character",
+	"unsupported escape sequence",
+	"unsupported roff request",
+	"nested .while loops",
+	"end of scope with open .while loop",
+	"end of .while loop in inner scope",
+	"cannot continue this .while loop",
+	"eqn delim option in tbl",
+	"unsupported tbl layout modifier",
+	"ignoring macro in table",
+
+	/* bad command line arguments */
+	NULL,
+	"bad command line argument",
+	"duplicate command line argument",
+	"option has a superfluous value",
+	"missing option value",
+	"bad option value",
+	"duplicate option value",
+	"no such tag",
+
+	/* system errors */
+	NULL,
+	"dup",
+	"exec",
+	"fdopen",
+	"fflush",
+	"fork",
+	"fstat",
+	"getline",
+	"glob",
+	"gzclose",
+	"gzdopen",
+	"mkstemp",
+	"open",
+	"pledge",
+	"read",
+	"wait",
+	"write",
+};
+
+static	FILE		*fileptr = NULL;
+static	const char	*filename = NULL;
+static	enum mandocerr	 min_type = MANDOCERR_BADARG;
+static	enum mandoclevel rc = MANDOCLEVEL_OK;
+
+
+void
+mandoc_msg_setoutfile(FILE *fp)
+{
+	fileptr = fp;
+}
+
+const char *
+mandoc_msg_getinfilename(void)
+{
+	return filename;
+}
+
+void
+mandoc_msg_setinfilename(const char *fn)
+{
+	filename = fn;
+}
+
+enum mandocerr
+mandoc_msg_getmin(void)
+{
+	return min_type;
+}
+
+void
+mandoc_msg_setmin(enum mandocerr t)
+{
+	min_type = t;
+}
+
+enum mandoclevel
+mandoc_msg_getrc(void)
+{
+	return rc;
+}
+
+void
+mandoc_msg_setrc(enum mandoclevel level)
+{
+	if (rc < level)
+		rc = level;
+}
+
+void
+mandoc_msg(enum mandocerr t, int line, int col, const char *fmt, ...)
+{
+	va_list			 ap;
+	enum mandoclevel	 level;
+
+	if (t < min_type)
+		return;
+
+	level = MANDOCLEVEL_SYSERR;
+	while (t < lowest_type[level])
+		level--;
+	mandoc_msg_setrc(level);
+
+	if (fileptr == NULL)
+		return;
+
+	fprintf(fileptr, "%s:", getprogname());
+	if (filename != NULL)
+		fprintf(fileptr, " %s:", filename);
+
+	if (line > 0)
+		fprintf(fileptr, "%d:%d:", line, col + 1);
+
+	fprintf(fileptr, " %s", level_name[level]);
+	if (type_message[t] != NULL)
+		fprintf(fileptr, ": %s", type_message[t]);
+
+	if (fmt != NULL) {
+		fprintf(fileptr, ": ");
+		va_start(ap, fmt);
+		vfprintf(fileptr, fmt, ap);
+		va_end(ap);
+	}
+	fputc('\n', fileptr);
+}
+
+void
+mandoc_msg_summary(void)
+{
+	if (fileptr != NULL && rc != MANDOCLEVEL_OK)
+		fprintf(fileptr,
+		    "%s: see above the output for %s messages\n",
+		    getprogname(), level_name[rc]);
+}
diff --git a/usr.bin/mandoc/mandoc_ohash.c b/usr.bin/mandoc/mandoc_ohash.c
new file mode 100644
index 0000000..9557caf
--- /dev/null
+++ b/usr.bin/mandoc/mandoc_ohash.c
@@ -0,0 +1,64 @@
+/*	$OpenBSD: mandoc_ohash.c,v 1.2 2015/10/19 18:58:20 schwarze Exp $ */
+/*
+ * Copyright (c) 2014, 2015 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/cdefs.h>
+#include <sys/types.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "mandoc_aux.h"
+#include "mandoc_ohash.h"
+
+static	void	 *hash_alloc(size_t, void *);
+static	void	 *hash_calloc(size_t, size_t, void *);
+static	void	  hash_free(void *, void *);
+
+
+void
+mandoc_ohash_init(struct ohash *h, unsigned int sz, ptrdiff_t ko)
+{
+	struct ohash_info info;
+
+	info.alloc = hash_alloc;
+	info.calloc = hash_calloc;
+	info.free = hash_free;
+	info.data = NULL;
+	info.key_offset = ko;
+
+	ohash_init(h, sz, &info);
+}
+
+static void *
+hash_alloc(size_t sz, void *arg)
+{
+
+	return mandoc_malloc(sz);
+}
+
+static void *
+hash_calloc(size_t n, size_t sz, void *arg)
+{
+
+	return mandoc_calloc(n, sz);
+}
+
+static void
+hash_free(void *p, void *arg)
+{
+
+	free(p);
+}
diff --git a/usr.bin/mandoc/mandoc_ohash.h b/usr.bin/mandoc/mandoc_ohash.h
new file mode 100644
index 0000000..0ea2d7a
--- /dev/null
+++ b/usr.bin/mandoc/mandoc_ohash.h
@@ -0,0 +1,19 @@
+/*	$OpenBSD: mandoc_ohash.h,v 1.2 2015/11/07 13:57:55 schwarze Exp $ */
+/*
+ * Copyright (c) 2015 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <ohash.h>
+
+void		  mandoc_ohash_init(struct ohash *, unsigned int, ptrdiff_t);
diff --git a/usr.bin/mandoc/mandoc_parse.h b/usr.bin/mandoc/mandoc_parse.h
new file mode 100644
index 0000000..1821c1e
--- /dev/null
+++ b/usr.bin/mandoc/mandoc_parse.h
@@ -0,0 +1,44 @@
+/*	$OpenBSD: mandoc_parse.h,v 1.4 2019/11/09 14:39:42 schwarze Exp $ */
+/*
+ * Copyright (c) 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014,2015,2016,2017,2018 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ *
+ * Top level parser interface.  For use in the main program
+ * and in the main parser, but not in formatters.
+ */
+
+/*
+ * Parse options.
+ */
+#define	MPARSE_MDOC	(1 << 0)  /* assume -mdoc */
+#define	MPARSE_MAN	(1 << 1)  /* assume -man */
+#define	MPARSE_SO	(1 << 2)  /* honour .so requests */
+#define	MPARSE_QUICK	(1 << 3)  /* abort the parse early */
+#define	MPARSE_UTF8	(1 << 4)  /* accept UTF-8 input */
+#define	MPARSE_LATIN1	(1 << 5)  /* accept ISO-LATIN-1 input */
+#define	MPARSE_VALIDATE	(1 << 6)  /* call validation functions */
+#define	MPARSE_COMMENT	(1 << 7)  /* save comments in the tree */
+
+
+struct	roff_meta;
+struct	mparse;
+
+struct mparse	 *mparse_alloc(int, enum mandoc_os, const char *);
+void		  mparse_copy(const struct mparse *);
+void		  mparse_free(struct mparse *);
+int		  mparse_open(struct mparse *, const char *);
+void		  mparse_readfd(struct mparse *, int, const char *);
+void		  mparse_reset(struct mparse *);
+struct roff_meta *mparse_result(struct mparse *);
diff --git a/usr.bin/mandoc/mandoc_xr.c b/usr.bin/mandoc/mandoc_xr.c
new file mode 100644
index 0000000..3f79da3
--- /dev/null
+++ b/usr.bin/mandoc/mandoc_xr.c
@@ -0,0 +1,122 @@
+/*	$OpenBSD: mandoc_xr.c,v 1.3 2017/07/02 21:17:12 schwarze Exp $ */
+/*
+ * Copyright (c) 2017 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/cdefs.h>
+#include <sys/types.h>
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mandoc_aux.h"
+#include "mandoc_ohash.h"
+#include "mandoc_xr.h"
+
+static struct ohash	 *xr_hash = NULL;
+static struct mandoc_xr	 *xr_first = NULL;
+static struct mandoc_xr	 *xr_last = NULL;
+
+static void		  mandoc_xr_clear(void);
+
+
+static void
+mandoc_xr_clear(void)
+{
+	struct mandoc_xr	*xr;
+	unsigned int		 slot;
+
+	if (xr_hash == NULL)
+		return;
+	for (xr = ohash_first(xr_hash, &slot); xr != NULL;
+	     xr = ohash_next(xr_hash, &slot))
+		free(xr);
+	ohash_delete(xr_hash);
+}
+
+void
+mandoc_xr_reset(void)
+{
+	if (xr_hash == NULL)
+		xr_hash = mandoc_malloc(sizeof(*xr_hash));
+	else
+		mandoc_xr_clear();
+	mandoc_ohash_init(xr_hash, 5,
+	    offsetof(struct mandoc_xr, hashkey));
+	xr_first = xr_last = NULL;
+}
+
+int
+mandoc_xr_add(const char *sec, const char *name, int line, int pos)
+{
+	struct mandoc_xr	 *xr, *oxr;
+	const char		 *pend;
+	size_t			  ssz, nsz, tsz;
+	unsigned int		  slot;
+	int			  ret;
+	uint32_t		  hv;
+
+	if (xr_hash == NULL)
+		return 0;
+
+	ssz = strlen(sec) + 1;
+	nsz = strlen(name) + 1;
+	tsz = ssz + nsz;
+	xr = mandoc_malloc(sizeof(*xr) + tsz);
+	xr->next = NULL;
+	xr->sec = xr->hashkey;
+	xr->name = xr->hashkey + ssz;
+	xr->line = line;
+	xr->pos = pos;
+	xr->count = 1;
+	memcpy(xr->sec, sec, ssz);
+	memcpy(xr->name, name, nsz);
+
+	pend = xr->hashkey + tsz;
+	hv = ohash_interval(xr->hashkey, &pend);
+	slot = ohash_lookup_memory(xr_hash, xr->hashkey, tsz, hv);
+	if ((oxr = ohash_find(xr_hash, slot)) == NULL) {
+		ohash_insert(xr_hash, slot, xr);
+		if (xr_first == NULL)
+			xr_first = xr;
+		else
+			xr_last->next = xr;
+		xr_last = xr;
+		return 0;
+	}
+
+	oxr->count++;
+	ret = (oxr->line == -1) ^ (xr->line == -1);
+	if (xr->line == -1)
+		oxr->line = -1;
+	free(xr);
+	return ret;
+}
+
+struct mandoc_xr *
+mandoc_xr_get(void)
+{
+	return xr_first;
+}
+
+void
+mandoc_xr_free(void)
+{
+	mandoc_xr_clear();
+	free(xr_hash);
+	xr_hash = NULL;
+}
diff --git a/usr.bin/mandoc/mandoc_xr.h b/usr.bin/mandoc/mandoc_xr.h
new file mode 100644
index 0000000..708f502
--- /dev/null
+++ b/usr.bin/mandoc/mandoc_xr.h
@@ -0,0 +1,31 @@
+/*	$OpenBSD: mandoc_xr.h,v 1.3 2017/07/02 21:17:12 schwarze Exp $ */
+/*
+ * Copyright (c) 2017 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ */
+
+struct	mandoc_xr {
+	struct mandoc_xr *next;
+	char		 *sec;
+	char		 *name;
+	int		  line;  /* Or -1 for this page's own names. */
+	int		  pos;
+	int		  count;
+	char		  hashkey[];
+};
+
+void		  mandoc_xr_reset(void);
+int		  mandoc_xr_add(const char *, const char *, int, int);
+struct mandoc_xr *mandoc_xr_get(void);
+void		  mandoc_xr_free(void);
diff --git a/usr.bin/mandoc/mandocdb.c b/usr.bin/mandoc/mandocdb.c
new file mode 100644
index 0000000..6e7efae
--- /dev/null
+++ b/usr.bin/mandoc/mandocdb.c
@@ -0,0 +1,2386 @@
+/* $OpenBSD: mandocdb.c,v 1.216 2020/04/03 11:34:19 schwarze Exp $ */
+/*
+ * Copyright (c) 2011-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2011, 2012 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2016 Ed Maste <emaste@freebsd.org>
+ *
+ * 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.
+ *
+ * Implementation of the makewhatis(8) program.
+ */
+#include <sys/types.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fts.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "mandoc_aux.h"
+#include "mandoc_ohash.h"
+#include "mandoc.h"
+#include "roff.h"
+#include "mdoc.h"
+#include "man.h"
+#include "mandoc_parse.h"
+#include "manconf.h"
+#include "mansearch.h"
+#include "dba_array.h"
+#include "dba.h"
+
+extern const char *const mansearch_keynames[];
+
+enum	op {
+	OP_DEFAULT = 0, /* new dbs from dir list or default config */
+	OP_CONFFILE, /* new databases from custom config file */
+	OP_UPDATE, /* delete/add entries in existing database */
+	OP_DELETE, /* delete entries from existing database */
+	OP_TEST /* change no databases, report potential problems */
+};
+
+struct	str {
+	const struct mpage *mpage; /* if set, the owning parse */
+	uint64_t	 mask; /* bitmask in sequence */
+	char		 key[]; /* rendered text */
+};
+
+struct	inodev {
+	ino_t		 st_ino;
+	dev_t		 st_dev;
+};
+
+struct	mpage {
+	struct inodev	 inodev;  /* used for hashing routine */
+	struct dba_array *dba;
+	char		*sec;     /* section from file content */
+	char		*arch;    /* architecture from file content */
+	char		*title;   /* title from file content */
+	char		*desc;    /* description from file content */
+	struct mpage	*next;    /* singly linked list */
+	struct mlink	*mlinks;  /* singly linked list */
+	int		 name_head_done;
+	enum form	 form;    /* format from file content */
+};
+
+struct	mlink {
+	char		 file[PATH_MAX]; /* filename rel. to manpath */
+	char		*dsec;    /* section from directory */
+	char		*arch;    /* architecture from directory */
+	char		*name;    /* name from file name (not empty) */
+	char		*fsec;    /* section from file name suffix */
+	struct mlink	*next;    /* singly linked list */
+	struct mpage	*mpage;   /* parent */
+	int		 gzip;	  /* filename has a .gz suffix */
+	enum form	 dform;   /* format from directory */
+	enum form	 fform;   /* format from file name suffix */
+};
+
+typedef	int (*mdoc_fp)(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+
+struct	mdoc_handler {
+	mdoc_fp		 fp; /* optional handler */
+	uint64_t	 mask;  /* set unless handler returns 0 */
+	int		 taboo;  /* node flags that must not be set */
+};
+
+
+int		 mandocdb(int, char *[]);
+
+static	void	 dbadd(struct dba *, struct mpage *);
+static	void	 dbadd_mlink(const struct mlink *);
+static	void	 dbprune(struct dba *);
+static	void	 dbwrite(struct dba *);
+static	void	 filescan(const char *);
+static	int	 fts_compare(const FTSENT **, const FTSENT **);
+static	void	 mlink_add(struct mlink *, const struct stat *);
+static	void	 mlink_check(struct mpage *, struct mlink *);
+static	void	 mlink_free(struct mlink *);
+static	void	 mlinks_undupe(struct mpage *);
+static	void	 mpages_free(void);
+static	void	 mpages_merge(struct dba *, struct mparse *);
+static	void	 parse_cat(struct mpage *, int);
+static	void	 parse_man(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	void	 parse_mdoc(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	int	 parse_mdoc_head(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	int	 parse_mdoc_Fa(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	int	 parse_mdoc_Fd(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	void	 parse_mdoc_fname(struct mpage *, const struct roff_node *);
+static	int	 parse_mdoc_Fn(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	int	 parse_mdoc_Fo(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	int	 parse_mdoc_Nd(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	int	 parse_mdoc_Nm(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	int	 parse_mdoc_Sh(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	int	 parse_mdoc_Va(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	int	 parse_mdoc_Xr(struct mpage *, const struct roff_meta *,
+			const struct roff_node *);
+static	void	 putkey(const struct mpage *, char *, uint64_t);
+static	void	 putkeys(const struct mpage *, char *, size_t, uint64_t);
+static	void	 putmdockey(const struct mpage *,
+			const struct roff_node *, uint64_t, int);
+static	int	 render_string(char **, size_t *);
+static	void	 say(const char *, const char *, ...)
+			__attribute__((__format__ (__printf__, 2, 3)));
+static	int	 set_basedir(const char *, int);
+static	int	 treescan(void);
+static	size_t	 utf8(unsigned int, char [7]);
+
+static	int		 nodb; /* no database changes */
+static	int		 mparse_options; /* abort the parse early */
+static	int		 use_all; /* use all found files */
+static	int		 debug; /* print what we're doing */
+static	int		 warnings; /* warn about crap */
+static	int		 write_utf8; /* write UTF-8 output; else ASCII */
+static	int		 exitcode; /* to be returned by main */
+static	enum op		 op; /* operational mode */
+static	char		 basedir[PATH_MAX]; /* current base directory */
+static	size_t		 basedir_len; /* strlen(basedir) */
+static	struct mpage	*mpage_head; /* list of distinct manual pages */
+static	struct ohash	 mpages; /* table of distinct manual pages */
+static	struct ohash	 mlinks; /* table of directory entries */
+static	struct ohash	 names; /* table of all names */
+static	struct ohash	 strings; /* table of all strings */
+static	uint64_t	 name_mask;
+
+static	const struct mdoc_handler mdoc_handlers[MDOC_MAX - MDOC_Dd] = {
+	{ NULL, 0, NODE_NOPRT },  /* Dd */
+	{ NULL, 0, NODE_NOPRT },  /* Dt */
+	{ NULL, 0, NODE_NOPRT },  /* Os */
+	{ parse_mdoc_Sh, TYPE_Sh, 0 }, /* Sh */
+	{ parse_mdoc_head, TYPE_Ss, 0 }, /* Ss */
+	{ NULL, 0, 0 },  /* Pp */
+	{ NULL, 0, 0 },  /* D1 */
+	{ NULL, 0, 0 },  /* Dl */
+	{ NULL, 0, 0 },  /* Bd */
+	{ NULL, 0, 0 },  /* Ed */
+	{ NULL, 0, 0 },  /* Bl */
+	{ NULL, 0, 0 },  /* El */
+	{ NULL, 0, 0 },  /* It */
+	{ NULL, 0, 0 },  /* Ad */
+	{ NULL, TYPE_An, 0 },  /* An */
+	{ NULL, 0, 0 },  /* Ap */
+	{ NULL, TYPE_Ar, 0 },  /* Ar */
+	{ NULL, TYPE_Cd, 0 },  /* Cd */
+	{ NULL, TYPE_Cm, 0 },  /* Cm */
+	{ NULL, TYPE_Dv, 0 },  /* Dv */
+	{ NULL, TYPE_Er, 0 },  /* Er */
+	{ NULL, TYPE_Ev, 0 },  /* Ev */
+	{ NULL, 0, 0 },  /* Ex */
+	{ parse_mdoc_Fa, 0, 0 },  /* Fa */
+	{ parse_mdoc_Fd, 0, 0 },  /* Fd */
+	{ NULL, TYPE_Fl, 0 },  /* Fl */
+	{ parse_mdoc_Fn, 0, 0 },  /* Fn */
+	{ NULL, TYPE_Ft | TYPE_Vt, 0 },  /* Ft */
+	{ NULL, TYPE_Ic, 0 },  /* Ic */
+	{ NULL, TYPE_In, 0 },  /* In */
+	{ NULL, TYPE_Li, 0 },  /* Li */
+	{ parse_mdoc_Nd, 0, 0 },  /* Nd */
+	{ parse_mdoc_Nm, 0, 0 },  /* Nm */
+	{ NULL, 0, 0 },  /* Op */
+	{ NULL, 0, 0 },  /* Ot */
+	{ NULL, TYPE_Pa, NODE_NOSRC },  /* Pa */
+	{ NULL, 0, 0 },  /* Rv */
+	{ NULL, TYPE_St, 0 },  /* St */
+	{ parse_mdoc_Va, TYPE_Va, 0 },  /* Va */
+	{ parse_mdoc_Va, TYPE_Vt, 0 },  /* Vt */
+	{ parse_mdoc_Xr, 0, 0 },  /* Xr */
+	{ NULL, 0, 0 },  /* %A */
+	{ NULL, 0, 0 },  /* %B */
+	{ NULL, 0, 0 },  /* %D */
+	{ NULL, 0, 0 },  /* %I */
+	{ NULL, 0, 0 },  /* %J */
+	{ NULL, 0, 0 },  /* %N */
+	{ NULL, 0, 0 },  /* %O */
+	{ NULL, 0, 0 },  /* %P */
+	{ NULL, 0, 0 },  /* %R */
+	{ NULL, 0, 0 },  /* %T */
+	{ NULL, 0, 0 },  /* %V */
+	{ NULL, 0, 0 },  /* Ac */
+	{ NULL, 0, 0 },  /* Ao */
+	{ NULL, 0, 0 },  /* Aq */
+	{ NULL, TYPE_At, 0 },  /* At */
+	{ NULL, 0, 0 },  /* Bc */
+	{ NULL, 0, 0 },  /* Bf */
+	{ NULL, 0, 0 },  /* Bo */
+	{ NULL, 0, 0 },  /* Bq */
+	{ NULL, TYPE_Bsx, NODE_NOSRC },  /* Bsx */
+	{ NULL, TYPE_Bx, NODE_NOSRC },  /* Bx */
+	{ NULL, 0, 0 },  /* Db */
+	{ NULL, 0, 0 },  /* Dc */
+	{ NULL, 0, 0 },  /* Do */
+	{ NULL, 0, 0 },  /* Dq */
+	{ NULL, 0, 0 },  /* Ec */
+	{ NULL, 0, 0 },  /* Ef */
+	{ NULL, TYPE_Em, 0 },  /* Em */
+	{ NULL, 0, 0 },  /* Eo */
+	{ NULL, TYPE_Fx, NODE_NOSRC },  /* Fx */
+	{ NULL, TYPE_Ms, 0 },  /* Ms */
+	{ NULL, 0, 0 },  /* No */
+	{ NULL, 0, 0 },  /* Ns */
+	{ NULL, TYPE_Nx, NODE_NOSRC },  /* Nx */
+	{ NULL, TYPE_Ox, NODE_NOSRC },  /* Ox */
+	{ NULL, 0, 0 },  /* Pc */
+	{ NULL, 0, 0 },  /* Pf */
+	{ NULL, 0, 0 },  /* Po */
+	{ NULL, 0, 0 },  /* Pq */
+	{ NULL, 0, 0 },  /* Qc */
+	{ NULL, 0, 0 },  /* Ql */
+	{ NULL, 0, 0 },  /* Qo */
+	{ NULL, 0, 0 },  /* Qq */
+	{ NULL, 0, 0 },  /* Re */
+	{ NULL, 0, 0 },  /* Rs */
+	{ NULL, 0, 0 },  /* Sc */
+	{ NULL, 0, 0 },  /* So */
+	{ NULL, 0, 0 },  /* Sq */
+	{ NULL, 0, 0 },  /* Sm */
+	{ NULL, 0, 0 },  /* Sx */
+	{ NULL, TYPE_Sy, 0 },  /* Sy */
+	{ NULL, TYPE_Tn, 0 },  /* Tn */
+	{ NULL, 0, NODE_NOSRC },  /* Ux */
+	{ NULL, 0, 0 },  /* Xc */
+	{ NULL, 0, 0 },  /* Xo */
+	{ parse_mdoc_Fo, 0, 0 },  /* Fo */
+	{ NULL, 0, 0 },  /* Fc */
+	{ NULL, 0, 0 },  /* Oo */
+	{ NULL, 0, 0 },  /* Oc */
+	{ NULL, 0, 0 },  /* Bk */
+	{ NULL, 0, 0 },  /* Ek */
+	{ NULL, 0, 0 },  /* Bt */
+	{ NULL, 0, 0 },  /* Hf */
+	{ NULL, 0, 0 },  /* Fr */
+	{ NULL, 0, 0 },  /* Ud */
+	{ NULL, TYPE_Lb, NODE_NOSRC },  /* Lb */
+	{ NULL, 0, 0 },  /* Lp */
+	{ NULL, TYPE_Lk, 0 },  /* Lk */
+	{ NULL, TYPE_Mt, NODE_NOSRC },  /* Mt */
+	{ NULL, 0, 0 },  /* Brq */
+	{ NULL, 0, 0 },  /* Bro */
+	{ NULL, 0, 0 },  /* Brc */
+	{ NULL, 0, 0 },  /* %C */
+	{ NULL, 0, 0 },  /* Es */
+	{ NULL, 0, 0 },  /* En */
+	{ NULL, TYPE_Dx, NODE_NOSRC },  /* Dx */
+	{ NULL, 0, 0 },  /* %Q */
+	{ NULL, 0, 0 },  /* %U */
+	{ NULL, 0, 0 },  /* Ta */
+};
+
+
+int
+mandocdb(int argc, char *argv[])
+{
+	struct manconf	  conf;
+	struct mparse	 *mp;
+	struct dba	 *dba;
+	const char	 *path_arg, *progname;
+	size_t		  j, sz;
+	int		  ch, i;
+
+	if (pledge("stdio rpath wpath cpath", NULL) == -1) {
+		warn("pledge");
+		return (int)MANDOCLEVEL_SYSERR;
+	}
+
+	memset(&conf, 0, sizeof(conf));
+
+	/*
+	 * We accept a few different invocations.
+	 * The CHECKOP macro makes sure that invocation styles don't
+	 * clobber each other.
+	 */
+#define	CHECKOP(_op, _ch) do \
+	if ((_op) != OP_DEFAULT) { \
+		warnx("-%c: Conflicting option", (_ch)); \
+		goto usage; \
+	} while (/*CONSTCOND*/0)
+
+	mparse_options = MPARSE_VALIDATE;
+	path_arg = NULL;
+	op = OP_DEFAULT;
+
+	while ((ch = getopt(argc, argv, "aC:Dd:npQT:tu:v")) != -1)
+		switch (ch) {
+		case 'a':
+			use_all = 1;
+			break;
+		case 'C':
+			CHECKOP(op, ch);
+			path_arg = optarg;
+			op = OP_CONFFILE;
+			break;
+		case 'D':
+			debug++;
+			break;
+		case 'd':
+			CHECKOP(op, ch);
+			path_arg = optarg;
+			op = OP_UPDATE;
+			break;
+		case 'n':
+			nodb = 1;
+			break;
+		case 'p':
+			warnings = 1;
+			break;
+		case 'Q':
+			mparse_options |= MPARSE_QUICK;
+			break;
+		case 'T':
+			if (strcmp(optarg, "utf8") != 0) {
+				warnx("-T%s: Unsupported output format",
+				    optarg);
+				goto usage;
+			}
+			write_utf8 = 1;
+			break;
+		case 't':
+			CHECKOP(op, ch);
+			dup2(STDOUT_FILENO, STDERR_FILENO);
+			op = OP_TEST;
+			nodb = warnings = 1;
+			break;
+		case 'u':
+			CHECKOP(op, ch);
+			path_arg = optarg;
+			op = OP_DELETE;
+			break;
+		case 'v':
+			/* Compatibility with espie@'s makewhatis. */
+			break;
+		default:
+			goto usage;
+		}
+
+	argc -= optind;
+	argv += optind;
+
+	if (nodb) {
+		if (pledge("stdio rpath", NULL) == -1) {
+			warn("pledge");
+			return (int)MANDOCLEVEL_SYSERR;
+		}
+	}
+
+	if (op == OP_CONFFILE && argc > 0) {
+		warnx("-C: Too many arguments");
+		goto usage;
+	}
+
+	exitcode = (int)MANDOCLEVEL_OK;
+	mchars_alloc();
+	mp = mparse_alloc(mparse_options, MANDOC_OS_OTHER, NULL);
+	mandoc_ohash_init(&mpages, 6, offsetof(struct mpage, inodev));
+	mandoc_ohash_init(&mlinks, 6, offsetof(struct mlink, file));
+
+	if (op == OP_UPDATE || op == OP_DELETE || op == OP_TEST) {
+
+		/*
+		 * Most of these deal with a specific directory.
+		 * Jump into that directory first.
+		 */
+		if (op != OP_TEST && set_basedir(path_arg, 1) == 0)
+			goto out;
+
+		dba = nodb ? dba_new(128) : dba_read(MANDOC_DB);
+		if (dba != NULL) {
+			/*
+			 * The existing database is usable.  Process
+			 * all files specified on the command-line.
+			 */
+			use_all = 1;
+			for (i = 0; i < argc; i++)
+				filescan(argv[i]);
+			if (nodb == 0)
+				dbprune(dba);
+		} else {
+			/* Database missing or corrupt. */
+			if (op != OP_UPDATE || errno != ENOENT)
+				say(MANDOC_DB, "%s: Automatically recreating"
+				    " from scratch", strerror(errno));
+			exitcode = (int)MANDOCLEVEL_OK;
+			op = OP_DEFAULT;
+			if (treescan() == 0)
+				goto out;
+			dba = dba_new(128);
+		}
+		if (op != OP_DELETE)
+			mpages_merge(dba, mp);
+		if (nodb == 0)
+			dbwrite(dba);
+		dba_free(dba);
+	} else {
+		/*
+		 * If we have arguments, use them as our manpaths.
+		 * If we don't, use man.conf(5).
+		 */
+		if (argc > 0) {
+			conf.manpath.paths = mandoc_reallocarray(NULL,
+			    argc, sizeof(char *));
+			conf.manpath.sz = (size_t)argc;
+			for (i = 0; i < argc; i++)
+				conf.manpath.paths[i] = mandoc_strdup(argv[i]);
+		} else
+			manconf_parse(&conf, path_arg, NULL, NULL);
+
+		if (conf.manpath.sz == 0) {
+			exitcode = (int)MANDOCLEVEL_BADARG;
+			say("", "Empty manpath");
+		}
+
+		/*
+		 * First scan the tree rooted at a base directory, then
+		 * build a new database and finally move it into place.
+		 * Ignore zero-length directories and strip trailing
+		 * slashes.
+		 */
+		for (j = 0; j < conf.manpath.sz; j++) {
+			sz = strlen(conf.manpath.paths[j]);
+			if (sz && conf.manpath.paths[j][sz - 1] == '/')
+				conf.manpath.paths[j][--sz] = '\0';
+			if (sz == 0)
+				continue;
+
+			if (j) {
+				mandoc_ohash_init(&mpages, 6,
+				    offsetof(struct mpage, inodev));
+				mandoc_ohash_init(&mlinks, 6,
+				    offsetof(struct mlink, file));
+			}
+
+			if (set_basedir(conf.manpath.paths[j], argc > 0) == 0)
+				continue;
+			if (treescan() == 0)
+				continue;
+			dba = dba_new(128);
+			mpages_merge(dba, mp);
+			if (nodb == 0)
+				dbwrite(dba);
+			dba_free(dba);
+
+			if (j + 1 < conf.manpath.sz) {
+				mpages_free();
+				ohash_delete(&mpages);
+				ohash_delete(&mlinks);
+			}
+		}
+	}
+out:
+	manconf_free(&conf);
+	mparse_free(mp);
+	mchars_free();
+	mpages_free();
+	ohash_delete(&mpages);
+	ohash_delete(&mlinks);
+	return exitcode;
+usage:
+	progname = getprogname();
+	fprintf(stderr, "usage: %s [-aDnpQ] [-C file] [-Tutf8]\n"
+			"       %s [-aDnpQ] [-Tutf8] dir ...\n"
+			"       %s [-DnpQ] [-Tutf8] -d dir [file ...]\n"
+			"       %s [-Dnp] -u dir [file ...]\n"
+			"       %s [-Q] -t file ...\n",
+		        progname, progname, progname, progname, progname);
+
+	return (int)MANDOCLEVEL_BADARG;
+}
+
+/*
+ * To get a singly linked list in alpha order while inserting entries
+ * at the beginning, process directory entries in reverse alpha order.
+ */
+static int
+fts_compare(const FTSENT **a, const FTSENT **b)
+{
+	return -strcmp((*a)->fts_name, (*b)->fts_name);
+}
+
+/*
+ * Scan a directory tree rooted at "basedir" for manpages.
+ * We use fts(), scanning directory parts along the way for clues to our
+ * section and architecture.
+ *
+ * If use_all has been specified, grok all files.
+ * If not, sanitise paths to the following:
+ *
+ *   [./]man*[/<arch>]/<name>.<section>
+ *   or
+ *   [./]cat<section>[/<arch>]/<name>.0
+ *
+ * TODO: accommodate for multi-language directories.
+ */
+static int
+treescan(void)
+{
+	char		 buf[PATH_MAX];
+	FTS		*f;
+	FTSENT		*ff;
+	struct mlink	*mlink;
+	int		 gzip;
+	enum form	 dform;
+	char		*dsec, *arch, *fsec, *cp;
+	const char	*path;
+	const char	*argv[2];
+
+	argv[0] = ".";
+	argv[1] = NULL;
+
+	f = fts_open((char * const *)argv, FTS_PHYSICAL | FTS_NOCHDIR,
+	    fts_compare);
+	if (f == NULL) {
+		exitcode = (int)MANDOCLEVEL_SYSERR;
+		say("", "&fts_open");
+		return 0;
+	}
+
+	dsec = arch = NULL;
+	dform = FORM_NONE;
+
+	while ((ff = fts_read(f)) != NULL) {
+		path = ff->fts_path + 2;
+		switch (ff->fts_info) {
+
+		/*
+		 * Symbolic links require various sanity checks,
+		 * then get handled just like regular files.
+		 */
+		case FTS_SL:
+			if (realpath(path, buf) == NULL) {
+				if (warnings)
+					say(path, "&realpath");
+				continue;
+			}
+			if (strncmp(buf, basedir, basedir_len) != 0) {
+				if (warnings) say("",
+				    "%s: outside base directory", buf);
+				continue;
+			}
+			/* Use logical inode to avoid mpages dupe. */
+			if (stat(path, ff->fts_statp) == -1) {
+				if (warnings)
+					say(path, "&stat");
+				continue;
+			}
+			/* FALLTHROUGH */
+
+		/*
+		 * If we're a regular file, add an mlink by using the
+		 * stored directory data and handling the filename.
+		 */
+		case FTS_F:
+			if ( ! strcmp(path, MANDOC_DB))
+				continue;
+			if ( ! use_all && ff->fts_level < 2) {
+				if (warnings)
+					say(path, "Extraneous file");
+				continue;
+			}
+			gzip = 0;
+			fsec = NULL;
+			while (fsec == NULL) {
+				fsec = strrchr(ff->fts_name, '.');
+				if (fsec == NULL || strcmp(fsec+1, "gz"))
+					break;
+				gzip = 1;
+				*fsec = '\0';
+				fsec = NULL;
+			}
+			if (fsec == NULL) {
+				if ( ! use_all) {
+					if (warnings)
+						say(path,
+						    "No filename suffix");
+					continue;
+				}
+			} else if ( ! strcmp(++fsec, "html")) {
+				if (warnings)
+					say(path, "Skip html");
+				continue;
+			} else if ( ! strcmp(fsec, "ps")) {
+				if (warnings)
+					say(path, "Skip ps");
+				continue;
+			} else if ( ! strcmp(fsec, "pdf")) {
+				if (warnings)
+					say(path, "Skip pdf");
+				continue;
+			} else if ( ! use_all &&
+			    ((dform == FORM_SRC &&
+			      strncmp(fsec, dsec, strlen(dsec))) ||
+			     (dform == FORM_CAT && strcmp(fsec, "0")))) {
+				if (warnings)
+					say(path, "Wrong filename suffix");
+				continue;
+			} else
+				fsec[-1] = '\0';
+
+			mlink = mandoc_calloc(1, sizeof(struct mlink));
+			if (strlcpy(mlink->file, path,
+			    sizeof(mlink->file)) >=
+			    sizeof(mlink->file)) {
+				say(path, "Filename too long");
+				free(mlink);
+				continue;
+			}
+			mlink->dform = dform;
+			mlink->dsec = dsec;
+			mlink->arch = arch;
+			mlink->name = ff->fts_name;
+			mlink->fsec = fsec;
+			mlink->gzip = gzip;
+			mlink_add(mlink, ff->fts_statp);
+			continue;
+
+		case FTS_D:
+		case FTS_DP:
+			break;
+
+		default:
+			if (warnings)
+				say(path, "Not a regular file");
+			continue;
+		}
+
+		switch (ff->fts_level) {
+		case 0:
+			/* Ignore the root directory. */
+			break;
+		case 1:
+			/*
+			 * This might contain manX/ or catX/.
+			 * Try to infer this from the name.
+			 * If we're not in use_all, enforce it.
+			 */
+			cp = ff->fts_name;
+			if (ff->fts_info == FTS_DP) {
+				dform = FORM_NONE;
+				dsec = NULL;
+				break;
+			}
+
+			if ( ! strncmp(cp, "man", 3)) {
+				dform = FORM_SRC;
+				dsec = cp + 3;
+			} else if ( ! strncmp(cp, "cat", 3)) {
+				dform = FORM_CAT;
+				dsec = cp + 3;
+			} else {
+				dform = FORM_NONE;
+				dsec = NULL;
+			}
+
+			if (dsec != NULL || use_all)
+				break;
+
+			if (warnings)
+				say(path, "Unknown directory part");
+			fts_set(f, ff, FTS_SKIP);
+			break;
+		case 2:
+			/*
+			 * Possibly our architecture.
+			 * If we're descending, keep tabs on it.
+			 */
+			if (ff->fts_info != FTS_DP && dsec != NULL)
+				arch = ff->fts_name;
+			else
+				arch = NULL;
+			break;
+		default:
+			if (ff->fts_info == FTS_DP || use_all)
+				break;
+			if (warnings)
+				say(path, "Extraneous directory part");
+			fts_set(f, ff, FTS_SKIP);
+			break;
+		}
+	}
+
+	fts_close(f);
+	return 1;
+}
+
+/*
+ * Add a file to the mlinks table.
+ * Do not verify that it's a "valid" looking manpage (we'll do that
+ * later).
+ *
+ * Try to infer the manual section, architecture, and page name from the
+ * path, assuming it looks like
+ *
+ *   [./]man*[/<arch>]/<name>.<section>
+ *   or
+ *   [./]cat<section>[/<arch>]/<name>.0
+ *
+ * See treescan() for the fts(3) version of this.
+ */
+static void
+filescan(const char *infile)
+{
+	struct stat	 st;
+	struct mlink	*mlink;
+	char		*linkfile, *p, *realdir, *start, *usefile;
+	size_t		 realdir_len;
+
+	assert(use_all);
+
+	if (strncmp(infile, "./", 2) == 0)
+		infile += 2;
+
+	/*
+	 * We have to do lstat(2) before realpath(3) loses
+	 * the information whether this is a symbolic link.
+	 * We need to know that because for symbolic links,
+	 * we want to use the orginal file name, while for
+	 * regular files, we want to use the real path.
+	 */
+	if (lstat(infile, &st) == -1) {
+		exitcode = (int)MANDOCLEVEL_BADARG;
+		say(infile, "&lstat");
+		return;
+	} else if (S_ISREG(st.st_mode) == 0 && S_ISLNK(st.st_mode) == 0) {
+		exitcode = (int)MANDOCLEVEL_BADARG;
+		say(infile, "Not a regular file");
+		return;
+	}
+
+	/*
+	 * We have to resolve the file name to the real path
+	 * in any case for the base directory check.
+	 */
+	if ((usefile = realpath(infile, NULL)) == NULL) {
+		exitcode = (int)MANDOCLEVEL_BADARG;
+		say(infile, "&realpath");
+		return;
+	}
+
+	if (op == OP_TEST)
+		start = usefile;
+	else if (strncmp(usefile, basedir, basedir_len) == 0)
+		start = usefile + basedir_len;
+	else {
+		exitcode = (int)MANDOCLEVEL_BADARG;
+		say("", "%s: outside base directory", infile);
+		free(usefile);
+		return;
+	}
+
+	/*
+	 * Now we are sure the file is inside our tree.
+	 * If it is a symbolic link, ignore the real path
+	 * and use the original name.
+	 */
+	do {
+		if (S_ISLNK(st.st_mode) == 0)
+			break;
+
+		/*
+		 * Some implementations of realpath(3) may succeed
+		 * even if the target of the link does not exist,
+		 * so check again for extra safety.
+		 */
+		if (stat(usefile, &st) == -1) {
+			exitcode = (int)MANDOCLEVEL_BADARG;
+			say(infile, "&stat");
+			free(usefile);
+			return;
+		}
+		linkfile = mandoc_strdup(infile);
+		if (op == OP_TEST) {
+			free(usefile);
+			start = usefile = linkfile;
+			break;
+		}
+		if (strncmp(infile, basedir, basedir_len) == 0) {
+			free(usefile);
+			usefile = linkfile;
+			start = usefile + basedir_len;
+			break;
+		}
+
+		/*
+		 * This symbolic link points into the basedir
+		 * from the outside.  Let's see whether any of
+		 * the parent directories resolve to the basedir.
+		 */
+		p = strchr(linkfile, '\0');
+		do {
+			while (*--p != '/')
+				continue;
+			*p = '\0';
+			if ((realdir = realpath(linkfile, NULL)) == NULL) {
+				exitcode = (int)MANDOCLEVEL_BADARG;
+				say(infile, "&realpath");
+				free(linkfile);
+				free(usefile);
+				return;
+			}
+			realdir_len = strlen(realdir) + 1;
+			free(realdir);
+			*p = '/';
+		} while (realdir_len > basedir_len);
+
+		/*
+		 * If one of the directories resolves to the basedir,
+		 * use the rest of the original name.
+		 * Otherwise, the best we can do
+		 * is to use the filename pointed to.
+		 */
+		if (realdir_len == basedir_len) {
+			free(usefile);
+			usefile = linkfile;
+			start = p + 1;
+		} else {
+			free(linkfile);
+			start = usefile + basedir_len;
+		}
+	} while (/* CONSTCOND */ 0);
+
+	mlink = mandoc_calloc(1, sizeof(struct mlink));
+	mlink->dform = FORM_NONE;
+	if (strlcpy(mlink->file, start, sizeof(mlink->file)) >=
+	    sizeof(mlink->file)) {
+		say(start, "Filename too long");
+		free(mlink);
+		free(usefile);
+		return;
+	}
+
+	/*
+	 * In test mode or when the original name is absolute
+	 * but outside our tree, guess the base directory.
+	 */
+
+	if (op == OP_TEST || (start == usefile && *start == '/')) {
+		if (strncmp(usefile, "man/", 4) == 0)
+			start = usefile + 4;
+		else if ((start = strstr(usefile, "/man/")) != NULL)
+			start += 5;
+		else
+			start = usefile;
+	}
+
+	/*
+	 * First try to guess our directory structure.
+	 * If we find a separator, try to look for man* or cat*.
+	 * If we find one of these and what's underneath is a directory,
+	 * assume it's an architecture.
+	 */
+	if ((p = strchr(start, '/')) != NULL) {
+		*p++ = '\0';
+		if (strncmp(start, "man", 3) == 0) {
+			mlink->dform = FORM_SRC;
+			mlink->dsec = start + 3;
+		} else if (strncmp(start, "cat", 3) == 0) {
+			mlink->dform = FORM_CAT;
+			mlink->dsec = start + 3;
+		}
+
+		start = p;
+		if (mlink->dsec != NULL && (p = strchr(start, '/')) != NULL) {
+			*p++ = '\0';
+			mlink->arch = start;
+			start = p;
+		}
+	}
+
+	/*
+	 * Now check the file suffix.
+	 * Suffix of `.0' indicates a catpage, `.1-9' is a manpage.
+	 */
+	p = strrchr(start, '\0');
+	while (p-- > start && *p != '/' && *p != '.')
+		continue;
+
+	if (*p == '.') {
+		*p++ = '\0';
+		mlink->fsec = p;
+	}
+
+	/*
+	 * Now try to parse the name.
+	 * Use the filename portion of the path.
+	 */
+	mlink->name = start;
+	if ((p = strrchr(start, '/')) != NULL) {
+		mlink->name = p + 1;
+		*p = '\0';
+	}
+	mlink_add(mlink, &st);
+	free(usefile);
+}
+
+static void
+mlink_add(struct mlink *mlink, const struct stat *st)
+{
+	struct inodev	 inodev;
+	struct mpage	*mpage;
+	unsigned int	 slot;
+
+	assert(NULL != mlink->file);
+
+	mlink->dsec = mandoc_strdup(mlink->dsec ? mlink->dsec : "");
+	mlink->arch = mandoc_strdup(mlink->arch ? mlink->arch : "");
+	mlink->name = mandoc_strdup(mlink->name ? mlink->name : "");
+	mlink->fsec = mandoc_strdup(mlink->fsec ? mlink->fsec : "");
+
+	if ('0' == *mlink->fsec) {
+		free(mlink->fsec);
+		mlink->fsec = mandoc_strdup(mlink->dsec);
+		mlink->fform = FORM_CAT;
+	} else if ('1' <= *mlink->fsec && '9' >= *mlink->fsec)
+		mlink->fform = FORM_SRC;
+	else
+		mlink->fform = FORM_NONE;
+
+	slot = ohash_qlookup(&mlinks, mlink->file);
+	assert(NULL == ohash_find(&mlinks, slot));
+	ohash_insert(&mlinks, slot, mlink);
+
+	memset(&inodev, 0, sizeof(inodev));  /* Clear padding. */
+	inodev.st_ino = st->st_ino;
+	inodev.st_dev = st->st_dev;
+	slot = ohash_lookup_memory(&mpages, (char *)&inodev,
+	    sizeof(struct inodev), inodev.st_ino);
+	mpage = ohash_find(&mpages, slot);
+	if (NULL == mpage) {
+		mpage = mandoc_calloc(1, sizeof(struct mpage));
+		mpage->inodev.st_ino = inodev.st_ino;
+		mpage->inodev.st_dev = inodev.st_dev;
+		mpage->form = FORM_NONE;
+		mpage->next = mpage_head;
+		mpage_head = mpage;
+		ohash_insert(&mpages, slot, mpage);
+	} else
+		mlink->next = mpage->mlinks;
+	mpage->mlinks = mlink;
+	mlink->mpage = mpage;
+}
+
+static void
+mlink_free(struct mlink *mlink)
+{
+
+	free(mlink->dsec);
+	free(mlink->arch);
+	free(mlink->name);
+	free(mlink->fsec);
+	free(mlink);
+}
+
+static void
+mpages_free(void)
+{
+	struct mpage	*mpage;
+	struct mlink	*mlink;
+
+	while ((mpage = mpage_head) != NULL) {
+		while ((mlink = mpage->mlinks) != NULL) {
+			mpage->mlinks = mlink->next;
+			mlink_free(mlink);
+		}
+		mpage_head = mpage->next;
+		free(mpage->sec);
+		free(mpage->arch);
+		free(mpage->title);
+		free(mpage->desc);
+		free(mpage);
+	}
+}
+
+/*
+ * For each mlink to the mpage, check whether the path looks like
+ * it is formatted, and if it does, check whether a source manual
+ * exists by the same name, ignoring the suffix.
+ * If both conditions hold, drop the mlink.
+ */
+static void
+mlinks_undupe(struct mpage *mpage)
+{
+	char		  buf[PATH_MAX];
+	struct mlink	**prev;
+	struct mlink	 *mlink;
+	char		 *bufp;
+
+	mpage->form = FORM_CAT;
+	prev = &mpage->mlinks;
+	while (NULL != (mlink = *prev)) {
+		if (FORM_CAT != mlink->dform) {
+			mpage->form = FORM_NONE;
+			goto nextlink;
+		}
+		(void)strlcpy(buf, mlink->file, sizeof(buf));
+		bufp = strstr(buf, "cat");
+		assert(NULL != bufp);
+		memcpy(bufp, "man", 3);
+		if (NULL != (bufp = strrchr(buf, '.')))
+			*++bufp = '\0';
+		(void)strlcat(buf, mlink->dsec, sizeof(buf));
+		if (NULL == ohash_find(&mlinks,
+		    ohash_qlookup(&mlinks, buf)))
+			goto nextlink;
+		if (warnings)
+			say(mlink->file, "Man source exists: %s", buf);
+		if (use_all)
+			goto nextlink;
+		*prev = mlink->next;
+		mlink_free(mlink);
+		continue;
+nextlink:
+		prev = &(*prev)->next;
+	}
+}
+
+static void
+mlink_check(struct mpage *mpage, struct mlink *mlink)
+{
+	struct str	*str;
+	unsigned int	 slot;
+
+	/*
+	 * Check whether the manual section given in a file
+	 * agrees with the directory where the file is located.
+	 * Some manuals have suffixes like (3p) on their
+	 * section number either inside the file or in the
+	 * directory name, some are linked into more than one
+	 * section, like encrypt(1) = makekey(8).
+	 */
+
+	if (FORM_SRC == mpage->form &&
+	    strcasecmp(mpage->sec, mlink->dsec))
+		say(mlink->file, "Section \"%s\" manual in %s directory",
+		    mpage->sec, mlink->dsec);
+
+	/*
+	 * Manual page directories exist for each kernel
+	 * architecture as returned by machine(1).
+	 * However, many manuals only depend on the
+	 * application architecture as returned by arch(1).
+	 * For example, some (2/ARM) manuals are shared
+	 * across the "armish" and "zaurus" kernel
+	 * architectures.
+	 * A few manuals are even shared across completely
+	 * different architectures, for example fdformat(1)
+	 * on amd64, i386, and sparc64.
+	 */
+
+	if (strcasecmp(mpage->arch, mlink->arch))
+		say(mlink->file, "Architecture \"%s\" manual in "
+		    "\"%s\" directory", mpage->arch, mlink->arch);
+
+	/*
+	 * XXX
+	 * parse_cat() doesn't set NAME_TITLE yet.
+	 */
+
+	if (FORM_CAT == mpage->form)
+		return;
+
+	/*
+	 * Check whether this mlink
+	 * appears as a name in the NAME section.
+	 */
+
+	slot = ohash_qlookup(&names, mlink->name);
+	str = ohash_find(&names, slot);
+	assert(NULL != str);
+	if ( ! (NAME_TITLE & str->mask))
+		say(mlink->file, "Name missing in NAME section");
+}
+
+/*
+ * Run through the files in the global vector "mpages"
+ * and add them to the database specified in "basedir".
+ *
+ * This handles the parsing scheme itself, using the cues of directory
+ * and filename to determine whether the file is parsable or not.
+ */
+static void
+mpages_merge(struct dba *dba, struct mparse *mp)
+{
+	struct mpage		*mpage, *mpage_dest;
+	struct mlink		*mlink, *mlink_dest;
+	struct roff_meta	*meta;
+	char			*cp;
+	int			 fd;
+
+	for (mpage = mpage_head; mpage != NULL; mpage = mpage->next) {
+		mlinks_undupe(mpage);
+		if ((mlink = mpage->mlinks) == NULL)
+			continue;
+
+		name_mask = NAME_MASK;
+		mandoc_ohash_init(&names, 4, offsetof(struct str, key));
+		mandoc_ohash_init(&strings, 6, offsetof(struct str, key));
+		mparse_reset(mp);
+		meta = NULL;
+
+		if ((fd = mparse_open(mp, mlink->file)) == -1) {
+			say(mlink->file, "&open");
+			goto nextpage;
+		}
+
+		/*
+		 * Interpret the file as mdoc(7) or man(7) source
+		 * code, unless it is known to be formatted.
+		 */
+		if (mlink->dform != FORM_CAT || mlink->fform != FORM_CAT) {
+			mparse_readfd(mp, fd, mlink->file);
+			close(fd);
+			fd = -1;
+			meta = mparse_result(mp);
+		}
+
+		if (meta != NULL && meta->sodest != NULL) {
+			mlink_dest = ohash_find(&mlinks,
+			    ohash_qlookup(&mlinks, meta->sodest));
+			if (mlink_dest == NULL) {
+				mandoc_asprintf(&cp, "%s.gz", meta->sodest);
+				mlink_dest = ohash_find(&mlinks,
+				    ohash_qlookup(&mlinks, cp));
+				free(cp);
+			}
+			if (mlink_dest != NULL) {
+
+				/* The .so target exists. */
+
+				mpage_dest = mlink_dest->mpage;
+				while (1) {
+					mlink->mpage = mpage_dest;
+
+					/*
+					 * If the target was already
+					 * processed, add the links
+					 * to the database now.
+					 * Otherwise, this will
+					 * happen when we come
+					 * to the target.
+					 */
+
+					if (mpage_dest->dba != NULL)
+						dbadd_mlink(mlink);
+
+					if (mlink->next == NULL)
+						break;
+					mlink = mlink->next;
+				}
+
+				/* Move all links to the target. */
+
+				mlink->next = mlink_dest->next;
+				mlink_dest->next = mpage->mlinks;
+				mpage->mlinks = NULL;
+				goto nextpage;
+			}
+			meta->macroset = MACROSET_NONE;
+		}
+		if (meta != NULL && meta->macroset == MACROSET_MDOC) {
+			mpage->form = FORM_SRC;
+			mpage->sec = meta->msec;
+			mpage->sec = mandoc_strdup(
+			    mpage->sec == NULL ? "" : mpage->sec);
+			mpage->arch = meta->arch;
+			mpage->arch = mandoc_strdup(
+			    mpage->arch == NULL ? "" : mpage->arch);
+			mpage->title = mandoc_strdup(meta->title);
+		} else if (meta != NULL && meta->macroset == MACROSET_MAN) {
+			if (*meta->msec != '\0' || *meta->title != '\0') {
+				mpage->form = FORM_SRC;
+				mpage->sec = mandoc_strdup(meta->msec);
+				mpage->arch = mandoc_strdup(mlink->arch);
+				mpage->title = mandoc_strdup(meta->title);
+			} else
+				meta = NULL;
+		}
+
+		assert(mpage->desc == NULL);
+		if (meta == NULL || meta->sodest != NULL) {
+			mpage->sec = mandoc_strdup(mlink->dsec);
+			mpage->arch = mandoc_strdup(mlink->arch);
+			mpage->title = mandoc_strdup(mlink->name);
+			if (meta == NULL) {
+				mpage->form = FORM_CAT;
+				parse_cat(mpage, fd);
+			} else
+				mpage->form = FORM_SRC;
+		} else if (meta->macroset == MACROSET_MDOC)
+			parse_mdoc(mpage, meta, meta->first);
+		else
+			parse_man(mpage, meta, meta->first);
+		if (mpage->desc == NULL) {
+			mpage->desc = mandoc_strdup(mlink->name);
+			if (warnings)
+				say(mlink->file, "No one-line description, "
+				    "using filename \"%s\"", mlink->name);
+		}
+
+		for (mlink = mpage->mlinks;
+		     mlink != NULL;
+		     mlink = mlink->next) {
+			putkey(mpage, mlink->name, NAME_FILE);
+			if (warnings && !use_all)
+				mlink_check(mpage, mlink);
+		}
+
+		dbadd(dba, mpage);
+
+nextpage:
+		ohash_delete(&strings);
+		ohash_delete(&names);
+	}
+}
+
+static void
+parse_cat(struct mpage *mpage, int fd)
+{
+	FILE		*stream;
+	struct mlink	*mlink;
+	char		*line, *p, *title, *sec;
+	size_t		 linesz, plen, titlesz;
+	ssize_t		 len;
+	int		 offs;
+
+	mlink = mpage->mlinks;
+	stream = fd == -1 ? fopen(mlink->file, "r") : fdopen(fd, "r");
+	if (stream == NULL) {
+		if (fd != -1)
+			close(fd);
+		if (warnings)
+			say(mlink->file, "&fopen");
+		return;
+	}
+
+	line = NULL;
+	linesz = 0;
+
+	/* Parse the section number from the header line. */
+
+	while (getline(&line, &linesz, stream) != -1) {
+		if (*line == '\n')
+			continue;
+		if ((sec = strchr(line, '(')) == NULL)
+			break;
+		if ((p = strchr(++sec, ')')) == NULL)
+			break;
+		free(mpage->sec);
+		mpage->sec = mandoc_strndup(sec, p - sec);
+		if (warnings && *mlink->dsec != '\0' &&
+		    strcasecmp(mpage->sec, mlink->dsec))
+			say(mlink->file,
+			    "Section \"%s\" manual in %s directory",
+			    mpage->sec, mlink->dsec);
+		break;
+	}
+
+	/* Skip to first blank line. */
+
+	while (line == NULL || *line != '\n')
+		if (getline(&line, &linesz, stream) == -1)
+			break;
+
+	/*
+	 * Assume the first line that is not indented
+	 * is the first section header.  Skip to it.
+	 */
+
+	while (getline(&line, &linesz, stream) != -1)
+		if (*line != '\n' && *line != ' ')
+			break;
+
+	/*
+	 * Read up until the next section into a buffer.
+	 * Strip the leading and trailing newline from each read line,
+	 * appending a trailing space.
+	 * Ignore empty (whitespace-only) lines.
+	 */
+
+	titlesz = 0;
+	title = NULL;
+
+	while ((len = getline(&line, &linesz, stream)) != -1) {
+		if (*line != ' ')
+			break;
+		offs = 0;
+		while (isspace((unsigned char)line[offs]))
+			offs++;
+		if (line[offs] == '\0')
+			continue;
+		title = mandoc_realloc(title, titlesz + len - offs);
+		memcpy(title + titlesz, line + offs, len - offs);
+		titlesz += len - offs;
+		title[titlesz - 1] = ' ';
+	}
+	free(line);
+
+	/*
+	 * If no page content can be found, or the input line
+	 * is already the next section header, or there is no
+	 * trailing newline, reuse the page title as the page
+	 * description.
+	 */
+
+	if (NULL == title || '\0' == *title) {
+		if (warnings)
+			say(mlink->file, "Cannot find NAME section");
+		fclose(stream);
+		free(title);
+		return;
+	}
+
+	title[titlesz - 1] = '\0';
+
+	/*
+	 * Skip to the first dash.
+	 * Use the remaining line as the description (no more than 70
+	 * bytes).
+	 */
+
+	if (NULL != (p = strstr(title, "- "))) {
+		for (p += 2; ' ' == *p || '\b' == *p; p++)
+			/* Skip to next word. */ ;
+	} else {
+		if (warnings)
+			say(mlink->file, "No dash in title line, "
+			    "reusing \"%s\" as one-line description", title);
+		p = title;
+	}
+
+	plen = strlen(p);
+
+	/* Strip backspace-encoding from line. */
+
+	while (NULL != (line = memchr(p, '\b', plen))) {
+		len = line - p;
+		if (0 == len) {
+			memmove(line, line + 1, plen--);
+			continue;
+		}
+		memmove(line - 1, line + 1, plen - len);
+		plen -= 2;
+	}
+
+	/*
+	 * Cut off excessive one-line descriptions.
+	 * Bad pages are not worth better heuristics.
+	 */
+
+	mpage->desc = mandoc_strndup(p, 150);
+	fclose(stream);
+	free(title);
+}
+
+/*
+ * Put a type/word pair into the word database for this particular file.
+ */
+static void
+putkey(const struct mpage *mpage, char *value, uint64_t type)
+{
+	putkeys(mpage, value, strlen(value), type);
+}
+
+/*
+ * Grok all nodes at or below a certain mdoc node into putkey().
+ */
+static void
+putmdockey(const struct mpage *mpage,
+	const struct roff_node *n, uint64_t m, int taboo)
+{
+
+	for ( ; NULL != n; n = n->next) {
+		if (n->flags & taboo)
+			continue;
+		if (NULL != n->child)
+			putmdockey(mpage, n->child, m, taboo);
+		if (n->type == ROFFT_TEXT)
+			putkey(mpage, n->string, m);
+	}
+}
+
+static void
+parse_man(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+	const struct roff_node *head, *body;
+	char		*start, *title;
+	char		 byte;
+	size_t		 sz;
+
+	if (n == NULL)
+		return;
+
+	/*
+	 * We're only searching for one thing: the first text child in
+	 * the BODY of a NAME section.  Since we don't keep track of
+	 * sections in -man, run some hoops to find out whether we're in
+	 * the correct section or not.
+	 */
+
+	if (n->type == ROFFT_BODY && n->tok == MAN_SH) {
+		body = n;
+		if ((head = body->parent->head) != NULL &&
+		    (head = head->child) != NULL &&
+		    head->next == NULL &&
+		    head->type == ROFFT_TEXT &&
+		    strcmp(head->string, "NAME") == 0 &&
+		    body->child != NULL) {
+
+			/*
+			 * Suck the entire NAME section into memory.
+			 * Yes, we might run away.
+			 * But too many manuals have big, spread-out
+			 * NAME sections over many lines.
+			 */
+
+			title = NULL;
+			deroff(&title, body);
+			if (NULL == title)
+				return;
+
+			/*
+			 * Go through a special heuristic dance here.
+			 * Conventionally, one or more manual names are
+			 * comma-specified prior to a whitespace, then a
+			 * dash, then a description.  Try to puzzle out
+			 * the name parts here.
+			 */
+
+			start = title;
+			for ( ;; ) {
+				sz = strcspn(start, " ,");
+				if ('\0' == start[sz])
+					break;
+
+				byte = start[sz];
+				start[sz] = '\0';
+
+				/*
+				 * Assume a stray trailing comma in the
+				 * name list if a name begins with a dash.
+				 */
+
+				if ('-' == start[0] ||
+				    ('\\' == start[0] && '-' == start[1]))
+					break;
+
+				putkey(mpage, start, NAME_TITLE);
+				if ( ! (mpage->name_head_done ||
+				    strcasecmp(start, meta->title))) {
+					putkey(mpage, start, NAME_HEAD);
+					mpage->name_head_done = 1;
+				}
+
+				if (' ' == byte) {
+					start += sz + 1;
+					break;
+				}
+
+				assert(',' == byte);
+				start += sz + 1;
+				while (' ' == *start)
+					start++;
+			}
+
+			if (start == title) {
+				putkey(mpage, start, NAME_TITLE);
+				if ( ! (mpage->name_head_done ||
+				    strcasecmp(start, meta->title))) {
+					putkey(mpage, start, NAME_HEAD);
+					mpage->name_head_done = 1;
+				}
+				free(title);
+				return;
+			}
+
+			while (isspace((unsigned char)*start))
+				start++;
+
+			if (0 == strncmp(start, "-", 1))
+				start += 1;
+			else if (0 == strncmp(start, "\\-\\-", 4))
+				start += 4;
+			else if (0 == strncmp(start, "\\-", 2))
+				start += 2;
+			else if (0 == strncmp(start, "\\(en", 4))
+				start += 4;
+			else if (0 == strncmp(start, "\\(em", 4))
+				start += 4;
+
+			while (' ' == *start)
+				start++;
+
+			/*
+			 * Cut off excessive one-line descriptions.
+			 * Bad pages are not worth better heuristics.
+			 */
+
+			mpage->desc = mandoc_strndup(start, 150);
+			free(title);
+			return;
+		}
+	}
+
+	for (n = n->child; n; n = n->next) {
+		if (NULL != mpage->desc)
+			break;
+		parse_man(mpage, meta, n);
+	}
+}
+
+static void
+parse_mdoc(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+	const struct mdoc_handler *handler;
+
+	for (n = n->child; n != NULL; n = n->next) {
+		if (n->tok == TOKEN_NONE || n->tok < ROFF_MAX)
+			continue;
+		assert(n->tok >= MDOC_Dd && n->tok < MDOC_MAX);
+		handler = mdoc_handlers + (n->tok - MDOC_Dd);
+		if (n->flags & handler->taboo)
+			continue;
+
+		switch (n->type) {
+		case ROFFT_ELEM:
+		case ROFFT_BLOCK:
+		case ROFFT_HEAD:
+		case ROFFT_BODY:
+		case ROFFT_TAIL:
+			if (handler->fp != NULL &&
+			    (*handler->fp)(mpage, meta, n) == 0)
+				break;
+			if (handler->mask)
+				putmdockey(mpage, n->child,
+				    handler->mask, handler->taboo);
+			break;
+		default:
+			continue;
+		}
+		if (NULL != n->child)
+			parse_mdoc(mpage, meta, n);
+	}
+}
+
+static int
+parse_mdoc_Fa(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+	uint64_t mask;
+
+	mask = TYPE_Fa;
+	if (n->sec == SEC_SYNOPSIS)
+		mask |= TYPE_Vt;
+
+	putmdockey(mpage, n->child, mask, 0);
+	return 0;
+}
+
+static int
+parse_mdoc_Fd(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+	char		*start, *end;
+	size_t		 sz;
+
+	if (SEC_SYNOPSIS != n->sec ||
+	    NULL == (n = n->child) ||
+	    n->type != ROFFT_TEXT)
+		return 0;
+
+	/*
+	 * Only consider those `Fd' macro fields that begin with an
+	 * "inclusion" token (versus, e.g., #define).
+	 */
+
+	if (strcmp("#include", n->string))
+		return 0;
+
+	if ((n = n->next) == NULL || n->type != ROFFT_TEXT)
+		return 0;
+
+	/*
+	 * Strip away the enclosing angle brackets and make sure we're
+	 * not zero-length.
+	 */
+
+	start = n->string;
+	if ('<' == *start || '"' == *start)
+		start++;
+
+	if (0 == (sz = strlen(start)))
+		return 0;
+
+	end = &start[(int)sz - 1];
+	if ('>' == *end || '"' == *end)
+		end--;
+
+	if (end > start)
+		putkeys(mpage, start, end - start + 1, TYPE_In);
+	return 0;
+}
+
+static void
+parse_mdoc_fname(struct mpage *mpage, const struct roff_node *n)
+{
+	char	*cp;
+	size_t	 sz;
+
+	if (n->type != ROFFT_TEXT)
+		return;
+
+	/* Skip function pointer punctuation. */
+
+	cp = n->string;
+	while (*cp == '(' || *cp == '*')
+		cp++;
+	sz = strcspn(cp, "()");
+
+	putkeys(mpage, cp, sz, TYPE_Fn);
+	if (n->sec == SEC_SYNOPSIS)
+		putkeys(mpage, cp, sz, NAME_SYN);
+}
+
+static int
+parse_mdoc_Fn(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+	uint64_t mask;
+
+	if (n->child == NULL)
+		return 0;
+
+	parse_mdoc_fname(mpage, n->child);
+
+	n = n->child->next;
+	if (n != NULL && n->type == ROFFT_TEXT) {
+		mask = TYPE_Fa;
+		if (n->sec == SEC_SYNOPSIS)
+			mask |= TYPE_Vt;
+		putmdockey(mpage, n, mask, 0);
+	}
+
+	return 0;
+}
+
+static int
+parse_mdoc_Fo(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+
+	if (n->type != ROFFT_HEAD)
+		return 1;
+
+	if (n->child != NULL)
+		parse_mdoc_fname(mpage, n->child);
+
+	return 0;
+}
+
+static int
+parse_mdoc_Va(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+	char *cp;
+
+	if (n->type != ROFFT_ELEM && n->type != ROFFT_BODY)
+		return 0;
+
+	if (n->child != NULL &&
+	    n->child->next == NULL &&
+	    n->child->type == ROFFT_TEXT)
+		return 1;
+
+	cp = NULL;
+	deroff(&cp, n);
+	if (cp != NULL) {
+		putkey(mpage, cp, TYPE_Vt | (n->tok == MDOC_Va ||
+		    n->type == ROFFT_BODY ? TYPE_Va : 0));
+		free(cp);
+	}
+
+	return 0;
+}
+
+static int
+parse_mdoc_Xr(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+	char	*cp;
+
+	if (NULL == (n = n->child))
+		return 0;
+
+	if (NULL == n->next) {
+		putkey(mpage, n->string, TYPE_Xr);
+		return 0;
+	}
+
+	mandoc_asprintf(&cp, "%s(%s)", n->string, n->next->string);
+	putkey(mpage, cp, TYPE_Xr);
+	free(cp);
+	return 0;
+}
+
+static int
+parse_mdoc_Nd(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+
+	if (n->type == ROFFT_BODY)
+		deroff(&mpage->desc, n);
+	return 0;
+}
+
+static int
+parse_mdoc_Nm(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+
+	if (SEC_NAME == n->sec)
+		putmdockey(mpage, n->child, NAME_TITLE, 0);
+	else if (n->sec == SEC_SYNOPSIS && n->type == ROFFT_HEAD) {
+		if (n->child == NULL)
+			putkey(mpage, meta->name, NAME_SYN);
+		else
+			putmdockey(mpage, n->child, NAME_SYN, 0);
+	}
+	if ( ! (mpage->name_head_done ||
+	    n->child == NULL || n->child->string == NULL ||
+	    strcasecmp(n->child->string, meta->title))) {
+		putkey(mpage, n->child->string, NAME_HEAD);
+		mpage->name_head_done = 1;
+	}
+	return 0;
+}
+
+static int
+parse_mdoc_Sh(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+
+	return n->sec == SEC_CUSTOM && n->type == ROFFT_HEAD;
+}
+
+static int
+parse_mdoc_head(struct mpage *mpage, const struct roff_meta *meta,
+	const struct roff_node *n)
+{
+
+	return n->type == ROFFT_HEAD;
+}
+
+/*
+ * Add a string to the hash table for the current manual.
+ * Each string has a bitmask telling which macros it belongs to.
+ * When we finish the manual, we'll dump the table.
+ */
+static void
+putkeys(const struct mpage *mpage, char *cp, size_t sz, uint64_t v)
+{
+	struct ohash	*htab;
+	struct str	*s;
+	const char	*end;
+	unsigned int	 slot;
+	int		 i, mustfree;
+
+	if (0 == sz)
+		return;
+
+	mustfree = render_string(&cp, &sz);
+
+	if (TYPE_Nm & v) {
+		htab = &names;
+		v &= name_mask;
+		if (v & NAME_FIRST)
+			name_mask &= ~NAME_FIRST;
+		if (debug > 1)
+			say(mpage->mlinks->file,
+			    "Adding name %*s, bits=0x%llx", (int)sz, cp,
+			    (unsigned long long)v);
+	} else {
+		htab = &strings;
+		if (debug > 1)
+		    for (i = 0; i < KEY_MAX; i++)
+			if ((uint64_t)1 << i & v)
+			    say(mpage->mlinks->file,
+				"Adding key %s=%*s",
+				mansearch_keynames[i], (int)sz, cp);
+	}
+
+	end = cp + sz;
+	slot = ohash_qlookupi(htab, cp, &end);
+	s = ohash_find(htab, slot);
+
+	if (NULL != s && mpage == s->mpage) {
+		s->mask |= v;
+		return;
+	} else if (NULL == s) {
+		s = mandoc_calloc(1, sizeof(struct str) + sz + 1);
+		memcpy(s->key, cp, sz);
+		ohash_insert(htab, slot, s);
+	}
+	s->mpage = mpage;
+	s->mask = v;
+
+	if (mustfree)
+		free(cp);
+}
+
+/*
+ * Take a Unicode codepoint and produce its UTF-8 encoding.
+ * This isn't the best way to do this, but it works.
+ * The magic numbers are from the UTF-8 packaging.
+ * They're not as scary as they seem: read the UTF-8 spec for details.
+ */
+static size_t
+utf8(unsigned int cp, char out[7])
+{
+	size_t		 rc;
+
+	rc = 0;
+	if (cp <= 0x0000007F) {
+		rc = 1;
+		out[0] = (char)cp;
+	} else if (cp <= 0x000007FF) {
+		rc = 2;
+		out[0] = (cp >> 6  & 31) | 192;
+		out[1] = (cp       & 63) | 128;
+	} else if (cp <= 0x0000FFFF) {
+		rc = 3;
+		out[0] = (cp >> 12 & 15) | 224;
+		out[1] = (cp >> 6  & 63) | 128;
+		out[2] = (cp       & 63) | 128;
+	} else if (cp <= 0x001FFFFF) {
+		rc = 4;
+		out[0] = (cp >> 18 &  7) | 240;
+		out[1] = (cp >> 12 & 63) | 128;
+		out[2] = (cp >> 6  & 63) | 128;
+		out[3] = (cp       & 63) | 128;
+	} else if (cp <= 0x03FFFFFF) {
+		rc = 5;
+		out[0] = (cp >> 24 &  3) | 248;
+		out[1] = (cp >> 18 & 63) | 128;
+		out[2] = (cp >> 12 & 63) | 128;
+		out[3] = (cp >> 6  & 63) | 128;
+		out[4] = (cp       & 63) | 128;
+	} else if (cp <= 0x7FFFFFFF) {
+		rc = 6;
+		out[0] = (cp >> 30 &  1) | 252;
+		out[1] = (cp >> 24 & 63) | 128;
+		out[2] = (cp >> 18 & 63) | 128;
+		out[3] = (cp >> 12 & 63) | 128;
+		out[4] = (cp >> 6  & 63) | 128;
+		out[5] = (cp       & 63) | 128;
+	} else
+		return 0;
+
+	out[rc] = '\0';
+	return rc;
+}
+
+/*
+ * If the string contains escape sequences,
+ * replace it with an allocated rendering and return 1,
+ * such that the caller can free it after use.
+ * Otherwise, do nothing and return 0.
+ */
+static int
+render_string(char **public, size_t *psz)
+{
+	const char	*src, *scp, *addcp, *seq;
+	char		*dst;
+	size_t		 ssz, dsz, addsz;
+	char		 utfbuf[7], res[6];
+	int		 seqlen, unicode;
+
+	res[0] = '\\';
+	res[1] = '\t';
+	res[2] = ASCII_NBRSP;
+	res[3] = ASCII_HYPH;
+	res[4] = ASCII_BREAK;
+	res[5] = '\0';
+
+	src = scp = *public;
+	ssz = *psz;
+	dst = NULL;
+	dsz = 0;
+
+	while (scp < src + *psz) {
+
+		/* Leave normal characters unchanged. */
+
+		if (strchr(res, *scp) == NULL) {
+			if (dst != NULL)
+				dst[dsz++] = *scp;
+			scp++;
+			continue;
+		}
+
+		/*
+		 * Found something that requires replacing,
+		 * make sure we have a destination buffer.
+		 */
+
+		if (dst == NULL) {
+			dst = mandoc_malloc(ssz + 1);
+			dsz = scp - src;
+			memcpy(dst, src, dsz);
+		}
+
+		/* Handle single-char special characters. */
+
+		switch (*scp) {
+		case '\\':
+			break;
+		case '\t':
+		case ASCII_NBRSP:
+			dst[dsz++] = ' ';
+			scp++;
+			continue;
+		case ASCII_HYPH:
+			dst[dsz++] = '-';
+			/* FALLTHROUGH */
+		case ASCII_BREAK:
+			scp++;
+			continue;
+		default:
+			abort();
+		}
+
+		/*
+		 * Found an escape sequence.
+		 * Read past the slash, then parse it.
+		 * Ignore everything except characters.
+		 */
+
+		scp++;
+		if (mandoc_escape(&scp, &seq, &seqlen) != ESCAPE_SPECIAL)
+			continue;
+
+		/*
+		 * Render the special character
+		 * as either UTF-8 or ASCII.
+		 */
+
+		if (write_utf8) {
+			unicode = mchars_spec2cp(seq, seqlen);
+			if (unicode <= 0)
+				continue;
+			addsz = utf8(unicode, utfbuf);
+			if (addsz == 0)
+				continue;
+			addcp = utfbuf;
+		} else {
+			addcp = mchars_spec2str(seq, seqlen, &addsz);
+			if (addcp == NULL)
+				continue;
+			if (*addcp == ASCII_NBRSP) {
+				addcp = " ";
+				addsz = 1;
+			}
+		}
+
+		/* Copy the rendered glyph into the stream. */
+
+		ssz += addsz;
+		dst = mandoc_realloc(dst, ssz + 1);
+		memcpy(dst + dsz, addcp, addsz);
+		dsz += addsz;
+	}
+	if (dst != NULL) {
+		*public = dst;
+		*psz = dsz;
+	}
+
+	/* Trim trailing whitespace and NUL-terminate. */
+
+	while (*psz > 0 && (*public)[*psz - 1] == ' ')
+		--*psz;
+	if (dst != NULL) {
+		(*public)[*psz] = '\0';
+		return 1;
+	} else
+		return 0;
+}
+
+static void
+dbadd_mlink(const struct mlink *mlink)
+{
+	dba_page_alias(mlink->mpage->dba, mlink->name, NAME_FILE);
+	dba_page_add(mlink->mpage->dba, DBP_SECT, mlink->dsec);
+	dba_page_add(mlink->mpage->dba, DBP_SECT, mlink->fsec);
+	dba_page_add(mlink->mpage->dba, DBP_ARCH, mlink->arch);
+	dba_page_add(mlink->mpage->dba, DBP_FILE, mlink->file);
+}
+
+/*
+ * Flush the current page's terms (and their bits) into the database.
+ * Also, handle escape sequences at the last possible moment.
+ */
+static void
+dbadd(struct dba *dba, struct mpage *mpage)
+{
+	struct mlink	*mlink;
+	struct str	*key;
+	char		*cp;
+	uint64_t	 mask;
+	size_t		 i;
+	unsigned int	 slot;
+	int		 mustfree;
+
+	mlink = mpage->mlinks;
+
+	if (nodb) {
+		for (key = ohash_first(&names, &slot); NULL != key;
+		     key = ohash_next(&names, &slot))
+			free(key);
+		for (key = ohash_first(&strings, &slot); NULL != key;
+		     key = ohash_next(&strings, &slot))
+			free(key);
+		if (0 == debug)
+			return;
+		while (NULL != mlink) {
+			fputs(mlink->name, stdout);
+			if (NULL == mlink->next ||
+			    strcmp(mlink->dsec, mlink->next->dsec) ||
+			    strcmp(mlink->fsec, mlink->next->fsec) ||
+			    strcmp(mlink->arch, mlink->next->arch)) {
+				putchar('(');
+				if ('\0' == *mlink->dsec)
+					fputs(mlink->fsec, stdout);
+				else
+					fputs(mlink->dsec, stdout);
+				if ('\0' != *mlink->arch)
+					printf("/%s", mlink->arch);
+				putchar(')');
+			}
+			mlink = mlink->next;
+			if (NULL != mlink)
+				fputs(", ", stdout);
+		}
+		printf(" - %s\n", mpage->desc);
+		return;
+	}
+
+	if (debug)
+		say(mlink->file, "Adding to database");
+
+	cp = mpage->desc;
+	i = strlen(cp);
+	mustfree = render_string(&cp, &i);
+	mpage->dba = dba_page_new(dba->pages,
+	    *mpage->arch == '\0' ? mlink->arch : mpage->arch,
+	    cp, mlink->file, mpage->form);
+	if (mustfree)
+		free(cp);
+	dba_page_add(mpage->dba, DBP_SECT, mpage->sec);
+
+	while (mlink != NULL) {
+		dbadd_mlink(mlink);
+		mlink = mlink->next;
+	}
+
+	for (key = ohash_first(&names, &slot); NULL != key;
+	     key = ohash_next(&names, &slot)) {
+		assert(key->mpage == mpage);
+		dba_page_alias(mpage->dba, key->key, key->mask);
+		free(key);
+	}
+	for (key = ohash_first(&strings, &slot); NULL != key;
+	     key = ohash_next(&strings, &slot)) {
+		assert(key->mpage == mpage);
+		i = 0;
+		for (mask = TYPE_Xr; mask <= TYPE_Lb; mask *= 2) {
+			if (key->mask & mask)
+				dba_macro_add(dba->macros, i,
+				    key->key, mpage->dba);
+			i++;
+		}
+		free(key);
+	}
+}
+
+static void
+dbprune(struct dba *dba)
+{
+	struct dba_array	*page, *files;
+	char			*file;
+
+	dba_array_FOREACH(dba->pages, page) {
+		files = dba_array_get(page, DBP_FILE);
+		dba_array_FOREACH(files, file) {
+			if (*file < ' ')
+				file++;
+			if (ohash_find(&mlinks, ohash_qlookup(&mlinks,
+			    file)) != NULL) {
+				if (debug)
+					say(file, "Deleting from database");
+				dba_array_del(dba->pages);
+				break;
+			}
+		}
+	}
+}
+
+/*
+ * Write the database from memory to disk.
+ */
+static void
+dbwrite(struct dba *dba)
+{
+	struct stat	 sb1, sb2;
+	char		 tfn[33], *cp1, *cp2;
+	off_t		 i;
+	int		 fd1, fd2;
+
+	/*
+	 * Do not write empty databases, and delete existing ones
+	 * when makewhatis -u causes them to become empty.
+	 */
+
+	dba_array_start(dba->pages);
+	if (dba_array_next(dba->pages) == NULL) {
+		if (unlink(MANDOC_DB) == -1 && errno != ENOENT)
+			say(MANDOC_DB, "&unlink");
+		return;
+	}
+
+	/*
+	 * Build the database in a temporary file,
+	 * then atomically move it into place.
+	 */
+
+	if (dba_write(MANDOC_DB "~", dba) != -1) {
+		if (rename(MANDOC_DB "~", MANDOC_DB) == -1) {
+			exitcode = (int)MANDOCLEVEL_SYSERR;
+			say(MANDOC_DB, "&rename");
+			unlink(MANDOC_DB "~");
+		}
+		return;
+	}
+
+	/*
+	 * We lack write permission and cannot replace the database
+	 * file, but let's at least check whether the data changed.
+	 */
+
+	(void)strlcpy(tfn, "/tmp/mandocdb.XXXXXXXX", sizeof(tfn));
+	if (mkdtemp(tfn) == NULL) {
+		exitcode = (int)MANDOCLEVEL_SYSERR;
+		say("", "&%s", tfn);
+		return;
+	}
+	cp1 = cp2 = MAP_FAILED;
+	fd1 = fd2 = -1;
+	(void)strlcat(tfn, "/" MANDOC_DB, sizeof(tfn));
+	if (dba_write(tfn, dba) == -1) {
+		say(tfn, "&dba_write");
+		goto err;
+	}
+	if ((fd1 = open(MANDOC_DB, O_RDONLY, 0)) == -1) {
+		say(MANDOC_DB, "&open");
+		goto err;
+	}
+	if ((fd2 = open(tfn, O_RDONLY, 0)) == -1) {
+		say(tfn, "&open");
+		goto err;
+	}
+	if (fstat(fd1, &sb1) == -1) {
+		say(MANDOC_DB, "&fstat");
+		goto err;
+	}
+	if (fstat(fd2, &sb2) == -1) {
+		say(tfn, "&fstat");
+		goto err;
+	}
+	if (sb1.st_size != sb2.st_size)
+		goto err;
+	if ((cp1 = mmap(NULL, sb1.st_size, PROT_READ, MAP_PRIVATE,
+	    fd1, 0)) == MAP_FAILED) {
+		say(MANDOC_DB, "&mmap");
+		goto err;
+	}
+	if ((cp2 = mmap(NULL, sb2.st_size, PROT_READ, MAP_PRIVATE,
+	    fd2, 0)) == MAP_FAILED) {
+		say(tfn, "&mmap");
+		goto err;
+	}
+	for (i = 0; i < sb1.st_size; i++)
+		if (cp1[i] != cp2[i])
+			goto err;
+	goto out;
+
+err:
+	exitcode = (int)MANDOCLEVEL_SYSERR;
+	say(MANDOC_DB, "Data changed, but cannot replace database");
+
+out:
+	if (cp1 != MAP_FAILED)
+		munmap(cp1, sb1.st_size);
+	if (cp2 != MAP_FAILED)
+		munmap(cp2, sb2.st_size);
+	if (fd1 != -1)
+		close(fd1);
+	if (fd2 != -1)
+		close(fd2);
+	unlink(tfn);
+	*strrchr(tfn, '/') = '\0';
+	rmdir(tfn);
+}
+
+static int
+set_basedir(const char *targetdir, int report_baddir)
+{
+	static char	 startdir[PATH_MAX];
+	static int	 getcwd_status;  /* 1 = ok, 2 = failure */
+	static int	 chdir_status;  /* 1 = changed directory */
+
+	/*
+	 * Remember the original working directory, if possible.
+	 * This will be needed if the second or a later directory
+	 * on the command line is given as a relative path.
+	 * Do not error out if the current directory is not
+	 * searchable: Maybe it won't be needed after all.
+	 */
+	if (getcwd_status == 0) {
+		if (getcwd(startdir, sizeof(startdir)) == NULL) {
+			getcwd_status = 2;
+			(void)strlcpy(startdir, strerror(errno),
+			    sizeof(startdir));
+		} else
+			getcwd_status = 1;
+	}
+
+	/*
+	 * We are leaving the old base directory.
+	 * Do not use it any longer, not even for messages.
+	 */
+	*basedir = '\0';
+	basedir_len = 0;
+
+	/*
+	 * If and only if the directory was changed earlier and
+	 * the next directory to process is given as a relative path,
+	 * first go back, or bail out if that is impossible.
+	 */
+	if (chdir_status && *targetdir != '/') {
+		if (getcwd_status == 2) {
+			exitcode = (int)MANDOCLEVEL_SYSERR;
+			say("", "getcwd: %s", startdir);
+			return 0;
+		}
+		if (chdir(startdir) == -1) {
+			exitcode = (int)MANDOCLEVEL_SYSERR;
+			say("", "&chdir %s", startdir);
+			return 0;
+		}
+	}
+
+	/*
+	 * Always resolve basedir to the canonicalized absolute
+	 * pathname and append a trailing slash, such that
+	 * we can reliably check whether files are inside.
+	 */
+	if (realpath(targetdir, basedir) == NULL) {
+		if (report_baddir || errno != ENOENT) {
+			exitcode = (int)MANDOCLEVEL_BADARG;
+			say("", "&%s: realpath", targetdir);
+		}
+		*basedir = '\0';
+		return 0;
+	} else if (chdir(basedir) == -1) {
+		if (report_baddir || errno != ENOENT) {
+			exitcode = (int)MANDOCLEVEL_BADARG;
+			say("", "&chdir");
+		}
+		*basedir = '\0';
+		return 0;
+	}
+	chdir_status = 1;
+	basedir_len = strlen(basedir);
+	if (basedir[basedir_len - 1] != '/') {
+		if (basedir_len >= PATH_MAX - 1) {
+			exitcode = (int)MANDOCLEVEL_SYSERR;
+			say("", "Filename too long");
+			*basedir = '\0';
+			basedir_len = 0;
+			return 0;
+		}
+		basedir[basedir_len++] = '/';
+		basedir[basedir_len] = '\0';
+	}
+	return 1;
+}
+
+static void
+say(const char *file, const char *format, ...)
+{
+	va_list		 ap;
+	int		 use_errno;
+
+	if (*basedir != '\0')
+		fprintf(stderr, "%s", basedir);
+	if (*basedir != '\0' && *file != '\0')
+		fputc('/', stderr);
+	if (*file != '\0')
+		fprintf(stderr, "%s", file);
+
+	use_errno = 1;
+	if (format != NULL) {
+		switch (*format) {
+		case '&':
+			format++;
+			break;
+		case '\0':
+			format = NULL;
+			break;
+		default:
+			use_errno = 0;
+			break;
+		}
+	}
+	if (format != NULL) {
+		if (*basedir != '\0' || *file != '\0')
+			fputs(": ", stderr);
+		va_start(ap, format);
+		vfprintf(stderr, format, ap);
+		va_end(ap);
+	}
+	if (use_errno) {
+		if (*basedir != '\0' || *file != '\0' || format != NULL)
+			fputs(": ", stderr);
+		perror(NULL);
+	} else
+		fputc('\n', stderr);
+}
diff --git a/usr.bin/mandoc/manpath.c b/usr.bin/mandoc/manpath.c
new file mode 100644
index 0000000..dcd12a0
--- /dev/null
+++ b/usr.bin/mandoc/manpath.c
@@ -0,0 +1,342 @@
+/*	$OpenBSD: manpath.c,v 1.28 2020/02/10 14:42:03 schwarze Exp $ */
+/*
+ * Copyright (c) 2011,2014,2015,2017-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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 <sys/types.h>
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mandoc_aux.h"
+#include "mandoc.h"
+#include "manconf.h"
+
+#define MAN_CONF_FILE	"/etc/man.conf"
+#define MANPATH_BASE	"/usr/share/man:/usr/X11R6/man"
+#define MANPATH_DEFAULT	"/usr/share/man:/usr/X11R6/man:/usr/local/man"
+
+static	void	 manconf_file(struct manconf *, const char *);
+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 *defp, char *auxp)
+{
+	char		*insert;
+
+	/* Always prepend -m. */
+	manpath_parseline(&conf->manpath, auxp, 'm');
+
+	/* If -M is given, it overrides everything else. */
+	if (NULL != defp) {
+		manpath_parseline(&conf->manpath, defp, 'M');
+		return;
+	}
+
+	/* MANPATH and man.conf(5) cooperate. */
+	defp = getenv("MANPATH");
+	if (NULL == file)
+		file = MAN_CONF_FILE;
+
+	/* No MANPATH; use man.conf(5) only. */
+	if (NULL == defp || '\0' == defp[0]) {
+		manconf_file(conf, file);
+		return;
+	}
+
+	/* Prepend man.conf(5) to MANPATH. */
+	if (':' == defp[0]) {
+		manconf_file(conf, file);
+		manpath_parseline(&conf->manpath, defp, '\0');
+		return;
+	}
+
+	/* Append man.conf(5) to MANPATH. */
+	if (':' == defp[strlen(defp) - 1]) {
+		manpath_parseline(&conf->manpath, defp, '\0');
+		manconf_file(conf, file);
+		return;
+	}
+
+	/* Insert man.conf(5) into MANPATH. */
+	insert = strstr(defp, "::");
+	if (NULL != insert) {
+		*insert++ = '\0';
+		manpath_parseline(&conf->manpath, defp, '\0');
+		manconf_file(conf, file);
+		manpath_parseline(&conf->manpath, insert + 1, '\0');
+		return;
+	}
+
+	/* MANPATH overrides man.conf(5) completely. */
+	manpath_parseline(&conf->manpath, defp, '\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)
+{
+	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 */
+			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 (*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[] = {
+	    "includes", "man", "paper", "style", "indent", "width",
+	    "tag", "fragment", "mdoc", "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 < 6 && *cp == '\0') {
+		mandoc_msg(MANDOCERR_BADVAL_MISS, 0, 0, "-O %s=?", toks[tok]);
+		return -1;
+	}
+	if (tok > 6 && 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->tag != NULL) {
+			oldval = mandoc_strdup(conf->tag);
+			break;
+		}
+		conf->tag = mandoc_strdup(cp);
+		return 0;
+	case 7:
+		conf->fragment = 1;
+		return 0;
+	case 8:
+		conf->mdoc = 1;
+		return 0;
+	case 9:
+		conf->noval = 1;
+		return 0;
+	case 10:
+		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/usr.bin/mandoc/mansearch.c b/usr.bin/mandoc/mansearch.c
new file mode 100644
index 0000000..7cb3468
--- /dev/null
+++ b/usr.bin/mandoc/mansearch.c
@@ -0,0 +1,842 @@
+/*	$OpenBSD: mansearch.c,v 1.65 2019/07/01 22:43:03 schwarze Exp $ */
+/*
+ * Copyright (c) 2012 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2013-2018 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/cdefs.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <limits.h>
+#include <regex.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "mandoc_aux.h"
+#include "mandoc_ohash.h"
+#include "manconf.h"
+#include "mansearch.h"
+#include "dbm.h"
+
+struct	expr {
+	/* Used for terms: */
+	struct dbm_match match;   /* Match type and expression. */
+	uint64_t	 bits;    /* Type mask. */
+	/* Used for OR and AND groups: */
+	struct expr	*next;    /* Next child in the parent group. */
+	struct expr	*child;   /* First child in this group. */
+	enum { EXPR_TERM, EXPR_OR, EXPR_AND } type;
+};
+
+const char *const mansearch_keynames[KEY_MAX] = {
+	"arch",	"sec",	"Xr",	"Ar",	"Fa",	"Fl",	"Dv",	"Fn",
+	"Ic",	"Pa",	"Cm",	"Li",	"Em",	"Cd",	"Va",	"Ft",
+	"Tn",	"Er",	"Ev",	"Sy",	"Sh",	"In",	"Ss",	"Ox",
+	"An",	"Mt",	"St",	"Bx",	"At",	"Nx",	"Fx",	"Lk",
+	"Ms",	"Bsx",	"Dx",	"Rs",	"Vt",	"Lb",	"Nm",	"Nd"
+};
+
+
+static	struct ohash	*manmerge(struct expr *, struct ohash *);
+static	struct ohash	*manmerge_term(struct expr *, struct ohash *);
+static	struct ohash	*manmerge_or(struct expr *, struct ohash *);
+static	struct ohash	*manmerge_and(struct expr *, struct ohash *);
+static	char		*buildnames(const struct dbm_page *);
+static	char		*buildoutput(size_t, struct dbm_page *);
+static	size_t		 lstlen(const char *, size_t);
+static	void		 lstcat(char *, size_t *, const char *, const char *);
+static	int		 lstmatch(const char *, const char *);
+static	struct expr	*exprcomp(const struct mansearch *,
+				int, char *[], int *);
+static	struct expr	*expr_and(const struct mansearch *,
+				int, char *[], int *);
+static	struct expr	*exprterm(const struct mansearch *,
+				int, char *[], int *);
+static	void		 exprfree(struct expr *);
+static	int		 manpage_compare(const void *, const void *);
+
+
+int
+mansearch(const struct mansearch *search,
+		const struct manpaths *paths,
+		int argc, char *argv[],
+		struct manpage **res, size_t *sz)
+{
+	char		 buf[PATH_MAX];
+	struct dbm_res	*rp;
+	struct expr	*e;
+	struct dbm_page	*page;
+	struct manpage	*mpage;
+	struct ohash	*htab;
+	size_t		 cur, i, maxres, outkey;
+	unsigned int	 slot;
+	int		 argi, chdir_status, getcwd_status, im;
+
+	argi = 0;
+	if ((e = exprcomp(search, argc, argv, &argi)) == NULL) {
+		*sz = 0;
+		return 0;
+	}
+
+	cur = maxres = 0;
+	if (res != NULL)
+		*res = NULL;
+
+	outkey = KEY_Nd;
+	if (search->outkey != NULL)
+		for (im = 0; im < KEY_MAX; im++)
+			if (0 == strcasecmp(search->outkey,
+			    mansearch_keynames[im])) {
+				outkey = im;
+				break;
+			}
+
+	/*
+	 * Remember the original working directory, if possible.
+	 * This will be needed if the second or a later directory
+	 * is given as a relative path.
+	 * Do not error out if the current directory is not
+	 * searchable: Maybe it won't be needed after all.
+	 */
+
+	if (getcwd(buf, PATH_MAX) == NULL) {
+		getcwd_status = 0;
+		(void)strlcpy(buf, strerror(errno), sizeof(buf));
+	} else
+		getcwd_status = 1;
+
+	/*
+	 * Loop over the directories (containing databases) for us to
+	 * search.
+	 * Don't let missing/bad databases/directories phase us.
+	 * In each, try to open the resident database and, if it opens,
+	 * scan it for our match expression.
+	 */
+
+	chdir_status = 0;
+	for (i = 0; i < paths->sz; i++) {
+		if (chdir_status && paths->paths[i][0] != '/') {
+			if ( ! getcwd_status) {
+				warnx("%s: getcwd: %s", paths->paths[i], buf);
+				continue;
+			} else if (chdir(buf) == -1) {
+				warn("%s", buf);
+				continue;
+			}
+		}
+		if (chdir(paths->paths[i]) == -1) {
+			warn("%s", paths->paths[i]);
+			continue;
+		}
+		chdir_status = 1;
+
+		if (dbm_open(MANDOC_DB) == -1) {
+			if (errno != ENOENT)
+				warn("%s/%s", paths->paths[i], MANDOC_DB);
+			continue;
+		}
+
+		if ((htab = manmerge(e, NULL)) == NULL) {
+			dbm_close();
+			continue;
+		}
+
+		for (rp = ohash_first(htab, &slot); rp != NULL;
+		    rp = ohash_next(htab, &slot)) {
+			page = dbm_page_get(rp->page);
+
+			if (lstmatch(search->sec, page->sect) == 0 ||
+			    lstmatch(search->arch, page->arch) == 0 ||
+			    (search->argmode == ARG_NAME &&
+			     rp->bits <= (int32_t)(NAME_SYN & NAME_MASK)))
+				continue;
+
+			if (res == NULL) {
+				cur = 1;
+				break;
+			}
+			if (cur + 1 > maxres) {
+				maxres += 1024;
+				*res = mandoc_reallocarray(*res,
+				    maxres, sizeof(**res));
+			}
+			mpage = *res + cur;
+			mandoc_asprintf(&mpage->file, "%s/%s",
+			    paths->paths[i], page->file + 1);
+			if (access(chdir_status ? page->file + 1 :
+			    mpage->file, R_OK) == -1) {
+				warn("%s", mpage->file);
+				warnx("outdated mandoc.db contains "
+				    "bogus %s entry, run makewhatis %s",
+				    page->file + 1, paths->paths[i]);
+				free(mpage->file);
+				free(rp);
+				continue;
+			}
+			mpage->names = buildnames(page);
+			mpage->output = buildoutput(outkey, page);
+			mpage->bits = search->firstmatch ? rp->bits : 0;
+			mpage->ipath = i;
+			mpage->sec = *page->sect - '0';
+			if (mpage->sec < 0 || mpage->sec > 9)
+				mpage->sec = 10;
+			mpage->form = *page->file;
+			free(rp);
+			cur++;
+		}
+		ohash_delete(htab);
+		free(htab);
+		dbm_close();
+
+		/*
+		 * In man(1) mode, prefer matches in earlier trees
+		 * over matches in later trees.
+		 */
+
+		if (cur && search->firstmatch)
+			break;
+	}
+	if (res != NULL)
+		qsort(*res, cur, sizeof(struct manpage), manpage_compare);
+	if (chdir_status && getcwd_status && chdir(buf) == -1)
+		warn("%s", buf);
+	exprfree(e);
+	*sz = cur;
+	return res != NULL || cur;
+}
+
+/*
+ * Merge the results for the expression tree rooted at e
+ * into the the result list htab.
+ */
+static struct ohash *
+manmerge(struct expr *e, struct ohash *htab)
+{
+	switch (e->type) {
+	case EXPR_TERM:
+		return manmerge_term(e, htab);
+	case EXPR_OR:
+		return manmerge_or(e->child, htab);
+	case EXPR_AND:
+		return manmerge_and(e->child, htab);
+	default:
+		abort();
+	}
+}
+
+static struct ohash *
+manmerge_term(struct expr *e, struct ohash *htab)
+{
+	struct dbm_res	 res, *rp;
+	uint64_t	 ib;
+	unsigned int	 slot;
+	int		 im;
+
+	if (htab == NULL) {
+		htab = mandoc_malloc(sizeof(*htab));
+		mandoc_ohash_init(htab, 4, offsetof(struct dbm_res, page));
+	}
+
+	for (im = 0, ib = 1; im < KEY_MAX; im++, ib <<= 1) {
+		if ((e->bits & ib) == 0)
+			continue;
+
+		switch (ib) {
+		case TYPE_arch:
+			dbm_page_byarch(&e->match);
+			break;
+		case TYPE_sec:
+			dbm_page_bysect(&e->match);
+			break;
+		case TYPE_Nm:
+			dbm_page_byname(&e->match);
+			break;
+		case TYPE_Nd:
+			dbm_page_bydesc(&e->match);
+			break;
+		default:
+			dbm_page_bymacro(im - 2, &e->match);
+			break;
+		}
+
+		/*
+		 * When hashing for deduplication, use the unique
+		 * page ID itself instead of a hash function;
+		 * that is quite efficient.
+		 */
+
+		for (;;) {
+			res = dbm_page_next();
+			if (res.page == -1)
+				break;
+			slot = ohash_lookup_memory(htab,
+			    (char *)&res, sizeof(res.page), res.page);
+			if ((rp = ohash_find(htab, slot)) != NULL) {
+				rp->bits |= res.bits;
+				continue;
+			}
+			rp = mandoc_malloc(sizeof(*rp));
+			*rp = res;
+			ohash_insert(htab, slot, rp);
+		}
+	}
+	return htab;
+}
+
+static struct ohash *
+manmerge_or(struct expr *e, struct ohash *htab)
+{
+	while (e != NULL) {
+		htab = manmerge(e, htab);
+		e = e->next;
+	}
+	return htab;
+}
+
+static struct ohash *
+manmerge_and(struct expr *e, struct ohash *htab)
+{
+	struct ohash	*hand, *h1, *h2;
+	struct dbm_res	*res;
+	unsigned int	 slot1, slot2;
+
+	/* Evaluate the first term of the AND clause. */
+
+	hand = manmerge(e, NULL);
+
+	while ((e = e->next) != NULL) {
+
+		/* Evaluate the next term and prepare for ANDing. */
+
+		h2 = manmerge(e, NULL);
+		if (ohash_entries(h2) < ohash_entries(hand)) {
+			h1 = h2;
+			h2 = hand;
+		} else
+			h1 = hand;
+		hand = mandoc_malloc(sizeof(*hand));
+		mandoc_ohash_init(hand, 4, offsetof(struct dbm_res, page));
+
+		/* Keep all pages that are in both result sets. */
+
+		for (res = ohash_first(h1, &slot1); res != NULL;
+		    res = ohash_next(h1, &slot1)) {
+			if (ohash_find(h2, ohash_lookup_memory(h2,
+			    (char *)res, sizeof(res->page),
+			    res->page)) == NULL)
+				free(res);
+			else
+				ohash_insert(hand, ohash_lookup_memory(hand,
+				    (char *)res, sizeof(res->page),
+				    res->page), res);
+		}
+
+		/* Discard the merged results. */
+
+		for (res = ohash_first(h2, &slot2); res != NULL;
+		    res = ohash_next(h2, &slot2))
+			free(res);
+		ohash_delete(h2);
+		free(h2);
+		ohash_delete(h1);
+		free(h1);
+	}
+
+	/* Merge the result of the AND into htab. */
+
+	if (htab == NULL)
+		return hand;
+
+	for (res = ohash_first(hand, &slot1); res != NULL;
+	    res = ohash_next(hand, &slot1)) {
+		slot2 = ohash_lookup_memory(htab,
+		    (char *)res, sizeof(res->page), res->page);
+		if (ohash_find(htab, slot2) == NULL)
+			ohash_insert(htab, slot2, res);
+		else
+			free(res);
+	}
+
+	/* Discard the merged result. */
+
+	ohash_delete(hand);
+	free(hand);
+	return htab;
+}
+
+void
+mansearch_free(struct manpage *res, size_t sz)
+{
+	size_t	 i;
+
+	for (i = 0; i < sz; i++) {
+		free(res[i].file);
+		free(res[i].names);
+		free(res[i].output);
+	}
+	free(res);
+}
+
+static int
+manpage_compare(const void *vp1, const void *vp2)
+{
+	const struct manpage	*mp1, *mp2;
+	const char		*cp1, *cp2;
+	size_t			 sz1, sz2;
+	int			 diff;
+
+	mp1 = vp1;
+	mp2 = vp2;
+	if ((diff = mp2->bits - mp1->bits) ||
+	    (diff = mp1->sec - mp2->sec))
+		return diff;
+
+	/* Fall back to alphabetic ordering of names. */
+	sz1 = strcspn(mp1->names, "(");
+	sz2 = strcspn(mp2->names, "(");
+	if (sz1 < sz2)
+		sz1 = sz2;
+	if ((diff = strncasecmp(mp1->names, mp2->names, sz1)))
+		return diff;
+
+	/* For identical names and sections, prefer arch-dependent. */
+	cp1 = strchr(mp1->names + sz1, '/');
+	cp2 = strchr(mp2->names + sz2, '/');
+	return cp1 != NULL && cp2 != NULL ? strcasecmp(cp1, cp2) :
+	    cp1 != NULL ? -1 : cp2 != NULL ? 1 : 0;
+}
+
+static char *
+buildnames(const struct dbm_page *page)
+{
+	char	*buf;
+	size_t	 i, sz;
+
+	sz = lstlen(page->name, 2) + 1 + lstlen(page->sect, 2) +
+	    (page->arch == NULL ? 0 : 1 + lstlen(page->arch, 2)) + 2;
+	buf = mandoc_malloc(sz);
+	i = 0;
+	lstcat(buf, &i, page->name, ", ");
+	buf[i++] = '(';
+	lstcat(buf, &i, page->sect, ", ");
+	if (page->arch != NULL) {
+		buf[i++] = '/';
+		lstcat(buf, &i, page->arch, ", ");
+	}
+	buf[i++] = ')';
+	buf[i++] = '\0';
+	assert(i == sz);
+	return buf;
+}
+
+/*
+ * Count the buffer space needed to print the NUL-terminated
+ * list of NUL-terminated strings, when printing sep separator
+ * characters between strings.
+ */
+static size_t
+lstlen(const char *cp, size_t sep)
+{
+	size_t	 sz;
+
+	for (sz = 0; *cp != '\0'; cp++) {
+
+		/* Skip names appearing only in the SYNOPSIS. */
+		if (*cp <= (char)(NAME_SYN & NAME_MASK)) {
+			while (*cp != '\0')
+				cp++;
+			continue;
+		}
+
+		/* Skip name class markers. */
+		if (*cp < ' ')
+			cp++;
+
+		/* Print a separator before each but the first string. */
+		if (sz)
+			sz += sep;
+
+		/* Copy one string. */
+		while (*cp != '\0') {
+			sz++;
+			cp++;
+		}
+	}
+	return sz;
+}
+
+/*
+ * Print the NUL-terminated list of NUL-terminated strings
+ * into the buffer, seperating strings with sep.
+ */
+static void
+lstcat(char *buf, size_t *i, const char *cp, const char *sep)
+{
+	const char	*s;
+	size_t		 i_start;
+
+	for (i_start = *i; *cp != '\0'; cp++) {
+
+		/* Skip names appearing only in the SYNOPSIS. */
+		if (*cp <= (char)(NAME_SYN & NAME_MASK)) {
+			while (*cp != '\0')
+				cp++;
+			continue;
+		}
+
+		/* Skip name class markers. */
+		if (*cp < ' ')
+			cp++;
+
+		/* Print a separator before each but the first string. */
+		if (*i > i_start) {
+			s = sep;
+			while (*s != '\0')
+				buf[(*i)++] = *s++;
+		}
+
+		/* Copy one string. */
+		while (*cp != '\0')
+			buf[(*i)++] = *cp++;
+	}
+
+}
+
+/*
+ * Return 1 if the string *want occurs in any of the strings
+ * in the NUL-terminated string list *have, or 0 otherwise.
+ * If either argument is NULL or empty, assume no filtering
+ * is desired and return 1.
+ */
+static int
+lstmatch(const char *want, const char *have)
+{
+        if (want == NULL || have == NULL || *have == '\0')
+                return 1;
+        while (*have != '\0') {
+                if (strcasestr(have, want) != NULL)
+                        return 1;
+                have = strchr(have, '\0') + 1;
+        }
+        return 0;
+}
+
+/*
+ * Build a list of values taken by the macro im in the manual page.
+ */
+static char *
+buildoutput(size_t im, struct dbm_page *page)
+{
+	const char	*oldoutput, *sep, *input;
+	char		*output, *newoutput, *value;
+	size_t		 sz, i;
+
+	switch (im) {
+	case KEY_Nd:
+		return mandoc_strdup(page->desc);
+	case KEY_Nm:
+		input = page->name;
+		break;
+	case KEY_sec:
+		input = page->sect;
+		break;
+	case KEY_arch:
+		input = page->arch;
+		if (input == NULL)
+			input = "all\0";
+		break;
+	default:
+		input = NULL;
+		break;
+	}
+
+	if (input != NULL) {
+		sz = lstlen(input, 3) + 1;
+		output = mandoc_malloc(sz);
+		i = 0;
+		lstcat(output, &i, input, " # ");
+		output[i++] = '\0';
+		assert(i == sz);
+		return output;
+	}
+
+	output = NULL;
+	dbm_macro_bypage(im - 2, page->addr);
+	while ((value = dbm_macro_next()) != NULL) {
+		if (output == NULL) {
+			oldoutput = "";
+			sep = "";
+		} else {
+			oldoutput = output;
+			sep = " # ";
+		}
+		mandoc_asprintf(&newoutput, "%s%s%s", oldoutput, sep, value);
+		free(output);
+		output = newoutput;
+	}
+	return output;
+}
+
+/*
+ * Compile a set of string tokens into an expression.
+ * Tokens in "argv" are assumed to be individual expression atoms (e.g.,
+ * "(", "foo=bar", etc.).
+ */
+static struct expr *
+exprcomp(const struct mansearch *search, int argc, char *argv[], int *argi)
+{
+	struct expr	*parent, *child;
+	int		 needterm, nested;
+
+	if ((nested = *argi) == argc)
+		return NULL;
+	needterm = 1;
+	parent = child = NULL;
+	while (*argi < argc) {
+		if (strcmp(")", argv[*argi]) == 0) {
+			if (needterm)
+				warnx("missing term "
+				    "before closing parenthesis");
+			needterm = 0;
+			if (nested)
+				break;
+			warnx("ignoring unmatched right parenthesis");
+			++*argi;
+			continue;
+		}
+		if (strcmp("-o", argv[*argi]) == 0) {
+			if (needterm) {
+				if (*argi > 0)
+					warnx("ignoring -o after %s",
+					    argv[*argi - 1]);
+				else
+					warnx("ignoring initial -o");
+			}
+			needterm = 1;
+			++*argi;
+			continue;
+		}
+		needterm = 0;
+		if (child == NULL) {
+			child = expr_and(search, argc, argv, argi);
+			continue;
+		}
+		if (parent == NULL) {
+			parent = mandoc_calloc(1, sizeof(*parent));
+			parent->type = EXPR_OR;
+			parent->next = NULL;
+			parent->child = child;
+		}
+		child->next = expr_and(search, argc, argv, argi);
+		child = child->next;
+	}
+	if (needterm && *argi)
+		warnx("ignoring trailing %s", argv[*argi - 1]);
+	return parent == NULL ? child : parent;
+}
+
+static struct expr *
+expr_and(const struct mansearch *search, int argc, char *argv[], int *argi)
+{
+	struct expr	*parent, *child;
+	int		 needterm;
+
+	needterm = 1;
+	parent = child = NULL;
+	while (*argi < argc) {
+		if (strcmp(")", argv[*argi]) == 0) {
+			if (needterm)
+				warnx("missing term "
+				    "before closing parenthesis");
+			needterm = 0;
+			break;
+		}
+		if (strcmp("-o", argv[*argi]) == 0)
+			break;
+		if (strcmp("-a", argv[*argi]) == 0) {
+			if (needterm) {
+				if (*argi > 0)
+					warnx("ignoring -a after %s",
+					    argv[*argi - 1]);
+				else
+					warnx("ignoring initial -a");
+			}
+			needterm = 1;
+			++*argi;
+			continue;
+		}
+		if (needterm == 0)
+			break;
+		if (child == NULL) {
+			child = exprterm(search, argc, argv, argi);
+			if (child != NULL)
+				needterm = 0;
+			continue;
+		}
+		needterm = 0;
+		if (parent == NULL) {
+			parent = mandoc_calloc(1, sizeof(*parent));
+			parent->type = EXPR_AND;
+			parent->next = NULL;
+			parent->child = child;
+		}
+		child->next = exprterm(search, argc, argv, argi);
+		if (child->next != NULL) {
+			child = child->next;
+			needterm = 0;
+		}
+	}
+	if (needterm && *argi)
+		warnx("ignoring trailing %s", argv[*argi - 1]);
+	return parent == NULL ? child : parent;
+}
+
+static struct expr *
+exprterm(const struct mansearch *search, int argc, char *argv[], int *argi)
+{
+	char		 errbuf[BUFSIZ];
+	struct expr	*e;
+	char		*key, *val;
+	uint64_t	 iterbit;
+	int		 cs, i, irc;
+
+	if (strcmp("(", argv[*argi]) == 0) {
+		++*argi;
+		e = exprcomp(search, argc, argv, argi);
+		if (*argi < argc) {
+			assert(strcmp(")", argv[*argi]) == 0);
+			++*argi;
+		} else
+			warnx("unclosed parenthesis");
+		return e;
+	}
+
+	if (strcmp("-i", argv[*argi]) == 0 && *argi + 1 < argc) {
+		cs = 0;
+		++*argi;
+	} else
+		cs = 1;
+
+	e = mandoc_calloc(1, sizeof(*e));
+	e->type = EXPR_TERM;
+	e->bits = 0;
+	e->next = NULL;
+	e->child = NULL;
+
+	if (search->argmode == ARG_NAME) {
+		e->bits = TYPE_Nm;
+		e->match.type = DBM_EXACT;
+		e->match.str = argv[(*argi)++];
+		return e;
+	}
+
+	/*
+	 * Separate macro keys from search string.
+	 * If needed, request regular expression handling.
+	 */
+
+	if (search->argmode == ARG_WORD) {
+		e->bits = TYPE_Nm;
+		e->match.type = DBM_REGEX;
+		mandoc_asprintf(&val, "[[:<:]]%s[[:>:]]", argv[*argi]);
+		cs = 0;
+	} else if ((val = strpbrk(argv[*argi], "=~")) == NULL) {
+		e->bits = TYPE_Nm | TYPE_Nd;
+		e->match.type = DBM_REGEX;
+		val = argv[*argi];
+		cs = 0;
+	} else {
+		if (val == argv[*argi])
+			e->bits = TYPE_Nm | TYPE_Nd;
+		if (*val == '=') {
+			e->match.type = DBM_SUB;
+			e->match.str = val + 1;
+		} else
+			e->match.type = DBM_REGEX;
+		*val++ = '\0';
+		if (strstr(argv[*argi], "arch") != NULL)
+			cs = 0;
+	}
+
+	/* Compile regular expressions. */
+
+	if (e->match.type == DBM_REGEX) {
+		e->match.re = mandoc_malloc(sizeof(*e->match.re));
+		irc = regcomp(e->match.re, val,
+		    REG_EXTENDED | REG_NOSUB | (cs ? 0 : REG_ICASE));
+		if (irc) {
+			regerror(irc, e->match.re, errbuf, sizeof(errbuf));
+			warnx("regcomp /%s/: %s", val, errbuf);
+		}
+		if (search->argmode == ARG_WORD)
+			free(val);
+		if (irc) {
+			free(e->match.re);
+			free(e);
+			++*argi;
+			return NULL;
+		}
+	}
+
+	if (e->bits) {
+		++*argi;
+		return e;
+	}
+
+	/*
+	 * Parse out all possible fields.
+	 * If the field doesn't resolve, bail.
+	 */
+
+	while (NULL != (key = strsep(&argv[*argi], ","))) {
+		if ('\0' == *key)
+			continue;
+		for (i = 0, iterbit = 1; i < KEY_MAX; i++, iterbit <<= 1) {
+			if (0 == strcasecmp(key, mansearch_keynames[i])) {
+				e->bits |= iterbit;
+				break;
+			}
+		}
+		if (i == KEY_MAX) {
+			if (strcasecmp(key, "any"))
+				warnx("treating unknown key "
+				    "\"%s\" as \"any\"", key);
+			e->bits |= ~0ULL;
+		}
+	}
+
+	++*argi;
+	return e;
+}
+
+static void
+exprfree(struct expr *e)
+{
+	if (e->next != NULL)
+		exprfree(e->next);
+	if (e->child != NULL)
+		exprfree(e->child);
+	free(e);
+}
diff --git a/usr.bin/mandoc/mansearch.h b/usr.bin/mandoc/mansearch.h
new file mode 100644
index 0000000..c2efe7c
--- /dev/null
+++ b/usr.bin/mandoc/mansearch.h
@@ -0,0 +1,118 @@
+/*	$OpenBSD: mansearch.h,v 1.24 2019/04/30 18:48:26 schwarze Exp $ */
+/*
+ * Copyright (c) 2012 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2013, 2014, 2016, 2017 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ */
+
+#define	MANDOC_DB	 "mandoc.db"
+#define	MANDOCDB_MAGIC	 0x3a7d0cdb
+#define	MANDOCDB_VERSION 1
+
+#define	MACRO_MAX	 36
+#define	KEY_arch	 0
+#define	KEY_sec		 1
+#define	KEY_Nm		 38
+#define	KEY_Nd		 39
+#define	KEY_MAX		 40
+
+#define	TYPE_arch	 0x0000000000000001ULL
+#define	TYPE_sec	 0x0000000000000002ULL
+#define	TYPE_Xr		 0x0000000000000004ULL
+#define	TYPE_Ar		 0x0000000000000008ULL
+#define	TYPE_Fa		 0x0000000000000010ULL
+#define	TYPE_Fl		 0x0000000000000020ULL
+#define	TYPE_Dv		 0x0000000000000040ULL
+#define	TYPE_Fn		 0x0000000000000080ULL
+#define	TYPE_Ic		 0x0000000000000100ULL
+#define	TYPE_Pa		 0x0000000000000200ULL
+#define	TYPE_Cm		 0x0000000000000400ULL
+#define	TYPE_Li		 0x0000000000000800ULL
+#define	TYPE_Em		 0x0000000000001000ULL
+#define	TYPE_Cd		 0x0000000000002000ULL
+#define	TYPE_Va		 0x0000000000004000ULL
+#define	TYPE_Ft		 0x0000000000008000ULL
+#define	TYPE_Tn		 0x0000000000010000ULL
+#define	TYPE_Er		 0x0000000000020000ULL
+#define	TYPE_Ev		 0x0000000000040000ULL
+#define	TYPE_Sy		 0x0000000000080000ULL
+#define	TYPE_Sh		 0x0000000000100000ULL
+#define	TYPE_In		 0x0000000000200000ULL
+#define	TYPE_Ss		 0x0000000000400000ULL
+#define	TYPE_Ox		 0x0000000000800000ULL
+#define	TYPE_An		 0x0000000001000000ULL
+#define	TYPE_Mt		 0x0000000002000000ULL
+#define	TYPE_St		 0x0000000004000000ULL
+#define	TYPE_Bx		 0x0000000008000000ULL
+#define	TYPE_At		 0x0000000010000000ULL
+#define	TYPE_Nx		 0x0000000020000000ULL
+#define	TYPE_Fx		 0x0000000040000000ULL
+#define	TYPE_Lk		 0x0000000080000000ULL
+#define	TYPE_Ms		 0x0000000100000000ULL
+#define	TYPE_Bsx	 0x0000000200000000ULL
+#define	TYPE_Dx		 0x0000000400000000ULL
+#define	TYPE_Rs		 0x0000000800000000ULL
+#define	TYPE_Vt		 0x0000001000000000ULL
+#define	TYPE_Lb		 0x0000002000000000ULL
+#define	TYPE_Nm		 0x0000004000000000ULL
+#define	TYPE_Nd		 0x0000008000000000ULL
+
+#define	NAME_SYN	 0x0000004000000001ULL
+#define	NAME_FIRST	 0x0000004000000004ULL
+#define	NAME_TITLE	 0x0000004000000006ULL
+#define	NAME_HEAD	 0x0000004000000008ULL
+#define	NAME_FILE	 0x0000004000000010ULL
+#define	NAME_MASK	 0x000000000000001fULL
+
+enum	form {
+	FORM_SRC = 1,	/* Format is mdoc(7) or man(7). */
+	FORM_CAT,	/* Manual page is preformatted. */
+	FORM_NONE	/* Format is unknown. */
+};
+
+enum	argmode {
+	ARG_FILE = 0,
+	ARG_NAME,
+	ARG_WORD,
+	ARG_EXPR
+};
+
+struct	manpage {
+	char		*file; /* to be prefixed by manpath */
+	char		*names; /* a list of names with sections */
+	char		*output; /* user-defined additional output */
+	uint64_t	 bits; /* name type mask */
+	size_t		 ipath; /* number of the manpath */
+	int		 sec; /* section number, 10 means invalid */
+	enum form	 form;
+};
+
+struct	mansearch {
+	const char	*arch; /* architecture/NULL */
+	const char	*sec; /* mansection/NULL */
+	const char	*outkey; /* show content of this macro */
+	enum argmode	 argmode; /* interpretation of arguments */
+	int		 firstmatch; /* first matching database only */
+};
+
+
+struct	manpaths;
+
+int	mansearch(const struct mansearch *cfg, /* options */
+		const struct manpaths *paths, /* manpaths */
+		int argc, /* size of argv */
+		char *argv[],  /* search terms */
+		struct manpage **res, /* results */
+		size_t *ressz); /* results returned */
+void	mansearch_free(struct manpage *, size_t);
diff --git a/usr.bin/mandoc/mdoc.c b/usr.bin/mandoc/mdoc.c
new file mode 100644
index 0000000..7920985
--- /dev/null
+++ b/usr.bin/mandoc/mdoc.c
@@ -0,0 +1,431 @@
+/* $OpenBSD: mdoc.c,v 1.164 2020/04/06 09:55:49 schwarze Exp $ */
+/*
+ * Copyright (c) 2010, 2012-2018, 2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ *
+ * Top level and utility functions of the mdoc(7) parser for mandoc(1).
+ */
+#include <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "mandoc_aux.h"
+#include "mandoc.h"
+#include "roff.h"
+#include "mdoc.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+#include "libmdoc.h"
+
+const	char *const __mdoc_argnames[MDOC_ARG_MAX] = {
+	"split",		"nosplit",		"ragged",
+	"unfilled",		"literal",		"file",
+	"offset",		"bullet",		"dash",
+	"hyphen",		"item",			"enum",
+	"tag",			"diag",			"hang",
+	"ohang",		"inset",		"column",
+	"width",		"compact",		"std",
+	"filled",		"words",		"emphasis",
+	"symbolic",		"nested",		"centered"
+};
+const	char * const *mdoc_argnames = __mdoc_argnames;
+
+static	int		  mdoc_ptext(struct roff_man *, int, char *, int);
+static	int		  mdoc_pmacro(struct roff_man *, int, char *, int);
+
+
+/*
+ * Main parse routine.  Parses a single line -- really just hands off to
+ * the macro (mdoc_pmacro()) or text parser (mdoc_ptext()).
+ */
+int
+mdoc_parseln(struct roff_man *mdoc, int ln, char *buf, int offs)
+{
+
+	if (mdoc->last->type != ROFFT_EQN || ln > mdoc->last->line)
+		mdoc->flags |= MDOC_NEWLINE;
+
+	/*
+	 * Let the roff nS register switch SYNOPSIS mode early,
+	 * such that the parser knows at all times
+	 * whether this mode is on or off.
+	 * Note that this mode is also switched by the Sh macro.
+	 */
+	if (roff_getreg(mdoc->roff, "nS"))
+		mdoc->flags |= MDOC_SYNOPSIS;
+	else
+		mdoc->flags &= ~MDOC_SYNOPSIS;
+
+	return roff_getcontrol(mdoc->roff, buf, &offs) ?
+	    mdoc_pmacro(mdoc, ln, buf, offs) :
+	    mdoc_ptext(mdoc, ln, buf, offs);
+}
+
+void
+mdoc_tail_alloc(struct roff_man *mdoc, int line, int pos, enum roff_tok tok)
+{
+	struct roff_node *p;
+
+	p = roff_node_alloc(mdoc, line, pos, ROFFT_TAIL, tok);
+	roff_node_append(mdoc, p);
+	mdoc->next = ROFF_NEXT_CHILD;
+}
+
+struct roff_node *
+mdoc_endbody_alloc(struct roff_man *mdoc, int line, int pos,
+    enum roff_tok tok, struct roff_node *body)
+{
+	struct roff_node *p;
+
+	body->flags |= NODE_ENDED;
+	body->parent->flags |= NODE_ENDED;
+	p = roff_node_alloc(mdoc, line, pos, ROFFT_BODY, tok);
+	p->body = body;
+	p->norm = body->norm;
+	p->end = ENDBODY_SPACE;
+	roff_node_append(mdoc, p);
+	mdoc->next = ROFF_NEXT_SIBLING;
+	return p;
+}
+
+struct roff_node *
+mdoc_block_alloc(struct roff_man *mdoc, int line, int pos,
+    enum roff_tok tok, struct mdoc_arg *args)
+{
+	struct roff_node *p;
+
+	p = roff_node_alloc(mdoc, line, pos, ROFFT_BLOCK, tok);
+	p->args = args;
+	if (p->args)
+		(args->refcnt)++;
+
+	switch (tok) {
+	case MDOC_Bd:
+	case MDOC_Bf:
+	case MDOC_Bl:
+	case MDOC_En:
+	case MDOC_Rs:
+		p->norm = mandoc_calloc(1, sizeof(union mdoc_data));
+		break;
+	default:
+		break;
+	}
+	roff_node_append(mdoc, p);
+	mdoc->next = ROFF_NEXT_CHILD;
+	return p;
+}
+
+void
+mdoc_elem_alloc(struct roff_man *mdoc, int line, int pos,
+     enum roff_tok tok, struct mdoc_arg *args)
+{
+	struct roff_node *p;
+
+	p = roff_node_alloc(mdoc, line, pos, ROFFT_ELEM, tok);
+	p->args = args;
+	if (p->args)
+		(args->refcnt)++;
+
+	switch (tok) {
+	case MDOC_An:
+		p->norm = mandoc_calloc(1, sizeof(union mdoc_data));
+		break;
+	default:
+		break;
+	}
+	roff_node_append(mdoc, p);
+	mdoc->next = ROFF_NEXT_CHILD;
+}
+
+/*
+ * Parse free-form text, that is, a line that does not begin with the
+ * control character.
+ */
+static int
+mdoc_ptext(struct roff_man *mdoc, int line, char *buf, int offs)
+{
+	struct roff_node *n;
+	const char	 *cp, *sp;
+	char		 *c, *ws, *end;
+
+	n = mdoc->last;
+
+	/*
+	 * If a column list contains plain text, assume an implicit item
+	 * macro.  This can happen one or more times at the beginning
+	 * of such a list, intermixed with non-It mdoc macros and with
+	 * nodes generated on the roff level, for example by tbl.
+	 */
+
+	if ((n->tok == MDOC_Bl && n->type == ROFFT_BODY &&
+	     n->end == ENDBODY_NOT && n->norm->Bl.type == LIST_column) ||
+	    (n->parent != NULL && n->parent->tok == MDOC_Bl &&
+	     n->parent->norm->Bl.type == LIST_column)) {
+		mdoc->flags |= MDOC_FREECOL;
+		(*mdoc_macro(MDOC_It)->fp)(mdoc, MDOC_It,
+		    line, offs, &offs, buf);
+		return 1;
+	}
+
+	/*
+	 * Search for the beginning of unescaped trailing whitespace (ws)
+	 * and for the first character not to be output (end).
+	 */
+
+	/* FIXME: replace with strcspn(). */
+	ws = NULL;
+	for (c = end = buf + offs; *c; c++) {
+		switch (*c) {
+		case ' ':
+			if (NULL == ws)
+				ws = c;
+			continue;
+		case '\t':
+			/*
+			 * Always warn about trailing tabs,
+			 * even outside literal context,
+			 * where they should be put on the next line.
+			 */
+			if (NULL == ws)
+				ws = c;
+			/*
+			 * Strip trailing tabs in literal context only;
+			 * outside, they affect the next line.
+			 */
+			if (mdoc->flags & ROFF_NOFILL)
+				continue;
+			break;
+		case '\\':
+			/* Skip the escaped character, too, if any. */
+			if (c[1])
+				c++;
+			/* FALLTHROUGH */
+		default:
+			ws = NULL;
+			break;
+		}
+		end = c + 1;
+	}
+	*end = '\0';
+
+	if (ws)
+		mandoc_msg(MANDOCERR_SPACE_EOL, line, (int)(ws - buf), NULL);
+
+	/*
+	 * Blank lines are allowed in no-fill mode
+	 * and cancel preceding \c,
+	 * but add a single vertical space elsewhere.
+	 */
+
+	if (buf[offs] == '\0' && (mdoc->flags & ROFF_NOFILL) == 0) {
+		switch (mdoc->last->type) {
+		case ROFFT_TEXT:
+			sp = mdoc->last->string;
+			cp = end = strchr(sp, '\0') - 2;
+			if (cp < sp || cp[0] != '\\' || cp[1] != 'c')
+				break;
+			while (cp > sp && cp[-1] == '\\')
+				cp--;
+			if ((end - cp) % 2)
+				break;
+			*end = '\0';
+			return 1;
+		default:
+			break;
+		}
+		mandoc_msg(MANDOCERR_FI_BLANK, line, (int)(c - buf), NULL);
+		roff_elem_alloc(mdoc, line, offs, ROFF_sp);
+		mdoc->last->flags |= NODE_VALID | NODE_ENDED;
+		mdoc->next = ROFF_NEXT_SIBLING;
+		return 1;
+	}
+
+	roff_word_alloc(mdoc, line, offs, buf+offs);
+
+	if (mdoc->flags & ROFF_NOFILL)
+		return 1;
+
+	/*
+	 * End-of-sentence check.  If the last character is an unescaped
+	 * EOS character, then flag the node as being the end of a
+	 * sentence.  The front-end will know how to interpret this.
+	 */
+
+	assert(buf < end);
+
+	if (mandoc_eos(buf+offs, (size_t)(end-buf-offs)))
+		mdoc->last->flags |= NODE_EOS;
+
+	for (c = buf + offs; c != NULL; c = strchr(c + 1, '.')) {
+		if (c - buf < offs + 2)
+			continue;
+		if (end - c < 3)
+			break;
+		if (c[1] != ' ' ||
+		    isalnum((unsigned char)c[-2]) == 0 ||
+		    isalnum((unsigned char)c[-1]) == 0 ||
+		    (c[-2] == 'n' && c[-1] == 'c') ||
+		    (c[-2] == 'v' && c[-1] == 's'))
+			continue;
+		c += 2;
+		if (*c == ' ')
+			c++;
+		if (*c == ' ')
+			c++;
+		if (isupper((unsigned char)(*c)))
+			mandoc_msg(MANDOCERR_EOS, line, (int)(c - buf), NULL);
+	}
+
+	return 1;
+}
+
+/*
+ * Parse a macro line, that is, a line beginning with the control
+ * character.
+ */
+static int
+mdoc_pmacro(struct roff_man *mdoc, int ln, char *buf, int offs)
+{
+	struct roff_node *n;
+	const char	 *cp;
+	size_t		  sz;
+	enum roff_tok	  tok;
+	int		  sv;
+
+	/* Determine the line macro. */
+
+	sv = offs;
+	tok = TOKEN_NONE;
+	for (sz = 0; sz < 4 && strchr(" \t\\", buf[offs]) == NULL; sz++)
+		offs++;
+	if (sz == 2 || sz == 3)
+		tok = roffhash_find(mdoc->mdocmac, buf + sv, sz);
+	if (tok == TOKEN_NONE) {
+		mandoc_msg(MANDOCERR_MACRO, ln, sv, "%s", buf + sv - 1);
+		return 1;
+	}
+
+	/* Skip a leading escape sequence or tab. */
+
+	switch (buf[offs]) {
+	case '\\':
+		cp = buf + offs + 1;
+		mandoc_escape(&cp, NULL, NULL);
+		offs = cp - buf;
+		break;
+	case '\t':
+		offs++;
+		break;
+	default:
+		break;
+	}
+
+	/* Jump to the next non-whitespace word. */
+
+	while (buf[offs] == ' ')
+		offs++;
+
+	/*
+	 * Trailing whitespace.  Note that tabs are allowed to be passed
+	 * into the parser as "text", so we only warn about spaces here.
+	 */
+
+	if ('\0' == buf[offs] && ' ' == buf[offs - 1])
+		mandoc_msg(MANDOCERR_SPACE_EOL, ln, offs - 1, NULL);
+
+	/*
+	 * If an initial or transparent macro or a list invocation,
+	 * divert directly into macro processing.
+	 */
+
+	n = mdoc->last;
+	if (n == NULL || tok == MDOC_It || tok == MDOC_El ||
+	    roff_tok_transparent(tok)) {
+		(*mdoc_macro(tok)->fp)(mdoc, tok, ln, sv, &offs, buf);
+		return 1;
+	}
+
+	/*
+	 * If a column list contains a non-It macro, assume an implicit
+	 * item macro.  This can happen one or more times at the
+	 * beginning of such a list, intermixed with text lines and
+	 * with nodes generated on the roff level, for example by tbl.
+	 */
+
+	if ((n->tok == MDOC_Bl && n->type == ROFFT_BODY &&
+	     n->end == ENDBODY_NOT && n->norm->Bl.type == LIST_column) ||
+	    (n->parent != NULL && n->parent->tok == MDOC_Bl &&
+	     n->parent->norm->Bl.type == LIST_column)) {
+		mdoc->flags |= MDOC_FREECOL;
+		(*mdoc_macro(MDOC_It)->fp)(mdoc, MDOC_It, ln, sv, &sv, buf);
+		return 1;
+	}
+
+	/* Normal processing of a macro. */
+
+	(*mdoc_macro(tok)->fp)(mdoc, tok, ln, sv, &offs, buf);
+
+	/* In quick mode (for mandocdb), abort after the NAME section. */
+
+	if (mdoc->quick && MDOC_Sh == tok &&
+	    SEC_NAME != mdoc->last->sec)
+		return 2;
+
+	return 1;
+}
+
+enum mdelim
+mdoc_isdelim(const char *p)
+{
+
+	if ('\0' == p[0])
+		return DELIM_NONE;
+
+	if ('\0' == p[1])
+		switch (p[0]) {
+		case '(':
+		case '[':
+			return DELIM_OPEN;
+		case '|':
+			return DELIM_MIDDLE;
+		case '.':
+		case ',':
+		case ';':
+		case ':':
+		case '?':
+		case '!':
+		case ')':
+		case ']':
+			return DELIM_CLOSE;
+		default:
+			return DELIM_NONE;
+		}
+
+	if ('\\' != p[0])
+		return DELIM_NONE;
+
+	if (0 == strcmp(p + 1, "."))
+		return DELIM_CLOSE;
+	if (0 == strcmp(p + 1, "fR|\\fP"))
+		return DELIM_MIDDLE;
+
+	return DELIM_NONE;
+}
diff --git a/usr.bin/mandoc/mdoc.h b/usr.bin/mandoc/mdoc.h
new file mode 100644
index 0000000..aa4a5ec
--- /dev/null
+++ b/usr.bin/mandoc/mdoc.h
@@ -0,0 +1,158 @@
+/*	$OpenBSD: mdoc.h,v 1.71 2018/12/30 00:48:47 schwarze Exp $ */
+/*
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2015 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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	roff_node;
+struct	roff_man;
+
+enum	mdocargt {
+	MDOC_Split, /* -split */
+	MDOC_Nosplit, /* -nospli */
+	MDOC_Ragged, /* -ragged */
+	MDOC_Unfilled, /* -unfilled */
+	MDOC_Literal, /* -literal */
+	MDOC_File, /* -file */
+	MDOC_Offset, /* -offset */
+	MDOC_Bullet, /* -bullet */
+	MDOC_Dash, /* -dash */
+	MDOC_Hyphen, /* -hyphen */
+	MDOC_Item, /* -item */
+	MDOC_Enum, /* -enum */
+	MDOC_Tag, /* -tag */
+	MDOC_Diag, /* -diag */
+	MDOC_Hang, /* -hang */
+	MDOC_Ohang, /* -ohang */
+	MDOC_Inset, /* -inset */
+	MDOC_Column, /* -column */
+	MDOC_Width, /* -width */
+	MDOC_Compact, /* -compact */
+	MDOC_Std, /* -std */
+	MDOC_Filled, /* -filled */
+	MDOC_Words, /* -words */
+	MDOC_Emphasis, /* -emphasis */
+	MDOC_Symbolic, /* -symbolic */
+	MDOC_Nested, /* -nested */
+	MDOC_Centred, /* -centered */
+	MDOC_ARG_MAX
+};
+
+/*
+ * An argument to a macro (multiple values = `-column xxx yyy').
+ */
+struct	mdoc_argv {
+	enum mdocargt	  arg; /* type of argument */
+	int		  line;
+	int		  pos;
+	size_t		  sz; /* elements in "value" */
+	char		**value; /* argument strings */
+};
+
+/*
+ * Reference-counted macro arguments.  These are refcounted because
+ * blocks have multiple instances of the same arguments spread across
+ * the HEAD, BODY, TAIL, and BLOCK node types.
+ */
+struct	mdoc_arg {
+	size_t		  argc;
+	struct mdoc_argv *argv;
+	unsigned int	  refcnt;
+};
+
+enum	mdoc_list {
+	LIST__NONE = 0,
+	LIST_bullet, /* -bullet */
+	LIST_column, /* -column */
+	LIST_dash, /* -dash */
+	LIST_diag, /* -diag */
+	LIST_enum, /* -enum */
+	LIST_hang, /* -hang */
+	LIST_hyphen, /* -hyphen */
+	LIST_inset, /* -inset */
+	LIST_item, /* -item */
+	LIST_ohang, /* -ohang */
+	LIST_tag, /* -tag */
+	LIST_MAX
+};
+
+enum	mdoc_disp {
+	DISP__NONE = 0,
+	DISP_centered, /* -centered */
+	DISP_ragged, /* -ragged */
+	DISP_unfilled, /* -unfilled */
+	DISP_filled, /* -filled */
+	DISP_literal /* -literal */
+};
+
+enum	mdoc_auth {
+	AUTH__NONE = 0,
+	AUTH_split, /* -split */
+	AUTH_nosplit /* -nosplit */
+};
+
+enum	mdoc_font {
+	FONT__NONE = 0,
+	FONT_Em, /* Em, -emphasis */
+	FONT_Li, /* Li, -literal */
+	FONT_Sy /* Sy, -symbolic */
+};
+
+struct	mdoc_bd {
+	const char	 *offs; /* -offset */
+	enum mdoc_disp	  type; /* -ragged, etc. */
+	int		  comp; /* -compact */
+};
+
+struct	mdoc_bl {
+	const char	 *width; /* -width */
+	const char	 *offs; /* -offset */
+	enum mdoc_list	  type; /* -tag, -enum, etc. */
+	int		  comp; /* -compact */
+	size_t		  ncols; /* -column arg count */
+	const char	**cols; /* -column val ptr */
+	int		  count; /* -enum counter */
+};
+
+struct	mdoc_bf {
+	enum mdoc_font	  font; /* font */
+};
+
+struct	mdoc_an {
+	enum mdoc_auth	  auth; /* -split, etc. */
+};
+
+struct	mdoc_rs {
+	int		  quote_T; /* whether to quote %T */
+};
+
+/*
+ * Consists of normalised node arguments.  These should be used instead
+ * of iterating through the mdoc_arg pointers of a node: defaults are
+ * provided, etc.
+ */
+union	mdoc_data {
+	struct mdoc_an	  An;
+	struct mdoc_bd	  Bd;
+	struct mdoc_bf	  Bf;
+	struct mdoc_bl	  Bl;
+	struct roff_node *Es;
+	struct mdoc_rs	  Rs;
+};
+
+/* Names of macro args.  Index is enum mdocargt. */
+extern	const char *const *mdoc_argnames;
+
+void		 mdoc_validate(struct roff_man *);
diff --git a/usr.bin/mandoc/mdoc_argv.c b/usr.bin/mandoc/mdoc_argv.c
new file mode 100644
index 0000000..bacac34
--- /dev/null
+++ b/usr.bin/mandoc/mdoc_argv.c
@@ -0,0 +1,680 @@
+/*	$OpenBSD: mdoc_argv.c,v 1.76 2019/07/11 16:56:52 schwarze Exp $ */
+/*
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2012, 2014-2019 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "mandoc_aux.h"
+#include "mandoc.h"
+#include "roff.h"
+#include "mdoc.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+#include "libmdoc.h"
+
+#define	MULTI_STEP	 5 /* pre-allocate argument values */
+#define	DELIMSZ		 6 /* max possible size of a delimiter */
+
+enum	argsflag {
+	ARGSFL_NONE = 0,
+	ARGSFL_DELIM, /* handle delimiters of [[::delim::][ ]+]+ */
+	ARGSFL_TABSEP /* handle tab/`Ta' separated phrases */
+};
+
+enum	argvflag {
+	ARGV_NONE, /* no args to flag (e.g., -split) */
+	ARGV_SINGLE, /* one arg to flag (e.g., -file xxx)  */
+	ARGV_MULTI /* multiple args (e.g., -column xxx yyy) */
+};
+
+struct	mdocarg {
+	enum argsflag	 flags;
+	const enum mdocargt *argvs;
+};
+
+static	void		 argn_free(struct mdoc_arg *, int);
+static	enum margserr	 args(struct roff_man *, int, int *,
+				char *, enum argsflag, char **);
+static	int		 args_checkpunct(const char *, int);
+static	void		 argv_multi(struct roff_man *, int,
+				struct mdoc_argv *, int *, char *);
+static	void		 argv_single(struct roff_man *, int,
+				struct mdoc_argv *, int *, char *);
+
+static	const enum argvflag argvflags[MDOC_ARG_MAX] = {
+	ARGV_NONE,	/* MDOC_Split */
+	ARGV_NONE,	/* MDOC_Nosplit */
+	ARGV_NONE,	/* MDOC_Ragged */
+	ARGV_NONE,	/* MDOC_Unfilled */
+	ARGV_NONE,	/* MDOC_Literal */
+	ARGV_SINGLE,	/* MDOC_File */
+	ARGV_SINGLE,	/* MDOC_Offset */
+	ARGV_NONE,	/* MDOC_Bullet */
+	ARGV_NONE,	/* MDOC_Dash */
+	ARGV_NONE,	/* MDOC_Hyphen */
+	ARGV_NONE,	/* MDOC_Item */
+	ARGV_NONE,	/* MDOC_Enum */
+	ARGV_NONE,	/* MDOC_Tag */
+	ARGV_NONE,	/* MDOC_Diag */
+	ARGV_NONE,	/* MDOC_Hang */
+	ARGV_NONE,	/* MDOC_Ohang */
+	ARGV_NONE,	/* MDOC_Inset */
+	ARGV_MULTI,	/* MDOC_Column */
+	ARGV_SINGLE,	/* MDOC_Width */
+	ARGV_NONE,	/* MDOC_Compact */
+	ARGV_NONE,	/* MDOC_Std */
+	ARGV_NONE,	/* MDOC_Filled */
+	ARGV_NONE,	/* MDOC_Words */
+	ARGV_NONE,	/* MDOC_Emphasis */
+	ARGV_NONE,	/* MDOC_Symbolic */
+	ARGV_NONE	/* MDOC_Symbolic */
+};
+
+static	const enum mdocargt args_Ex[] = {
+	MDOC_Std,
+	MDOC_ARG_MAX
+};
+
+static	const enum mdocargt args_An[] = {
+	MDOC_Split,
+	MDOC_Nosplit,
+	MDOC_ARG_MAX
+};
+
+static	const enum mdocargt args_Bd[] = {
+	MDOC_Ragged,
+	MDOC_Unfilled,
+	MDOC_Filled,
+	MDOC_Literal,
+	MDOC_File,
+	MDOC_Offset,
+	MDOC_Compact,
+	MDOC_Centred,
+	MDOC_ARG_MAX
+};
+
+static	const enum mdocargt args_Bf[] = {
+	MDOC_Emphasis,
+	MDOC_Literal,
+	MDOC_Symbolic,
+	MDOC_ARG_MAX
+};
+
+static	const enum mdocargt args_Bk[] = {
+	MDOC_Words,
+	MDOC_ARG_MAX
+};
+
+static	const enum mdocargt args_Bl[] = {
+	MDOC_Bullet,
+	MDOC_Dash,
+	MDOC_Hyphen,
+	MDOC_Item,
+	MDOC_Enum,
+	MDOC_Tag,
+	MDOC_Diag,
+	MDOC_Hang,
+	MDOC_Ohang,
+	MDOC_Inset,
+	MDOC_Column,
+	MDOC_Width,
+	MDOC_Offset,
+	MDOC_Compact,
+	MDOC_Nested,
+	MDOC_ARG_MAX
+};
+
+static	const struct mdocarg mdocargs[MDOC_MAX - MDOC_Dd] = {
+	{ ARGSFL_NONE, NULL }, /* Dd */
+	{ ARGSFL_NONE, NULL }, /* Dt */
+	{ ARGSFL_NONE, NULL }, /* Os */
+	{ ARGSFL_NONE, NULL }, /* Sh */
+	{ ARGSFL_NONE, NULL }, /* Ss */
+	{ ARGSFL_NONE, NULL }, /* Pp */
+	{ ARGSFL_DELIM, NULL }, /* D1 */
+	{ ARGSFL_DELIM, NULL }, /* Dl */
+	{ ARGSFL_NONE, args_Bd }, /* Bd */
+	{ ARGSFL_NONE, NULL }, /* Ed */
+	{ ARGSFL_NONE, args_Bl }, /* Bl */
+	{ ARGSFL_NONE, NULL }, /* El */
+	{ ARGSFL_NONE, NULL }, /* It */
+	{ ARGSFL_DELIM, NULL }, /* Ad */
+	{ ARGSFL_DELIM, args_An }, /* An */
+	{ ARGSFL_DELIM, NULL }, /* Ap */
+	{ ARGSFL_DELIM, NULL }, /* Ar */
+	{ ARGSFL_DELIM, NULL }, /* Cd */
+	{ ARGSFL_DELIM, NULL }, /* Cm */
+	{ ARGSFL_DELIM, NULL }, /* Dv */
+	{ ARGSFL_DELIM, NULL }, /* Er */
+	{ ARGSFL_DELIM, NULL }, /* Ev */
+	{ ARGSFL_NONE, args_Ex }, /* Ex */
+	{ ARGSFL_DELIM, NULL }, /* Fa */
+	{ ARGSFL_NONE, NULL }, /* Fd */
+	{ ARGSFL_DELIM, NULL }, /* Fl */
+	{ ARGSFL_DELIM, NULL }, /* Fn */
+	{ ARGSFL_DELIM, NULL }, /* Ft */
+	{ ARGSFL_DELIM, NULL }, /* Ic */
+	{ ARGSFL_DELIM, NULL }, /* In */
+	{ ARGSFL_DELIM, NULL }, /* Li */
+	{ ARGSFL_NONE, NULL }, /* Nd */
+	{ ARGSFL_DELIM, NULL }, /* Nm */
+	{ ARGSFL_DELIM, NULL }, /* Op */
+	{ ARGSFL_DELIM, NULL }, /* Ot */
+	{ ARGSFL_DELIM, NULL }, /* Pa */
+	{ ARGSFL_NONE, args_Ex }, /* Rv */
+	{ ARGSFL_DELIM, NULL }, /* St */
+	{ ARGSFL_DELIM, NULL }, /* Va */
+	{ ARGSFL_DELIM, NULL }, /* Vt */
+	{ ARGSFL_DELIM, NULL }, /* Xr */
+	{ ARGSFL_NONE, NULL }, /* %A */
+	{ ARGSFL_NONE, NULL }, /* %B */
+	{ ARGSFL_NONE, NULL }, /* %D */
+	{ ARGSFL_NONE, NULL }, /* %I */
+	{ ARGSFL_NONE, NULL }, /* %J */
+	{ ARGSFL_NONE, NULL }, /* %N */
+	{ ARGSFL_NONE, NULL }, /* %O */
+	{ ARGSFL_NONE, NULL }, /* %P */
+	{ ARGSFL_NONE, NULL }, /* %R */
+	{ ARGSFL_NONE, NULL }, /* %T */
+	{ ARGSFL_NONE, NULL }, /* %V */
+	{ ARGSFL_DELIM, NULL }, /* Ac */
+	{ ARGSFL_NONE, NULL }, /* Ao */
+	{ ARGSFL_DELIM, NULL }, /* Aq */
+	{ ARGSFL_DELIM, NULL }, /* At */
+	{ ARGSFL_DELIM, NULL }, /* Bc */
+	{ ARGSFL_NONE, args_Bf }, /* Bf */
+	{ ARGSFL_NONE, NULL }, /* Bo */
+	{ ARGSFL_DELIM, NULL }, /* Bq */
+	{ ARGSFL_DELIM, NULL }, /* Bsx */
+	{ ARGSFL_DELIM, NULL }, /* Bx */
+	{ ARGSFL_NONE, NULL }, /* Db */
+	{ ARGSFL_DELIM, NULL }, /* Dc */
+	{ ARGSFL_NONE, NULL }, /* Do */
+	{ ARGSFL_DELIM, NULL }, /* Dq */
+	{ ARGSFL_DELIM, NULL }, /* Ec */
+	{ ARGSFL_NONE, NULL }, /* Ef */
+	{ ARGSFL_DELIM, NULL }, /* Em */
+	{ ARGSFL_NONE, NULL }, /* Eo */
+	{ ARGSFL_DELIM, NULL }, /* Fx */
+	{ ARGSFL_DELIM, NULL }, /* Ms */
+	{ ARGSFL_DELIM, NULL }, /* No */
+	{ ARGSFL_DELIM, NULL }, /* Ns */
+	{ ARGSFL_DELIM, NULL }, /* Nx */
+	{ ARGSFL_DELIM, NULL }, /* Ox */
+	{ ARGSFL_DELIM, NULL }, /* Pc */
+	{ ARGSFL_DELIM, NULL }, /* Pf */
+	{ ARGSFL_NONE, NULL }, /* Po */
+	{ ARGSFL_DELIM, NULL }, /* Pq */
+	{ ARGSFL_DELIM, NULL }, /* Qc */
+	{ ARGSFL_DELIM, NULL }, /* Ql */
+	{ ARGSFL_NONE, NULL }, /* Qo */
+	{ ARGSFL_DELIM, NULL }, /* Qq */
+	{ ARGSFL_NONE, NULL }, /* Re */
+	{ ARGSFL_NONE, NULL }, /* Rs */
+	{ ARGSFL_DELIM, NULL }, /* Sc */
+	{ ARGSFL_NONE, NULL }, /* So */
+	{ ARGSFL_DELIM, NULL }, /* Sq */
+	{ ARGSFL_NONE, NULL }, /* Sm */
+	{ ARGSFL_DELIM, NULL }, /* Sx */
+	{ ARGSFL_DELIM, NULL }, /* Sy */
+	{ ARGSFL_DELIM, NULL }, /* Tn */
+	{ ARGSFL_DELIM, NULL }, /* Ux */
+	{ ARGSFL_DELIM, NULL }, /* Xc */
+	{ ARGSFL_NONE, NULL }, /* Xo */
+	{ ARGSFL_NONE, NULL }, /* Fo */
+	{ ARGSFL_DELIM, NULL }, /* Fc */
+	{ ARGSFL_NONE, NULL }, /* Oo */
+	{ ARGSFL_DELIM, NULL }, /* Oc */
+	{ ARGSFL_NONE, args_Bk }, /* Bk */
+	{ ARGSFL_NONE, NULL }, /* Ek */
+	{ ARGSFL_NONE, NULL }, /* Bt */
+	{ ARGSFL_NONE, NULL }, /* Hf */
+	{ ARGSFL_DELIM, NULL }, /* Fr */
+	{ ARGSFL_NONE, NULL }, /* Ud */
+	{ ARGSFL_DELIM, NULL }, /* Lb */
+	{ ARGSFL_NONE, NULL }, /* Lp */
+	{ ARGSFL_DELIM, NULL }, /* Lk */
+	{ ARGSFL_DELIM, NULL }, /* Mt */
+	{ ARGSFL_DELIM, NULL }, /* Brq */
+	{ ARGSFL_NONE, NULL }, /* Bro */
+	{ ARGSFL_DELIM, NULL }, /* Brc */
+	{ ARGSFL_NONE, NULL }, /* %C */
+	{ ARGSFL_NONE, NULL }, /* Es */
+	{ ARGSFL_DELIM, NULL }, /* En */
+	{ ARGSFL_DELIM, NULL }, /* Dx */
+	{ ARGSFL_NONE, NULL }, /* %Q */
+	{ ARGSFL_NONE, NULL }, /* %U */
+	{ ARGSFL_NONE, NULL }, /* Ta */
+};
+
+
+/*
+ * Parse flags and their arguments from the input line.
+ * These come in the form -flag [argument ...].
+ * Some flags take no argument, some one, some multiple.
+ */
+void
+mdoc_argv(struct roff_man *mdoc, int line, enum roff_tok tok,
+	struct mdoc_arg **reta, int *pos, char *buf)
+{
+	struct mdoc_argv	  tmpv;
+	struct mdoc_argv	**retv;
+	const enum mdocargt	 *argtable;
+	char			 *argname;
+	int			  ipos, retc;
+	char			  savechar;
+
+	*reta = NULL;
+
+	/* Which flags does this macro support? */
+
+	assert(tok >= MDOC_Dd && tok < MDOC_MAX);
+	argtable = mdocargs[tok - MDOC_Dd].argvs;
+	if (argtable == NULL)
+		return;
+
+	/* Loop over the flags on the input line. */
+
+	ipos = *pos;
+	while (buf[ipos] == '-') {
+
+		/* Seek to the first unescaped space. */
+
+		for (argname = buf + ++ipos; buf[ipos] != '\0'; ipos++)
+			if (buf[ipos] == ' ' && buf[ipos - 1] != '\\')
+				break;
+
+		/*
+		 * We want to nil-terminate the word to look it up.
+		 * But we may not have a flag, in which case we need
+		 * to restore the line as-is.  So keep around the
+		 * stray byte, which we'll reset upon exiting.
+		 */
+
+		if ((savechar = buf[ipos]) != '\0')
+			buf[ipos++] = '\0';
+
+		/*
+		 * Now look up the word as a flag.  Use temporary
+		 * storage that we'll copy into the node's flags.
+		 */
+
+		while ((tmpv.arg = *argtable++) != MDOC_ARG_MAX)
+			if ( ! strcmp(argname, mdoc_argnames[tmpv.arg]))
+				break;
+
+		/* If it isn't a flag, restore the saved byte. */
+
+		if (tmpv.arg == MDOC_ARG_MAX) {
+			if (savechar != '\0')
+				buf[ipos - 1] = savechar;
+			break;
+		}
+
+		/* Read to the next word (the first argument). */
+
+		while (buf[ipos] == ' ')
+			ipos++;
+
+		/* Parse the arguments of the flag. */
+
+		tmpv.line  = line;
+		tmpv.pos   = *pos;
+		tmpv.sz    = 0;
+		tmpv.value = NULL;
+
+		switch (argvflags[tmpv.arg]) {
+		case ARGV_SINGLE:
+			argv_single(mdoc, line, &tmpv, &ipos, buf);
+			break;
+		case ARGV_MULTI:
+			argv_multi(mdoc, line, &tmpv, &ipos, buf);
+			break;
+		case ARGV_NONE:
+			break;
+		}
+
+		/* Append to the return values. */
+
+		if (*reta == NULL)
+			*reta = mandoc_calloc(1, sizeof(**reta));
+
+		retc = ++(*reta)->argc;
+		retv = &(*reta)->argv;
+		*retv = mandoc_reallocarray(*retv, retc, sizeof(**retv));
+		memcpy(*retv + retc - 1, &tmpv, sizeof(**retv));
+
+		/* Prepare for parsing the next flag. */
+
+		*pos = ipos;
+		argtable = mdocargs[tok - MDOC_Dd].argvs;
+	}
+}
+
+void
+mdoc_argv_free(struct mdoc_arg *p)
+{
+	int		 i;
+
+	if (NULL == p)
+		return;
+
+	if (p->refcnt) {
+		--(p->refcnt);
+		if (p->refcnt)
+			return;
+	}
+	assert(p->argc);
+
+	for (i = (int)p->argc - 1; i >= 0; i--)
+		argn_free(p, i);
+
+	free(p->argv);
+	free(p);
+}
+
+static void
+argn_free(struct mdoc_arg *p, int iarg)
+{
+	struct mdoc_argv *arg;
+	int		  j;
+
+	arg = &p->argv[iarg];
+
+	if (arg->sz && arg->value) {
+		for (j = (int)arg->sz - 1; j >= 0; j--)
+			free(arg->value[j]);
+		free(arg->value);
+	}
+
+	for (--p->argc; iarg < (int)p->argc; iarg++)
+		p->argv[iarg] = p->argv[iarg+1];
+}
+
+enum margserr
+mdoc_args(struct roff_man *mdoc, int line, int *pos,
+	char *buf, enum roff_tok tok, char **v)
+{
+	struct roff_node *n;
+	enum argsflag	  fl;
+
+	fl = tok == TOKEN_NONE ? ARGSFL_NONE : mdocargs[tok - MDOC_Dd].flags;
+
+	/*
+	 * We know that we're in an `It', so it's reasonable to expect
+	 * us to be sitting in a `Bl'.  Someday this may not be the case
+	 * (if we allow random `It's sitting out there), so provide a
+	 * safe fall-back into the default behaviour.
+	 */
+
+	if (tok == MDOC_It) {
+		for (n = mdoc->last; n != NULL; n = n->parent) {
+			if (n->tok != MDOC_Bl)
+				continue;
+			if (n->norm->Bl.type == LIST_column)
+				fl = ARGSFL_TABSEP;
+			break;
+		}
+	}
+
+	return args(mdoc, line, pos, buf, fl, v);
+}
+
+static enum margserr
+args(struct roff_man *mdoc, int line, int *pos,
+		char *buf, enum argsflag fl, char **v)
+{
+	char		*p;
+	char		*v_local;
+	int		 pairs;
+
+	if (buf[*pos] == '\0') {
+		if (mdoc->flags & MDOC_PHRASELIT &&
+		    ! (mdoc->flags & MDOC_PHRASE)) {
+			mandoc_msg(MANDOCERR_ARG_QUOTE, line, *pos, NULL);
+			mdoc->flags &= ~MDOC_PHRASELIT;
+		}
+		mdoc->flags &= ~MDOC_PHRASEQL;
+		return ARGS_EOLN;
+	}
+
+	if (v == NULL)
+		v = &v_local;
+	*v = buf + *pos;
+
+	if (fl == ARGSFL_DELIM && args_checkpunct(buf, *pos))
+		return ARGS_PUNCT;
+
+	/*
+	 * Tabs in `It' lines in `Bl -column' can't be escaped.
+	 * Phrases are reparsed for `Ta' and other macros later.
+	 */
+
+	if (fl == ARGSFL_TABSEP) {
+		if ((p = strchr(*v, '\t')) != NULL) {
+
+			/*
+			 * Words right before and right after
+			 * tab characters are not parsed,
+			 * unless there is a blank in between.
+			 */
+
+			if (p > buf && p[-1] != ' ')
+				mdoc->flags |= MDOC_PHRASEQL;
+			if (p[1] != ' ')
+				mdoc->flags |= MDOC_PHRASEQN;
+
+			/*
+			 * One or more blanks after a tab cause
+			 * one leading blank in the next column.
+			 * So skip all but one of them.
+			 */
+
+			*pos += (int)(p - *v) + 1;
+			while (buf[*pos] == ' ' && buf[*pos + 1] == ' ')
+				(*pos)++;
+
+			/*
+			 * A tab at the end of an input line
+			 * switches to the next column.
+			 */
+
+			if (buf[*pos] == '\0' || buf[*pos + 1] == '\0')
+				mdoc->flags |= MDOC_PHRASEQN;
+		} else {
+			p = strchr(*v, '\0');
+			if (p[-1] == ' ')
+				mandoc_msg(MANDOCERR_SPACE_EOL,
+				    line, *pos, NULL);
+			*pos += (int)(p - *v);
+		}
+
+		/* Skip any trailing blank characters. */
+		while (p > *v && p[-1] == ' ' &&
+		    (p - 1 == *v || p[-2] != '\\'))
+			p--;
+		*p = '\0';
+
+		return ARGS_PHRASE;
+	}
+
+	/*
+	 * Process a quoted literal.  A quote begins with a double-quote
+	 * and ends with a double-quote NOT preceded by a double-quote.
+	 * NUL-terminate the literal in place.
+	 * Collapse pairs of quotes inside quoted literals.
+	 * Whitespace is NOT involved in literal termination.
+	 */
+
+	if (mdoc->flags & MDOC_PHRASELIT ||
+	    (mdoc->flags & MDOC_PHRASE && buf[*pos] == '\"')) {
+		if ((mdoc->flags & MDOC_PHRASELIT) == 0) {
+			*v = &buf[++(*pos)];
+			mdoc->flags |= MDOC_PHRASELIT;
+		}
+		pairs = 0;
+		for ( ; buf[*pos]; (*pos)++) {
+			/* Move following text left after quoted quotes. */
+			if (pairs)
+				buf[*pos - pairs] = buf[*pos];
+			if ('\"' != buf[*pos])
+				continue;
+			/* Unquoted quotes end quoted args. */
+			if ('\"' != buf[*pos + 1])
+				break;
+			/* Quoted quotes collapse. */
+			pairs++;
+			(*pos)++;
+		}
+		if (pairs)
+			buf[*pos - pairs] = '\0';
+
+		if (buf[*pos] == '\0') {
+			if ( ! (mdoc->flags & MDOC_PHRASE))
+				mandoc_msg(MANDOCERR_ARG_QUOTE,
+				    line, *pos, NULL);
+			return ARGS_WORD;
+		}
+
+		mdoc->flags &= ~MDOC_PHRASELIT;
+		buf[(*pos)++] = '\0';
+
+		if ('\0' == buf[*pos])
+			return ARGS_WORD;
+
+		while (' ' == buf[*pos])
+			(*pos)++;
+
+		if ('\0' == buf[*pos])
+			mandoc_msg(MANDOCERR_SPACE_EOL, line, *pos, NULL);
+
+		return ARGS_WORD;
+	}
+
+	p = &buf[*pos];
+	*v = roff_getarg(mdoc->roff, &p, line, pos);
+	if (v == &v_local)
+		free(*v);
+
+	/*
+	 * After parsing the last word in this phrase,
+	 * tell lookup() whether or not to interpret it.
+	 */
+
+	if (*p == '\0' && mdoc->flags & MDOC_PHRASEQL) {
+		mdoc->flags &= ~MDOC_PHRASEQL;
+		mdoc->flags |= MDOC_PHRASEQF;
+	}
+	return ARGS_ALLOC;
+}
+
+/*
+ * Check if the string consists only of space-separated closing
+ * delimiters.  This is a bit of a dance: the first must be a close
+ * delimiter, but it may be followed by middle delimiters.  Arbitrary
+ * whitespace may separate these tokens.
+ */
+static int
+args_checkpunct(const char *buf, int i)
+{
+	int		 j;
+	char		 dbuf[DELIMSZ];
+	enum mdelim	 d;
+
+	/* First token must be a close-delimiter. */
+
+	for (j = 0; buf[i] && ' ' != buf[i] && j < DELIMSZ; j++, i++)
+		dbuf[j] = buf[i];
+
+	if (DELIMSZ == j)
+		return 0;
+
+	dbuf[j] = '\0';
+	if (DELIM_CLOSE != mdoc_isdelim(dbuf))
+		return 0;
+
+	while (' ' == buf[i])
+		i++;
+
+	/* Remaining must NOT be open/none. */
+
+	while (buf[i]) {
+		j = 0;
+		while (buf[i] && ' ' != buf[i] && j < DELIMSZ)
+			dbuf[j++] = buf[i++];
+
+		if (DELIMSZ == j)
+			return 0;
+
+		dbuf[j] = '\0';
+		d = mdoc_isdelim(dbuf);
+		if (DELIM_NONE == d || DELIM_OPEN == d)
+			return 0;
+
+		while (' ' == buf[i])
+			i++;
+	}
+
+	return '\0' == buf[i];
+}
+
+static void
+argv_multi(struct roff_man *mdoc, int line,
+		struct mdoc_argv *v, int *pos, char *buf)
+{
+	enum margserr	 ac;
+	char		*p;
+
+	for (v->sz = 0; ; v->sz++) {
+		if (buf[*pos] == '-')
+			break;
+		ac = args(mdoc, line, pos, buf, ARGSFL_NONE, &p);
+		if (ac == ARGS_EOLN)
+			break;
+
+		if (v->sz % MULTI_STEP == 0)
+			v->value = mandoc_reallocarray(v->value,
+			    v->sz + MULTI_STEP, sizeof(char *));
+
+		if (ac != ARGS_ALLOC)
+			p = mandoc_strdup(p);
+		v->value[(int)v->sz] = p;
+	}
+}
+
+static void
+argv_single(struct roff_man *mdoc, int line,
+		struct mdoc_argv *v, int *pos, char *buf)
+{
+	enum margserr	 ac;
+	char		*p;
+
+	ac = args(mdoc, line, pos, buf, ARGSFL_NONE, &p);
+	if (ac == ARGS_EOLN)
+		return;
+
+	if (ac != ARGS_ALLOC)
+		p = mandoc_strdup(p);
+
+	v->sz = 1;
+	v->value = mandoc_malloc(sizeof(char *));
+	v->value[0] = p;
+}
diff --git a/usr.bin/mandoc/mdoc_html.c b/usr.bin/mandoc/mdoc_html.c
new file mode 100644
index 0000000..fcff005
--- /dev/null
+++ b/usr.bin/mandoc/mdoc_html.c
@@ -0,0 +1,1758 @@
+/* $OpenBSD: mdoc_html.c,v 1.215 2020/04/19 15:15:54 schwarze Exp $ */
+/*
+ * Copyright (c) 2014-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008-2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#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, "");
+		if (n != NULL && n->type == ROFFT_COMMENT)
+			print_gen_comment(h, n);
+		t = print_otag(h, TAG_HEAD, "");
+		print_mdoc_head(mdoc, h);
+		print_tagq(h, t);
+		print_otag(h, TAG_BODY, "");
+	}
+
+	mdoc_root_pre(mdoc, h);
+	t = print_otag(h, TAG_DIV, "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) {
+		html_fillmode(h, ROFF_nf);
+		if (n->flags & NODE_LINE)
+			print_endline(h);
+	} else
+		html_fillmode(h, ROFF_fi);
+
+	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, *tt;
+
+	t = print_otag(h, TAG_TABLE, "c", "foot");
+	tt = print_otag(h, TAG_TR, "");
+
+	print_otag(h, TAG_TD, "c", "foot-date");
+	print_text(h, meta->date);
+	print_stagq(h, tt);
+
+	print_otag(h, TAG_TD, "c", "foot-os");
+	print_text(h, meta->os);
+	print_tagq(h, t);
+}
+
+static int
+mdoc_root_pre(const struct roff_meta *meta, struct html *h)
+{
+	struct tag	*t, *tt;
+	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_TABLE, "c", "head");
+	tt = print_otag(h, TAG_TR, "");
+
+	print_otag(h, TAG_TD, "c", "head-ltitle");
+	print_text(h, title);
+	print_stagq(h, tt);
+
+	print_otag(h, TAG_TD, "c", "head-vol");
+	print_text(h, volume);
+	print_stagq(h, tt);
+
+	print_otag(h, TAG_TD, "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, *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;
+		t = print_otag(h, TAG_H1, "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, t);
+		print_otag(h, TAG_SECTION, "c", "Sh");
+		break;
+	case ROFFT_HEAD:
+		print_otag_id(h, TAG_H1, "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_H2, "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, "c", "Nd");
+	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)
+{
+	if (NULL == n->child)
+		return 0;
+
+	if (h->base_man1)
+		print_otag(h, TAG_A, "chM", "Xr",
+		    n->child->string, n->child->next == NULL ?
+		    NULL : n->child->next->string);
+	else
+		print_otag(h, TAG_A, "c", "Xr");
+
+	n = n->child;
+	print_text(h, n->string);
+
+	if (NULL == (n = n->next))
+		return 0;
+
+	h->flags |= HTML_NOSPACE;
+	print_text(h, "(");
+	h->flags |= HTML_NOSPACE;
+	print_text(h, n->string);
+	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[16];
+	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));
+
+	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_CITE, "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 char		*cattr;
+	enum htmltag		 t;
+
+	t = TAG_SPAN;
+
+	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_I;
+		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:
+		cattr = "RsR";
+		break;
+	case MDOC__T:
+		cattr = "RsT";
+		break;
+	case MDOC__U:
+		print_otag(h, TAG_A, "ch", "RsU", n->child->string);
+		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;
+
+	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;
+
+	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/usr.bin/mandoc/mdoc_macro.c b/usr.bin/mandoc/mdoc_macro.c
new file mode 100644
index 0000000..a8aa3c9
--- /dev/null
+++ b/usr.bin/mandoc/mdoc_macro.c
@@ -0,0 +1,1598 @@
+/*	$OpenBSD: mdoc_macro.c,v 1.191 2020/01/19 17:59:01 schwarze Exp $ */
+/*
+ * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2010, 2012-2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "mdoc.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+#include "libmdoc.h"
+
+static	void		blk_full(MACRO_PROT_ARGS);
+static	void		blk_exp_close(MACRO_PROT_ARGS);
+static	void		blk_part_exp(MACRO_PROT_ARGS);
+static	void		blk_part_imp(MACRO_PROT_ARGS);
+static	void		ctx_synopsis(MACRO_PROT_ARGS);
+static	void		in_line_eoln(MACRO_PROT_ARGS);
+static	void		in_line_argn(MACRO_PROT_ARGS);
+static	void		in_line(MACRO_PROT_ARGS);
+static	void		phrase_ta(MACRO_PROT_ARGS);
+
+static	void		append_delims(struct roff_man *, int, int *, char *);
+static	void		dword(struct roff_man *, int, int, const char *,
+				enum mdelim, int);
+static	int		find_pending(struct roff_man *, enum roff_tok,
+				int, int, struct roff_node *);
+static	int		lookup(struct roff_man *, int, int, int, const char *);
+static	int		macro_or_word(MACRO_PROT_ARGS, char *, int);
+static	void		break_intermediate(struct roff_node *,
+				struct roff_node *);
+static	int		parse_rest(struct roff_man *, enum roff_tok,
+				int, int *, char *);
+static	enum roff_tok	rew_alt(enum roff_tok);
+static	void		rew_elem(struct roff_man *, enum roff_tok);
+static	void		rew_last(struct roff_man *, const struct roff_node *);
+static	void		rew_pending(struct roff_man *,
+				const struct roff_node *);
+
+static const struct mdoc_macro mdoc_macros[MDOC_MAX - MDOC_Dd] = {
+	{ in_line_eoln, MDOC_PROLOGUE | MDOC_JOIN }, /* Dd */
+	{ in_line_eoln, MDOC_PROLOGUE }, /* Dt */
+	{ in_line_eoln, MDOC_PROLOGUE }, /* Os */
+	{ blk_full, MDOC_PARSED | MDOC_JOIN }, /* Sh */
+	{ blk_full, MDOC_PARSED | MDOC_JOIN }, /* Ss */
+	{ in_line_eoln, 0 }, /* Pp */
+	{ blk_part_imp, MDOC_PARSED | MDOC_JOIN }, /* D1 */
+	{ blk_part_imp, MDOC_PARSED | MDOC_JOIN }, /* Dl */
+	{ blk_full, MDOC_EXPLICIT }, /* Bd */
+	{ blk_exp_close, MDOC_EXPLICIT | MDOC_JOIN }, /* Ed */
+	{ blk_full, MDOC_EXPLICIT }, /* Bl */
+	{ blk_exp_close, MDOC_EXPLICIT | MDOC_JOIN }, /* El */
+	{ blk_full, MDOC_PARSED | MDOC_JOIN }, /* It */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Ad */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* An */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_IGNDELIM | MDOC_JOIN }, /* Ap */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Ar */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Cd */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Cm */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Dv */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Er */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Ev */
+	{ in_line_eoln, 0 }, /* Ex */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Fa */
+	{ in_line_eoln, 0 }, /* Fd */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Fl */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Fn */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Ft */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Ic */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* In */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Li */
+	{ blk_full, MDOC_JOIN }, /* Nd */
+	{ ctx_synopsis, MDOC_CALLABLE | MDOC_PARSED }, /* Nm */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED }, /* Op */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Ot */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Pa */
+	{ in_line_eoln, 0 }, /* Rv */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* St */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Va */
+	{ ctx_synopsis, MDOC_CALLABLE | MDOC_PARSED }, /* Vt */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* Xr */
+	{ in_line_eoln, MDOC_JOIN }, /* %A */
+	{ in_line_eoln, MDOC_JOIN }, /* %B */
+	{ in_line_eoln, MDOC_JOIN }, /* %D */
+	{ in_line_eoln, MDOC_JOIN }, /* %I */
+	{ in_line_eoln, MDOC_JOIN }, /* %J */
+	{ in_line_eoln, 0 }, /* %N */
+	{ in_line_eoln, MDOC_JOIN }, /* %O */
+	{ in_line_eoln, 0 }, /* %P */
+	{ in_line_eoln, MDOC_JOIN }, /* %R */
+	{ in_line_eoln, MDOC_JOIN }, /* %T */
+	{ in_line_eoln, 0 }, /* %V */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED |
+			 MDOC_EXPLICIT | MDOC_JOIN }, /* Ac */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_EXPLICIT | MDOC_JOIN }, /* Ao */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Aq */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* At */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED |
+			 MDOC_EXPLICIT | MDOC_JOIN }, /* Bc */
+	{ blk_full, MDOC_EXPLICIT }, /* Bf */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_EXPLICIT | MDOC_JOIN }, /* Bo */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Bq */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* Bsx */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* Bx */
+	{ in_line_eoln, 0 }, /* Db */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED |
+			 MDOC_EXPLICIT | MDOC_JOIN }, /* Dc */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_EXPLICIT | MDOC_JOIN }, /* Do */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Dq */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED | MDOC_EXPLICIT }, /* Ec */
+	{ blk_exp_close, MDOC_EXPLICIT | MDOC_JOIN }, /* Ef */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Em */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED | MDOC_EXPLICIT }, /* Eo */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* Fx */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Ms */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* No */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_IGNDELIM | MDOC_JOIN }, /* Ns */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* Nx */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* Ox */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED |
+			 MDOC_EXPLICIT | MDOC_JOIN }, /* Pc */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED | MDOC_IGNDELIM }, /* Pf */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_EXPLICIT | MDOC_JOIN }, /* Po */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Pq */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED |
+			 MDOC_EXPLICIT | MDOC_JOIN }, /* Qc */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Ql */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_EXPLICIT | MDOC_JOIN }, /* Qo */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Qq */
+	{ blk_exp_close, MDOC_EXPLICIT | MDOC_JOIN }, /* Re */
+	{ blk_full, MDOC_EXPLICIT }, /* Rs */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED |
+			 MDOC_EXPLICIT | MDOC_JOIN }, /* Sc */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_EXPLICIT | MDOC_JOIN }, /* So */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Sq */
+	{ in_line_argn, 0 }, /* Sm */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Sx */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Sy */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Tn */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Ux */
+	{ blk_exp_close, MDOC_EXPLICIT | MDOC_CALLABLE | MDOC_PARSED }, /* Xc */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED | MDOC_EXPLICIT }, /* Xo */
+	{ blk_full, MDOC_EXPLICIT | MDOC_CALLABLE }, /* Fo */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED |
+			 MDOC_EXPLICIT | MDOC_JOIN }, /* Fc */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_EXPLICIT | MDOC_JOIN }, /* Oo */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED |
+			 MDOC_EXPLICIT | MDOC_JOIN }, /* Oc */
+	{ blk_full, MDOC_EXPLICIT }, /* Bk */
+	{ blk_exp_close, MDOC_EXPLICIT | MDOC_JOIN }, /* Ek */
+	{ in_line_eoln, 0 }, /* Bt */
+	{ in_line_eoln, 0 }, /* Hf */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Fr */
+	{ in_line_eoln, 0 }, /* Ud */
+	{ in_line, 0 }, /* Lb */
+	{ in_line_eoln, 0 }, /* Lp */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Lk */
+	{ in_line, MDOC_CALLABLE | MDOC_PARSED }, /* Mt */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Brq */
+	{ blk_part_exp, MDOC_CALLABLE | MDOC_PARSED |
+			MDOC_EXPLICIT | MDOC_JOIN }, /* Bro */
+	{ blk_exp_close, MDOC_CALLABLE | MDOC_PARSED |
+			 MDOC_EXPLICIT | MDOC_JOIN }, /* Brc */
+	{ in_line_eoln, MDOC_JOIN }, /* %C */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* Es */
+	{ blk_part_imp, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* En */
+	{ in_line_argn, MDOC_CALLABLE | MDOC_PARSED }, /* Dx */
+	{ in_line_eoln, MDOC_JOIN }, /* %Q */
+	{ in_line_eoln, 0 }, /* %U */
+	{ phrase_ta, MDOC_CALLABLE | MDOC_PARSED | MDOC_JOIN }, /* Ta */
+	{ in_line_eoln, 0 }, /* Tg */
+};
+
+
+const struct mdoc_macro *
+mdoc_macro(enum roff_tok tok)
+{
+	assert(tok >= MDOC_Dd && tok < MDOC_MAX);
+	return mdoc_macros + (tok - MDOC_Dd);
+}
+
+/*
+ * This is called at the end of parsing.  It must traverse up the tree,
+ * closing out open [implicit] scopes.  Obviously, open explicit scopes
+ * are errors.
+ */
+void
+mdoc_endparse(struct roff_man *mdoc)
+{
+	struct roff_node *n;
+
+	/* Scan for open explicit scopes. */
+
+	n = mdoc->last->flags & NODE_VALID ?
+	    mdoc->last->parent : mdoc->last;
+
+	for ( ; n; n = n->parent)
+		if (n->type == ROFFT_BLOCK &&
+		    mdoc_macro(n->tok)->flags & MDOC_EXPLICIT)
+			mandoc_msg(MANDOCERR_BLK_NOEND,
+			    n->line, n->pos, "%s", roff_name[n->tok]);
+
+	/* Rewind to the first. */
+
+	rew_last(mdoc, mdoc->meta.first);
+}
+
+/*
+ * Look up the macro at *p called by "from",
+ * or as a line macro if from == TOKEN_NONE.
+ */
+static int
+lookup(struct roff_man *mdoc, int from, int line, int ppos, const char *p)
+{
+	enum roff_tok	 res;
+
+	if (mdoc->flags & MDOC_PHRASEQF) {
+		mdoc->flags &= ~MDOC_PHRASEQF;
+		return TOKEN_NONE;
+	}
+	if (from == TOKEN_NONE || mdoc_macro(from)->flags & MDOC_PARSED) {
+		res = roffhash_find(mdoc->mdocmac, p, 0);
+		if (res != TOKEN_NONE) {
+			if (mdoc_macro(res)->flags & MDOC_CALLABLE)
+				return res;
+			mandoc_msg(MANDOCERR_MACRO_CALL, line, ppos, "%s", p);
+		}
+	}
+	return TOKEN_NONE;
+}
+
+/*
+ * Rewind up to and including a specific node.
+ */
+static void
+rew_last(struct roff_man *mdoc, const struct roff_node *to)
+{
+
+	if (to->flags & NODE_VALID)
+		return;
+
+	while (mdoc->last != to) {
+		mdoc_state(mdoc, mdoc->last);
+		mdoc->last->flags |= NODE_VALID | NODE_ENDED;
+		mdoc->last = mdoc->last->parent;
+	}
+	mdoc_state(mdoc, mdoc->last);
+	mdoc->last->flags |= NODE_VALID | NODE_ENDED;
+	mdoc->next = ROFF_NEXT_SIBLING;
+}
+
+/*
+ * Rewind up to a specific block, including all blocks that broke it.
+ */
+static void
+rew_pending(struct roff_man *mdoc, const struct roff_node *n)
+{
+
+	for (;;) {
+		rew_last(mdoc, n);
+
+		if (mdoc->last == n) {
+			switch (n->type) {
+			case ROFFT_HEAD:
+				roff_body_alloc(mdoc, n->line, n->pos,
+				    n->tok);
+				if (n->tok == MDOC_Ss)
+					mdoc->flags &= ~ROFF_NONOFILL;
+				break;
+			case ROFFT_BLOCK:
+				break;
+			default:
+				return;
+			}
+			if ( ! (n->flags & NODE_BROKEN))
+				return;
+		} else
+			n = mdoc->last;
+
+		for (;;) {
+			if ((n = n->parent) == NULL)
+				return;
+
+			if (n->type == ROFFT_BLOCK ||
+			    n->type == ROFFT_HEAD) {
+				if (n->flags & NODE_ENDED)
+					break;
+				else
+					return;
+			}
+		}
+	}
+}
+
+/*
+ * For a block closing macro, return the corresponding opening one.
+ * Otherwise, return the macro itself.
+ */
+static enum roff_tok
+rew_alt(enum roff_tok tok)
+{
+	switch (tok) {
+	case MDOC_Ac:
+		return MDOC_Ao;
+	case MDOC_Bc:
+		return MDOC_Bo;
+	case MDOC_Brc:
+		return MDOC_Bro;
+	case MDOC_Dc:
+		return MDOC_Do;
+	case MDOC_Ec:
+		return MDOC_Eo;
+	case MDOC_Ed:
+		return MDOC_Bd;
+	case MDOC_Ef:
+		return MDOC_Bf;
+	case MDOC_Ek:
+		return MDOC_Bk;
+	case MDOC_El:
+		return MDOC_Bl;
+	case MDOC_Fc:
+		return MDOC_Fo;
+	case MDOC_Oc:
+		return MDOC_Oo;
+	case MDOC_Pc:
+		return MDOC_Po;
+	case MDOC_Qc:
+		return MDOC_Qo;
+	case MDOC_Re:
+		return MDOC_Rs;
+	case MDOC_Sc:
+		return MDOC_So;
+	case MDOC_Xc:
+		return MDOC_Xo;
+	default:
+		return tok;
+	}
+}
+
+static void
+rew_elem(struct roff_man *mdoc, enum roff_tok tok)
+{
+	struct roff_node *n;
+
+	n = mdoc->last;
+	if (n->type != ROFFT_ELEM)
+		n = n->parent;
+	assert(n->type == ROFFT_ELEM);
+	assert(tok == n->tok);
+	rew_last(mdoc, n);
+}
+
+static void
+break_intermediate(struct roff_node *n, struct roff_node *breaker)
+{
+	if (n != breaker &&
+	    n->type != ROFFT_BLOCK && n->type != ROFFT_HEAD &&
+	    (n->type != ROFFT_BODY || n->end != ENDBODY_NOT))
+		n = n->parent;
+	while (n != breaker) {
+		if ( ! (n->flags & NODE_VALID))
+			n->flags |= NODE_BROKEN;
+		n = n->parent;
+	}
+}
+
+/*
+ * If there is an open sub-block of the target requiring
+ * explicit close-out, postpone closing out the target until
+ * the rew_pending() call closing out the sub-block.
+ */
+static int
+find_pending(struct roff_man *mdoc, enum roff_tok tok, int line, int ppos,
+	struct roff_node *target)
+{
+	struct roff_node	*n;
+	int			 irc;
+
+	if (target->flags & NODE_VALID)
+		return 0;
+
+	irc = 0;
+	for (n = mdoc->last; n != NULL && n != target; n = n->parent) {
+		if (n->flags & NODE_ENDED)
+			continue;
+		if (n->type == ROFFT_BLOCK &&
+		    mdoc_macro(n->tok)->flags & MDOC_EXPLICIT) {
+			irc = 1;
+			break_intermediate(mdoc->last, target);
+			if (target->type == ROFFT_HEAD)
+				target->flags |= NODE_ENDED;
+			else if ( ! (target->flags & NODE_ENDED)) {
+				mandoc_msg(MANDOCERR_BLK_NEST,
+				    line, ppos, "%s breaks %s",
+				    roff_name[tok], roff_name[n->tok]);
+				mdoc_endbody_alloc(mdoc, line, ppos,
+				    tok, target);
+			}
+		}
+	}
+	return irc;
+}
+
+/*
+ * Allocate a word and check whether it's punctuation or not.
+ * Punctuation consists of those tokens found in mdoc_isdelim().
+ */
+static void
+dword(struct roff_man *mdoc, int line, int col, const char *p,
+		enum mdelim d, int may_append)
+{
+
+	if (d == DELIM_MAX)
+		d = mdoc_isdelim(p);
+
+	if (may_append &&
+	    ! (mdoc->flags & (MDOC_SYNOPSIS | MDOC_KEEP | MDOC_SMOFF)) &&
+	    d == DELIM_NONE && mdoc->last->type == ROFFT_TEXT &&
+	    mdoc_isdelim(mdoc->last->string) == DELIM_NONE) {
+		roff_word_append(mdoc, p);
+		return;
+	}
+
+	roff_word_alloc(mdoc, line, col, p);
+
+	/*
+	 * If the word consists of a bare delimiter,
+	 * flag the new node accordingly,
+	 * unless doing so was vetoed by the invoking macro.
+	 * Always clear the veto, it is only valid for one word.
+	 */
+
+	if (d == DELIM_OPEN)
+		mdoc->last->flags |= NODE_DELIMO;
+	else if (d == DELIM_CLOSE &&
+	    ! (mdoc->flags & MDOC_NODELIMC) &&
+	    mdoc->last->parent->tok != MDOC_Fd)
+		mdoc->last->flags |= NODE_DELIMC;
+	mdoc->flags &= ~MDOC_NODELIMC;
+}
+
+static void
+append_delims(struct roff_man *mdoc, int line, int *pos, char *buf)
+{
+	char		*p;
+	int		 la;
+	enum margserr	 ac;
+
+	if (buf[*pos] == '\0')
+		return;
+
+	for (;;) {
+		la = *pos;
+		ac = mdoc_args(mdoc, line, pos, buf, TOKEN_NONE, &p);
+		if (ac == ARGS_EOLN)
+			break;
+		dword(mdoc, line, la, p, DELIM_MAX, 1);
+
+		/*
+		 * If we encounter end-of-sentence symbols, then trigger
+		 * the double-space.
+		 *
+		 * XXX: it's easy to allow this to propagate outward to
+		 * the last symbol, such that `. )' will cause the
+		 * correct double-spacing.  However, (1) groff isn't
+		 * smart enough to do this and (2) it would require
+		 * knowing which symbols break this behaviour, for
+		 * example, `.  ;' shouldn't propagate the double-space.
+		 */
+
+		if (mandoc_eos(p, strlen(p)))
+			mdoc->last->flags |= NODE_EOS;
+		if (ac == ARGS_ALLOC)
+			free(p);
+	}
+}
+
+/*
+ * Parse one word.
+ * If it is a macro, call it and return 1.
+ * Otherwise, allocate it and return 0.
+ */
+static int
+macro_or_word(MACRO_PROT_ARGS, char *p, int parsed)
+{
+	int		 ntok;
+
+	ntok = buf[ppos] == '"' || parsed == 0 ||
+	    mdoc->flags & MDOC_PHRASELIT ? TOKEN_NONE :
+	    lookup(mdoc, tok, line, ppos, p);
+
+	if (ntok == TOKEN_NONE) {
+		dword(mdoc, line, ppos, p, DELIM_MAX, tok == TOKEN_NONE ||
+		    mdoc_macro(tok)->flags & MDOC_JOIN);
+		return 0;
+	} else {
+		if (tok != TOKEN_NONE &&
+		    mdoc_macro(tok)->fp == in_line_eoln)
+			rew_elem(mdoc, tok);
+		(*mdoc_macro(ntok)->fp)(mdoc, ntok, line, ppos, pos, buf);
+		if (tok == TOKEN_NONE)
+			append_delims(mdoc, line, pos, buf);
+		return 1;
+	}
+}
+
+/*
+ * Close out block partial/full explicit.
+ */
+static void
+blk_exp_close(MACRO_PROT_ARGS)
+{
+	struct roff_node *body;		/* Our own body. */
+	struct roff_node *endbody;	/* Our own end marker. */
+	struct roff_node *itblk;	/* An It block starting later. */
+	struct roff_node *later;	/* A sub-block starting later. */
+	struct roff_node *n;		/* Search back to our block. */
+	struct roff_node *target;	/* For find_pending(). */
+
+	int		 j, lastarg, maxargs, nl, pending;
+	enum margserr	 ac;
+	enum roff_tok	 atok, ntok;
+	char		*p;
+
+	nl = MDOC_NEWLINE & mdoc->flags;
+
+	switch (tok) {
+	case MDOC_Ec:
+		maxargs = 1;
+		break;
+	case MDOC_Ek:
+		mdoc->flags &= ~MDOC_KEEP;
+		/* FALLTHROUGH */
+	default:
+		maxargs = 0;
+		break;
+	}
+
+	/* Search backwards for the beginning of our own body. */
+
+	atok = rew_alt(tok);
+	body = NULL;
+	for (n = mdoc->last; n; n = n->parent) {
+		if (n->flags & NODE_ENDED || n->tok != atok ||
+		    n->type != ROFFT_BODY || n->end != ENDBODY_NOT)
+			continue;
+		body = n;
+		break;
+	}
+
+	/*
+	 * Search backwards for beginnings of blocks,
+	 * both of our own and of pending sub-blocks.
+	 */
+
+	endbody = itblk = later = NULL;
+	for (n = mdoc->last; n; n = n->parent) {
+		if (n->flags & NODE_ENDED)
+			continue;
+
+		/*
+		 * Mismatching end macros can never break anything
+		 * and we only care about the breaking of BLOCKs.
+		 */
+
+		if (body == NULL || n->type != ROFFT_BLOCK)
+			continue;
+
+		/*
+		 * SYNOPSIS name blocks can not be broken themselves,
+		 * but they do get broken together with a broken child.
+		 */
+
+		if (n->tok == MDOC_Nm) {
+			if (later != NULL)
+				n->flags |= NODE_BROKEN | NODE_ENDED;
+			continue;
+		}
+
+		if (n->tok == MDOC_It) {
+			itblk = n;
+			continue;
+		}
+
+		if (atok == n->tok) {
+
+			/*
+			 * Found the start of our own block.
+			 * When there is no pending sub block,
+			 * just proceed to closing out.
+			 */
+
+			if (later == NULL ||
+			    (tok == MDOC_El && itblk == NULL))
+				break;
+
+			/*
+			 * When there is a pending sub block, postpone
+			 * closing out the current block until the
+			 * rew_pending() closing out the sub-block.
+			 * Mark the place where the formatting - but not
+			 * the scope - of the current block ends.
+			 */
+
+			mandoc_msg(MANDOCERR_BLK_NEST,
+			    line, ppos, "%s breaks %s",
+			    roff_name[atok], roff_name[later->tok]);
+
+			endbody = mdoc_endbody_alloc(mdoc, line, ppos,
+			    atok, body);
+
+			if (tok == MDOC_El)
+				itblk->flags |= NODE_ENDED | NODE_BROKEN;
+
+			/*
+			 * If a block closing macro taking arguments
+			 * breaks another block, put the arguments
+			 * into the end marker.
+			 */
+
+			if (maxargs)
+				mdoc->next = ROFF_NEXT_CHILD;
+			break;
+		}
+
+		/*
+		 * Explicit blocks close out description lines, but
+		 * even those can get broken together with a child.
+		 */
+
+		if (n->tok == MDOC_Nd) {
+			if (later != NULL)
+				n->flags |= NODE_BROKEN | NODE_ENDED;
+			else
+				rew_last(mdoc, n);
+			continue;
+		}
+
+		/* Breaking an open sub block. */
+
+		break_intermediate(mdoc->last, body);
+		n->flags |= NODE_BROKEN;
+		if (later == NULL)
+			later = n;
+	}
+
+	if (body == NULL) {
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN, line, ppos,
+		    "%s", roff_name[tok]);
+		if (maxargs && endbody == NULL) {
+			/*
+			 * Stray .Ec without previous .Eo:
+			 * Break the output line, keep the arguments.
+			 */
+			roff_elem_alloc(mdoc, line, ppos, ROFF_br);
+			rew_elem(mdoc, ROFF_br);
+		}
+	} else if (endbody == NULL) {
+		rew_last(mdoc, body);
+		if (maxargs)
+			mdoc_tail_alloc(mdoc, line, ppos, atok);
+	}
+
+	if ((mdoc_macro(tok)->flags & MDOC_PARSED) == 0) {
+		if (buf[*pos] != '\0')
+			mandoc_msg(MANDOCERR_ARG_SKIP, line, ppos,
+			    "%s %s", roff_name[tok], buf + *pos);
+		if (endbody == NULL && n != NULL)
+			rew_pending(mdoc, n);
+
+		/*
+		 * Restore the fill mode that was set before the display.
+		 * This needs to be done here rather than during validation
+		 * such that subsequent nodes get the right flags.
+		 */
+
+		if (tok == MDOC_Ed && body != NULL) {
+			if (body->flags & NODE_NOFILL)
+				mdoc->flags |= ROFF_NOFILL;
+			else
+				mdoc->flags &= ~ROFF_NOFILL;
+		}
+		return;
+	}
+
+	if (endbody != NULL)
+		n = endbody;
+
+	ntok = TOKEN_NONE;
+	for (j = 0; ; j++) {
+		lastarg = *pos;
+
+		if (j == maxargs && n != NULL)
+			rew_last(mdoc, n);
+
+		ac = mdoc_args(mdoc, line, pos, buf, tok, &p);
+		if (ac == ARGS_PUNCT || ac == ARGS_EOLN)
+			break;
+
+		ntok = lookup(mdoc, tok, line, lastarg, p);
+
+		if (ntok == TOKEN_NONE) {
+			dword(mdoc, line, lastarg, p, DELIM_MAX,
+			    mdoc_macro(tok)->flags & MDOC_JOIN);
+			if (ac == ARGS_ALLOC)
+				free(p);
+			continue;
+		}
+		if (ac == ARGS_ALLOC)
+			free(p);
+
+		if (n != NULL)
+			rew_last(mdoc, n);
+		mdoc->flags &= ~MDOC_NEWLINE;
+		(*mdoc_macro(ntok)->fp)(mdoc, ntok, line, lastarg, pos, buf);
+		break;
+	}
+
+	if (n != NULL) {
+		pending = 0;
+		if (ntok != TOKEN_NONE && n->flags & NODE_BROKEN) {
+			target = n;
+			do
+				target = target->parent;
+			while ( ! (target->flags & NODE_ENDED));
+			pending = find_pending(mdoc, ntok, line, ppos, target);
+		}
+		if ( ! pending)
+			rew_pending(mdoc, n);
+	}
+	if (nl)
+		append_delims(mdoc, line, pos, buf);
+}
+
+static void
+in_line(MACRO_PROT_ARGS)
+{
+	int		 la, scope, cnt, firstarg, mayopen, nc, nl;
+	enum roff_tok	 ntok;
+	enum margserr	 ac;
+	enum mdelim	 d;
+	struct mdoc_arg	*arg;
+	char		*p;
+
+	nl = MDOC_NEWLINE & mdoc->flags;
+
+	/*
+	 * Whether we allow ignored elements (those without content,
+	 * usually because of reserved words) to squeak by.
+	 */
+
+	switch (tok) {
+	case MDOC_An:
+	case MDOC_Ar:
+	case MDOC_Fl:
+	case MDOC_Mt:
+	case MDOC_Nm:
+	case MDOC_Pa:
+		nc = 1;
+		break;
+	default:
+		nc = 0;
+		break;
+	}
+
+	mdoc_argv(mdoc, line, tok, &arg, pos, buf);
+
+	d = DELIM_NONE;
+	firstarg = 1;
+	mayopen = 1;
+	for (cnt = scope = 0;; ) {
+		la = *pos;
+		ac = mdoc_args(mdoc, line, pos, buf, tok, &p);
+
+		/*
+		 * At the end of a macro line,
+		 * opening delimiters do not suppress spacing.
+		 */
+
+		if (ac == ARGS_EOLN) {
+			if (d == DELIM_OPEN)
+				mdoc->last->flags &= ~NODE_DELIMO;
+			break;
+		}
+
+		/*
+		 * The rest of the macro line is only punctuation,
+		 * to be handled by append_delims().
+		 * If there were no other arguments,
+		 * do not allow the first one to suppress spacing,
+		 * even if it turns out to be a closing one.
+		 */
+
+		if (ac == ARGS_PUNCT) {
+			if (cnt == 0 && (nc == 0 || tok == MDOC_An))
+				mdoc->flags |= MDOC_NODELIMC;
+			break;
+		}
+
+		ntok = (tok == MDOC_Fn && !cnt) ?
+		    TOKEN_NONE : lookup(mdoc, tok, line, la, p);
+
+		/*
+		 * In this case, we've located a submacro and must
+		 * execute it.  Close out scope, if open.  If no
+		 * elements have been generated, either create one (nc)
+		 * or raise a warning.
+		 */
+
+		if (ntok != TOKEN_NONE) {
+			if (scope)
+				rew_elem(mdoc, tok);
+			if (nc && ! cnt) {
+				mdoc_elem_alloc(mdoc, line, ppos, tok, arg);
+				rew_last(mdoc, mdoc->last);
+			} else if ( ! nc && ! cnt) {
+				mdoc_argv_free(arg);
+				mandoc_msg(MANDOCERR_MACRO_EMPTY,
+				    line, ppos, "%s", roff_name[tok]);
+			}
+			(*mdoc_macro(ntok)->fp)(mdoc, ntok,
+			    line, la, pos, buf);
+			if (nl)
+				append_delims(mdoc, line, pos, buf);
+			if (ac == ARGS_ALLOC)
+				free(p);
+			return;
+		}
+
+		/*
+		 * Handle punctuation.  Set up our scope, if a word;
+		 * rewind the scope, if a delimiter; then append the word.
+		 */
+
+		if ((d = mdoc_isdelim(p)) != DELIM_NONE) {
+			/*
+			 * If we encounter closing punctuation, no word
+			 * has been emitted, no scope is open, and we're
+			 * allowed to have an empty element, then start
+			 * a new scope.
+			 */
+			if ((d == DELIM_CLOSE ||
+			     (d == DELIM_MIDDLE && tok == MDOC_Fl)) &&
+			    !cnt && !scope && nc && mayopen) {
+				mdoc_elem_alloc(mdoc, line, ppos, tok, arg);
+				scope = 1;
+				cnt++;
+				if (tok == MDOC_Nm)
+					mayopen = 0;
+			}
+			/*
+			 * Close out our scope, if one is open, before
+			 * any punctuation.
+			 */
+			if (scope && tok != MDOC_Lk) {
+				rew_elem(mdoc, tok);
+				scope = 0;
+				if (tok == MDOC_Fn)
+					mayopen = 0;
+			}
+		} else if (mayopen && !scope) {
+			mdoc_elem_alloc(mdoc, line, ppos, tok, arg);
+			scope = 1;
+			cnt++;
+		}
+
+		dword(mdoc, line, la, p, d,
+		    mdoc_macro(tok)->flags & MDOC_JOIN);
+
+		if (ac == ARGS_ALLOC)
+			free(p);
+
+		/*
+		 * If the first argument is a closing delimiter,
+		 * do not suppress spacing before it.
+		 */
+
+		if (firstarg && d == DELIM_CLOSE && !nc)
+			mdoc->last->flags &= ~NODE_DELIMC;
+		firstarg = 0;
+
+		/*
+		 * `Fl' macros have their scope re-opened with each new
+		 * word so that the `-' can be added to each one without
+		 * having to parse out spaces.
+		 */
+		if (scope && tok == MDOC_Fl) {
+			rew_elem(mdoc, tok);
+			scope = 0;
+		}
+	}
+
+	if (scope && tok != MDOC_Lk) {
+		rew_elem(mdoc, tok);
+		scope = 0;
+	}
+
+	/*
+	 * If no elements have been collected and we're allowed to have
+	 * empties (nc), open a scope and close it out.  Otherwise,
+	 * raise a warning.
+	 */
+
+	if ( ! cnt) {
+		if (nc) {
+			mdoc_elem_alloc(mdoc, line, ppos, tok, arg);
+			rew_last(mdoc, mdoc->last);
+		} else {
+			mdoc_argv_free(arg);
+			mandoc_msg(MANDOCERR_MACRO_EMPTY,
+			    line, ppos, "%s", roff_name[tok]);
+		}
+	}
+	if (nl)
+		append_delims(mdoc, line, pos, buf);
+	if (scope)
+		rew_elem(mdoc, tok);
+}
+
+static void
+blk_full(MACRO_PROT_ARGS)
+{
+	struct mdoc_arg	 *arg;
+	struct roff_node *blk; /* Our own or a broken block. */
+	struct roff_node *head; /* Our own head. */
+	struct roff_node *body; /* Our own body. */
+	struct roff_node *n;
+	char		 *p;
+	size_t		  iarg;
+	int		  done, la, nl, parsed;
+	enum margserr	  ac, lac;
+
+	nl = MDOC_NEWLINE & mdoc->flags;
+
+	if (buf[*pos] == '\0' && (tok == MDOC_Sh || tok == MDOC_Ss)) {
+		mandoc_msg(MANDOCERR_MACRO_EMPTY,
+		    line, ppos, "%s", roff_name[tok]);
+		return;
+	}
+
+	if ((mdoc_macro(tok)->flags & MDOC_EXPLICIT) == 0) {
+
+		/* Here, tok is one of Sh Ss Nm Nd It. */
+
+		blk = NULL;
+		for (n = mdoc->last; n != NULL; n = n->parent) {
+			if (n->flags & NODE_ENDED) {
+				if ( ! (n->flags & NODE_VALID))
+					n->flags |= NODE_BROKEN;
+				continue;
+			}
+			if (n->type != ROFFT_BLOCK)
+				continue;
+
+			if (tok == MDOC_It && n->tok == MDOC_Bl) {
+				if (blk != NULL) {
+					mandoc_msg(MANDOCERR_BLK_BROKEN,
+					    line, ppos, "It breaks %s",
+					    roff_name[blk->tok]);
+					rew_pending(mdoc, blk);
+				}
+				break;
+			}
+
+			if (mdoc_macro(n->tok)->flags & MDOC_EXPLICIT) {
+				switch (tok) {
+				case MDOC_Sh:
+				case MDOC_Ss:
+					mandoc_msg(MANDOCERR_BLK_BROKEN,
+					    line, ppos,
+					    "%s breaks %s", roff_name[tok],
+					    roff_name[n->tok]);
+					rew_pending(mdoc, n);
+					n = mdoc->last;
+					continue;
+				case MDOC_It:
+					/* Delay in case it's astray. */
+					blk = n;
+					continue;
+				default:
+					break;
+				}
+				break;
+			}
+
+			/* Here, n is one of Sh Ss Nm Nd It. */
+
+			if (tok != MDOC_Sh && (n->tok == MDOC_Sh ||
+			    (tok != MDOC_Ss && (n->tok == MDOC_Ss ||
+			     (tok != MDOC_It && n->tok == MDOC_It)))))
+				break;
+
+			/* Item breaking an explicit block. */
+
+			if (blk != NULL) {
+				mandoc_msg(MANDOCERR_BLK_BROKEN, line, ppos,
+				    "It breaks %s", roff_name[blk->tok]);
+				rew_pending(mdoc, blk);
+				blk = NULL;
+			}
+
+			/* Close out prior implicit scopes. */
+
+			rew_pending(mdoc, n);
+		}
+
+		/* Skip items outside lists. */
+
+		if (tok == MDOC_It && (n == NULL || n->tok != MDOC_Bl)) {
+			mandoc_msg(MANDOCERR_IT_STRAY,
+			    line, ppos, "It %s", buf + *pos);
+			roff_elem_alloc(mdoc, line, ppos, ROFF_br);
+			rew_elem(mdoc, ROFF_br);
+			return;
+		}
+	}
+
+	/*
+	 * This routine accommodates implicitly- and explicitly-scoped
+	 * macro openings.  Implicit ones first close out prior scope
+	 * (seen above).  Delay opening the head until necessary to
+	 * allow leading punctuation to print.  Special consideration
+	 * for `It -column', which has phrase-part syntax instead of
+	 * regular child nodes.
+	 */
+
+	switch (tok) {
+	case MDOC_Sh:
+		mdoc->flags &= ~ROFF_NOFILL;
+		break;
+	case MDOC_Ss:
+		mdoc->flags |= ROFF_NONOFILL;
+		break;
+	default:
+		break;
+	}
+	mdoc_argv(mdoc, line, tok, &arg, pos, buf);
+	blk = mdoc_block_alloc(mdoc, line, ppos, tok, arg);
+	head = body = NULL;
+
+	/*
+	 * Exception: Heads of `It' macros in `-diag' lists are not
+	 * parsed, even though `It' macros in general are parsed.
+	 */
+
+	parsed = tok != MDOC_It ||
+	    mdoc->last->parent->tok != MDOC_Bl ||
+	    mdoc->last->parent->norm->Bl.type != LIST_diag;
+
+	/*
+	 * The `Nd' macro has all arguments in its body: it's a hybrid
+	 * of block partial-explicit and full-implicit.  Stupid.
+	 */
+
+	if (tok == MDOC_Nd) {
+		head = roff_head_alloc(mdoc, line, ppos, tok);
+		rew_last(mdoc, head);
+		body = roff_body_alloc(mdoc, line, ppos, tok);
+	}
+
+	if (tok == MDOC_Bk)
+		mdoc->flags |= MDOC_KEEP;
+
+	ac = ARGS_EOLN;
+	for (;;) {
+
+		/*
+		 * If we are right after a tab character,
+		 * do not parse the first word for macros.
+		 */
+
+		if (mdoc->flags & MDOC_PHRASEQN) {
+			mdoc->flags &= ~MDOC_PHRASEQN;
+			mdoc->flags |= MDOC_PHRASEQF;
+		}
+
+		la = *pos;
+		lac = ac;
+		ac = mdoc_args(mdoc, line, pos, buf, tok, &p);
+		if (ac == ARGS_EOLN) {
+			if (lac != ARGS_PHRASE ||
+			    ! (mdoc->flags & MDOC_PHRASEQF))
+				break;
+
+			/*
+			 * This line ends in a tab; start the next
+			 * column now, with a leading blank.
+			 */
+
+			if (body != NULL)
+				rew_last(mdoc, body);
+			body = roff_body_alloc(mdoc, line, ppos, tok);
+			roff_word_alloc(mdoc, line, ppos, "\\&");
+			break;
+		}
+
+		if (tok == MDOC_Bd || tok == MDOC_Bk) {
+			mandoc_msg(MANDOCERR_ARG_EXCESS, line, la,
+			    "%s ... %s", roff_name[tok], buf + la);
+			if (ac == ARGS_ALLOC)
+				free(p);
+			break;
+		}
+		if (tok == MDOC_Rs) {
+			mandoc_msg(MANDOCERR_ARG_SKIP,
+			    line, la, "Rs %s", buf + la);
+			if (ac == ARGS_ALLOC)
+				free(p);
+			break;
+		}
+		if (ac == ARGS_PUNCT)
+			break;
+
+		/*
+		 * Emit leading punctuation (i.e., punctuation before
+		 * the ROFFT_HEAD) for non-phrase types.
+		 */
+
+		if (head == NULL &&
+		    ac != ARGS_PHRASE &&
+		    mdoc_isdelim(p) == DELIM_OPEN) {
+			dword(mdoc, line, la, p, DELIM_OPEN, 0);
+			if (ac == ARGS_ALLOC)
+				free(p);
+			continue;
+		}
+
+		/* Open a head if one hasn't been opened. */
+
+		if (head == NULL)
+			head = roff_head_alloc(mdoc, line, ppos, tok);
+
+		if (ac == ARGS_PHRASE) {
+
+			/*
+			 * If we haven't opened a body yet, rewind the
+			 * head; if we have, rewind that instead.
+			 */
+
+			rew_last(mdoc, body == NULL ? head : body);
+			body = roff_body_alloc(mdoc, line, ppos, tok);
+
+			/* Process to the tab or to the end of the line. */
+
+			mdoc->flags |= MDOC_PHRASE;
+			parse_rest(mdoc, TOKEN_NONE, line, &la, buf);
+			mdoc->flags &= ~MDOC_PHRASE;
+
+			/* There may have been `Ta' macros. */
+
+			while (body->next != NULL)
+				body = body->next;
+			continue;
+		}
+
+		done = macro_or_word(mdoc, tok, line, la, pos, buf, p, parsed);
+		if (ac == ARGS_ALLOC)
+			free(p);
+		if (done)
+			break;
+	}
+
+	if (blk->flags & NODE_VALID)
+		return;
+	if (head == NULL)
+		head = roff_head_alloc(mdoc, line, ppos, tok);
+	if (nl && tok != MDOC_Bd && tok != MDOC_Bl && tok != MDOC_Rs)
+		append_delims(mdoc, line, pos, buf);
+	if (body != NULL)
+		goto out;
+	if (find_pending(mdoc, tok, line, ppos, head))
+		return;
+
+	/* Close out scopes to remain in a consistent state. */
+
+	rew_last(mdoc, head);
+	body = roff_body_alloc(mdoc, line, ppos, tok);
+	if (tok == MDOC_Ss)
+		mdoc->flags &= ~ROFF_NONOFILL;
+
+	/*
+	 * Set up fill mode for display blocks.
+	 * This needs to be done here up front rather than during
+	 * validation such that child nodes get the right flags.
+	 */
+
+	if (tok == MDOC_Bd && arg != NULL) {
+		for (iarg = 0; iarg < arg->argc; iarg++) {
+			switch (arg->argv[iarg].arg) {
+			case MDOC_Unfilled:
+			case MDOC_Literal:
+				mdoc->flags |= ROFF_NOFILL;
+				break;
+			case MDOC_Filled:
+			case MDOC_Ragged:
+			case MDOC_Centred:
+				mdoc->flags &= ~ROFF_NOFILL;
+				break;
+			default:
+				continue;
+			}
+			break;
+		}
+	}
+out:
+	if (mdoc->flags & MDOC_FREECOL) {
+		rew_last(mdoc, body);
+		rew_last(mdoc, blk);
+		mdoc->flags &= ~MDOC_FREECOL;
+	}
+}
+
+static void
+blk_part_imp(MACRO_PROT_ARGS)
+{
+	int		  done, la, nl;
+	enum margserr	  ac;
+	char		 *p;
+	struct roff_node *blk; /* saved block context */
+	struct roff_node *body; /* saved body context */
+	struct roff_node *n;
+
+	nl = MDOC_NEWLINE & mdoc->flags;
+
+	/*
+	 * A macro that spans to the end of the line.  This is generally
+	 * (but not necessarily) called as the first macro.  The block
+	 * has a head as the immediate child, which is always empty,
+	 * followed by zero or more opening punctuation nodes, then the
+	 * body (which may be empty, depending on the macro), then zero
+	 * or more closing punctuation nodes.
+	 */
+
+	blk = mdoc_block_alloc(mdoc, line, ppos, tok, NULL);
+	rew_last(mdoc, roff_head_alloc(mdoc, line, ppos, tok));
+
+	/*
+	 * Open the body scope "on-demand", that is, after we've
+	 * processed all our the leading delimiters (open parenthesis,
+	 * etc.).
+	 */
+
+	for (body = NULL; ; ) {
+		la = *pos;
+		ac = mdoc_args(mdoc, line, pos, buf, tok, &p);
+		if (ac == ARGS_EOLN || ac == ARGS_PUNCT)
+			break;
+
+		if (body == NULL && mdoc_isdelim(p) == DELIM_OPEN) {
+			dword(mdoc, line, la, p, DELIM_OPEN, 0);
+			if (ac == ARGS_ALLOC)
+				free(p);
+			continue;
+		}
+
+		if (body == NULL)
+			body = roff_body_alloc(mdoc, line, ppos, tok);
+
+		done = macro_or_word(mdoc, tok, line, la, pos, buf, p, 1);
+		if (ac == ARGS_ALLOC)
+			free(p);
+		if (done)
+			break;
+	}
+	if (body == NULL)
+		body = roff_body_alloc(mdoc, line, ppos, tok);
+
+	if (find_pending(mdoc, tok, line, ppos, body))
+		return;
+
+	rew_last(mdoc, body);
+	if (nl)
+		append_delims(mdoc, line, pos, buf);
+	rew_pending(mdoc, blk);
+
+	/* Move trailing .Ns out of scope. */
+
+	for (n = body->child; n && n->next; n = n->next)
+		/* Do nothing. */ ;
+	if (n && n->tok == MDOC_Ns)
+		roff_node_relink(mdoc, n);
+}
+
+static void
+blk_part_exp(MACRO_PROT_ARGS)
+{
+	int		  done, la, nl;
+	enum margserr	  ac;
+	struct roff_node *head; /* keep track of head */
+	char		 *p;
+
+	nl = MDOC_NEWLINE & mdoc->flags;
+
+	/*
+	 * The opening of an explicit macro having zero or more leading
+	 * punctuation nodes; a head with optional single element (the
+	 * case of `Eo'); and a body that may be empty.
+	 */
+
+	roff_block_alloc(mdoc, line, ppos, tok);
+	head = NULL;
+	for (;;) {
+		la = *pos;
+		ac = mdoc_args(mdoc, line, pos, buf, tok, &p);
+		if (ac == ARGS_PUNCT || ac == ARGS_EOLN)
+			break;
+
+		/* Flush out leading punctuation. */
+
+		if (head == NULL && mdoc_isdelim(p) == DELIM_OPEN) {
+			dword(mdoc, line, la, p, DELIM_OPEN, 0);
+			if (ac == ARGS_ALLOC)
+				free(p);
+			continue;
+		}
+
+		if (head == NULL) {
+			head = roff_head_alloc(mdoc, line, ppos, tok);
+			if (tok == MDOC_Eo)  /* Not parsed. */
+				dword(mdoc, line, la, p, DELIM_MAX, 0);
+			rew_last(mdoc, head);
+			roff_body_alloc(mdoc, line, ppos, tok);
+			if (tok == MDOC_Eo) {
+				if (ac == ARGS_ALLOC)
+					free(p);
+				continue;
+			}
+		}
+
+		done = macro_or_word(mdoc, tok, line, la, pos, buf, p, 1);
+		if (ac == ARGS_ALLOC)
+			free(p);
+		if (done)
+			break;
+	}
+
+	/* Clean-up to leave in a consistent state. */
+
+	if (head == NULL) {
+		rew_last(mdoc, roff_head_alloc(mdoc, line, ppos, tok));
+		roff_body_alloc(mdoc, line, ppos, tok);
+	}
+	if (nl)
+		append_delims(mdoc, line, pos, buf);
+}
+
+static void
+in_line_argn(MACRO_PROT_ARGS)
+{
+	struct mdoc_arg	*arg;
+	char		*p;
+	enum margserr	 ac;
+	enum roff_tok	 ntok;
+	int		 state; /* arg#; -1: not yet open; -2: closed */
+	int		 la, maxargs, nl;
+
+	nl = mdoc->flags & MDOC_NEWLINE;
+
+	/*
+	 * A line macro that has a fixed number of arguments (maxargs).
+	 * Only open the scope once the first non-leading-punctuation is
+	 * found (unless MDOC_IGNDELIM is noted, like in `Pf'), then
+	 * keep it open until the maximum number of arguments are
+	 * exhausted.
+	 */
+
+	switch (tok) {
+	case MDOC_Ap:
+	case MDOC_Ns:
+	case MDOC_Ux:
+		maxargs = 0;
+		break;
+	case MDOC_Bx:
+	case MDOC_Es:
+	case MDOC_Xr:
+		maxargs = 2;
+		break;
+	default:
+		maxargs = 1;
+		break;
+	}
+
+	mdoc_argv(mdoc, line, tok, &arg, pos, buf);
+
+	state = -1;
+	p = NULL;
+	for (;;) {
+		la = *pos;
+		ac = mdoc_args(mdoc, line, pos, buf, tok, &p);
+
+		if ((ac == ARGS_WORD || ac == ARGS_ALLOC) && state == -1 &&
+		    (mdoc_macro(tok)->flags & MDOC_IGNDELIM) == 0 &&
+		    mdoc_isdelim(p) == DELIM_OPEN) {
+			dword(mdoc, line, la, p, DELIM_OPEN, 0);
+			if (ac == ARGS_ALLOC)
+				free(p);
+			continue;
+		}
+
+		if (state == -1 && tok != MDOC_In &&
+		    tok != MDOC_St && tok != MDOC_Xr) {
+			mdoc_elem_alloc(mdoc, line, ppos, tok, arg);
+			state = 0;
+		}
+
+		if (ac == ARGS_PUNCT || ac == ARGS_EOLN) {
+			if (abs(state) < 2 && tok == MDOC_Pf)
+				mandoc_msg(MANDOCERR_PF_SKIP,
+				    line, ppos, "Pf %s",
+				    p == NULL ? "at eol" : p);
+			break;
+		}
+
+		if (state == maxargs) {
+			rew_elem(mdoc, tok);
+			state = -2;
+		}
+
+		ntok = (tok == MDOC_Pf && state == 0) ?
+		    TOKEN_NONE : lookup(mdoc, tok, line, la, p);
+
+		if (ntok != TOKEN_NONE) {
+			if (state >= 0) {
+				rew_elem(mdoc, tok);
+				state = -2;
+			}
+			(*mdoc_macro(ntok)->fp)(mdoc, ntok,
+			    line, la, pos, buf);
+			if (ac == ARGS_ALLOC)
+				free(p);
+			break;
+		}
+
+		if (mdoc_macro(tok)->flags & MDOC_IGNDELIM ||
+		    mdoc_isdelim(p) == DELIM_NONE) {
+			if (state == -1) {
+				mdoc_elem_alloc(mdoc, line, ppos, tok, arg);
+				state = 1;
+			} else if (state >= 0)
+				state++;
+		} else if (state >= 0) {
+			rew_elem(mdoc, tok);
+			state = -2;
+		}
+
+		dword(mdoc, line, la, p, DELIM_MAX,
+		    mdoc_macro(tok)->flags & MDOC_JOIN);
+		if (ac == ARGS_ALLOC)
+			free(p);
+		p = mdoc->last->string;
+	}
+
+	if (state == -1) {
+		mandoc_msg(MANDOCERR_MACRO_EMPTY,
+		    line, ppos, "%s", roff_name[tok]);
+		return;
+	}
+
+	if (state == 0 && tok == MDOC_Pf)
+		append_delims(mdoc, line, pos, buf);
+	if (state >= 0)
+		rew_elem(mdoc, tok);
+	if (nl)
+		append_delims(mdoc, line, pos, buf);
+}
+
+static void
+in_line_eoln(MACRO_PROT_ARGS)
+{
+	struct roff_node	*n;
+	struct mdoc_arg		*arg;
+
+	if ((tok == MDOC_Pp || tok == MDOC_Lp) &&
+	    ! (mdoc->flags & MDOC_SYNOPSIS)) {
+		n = mdoc->last;
+		if (mdoc->next == ROFF_NEXT_SIBLING)
+			n = n->parent;
+		if (n->tok == MDOC_Nm)
+			rew_last(mdoc, n->parent);
+	}
+
+	if (buf[*pos] == '\0' &&
+	    (tok == MDOC_Fd || *roff_name[tok] == '%')) {
+		mandoc_msg(MANDOCERR_MACRO_EMPTY,
+		    line, ppos, "%s", roff_name[tok]);
+		return;
+	}
+
+	mdoc_argv(mdoc, line, tok, &arg, pos, buf);
+	mdoc_elem_alloc(mdoc, line, ppos, tok, arg);
+	if (parse_rest(mdoc, tok, line, pos, buf))
+		return;
+	rew_elem(mdoc, tok);
+}
+
+/*
+ * The simplest argument parser available: Parse the remaining
+ * words until the end of the phrase or line and return 0
+ * or until the next macro, call that macro, and return 1.
+ */
+static int
+parse_rest(struct roff_man *mdoc, enum roff_tok tok,
+    int line, int *pos, char *buf)
+{
+	char		*p;
+	int		 done, la;
+	enum margserr	 ac;
+
+	for (;;) {
+		la = *pos;
+		ac = mdoc_args(mdoc, line, pos, buf, tok, &p);
+		if (ac == ARGS_EOLN)
+			return 0;
+		done = macro_or_word(mdoc, tok, line, la, pos, buf, p, 1);
+		if (ac == ARGS_ALLOC)
+			free(p);
+		if (done)
+			return 1;
+	}
+}
+
+static void
+ctx_synopsis(MACRO_PROT_ARGS)
+{
+
+	if (~mdoc->flags & (MDOC_SYNOPSIS | MDOC_NEWLINE))
+		in_line(mdoc, tok, line, ppos, pos, buf);
+	else if (tok == MDOC_Nm)
+		blk_full(mdoc, tok, line, ppos, pos, buf);
+	else {
+		assert(tok == MDOC_Vt);
+		blk_part_imp(mdoc, tok, line, ppos, pos, buf);
+	}
+}
+
+/*
+ * Phrases occur within `Bl -column' entries, separated by `Ta' or tabs.
+ * They're unusual because they're basically free-form text until a
+ * macro is encountered.
+ */
+static void
+phrase_ta(MACRO_PROT_ARGS)
+{
+	struct roff_node *body, *n;
+
+	/* Make sure we are in a column list or ignore this macro. */
+
+	body = NULL;
+	for (n = mdoc->last; n != NULL; n = n->parent) {
+		if (n->flags & NODE_ENDED)
+			continue;
+		if (n->tok == MDOC_It && n->type == ROFFT_BODY)
+			body = n;
+		if (n->tok == MDOC_Bl && n->end == ENDBODY_NOT)
+			break;
+	}
+
+	if (n == NULL || n->norm->Bl.type != LIST_column) {
+		mandoc_msg(MANDOCERR_TA_STRAY, line, ppos, "Ta");
+		return;
+	}
+
+	/* Advance to the next column. */
+
+	rew_last(mdoc, body);
+	roff_body_alloc(mdoc, line, ppos, MDOC_It);
+	parse_rest(mdoc, TOKEN_NONE, line, pos, buf);
+}
diff --git a/usr.bin/mandoc/mdoc_man.c b/usr.bin/mandoc/mdoc_man.c
new file mode 100644
index 0000000..25d59a5
--- /dev/null
+++ b/usr.bin/mandoc/mdoc_man.c
@@ -0,0 +1,1836 @@
+/*	$OpenBSD: mdoc_man.c,v 1.134 2020/02/27 01:25:57 schwarze Exp $ */
+/*
+ * Copyright (c) 2011-2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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;
+
+	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;
+
+	if (n->type == 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;
+	} else if (n->tok < ROFF_MAX) {
+		(*roff_man_acts[n->tok])(meta, n);
+		return;
+	} else {
+		/*
+		 * Conditionally run the pre-node action handler for a
+		 * node.
+		 */
+		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);
+	}
+
+	/*
+	 * 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 ((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);
+
+	/* Remeber 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/usr.bin/mandoc/mdoc_markdown.c b/usr.bin/mandoc/mdoc_markdown.c
new file mode 100644
index 0000000..e0572cb
--- /dev/null
+++ b/usr.bin/mandoc/mdoc_markdown.c
@@ -0,0 +1,1606 @@
+/* $OpenBSD: mdoc_markdown.c,v 1.35 2020/04/03 11:34:19 schwarze Exp $ */
+/*
+ * Copyright (c) 2017, 2018, 2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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__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, NULL, 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);
+	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:
+				nextfont = "**";
+				break;
+			case ESCAPE_FONTITALIC:
+				nextfont = "*";
+				break;
+			case ESCAPE_FONTBI:
+				nextfont = "***";
+				break;
+			case ESCAPE_FONT:
+			case ESCAPE_FONTCW:
+			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 (*prefix == '`')
+			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 (*suffix == '`')
+			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__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/usr.bin/mandoc/mdoc_state.c b/usr.bin/mandoc/mdoc_state.c
new file mode 100644
index 0000000..954f709
--- /dev/null
+++ b/usr.bin/mandoc/mdoc_state.c
@@ -0,0 +1,254 @@
+/*	$OpenBSD: mdoc_state.c,v 1.16 2020/01/19 17:59:01 schwarze Exp $ */
+/*
+ * Copyright (c) 2014, 2015, 2017, 2018 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "mdoc.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+#include "libmdoc.h"
+
+#define STATE_ARGS  struct roff_man *mdoc, struct roff_node *n
+
+typedef	void	(*state_handler)(STATE_ARGS);
+
+static	void	 state_bl(STATE_ARGS);
+static	void	 state_sh(STATE_ARGS);
+static	void	 state_sm(STATE_ARGS);
+
+static	const state_handler state_handlers[MDOC_MAX - MDOC_Dd] = {
+	NULL,		/* Dd */
+	NULL,		/* Dt */
+	NULL,		/* Os */
+	state_sh,	/* Sh */
+	NULL,		/* Ss */
+	NULL,		/* Pp */
+	NULL,		/* D1 */
+	NULL,		/* Dl */
+	NULL,		/* Bd */
+	NULL,		/* Ed */
+	state_bl,	/* Bl */
+	NULL,		/* El */
+	NULL,		/* It */
+	NULL,		/* Ad */
+	NULL,		/* An */
+	NULL,		/* Ap */
+	NULL,		/* Ar */
+	NULL,		/* Cd */
+	NULL,		/* Cm */
+	NULL,		/* Dv */
+	NULL,		/* Er */
+	NULL,		/* Ev */
+	NULL,		/* Ex */
+	NULL,		/* Fa */
+	NULL,		/* Fd */
+	NULL,		/* Fl */
+	NULL,		/* Fn */
+	NULL,		/* Ft */
+	NULL,		/* Ic */
+	NULL,		/* In */
+	NULL,		/* Li */
+	NULL,		/* Nd */
+	NULL,		/* Nm */
+	NULL,		/* Op */
+	NULL,		/* Ot */
+	NULL,		/* Pa */
+	NULL,		/* Rv */
+	NULL,		/* St */
+	NULL,		/* Va */
+	NULL,		/* Vt */
+	NULL,		/* Xr */
+	NULL,		/* %A */
+	NULL,		/* %B */
+	NULL,		/* %D */
+	NULL,		/* %I */
+	NULL,		/* %J */
+	NULL,		/* %N */
+	NULL,		/* %O */
+	NULL,		/* %P */
+	NULL,		/* %R */
+	NULL,		/* %T */
+	NULL,		/* %V */
+	NULL,		/* Ac */
+	NULL,		/* Ao */
+	NULL,		/* Aq */
+	NULL,		/* At */
+	NULL,		/* Bc */
+	NULL,		/* Bf */
+	NULL,		/* Bo */
+	NULL,		/* Bq */
+	NULL,		/* Bsx */
+	NULL,		/* Bx */
+	NULL,		/* Db */
+	NULL,		/* Dc */
+	NULL,		/* Do */
+	NULL,		/* Dq */
+	NULL,		/* Ec */
+	NULL,		/* Ef */
+	NULL,		/* Em */
+	NULL,		/* Eo */
+	NULL,		/* Fx */
+	NULL,		/* Ms */
+	NULL,		/* No */
+	NULL,		/* Ns */
+	NULL,		/* Nx */
+	NULL,		/* Ox */
+	NULL,		/* Pc */
+	NULL,		/* Pf */
+	NULL,		/* Po */
+	NULL,		/* Pq */
+	NULL,		/* Qc */
+	NULL,		/* Ql */
+	NULL,		/* Qo */
+	NULL,		/* Qq */
+	NULL,		/* Re */
+	NULL,		/* Rs */
+	NULL,		/* Sc */
+	NULL,		/* So */
+	NULL,		/* Sq */
+	state_sm,	/* Sm */
+	NULL,		/* Sx */
+	NULL,		/* Sy */
+	NULL,		/* Tn */
+	NULL,		/* Ux */
+	NULL,		/* Xc */
+	NULL,		/* Xo */
+	NULL,		/* Fo */
+	NULL,		/* Fc */
+	NULL,		/* Oo */
+	NULL,		/* Oc */
+	NULL,		/* Bk */
+	NULL,		/* Ek */
+	NULL,		/* Bt */
+	NULL,		/* Hf */
+	NULL,		/* Fr */
+	NULL,		/* Ud */
+	NULL,		/* Lb */
+	NULL,		/* Lp */
+	NULL,		/* Lk */
+	NULL,		/* Mt */
+	NULL,		/* Brq */
+	NULL,		/* Bro */
+	NULL,		/* Brc */
+	NULL,		/* %C */
+	NULL,		/* Es */
+	NULL,		/* En */
+	NULL,		/* Dx */
+	NULL,		/* %Q */
+	NULL,		/* %U */
+	NULL,		/* Ta */
+	NULL,		/* Tg */
+};
+
+
+void
+mdoc_state(struct roff_man *mdoc, struct roff_node *n)
+{
+	state_handler handler;
+
+	if (n->tok == TOKEN_NONE || n->tok < ROFF_MAX)
+		return;
+
+	assert(n->tok >= MDOC_Dd && n->tok < MDOC_MAX);
+	if ((mdoc_macro(n->tok)->flags & MDOC_PROLOGUE) == 0)
+		mdoc->flags |= MDOC_PBODY;
+
+	handler = state_handlers[n->tok - MDOC_Dd];
+	if (*handler)
+		(*handler)(mdoc, n);
+}
+
+static void
+state_bl(STATE_ARGS)
+{
+	struct mdoc_arg	*args;
+	size_t		 i;
+
+	if (n->type != ROFFT_HEAD || n->parent->args == NULL)
+		return;
+
+	args = n->parent->args;
+	for (i = 0; i < args->argc; i++) {
+		switch(args->argv[i].arg) {
+		case MDOC_Diag:
+			n->norm->Bl.type = LIST_diag;
+			return;
+		case MDOC_Column:
+			n->norm->Bl.type = LIST_column;
+			return;
+		default:
+			break;
+		}
+	}
+}
+
+static void
+state_sh(STATE_ARGS)
+{
+	struct roff_node *nch;
+	char		 *secname;
+
+	if (n->type != ROFFT_HEAD)
+		return;
+
+	if ( ! (n->flags & NODE_VALID)) {
+		secname = NULL;
+		deroff(&secname, n);
+
+		/*
+		 * Set the section attribute for the BLOCK, HEAD,
+		 * and HEAD children; the latter can only be TEXT
+		 * nodes, so no recursion is needed.  For other
+		 * nodes, including the .Sh BODY, this is done
+		 * when allocating the node data structures, but
+		 * for .Sh BLOCK and HEAD, the section is still
+		 * unknown at that time.
+		 */
+
+		n->sec = n->parent->sec = secname == NULL ?
+		    SEC_CUSTOM : mdoc_a2sec(secname);
+		for (nch = n->child; nch != NULL; nch = nch->next)
+			nch->sec = n->sec;
+		free(secname);
+	}
+
+	if ((mdoc->lastsec = n->sec) == SEC_SYNOPSIS) {
+		roff_setreg(mdoc->roff, "nS", 1, '=');
+		mdoc->flags |= MDOC_SYNOPSIS;
+	} else {
+		roff_setreg(mdoc->roff, "nS", 0, '=');
+		mdoc->flags &= ~MDOC_SYNOPSIS;
+	}
+}
+
+static void
+state_sm(STATE_ARGS)
+{
+
+	if (n->child == NULL)
+		mdoc->flags ^= MDOC_SMOFF;
+	else if ( ! strcmp(n->child->string, "on"))
+		mdoc->flags &= ~MDOC_SMOFF;
+	else if ( ! strcmp(n->child->string, "off"))
+		mdoc->flags |= MDOC_SMOFF;
+}
diff --git a/usr.bin/mandoc/mdoc_term.c b/usr.bin/mandoc/mdoc_term.c
new file mode 100644
index 0000000..6362a21
--- /dev/null
+++ b/usr.bin/mandoc/mdoc_term.c
@@ -0,0 +1,1962 @@
+/* $OpenBSD: mdoc_term.c,v 1.279 2020/04/06 09:55:49 schwarze Exp $ */
+/*
+ * Copyright (c) 2010, 2012-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2013 Franco Fichtner <franco@lastsummer.de>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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;
+	size_t			 save_defindent;
+
+	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 {
+		save_defindent = p->defindent;
+		if (p->defindent == 0)
+			p->defindent = 5;
+		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);
+		p->defindent = save_defindent;
+	}
+}
+
+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;
+	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
+		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;
+
+	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
+	 */
+
+	term_vspace(p);
+
+	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->trailspace = 1;
+	p->flags |= TERMP_NOSPACE | TERMP_NOBREAK;
+
+	term_word(p, meta->os);
+	term_flushln(p);
+
+	p->tcol->offset = p->tcol->rmargin;
+	sz = term_strlen(p, meta->os);
+	p->tcol->rmargin = p->maxrmargin > sz ? p->maxrmargin - sz : 0;
+	p->flags |= TERMP_NOSPACE;
+
+	term_word(p, meta->date);
+	term_flushln(p);
+
+	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_flushln(p);
+
+	p->tcol->offset = 0;
+	p->tcol->rmargin = p->maxrmargin;
+	p->flags = 0;
+}
+
+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.
+	 */
+
+	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);
+
+	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->maxrmargin - vollen + term_len(p, 1)) / 2 :
+	    vollen < p->maxrmargin ?  p->maxrmargin - vollen : 0;
+
+	term_word(p, title);
+	term_flushln(p);
+
+	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);
+
+	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);
+}
+
+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') {
+		SCALE_HS_INIT(&su, term_strlen(p, v));
+		su.scale /= term_strlen(p, "0");
+	}
+	return term_hen(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;
+	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++) {
+			SCALE_HS_INIT(&su,
+			    term_strlen(p, bl->norm->Bl.cols[i]));
+			su.scale /= term_strlen(p, "0");
+			offset += term_hen(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.
+		 */
+		SCALE_HS_INIT(&su, term_strlen(p, bl->norm->Bl.cols[i]));
+		su.scale /= term_strlen(p, "0");
+		width = term_hen(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:
+		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:
+		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;
+
+	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);
+		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)
+		term_newln(p);
+}
+
+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/usr.bin/mandoc/mdoc_validate.c b/usr.bin/mandoc/mdoc_validate.c
new file mode 100644
index 0000000..34668bf
--- /dev/null
+++ b/usr.bin/mandoc/mdoc_validate.c
@@ -0,0 +1,3047 @@
+/* $OpenBSD: mdoc_validate.c,v 1.302 2020/04/26 21:29:45 schwarze Exp $ */
+/*
+ * Copyright (c) 2010-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2010 Joerg Sonnenberger <joerg@netbsd.org>
+ *
+ * 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 <sys/types.h>
+#ifndef OSNAME
+#include <sys/utsname.h>
+#endif
+
+#include <assert.h>
+#include <ctype.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#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_delim_nb,	/* 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;
+
+	post_delim_nb(mdoc);
+
+	n = mdoc->last;
+	assert(n->child->type == ROFFT_TEXT);
+	mdoc->next = ROFF_NEXT_CHILD;
+	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->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";
+		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 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;
+
+	if (*arg == NULL)
+		return;
+	else if ( ! strcmp(*arg, "Ds"))
+		width = 6;
+	else if ((tok = roffhash_find(mdoc->mdocmac, *arg, 0)) == TOKEN_NONE)
+		return;
+	else
+		width = macro2len(tok);
+
+	free(*arg);
+	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_WEAK, 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)
+		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. */
+	}
+
+	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;
+	static char	 *defbuf;
+#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 (defbuf == NULL) {
+		if (uname(&utsname) == -1) {
+			mandoc_msg(MANDOCERR_OS_UNAME, n->line, n->pos, "Os");
+			defbuf = mandoc_strdup("UNKNOWN");
+		} else
+			mandoc_asprintf(&defbuf, "%s %s",
+			    utsname.sysname, utsname.release);
+	}
+	mdoc->meta.os = mandoc_strdup(defbuf);
+#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/usr.bin/mandoc/msec.c b/usr.bin/mandoc/msec.c
new file mode 100644
index 0000000..813a140
--- /dev/null
+++ b/usr.bin/mandoc/msec.c
@@ -0,0 +1,35 @@
+/*	$OpenBSD: msec.c,v 1.13 2018/12/14 01:17:46 schwarze Exp $ */
+/*
+ * Copyright (c) 2009 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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 <sys/types.h>
+
+#include <stdio.h>
+#include <string.h>
+
+#include "mandoc.h"
+#include "libmandoc.h"
+
+#define LINE(x, y) \
+	if (0 == strcmp(p, x)) return(y);
+
+const char *
+mandoc_a2msec(const char *p)
+{
+
+#include "msec.in"
+
+	return NULL;
+}
diff --git a/usr.bin/mandoc/msec.in b/usr.bin/mandoc/msec.in
new file mode 100644
index 0000000..f8d786e
--- /dev/null
+++ b/usr.bin/mandoc/msec.in
@@ -0,0 +1,34 @@
+/*	$OpenBSD: msec.in,v 1.6 2017/06/24 17:36:50 schwarze Exp $ */
+/*
+ * Copyright (c) 2009 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ */
+
+/*
+ * These are all possible manual-section macros and what they correspond
+ * to when rendered as the volume title.
+ *
+ * Be sure to escape strings.
+ */
+
+LINE("1",		"General Commands Manual")
+LINE("2",		"System Calls Manual")
+LINE("3",		"Library Functions Manual")
+LINE("3p",		"Perl Library Manual")
+LINE("4",		"Device Drivers Manual")
+LINE("5",		"File Formats Manual")
+LINE("6",		"Games Manual")
+LINE("7",		"Miscellaneous Information Manual")
+LINE("8",		"System Manager\'s Manual")
+LINE("9",		"Kernel Developer\'s Manual")
diff --git a/usr.bin/mandoc/out.c b/usr.bin/mandoc/out.c
new file mode 100644
index 0000000..7cc5702
--- /dev/null
+++ b/usr.bin/mandoc/out.c
@@ -0,0 +1,563 @@
+/*	$OpenBSD: out.c,v 1.51 2019/12/31 22:49:17 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2011,2014,2015,2017,2018 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "mandoc_aux.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)
+{
+	struct roffsu		 su;
+	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;
+
+	/*
+	 * 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;
+	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.
+		 */
+
+		gp = &first_group;
+		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 (dp->layout->wstr != NULL &&
+			    dp->layout->width == 0 &&
+			    a2roffsu(dp->layout->wstr, &su, SCALE_EN)
+			    != NULL)
+				dp->layout->width =
+				    (*tbl->sulen)(&su, tbl->arg);
+			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);
+			if (dp->hspans == 0)
+				continue;
+
+			/*
+			 * Build an ordered, singly linked list
+			 * of all groups of columns joined by spans,
+			 * recording the minimum width for each group.
+			 */
+
+			while (*gp != NULL && ((*gp)->startcol < icol ||
+			    (*gp)->endcol < icol + dp->hspans))
+				gp = &(*gp)->next;
+			if (*gp == NULL || (*gp)->startcol > icol ||
+                            (*gp)->endcol > icol + dp->hspans) {
+				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;
+
+		/*
+		 * 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;
+			if (g->wanted <= width) {
+				done = 1;
+				break;
+			} else
+				(*gp)->wanted -= width;
+		}
+		if (done) {
+			*gp = g->next;
+			free(g);
+		} else
+			gp = &(*gp)->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++) {
+			if (min1 > colwidth[icol]) {
+				min2 = min1;
+				min1 = colwidth[icol];
+			} else if (min1 < colwidth[icol] &&
+			    min2 > colwidth[icol])
+				min2 = colwidth[icol];
+		}
+
+		/*
+		 * 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 (tbl->cols[icol].width == min1)
+					necol++;
+			if (necol == 0)
+				continue;
+			width = min1 + (g->wanted - 1) / necol + 1;
+			if (width > min2)
+				width = min2;
+			if (wanted > width)
+				wanted = width;
+			for (icol = g->startcol; icol <= g->endcol; icol++)
+				if (colwidth[icol] == min1 ||
+				    (colwidth[icol] < min2 &&
+				     colwidth[icol] > width))
+					colwidth[icol] = width;
+		}
+
+		/* Record the effect of the widening on the group list. */
+
+		gp = &first_group;
+		while ((g = *gp) != NULL) {
+			done = 0;
+			for (icol = g->startcol; icol <= g->endcol; icol++) {
+				if (colwidth[icol] != wanted ||
+				    tbl->cols[icol].width == wanted)
+					continue;
+				if (g->wanted <= wanted - min1) {
+					done = 1;
+					break;
+				}
+				g->wanted -= wanted - min1;
+			}
+			if (done) {
+				*gp = g->next;
+				free(g);
+			} else
+				gp = &(*gp)->next;
+		}
+
+		/* Record the effect of the widening on the columns. */
+
+		for (icol = 0; icol <= maxcol; icol++)
+			if (colwidth[icol] == wanted)
+				tbl->cols[icol].width = wanted;
+	}
+	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;
+		else
+			col->width = col->nwidth;
+		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 +
+		    (opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX) ?
+		     2 : !!opts->lvert + !!opts->rvert);
+		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;
+			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--;
+			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. */
+
+	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;
+		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;
+	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;
+	return totsz;
+}
diff --git a/usr.bin/mandoc/out.h b/usr.bin/mandoc/out.h
new file mode 100644
index 0000000..fcd691c
--- /dev/null
+++ b/usr.bin/mandoc/out.h
@@ -0,0 +1,70 @@
+/* $OpenBSD: out.h,v 1.25 2020/04/03 11:34:19 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2017, 2018 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 */
+};
+
+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 */
+};
+
+#define	SCALE_HS_INIT(p, v) \
+	do { (p)->unit = SCALE_EN; \
+	     (p)->scale = (v); } \
+	while (/* CONSTCOND */ 0)
+
+
+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/usr.bin/mandoc/preconv.c b/usr.bin/mandoc/preconv.c
new file mode 100644
index 0000000..2cbdcda
--- /dev/null
+++ b/usr.bin/mandoc/preconv.c
@@ -0,0 +1,177 @@
+/*	$OpenBSD: preconv.c,v 1.9 2018/12/13 11:55:14 schwarze Exp $ */
+/*
+ * Copyright (c) 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "mandoc_parse.h"
+#include "libmandoc.h"
+
+int
+preconv_encode(const struct buf *ib, size_t *ii, struct buf *ob, size_t *oi,
+    int *filenc)
+{
+	const unsigned char	*cu;
+	int			 nby;
+	unsigned int		 accum;
+
+	cu = (const unsigned char *)ib->buf + *ii;
+	assert(*cu & 0x80);
+
+	if ( ! (*filenc & MPARSE_UTF8))
+		goto latin;
+
+	nby = 1;
+	while (nby < 5 && *cu & (1 << (7 - nby)))
+		nby++;
+
+	switch (nby) {
+	case 2:
+		accum = *cu & 0x1f;
+		if (accum < 0x02)  /* Obfuscated ASCII. */
+			goto latin;
+		break;
+	case 3:
+		accum = *cu & 0x0f;
+		break;
+	case 4:
+		accum = *cu & 0x07;
+		if (accum > 0x04) /* Beyond Unicode. */
+			goto latin;
+		break;
+	default:  /* Bad sequence header. */
+		goto latin;
+	}
+
+	cu++;
+	switch (nby) {
+	case 3:
+		if ((accum == 0x00 && ! (*cu & 0x20)) ||  /* Use 2-byte. */
+		    (accum == 0x0d && *cu & 0x20))  /* Surrogates. */
+			goto latin;
+		break;
+	case 4:
+		if ((accum == 0x00 && ! (*cu & 0x30)) ||  /* Use 3-byte. */
+		    (accum == 0x04 && *cu & 0x30))  /* Beyond Unicode. */
+			goto latin;
+		break;
+	default:
+		break;
+	}
+
+	while (--nby) {
+		if ((*cu & 0xc0) != 0x80)  /* Invalid continuation. */
+			goto latin;
+		accum <<= 6;
+		accum += *cu & 0x3f;
+		cu++;
+	}
+
+	assert(accum > 0x7f);
+	assert(accum < 0x110000);
+	assert(accum < 0xd800 || accum > 0xdfff);
+
+	*oi += snprintf(ob->buf + *oi, 11, "\\[u%.4X]", accum);
+	*ii = (const char *)cu - ib->buf;
+	*filenc &= ~MPARSE_LATIN1;
+	return 1;
+
+latin:
+	if ( ! (*filenc & MPARSE_LATIN1))
+		return 0;
+
+	*oi += snprintf(ob->buf + *oi, 11,
+	    "\\[u%.4X]", (unsigned char)ib->buf[(*ii)++]);
+
+	*filenc &= ~MPARSE_UTF8;
+	return 1;
+}
+
+int
+preconv_cue(const struct buf *b, size_t offset)
+{
+	const char	*ln, *eoln, *eoph;
+	size_t		 sz, phsz;
+
+	ln = b->buf + offset;
+	sz = b->sz - offset;
+
+	/* Look for the end-of-line. */
+
+	if (NULL == (eoln = memchr(ln, '\n', sz)))
+		eoln = ln + sz;
+
+	/* Check if we have the correct header/trailer. */
+
+	if ((sz = (size_t)(eoln - ln)) < 10 ||
+	    memcmp(ln, ".\\\" -*-", 7) || memcmp(eoln - 3, "-*-", 3))
+		return MPARSE_UTF8 | MPARSE_LATIN1;
+
+	/* Move after the header and adjust for the trailer. */
+
+	ln += 7;
+	sz -= 10;
+
+	while (sz > 0) {
+		while (sz > 0 && ' ' == *ln) {
+			ln++;
+			sz--;
+		}
+		if (0 == sz)
+			break;
+
+		/* Find the end-of-phrase marker (or eoln). */
+
+		if (NULL == (eoph = memchr(ln, ';', sz)))
+			eoph = eoln - 3;
+		else
+			eoph++;
+
+		/* Only account for the "coding" phrase. */
+
+		if ((phsz = eoph - ln) < 7 ||
+		    strncasecmp(ln, "coding:", 7)) {
+			sz -= phsz;
+			ln += phsz;
+			continue;
+		}
+
+		sz -= 7;
+		ln += 7;
+
+		while (sz > 0 && ' ' == *ln) {
+			ln++;
+			sz--;
+		}
+		if (0 == sz)
+			return 0;
+
+		/* Check us against known encodings. */
+
+		if (phsz > 4 && !strncasecmp(ln, "utf-8", 5))
+			return MPARSE_UTF8;
+		if (phsz > 10 && !strncasecmp(ln, "iso-latin-1", 11))
+			return MPARSE_LATIN1;
+		return 0;
+	}
+	return MPARSE_UTF8 | MPARSE_LATIN1;
+}
diff --git a/usr.bin/mandoc/predefs.in b/usr.bin/mandoc/predefs.in
new file mode 100644
index 0000000..f022028
--- /dev/null
+++ b/usr.bin/mandoc/predefs.in
@@ -0,0 +1,65 @@
+/*	$OpenBSD: predefs.in,v 1.4 2014/11/28 19:25:03 schwarze Exp $ */
+/*
+ * Copyright (c) 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ */
+
+/*
+ * The predefined-string translation tables.  Each corresponds to a
+ * predefined strings from (e.g.) tmac/mdoc/doc-nroff.  The left-hand
+ * side corresponds to the input sequence (\*x, \*(xx and so on).  The
+ * right-hand side is what's produced by libroff.
+ *
+ * XXX - C-escape strings!
+ * XXX - update PREDEF_MAX in roff.c if adding more!
+ */
+
+PREDEF("Am", "&")
+PREDEF("Ba", "\\fR|\\fP")
+PREDEF("Ge", "\\(>=")
+PREDEF("Gt", ">")
+PREDEF("If", "infinity")
+PREDEF("Le", "\\(<=")
+PREDEF("Lq", "\\(lq")
+PREDEF("Lt", "<")
+PREDEF("Na", "NaN")
+PREDEF("Ne", "\\(!=")
+PREDEF("Pi", "pi")
+PREDEF("Pm", "\\(+-")
+PREDEF("Rq", "\\(rq")
+PREDEF("left-bracket", "[")
+PREDEF("left-parenthesis", "(")
+PREDEF("lp", "(")
+PREDEF("left-singlequote", "\\(oq")
+PREDEF("q", "\\(dq")
+PREDEF("quote-left", "\\(oq")
+PREDEF("quote-right", "\\(cq")
+PREDEF("R", "\\(rg")
+PREDEF("right-bracket", "]")
+PREDEF("right-parenthesis", ")")
+PREDEF("rp", ")")
+PREDEF("right-singlequote", "\\(cq")
+PREDEF("Tm", "(Tm)")
+PREDEF("Px", "POSIX")
+PREDEF("Ai", "ANSI")
+PREDEF("\'", "\\\'")
+PREDEF("aa", "\\(aa")
+PREDEF("ga", "\\(ga")
+PREDEF("`",  "\\`")
+PREDEF("lq", "\\(lq")
+PREDEF("rq", "\\(rq")
+PREDEF("ua", "\\(ua")
+PREDEF("va", "\\(va")
+PREDEF("<=", "\\(<=")
+PREDEF(">=", "\\(>=")
diff --git a/usr.bin/mandoc/read.c b/usr.bin/mandoc/read.c
new file mode 100644
index 0000000..a6a25de
--- /dev/null
+++ b/usr.bin/mandoc/read.c
@@ -0,0 +1,727 @@
+/* $OpenBSD: read.c,v 1.190 2020/04/24 11:58:02 schwarze Exp $ */
+/*
+ * Copyright (c) 2010-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2010, 2012 Joerg Sonnenberger <joerg@netbsd.org>
+ *
+ * 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.
+ *
+ * Top-level functions of the mandoc(3) parser:
+ * Parser and input encoding selection, decompression,
+ * handling of input bytes, characters, lines, and files,
+ * handling of roff(7) loops and file inclusion,
+ * and steering of the various parsers.
+ */
+#include <sys/types.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <zlib.h>
+
+#include "mandoc_aux.h"
+#include "mandoc.h"
+#include "roff.h"
+#include "mdoc.h"
+#include "man.h"
+#include "mandoc_parse.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+#include "tag.h"
+
+#define	REPARSE_LIMIT	1000
+
+struct	mparse {
+	struct roff	 *roff; /* roff parser (!NULL) */
+	struct roff_man	 *man; /* man parser */
+	struct buf	 *primary; /* buffer currently being parsed */
+	struct buf	 *secondary; /* copy of top level input */
+	struct buf	 *loop; /* open .while request line */
+	const char	 *os_s; /* default operating system */
+	int		  options; /* parser options */
+	int		  gzip; /* current input file is gzipped */
+	int		  filenc; /* encoding of the current file */
+	int		  reparse_count; /* finite interp. stack */
+	int		  line; /* line number in the file */
+};
+
+static	void	  choose_parser(struct mparse *);
+static	void	  free_buf_list(struct buf *);
+static	void	  resize_buf(struct buf *, size_t);
+static	int	  mparse_buf_r(struct mparse *, struct buf, size_t, int);
+static	int	  read_whole_file(struct mparse *, int, struct buf *, int *);
+static	void	  mparse_end(struct mparse *);
+
+
+static void
+resize_buf(struct buf *buf, size_t initial)
+{
+
+	buf->sz = buf->sz > initial/2 ? 2 * buf->sz : initial;
+	buf->buf = mandoc_realloc(buf->buf, buf->sz);
+}
+
+static void
+free_buf_list(struct buf *buf)
+{
+	struct buf *tmp;
+
+	while (buf != NULL) {
+		tmp = buf;
+		buf = tmp->next;
+		free(tmp->buf);
+		free(tmp);
+	}
+}
+
+static void
+choose_parser(struct mparse *curp)
+{
+	char		*cp, *ep;
+	int		 format;
+
+	/*
+	 * If neither command line arguments -mdoc or -man select
+	 * a parser nor the roff parser found a .Dd or .TH macro
+	 * yet, look ahead in the main input buffer.
+	 */
+
+	if ((format = roff_getformat(curp->roff)) == 0) {
+		cp = curp->primary->buf;
+		ep = cp + curp->primary->sz;
+		while (cp < ep) {
+			if (*cp == '.' || *cp == '\'') {
+				cp++;
+				if (cp[0] == 'D' && cp[1] == 'd') {
+					format = MPARSE_MDOC;
+					break;
+				}
+				if (cp[0] == 'T' && cp[1] == 'H') {
+					format = MPARSE_MAN;
+					break;
+				}
+			}
+			cp = memchr(cp, '\n', ep - cp);
+			if (cp == NULL)
+				break;
+			cp++;
+		}
+	}
+
+	if (format == MPARSE_MDOC) {
+		curp->man->meta.macroset = MACROSET_MDOC;
+		if (curp->man->mdocmac == NULL)
+			curp->man->mdocmac = roffhash_alloc(MDOC_Dd, MDOC_MAX);
+	} else {
+		curp->man->meta.macroset = MACROSET_MAN;
+		if (curp->man->manmac == NULL)
+			curp->man->manmac = roffhash_alloc(MAN_TH, MAN_MAX);
+	}
+	curp->man->meta.first->tok = TOKEN_NONE;
+}
+
+/*
+ * Main parse routine for a buffer.
+ * It assumes encoding and line numbering are already set up.
+ * It can recurse directly (for invocations of user-defined
+ * macros, inline equations, and input line traps)
+ * and indirectly (for .so file inclusion).
+ */
+static int
+mparse_buf_r(struct mparse *curp, struct buf blk, size_t i, int start)
+{
+	struct buf	 ln;
+	struct buf	*firstln, *lastln, *thisln, *loop;
+	char		*cp;
+	size_t		 pos; /* byte number in the ln buffer */
+	int		 line_result, result;
+	int		 of;
+	int		 lnn; /* line number in the real file */
+	int		 fd;
+	int		 inloop; /* Saw .while on this level. */
+	unsigned char	 c;
+
+	ln.sz = 256;
+	ln.buf = mandoc_malloc(ln.sz);
+	ln.next = NULL;
+	firstln = lastln = loop = NULL;
+	lnn = curp->line;
+	pos = 0;
+	inloop = 0;
+	result = ROFF_CONT;
+
+	while (i < blk.sz && (blk.buf[i] != '\0' || pos != 0)) {
+		if (start) {
+			curp->line = lnn;
+			curp->reparse_count = 0;
+
+			if (lnn < 3 &&
+			    curp->filenc & MPARSE_UTF8 &&
+			    curp->filenc & MPARSE_LATIN1)
+				curp->filenc = preconv_cue(&blk, i);
+		}
+
+		while (i < blk.sz && (start || blk.buf[i] != '\0')) {
+
+			/*
+			 * When finding an unescaped newline character,
+			 * leave the character loop to process the line.
+			 * Skip a preceding carriage return, if any.
+			 */
+
+			if ('\r' == blk.buf[i] && i + 1 < blk.sz &&
+			    '\n' == blk.buf[i + 1])
+				++i;
+			if ('\n' == blk.buf[i]) {
+				++i;
+				++lnn;
+				break;
+			}
+
+			/*
+			 * Make sure we have space for the worst
+			 * case of 12 bytes: "\\[u10ffff]\n\0"
+			 */
+
+			if (pos + 12 > ln.sz)
+				resize_buf(&ln, 256);
+
+			/*
+			 * Encode 8-bit input.
+			 */
+
+			c = blk.buf[i];
+			if (c & 0x80) {
+				if ( ! (curp->filenc && preconv_encode(
+				    &blk, &i, &ln, &pos, &curp->filenc))) {
+					mandoc_msg(MANDOCERR_CHAR_BAD,
+					    curp->line, pos, "0x%x", c);
+					ln.buf[pos++] = '?';
+					i++;
+				}
+				continue;
+			}
+
+			/*
+			 * Exclude control characters.
+			 */
+
+			if (c == 0x7f || (c < 0x20 && c != 0x09)) {
+				mandoc_msg(c == 0x00 || c == 0x04 ||
+				    c > 0x0a ? MANDOCERR_CHAR_BAD :
+				    MANDOCERR_CHAR_UNSUPP,
+				    curp->line, pos, "0x%x", c);
+				i++;
+				if (c != '\r')
+					ln.buf[pos++] = '?';
+				continue;
+			}
+
+			ln.buf[pos++] = blk.buf[i++];
+		}
+		ln.buf[pos] = '\0';
+
+		/*
+		 * Maintain a lookaside buffer of all lines.
+		 * parsed from this input source.
+		 */
+
+		thisln = mandoc_malloc(sizeof(*thisln));
+		thisln->buf = mandoc_strdup(ln.buf);
+		thisln->sz = strlen(ln.buf) + 1;
+		thisln->next = NULL;
+		if (firstln == NULL) {
+			firstln = lastln = thisln;
+			if (curp->secondary == NULL)
+				curp->secondary = firstln;
+		} else {
+			lastln->next = thisln;
+			lastln = thisln;
+		}
+
+		/* XXX Ugly hack to mark the end of the input. */
+
+		if (i == blk.sz || blk.buf[i] == '\0') {
+			if (pos + 2 > ln.sz)
+				resize_buf(&ln, 256);
+			ln.buf[pos++] = '\n';
+			ln.buf[pos] = '\0';
+		}
+
+		/*
+		 * A significant amount of complexity is contained by
+		 * the roff preprocessor.  It's line-oriented but can be
+		 * expressed on one line, so we need at times to
+		 * readjust our starting point and re-run it.  The roff
+		 * preprocessor can also readjust the buffers with new
+		 * data, so we pass them in wholesale.
+		 */
+
+		of = 0;
+rerun:
+		line_result = roff_parseln(curp->roff, curp->line, &ln, &of);
+
+		/* Process options. */
+
+		if (line_result & ROFF_APPEND)
+			assert(line_result == (ROFF_IGN | ROFF_APPEND));
+
+		if (line_result & ROFF_USERCALL)
+			assert((line_result & ROFF_MASK) == ROFF_REPARSE);
+
+		if (line_result & ROFF_USERRET) {
+			assert(line_result == (ROFF_IGN | ROFF_USERRET));
+			if (start == 0) {
+				/* Return from the current macro. */
+				result = ROFF_USERRET;
+				goto out;
+			}
+		}
+
+		switch (line_result & ROFF_LOOPMASK) {
+		case ROFF_IGN:
+			break;
+		case ROFF_WHILE:
+			if (curp->loop != NULL) {
+				if (loop == curp->loop)
+					break;
+				mandoc_msg(MANDOCERR_WHILE_NEST,
+				    curp->line, pos, NULL);
+			}
+			curp->loop = thisln;
+			loop = NULL;
+			inloop = 1;
+			break;
+		case ROFF_LOOPCONT:
+		case ROFF_LOOPEXIT:
+			if (curp->loop == NULL) {
+				mandoc_msg(MANDOCERR_WHILE_FAIL,
+				    curp->line, pos, NULL);
+				break;
+			}
+			if (inloop == 0) {
+				mandoc_msg(MANDOCERR_WHILE_INTO,
+				    curp->line, pos, NULL);
+				curp->loop = loop = NULL;
+				break;
+			}
+			if (line_result & ROFF_LOOPCONT)
+				loop = curp->loop;
+			else {
+				curp->loop = loop = NULL;
+				inloop = 0;
+			}
+			break;
+		default:
+			abort();
+		}
+
+		/* Process the main instruction from the roff parser. */
+
+		switch (line_result & ROFF_MASK) {
+		case ROFF_IGN:
+			break;
+		case ROFF_CONT:
+			if (curp->man->meta.macroset == MACROSET_NONE)
+				choose_parser(curp);
+			if ((curp->man->meta.macroset == MACROSET_MDOC ?
+			     mdoc_parseln(curp->man, curp->line, ln.buf, of) :
+			     man_parseln(curp->man, curp->line, ln.buf, of)
+			    ) == 2)
+				goto out;
+			break;
+		case ROFF_RERUN:
+			goto rerun;
+		case ROFF_REPARSE:
+			if (++curp->reparse_count > REPARSE_LIMIT) {
+				/* Abort and return to the top level. */
+				result = ROFF_IGN;
+				mandoc_msg(MANDOCERR_ROFFLOOP,
+				    curp->line, pos, NULL);
+				goto out;
+			}
+			result = mparse_buf_r(curp, ln, of, 0);
+			if (line_result & ROFF_USERCALL) {
+				roff_userret(curp->roff);
+				/* Continue normally. */
+				if (result & ROFF_USERRET)
+					result = ROFF_CONT;
+			}
+			if (start == 0 && result != ROFF_CONT)
+				goto out;
+			break;
+		case ROFF_SO:
+			if ( ! (curp->options & MPARSE_SO) &&
+			    (i >= blk.sz || blk.buf[i] == '\0')) {
+				curp->man->meta.sodest =
+				    mandoc_strdup(ln.buf + of);
+				goto out;
+			}
+			if ((fd = mparse_open(curp, ln.buf + of)) != -1) {
+				mparse_readfd(curp, fd, ln.buf + of);
+				close(fd);
+			} else {
+				mandoc_msg(MANDOCERR_SO_FAIL,
+				    curp->line, of, ".so %s: %s",
+				    ln.buf + of, strerror(errno));
+				ln.sz = mandoc_asprintf(&cp,
+				    ".sp\nSee the file %s.\n.sp",
+				    ln.buf + of);
+				free(ln.buf);
+				ln.buf = cp;
+				of = 0;
+				mparse_buf_r(curp, ln, of, 0);
+			}
+			break;
+		default:
+			abort();
+		}
+
+		/* Start the next input line. */
+
+		if (loop != NULL &&
+		    (line_result & ROFF_LOOPMASK) == ROFF_IGN)
+			loop = loop->next;
+
+		if (loop != NULL) {
+			if ((line_result & ROFF_APPEND) == 0)
+				*ln.buf = '\0';
+			if (ln.sz < loop->sz)
+				resize_buf(&ln, loop->sz);
+			(void)strlcat(ln.buf, loop->buf, ln.sz);
+			of = 0;
+			goto rerun;
+		}
+
+		pos = (line_result & ROFF_APPEND) ? strlen(ln.buf) : 0;
+	}
+out:
+	if (inloop) {
+		if (result != ROFF_USERRET)
+			mandoc_msg(MANDOCERR_WHILE_OUTOF,
+			    curp->line, pos, NULL);
+		curp->loop = NULL;
+	}
+	free(ln.buf);
+	if (firstln != curp->secondary)
+		free_buf_list(firstln);
+	return result;
+}
+
+static int
+read_whole_file(struct mparse *curp, int fd, struct buf *fb, int *with_mmap)
+{
+	struct stat	 st;
+	gzFile		 gz;
+	size_t		 off;
+	ssize_t		 ssz;
+	int		 gzerrnum, retval;
+
+	if (fstat(fd, &st) == -1) {
+		mandoc_msg(MANDOCERR_FSTAT, 0, 0, "%s", strerror(errno));
+		return -1;
+	}
+
+	/*
+	 * If we're a regular file, try just reading in the whole entry
+	 * via mmap().  This is faster than reading it into blocks, and
+	 * since each file is only a few bytes to begin with, I'm not
+	 * concerned that this is going to tank any machines.
+	 */
+
+	if (curp->gzip == 0 && S_ISREG(st.st_mode)) {
+		if (st.st_size > 0x7fffffff) {
+			mandoc_msg(MANDOCERR_TOOLARGE, 0, 0, NULL);
+			return -1;
+		}
+		*with_mmap = 1;
+		fb->sz = (size_t)st.st_size;
+		fb->buf = mmap(NULL, fb->sz, PROT_READ, MAP_SHARED, fd, 0);
+		if (fb->buf != MAP_FAILED)
+			return 0;
+	}
+
+	if (curp->gzip) {
+		/*
+		 * Duplicating the file descriptor is required
+		 * because we will have to call gzclose(3)
+		 * to free memory used internally by zlib,
+		 * but that will also close the file descriptor,
+		 * which this function must not do.
+		 */
+		if ((fd = dup(fd)) == -1) {
+			mandoc_msg(MANDOCERR_DUP, 0, 0,
+			    "%s", strerror(errno));
+			return -1;
+		}
+		if ((gz = gzdopen(fd, "rb")) == NULL) {
+			mandoc_msg(MANDOCERR_GZDOPEN, 0, 0,
+			    "%s", strerror(errno));
+			close(fd);
+			return -1;
+		}
+	} else
+		gz = NULL;
+
+	/*
+	 * If this isn't a regular file (like, say, stdin), then we must
+	 * go the old way and just read things in bit by bit.
+	 */
+
+	*with_mmap = 0;
+	off = 0;
+	retval = -1;
+	fb->sz = 0;
+	fb->buf = NULL;
+	for (;;) {
+		if (off == fb->sz) {
+			if (fb->sz == (1U << 31)) {
+				mandoc_msg(MANDOCERR_TOOLARGE, 0, 0, NULL);
+				break;
+			}
+			resize_buf(fb, 65536);
+		}
+		ssz = curp->gzip ?
+		    gzread(gz, fb->buf + (int)off, fb->sz - off) :
+		    read(fd, fb->buf + (int)off, fb->sz - off);
+		if (ssz == 0) {
+			fb->sz = off;
+			retval = 0;
+			break;
+		}
+		if (ssz == -1) {
+			if (curp->gzip)
+				(void)gzerror(gz, &gzerrnum);
+			mandoc_msg(MANDOCERR_READ, 0, 0, "%s",
+			    curp->gzip && gzerrnum != Z_ERRNO ?
+			    zError(gzerrnum) : strerror(errno));
+			break;
+		}
+		off += (size_t)ssz;
+	}
+
+	if (curp->gzip && (gzerrnum = gzclose(gz)) != Z_OK)
+		mandoc_msg(MANDOCERR_GZCLOSE, 0, 0, "%s",
+		    gzerrnum == Z_ERRNO ? strerror(errno) :
+		    zError(gzerrnum));
+	if (retval == -1) {
+		free(fb->buf);
+		fb->buf = NULL;
+	}
+	return retval;
+}
+
+static void
+mparse_end(struct mparse *curp)
+{
+	if (curp->man->meta.macroset == MACROSET_NONE)
+		curp->man->meta.macroset = MACROSET_MAN;
+	if (curp->man->meta.macroset == MACROSET_MDOC)
+		mdoc_endparse(curp->man);
+	else
+		man_endparse(curp->man);
+	roff_endparse(curp->roff);
+}
+
+/*
+ * Read the whole file into memory and call the parsers.
+ * Called recursively when an .so request is encountered.
+ */
+void
+mparse_readfd(struct mparse *curp, int fd, const char *filename)
+{
+	static int	 recursion_depth;
+
+	struct buf	 blk;
+	struct buf	*save_primary;
+	const char	*save_filename, *cp;
+	size_t		 offset;
+	int		 save_filenc, save_lineno;
+	int		 with_mmap;
+
+	if (recursion_depth > 64) {
+		mandoc_msg(MANDOCERR_ROFFLOOP, curp->line, 0, NULL);
+		return;
+	} else if (recursion_depth == 0 &&
+	    (cp = strrchr(filename, '.')) != NULL &&
+            cp[1] >= '1' && cp[1] <= '9')
+                curp->man->filesec = cp[1];
+        else
+                curp->man->filesec = '\0';
+
+	if (read_whole_file(curp, fd, &blk, &with_mmap) == -1)
+		return;
+
+	/*
+	 * Save some properties of the parent file.
+	 */
+
+	save_primary = curp->primary;
+	save_filenc = curp->filenc;
+	save_lineno = curp->line;
+	save_filename = mandoc_msg_getinfilename();
+
+	curp->primary = &blk;
+	curp->filenc = curp->options & (MPARSE_UTF8 | MPARSE_LATIN1);
+	curp->line = 1;
+	mandoc_msg_setinfilename(filename);
+
+	/* Skip an UTF-8 byte order mark. */
+	if (curp->filenc & MPARSE_UTF8 && blk.sz > 2 &&
+	    (unsigned char)blk.buf[0] == 0xef &&
+	    (unsigned char)blk.buf[1] == 0xbb &&
+	    (unsigned char)blk.buf[2] == 0xbf) {
+		offset = 3;
+		curp->filenc &= ~MPARSE_LATIN1;
+	} else
+		offset = 0;
+
+	recursion_depth++;
+	mparse_buf_r(curp, blk, offset, 1);
+	if (--recursion_depth == 0)
+		mparse_end(curp);
+
+	/*
+	 * Clean up and restore saved parent properties.
+	 */
+
+	if (with_mmap)
+		munmap(blk.buf, blk.sz);
+	else
+		free(blk.buf);
+
+	curp->primary = save_primary;
+	curp->filenc = save_filenc;
+	curp->line = save_lineno;
+	if (save_filename != NULL)
+		mandoc_msg_setinfilename(save_filename);
+}
+
+int
+mparse_open(struct mparse *curp, const char *file)
+{
+	char		 *cp;
+	int		  fd, save_errno;
+
+	cp = strrchr(file, '.');
+	curp->gzip = (cp != NULL && ! strcmp(cp + 1, "gz"));
+
+	/* First try to use the filename as it is. */
+
+	if ((fd = open(file, O_RDONLY)) != -1)
+		return fd;
+
+	/*
+	 * If that doesn't work and the filename doesn't
+	 * already  end in .gz, try appending .gz.
+	 */
+
+	if ( ! curp->gzip) {
+		save_errno = errno;
+		mandoc_asprintf(&cp, "%s.gz", file);
+		fd = open(cp, O_RDONLY);
+		free(cp);
+		errno = save_errno;
+		if (fd != -1) {
+			curp->gzip = 1;
+			return fd;
+		}
+	}
+
+	/* Neither worked, give up. */
+
+	return -1;
+}
+
+struct mparse *
+mparse_alloc(int options, enum mandoc_os os_e, const char *os_s)
+{
+	struct mparse	*curp;
+
+	curp = mandoc_calloc(1, sizeof(struct mparse));
+
+	curp->options = options;
+	curp->os_s = os_s;
+
+	curp->roff = roff_alloc(options);
+	curp->man = roff_man_alloc(curp->roff, curp->os_s,
+		curp->options & MPARSE_QUICK ? 1 : 0);
+	if (curp->options & MPARSE_MDOC) {
+		curp->man->meta.macroset = MACROSET_MDOC;
+		if (curp->man->mdocmac == NULL)
+			curp->man->mdocmac = roffhash_alloc(MDOC_Dd, MDOC_MAX);
+	} else if (curp->options & MPARSE_MAN) {
+		curp->man->meta.macroset = MACROSET_MAN;
+		if (curp->man->manmac == NULL)
+			curp->man->manmac = roffhash_alloc(MAN_TH, MAN_MAX);
+	}
+	curp->man->meta.first->tok = TOKEN_NONE;
+	curp->man->meta.os_e = os_e;
+	tag_alloc();
+	return curp;
+}
+
+void
+mparse_reset(struct mparse *curp)
+{
+	tag_free();
+	roff_reset(curp->roff);
+	roff_man_reset(curp->man);
+	free_buf_list(curp->secondary);
+	curp->secondary = NULL;
+	curp->gzip = 0;
+	tag_alloc();
+}
+
+void
+mparse_free(struct mparse *curp)
+{
+	tag_free();
+	roffhash_free(curp->man->mdocmac);
+	roffhash_free(curp->man->manmac);
+	roff_man_free(curp->man);
+	roff_free(curp->roff);
+	free_buf_list(curp->secondary);
+	free(curp);
+}
+
+struct roff_meta *
+mparse_result(struct mparse *curp)
+{
+	roff_state_reset(curp->man);
+	if (curp->options & MPARSE_VALIDATE) {
+		if (curp->man->meta.macroset == MACROSET_MDOC)
+			mdoc_validate(curp->man);
+		else
+			man_validate(curp->man);
+		tag_postprocess(curp->man, curp->man->meta.first);
+	}
+	return &curp->man->meta;
+}
+
+void
+mparse_copy(const struct mparse *p)
+{
+	struct buf	*buf;
+
+	for (buf = p->secondary; buf != NULL; buf = buf->next)
+		puts(buf->buf);
+}
diff --git a/usr.bin/mandoc/roff.c b/usr.bin/mandoc/roff.c
new file mode 100644
index 0000000..870305e
--- /dev/null
+++ b/usr.bin/mandoc/roff.c
@@ -0,0 +1,4372 @@
+/* $OpenBSD: roff.c,v 1.246 2020/04/08 11:54:14 schwarze Exp $ */
+/*
+ * Copyright (c) 2010-2015, 2017-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008-2012, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ *
+ * Implementation of the roff(7) parser for mandoc(1).
+ */
+#include <sys/cdefs.h>
+#include <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <limits.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mandoc_aux.h"
+#include "mandoc_ohash.h"
+#include "mandoc.h"
+#include "roff.h"
+#include "mandoc_parse.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+#include "tbl_parse.h"
+#include "eqn_parse.h"
+
+/*
+ * ASCII_ESC is used to signal from roff_getarg() to roff_expand()
+ * that an escape sequence resulted from copy-in processing and
+ * needs to be checked or interpolated.  As it is used nowhere
+ * else, it is defined here rather than in a header file.
+ */
+#define	ASCII_ESC	27
+
+/* Maximum number of string expansions per line, to break infinite loops. */
+#define	EXPAND_LIMIT	1000
+
+/* Types of definitions of macros and strings. */
+#define	ROFFDEF_USER	(1 << 1)  /* User-defined. */
+#define	ROFFDEF_PRE	(1 << 2)  /* Predefined. */
+#define	ROFFDEF_REN	(1 << 3)  /* Renamed standard macro. */
+#define	ROFFDEF_STD	(1 << 4)  /* mdoc(7) or man(7) macro. */
+#define	ROFFDEF_ANY	(ROFFDEF_USER | ROFFDEF_PRE | \
+			 ROFFDEF_REN | ROFFDEF_STD)
+#define	ROFFDEF_UNDEF	(1 << 5)  /* Completely undefined. */
+
+/* --- data types --------------------------------------------------------- */
+
+/*
+ * An incredibly-simple string buffer.
+ */
+struct	roffstr {
+	char		*p; /* nil-terminated buffer */
+	size_t		 sz; /* saved strlen(p) */
+};
+
+/*
+ * A key-value roffstr pair as part of a singly-linked list.
+ */
+struct	roffkv {
+	struct roffstr	 key;
+	struct roffstr	 val;
+	struct roffkv	*next; /* next in list */
+};
+
+/*
+ * A single number register as part of a singly-linked list.
+ */
+struct	roffreg {
+	struct roffstr	 key;
+	int		 val;
+	int		 step;
+	struct roffreg	*next;
+};
+
+/*
+ * Association of request and macro names with token IDs.
+ */
+struct	roffreq {
+	enum roff_tok	 tok;
+	char		 name[];
+};
+
+/*
+ * A macro processing context.
+ * More than one is needed when macro calls are nested.
+ */
+struct	mctx {
+	char		**argv;
+	int		 argc;
+	int		 argsz;
+};
+
+struct	roff {
+	struct roff_man	*man; /* mdoc or man parser */
+	struct roffnode	*last; /* leaf of stack */
+	struct mctx	*mstack; /* stack of macro contexts */
+	int		*rstack; /* stack of inverted `ie' values */
+	struct ohash	*reqtab; /* request lookup table */
+	struct roffreg	*regtab; /* number registers */
+	struct roffkv	*strtab; /* user-defined strings & macros */
+	struct roffkv	*rentab; /* renamed strings & macros */
+	struct roffkv	*xmbtab; /* multi-byte trans table (`tr') */
+	struct roffstr	*xtab; /* single-byte trans table (`tr') */
+	const char	*current_string; /* value of last called user macro */
+	struct tbl_node	*first_tbl; /* first table parsed */
+	struct tbl_node	*last_tbl; /* last table parsed */
+	struct tbl_node	*tbl; /* current table being parsed */
+	struct eqn_node	*last_eqn; /* equation parser */
+	struct eqn_node	*eqn; /* active equation parser */
+	int		 eqn_inline; /* current equation is inline */
+	int		 options; /* parse options */
+	int		 mstacksz; /* current size of mstack */
+	int		 mstackpos; /* position in mstack */
+	int		 rstacksz; /* current size limit of rstack */
+	int		 rstackpos; /* position in rstack */
+	int		 format; /* current file in mdoc or man format */
+	char		 control; /* control character */
+	char		 escape; /* escape character */
+};
+
+/*
+ * A macro definition, condition, or ignored block.
+ */
+struct	roffnode {
+	enum roff_tok	 tok; /* type of node */
+	struct roffnode	*parent; /* up one in stack */
+	int		 line; /* parse line */
+	int		 col; /* parse col */
+	char		*name; /* node name, e.g. macro name */
+	char		*end; /* custom end macro of the block */
+	int		 endspan; /* scope to: 1=eol 2=next line -1=\} */
+	int		 rule; /* content is: 1=evaluated 0=skipped */
+};
+
+#define	ROFF_ARGS	 struct roff *r, /* parse ctx */ \
+			 enum roff_tok tok, /* tok of macro */ \
+			 struct buf *buf, /* input buffer */ \
+			 int ln, /* parse line */ \
+			 int ppos, /* original pos in buffer */ \
+			 int pos, /* current pos in buffer */ \
+			 int *offs /* reset offset of buffer data */
+
+typedef	int (*roffproc)(ROFF_ARGS);
+
+struct	roffmac {
+	roffproc	 proc; /* process new macro */
+	roffproc	 text; /* process as child text of macro */
+	roffproc	 sub; /* process as child of macro */
+	int		 flags;
+#define	ROFFMAC_STRUCT	(1 << 0) /* always interpret */
+};
+
+struct	predef {
+	const char	*name; /* predefined input name */
+	const char	*str; /* replacement symbol */
+};
+
+#define	PREDEF(__name, __str) \
+	{ (__name), (__str) },
+
+/* --- function prototypes ------------------------------------------------ */
+
+static	int		 roffnode_cleanscope(struct roff *);
+static	int		 roffnode_pop(struct roff *);
+static	void		 roffnode_push(struct roff *, enum roff_tok,
+				const char *, int, int);
+static	void		 roff_addtbl(struct roff_man *, int, struct tbl_node *);
+static	int		 roff_als(ROFF_ARGS);
+static	int		 roff_block(ROFF_ARGS);
+static	int		 roff_block_text(ROFF_ARGS);
+static	int		 roff_block_sub(ROFF_ARGS);
+static	int		 roff_break(ROFF_ARGS);
+static	int		 roff_cblock(ROFF_ARGS);
+static	int		 roff_cc(ROFF_ARGS);
+static	int		 roff_ccond(struct roff *, int, int);
+static	int		 roff_char(ROFF_ARGS);
+static	int		 roff_cond(ROFF_ARGS);
+static	int		 roff_cond_text(ROFF_ARGS);
+static	int		 roff_cond_sub(ROFF_ARGS);
+static	int		 roff_ds(ROFF_ARGS);
+static	int		 roff_ec(ROFF_ARGS);
+static	int		 roff_eo(ROFF_ARGS);
+static	int		 roff_eqndelim(struct roff *, struct buf *, int);
+static	int		 roff_evalcond(struct roff *, int, char *, int *);
+static	int		 roff_evalnum(struct roff *, int,
+				const char *, int *, int *, int);
+static	int		 roff_evalpar(struct roff *, int,
+				const char *, int *, int *, int);
+static	int		 roff_evalstrcond(const char *, int *);
+static	int		 roff_expand(struct roff *, struct buf *,
+				int, int, char);
+static	void		 roff_free1(struct roff *);
+static	void		 roff_freereg(struct roffreg *);
+static	void		 roff_freestr(struct roffkv *);
+static	size_t		 roff_getname(struct roff *, char **, int, int);
+static	int		 roff_getnum(const char *, int *, int *, int);
+static	int		 roff_getop(const char *, int *, char *);
+static	int		 roff_getregn(struct roff *,
+				const char *, size_t, char);
+static	int		 roff_getregro(const struct roff *,
+				const char *name);
+static	const char	*roff_getstrn(struct roff *,
+				const char *, size_t, int *);
+static	int		 roff_hasregn(const struct roff *,
+				const char *, size_t);
+static	int		 roff_insec(ROFF_ARGS);
+static	int		 roff_it(ROFF_ARGS);
+static	int		 roff_line_ignore(ROFF_ARGS);
+static	void		 roff_man_alloc1(struct roff_man *);
+static	void		 roff_man_free1(struct roff_man *);
+static	int		 roff_manyarg(ROFF_ARGS);
+static	int		 roff_noarg(ROFF_ARGS);
+static	int		 roff_nop(ROFF_ARGS);
+static	int		 roff_nr(ROFF_ARGS);
+static	int		 roff_onearg(ROFF_ARGS);
+static	enum roff_tok	 roff_parse(struct roff *, char *, int *,
+				int, int);
+static	int		 roff_parsetext(struct roff *, struct buf *,
+				int, int *);
+static	int		 roff_renamed(ROFF_ARGS);
+static	int		 roff_return(ROFF_ARGS);
+static	int		 roff_rm(ROFF_ARGS);
+static	int		 roff_rn(ROFF_ARGS);
+static	int		 roff_rr(ROFF_ARGS);
+static	void		 roff_setregn(struct roff *, const char *,
+				size_t, int, char, int);
+static	void		 roff_setstr(struct roff *,
+				const char *, const char *, int);
+static	void		 roff_setstrn(struct roffkv **, const char *,
+				size_t, const char *, size_t, int);
+static	int		 roff_shift(ROFF_ARGS);
+static	int		 roff_so(ROFF_ARGS);
+static	int		 roff_tr(ROFF_ARGS);
+static	int		 roff_Dd(ROFF_ARGS);
+static	int		 roff_TE(ROFF_ARGS);
+static	int		 roff_TS(ROFF_ARGS);
+static	int		 roff_EQ(ROFF_ARGS);
+static	int		 roff_EN(ROFF_ARGS);
+static	int		 roff_T_(ROFF_ARGS);
+static	int		 roff_unsupp(ROFF_ARGS);
+static	int		 roff_userdef(ROFF_ARGS);
+
+/* --- constant data ------------------------------------------------------ */
+
+#define	ROFFNUM_SCALE	(1 << 0)  /* Honour scaling in roff_getnum(). */
+#define	ROFFNUM_WHITE	(1 << 1)  /* Skip whitespace in roff_evalnum(). */
+
+const char *__roff_name[MAN_MAX + 1] = {
+	"br",		"ce",		"fi",		"ft",
+	"ll",		"mc",		"nf",
+	"po",		"rj",		"sp",
+	"ta",		"ti",		NULL,
+	"ab",		"ad",		"af",		"aln",
+	"als",		"am",		"am1",		"ami",
+	"ami1",		"as",		"as1",		"asciify",
+	"backtrace",	"bd",		"bleedat",	"blm",
+        "box",		"boxa",		"bp",		"BP",
+	"break",	"breakchar",	"brnl",		"brp",
+	"brpnl",	"c2",		"cc",
+	"cf",		"cflags",	"ch",		"char",
+	"chop",		"class",	"close",	"CL",
+	"color",	"composite",	"continue",	"cp",
+	"cropat",	"cs",		"cu",		"da",
+	"dch",		"Dd",		"de",		"de1",
+	"defcolor",	"dei",		"dei1",		"device",
+	"devicem",	"di",		"do",		"ds",
+	"ds1",		"dwh",		"dt",		"ec",
+	"ecr",		"ecs",		"el",		"em",
+	"EN",		"eo",		"EP",		"EQ",
+	"errprint",	"ev",		"evc",		"ex",
+	"fallback",	"fam",		"fc",		"fchar",
+	"fcolor",	"fdeferlig",	"feature",	"fkern",
+	"fl",		"flig",		"fp",		"fps",
+	"fschar",	"fspacewidth",	"fspecial",	"ftr",
+	"fzoom",	"gcolor",	"hc",		"hcode",
+	"hidechar",	"hla",		"hlm",		"hpf",
+	"hpfa",		"hpfcode",	"hw",		"hy",
+	"hylang",	"hylen",	"hym",		"hypp",
+	"hys",		"ie",		"if",		"ig",
+	"index",	"it",		"itc",		"IX",
+	"kern",		"kernafter",	"kernbefore",	"kernpair",
+	"lc",		"lc_ctype",	"lds",		"length",
+	"letadj",	"lf",		"lg",		"lhang",
+	"linetabs",	"lnr",		"lnrf",		"lpfx",
+	"ls",		"lsm",		"lt",
+	"mediasize",	"minss",	"mk",		"mso",
+	"na",		"ne",		"nh",		"nhychar",
+	"nm",		"nn",		"nop",		"nr",
+	"nrf",		"nroff",	"ns",		"nx",
+	"open",		"opena",	"os",		"output",
+	"padj",		"papersize",	"pc",		"pev",
+	"pi",		"PI",		"pl",		"pm",
+	"pn",		"pnr",		"ps",
+	"psbb",		"pshape",	"pso",		"ptr",
+	"pvs",		"rchar",	"rd",		"recursionlimit",
+	"return",	"rfschar",	"rhang",
+	"rm",		"rn",		"rnn",		"rr",
+	"rs",		"rt",		"schar",	"sentchar",
+	"shc",		"shift",	"sizes",	"so",
+	"spacewidth",	"special",	"spreadwarn",	"ss",
+	"sty",		"substring",	"sv",		"sy",
+	"T&",		"tc",		"TE",
+	"TH",		"tkf",		"tl",
+	"tm",		"tm1",		"tmc",		"tr",
+	"track",	"transchar",	"trf",		"trimat",
+	"trin",		"trnt",		"troff",	"TS",
+	"uf",		"ul",		"unformat",	"unwatch",
+	"unwatchn",	"vpt",		"vs",		"warn",
+	"warnscale",	"watch",	"watchlength",	"watchn",
+	"wh",		"while",	"write",	"writec",
+	"writem",	"xflag",	".",		NULL,
+	NULL,		"text",
+	"Dd",		"Dt",		"Os",		"Sh",
+	"Ss",		"Pp",		"D1",		"Dl",
+	"Bd",		"Ed",		"Bl",		"El",
+	"It",		"Ad",		"An",		"Ap",
+	"Ar",		"Cd",		"Cm",		"Dv",
+	"Er",		"Ev",		"Ex",		"Fa",
+	"Fd",		"Fl",		"Fn",		"Ft",
+	"Ic",		"In",		"Li",		"Nd",
+	"Nm",		"Op",		"Ot",		"Pa",
+	"Rv",		"St",		"Va",		"Vt",
+	"Xr",		"%A",		"%B",		"%D",
+	"%I",		"%J",		"%N",		"%O",
+	"%P",		"%R",		"%T",		"%V",
+	"Ac",		"Ao",		"Aq",		"At",
+	"Bc",		"Bf",		"Bo",		"Bq",
+	"Bsx",		"Bx",		"Db",		"Dc",
+	"Do",		"Dq",		"Ec",		"Ef",
+	"Em",		"Eo",		"Fx",		"Ms",
+	"No",		"Ns",		"Nx",		"Ox",
+	"Pc",		"Pf",		"Po",		"Pq",
+	"Qc",		"Ql",		"Qo",		"Qq",
+	"Re",		"Rs",		"Sc",		"So",
+	"Sq",		"Sm",		"Sx",		"Sy",
+	"Tn",		"Ux",		"Xc",		"Xo",
+	"Fo",		"Fc",		"Oo",		"Oc",
+	"Bk",		"Ek",		"Bt",		"Hf",
+	"Fr",		"Ud",		"Lb",		"Lp",
+	"Lk",		"Mt",		"Brq",		"Bro",
+	"Brc",		"%C",		"Es",		"En",
+	"Dx",		"%Q",		"%U",		"Ta",
+	"Tg",		NULL,
+	"TH",		"SH",		"SS",		"TP",
+	"TQ",
+	"LP",		"PP",		"P",		"IP",
+	"HP",		"SM",		"SB",		"BI",
+	"IB",		"BR",		"RB",		"R",
+	"B",		"I",		"IR",		"RI",
+	"RE",		"RS",		"DT",		"UC",
+	"PD",		"AT",		"in",
+	"SY",		"YS",		"OP",
+	"EX",		"EE",		"UR",
+	"UE",		"MT",		"ME",		NULL
+};
+const	char *const *roff_name = __roff_name;
+
+static	struct roffmac	 roffs[TOKEN_NONE] = {
+	{ roff_noarg, NULL, NULL, 0 },  /* br */
+	{ roff_onearg, NULL, NULL, 0 },  /* ce */
+	{ roff_noarg, NULL, NULL, 0 },  /* fi */
+	{ roff_onearg, NULL, NULL, 0 },  /* ft */
+	{ roff_onearg, NULL, NULL, 0 },  /* ll */
+	{ roff_onearg, NULL, NULL, 0 },  /* mc */
+	{ roff_noarg, NULL, NULL, 0 },  /* nf */
+	{ roff_onearg, NULL, NULL, 0 },  /* po */
+	{ roff_onearg, NULL, NULL, 0 },  /* rj */
+	{ roff_onearg, NULL, NULL, 0 },  /* sp */
+	{ roff_manyarg, NULL, NULL, 0 },  /* ta */
+	{ roff_onearg, NULL, NULL, 0 },  /* ti */
+	{ NULL, NULL, NULL, 0 },  /* ROFF_MAX */
+	{ roff_unsupp, NULL, NULL, 0 },  /* ab */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ad */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* af */
+	{ roff_unsupp, NULL, NULL, 0 },  /* aln */
+	{ roff_als, NULL, NULL, 0 },  /* als */
+	{ roff_block, roff_block_text, roff_block_sub, 0 },  /* am */
+	{ roff_block, roff_block_text, roff_block_sub, 0 },  /* am1 */
+	{ roff_block, roff_block_text, roff_block_sub, 0 },  /* ami */
+	{ roff_block, roff_block_text, roff_block_sub, 0 },  /* ami1 */
+	{ roff_ds, NULL, NULL, 0 },  /* as */
+	{ roff_ds, NULL, NULL, 0 },  /* as1 */
+	{ roff_unsupp, NULL, NULL, 0 },  /* asciify */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* backtrace */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* bd */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* bleedat */
+	{ roff_unsupp, NULL, NULL, 0 },  /* blm */
+	{ roff_unsupp, NULL, NULL, 0 },  /* box */
+	{ roff_unsupp, NULL, NULL, 0 },  /* boxa */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* bp */
+	{ roff_unsupp, NULL, NULL, 0 },  /* BP */
+	{ roff_break, NULL, NULL, 0 },  /* break */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* breakchar */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* brnl */
+	{ roff_noarg, NULL, NULL, 0 },  /* brp */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* brpnl */
+	{ roff_unsupp, NULL, NULL, 0 },  /* c2 */
+	{ roff_cc, NULL, NULL, 0 },  /* cc */
+	{ roff_insec, NULL, NULL, 0 },  /* cf */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* cflags */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ch */
+	{ roff_char, NULL, NULL, 0 },  /* char */
+	{ roff_unsupp, NULL, NULL, 0 },  /* chop */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* class */
+	{ roff_insec, NULL, NULL, 0 },  /* close */
+	{ roff_unsupp, NULL, NULL, 0 },  /* CL */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* color */
+	{ roff_unsupp, NULL, NULL, 0 },  /* composite */
+	{ roff_unsupp, NULL, NULL, 0 },  /* continue */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* cp */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* cropat */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* cs */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* cu */
+	{ roff_unsupp, NULL, NULL, 0 },  /* da */
+	{ roff_unsupp, NULL, NULL, 0 },  /* dch */
+	{ roff_Dd, NULL, NULL, 0 },  /* Dd */
+	{ roff_block, roff_block_text, roff_block_sub, 0 },  /* de */
+	{ roff_block, roff_block_text, roff_block_sub, 0 },  /* de1 */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* defcolor */
+	{ roff_block, roff_block_text, roff_block_sub, 0 },  /* dei */
+	{ roff_block, roff_block_text, roff_block_sub, 0 },  /* dei1 */
+	{ roff_unsupp, NULL, NULL, 0 },  /* device */
+	{ roff_unsupp, NULL, NULL, 0 },  /* devicem */
+	{ roff_unsupp, NULL, NULL, 0 },  /* di */
+	{ roff_unsupp, NULL, NULL, 0 },  /* do */
+	{ roff_ds, NULL, NULL, 0 },  /* ds */
+	{ roff_ds, NULL, NULL, 0 },  /* ds1 */
+	{ roff_unsupp, NULL, NULL, 0 },  /* dwh */
+	{ roff_unsupp, NULL, NULL, 0 },  /* dt */
+	{ roff_ec, NULL, NULL, 0 },  /* ec */
+	{ roff_unsupp, NULL, NULL, 0 },  /* ecr */
+	{ roff_unsupp, NULL, NULL, 0 },  /* ecs */
+	{ roff_cond, roff_cond_text, roff_cond_sub, ROFFMAC_STRUCT },  /* el */
+	{ roff_unsupp, NULL, NULL, 0 },  /* em */
+	{ roff_EN, NULL, NULL, 0 },  /* EN */
+	{ roff_eo, NULL, NULL, 0 },  /* eo */
+	{ roff_unsupp, NULL, NULL, 0 },  /* EP */
+	{ roff_EQ, NULL, NULL, 0 },  /* EQ */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* errprint */
+	{ roff_unsupp, NULL, NULL, 0 },  /* ev */
+	{ roff_unsupp, NULL, NULL, 0 },  /* evc */
+	{ roff_unsupp, NULL, NULL, 0 },  /* ex */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fallback */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fam */
+	{ roff_unsupp, NULL, NULL, 0 },  /* fc */
+	{ roff_unsupp, NULL, NULL, 0 },  /* fchar */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fcolor */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fdeferlig */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* feature */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fkern */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fl */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* flig */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fp */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fps */
+	{ roff_unsupp, NULL, NULL, 0 },  /* fschar */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fspacewidth */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fspecial */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ftr */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* fzoom */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* gcolor */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hc */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hcode */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hidechar */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hla */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hlm */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hpf */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hpfa */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hpfcode */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hw */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hy */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hylang */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hylen */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hym */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hypp */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* hys */
+	{ roff_cond, roff_cond_text, roff_cond_sub, ROFFMAC_STRUCT },  /* ie */
+	{ roff_cond, roff_cond_text, roff_cond_sub, ROFFMAC_STRUCT },  /* if */
+	{ roff_block, roff_block_text, roff_block_sub, 0 },  /* ig */
+	{ roff_unsupp, NULL, NULL, 0 },  /* index */
+	{ roff_it, NULL, NULL, 0 },  /* it */
+	{ roff_unsupp, NULL, NULL, 0 },  /* itc */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* IX */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* kern */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* kernafter */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* kernbefore */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* kernpair */
+	{ roff_unsupp, NULL, NULL, 0 },  /* lc */
+	{ roff_unsupp, NULL, NULL, 0 },  /* lc_ctype */
+	{ roff_unsupp, NULL, NULL, 0 },  /* lds */
+	{ roff_unsupp, NULL, NULL, 0 },  /* length */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* letadj */
+	{ roff_insec, NULL, NULL, 0 },  /* lf */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* lg */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* lhang */
+	{ roff_unsupp, NULL, NULL, 0 },  /* linetabs */
+	{ roff_unsupp, NULL, NULL, 0 },  /* lnr */
+	{ roff_unsupp, NULL, NULL, 0 },  /* lnrf */
+	{ roff_unsupp, NULL, NULL, 0 },  /* lpfx */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ls */
+	{ roff_unsupp, NULL, NULL, 0 },  /* lsm */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* lt */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* mediasize */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* minss */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* mk */
+	{ roff_insec, NULL, NULL, 0 },  /* mso */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* na */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ne */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* nh */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* nhychar */
+	{ roff_unsupp, NULL, NULL, 0 },  /* nm */
+	{ roff_unsupp, NULL, NULL, 0 },  /* nn */
+	{ roff_nop, NULL, NULL, 0 },  /* nop */
+	{ roff_nr, NULL, NULL, 0 },  /* nr */
+	{ roff_unsupp, NULL, NULL, 0 },  /* nrf */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* nroff */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ns */
+	{ roff_insec, NULL, NULL, 0 },  /* nx */
+	{ roff_insec, NULL, NULL, 0 },  /* open */
+	{ roff_insec, NULL, NULL, 0 },  /* opena */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* os */
+	{ roff_unsupp, NULL, NULL, 0 },  /* output */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* padj */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* papersize */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* pc */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* pev */
+	{ roff_insec, NULL, NULL, 0 },  /* pi */
+	{ roff_unsupp, NULL, NULL, 0 },  /* PI */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* pl */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* pm */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* pn */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* pnr */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ps */
+	{ roff_unsupp, NULL, NULL, 0 },  /* psbb */
+	{ roff_unsupp, NULL, NULL, 0 },  /* pshape */
+	{ roff_insec, NULL, NULL, 0 },  /* pso */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ptr */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* pvs */
+	{ roff_unsupp, NULL, NULL, 0 },  /* rchar */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* rd */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* recursionlimit */
+	{ roff_return, NULL, NULL, 0 },  /* return */
+	{ roff_unsupp, NULL, NULL, 0 },  /* rfschar */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* rhang */
+	{ roff_rm, NULL, NULL, 0 },  /* rm */
+	{ roff_rn, NULL, NULL, 0 },  /* rn */
+	{ roff_unsupp, NULL, NULL, 0 },  /* rnn */
+	{ roff_rr, NULL, NULL, 0 },  /* rr */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* rs */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* rt */
+	{ roff_unsupp, NULL, NULL, 0 },  /* schar */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* sentchar */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* shc */
+	{ roff_shift, NULL, NULL, 0 },  /* shift */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* sizes */
+	{ roff_so, NULL, NULL, 0 },  /* so */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* spacewidth */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* special */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* spreadwarn */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ss */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* sty */
+	{ roff_unsupp, NULL, NULL, 0 },  /* substring */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* sv */
+	{ roff_insec, NULL, NULL, 0 },  /* sy */
+	{ roff_T_, NULL, NULL, 0 },  /* T& */
+	{ roff_unsupp, NULL, NULL, 0 },  /* tc */
+	{ roff_TE, NULL, NULL, 0 },  /* TE */
+	{ roff_Dd, NULL, NULL, 0 },  /* TH */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* tkf */
+	{ roff_unsupp, NULL, NULL, 0 },  /* tl */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* tm */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* tm1 */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* tmc */
+	{ roff_tr, NULL, NULL, 0 },  /* tr */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* track */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* transchar */
+	{ roff_insec, NULL, NULL, 0 },  /* trf */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* trimat */
+	{ roff_unsupp, NULL, NULL, 0 },  /* trin */
+	{ roff_unsupp, NULL, NULL, 0 },  /* trnt */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* troff */
+	{ roff_TS, NULL, NULL, 0 },  /* TS */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* uf */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* ul */
+	{ roff_unsupp, NULL, NULL, 0 },  /* unformat */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* unwatch */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* unwatchn */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* vpt */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* vs */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* warn */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* warnscale */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* watch */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* watchlength */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* watchn */
+	{ roff_unsupp, NULL, NULL, 0 },  /* wh */
+	{ roff_cond, roff_cond_text, roff_cond_sub, ROFFMAC_STRUCT }, /*while*/
+	{ roff_insec, NULL, NULL, 0 },  /* write */
+	{ roff_insec, NULL, NULL, 0 },  /* writec */
+	{ roff_insec, NULL, NULL, 0 },  /* writem */
+	{ roff_line_ignore, NULL, NULL, 0 },  /* xflag */
+	{ roff_cblock, NULL, NULL, 0 },  /* . */
+	{ roff_renamed, NULL, NULL, 0 },
+	{ roff_userdef, NULL, NULL, 0 }
+};
+
+/* Array of injected predefined strings. */
+#define	PREDEFS_MAX	 38
+static	const struct predef predefs[PREDEFS_MAX] = {
+#include "predefs.in"
+};
+
+static	int	 roffce_lines;	/* number of input lines to center */
+static	struct roff_node *roffce_node;  /* active request */
+static	int	 roffit_lines;  /* number of lines to delay */
+static	char	*roffit_macro;  /* nil-terminated macro line */
+
+
+/* --- request table ------------------------------------------------------ */
+
+struct ohash *
+roffhash_alloc(enum roff_tok mintok, enum roff_tok maxtok)
+{
+	struct ohash	*htab;
+	struct roffreq	*req;
+	enum roff_tok	 tok;
+	size_t		 sz;
+	unsigned int	 slot;
+
+	htab = mandoc_malloc(sizeof(*htab));
+	mandoc_ohash_init(htab, 8, offsetof(struct roffreq, name));
+
+	for (tok = mintok; tok < maxtok; tok++) {
+		if (roff_name[tok] == NULL)
+			continue;
+		sz = strlen(roff_name[tok]);
+		req = mandoc_malloc(sizeof(*req) + sz + 1);
+		req->tok = tok;
+		memcpy(req->name, roff_name[tok], sz + 1);
+		slot = ohash_qlookup(htab, req->name);
+		ohash_insert(htab, slot, req);
+	}
+	return htab;
+}
+
+void
+roffhash_free(struct ohash *htab)
+{
+	struct roffreq	*req;
+	unsigned int	 slot;
+
+	if (htab == NULL)
+		return;
+	for (req = ohash_first(htab, &slot); req != NULL;
+	     req = ohash_next(htab, &slot))
+		free(req);
+	ohash_delete(htab);
+	free(htab);
+}
+
+enum roff_tok
+roffhash_find(struct ohash *htab, const char *name, size_t sz)
+{
+	struct roffreq	*req;
+	const char	*end;
+
+	if (sz) {
+		end = name + sz;
+		req = ohash_find(htab, ohash_qlookupi(htab, name, &end));
+	} else
+		req = ohash_find(htab, ohash_qlookup(htab, name));
+	return req == NULL ? TOKEN_NONE : req->tok;
+}
+
+/* --- stack of request blocks -------------------------------------------- */
+
+/*
+ * Pop the current node off of the stack of roff instructions currently
+ * pending.  Return 1 if it is a loop or 0 otherwise.
+ */
+static int
+roffnode_pop(struct roff *r)
+{
+	struct roffnode	*p;
+	int		 inloop;
+
+	p = r->last;
+	inloop = p->tok == ROFF_while;
+	r->last = p->parent;
+	free(p->name);
+	free(p->end);
+	free(p);
+	return inloop;
+}
+
+/*
+ * Push a roff node onto the instruction stack.  This must later be
+ * removed with roffnode_pop().
+ */
+static void
+roffnode_push(struct roff *r, enum roff_tok tok, const char *name,
+		int line, int col)
+{
+	struct roffnode	*p;
+
+	p = mandoc_calloc(1, sizeof(struct roffnode));
+	p->tok = tok;
+	if (name)
+		p->name = mandoc_strdup(name);
+	p->parent = r->last;
+	p->line = line;
+	p->col = col;
+	p->rule = p->parent ? p->parent->rule : 0;
+
+	r->last = p;
+}
+
+/* --- roff parser state data management ---------------------------------- */
+
+static void
+roff_free1(struct roff *r)
+{
+	int		 i;
+
+	tbl_free(r->first_tbl);
+	r->first_tbl = r->last_tbl = r->tbl = NULL;
+
+	eqn_free(r->last_eqn);
+	r->last_eqn = r->eqn = NULL;
+
+	while (r->mstackpos >= 0)
+		roff_userret(r);
+
+	while (r->last)
+		roffnode_pop(r);
+
+	free (r->rstack);
+	r->rstack = NULL;
+	r->rstacksz = 0;
+	r->rstackpos = -1;
+
+	roff_freereg(r->regtab);
+	r->regtab = NULL;
+
+	roff_freestr(r->strtab);
+	roff_freestr(r->rentab);
+	roff_freestr(r->xmbtab);
+	r->strtab = r->rentab = r->xmbtab = NULL;
+
+	if (r->xtab)
+		for (i = 0; i < 128; i++)
+			free(r->xtab[i].p);
+	free(r->xtab);
+	r->xtab = NULL;
+}
+
+void
+roff_reset(struct roff *r)
+{
+	roff_free1(r);
+	r->options |= MPARSE_COMMENT;
+	r->format = r->options & (MPARSE_MDOC | MPARSE_MAN);
+	r->control = '\0';
+	r->escape = '\\';
+	roffce_lines = 0;
+	roffce_node = NULL;
+	roffit_lines = 0;
+	roffit_macro = NULL;
+}
+
+void
+roff_free(struct roff *r)
+{
+	int		 i;
+
+	roff_free1(r);
+	for (i = 0; i < r->mstacksz; i++)
+		free(r->mstack[i].argv);
+	free(r->mstack);
+	roffhash_free(r->reqtab);
+	free(r);
+}
+
+struct roff *
+roff_alloc(int options)
+{
+	struct roff	*r;
+
+	r = mandoc_calloc(1, sizeof(struct roff));
+	r->reqtab = roffhash_alloc(0, ROFF_RENAMED);
+	r->options = options | MPARSE_COMMENT;
+	r->format = options & (MPARSE_MDOC | MPARSE_MAN);
+	r->mstackpos = -1;
+	r->rstackpos = -1;
+	r->escape = '\\';
+	return r;
+}
+
+/* --- syntax tree state data management ---------------------------------- */
+
+static void
+roff_man_free1(struct roff_man *man)
+{
+	if (man->meta.first != NULL)
+		roff_node_delete(man, man->meta.first);
+	free(man->meta.msec);
+	free(man->meta.vol);
+	free(man->meta.os);
+	free(man->meta.arch);
+	free(man->meta.title);
+	free(man->meta.name);
+	free(man->meta.date);
+	free(man->meta.sodest);
+}
+
+void
+roff_state_reset(struct roff_man *man)
+{
+	man->last = man->meta.first;
+	man->last_es = NULL;
+	man->flags = 0;
+	man->lastsec = man->lastnamed = SEC_NONE;
+	man->next = ROFF_NEXT_CHILD;
+	roff_setreg(man->roff, "nS", 0, '=');
+}
+
+static void
+roff_man_alloc1(struct roff_man *man)
+{
+	memset(&man->meta, 0, sizeof(man->meta));
+	man->meta.first = mandoc_calloc(1, sizeof(*man->meta.first));
+	man->meta.first->type = ROFFT_ROOT;
+	man->meta.macroset = MACROSET_NONE;
+	roff_state_reset(man);
+}
+
+void
+roff_man_reset(struct roff_man *man)
+{
+	roff_man_free1(man);
+	roff_man_alloc1(man);
+}
+
+void
+roff_man_free(struct roff_man *man)
+{
+	roff_man_free1(man);
+	free(man);
+}
+
+struct roff_man *
+roff_man_alloc(struct roff *roff, const char *os_s, int quick)
+{
+	struct roff_man *man;
+
+	man = mandoc_calloc(1, sizeof(*man));
+	man->roff = roff;
+	man->os_s = os_s;
+	man->quick = quick;
+	roff_man_alloc1(man);
+	roff->man = man;
+	return man;
+}
+
+/* --- syntax tree handling ----------------------------------------------- */
+
+struct roff_node *
+roff_node_alloc(struct roff_man *man, int line, int pos,
+	enum roff_type type, int tok)
+{
+	struct roff_node	*n;
+
+	n = mandoc_calloc(1, sizeof(*n));
+	n->line = line;
+	n->pos = pos;
+	n->tok = tok;
+	n->type = type;
+	n->sec = man->lastsec;
+
+	if (man->flags & MDOC_SYNOPSIS)
+		n->flags |= NODE_SYNPRETTY;
+	else
+		n->flags &= ~NODE_SYNPRETTY;
+	if ((man->flags & (ROFF_NOFILL | ROFF_NONOFILL)) == ROFF_NOFILL)
+		n->flags |= NODE_NOFILL;
+	else
+		n->flags &= ~NODE_NOFILL;
+	if (man->flags & MDOC_NEWLINE)
+		n->flags |= NODE_LINE;
+	man->flags &= ~MDOC_NEWLINE;
+
+	return n;
+}
+
+void
+roff_node_append(struct roff_man *man, struct roff_node *n)
+{
+
+	switch (man->next) {
+	case ROFF_NEXT_SIBLING:
+		if (man->last->next != NULL) {
+			n->next = man->last->next;
+			man->last->next->prev = n;
+		} else
+			man->last->parent->last = n;
+		man->last->next = n;
+		n->prev = man->last;
+		n->parent = man->last->parent;
+		break;
+	case ROFF_NEXT_CHILD:
+		if (man->last->child != NULL) {
+			n->next = man->last->child;
+			man->last->child->prev = n;
+		} else
+			man->last->last = n;
+		man->last->child = n;
+		n->parent = man->last;
+		break;
+	default:
+		abort();
+	}
+	man->last = n;
+
+	switch (n->type) {
+	case ROFFT_HEAD:
+		n->parent->head = n;
+		break;
+	case ROFFT_BODY:
+		if (n->end != ENDBODY_NOT)
+			return;
+		n->parent->body = n;
+		break;
+	case ROFFT_TAIL:
+		n->parent->tail = n;
+		break;
+	default:
+		return;
+	}
+
+	/*
+	 * Copy over the normalised-data pointer of our parent.  Not
+	 * everybody has one, but copying a null pointer is fine.
+	 */
+
+	n->norm = n->parent->norm;
+	assert(n->parent->type == ROFFT_BLOCK);
+}
+
+void
+roff_word_alloc(struct roff_man *man, int line, int pos, const char *word)
+{
+	struct roff_node	*n;
+
+	n = roff_node_alloc(man, line, pos, ROFFT_TEXT, TOKEN_NONE);
+	n->string = roff_strdup(man->roff, word);
+	roff_node_append(man, n);
+	n->flags |= NODE_VALID | NODE_ENDED;
+	man->next = ROFF_NEXT_SIBLING;
+}
+
+void
+roff_word_append(struct roff_man *man, const char *word)
+{
+	struct roff_node	*n;
+	char			*addstr, *newstr;
+
+	n = man->last;
+	addstr = roff_strdup(man->roff, word);
+	mandoc_asprintf(&newstr, "%s %s", n->string, addstr);
+	free(addstr);
+	free(n->string);
+	n->string = newstr;
+	man->next = ROFF_NEXT_SIBLING;
+}
+
+void
+roff_elem_alloc(struct roff_man *man, int line, int pos, int tok)
+{
+	struct roff_node	*n;
+
+	n = roff_node_alloc(man, line, pos, ROFFT_ELEM, tok);
+	roff_node_append(man, n);
+	man->next = ROFF_NEXT_CHILD;
+}
+
+struct roff_node *
+roff_block_alloc(struct roff_man *man, int line, int pos, int tok)
+{
+	struct roff_node	*n;
+
+	n = roff_node_alloc(man, line, pos, ROFFT_BLOCK, tok);
+	roff_node_append(man, n);
+	man->next = ROFF_NEXT_CHILD;
+	return n;
+}
+
+struct roff_node *
+roff_head_alloc(struct roff_man *man, int line, int pos, int tok)
+{
+	struct roff_node	*n;
+
+	n = roff_node_alloc(man, line, pos, ROFFT_HEAD, tok);
+	roff_node_append(man, n);
+	man->next = ROFF_NEXT_CHILD;
+	return n;
+}
+
+struct roff_node *
+roff_body_alloc(struct roff_man *man, int line, int pos, int tok)
+{
+	struct roff_node	*n;
+
+	n = roff_node_alloc(man, line, pos, ROFFT_BODY, tok);
+	roff_node_append(man, n);
+	man->next = ROFF_NEXT_CHILD;
+	return n;
+}
+
+static void
+roff_addtbl(struct roff_man *man, int line, struct tbl_node *tbl)
+{
+	struct roff_node	*n;
+	struct tbl_span		*span;
+
+	if (man->meta.macroset == MACROSET_MAN)
+		man_breakscope(man, ROFF_TS);
+	while ((span = tbl_span(tbl)) != NULL) {
+		n = roff_node_alloc(man, line, 0, ROFFT_TBL, TOKEN_NONE);
+		n->span = span;
+		roff_node_append(man, n);
+		n->flags |= NODE_VALID | NODE_ENDED;
+		man->next = ROFF_NEXT_SIBLING;
+	}
+}
+
+void
+roff_node_unlink(struct roff_man *man, struct roff_node *n)
+{
+
+	/* Adjust siblings. */
+
+	if (n->prev)
+		n->prev->next = n->next;
+	if (n->next)
+		n->next->prev = n->prev;
+
+	/* Adjust parent. */
+
+	if (n->parent != NULL) {
+		if (n->parent->child == n)
+			n->parent->child = n->next;
+		if (n->parent->last == n)
+			n->parent->last = n->prev;
+	}
+
+	/* Adjust parse point. */
+
+	if (man == NULL)
+		return;
+	if (man->last == n) {
+		if (n->prev == NULL) {
+			man->last = n->parent;
+			man->next = ROFF_NEXT_CHILD;
+		} else {
+			man->last = n->prev;
+			man->next = ROFF_NEXT_SIBLING;
+		}
+	}
+	if (man->meta.first == n)
+		man->meta.first = NULL;
+}
+
+void
+roff_node_relink(struct roff_man *man, struct roff_node *n)
+{
+	roff_node_unlink(man, n);
+	n->prev = n->next = NULL;
+	roff_node_append(man, n);
+}
+
+void
+roff_node_free(struct roff_node *n)
+{
+
+	if (n->args != NULL)
+		mdoc_argv_free(n->args);
+	if (n->type == ROFFT_BLOCK || n->type == ROFFT_ELEM)
+		free(n->norm);
+	eqn_box_free(n->eqn);
+	free(n->string);
+	free(n->tag);
+	free(n);
+}
+
+void
+roff_node_delete(struct roff_man *man, struct roff_node *n)
+{
+
+	while (n->child != NULL)
+		roff_node_delete(man, n->child);
+	roff_node_unlink(man, n);
+	roff_node_free(n);
+}
+
+int
+roff_node_transparent(struct roff_node *n)
+{
+	if (n == NULL)
+		return 0;
+	if (n->type == ROFFT_COMMENT || n->flags & NODE_NOPRT)
+		return 1;
+	return roff_tok_transparent(n->tok);
+}
+
+int
+roff_tok_transparent(enum roff_tok tok)
+{
+	switch (tok) {
+	case ROFF_ft:
+	case ROFF_ll:
+	case ROFF_mc:
+	case ROFF_po:
+	case ROFF_ta:
+	case MDOC_Db:
+	case MDOC_Es:
+	case MDOC_Sm:
+	case MDOC_Tg:
+	case MAN_DT:
+	case MAN_UC:
+	case MAN_PD:
+	case MAN_AT:
+		return 1;
+	default:
+		return 0;
+	}
+}
+
+struct roff_node *
+roff_node_child(struct roff_node *n)
+{
+	for (n = n->child; roff_node_transparent(n); n = n->next)
+		continue;
+	return n;
+}
+
+struct roff_node *
+roff_node_prev(struct roff_node *n)
+{
+	do {
+		n = n->prev;
+	} while (roff_node_transparent(n));
+	return n;
+}
+
+struct roff_node *
+roff_node_next(struct roff_node *n)
+{
+	do {
+		n = n->next;
+	} while (roff_node_transparent(n));
+	return n;
+}
+
+void
+deroff(char **dest, const struct roff_node *n)
+{
+	char	*cp;
+	size_t	 sz;
+
+	if (n->string == NULL) {
+		for (n = n->child; n != NULL; n = n->next)
+			deroff(dest, n);
+		return;
+	}
+
+	/* Skip leading whitespace. */
+
+	for (cp = n->string; *cp != '\0'; cp++) {
+		if (cp[0] == '\\' && cp[1] != '\0' &&
+		    strchr(" %&0^|~", cp[1]) != NULL)
+			cp++;
+		else if ( ! isspace((unsigned char)*cp))
+			break;
+	}
+
+	/* Skip trailing backslash. */
+
+	sz = strlen(cp);
+	if (sz > 0 && cp[sz - 1] == '\\')
+		sz--;
+
+	/* Skip trailing whitespace. */
+
+	for (; sz; sz--)
+		if ( ! isspace((unsigned char)cp[sz-1]))
+			break;
+
+	/* Skip empty strings. */
+
+	if (sz == 0)
+		return;
+
+	if (*dest == NULL) {
+		*dest = mandoc_strndup(cp, sz);
+		return;
+	}
+
+	mandoc_asprintf(&cp, "%s %*s", *dest, (int)sz, cp);
+	free(*dest);
+	*dest = cp;
+}
+
+/* --- main functions of the roff parser ---------------------------------- */
+
+/*
+ * In the current line, expand escape sequences that produce parsable
+ * input text.  Also check the syntax of the remaining escape sequences,
+ * which typically produce output glyphs or change formatter state.
+ */
+static int
+roff_expand(struct roff *r, struct buf *buf, int ln, int pos, char newesc)
+{
+	struct mctx	*ctx;	/* current macro call context */
+	char		 ubuf[24]; /* buffer to print the number */
+	struct roff_node *n;	/* used for header comments */
+	const char	*start;	/* start of the string to process */
+	char		*stesc;	/* start of an escape sequence ('\\') */
+	const char	*esct;	/* type of esccape sequence */
+	char		*ep;	/* end of comment string */
+	const char	*stnam;	/* start of the name, after "[(*" */
+	const char	*cp;	/* end of the name, e.g. before ']' */
+	const char	*res;	/* the string to be substituted */
+	char		*nbuf;	/* new buffer to copy buf->buf to */
+	size_t		 maxl;  /* expected length of the escape name */
+	size_t		 naml;	/* actual length of the escape name */
+	size_t		 asz;	/* length of the replacement */
+	size_t		 rsz;	/* length of the rest of the string */
+	int		 inaml;	/* length returned from mandoc_escape() */
+	int		 expand_count;	/* to avoid infinite loops */
+	int		 npos;	/* position in numeric expression */
+	int		 arg_complete; /* argument not interrupted by eol */
+	int		 quote_args; /* true for \\$@, false for \\$* */
+	int		 done;	/* no more input available */
+	int		 deftype; /* type of definition to paste */
+	int		 rcsid;	/* kind of RCS id seen */
+	enum mandocerr	 err;	/* for escape sequence problems */
+	char		 sign;	/* increment number register */
+	char		 term;	/* character terminating the escape */
+
+	/* Search forward for comments. */
+
+	done = 0;
+	start = buf->buf + pos;
+	for (stesc = buf->buf + pos; *stesc != '\0'; stesc++) {
+		if (stesc[0] != newesc || stesc[1] == '\0')
+			continue;
+		stesc++;
+		if (*stesc != '"' && *stesc != '#')
+			continue;
+
+		/* Comment found, look for RCS id. */
+
+		rcsid = 0;
+		if ((cp = strstr(stesc, "$" "OpenBSD")) != NULL) {
+			rcsid = 1 << MANDOC_OS_OPENBSD;
+			cp += 8;
+		} else if ((cp = strstr(stesc, "$" "NetBSD")) != NULL) {
+			rcsid = 1 << MANDOC_OS_NETBSD;
+			cp += 7;
+		}
+		if (cp != NULL &&
+		    isalnum((unsigned char)*cp) == 0 &&
+		    strchr(cp, '$') != NULL) {
+			if (r->man->meta.rcsids & rcsid)
+				mandoc_msg(MANDOCERR_RCS_REP, ln,
+				    (int)(stesc - buf->buf) + 1,
+				    "%s", stesc + 1);
+			r->man->meta.rcsids |= rcsid;
+		}
+
+		/* Handle trailing whitespace. */
+
+		ep = strchr(stesc--, '\0') - 1;
+		if (*ep == '\n') {
+			done = 1;
+			ep--;
+		}
+		if (*ep == ' ' || *ep == '\t')
+			mandoc_msg(MANDOCERR_SPACE_EOL,
+			    ln, (int)(ep - buf->buf), NULL);
+
+		/*
+		 * Save comments preceding the title macro
+		 * in the syntax tree.
+		 */
+
+		if (newesc != ASCII_ESC && r->options & MPARSE_COMMENT) {
+			while (*ep == ' ' || *ep == '\t')
+				ep--;
+			ep[1] = '\0';
+			n = roff_node_alloc(r->man,
+			    ln, stesc + 1 - buf->buf,
+			    ROFFT_COMMENT, TOKEN_NONE);
+			n->string = mandoc_strdup(stesc + 2);
+			roff_node_append(r->man, n);
+			n->flags |= NODE_VALID | NODE_ENDED;
+			r->man->next = ROFF_NEXT_SIBLING;
+		}
+
+		/* Line continuation with comment. */
+
+		if (stesc[1] == '#') {
+			*stesc = '\0';
+			return ROFF_IGN | ROFF_APPEND;
+		}
+
+		/* Discard normal comments. */
+
+		while (stesc > start && stesc[-1] == ' ' &&
+		    (stesc == start + 1 || stesc[-2] != '\\'))
+			stesc--;
+		*stesc = '\0';
+		break;
+	}
+	if (stesc == start)
+		return ROFF_CONT;
+	stesc--;
+
+	/* Notice the end of the input. */
+
+	if (*stesc == '\n') {
+		*stesc-- = '\0';
+		done = 1;
+	}
+
+	expand_count = 0;
+	while (stesc >= start) {
+		if (*stesc != newesc) {
+
+			/*
+			 * If we have a non-standard escape character,
+			 * escape literal backslashes because all
+			 * processing in subsequent functions uses
+			 * the standard escaping rules.
+			 */
+
+			if (newesc != ASCII_ESC && *stesc == '\\') {
+				*stesc = '\0';
+				buf->sz = mandoc_asprintf(&nbuf, "%s\\e%s",
+				    buf->buf, stesc + 1) + 1;
+				start = nbuf + pos;
+				stesc = nbuf + (stesc - buf->buf);
+				free(buf->buf);
+				buf->buf = nbuf;
+			}
+
+			/* Search backwards for the next escape. */
+
+			stesc--;
+			continue;
+		}
+
+		/* If it is escaped, skip it. */
+
+		for (cp = stesc - 1; cp >= start; cp--)
+			if (*cp != r->escape)
+				break;
+
+		if ((stesc - cp) % 2 == 0) {
+			while (stesc > cp)
+				*stesc-- = '\\';
+			continue;
+		} else if (stesc[1] != '\0') {
+			*stesc = '\\';
+		} else {
+			*stesc-- = '\0';
+			if (done)
+				continue;
+			else
+				return ROFF_IGN | ROFF_APPEND;
+		}
+
+		/* Decide whether to expand or to check only. */
+
+		term = '\0';
+		cp = stesc + 1;
+		if (*cp == 'E')
+			cp++;
+		esct = cp;
+		switch (*esct) {
+		case '*':
+		case '$':
+			res = NULL;
+			break;
+		case 'B':
+		case 'w':
+			term = cp[1];
+			/* FALLTHROUGH */
+		case 'n':
+			sign = cp[1];
+			if (sign == '+' || sign == '-')
+				cp++;
+			res = ubuf;
+			break;
+		default:
+			err = MANDOCERR_OK;
+			switch(mandoc_escape(&cp, &stnam, &inaml)) {
+			case ESCAPE_SPECIAL:
+				if (mchars_spec2cp(stnam, inaml) >= 0)
+					break;
+				/* FALLTHROUGH */
+			case ESCAPE_ERROR:
+				err = MANDOCERR_ESC_BAD;
+				break;
+			case ESCAPE_UNDEF:
+				err = MANDOCERR_ESC_UNDEF;
+				break;
+			case ESCAPE_UNSUPP:
+				err = MANDOCERR_ESC_UNSUPP;
+				break;
+			default:
+				break;
+			}
+			if (err != MANDOCERR_OK)
+				mandoc_msg(err, ln, (int)(stesc - buf->buf),
+				    "%.*s", (int)(cp - stesc), stesc);
+			stesc--;
+			continue;
+		}
+
+		if (EXPAND_LIMIT < ++expand_count) {
+			mandoc_msg(MANDOCERR_ROFFLOOP,
+			    ln, (int)(stesc - buf->buf), NULL);
+			return ROFF_IGN;
+		}
+
+		/*
+		 * The third character decides the length
+		 * of the name of the string or register.
+		 * Save a pointer to the name.
+		 */
+
+		if (term == '\0') {
+			switch (*++cp) {
+			case '\0':
+				maxl = 0;
+				break;
+			case '(':
+				cp++;
+				maxl = 2;
+				break;
+			case '[':
+				cp++;
+				term = ']';
+				maxl = 0;
+				break;
+			default:
+				maxl = 1;
+				break;
+			}
+		} else {
+			cp += 2;
+			maxl = 0;
+		}
+		stnam = cp;
+
+		/* Advance to the end of the name. */
+
+		naml = 0;
+		arg_complete = 1;
+		while (maxl == 0 || naml < maxl) {
+			if (*cp == '\0') {
+				mandoc_msg(MANDOCERR_ESC_BAD, ln,
+				    (int)(stesc - buf->buf), "%s", stesc);
+				arg_complete = 0;
+				break;
+			}
+			if (maxl == 0 && *cp == term) {
+				cp++;
+				break;
+			}
+			if (*cp++ != '\\' || *esct != 'w') {
+				naml++;
+				continue;
+			}
+			switch (mandoc_escape(&cp, NULL, NULL)) {
+			case ESCAPE_SPECIAL:
+			case ESCAPE_UNICODE:
+			case ESCAPE_NUMBERED:
+			case ESCAPE_UNDEF:
+			case ESCAPE_OVERSTRIKE:
+				naml++;
+				break;
+			default:
+				break;
+			}
+		}
+
+		/*
+		 * Retrieve the replacement string; if it is
+		 * undefined, resume searching for escapes.
+		 */
+
+		switch (*esct) {
+		case '*':
+			if (arg_complete) {
+				deftype = ROFFDEF_USER | ROFFDEF_PRE;
+				res = roff_getstrn(r, stnam, naml, &deftype);
+
+				/*
+				 * If not overriden, let \*(.T
+				 * through to the formatters.
+				 */
+
+				if (res == NULL && naml == 2 &&
+				    stnam[0] == '.' && stnam[1] == 'T') {
+					roff_setstrn(&r->strtab,
+					    ".T", 2, NULL, 0, 0);
+					stesc--;
+					continue;
+				}
+			}
+			break;
+		case '$':
+			if (r->mstackpos < 0) {
+				mandoc_msg(MANDOCERR_ARG_UNDEF, ln,
+				    (int)(stesc - buf->buf), "%.3s", stesc);
+				break;
+			}
+			ctx = r->mstack + r->mstackpos;
+			npos = esct[1] - '1';
+			if (npos >= 0 && npos <= 8) {
+				res = npos < ctx->argc ?
+				    ctx->argv[npos] : "";
+				break;
+			}
+			if (esct[1] == '*')
+				quote_args = 0;
+			else if (esct[1] == '@')
+				quote_args = 1;
+			else {
+				mandoc_msg(MANDOCERR_ARG_NONUM, ln,
+				    (int)(stesc - buf->buf), "%.3s", stesc);
+				break;
+			}
+			asz = 0;
+			for (npos = 0; npos < ctx->argc; npos++) {
+				if (npos)
+					asz++;  /* blank */
+				if (quote_args)
+					asz += 2;  /* quotes */
+				asz += strlen(ctx->argv[npos]);
+			}
+			if (asz != 3) {
+				rsz = buf->sz - (stesc - buf->buf) - 3;
+				if (asz < 3)
+					memmove(stesc + asz, stesc + 3, rsz);
+				buf->sz += asz - 3;
+				nbuf = mandoc_realloc(buf->buf, buf->sz);
+				start = nbuf + pos;
+				stesc = nbuf + (stesc - buf->buf);
+				buf->buf = nbuf;
+				if (asz > 3)
+					memmove(stesc + asz, stesc + 3, rsz);
+			}
+			for (npos = 0; npos < ctx->argc; npos++) {
+				if (npos)
+					*stesc++ = ' ';
+				if (quote_args)
+					*stesc++ = '"';
+				cp = ctx->argv[npos];
+				while (*cp != '\0')
+					*stesc++ = *cp++;
+				if (quote_args)
+					*stesc++ = '"';
+			}
+			continue;
+		case 'B':
+			npos = 0;
+			ubuf[0] = arg_complete &&
+			    roff_evalnum(r, ln, stnam, &npos,
+			      NULL, ROFFNUM_SCALE) &&
+			    stnam + npos + 1 == cp ? '1' : '0';
+			ubuf[1] = '\0';
+			break;
+		case 'n':
+			if (arg_complete)
+				(void)snprintf(ubuf, sizeof(ubuf), "%d",
+				    roff_getregn(r, stnam, naml, sign));
+			else
+				ubuf[0] = '\0';
+			break;
+		case 'w':
+			/* use even incomplete args */
+			(void)snprintf(ubuf, sizeof(ubuf), "%d",
+			    24 * (int)naml);
+			break;
+		}
+
+		if (res == NULL) {
+			if (*esct == '*')
+				mandoc_msg(MANDOCERR_STR_UNDEF,
+				    ln, (int)(stesc - buf->buf),
+				    "%.*s", (int)naml, stnam);
+			res = "";
+		} else if (buf->sz + strlen(res) > SHRT_MAX) {
+			mandoc_msg(MANDOCERR_ROFFLOOP,
+			    ln, (int)(stesc - buf->buf), NULL);
+			return ROFF_IGN;
+		}
+
+		/* Replace the escape sequence by the string. */
+
+		*stesc = '\0';
+		buf->sz = mandoc_asprintf(&nbuf, "%s%s%s",
+		    buf->buf, res, cp) + 1;
+
+		/* Prepare for the next replacement. */
+
+		start = nbuf + pos;
+		stesc = nbuf + (stesc - buf->buf) + strlen(res);
+		free(buf->buf);
+		buf->buf = nbuf;
+	}
+	return ROFF_CONT;
+}
+
+/*
+ * Parse a quoted or unquoted roff-style request or macro argument.
+ * Return a pointer to the parsed argument, which is either the original
+ * pointer or advanced by one byte in case the argument is quoted.
+ * NUL-terminate the argument in place.
+ * Collapse pairs of quotes inside quoted arguments.
+ * Advance the argument pointer to the next argument,
+ * or to the NUL byte terminating the argument line.
+ */
+char *
+roff_getarg(struct roff *r, char **cpp, int ln, int *pos)
+{
+	struct buf	 buf;
+	char		*cp, *start;
+	int		 newesc, pairs, quoted, white;
+
+	/* Quoting can only start with a new word. */
+	start = *cpp;
+	quoted = 0;
+	if ('"' == *start) {
+		quoted = 1;
+		start++;
+	}
+
+	newesc = pairs = white = 0;
+	for (cp = start; '\0' != *cp; cp++) {
+
+		/*
+		 * Move the following text left
+		 * after quoted quotes and after "\\" and "\t".
+		 */
+		if (pairs)
+			cp[-pairs] = cp[0];
+
+		if ('\\' == cp[0]) {
+			/*
+			 * In copy mode, translate double to single
+			 * backslashes and backslash-t to literal tabs.
+			 */
+			switch (cp[1]) {
+			case 'a':
+			case 't':
+				cp[-pairs] = '\t';
+				pairs++;
+				cp++;
+				break;
+			case '\\':
+				newesc = 1;
+				cp[-pairs] = ASCII_ESC;
+				pairs++;
+				cp++;
+				break;
+			case ' ':
+				/* Skip escaped blanks. */
+				if (0 == quoted)
+					cp++;
+				break;
+			default:
+				break;
+			}
+		} else if (0 == quoted) {
+			if (' ' == cp[0]) {
+				/* Unescaped blanks end unquoted args. */
+				white = 1;
+				break;
+			}
+		} else if ('"' == cp[0]) {
+			if ('"' == cp[1]) {
+				/* Quoted quotes collapse. */
+				pairs++;
+				cp++;
+			} else {
+				/* Unquoted quotes end quoted args. */
+				quoted = 2;
+				break;
+			}
+		}
+	}
+
+	/* Quoted argument without a closing quote. */
+	if (1 == quoted)
+		mandoc_msg(MANDOCERR_ARG_QUOTE, ln, *pos, NULL);
+
+	/* NUL-terminate this argument and move to the next one. */
+	if (pairs)
+		cp[-pairs] = '\0';
+	if ('\0' != *cp) {
+		*cp++ = '\0';
+		while (' ' == *cp)
+			cp++;
+	}
+	*pos += (int)(cp - start) + (quoted ? 1 : 0);
+	*cpp = cp;
+
+	if ('\0' == *cp && (white || ' ' == cp[-1]))
+		mandoc_msg(MANDOCERR_SPACE_EOL, ln, *pos, NULL);
+
+	start = mandoc_strdup(start);
+	if (newesc == 0)
+		return start;
+
+	buf.buf = start;
+	buf.sz = strlen(start) + 1;
+	buf.next = NULL;
+	if (roff_expand(r, &buf, ln, 0, ASCII_ESC) & ROFF_IGN) {
+		free(buf.buf);
+		buf.buf = mandoc_strdup("");
+	}
+	return buf.buf;
+}
+
+
+/*
+ * Process text streams.
+ */
+static int
+roff_parsetext(struct roff *r, struct buf *buf, int pos, int *offs)
+{
+	size_t		 sz;
+	const char	*start;
+	char		*p;
+	int		 isz;
+	enum mandoc_esc	 esc;
+
+	/* Spring the input line trap. */
+
+	if (roffit_lines == 1) {
+		isz = mandoc_asprintf(&p, "%s\n.%s", buf->buf, roffit_macro);
+		free(buf->buf);
+		buf->buf = p;
+		buf->sz = isz + 1;
+		*offs = 0;
+		free(roffit_macro);
+		roffit_lines = 0;
+		return ROFF_REPARSE;
+	} else if (roffit_lines > 1)
+		--roffit_lines;
+
+	if (roffce_node != NULL && buf->buf[pos] != '\0') {
+		if (roffce_lines < 1) {
+			r->man->last = roffce_node;
+			r->man->next = ROFF_NEXT_SIBLING;
+			roffce_lines = 0;
+			roffce_node = NULL;
+		} else
+			roffce_lines--;
+	}
+
+	/* Convert all breakable hyphens into ASCII_HYPH. */
+
+	start = p = buf->buf + pos;
+
+	while (*p != '\0') {
+		sz = strcspn(p, "-\\");
+		p += sz;
+
+		if (*p == '\0')
+			break;
+
+		if (*p == '\\') {
+			/* Skip over escapes. */
+			p++;
+			esc = mandoc_escape((const char **)&p, NULL, NULL);
+			if (esc == ESCAPE_ERROR)
+				break;
+			while (*p == '-')
+				p++;
+			continue;
+		} else if (p == start) {
+			p++;
+			continue;
+		}
+
+		if (isalpha((unsigned char)p[-1]) &&
+		    isalpha((unsigned char)p[1]))
+			*p = ASCII_HYPH;
+		p++;
+	}
+	return ROFF_CONT;
+}
+
+int
+roff_parseln(struct roff *r, int ln, struct buf *buf, int *offs)
+{
+	enum roff_tok	 t;
+	int		 e;
+	int		 pos;	/* parse point */
+	int		 spos;	/* saved parse point for messages */
+	int		 ppos;	/* original offset in buf->buf */
+	int		 ctl;	/* macro line (boolean) */
+
+	ppos = pos = *offs;
+
+	/* Handle in-line equation delimiters. */
+
+	if (r->tbl == NULL &&
+	    r->last_eqn != NULL && r->last_eqn->delim &&
+	    (r->eqn == NULL || r->eqn_inline)) {
+		e = roff_eqndelim(r, buf, pos);
+		if (e == ROFF_REPARSE)
+			return e;
+		assert(e == ROFF_CONT);
+	}
+
+	/* Expand some escape sequences. */
+
+	e = roff_expand(r, buf, ln, pos, r->escape);
+	if ((e & ROFF_MASK) == ROFF_IGN)
+		return e;
+	assert(e == ROFF_CONT);
+
+	ctl = roff_getcontrol(r, buf->buf, &pos);
+
+	/*
+	 * First, if a scope is open and we're not a macro, pass the
+	 * text through the macro's filter.
+	 * Equations process all content themselves.
+	 * Tables process almost all content themselves, but we want
+	 * to warn about macros before passing it there.
+	 */
+
+	if (r->last != NULL && ! ctl) {
+		t = r->last->tok;
+		e = (*roffs[t].text)(r, t, buf, ln, pos, pos, offs);
+		if ((e & ROFF_MASK) == ROFF_IGN)
+			return e;
+		e &= ~ROFF_MASK;
+	} else
+		e = ROFF_IGN;
+	if (r->eqn != NULL && strncmp(buf->buf + ppos, ".EN", 3)) {
+		eqn_read(r->eqn, buf->buf + ppos);
+		return e;
+	}
+	if (r->tbl != NULL && (ctl == 0 || buf->buf[pos] == '\0')) {
+		tbl_read(r->tbl, ln, buf->buf, ppos);
+		roff_addtbl(r->man, ln, r->tbl);
+		return e;
+	}
+	if ( ! ctl) {
+		r->options &= ~MPARSE_COMMENT;
+		return roff_parsetext(r, buf, pos, offs) | e;
+	}
+
+	/* Skip empty request lines. */
+
+	if (buf->buf[pos] == '"') {
+		mandoc_msg(MANDOCERR_COMMENT_BAD, ln, pos, NULL);
+		return ROFF_IGN;
+	} else if (buf->buf[pos] == '\0')
+		return ROFF_IGN;
+
+	/*
+	 * If a scope is open, go to the child handler for that macro,
+	 * as it may want to preprocess before doing anything with it.
+	 * Don't do so if an equation is open.
+	 */
+
+	if (r->last) {
+		t = r->last->tok;
+		return (*roffs[t].sub)(r, t, buf, ln, ppos, pos, offs);
+	}
+
+	/* No scope is open.  This is a new request or macro. */
+
+	r->options &= ~MPARSE_COMMENT;
+	spos = pos;
+	t = roff_parse(r, buf->buf, &pos, ln, ppos);
+
+	/* Tables ignore most macros. */
+
+	if (r->tbl != NULL && (t == TOKEN_NONE || t == ROFF_TS ||
+	    t == ROFF_br || t == ROFF_ce || t == ROFF_rj || t == ROFF_sp)) {
+		mandoc_msg(MANDOCERR_TBLMACRO,
+		    ln, pos, "%s", buf->buf + spos);
+		if (t != TOKEN_NONE)
+			return ROFF_IGN;
+		while (buf->buf[pos] != '\0' && buf->buf[pos] != ' ')
+			pos++;
+		while (buf->buf[pos] == ' ')
+			pos++;
+		tbl_read(r->tbl, ln, buf->buf, pos);
+		roff_addtbl(r->man, ln, r->tbl);
+		return ROFF_IGN;
+	}
+
+	/* For now, let high level macros abort .ce mode. */
+
+	if (ctl && roffce_node != NULL &&
+	    (t == TOKEN_NONE || t == ROFF_Dd || t == ROFF_EQ ||
+	     t == ROFF_TH || t == ROFF_TS)) {
+		r->man->last = roffce_node;
+		r->man->next = ROFF_NEXT_SIBLING;
+		roffce_lines = 0;
+		roffce_node = NULL;
+	}
+
+	/*
+	 * This is neither a roff request nor a user-defined macro.
+	 * Let the standard macro set parsers handle it.
+	 */
+
+	if (t == TOKEN_NONE)
+		return ROFF_CONT;
+
+	/* Execute a roff request or a user defined macro. */
+
+	return (*roffs[t].proc)(r, t, buf, ln, spos, pos, offs);
+}
+
+/*
+ * Internal interface function to tell the roff parser that execution
+ * of the current macro ended.  This is required because macro
+ * definitions usually do not end with a .return request.
+ */
+void
+roff_userret(struct roff *r)
+{
+	struct mctx	*ctx;
+	int		 i;
+
+	assert(r->mstackpos >= 0);
+	ctx = r->mstack + r->mstackpos;
+	for (i = 0; i < ctx->argc; i++)
+		free(ctx->argv[i]);
+	ctx->argc = 0;
+	r->mstackpos--;
+}
+
+void
+roff_endparse(struct roff *r)
+{
+	if (r->last != NULL)
+		mandoc_msg(MANDOCERR_BLK_NOEND, r->last->line,
+		    r->last->col, "%s", roff_name[r->last->tok]);
+
+	if (r->eqn != NULL) {
+		mandoc_msg(MANDOCERR_BLK_NOEND,
+		    r->eqn->node->line, r->eqn->node->pos, "EQ");
+		eqn_parse(r->eqn);
+		r->eqn = NULL;
+	}
+
+	if (r->tbl != NULL) {
+		tbl_end(r->tbl, 1);
+		r->tbl = NULL;
+	}
+}
+
+/*
+ * Parse a roff node's type from the input buffer.  This must be in the
+ * form of ".foo xxx" in the usual way.
+ */
+static enum roff_tok
+roff_parse(struct roff *r, char *buf, int *pos, int ln, int ppos)
+{
+	char		*cp;
+	const char	*mac;
+	size_t		 maclen;
+	int		 deftype;
+	enum roff_tok	 t;
+
+	cp = buf + *pos;
+
+	if ('\0' == *cp || '"' == *cp || '\t' == *cp || ' ' == *cp)
+		return TOKEN_NONE;
+
+	mac = cp;
+	maclen = roff_getname(r, &cp, ln, ppos);
+
+	deftype = ROFFDEF_USER | ROFFDEF_REN;
+	r->current_string = roff_getstrn(r, mac, maclen, &deftype);
+	switch (deftype) {
+	case ROFFDEF_USER:
+		t = ROFF_USERDEF;
+		break;
+	case ROFFDEF_REN:
+		t = ROFF_RENAMED;
+		break;
+	default:
+		t = roffhash_find(r->reqtab, mac, maclen);
+		break;
+	}
+	if (t != TOKEN_NONE)
+		*pos = cp - buf;
+	else if (deftype == ROFFDEF_UNDEF) {
+		/* Using an undefined macro defines it to be empty. */
+		roff_setstrn(&r->strtab, mac, maclen, "", 0, 0);
+		roff_setstrn(&r->rentab, mac, maclen, NULL, 0, 0);
+	}
+	return t;
+}
+
+/* --- handling of request blocks ----------------------------------------- */
+
+static int
+roff_cblock(ROFF_ARGS)
+{
+
+	/*
+	 * A block-close `..' should only be invoked as a child of an
+	 * ignore macro, otherwise raise a warning and just ignore it.
+	 */
+
+	if (r->last == NULL) {
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN, ln, ppos, "..");
+		return ROFF_IGN;
+	}
+
+	switch (r->last->tok) {
+	case ROFF_am:
+		/* ROFF_am1 is remapped to ROFF_am in roff_block(). */
+	case ROFF_ami:
+	case ROFF_de:
+		/* ROFF_de1 is remapped to ROFF_de in roff_block(). */
+	case ROFF_dei:
+	case ROFF_ig:
+		break;
+	default:
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN, ln, ppos, "..");
+		return ROFF_IGN;
+	}
+
+	if (buf->buf[pos] != '\0')
+		mandoc_msg(MANDOCERR_ARG_SKIP, ln, pos,
+		    ".. %s", buf->buf + pos);
+
+	roffnode_pop(r);
+	roffnode_cleanscope(r);
+	return ROFF_IGN;
+
+}
+
+/*
+ * Pop all nodes ending at the end of the current input line.
+ * Return the number of loops ended.
+ */
+static int
+roffnode_cleanscope(struct roff *r)
+{
+	int inloop;
+
+	inloop = 0;
+	while (r->last != NULL) {
+		if (--r->last->endspan != 0)
+			break;
+		inloop += roffnode_pop(r);
+	}
+	return inloop;
+}
+
+/*
+ * Handle the closing \} of a conditional block.
+ * Apart from generating warnings, this only pops nodes.
+ * Return the number of loops ended.
+ */
+static int
+roff_ccond(struct roff *r, int ln, int ppos)
+{
+	if (NULL == r->last) {
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN, ln, ppos, "\\}");
+		return 0;
+	}
+
+	switch (r->last->tok) {
+	case ROFF_el:
+	case ROFF_ie:
+	case ROFF_if:
+	case ROFF_while:
+		break;
+	default:
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN, ln, ppos, "\\}");
+		return 0;
+	}
+
+	if (r->last->endspan > -1) {
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN, ln, ppos, "\\}");
+		return 0;
+	}
+
+	return roffnode_pop(r) + roffnode_cleanscope(r);
+}
+
+static int
+roff_block(ROFF_ARGS)
+{
+	const char	*name, *value;
+	char		*call, *cp, *iname, *rname;
+	size_t		 csz, namesz, rsz;
+	int		 deftype;
+
+	/* Ignore groff compatibility mode for now. */
+
+	if (tok == ROFF_de1)
+		tok = ROFF_de;
+	else if (tok == ROFF_dei1)
+		tok = ROFF_dei;
+	else if (tok == ROFF_am1)
+		tok = ROFF_am;
+	else if (tok == ROFF_ami1)
+		tok = ROFF_ami;
+
+	/* Parse the macro name argument. */
+
+	cp = buf->buf + pos;
+	if (tok == ROFF_ig) {
+		iname = NULL;
+		namesz = 0;
+	} else {
+		iname = cp;
+		namesz = roff_getname(r, &cp, ln, ppos);
+		iname[namesz] = '\0';
+	}
+
+	/* Resolve the macro name argument if it is indirect. */
+
+	if (namesz && (tok == ROFF_dei || tok == ROFF_ami)) {
+		deftype = ROFFDEF_USER;
+		name = roff_getstrn(r, iname, namesz, &deftype);
+		if (name == NULL) {
+			mandoc_msg(MANDOCERR_STR_UNDEF,
+			    ln, (int)(iname - buf->buf),
+			    "%.*s", (int)namesz, iname);
+			namesz = 0;
+		} else
+			namesz = strlen(name);
+	} else
+		name = iname;
+
+	if (namesz == 0 && tok != ROFF_ig) {
+		mandoc_msg(MANDOCERR_REQ_EMPTY,
+		    ln, ppos, "%s", roff_name[tok]);
+		return ROFF_IGN;
+	}
+
+	roffnode_push(r, tok, name, ln, ppos);
+
+	/*
+	 * At the beginning of a `de' macro, clear the existing string
+	 * with the same name, if there is one.  New content will be
+	 * appended from roff_block_text() in multiline mode.
+	 */
+
+	if (tok == ROFF_de || tok == ROFF_dei) {
+		roff_setstrn(&r->strtab, name, namesz, "", 0, 0);
+		roff_setstrn(&r->rentab, name, namesz, NULL, 0, 0);
+	} else if (tok == ROFF_am || tok == ROFF_ami) {
+		deftype = ROFFDEF_ANY;
+		value = roff_getstrn(r, iname, namesz, &deftype);
+		switch (deftype) {  /* Before appending, ... */
+		case ROFFDEF_PRE: /* copy predefined to user-defined. */
+			roff_setstrn(&r->strtab, name, namesz,
+			    value, strlen(value), 0);
+			break;
+		case ROFFDEF_REN: /* call original standard macro. */
+			csz = mandoc_asprintf(&call, ".%.*s \\$* \\\"\n",
+			    (int)strlen(value), value);
+			roff_setstrn(&r->strtab, name, namesz, call, csz, 0);
+			roff_setstrn(&r->rentab, name, namesz, NULL, 0, 0);
+			free(call);
+			break;
+		case ROFFDEF_STD:  /* rename and call standard macro. */
+			rsz = mandoc_asprintf(&rname, "__%s_renamed", name);
+			roff_setstrn(&r->rentab, rname, rsz, name, namesz, 0);
+			csz = mandoc_asprintf(&call, ".%.*s \\$* \\\"\n",
+			    (int)rsz, rname);
+			roff_setstrn(&r->strtab, name, namesz, call, csz, 0);
+			free(call);
+			free(rname);
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (*cp == '\0')
+		return ROFF_IGN;
+
+	/* Get the custom end marker. */
+
+	iname = cp;
+	namesz = roff_getname(r, &cp, ln, ppos);
+
+	/* Resolve the end marker if it is indirect. */
+
+	if (namesz && (tok == ROFF_dei || tok == ROFF_ami)) {
+		deftype = ROFFDEF_USER;
+		name = roff_getstrn(r, iname, namesz, &deftype);
+		if (name == NULL) {
+			mandoc_msg(MANDOCERR_STR_UNDEF,
+			    ln, (int)(iname - buf->buf),
+			    "%.*s", (int)namesz, iname);
+			namesz = 0;
+		} else
+			namesz = strlen(name);
+	} else
+		name = iname;
+
+	if (namesz)
+		r->last->end = mandoc_strndup(name, namesz);
+
+	if (*cp != '\0')
+		mandoc_msg(MANDOCERR_ARG_EXCESS,
+		    ln, pos, ".%s ... %s", roff_name[tok], cp);
+
+	return ROFF_IGN;
+}
+
+static int
+roff_block_sub(ROFF_ARGS)
+{
+	enum roff_tok	t;
+	int		i, j;
+
+	/*
+	 * First check whether a custom macro exists at this level.  If
+	 * it does, then check against it.  This is some of groff's
+	 * stranger behaviours.  If we encountered a custom end-scope
+	 * tag and that tag also happens to be a "real" macro, then we
+	 * need to try interpreting it again as a real macro.  If it's
+	 * not, then return ignore.  Else continue.
+	 */
+
+	if (r->last->end) {
+		for (i = pos, j = 0; r->last->end[j]; j++, i++)
+			if (buf->buf[i] != r->last->end[j])
+				break;
+
+		if (r->last->end[j] == '\0' &&
+		    (buf->buf[i] == '\0' ||
+		     buf->buf[i] == ' ' ||
+		     buf->buf[i] == '\t')) {
+			roffnode_pop(r);
+			roffnode_cleanscope(r);
+
+			while (buf->buf[i] == ' ' || buf->buf[i] == '\t')
+				i++;
+
+			pos = i;
+			if (roff_parse(r, buf->buf, &pos, ln, ppos) !=
+			    TOKEN_NONE)
+				return ROFF_RERUN;
+			return ROFF_IGN;
+		}
+	}
+
+	/*
+	 * If we have no custom end-query or lookup failed, then try
+	 * pulling it out of the hashtable.
+	 */
+
+	t = roff_parse(r, buf->buf, &pos, ln, ppos);
+
+	if (t != ROFF_cblock) {
+		if (tok != ROFF_ig)
+			roff_setstr(r, r->last->name, buf->buf + ppos, 2);
+		return ROFF_IGN;
+	}
+
+	return (*roffs[t].proc)(r, t, buf, ln, ppos, pos, offs);
+}
+
+static int
+roff_block_text(ROFF_ARGS)
+{
+
+	if (tok != ROFF_ig)
+		roff_setstr(r, r->last->name, buf->buf + pos, 2);
+
+	return ROFF_IGN;
+}
+
+static int
+roff_cond_sub(ROFF_ARGS)
+{
+	struct roffnode	*bl;
+	char		*ep;
+	int		 endloop, irc, rr;
+	enum roff_tok	 t;
+
+	irc = ROFF_IGN;
+	rr = r->last->rule;
+	endloop = tok != ROFF_while ? ROFF_IGN :
+	    rr ? ROFF_LOOPCONT : ROFF_LOOPEXIT;
+	if (roffnode_cleanscope(r))
+		irc |= endloop;
+
+	/*
+	 * If `\}' occurs on a macro line without a preceding macro,
+	 * drop the line completely.
+	 */
+
+	ep = buf->buf + pos;
+	if (ep[0] == '\\' && ep[1] == '}')
+		rr = 0;
+
+	/*
+	 * The closing delimiter `\}' rewinds the conditional scope
+	 * but is otherwise ignored when interpreting the line.
+	 */
+
+	while ((ep = strchr(ep, '\\')) != NULL) {
+		switch (ep[1]) {
+		case '}':
+			memmove(ep, ep + 2, strlen(ep + 2) + 1);
+			if (roff_ccond(r, ln, ep - buf->buf))
+				irc |= endloop;
+			break;
+		case '\0':
+			++ep;
+			break;
+		default:
+			ep += 2;
+			break;
+		}
+	}
+
+	t = roff_parse(r, buf->buf, &pos, ln, ppos);
+
+	/* For now, let high level macros abort .ce mode. */
+
+	if (roffce_node != NULL &&
+	    (t == TOKEN_NONE || t == ROFF_Dd || t == ROFF_EQ ||
+             t == ROFF_TH || t == ROFF_TS)) {
+		r->man->last = roffce_node;
+		r->man->next = ROFF_NEXT_SIBLING;
+		roffce_lines = 0;
+		roffce_node = NULL;
+	}
+
+	/*
+	 * Fully handle known macros when they are structurally
+	 * required or when the conditional evaluated to true.
+	 */
+
+	if (t == ROFF_break) {
+		if (irc & ROFF_LOOPMASK)
+			irc = ROFF_IGN | ROFF_LOOPEXIT;
+		else if (rr) {
+			for (bl = r->last; bl != NULL; bl = bl->parent) {
+				bl->rule = 0;
+				if (bl->tok == ROFF_while)
+					break;
+			}
+		}
+	} else if (t != TOKEN_NONE &&
+	    (rr || roffs[t].flags & ROFFMAC_STRUCT))
+		irc |= (*roffs[t].proc)(r, t, buf, ln, ppos, pos, offs);
+	else
+		irc |= rr ? ROFF_CONT : ROFF_IGN;
+	return irc;
+}
+
+static int
+roff_cond_text(ROFF_ARGS)
+{
+	char		*ep;
+	int		 endloop, irc, rr;
+
+	irc = ROFF_IGN;
+	rr = r->last->rule;
+	endloop = tok != ROFF_while ? ROFF_IGN :
+	    rr ? ROFF_LOOPCONT : ROFF_LOOPEXIT;
+	if (roffnode_cleanscope(r))
+		irc |= endloop;
+
+	/*
+	 * If `\}' occurs on a text line with neither preceding
+	 * nor following characters, drop the line completely.
+	 */
+
+	ep = buf->buf + pos;
+	if (strcmp(ep, "\\}") == 0)
+		rr = 0;
+
+	/*
+	 * The closing delimiter `\}' rewinds the conditional scope
+	 * but is otherwise ignored when interpreting the line.
+	 */
+
+	while ((ep = strchr(ep, '\\')) != NULL) {
+		switch (ep[1]) {
+		case '}':
+			memmove(ep, ep + 2, strlen(ep + 2) + 1);
+			if (roff_ccond(r, ln, ep - buf->buf))
+				irc |= endloop;
+			break;
+		case '\0':
+			++ep;
+			break;
+		default:
+			ep += 2;
+			break;
+		}
+	}
+	if (rr)
+		irc |= ROFF_CONT;
+	return irc;
+}
+
+/* --- handling of numeric and conditional expressions -------------------- */
+
+/*
+ * Parse a single signed integer number.  Stop at the first non-digit.
+ * If there is at least one digit, return success and advance the
+ * parse point, else return failure and let the parse point unchanged.
+ * Ignore overflows, treat them just like the C language.
+ */
+static int
+roff_getnum(const char *v, int *pos, int *res, int flags)
+{
+	int	 myres, scaled, n, p;
+
+	if (NULL == res)
+		res = &myres;
+
+	p = *pos;
+	n = v[p] == '-';
+	if (n || v[p] == '+')
+		p++;
+
+	if (flags & ROFFNUM_WHITE)
+		while (isspace((unsigned char)v[p]))
+			p++;
+
+	for (*res = 0; isdigit((unsigned char)v[p]); p++)
+		*res = 10 * *res + v[p] - '0';
+	if (p == *pos + n)
+		return 0;
+
+	if (n)
+		*res = -*res;
+
+	/* Each number may be followed by one optional scaling unit. */
+
+	switch (v[p]) {
+	case 'f':
+		scaled = *res * 65536;
+		break;
+	case 'i':
+		scaled = *res * 240;
+		break;
+	case 'c':
+		scaled = *res * 240 / 2.54;
+		break;
+	case 'v':
+	case 'P':
+		scaled = *res * 40;
+		break;
+	case 'm':
+	case 'n':
+		scaled = *res * 24;
+		break;
+	case 'p':
+		scaled = *res * 10 / 3;
+		break;
+	case 'u':
+		scaled = *res;
+		break;
+	case 'M':
+		scaled = *res * 6 / 25;
+		break;
+	default:
+		scaled = *res;
+		p--;
+		break;
+	}
+	if (flags & ROFFNUM_SCALE)
+		*res = scaled;
+
+	*pos = p + 1;
+	return 1;
+}
+
+/*
+ * Evaluate a string comparison condition.
+ * The first character is the delimiter.
+ * Succeed if the string up to its second occurrence
+ * matches the string up to its third occurence.
+ * Advance the cursor after the third occurrence
+ * or lacking that, to the end of the line.
+ */
+static int
+roff_evalstrcond(const char *v, int *pos)
+{
+	const char	*s1, *s2, *s3;
+	int		 match;
+
+	match = 0;
+	s1 = v + *pos;		/* initial delimiter */
+	s2 = s1 + 1;		/* for scanning the first string */
+	s3 = strchr(s2, *s1);	/* for scanning the second string */
+
+	if (NULL == s3)		/* found no middle delimiter */
+		goto out;
+
+	while ('\0' != *++s3) {
+		if (*s2 != *s3) {  /* mismatch */
+			s3 = strchr(s3, *s1);
+			break;
+		}
+		if (*s3 == *s1) {  /* found the final delimiter */
+			match = 1;
+			break;
+		}
+		s2++;
+	}
+
+out:
+	if (NULL == s3)
+		s3 = strchr(s2, '\0');
+	else if (*s3 != '\0')
+		s3++;
+	*pos = s3 - v;
+	return match;
+}
+
+/*
+ * Evaluate an optionally negated single character, numerical,
+ * or string condition.
+ */
+static int
+roff_evalcond(struct roff *r, int ln, char *v, int *pos)
+{
+	const char	*start, *end;
+	char		*cp, *name;
+	size_t		 sz;
+	int		 deftype, len, number, savepos, istrue, wanttrue;
+
+	if ('!' == v[*pos]) {
+		wanttrue = 0;
+		(*pos)++;
+	} else
+		wanttrue = 1;
+
+	switch (v[*pos]) {
+	case '\0':
+		return 0;
+	case 'n':
+	case 'o':
+		(*pos)++;
+		return wanttrue;
+	case 'e':
+	case 't':
+	case 'v':
+		(*pos)++;
+		return !wanttrue;
+	case 'c':
+		do {
+			(*pos)++;
+		} while (v[*pos] == ' ');
+
+		/*
+		 * Quirk for groff compatibility:
+		 * The horizontal tab is neither available nor unavailable.
+		 */
+
+		if (v[*pos] == '\t') {
+			(*pos)++;
+			return 0;
+		}
+
+		/* Printable ASCII characters are available. */
+
+		if (v[*pos] != '\\') {
+			(*pos)++;
+			return wanttrue;
+		}
+
+		end = v + ++*pos;
+		switch (mandoc_escape(&end, &start, &len)) {
+		case ESCAPE_SPECIAL:
+			istrue = mchars_spec2cp(start, len) != -1;
+			break;
+		case ESCAPE_UNICODE:
+			istrue = 1;
+			break;
+		case ESCAPE_NUMBERED:
+			istrue = mchars_num2char(start, len) != -1;
+			break;
+		default:
+			istrue = !wanttrue;
+			break;
+		}
+		*pos = end - v;
+		return istrue == wanttrue;
+	case 'd':
+	case 'r':
+		cp = v + *pos + 1;
+		while (*cp == ' ')
+			cp++;
+		name = cp;
+		sz = roff_getname(r, &cp, ln, cp - v);
+		if (sz == 0)
+			istrue = 0;
+		else if (v[*pos] == 'r')
+			istrue = roff_hasregn(r, name, sz);
+		else {
+			deftype = ROFFDEF_ANY;
+		        roff_getstrn(r, name, sz, &deftype);
+			istrue = !!deftype;
+		}
+		*pos = (name + sz) - v;
+		return istrue == wanttrue;
+	default:
+		break;
+	}
+
+	savepos = *pos;
+	if (roff_evalnum(r, ln, v, pos, &number, ROFFNUM_SCALE))
+		return (number > 0) == wanttrue;
+	else if (*pos == savepos)
+		return roff_evalstrcond(v, pos) == wanttrue;
+	else
+		return 0;
+}
+
+static int
+roff_line_ignore(ROFF_ARGS)
+{
+
+	return ROFF_IGN;
+}
+
+static int
+roff_insec(ROFF_ARGS)
+{
+
+	mandoc_msg(MANDOCERR_REQ_INSEC, ln, ppos, "%s", roff_name[tok]);
+	return ROFF_IGN;
+}
+
+static int
+roff_unsupp(ROFF_ARGS)
+{
+
+	mandoc_msg(MANDOCERR_REQ_UNSUPP, ln, ppos, "%s", roff_name[tok]);
+	return ROFF_IGN;
+}
+
+static int
+roff_cond(ROFF_ARGS)
+{
+	int	 irc;
+
+	roffnode_push(r, tok, NULL, ln, ppos);
+
+	/*
+	 * An `.el' has no conditional body: it will consume the value
+	 * of the current rstack entry set in prior `ie' calls or
+	 * defaults to DENY.
+	 *
+	 * If we're not an `el', however, then evaluate the conditional.
+	 */
+
+	r->last->rule = tok == ROFF_el ?
+	    (r->rstackpos < 0 ? 0 : r->rstack[r->rstackpos--]) :
+	    roff_evalcond(r, ln, buf->buf, &pos);
+
+	/*
+	 * An if-else will put the NEGATION of the current evaluated
+	 * conditional into the stack of rules.
+	 */
+
+	if (tok == ROFF_ie) {
+		if (r->rstackpos + 1 == r->rstacksz) {
+			r->rstacksz += 16;
+			r->rstack = mandoc_reallocarray(r->rstack,
+			    r->rstacksz, sizeof(int));
+		}
+		r->rstack[++r->rstackpos] = !r->last->rule;
+	}
+
+	/* If the parent has false as its rule, then so do we. */
+
+	if (r->last->parent && !r->last->parent->rule)
+		r->last->rule = 0;
+
+	/*
+	 * Determine scope.
+	 * If there is nothing on the line after the conditional,
+	 * not even whitespace, use next-line scope.
+	 * Except that .while does not support next-line scope.
+	 */
+
+	if (buf->buf[pos] == '\0' && tok != ROFF_while) {
+		r->last->endspan = 2;
+		goto out;
+	}
+
+	while (buf->buf[pos] == ' ')
+		pos++;
+
+	/* An opening brace requests multiline scope. */
+
+	if (buf->buf[pos] == '\\' && buf->buf[pos + 1] == '{') {
+		r->last->endspan = -1;
+		pos += 2;
+		while (buf->buf[pos] == ' ')
+			pos++;
+		goto out;
+	}
+
+	/*
+	 * Anything else following the conditional causes
+	 * single-line scope.  Warn if the scope contains
+	 * nothing but trailing whitespace.
+	 */
+
+	if (buf->buf[pos] == '\0')
+		mandoc_msg(MANDOCERR_COND_EMPTY,
+		    ln, ppos, "%s", roff_name[tok]);
+
+	r->last->endspan = 1;
+
+out:
+	*offs = pos;
+	irc = ROFF_RERUN;
+	if (tok == ROFF_while)
+		irc |= ROFF_WHILE;
+	return irc;
+}
+
+static int
+roff_ds(ROFF_ARGS)
+{
+	char		*string;
+	const char	*name;
+	size_t		 namesz;
+
+	/* Ignore groff compatibility mode for now. */
+
+	if (tok == ROFF_ds1)
+		tok = ROFF_ds;
+	else if (tok == ROFF_as1)
+		tok = ROFF_as;
+
+	/*
+	 * The first word is the name of the string.
+	 * If it is empty or terminated by an escape sequence,
+	 * abort the `ds' request without defining anything.
+	 */
+
+	name = string = buf->buf + pos;
+	if (*name == '\0')
+		return ROFF_IGN;
+
+	namesz = roff_getname(r, &string, ln, pos);
+	switch (name[namesz]) {
+	case '\\':
+		return ROFF_IGN;
+	case '\t':
+		string = buf->buf + pos + namesz;
+		break;
+	default:
+		break;
+	}
+
+	/* Read past the initial double-quote, if any. */
+	if (*string == '"')
+		string++;
+
+	/* The rest is the value. */
+	roff_setstrn(&r->strtab, name, namesz, string, strlen(string),
+	    ROFF_as == tok);
+	roff_setstrn(&r->rentab, name, namesz, NULL, 0, 0);
+	return ROFF_IGN;
+}
+
+/*
+ * Parse a single operator, one or two characters long.
+ * If the operator is recognized, return success and advance the
+ * parse point, else return failure and let the parse point unchanged.
+ */
+static int
+roff_getop(const char *v, int *pos, char *res)
+{
+
+	*res = v[*pos];
+
+	switch (*res) {
+	case '+':
+	case '-':
+	case '*':
+	case '/':
+	case '%':
+	case '&':
+	case ':':
+		break;
+	case '<':
+		switch (v[*pos + 1]) {
+		case '=':
+			*res = 'l';
+			(*pos)++;
+			break;
+		case '>':
+			*res = '!';
+			(*pos)++;
+			break;
+		case '?':
+			*res = 'i';
+			(*pos)++;
+			break;
+		default:
+			break;
+		}
+		break;
+	case '>':
+		switch (v[*pos + 1]) {
+		case '=':
+			*res = 'g';
+			(*pos)++;
+			break;
+		case '?':
+			*res = 'a';
+			(*pos)++;
+			break;
+		default:
+			break;
+		}
+		break;
+	case '=':
+		if ('=' == v[*pos + 1])
+			(*pos)++;
+		break;
+	default:
+		return 0;
+	}
+	(*pos)++;
+
+	return *res;
+}
+
+/*
+ * Evaluate either a parenthesized numeric expression
+ * or a single signed integer number.
+ */
+static int
+roff_evalpar(struct roff *r, int ln,
+	const char *v, int *pos, int *res, int flags)
+{
+
+	if ('(' != v[*pos])
+		return roff_getnum(v, pos, res, flags);
+
+	(*pos)++;
+	if ( ! roff_evalnum(r, ln, v, pos, res, flags | ROFFNUM_WHITE))
+		return 0;
+
+	/*
+	 * Omission of the closing parenthesis
+	 * is an error in validation mode,
+	 * but ignored in evaluation mode.
+	 */
+
+	if (')' == v[*pos])
+		(*pos)++;
+	else if (NULL == res)
+		return 0;
+
+	return 1;
+}
+
+/*
+ * Evaluate a complete numeric expression.
+ * Proceed left to right, there is no concept of precedence.
+ */
+static int
+roff_evalnum(struct roff *r, int ln, const char *v,
+	int *pos, int *res, int flags)
+{
+	int		 mypos, operand2;
+	char		 operator;
+
+	if (NULL == pos) {
+		mypos = 0;
+		pos = &mypos;
+	}
+
+	if (flags & ROFFNUM_WHITE)
+		while (isspace((unsigned char)v[*pos]))
+			(*pos)++;
+
+	if ( ! roff_evalpar(r, ln, v, pos, res, flags))
+		return 0;
+
+	while (1) {
+		if (flags & ROFFNUM_WHITE)
+			while (isspace((unsigned char)v[*pos]))
+				(*pos)++;
+
+		if ( ! roff_getop(v, pos, &operator))
+			break;
+
+		if (flags & ROFFNUM_WHITE)
+			while (isspace((unsigned char)v[*pos]))
+				(*pos)++;
+
+		if ( ! roff_evalpar(r, ln, v, pos, &operand2, flags))
+			return 0;
+
+		if (flags & ROFFNUM_WHITE)
+			while (isspace((unsigned char)v[*pos]))
+				(*pos)++;
+
+		if (NULL == res)
+			continue;
+
+		switch (operator) {
+		case '+':
+			*res += operand2;
+			break;
+		case '-':
+			*res -= operand2;
+			break;
+		case '*':
+			*res *= operand2;
+			break;
+		case '/':
+			if (operand2 == 0) {
+				mandoc_msg(MANDOCERR_DIVZERO,
+					ln, *pos, "%s", v);
+				*res = 0;
+				break;
+			}
+			*res /= operand2;
+			break;
+		case '%':
+			if (operand2 == 0) {
+				mandoc_msg(MANDOCERR_DIVZERO,
+					ln, *pos, "%s", v);
+				*res = 0;
+				break;
+			}
+			*res %= operand2;
+			break;
+		case '<':
+			*res = *res < operand2;
+			break;
+		case '>':
+			*res = *res > operand2;
+			break;
+		case 'l':
+			*res = *res <= operand2;
+			break;
+		case 'g':
+			*res = *res >= operand2;
+			break;
+		case '=':
+			*res = *res == operand2;
+			break;
+		case '!':
+			*res = *res != operand2;
+			break;
+		case '&':
+			*res = *res && operand2;
+			break;
+		case ':':
+			*res = *res || operand2;
+			break;
+		case 'i':
+			if (operand2 < *res)
+				*res = operand2;
+			break;
+		case 'a':
+			if (operand2 > *res)
+				*res = operand2;
+			break;
+		default:
+			abort();
+		}
+	}
+	return 1;
+}
+
+/* --- register management ------------------------------------------------ */
+
+void
+roff_setreg(struct roff *r, const char *name, int val, char sign)
+{
+	roff_setregn(r, name, strlen(name), val, sign, INT_MIN);
+}
+
+static void
+roff_setregn(struct roff *r, const char *name, size_t len,
+    int val, char sign, int step)
+{
+	struct roffreg	*reg;
+
+	/* Search for an existing register with the same name. */
+	reg = r->regtab;
+
+	while (reg != NULL && (reg->key.sz != len ||
+	    strncmp(reg->key.p, name, len) != 0))
+		reg = reg->next;
+
+	if (NULL == reg) {
+		/* Create a new register. */
+		reg = mandoc_malloc(sizeof(struct roffreg));
+		reg->key.p = mandoc_strndup(name, len);
+		reg->key.sz = len;
+		reg->val = 0;
+		reg->step = 0;
+		reg->next = r->regtab;
+		r->regtab = reg;
+	}
+
+	if ('+' == sign)
+		reg->val += val;
+	else if ('-' == sign)
+		reg->val -= val;
+	else
+		reg->val = val;
+	if (step != INT_MIN)
+		reg->step = step;
+}
+
+/*
+ * Handle some predefined read-only number registers.
+ * For now, return -1 if the requested register is not predefined;
+ * in case a predefined read-only register having the value -1
+ * were to turn up, another special value would have to be chosen.
+ */
+static int
+roff_getregro(const struct roff *r, const char *name)
+{
+
+	switch (*name) {
+	case '$':  /* Number of arguments of the last macro evaluated. */
+		return r->mstackpos < 0 ? 0 : r->mstack[r->mstackpos].argc;
+	case 'A':  /* ASCII approximation mode is always off. */
+		return 0;
+	case 'g':  /* Groff compatibility mode is always on. */
+		return 1;
+	case 'H':  /* Fixed horizontal resolution. */
+		return 24;
+	case 'j':  /* Always adjust left margin only. */
+		return 0;
+	case 'T':  /* Some output device is always defined. */
+		return 1;
+	case 'V':  /* Fixed vertical resolution. */
+		return 40;
+	default:
+		return -1;
+	}
+}
+
+int
+roff_getreg(struct roff *r, const char *name)
+{
+	return roff_getregn(r, name, strlen(name), '\0');
+}
+
+static int
+roff_getregn(struct roff *r, const char *name, size_t len, char sign)
+{
+	struct roffreg	*reg;
+	int		 val;
+
+	if ('.' == name[0] && 2 == len) {
+		val = roff_getregro(r, name + 1);
+		if (-1 != val)
+			return val;
+	}
+
+	for (reg = r->regtab; reg; reg = reg->next) {
+		if (len == reg->key.sz &&
+		    0 == strncmp(name, reg->key.p, len)) {
+			switch (sign) {
+			case '+':
+				reg->val += reg->step;
+				break;
+			case '-':
+				reg->val -= reg->step;
+				break;
+			default:
+				break;
+			}
+			return reg->val;
+		}
+	}
+
+	roff_setregn(r, name, len, 0, '\0', INT_MIN);
+	return 0;
+}
+
+static int
+roff_hasregn(const struct roff *r, const char *name, size_t len)
+{
+	struct roffreg	*reg;
+	int		 val;
+
+	if ('.' == name[0] && 2 == len) {
+		val = roff_getregro(r, name + 1);
+		if (-1 != val)
+			return 1;
+	}
+
+	for (reg = r->regtab; reg; reg = reg->next)
+		if (len == reg->key.sz &&
+		    0 == strncmp(name, reg->key.p, len))
+			return 1;
+
+	return 0;
+}
+
+static void
+roff_freereg(struct roffreg *reg)
+{
+	struct roffreg	*old_reg;
+
+	while (NULL != reg) {
+		free(reg->key.p);
+		old_reg = reg;
+		reg = reg->next;
+		free(old_reg);
+	}
+}
+
+static int
+roff_nr(ROFF_ARGS)
+{
+	char		*key, *val, *step;
+	size_t		 keysz;
+	int		 iv, is, len;
+	char		 sign;
+
+	key = val = buf->buf + pos;
+	if (*key == '\0')
+		return ROFF_IGN;
+
+	keysz = roff_getname(r, &val, ln, pos);
+	if (key[keysz] == '\\' || key[keysz] == '\t')
+		return ROFF_IGN;
+
+	sign = *val;
+	if (sign == '+' || sign == '-')
+		val++;
+
+	len = 0;
+	if (roff_evalnum(r, ln, val, &len, &iv, ROFFNUM_SCALE) == 0)
+		return ROFF_IGN;
+
+	step = val + len;
+	while (isspace((unsigned char)*step))
+		step++;
+	if (roff_evalnum(r, ln, step, NULL, &is, 0) == 0)
+		is = INT_MIN;
+
+	roff_setregn(r, key, keysz, iv, sign, is);
+	return ROFF_IGN;
+}
+
+static int
+roff_rr(ROFF_ARGS)
+{
+	struct roffreg	*reg, **prev;
+	char		*name, *cp;
+	size_t		 namesz;
+
+	name = cp = buf->buf + pos;
+	if (*name == '\0')
+		return ROFF_IGN;
+	namesz = roff_getname(r, &cp, ln, pos);
+	name[namesz] = '\0';
+
+	prev = &r->regtab;
+	while (1) {
+		reg = *prev;
+		if (reg == NULL || !strcmp(name, reg->key.p))
+			break;
+		prev = ®->next;
+	}
+	if (reg != NULL) {
+		*prev = reg->next;
+		free(reg->key.p);
+		free(reg);
+	}
+	return ROFF_IGN;
+}
+
+/* --- handler functions for roff requests -------------------------------- */
+
+static int
+roff_rm(ROFF_ARGS)
+{
+	const char	 *name;
+	char		 *cp;
+	size_t		  namesz;
+
+	cp = buf->buf + pos;
+	while (*cp != '\0') {
+		name = cp;
+		namesz = roff_getname(r, &cp, ln, (int)(cp - buf->buf));
+		roff_setstrn(&r->strtab, name, namesz, NULL, 0, 0);
+		roff_setstrn(&r->rentab, name, namesz, NULL, 0, 0);
+		if (name[namesz] == '\\' || name[namesz] == '\t')
+			break;
+	}
+	return ROFF_IGN;
+}
+
+static int
+roff_it(ROFF_ARGS)
+{
+	int		 iv;
+
+	/* Parse the number of lines. */
+
+	if ( ! roff_evalnum(r, ln, buf->buf, &pos, &iv, 0)) {
+		mandoc_msg(MANDOCERR_IT_NONUM,
+		    ln, ppos, "%s", buf->buf + 1);
+		return ROFF_IGN;
+	}
+
+	while (isspace((unsigned char)buf->buf[pos]))
+		pos++;
+
+	/*
+	 * Arm the input line trap.
+	 * Special-casing "an-trap" is an ugly workaround to cope
+	 * with DocBook stupidly fiddling with man(7) internals.
+	 */
+
+	roffit_lines = iv;
+	roffit_macro = mandoc_strdup(iv != 1 ||
+	    strcmp(buf->buf + pos, "an-trap") ?
+	    buf->buf + pos : "br");
+	return ROFF_IGN;
+}
+
+static int
+roff_Dd(ROFF_ARGS)
+{
+	int		 mask;
+	enum roff_tok	 t, te;
+
+	switch (tok) {
+	case ROFF_Dd:
+		tok = MDOC_Dd;
+		te = MDOC_MAX;
+		if (r->format == 0)
+			r->format = MPARSE_MDOC;
+		mask = MPARSE_MDOC | MPARSE_QUICK;
+		break;
+	case ROFF_TH:
+		tok = MAN_TH;
+		te = MAN_MAX;
+		if (r->format == 0)
+			r->format = MPARSE_MAN;
+		mask = MPARSE_QUICK;
+		break;
+	default:
+		abort();
+	}
+	if ((r->options & mask) == 0)
+		for (t = tok; t < te; t++)
+			roff_setstr(r, roff_name[t], NULL, 0);
+	return ROFF_CONT;
+}
+
+static int
+roff_TE(ROFF_ARGS)
+{
+	r->man->flags &= ~ROFF_NONOFILL;
+	if (r->tbl == NULL) {
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN, ln, ppos, "TE");
+		return ROFF_IGN;
+	}
+	if (tbl_end(r->tbl, 0) == 0) {
+		r->tbl = NULL;
+		free(buf->buf);
+		buf->buf = mandoc_strdup(".sp");
+		buf->sz = 4;
+		*offs = 0;
+		return ROFF_REPARSE;
+	}
+	r->tbl = NULL;
+	return ROFF_IGN;
+}
+
+static int
+roff_T_(ROFF_ARGS)
+{
+
+	if (NULL == r->tbl)
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN, ln, ppos, "T&");
+	else
+		tbl_restart(ln, ppos, r->tbl);
+
+	return ROFF_IGN;
+}
+
+/*
+ * Handle in-line equation delimiters.
+ */
+static int
+roff_eqndelim(struct roff *r, struct buf *buf, int pos)
+{
+	char		*cp1, *cp2;
+	const char	*bef_pr, *bef_nl, *mac, *aft_nl, *aft_pr;
+
+	/*
+	 * Outside equations, look for an opening delimiter.
+	 * If we are inside an equation, we already know it is
+	 * in-line, or this function wouldn't have been called;
+	 * so look for a closing delimiter.
+	 */
+
+	cp1 = buf->buf + pos;
+	cp2 = strchr(cp1, r->eqn == NULL ?
+	    r->last_eqn->odelim : r->last_eqn->cdelim);
+	if (cp2 == NULL)
+		return ROFF_CONT;
+
+	*cp2++ = '\0';
+	bef_pr = bef_nl = aft_nl = aft_pr = "";
+
+	/* Handle preceding text, protecting whitespace. */
+
+	if (*buf->buf != '\0') {
+		if (r->eqn == NULL)
+			bef_pr = "\\&";
+		bef_nl = "\n";
+	}
+
+	/*
+	 * Prepare replacing the delimiter with an equation macro
+	 * and drop leading white space from the equation.
+	 */
+
+	if (r->eqn == NULL) {
+		while (*cp2 == ' ')
+			cp2++;
+		mac = ".EQ";
+	} else
+		mac = ".EN";
+
+	/* Handle following text, protecting whitespace. */
+
+	if (*cp2 != '\0') {
+		aft_nl = "\n";
+		if (r->eqn != NULL)
+			aft_pr = "\\&";
+	}
+
+	/* Do the actual replacement. */
+
+	buf->sz = mandoc_asprintf(&cp1, "%s%s%s%s%s%s%s", buf->buf,
+	    bef_pr, bef_nl, mac, aft_nl, aft_pr, cp2) + 1;
+	free(buf->buf);
+	buf->buf = cp1;
+
+	/* Toggle the in-line state of the eqn subsystem. */
+
+	r->eqn_inline = r->eqn == NULL;
+	return ROFF_REPARSE;
+}
+
+static int
+roff_EQ(ROFF_ARGS)
+{
+	struct roff_node	*n;
+
+	if (r->man->meta.macroset == MACROSET_MAN)
+		man_breakscope(r->man, ROFF_EQ);
+	n = roff_node_alloc(r->man, ln, ppos, ROFFT_EQN, TOKEN_NONE);
+	if (ln > r->man->last->line)
+		n->flags |= NODE_LINE;
+	n->eqn = eqn_box_new();
+	roff_node_append(r->man, n);
+	r->man->next = ROFF_NEXT_SIBLING;
+
+	assert(r->eqn == NULL);
+	if (r->last_eqn == NULL)
+		r->last_eqn = eqn_alloc();
+	else
+		eqn_reset(r->last_eqn);
+	r->eqn = r->last_eqn;
+	r->eqn->node = n;
+
+	if (buf->buf[pos] != '\0')
+		mandoc_msg(MANDOCERR_ARG_SKIP, ln, pos,
+		    ".EQ %s", buf->buf + pos);
+
+	return ROFF_IGN;
+}
+
+static int
+roff_EN(ROFF_ARGS)
+{
+	if (r->eqn != NULL) {
+		eqn_parse(r->eqn);
+		r->eqn = NULL;
+	} else
+		mandoc_msg(MANDOCERR_BLK_NOTOPEN, ln, ppos, "EN");
+	if (buf->buf[pos] != '\0')
+		mandoc_msg(MANDOCERR_ARG_SKIP, ln, pos,
+		    "EN %s", buf->buf + pos);
+	return ROFF_IGN;
+}
+
+static int
+roff_TS(ROFF_ARGS)
+{
+	if (r->tbl != NULL) {
+		mandoc_msg(MANDOCERR_BLK_BROKEN, ln, ppos, "TS breaks TS");
+		tbl_end(r->tbl, 0);
+	}
+	r->man->flags |= ROFF_NONOFILL;
+	r->tbl = tbl_alloc(ppos, ln, r->last_tbl);
+	if (r->last_tbl == NULL)
+		r->first_tbl = r->tbl;
+	r->last_tbl = r->tbl;
+	return ROFF_IGN;
+}
+
+static int
+roff_noarg(ROFF_ARGS)
+{
+	if (r->man->flags & (MAN_BLINE | MAN_ELINE))
+		man_breakscope(r->man, tok);
+	if (tok == ROFF_brp)
+		tok = ROFF_br;
+	roff_elem_alloc(r->man, ln, ppos, tok);
+	if (buf->buf[pos] != '\0')
+		mandoc_msg(MANDOCERR_ARG_SKIP, ln, pos,
+		   "%s %s", roff_name[tok], buf->buf + pos);
+	if (tok == ROFF_nf)
+		r->man->flags |= ROFF_NOFILL;
+	else if (tok == ROFF_fi)
+		r->man->flags &= ~ROFF_NOFILL;
+	r->man->last->flags |= NODE_LINE | NODE_VALID | NODE_ENDED;
+	r->man->next = ROFF_NEXT_SIBLING;
+	return ROFF_IGN;
+}
+
+static int
+roff_onearg(ROFF_ARGS)
+{
+	struct roff_node	*n;
+	char			*cp;
+	int			 npos;
+
+	if (r->man->flags & (MAN_BLINE | MAN_ELINE) &&
+	    (tok == ROFF_ce || tok == ROFF_rj || tok == ROFF_sp ||
+	     tok == ROFF_ti))
+		man_breakscope(r->man, tok);
+
+	if (roffce_node != NULL && (tok == ROFF_ce || tok == ROFF_rj)) {
+		r->man->last = roffce_node;
+		r->man->next = ROFF_NEXT_SIBLING;
+	}
+
+	roff_elem_alloc(r->man, ln, ppos, tok);
+	n = r->man->last;
+
+	cp = buf->buf + pos;
+	if (*cp != '\0') {
+		while (*cp != '\0' && *cp != ' ')
+			cp++;
+		while (*cp == ' ')
+			*cp++ = '\0';
+		if (*cp != '\0')
+			mandoc_msg(MANDOCERR_ARG_EXCESS,
+			    ln, (int)(cp - buf->buf),
+			    "%s ... %s", roff_name[tok], cp);
+		roff_word_alloc(r->man, ln, pos, buf->buf + pos);
+	}
+
+	if (tok == ROFF_ce || tok == ROFF_rj) {
+		if (r->man->last->type == ROFFT_ELEM) {
+			roff_word_alloc(r->man, ln, pos, "1");
+			r->man->last->flags |= NODE_NOSRC;
+		}
+		npos = 0;
+		if (roff_evalnum(r, ln, r->man->last->string, &npos,
+		    &roffce_lines, 0) == 0) {
+			mandoc_msg(MANDOCERR_CE_NONUM,
+			    ln, pos, "ce %s", buf->buf + pos);
+			roffce_lines = 1;
+		}
+		if (roffce_lines < 1) {
+			r->man->last = r->man->last->parent;
+			roffce_node = NULL;
+			roffce_lines = 0;
+		} else
+			roffce_node = r->man->last->parent;
+	} else {
+		n->flags |= NODE_VALID | NODE_ENDED;
+		r->man->last = n;
+	}
+	n->flags |= NODE_LINE;
+	r->man->next = ROFF_NEXT_SIBLING;
+	return ROFF_IGN;
+}
+
+static int
+roff_manyarg(ROFF_ARGS)
+{
+	struct roff_node	*n;
+	char			*sp, *ep;
+
+	roff_elem_alloc(r->man, ln, ppos, tok);
+	n = r->man->last;
+
+	for (sp = ep = buf->buf + pos; *sp != '\0'; sp = ep) {
+		while (*ep != '\0' && *ep != ' ')
+			ep++;
+		while (*ep == ' ')
+			*ep++ = '\0';
+		roff_word_alloc(r->man, ln, sp - buf->buf, sp);
+	}
+
+	n->flags |= NODE_LINE | NODE_VALID | NODE_ENDED;
+	r->man->last = n;
+	r->man->next = ROFF_NEXT_SIBLING;
+	return ROFF_IGN;
+}
+
+static int
+roff_als(ROFF_ARGS)
+{
+	char		*oldn, *newn, *end, *value;
+	size_t		 oldsz, newsz, valsz;
+
+	newn = oldn = buf->buf + pos;
+	if (*newn == '\0')
+		return ROFF_IGN;
+
+	newsz = roff_getname(r, &oldn, ln, pos);
+	if (newn[newsz] == '\\' || newn[newsz] == '\t' || *oldn == '\0')
+		return ROFF_IGN;
+
+	end = oldn;
+	oldsz = roff_getname(r, &end, ln, oldn - buf->buf);
+	if (oldsz == 0)
+		return ROFF_IGN;
+
+	valsz = mandoc_asprintf(&value, ".%.*s \\$@\\\"\n",
+	    (int)oldsz, oldn);
+	roff_setstrn(&r->strtab, newn, newsz, value, valsz, 0);
+	roff_setstrn(&r->rentab, newn, newsz, NULL, 0, 0);
+	free(value);
+	return ROFF_IGN;
+}
+
+/*
+ * The .break request only makes sense inside conditionals,
+ * and that case is already handled in roff_cond_sub().
+ */
+static int
+roff_break(ROFF_ARGS)
+{
+	mandoc_msg(MANDOCERR_BLK_NOTOPEN, ln, pos, "break");
+	return ROFF_IGN;
+}
+
+static int
+roff_cc(ROFF_ARGS)
+{
+	const char	*p;
+
+	p = buf->buf + pos;
+
+	if (*p == '\0' || (r->control = *p++) == '.')
+		r->control = '\0';
+
+	if (*p != '\0')
+		mandoc_msg(MANDOCERR_ARG_EXCESS,
+		    ln, p - buf->buf, "cc ... %s", p);
+
+	return ROFF_IGN;
+}
+
+static int
+roff_char(ROFF_ARGS)
+{
+	const char	*p, *kp, *vp;
+	size_t		 ksz, vsz;
+	int		 font;
+
+	/* Parse the character to be replaced. */
+
+	kp = buf->buf + pos;
+	p = kp + 1;
+	if (*kp == '\0' || (*kp == '\\' &&
+	     mandoc_escape(&p, NULL, NULL) != ESCAPE_SPECIAL) ||
+	    (*p != ' ' && *p != '\0')) {
+		mandoc_msg(MANDOCERR_CHAR_ARG, ln, pos, "char %s", kp);
+		return ROFF_IGN;
+	}
+	ksz = p - kp;
+	while (*p == ' ')
+		p++;
+
+	/*
+	 * If the replacement string contains a font escape sequence,
+	 * we have to restore the font at the end.
+	 */
+
+	vp = p;
+	vsz = strlen(p);
+	font = 0;
+	while (*p != '\0') {
+		if (*p++ != '\\')
+			continue;
+		switch (mandoc_escape(&p, NULL, NULL)) {
+		case ESCAPE_FONT:
+		case ESCAPE_FONTROMAN:
+		case ESCAPE_FONTITALIC:
+		case ESCAPE_FONTBOLD:
+		case ESCAPE_FONTBI:
+		case ESCAPE_FONTCW:
+		case ESCAPE_FONTPREV:
+			font++;
+			break;
+		default:
+			break;
+		}
+	}
+	if (font > 1)
+		mandoc_msg(MANDOCERR_CHAR_FONT,
+		    ln, (int)(vp - buf->buf), "%s", vp);
+
+	/*
+	 * Approximate the effect of .char using the .tr tables.
+	 * XXX In groff, .char and .tr interact differently.
+	 */
+
+	if (ksz == 1) {
+		if (r->xtab == NULL)
+			r->xtab = mandoc_calloc(128, sizeof(*r->xtab));
+		assert((unsigned int)*kp < 128);
+		free(r->xtab[(int)*kp].p);
+		r->xtab[(int)*kp].sz = mandoc_asprintf(&r->xtab[(int)*kp].p,
+		    "%s%s", vp, font ? "\fP" : "");
+	} else {
+		roff_setstrn(&r->xmbtab, kp, ksz, vp, vsz, 0);
+		if (font)
+			roff_setstrn(&r->xmbtab, kp, ksz, "\\fP", 3, 1);
+	}
+	return ROFF_IGN;
+}
+
+static int
+roff_ec(ROFF_ARGS)
+{
+	const char	*p;
+
+	p = buf->buf + pos;
+	if (*p == '\0')
+		r->escape = '\\';
+	else {
+		r->escape = *p;
+		if (*++p != '\0')
+			mandoc_msg(MANDOCERR_ARG_EXCESS, ln,
+			    (int)(p - buf->buf), "ec ... %s", p);
+	}
+	return ROFF_IGN;
+}
+
+static int
+roff_eo(ROFF_ARGS)
+{
+	r->escape = '\0';
+	if (buf->buf[pos] != '\0')
+		mandoc_msg(MANDOCERR_ARG_SKIP,
+		    ln, pos, "eo %s", buf->buf + pos);
+	return ROFF_IGN;
+}
+
+static int
+roff_nop(ROFF_ARGS)
+{
+	while (buf->buf[pos] == ' ')
+		pos++;
+	*offs = pos;
+	return ROFF_RERUN;
+}
+
+static int
+roff_tr(ROFF_ARGS)
+{
+	const char	*p, *first, *second;
+	size_t		 fsz, ssz;
+	enum mandoc_esc	 esc;
+
+	p = buf->buf + pos;
+
+	if (*p == '\0') {
+		mandoc_msg(MANDOCERR_REQ_EMPTY, ln, ppos, "tr");
+		return ROFF_IGN;
+	}
+
+	while (*p != '\0') {
+		fsz = ssz = 1;
+
+		first = p++;
+		if (*first == '\\') {
+			esc = mandoc_escape(&p, NULL, NULL);
+			if (esc == ESCAPE_ERROR) {
+				mandoc_msg(MANDOCERR_ESC_BAD, ln,
+				    (int)(p - buf->buf), "%s", first);
+				return ROFF_IGN;
+			}
+			fsz = (size_t)(p - first);
+		}
+
+		second = p++;
+		if (*second == '\\') {
+			esc = mandoc_escape(&p, NULL, NULL);
+			if (esc == ESCAPE_ERROR) {
+				mandoc_msg(MANDOCERR_ESC_BAD, ln,
+				    (int)(p - buf->buf), "%s", second);
+				return ROFF_IGN;
+			}
+			ssz = (size_t)(p - second);
+		} else if (*second == '\0') {
+			mandoc_msg(MANDOCERR_TR_ODD, ln,
+			    (int)(first - buf->buf), "tr %s", first);
+			second = " ";
+			p--;
+		}
+
+		if (fsz > 1) {
+			roff_setstrn(&r->xmbtab, first, fsz,
+			    second, ssz, 0);
+			continue;
+		}
+
+		if (r->xtab == NULL)
+			r->xtab = mandoc_calloc(128,
+			    sizeof(struct roffstr));
+
+		free(r->xtab[(int)*first].p);
+		r->xtab[(int)*first].p = mandoc_strndup(second, ssz);
+		r->xtab[(int)*first].sz = ssz;
+	}
+
+	return ROFF_IGN;
+}
+
+/*
+ * Implementation of the .return request.
+ * There is no need to call roff_userret() from here.
+ * The read module will call that after rewinding the reader stack
+ * to the place from where the current macro was called.
+ */
+static int
+roff_return(ROFF_ARGS)
+{
+	if (r->mstackpos >= 0)
+		return ROFF_IGN | ROFF_USERRET;
+
+	mandoc_msg(MANDOCERR_REQ_NOMAC, ln, ppos, "return");
+	return ROFF_IGN;
+}
+
+static int
+roff_rn(ROFF_ARGS)
+{
+	const char	*value;
+	char		*oldn, *newn, *end;
+	size_t		 oldsz, newsz;
+	int		 deftype;
+
+	oldn = newn = buf->buf + pos;
+	if (*oldn == '\0')
+		return ROFF_IGN;
+
+	oldsz = roff_getname(r, &newn, ln, pos);
+	if (oldn[oldsz] == '\\' || oldn[oldsz] == '\t' || *newn == '\0')
+		return ROFF_IGN;
+
+	end = newn;
+	newsz = roff_getname(r, &end, ln, newn - buf->buf);
+	if (newsz == 0)
+		return ROFF_IGN;
+
+	deftype = ROFFDEF_ANY;
+	value = roff_getstrn(r, oldn, oldsz, &deftype);
+	switch (deftype) {
+	case ROFFDEF_USER:
+		roff_setstrn(&r->strtab, newn, newsz, value, strlen(value), 0);
+		roff_setstrn(&r->strtab, oldn, oldsz, NULL, 0, 0);
+		roff_setstrn(&r->rentab, newn, newsz, NULL, 0, 0);
+		break;
+	case ROFFDEF_PRE:
+		roff_setstrn(&r->strtab, newn, newsz, value, strlen(value), 0);
+		roff_setstrn(&r->rentab, newn, newsz, NULL, 0, 0);
+		break;
+	case ROFFDEF_REN:
+		roff_setstrn(&r->rentab, newn, newsz, value, strlen(value), 0);
+		roff_setstrn(&r->rentab, oldn, oldsz, NULL, 0, 0);
+		roff_setstrn(&r->strtab, newn, newsz, NULL, 0, 0);
+		break;
+	case ROFFDEF_STD:
+		roff_setstrn(&r->rentab, newn, newsz, oldn, oldsz, 0);
+		roff_setstrn(&r->strtab, newn, newsz, NULL, 0, 0);
+		break;
+	default:
+		roff_setstrn(&r->strtab, newn, newsz, NULL, 0, 0);
+		roff_setstrn(&r->rentab, newn, newsz, NULL, 0, 0);
+		break;
+	}
+	return ROFF_IGN;
+}
+
+static int
+roff_shift(ROFF_ARGS)
+{
+	struct mctx	*ctx;
+	int		 levels, i;
+
+	levels = 1;
+	if (buf->buf[pos] != '\0' &&
+	    roff_evalnum(r, ln, buf->buf, &pos, &levels, 0) == 0) {
+		mandoc_msg(MANDOCERR_CE_NONUM,
+		    ln, pos, "shift %s", buf->buf + pos);
+		levels = 1;
+	}
+	if (r->mstackpos < 0) {
+		mandoc_msg(MANDOCERR_REQ_NOMAC, ln, ppos, "shift");
+		return ROFF_IGN;
+	}
+	ctx = r->mstack + r->mstackpos;
+	if (levels > ctx->argc) {
+		mandoc_msg(MANDOCERR_SHIFT,
+		    ln, pos, "%d, but max is %d", levels, ctx->argc);
+		levels = ctx->argc;
+	}
+	if (levels == 0)
+		return ROFF_IGN;
+	for (i = 0; i < levels; i++)
+		free(ctx->argv[i]);
+	ctx->argc -= levels;
+	for (i = 0; i < ctx->argc; i++)
+		ctx->argv[i] = ctx->argv[i + levels];
+	return ROFF_IGN;
+}
+
+static int
+roff_so(ROFF_ARGS)
+{
+	char *name, *cp;
+
+	name = buf->buf + pos;
+	mandoc_msg(MANDOCERR_SO, ln, ppos, "so %s", name);
+
+	/*
+	 * Handle `so'.  Be EXTREMELY careful, as we shouldn't be
+	 * opening anything that's not in our cwd or anything beneath
+	 * it.  Thus, explicitly disallow traversing up the file-system
+	 * or using absolute paths.
+	 */
+
+	if (*name == '/' || strstr(name, "../") || strstr(name, "/..")) {
+		mandoc_msg(MANDOCERR_SO_PATH, ln, ppos, ".so %s", name);
+		buf->sz = mandoc_asprintf(&cp,
+		    ".sp\nSee the file %s.\n.sp", name) + 1;
+		free(buf->buf);
+		buf->buf = cp;
+		*offs = 0;
+		return ROFF_REPARSE;
+	}
+
+	*offs = pos;
+	return ROFF_SO;
+}
+
+/* --- user defined strings and macros ------------------------------------ */
+
+static int
+roff_userdef(ROFF_ARGS)
+{
+	struct mctx	 *ctx;
+	char		 *arg, *ap, *dst, *src;
+	size_t		  sz;
+
+	/* If the macro is empty, ignore it altogether. */
+
+	if (*r->current_string == '\0')
+		return ROFF_IGN;
+
+	/* Initialize a new macro stack context. */
+
+	if (++r->mstackpos == r->mstacksz) {
+		r->mstack = mandoc_recallocarray(r->mstack,
+		    r->mstacksz, r->mstacksz + 8, sizeof(*r->mstack));
+		r->mstacksz += 8;
+	}
+	ctx = r->mstack + r->mstackpos;
+	ctx->argsz = 0;
+	ctx->argc = 0;
+	ctx->argv = NULL;
+
+	/*
+	 * Collect pointers to macro argument strings,
+	 * NUL-terminating them and escaping quotes.
+	 */
+
+	src = buf->buf + pos;
+	while (*src != '\0') {
+		if (ctx->argc == ctx->argsz) {
+			ctx->argsz += 8;
+			ctx->argv = mandoc_reallocarray(ctx->argv,
+			    ctx->argsz, sizeof(*ctx->argv));
+		}
+		arg = roff_getarg(r, &src, ln, &pos);
+		sz = 1;  /* For the terminating NUL. */
+		for (ap = arg; *ap != '\0'; ap++)
+			sz += *ap == '"' ? 4 : 1;
+		ctx->argv[ctx->argc++] = dst = mandoc_malloc(sz);
+		for (ap = arg; *ap != '\0'; ap++) {
+			if (*ap == '"') {
+				memcpy(dst, "\\(dq", 4);
+				dst += 4;
+			} else
+				*dst++ = *ap;
+		}
+		*dst = '\0';
+		free(arg);
+	}
+
+	/* Replace the macro invocation by the macro definition. */
+
+	free(buf->buf);
+	buf->buf = mandoc_strdup(r->current_string);
+	buf->sz = strlen(buf->buf) + 1;
+	*offs = 0;
+
+	return buf->buf[buf->sz - 2] == '\n' ?
+	    ROFF_REPARSE | ROFF_USERCALL : ROFF_IGN | ROFF_APPEND;
+}
+
+/*
+ * Calling a high-level macro that was renamed with .rn.
+ * r->current_string has already been set up by roff_parse().
+ */
+static int
+roff_renamed(ROFF_ARGS)
+{
+	char	*nbuf;
+
+	buf->sz = mandoc_asprintf(&nbuf, ".%s%s%s", r->current_string,
+	    buf->buf[pos] == '\0' ? "" : " ", buf->buf + pos) + 1;
+	free(buf->buf);
+	buf->buf = nbuf;
+	*offs = 0;
+	return ROFF_CONT;
+}
+
+/*
+ * Measure the length in bytes of the roff identifier at *cpp
+ * and advance the pointer to the next word.
+ */
+static size_t
+roff_getname(struct roff *r, char **cpp, int ln, int pos)
+{
+	char	 *name, *cp;
+	size_t	  namesz;
+
+	name = *cpp;
+	if (*name == '\0')
+		return 0;
+
+	/* Advance cp to the byte after the end of the name. */
+
+	for (cp = name; 1; cp++) {
+		namesz = cp - name;
+		if (*cp == '\0')
+			break;
+		if (*cp == ' ' || *cp == '\t') {
+			cp++;
+			break;
+		}
+		if (*cp != '\\')
+			continue;
+		if (cp[1] == '{' || cp[1] == '}')
+			break;
+		if (*++cp == '\\')
+			continue;
+		mandoc_msg(MANDOCERR_NAMESC, ln, pos,
+		    "%.*s", (int)(cp - name + 1), name);
+		mandoc_escape((const char **)&cp, NULL, NULL);
+		break;
+	}
+
+	/* Read past spaces. */
+
+	while (*cp == ' ')
+		cp++;
+
+	*cpp = cp;
+	return namesz;
+}
+
+/*
+ * Store *string into the user-defined string called *name.
+ * To clear an existing entry, call with (*r, *name, NULL, 0).
+ * append == 0: replace mode
+ * append == 1: single-line append mode
+ * append == 2: multiline append mode, append '\n' after each call
+ */
+static void
+roff_setstr(struct roff *r, const char *name, const char *string,
+	int append)
+{
+	size_t	 namesz;
+
+	namesz = strlen(name);
+	roff_setstrn(&r->strtab, name, namesz, string,
+	    string ? strlen(string) : 0, append);
+	roff_setstrn(&r->rentab, name, namesz, NULL, 0, 0);
+}
+
+static void
+roff_setstrn(struct roffkv **r, const char *name, size_t namesz,
+		const char *string, size_t stringsz, int append)
+{
+	struct roffkv	*n;
+	char		*c;
+	int		 i;
+	size_t		 oldch, newch;
+
+	/* Search for an existing string with the same name. */
+	n = *r;
+
+	while (n && (namesz != n->key.sz ||
+			strncmp(n->key.p, name, namesz)))
+		n = n->next;
+
+	if (NULL == n) {
+		/* Create a new string table entry. */
+		n = mandoc_malloc(sizeof(struct roffkv));
+		n->key.p = mandoc_strndup(name, namesz);
+		n->key.sz = namesz;
+		n->val.p = NULL;
+		n->val.sz = 0;
+		n->next = *r;
+		*r = n;
+	} else if (0 == append) {
+		free(n->val.p);
+		n->val.p = NULL;
+		n->val.sz = 0;
+	}
+
+	if (NULL == string)
+		return;
+
+	/*
+	 * One additional byte for the '\n' in multiline mode,
+	 * and one for the terminating '\0'.
+	 */
+	newch = stringsz + (1 < append ? 2u : 1u);
+
+	if (NULL == n->val.p) {
+		n->val.p = mandoc_malloc(newch);
+		*n->val.p = '\0';
+		oldch = 0;
+	} else {
+		oldch = n->val.sz;
+		n->val.p = mandoc_realloc(n->val.p, oldch + newch);
+	}
+
+	/* Skip existing content in the destination buffer. */
+	c = n->val.p + (int)oldch;
+
+	/* Append new content to the destination buffer. */
+	i = 0;
+	while (i < (int)stringsz) {
+		/*
+		 * Rudimentary roff copy mode:
+		 * Handle escaped backslashes.
+		 */
+		if ('\\' == string[i] && '\\' == string[i + 1])
+			i++;
+		*c++ = string[i++];
+	}
+
+	/* Append terminating bytes. */
+	if (1 < append)
+		*c++ = '\n';
+
+	*c = '\0';
+	n->val.sz = (int)(c - n->val.p);
+}
+
+static const char *
+roff_getstrn(struct roff *r, const char *name, size_t len,
+    int *deftype)
+{
+	const struct roffkv	*n;
+	int			 found, i;
+	enum roff_tok		 tok;
+
+	found = 0;
+	for (n = r->strtab; n != NULL; n = n->next) {
+		if (strncmp(name, n->key.p, len) != 0 ||
+		    n->key.p[len] != '\0' || n->val.p == NULL)
+			continue;
+		if (*deftype & ROFFDEF_USER) {
+			*deftype = ROFFDEF_USER;
+			return n->val.p;
+		} else {
+			found = 1;
+			break;
+		}
+	}
+	for (n = r->rentab; n != NULL; n = n->next) {
+		if (strncmp(name, n->key.p, len) != 0 ||
+		    n->key.p[len] != '\0' || n->val.p == NULL)
+			continue;
+		if (*deftype & ROFFDEF_REN) {
+			*deftype = ROFFDEF_REN;
+			return n->val.p;
+		} else {
+			found = 1;
+			break;
+		}
+	}
+	for (i = 0; i < PREDEFS_MAX; i++) {
+		if (strncmp(name, predefs[i].name, len) != 0 ||
+		    predefs[i].name[len] != '\0')
+			continue;
+		if (*deftype & ROFFDEF_PRE) {
+			*deftype = ROFFDEF_PRE;
+			return predefs[i].str;
+		} else {
+			found = 1;
+			break;
+		}
+	}
+	if (r->man->meta.macroset != MACROSET_MAN) {
+		for (tok = MDOC_Dd; tok < MDOC_MAX; tok++) {
+			if (strncmp(name, roff_name[tok], len) != 0 ||
+			    roff_name[tok][len] != '\0')
+				continue;
+			if (*deftype & ROFFDEF_STD) {
+				*deftype = ROFFDEF_STD;
+				return NULL;
+			} else {
+				found = 1;
+				break;
+			}
+		}
+	}
+	if (r->man->meta.macroset != MACROSET_MDOC) {
+		for (tok = MAN_TH; tok < MAN_MAX; tok++) {
+			if (strncmp(name, roff_name[tok], len) != 0 ||
+			    roff_name[tok][len] != '\0')
+				continue;
+			if (*deftype & ROFFDEF_STD) {
+				*deftype = ROFFDEF_STD;
+				return NULL;
+			} else {
+				found = 1;
+				break;
+			}
+		}
+	}
+
+	if (found == 0 && *deftype != ROFFDEF_ANY) {
+		if (*deftype & ROFFDEF_REN) {
+			/*
+			 * This might still be a request,
+			 * so do not treat it as undefined yet.
+			 */
+			*deftype = ROFFDEF_UNDEF;
+			return NULL;
+		}
+
+		/* Using an undefined string defines it to be empty. */
+
+		roff_setstrn(&r->strtab, name, len, "", 0, 0);
+		roff_setstrn(&r->rentab, name, len, NULL, 0, 0);
+	}
+
+	*deftype = 0;
+	return NULL;
+}
+
+static void
+roff_freestr(struct roffkv *r)
+{
+	struct roffkv	 *n, *nn;
+
+	for (n = r; n; n = nn) {
+		free(n->key.p);
+		free(n->val.p);
+		nn = n->next;
+		free(n);
+	}
+}
+
+/* --- accessors and utility functions ------------------------------------ */
+
+/*
+ * Duplicate an input string, making the appropriate character
+ * conversations (as stipulated by `tr') along the way.
+ * Returns a heap-allocated string with all the replacements made.
+ */
+char *
+roff_strdup(const struct roff *r, const char *p)
+{
+	const struct roffkv *cp;
+	char		*res;
+	const char	*pp;
+	size_t		 ssz, sz;
+	enum mandoc_esc	 esc;
+
+	if (NULL == r->xmbtab && NULL == r->xtab)
+		return mandoc_strdup(p);
+	else if ('\0' == *p)
+		return mandoc_strdup("");
+
+	/*
+	 * Step through each character looking for term matches
+	 * (remember that a `tr' can be invoked with an escape, which is
+	 * a glyph but the escape is multi-character).
+	 * We only do this if the character hash has been initialised
+	 * and the string is >0 length.
+	 */
+
+	res = NULL;
+	ssz = 0;
+
+	while ('\0' != *p) {
+		assert((unsigned int)*p < 128);
+		if ('\\' != *p && r->xtab && r->xtab[(unsigned int)*p].p) {
+			sz = r->xtab[(int)*p].sz;
+			res = mandoc_realloc(res, ssz + sz + 1);
+			memcpy(res + ssz, r->xtab[(int)*p].p, sz);
+			ssz += sz;
+			p++;
+			continue;
+		} else if ('\\' != *p) {
+			res = mandoc_realloc(res, ssz + 2);
+			res[ssz++] = *p++;
+			continue;
+		}
+
+		/* Search for term matches. */
+		for (cp = r->xmbtab; cp; cp = cp->next)
+			if (0 == strncmp(p, cp->key.p, cp->key.sz))
+				break;
+
+		if (NULL != cp) {
+			/*
+			 * A match has been found.
+			 * Append the match to the array and move
+			 * forward by its keysize.
+			 */
+			res = mandoc_realloc(res,
+			    ssz + cp->val.sz + 1);
+			memcpy(res + ssz, cp->val.p, cp->val.sz);
+			ssz += cp->val.sz;
+			p += (int)cp->key.sz;
+			continue;
+		}
+
+		/*
+		 * Handle escapes carefully: we need to copy
+		 * over just the escape itself, or else we might
+		 * do replacements within the escape itself.
+		 * Make sure to pass along the bogus string.
+		 */
+		pp = p++;
+		esc = mandoc_escape(&p, NULL, NULL);
+		if (ESCAPE_ERROR == esc) {
+			sz = strlen(pp);
+			res = mandoc_realloc(res, ssz + sz + 1);
+			memcpy(res + ssz, pp, sz);
+			break;
+		}
+		/*
+		 * We bail out on bad escapes.
+		 * No need to warn: we already did so when
+		 * roff_expand() was called.
+		 */
+		sz = (int)(p - pp);
+		res = mandoc_realloc(res, ssz + sz + 1);
+		memcpy(res + ssz, pp, sz);
+		ssz += sz;
+	}
+
+	res[(int)ssz] = '\0';
+	return res;
+}
+
+int
+roff_getformat(const struct roff *r)
+{
+
+	return r->format;
+}
+
+/*
+ * Find out whether a line is a macro line or not.
+ * If it is, adjust the current position and return one; if it isn't,
+ * return zero and don't change the current position.
+ * If the control character has been set with `.cc', then let that grain
+ * precedence.
+ * This is slighly contrary to groff, where using the non-breaking
+ * control character when `cc' has been invoked will cause the
+ * non-breaking macro contents to be printed verbatim.
+ */
+int
+roff_getcontrol(const struct roff *r, const char *cp, int *ppos)
+{
+	int		pos;
+
+	pos = *ppos;
+
+	if (r->control != '\0' && cp[pos] == r->control)
+		pos++;
+	else if (r->control != '\0')
+		return 0;
+	else if ('\\' == cp[pos] && '.' == cp[pos + 1])
+		pos += 2;
+	else if ('.' == cp[pos] || '\'' == cp[pos])
+		pos++;
+	else
+		return 0;
+
+	while (' ' == cp[pos] || '\t' == cp[pos])
+		pos++;
+
+	*ppos = pos;
+	return 1;
+}
diff --git a/usr.bin/mandoc/roff.h b/usr.bin/mandoc/roff.h
new file mode 100644
index 0000000..aefeafc
--- /dev/null
+++ b/usr.bin/mandoc/roff.h
@@ -0,0 +1,561 @@
+/* $OpenBSD: roff.h,v 1.56 2020/04/08 11:54:14 schwarze Exp $	*/
+/*
+ * Copyright (c) 2013-2015, 2017-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ *
+ * Common data types for all syntax trees and related functions.
+ */
+
+struct	ohash;
+struct	mdoc_arg;
+union	mdoc_data;
+struct	tbl_span;
+struct	eqn_box;
+
+enum	roff_macroset {
+	MACROSET_NONE = 0,
+	MACROSET_MDOC,
+	MACROSET_MAN
+};
+
+enum	roff_sec {
+	SEC_NONE = 0,
+	SEC_NAME,
+	SEC_LIBRARY,
+	SEC_SYNOPSIS,
+	SEC_DESCRIPTION,
+	SEC_CONTEXT,
+	SEC_IMPLEMENTATION,	/* IMPLEMENTATION NOTES */
+	SEC_RETURN_VALUES,
+	SEC_ENVIRONMENT,
+	SEC_FILES,
+	SEC_EXIT_STATUS,
+	SEC_EXAMPLES,
+	SEC_DIAGNOSTICS,
+	SEC_COMPATIBILITY,
+	SEC_ERRORS,
+	SEC_SEE_ALSO,
+	SEC_STANDARDS,
+	SEC_HISTORY,
+	SEC_AUTHORS,
+	SEC_CAVEATS,
+	SEC_BUGS,
+	SEC_SECURITY,
+	SEC_CUSTOM,
+	SEC__MAX
+};
+
+enum	roff_type {
+	ROFFT_ROOT,
+	ROFFT_BLOCK,
+	ROFFT_HEAD,
+	ROFFT_BODY,
+	ROFFT_TAIL,
+	ROFFT_ELEM,
+	ROFFT_TEXT,
+	ROFFT_COMMENT,
+	ROFFT_TBL,
+	ROFFT_EQN
+};
+
+enum	roff_tok {
+	ROFF_br = 0,
+	ROFF_ce,
+	ROFF_fi,
+	ROFF_ft,
+	ROFF_ll,
+	ROFF_mc,
+	ROFF_nf,
+	ROFF_po,
+	ROFF_rj,
+	ROFF_sp,
+	ROFF_ta,
+	ROFF_ti,
+	ROFF_MAX,
+	ROFF_ab,
+	ROFF_ad,
+	ROFF_af,
+	ROFF_aln,
+	ROFF_als,
+	ROFF_am,
+	ROFF_am1,
+	ROFF_ami,
+	ROFF_ami1,
+	ROFF_as,
+	ROFF_as1,
+	ROFF_asciify,
+	ROFF_backtrace,
+	ROFF_bd,
+	ROFF_bleedat,
+	ROFF_blm,
+	ROFF_box,
+	ROFF_boxa,
+	ROFF_bp,
+	ROFF_BP,
+	ROFF_break,
+	ROFF_breakchar,
+	ROFF_brnl,
+	ROFF_brp,
+	ROFF_brpnl,
+	ROFF_c2,
+	ROFF_cc,
+	ROFF_cf,
+	ROFF_cflags,
+	ROFF_ch,
+	ROFF_char,
+	ROFF_chop,
+	ROFF_class,
+	ROFF_close,
+	ROFF_CL,
+	ROFF_color,
+	ROFF_composite,
+	ROFF_continue,
+	ROFF_cp,
+	ROFF_cropat,
+	ROFF_cs,
+	ROFF_cu,
+	ROFF_da,
+	ROFF_dch,
+	ROFF_Dd,
+	ROFF_de,
+	ROFF_de1,
+	ROFF_defcolor,
+	ROFF_dei,
+	ROFF_dei1,
+	ROFF_device,
+	ROFF_devicem,
+	ROFF_di,
+	ROFF_do,
+	ROFF_ds,
+	ROFF_ds1,
+	ROFF_dwh,
+	ROFF_dt,
+	ROFF_ec,
+	ROFF_ecr,
+	ROFF_ecs,
+	ROFF_el,
+	ROFF_em,
+	ROFF_EN,
+	ROFF_eo,
+	ROFF_EP,
+	ROFF_EQ,
+	ROFF_errprint,
+	ROFF_ev,
+	ROFF_evc,
+	ROFF_ex,
+	ROFF_fallback,
+	ROFF_fam,
+	ROFF_fc,
+	ROFF_fchar,
+	ROFF_fcolor,
+	ROFF_fdeferlig,
+	ROFF_feature,
+	ROFF_fkern,
+	ROFF_fl,
+	ROFF_flig,
+	ROFF_fp,
+	ROFF_fps,
+	ROFF_fschar,
+	ROFF_fspacewidth,
+	ROFF_fspecial,
+	ROFF_ftr,
+	ROFF_fzoom,
+	ROFF_gcolor,
+	ROFF_hc,
+	ROFF_hcode,
+	ROFF_hidechar,
+	ROFF_hla,
+	ROFF_hlm,
+	ROFF_hpf,
+	ROFF_hpfa,
+	ROFF_hpfcode,
+	ROFF_hw,
+	ROFF_hy,
+	ROFF_hylang,
+	ROFF_hylen,
+	ROFF_hym,
+	ROFF_hypp,
+	ROFF_hys,
+	ROFF_ie,
+	ROFF_if,
+	ROFF_ig,
+	/* MAN_in; ignored in mdoc(7) */
+	ROFF_index,
+	ROFF_it,
+	ROFF_itc,
+	ROFF_IX,
+	ROFF_kern,
+	ROFF_kernafter,
+	ROFF_kernbefore,
+	ROFF_kernpair,
+	ROFF_lc,
+	ROFF_lc_ctype,
+	ROFF_lds,
+	ROFF_length,
+	ROFF_letadj,
+	ROFF_lf,
+	ROFF_lg,
+	ROFF_lhang,
+	ROFF_linetabs,
+	ROFF_lnr,
+	ROFF_lnrf,
+	ROFF_lpfx,
+	ROFF_ls,
+	ROFF_lsm,
+	ROFF_lt,
+	ROFF_mediasize,
+	ROFF_minss,
+	ROFF_mk,
+	ROFF_mso,
+	ROFF_na,
+	ROFF_ne,
+	ROFF_nh,
+	ROFF_nhychar,
+	ROFF_nm,
+	ROFF_nn,
+	ROFF_nop,
+	ROFF_nr,
+	ROFF_nrf,
+	ROFF_nroff,
+	ROFF_ns,
+	ROFF_nx,
+	ROFF_open,
+	ROFF_opena,
+	ROFF_os,
+	ROFF_output,
+	ROFF_padj,
+	ROFF_papersize,
+	ROFF_pc,
+	ROFF_pev,
+	ROFF_pi,
+	ROFF_PI,
+	ROFF_pl,
+	ROFF_pm,
+	ROFF_pn,
+	ROFF_pnr,
+	ROFF_ps,
+	ROFF_psbb,
+	ROFF_pshape,
+	ROFF_pso,
+	ROFF_ptr,
+	ROFF_pvs,
+	ROFF_rchar,
+	ROFF_rd,
+	ROFF_recursionlimit,
+	ROFF_return,
+	ROFF_rfschar,
+	ROFF_rhang,
+	ROFF_rm,
+	ROFF_rn,
+	ROFF_rnn,
+	ROFF_rr,
+	ROFF_rs,
+	ROFF_rt,
+	ROFF_schar,
+	ROFF_sentchar,
+	ROFF_shc,
+	ROFF_shift,
+	ROFF_sizes,
+	ROFF_so,
+	ROFF_spacewidth,
+	ROFF_special,
+	ROFF_spreadwarn,
+	ROFF_ss,
+	ROFF_sty,
+	ROFF_substring,
+	ROFF_sv,
+	ROFF_sy,
+	ROFF_T_,
+	ROFF_tc,
+	ROFF_TE,
+	ROFF_TH,
+	ROFF_tkf,
+	ROFF_tl,
+	ROFF_tm,
+	ROFF_tm1,
+	ROFF_tmc,
+	ROFF_tr,
+	ROFF_track,
+	ROFF_transchar,
+	ROFF_trf,
+	ROFF_trimat,
+	ROFF_trin,
+	ROFF_trnt,
+	ROFF_troff,
+	ROFF_TS,
+	ROFF_uf,
+	ROFF_ul,
+	ROFF_unformat,
+	ROFF_unwatch,
+	ROFF_unwatchn,
+	ROFF_vpt,
+	ROFF_vs,
+	ROFF_warn,
+	ROFF_warnscale,
+	ROFF_watch,
+	ROFF_watchlength,
+	ROFF_watchn,
+	ROFF_wh,
+	ROFF_while,
+	ROFF_write,
+	ROFF_writec,
+	ROFF_writem,
+	ROFF_xflag,
+	ROFF_cblock,
+	ROFF_RENAMED,
+	ROFF_USERDEF,
+	TOKEN_NONE,
+	MDOC_Dd,
+	MDOC_Dt,
+	MDOC_Os,
+	MDOC_Sh,
+	MDOC_Ss,
+	MDOC_Pp,
+	MDOC_D1,
+	MDOC_Dl,
+	MDOC_Bd,
+	MDOC_Ed,
+	MDOC_Bl,
+	MDOC_El,
+	MDOC_It,
+	MDOC_Ad,
+	MDOC_An,
+	MDOC_Ap,
+	MDOC_Ar,
+	MDOC_Cd,
+	MDOC_Cm,
+	MDOC_Dv,
+	MDOC_Er,
+	MDOC_Ev,
+	MDOC_Ex,
+	MDOC_Fa,
+	MDOC_Fd,
+	MDOC_Fl,
+	MDOC_Fn,
+	MDOC_Ft,
+	MDOC_Ic,
+	MDOC_In,
+	MDOC_Li,
+	MDOC_Nd,
+	MDOC_Nm,
+	MDOC_Op,
+	MDOC_Ot,
+	MDOC_Pa,
+	MDOC_Rv,
+	MDOC_St,
+	MDOC_Va,
+	MDOC_Vt,
+	MDOC_Xr,
+	MDOC__A,
+	MDOC__B,
+	MDOC__D,
+	MDOC__I,
+	MDOC__J,
+	MDOC__N,
+	MDOC__O,
+	MDOC__P,
+	MDOC__R,
+	MDOC__T,
+	MDOC__V,
+	MDOC_Ac,
+	MDOC_Ao,
+	MDOC_Aq,
+	MDOC_At,
+	MDOC_Bc,
+	MDOC_Bf,
+	MDOC_Bo,
+	MDOC_Bq,
+	MDOC_Bsx,
+	MDOC_Bx,
+	MDOC_Db,
+	MDOC_Dc,
+	MDOC_Do,
+	MDOC_Dq,
+	MDOC_Ec,
+	MDOC_Ef,
+	MDOC_Em,
+	MDOC_Eo,
+	MDOC_Fx,
+	MDOC_Ms,
+	MDOC_No,
+	MDOC_Ns,
+	MDOC_Nx,
+	MDOC_Ox,
+	MDOC_Pc,
+	MDOC_Pf,
+	MDOC_Po,
+	MDOC_Pq,
+	MDOC_Qc,
+	MDOC_Ql,
+	MDOC_Qo,
+	MDOC_Qq,
+	MDOC_Re,
+	MDOC_Rs,
+	MDOC_Sc,
+	MDOC_So,
+	MDOC_Sq,
+	MDOC_Sm,
+	MDOC_Sx,
+	MDOC_Sy,
+	MDOC_Tn,
+	MDOC_Ux,
+	MDOC_Xc,
+	MDOC_Xo,
+	MDOC_Fo,
+	MDOC_Fc,
+	MDOC_Oo,
+	MDOC_Oc,
+	MDOC_Bk,
+	MDOC_Ek,
+	MDOC_Bt,
+	MDOC_Hf,
+	MDOC_Fr,
+	MDOC_Ud,
+	MDOC_Lb,
+	MDOC_Lp,
+	MDOC_Lk,
+	MDOC_Mt,
+	MDOC_Brq,
+	MDOC_Bro,
+	MDOC_Brc,
+	MDOC__C,
+	MDOC_Es,
+	MDOC_En,
+	MDOC_Dx,
+	MDOC__Q,
+	MDOC__U,
+	MDOC_Ta,
+	MDOC_Tg,
+	MDOC_MAX,
+	MAN_TH,
+	MAN_SH,
+	MAN_SS,
+	MAN_TP,
+	MAN_TQ,
+	MAN_LP,
+	MAN_PP,
+	MAN_P,
+	MAN_IP,
+	MAN_HP,
+	MAN_SM,
+	MAN_SB,
+	MAN_BI,
+	MAN_IB,
+	MAN_BR,
+	MAN_RB,
+	MAN_R,
+	MAN_B,
+	MAN_I,
+	MAN_IR,
+	MAN_RI,
+	MAN_RE,
+	MAN_RS,
+	MAN_DT,
+	MAN_UC,
+	MAN_PD,
+	MAN_AT,
+	MAN_in,
+	MAN_SY,
+	MAN_YS,
+	MAN_OP,
+	MAN_EX,
+	MAN_EE,
+	MAN_UR,
+	MAN_UE,
+	MAN_MT,
+	MAN_ME,
+	MAN_MAX
+};
+
+/*
+ * Indicates that a BODY's formatting has ended, but
+ * the scope is still open.  Used for badly nested blocks.
+ */
+enum	mdoc_endbody {
+	ENDBODY_NOT = 0,
+	ENDBODY_SPACE	/* Is broken: append a space. */
+};
+
+enum	mandoc_os {
+	MANDOC_OS_OTHER = 0,
+	MANDOC_OS_NETBSD,
+	MANDOC_OS_OPENBSD
+};
+
+struct	roff_node {
+	struct roff_node *parent;  /* Parent AST node. */
+	struct roff_node *child;   /* First child AST node. */
+	struct roff_node *last;    /* Last child AST node. */
+	struct roff_node *next;    /* Sibling AST node. */
+	struct roff_node *prev;    /* Prior sibling AST node. */
+	struct roff_node *head;    /* BLOCK */
+	struct roff_node *body;    /* BLOCK/ENDBODY */
+	struct roff_node *tail;    /* BLOCK */
+	struct mdoc_arg	 *args;    /* BLOCK/ELEM */
+	union mdoc_data	 *norm;    /* Normalized arguments. */
+	char		 *string;  /* TEXT */
+	char		 *tag;     /* For less(1) :t and HTML id=. */
+	struct tbl_span	 *span;    /* TBL */
+	struct eqn_box	 *eqn;     /* EQN */
+	int		  line;    /* Input file line number. */
+	int		  pos;     /* Input file column number. */
+	int		  flags;
+#define	NODE_VALID	 (1 << 0)  /* Has been validated. */
+#define	NODE_ENDED	 (1 << 1)  /* Gone past body end mark. */
+#define	NODE_BROKEN	 (1 << 2)  /* Must validate parent when ending. */
+#define	NODE_LINE	 (1 << 3)  /* First macro/text on line. */
+#define	NODE_DELIMO	 (1 << 4)
+#define	NODE_DELIMC	 (1 << 5)
+#define	NODE_EOS	 (1 << 6)  /* At sentence boundary. */
+#define	NODE_SYNPRETTY	 (1 << 7)  /* SYNOPSIS-style formatting. */
+#define	NODE_NOFILL	 (1 << 8)  /* Fill mode switched off. */
+#define	NODE_NOSRC	 (1 << 9)  /* Generated node, not in input file. */
+#define	NODE_NOPRT	 (1 << 10) /* Shall not print anything. */
+#define	NODE_ID		 (1 << 11) /* Target for deep linking. */
+#define	NODE_HREF	 (1 << 12) /* Link to another place in this page. */
+	int		  prev_font; /* Before entering this node. */
+	int		  aux;     /* Decoded node data, type-dependent. */
+	enum roff_tok	  tok;     /* Request or macro ID. */
+	enum roff_type	  type;    /* AST node type. */
+	enum roff_sec	  sec;     /* Current named section. */
+	enum mdoc_endbody end;     /* BODY */
+};
+
+struct	roff_meta {
+	struct roff_node *first;   /* The first node parsed. */
+	char		 *msec;    /* Manual section, usually a digit. */
+	char		 *vol;     /* Manual volume title. */
+	char		 *os;      /* Operating system. */
+	char		 *arch;    /* Machine architecture. */
+	char		 *title;   /* Manual title, usually CAPS. */
+	char		 *name;    /* Leading manual name. */
+	char		 *date;    /* Normalized date. */
+	char		 *sodest;  /* .so target file name or NULL. */
+	int		  hasbody; /* Document is not empty. */
+	int		  rcsids;  /* Bits indexed by enum mandoc_os. */
+	enum mandoc_os	  os_e;    /* Operating system. */
+	enum roff_macroset macroset; /* Kind of high-level macros used. */
+};
+
+extern	const char *const *roff_name;
+
+
+int		  arch_valid(const char *, enum mandoc_os);
+void		  deroff(char **, const struct roff_node *);
+struct roff_node *roff_node_child(struct roff_node *);
+struct roff_node *roff_node_next(struct roff_node *);
+struct roff_node *roff_node_prev(struct roff_node *);
+int		  roff_node_transparent(struct roff_node *);
+int		  roff_tok_transparent(enum roff_tok);
diff --git a/usr.bin/mandoc/roff_html.c b/usr.bin/mandoc/roff_html.c
new file mode 100644
index 0000000..ed9639a
--- /dev/null
+++ b/usr.bin/mandoc/roff_html.c
@@ -0,0 +1,117 @@
+/*	$OpenBSD: roff_html.c,v 1.20 2019/04/30 15:52:42 schwarze Exp $ */
+/*
+ * Copyright (c) 2010 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2017, 2018, 2019 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "out.h"
+#include "html.h"
+
+#define	ROFF_HTML_ARGS struct html *h, const struct roff_node *n
+
+typedef	void	(*roff_html_pre_fp)(ROFF_HTML_ARGS);
+
+static	void	  roff_html_pre_br(ROFF_HTML_ARGS);
+static	void	  roff_html_pre_ce(ROFF_HTML_ARGS);
+static	void	  roff_html_pre_fi(ROFF_HTML_ARGS);
+static	void	  roff_html_pre_ft(ROFF_HTML_ARGS);
+static	void	  roff_html_pre_nf(ROFF_HTML_ARGS);
+static	void	  roff_html_pre_sp(ROFF_HTML_ARGS);
+
+static	const roff_html_pre_fp roff_html_pre_acts[ROFF_MAX] = {
+	roff_html_pre_br,  /* br */
+	roff_html_pre_ce,  /* ce */
+	roff_html_pre_fi,  /* fi */
+	roff_html_pre_ft,  /* ft */
+	NULL,  /* ll */
+	NULL,  /* mc */
+	roff_html_pre_nf,  /* nf */
+	NULL,  /* po */
+	roff_html_pre_ce,  /* rj */
+	roff_html_pre_sp,  /* sp */
+	NULL,  /* ta */
+	NULL,  /* ti */
+};
+
+
+void
+roff_html_pre(struct html *h, const struct roff_node *n)
+{
+	assert(n->tok < ROFF_MAX);
+	if (roff_html_pre_acts[n->tok] != NULL)
+		(*roff_html_pre_acts[n->tok])(h, n);
+}
+
+static void
+roff_html_pre_br(ROFF_HTML_ARGS)
+{
+	print_otag(h, TAG_BR, "");
+}
+
+static void
+roff_html_pre_ce(ROFF_HTML_ARGS)
+{
+	for (n = n->child->next; n != NULL; n = n->next) {
+		if (n->type == ROFFT_TEXT) {
+			if (n->flags & NODE_LINE)
+				roff_html_pre_br(h, n);
+			print_text(h, n->string);
+		} else
+			roff_html_pre(h, n);
+	}
+	roff_html_pre_br(h, n);
+}
+
+static void
+roff_html_pre_fi(ROFF_HTML_ARGS)
+{
+	if (html_fillmode(h, TOKEN_NONE) == ROFF_fi)
+		print_otag(h, TAG_BR, "");
+}
+
+static void
+roff_html_pre_ft(ROFF_HTML_ARGS)
+{
+	const char	*cp;
+
+	cp = n->child->string;
+	html_setfont(h, mandoc_font(cp, (int)strlen(cp)));
+}
+
+static void
+roff_html_pre_nf(ROFF_HTML_ARGS)
+{
+	if (html_fillmode(h, TOKEN_NONE) == ROFF_nf)
+		print_otag(h, TAG_BR, "");
+}
+
+static void
+roff_html_pre_sp(ROFF_HTML_ARGS)
+{
+	if (html_fillmode(h, TOKEN_NONE) == ROFF_nf) {
+		h->col++;
+		print_endline(h);
+	} else {
+		html_close_paragraph(h);
+		print_otag(h, TAG_P, "c", "Pp");
+	}
+}
diff --git a/usr.bin/mandoc/roff_int.h b/usr.bin/mandoc/roff_int.h
new file mode 100644
index 0000000..779f50f
--- /dev/null
+++ b/usr.bin/mandoc/roff_int.h
@@ -0,0 +1,94 @@
+/* $OpenBSD: roff_int.h,v 1.17 2020/04/24 11:58:02 schwarze Exp $	*/
+/*
+ * Copyright (c) 2013-2015, 2017-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ *
+ * Parser internals shared by multiple parsers.
+ */
+
+struct	ohash;
+struct	roff_node;
+struct	roff_meta;
+struct	roff;
+struct	mdoc_arg;
+
+enum	roff_next {
+	ROFF_NEXT_SIBLING = 0,
+	ROFF_NEXT_CHILD
+};
+
+struct	roff_man {
+	struct roff_meta  meta;    /* Public parse results. */
+	struct roff	 *roff;    /* Roff parser state data. */
+	struct ohash	 *mdocmac; /* Mdoc macro lookup table. */
+	struct ohash	 *manmac;  /* Man macro lookup table. */
+	const char	 *os_s;    /* Default operating system. */
+	struct roff_node *last;    /* The last node parsed. */
+	struct roff_node *last_es; /* The most recent Es node. */
+	int		  quick;   /* Abort parse early. */
+	int		  flags;   /* Parse flags. */
+#define	ROFF_NOFILL	 (1 << 1)  /* Fill mode switched off. */
+#define	MDOC_PBODY	 (1 << 2)  /* In the document body. */
+#define	MDOC_NEWLINE	 (1 << 3)  /* First macro/text in a line. */
+#define	MDOC_PHRASE	 (1 << 4)  /* In a Bl -column phrase. */
+#define	MDOC_PHRASELIT	 (1 << 5)  /* Literal within a phrase. */
+#define	MDOC_FREECOL	 (1 << 6)  /* `It' invocation should close. */
+#define	MDOC_SYNOPSIS	 (1 << 7)  /* SYNOPSIS-style formatting. */
+#define	MDOC_KEEP	 (1 << 8)  /* In a word keep. */
+#define	MDOC_SMOFF	 (1 << 9)  /* Spacing is off. */
+#define	MDOC_NODELIMC	 (1 << 10) /* Disable closing delimiter handling. */
+#define	MAN_ELINE	 (1 << 11) /* Next-line element scope. */
+#define	MAN_BLINE	 (1 << 12) /* Next-line block scope. */
+#define	MDOC_PHRASEQF	 (1 << 13) /* Quote first word encountered. */
+#define	MDOC_PHRASEQL	 (1 << 14) /* Quote last word of this phrase. */
+#define	MDOC_PHRASEQN	 (1 << 15) /* Quote first word of the next phrase. */
+#define	ROFF_NONOFILL	 (1 << 16) /* Temporarily suspend no-fill mode. */
+#define	MAN_NEWLINE	  MDOC_NEWLINE
+	enum roff_sec	  lastsec; /* Last section seen. */
+	enum roff_sec	  lastnamed; /* Last standard section seen. */
+	enum roff_next	  next;    /* Where to put the next node. */
+	char		  filesec; /* Section digit in the file name. */
+};
+
+
+struct roff_node *roff_node_alloc(struct roff_man *, int, int,
+			enum roff_type, int);
+void		  roff_node_append(struct roff_man *, struct roff_node *);
+void		  roff_word_alloc(struct roff_man *, int, int, const char *);
+void		  roff_word_append(struct roff_man *, const char *);
+void		  roff_elem_alloc(struct roff_man *, int, int, int);
+struct roff_node *roff_block_alloc(struct roff_man *, int, int, int);
+struct roff_node *roff_head_alloc(struct roff_man *, int, int, int);
+struct roff_node *roff_body_alloc(struct roff_man *, int, int, int);
+void		  roff_node_unlink(struct roff_man *, struct roff_node *);
+void		  roff_node_relink(struct roff_man *, struct roff_node *);
+void		  roff_node_free(struct roff_node *);
+void		  roff_node_delete(struct roff_man *, struct roff_node *);
+
+struct ohash	 *roffhash_alloc(enum roff_tok, enum roff_tok);
+enum roff_tok	  roffhash_find(struct ohash *, const char *, size_t);
+void		  roffhash_free(struct ohash *);
+
+void		  roff_state_reset(struct roff_man *);
+void		  roff_validate(struct roff_man *);
+
+/*
+ * Functions called from roff.c need to be declared here,
+ * not in libmdoc.h or libman.h, even if they are specific
+ * to either the mdoc(7) or the man(7) parser.
+ */
+
+void		  man_breakscope(struct roff_man *, int);
+void		  mdoc_argv_free(struct mdoc_arg *);
diff --git a/usr.bin/mandoc/roff_term.c b/usr.bin/mandoc/roff_term.c
new file mode 100644
index 0000000..ef90623
--- /dev/null
+++ b/usr.bin/mandoc/roff_term.c
@@ -0,0 +1,244 @@
+/*	$OpenBSD: roff_term.c,v 1.19 2019/01/04 03:24:30 schwarze Exp $ */
+/*
+ * Copyright (c) 2010,2014,2015,2017-2019 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#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:
+		term_fontrepl(p, TERMFONT_BOLD);
+		break;
+	case ESCAPE_FONTITALIC:
+		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_FONTCW:
+		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, polast;
+	int		 ponew;
+
+	if (n->child != NULL &&
+	    a2roffsu(n->child->string, &su, SCALE_EM) != NULL) {
+		ponew = term_hen(p, &su);
+		if (*n->child->string == '+' ||
+		    *n->child->string == '-')
+			ponew += po;
+	} else
+		ponew = polast;
+	polast = po;
+	po = ponew;
+
+	ponew = po - polast + (int)p->tcol->offset;
+	p->tcol->offset = ponew > 0 ? ponew : 0;
+}
+
+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;
+	int		 len, 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);
+
+	if (sign == 0) {
+		p->ti = len - p->tcol->offset;
+		p->tcol->offset = len;
+	} else if (sign == 1) {
+		p->ti = len;
+		p->tcol->offset += len;
+	} else if ((size_t)len < p->tcol->offset) {
+		p->ti = -len;
+		p->tcol->offset -= len;
+	} else {
+		p->ti = -p->tcol->offset;
+		p->tcol->offset = 0;
+	}
+}
diff --git a/usr.bin/mandoc/roff_validate.c b/usr.bin/mandoc/roff_validate.c
new file mode 100644
index 0000000..6fa4f33
--- /dev/null
+++ b/usr.bin/mandoc/roff_validate.c
@@ -0,0 +1,149 @@
+/*	$OpenBSD: roff_validate.c,v 1.19 2020/02/27 01:25:58 schwarze Exp $ */
+/*
+ * Copyright (c) 2010, 2017, 2018, 2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+
+#define	ROFF_VALID_ARGS struct roff_man *man, struct roff_node *n
+
+typedef	void	(*roff_valid_fp)(ROFF_VALID_ARGS);
+
+static	void	  roff_valid_br(ROFF_VALID_ARGS);
+static	void	  roff_valid_fi(ROFF_VALID_ARGS);
+static	void	  roff_valid_ft(ROFF_VALID_ARGS);
+static	void	  roff_valid_nf(ROFF_VALID_ARGS);
+static	void	  roff_valid_sp(ROFF_VALID_ARGS);
+
+static	const roff_valid_fp roff_valids[ROFF_MAX] = {
+	roff_valid_br,  /* br */
+	NULL,  /* ce */
+	roff_valid_fi,  /* fi */
+	roff_valid_ft,  /* ft */
+	NULL,  /* ll */
+	NULL,  /* mc */
+	roff_valid_nf,  /* nf */
+	NULL,  /* po */
+	NULL,  /* rj */
+	roff_valid_sp,  /* sp */
+	NULL,  /* ta */
+	NULL,  /* ti */
+};
+
+
+void
+roff_validate(struct roff_man *man)
+{
+	struct roff_node	*n;
+
+	n = man->last;
+	assert(n->tok < ROFF_MAX);
+	if (roff_valids[n->tok] != NULL)
+		(*roff_valids[n->tok])(man, n);
+}
+
+static void
+roff_valid_br(ROFF_VALID_ARGS)
+{
+	struct roff_node	*np;
+
+	if (n->next != NULL && n->next->type == ROFFT_TEXT &&
+	    *n->next->string == ' ') {
+		mandoc_msg(MANDOCERR_PAR_SKIP, n->line, n->pos,
+		    "br before text line with leading blank");
+		roff_node_delete(man, n);
+		return;
+	}
+
+	if ((np = roff_node_prev(n)) == NULL)
+		return;
+
+	switch (np->tok) {
+	case ROFF_br:
+	case ROFF_sp:
+	case MDOC_Pp:
+		mandoc_msg(MANDOCERR_PAR_SKIP,
+		    n->line, n->pos, "br after %s", roff_name[np->tok]);
+		roff_node_delete(man, n);
+		break;
+	default:
+		break;
+	}
+}
+
+static void
+roff_valid_fi(ROFF_VALID_ARGS)
+{
+	if ((n->flags & NODE_NOFILL) == 0)
+		mandoc_msg(MANDOCERR_FI_SKIP, n->line, n->pos, "fi");
+}
+
+static void
+roff_valid_ft(ROFF_VALID_ARGS)
+{
+	const char		*cp;
+
+	if (n->child == NULL) {
+		man->next = ROFF_NEXT_CHILD;
+		roff_word_alloc(man, n->line, n->pos, "P");
+		man->last = n;
+		return;
+	}
+
+	cp = n->child->string;
+	if (mandoc_font(cp, (int)strlen(cp)) != ESCAPE_ERROR)
+		return;
+	mandoc_msg(MANDOCERR_FT_BAD, n->line, n->pos, "ft %s", cp);
+	roff_node_delete(man, n);
+}
+
+static void
+roff_valid_nf(ROFF_VALID_ARGS)
+{
+	if (n->flags & NODE_NOFILL)
+		mandoc_msg(MANDOCERR_NF_SKIP, n->line, n->pos, "nf");
+}
+
+static void
+roff_valid_sp(ROFF_VALID_ARGS)
+{
+	struct roff_node	*np;
+
+	if ((np = roff_node_prev(n)) == NULL)
+		return;
+
+	switch (np->tok) {
+	case ROFF_br:
+		mandoc_msg(MANDOCERR_PAR_SKIP,
+		    np->line, np->pos, "br before sp");
+		roff_node_delete(man, np);
+		break;
+	case MDOC_Pp:
+		mandoc_msg(MANDOCERR_PAR_SKIP,
+		    n->line, n->pos, "sp after Pp");
+		roff_node_delete(man, n);
+		break;
+	default:
+		break;
+	}
+}
diff --git a/usr.bin/mandoc/st.c b/usr.bin/mandoc/st.c
new file mode 100644
index 0000000..27039fe
--- /dev/null
+++ b/usr.bin/mandoc/st.c
@@ -0,0 +1,80 @@
+/*	$OpenBSD: st.c,v 1.13 2018/12/14 01:17:46 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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 <sys/types.h>
+
+#include <stdio.h>
+#include <string.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "libmdoc.h"
+
+#define LINE(x, y) \
+	if (0 == strcmp(p, x)) return(y);
+
+const char *
+mdoc_a2st(const char *p)
+{
+LINE("-p1003.1-88",	"IEEE Std 1003.1-1988 (\\(lqPOSIX.1\\(rq)")
+LINE("-p1003.1-90",	"IEEE Std 1003.1-1990 (\\(lqPOSIX.1\\(rq)")
+LINE("-p1003.1-96",	"ISO/IEC 9945-1:1996 (\\(lqPOSIX.1\\(rq)")
+LINE("-p1003.1-2001",	"IEEE Std 1003.1-2001 (\\(lqPOSIX.1\\(rq)")
+LINE("-p1003.1-2004",	"IEEE Std 1003.1-2004 (\\(lqPOSIX.1\\(rq)")
+LINE("-p1003.1-2008",	"IEEE Std 1003.1-2008 (\\(lqPOSIX.1\\(rq)")
+LINE("-p1003.1",	"IEEE Std 1003.1 (\\(lqPOSIX.1\\(rq)")
+LINE("-p1003.1b",	"IEEE Std 1003.1b (\\(lqPOSIX.1b\\(rq)")
+LINE("-p1003.1b-93",	"IEEE Std 1003.1b-1993 (\\(lqPOSIX.1b\\(rq)")
+LINE("-p1003.1c-95",	"IEEE Std 1003.1c-1995 (\\(lqPOSIX.1c\\(rq)")
+LINE("-p1003.1g-2000",	"IEEE Std 1003.1g-2000 (\\(lqPOSIX.1g\\(rq)")
+LINE("-p1003.1i-95",	"IEEE Std 1003.1i-1995 (\\(lqPOSIX.1i\\(rq)")
+LINE("-p1003.2",	"IEEE Std 1003.2 (\\(lqPOSIX.2\\(rq)")
+LINE("-p1003.2-92",	"IEEE Std 1003.2-1992 (\\(lqPOSIX.2\\(rq)")
+LINE("-p1003.2a-92",	"IEEE Std 1003.2a-1992 (\\(lqPOSIX.2\\(rq)")
+LINE("-isoC",		"ISO/IEC 9899:1990 (\\(lqISO\\~C90\\(rq)")
+LINE("-isoC-90",	"ISO/IEC 9899:1990 (\\(lqISO\\~C90\\(rq)")
+LINE("-isoC-amd1",	"ISO/IEC 9899/AMD1:1995 (\\(lqISO\\~C90, Amendment 1\\(rq)")
+LINE("-isoC-tcor1",	"ISO/IEC 9899/TCOR1:1994 (\\(lqISO\\~C90, Technical Corrigendum 1\\(rq)")
+LINE("-isoC-tcor2",	"ISO/IEC 9899/TCOR2:1995 (\\(lqISO\\~C90, Technical Corrigendum 2\\(rq)")
+LINE("-isoC-99",	"ISO/IEC 9899:1999 (\\(lqISO\\~C99\\(rq)")
+LINE("-isoC-2011",	"ISO/IEC 9899:2011 (\\(lqISO\\~C11\\(rq)")
+LINE("-iso9945-1-90",	"ISO/IEC 9945-1:1990 (\\(lqPOSIX.1\\(rq)")
+LINE("-iso9945-1-96",	"ISO/IEC 9945-1:1996 (\\(lqPOSIX.1\\(rq)")
+LINE("-iso9945-2-93",	"ISO/IEC 9945-2:1993 (\\(lqPOSIX.2\\(rq)")
+LINE("-ansiC",		"ANSI X3.159-1989 (\\(lqANSI\\~C89\\(rq)")
+LINE("-ansiC-89",	"ANSI X3.159-1989 (\\(lqANSI\\~C89\\(rq)")
+LINE("-ieee754",	"IEEE Std 754-1985")
+LINE("-iso8802-3",	"ISO 8802-3: 1989")
+LINE("-iso8601",	"ISO 8601")
+LINE("-ieee1275-94",	"IEEE Std 1275-1994 (\\(lqOpen Firmware\\(rq)")
+LINE("-xpg3",		"X/Open Portability Guide Issue\\~3 (\\(lqXPG3\\(rq)")
+LINE("-xpg4",		"X/Open Portability Guide Issue\\~4 (\\(lqXPG4\\(rq)")
+LINE("-xpg4.2",		"X/Open Portability Guide Issue\\~4, Version\\~2 (\\(lqXPG4.2\\(rq)")
+LINE("-xbd5",		"X/Open Base Definitions Issue\\~5 (\\(lqXBD5\\(rq)")
+LINE("-xcu5",		"X/Open Commands and Utilities Issue\\~5 (\\(lqXCU5\\(rq)")
+LINE("-xsh4.2",		"X/Open System Interfaces and Headers Issue\\~4, Version\\~2 (\\(lqXSH4.2\\(rq)")
+LINE("-xsh5",		"X/Open System Interfaces and Headers Issue\\~5 (\\(lqXSH5\\(rq)")
+LINE("-xns5",		"X/Open Networking Services Issue\\~5 (\\(lqXNS5\\(rq)")
+LINE("-xns5.2",		"X/Open Networking Services Issue\\~5.2 (\\(lqXNS5.2\\(rq)")
+LINE("-xcurses4.2",	"X/Open Curses Issue\\~4, Version\\~2 (\\(lqXCURSES4.2\\(rq)")
+LINE("-susv1",		"Version\\~1 of the Single UNIX Specification (\\(lqSUSv1\\(rq)")
+LINE("-susv2",		"Version\\~2 of the Single UNIX Specification (\\(lqSUSv2\\(rq)")
+LINE("-susv3",		"Version\\~3 of the Single UNIX Specification (\\(lqSUSv3\\(rq)")
+LINE("-susv4",		"Version\\~4 of the Single UNIX Specification (\\(lqSUSv4\\(rq)")
+LINE("-svid4",		"System\\~V Interface Definition, Fourth Edition (\\(lqSVID4\\(rq)")
+
+	return NULL;
+}
diff --git a/usr.bin/mandoc/tag.c b/usr.bin/mandoc/tag.c
new file mode 100644
index 0000000..c580d3b
--- /dev/null
+++ b/usr.bin/mandoc/tag.c
@@ -0,0 +1,326 @@
+/* $OpenBSD: tag.c,v 1.36 2020/04/19 16:26:11 schwarze Exp $ */
+/*
+ * Copyright (c) 2015,2016,2018,2019,2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ *
+ * Functions to tag syntax tree nodes.
+ * For internal use by mandoc(1) validation modules only.
+ */
+#include <sys/cdefs.h>
+#include <sys/types.h>
+
+#include <assert.h>
+#include <limits.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mandoc_aux.h"
+#include "mandoc_ohash.h"
+#include "roff.h"
+#include "mdoc.h"
+#include "roff_int.h"
+#include "tag.h"
+
+struct tag_entry {
+	struct roff_node **nodes;
+	size_t	 maxnodes;
+	size_t	 nnodes;
+	int	 prio;
+	char	 s[];
+};
+
+static void		 tag_move_href(struct roff_man *,
+				struct roff_node *, const char *);
+static void		 tag_move_id(struct roff_node *);
+
+static struct ohash	 tag_data;
+
+
+/*
+ * Set up the ohash table to collect nodes
+ * where various marked-up terms are documented.
+ */
+void
+tag_alloc(void)
+{
+	mandoc_ohash_init(&tag_data, 4, offsetof(struct tag_entry, s));
+}
+
+void
+tag_free(void)
+{
+	struct tag_entry	*entry;
+	unsigned int		 slot;
+
+	if (tag_data.info.free == NULL)
+		return;
+	entry = ohash_first(&tag_data, &slot);
+	while (entry != NULL) {
+		free(entry->nodes);
+		free(entry);
+		entry = ohash_next(&tag_data, &slot);
+	}
+	ohash_delete(&tag_data);
+	tag_data.info.free = NULL;
+}
+
+/*
+ * Set a node where a term is defined,
+ * unless it is already defined at a lower priority.
+ */
+void
+tag_put(const char *s, int prio, struct roff_node *n)
+{
+	struct tag_entry	*entry;
+	struct roff_node	*nold;
+	const char		*se;
+	size_t			 len;
+	unsigned int		 slot;
+
+	assert(prio <= TAG_FALLBACK);
+
+	if (s == NULL) {
+		if (n->child == NULL || n->child->type != ROFFT_TEXT)
+			return;
+		s = n->child->string;
+		switch (s[0]) {
+		case '-':
+			s++;
+			break;
+		case '\\':
+			switch (s[1]) {
+			case '&':
+			case '-':
+			case 'e':
+				s += 2;
+				break;
+			default:
+				break;
+			}
+			break;
+		default:
+			break;
+		}
+	}
+
+	/*
+	 * Skip whitespace and escapes and whatever follows,
+	 * and if there is any, downgrade the priority.
+	 */
+
+	len = strcspn(s, " \t\\");
+	if (len == 0)
+		return;
+
+	se = s + len;
+	if (*se != '\0' && prio < TAG_WEAK)
+		prio = TAG_WEAK;
+
+	slot = ohash_qlookupi(&tag_data, s, &se);
+	entry = ohash_find(&tag_data, slot);
+
+	/* Build a new entry. */
+
+	if (entry == NULL) {
+		entry = mandoc_malloc(sizeof(*entry) + len + 1);
+		memcpy(entry->s, s, len);
+		entry->s[len] = '\0';
+		entry->nodes = NULL;
+		entry->maxnodes = entry->nnodes = 0;
+		ohash_insert(&tag_data, slot, entry);
+	}
+
+	/*
+	 * Lower priority numbers take precedence.
+	 * If a better entry is already present, ignore the new one.
+	 */
+
+	else if (entry->prio < prio)
+			return;
+
+	/*
+	 * If the existing entry is worse, clear it.
+	 * In addition, a tag with priority TAG_FALLBACK
+	 * is only used if the tag occurs exactly once.
+	 */
+
+	else if (entry->prio > prio || prio == TAG_FALLBACK) {
+		while (entry->nnodes > 0) {
+			nold = entry->nodes[--entry->nnodes];
+			nold->flags &= ~NODE_ID;
+			free(nold->tag);
+			nold->tag = NULL;
+		}
+		if (prio == TAG_FALLBACK) {
+			entry->prio = TAG_DELETE;
+			return;
+		}
+	}
+
+	/* Remember the new node. */
+
+	if (entry->maxnodes == entry->nnodes) {
+		entry->maxnodes += 4;
+		entry->nodes = mandoc_reallocarray(entry->nodes,
+		    entry->maxnodes, sizeof(*entry->nodes));
+	}
+	entry->nodes[entry->nnodes++] = n;
+	entry->prio = prio;
+	n->flags |= NODE_ID;
+	if (n->child == NULL || n->child->string != s || *se != '\0') {
+		assert(n->tag == NULL);
+		n->tag = mandoc_strndup(s, len);
+	}
+}
+
+int
+tag_exists(const char *tag)
+{
+	return ohash_find(&tag_data, ohash_qlookup(&tag_data, tag)) != NULL;
+}
+
+/*
+ * For in-line elements, move the link target
+ * to the enclosing paragraph when appropriate.
+ */
+static void
+tag_move_id(struct roff_node *n)
+{
+	struct roff_node *np;
+
+	np = n;
+	for (;;) {
+		if (np->prev != NULL)
+			np = np->prev;
+		else if ((np = np->parent) == NULL)
+			return;
+		switch (np->tok) {
+		case MDOC_It:
+			switch (np->parent->parent->norm->Bl.type) {
+			case LIST_column:
+				/* Target the ROFFT_BLOCK = <tr>. */
+				np = np->parent;
+				break;
+			case LIST_diag:
+			case LIST_hang:
+			case LIST_inset:
+			case LIST_ohang:
+			case LIST_tag:
+				/* Target the ROFFT_HEAD = <dt>. */
+				np = np->parent->head;
+				break;
+			default:
+				/* Target the ROFF_BODY = <li>. */
+				break;
+			}
+			/* FALLTHROUGH */
+		case MDOC_Pp:	/* Target the ROFFT_ELEM = <p>. */
+			if (np->tag == NULL) {
+				np->tag = mandoc_strdup(n->tag == NULL ?
+				    n->child->string : n->tag);
+				np->flags |= NODE_ID;
+				n->flags &= ~NODE_ID;
+			}
+			return;
+		case MDOC_Sh:
+		case MDOC_Ss:
+		case MDOC_Bd:
+		case MDOC_Bl:
+		case MDOC_D1:
+		case MDOC_Dl:
+		case MDOC_Rs:
+			/* Do not move past major blocks. */
+			return;
+		default:
+			/*
+			 * Move past in-line content and partial
+			 * blocks, for example .It Xo or .It Bq Er.
+			 */
+			break;
+		}
+	}
+}
+
+/*
+ * When a paragraph is tagged and starts with text,
+ * move the permalink to the first few words.
+ */
+static void
+tag_move_href(struct roff_man *man, struct roff_node *n, const char *tag)
+{
+	char	*cp;
+
+	if (n == NULL || n->type != ROFFT_TEXT ||
+	    *n->string == '\0' || *n->string == ' ')
+		return;
+
+	cp = n->string;
+	while (cp != NULL && cp - n->string < 5)
+		cp = strchr(cp + 1, ' ');
+
+	/* If the first text node is longer, split it. */
+
+	if (cp != NULL && cp[1] != '\0') {
+		man->last = n;
+		man->next = ROFF_NEXT_SIBLING;
+		roff_word_alloc(man, n->line,
+		    n->pos + (cp - n->string), cp + 1);
+		man->last->flags = n->flags & ~NODE_LINE;
+		*cp = '\0';
+	}
+
+	assert(n->tag == NULL);
+	n->tag = mandoc_strdup(tag);
+	n->flags |= NODE_HREF;
+}
+
+/*
+ * When all tags have been set, decide where to put
+ * the associated permalinks, and maybe move some tags
+ * to the beginning of the respective paragraphs.
+ */
+void
+tag_postprocess(struct roff_man *man, struct roff_node *n)
+{
+	if (n->flags & NODE_ID) {
+		switch (n->tok) {
+		case MDOC_Pp:
+			tag_move_href(man, n->next, n->tag);
+			break;
+		case MDOC_Bd:
+		case MDOC_D1:
+		case MDOC_Dl:
+			tag_move_href(man, n->child, n->tag);
+			break;
+		case MDOC_Bl:
+			/* XXX No permalink for now. */
+			break;
+		default:
+			if (n->type == ROFFT_ELEM || n->tok == MDOC_Fo)
+				tag_move_id(n);
+			if (n->tok != MDOC_Tg)
+				n->flags |= NODE_HREF;
+			else if ((n->flags & NODE_ID) == 0) {
+				n->flags |= NODE_NOPRT;
+				free(n->tag);
+				n->tag = NULL;
+			}
+			break;
+		}
+	}
+	for (n = n->child; n != NULL; n = n->next)
+		tag_postprocess(man, n);
+}
diff --git a/usr.bin/mandoc/tag.h b/usr.bin/mandoc/tag.h
new file mode 100644
index 0000000..7fa7504
--- /dev/null
+++ b/usr.bin/mandoc/tag.h
@@ -0,0 +1,35 @@
+/* $OpenBSD: tag.h,v 1.14 2020/04/18 20:28:46 schwarze Exp $ */
+/*
+ * Copyright (c) 2015, 2018, 2019, 2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ *
+ * Internal interfaces to tag syntax tree nodes.
+ * For use by mandoc(1) validation modules only.
+ */
+
+/*
+ * Tagging priorities.
+ * Lower numbers indicate higher importance.
+ */
+#define	TAG_MANUAL	1		/* Set with a .Tg macro. */
+#define	TAG_STRONG	2		/* Good automatic tagging. */
+#define	TAG_WEAK	(INT_MAX - 2)	/* Dubious automatic tagging. */
+#define	TAG_FALLBACK	(INT_MAX - 1)	/* Tag only used if unique. */
+#define	TAG_DELETE	(INT_MAX)	/* Tag not used at all. */
+
+void		 tag_alloc(void);
+int		 tag_exists(const char *);
+void		 tag_put(const char *, int, struct roff_node *);
+void		 tag_postprocess(struct roff_man *, struct roff_node *);
+void		 tag_free(void);
diff --git a/usr.bin/mandoc/tbl.c b/usr.bin/mandoc/tbl.c
new file mode 100644
index 0000000..a3da958
--- /dev/null
+++ b/usr.bin/mandoc/tbl.c
@@ -0,0 +1,181 @@
+/*	$OpenBSD: tbl.c,v 1.27 2018/12/14 06:33:03 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2011, 2015 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "mandoc_aux.h"
+#include "mandoc.h"
+#include "tbl.h"
+#include "libmandoc.h"
+#include "tbl_parse.h"
+#include "tbl_int.h"
+
+
+void
+tbl_read(struct tbl_node *tbl, int ln, const char *p, int pos)
+{
+	const char	*cp;
+	int		 active;
+
+	/*
+	 * In the options section, proceed to the layout section
+	 * after a semicolon, or right away if there is no semicolon.
+	 * Ignore semicolons in arguments.
+	 */
+
+	if (tbl->part == TBL_PART_OPTS) {
+		tbl->part = TBL_PART_LAYOUT;
+		active = 1;
+		for (cp = p + pos; *cp != '\0'; cp++) {
+			switch (*cp) {
+			case '(':
+				active = 0;
+				continue;
+			case ')':
+				active = 1;
+				continue;
+			case ';':
+				if (active)
+					break;
+				continue;
+			default:
+				continue;
+			}
+			break;
+		}
+		if (*cp == ';') {
+			tbl_option(tbl, ln, p, &pos);
+			if (p[pos] == '\0')
+				return;
+		}
+	}
+
+	/* Process the other section types.  */
+
+	switch (tbl->part) {
+	case TBL_PART_LAYOUT:
+		tbl_layout(tbl, ln, p, pos);
+		break;
+	case TBL_PART_CDATA:
+		tbl_cdata(tbl, ln, p, pos);
+		break;
+	default:
+		tbl_data(tbl, ln, p, pos);
+		break;
+	}
+}
+
+struct tbl_node *
+tbl_alloc(int pos, int line, struct tbl_node *last_tbl)
+{
+	struct tbl_node	*tbl;
+
+	tbl = mandoc_calloc(1, sizeof(*tbl));
+	if (last_tbl != NULL)
+		last_tbl->next = tbl;
+	tbl->line = line;
+	tbl->pos = pos;
+	tbl->part = TBL_PART_OPTS;
+	tbl->opts.tab = '\t';
+	tbl->opts.decimal = '.';
+	return tbl;
+}
+
+void
+tbl_free(struct tbl_node *tbl)
+{
+	struct tbl_node	*old_tbl;
+	struct tbl_row	*rp;
+	struct tbl_cell	*cp;
+	struct tbl_span	*sp;
+	struct tbl_dat	*dp;
+
+	while (tbl != NULL) {
+		while ((rp = tbl->first_row) != NULL) {
+			tbl->first_row = rp->next;
+			while (rp->first != NULL) {
+				cp = rp->first;
+				rp->first = cp->next;
+				free(cp->wstr);
+				free(cp);
+			}
+			free(rp);
+		}
+		while ((sp = tbl->first_span) != NULL) {
+			tbl->first_span = sp->next;
+			while (sp->first != NULL) {
+				dp = sp->first;
+				sp->first = dp->next;
+				free(dp->string);
+				free(dp);
+			}
+			free(sp);
+		}
+		old_tbl = tbl;
+		tbl = tbl->next;
+		free(old_tbl);
+	}
+}
+
+void
+tbl_restart(int line, int pos, struct tbl_node *tbl)
+{
+	if (tbl->part == TBL_PART_CDATA)
+		mandoc_msg(MANDOCERR_TBLDATA_BLK, line, pos, "T&");
+
+	tbl->part = TBL_PART_LAYOUT;
+	tbl->line = line;
+	tbl->pos = pos;
+}
+
+struct tbl_span *
+tbl_span(struct tbl_node *tbl)
+{
+	struct tbl_span	 *span;
+
+	span = tbl->current_span ? tbl->current_span->next
+				 : tbl->first_span;
+	if (span != NULL)
+		tbl->current_span = span;
+	return span;
+}
+
+int
+tbl_end(struct tbl_node *tbl, int still_open)
+{
+	struct tbl_span *sp;
+
+	if (still_open)
+		mandoc_msg(MANDOCERR_BLK_NOEND, tbl->line, tbl->pos, "TS");
+	else if (tbl->part == TBL_PART_CDATA)
+		mandoc_msg(MANDOCERR_TBLDATA_BLK, tbl->line, tbl->pos, "TE");
+
+	sp = tbl->first_span;
+	while (sp != NULL && sp->first == NULL)
+		sp = sp->next;
+	if (sp == NULL) {
+		mandoc_msg(MANDOCERR_TBLDATA_NONE, tbl->line, tbl->pos, NULL);
+		return 0;
+	}
+	return 1;
+}
diff --git a/usr.bin/mandoc/tbl.h b/usr.bin/mandoc/tbl.h
new file mode 100644
index 0000000..2e77ac1
--- /dev/null
+++ b/usr.bin/mandoc/tbl.h
@@ -0,0 +1,122 @@
+/*	$OpenBSD: tbl.h,v 1.5 2018/12/12 21:54:30 schwarze Exp $ */
+/*
+ * Copyright (c) 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2015, 2017, 2018 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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. */
+	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. */
+	char		 *wstr;     /* Min width represented as a string. */
+	size_t		  width;    /* Minimum column width. */
+	size_t		  spacing;  /* To the right of the column. */
+	int		  vert;     /* Width of subsequent vertical line. */
+	int		  col;      /* Column number, starting from 0. */
+	int		  flags;
+#define	TBL_CELL_BOLD	 (1 << 0)   /* b, B, fB */
+#define	TBL_CELL_ITALIC	 (1 << 1)   /* i, I, fI */
+#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 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. */
+};
+
+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/usr.bin/mandoc/tbl_data.c b/usr.bin/mandoc/tbl_data.c
new file mode 100644
index 0000000..b2556d9
--- /dev/null
+++ b/usr.bin/mandoc/tbl_data.c
@@ -0,0 +1,300 @@
+/*	$OpenBSD: tbl_data.c,v 1.40 2020/01/11 20:48:13 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2011,2015,2017,2018,2019 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "mandoc_aux.h"
+#include "mandoc.h"
+#include "tbl.h"
+#include "libmandoc.h"
+#include "tbl_int.h"
+
+static	void		 getdata(struct tbl_node *, struct tbl_span *,
+				int, const char *, int *);
+static	struct tbl_span	*newspan(struct tbl_node *, int,
+				struct tbl_row *);
+
+
+static void
+getdata(struct tbl_node *tbl, struct tbl_span *dp,
+		int ln, const char *p, int *pos)
+{
+	struct tbl_dat	*dat, *pdat;
+	struct tbl_cell	*cp;
+	struct tbl_span	*pdp;
+	int		 sv;
+
+	/*
+	 * Determine the length of the string in the cell
+	 * and advance the parse point to the end of the cell.
+	 */
+
+	sv = *pos;
+	while (p[*pos] != '\0' && p[*pos] != tbl->opts.tab)
+		(*pos)++;
+
+	/* Advance to the next layout cell, skipping spanners. */
+
+	cp = dp->last == NULL ? dp->layout->first : dp->last->layout->next;
+	while (cp != NULL && cp->pos == TBL_CELL_SPAN)
+		cp = cp->next;
+
+	/*
+	 * If the current layout row is out of cells, allocate
+	 * a new cell if another row of the table has at least
+	 * this number of columns, or discard the input if we
+	 * are beyond the last column of the table as a whole.
+	 */
+
+	if (cp == NULL) {
+		if (dp->layout->last->col + 1 < dp->opts->cols) {
+			cp = mandoc_calloc(1, sizeof(*cp));
+			cp->pos = TBL_CELL_LEFT;
+			cp->spacing = SIZE_MAX;
+			dp->layout->last->next = cp;
+			cp->col = dp->layout->last->col + 1;
+			dp->layout->last = cp;
+		} else {
+			mandoc_msg(MANDOCERR_TBLDATA_EXTRA,
+			    ln, sv, "%s", p + sv);
+			while (p[*pos] != '\0')
+				(*pos)++;
+			return;
+		}
+	}
+
+	dat = mandoc_malloc(sizeof(*dat));
+	dat->layout = cp;
+	dat->next = NULL;
+	dat->string = NULL;
+	dat->hspans = 0;
+	dat->vspans = 0;
+	dat->block = 0;
+	dat->pos = TBL_DATA_NONE;
+
+	/*
+	 * Increment the number of vertical spans in a data cell above,
+	 * if this cell vertically extends one or more cells above.
+	 * The iteration must be done over data rows,
+	 * not over layout rows, because one layout row
+	 * can be reused for more than one data row.
+	 */
+
+	if (cp->pos == TBL_CELL_DOWN ||
+	    (*pos - sv == 2 && p[sv] == '\\' && p[sv + 1] == '^')) {
+		pdp = dp;
+		while ((pdp = pdp->prev) != NULL) {
+			pdat = pdp->first;
+			while (pdat != NULL &&
+			    pdat->layout->col < dat->layout->col)
+				pdat = pdat->next;
+			if (pdat == NULL)
+				break;
+			if (pdat->layout->pos != TBL_CELL_DOWN &&
+			    strcmp(pdat->string, "\\^") != 0) {
+				pdat->vspans++;
+				break;
+			}
+		}
+	}
+
+	/*
+	 * Count the number of horizontal spans to the right of this cell.
+	 * This is purely a matter of the layout, independent of the data.
+	 */
+
+	for (cp = cp->next; cp != NULL; cp = cp->next)
+		if (cp->pos == TBL_CELL_SPAN)
+			dat->hspans++;
+		else
+			break;
+
+	/* Append the new data cell to the data row. */
+
+	if (dp->last == NULL)
+		dp->first = dat;
+	else
+		dp->last->next = dat;
+	dp->last = dat;
+
+	/*
+	 * Check for a continued-data scope opening.  This consists of a
+	 * trailing `T{' at the end of the line.  Subsequent lines,
+	 * until a standalone `T}', are included in our cell.
+	 */
+
+	if (*pos - sv == 2 && p[sv] == 'T' && p[sv + 1] == '{') {
+		tbl->part = TBL_PART_CDATA;
+		return;
+	}
+
+	dat->string = mandoc_strndup(p + sv, *pos - sv);
+
+	if (p[*pos] != '\0')
+		(*pos)++;
+
+	if ( ! strcmp(dat->string, "_"))
+		dat->pos = TBL_DATA_HORIZ;
+	else if ( ! strcmp(dat->string, "="))
+		dat->pos = TBL_DATA_DHORIZ;
+	else if ( ! strcmp(dat->string, "\\_"))
+		dat->pos = TBL_DATA_NHORIZ;
+	else if ( ! strcmp(dat->string, "\\="))
+		dat->pos = TBL_DATA_NDHORIZ;
+	else
+		dat->pos = TBL_DATA_DATA;
+
+	if ((dat->layout->pos == TBL_CELL_HORIZ ||
+	    dat->layout->pos == TBL_CELL_DHORIZ ||
+	    dat->layout->pos == TBL_CELL_DOWN) &&
+	    dat->pos == TBL_DATA_DATA && *dat->string != '\0')
+		mandoc_msg(MANDOCERR_TBLDATA_SPAN,
+		    ln, sv, "%s", dat->string);
+}
+
+void
+tbl_cdata(struct tbl_node *tbl, int ln, const char *p, int pos)
+{
+	struct tbl_dat	*dat;
+	size_t		 sz;
+
+	dat = tbl->last_span->last;
+
+	if (p[pos] == 'T' && p[pos + 1] == '}') {
+		pos += 2;
+		if (p[pos] == tbl->opts.tab) {
+			tbl->part = TBL_PART_DATA;
+			pos++;
+			while (p[pos] != '\0')
+				getdata(tbl, tbl->last_span, ln, p, &pos);
+			return;
+		} else if (p[pos] == '\0') {
+			tbl->part = TBL_PART_DATA;
+			return;
+		}
+
+		/* Fallthrough: T} is part of a word. */
+	}
+
+	dat->pos = TBL_DATA_DATA;
+	dat->block = 1;
+
+	if (dat->string != NULL) {
+		sz = strlen(p + pos) + strlen(dat->string) + 2;
+		dat->string = mandoc_realloc(dat->string, sz);
+		(void)strlcat(dat->string, " ", sz);
+		(void)strlcat(dat->string, p + pos, sz);
+	} else
+		dat->string = mandoc_strdup(p + pos);
+
+	if (dat->layout->pos == TBL_CELL_DOWN)
+		mandoc_msg(MANDOCERR_TBLDATA_SPAN,
+		    ln, pos, "%s", dat->string);
+}
+
+static struct tbl_span *
+newspan(struct tbl_node *tbl, int line, struct tbl_row *rp)
+{
+	struct tbl_span	*dp;
+
+	dp = mandoc_calloc(1, sizeof(*dp));
+	dp->line = line;
+	dp->opts = &tbl->opts;
+	dp->layout = rp;
+	dp->prev = tbl->last_span;
+
+	if (dp->prev == NULL) {
+		tbl->first_span = dp;
+		tbl->current_span = NULL;
+	} else
+		dp->prev->next = dp;
+	tbl->last_span = dp;
+
+	return dp;
+}
+
+void
+tbl_data(struct tbl_node *tbl, int ln, const char *p, int pos)
+{
+	struct tbl_row	*rp;
+	struct tbl_cell	*cp;
+	struct tbl_span	*sp;
+
+	rp = (sp = tbl->last_span) == NULL ? tbl->first_row :
+	    sp->pos == TBL_SPAN_DATA && sp->layout->next != NULL ?
+	    sp->layout->next : sp->layout;
+
+	assert(rp != NULL);
+
+	if (p[1] == '\0') {
+		switch (p[0]) {
+		case '.':
+			/*
+			 * Empty request lines must be handled here
+			 * and cannot be discarded in roff_parseln()
+			 * because in the layout section, they
+			 * are significant and end the layout.
+			 */
+			return;
+		case '_':
+			sp = newspan(tbl, ln, rp);
+			sp->pos = TBL_SPAN_HORIZ;
+			return;
+		case '=':
+			sp = newspan(tbl, ln, rp);
+			sp->pos = TBL_SPAN_DHORIZ;
+			return;
+		default:
+			break;
+		}
+	}
+
+	/*
+	 * If the layout row contains nothing but horizontal lines,
+	 * allocate an empty span for it and assign the current span
+	 * to the next layout row accepting data.
+	 */
+
+	while (rp->next != NULL) {
+		if (rp->last->col + 1 < tbl->opts.cols)
+			break;
+		for (cp = rp->first; cp != NULL; cp = cp->next)
+			if (cp->pos != TBL_CELL_HORIZ &&
+			    cp->pos != TBL_CELL_DHORIZ)
+				break;
+		if (cp != NULL)
+			break;
+		sp = newspan(tbl, ln, rp);
+		sp->pos = TBL_SPAN_DATA;
+		rp = rp->next;
+	}
+
+	/* Process a real data row. */
+
+	sp = newspan(tbl, ln, rp);
+	sp->pos = TBL_SPAN_DATA;
+	while (p[pos] != '\0')
+		getdata(tbl, sp, ln, p, &pos);
+}
diff --git a/usr.bin/mandoc/tbl_html.c b/usr.bin/mandoc/tbl_html.c
new file mode 100644
index 0000000..cbc4bfa
--- /dev/null
+++ b/usr.bin/mandoc/tbl_html.c
@@ -0,0 +1,255 @@
+/*	$OpenBSD: tbl_html.c,v 1.28 2019/03/17 18:20:07 schwarze Exp $ */
+/*
+ * Copyright (c) 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2015, 2017, 2018 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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;
+	struct tag		*tt;
+	const char		*hspans, *vspans, *halign, *valign;
+	const char		*bborder, *lborder, *rborder;
+	char			 hbuf[4], vbuf[4];
+	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)
+		return;
+
+	/* 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 <td> elements for continuations
+		 * of spanned cells.  Larger <td> 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->string != NULL)
+			print_text(h, dp->string);
+	}
+
+	print_tagq(h, tt);
+
+	h->flags &= ~HTML_NONOSPACE;
+
+	if (sp->next == NULL) {
+		assert(h->tbl.cols);
+		free(h->tbl.cols);
+		h->tbl.cols = NULL;
+		print_tblclose(h);
+	}
+}
diff --git a/usr.bin/mandoc/tbl_int.h b/usr.bin/mandoc/tbl_int.h
new file mode 100644
index 0000000..299ceaa
--- /dev/null
+++ b/usr.bin/mandoc/tbl_int.h
@@ -0,0 +1,47 @@
+/*	$OpenBSD: tbl_int.h,v 1.2 2018/12/14 06:33:03 schwarze Exp $ */
+/*
+ * Copyright (c) 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2011,2013,2015,2017,2018 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ *
+ * Internal interfaces of the tbl(7) parser.
+ * For use inside the tbl(7) parser only.
+ */
+
+enum	tbl_part {
+	TBL_PART_OPTS,    /* In the first line, ends with semicolon. */
+	TBL_PART_LAYOUT,  /* In the layout section, ends with full stop. */
+	TBL_PART_DATA,    /* In the data section, ends with TE. */
+	TBL_PART_CDATA    /* In a T{ block, ends with T} */
+};
+
+struct	tbl_node {
+	struct tbl_opts	  opts;		/* Options for the whole table. */
+	struct tbl_node	 *next;		/* Next table. */
+	struct tbl_row	 *first_row;	/* First layout row. */
+	struct tbl_row	 *last_row;	/* Last layout row. */
+	struct tbl_span	 *first_span;	/* First data row. */
+	struct tbl_span	 *current_span;	/* Data row being parsed. */
+	struct tbl_span	 *last_span;	/* Last data row. */
+	int		  line;		/* Line number in input file. */
+	int		  pos;		/* Column number in input file. */
+	enum tbl_part	  part;		/* Table section being parsed. */
+};
+
+
+void		 tbl_option(struct tbl_node *, int, const char *, int *);
+void		 tbl_layout(struct tbl_node *, int, const char *, int);
+void		 tbl_data(struct tbl_node *, int, const char *, int);
+void		 tbl_cdata(struct tbl_node *, int, const char *, int);
+void		 tbl_reset(struct tbl_node *);
diff --git a/usr.bin/mandoc/tbl_layout.c b/usr.bin/mandoc/tbl_layout.c
new file mode 100644
index 0000000..aae36d9
--- /dev/null
+++ b/usr.bin/mandoc/tbl_layout.c
@@ -0,0 +1,371 @@
+/*	$OpenBSD: tbl_layout.c,v 1.35 2018/12/14 05:17:45 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2012, 2014, 2015, 2017 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#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;
+	size_t		 sz;
+
+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])) {
+		cp->spacing = strtoull(p + *pos, &endptr, 10);
+		*pos = endptr - p;
+		goto mod;
+	}
+
+	switch (tolower((unsigned char)p[(*pos)++])) {
+	case 'b':
+		cp->flags |= TBL_CELL_BOLD;
+		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->flags |= TBL_CELL_ITALIC;
+		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':
+		sz = 0;
+		if (p[*pos] == '(') {
+			(*pos)++;
+			while (p[*pos + sz] != '\0' && p[*pos + sz] != ')')
+				sz++;
+		} else
+			while (isdigit((unsigned char)p[*pos + sz]))
+				sz++;
+		if (sz) {
+			free(cp->wstr);
+			cp->wstr = mandoc_strndup(p + *pos, sz);
+			*pos += sz;
+			if (p[*pos] == ')')
+				(*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;
+	}
+
+	/* Ignore parenthised font names for now. */
+
+	if (p[*pos] == '(')
+		goto mod;
+
+	/* Support only one-character font-names for now. */
+
+	if (p[*pos] == '\0' || (p[*pos + 1] != ' ' && p[*pos + 1] != '.')) {
+		mandoc_msg(MANDOCERR_FT_BAD,
+		    ln, *pos, "TS %s", p + *pos - 1);
+		if (p[*pos] != '\0')
+			(*pos)++;
+		if (p[*pos] != '\0')
+			(*pos)++;
+		goto mod;
+	}
+
+	switch (p[(*pos)++]) {
+	case '3':
+	case 'B':
+		cp->flags |= TBL_CELL_BOLD;
+		goto mod;
+	case '2':
+	case 'I':
+		cp->flags |= TBL_CELL_ITALIC;
+		goto mod;
+	case '1':
+	case 'R':
+		goto mod;
+	default:
+		mandoc_msg(MANDOCERR_FT_BAD,
+		    ln, *pos - 1, "TS f%c", p[*pos - 1]);
+		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->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/usr.bin/mandoc/tbl_opts.c b/usr.bin/mandoc/tbl_opts.c
new file mode 100644
index 0000000..8f1e77c
--- /dev/null
+++ b/usr.bin/mandoc/tbl_opts.c
@@ -0,0 +1,171 @@
+/*	$OpenBSD: tbl_opts.c,v 1.16 2018/12/14 05:17:45 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2015 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mandoc.h"
+#include "tbl.h"
+#include "libmandoc.h"
+#include "tbl_int.h"
+
+#define	KEY_DPOINT	0
+#define	KEY_DELIM	1
+#define	KEY_LINESIZE	2
+#define	KEY_TAB		3
+
+struct	tbl_phrase {
+	const char	*name;
+	int		 key;
+};
+
+static	const struct tbl_phrase keys[] = {
+	{"decimalpoint", 0},
+	{"delim",	 0},
+	{"linesize",	 0},
+	{"tab",		 0},
+	{"allbox",	 TBL_OPT_ALLBOX | TBL_OPT_BOX},
+	{"box",		 TBL_OPT_BOX},
+	{"frame",	 TBL_OPT_BOX},
+	{"center",	 TBL_OPT_CENTRE},
+	{"centre",	 TBL_OPT_CENTRE},
+	{"doublebox",	 TBL_OPT_DBOX},
+	{"doubleframe",  TBL_OPT_DBOX},
+	{"expand",	 TBL_OPT_EXPAND},
+	{"nokeep",	 TBL_OPT_NOKEEP},
+	{"nospaces",	 TBL_OPT_NOSPACE},
+	{"nowarn",	 TBL_OPT_NOWARN},
+};
+
+#define KEY_MAXKEYS ((int)(sizeof(keys)/sizeof(keys[0])))
+
+static	void	 arg(struct tbl_node *, int, const char *, int *, int);
+
+
+static void
+arg(struct tbl_node *tbl, int ln, const char *p, int *pos, int key)
+{
+	int		 len, want;
+
+	while (p[*pos] == ' ' || p[*pos] == '\t')
+		(*pos)++;
+
+	/* Arguments are enclosed in parentheses. */
+
+	len = 0;
+	if (p[*pos] == '(') {
+		(*pos)++;
+		while (p[*pos + len] != ')')
+			len++;
+	}
+
+	switch (key) {
+	case KEY_DELIM:
+		mandoc_msg(MANDOCERR_TBLOPT_EQN,
+		    ln, *pos, "%.*s", len, p + *pos);
+		want = 2;
+		break;
+	case KEY_TAB:
+		want = 1;
+		if (len == want)
+			tbl->opts.tab = p[*pos];
+		break;
+	case KEY_LINESIZE:
+		want = 0;
+		break;
+	case KEY_DPOINT:
+		want = 1;
+		if (len == want)
+			tbl->opts.decimal = p[*pos];
+		break;
+	default:
+		abort();
+	}
+
+	if (len == 0)
+		mandoc_msg(MANDOCERR_TBLOPT_NOARG, ln, *pos,
+		    "%s", keys[key].name);
+	else if (want && len != want)
+		mandoc_msg(MANDOCERR_TBLOPT_ARGSZ, ln, *pos,
+		    "%s want %d have %d", keys[key].name, want, len);
+
+	*pos += len;
+	if (p[*pos] == ')')
+		(*pos)++;
+}
+
+/*
+ * Parse one line of options up to the semicolon.
+ * Each option can be preceded by blanks and/or commas,
+ * and some options are followed by arguments.
+ */
+void
+tbl_option(struct tbl_node *tbl, int ln, const char *p, int *offs)
+{
+	int		 i, pos, len;
+
+	pos = *offs;
+	for (;;) {
+		while (p[pos] == ' ' || p[pos] == '\t' || p[pos] == ',')
+			pos++;
+
+		if (p[pos] == ';') {
+			*offs = pos + 1;
+			return;
+		}
+
+		/* Parse one option name. */
+
+		len = 0;
+		while (isalpha((unsigned char)p[pos + len]))
+			len++;
+
+		if (len == 0) {
+			mandoc_msg(MANDOCERR_TBLOPT_ALPHA,
+			    ln, pos, "%c", p[pos]);
+			pos++;
+			continue;
+		}
+
+		/* Look up the option name. */
+
+		i = 0;
+		while (i < KEY_MAXKEYS &&
+		    (strncasecmp(p + pos, keys[i].name, len) ||
+		     keys[i].name[len] != '\0'))
+			i++;
+
+		if (i == KEY_MAXKEYS) {
+			mandoc_msg(MANDOCERR_TBLOPT_BAD,
+			    ln, pos, "%.*s", len, p + pos);
+			pos += len;
+			continue;
+		}
+
+		/* Handle the option. */
+
+		pos += len;
+		if (keys[i].key)
+			tbl->opts.opts |= keys[i].key;
+		else
+			arg(tbl, ln, p, &pos, i);
+	}
+}
diff --git a/usr.bin/mandoc/tbl_parse.h b/usr.bin/mandoc/tbl_parse.h
new file mode 100644
index 0000000..b564490
--- /dev/null
+++ b/usr.bin/mandoc/tbl_parse.h
@@ -0,0 +1,30 @@
+/*	$OpenBSD: tbl_parse.h,v 1.2 2018/12/14 06:33:03 schwarze Exp $ */
+/*
+ * Copyright (c) 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2011, 2017 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ *
+ * External interface of the tbl(7) parser.
+ * For use in the roff(7) and tbl(7) parsers only.
+ */
+
+struct tbl_node;
+struct tbl_span;
+
+struct tbl_node	*tbl_alloc(int, int, struct tbl_node *);
+int		 tbl_end(struct tbl_node *, int);
+void		 tbl_free(struct tbl_node *);
+void		 tbl_read(struct tbl_node *, int, const char *, int);
+void		 tbl_restart(int, int, struct tbl_node *);
+struct tbl_span	*tbl_span(struct tbl_node *);
diff --git a/usr.bin/mandoc/tbl_term.c b/usr.bin/mandoc/tbl_term.c
new file mode 100644
index 0000000..238bf7a
--- /dev/null
+++ b/usr.bin/mandoc/tbl_term.c
@@ -0,0 +1,943 @@
+/*	$OpenBSD: tbl_term.c,v 1.61 2020/01/11 16:24:33 schwarze Exp $ */
+/*
+ * Copyright (c) 2009, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2011-2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 *);
+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_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 *);
+static	void	tbl_number(struct termp *, const struct tbl_opts *,
+			const struct tbl_dat *,
+			const struct roffcol *);
+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;
+
+	/* Inhibit printing of spaces: we do padding ourselves. */
+
+	tp->flags |= TERMP_NOSPACE | TERMP_NONOSPACE;
+	save_offset = tp->tcol->offset;
+
+	/*
+	 * 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);
+
+		/* Tables leak .ta settings to subsequent text. */
+
+		term_tab_set(tp, NULL);
+		coloff = sp->opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX) ||
+		    sp->opts->lvert;
+		for (ic = 0; ic < sp->opts->cols; ic++) {
+			coloff += tp->tbl.cols[ic].width;
+			term_tab_iset(coloff);
+			coloff += tp->tbl.cols[ic].spacing;
+		}
+
+		/* 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;
+			for (ic = 0; ic + 1 < sp->opts->cols; ic++)
+				tsz += tp->tbl.cols[ic].width +
+				    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;
+			offset = offset + tp->tcol->rmargin > tsz ?
+			    (offset + tp->tcol->rmargin - tsz) / 2 : 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++;
+		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;
+			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->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;
+		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;
+			tbl_data(tp, sp->opts, cp, dp, tp->tbl.cols + ic);
+			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);
+		}
+
+		/* 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++) {
+
+				/*
+				 * 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_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);
+				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;
+				}
+				while (tp->viscol < tp->tcol->rmargin +
+				    tp->tbl.cols[ic].spacing / 2)
+					tbl_direct_border(tp,
+					    BHORIZ * lhori, 1);
+
+				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);
+
+				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);
+			}
+		}
+
+		/* 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 {
+					tbl_direct_border(tp,
+					    BHORIZ * lhori, 1);
+				} while (tp->viscol < tp->tcol->offset);
+			}
+			tbl_direct_border(tp, fc, 1);
+		}
+		(*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);
+			tp->skipvsp = 1;
+		}
+		if (tp->enc == TERMENC_ASCII &&
+		    sp->opts->opts & TBL_OPT_DBOX) {
+			tbl_hrule(tp, sp, sp, NULL, TBL_OPT_DBOX);
+			tp->skipvsp = 2;
+		}
+		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. */
+	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) {
+		(*tp->advance)(tp, tp->tcols->offset);
+		tp->viscol = tp->tcols->offset;
+	}
+	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);
+
+	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);
+
+		/*
+		 * 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);
+
+		/*
+		 * 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);
+
+		/* Padding before the start of the next column. */
+
+		if (col->spacing > 4)
+			tbl_direct_border(tp,
+			    BHORIZ * rw, (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);
+		(*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)
+{
+	switch (cp->pos) {
+	case TBL_CELL_HORIZ:
+		tbl_fill_border(tp, BHORIZ, col->width);
+		return;
+	case TBL_CELL_DHORIZ:
+		tbl_fill_border(tp, BHORIZ * 2, 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);
+		return;
+	case TBL_DATA_NDHORIZ:
+	case TBL_DATA_DHORIZ:
+		tbl_fill_border(tp, BHORIZ * 2, 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);
+		break;
+	case TBL_CELL_NUMBER:
+		tbl_number(tp, opts, dp, col);
+		break;
+	case TBL_CELL_DOWN:
+	case TBL_CELL_SPAN:
+		break;
+	default:
+		abort();
+	}
+}
+
+static void
+tbl_fill_string(struct termp *tp, const char *cp, size_t len)
+{
+	size_t	 i, sz;
+
+	sz = term_strlen(tp, cp);
+	for (i = 0; i < len; i += sz)
+		term_word(tp, cp);
+}
+
+static void
+tbl_fill_char(struct termp *tp, char c, size_t len)
+{
+	char	 cp[2];
+
+	cp[0] = c;
+	cp[1] = '\0';
+	tbl_fill_string(tp, cp, len);
+}
+
+static void
+tbl_fill_border(struct termp *tp, int c, 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);
+	} else
+		tbl_fill_char(tp, c, len);
+}
+
+static void
+tbl_direct_border(struct termp *tp, int c, size_t len)
+{
+	size_t	 i, sz;
+
+	c = borders_locale[c];
+	sz = (*tp->width)(tp, c);
+	for (i = 0; i < len; i += sz) {
+		(*tp->letter)(tp, c);
+		tp->viscol += sz;
+	}
+}
+
+static void
+tbl_literal(struct termp *tp, const struct tbl_dat *dp,
+		const struct roffcol *col)
+{
+	size_t		 len, padl, padr, width;
+	int		 ic, hspans;
+
+	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].width + 3;
+
+	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);
+}
+
+static void
+tbl_number(struct termp *tp, const struct tbl_opts *opts,
+		const struct tbl_dat *dp,
+		const struct roffcol *col)
+{
+	const char	*cp, *lastdigit, *lastpoint;
+	size_t		 intsz, padl, totsz;
+	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;
+	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;
+		}
+
+	/* If it is not a number, simply center the string. */
+
+	} else if (col->width > totsz)
+		padl = (col->width - totsz) / 2;
+
+	tbl_fill_char(tp, ASCII_NBRSP, padl);
+	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);
+}
+
+static void
+tbl_word(struct termp *tp, const struct tbl_dat *dp)
+{
+	int		 prev_font;
+
+	prev_font = tp->fonti;
+	if (dp->layout->flags & TBL_CELL_BOLD)
+		term_fontpush(tp, TERMFONT_BOLD);
+	else if (dp->layout->flags & TBL_CELL_ITALIC)
+		term_fontpush(tp, TERMFONT_UNDER);
+
+	term_word(tp, dp->string);
+
+	term_fontpopq(tp, prev_font);
+}
diff --git a/usr.bin/mandoc/term.c b/usr.bin/mandoc/term.c
new file mode 100644
index 0000000..68f78d7
--- /dev/null
+++ b/usr.bin/mandoc/term.c
@@ -0,0 +1,1112 @@
+/*	$OpenBSD: term.c,v 1.141 2019/06/03 20:23:39 schwarze Exp $ */
+/*
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2010-2019 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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,
+				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)
+{
+	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. */
+	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. */
+
+	/*
+	 * 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->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_BRNEVER ? SIZE_MAX :
+		    (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, 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, vbr, vtarget);
+
+		/*
+		 * 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, ' ');
+				continue;
+			case '\n':
+			case ASCII_BREAK:
+				continue;
+			default:
+				break;
+			}
+			break;
+		}
+		if (ic == p->tcol->lastcol)
+			break;
+
+		/*
+		 * At the location of an automtic 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);
+		p->viscol = 0;
+
+		/*
+		 * 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))
+		endline(p);
+}
+
+/*
+ * Store the number of input characters to print in this field in *nbr
+ * and their total visual width to print 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. */
+	size_t	 vis;       /* Visual position of the current character. */
+	size_t	 vn;        /* Visual position of the next character. */
+	int	 breakline; /* Break at the end of this word. */
+	int	 graph;     /* Last character was non-blank. */
+
+	*nbr = *vbr = vis = 0;
+	breakline = graph = 0;
+	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]);
+			continue;
+
+		case '\t':  /* Normal ASCII whitespace. */
+		case ' ':
+		case ASCII_BREAK:  /* Escape \: (breakpoint). */
+			switch (p->tcol->buf[ic]) {
+			case '\t':
+				vn = term_tab_next(vis);
+				break;
+			case ' ':
+				vn = vis + (*p->width)(p, ' ');
+				break;
+			case ASCII_BREAK:
+				vn = vis;
+				break;
+			default:
+				abort();
+			}
+			/* 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, '-');
+			if (vis > vtarget) {
+				ic++;
+				break;
+			}
+			*nbr = ic + 1;
+			*vbr = vis;
+			continue;
+
+		case ASCII_NBRSP:  /* Non-breakable space. */
+			p->tcol->buf[ic] = ' ';
+			/* FALLTHROUGH */
+		default:  /* Printable character. */
+			graph = 1;
+			vis += (*p->width)(p, p->tcol->buf[ic]);
+			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,
+ * an input string length of	 nbr	  characters,
+ * an output width of		 vbr	  visual columns,
+ * and a desired field width of	 vtarget  visual columns.
+ */
+static void
+term_field(struct termp *p, size_t vbl, size_t nbr, size_t vbr, size_t vtarget)
+{
+	size_t	 ic;	/* Character position in the input buffer. */
+	size_t	 vis;	/* Visual position of the current character. */
+	size_t	 dv;	/* Visual width of the current character. */
+	size_t	 vn;	/* Visual position of the next character. */
+
+	vis = 0;
+	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:
+			continue;
+		case '\t':
+			vn = term_tab_next(vis);
+			vbl += vn - vis;
+			vis = vn;
+			continue;
+		case ' ':
+		case ASCII_NBRSP:
+			dv = (*p->width)(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]);
+			p->viscol -= dv;
+			vis -= dv;
+		} else {
+			dv = (*p->width)(p, p->tcol->buf[ic]);
+			p->viscol += dv;
+			vis += dv;
+		}
+	}
+	p->tcol->col = nbr;
+}
+
+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);
+		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);
+}
+
+/*
+ * 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. */
+void
+term_fontrepl(struct termp *p, enum termfont f)
+{
+
+	p->fontl = p->fontq[p->fonti];
+	p->fontq[p->fonti] = f;
+}
+
+/* Set font, save previous. */
+void
+term_fontpush(struct termp *p, enum termfont f)
+{
+
+	p->fontl = 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;
+}
+
+/* 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);
+	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;
+	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)
+				continue;
+			break;
+		case ESCAPE_SPECIAL:
+			if (p->enc == TERMENC_ASCII) {
+				cp = mchars_spec2str(seq, sz, &ssz);
+				if (cp != NULL)
+					encode(p, cp, ssz);
+			} else {
+				uc = mchars_spec2cp(seq, sz);
+				if (uc > 0)
+					encode1(p, uc);
+			}
+			continue;
+		case ESCAPE_UNDEF:
+			uc = *seq;
+			break;
+		case ESCAPE_FONTBOLD:
+			term_fontrepl(p, TERMFONT_BOLD);
+			continue;
+		case ESCAPE_FONTITALIC:
+			term_fontrepl(p, TERMFONT_UNDER);
+			continue;
+		case ESCAPE_FONTBI:
+			term_fontrepl(p, TERMFONT_BI);
+			continue;
+		case ESCAPE_FONT:
+		case ESCAPE_FONTCW:
+		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 (*seq == '|') {
+				seq++;
+				uc = -p->col;
+			} else
+				uc = 0;
+			if (a2roffsu(seq, &su, SCALE_EM) == NULL)
+				continue;
+			uc += term_hen(p, &su);
+			if (uc > 0)
+				while (uc-- > 0)
+					bufferc(p, ASCII_NBRSP);
+			else if (p->col > (size_t)(-uc))
+				p->col += uc;
+			else {
+				uc += p->col;
+				p->col = 0;
+				if (p->tcol->offset > (size_t)(-uc)) {
+					p->ti += uc;
+					p->tcol->offset += uc;
+				} 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) {
+				if (p->tcol->rmargin <= p->tcol->offset)
+					continue;
+				lsz = p->tcol->rmargin - p->tcol->offset;
+			} else
+				lsz = uc;
+			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) {
+				if (p->enc == TERMENC_ASCII)
+					encode(p, cp, ssz);
+				else
+					encode1(p, uc);
+				lsz -= csz;
+			}
+			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;
+		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;
+}
+
+/*
+ * 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;
+}
+
+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);
+}
+
+size_t
+term_strlen(const struct termp *p, const char *cp)
+{
+	size_t		 sz, rsz, i;
+	int		 ssz, skip, uc;
+	const char	*seq, *rhs;
+	enum mandoc_esc	 esc;
+	static const char rej[] = { '\\', ASCII_NBRSP, ASCII_HYPH,
+			ASCII_BREAK, '\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;
+				rhs = seq + ssz;
+				while (seq < rhs) {
+					if (*seq == '\\') {
+						mandoc_escape(&seq, NULL, NULL);
+						continue;
+					}
+					i = (*p->width)(p, *seq++);
+					if (rsz < i)
+						rsz = i;
+				}
+				sz += rsz;
+				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++);
+			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.
+ */
+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/usr.bin/mandoc/term.h b/usr.bin/mandoc/term.h
new file mode 100644
index 0000000..525aa3f
--- /dev/null
+++ b/usr.bin/mandoc/term.h
@@ -0,0 +1,158 @@
+/*	$OpenBSD: term.h,v 1.75 2019/01/04 03:20:44 schwarze Exp $ */
+/*
+ * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2011-2015, 2017, 2019 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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. */
+};
+
+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		  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. */
+	int		  synopsisonly; /* Print the synopsis only. */
+	int		  mdocstyle;	/* Imitate mdoc(7) output. */
+	int		  ti;		/* Temporary indent for one line. */
+	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. */
+	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);
+	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);
+size_t		  term_tab_next(size_t);
+
+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/usr.bin/mandoc/term_ascii.c b/usr.bin/mandoc/term_ascii.c
new file mode 100644
index 0000000..9b28060
--- /dev/null
+++ b/usr.bin/mandoc/term_ascii.c
@@ -0,0 +1,392 @@
+/*	$OpenBSD: term_ascii.c,v 1.50 2019/07/19 21:45:37 schwarze Exp $ */
+/*
+ * Copyright (c) 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2015, 2017, 2018 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <langinfo.h>
+#include <locale.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <wchar.h>
+
+#include "mandoc.h"
+#include "mandoc_aux.h"
+#include "out.h"
+#include "term.h"
+#include "manconf.h"
+#include "main.h"
+
+#define UTF8_LOCALE	"en_US.UTF-8"
+
+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	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);
+
+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 struct termp *
+ascii_init(enum termenc enc, const struct manoutput *outopts)
+{
+	char		*v;
+	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->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;
+
+	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;
+		}
+	}
+
+	if (outopts->mdoc) {
+		p->mdocstyle = 1;
+		p->defindent = 5;
+	}
+	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;
+	p->lastrmargin = p->tcol->rmargin;
+	p->tcol->rmargin = p->maxrmargin = p->defrmargin;
+}
+
+void
+terminal_sepline(void *arg)
+{
+	struct termp	*p;
+	size_t		 i;
+
+	p = (struct termp *)arg;
+	(*p->endline)(p);
+	for (i = 0; i < p->defrmargin; i++)
+		(*p->letter)(p, '-');
+	(*p->endline)(p);
+	(*p->endline)(p);
+}
+
+static size_t
+ascii_width(const struct termp *p, int c)
+{
+	return c != ASCII_BREAK;
+}
+
+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++;
+	p->tcol->offset -= p->ti;
+	p->ti = 0;
+	putchar('\n');
+}
+
+static void
+ascii_advance(struct termp *p, size_t len)
+{
+	size_t		i;
+
+	assert(len < UINT16_MAX);
+	for (i = 0; i < len; i++)
+		putchar(' ');
+}
+
+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[] = {
+	"<NUL>","<SOH>","<STX>","<ETX>","<EOT>","<ENQ>","<ACK>","<BEL>",
+	"<BS>",	"\t",	"<LF>",	"<VT>",	"<FF>",	"<CR>",	"<SO>",	"<SI>",
+	"<DLE>","<DC1>","<DC2>","<DC3>","<DC4>","<NAK>","<SYN>","<ETB>",
+	"<CAN>","<EM>",	"<SUB>","<ESC>","<FS>",	"<GS>",	"<RS>",	"<US>",
+	" ",	"!",	"\"",	"#",	"$",	"%",	"&",	"'",
+	"(",	")",	"*",	"+",	",",	"-",	".",	"/",
+	"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",	"{",	"|",	"}",	"~",	"<DEL>",
+	"<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",	"|",	"<section>",
+	"\"",	"(C)",	"_\ba",	"<<",	"~",	"",	"(R)",	"-",
+	"<degree>","+-","^2",	"^3",	"'","<micro>","<paragraph>",".",
+	",",	"^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);
+}
+
+static size_t
+locale_width(const struct termp *p, int c)
+{
+	int		rc;
+
+	if (c == ASCII_NBRSP)
+		c = ' ';
+	rc = wcwidth(c);
+	if (rc < 0)
+		rc = 0;
+	return rc;
+}
+
+static void
+locale_advance(struct termp *p, size_t len)
+{
+	size_t		i;
+
+	assert(len < UINT16_MAX);
+	for (i = 0; i < len; i++)
+		putwchar(L' ');
+}
+
+static void
+locale_endline(struct termp *p)
+{
+
+	p->line++;
+	p->tcol->offset -= p->ti;
+	p->ti = 0;
+	putwchar(L'\n');
+}
+
+static void
+locale_letter(struct termp *p, int c)
+{
+
+	putwchar(c);
+}
diff --git a/usr.bin/mandoc/term_ps.c b/usr.bin/mandoc/term_ps.c
new file mode 100644
index 0000000..9460c88
--- /dev/null
+++ b/usr.bin/mandoc/term_ps.c
@@ -0,0 +1,1355 @@
+/*	$OpenBSD: term_ps.c,v 1.55 2017/11/10 14:16:28 espie Exp $ */
+/*
+ * Copyright (c) 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
+ * Copyright (c) 2014, 2015, 2016, 2017 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2017 Marc Espie <espie@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <assert.h>
+#include <err.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#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	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;
+
+	/* 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, "<</PageSize [%zu %zu]>>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;
+}
+
+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;
+
+	/* 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);
+
+	p->tcol->offset -= p->ti;
+	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)
+{
+
+	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);
+		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;
+}
+
+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/usr.bin/mandoc/term_tab.c b/usr.bin/mandoc/term_tab.c
new file mode 100644
index 0000000..0c80c72
--- /dev/null
+++ b/usr.bin/mandoc/term_tab.c
@@ -0,0 +1,128 @@
+/*	$OpenBSD: term_tab.c,v 1.4 2017/06/17 14:55:02 schwarze Exp $ */
+/*
+ * Copyright (c) 2017 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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 <sys/types.h>
+
+#include <stddef.h>
+
+#include "mandoc_aux.h"
+#include "out.h"
+#include "term.h"
+
+struct tablist {
+	size_t	*t;	/* Allocated array of tab positions. */
+	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. */
+	size_t		 d;	/* Default tab width in units of n. */
+} tabs;
+
+
+void
+term_tab_set(const struct termp *p, const char *arg)
+{
+	static int	 recording_period;
+
+	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;
+		recording_period = 0;
+		if (tabs.d == 0) {
+			a2roffsu(".8i", &su, SCALE_IN);
+			tabs.d = term_hen(p, &su);
+		}
+		return;
+	}
+	if (arg[0] == 'T' && arg[1] == '\0') {
+		recording_period = 1;
+		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 = recording_period ? &tabs.p : &tabs.a;
+	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);
+	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];
+	}
+}
diff --git a/usr.bin/mandoc/term_tag.c b/usr.bin/mandoc/term_tag.c
new file mode 100644
index 0000000..1c67dcc
--- /dev/null
+++ b/usr.bin/mandoc/term_tag.c
@@ -0,0 +1,199 @@
+/* $OpenBSD: term_tag.c,v 1.4 2020/04/18 20:28:46 schwarze Exp $ */
+/*
+ * Copyright (c) 2015,2016,2018,2019,2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ *
+ * Functions to write a ctags(1) file.
+ * For use by the mandoc(1) ASCII and UTF-8 formatters only.
+ */
+#include <sys/types.h>
+
+#include <errno.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "roff_int.h"
+#include "tag.h"
+#include "term_tag.h"
+
+static void tag_signal(int) __attribute__((__noreturn__));
+
+static struct tag_files tag_files;
+
+
+/*
+ * Prepare for using a pager.
+ * Not all pagers are capable of using a tag file,
+ * but for simplicity, create it anyway.
+ */
+struct tag_files *
+term_tag_init(void)
+{
+	struct sigaction	 sa;
+	int			 ofd;	/* In /tmp/, dup(2)ed to stdout. */
+	int			 tfd;
+
+	ofd = tfd = -1;
+	tag_files.tfs = NULL;
+	tag_files.tcpgid = -1;
+
+	/* Clean up when dying from a signal. */
+
+	memset(&sa, 0, sizeof(sa));
+	sigfillset(&sa.sa_mask);
+	sa.sa_handler = tag_signal;
+	sigaction(SIGHUP, &sa, NULL);
+	sigaction(SIGINT, &sa, NULL);
+	sigaction(SIGTERM, &sa, NULL);
+
+	/*
+	 * POSIX requires that a process calling tcsetpgrp(3)
+	 * from the background gets a SIGTTOU signal.
+	 * In that case, do not stop.
+	 */
+
+	sa.sa_handler = SIG_IGN;
+	sigaction(SIGTTOU, &sa, NULL);
+
+	/* Save the original standard output for use by the pager. */
+
+	if ((tag_files.ofd = dup(STDOUT_FILENO)) == -1) {
+		mandoc_msg(MANDOCERR_DUP, 0, 0, "%s", strerror(errno));
+		goto fail;
+	}
+
+	/* Create both temporary output files. */
+
+	(void)strlcpy(tag_files.ofn, "/tmp/man.XXXXXXXXXX",
+	    sizeof(tag_files.ofn));
+	(void)strlcpy(tag_files.tfn, "/tmp/man.XXXXXXXXXX",
+	    sizeof(tag_files.tfn));
+	if ((ofd = mkstemp(tag_files.ofn)) == -1) {
+		mandoc_msg(MANDOCERR_MKSTEMP, 0, 0,
+		    "%s: %s", tag_files.ofn, strerror(errno));
+		goto fail;
+	}
+	if ((tfd = mkstemp(tag_files.tfn)) == -1) {
+		mandoc_msg(MANDOCERR_MKSTEMP, 0, 0,
+		    "%s: %s", tag_files.tfn, strerror(errno));
+		goto fail;
+	}
+	if ((tag_files.tfs = fdopen(tfd, "w")) == NULL) {
+		mandoc_msg(MANDOCERR_FDOPEN, 0, 0, "%s", strerror(errno));
+		goto fail;
+	}
+	tfd = -1;
+	if (dup2(ofd, STDOUT_FILENO) == -1) {
+		mandoc_msg(MANDOCERR_DUP, 0, 0, "%s", strerror(errno));
+		goto fail;
+	}
+	close(ofd);
+	return &tag_files;
+
+fail:
+	term_tag_unlink();
+	if (ofd != -1)
+		close(ofd);
+	if (tfd != -1)
+		close(tfd);
+	if (tag_files.ofd != -1) {
+		close(tag_files.ofd);
+		tag_files.ofd = -1;
+	}
+	return NULL;
+}
+
+void
+term_tag_write(struct roff_node *n, size_t line)
+{
+	const char	*cp;
+	int		 len;
+
+	if (tag_files.tfs == NULL)
+		return;
+	cp = n->tag == NULL ? n->child->string : n->tag;
+	if (cp[0] == '\\' && (cp[1] == '&' || cp[1] == 'e'))
+		cp += 2;
+	len = strcspn(cp, " \t\\");
+	fprintf(tag_files.tfs, "%.*s %s %zu\n",
+	    len, cp, tag_files.ofn, line);
+}
+
+/*
+ * Close both output files and restore the original standard output
+ * to the terminal.  In the unlikely case that the latter fails,
+ * trying to start a pager would be useless, so report the failure
+ * to the main program.
+ */
+int
+term_tag_close(void)
+{
+	int irc = 0;
+
+	if (tag_files.tfs != NULL) {
+		fclose(tag_files.tfs);
+		tag_files.tfs = NULL;
+	}
+	if (tag_files.ofd != -1) {
+		fflush(stdout);
+		if ((irc = dup2(tag_files.ofd, STDOUT_FILENO)) == -1)
+			mandoc_msg(MANDOCERR_DUP, 0, 0, "%s", strerror(errno));
+		close(tag_files.ofd);
+		tag_files.ofd = -1;
+	}
+	return irc;
+}
+
+void
+term_tag_unlink(void)
+{
+	pid_t	 tc_pgid;
+
+	if (tag_files.tcpgid != -1) {
+		tc_pgid = tcgetpgrp(STDOUT_FILENO);
+		if (tc_pgid == tag_files.pager_pid ||
+		    tc_pgid == getpgid(0) ||
+		    getpgid(tc_pgid) == -1)
+			(void)tcsetpgrp(STDOUT_FILENO, tag_files.tcpgid);
+	}
+	if (*tag_files.ofn != '\0') {
+		unlink(tag_files.ofn);
+		*tag_files.ofn = '\0';
+	}
+	if (*tag_files.tfn != '\0') {
+		unlink(tag_files.tfn);
+		*tag_files.tfn = '\0';
+	}
+}
+
+static void
+tag_signal(int signum)
+{
+	struct sigaction	 sa;
+
+	term_tag_unlink();
+	memset(&sa, 0, sizeof(sa));
+	sigemptyset(&sa.sa_mask);
+	sa.sa_handler = SIG_DFL;
+	sigaction(signum, &sa, NULL);
+	kill(getpid(), signum);
+	/* NOTREACHED */
+	_exit(1);
+}
diff --git a/usr.bin/mandoc/term_tag.h b/usr.bin/mandoc/term_tag.h
new file mode 100644
index 0000000..62be2d3
--- /dev/null
+++ b/usr.bin/mandoc/term_tag.h
@@ -0,0 +1,34 @@
+/* $OpenBSD: term_tag.h,v 1.2 2020/04/02 22:10:27 schwarze Exp $ */
+/*
+ * Copyright (c) 2015, 2018, 2019, 2020 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * 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.
+ *
+ * Internal interfaces to write a ctags(1) file.
+ * For use by the mandoc(1) ASCII and UTF-8 formatters only.
+ */
+
+struct	tag_files {
+	char	 ofn[20];	/* Output file name. */
+	char	 tfn[20];	/* Tag file name. */
+	FILE	*tfs;		/* Tag file object. */
+	int	 ofd;		/* Original output file descriptor. */
+	pid_t	 tcpgid;	/* Process group controlling the terminal. */
+	pid_t	 pager_pid;	/* Process ID of the pager. */
+};
+
+
+struct tag_files	*term_tag_init(void);
+void			 term_tag_write(struct roff_node *, size_t);
+int			 term_tag_close(void);
+void			 term_tag_unlink(void);
diff --git a/usr.bin/mandoc/tree.c b/usr.bin/mandoc/tree.c
new file mode 100644
index 0000000..0125ebd
--- /dev/null
+++ b/usr.bin/mandoc/tree.c
@@ -0,0 +1,514 @@
+/* $OpenBSD: tree.c,v 1.56 2020/04/08 11:54:14 schwarze Exp $ */
+/*
+ * Copyright (c) 2013-2015, 2017-2020 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2008, 2009, 2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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.
+ *
+ * Formatting module to let mandoc(1) show
+ * a human readable representation of the syntax tree.
+ */
+#include <sys/types.h>
+
+#include <assert.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "mdoc.h"
+#include "man.h"
+#include "tbl.h"
+#include "eqn.h"
+#include "main.h"
+
+static	void	print_attr(const struct roff_node *);
+static	void	print_box(const struct eqn_box *, int);
+static	void	print_cellt(enum tbl_cellt);
+static	void	print_man(const struct roff_node *, int);
+static	void	print_meta(const struct roff_meta *);
+static	void	print_mdoc(const struct roff_node *, int);
+static	void	print_span(const struct tbl_span *, int);
+
+
+void
+tree_mdoc(void *arg, const struct roff_meta *mdoc)
+{
+	print_meta(mdoc);
+	putchar('\n');
+	print_mdoc(mdoc->first->child, 0);
+}
+
+void
+tree_man(void *arg, const struct roff_meta *man)
+{
+	print_meta(man);
+	if (man->hasbody == 0)
+		puts("body  = empty");
+	putchar('\n');
+	print_man(man->first->child, 0);
+}
+
+static void
+print_meta(const struct roff_meta *meta)
+{
+	if (meta->title != NULL)
+		printf("title = \"%s\"\n", meta->title);
+	if (meta->name != NULL)
+		printf("name  = \"%s\"\n", meta->name);
+	if (meta->msec != NULL)
+		printf("sec   = \"%s\"\n", meta->msec);
+	if (meta->vol != NULL)
+		printf("vol   = \"%s\"\n", meta->vol);
+	if (meta->arch != NULL)
+		printf("arch  = \"%s\"\n", meta->arch);
+	if (meta->os != NULL)
+		printf("os    = \"%s\"\n", meta->os);
+	if (meta->date != NULL)
+		printf("date  = \"%s\"\n", meta->date);
+}
+
+static void
+print_mdoc(const struct roff_node *n, int indent)
+{
+	const char	 *p, *t;
+	int		  i, j;
+	size_t		  argc;
+	struct mdoc_argv *argv;
+
+	if (n == NULL)
+		return;
+
+	argv = NULL;
+	argc = 0;
+	t = p = NULL;
+
+	switch (n->type) {
+	case ROFFT_ROOT:
+		t = "root";
+		break;
+	case ROFFT_BLOCK:
+		t = "block";
+		break;
+	case ROFFT_HEAD:
+		t = "head";
+		break;
+	case ROFFT_BODY:
+		if (n->end)
+			t = "body-end";
+		else
+			t = "body";
+		break;
+	case ROFFT_TAIL:
+		t = "tail";
+		break;
+	case ROFFT_ELEM:
+		t = "elem";
+		break;
+	case ROFFT_TEXT:
+		t = "text";
+		break;
+	case ROFFT_COMMENT:
+		t = "comment";
+		break;
+	case ROFFT_TBL:
+		break;
+	case ROFFT_EQN:
+		t = "eqn";
+		break;
+	default:
+		abort();
+	}
+
+	switch (n->type) {
+	case ROFFT_TEXT:
+	case ROFFT_COMMENT:
+		p = n->string;
+		break;
+	case ROFFT_BODY:
+		p = roff_name[n->tok];
+		break;
+	case ROFFT_HEAD:
+		p = roff_name[n->tok];
+		break;
+	case ROFFT_TAIL:
+		p = roff_name[n->tok];
+		break;
+	case ROFFT_ELEM:
+		p = roff_name[n->tok];
+		if (n->args) {
+			argv = n->args->argv;
+			argc = n->args->argc;
+		}
+		break;
+	case ROFFT_BLOCK:
+		p = roff_name[n->tok];
+		if (n->args) {
+			argv = n->args->argv;
+			argc = n->args->argc;
+		}
+		break;
+	case ROFFT_TBL:
+		break;
+	case ROFFT_EQN:
+		p = "EQ";
+		break;
+	case ROFFT_ROOT:
+		p = "root";
+		break;
+	default:
+		abort();
+	}
+
+	if (n->span) {
+		assert(NULL == p && NULL == t);
+		print_span(n->span, indent);
+	} else {
+		for (i = 0; i < indent; i++)
+			putchar(' ');
+
+		printf("%s (%s)", p, t);
+
+		for (i = 0; i < (int)argc; i++) {
+			printf(" -%s", mdoc_argnames[argv[i].arg]);
+			if (argv[i].sz > 0)
+				printf(" [");
+			for (j = 0; j < (int)argv[i].sz; j++)
+				printf(" [%s]", argv[i].value[j]);
+			if (argv[i].sz > 0)
+				printf(" ]");
+		}
+		print_attr(n);
+	}
+	if (n->eqn)
+		print_box(n->eqn->first, indent + 4);
+	if (n->child)
+		print_mdoc(n->child, indent +
+		    (n->type == ROFFT_BLOCK ? 2 : 4));
+	if (n->next)
+		print_mdoc(n->next, indent);
+}
+
+static void
+print_man(const struct roff_node *n, int indent)
+{
+	const char	 *p, *t;
+	int		  i;
+
+	if (n == NULL)
+		return;
+
+	t = p = NULL;
+
+	switch (n->type) {
+	case ROFFT_ROOT:
+		t = "root";
+		break;
+	case ROFFT_ELEM:
+		t = "elem";
+		break;
+	case ROFFT_TEXT:
+		t = "text";
+		break;
+	case ROFFT_COMMENT:
+		t = "comment";
+		break;
+	case ROFFT_BLOCK:
+		t = "block";
+		break;
+	case ROFFT_HEAD:
+		t = "head";
+		break;
+	case ROFFT_BODY:
+		t = "body";
+		break;
+	case ROFFT_TBL:
+		break;
+	case ROFFT_EQN:
+		t = "eqn";
+		break;
+	default:
+		abort();
+	}
+
+	switch (n->type) {
+	case ROFFT_TEXT:
+	case ROFFT_COMMENT:
+		p = n->string;
+		break;
+	case ROFFT_ELEM:
+	case ROFFT_BLOCK:
+	case ROFFT_HEAD:
+	case ROFFT_BODY:
+		p = roff_name[n->tok];
+		break;
+	case ROFFT_ROOT:
+		p = "root";
+		break;
+	case ROFFT_TBL:
+		break;
+	case ROFFT_EQN:
+		p = "EQ";
+		break;
+	default:
+		abort();
+	}
+
+	if (n->span) {
+		assert(NULL == p && NULL == t);
+		print_span(n->span, indent);
+	} else {
+		for (i = 0; i < indent; i++)
+			putchar(' ');
+		printf("%s (%s)", p, t);
+		print_attr(n);
+	}
+	if (n->eqn)
+		print_box(n->eqn->first, indent + 4);
+	if (n->child)
+		print_man(n->child, indent +
+		    (n->type == ROFFT_BLOCK ? 2 : 4));
+	if (n->next)
+		print_man(n->next, indent);
+}
+
+static void
+print_attr(const struct roff_node *n)
+{
+	putchar(' ');
+	if (n->flags & NODE_DELIMO)
+		putchar('(');
+	if (n->flags & NODE_LINE)
+		putchar('*');
+	printf("%d:%d", n->line, n->pos + 1);
+	if (n->flags & NODE_DELIMC)
+		putchar(')');
+	if (n->flags & NODE_EOS)
+		putchar('.');
+	if (n->flags & NODE_ID) {
+		printf(" ID");
+		if (n->flags & NODE_HREF)
+			printf("=HREF");
+	} else if (n->flags & NODE_HREF)
+		printf(" HREF");
+	else if (n->tag != NULL)
+		printf(" STRAYTAG");
+	if (n->tag != NULL)
+		printf("=%s", n->tag);
+	if (n->flags & NODE_BROKEN)
+		printf(" BROKEN");
+	if (n->flags & NODE_NOFILL)
+		printf(" NOFILL");
+	if (n->flags & NODE_NOSRC)
+		printf(" NOSRC");
+	if (n->flags & NODE_NOPRT)
+		printf(" NOPRT");
+	putchar('\n');
+}
+
+static void
+print_box(const struct eqn_box *ep, int indent)
+{
+	int		 i;
+	const char	*t;
+
+	static const char *posnames[] = {
+	    NULL, "sup", "subsup", "sub",
+	    "to", "from", "fromto",
+	    "over", "sqrt", NULL };
+
+	if (NULL == ep)
+		return;
+	for (i = 0; i < indent; i++)
+		putchar(' ');
+
+	t = NULL;
+	switch (ep->type) {
+	case EQN_LIST:
+		t = "eqn-list";
+		break;
+	case EQN_SUBEXPR:
+		t = "eqn-expr";
+		break;
+	case EQN_TEXT:
+		t = "eqn-text";
+		break;
+	case EQN_PILE:
+		t = "eqn-pile";
+		break;
+	case EQN_MATRIX:
+		t = "eqn-matrix";
+		break;
+	}
+
+	fputs(t, stdout);
+	if (ep->pos)
+		printf(" pos=%s", posnames[ep->pos]);
+	if (ep->left)
+		printf(" left=\"%s\"", ep->left);
+	if (ep->right)
+		printf(" right=\"%s\"", ep->right);
+	if (ep->top)
+		printf(" top=\"%s\"", ep->top);
+	if (ep->bottom)
+		printf(" bottom=\"%s\"", ep->bottom);
+	if (ep->text)
+		printf(" text=\"%s\"", ep->text);
+	if (ep->font)
+		printf(" font=%d", ep->font);
+	if (ep->size != EQN_DEFSIZE)
+		printf(" size=%d", ep->size);
+	if (ep->expectargs != UINT_MAX && ep->expectargs != ep->args)
+		printf(" badargs=%zu(%zu)", ep->args, ep->expectargs);
+	else if (ep->args)
+		printf(" args=%zu", ep->args);
+	putchar('\n');
+
+	print_box(ep->first, indent + 4);
+	print_box(ep->next, indent);
+}
+
+static void
+print_cellt(enum tbl_cellt pos)
+{
+	switch(pos) {
+	case TBL_CELL_LEFT:
+		putchar('L');
+		break;
+	case TBL_CELL_LONG:
+		putchar('a');
+		break;
+	case TBL_CELL_CENTRE:
+		putchar('c');
+		break;
+	case TBL_CELL_RIGHT:
+		putchar('r');
+		break;
+	case TBL_CELL_NUMBER:
+		putchar('n');
+		break;
+	case TBL_CELL_SPAN:
+		putchar('s');
+		break;
+	case TBL_CELL_DOWN:
+		putchar('^');
+		break;
+	case TBL_CELL_HORIZ:
+		putchar('-');
+		break;
+	case TBL_CELL_DHORIZ:
+		putchar('=');
+		break;
+	case TBL_CELL_MAX:
+		putchar('#');
+		break;
+	}
+}
+
+static void
+print_span(const struct tbl_span *sp, int indent)
+{
+	const struct tbl_dat *dp;
+	const struct tbl_cell *cp;
+	int		 i;
+
+	if (sp->prev == NULL) {
+		for (i = 0; i < indent; i++)
+			putchar(' ');
+		printf("%d", sp->opts->cols);
+		if (sp->opts->opts & TBL_OPT_CENTRE)
+			fputs(" center", stdout);
+		if (sp->opts->opts & TBL_OPT_EXPAND)
+			fputs(" expand", stdout);
+		if (sp->opts->opts & TBL_OPT_ALLBOX)
+			fputs(" allbox", stdout);
+		if (sp->opts->opts & TBL_OPT_BOX)
+			fputs(" box", stdout);
+		if (sp->opts->opts & TBL_OPT_DBOX)
+			fputs(" doublebox", stdout);
+		if (sp->opts->opts & TBL_OPT_NOKEEP)
+			fputs(" nokeep", stdout);
+		if (sp->opts->opts & TBL_OPT_NOSPACE)
+			fputs(" nospaces", stdout);
+		if (sp->opts->opts & TBL_OPT_NOWARN)
+			fputs(" nowarn", stdout);
+		printf(" (tbl options) %d:1\n", sp->line);
+	}
+
+	for (i = 0; i < indent; i++)
+		putchar(' ');
+
+	switch (sp->pos) {
+	case TBL_SPAN_HORIZ:
+		putchar('-');
+		putchar(' ');
+		break;
+	case TBL_SPAN_DHORIZ:
+		putchar('=');
+		putchar(' ');
+		break;
+	default:
+		for (cp = sp->layout->first; cp != NULL; cp = cp->next)
+			print_cellt(cp->pos);
+		putchar(' ');
+		for (dp = sp->first; dp; dp = dp->next) {
+			if ((cp = dp->layout) == NULL)
+				putchar('*');
+			else {
+				printf("%d", cp->col);
+				print_cellt(dp->layout->pos);
+				if (cp->flags & TBL_CELL_BOLD)
+					putchar('b');
+				if (cp->flags & TBL_CELL_ITALIC)
+					putchar('i');
+				if (cp->flags & TBL_CELL_TALIGN)
+					putchar('t');
+				if (cp->flags & TBL_CELL_UP)
+					putchar('u');
+				if (cp->flags & TBL_CELL_BALIGN)
+					putchar('d');
+				if (cp->flags & TBL_CELL_WIGN)
+					putchar('z');
+				if (cp->flags & TBL_CELL_EQUAL)
+					putchar('e');
+				if (cp->flags & TBL_CELL_WMAX)
+					putchar('x');
+			}
+			switch (dp->pos) {
+			case TBL_DATA_HORIZ:
+			case TBL_DATA_NHORIZ:
+				putchar('-');
+				break;
+			case TBL_DATA_DHORIZ:
+			case TBL_DATA_NDHORIZ:
+				putchar('=');
+				break;
+			default:
+				putchar(dp->block ? '{' : '[');
+				if (dp->string != NULL)
+					fputs(dp->string, stdout);
+				putchar(dp->block ? '}' : ']');
+				break;
+			}
+			if (dp->hspans)
+				printf(">%d", dp->hspans);
+			if (dp->vspans)
+				printf("v%d", dp->vspans);
+			putchar(' ');
+		}
+		break;
+	}
+	printf("(tbl) %d:1\n", sp->line);
+}
-- 
cgit v1.2.3