File 1.14.5-master.patch of Package mandoc

--- a/Makefile.depend
+++ b/Makefile.depend
@@ -29,7 +29,7 @@ dbm.o: dbm.c config.h mansearch.h dbm_ma
 dbm_map.o: dbm_map.c config.h mansearch.h dbm_map.h dbm.h
 demandoc.o: demandoc.c config.h mandoc.h roff.h man.h mdoc.h mandoc_parse.h
 eqn.o: eqn.c config.h mandoc_aux.h mandoc.h roff.h eqn.h libmandoc.h eqn_parse.h
-eqn_html.o: eqn_html.c config.h mandoc.h eqn.h out.h html.h
+eqn_html.o: eqn_html.c config.h mandoc.h roff.h eqn.h out.h html.h
 eqn_term.o: eqn_term.c config.h eqn.h out.h term.h
 html.o: html.c config.h mandoc_aux.h mandoc_ohash.h compat_ohash.h mandoc.h roff.h out.h html.h manconf.h main.h
 lib.o: lib.c config.h roff.h libmdoc.h lib.in
@@ -37,16 +37,16 @@ main.o: main.c config.h mandoc_aux.h man
 man.o: man.c config.h mandoc_aux.h mandoc.h roff.h man.h libmandoc.h roff_int.h libman.h
 man_html.o: man_html.c config.h mandoc_aux.h mandoc.h roff.h man.h out.h html.h main.h
 man_macro.o: man_macro.c config.h mandoc.h roff.h man.h libmandoc.h roff_int.h libman.h
-man_term.o: man_term.c config.h mandoc_aux.h roff.h man.h out.h term.h main.h
+man_term.o: man_term.c config.h mandoc_aux.h mandoc.h roff.h man.h out.h term.h tag.h main.h
 man_validate.o: man_validate.c config.h mandoc_aux.h mandoc.h roff.h man.h libmandoc.h roff_int.h libman.h
 mandoc.o: mandoc.c config.h mandoc_aux.h mandoc.h roff.h libmandoc.h roff_int.h
 mandoc_aux.o: mandoc_aux.c config.h mandoc.h mandoc_aux.h
-mandoc_msg.o: mandoc_msg.c mandoc.h
+mandoc_msg.o: mandoc_msg.c config.h mandoc.h
 mandoc_ohash.o: mandoc_ohash.c mandoc_aux.h mandoc_ohash.h compat_ohash.h
 mandoc_xr.o: mandoc_xr.c mandoc_aux.h mandoc_ohash.h compat_ohash.h mandoc_xr.h
 mandocd.o: mandocd.c config.h mandoc.h roff.h mdoc.h man.h mandoc_parse.h main.h manconf.h
 mandocdb.o: mandocdb.c config.h compat_fts.h mandoc_aux.h mandoc_ohash.h compat_ohash.h mandoc.h roff.h mdoc.h man.h mandoc_parse.h manconf.h mansearch.h dba_array.h dba.h
-manpath.o: manpath.c config.h mandoc_aux.h manconf.h
+manpath.o: manpath.c config.h mandoc_aux.h mandoc.h manconf.h
 mansearch.o: mansearch.c config.h mandoc_aux.h mandoc_ohash.h compat_ohash.h manconf.h mansearch.h dbm.h
 mdoc.o: mdoc.c config.h mandoc_aux.h mandoc.h roff.h mdoc.h libmandoc.h roff_int.h libmdoc.h
 mdoc_argv.o: mdoc_argv.c config.h mandoc_aux.h mandoc.h roff.h mdoc.h libmandoc.h roff_int.h libmdoc.h
@@ -67,10 +67,10 @@ roff_term.o: roff_term.c mandoc.h roff.h
 roff_validate.o: roff_validate.c mandoc.h roff.h libmandoc.h roff_int.h
 soelim.o: soelim.c config.h compat_stringlist.h
 st.o: st.c config.h mandoc.h roff.h libmdoc.h
-tag.o: tag.c config.h mandoc_aux.h mandoc_ohash.h compat_ohash.h tag.h
+tag.o: tag.c config.h mandoc_aux.h mandoc_ohash.h compat_ohash.h mandoc.h tag.h
 tbl.o: tbl.c config.h mandoc_aux.h mandoc.h tbl.h libmandoc.h tbl_parse.h tbl_int.h
 tbl_data.o: tbl_data.c config.h mandoc_aux.h mandoc.h tbl.h libmandoc.h tbl_int.h
-tbl_html.o: tbl_html.c config.h mandoc.h tbl.h out.h html.h
+tbl_html.o: tbl_html.c config.h mandoc.h roff.h tbl.h out.h html.h
 tbl_layout.o: tbl_layout.c config.h mandoc_aux.h mandoc.h tbl.h libmandoc.h tbl_int.h
 tbl_opts.o: tbl_opts.c config.h mandoc.h tbl.h libmandoc.h tbl_int.h
 tbl_term.o: tbl_term.c config.h mandoc.h tbl.h out.h term.h
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,88 @@ $Id: NEWS,v 1.34 2019/03/10 09:32:00 sch
 
 This file lists the most important changes in the mandoc.bsd.lv distribution.
 
+Changes in version 1.14.6, released on XXX XXX, 2019
+
+    --- MAJOR NEW FEATURES ---
+ * man(1) -T ascii: slowly start implementing tagging support for man(7)
+   pages: tag alphabetic arguments of .IP, .TP, and .TQ macros
+ * -T html: wrap text and phrasing elements in paragraphs unless
+   already contained in flow containers; never put them directly
+   into sections.  This helps to format paragraphs with the CSS
+   class selector .Pp.
+    --- MINOR NEW FEATURES ---
+ * roff(7): implement the .break request (break out of a .while loop)
+ * if messages are shown and output is printed without a pager,
+   display a heads-up on stderr at the end because otherwise, users
+   may easily miss the messages
+ * mandoc.css: support prefers-color-scheme: dark
+    --- RELIABILITY BUGFIXES ---
+ * man(1): do not segfault if /tmp/ is not writeable
+ * tbl(7): fix a crash when the last column is only reached by spans
+ * tbl(7) -T ascii: fix a NULL pointer access on empty data cells
+ * tbl(7) -T ascii: fix a NULL pointer access on a line next to a short row
+ * -T html: fix an assertion failure caused by .ft in rare situations
+ * roff(7): fix a rare case of writing one byte past the end of the input buffer
+    --- MINOR FUNCTIONAL IMPROVEMENTS ---
+ * man(1) -h: for pages lacking a SYNOPSIS, show the NAME section
+ * man(1): when the first argument starts with a digit, optionally
+   followed by a letter, and at least one more argument follows,
+   interpret the first argument as a section name even when additional
+   characters follow after the digit and letter
+ * man(1): with a specific section requested, try harder to find
+   the best match; use this order of preference:
+   1. The section in both the directory name and the file name matches exactly.
+   2. The section in the file name matches exactly.
+   3. The section in the directory name matches exactly.
+   4. Neither of them matches exactly.
+ * man(1): if no tags were generated at all, unlink(2) the empty
+   tags file as soon the condition can be detected and do not pass
+   it to less(1)
+ * makewhatis(8): handle both dangling symlinks and .so links
+   in manual page directories more gracefully
+ * man.cgi(8): for invalid queries and for valid queries returning
+   no result, return the appropriate 40x status code rather than 200
+ * tbl(7) -T utf8: improved rendering of horizontal lines
+ * mdoc(7) -T html: format .Nd with <span> rather than <div>
+ * mdoc(7) -T lint: do not warn about $Mdocdate$ without an actual date
+ * mdoc(7) -T lint: do not complain about function types of the
+   form "ret_type (fname)(args)", but otherwise check names more strictly
+    --- MINOR BUGFIXES ---
+ * man(1): do the search for each name independently, and show the
+   results in the order of the command line argument
+ * man(1): when asking for a single manual page by name, prefer
+   file name matches over .Dt/.TH matches over first NAME matches
+   over later NAME matches, but do not change the ordering for
+   apropos(1) nor for man -a
+ * roff(7): when calling an empty macro, do not clobber existing arguments
+ * mdoc(7) .Bl -column: parse Macro in .It "word<tab>word" Ta word Macro<eol>
+ * -T html: remove some spurious line breaks, in particular inside <pre>
+    --- STRUCTURAL IMPROVEMENTS ---
+ * move some code out of the giant main() into separate functions
+   doing one well-defined task each
+ * clearly separate parser state (struct curparse) and formatter state
+   (struct outstate), don't mix them in the same struct
+ * in the HTML formatter, assert(3) that no HTML nesting violation occurs
+ * let html_close_paragraph() close any phrasing context
+    --- THANKS TO ---
+ * Marc Espie (OpenBSD) for a patch and for suggesting a feature impovement
+ * Anton Lindqvist (OpenBSD) for a patch
+ * Armin Besirovic for a contribution to mandoc.css
+ * Lorenzo Beretta for three bug reports
+   and for suggesting two feature impovements
+ * Anthony Bentley (OpenBSD) for three bug reports
+   and for suggesting a feature impovement
+ * Michael Stapelberg (Debian) and Jan Stary for a bug report
+   and for suggesting a feature impovement
+ * Stephen Gregoratto for two bug reports
+ * Brian Callahan, Klemens Nanni (OpenBSD), Jason Thorpe (NetBSD),
+   Yuri Pankov (FreeBSD), and Edgar Pettijohn for bug reports
+ * Theo Buehler (OpenBSD), Leah Neukirchen (Void Linux), Colin Watson (Debian),
+   and John Gardner for suggesting feature impovements
+ * TJ Townsend (OpenBSD) for help with CSS
+ * Christos Zoulas (NetBSD) for a report regarding portability
+ * Michal Nowak for reporting four code style issues
+
 Changes in version 1.14.5, released on March 10, 2019
 
     --- MAJOR NEW FEATURES ---
--- a/TODO
+++ b/TODO
@@ -62,8 +62,33 @@ are mere guesses, and some may be wrong.
   needed for Tcl_NewStringObj(3) via wiz@  Wed, 5 Mar 2014 22:27:43 +0100
   loc **  exist ***  algo ***  size *  imp ***
 
+- .als only works for macros in mandoc, not for user-defined strings.
+  Also, the "val" field in struct roffkv would have to be replaced
+  with a pointer to a reference-counted wrapper, and an alias
+  would have to point to the same wrapper as the original.
+  .als to undefined does nothing; the alias is not created.
+  .rm'ing the original leaves the alias to point to the old value.
+  .de .als .de changes both, but
+  .de .als .rm .de only changes the new value, not the alias.
+  Found in groffer(1) version 1.19
+  Jan Stary 20 Apr 2019 20:16:54 +0200
+  loc *  exist **  algo **  size **  imp *
+
+- roff string condition comparisons fail when vars contain quotes:
+  .ds s '
+  .if '\*s'' \&...
+  hard to fix because of the basic architecture (string replacement
+  happens before roff(7) syntax parsing)
+  Found in groffer(1) version 1.19
+  Jan Stary 20 Apr 2019 20:16:54 +0200
+  loc *  exist ***  algo ***  size **  imp *
+
 --- missing mdoc features ----------------------------------------------
 
+- .Sh and .Ss should be parsed and partially callable, see groff_mdoc(7)
+  reed at reedmedia dot net Sat, 21 Dec 2019 17:13:07 -0600
+  loc **  exist **  algo **  size **  imp *
+
 - .Bl -column .Xo support is missing
   ultimate goal:
   restore .Xr and .Dv to
@@ -264,6 +289,9 @@ are mere guesses, and some may be wrong.
   https://github.com/schmonz/ikiwiki/compare/mandoc
   Amitai Schlair  Mon, 19 May 2014 14:05:53 -0400
 
+- check compatibility with
+  https://git.sr.ht/~sircmpwn/scdoc
+
 - check features of the Slackware man.conf(5) format
   Carsten Kunze  Wed, 11 Mar 2015 17:57:24 +0100
 
@@ -350,6 +378,11 @@ are mere guesses, and some may be wrong.
 
 --- HTML issues --------------------------------------------------------
 
+- get rid of the last handful of style= attributes such that
+  Content-Security-Policy: can be enabled without unsafe-inline
+  suggested by bentley@  Nov 10, 2019 at 06:02:49AM -0700
+  loc *  exist *  algo *  size *  imp **
+
 - .Bf at the beginning of a paragraph inserts a bogus 1ex horizontal
   space, see for example random(3).  Introduced in
   http://mdocml.bsd.lv/cgi-bin/cvsweb/mdoc_html.c.diff?r1=1.91&r2=1.92
@@ -362,6 +395,11 @@ are mere guesses, and some may be wrong.
   https://github.com/Debian/debiman/issues/15
   loc *  exist *  algo **  size **  imp **
 
+- space characters can end up in href= attributes, for example coming
+  from the first .Xr argument (where they make no sense, but still);
+  does this affect other characters, other source macros...?
+  Jackson Pauls  29 Aug 2017 16:56:27 +0100
+
 - The tables used to render the three-part page headers actually force
   the width of the <body> to the max-width given for <html>.
   Not yet sure how to fix that...
@@ -538,6 +576,15 @@ are mere guesses, and some may be wrong.
 * to improve in the groff_mdoc(7) macros
 ************************************************************************
 
+- delete OS release verification from .Dx, .Fx, .Nx, .Ox etc.
+  https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=629161
+  also Branden Robinson 18 Dec 2019 00:59:52 +1100
+
+- Can the distinction between .Vt and .Va be made stricter,
+  recommending .Vt extern char * Ns Va optarg ; ?
+  What about the block macro properties of .Vt in the SYNOPSIS?
+  zeurkous 25 Dec 2019 08:48:36 +0100
+
 - .Cd # arch1, arch2 in section 4 pages:
   find better way to indicate multiple architectures, maybe:
   allow .Dt vgafb 4 "macppc sparc64"
--- a/arch.c
+++ b/arch.c
@@ -26,7 +26,7 @@ arch_valid(const char *arch, enum mandoc
 	const char *openbsd_arch[] = {
 		"alpha", "amd64", "arm64", "armv7", "hppa", "i386",
 		"landisk", "loongson", "luna88k", "macppc", "mips64",
-		"octeon", "sgi", "socppc", "sparc64", NULL
+		"octeon", "sgi", "sparc64", NULL
 	};
 	const char *netbsd_arch[] = {
 		"acorn26", "acorn32", "algor", "alpha", "amiga",
--- a/cgi.c
+++ b/cgi.c
@@ -1,7 +1,7 @@
 /*	$Id: cgi.c,v 1.166 2019/03/06 12:32:41 schwarze Exp $ */
 /*
  * Copyright (c) 2011, 2012 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2014, 2015, 2016, 2017, 2018 Ingo Schwarze <schwarze@usta.de>
+ * Copyright (c) 2014-2019 Ingo Schwarze <schwarze@usta.de>
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -77,7 +77,8 @@ static	void		 parse_query_string(struct
 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 *, const char *);
+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 *,
@@ -339,6 +340,8 @@ resp_begin_http(int code, const char *ms
 
 	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");
 
@@ -408,7 +411,8 @@ resp_searchform(const struct req *req, e
 {
 	int		 i;
 
-	printf("<form action=\"/%s\" method=\"get\">\n"
+	printf("<form action=\"/%s\" method=\"get\" "
+	       "autocomplete=\"off\" autocapitalize=\"none\">\n"
 	       "  <fieldset>\n"
 	       "    <legend>Manual Page Search Parameters</legend>\n",
 	       scriptname);
@@ -546,12 +550,13 @@ pg_index(const struct req *req)
 }
 
 static void
-pg_noresult(const struct req *req, const char *msg)
+pg_noresult(const struct req *req, int code, const char *http_msg,
+    const char *user_msg)
 {
-	resp_begin_html(200, NULL, NULL);
+	resp_begin_html(code, http_msg, NULL);
 	resp_searchform(req, FOCUS_QUERY);
 	puts("<p>");
-	puts(msg);
+	puts(user_msg);
 	puts("</p>");
 	resp_end_html();
 }
@@ -869,7 +874,6 @@ resp_format(const struct req *req, const
 	memset(&conf, 0, sizeof(conf));
 	conf.fragment = 1;
 	conf.style = mandoc_strdup(CSS_DIR "/mandoc.css");
-	conf.toc = 1;
 	usepath = strcmp(req->q.manpath, req->p[0]);
 	mandoc_asprintf(&conf.man, "/%s%s%s%s%%N.%%S",
 	    scriptname, *scriptname == '\0' ? "" : "/",
@@ -1017,9 +1021,10 @@ pg_search(const struct req *req)
 	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, "You entered an invalid query.");
+		pg_noresult(req, 400, "Bad Request",
+		    "You entered an invalid query.");
 	else if (ressz == 0)
-		pg_noresult(req, "No results found.");
+		pg_noresult(req, 404, "Not Found", "No results found.");
 	else
 		pg_searchres(req, res, ressz);
 
--- a/configure
+++ b/configure
@@ -41,7 +41,7 @@ OSENUM=
 OSNAME=
 UTF8_LOCALE=
 
-CC=`printf "all:\\n\\t@echo \\\$(CC)\\n" | env -i make -sf -`
+CC=cc
 CFLAGS=
 LDADD=
 LDFLAGS=
@@ -529,7 +529,7 @@ fi
 	echo "extern	char	 *mkdtemp(char *);"
 
 if [ ${HAVE_PROGNAME} -eq 0 ]; then
-	echo "extern 	const char *getprogname(void);"
+	echo "extern	const char *getprogname(void);"
 	echo "extern	void	  setprogname(const char *);"
 fi
 
--- a/configure.local.example
+++ b/configure.local.example
@@ -28,6 +28,14 @@
 
 # --- user settings relevant for all builds ----------------------------
 
+# By default, "cc" is used as the C compiler, but it can be overridden.
+# For example, the system compiler in SunOS 5.9 may not provide <stdint.h>,
+# which may require this line:
+CC=gcc
+
+# IBM AIX may need:
+CC=xlc
+
 # For -Tutf8 and -Tlocale operation, mandoc(1) requires <locale.h>
 # providing setlocale(3) and <wchar.h> providing wcwidth(3) and
 # putwchar(3) with a wchar_t storing UCS-4 values.  Theoretically,
@@ -268,21 +276,6 @@ BINM_CATMAN=mcatman		# default is "catma
 
 # Do not set these variables unless you really need to.
 
-# You can manually override the compiler to be used.
-# But that's rarely useful because ./configure asks your make(1)
-# which compiler to use, and that answer will hardly be wrong.
-
-CC=cc
-
-# Because the system compiler may not provide <stdint.h>,
-# SunOS 5.9 may need:
-
-CC=gcc
-
-# IBM AIX may need:
-
-CC=xlc
-
 # Normally, leave CFLAGS unset.  In that case, -g will automatically
 # be used, and various -W options will be added if the compiler
 # supports them.  If you define CFLAGS manually, it will be used
--- a/dbm.c
+++ b/dbm.c
@@ -233,7 +233,7 @@ 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 const char		*cp;
 	static int32_t			 ip;
 	struct dbm_res			 res = {-1, 0};
 
@@ -315,7 +315,7 @@ page_byarch(const struct dbm_match *arg_
 	static const struct dbm_match	*match;
 	struct dbm_res			 res = {-1, 0};
 	static int32_t			 ip;
-	const char			*cp;	
+	const char			*cp;
 
 	/* Initialize for a new iteration. */
 
--- a/dbm_map.h
+++ b/dbm_map.h
@@ -14,7 +14,7 @@
  * 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 
+ * 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.
  */
--- a/eqn.7
+++ b/eqn.7
@@ -44,28 +44,16 @@ specification (see
 .Sx SEE ALSO
 for references).
 .Pp
-Equations within
-.Xr mdoc 7
-or
-.Xr man 7
-documents are enclosed by the standalone
-.Sq \&.EQ
-and
-.Sq \&.EN
-tags.
-Equations are multi-line blocks consisting of formulas and control
-statements.
-.Sh EQUATION STRUCTURE
-Each equation is bracketed by
-.Sq \&.EQ
-and
-.Sq \&.EN
-strings.
-.Em Note :
-these are not the same as
-.Xr roff 7
-macros, and may only be invoked as
-.Sq \&.EQ .
+An equation starts with an input line containing exactly the characters
+.Sq \&.EQ ,
+may contain multiple input lines, and ends with an input line
+containing exactly the characters
+.Sq \&.EN .
+Equivalently, an equation can be given in the middle of a single
+text input line by surrounding it with the equation delimiters
+defined with the
+.Cm delim
+statement.
 .Pp
 The equation grammar is as follows, where quoted strings are
 case-sensitive literals in the input:
@@ -178,6 +166,25 @@ statement is a synonym for
 while
 .Cm tdefine
 is discarded.
+.It Cm delim
+This statement takes a string argument consisting of two bytes,
+to be used as the opening and closing delimiters for equations
+in the middle of text input lines.
+Conventionally, the dollar sign is used for both delimiters,
+as follows:
+.Bd -literal -offset indent
+\&.EQ
+delim $$
+\&.EN
+An equation like $sin pi = 0$ can now be entered
+in the middle of a text input line.
+.Ed
+.Pp
+The special statement
+.Cm delim off
+temporarily disables previously declared delimiters and
+.Cm delim on
+reenables them.
 .It Cm gfont
 Set the default font of subsequent output.
 Its syntax is as follows:
@@ -470,7 +477,7 @@ commands are also ignored.
 .%T System for Typesetting Mathematics
 .%J Communications of the ACM
 .%V 18
-.%P 151\(en157
+.%P pp. 151\(en157
 .%D March, 1975
 .Re
 .Rs
--- a/eqn.c
+++ b/eqn.c
@@ -1,7 +1,7 @@
 /*	$Id: eqn.c,v 1.83 2018/12/14 06:33:14 schwarze Exp $ */
 /*
  * Copyright (c) 2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2014, 2015, 2017, 2018 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2014,2015,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
@@ -399,6 +399,14 @@ eqn_next(struct eqn_node *ep, enum parse
 		case '"':
 			quoted = 1;
 			break;
+		case ' ':
+		case '\t':
+		case '~':
+		case '^':
+			if (quoted)
+				break;
+			ep->start++;
+			continue;
 		default:
 			break;
 		}
@@ -669,7 +677,7 @@ eqn_parse(struct eqn_node *ep)
 	if (ep->data == NULL)
 		return;
 
-	ep->start = ep->end = ep->data + strspn(ep->data, " ^~");
+	ep->start = ep->end = ep->data;
 
 next_tok:
 	tok = eqn_next(ep, MODE_TOK);
--- a/eqn_html.c
+++ b/eqn_html.c
@@ -26,6 +26,7 @@
 #include <string.h>
 
 #include "mandoc.h"
+#include "roff.h"
 #include "eqn.h"
 #include "out.h"
 #include "html.h"
--- a/html.c
+++ b/html.c
@@ -42,34 +42,30 @@
 struct	htmldata {
 	const char	 *name;
 	int		  flags;
-#define	HTML_NOSTACK	 (1 << 0)
-#define	HTML_AUTOCLOSE	 (1 << 1)
-#define	HTML_NLBEFORE	 (1 << 2)
-#define	HTML_NLBEGIN	 (1 << 3)
-#define	HTML_NLEND	 (1 << 4)
-#define	HTML_NLAFTER	 (1 << 5)
+#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 << 6)
-#define	HTML_NOINDENT	 (1 << 7)
+#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},
-	{"body",	HTML_NLALL},
-	{"meta",	HTML_NOSTACK | HTML_AUTOCLOSE | HTML_NLALL},
+	{"meta",	HTML_NOSTACK | HTML_NLALL},
+	{"link",	HTML_NOSTACK | HTML_NLALL},
+	{"style",	HTML_NLALL | HTML_INDENT},
 	{"title",	HTML_NLAROUND},
+	{"body",	HTML_NLALL},
 	{"div",		HTML_NLAROUND},
-	{"div",		0},
 	{"section",	HTML_NLALL},
-	{"h1",		HTML_NLAROUND},
-	{"h2",		HTML_NLAROUND},
-	{"span",	0},
-	{"link",	HTML_NOSTACK | HTML_AUTOCLOSE | HTML_NLALL},
-	{"br",		HTML_NOSTACK | HTML_AUTOCLOSE | HTML_NLALL},
-	{"a",		0},
 	{"table",	HTML_NLALL | HTML_INDENT},
 	{"tr",		HTML_NLALL | HTML_INDENT},
 	{"td",		HTML_NLAROUND},
@@ -79,16 +75,21 @@ static	const struct htmldata htmltags[TA
 	{"dl",		HTML_NLALL | HTML_INDENT},
 	{"dt",		HTML_NLAROUND},
 	{"dd",		HTML_NLAROUND | HTML_INDENT},
-	{"p",		HTML_NLAROUND | HTML_INDENT},
-	{"pre",		HTML_NLALL | HTML_NOINDENT},
-	{"var",		0},
-	{"cite",	0},
-	{"b",		0},
-	{"i",		0},
-	{"code",	0},
-	{"small",	0},
-	{"style",	HTML_NLALL | HTML_INDENT},
-	{"math",	HTML_NLALL | 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 | HTML_NOSTACK },
+	{"math",	HTML_INPHRASE | HTML_NLALL | HTML_INDENT},
 	{"mrow",	0},
 	{"mi",		0},
 	{"mn",		0},
@@ -120,6 +121,7 @@ static	void	 print_ctag(struct html *, s
 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 *
@@ -202,75 +204,64 @@ print_gen_head(struct html *h)
 	print_endline(h);
 	print_text(h, "td.head-vol { text-align: center; }");
 	print_endline(h);
-	print_text(h, "div.Pp { margin: 1ex 0ex; }");
-	print_endline(h);
-	print_text(h, "div.Nd, div.Bf, div.Op { display: inline; }");
+	print_text(h, ".Nd, .Bf, .Op { display: inline; }");
 	print_endline(h);
-	print_text(h, "span.Pa, span.Ad { font-style: italic; }");
+	print_text(h, ".Pa, .Ad { font-style: italic; }");
 	print_endline(h);
-	print_text(h, "span.Ms { font-weight: bold; }");
+	print_text(h, ".Ms { font-weight: bold; }");
 	print_endline(h);
-	print_text(h, "dl.Bl-diag ");
+	print_text(h, ".Bl-diag ");
 	print_byte(h, '>');
 	print_text(h, " dt { font-weight: bold; }");
 	print_endline(h);
-	print_text(h, "code.Nm, code.Fl, code.Cm, code.Ic, "
-	    "code.In, code.Fd, code.Fn,");
-	print_endline(h);
-	print_text(h, "code.Cd { font-weight: bold; "
-	    "font-family: inherit; }");
+	print_text(h, "code.Nm, .Fl, .Cm, .Ic, code.In, .Fd, .Fn, .Cd "
+	    "{ font-weight: bold; font-family: inherit; }");
 	print_tagq(h, t);
 }
 
-void
-print_metaf(struct html *h, enum mandoc_esc deco)
+int
+html_setfont(struct html *h, enum mandoc_esc font)
 {
-	enum htmlfont	 font;
-
-	switch (deco) {
+	switch (font) {
 	case ESCAPE_FONTPREV:
 		font = h->metal;
 		break;
 	case ESCAPE_FONTITALIC:
-		font = HTMLFONT_ITALIC;
-		break;
 	case ESCAPE_FONTBOLD:
-		font = HTMLFONT_BOLD;
-		break;
 	case ESCAPE_FONTBI:
-		font = HTMLFONT_BI;
-		break;
 	case ESCAPE_FONTCW:
-		font = HTMLFONT_CW;
+	case ESCAPE_FONTROMAN:
 		break;
 	case ESCAPE_FONT:
-	case ESCAPE_FONTROMAN:
-		font = HTMLFONT_NONE;
+		font = ESCAPE_FONTROMAN;
 		break;
 	default:
-		return;
+		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;
 	}
-
-	h->metal = h->metac;
-	h->metac = font;
-
-	switch (font) {
-	case HTMLFONT_ITALIC:
+	switch (h->metac) {
+	case ESCAPE_FONTITALIC:
 		h->metaf = print_otag(h, TAG_I, "");
 		break;
-	case HTMLFONT_BOLD:
+	case ESCAPE_FONTBOLD:
 		h->metaf = print_otag(h, TAG_B, "");
 		break;
-	case HTMLFONT_BI:
+	case ESCAPE_FONTBI:
 		h->metaf = print_otag(h, TAG_B, "");
 		print_otag(h, TAG_I, "");
 		break;
-	case HTMLFONT_CW:
+	case ESCAPE_FONTCW:
 		h->metaf = print_otag(h, TAG_SPAN, "c", "Li");
 		break;
 	default:
@@ -281,21 +272,18 @@ print_metaf(struct html *h, enum mandoc_
 void
 html_close_paragraph(struct html *h)
 {
-	struct tag	*t;
+	struct tag	*this, *next;
+	int		 flags;
 
-	for (t = h->tag; t != NULL && t->closed == 0; t = t->next) {
-		switch(t->tag) {
-		case TAG_P:
-		case TAG_PRE:
-			print_tagq(h, t);
+	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;
-		case TAG_A:
-			print_tagq(h, t);
-			continue;
-		default:
-			continue;
-		}
-		break;
+		this = next;
 	}
 }
 
@@ -479,7 +467,8 @@ print_encode(struct html *h, const char
 		case ESCAPE_FONTROMAN:
 			if (0 == norecurse) {
 				h->flags |= HTML_NOSPACE;
-				print_metaf(h, esc);
+				if (html_setfont(h, esc))
+					print_metaf(h);
 				h->flags &= ~HTML_NOSPACE;
 			}
 			continue;
@@ -593,6 +582,25 @@ print_otag(struct html *h, enum htmltag
 
 	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) {
@@ -710,7 +718,7 @@ print_otag(struct html *h, enum htmltag
 
 	/* Accommodate for "well-formed" singleton escaping. */
 
-	if (HTML_AUTOCLOSE & htmltags[tag].flags)
+	if (htmltags[tag].flags & HTML_NOSTACK)
 		print_byte(h, '/');
 
 	print_byte(h, '>');
@@ -797,6 +805,16 @@ print_gen_comment(struct html *h, struct
 void
 print_text(struct html *h, const char *word)
 {
+	/*
+	 * 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)
@@ -806,27 +824,14 @@ print_text(struct html *h, const char *w
 			print_word(h, "&#x00A0;");
 	}
 
-	assert(NULL == h->metaf);
-	switch (h->metac) {
-	case HTMLFONT_ITALIC:
-		h->metaf = print_otag(h, TAG_I, "");
-		break;
-	case HTMLFONT_BOLD:
-		h->metaf = print_otag(h, TAG_B, "");
-		break;
-	case HTMLFONT_BI:
-		h->metaf = print_otag(h, TAG_B, "");
-		print_otag(h, TAG_I, "");
-		break;
-	case HTMLFONT_CW:
-		h->metaf = print_otag(h, TAG_SPAN, "c", "Li");
-		break;
-	default:
-		print_indent(h);
-		break;
-	}
+	/*
+	 * Print the text, optionally surrounded by HTML whitespace,
+	 * optionally manually switching fonts before and after.
+	 */
 
-	assert(word);
+	assert(h->metaf == NULL);
+	print_metaf(h);
+	print_indent(h);
 	if ( ! print_encode(h, word, NULL, 0)) {
 		if ( ! (h->flags & HTML_NONOSPACE))
 			h->flags &= ~HTML_NOSPACE;
@@ -834,7 +839,7 @@ print_text(struct html *h, const char *w
 	} else
 		h->flags |= HTML_NOSPACE | HTML_NONEWLINE;
 
-	if (h->metaf) {
+	if (h->metaf != NULL) {
 		print_tagq(h, h->metaf);
 		h->metaf = NULL;
 	}
@@ -964,15 +969,12 @@ print_indent(struct html *h)
 {
 	size_t	 i;
 
-	if (h->col)
+	if (h->col || h->noindent)
 		return;
 
-	if (h->noindent == 0) {
-		h->col = h->indent * 2;
-		for (i = 0; i < h->col; i++)
-			putchar(' ');
-	}
-	h->flags &= ~HTML_NOSPACE;
+	h->col = h->indent * 2;
+	for (i = 0; i < h->col; i++)
+		putchar(' ');
 }
 
 /*
--- a/html.h
+++ b/html.h
@@ -19,18 +19,13 @@
 enum	htmltag {
 	TAG_HTML,
 	TAG_HEAD,
-	TAG_BODY,
 	TAG_META,
+	TAG_LINK,
+	TAG_STYLE,
 	TAG_TITLE,
+	TAG_BODY,
 	TAG_DIV,
-	TAG_IDIV,
 	TAG_SECTION,
-	TAG_H1,
-	TAG_H2,
-	TAG_SPAN,
-	TAG_LINK,
-	TAG_BR,
-	TAG_A,
 	TAG_TABLE,
 	TAG_TR,
 	TAG_TD,
@@ -40,15 +35,20 @@ enum	htmltag {
 	TAG_DL,
 	TAG_DT,
 	TAG_DD,
+	TAG_H1,
+	TAG_H2,
 	TAG_P,
 	TAG_PRE,
-	TAG_VAR,
-	TAG_CITE,
+	TAG_A,
 	TAG_B,
-	TAG_I,
+	TAG_CITE,
 	TAG_CODE,
+	TAG_I,
 	TAG_SMALL,
-	TAG_STYLE,
+	TAG_SPAN,
+	TAG_VAR,
+	TAG_BR,
+	TAG_MARK,
 	TAG_MATH,
 	TAG_MROW,
 	TAG_MI,
@@ -69,15 +69,6 @@ enum	htmltag {
 	TAG_MAX
 };
 
-enum	htmlfont {
-	HTMLFONT_NONE = 0,
-	HTMLFONT_BOLD,
-	HTMLFONT_ITALIC,
-	HTMLFONT_BI,
-	HTMLFONT_CW,
-	HTMLFONT_MAX
-};
-
 struct	tag {
 	struct tag	 *next;
 	int		  refcnt;
@@ -111,8 +102,8 @@ struct	html {
 	char		 *base_includes; /* base for include href */
 	char		 *style; /* style-sheet URI */
 	struct tag	 *metaf; /* current open font scope */
-	enum htmlfont	  metal; /* last used font */
-	enum htmlfont	  metac; /* current font mode */
+	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 */
@@ -128,7 +119,6 @@ void		  roff_html_pre(struct html *, con
 void		  print_gen_comment(struct html *, struct roff_node *);
 void		  print_gen_decls(struct html *);
 void		  print_gen_head(struct html *);
-void		  print_metaf(struct html *, enum mandoc_esc);
 struct tag	 *print_otag(struct html *, enum htmltag, const char *, ...);
 void		  print_tagq(struct html *, const struct tag *);
 void		  print_stagq(struct html *, const struct tag *);
@@ -141,3 +131,4 @@ 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);
--- a/lib.in
+++ b/lib.in
@@ -43,7 +43,7 @@ LINE("libcipher",	"FreeSec Crypt Library
 LINE("libcompat",	"Compatibility Library (libcompat, \\-lcompat)")
 LINE("libcrypt",	"Crypt Library (libcrypt, \\-lcrypt)")
 LINE("libcurses",	"Curses Library (libcurses, \\-lcurses)")
-LINE("libcuse", 	"Userland Character Device Library (libcuse, \\-lcuse)")
+LINE("libcuse",		"Userland Character Device Library (libcuse, \\-lcuse)")
 LINE("libdevattr",	"Device attribute and event library (libdevattr, \\-ldevattr)")
 LINE("libdevctl",	"Device Control Library (libdevctl, \\-ldevctl)")
 LINE("libdevinfo",	"Device and Resource Information Utility Library (libdevinfo, \\-ldevinfo)")
--- a/libmandoc.h
+++ b/libmandoc.h
@@ -1,7 +1,7 @@
 /*	$Id: libmandoc.h,v 1.77 2018/12/21 17:15:18 schwarze Exp $ */
 /*
  * Copyright (c) 2009, 2010, 2011, 2012 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2013,2014,2015,2017,2018 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2013-2015,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
@@ -47,8 +47,9 @@ struct	buf {
 
 struct	roff;
 struct	roff_man;
+struct	roff_node;
 
-char		*mandoc_normdate(struct roff_man *, char *, int, int);
+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*);
--- a/main.c
+++ b/main.c
@@ -1,7 +1,7 @@
 /*	$Id: main.c,v 1.322 2019/03/06 10:18:58 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2010-2012, 2014-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2010-2012, 2014-2020 Ingo Schwarze <schwarze@openbsd.org>
  * Copyright (c) 2010 Joerg Sonnenberger <joerg@netbsd.org>
  *
  * Permission to use, copy, modify, and distribute this software for any
@@ -21,6 +21,7 @@
 #include <sys/types.h>
 #include <sys/ioctl.h>
 #include <sys/param.h>	/* MACHINE */
+#include <sys/stat.h>
 #include <sys/wait.h>
 
 #include <assert.h>
@@ -31,6 +32,7 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <glob.h>
+#include <limits.h>
 #if HAVE_SANDBOX_INIT
 #include <sandbox.h>
 #endif
@@ -76,13 +78,12 @@ enum	outt {
 	OUTT_PDF	/* -Tpdf */
 };
 
-struct	curparse {
-	struct mparse	 *mp;
-	struct manoutput *outopts;	/* output options */
+struct	outstate {
+	struct tag_files *tag_files;	/* Tagging state variables. */
 	void		 *outdata;	/* data for output */
-	char		 *os_s;		/* operating system for display */
+	int		  use_pager;
 	int		  wstop;	/* stop after a file with a warning */
-	enum mandoc_os	  os_e;		/* check base system conventions */
+	int		  had_output;	/* Some output was generated. */
 	enum outt	  outtype;	/* which output to use */
 };
 
@@ -95,17 +96,18 @@ static	int		  fs_lookup(const struct man
 				const char *, const char *,
 				struct manpage **, size_t *);
 static	int		  fs_search(const struct mansearch *,
-				const struct manpaths *, int, char**,
+				const struct manpaths *, const char *,
 				struct manpage **, size_t *);
-static	int		  koptions(int *, char *);
-static	void		  moptions(int *, char *);
-static	void		  outdata_alloc(struct curparse *);
-static	void		  parse(struct curparse *, int, const char *);
-static	void		  passthrough(const char *, int, int);
+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 *);
 static	pid_t		  spawn_pager(struct tag_files *);
-static	int		  toptions(struct curparse *, char *);
 static	void		  usage(enum argmode) __attribute__((__noreturn__));
-static	int		  woptions(struct curparse *, char *);
+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";
@@ -115,26 +117,31 @@ static	char		 *help_argv[] = {help_arg,
 int
 main(int argc, char *argv[])
 {
-	struct manconf	 conf;
-	struct mansearch search;
-	struct curparse	 curp;
-	struct winsize	 ws;
-	struct tag_files *tag_files;
-	struct manpage	*res, *resp;
-	const char	*progname, *sec, *thisarg;
-	char		*conf_file, *defpaths, *auxpaths;
-	char		*oarg, *tagarg;
+	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		 i, sz;
+	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;
-	enum outmode	 outmode;
-	int		 fd, startdir;
-	int		 show_usage;
-	int		 options;
-	int		 use_pager;
-	int		 status, signum;
+	int		 startdir;
 	int		 c;
-	pid_t		 pager_pid, tc_pgid, man_pgid, pid;
+	enum mandoc_os	 os_e;		/* Check base system conventions. */
+	enum outmode	 outmode;	/* According to command line. */
 
 #if HAVE_PROGNAME
 	progname = getprogname();
@@ -154,10 +161,11 @@ main(int argc, char *argv[])
 		return mandocdb(argc, argv);
 
 #if HAVE_PLEDGE
-	if (pledge("stdio rpath tmppath tty proc exec", NULL) == -1)
-		err((int)MANDOCLEVEL_SYSERR, "pledge");
+	if (pledge("stdio rpath tmppath tty proc exec", NULL) == -1) {
+		mandoc_msg(MANDOCERR_PLEDGE, 0, 0, "%s", strerror(errno));
+		return mandoc_msg_getrc();
+	}
 #endif
-
 #if HAVE_SANDBOX_INIT
 	if (sandbox_init(kSBXProfileNoInternet, SANDBOX_NAMED, NULL) == -1)
 		errx((int)MANDOCLEVEL_SYSERR, "sandbox_init");
@@ -166,8 +174,8 @@ main(int argc, char *argv[])
 	/* Search options. */
 
 	memset(&conf, 0, sizeof(conf));
-	conf_file = defpaths = NULL;
-	auxpaths = NULL;
+	conf_file = NULL;
+	defpaths = auxpaths = NULL;
 
 	memset(&search, 0, sizeof(struct mansearch));
 	search.outkey = "Nd";
@@ -184,15 +192,19 @@ main(int argc, char *argv[])
 	else
 		search.argmode = ARG_FILE;
 
-	/* Parser and formatter options. */
+	/* Parser options. */
 
-	memset(&curp, 0, sizeof(struct curparse));
-	curp.outtype = OUTT_LOCALE;
-	curp.outopts = &conf.output;
 	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;
 
-	use_pager = 1;
-	tag_files = NULL;
 	show_usage = 0;
 	outmode = OUTMODE_DEF;
 
@@ -210,30 +222,40 @@ main(int argc, char *argv[])
 			conf_file = optarg;
 			break;
 		case 'c':
-			use_pager = 0;
+			outst.use_pager = 0;
 			break;
 		case 'f':
 			search.argmode = ARG_WORD;
 			break;
 		case 'h':
 			conf.output.synopsisonly = 1;
-			use_pager = 0;
+			outst.use_pager = 0;
 			outmode = OUTMODE_ALL;
 			break;
 		case 'I':
-			if (strncmp(optarg, "os=", 3)) {
-				warnx("-I %s: Bad argument", optarg);
-				return (int)MANDOCLEVEL_BADARG;
+			if (strncmp(optarg, "os=", 3) != 0) {
+				mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0,
+				    "-I %s", optarg);
+				return mandoc_msg_getrc();
 			}
-			if (curp.os_s != NULL) {
-				warnx("-I %s: Duplicate argument", optarg);
-				return (int)MANDOCLEVEL_BADARG;
+			if (os_s != NULL) {
+				mandoc_msg(MANDOCERR_BADARG_DUPE, 0, 0,
+				    "-I %s", optarg);
+				return mandoc_msg_getrc();
 			}
-			curp.os_s = mandoc_strdup(optarg + 3);
+			os_s = optarg + 3;
 			break;
 		case 'K':
-			if ( ! koptions(&options, optarg))
-				return (int)MANDOCLEVEL_BADARG;
+			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;
@@ -258,12 +280,37 @@ main(int argc, char *argv[])
 			search.sec = optarg;
 			break;
 		case 'T':
-			if ( ! toptions(&curp, optarg))
-				return (int)MANDOCLEVEL_BADARG;
+			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(&curp, optarg))
-				return (int)MANDOCLEVEL_BADARG;
+			if (woptions(optarg, &os_e, &outst.wstop) == -1)
+				return mandoc_msg_getrc();
 			break;
 		case 'w':
 			outmode = OUTMODE_FLN;
@@ -279,11 +326,12 @@ main(int argc, char *argv[])
 
 	/* Postprocess options. */
 
-	if (outmode == OUTMODE_DEF) {
+	switch (outmode) {
+	case OUTMODE_DEF:
 		switch (search.argmode) {
 		case ARG_FILE:
 			outmode = OUTMODE_ALL;
-			use_pager = 0;
+			outst.use_pager = 0;
 			break;
 		case ARG_NAME:
 			outmode = OUTMODE_ONE;
@@ -292,6 +340,16 @@ main(int argc, char *argv[])
 			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) {
@@ -299,25 +357,22 @@ main(int argc, char *argv[])
 			search.outkey = oarg;
 		else {
 			while (oarg != NULL) {
-				thisarg = oarg;
 				if (manconf_output(&conf.output,
-				    strsep(&oarg, ","), 0) == 0)
-					continue;
-				warnx("-O %s: Bad argument", thisarg);
-				return (int)MANDOCLEVEL_BADARG;
+				    strsep(&oarg, ","), 0) == -1)
+					return mandoc_msg_getrc();
 			}
 		}
 	}
 
-	if (curp.outtype != OUTT_TREE || !curp.outopts->noval)
+	if (outst.outtype != OUTT_TREE || conf.output.noval == 0)
 		options |= MPARSE_VALIDATE;
 
 	if (outmode == OUTMODE_FLN ||
 	    outmode == OUTMODE_LST ||
 	    !isatty(STDOUT_FILENO))
-		use_pager = 0;
+		outst.use_pager = 0;
 
-	if (use_pager &&
+	if (outst.use_pager &&
 	    (conf.output.width == 0 || conf.output.indent == 0) &&
 	    ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1 &&
 	    ws.ws_col > 1) {
@@ -328,9 +383,13 @@ main(int argc, char *argv[])
 	}
 
 #if HAVE_PLEDGE
-	if (!use_pager)
-		if (pledge("stdio rpath", NULL) == -1)
-			err((int)MANDOCLEVEL_SYSERR, "pledge");
+	if (outst.use_pager == 0) {
+		if (pledge("stdio rpath", NULL) == -1) {
+			mandoc_msg(MANDOCERR_PLEDGE, 0, 0,
+			    "%s", strerror(errno));
+			return mandoc_msg_getrc();
+		}
+	}
 #endif
 
 	/* Parse arguments. */
@@ -339,11 +398,10 @@ main(int argc, char *argv[])
 		argc -= optind;
 		argv += optind;
 	}
-	resp = NULL;
 
 	/*
-	 * Quirks for help(1)
-	 * and for a man(1) section argument without -s.
+	 * Quirks for help(1) and man(1),
+	 * in particular for a section argument without -s.
 	 */
 
 	if (search.argmode == ARG_NAME) {
@@ -355,7 +413,7 @@ main(int argc, char *argv[])
 		} else if (argc > 1 &&
 		    ((uc = (unsigned char *)argv[0]) != NULL) &&
 		    ((isdigit(uc[0]) && (uc[1] == '\0' ||
-		      (isalpha(uc[1]) && uc[2] == '\0'))) ||
+		      isalpha(uc[1]))) ||
 		     (uc[0] == 'n' && uc[1] == '\0'))) {
 			search.sec = (char *)uc;
 			argv++;
@@ -367,6 +425,8 @@ main(int argc, char *argv[])
 		if (search.arch == NULL)
 			search.arch = MACHINE;
 #endif
+		if (outmode == OUTMODE_ONE)
+			search.firstmatch = 1;
 	}
 
 	/*
@@ -380,128 +440,155 @@ main(int argc, char *argv[])
 		conf.output.tag = tagarg == NULL ? *argv : tagarg + 1;
 	}
 
-	/* man(1), whatis(1), apropos(1) */
-
-	if (search.argmode != ARG_FILE) {
-		if (search.argmode == ARG_NAME &&
-		    outmode == OUTMODE_ONE)
-			search.firstmatch = 1;
-
-		/* Access the mandoc database. */
+	/* Read the configuration file. */
 
+	if (search.argmode != ARG_FILE)
 		manconf_parse(&conf, conf_file, defpaths, auxpaths);
-		if ( ! mansearch(&search, &conf.manpath,
-		    argc, argv, &res, &sz))
-			usage(search.argmode);
 
-		if (sz == 0 && search.argmode == ARG_NAME)
-			fs_search(&search, &conf.manpath,
-			    argc, argv, &res, &sz);
-
-		if (search.argmode == ARG_NAME) {
-			for (c = 0; c < argc; c++) {
-				if (strchr(argv[c], '/') == NULL)
-					continue;
-				if (access(argv[c], R_OK) == -1) {
-					warn("%s", argv[c]);
+	/* 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,
-				    sz + 1, sizeof(*res));
-				res[sz].file = mandoc_strdup(argv[c]);
-				res[sz].names = NULL;
-				res[sz].output = NULL;
-				res[sz].ipath = SIZE_MAX;
-				res[sz].sec = 10;
-				res[sz].form = FORM_SRC;
-				sz++;
+				    ressz + resnsz, sizeof(*res));
+				memcpy(res + ressz, resn,
+				    sizeof(*resn) * resnsz);
+				ressz += resnsz;
+				continue;
 			}
-		}
 
-		if (sz == 0) {
-			if (search.argmode != ARG_NAME)
-				warnx("nothing appropriate");
-			mandoc_msg_setrc(MANDOCLEVEL_BADARG);
-			goto out;
-		}
+			/* Search for the best section. */
 
-		/*
-		 * For standard man(1) and -a output mode,
-		 * prepare for copying filename pointers
-		 * into the program parameter array.
-		 */
-
-		if (outmode == OUTMODE_ONE) {
-			argc = 1;
-			best_prio = 20;
-		} else if (outmode == OUTMODE_ALL)
-			argc = (int)sz;
-
-		/* Iterate all matching manuals. */
-
-		resp = res;
-		for (i = 0; i < sz; i++) {
-			if (outmode == OUTMODE_FLN)
-				puts(res[i].file);
-			else if (outmode == OUTMODE_LST)
-				printf("%s - %s\n", res[i].names,
-				    res[i].output == NULL ? "" :
-				    res[i].output);
-			else if (outmode == OUTMODE_ONE) {
-				/* Search for the best section. */
-				sec = res[i].file;
+			best_prio = 40;
+			for (ib = i = 0; i < resnsz; i++) {
+				sec = resn[i].file;
 				sec += strcspn(sec, "123456789");
 				if (sec[0] == '\0')
-					continue;
+					continue; /* No section at all. */
 				prio = sec_prios[sec[0] - '1'];
-				if (sec[1] != '/')
-					prio += 10;
+				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;
-				resp = res + i;
+				ib = i;
 			}
+			res = mandoc_reallocarray(res, ressz + 1,
+			    sizeof(*res));
+			memcpy(res + ressz++, resn + ib, sizeof(*resn));
 		}
 
-		/*
-		 * For man(1), -a and -i output mode, fall through
-		 * to the main mandoc(1) code iterating files
-		 * and running the parsers on each of them.
-		 */
+	/* 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 (outmode == OUTMODE_FLN || outmode == OUTMODE_LST)
+		if (ressz == 0) {
+			warnx("nothing appropriate");
+			mandoc_msg_setrc(MANDOCLEVEL_BADARG);
 			goto out;
-	}
+		}
 
-	/* mandoc(1) */
+	/* mandoc(1): Take command line arguments as file names. */
 
-#if HAVE_PLEDGE
-	if (use_pager) {
-		if (pledge("stdio rpath tmppath tty proc exec", NULL) == -1)
-			err((int)MANDOCLEVEL_SYSERR, "pledge");
 	} else {
-		if (pledge("stdio rpath", NULL) == -1)
-			err((int)MANDOCLEVEL_SYSERR, "pledge");
+		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;
+		}
 	}
-#endif
 
-	if (search.argmode == ARG_FILE)
-		moptions(&options, auxpaths);
-
-	mchars_alloc();
-	curp.mp = mparse_alloc(options, curp.os_e, curp.os_s);
+	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 (argc < 1) {
-		if (use_pager) {
-			tag_files = tag_init();
-			tag_files->tagname = conf.output.tag;
-		}
-		thisarg = "<stdin>";
-		mandoc_msg_setinfilename(thisarg);
-		parse(&curp, STDIN_FILENO, thisarg);
-		mandoc_msg_setinfilename(NULL);
+	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
@@ -511,166 +598,57 @@ main(int argc, char *argv[])
 	 */
 	startdir = open(".", O_RDONLY | O_DIRECTORY);
 
-	while (argc > 0) {
-
-		/*
-		 * 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 != NULL) {
-			if (resp->ipath != SIZE_MAX)
-				(void)chdir(conf.manpath.paths[resp->ipath]);
-			else if (startdir != -1)
-				(void)fchdir(startdir);
-			thisarg = resp->file;
-		} else
-			thisarg = *argv;
-
-		fd = mparse_open(curp.mp, thisarg);
-		if (fd != -1) {
-			if (use_pager) {
-				use_pager = 0;
-				tag_files = tag_init();
-				tag_files->tagname = conf.output.tag;
-			}
-
-			mandoc_msg_setinfilename(thisarg);
-			if (resp == NULL || resp->form == FORM_SRC)
-				parse(&curp, fd, thisarg);
-			else
-				passthrough(resp->file, fd,
-				    conf.output.synopsisonly);
-			mandoc_msg_setinfilename(NULL);
-
-			if (ferror(stdout)) {
-				if (tag_files != NULL) {
-					warn("%s", tag_files->ofn);
-					tag_unlink();
-					tag_files = NULL;
-				} else
-					warn("stdout");
-				mandoc_msg_setrc(MANDOCLEVEL_SYSERR);
-				break;
-			}
-
-			if (argc > 1 && curp.outtype <= OUTT_UTF8) {
-				if (curp.outdata == NULL)
-					outdata_alloc(&curp);
-				terminal_sepline(curp.outdata);
-			}
-		} else
-			mandoc_msg(MANDOCERR_FILE, 0, 0,
-			    "%s: %s", thisarg, strerror(errno));
-
-		if (curp.wstop && mandoc_msg_getrc() != MANDOCLEVEL_OK)
+	for (i = 0; i < ressz; i++) {
+		process_onefile(mp, res + i, startdir, &outst, &conf);
+		if (outst.wstop && mandoc_msg_getrc() != MANDOCLEVEL_OK)
 			break;
-
-		if (resp != NULL)
-			resp++;
-		else
-			argv++;
-		if (--argc)
-			mparse_reset(curp.mp);
 	}
 	if (startdir != -1) {
 		(void)fchdir(startdir);
 		close(startdir);
 	}
 
-	if (curp.outdata != NULL) {
-		switch (curp.outtype) {
+	if (outst.outdata != NULL) {
+		switch (outst.outtype) {
 		case OUTT_HTML:
-			html_free(curp.outdata);
+			html_free(outst.outdata);
 			break;
 		case OUTT_UTF8:
 		case OUTT_LOCALE:
 		case OUTT_ASCII:
-			ascii_free(curp.outdata);
+			ascii_free(outst.outdata);
 			break;
 		case OUTT_PDF:
 		case OUTT_PS:
-			pspdf_free(curp.outdata);
+			pspdf_free(outst.outdata);
 			break;
 		default:
 			break;
 		}
 	}
 	mandoc_xr_free();
-	mparse_free(curp.mp);
+	mparse_free(mp);
 	mchars_free();
 
 out:
-	if (search.argmode != ARG_FILE) {
+	mansearch_free(res, ressz);
+	if (search.argmode != ARG_FILE)
 		manconf_free(&conf);
-		mansearch_free(res, sz);
-	}
 
-	free(curp.os_s);
-
-	/*
-	 * When using a pager, finish writing both temporary files,
-	 * fork it, wait for the user to close it, and clean up.
-	 */
-
-	if (tag_files != NULL) {
+	if (outst.tag_files != NULL) {
 		fclose(stdout);
 		tag_write();
-		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(tag_files->ofd);
-			if (tc_pgid != man_pgid) {
-				if (tc_pgid == pager_pid) {
-					(void)tcsetpgrp(tag_files->ofd,
-					    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(tag_files->ofd, pager_pid);
-				kill(pager_pid, SIGCONT);
-			} else
-				pager_pid = spawn_pager(tag_files);
-
-			/* Wait for the pager to stop or exit. */
-
-			while ((pid = waitpid(pager_pid, &status,
-			    WUNTRACED)) == -1 && errno == EINTR)
-				continue;
-
-			if (pid == -1) {
-				warn("wait");
-				mandoc_msg_setrc(MANDOCLEVEL_SYSERR);
-				break;
-			}
-			if (!WIFSTOPPED(status))
-				break;
-
-			signum = WSTOPSIG(status);
-		}
+		run_pager(outst.tag_files);
 		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] "
@@ -701,6 +679,7 @@ fs_lookup(const struct manpaths *paths,
 	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;
@@ -710,13 +689,13 @@ fs_lookup(const struct manpaths *paths,
 	form = FORM_SRC;
 	mandoc_asprintf(&file, "%s/man%s/%s.%s",
 	    paths->paths[ipath], sec, name, sec);
-	if (access(file, R_OK) != -1)
+	if (stat(file, &sb) != -1)
 		goto found;
 	free(file);
 
 	mandoc_asprintf(&file, "%s/cat%s/%s.0",
 	    paths->paths[ipath], sec, name);
-	if (access(file, R_OK) != -1) {
+	if (stat(file, &sb) != -1) {
 		form = FORM_CAT;
 		goto found;
 	}
@@ -725,7 +704,7 @@ fs_lookup(const struct manpaths *paths,
 	if (arch != NULL) {
 		mandoc_asprintf(&file, "%s/man%s/%s/%s.%s",
 		    paths->paths[ipath], sec, arch, name, sec);
-		if (access(file, R_OK) != -1)
+		if (stat(file, &sb) != -1)
 			goto found;
 		free(file);
 	}
@@ -734,98 +713,150 @@ fs_lookup(const struct manpaths *paths,
 	    paths->paths[ipath], sec, name);
 	globres = glob(file, 0, NULL, &globinfo);
 	if (globres != 0 && globres != GLOB_NOMATCH)
-		warn("%s: glob", file);
+		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)
-		goto found;
+	if (globres == 0) {
+		if (stat(file, &sb) != -1)
+			goto found;
+		free(file);
+	}
 	if (res != NULL || ipath + 1 != paths->sz)
-		return 0;
+		return -1;
 
 	mandoc_asprintf(&file, "%s.%s", name, sec);
-	globres = access(file, R_OK);
+	globres = stat(file, &sb);
 	free(file);
-	return globres != -1;
+	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 1;
+		return 0;
 	}
-	*res = mandoc_reallocarray(*res, ++*ressz, sizeof(struct manpage));
+	*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 1;
+	return 0;
 }
 
 static int
 fs_search(const struct mansearch *cfg, const struct manpaths *paths,
-	int argc, char **argv, struct manpage **res, size_t *ressz)
+	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, lastsz;
+	size_t		 ipath, isec;
 
 	assert(cfg->argmode == ARG_NAME);
-
 	if (res != NULL)
 		*res = NULL;
-	*ressz = lastsz = 0;
-	while (argc) {
-		for (ipath = 0; ipath < paths->sz; ipath++) {
-			if (cfg->sec != NULL) {
-				if (fs_lookup(paths, ipath, cfg->sec,
-				    cfg->arch, *argv, res, ressz) &&
-				    cfg->firstmatch)
-					return 1;
-			} else for (isec = 0; isec < nsec; isec++)
+	*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, *argv, res, ressz) &&
+				    cfg->arch, name, res, ressz) != -1 &&
 				    cfg->firstmatch)
-					return 1;
+					return 0;
 		}
-		if (res != NULL && *ressz == lastsz &&
-		    strchr(*argv, '/') == NULL) {
-			if (cfg->arch != NULL &&
-			    arch_valid(cfg->arch, OSENUM) == 0)
-				warnx("Unknown architecture \"%s\".",
-				    cfg->arch);
-			else if (cfg->sec == NULL)
-				warnx("No entry for %s in the manual.",
-				    *argv);
-			else
-				warnx("No entry for %s in section %s "
-				    "of the manual.", *argv, cfg->sec);
-		}
-		lastsz = *ressz;
-		argv++;
-		argc--;
 	}
-	return 0;
+	return -1;
 }
 
 static void
-parse(struct curparse *curp, int fd, const char *file)
+process_onefile(struct mparse *mp, struct manpage *resp, int startdir,
+    struct outstate *outst, struct manconf *conf)
 {
-	struct roff_meta *meta;
+	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 = tag_init(conf->output.tag);
+	}
+
+	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;
+	}
 
-	/* Begin by parsing the file itself. */
+	if (ferror(stdout)) {
+		if (outst->tag_files != NULL) {
+			mandoc_msg(MANDOCERR_WRITE, 0, 0, "%s: %s",
+			    outst->tag_files->ofn, strerror(errno));
+			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(file);
 	assert(fd >= 0);
+	if (file == NULL)
+		file = "<stdin>";
+
+	if (previous)
+		mparse_reset(mp);
+	else
+		previous = 1;
 
-	mparse_readfd(curp->mp, fd, file);
+	mparse_readfd(mp, fd, file);
 	if (fd != STDIN_FILENO)
 		close(fd);
 
@@ -834,61 +865,62 @@ parse(struct curparse *curp, int fd, con
 	 * level, do not produce output.
 	 */
 
-	if (curp->wstop && mandoc_msg_getrc() != MANDOCLEVEL_OK)
+	if (outst->wstop && mandoc_msg_getrc() != MANDOCLEVEL_OK)
 		return;
 
-	if (curp->outdata == NULL)
-		outdata_alloc(curp);
-	else if (curp->outtype == OUTT_HTML)
-		html_reset(curp);
+	if (outst->outdata == NULL)
+		outdata_alloc(outst, outconf);
+	else if (outst->outtype == OUTT_HTML)
+		html_reset(outst);
 
 	mandoc_xr_reset();
-	meta = mparse_result(curp->mp);
+	meta = mparse_result(mp);
 
 	/* Execute the out device, if it exists. */
 
+	outst->had_output = 1;
 	if (meta->macroset == MACROSET_MDOC) {
-		switch (curp->outtype) {
+		switch (outst->outtype) {
 		case OUTT_HTML:
-			html_mdoc(curp->outdata, meta);
+			html_mdoc(outst->outdata, meta);
 			break;
 		case OUTT_TREE:
-			tree_mdoc(curp->outdata, meta);
+			tree_mdoc(outst->outdata, meta);
 			break;
 		case OUTT_MAN:
-			man_mdoc(curp->outdata, meta);
+			man_mdoc(outst->outdata, meta);
 			break;
 		case OUTT_PDF:
 		case OUTT_ASCII:
 		case OUTT_UTF8:
 		case OUTT_LOCALE:
 		case OUTT_PS:
-			terminal_mdoc(curp->outdata, meta);
+			terminal_mdoc(outst->outdata, meta);
 			break;
 		case OUTT_MARKDOWN:
-			markdown_mdoc(curp->outdata, meta);
+			markdown_mdoc(outst->outdata, meta);
 			break;
 		default:
 			break;
 		}
 	}
 	if (meta->macroset == MACROSET_MAN) {
-		switch (curp->outtype) {
+		switch (outst->outtype) {
 		case OUTT_HTML:
-			html_man(curp->outdata, meta);
+			html_man(outst->outdata, meta);
 			break;
 		case OUTT_TREE:
-			tree_man(curp->outdata, meta);
+			tree_man(outst->outdata, meta);
 			break;
 		case OUTT_MAN:
-			mparse_copy(curp->mp);
+			mparse_copy(mp);
 			break;
 		case OUTT_PDF:
 		case OUTT_ASCII:
 		case OUTT_UTF8:
 		case OUTT_LOCALE:
 		case OUTT_PS:
-			terminal_man(curp->outdata, meta);
+			terminal_man(outst->outdata, meta);
 			break;
 		default:
 			break;
@@ -919,7 +951,7 @@ check_xr(void)
 		search.firstmatch = 1;
 		if (mansearch(&search, &paths, 1, &xr->name, NULL, &sz))
 			continue;
-		if (fs_search(&search, &paths, 1, &xr->name, NULL, &sz))
+		if (fs_search(&search, &paths, xr->name, NULL, &sz) != -1)
 			continue;
 		if (xr->count == 1)
 			mandoc_msg(MANDOCERR_XR_BAD, xr->line,
@@ -932,26 +964,26 @@ check_xr(void)
 }
 
 static void
-outdata_alloc(struct curparse *curp)
+outdata_alloc(struct outstate *outst, struct manoutput *outconf)
 {
-	switch (curp->outtype) {
+	switch (outst->outtype) {
 	case OUTT_HTML:
-		curp->outdata = html_alloc(curp->outopts);
+		outst->outdata = html_alloc(outconf);
 		break;
 	case OUTT_UTF8:
-		curp->outdata = utf8_alloc(curp->outopts);
+		outst->outdata = utf8_alloc(outconf);
 		break;
 	case OUTT_LOCALE:
-		curp->outdata = locale_alloc(curp->outopts);
+		outst->outdata = locale_alloc(outconf);
 		break;
 	case OUTT_ASCII:
-		curp->outdata = ascii_alloc(curp->outopts);
+		outst->outdata = ascii_alloc(outconf);
 		break;
 	case OUTT_PDF:
-		curp->outdata = pdf_alloc(curp->outopts);
+		outst->outdata = pdf_alloc(outconf);
 		break;
 	case OUTT_PS:
-		curp->outdata = ps_alloc(curp->outopts);
+		outst->outdata = ps_alloc(outconf);
 		break;
 	default:
 		break;
@@ -959,34 +991,34 @@ outdata_alloc(struct curparse *curp)
 }
 
 static void
-passthrough(const char *file, int fd, int synopsis_only)
+passthrough(int fd, int synopsis_only)
 {
 	const char	 synb[] = "S\bSY\bYN\bNO\bOP\bPS\bSI\bIS\bS";
 	const char	 synr[] = "SYNOPSIS";
 
 	FILE		*stream;
-	const char	*syscall;
 	char		*line, *cp;
 	size_t		 linesz;
 	ssize_t		 len, written;
-	int		 print;
+	int		 lno, print;
 
+	stream = NULL;
 	line = NULL;
 	linesz = 0;
 
 	if (fflush(stdout) == EOF) {
-		syscall = "fflush";
-		goto fail;
+		mandoc_msg(MANDOCERR_FFLUSH, 0, 0, "%s", strerror(errno));
+		goto done;
 	}
-
 	if ((stream = fdopen(fd, "r")) == NULL) {
 		close(fd);
-		syscall = "fdopen";
-		goto fail;
+		mandoc_msg(MANDOCERR_FDOPEN, 0, 0, "%s", strerror(errno));
+		goto done;
 	}
 
-	print = 0;
+	lno = print = 0;
 	while ((len = getline(&line, &linesz, stream)) != -1) {
+		lno++;
 		cp = line;
 		if (synopsis_only) {
 			if (print) {
@@ -1004,98 +1036,24 @@ passthrough(const char *file, int fd, in
 			}
 		}
 		for (; len > 0; len -= written) {
-			if ((written = write(STDOUT_FILENO, cp, len)) != -1)
-				continue;
-			fclose(stream);
-			syscall = "write";
-			goto fail;
+			if ((written = write(STDOUT_FILENO, cp, len)) == -1) {
+				mandoc_msg(MANDOCERR_WRITE, 0, 0,
+				    "%s", strerror(errno));
+				goto done;
+			}
 		}
 	}
-
-	if (ferror(stream)) {
-		fclose(stream);
-		syscall = "getline";
-		goto fail;
-	}
+	if (ferror(stream))
+		mandoc_msg(MANDOCERR_GETLINE, lno, 0, "%s", strerror(errno));
 
 done:
 	free(line);
-	fclose(stream);
-	return;
-
-fail:
-	free(line);
-	warn("%s: SYSERR: %s", file, syscall);
-	mandoc_msg_setrc(MANDOCLEVEL_SYSERR);
-}
-
-static int
-koptions(int *options, char *arg)
-{
-
-	if ( ! strcmp(arg, "utf-8")) {
-		*options |=  MPARSE_UTF8;
-		*options &= ~MPARSE_LATIN1;
-	} else if ( ! strcmp(arg, "iso-8859-1")) {
-		*options |=  MPARSE_LATIN1;
-		*options &= ~MPARSE_UTF8;
-	} else if ( ! strcmp(arg, "us-ascii")) {
-		*options &= ~(MPARSE_UTF8 | MPARSE_LATIN1);
-	} else {
-		warnx("-K %s: Bad argument", arg);
-		return 0;
-	}
-	return 1;
-}
-
-static void
-moptions(int *options, char *arg)
-{
-
-	if (arg == NULL)
-		return;
-	if (strcmp(arg, "doc") == 0)
-		*options |= MPARSE_MDOC;
-	else if (strcmp(arg, "an") == 0)
-		*options |= MPARSE_MAN;
-}
-
-static int
-toptions(struct curparse *curp, char *arg)
-{
-
-	if (0 == strcmp(arg, "ascii"))
-		curp->outtype = OUTT_ASCII;
-	else if (0 == strcmp(arg, "lint")) {
-		curp->outtype = OUTT_LINT;
-		mandoc_msg_setoutfile(stdout);
-		mandoc_msg_setmin(MANDOCERR_BASE);
-	} else if (0 == strcmp(arg, "tree"))
-		curp->outtype = OUTT_TREE;
-	else if (0 == strcmp(arg, "man"))
-		curp->outtype = OUTT_MAN;
-	else if (0 == strcmp(arg, "html"))
-		curp->outtype = OUTT_HTML;
-	else if (0 == strcmp(arg, "markdown"))
-		curp->outtype = OUTT_MARKDOWN;
-	else if (0 == strcmp(arg, "utf8"))
-		curp->outtype = OUTT_UTF8;
-	else if (0 == strcmp(arg, "locale"))
-		curp->outtype = OUTT_LOCALE;
-	else if (0 == strcmp(arg, "ps"))
-		curp->outtype = OUTT_PS;
-	else if (0 == strcmp(arg, "pdf"))
-		curp->outtype = OUTT_PDF;
-	else {
-		warnx("-T %s: Bad argument", arg);
-		return 0;
-	}
-
-	return 1;
+	if (stream != NULL)
+		fclose(stream);
 }
 
 static int
-woptions(struct curparse *curp, char *arg)
+woptions(char *arg, enum mandoc_os *os_e, int *wstop)
 {
 	char		*v, *o;
 	const char	*toks[11];
@@ -1116,7 +1074,7 @@ woptions(struct curparse *curp, char *ar
 		o = arg;
 		switch (getsubopt(&arg, (char * const *)toks, &v)) {
 		case 0:
-			curp->wstop = 1;
+			*wstop = 1;
 			break;
 		case 1:
 		case 2:
@@ -1135,22 +1093,80 @@ woptions(struct curparse *curp, char *ar
 			mandoc_msg_setmin(MANDOCERR_UNSUPP);
 			break;
 		case 7:
-			mandoc_msg_setmin(MANDOCERR_MAX);
+			mandoc_msg_setmin(MANDOCERR_BADARG);
 			break;
 		case 8:
 			mandoc_msg_setmin(MANDOCERR_BASE);
-			curp->os_e = MANDOC_OS_OPENBSD;
+			*os_e = MANDOC_OS_OPENBSD;
 			break;
 		case 9:
 			mandoc_msg_setmin(MANDOCERR_BASE);
-			curp->os_e = MANDOC_OS_NETBSD;
+			*os_e = MANDOC_OS_NETBSD;
 			break;
 		default:
-			warnx("-W %s: Bad argument", o);
-			return 0;
+			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)
+{
+	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(tag_files->ofd);
+		if (tc_pgid != man_pgid) {
+			if (tc_pgid == pager_pid) {
+				(void)tcsetpgrp(tag_files->ofd, 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(tag_files->ofd, pager_pid);
+			kill(pager_pid, SIGCONT);
+		} else
+			pager_pid = spawn_pager(tag_files);
+
+		/* 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);
 	}
-	return 1;
 }
 
 static pid_t
@@ -1196,7 +1212,7 @@ spawn_pager(struct tag_files *tag_files)
 
 	use_ofn = 1;
 #if HAVE_LESS_T
-	if ((cmdlen = strlen(argv[0])) >= 4) {
+	if (*tag_files->tfn != '\0' && (cmdlen = strlen(argv[0])) >= 4) {
 		cp = argv[0] + cmdlen - 4;
 		if (strcmp(cp, "less") == 0) {
 			argv[argc++] = mandoc_strdup("-T");
@@ -1215,15 +1231,19 @@ spawn_pager(struct tag_files *tag_files)
 
 	switch (pager_pid = fork()) {
 	case -1:
-		err((int)MANDOCLEVEL_SYSERR, "fork");
+		mandoc_msg(MANDOCERR_FORK, 0, 0, "%s", strerror(errno));
+		exit(mandoc_msg_getrc());
 	case 0:
 		break;
 	default:
 		(void)setpgid(pager_pid, 0);
 		(void)tcsetpgrp(tag_files->ofd, pager_pid);
 #if HAVE_PLEDGE
-		if (pledge("stdio rpath tmppath tty proc", NULL) == -1)
-			err((int)MANDOCLEVEL_SYSERR, "pledge");
+		if (pledge("stdio rpath tmppath tty proc", NULL) == -1) {
+			mandoc_msg(MANDOCERR_PLEDGE, 0, 0,
+			    "%s", strerror(errno));
+			exit(mandoc_msg_getrc());
+		}
 #endif
 		tag_files->pager_pid = pager_pid;
 		return pager_pid;
@@ -1231,8 +1251,10 @@ spawn_pager(struct tag_files *tag_files)
 
 	/* The child process becomes the pager. */
 
-	if (dup2(tag_files->ofd, STDOUT_FILENO) == -1)
-		err((int)MANDOCLEVEL_SYSERR, "pager stdout");
+	if (dup2(tag_files->ofd, STDOUT_FILENO) == -1) {
+		mandoc_msg(MANDOCERR_DUP, 0, 0, "%s", strerror(errno));
+		_exit(mandoc_msg_getrc());
+	}
 	close(tag_files->ofd);
 	assert(tag_files->tfd == -1);
 
@@ -1242,5 +1264,6 @@ spawn_pager(struct tag_files *tag_files)
 		nanosleep(&timeout, NULL);
 
 	execvp(argv[0], argv);
-	err((int)MANDOCLEVEL_SYSERR, "exec %s", argv[0]);
+	mandoc_msg(MANDOCERR_EXEC, 0, 0, "%s: %s", argv[0], strerror(errno));
+	_exit(mandoc_msg_getrc());
 }
--- a/man.1
+++ b/man.1
@@ -3,7 +3,7 @@
 .\" Copyright (c) 1989, 1990, 1993
 .\"	The Regents of the University of California.  All rights reserved.
 .\" Copyright (c) 2003, 2007, 2008, 2014 Jason McIntyre <jmc@openbsd.org>
-.\" Copyright (c) 2010, 2011, 2014-2018 Ingo Schwarze <schwarze@openbsd.org>
+.\" Copyright (c) 2010, 2011, 2014-2020 Ingo Schwarze <schwarze@openbsd.org>
 .\"
 .\" Redistribution and use in source and binary forms, with or without
 .\" modification, are permitted provided that the following conditions
@@ -51,7 +51,7 @@ The
 .Nm
 utility
 displays the
-manual pages entitled
+manual page entitled
 .Ar name .
 Pages may be selected according to
 a specific category
@@ -64,7 +64,6 @@ The options are as follows:
 .Bl -tag -width Ds
 .It Fl a
 Display all matching manual pages.
-Normally, only the first page found is displayed.
 .It Fl C Ar file
 Use the specified
 .Ar file
@@ -129,31 +128,31 @@ are ignored.
 This option implies
 .Fl a .
 .It Fl M Ar path
-Override the list of standard directories which
-.Nm
-searches for manual pages.
+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 search path may also be set using the environment variable
-.Ev MANPATH .
+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 standard directories which
-.Nm
-searches for manual pages.
+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 the standard directories or
-the directories specified using the
+These directories will be searched before those specified using the
 .Fl M
-option or the
+option, the
 .Ev MANPATH
-environment variable.
+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
@@ -197,13 +196,12 @@ System maintenance and operation command
 .It 9
 Kernel internals.
 .El
-.Pp
-If not specified and a match is found in more than one section,
-the first match is selected from the following list:
-1, 8, 6, 2, 3, 5, 7, 4, 9, 3p.
 .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
@@ -214,9 +212,23 @@ The options
 .Fl fkl
 are mutually exclusive and override each other.
 .Pp
-Guidelines for writing
-man pages can be found in
-.Xr mdoc 7 .
+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
@@ -236,6 +248,10 @@ The database is kept up to date with
 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
@@ -286,15 +302,15 @@ manual opens a manual page at the defini
 .Ar term
 rather than at the beginning.
 .It Ev MANPATH
-The standard search path used by
-.Nm
-may be changed by specifying a path in the
+Override the standard search path which is either specified in
+.Xr man.conf 5
+or the default path.
+The format of
 .Ev MANPATH
-environment variable.
-The format of the path is a colon
+is a colon
 .Pq Ql \&:
 separated list of directories.
-Invalid paths are ignored.
+Invalid directories are ignored.
 Overridden by
 .Fl M ,
 ignored if
@@ -303,12 +319,10 @@ is specified.
 .Pp
 If
 .Ev MANPATH
-begins with a colon, it is appended to the default list;
-if it ends with a colon, it is prepended to the default list;
+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 search path is inserted between the colons.
-If none of these conditions are met, it overrides the
-standard search path.
+the standard path is inserted between the colons.
 .It Ev PAGER
 Specifies the pagination program to use when
 .Ev MANPAGER
@@ -321,7 +335,9 @@ is used.
 .Sh FILES
 .Bl -tag -width /etc/man.conf -compact
 .It Pa /etc/man.conf
-default man configuration file
+default
+.Nm
+configuration file
 .El
 .Sh EXIT STATUS
 .Ex -std man
@@ -365,7 +381,7 @@ are extensions to that specification.
 A
 .Nm
 command first appeared in
-.At v3 .
+.At v2 .
 .Pp
 The
 .Fl w
--- a/man.7
+++ b/man.7
@@ -160,7 +160,9 @@ This has no effect unless the tabulator
 .Ic ta
 request.
 .It Ic EE
-This is a non-standard GNU extension.
+This is a non-standard Version 9
+.At
+extension later adopted by GNU.
 In
 .Xr mandoc 1 ,
 it does the same as the
@@ -168,7 +170,9 @@ it does the same as the
 .Ic fi
 request (switch to fill mode).
 .It Ic EX
-This is a non-standard GNU extension.
+This is a non-standard Version 9
+.At
+extension later adopted by GNU.
 In
 .Xr mandoc 1 ,
 it does the same as the
@@ -496,8 +500,8 @@ The syntax is as follows:
 .It Ic BI  Ta    n         Ta    current   Ta    \&
 .It Ic BR  Ta    n         Ta    current   Ta    \&
 .It Ic DT  Ta    0         Ta    current   Ta    \&
-.It Ic EE  Ta    0         Ta    current   Ta    GNU
-.It Ic EX  Ta    0         Ta    current   Ta    GNU
+.It Ic EE  Ta    0         Ta    current   Ta    Version 9 At
+.It Ic EX  Ta    0         Ta    current   Ta    Version 9 At
 .It Ic I   Ta    n         Ta    next-line Ta    \&
 .It Ic IB  Ta    n         Ta    current   Ta    \&
 .It Ic IR  Ta    n         Ta    current   Ta    \&
--- a/man.conf.5
+++ b/man.conf.5
@@ -101,15 +101,11 @@ manual.
 .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
-.It Ic _whatdb Ar path Ns Cm /whatis.db
-This directive provides the same functionality as
-.Ic manpath ,
-but using a historic and misleading syntax.
-It is kept for backward compatibility for now,
-but will eventually be removed.
 .El
 .Sh FILES
-.Pa /etc/man.conf
+.Bl -tag -width /etc/examples/man.conf -compact
+.It Pa /etc/man.conf
+.El
 .Sh EXAMPLES
 The following configuration file reproduces the defaults:
 installing it is equivalent to not having a
--- a/man_html.c
+++ b/man_html.c
@@ -203,9 +203,9 @@ print_man_node(MAN_ARGS)
 		 * Close out scope of font prior to opening a macro
 		 * scope.
 		 */
-		if (HTMLFONT_NONE != h->metac) {
+		if (h->metac != ESCAPE_FONTROMAN) {
 			h->metal = h->metac;
-			h->metac = HTMLFONT_NONE;
+			h->metac = ESCAPE_FONTROMAN;
 		}
 
 		/*
--- a/man_term.c
+++ b/man_term.c
@@ -1,7 +1,7 @@
 /*	$Id: man_term.c,v 1.228 2019/01/05 21:18:26 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2010-2015, 2017-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2010-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
@@ -27,10 +27,12 @@
 #include <string.h>
 
 #include "mandoc_aux.h"
+#include "mandoc.h"
 #include "roff.h"
 #include "man.h"
 #include "out.h"
 #include "term.h"
+#include "tag.h"
 #include "main.h"
 
 #define	MAXMARGINS	  64 /* maximum number of indented scopes */
@@ -92,6 +94,8 @@ static	void		  post_SY(DECL_ARGS);
 static	void		  post_TP(DECL_ARGS);
 static	void		  post_UR(DECL_ARGS);
 
+static	void		  tag_man(struct termp *, struct roff_node *);
+
 static const struct man_term_act man_term_acts[MAN_MAX - MAN_TH] = {
 	{ NULL, NULL, 0 }, /* TH */
 	{ pre_SH, post_SH, 0 }, /* SH */
@@ -146,7 +150,7 @@ terminal_man(void *arg, const struct rof
 {
 	struct mtermp		 mt;
 	struct termp		*p;
-	struct roff_node	*n;
+	struct roff_node	*n, *nc, *nn;
 	size_t			 save_defindent;
 
 	p = (struct termp *)arg;
@@ -165,18 +169,23 @@ terminal_man(void *arg, const struct rof
 
 	n = man->first->child;
 	if (p->synopsisonly) {
-		while (n != NULL) {
-			if (n->tok == MAN_SH &&
-			    n->child->child->type == ROFFT_TEXT &&
-			    !strcmp(n->child->child->string, "SYNOPSIS")) {
-				if (n->child->next->child != NULL)
-					print_man_nodelist(p, &mt,
-					    n->child->next->child, man);
-				term_newln(p);
+		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;
-			}
-			n = n->next;
+			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;
@@ -310,7 +319,7 @@ pre_alternate(DECL_ARGS)
 		assert(nn->type == ROFFT_TEXT);
 		term_word(p, nn->string);
 		if (nn->flags & NODE_EOS)
-                	p->flags |= TERMP_SENTENCE;
+			p->flags |= TERMP_SENTENCE;
 		if (nn->next != NULL)
 			p->flags |= TERMP_NOSPACE;
 	}
@@ -529,8 +538,10 @@ pre_IP(DECL_ARGS)
 	case ROFFT_HEAD:
 		p->tcol->offset = mt->offset;
 		p->tcol->rmargin = mt->offset + len;
-		if (n->child != NULL)
+		if (n->child != NULL) {
 			print_man_node(p, mt, n->child, meta);
+			tag_man(p, n->child);
+		}
 		return 0;
 	case ROFFT_BODY:
 		p->tcol->offset = mt->offset + len;
@@ -610,6 +621,18 @@ pre_TP(DECL_ARGS)
 		while (nn != NULL && (nn->flags & NODE_LINE) == 0)
 			nn = nn->next;
 
+		if (nn == NULL)
+			return 0;
+
+		if (nn->type == ROFFT_TEXT)
+			tag_man(p, nn);
+		else if (nn->child != NULL &&
+		    nn->child->type == ROFFT_TEXT &&
+		    (nn->tok == MAN_B || nn->tok == MAN_BI ||
+		     nn->tok == MAN_BR || nn->tok == MAN_I ||
+		     nn->tok == MAN_IB || nn->tok == MAN_IR))
+			tag_man(p, nn->child);
+
 		while (nn != NULL) {
 			print_man_node(p, mt, nn, meta);
 			nn = nn->next;
@@ -1143,3 +1166,60 @@ print_man_head(struct termp *p, const st
 	}
 	free(title);
 }
+
+/*
+ * 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
+tag_man(struct termp *p, struct roff_node *n)
+{
+	const char	*cp, *arg;
+	int		 prio, sz;
+
+	assert(n->type == ROFFT_TEXT);
+	cp = n->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_FONTROMAN:
+			case ESCAPE_FONTITALIC:
+			case ESCAPE_FONTBOLD:
+			case ESCAPE_FONTPREV:
+			case ESCAPE_FONTBI:
+				break;
+			case ESCAPE_SPECIAL:
+				if (sz != 1)
+					return;
+				switch (*arg) {
+				case '&':
+				case '-':
+				case 'e':
+					break;
+				default:
+					return;
+				}
+				break;
+			default:
+				return;
+			}
+			break;
+		default:
+			if (isalpha((unsigned char)*cp))
+				tag_put(cp, prio, p->line);
+			return;
+		}
+	}
+}
--- a/man_validate.c
+++ b/man_validate.c
@@ -1,7 +1,7 @@
 /*	$Id: man_validate.c,v 1.146 2018/12/31 10:04:39 schwarze Exp $ */
 /*
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2010, 2012-2018 Ingo Schwarze <schwarze@openbsd.org>
+ * 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
@@ -41,7 +41,7 @@
 
 typedef	void	(*v_check)(CHKARGS);
 
-static	void	  check_abort(CHKARGS);
+static	void	  check_abort(CHKARGS) __attribute__((__noreturn__));
 static	void	  check_par(CHKARGS);
 static	void	  check_part(CHKARGS);
 static	void	  check_root(CHKARGS);
@@ -185,8 +185,7 @@ check_root(CHKARGS)
 
 		man->meta.title = mandoc_strdup("");
 		man->meta.msec = mandoc_strdup("");
-		man->meta.date = man->quick ? mandoc_strdup("") :
-		    mandoc_normdate(man, NULL, n->line, n->pos);
+		man->meta.date = mandoc_normdate(NULL, NULL);
 	}
 
 	if (man->meta.os_e &&
@@ -369,8 +368,8 @@ post_TH(CHKARGS)
 	/* ->TITLE<- MSEC DATE OS VOL */
 
 	n = n->child;
-	if (n && n->string) {
-		for (p = n->string; '\0' != *p; p++) {
+	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)) {
@@ -388,9 +387,9 @@ post_TH(CHKARGS)
 
 	/* TITLE ->MSEC<- DATE OS VOL */
 
-	if (n)
+	if (n != NULL)
 		n = n->next;
-	if (n && n->string)
+	if (n != NULL && n->string != NULL)
 		man->meta.msec = mandoc_strdup(n->string);
 	else {
 		man->meta.msec = mandoc_strdup("");
@@ -400,18 +399,12 @@ post_TH(CHKARGS)
 
 	/* TITLE MSEC ->DATE<- OS VOL */
 
-	if (n)
+	if (n != NULL)
 		n = n->next;
-	if (n && n->string && '\0' != n->string[0]) {
-		man->meta.date = man->quick ?
-		    mandoc_strdup(n->string) :
-		    mandoc_normdate(man, n->string, n->line, n->pos);
-	} else {
+	if (man->quick && n != NULL)
 		man->meta.date = mandoc_strdup("");
-		mandoc_msg(MANDOCERR_DATE_MISSING,
-		    n ? n->line : nb->line,
-		    n ? n->pos : nb->pos, "TH");
-	}
+	else
+		man->meta.date = mandoc_normdate(n, nb);
 
 	/* TITLE MSEC DATE ->OS<- VOL */
 
--- a/mandoc.1
+++ b/mandoc.1
@@ -1,7 +1,7 @@
 .\"	$Id: mandoc.1,v 1.237 2019/02/23 18:53:54 schwarze Exp $
 .\"
 .\" Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
-.\" Copyright (c) 2012, 2014-2018 Ingo Schwarze <schwarze@openbsd.org>
+.\" 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
@@ -222,7 +222,8 @@ reads from standard input.
 .Pp
 The options
 .Fl fhklw
-are also supported and are documented in man(1).
+are also supported and are documented in
+.Xr man 1 .
 In
 .Fl f
 and
@@ -697,7 +698,7 @@ 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 cause
+Such errors may cause
 .Nm
 to exit at once, possibly in the middle of parsing or formatting a file.
 .El
@@ -777,6 +778,13 @@ 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
@@ -825,8 +833,7 @@ Messages of the
 .Cm error ,
 and
 .Cm unsupp
-levels except those about non-existent or unreadable input files
-are hidden unless their level, or a lower level, is requested using a
+levels are hidden unless their level, or a lower level, is requested using a
 .Fl W
 option or
 .Fl T Cm lint
@@ -1066,7 +1073,7 @@ macro lacks the mandatory section argume
 The section number in a
 .Ic \&Dt
 line is invalid, but still used.
-.It Sy "missing date, using today's date"
+.It Sy "missing date, using \(dq\(dq"
 .Pq mdoc, man
 The document was parsed as
 .Xr mdoc 7
@@ -1699,9 +1706,12 @@ The meaning of blank input lines is only
 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 replaced with
+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:
@@ -2238,6 +2248,43 @@ 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 ,
--- a/mandoc.c
+++ b/mandoc.c
@@ -1,7 +1,7 @@
 /*	$Id: mandoc.c,v 1.114 2018/12/30 00:49:55 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2011-2015, 2017, 2018 Ingo Schwarze <schwarze@openbsd.org>
+ * 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
@@ -494,9 +494,10 @@ time2a(time_t t)
 	size_t		 ssz;
 	int		 isz;
 
+	buf = NULL;
 	tm = localtime(&t);
 	if (tm == NULL)
-		return NULL;
+		goto fail;
 
 	/*
 	 * Reserve space:
@@ -520,7 +521,8 @@ time2a(time_t t)
 	 * of looking at LC_TIME.
 	 */
 
-	if ((isz = snprintf(p, 4 + 1, "%d, ", tm->tm_mday)) == -1)
+	isz = snprintf(p, 4 + 1, "%d, ", tm->tm_mday);
+	if (isz < 0 || isz > 4)
 		goto fail;
 	p += isz;
 
@@ -530,46 +532,63 @@ time2a(time_t t)
 
 fail:
 	free(buf);
-	return NULL;
+	return mandoc_strdup("");
 }
 
 char *
-mandoc_normdate(struct roff_man *man, char *in, int ln, int pos)
+mandoc_normdate(struct roff_node *nch, struct roff_node *nbl)
 {
 	char		*cp;
 	time_t		 t;
 
-	/* No date specified: use today's date. */
+	/* No date specified. */
 
-	if (in == NULL || *in == '\0' || strcmp(in, "$" "Mdocdate$") == 0) {
-		mandoc_msg(MANDOCERR_DATE_MISSING, ln, pos, NULL);
-		return time2a(time(NULL));
+	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 $", in) ||
-	    a2time(&t, "%b %d, %Y", in)) {
+	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, ln, pos, "%s", cp);
-		else if (*in != '$' && strcmp(in, cp) != 0)
-			mandoc_msg(MANDOCERR_DATE_NORM, ln, pos, "%s", cp);
+			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", in) == 0)
-		mandoc_msg(MANDOCERR_DATE_BAD, ln, pos, "%s", in);
+	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, ln, pos, "%s", in);
-	else if (man->meta.macroset == MACROSET_MDOC)
-		mandoc_msg(MANDOCERR_DATE_LEGACY, ln, pos, "Dd %s", in);
+		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(in);
+	return mandoc_strdup(nch->string);
 }
 
 int
--- a/mandoc.css
+++ b/mandoc.css
@@ -10,8 +10,13 @@
 
 /* Global defaults. */
 
-html {		max-width: 65em; }
-body {		font-family: Helvetica,Arial,sans-serif; }
+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; }
@@ -69,8 +74,7 @@ td.foot-os {	text-align: right; }
 section.Sh { }
 h1.Sh {		margin-top: 1.2em;
 		margin-bottom: 0.6em;
-		margin-left: -3.2em;
-		font-size: 110%; }
+		margin-left: -3.2em; }
 section.Ss { }
 h2.Ss {		margin-top: 1.2em;
 		margin-bottom: 0.6em;
@@ -310,14 +314,14 @@ h1.Sh::before, h2.Ss::before, .St::befor
 		pointer-events: none;
 		position: absolute;
 		bottom: 100%;
-		box-shadow: 0 0 .35em #000;
+		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;
-		color: black;
-		background: #fff; }
+		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,
@@ -345,3 +349,12 @@ h1.Sh, h2.Ss {	margin-left: 0em; }
 .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; }
+}
--- a/mandoc.h
+++ b/mandoc.h
@@ -1,7 +1,7 @@
 /*	$Id: mandoc.h,v 1.262 2018/12/16 00:17:02 schwarze Exp $ */
 /*
  * Copyright (c) 2010, 2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2012-2018 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 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
@@ -83,7 +83,7 @@ enum	mandocerr {
 	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_DATE_MISSING, /* missing date, using today's date */
+	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 "" */
@@ -193,7 +193,6 @@ enum	mandocerr {
 	MANDOCERR_TBLDATA_BLK, /* data block open at end of tbl: macro */
 
 	/* related to document structure and macros */
-	MANDOCERR_FILE, /* cannot open file */
 	MANDOCERR_PROLOG_REP, /* duplicate prologue macro: macro */
 	MANDOCERR_DT_LATE, /* skipping late title macro: Dt args */
 	MANDOCERR_ROFFLOOP, /* input stack limit exceeded, infinite loop? */
@@ -224,6 +223,7 @@ enum	mandocerr {
 	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 */
@@ -242,6 +242,35 @@ enum	mandocerr {
 	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
 };
 
@@ -281,6 +310,7 @@ 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);
--- a/mandoc_char.7
+++ b/mandoc_char.7
@@ -107,8 +107,8 @@ supporting it, for example in
 .Fl T Cm utf8
 and
 .Fl T Cm html .
-But currently, no practically relevant manual page formatter actually
-requires that subtlety, so in manual pages just write plain
+But currently, no practically relevant manual page formatter requires
+that subtlety, so in manual pages, it is sufficient to write plain
 .Sq -
 to represent hyphen, minus, and hyphen-minus.
 .Pp
--- a/mandoc_headers.3
+++ b/mandoc_headers.3
@@ -1,3 +1,19 @@
+.\"	$Id$
+.\"
+.\" Copyright (c) 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 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: December 30 2018 $
 .Dt MANDOC_HEADERS 3
 .Os
@@ -129,19 +145,19 @@ and the function
 Uses pointers to the types
 .Vt struct ohash
 from
-.Pa mandoc_ohash.h ,
+.Qq Pa mandoc_ohash.h ,
 .Vt struct mdoc_arg
 and
 .Vt union mdoc_data
 from
-.Pa mdoc.h ,
+.Qq Pa mdoc.h ,
 .Vt struct tbl_span
 from
-.Pa tbl.h ,
+.Qq Pa tbl.h ,
 and
 .Vt struct eqn_box
 from
-.Pa eqn.h
+.Qq Pa eqn.h
 as opaque struct members.
 .It Qq Pa tbl.h
 Data structures for the
@@ -184,13 +200,13 @@ Top level parser interface, for use in t
 and in the main parser, but not in formatters.
 .Pp
 Requires
-.Pa mandoc.h
+.Qq Pa mandoc.h
 for
 .Vt enum mandocerr
 and
 .Vt enum mandoclevel
 and
-.Pa roff.h
+.Qq Pa roff.h
 for
 .Vt enum mandoc_os .
 .Pp
@@ -202,7 +218,7 @@ for function prototypes.
 Uses
 .Vt struct roff_meta
 from
-.Pa roff.h
+.Qq Pa roff.h
 as an opaque type for function prototypes.
 .It Qq Pa mandoc_xr.h
 Cross reference validation; intended for use in the main program
@@ -251,11 +267,11 @@ described in
 Uses the types
 .Vt struct roff_node
 from
-.Pa roff.h
+.Qq Pa roff.h
 and
 .Vt struct roff_man
 from
-.Pa roff_int.h
+.Qq Pa roff_int.h
 as opaque types for function prototypes.
 .Pp
 When this header is included, the same file should not include
@@ -269,7 +285,7 @@ described in
 Uses the type
 .Vt struct roff_man
 from
-.Pa roff.h
+.Qq Pa roff.h
 as an opaque type for function prototypes.
 .Pp
 When this header is included, the same file should not include
@@ -305,7 +321,7 @@ for function prototypes.
 Uses the type
 .Vt struct roff_man
 from
-.Pa roff.h
+.Qq Pa roff.h
 as an opaque type for function prototypes.
 .It Qq Pa roff_int.h
 Parser internals shared by multiple parsers.
@@ -334,24 +350,24 @@ and the two special functions
 and
 .Fn mdoc_argv_free
 because the latter two are needed by
-.Qq Pa roff.c .
+.Pa roff.c .
 .Pp
 Uses the types
 .Vt struct ohash
 from
-.Pa mandoc_ohash.h ,
+.Qq Pa mandoc_ohash.h ,
 .Vt struct roff_node
 and
 .Vt struct roff_meta
 from
-.Pa roff.h ,
+.Qq Pa roff.h ,
 .Vt struct roff
 from
 .Pa roff.c ,
 and
 .Vt struct mdoc_arg
 from
-.Pa mdoc.h
+.Qq Pa mdoc.h
 as opaque types for function prototypes.
 .It Qq Pa libmdoc.h
 Requires
@@ -372,14 +388,14 @@ parser.
 Uses the types
 .Vt struct roff_node
 from
-.Pa roff.h ,
+.Qq Pa roff.h ,
 .Vt struct roff_man
 from
-.Pa roff_int.h ,
+.Qq Pa roff_int.h ,
 and
 .Vt struct mdoc_arg
 from
-.Pa mdoc.h
+.Qq Pa mdoc.h
 as opaque types for function prototypes.
 .Pp
 When this header is included, the same file should not include
@@ -399,11 +415,11 @@ parser.
 Uses the types
 .Vt struct roff_node
 from
-.Pa roff.h
+.Qq Pa roff.h
 and
 .Vt struct roff_man
 from
-.Pa roff_int.h
+.Qq Pa roff_int.h
 as opaque types for function prototypes.
 .Pp
 When this header is included, the same file should not include
@@ -437,12 +453,12 @@ and
 Uses the type
 .Vt struct eqn_box
 from
-.Pa mandoc.h
+.Qq Pa mandoc.h
 as an opaque type for function prototypes.
 Uses the types
 .Vt struct roff_node
 from
-.Pa roff.h
+.Qq Pa roff.h
 and
 .Vt struct eqn_def
 from
@@ -466,11 +482,11 @@ Provides the functions documented in
 Uses the types
 .Vt struct tbl_span
 from
-.Pa tbl.h
+.Qq Pa tbl.h
 and
 .Vt struct tbl_node
 from
-.Pa tbl_int.h
+.Qq Pa tbl_int.h
 as opaque types for function prototypes.
 .Pp
 When this header is included, the same file should not include
@@ -523,11 +539,11 @@ and
 Uses
 .Vt struct tbl_span
 from
-.Pa mandoc.h
+.Qq Pa mandoc.h
 as an opaque type for function prototypes.
 .Pp
 When this header is included, the same file should not include
-.Pa mansearch.h .
+.Qq Pa mansearch.h .
 .It Qq Pa term.h
 Requires
 .In sys/types.h
@@ -558,27 +574,30 @@ Uses
 and
 .Vt struct eqn_box
 from
-.Pa mandoc.h
+.Qq Pa mandoc.h
 and
 .Vt struct roff_meta
 and
 .Vt struct roff_node
 from
-.Pa roff.h
+.Qq Pa roff.h
 as opaque types for function prototypes.
 .Pp
 When this header is included, the same file should not include
-.Pa html.h
+.Qq Pa html.h
 or
-.Pa mansearch.h .
+.Qq Pa mansearch.h .
 .It Qq Pa html.h
 Requires
 .In sys/types.h
 for
 .Vt size_t ,
-.Pa mandoc.h
+.Qq Pa mandoc.h
 for
 .Vt enum mandoc_esc ,
+.Qq Pa roff.h
+for
+.Vt enum roff_tok ,
 and
 .Qq Pa out.h
 for
@@ -602,22 +621,26 @@ Uses
 and
 .Vt struct eqn_box
 from
-.Pa mandoc.h
+.Qq Pa mandoc.h
 and
 .Vt struct roff_node
 from
-.Pa roff.h
+.Qq Pa roff.h
 as opaque types for function prototypes.
 .Pp
 When this header is included, the same file should not include
-.Pa term.h
+.Qq Pa term.h
 or
-.Pa mansearch.h .
+.Qq Pa mansearch.h .
 .It Qq Pa tag.h
 Requires
 .In sys/types.h
 for
-.Vt size_t .
+.Vt size_t
+and
+.In limits.h
+for
+.Dv INT_MAX .
 .Pp
 Provides an interface to generate
 .Xr ctags 1
@@ -631,7 +654,7 @@ Provides the top level steering function
 Uses the type
 .Vt struct roff_meta
 from
-.Pa roff.h
+.Qq Pa roff.h
 as an opaque type for function prototypes.
 .It Qq Pa manconf.h
 Requires
@@ -671,12 +694,12 @@ and
 Uses
 .Vt struct manpaths
 from
-.Pa manconf.h
+.Qq Pa manconf.h
 as an opaque type for function prototypes.
 .Pp
 When this header is included, the same file should not include
-.Pa out.h ,
-.Pa term.h ,
+.Qq Pa out.h ,
+.Qq Pa term.h ,
 or
-.Pa html.h .
+.Qq Pa html.h .
 .El
--- a/mandoc_msg.c
+++ b/mandoc_msg.c
@@ -1,7 +1,7 @@
 /*	$Id: mandoc_msg.c,v 1.6 2019/03/06 15:55:38 schwarze Exp $ */
 /*
  * Copyright (c) 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2014,2015,2016,2017,2018 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2014-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
@@ -29,8 +29,8 @@ static	const enum mandocerr lowest_type[
 	MANDOCERR_WARNING,
 	MANDOCERR_ERROR,
 	MANDOCERR_UNSUPP,
-	MANDOCERR_MAX,
-	MANDOCERR_MAX
+	MANDOCERR_BADARG,
+	MANDOCERR_SYSERR
 };
 
 static	const char *const level_name[MANDOCLEVEL_MAX] = {
@@ -83,7 +83,7 @@ static	const char *const type_message[MA
 	"missing manual title, using \"\"",
 	"missing manual section, using \"\"",
 	"unknown manual section",
-	"missing date, using today's date",
+	"missing date, using \"\"",
 	"cannot parse date, using it verbatim",
 	"date in the future, using it anyway",
 	"missing Os macro, using \"\"",
@@ -193,7 +193,6 @@ static	const char *const type_message[MA
 	"data block open at end of tbl",
 
 	/* related to document structure and macros */
-	NULL,
 	"duplicate prologue macro",
 	"skipping late title macro",
 	"input stack limit exceeded, infinite loop?",
@@ -224,6 +223,7 @@ static	const char *const type_message[MA
 	"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",
@@ -240,11 +240,40 @@ static	const char *const type_message[MA
 	"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_MAX;
+static	enum mandocerr	 min_type = MANDOCERR_BADARG;
 static	enum mandoclevel rc = MANDOCLEVEL_OK;
 
 
@@ -297,10 +326,10 @@ mandoc_msg(enum mandocerr t, int line, i
 	va_list			 ap;
 	enum mandoclevel	 level;
 
-	if (t < min_type && t != MANDOCERR_FILE)
+	if (t < min_type)
 		return;
 
-	level = MANDOCLEVEL_UNSUPP;
+	level = MANDOCLEVEL_SYSERR;
 	while (t < lowest_type[level])
 		level--;
 	mandoc_msg_setrc(level);
@@ -327,3 +356,12 @@ mandoc_msg(enum mandocerr t, int line, i
 	}
 	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]);
+}
--- a/mandoc_parse.h
+++ b/mandoc_parse.h
@@ -29,6 +29,7 @@
 #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;
--- a/mandocdb.c
+++ b/mandocdb.c
@@ -1,7 +1,7 @@
 /*	$Id: mandocdb.c,v 1.262 2018/12/30 00:49:55 schwarze Exp $ */
 /*
  * Copyright (c) 2011, 2012 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2011-2018 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2011-2020 Ingo Schwarze <schwarze@openbsd.org>
  * Copyright (c) 2016 Ed Maste <emaste@freebsd.org>
  *
  * Permission to use, copy, modify, and distribute this software for any
@@ -179,6 +179,7 @@ static	int		 write_utf8; /* write UTF-8
 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 */
@@ -342,7 +343,7 @@ mandocdb(int argc, char *argv[])
 	 * clobber each other.
 	 */
 #define	CHECKOP(_op, _ch) do \
-	if (OP_DEFAULT != (_op)) { \
+	if ((_op) != OP_DEFAULT) { \
 		warnx("-%c: Conflicting option", (_ch)); \
 		goto usage; \
 	} while (/*CONSTCOND*/0)
@@ -351,7 +352,7 @@ mandocdb(int argc, char *argv[])
 	path_arg = NULL;
 	op = OP_DEFAULT;
 
-	while (-1 != (ch = getopt(argc, argv, "aC:Dd:npQT:tu:v")))
+	while ((ch = getopt(argc, argv, "aC:Dd:npQT:tu:v")) != -1)
 		switch (ch) {
 		case 'a':
 			use_all = 1;
@@ -379,7 +380,7 @@ mandocdb(int argc, char *argv[])
 			mparse_options |= MPARSE_QUICK;
 			break;
 		case 'T':
-			if (strcmp(optarg, "utf8")) {
+			if (strcmp(optarg, "utf8") != 0) {
 				warnx("-T%s: Unsupported output format",
 				    optarg);
 				goto usage;
@@ -416,7 +417,7 @@ mandocdb(int argc, char *argv[])
 	}
 #endif
 
-	if (OP_CONFFILE == op && argc > 0) {
+	if (op == OP_CONFFILE && argc > 0) {
 		warnx("-C: Too many arguments");
 		goto usage;
 	}
@@ -427,13 +428,13 @@ mandocdb(int argc, char *argv[])
 	mandoc_ohash_init(&mpages, 6, offsetof(struct mpage, inodev));
 	mandoc_ohash_init(&mlinks, 6, offsetof(struct mlink, file));
 
-	if (OP_UPDATE == op || OP_DELETE == op || OP_TEST == op) {
+	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_TEST != op && 0 == set_basedir(path_arg, 1))
+		if (op != OP_TEST && set_basedir(path_arg, 1) == 0)
 			goto out;
 
 		dba = nodb ? dba_new(128) : dba_read(MANDOC_DB);
@@ -454,11 +455,11 @@ mandocdb(int argc, char *argv[])
 				    " from scratch", strerror(errno));
 			exitcode = (int)MANDOCLEVEL_OK;
 			op = OP_DEFAULT;
-			if (0 == treescan())
+			if (treescan() == 0)
 				goto out;
 			dba = dba_new(128);
 		}
-		if (OP_DELETE != op)
+		if (op != OP_DELETE)
 			mpages_merge(dba, mp);
 		if (nodb == 0)
 			dbwrite(dba);
@@ -492,7 +493,7 @@ mandocdb(int argc, char *argv[])
 			sz = strlen(conf.manpath.paths[j]);
 			if (sz && conf.manpath.paths[j][sz - 1] == '/')
 				conf.manpath.paths[j][--sz] = '\0';
-			if (0 == sz)
+			if (sz == 0)
 				continue;
 
 			if (j) {
@@ -502,9 +503,9 @@ mandocdb(int argc, char *argv[])
 				    offsetof(struct mlink, file));
 			}
 
-			if ( ! set_basedir(conf.manpath.paths[j], argc > 0))
+			if (set_basedir(conf.manpath.paths[j], argc > 0) == 0)
 				continue;
-			if (0 == treescan())
+			if (treescan() == 0)
 				continue;
 			dba = dba_new(128);
 			mpages_merge(dba, mp);
@@ -608,9 +609,9 @@ treescan(void)
 					say(path, "&realpath");
 				continue;
 			}
-			if (strstr(buf, basedir) != buf
+			if (strncmp(buf, basedir, basedir_len) != 0
 #ifdef HOMEBREWDIR
-			    && strstr(buf, HOMEBREWDIR) != buf
+			    && strncmp(buf, HOMEBREWDIR, strlen(HOMEBREWDIR))
 #endif
 			) {
 				if (warnings) say("",
@@ -777,17 +778,17 @@ treescan(void)
  * See treescan() for the fts(3) version of this.
  */
 static void
-filescan(const char *file)
+filescan(const char *infile)
 {
-	char		 buf[PATH_MAX];
 	struct stat	 st;
 	struct mlink	*mlink;
-	char		*p, *start;
+	char		*linkfile, *p, *realdir, *start, *usefile;
+	size_t		 realdir_len;
 
 	assert(use_all);
 
-	if (0 == strncmp(file, "./", 2))
-		file += 2;
+	if (strncmp(infile, "./", 2) == 0)
+		infile += 2;
 
 	/*
 	 * We have to do lstat(2) before realpath(3) loses
@@ -796,13 +797,13 @@ filescan(const char *file)
 	 * we want to use the orginal file name, while for
 	 * regular files, we want to use the real path.
 	 */
-	if (-1 == lstat(file, &st)) {
+	if (lstat(infile, &st) == -1) {
 		exitcode = (int)MANDOCLEVEL_BADARG;
-		say(file, "&lstat");
+		say(infile, "&lstat");
 		return;
-	} else if (0 == ((S_IFREG | S_IFLNK) & st.st_mode)) {
+	} else if (S_ISREG(st.st_mode) == 0 && S_ISLNK(st.st_mode) == 0) {
 		exitcode = (int)MANDOCLEVEL_BADARG;
-		say(file, "Not a regular file");
+		say(infile, "Not a regular file");
 		return;
 	}
 
@@ -810,23 +811,24 @@ filescan(const char *file)
 	 * We have to resolve the file name to the real path
 	 * in any case for the base directory check.
 	 */
-	if (NULL == realpath(file, buf)) {
+	if ((usefile = realpath(infile, NULL)) == NULL) {
 		exitcode = (int)MANDOCLEVEL_BADARG;
-		say(file, "&realpath");
+		say(infile, "&realpath");
 		return;
 	}
 
-	if (OP_TEST == op)
-		start = buf;
-	else if (strstr(buf, basedir) == buf)
-		start = buf + strlen(basedir);
+	if (op == OP_TEST)
+		start = usefile;
+	else if (strncmp(usefile, basedir, basedir_len) == 0)
+		start = usefile + basedir_len;
 #ifdef HOMEBREWDIR
-	else if (strstr(buf, HOMEBREWDIR) == buf)
-		start = buf;
+	else if (strncmp(usefile, HOMEBREWDIR, strlen(HOMEBREWDIR)) == 0)
+		start = usefile;
 #endif
 	else {
 		exitcode = (int)MANDOCLEVEL_BADARG;
-		say("", "%s: outside base directory", buf);
+		say("", "%s: outside base directory", infile);
+		free(usefile);
 		return;
 	}
 
@@ -834,25 +836,72 @@ filescan(const char *file)
 	 * 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.
-	 * This implies passing stuff like "cat1/../man1/foo.1"
-	 * on the command line won't work.  So don't do that.
-	 * Note the stat(2) can still fail if the link target
-	 * doesn't exist.
 	 */
-	if (S_IFLNK & st.st_mode) {
-		if (-1 == stat(buf, &st)) {
+	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(file, "&stat");
+			say(infile, "&stat");
+			free(usefile);
 			return;
 		}
-		if (strlcpy(buf, file, sizeof(buf)) >= sizeof(buf)) {
-			say(file, "Filename too long");
-			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;
 		}
-		start = buf;
-		if (OP_TEST != op && strstr(buf, basedir) == buf)
-			start += strlen(basedir);
-	}
+
+		/*
+		 * 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;
@@ -860,6 +909,7 @@ filescan(const char *file)
 	    sizeof(mlink->file)) {
 		say(start, "Filename too long");
 		free(mlink);
+		free(usefile);
 		return;
 	}
 
@@ -868,13 +918,13 @@ filescan(const char *file)
 	 * but outside our tree, guess the base directory.
 	 */
 
-	if (op == OP_TEST || (start == buf && *start == '/')) {
-		if (strncmp(buf, "man/", 4) == 0)
-			start = buf + 4;
-		else if ((start = strstr(buf, "/man/")) != NULL)
+	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 = buf;
+			start = usefile;
 	}
 
 	/*
@@ -883,18 +933,18 @@ filescan(const char *file)
 	 * If we find one of these and what's underneath is a directory,
 	 * assume it's an architecture.
 	 */
-	if (NULL != (p = strchr(start, '/'))) {
+	if ((p = strchr(start, '/')) != NULL) {
 		*p++ = '\0';
-		if (0 == strncmp(start, "man", 3)) {
+		if (strncmp(start, "man", 3) == 0) {
 			mlink->dform = FORM_SRC;
 			mlink->dsec = start + 3;
-		} else if (0 == strncmp(start, "cat", 3)) {
+		} else if (strncmp(start, "cat", 3) == 0) {
 			mlink->dform = FORM_CAT;
 			mlink->dsec = start + 3;
 		}
 
 		start = p;
-		if (NULL != mlink->dsec && NULL != (p = strchr(start, '/'))) {
+		if (mlink->dsec != NULL && (p = strchr(start, '/')) != NULL) {
 			*p++ = '\0';
 			mlink->arch = start;
 			start = p;
@@ -906,10 +956,10 @@ filescan(const char *file)
 	 * Suffix of `.0' indicates a catpage, `.1-9' is a manpage.
 	 */
 	p = strrchr(start, '\0');
-	while (p-- > start && '/' != *p && '.' != *p)
-		/* Loop. */ ;
+	while (p-- > start && *p != '/' && *p != '.')
+		continue;
 
-	if ('.' == *p) {
+	if (*p == '.') {
 		*p++ = '\0';
 		mlink->fsec = p;
 	}
@@ -919,11 +969,12 @@ filescan(const char *file)
 	 * Use the filename portion of the path.
 	 */
 	mlink->name = start;
-	if (NULL != (p = strrchr(start, '/'))) {
+	if ((p = strrchr(start, '/')) != NULL) {
 		mlink->name = p + 1;
 		*p = '\0';
 	}
 	mlink_add(mlink, &st);
+	free(usefile);
 }
 
 static void
@@ -1186,9 +1237,11 @@ mpages_merge(struct dba *dba, struct mpa
 				mlink->next = mlink_dest->next;
 				mlink_dest->next = mpage->mlinks;
 				mpage->mlinks = NULL;
+				goto nextpage;
 			}
-			goto nextpage;
-		} else if (meta != NULL && meta->macroset == MACROSET_MDOC) {
+			meta->macroset = MACROSET_NONE;
+		}
+		if (meta != NULL && meta->macroset == MACROSET_MDOC) {
 			mpage->form = FORM_SRC;
 			mpage->sec = meta->msec;
 			mpage->sec = mandoc_strdup(
@@ -1208,12 +1261,15 @@ mpages_merge(struct dba *dba, struct mpa
 		}
 
 		assert(mpage->desc == NULL);
-		if (meta == NULL) {
-			mpage->form = FORM_CAT;
+		if (meta == NULL || meta->sodest != NULL) {
 			mpage->sec = mandoc_strdup(mlink->dsec);
 			mpage->arch = mandoc_strdup(mlink->arch);
 			mpage->title = mandoc_strdup(mlink->name);
-			parse_cat(mpage, fd);
+			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
@@ -2245,7 +2301,6 @@ set_basedir(const char *targetdir, int r
 	static char	 startdir[PATH_MAX];
 	static int	 getcwd_status;  /* 1 = ok, 2 = failure */
 	static int	 chdir_status;  /* 1 = changed directory */
-	char		*cp;
 
 	/*
 	 * Remember the original working directory, if possible.
@@ -2254,8 +2309,8 @@ set_basedir(const char *targetdir, int r
 	 * Do not error out if the current directory is not
 	 * searchable: Maybe it won't be needed after all.
 	 */
-	if (0 == getcwd_status) {
-		if (NULL == getcwd(startdir, sizeof(startdir))) {
+	if (getcwd_status == 0) {
+		if (getcwd(startdir, sizeof(startdir)) == NULL) {
 			getcwd_status = 2;
 			(void)strlcpy(startdir, strerror(errno),
 			    sizeof(startdir));
@@ -2268,19 +2323,20 @@ set_basedir(const char *targetdir, int r
 	 * 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 (2 == getcwd_status) {
+	if (chdir_status && *targetdir != '/') {
+		if (getcwd_status == 2) {
 			exitcode = (int)MANDOCLEVEL_SYSERR;
 			say("", "getcwd: %s", startdir);
 			return 0;
 		}
-		if (-1 == chdir(startdir)) {
+		if (chdir(startdir) == -1) {
 			exitcode = (int)MANDOCLEVEL_SYSERR;
 			say("", "&chdir %s", startdir);
 			return 0;
@@ -2292,29 +2348,33 @@ set_basedir(const char *targetdir, int r
 	 * pathname and append a trailing slash, such that
 	 * we can reliably check whether files are inside.
 	 */
-	if (NULL == realpath(targetdir, basedir)) {
+	if (realpath(targetdir, basedir) == NULL) {
 		if (report_baddir || errno != ENOENT) {
 			exitcode = (int)MANDOCLEVEL_BADARG;
 			say("", "&%s: realpath", targetdir);
 		}
+		*basedir = '\0';
 		return 0;
-	} else if (-1 == chdir(basedir)) {
+	} else if (chdir(basedir) == -1) {
 		if (report_baddir || errno != ENOENT) {
 			exitcode = (int)MANDOCLEVEL_BADARG;
 			say("", "&chdir");
 		}
+		*basedir = '\0';
 		return 0;
 	}
 	chdir_status = 1;
-	cp = strchr(basedir, '\0');
-	if ('/' != cp[-1]) {
-		if (cp - basedir >= PATH_MAX - 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;
 		}
-		*cp++ = '/';
-		*cp = '\0';
+		basedir[basedir_len++] = '/';
+		basedir[basedir_len] = '\0';
 	}
 	return 1;
 }
@@ -2325,15 +2385,15 @@ say(const char *file, const char *format
 	va_list		 ap;
 	int		 use_errno;
 
-	if ('\0' != *basedir)
+	if (*basedir != '\0')
 		fprintf(stderr, "%s", basedir);
-	if ('\0' != *basedir && '\0' != *file)
+	if (*basedir != '\0' && *file != '\0')
 		fputc('/', stderr);
-	if ('\0' != *file)
+	if (*file != '\0')
 		fprintf(stderr, "%s", file);
 
 	use_errno = 1;
-	if (NULL != format) {
+	if (format != NULL) {
 		switch (*format) {
 		case '&':
 			format++;
@@ -2346,15 +2406,15 @@ say(const char *file, const char *format
 			break;
 		}
 	}
-	if (NULL != format) {
-		if ('\0' != *basedir || '\0' != *file)
+	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 ('\0' != *basedir || '\0' != *file || NULL != format)
+		if (*basedir != '\0' || *file != '\0' || format != NULL)
 			fputs(": ", stderr);
 		perror(NULL);
 	} else
--- a/manpath.c
+++ b/manpath.c
@@ -1,6 +1,6 @@
 /*	$Id: manpath.c,v 1.37 2018/11/22 11:30:23 schwarze Exp $ */
 /*
- * Copyright (c) 2011,2014,2015,2017,2018 Ingo Schwarze <schwarze@openbsd.org>
+ * 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
@@ -21,20 +21,19 @@
 #include <sys/stat.h>
 
 #include <ctype.h>
-#if HAVE_ERR
-#include <err.h>
-#endif
+#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"
 
 static	void	 manconf_file(struct manconf *, const char *);
-static	void	 manpath_add(struct manpaths *, const char *, int);
-static	void	 manpath_parseline(struct manpaths *, char *, int);
+static	void	 manpath_add(struct manpaths *, const char *, char);
+static	void	 manpath_parseline(struct manpaths *, char *, char);
 
 
 void
@@ -44,11 +43,11 @@ manconf_parse(struct manconf *conf, cons
 	char		*insert;
 
 	/* Always prepend -m. */
-	manpath_parseline(&conf->manpath, auxp, 1);
+	manpath_parseline(&conf->manpath, auxp, 'm');
 
 	/* If -M is given, it overrides everything else. */
 	if (NULL != defp) {
-		manpath_parseline(&conf->manpath, defp, 1);
+		manpath_parseline(&conf->manpath, defp, 'M');
 		return;
 	}
 
@@ -66,13 +65,13 @@ manconf_parse(struct manconf *conf, cons
 	/* Prepend man.conf(5) to MANPATH. */
 	if (':' == defp[0]) {
 		manconf_file(conf, file);
-		manpath_parseline(&conf->manpath, defp, 0);
+		manpath_parseline(&conf->manpath, defp, '\0');
 		return;
 	}
 
 	/* Append man.conf(5) to MANPATH. */
 	if (':' == defp[strlen(defp) - 1]) {
-		manpath_parseline(&conf->manpath, defp, 0);
+		manpath_parseline(&conf->manpath, defp, '\0');
 		manconf_file(conf, file);
 		return;
 	}
@@ -81,28 +80,28 @@ manconf_parse(struct manconf *conf, cons
 	insert = strstr(defp, "::");
 	if (NULL != insert) {
 		*insert++ = '\0';
-		manpath_parseline(&conf->manpath, defp, 0);
+		manpath_parseline(&conf->manpath, defp, '\0');
 		manconf_file(conf, file);
-		manpath_parseline(&conf->manpath, insert + 1, 0);
+		manpath_parseline(&conf->manpath, insert + 1, '\0');
 		return;
 	}
 
 	/* MANPATH overrides man.conf(5) completely. */
-	manpath_parseline(&conf->manpath, defp, 0);
+	manpath_parseline(&conf->manpath, defp, '\0');
 }
 
 void
 manpath_base(struct manpaths *dirs)
 {
 	char path_base[] = MANPATH_BASE;
-	manpath_parseline(dirs, path_base, 0);
+	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, int complain)
+manpath_parseline(struct manpaths *dirs, char *path, char option)
 {
 	char	*dir;
 
@@ -110,7 +109,7 @@ manpath_parseline(struct manpaths *dirs,
 		return;
 
 	for (dir = strtok(path, ":"); dir; dir = strtok(NULL, ":"))
-		manpath_add(dirs, dir, complain);
+		manpath_add(dirs, dir, option);
 }
 
 /*
@@ -118,33 +117,32 @@ manpath_parseline(struct manpaths *dirs,
  * Grow the array one-by-one for simplicity's sake.
  */
 static void
-manpath_add(struct manpaths *dirs, const char *dir, int complain)
+manpath_add(struct manpaths *dirs, const char *dir, char option)
 {
 	char		 buf[PATH_MAX];
 	struct stat	 sb;
 	char		*cp;
 	size_t		 i;
 
-	if (NULL == (cp = realpath(dir, buf))) {
-		if (complain)
-			warn("manpath: %s", dir);
-		return;
-	}
+	if ((cp = realpath(dir, buf)) == NULL)
+		goto fail;
 
 	for (i = 0; i < dirs->sz; i++)
-		if (0 == strcmp(dirs->paths[i], dir))
+		if (strcmp(dirs->paths[i], dir) == 0)
 			return;
 
-	if (stat(cp, &sb) == -1) {
-		if (complain)
-			warn("manpath: %s", dir);
-		return;
-	}
+	if (stat(cp, &sb) == -1)
+		goto fail;
 
 	dirs->paths = mandoc_reallocarray(dirs->paths,
-	    dirs->sz + 1, sizeof(char *));
-
+	    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
@@ -165,7 +163,7 @@ manconf_free(struct manconf *conf)
 static void
 manconf_file(struct manconf *conf, const char *file)
 {
-	const char *const toks[] = { "manpath", "output", "_whatdb" };
+	const char *const toks[] = { "manpath", "output" };
 	char manpath_default[] = MANPATH_DEFAULT;
 
 	FILE		*stream;
@@ -202,15 +200,8 @@ manconf_file(struct manconf *conf, const
 		}
 
 		switch (tok) {
-		case 2:  /* _whatdb */
-			while (ep > cp && ep[-1] != '/')
-				ep--;
-			if (ep == cp)
-				continue;
-			*ep = '\0';
-			/* FALLTHROUGH */
 		case 0:  /* manpath */
-			manpath_add(&conf->manpath, cp, 0);
+			manpath_add(&conf->manpath, cp, '\0');
 			*manpath_default = '\0';
 			break;
 		case 1:  /* output */
@@ -225,7 +216,7 @@ manconf_file(struct manconf *conf, const
 
 out:
 	if (*manpath_default != '\0')
-		manpath_parseline(&conf->manpath, manpath_default, 0);
+		manpath_parseline(&conf->manpath, manpath_default, '\0');
 }
 
 int
@@ -235,14 +226,15 @@ manconf_output(struct manoutput *conf, c
 	    "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 < sizeof(toks)/sizeof(toks[0]); tok++) {
+	for (tok = 0; tok < ntoks; tok++) {
 		len = strlen(toks[tok]);
-		if ( ! strncmp(cp, toks[tok], len) &&
+		if (strncmp(cp, toks[tok], len) == 0 &&
 		    strchr(" =	", cp[len]) != NULL) {
 			cp += len;
 			if (*cp == '=')
@@ -254,11 +246,11 @@ manconf_output(struct manoutput *conf, c
 	}
 
 	if (tok < 6 && *cp == '\0') {
-		warnx("-O %s=?: Missing argument value", toks[tok]);
+		mandoc_msg(MANDOCERR_BADVAL_MISS, 0, 0, "-O %s=?", toks[tok]);
 		return -1;
 	}
-	if (tok > 6 && *cp != '\0') {
-		warnx("-O %s: Does not take a value: %s", toks[tok], cp);
+	if (tok > 6 && tok < ntoks && *cp != '\0') {
+		mandoc_msg(MANDOCERR_BADVAL, 0, 0, "-O %s=%s", toks[tok], cp);
 		return -1;
 	}
 
@@ -299,7 +291,8 @@ manconf_output(struct manoutput *conf, c
 		conf->indent = strtonum(cp, 0, 1000, &errstr);
 		if (errstr == NULL)
 			return 0;
-		warnx("-O indent=%s is %s", cp, errstr);
+		mandoc_msg(MANDOCERR_BADVAL_BAD, 0, 0,
+		    "-O indent=%s is %s", cp, errstr);
 		return -1;
 	case 5:
 		if (conf->width) {
@@ -309,7 +302,8 @@ manconf_output(struct manoutput *conf, c
 		conf->width = strtonum(cp, 1, 1000, &errstr);
 		if (errstr == NULL)
 			return 0;
-		warnx("-O width=%s is %s", cp, errstr);
+		mandoc_msg(MANDOCERR_BADVAL_BAD, 0, 0,
+		    "-O width=%s is %s", cp, errstr);
 		return -1;
 	case 6:
 		if (conf->tag != NULL) {
@@ -331,13 +325,16 @@ manconf_output(struct manoutput *conf, c
 		conf->toc = 1;
 		return 0;
 	default:
-		if (fromfile)
-			warnx("-O %s: Bad argument", cp);
+		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;
 	}
-	if (fromfile == 0)
-		warnx("-O %s=%s: Option already set to %s",
-		    toks[tok], cp, oldval);
-	free(oldval);
-	return -1;
 }
--- a/mansearch.c
+++ b/mansearch.c
@@ -191,7 +191,7 @@ mansearch(const struct mansearch *search
 			    mpage->file, R_OK) == -1) {
 				warn("%s", mpage->file);
 				warnx("outdated mandoc.db contains "
-				    "bogus %s entry, run makewhatis %s", 
+				    "bogus %s entry, run makewhatis %s",
 				    page->file + 1, paths->paths[i]);
 				free(mpage->file);
 				free(rp);
@@ -199,6 +199,7 @@ mansearch(const struct mansearch *search
 			}
 			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)
@@ -294,8 +295,10 @@ manmerge_term(struct expr *e, struct oha
 				break;
 			slot = ohash_lookup_memory(htab,
 			    (char *)&res, sizeof(res.page), res.page);
-			if ((rp = ohash_find(htab, slot)) != NULL)
+			if ((rp = ohash_find(htab, slot)) != NULL) {
+				rp->bits |= res.bits;
 				continue;
+			}
 			rp = mandoc_malloc(sizeof(*rp));
 			*rp = res;
 			ohash_insert(htab, slot, rp);
@@ -408,7 +411,8 @@ manpage_compare(const void *vp1, const v
 
 	mp1 = vp1;
 	mp2 = vp2;
-	if ((diff = mp1->sec - mp2->sec))
+	if ((diff = mp2->bits - mp1->bits) ||
+	    (diff = mp1->sec - mp2->sec))
 		return diff;
 
 	/* Fall back to alphabetic ordering of names. */
--- a/mansearch.h
+++ b/mansearch.h
@@ -92,6 +92,7 @@ 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;
--- a/mdoc.7
+++ b/mdoc.7
@@ -1,7 +1,7 @@
 .\"	$Id: mdoc.7,v 1.276 2019/02/07 15:45:53 schwarze Exp $
 .\"
 .\" Copyright (c) 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
-.\" Copyright (c) 2010, 2011, 2013-2018 Ingo Schwarze <schwarze@openbsd.org>
+.\" Copyright (c) 2010, 2011, 2013-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
@@ -449,6 +449,7 @@ in the alphabetical
 .It Ic \&Ss Ta subsection header (one line)
 .It Ic \&Sx Ta internal cross reference to a section or subsection
 .It Ic \&Xr Ta cross reference to another manual page: Ar name section
+.It Ic \&Tg Ta tag the definition of a Ar term Pq <= 1 arguments
 .It Ic \&Pp Ta start a text paragraph (no arguments)
 .El
 .Ss Displays and lists
@@ -596,6 +597,13 @@ block.
 Book or journal page number of an
 .Ic \&Rs
 block.
+Conventionally, the argument starts with
+.Ql p.\&
+for a single page or
+.Ql pp.\&
+for a range of pages, for example:
+.Pp
+.Dl .%P pp. 42\e(en47
 .It Ic \&%Q Ar name
 Institutional author (school, government, etc.) of an
 .Ic \&Rs
@@ -1156,17 +1164,19 @@ declarations.
 This practise is discouraged.
 .It Ic \&Cm Ar keyword ...
 Command modifiers.
-Typically used for fixed strings passed as arguments, unless
+Typically used for fixed strings passed as arguments to interactive
+commands, to commands in interpreted scripts, or to configuration
+file directives, unless
 .Ic \&Fl
 is more appropriate.
-Also useful when specifying configuration options or keys.
 .Pp
 Examples:
 .Dl ".Nm mt Fl f Ar device Cm rewind"
 .Dl ".Nm ps Fl o Cm pid , Ns Cm command"
 .Dl ".Nm dd Cm if= Ns Ar file1 Cm of= Ns Ar file2"
-.Dl ".Cm IdentityFile Pa ~/.ssh/id_rsa"
-.Dl ".Cm LogLevel Dv DEBUG"
+.Dl ".Ic set Fl o Cm vi"
+.Dl ".Ic lookup Cm file bind"
+.Dl ".Ic permit Ar identity Op Cm as Ar target"
 .It Ic \&D1 Ar line
 One-line indented display.
 This is formatted by the default rules and is useful for simple indented
@@ -1677,10 +1687,10 @@ This macro is not implemented in
 .Xr mandoc 1 .
 It was used to include the contents of a (header) file literally.
 .It Ic \&Ic Ar keyword ...
-Designate an internal or interactive command.
-This is similar to
-.Ic \&Cm
-but used for instructions rather than values.
+Internal or interactive command, or configuration instruction
+in a configuration file.
+See also
+.Ic \&Cm .
 .Pp
 Examples:
 .Dl \&.Ic :wq
@@ -2539,6 +2549,49 @@ Table cell separator in
 .Ic \&Bl Fl column
 lists; can only be used below
 .Ic \&It .
+.It Ic \&Tg Op Ar term
+Announce that the next input line starts a definition of the
+.Ar term .
+This macro must appear alone on its own input line.
+The argument defaults to the first argument of the first macro
+on the next line.
+The argument may not contain whitespace characters, not even when it is quoted.
+This macro is a
+.Xr mandoc 1
+extension and is typically ignored by other formatters.
+.Pp
+When viewing terminal output with
+.Xr less 1 ,
+the interactive
+.Ic :t
+command can be used to go to the definition of the
+.Ar term
+as described for the
+.Ev MANPAGER
+variable in
+.Xr man 1 ;
+when producing HTML output, a fragment identifier
+.Pq Ic id No attribute
+is generated, to be used for deep linking to this place of the document.
+.Pp
+In most cases, adding a
+.Ic \&Tg
+macro would be redundant because
+.Xr mandoc 1
+is able to automatically tag most definitions.
+This macro is intended for cases where automatic tagging of a
+.Ar term
+is unsatisfactory, for example if a definition is not tagged
+automatically (false negative) or if places are tagged that do
+not define the
+.Ar term
+(false positives).
+When there is at least one
+.Ic \&Tg
+macro for a
+.Ar term ,
+no other places are automatically marked as definitions of that
+.Ar term .
 .It Ic \&Tn Ar word ...
 Supported only for compatibility, do not use this in new manuals.
 Even though the macro name
@@ -2903,6 +2956,7 @@ then the macro accepts an arbitrary numb
 .It Ic \&St  Ta    \&No     Ta    Yes      Ta    1
 .It Ic \&Sx  Ta    Yes      Ta    Yes      Ta    >0
 .It Ic \&Sy  Ta    Yes      Ta    Yes      Ta    >0
+.It Ic \&Tg  Ta    \&No     Ta    \&No     Ta    <2
 .It Ic \&Tn  Ta    Yes      Ta    Yes      Ta    >0
 .It Ic \&Ud  Ta    \&No     Ta    \&No     Ta    0
 .It Ic \&Ux  Ta    Yes      Ta    Yes      Ta    n
@@ -2969,7 +3023,7 @@ exclamation mark
 Note that even a period preceded by a backslash
 .Pq Sq \e.\&
 gets this special handling; use
-.Sq \e&.
+.Sq \e&.\&
 to prevent that.
 .Pp
 Many in-line macros interrupt their scope when they encounter
@@ -2996,6 +3050,13 @@ in the same way as a plain
 .Sq \&|
 character.
 Using this predefined string is not recommended in new manuals.
+.Pp
+Appending a zero-width space
+.Pq Sq \e&
+to the end of an input line is also useful to prevent the interpretation
+of a trailing period, exclamation or question mark as the end of a
+sentence, for example when an abbreviation happens to occur
+at the end of a text or macro input line.
 .Ss Font handling
 In
 .Nm
--- a/mdoc_argv.c
+++ b/mdoc_argv.c
@@ -1,7 +1,7 @@
 /*	$Id: mdoc_argv.c,v 1.119 2018/12/21 17:15:19 schwarze Exp $ */
 /*
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2012, 2014-2018 Ingo Schwarze <schwarze@openbsd.org>
+ * 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
@@ -454,6 +454,7 @@ args(struct roff_man *mdoc, int line, in
 			mandoc_msg(MANDOCERR_ARG_QUOTE, line, *pos, NULL);
 			mdoc->flags &= ~MDOC_PHRASELIT;
 		}
+		mdoc->flags &= ~MDOC_PHRASEQL;
 		return ARGS_EOLN;
 	}
 
--- a/mdoc_html.c
+++ b/mdoc_html.c
@@ -1,7 +1,7 @@
 /*	$Id: mdoc_html.c,v 1.328 2019/03/01 10:57:18 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2014-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2014-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
@@ -115,6 +115,7 @@ 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);
@@ -241,6 +242,7 @@ static const struct mdoc_html_act mdoc_h
 	{mdoc__x_pre, mdoc__x_post}, /* %Q */
 	{mdoc__x_pre, mdoc__x_post}, /* %U */
 	{NULL, NULL}, /* Ta */
+	{mdoc_tg_pre, NULL}, /* Tg */
 };
 
 
@@ -351,26 +353,34 @@ print_mdoc_node(MDOC_ARGS)
 	if (n->type == ROFFT_COMMENT || n->flags & NODE_NOPRT)
 		return;
 
-	html_fillmode(h, n->flags & NODE_NOFILL ? ROFF_nf : ROFF_fi);
+	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++;
-
-		/* No tables in this mode... */
-		assert(NULL == h->tblt);
-
-		/*
-		 * Make sure that if we're in a literal mode already
-		 * (i.e., within a <PRE>) don't print the newline.
-		 */
-		if (*n->string == ' ' && n->flags & NODE_LINE &&
-		    (h->flags & HTML_NONEWLINE) == 0 &&
-		    (n->flags & NODE_NOFILL) == 0)
-			print_otag(h, TAG_BR, "");
 		if (NODE_DELIMC & n->flags)
 			h->flags |= HTML_NOSPACE;
 		print_text(h, n->string);
@@ -439,12 +449,6 @@ print_mdoc_node(MDOC_ARGS)
 			n->body->flags |= NODE_ENDED;
 		break;
 	}
-
-	if (n->flags & NODE_NOFILL &&
-	    (n->next == NULL || n->next->flags & NODE_LINE)) {
-		h->col++;
-		print_endline(h);
-	}
 }
 
 static void
@@ -653,7 +657,6 @@ mdoc_nd_pre(MDOC_ARGS)
 {
 	switch (n->type) {
 	case ROFFT_BLOCK:
-		html_close_paragraph(h);
 		return 1;
 	case ROFFT_HEAD:
 		return 0;
@@ -663,8 +666,7 @@ mdoc_nd_pre(MDOC_ARGS)
 		abort();
 	}
 	print_text(h, "\\(em");
-	/* Cannot use TAG_SPAN because it may contain blocks. */
-	print_otag(h, TAG_DIV, "c", "Nd");
+	print_otag(h, TAG_SPAN, "c", "Nd");
 	return 1;
 }
 
@@ -722,6 +724,16 @@ mdoc_xr_pre(MDOC_ARGS)
 }
 
 static int
+mdoc_tg_pre(MDOC_ARGS)
+{
+	char	*id;
+
+	if ((id = html_make_id(n, 1)) != NULL)
+		print_otag(h, TAG_MARK, "i", id);
+	return 0;
+}
+
+static int
 mdoc_ns_pre(MDOC_ARGS)
 {
 
@@ -1272,7 +1284,11 @@ mdoc_skip_pre(MDOC_ARGS)
 static int
 mdoc_pp_pre(MDOC_ARGS)
 {
-	if ((n->flags & NODE_NOFILL) == 0) {
+	if (n->flags & NODE_NOFILL) {
+		print_endline(h);
+		h->col = 1;
+		print_endline(h);
+	} else {
 		html_close_paragraph(h);
 		print_otag(h, TAG_P, "c", "Pp");
 	}
@@ -1700,7 +1716,7 @@ mdoc_quote_pre(MDOC_ARGS)
 		/*
 		 * Give up on semantic markup for now.
 		 * We cannot use TAG_SPAN because .Oo may contain blocks.
-		 * We cannot use TAG_IDIV because we might be in a
+		 * 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.
@@ -1715,9 +1731,11 @@ mdoc_quote_pre(MDOC_ARGS)
 		break;
 	case MDOC_Do:
 	case MDOC_Dq:
+		print_text(h, "\\(lq");
+		break;
 	case MDOC_Qo:
 	case MDOC_Qq:
-		print_text(h, "\\(lq");
+		print_text(h, "\"");
 		break;
 	case MDOC_Po:
 	case MDOC_Pq:
@@ -1773,12 +1791,14 @@ mdoc_quote_post(MDOC_ARGS)
 		else
 			print_text(h, n->norm->Es->child->next->string);
 		break;
-	case MDOC_Qo:
-	case MDOC_Qq:
 	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, ")");
--- a/mdoc_macro.c
+++ b/mdoc_macro.c
@@ -1,7 +1,7 @@
 /*	$Id: mdoc_macro.c,v 1.232 2019/01/07 07:26:29 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2010, 2012-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * 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
@@ -61,7 +61,7 @@ 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 }, /* 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 */
@@ -200,6 +200,7 @@ static const struct mdoc_macro mdoc_macr
 	{ 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 */
 };
 
 
--- a/mdoc_man.c
+++ b/mdoc_man.c
@@ -262,6 +262,7 @@ static const struct mdoc_man_act mdoc_ma
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %Q */
 	{ NULL, NULL, post_percent, NULL, NULL }, /* %U */
 	{ NULL, NULL, NULL, NULL, NULL }, /* Ta */
+	{ NULL, NULL, NULL, NULL, NULL }, /* Tg */
 };
 static const struct mdoc_man_act *mdoc_man_act(enum roff_tok);
 
--- a/mdoc_markdown.c
+++ b/mdoc_markdown.c
@@ -226,6 +226,7 @@ static	const struct md_act md_acts[MDOC_
 	{ 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, NULL, NULL, NULL, NULL }, /* Tg */
 };
 static const struct md_act *md_act(enum roff_tok);
 
@@ -1290,7 +1291,7 @@ md_post_It(struct roff_node *n)
 		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.
--- a/mdoc_state.c
+++ b/mdoc_state.c
@@ -157,6 +157,7 @@ static	const state_handler state_handler
 	NULL,		/* %Q */
 	NULL,		/* %U */
 	NULL,		/* Ta */
+	NULL,		/* Tg */
 };
 
 
--- a/mdoc_term.c
+++ b/mdoc_term.c
@@ -1,7 +1,7 @@
 /*	$Id: mdoc_term.c,v 1.372 2019/01/04 03:39:01 schwarze Exp $ */
 /*
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2010, 2012-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2010, 2012-2020 Ingo Schwarze <schwarze@openbsd.org>
  * Copyright (c) 2013 Franco Fichtner <franco@lastsummer.de>
  *
  * Permission to use, copy, modify, and distribute this software for any
@@ -119,6 +119,7 @@ static	int	  termp_pp_pre(DECL_ARGS);
 static	int	  termp_ss_pre(DECL_ARGS);
 static	int	  termp_sy_pre(DECL_ARGS);
 static	int	  termp_tag_pre(DECL_ARGS);
+static	int	  termp_tg_pre(DECL_ARGS);
 static	int	  termp_under_pre(DECL_ARGS);
 static	int	  termp_vt_pre(DECL_ARGS);
 static	int	  termp_xr_pre(DECL_ARGS);
@@ -245,15 +246,16 @@ static const struct mdoc_term_act mdoc_t
 	{ NULL, termp____post }, /* %Q */
 	{ NULL, termp____post }, /* %U */
 	{ NULL, NULL }, /* Ta */
+	{ termp_tg_pre, NULL }, /* Tg */
 };
 
-static	int	 fn_prio;
+static	int	 fn_prio = TAG_STRONG;
 
 
 void
 terminal_mdoc(void *arg, const struct roff_meta *mdoc)
 {
-	struct roff_node	*n;
+	struct roff_node	*n, *nn;
 	struct termp		*p;
 	size_t			 save_defindent;
 
@@ -265,16 +267,20 @@ terminal_mdoc(void *arg, const struct ro
 
 	n = mdoc->first->child;
 	if (p->synopsisonly) {
-		while (n != NULL) {
-			if (n->tok == MDOC_Sh && n->sec == SEC_SYNOPSIS) {
-				if (n->child->next->child != NULL)
-					print_mdoc_nodelist(p, NULL,
-					    mdoc, n->child->next->child);
-				term_newln(p);
+		for (nn = NULL; n != NULL; n = n->next) {
+			if (n->tok != MDOC_Sh)
+				continue;
+			if (n->sec == SEC_SYNOPSIS)
 				break;
-			}
-			n = n->next;
+			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)
@@ -352,6 +358,7 @@ print_mdoc_node(DECL_ARGS)
 	 * produce output.  Note that some pre-handlers do so.
 	 */
 
+	act = NULL;
 	switch (n->type) {
 	case ROFFT_TEXT:
 		if (n->flags & NODE_LINE) {
@@ -1287,7 +1294,7 @@ termp_sh_pre(DECL_ARGS)
 		term_tab_set(p, ".5i");
 		switch (n->sec) {
 		case SEC_DESCRIPTION:
-			fn_prio = 0;
+			fn_prio = TAG_STRONG;
 			break;
 		case SEC_AUTHORS:
 			p->flags &= ~(TERMP_SPLIT|TERMP_NOSPLIT);
@@ -1376,7 +1383,7 @@ termp_fn_pre(DECL_ARGS)
 	term_fontpop(p);
 
 	if (n->sec == SEC_DESCRIPTION || n->sec == SEC_CUSTOM)
-		tag_put(n->string, ++fn_prio, p->line);
+		tag_put(n->string, fn_prio++, p->line);
 
 	if (pretty) {
 		term_flushln(p);
@@ -1607,7 +1614,7 @@ termp_in_post(DECL_ARGS)
 static int
 termp_pp_pre(DECL_ARGS)
 {
-	fn_prio = 0;
+	fn_prio = TAG_STRONG;
 	term_vspace(p);
 	return 0;
 }
@@ -2032,7 +2039,7 @@ termp_em_pre(DECL_ARGS)
 {
 	if (n->child != NULL &&
 	    n->child->type == ROFFT_TEXT)
-		tag_put(n->child->string, 0, p->line);
+		tag_put(n->child->string, TAG_FALLBACK, p->line);
 	term_fontpush(p, TERMFONT_UNDER);
 	return 1;
 }
@@ -2042,7 +2049,7 @@ termp_sy_pre(DECL_ARGS)
 {
 	if (n->child != NULL &&
 	    n->child->type == ROFFT_TEXT)
-		tag_put(n->child->string, 0, p->line);
+		tag_put(n->child->string, TAG_FALLBACK, p->line);
 	term_fontpush(p, TERMFONT_BOLD);
 	return 1;
 }
@@ -2055,7 +2062,7 @@ termp_er_pre(DECL_ARGS)
 	    (n->parent->tok == MDOC_It ||
 	     (n->parent->tok == MDOC_Bq &&
 	      n->parent->parent->parent->tok == MDOC_It)))
-		tag_put(n->child->string, 1, p->line);
+		tag_put(n->child->string, TAG_STRONG, p->line);
 	return 1;
 }
 
@@ -2072,11 +2079,18 @@ termp_tag_pre(DECL_ARGS)
 	     (n->parent->tok == MDOC_Xo &&
 	      n->parent->parent->prev == NULL &&
 	      n->parent->parent->parent->tok == MDOC_It)))
-		tag_put(n->child->string, 1, p->line);
+		tag_put(n->child->string, TAG_STRONG, p->line);
 	return 1;
 }
 
 static int
+termp_tg_pre(DECL_ARGS)
+{
+	tag_put(n->child->string, TAG_MANUAL, p->line);
+	return 0;
+}
+
+static int
 termp_abort_pre(DECL_ARGS)
 {
 	abort();
--- a/mdoc_validate.c
+++ b/mdoc_validate.c
@@ -1,7 +1,7 @@
 /*	$Id: mdoc_validate.c,v 1.371 2019/03/04 13:01:57 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2010-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2010-2020 Ingo Schwarze <schwarze@openbsd.org>
  * Copyright (c) 2010 Joerg Sonnenberger <joerg@netbsd.org>
  *
  * Permission to use, copy, modify, and distribute this software for any
@@ -64,7 +64,7 @@ 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);
+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);
@@ -113,6 +113,7 @@ 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_tg(POST_ARGS);
 static	void	 post_useless(POST_ARGS);
 static	void	 post_xr(POST_ARGS);
 static	void	 post_xx(POST_ARGS);
@@ -238,6 +239,7 @@ static	const v_post mdoc_valids[MDOC_MAX
 	NULL,		/* %Q */
 	NULL,		/* %U */
 	NULL,		/* Ta */
+	post_tg,	/* Tg */
 };
 
 #define	RSORD_MAX 14 /* Number of `Rs' blocks. */
@@ -1090,6 +1092,41 @@ post_st(POST_ARGS)
 }
 
 static void
+post_tg(POST_ARGS)
+{
+	struct roff_node	*n, *nch;
+	size_t			len;
+
+	n = mdoc->last;
+	nch = n->child;
+	if (nch == NULL && n->next != NULL &&
+	    n->next->child->type == ROFFT_TEXT) {
+		mdoc->next = ROFF_NEXT_CHILD;
+		roff_word_alloc(mdoc, n->line, n->pos, n->next->child->string);
+		nch = mdoc->last;
+		nch->flags |= NODE_NOSRC;
+		mdoc->last = n;
+	}
+	if (nch == NULL || *nch->string == '\0') {
+		mandoc_msg(MANDOCERR_MACRO_EMPTY, n->line, n->pos, "Tg");
+		roff_node_delete(mdoc, n);
+		return;
+	}
+	len = strcspn(nch->string, " \t");
+	if (nch->string[len] != '\0')
+		mandoc_msg(MANDOCERR_TG_SPC, nch->line, nch->pos + len + 1,
+		    "Tg %s", nch->string);
+	if (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);
+	}
+	if (nch->string[len] != '\0')
+		roff_node_delete(mdoc, n);
+}
+
+static void
 post_obsolete(POST_ARGS)
 {
 	struct roff_node *n;
@@ -1186,11 +1223,17 @@ post_fname(POST_ARGS)
 	size_t			 pos;
 
 	n = mdoc->last->child;
-	pos = strcspn(n->string, "()");
-	cp = n->string + pos;
-	if ( ! (cp[0] == '\0' || (cp[0] == '(' && cp[1] == '*')))
-		mandoc_msg(MANDOCERR_FN_PAREN, n->line, n->pos + pos,
-		    "%s", n->string);
+	cp = n->string;
+	if (*cp == '(') {
+		if (cp[strlen(cp + 1)] == ')')
+			return;
+		pos = 0;
+	} else {
+		pos = strcspn(cp, "()");
+		if (cp[pos] == '\0')
+			return;
+	}
+	mandoc_msg(MANDOCERR_FN_PAREN, n->line, n->pos + pos, "%s", cp);
 }
 
 static void
@@ -1748,7 +1791,7 @@ post_bl(POST_ARGS)
 	while (nchild != NULL) {
 		nnext = nchild->next;
 		if (nchild->tok == MDOC_It ||
-		    (nchild->tok == MDOC_Sm &&
+		    ((nchild->tok == MDOC_Sm || nchild->tok == MDOC_Tg) &&
 		     nnext != NULL && nnext->tok == MDOC_It)) {
 			nchild = nnext;
 			continue;
@@ -1903,8 +1946,7 @@ post_root(POST_ARGS)
 	/* Add missing prologue data. */
 
 	if (mdoc->meta.date == NULL)
-		mdoc->meta.date = mdoc->quick ? mandoc_strdup("") :
-		    mandoc_normdate(mdoc, NULL, 0, 0);
+		mdoc->meta.date = mandoc_normdate(NULL, NULL);
 
 	if (mdoc->meta.title == NULL) {
 		mandoc_msg(MANDOCERR_DT_NOTITLE, 0, 0, "EOF");
@@ -2502,7 +2544,6 @@ static void
 post_dd(POST_ARGS)
 {
 	struct roff_node *n;
-	char		 *datestr;
 
 	n = mdoc->last;
 	n->flags |= NODE_NOPRT;
@@ -2519,21 +2560,10 @@ post_dd(POST_ARGS)
 		mandoc_msg(MANDOCERR_PROLOG_ORDER,
 		    n->line, n->pos, "Dd after Os");
 
-	if (n->child == NULL || n->child->string[0] == '\0') {
-		mdoc->meta.date = mdoc->quick ? mandoc_strdup("") :
-		    mandoc_normdate(mdoc, NULL, n->line, n->pos);
-		return;
-	}
-
-	datestr = NULL;
-	deroff(&datestr, n);
-	if (mdoc->quick)
-		mdoc->meta.date = datestr;
-	else {
-		mdoc->meta.date = mandoc_normdate(mdoc,
-		    datestr, n->line, n->pos);
-		free(datestr);
-	}
+	if (mdoc->quick && n != NULL)
+		mdoc->meta.date = mandoc_strdup("");
+	else
+		mdoc->meta.date = mandoc_normdate(n->child, n);
 }
 
 static void
--- a/out.c
+++ b/out.c
@@ -149,7 +149,7 @@ tblcalc(struct rofftbl *tbl, const struc
 		gp = &first_group;
 		for (dp = sp->first; dp != NULL; dp = dp->next) {
 			icol = dp->layout->col;
-			while (icol > maxcol)
+			while (maxcol < icol + dp->hspans)
 				tbl->cols[++maxcol].spacing = SIZE_MAX;
 			col = tbl->cols + icol;
 			col->flags |= dp->layout->flags;
@@ -209,13 +209,25 @@ tblcalc(struct rofftbl *tbl, const struc
 	}
 
 	/*
-	 * Column spacings are needed for span width calculations,
-	 * so set the default values now.
+	 * The minimum width of columns explicitly specified
+	 * in the layout is 1n.
 	 */
 
-	for (icol = 0; icol <= maxcol; icol++)
-		if (tbl->cols[icol].spacing == SIZE_MAX || icol == maxcol)
-			tbl->cols[icol].spacing = 3;
+	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,
--- a/read.c
+++ b/read.c
@@ -157,7 +157,7 @@ mparse_buf_r(struct mparse *curp, struct
 	ln.sz = 256;
 	ln.buf = mandoc_malloc(ln.sz);
 	ln.next = NULL;
-	firstln = loop = NULL;
+	firstln = lastln = loop = NULL;
 	lnn = curp->line;
 	pos = 0;
 	inloop = 0;
@@ -255,6 +255,8 @@ mparse_buf_r(struct mparse *curp, struct
 		/* 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';
 		}
@@ -429,9 +431,8 @@ read_whole_file(struct mparse *curp, int
 	int		 gzerrnum, retval;
 
 	if (fstat(fd, &st) == -1) {
-		mandoc_msg(MANDOCERR_FILE, 0, 0,
-		    "fstat: %s", strerror(errno));
-		return 0;
+		mandoc_msg(MANDOCERR_FSTAT, 0, 0, "%s", strerror(errno));
+		return -1;
 	}
 
 	/*
@@ -444,13 +445,13 @@ read_whole_file(struct mparse *curp, int
 	if (curp->gzip == 0 && S_ISREG(st.st_mode)) {
 		if (st.st_size > 0x7fffffff) {
 			mandoc_msg(MANDOCERR_TOOLARGE, 0, 0, NULL);
-			return 0;
+			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 1;
+			return 0;
 	}
 
 	if (curp->gzip) {
@@ -462,15 +463,15 @@ read_whole_file(struct mparse *curp, int
 		 * which this function must not do.
 		 */
 		if ((fd = dup(fd)) == -1) {
-			mandoc_msg(MANDOCERR_FILE, 0, 0,
-			    "dup: %s", strerror(errno));
-			return 0;
+			mandoc_msg(MANDOCERR_DUP, 0, 0,
+			    "%s", strerror(errno));
+			return -1;
 		}
 		if ((gz = gzdopen(fd, "rb")) == NULL) {
-			mandoc_msg(MANDOCERR_FILE, 0, 0,
-			    "gzdopen: %s", strerror(errno));
+			mandoc_msg(MANDOCERR_GZDOPEN, 0, 0,
+			    "%s", strerror(errno));
 			close(fd);
-			return 0;
+			return -1;
 		}
 	} else
 		gz = NULL;
@@ -482,7 +483,7 @@ read_whole_file(struct mparse *curp, int
 
 	*with_mmap = 0;
 	off = 0;
-	retval = 0;
+	retval = -1;
 	fb->sz = 0;
 	fb->buf = NULL;
 	for (;;) {
@@ -498,13 +499,13 @@ read_whole_file(struct mparse *curp, int
 		    read(fd, fb->buf + (int)off, fb->sz - off);
 		if (ssz == 0) {
 			fb->sz = off;
-			retval = 1;
+			retval = 0;
 			break;
 		}
 		if (ssz == -1) {
 			if (curp->gzip)
 				(void)gzerror(gz, &gzerrnum);
-			mandoc_msg(MANDOCERR_FILE, 0, 0, "read: %s",
+			mandoc_msg(MANDOCERR_READ, 0, 0, "%s",
 			    curp->gzip && gzerrnum != Z_ERRNO ?
 			    zError(gzerrnum) : strerror(errno));
 			break;
@@ -513,10 +514,10 @@ read_whole_file(struct mparse *curp, int
 	}
 
 	if (curp->gzip && (gzerrnum = gzclose(gz)) != Z_OK)
-		mandoc_msg(MANDOCERR_FILE, 0, 0, "gzclose: %s",
+		mandoc_msg(MANDOCERR_GZCLOSE, 0, 0, "%s",
 		    gzerrnum == Z_ERRNO ? strerror(errno) :
 		    zError(gzerrnum));
-	if (retval == 0) {
+	if (retval == -1) {
 		free(fb->buf);
 		fb->buf = NULL;
 	}
@@ -555,7 +556,7 @@ mparse_readfd(struct mparse *curp, int f
 		mandoc_msg(MANDOCERR_ROFFLOOP, curp->line, 0, NULL);
 		return;
 	}
-	if (read_whole_file(curp, fd, &blk, &with_mmap) == 0)
+	if (read_whole_file(curp, fd, &blk, &with_mmap) == -1)
 		return;
 
 	/*
--- a/regress/char/space/zerowidth.out_html
+++ b/regress/char/space/zerowidth.out_html
@@ -1,6 +1,4 @@
-BEGINTEST
 zero width space \&amp; between A and B: AB
 hyphenation allowed \% between A and B: AB
 half-narrow (1/12) space \^ between A and B: AB
 narrow space (1/6) \| between A and B: AB
-ENDTEST
--- a/regress/char/unicode/ascii.out_html
+++ b/regress/char/unicode/ascii.out_html
@@ -1,4 +1,3 @@
-BEGINTEST
 &quot;&quot;&quot;	QUOTATION MARK
 ###	NUMBER SIGN
 $$$	DOLLAR SIGN
@@ -19,4 +18,3 @@ ____	LOW LINE
 ||||	VERTICAL LINE
 }}}	RIGHT CURLY BRACKET
 ~~~~	TILDE
-ENDTEST
--- a/regress/char/unicode/invalid.out_html
+++ b/regress/char/unicode/invalid.out_html
@@ -1,8 +1,6 @@
-BEGINTEST
 too short: &gt;.&lt;
 just right: &gt;+&lt;
 too long: &gt;..&lt;
 too large: &gt;..&lt;
 trailing garbage: &gt;&lt;
 not unicode: &gt;_.&#x2191;.&#x21D1;&lt;
-ENDTEST
--- a/regress/char/unicode/latin1.out_html
+++ b/regress/char/unicode/latin1.out_html
@@ -1,4 +1,3 @@
-BEGINTEST
 &#x00A1;&#x00A1;	INVERTED EXCLAMATION MARK
 &#x00A2;&#x00A2;	CENT SIGN
 &#x00A3;&#x00A3;	POUND SIGN
@@ -93,4 +92,3 @@ BEGINTEST
 &#x00FD;&#x00FD;	LATIN SMALL LETTER Y WITH ACUTE
 &#x00FE;&#x00FE;	LATIN SMALL LETTER THORN
 &#x00FF;&#x00FF;	LATIN SMALL LETTER Y WITH DIAERESIS
-ENDTEST
--- a/regress/char/unicode/latin1diff.out_html
+++ b/regress/char/unicode/latin1diff.out_html
@@ -1,3 +1 @@
-BEGINTEST
 &#x00AF;&#x00AF;	MACRON
-ENDTEST
--- a/regress/char/unicode/named.out_html
+++ b/regress/char/unicode/named.out_html
@@ -1,4 +1,3 @@
-BEGINTEST
 &#x0131;&#x0131;	LATIN SMALL LETTER DOTLESS I
 &#x0132;&#x0132;	LATIN CAPITAL LIGATURE IJ
 &#x0133;&#x0133;	LATIN SMALL LIGATURE IJ
@@ -167,4 +166,3 @@ BEGINTEST
 &#x2666;&#x2666;	BLACK DIAMOND SUIT
 &#x27E8;&#x27E8;	MATHEMATICAL LEFT ANGLE BRACKET
 &#x27E9;&#x27E9;	MATHEMATICAL RIGHT ANGLE BRACKET
-ENDTEST
--- a/regress/char/unicode/namediff.out_html
+++ b/regress/char/unicode/namediff.out_html
@@ -1,4 +1,3 @@
-BEGINTEST
 &#x203E;&#x203E; OVERLINE
 &#x210F;&#x210F;&#x210F; PLANCK CONSTANT OVER TWO PI
 &#x2195;&#x2195; UP DOWN ARROW
@@ -22,4 +21,3 @@ BEGINTEST
 &#x23AB;&#x23AB; RIGHT CURLY BRACKET UPPER HOOK
 &#x23AC;&#x23AC; RIGHT CURLY BRACKET MIDDLE PIECE
 &#x23AD;&#x23AD; RIGHT CURLY BRACKET LOWER HOOK
-ENDTEST
--- a/regress/char/unicode/nogroff.out_html
+++ b/regress/char/unicode/nogroff.out_html
@@ -1,4 +1,3 @@
-BEGINTEST
 &#xFFFD;&#xFFFD;	&lt;control&gt; NULL
 &#xFFFD;&#xFFFD;	&lt;control&gt; START OF HEADING
 &#xFFFD;&#xFFFD;	&lt;control&gt; BELL
@@ -34,4 +33,3 @@ BEGINTEST
 &#x10FFFD;	&lt;Plane 16 Private Use, Last&gt;
 &#x10FFFE;	&lt;undefined&gt;
 &#x10FFFF;	&lt;undefined&gt;
-ENDTEST
--- a/regress/eqn/Makefile.inc
+++ b/regress/eqn/Makefile.inc
@@ -1,6 +1,4 @@
-# $OpenBSD: Makefile.inc,v 1.2 2015/02/03 19:37:25 schwarze Exp $
-
-EQN = /usr/local/bin/eqn
+# $OpenBSD: Makefile.inc,v 1.4 2020/01/08 10:17:15 schwarze Exp $
 
 SKIP_GROFF ?= ${REGRESS_TARGETS}
 SKIP_TMAN ?= ALL
--- a/regress/eqn/delim/Makefile
+++ b/regress/eqn/delim/Makefile
@@ -1,5 +1,8 @@
-# $OpenBSD: Makefile,v 1.1.1.1 2015/01/01 12:53:46 schwarze Exp $
+# $OpenBSD: Makefile,v 1.3 2020/01/08 12:09:14 schwarze Exp $
 
 REGRESS_TARGETS	 = basic
+UTF8_TARGETS	 = basic
+GOPTS		 = -e
+SKIP_GROFF	 =
 
 .include <bsd.regress.mk>
--- a/regress/eqn/delim/basic.in
+++ b/regress/eqn/delim/basic.in
@@ -8,15 +8,24 @@
 .Sh DESCRIPTION
 initial text
 .EQ
-delim <>alpha
+delim []alpha
 .EN
-inline <beta>
+inline [beta]
 .EQ
 delim offgamma
 .EN
-inline <delta>
+inline [delta]
 .EQ
 delim onepsilon
 .EN
-inline <zeta>
+inline [zeta]
+.EQ
+delim $$
+delim off
+.EN
+inline $eta$
+.EQ
+delim on
+.EN
+inline $theta$
 final text
--- a/regress/eqn/delim/basic.out_ascii
+++ b/regress/eqn/delim/basic.out_ascii
@@ -4,7 +4,7 @@ NNAAMMEE
      ddeelliimm--bbaassiicc - inline eqn delimiters
 
 DDEESSCCRRIIPPTTIIOONN
-     initial text <alpha> inline <beta> <gamma> inline <delta> <epsilon>
-     inline <zeta> final text
+     initial text <alpha> inline <beta> <gamma> inline [delta] <epsilon>
+     inline <zeta> inline $eta$ inline <theta> final text
 
-OpenBSD                          July 4, 2017                          OpenBSD
+OpenBSD                         January 8, 2020                        OpenBSD
--- /dev/null
+++ b/regress/eqn/delim/basic.out_utf8
@@ -0,0 +1,10 @@
+DELIM-BASIC(1)              General Commands Manual             DELIM-BASIC(1)
+
+NNAAMMEE
+     ddeelliimm--bbaassiicc – inline eqn delimiters
+
+DDEESSCCRRIIPPTTIIOONN
+     initial text α inline β γ inline [delta] ε inline ζ inline $eta$ inline θ
+     final text
+
+OpenBSD                         January 8, 2020                        OpenBSD
--- a/regress/eqn/nullary/roman.out_html
+++ b/regress/eqn/nullary/roman.out_html
@@ -1,15 +1,5 @@
 <mrow><mrow><mi>unquoted
-words:</mi></mrow><mi>sin</mi><mi>cos</mi><mi>tan</mi>
-<mi>sec</mi><mi>csc</mi>
-<mi>asin</mi><mi>acos</mi><mi>atan</mi><mi>asec</mi>
-<mi>acsc</mi><mi>sinh</mi>
-<mi>cosh</mi><mi>tanh</mi><mi>coth</mi><mi>arc</mi>
-<mi>max</mi><mi>min</mi><mi>lim</mi><mi>log</mi><mi>ln</mi><mi>exp</mi><mi>Re</mi><mi>Im</mi><mi>and</mi><mi>if</mi><mi>for</mi><mi>det</mi><mo>&#x2014;</mo><mrow><mi>quoted
-words:</mi></mrow>
-<mi fontstyle="italic">sin</mi><mi fontstyle="italic">cos</mi>
-<mi fontstyle="italic">tan</mi><mi fontstyle="italic">sec</mi><mi fontstyle="italic">csc</mi><mi fontstyle="italic">asin</mi><mi fontstyle="italic">acos</mi><mi fontstyle="italic">atan</mi><mi fontstyle="italic">asec</mi><mi fontstyle="italic">acsc</mi><mi fontstyle="italic">sinh</mi><mi fontstyle="italic">cosh</mi><mi fontstyle="italic">tanh</mi><mi fontstyle="italic">coth</mi><mi fontstyle="italic">arc</mi><mi fontstyle="italic">max</mi><mi fontstyle="italic">min</mi><mi fontstyle="italic">lim</mi><mi fontstyle="italic">log</mi><mi fontstyle="italic">ln</mi><mi fontstyle="italic">exp</mi><mi fontstyle="italic">Re</mi><mi fontstyle="italic">Im</mi><mi fontstyle="italic">and</mi><mi fontstyle="italic">if</mi><mi fontstyle="italic">for</mi><mi fontstyle="italic">det</mi><mo>&#x2014;</mo><mrow><mi>font
-operations:</mi></mrow>
-<mrow><mi>sin</mi></mrow><mrow><mi fontweight="bold">sin</mi></mrow><mo>&#x2014;</mo><mrow><mi>superstring:</mi></mrow><mi fontstyle="italic">sinus</mi><mo>&#x2014;</mo><mrow><mi>composite
-word:</mi></mrow>
-<mi>tan</mi><mo>=</mo><mi fontstyle="italic">sin</mi><mo>/</mo>
-<mi fontstyle="italic">cos</mi></mrow>
+words:</mi></mrow><mi>sin</mi><mi>cos</mi><mi>tan</mi><mi>sec</mi><mi>csc</mi><mi>asin</mi><mi>acos</mi><mi>atan</mi><mi>asec</mi><mi>acsc</mi><mi>sinh</mi><mi>cosh</mi><mi>tanh</mi><mi>coth</mi><mi>arc</mi><mi>max</mi><mi>min</mi><mi>lim</mi><mi>log</mi><mi>ln</mi><mi>exp</mi><mi>Re</mi><mi>Im</mi><mi>and</mi><mi>if</mi><mi>for</mi><mi>det</mi><mo>&#x2014;</mo><mrow><mi>quoted
+words:</mi></mrow><mi fontstyle="italic">sin</mi><mi fontstyle="italic">cos</mi><mi fontstyle="italic">tan</mi><mi fontstyle="italic">sec</mi><mi fontstyle="italic">csc</mi><mi fontstyle="italic">asin</mi><mi fontstyle="italic">acos</mi><mi fontstyle="italic">atan</mi><mi fontstyle="italic">asec</mi><mi fontstyle="italic">acsc</mi><mi fontstyle="italic">sinh</mi><mi fontstyle="italic">cosh</mi><mi fontstyle="italic">tanh</mi><mi fontstyle="italic">coth</mi><mi fontstyle="italic">arc</mi><mi fontstyle="italic">max</mi><mi fontstyle="italic">min</mi><mi fontstyle="italic">lim</mi><mi fontstyle="italic">log</mi><mi fontstyle="italic">ln</mi><mi fontstyle="italic">exp</mi><mi fontstyle="italic">Re</mi><mi fontstyle="italic">Im</mi><mi fontstyle="italic">and</mi><mi fontstyle="italic">if</mi><mi fontstyle="italic">for</mi><mi fontstyle="italic">det</mi><mo>&#x2014;</mo><mrow><mi>font
+operations:</mi></mrow><mrow><mi>sin</mi></mrow><mrow><mi fontweight="bold">sin</mi></mrow><mo>&#x2014;</mo><mrow><mi>superstring:</mi></mrow><mi fontstyle="italic">sinus</mi><mo>&#x2014;</mo><mrow><mi>composite
+word:</mi></mrow><mi>tan</mi><mo>=</mo><mi fontstyle="italic">sin</mi><mo>/</mo><mi fontstyle="italic">cos</mi></mrow>
--- a/regress/eqn/nullary/symbol.out_html
+++ b/regress/eqn/nullary/symbol.out_html
@@ -1,6 +1,4 @@
 <mrow><mrow><mi>unquoted
 words:</mi></mrow><mo>&#x03B5;</mo><mo>&#x2032;</mo><mo>&#x2014;</mo><mrow><mi>quoted
-words:</mi></mrow>
-<mi fontstyle="italic">epsilon</mi><mi fontstyle="italic">prime</mi><mo>&#x2014;</mo><mrow><mi>composite
-word:</mi></mrow>
-<mi fontstyle="italic">epsilon</mi><mo>-</mo><mi fontstyle="italic">prime</mi></mrow>
+words:</mi></mrow><mi fontstyle="italic">epsilon</mi><mi fontstyle="italic">prime</mi><mo>&#x2014;</mo><mrow><mi>composite
+word:</mi></mrow><mi fontstyle="italic">epsilon</mi><mo>-</mo><mi fontstyle="italic">prime</mi></mrow>
--- a/regress/man/HP/literal.out_html
+++ b/regress/man/HP/literal.out_html
@@ -1,4 +1,3 @@
-BEGINTEST before hanged paragraph
 <p class="Pp HP">tag indented text</p>
 <p class="Pp">regular paragraph</p>
 <pre>
@@ -17,4 +16,3 @@ paragraph
 </pre>
 regular text
 <br/>
-ENDTEST
--- a/regress/man/IP/literal.out_html
+++ b/regress/man/IP/literal.out_html
@@ -1,4 +1,3 @@
-BEGINTEST before indentation
 <dl class="Bl-tag">
   <dt>tag</dt>
   <dd>indented regular text</dd>
@@ -27,7 +26,7 @@ regular text
 <section class="Ss">
 <h2 class="Ss" id="literal_into_indented_paragraph"><a class="permalink" href="#literal_into_indented_paragraph">literal
   into indented paragraph</a></h2>
-regular text
+<p class="Pp">regular text</p>
 <pre>
 literal
 text
@@ -47,7 +46,7 @@ text
 <section class="Ss">
 <h2 class="Ss" id="literal_out_of_indented_paragraph"><a class="permalink" href="#literal_out_of_indented_paragraph">literal
   out of indented paragraph</a></h2>
-regular text
+<p class="Pp">regular text</p>
 <dl class="Bl-tag">
   <dt>tag</dt>
   <dd>indented regular text
@@ -65,4 +64,3 @@ paragraph
 </pre>
 regular text
 <br/>
-ENDTEST
--- a/regress/man/RS/literal.out_html
+++ b/regress/man/RS/literal.out_html
@@ -1,6 +1,5 @@
-BEGINTEST
-<br/>
-initial regular text
+  <br/>
+  initial regular text</p>
 <pre>
 literal text
 before display
@@ -15,6 +14,5 @@ This is a very long line that would wrap
 literal text
 after display
 </pre>
-final regular text
-<br/>
-ENDTEST
+<p class="Pp">final regular text
+  <br/>
--- a/regress/man/RS/paragraph.out_html
+++ b/regress/man/RS/paragraph.out_html
@@ -1,8 +1,6 @@
-BEGINTEST before paragraph
 <p class="Pp">regular paragraph</p>
 <div class="Bd-indent">indented paragraph
 <p class="Pp">nested paragraph</p>
 </div>
 regular text after display
 <br/>
-ENDTEST
--- a/regress/man/SH/paragraph.out_html
+++ b/regress/man/SH/paragraph.out_html
@@ -1,10 +1,8 @@
-BEGINTEST
 </section>
 <section class="Sh">
 <h1 class="Sh" id="DESCRIPTION"><a class="permalink" href="#DESCRIPTION">DESCRIPTION</a></h1>
-This text immediately follows a section header.
+<p class="Pp">This text immediately follows a section header.</p>
 <p class="Pp">This is a paragraph.</p>
 </section>
 <section class="Sh">
 <h1 class="Sh" id="EXAMPLES"><a class="permalink" href="#EXAMPLES">EXAMPLES</a></h1>
-ENDTEST
--- a/regress/man/SS/paragraph.out_html
+++ b/regress/man/SS/paragraph.out_html
@@ -1,11 +1,9 @@
-BEGINTEST
 <section class="Ss">
 <h2 class="Ss" id="First_subsection"><a class="permalink" href="#First_subsection">First
   subsection</a></h2>
-This text immediately follows a subsection header.
+<p class="Pp">This text immediately follows a subsection header.</p>
 <p class="Pp">This is a paragraph.</p>
 </section>
 <section class="Ss">
 <h2 class="Ss" id="Second_subsection"><a class="permalink" href="#Second_subsection">Second
   subsection</a></h2>
-ENDTEST
--- a/regress/man/SY/literal.out_html
+++ b/regress/man/SY/literal.out_html
@@ -1,6 +1,5 @@
-BEGINTEST
-<br/>
-initial regular text
+  <br/>
+  initial regular text</p>
 <table class="Nm">
   <tr>
     <td><code class="Nm">command</code></td>
@@ -26,6 +25,5 @@ before display
 literal text
 after display
 </pre>
-final regular text
-<br/>
-ENDTEST
+<p class="Pp">final regular text
+  <br/>
--- a/regress/man/TH/baddate.out_lint
+++ b/regress/man/TH/baddate.out_lint
@@ -1 +1 @@
-mandoc: baddate.in:2:18: WARNING: cannot parse date, using it verbatim: three bad words
+mandoc: baddate.in:2:18: WARNING: cannot parse date, using it verbatim: TH three bad words
--- a/regress/man/TH/emptydate.out_lint
+++ b/regress/man/TH/emptydate.out_lint
@@ -1 +1 @@
-mandoc: emptydate.in:2:20: WARNING: missing date, using today's date: TH
+mandoc: emptydate.in:2:20: WARNING: missing date, using "": TH
--- a/regress/man/TH/longdate.out_lint
+++ b/regress/man/TH/longdate.out_lint
@@ -1 +1 @@
-mandoc: longdate.in:2:19: WARNING: cannot parse date, using it verbatim: 1234567890123456789012345678901234567890123456789012345678901234567890123456789012
+mandoc: longdate.in:2:19: WARNING: cannot parse date, using it verbatim: TH 1234567890123456789012345678901234567890123456789012345678901234567890123456789012
--- a/regress/man/TH/noTH.out_lint
+++ b/regress/man/TH/noTH.out_lint
@@ -1,2 +1,2 @@
 mandoc: noTH.in: WARNING: missing manual title, using ""
-mandoc: noTH.in: WARNING: missing date, using today's date
+mandoc: noTH.in: WARNING: missing date, using ""
--- a/regress/man/TH/noarg.out_lint
+++ b/regress/man/TH/noarg.out_lint
@@ -1,3 +1,3 @@
 mandoc: noarg.in:2:2: WARNING: missing manual title, using "": TH
 mandoc: noarg.in:2:2: WARNING: missing manual section, using "": TH 
-mandoc: noarg.in:2:2: WARNING: missing date, using today's date: TH
+mandoc: noarg.in:2:2: WARNING: missing date, using "": TH
--- a/regress/man/TH/onearg.out_lint
+++ b/regress/man/TH/onearg.out_lint
@@ -1,2 +1,2 @@
 mandoc: onearg.in:2:2: WARNING: missing manual section, using "": TH TH-ONEARG
-mandoc: onearg.in:2:2: WARNING: missing date, using today's date: TH
+mandoc: onearg.in:2:2: WARNING: missing date, using "": TH
--- a/regress/man/TH/twoargs.out_lint
+++ b/regress/man/TH/twoargs.out_lint
@@ -1,2 +1,2 @@
 mandoc: twoargs.in:2:2: WARNING: missing manual section, using "": TH TH-TWOARGS
-mandoc: twoargs.in:2:2: WARNING: missing date, using today's date: TH
+mandoc: twoargs.in:2:2: WARNING: missing date, using "": TH
--- a/regress/man/TP/literal.out_html
+++ b/regress/man/TP/literal.out_html
@@ -1,4 +1,3 @@
-BEGINTEST before indentation
 <dl class="Bl-tag">
   <dt>tag</dt>
   <dd>regular indented text</dd>
@@ -24,4 +23,3 @@ paragraph
 </pre>
 regular text
 <br/>
-ENDTEST
--- a/regress/man/TS/Makefile
+++ b/regress/man/TS/Makefile
@@ -1,7 +1,8 @@
-# $OpenBSD: Makefile,v 1.3 2015/01/30 21:28:21 schwarze Exp $
+# $OpenBSD: Makefile,v 1.4 2020/01/08 10:17:15 schwarze Exp $
 
 REGRESS_TARGETS	= break vspace
 LINT_TARGETS	= break
+GOPTS		= -t
 
 # groff-1.22.3 defect:
 # - Starting a table in next-line scope confuses font handling,
@@ -9,14 +10,4 @@ LINT_TARGETS	= break
 
 SKIP_GROFF	= break
 
-
-# OpenBSD only: maintainer targets
-
-TBL=/usr/local/bin/tbl
-
-.for t in ${REGRESS_TARGETS}
-${t}.out_ascii: ${t}.in
-	${TBL} ${.ALLSRC} | ${NROFF} ${NOPTS} -Tascii > ${.TARGET}
-.endfor
-
 .include <bsd.regress.mk>
--- a/regress/mdoc/Bd/nf.out_html
+++ b/regress/mdoc/Bd/nf.out_html
@@ -1,9 +1,8 @@
-BEGINTEST initial text
 <pre>
 after .nf
 request
 </pre>
-after .fi request
+<p class="Pp">after .fi request</p>
 <div class="Bd Pp">
 <pre>
 in unfilled
@@ -19,4 +18,3 @@ in filled block
 </div>
 after end of filled block
 <br/>
-ENDTEST
--- a/regress/mdoc/Bd/paragraph.out_html
+++ b/regress/mdoc/Bd/paragraph.out_html
@@ -1,4 +1,3 @@
-BEGINTEST initial text
 <p class="Pp">normal paragraph</p>
 <div class="Bd Pp">filled display
 <p class="Pp">paragraph in display</p>
@@ -16,4 +15,3 @@ paragraph
 </div>
 again back to normal
 <br/>
-ENDTEST
--- a/regress/mdoc/Bf/paragraph.out_html
+++ b/regress/mdoc/Bf/paragraph.out_html
@@ -1,6 +1,4 @@
-BEGINTEST
 <p class="Pp">normal text</p>
 <div class="Bf Li">literal text
 <p class="Pp">literal paragraph</p>
 </div>
-ENDTEST
--- a/regress/mdoc/Bl/column.in
+++ b/regress/mdoc/Bl/column.in
@@ -81,7 +81,8 @@
 .\" Mixed tab and Ta
 .Bl -column a b c d
 .It a	b	c	d
-.It a	b	c Ta d
+.It "a	b	c" Ta
+d
 .It a	b Ta c	d
 .It a	b Ta c Ta d
 .It a Ta b	c	d
--- a/regress/mdoc/Bl/column.out_ascii
+++ b/regress/mdoc/Bl/column.out_ascii
@@ -71,4 +71,4 @@ DDEESSCCRRIIPPTTIIOONN
      aa    bb                          tab at eol
      aa    bb    cc    dd
 
-OpenBSD                          July 4, 2017                          OpenBSD
+OpenBSD                          July 11, 2019                         OpenBSD
--- a/regress/mdoc/Bl/column.out_lint
+++ b/regress/mdoc/Bl/column.out_lint
@@ -4,4 +4,4 @@ mandoc: column.in:75:2: WARNING: skippin
 mandoc: column.in:77:2: WARNING: wrong number of cells: 2 columns, 4 cells
 mandoc: column.in:78:2: WARNING: wrong number of cells: 2 columns, 5 cells
 mandoc: column.in:79:2: WARNING: skipping empty macro: It
-mandoc: column.in:107:18: WARNING: skipping -width argument: Bl -column
+mandoc: column.in:108:18: WARNING: skipping -width argument: Bl -column
--- a/regress/mdoc/Bl/column.out_markdown
+++ b/regress/mdoc/Bl/column.out_markdown
@@ -75,4 +75,4 @@ BL-COLUMN(1) - General Commands Manual
 
 	aa    bb    cc    dd
 
-OpenBSD - July 4, 2017
+OpenBSD - July 11, 2019
--- a/regress/mdoc/D1/spacing.out_html
+++ b/regress/mdoc/D1/spacing.out_html
@@ -1,8 +1,6 @@
-BEGINTEST
 <p class="Pp">preceding paragraph</p>
 <div class="Bd Bd-indent">spacing in and around one-line displays</div>
 empty display:
 <div class="Bd Bd-indent"></div>
 following text
 <br/>
-ENDTEST
--- a/regress/mdoc/Dd/Makefile
+++ b/regress/mdoc/Dd/Makefile
@@ -1,23 +1,15 @@
-# $OpenBSD: Makefile,v 1.2 2014/11/21 01:52:45 schwarze Exp $
+# $OpenBSD: Makefile,v 1.6 2020/01/19 16:16:33 schwarze Exp $
 
 REGRESS_TARGETS	 = badarg dupe late long manarg noarg order
 LINT_TARGETS	 = badarg dupe late long manarg noarg order
 
-# noarg output contains the date when the file is formatted
+# groff-1.22.4 prints footer fields of excessive length on top of
+# each other rather than breaking the output line.
 
-SKIP_ASCII	?= noarg
-SKIP_MARKDOWN	?= noarg
-
-# If groff finds exactly three arguments, it assumes they are month,
-# day and year without further checking.  If there are no arguments,
-# groff uses the string "Epoch".  Otherwise, it silently falls back
-# to today's date.
-# That is not at all sane behaviour, we are not going to imitate it.
-
-SKIP_GROFF	 = badarg long manarg noarg
+SKIP_GROFF	 = long
 
 # Autodetection fails for late .Dd, so specify -mdoc explicitly.
 
-MOPTS		+= -mdoc
+MOPTS		 = -mdoc
 
 .include <bsd.regress.mk>
--- a/regress/mdoc/Dd/badarg.out_lint
+++ b/regress/mdoc/Dd/badarg.out_lint
@@ -1,2 +1,2 @@
-mandoc: badarg.in:2:2: WARNING: cannot parse date, using it verbatim: bad date
+mandoc: badarg.in:2:5: WARNING: cannot parse date, using it verbatim: Dd bad date
 mandoc: badarg.in:2:5: STYLE: Mdocdate missing: Dd bad date (OpenBSD)
--- a/regress/mdoc/Dd/dupe.out_lint
+++ b/regress/mdoc/Dd/dupe.out_lint
@@ -1,3 +1,3 @@
-mandoc: dupe.in:2:5: STYLE: Mdocdate missing: Dd August (OpenBSD)
+mandoc: dupe.in:2:5: STYLE: Mdocdate missing: Dd August 1, 2014 (OpenBSD)
 mandoc: dupe.in:5:2: ERROR: duplicate prologue macro: Dd
 mandoc: dupe.in:11:2: ERROR: duplicate prologue macro: Dd
--- a/regress/mdoc/Dd/long.out_lint
+++ b/regress/mdoc/Dd/long.out_lint
@@ -1,2 +1,2 @@
-mandoc: long.in:2:2: WARNING: cannot parse date, using it verbatim: 1234567890123456789012345678901234567890123456789012345678901234567890123456789
+mandoc: long.in:2:5: WARNING: cannot parse date, using it verbatim: Dd 1234567890123456789012345678901234567890123456789012345678901234567890123456789
 mandoc: long.in:2:5: STYLE: Mdocdate missing: Dd 1234567890123456789012345678901234567890123456789012345678901234567890123456789 (OpenBSD)
--- a/regress/mdoc/Dd/manarg.out_lint
+++ b/regress/mdoc/Dd/manarg.out_lint
@@ -1,2 +1,2 @@
-mandoc: manarg.in:2:2: STYLE: legacy man(7) date format: Dd 2014-08-07
+mandoc: manarg.in:2:5: STYLE: legacy man(7) date format: Dd 2014-08-07
 mandoc: manarg.in:2:5: STYLE: Mdocdate missing: Dd 2014-08-07 (OpenBSD)
--- /dev/null
+++ b/regress/mdoc/Dd/noarg.out_ascii
@@ -0,0 +1,9 @@
+DD-NOARG(1)                 General Commands Manual                DD-NOARG(1)
+
+NNAAMMEE
+     DDdd--nnooaarrgg - date macro without an argument
+
+DDEESSCCRRIIPPTTIIOONN
+     some text
+
+OpenBSD                                                                OpenBSD
--- a/regress/mdoc/Dd/noarg.out_lint
+++ b/regress/mdoc/Dd/noarg.out_lint
@@ -1 +1 @@
-mandoc: noarg.in:2:2: WARNING: missing date, using today's date
+mandoc: noarg.in:2:2: WARNING: missing date, using "": Dd
--- /dev/null
+++ b/regress/mdoc/Dd/noarg.out_markdown
@@ -0,0 +1,11 @@
+DD-NOARG(1) - General Commands Manual
+
+# NAME
+
+**Dd-noarg** - date macro without an argument
+
+# DESCRIPTION
+
+some text
+
+OpenBSD -
--- a/regress/mdoc/Dd/order.out_lint
+++ b/regress/mdoc/Dd/order.out_lint
@@ -1,2 +1,2 @@
 mandoc: order.in:3:2: WARNING: prologue macros out of order: Dd after Dt
-mandoc: order.in:3:5: STYLE: Mdocdate missing: Dd August (OpenBSD)
+mandoc: order.in:3:5: STYLE: Mdocdate missing: Dd August 5, 2014 (OpenBSD)
--- a/regress/mdoc/Fo/warn.in
+++ b/regress/mdoc/Fo/warn.in
@@ -12,3 +12,17 @@
 .Fc
 .Ft double
 .Fn atan2 "double y, double x"
+.Ft int
+.Fn close) "int fd"
+.Ft typedef void
+.Fn (handler) int
+.Ft typedef void
+.Fn (*fp) int
+.Ft int
+.Fn (open "const char *path"
+.Ft FILE *
+.Fn (*popen "const char *cmd"
+.Ft void
+.Fn (trail)x void
+.Ft void
+.Fn *star void
--- a/regress/mdoc/Fo/warn.out_ascii
+++ b/regress/mdoc/Fo/warn.out_ascii
@@ -10,4 +10,25 @@ SSYYNNOOPPSSIISS
      _d_o_u_b_l_e
      aattaann22(_d_o_u_b_l_e _y_, _d_o_u_b_l_e _x);
 
-OpenBSD                          July 4, 2017                          OpenBSD
+     _i_n_t
+     cclloossee))(_i_n_t _f_d);
+
+     _t_y_p_e_d_e_f _v_o_i_d
+     ((hhaannddlleerr))(_i_n_t);
+
+     _t_y_p_e_d_e_f _v_o_i_d
+     ((**ffpp))(_i_n_t);
+
+     _i_n_t
+     ((ooppeenn(_c_o_n_s_t _c_h_a_r _*_p_a_t_h);
+
+     _F_I_L_E _*
+     ((**ppooppeenn(_c_o_n_s_t _c_h_a_r _*_c_m_d);
+
+     _v_o_i_d
+     ((ttrraaiill))xx(_v_o_i_d);
+
+     _v_o_i_d
+     **ssttaarr(_v_o_i_d);
+
+OpenBSD                       September 13, 2019                       OpenBSD
--- a/regress/mdoc/Fo/warn.out_lint
+++ b/regress/mdoc/Fo/warn.out_lint
@@ -1,2 +1,6 @@
 mandoc: warn.in:10:8: WARNING: parenthesis in function name: sin()
 mandoc: warn.in:14:19: WARNING: comma in function argument: double y, double x
+mandoc: warn.in:16:10: WARNING: parenthesis in function name: close)
+mandoc: warn.in:22:5: WARNING: parenthesis in function name: (open
+mandoc: warn.in:24:5: WARNING: parenthesis in function name: (*popen
+mandoc: warn.in:26:5: WARNING: parenthesis in function name: (trail)x
--- a/regress/mdoc/Fo/warn.out_markdown
+++ b/regress/mdoc/Fo/warn.out_markdown
@@ -12,4 +12,25 @@ FO-WARN(1) - General Commands Manual
 *double*  
 **atan2**(*double y, double x*);
 
-OpenBSD - July 4, 2017
+*int*
+**close)**(*int fd*);
+
+*typedef void*
+**(handler)**(*int*);
+
+*typedef void*
+**(\*fp)**(*int*);
+
+*int*
+**(open**(*const char \*path*);
+
+*FILE \*&zwnj;*
+**(\*popen**(*const char \*cmd*);
+
+*void*
+**(trail)x**(*void*);
+
+*void*
+**\*star**(*void*);
+
+OpenBSD - September 13, 2019
--- a/regress/mdoc/Os/dupe.out_ascii
+++ b/regress/mdoc/Os/dupe.out_ascii
@@ -6,4 +6,4 @@ NNAAMMEE
 DDEESSCCRRIIPPTTIIOONN
      initial text final text
 
-OpenBSD                          July 4, 2017                          OpenBSD
+OpenBSD                        January 19, 2020                        OpenBSD
--- a/regress/mdoc/Os/dupe.out_lint
+++ b/regress/mdoc/Os/dupe.out_lint
@@ -1,9 +1,9 @@
 mandoc: dupe.in:3:5: STYLE: operating system explicitly specified: Os NetBSD (NetBSD)
-mandoc: dupe.in:2:5: STYLE: Mdocdate found: Dd $Mdocdate: (NetBSD)
+mandoc: dupe.in:2:5: STYLE: Mdocdate found: Dd $Mdocdate$ (NetBSD)
 mandoc: dupe.in:4:2: WARNING: prologue macros out of order: Dt after Os
 mandoc: dupe.in:5:2: ERROR: duplicate prologue macro: Os
 mandoc: dupe.in:5:5: STYLE: operating system explicitly specified: Os FreeBSD (NetBSD)
-mandoc: dupe.in:2:5: STYLE: Mdocdate found: Dd $Mdocdate: (NetBSD)
+mandoc: dupe.in:2:5: STYLE: Mdocdate found: Dd $Mdocdate$ (NetBSD)
 mandoc: dupe.in:11:2: ERROR: duplicate prologue macro: Os
 mandoc: dupe.in:11:5: STYLE: operating system explicitly specified: Os OpenBSD (NetBSD)
 mandoc: dupe.in: STYLE: RCS id missing: (NetBSD)
--- a/regress/mdoc/Os/dupe.out_markdown
+++ b/regress/mdoc/Os/dupe.out_markdown
@@ -9,4 +9,4 @@ OS-DUPE(1) - General Commands Manual
 initial text
 final text
 
-OpenBSD - July 4, 2017
+OpenBSD - January 19, 2020
--- a/regress/mdoc/Rs/paragraph.out_html
+++ b/regress/mdoc/Rs/paragraph.out_html
@@ -1,18 +1,16 @@
-BEGINTEST
-<br/>
-initial reference: <cite class="Rs"><span class="RsA">author name</span>,
-  <i class="RsB">book title</i>.</cite>
+  <br/>
+  initial reference: <cite class="Rs"><span class="RsA">author name</span>,
+    <i class="RsB">book title</i>.</cite></p>
 <p class="Pp">in a paragraph: <cite class="Rs"><span class="RsA">another
     author</span>, <i class="RsB">another book</i>.</cite></p>
 </section>
 <section class="Sh">
 <h1 class="Sh" id="SEE_ALSO"><a class="permalink" href="#SEE_ALSO">SEE
   ALSO</a></h1>
-initial reference:
+<p class="Pp">initial reference:</p>
 <p class="Pp"><cite class="Rs"><span class="RsA">author name</span>,
     <i class="RsB">book title</i>.</cite></p>
 <p class="Pp">in a paragraph:</p>
 <p class="Pp"><cite class="Rs"><span class="RsA">another author</span>,
     <i class="RsB">another book</i>.</cite></p>
 <pre>
-ENDTEST
--- a/regress/mdoc/Sh/paragraph.out_html
+++ b/regress/mdoc/Sh/paragraph.out_html
@@ -1,11 +1,9 @@
-BEGINTEST
 <p class="Pp">descriptive text</p>
 <section class="Ss">
 <h2 class="Ss" id="Subsection"><a class="permalink" href="#Subsection">Subsection</a></h2>
-initial subsection text
+<p class="Pp">initial subsection text</p>
 <p class="Pp">subsection paragraph</p>
 </section>
 </section>
 <section class="Sh">
 <h1 class="Sh" id="EXAMPLES"><a class="permalink" href="#EXAMPLES">EXAMPLES</a></h1>
-ENDTEST
--- a/regress/regress.pl
+++ b/regress/regress.pl
@@ -86,8 +86,12 @@ sub syshtml ($@) {
 		if (!$state && s/.*<math class="eqn">//) {
 			$state = 'math';
 			next unless length;
-		} elsif (/^BEGINTEST/) {
+		} elsif (/BEGINTEST/) {
 			$state = 'other';
+			next;
+		} elsif (/ENDTEST/) {
+			$state = 0;
+			next;
 		}
 		if ($state eq 'math') {
 			s/^ *//;
@@ -98,7 +102,6 @@ sub syshtml ($@) {
 			}
 		}
 		print $outfd "$_\n" if $state;
-		$state = 0 if /^ENDTEST/;
 	}
 	close $outfd;
 	close $infd;
@@ -162,10 +165,9 @@ foreach my $module (qw(roff char mdoc ma
 		my %subvars = (MOPTS => '');
 		parse_makefile "$module/$subdir/Makefile", \%subvars;
 		parse_makefile "$module/Makefile.inc", \%subvars;
+		delete $subvars{GOPTS};
 		delete $subvars{SKIP_GROFF};
 		delete $subvars{SKIP_GROFF_ASCII};
-		delete $subvars{TBL};
-		delete $subvars{EQN};
 		my @mandoc = ('../mandoc', split ' ', $subvars{MOPTS});
 		delete $subvars{MOPTS};
 		my @regress_testnames;
@@ -424,7 +426,7 @@ if ($count_total == 1) {
 	print "\n";
 } else {
 	print "No tests were run.\n";
-} 
+}
 if ($targets{clean}) {
 	if ($count_rm) {
 		print "Deleted $count_rm test output files.\n";
--- a/regress/roff/de/Makefile
+++ b/regress/roff/de/Makefile
@@ -1,7 +1,7 @@
-# $OpenBSD: Makefile,v 1.12 2019/02/06 20:54:28 schwarze Exp $
+# $OpenBSD: Makefile,v 1.13 2019/04/21 23:45:50 schwarze Exp $
 
-REGRESS_TARGETS	 = append cond escname factorial indir infinite startde tab
-REGRESS_TARGETS	+= TH Dd
+REGRESS_TARGETS	 = append cond empty escname factorial
+REGRESS_TARGETS	+= indir infinite startde tab TH Dd
 LINT_TARGETS	 = escname indir infinite
 
 # groff-1.22.4 defect:
--- /dev/null
+++ b/regress/roff/de/empty.in
@@ -0,0 +1,18 @@
+.\" $OpenBSD: empty.in,v 1.1 2019/04/21 23:45:50 schwarze Exp $
+.Dd $Mdocdate$
+.Dt DE-EMPTY 1
+.Os
+.Sh NAME
+.Nm de-empty
+.Nd empty user-defined macro with arguments
+.Sh DESCRIPTION
+initial text
+.de empty
+..
+.de real
+arg=\\$1
+.empty wrong
+arg=\\$1
+..
+.real right
+final text
--- /dev/null
+++ b/regress/roff/de/empty.out_ascii
@@ -0,0 +1,9 @@
+DE-EMPTY(1)                 General Commands Manual                DE-EMPTY(1)
+
+NNAAMMEE
+     ddee--eemmppttyy - empty user-defined macro with arguments
+
+DDEESSCCRRIIPPTTIIOONN
+     initial text arg=right arg=right final text
+
+OpenBSD                         April 21, 2019                         OpenBSD
--- a/regress/roff/esc/f.out_html
+++ b/regress/roff/esc/f.out_html
@@ -1,6 +1,4 @@
-BEGINTEST
 numbers: <b><i>bolditalic</i></b><b>bold</b><i>italic</i>roman
 letters: <b>bold</b><i>italic</i><b>back</b><b><i>bolditalic</i></b>roman
 multiletter: <b>bold</b>empty<i>italic</i>back<b><i>bolditalic</i></b>roman
 typewriter: <span class="Li">roman</span><b>bold</b><span class="Li">roman</span><i>italic</i>roman
-ENDTEST
--- a/regress/roff/ft/badargs.out_html
+++ b/regress/roff/ft/badargs.out_html
@@ -1,9 +1,6 @@
-BEGINTEST
-<br/>
-default font <i></i><i>italic</i> <b><i></i></b><b><i>bold italic</i></b>
-  <span class="Li"></span><span class="Li">typeqriter</span>
-  <span class="Li"></span> <span class="Li">roman</span> <b></b><b>bold</b>
-  <i></i> <i>italic</i> <b></b><b>bold</b> <b>still bold</b>
-  <i></i><i>italic</i> <i></i><i>back to bold</i> <i></i><i>back to italic</i>
-<br/>
-ENDTEST
+  <br/>
+  default font <i>italic</i> <b><i>bold italic</i></b>
+    <span class="Li">typeqriter</span> <span class="Li">roman</span> <b>bold</b>
+    <i>italic</i> <b>bold</b> <b>still bold</b> <i>italic</i> <i>back to
+    bold</i> <i>back to italic</i>
+  <br/>
--- a/regress/roff/sp/fill-man.out_html
+++ b/regress/roff/sp/fill-man.out_html
@@ -1,4 +1,3 @@
-BEGINTEST in fill mode:
 <p class="Pp">switch to no-fill mode:</p>
 <pre>
 in no-fill mode:
@@ -6,4 +5,3 @@ in no-fill mode:
 back to
 fill mode:
 </pre>
-ENDTEST
--- a/regress/roff/string/dotT.out_html
+++ b/regress/roff/string/dotT.out_html
@@ -1,5 +1,3 @@
-BEGINTEST
 <p class="Pp">We are using the html device.</p>
 <p class="Pp">The device name can be overridden.</p>
 <pre>
-ENDTEST
--- a/regress/roff/while/Makefile
+++ b/regress/roff/while/Makefile
@@ -1,6 +1,6 @@
-# $OpenBSD: Makefile,v 1.1 2018/08/24 22:56:37 schwarze Exp $
+# $OpenBSD: Makefile,v 1.2 2019/04/21 22:43:00 schwarze Exp $
 
-REGRESS_TARGETS	= basic badargs into nesting outof
+REGRESS_TARGETS	= basic badargs break into nesting outof
 LINT_TARGETS	= badargs into nesting outof
 
 # mandoc defects:
--- /dev/null
+++ b/regress/roff/while/break.in
@@ -0,0 +1,16 @@
+.\" $OpenBSD: break.in,v 1.1 2019/04/21 22:43:00 schwarze Exp $
+.Dd $Mdocdate$
+.Dt WHILE-BREAK 1
+.Os
+.Sh NAME
+.Nm while-break
+.Nd break request inside a while loop
+.Sh DESCRIPTION
+initial text
+.nr cnt 11 1
+.while n \{\
+\n-[cnt]
+.if !\n[cnt] .break
+\(en
+.\}
+final text
--- /dev/null
+++ b/regress/roff/while/break.out_ascii
@@ -0,0 +1,9 @@
+WHILE-BREAK(1)              General Commands Manual             WHILE-BREAK(1)
+
+NNAAMMEE
+     wwhhiillee--bbrreeaakk - break request inside a while loop
+
+DDEESSCCRRIIPPTTIIOONN
+     initial text 10 - 9 - 8 - 7 - 6 - 5 - 4 - 3 - 2 - 1 - 0 final text
+
+OpenBSD                         April 21, 2019                         OpenBSD
--- a/regress/tbl/Makefile.inc
+++ b/regress/tbl/Makefile.inc
@@ -1,16 +1,7 @@
-# $OpenBSD: Makefile.inc,v 1.2 2015/02/03 19:37:25 schwarze Exp $
-
-TBL = /usr/local/bin/tbl
+# $OpenBSD: Makefile.inc,v 1.4 2020/01/08 10:17:15 schwarze Exp $
 
 SKIP_TMAN ?= ALL
 SKIP_MARKDOWN ?= ALL
-
-
-# OpenBSD only: maintainer targets
-
-.for t in ${REGRESS_TARGETS}
-${t}.out_ascii: ${t}.in
-	${TBL} ${.ALLSRC} | ${NROFF} ${NOPTS} -Tascii > ${.TARGET}
-.endfor
+GOPTS = -t
 
 .include "../Makefile.inc"
--- a/regress/tbl/data/Makefile
+++ b/regress/tbl/data/Makefile
@@ -1,6 +1,7 @@
-# $OpenBSD: Makefile,v 1.4 2017/07/04 20:59:17 schwarze Exp $
+# $OpenBSD: Makefile,v 1.5 2019/07/18 14:38:47 schwarze Exp $
 
-REGRESS_TARGETS  = blankline block_unclosed block_width block_wrap empty insert
+REGRESS_TARGETS	 = blankline block_empty block_unclosed block_width
+REGRESS_TARGETS	+= block_wrap empty insert
 LINT_TARGETS	 = block_unclosed empty insert
 
 # groff-1.22.3 defect:
--- /dev/null
+++ b/regress/tbl/data/block_empty.in
@@ -0,0 +1,19 @@
+.\" $OpenBSD: block_empty.in,v 1.1 2019/07/18 14:38:47 schwarze Exp $
+.TH TBL-DATA-BLOCK_EMPTY 1 "July 17, 2019"
+.SH NAME
+tbl-data-block_empty \- empty text block
+.SH DESCRIPTION
+normal text
+.TS
+|l|l|.
+_
+A	test
+_
+table	T{
+T}
+_
+.TE
+.SH AUTHORS
+.MT rea@FreeBSD.org
+Eygene Ryabinkin
+.ME
--- /dev/null
+++ b/regress/tbl/data/block_empty.out_ascii
@@ -0,0 +1,22 @@
+TBL-DATA-BLOCK_EMPTY(1)     General Commands Manual    TBL-DATA-BLOCK_EMPTY(1)
+
+
+
+NNAAMMEE
+       tbl-data-block_empty - empty text block
+
+DDEESSCCRRIIPPTTIIOONN
+       normal text
+
+       +------+------+
+       |A     | test |
+       +------+------+
+       |table |      |
+       +------+------+
+
+AAUUTTHHOORRSS
+       Eygene Ryabinkin <rea@FreeBSD.org>
+
+
+
+OpenBSD                          July 17, 2019         TBL-DATA-BLOCK_EMPTY(1)
--- a/regress/tbl/layout/Makefile
+++ b/regress/tbl/layout/Makefile
@@ -1,8 +1,8 @@
-# $OpenBSD: Makefile,v 1.2 2015/01/30 00:27:09 schwarze Exp $
+# $OpenBSD: Makefile,v 1.6 2020/01/11 20:56:26 schwarze Exp $
 
-REGRESS_TARGETS	 = center complex empty emptyline
-REGRESS_TARGETS	+= lines lines-nogroff numbers span
-LINT_TARGETS	 = complex empty
+REGRESS_TARGETS	 = badspan center complex empty emptycol emptyline
+REGRESS_TARGETS	+= lines lines-nogroff numbers shortlines span
+LINT_TARGETS	 = badspan complex empty
 
 # groff-1.22.3 defects:
 # - When the layout is completely empty,
--- /dev/null
+++ b/regress/tbl/layout/badspan.in
@@ -0,0 +1,13 @@
+.\" $OpenBSD: badspan.in,v 1.1 2020/01/11 20:56:26 schwarze Exp $
+.TH TBL-LAYOUT-BADSPAN 1 "January 11, 2020"
+.SH NAME
+tbl-layout-badspan \- invalid spanned cells
+.SH DESCRIPTION
+normal text
+.TS
+allbox tab(:);
+S L S S
+L L L L L L.
+span:end
+1:2:3:4:5:6
+.TE
--- /dev/null
+++ b/regress/tbl/layout/badspan.out_ascii
@@ -0,0 +1,18 @@
+TBL-LAYOUT-BADSPAN(1)       General Commands Manual      TBL-LAYOUT-BADSPAN(1)
+
+
+
+NNAAMMEE
+       tbl-layout-badspan - invalid spanned cells
+
+DDEESSCCRRIIPPTTIIOONN
+       normal text
+
+       +--+-----------+-----+---+
+       |  | span      | end |   |
+       +--+---+---+---+-----+---+
+       |1 | 2 | 3 | 4 | 5   | 6 |
+       +--+---+---+---+-----+---+
+
+
+OpenBSD                        January 11, 2020          TBL-LAYOUT-BADSPAN(1)
--- /dev/null
+++ b/regress/tbl/layout/badspan.out_lint
@@ -0,0 +1 @@
+mandoc: badspan.in:9:1: WARNING: tbl line starts with span
--- /dev/null
+++ b/regress/tbl/layout/emptycol.in
@@ -0,0 +1,49 @@
+.\" $OpenBSD: emptycol.in,v 1.1 2019/12/31 22:49:17 schwarze Exp $
+.TH TBL-LAYOUT-EMPTYCOL 1 "December 31, 2019"
+.SH NAME
+tbl-layout-emptycol \- empty columns in tables
+.SH DESCRIPTION
+missing final column:
+.TS
+allbox tab(:);
+L L L
+L L.
+1:2
+a:b
+.TE
+.sp
+empty final column:
+.TS
+allbox tab(:);
+L L L
+L L.
+1:2:
+a:b
+.TE
+.sp
+final column with zero-width content:
+.TS
+allbox tab(:);
+L L L
+L L.
+1:2:\&
+a:b
+.TE
+.sp
+empty middle column:
+.TS
+allbox tab(:);
+L L L
+L.
+1::3
+a
+.TE
+.sp
+span crossing empty middle column:
+.TS
+allbox tab(:);
+L L L
+L S S.
+1::3
+span
+.TE
--- /dev/null
+++ b/regress/tbl/layout/emptycol.out_ascii
@@ -0,0 +1,46 @@
+TBL-LAYOUT-EMPTYCOL(1)      General Commands Manual     TBL-LAYOUT-EMPTYCOL(1)
+
+
+
+NNAAMMEE
+       tbl-layout-emptycol - empty columns in tables
+
+DDEESSCCRRIIPPTTIIOONN
+       missing final column:
+
+       +--+---+---+
+       |1 | 2 |   |
+       +--+---+---+
+       |a | b |   |
+       +--+---+---+
+       empty final column:
+
+       +--+---+---+
+       |1 | 2 |   |
+       +--+---+---+
+       |a | b |   |
+       +--+---+---+
+       final column with zero-width content:
+
+       +--+---+---+
+       |1 | 2 |   |
+       +--+---+---+
+       |a | b |   |
+       +--+---+---+
+       empty middle column:
+
+       +--+---+---+
+       |1 |   | 3 |
+       +--+---+---+
+       |a |   |   |
+       +--+---+---+
+       span crossing empty middle column:
+
+       +--+---+---+
+       |1 |   | 3 |
+       +--+---+---+
+       |span      |
+       +----------+
+
+
+OpenBSD                        December 31, 2019        TBL-LAYOUT-EMPTYCOL(1)
--- /dev/null
+++ b/regress/tbl/layout/shortlines.in
@@ -0,0 +1,47 @@
+.\" $OpenBSD: shortlines.in,v 1.2 2020/01/11 20:56:26 schwarze Exp $
+.TH TBL-LAYOUT-SHORTLINES 1 "January 11, 2020"
+.SH NAME
+tbl-layout-shortlines \- table lines of different length
+.SH DESCRIPTION
+normal text
+.TS
+allbox tab(:);
+L L
+L
+L L.
+left:right
+short
+left:right
+.TE
+.sp
+.TS
+allbox tab(:);
+L L
+L
+L
+L L.
+left:right
+first short
+second short
+left:right
+.TE
+.sp
+.TS
+allbox tab(:);
+L L L
+L L
+L.
+left:middle:right
+short:line
+very short
+.TE
+.sp
+.TS
+allbox tab(:);
+L
+L
+L L L.
+very short
+short:line
+left:middle:right
+.TE
--- /dev/null
+++ b/regress/tbl/layout/shortlines.out_ascii
@@ -0,0 +1,46 @@
+TBL-LAYOUT-SHORTLINES(1)    General Commands Manual   TBL-LAYOUT-SHORTLINES(1)
+
+
+
+NNAAMMEE
+       tbl-layout-shortlines - table lines of different length
+
+DDEESSCCRRIIPPTTIIOONN
+       normal text
+
+       +------+-------+
+       |left  | right |
+       +------+-------+
+       |short |       |
+       +------+-------+
+       |left  | right |
+       +------+-------+
+
+       +-------------+-------+
+       |left         | right |
+       +-------------+-------+
+       |first short  |       |
+       +-------------+-------+
+       |second short |       |
+       +-------------+-------+
+       |left         | right |
+       +-------------+-------+
+
+       +-----------+--------+-------+
+       |left       | middle | right |
+       +-----------+--------+-------+
+       |short      | line   |       |
+       +-----------+--------+-------+
+       |very short |        |       |
+       +-----------+--------+-------+
+
+       +-----------+--------+-------+
+       |very short |        |       |
+       +-----------+--------+-------+
+       |short      | line   |       |
+       +-----------+--------+-------+
+       |left       | middle | right |
+       +-----------+--------+-------+
+
+
+OpenBSD                        January 11, 2020       TBL-LAYOUT-SHORTLINES(1)
--- a/roff.7
+++ b/roff.7
@@ -315,12 +315,18 @@ delimiters
 The proper spacing is also intelligently preserved if a sentence ends at
 the boundary of a macro line.
 .Pp
+If an input line happens to end with a period, exclamation or question
+mark that isn't the end of a sentence, append a zero-width space
+.Pq Sq \e& .
+.Pp
 Examples:
 .Bd -literal -offset indent -compact
 Do not end sentences mid-line like this.  Instead,
 end a sentence like this.
 A macro would end like this:
 \&.Xr mandoc 1 \&.
+An abbreviation at the end of an input line needs escaping, e.g.\e&
+like this.
 .Ed
 .Sh REQUEST SYNTAX
 A request or macro line consists of:
@@ -503,10 +509,9 @@ This is a Heirloom extension and current
 .It Ic \&br
 Break the output line.
 .It Ic \&break
-Break out of a
+Break out of the innermost
 .Ic \&while
 loop.
-Currently unsupported.
 .It Ic \&breakchar Ar char ...
 Optional line break characters.
 This is a Heirloom extension and currently ignored.
@@ -2279,7 +2284,7 @@ code.
 .Pp
 The special semantics of the
 .Cm nS
-number register is an idiosyncracy of
+number register is an idiosyncrasy of
 .Ox
 manuals and not supported by other
 .Xr mdoc 7
--- a/roff.c
+++ b/roff.c
@@ -1,7 +1,7 @@
 /*	$Id: roff.c,v 1.363 2019/02/06 21:11:43 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2012, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2010-2015, 2017-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2010-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
@@ -133,15 +133,18 @@ struct	roff {
 	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; /* end-rules: custom token */
-	int		 endspan; /* end-rules: next-line or infty */
-	int		 rule; /* current evaluation rule */
+	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 */ \
@@ -181,6 +184,7 @@ 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);
@@ -351,7 +355,7 @@ const char *__roff_name[MAN_MAX + 1] = {
 	"Lk",		"Mt",		"Brq",		"Bro",
 	"Brc",		"%C",		"Es",		"En",
 	"Dx",		"%Q",		"%U",		"Ta",
-	NULL,
+	"Tg",		NULL,
 	"TH",		"SH",		"SS",		"TP",
 	"TQ",
 	"LP",		"PP",		"P",		"IP",
@@ -400,7 +404,7 @@ static	struct roffmac	 roffs[TOKEN_NONE]
 	{ roff_unsupp, NULL, NULL, 0 },  /* boxa */
 	{ roff_line_ignore, NULL, NULL, 0 },  /* bp */
 	{ roff_unsupp, NULL, NULL, 0 },  /* BP */
-	{ roff_unsupp, NULL, NULL, 0 },  /* break */
+	{ 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 */
@@ -685,7 +689,7 @@ roffhash_find(struct ohash *htab, const
 
 /*
  * Pop the current node off of the stack of roff instructions currently
- * pending.
+ * pending.  Return 1 if it is a loop or 0 otherwise.
  */
 static int
 roffnode_pop(struct roff *r)
@@ -767,6 +771,7 @@ 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 = '\\';
@@ -779,7 +784,7 @@ roff_reset(struct roff *r)
 void
 roff_free(struct roff *r)
 {
-	int	 	 i;
+	int		 i;
 
 	roff_free1(r);
 	for (i = 0; i < r->mstacksz; i++)
@@ -796,7 +801,7 @@ roff_alloc(int options)
 
 	r = mandoc_calloc(1, sizeof(struct roff));
 	r->reqtab = roffhash_alloc(0, ROFF_RENAMED);
-	r->options = options;
+	r->options = options | MPARSE_COMMENT;
 	r->format = options & (MPARSE_MDOC | MPARSE_MAN);
 	r->mstackpos = -1;
 	r->rstackpos = -1;
@@ -1242,7 +1247,7 @@ roff_expand(struct roff *r, struct buf *
 		 * in the syntax tree.
 		 */
 
-		if (newesc != ASCII_ESC && r->format == 0) {
+		if (newesc != ASCII_ESC && r->options & MPARSE_COMMENT) {
 			while (*ep == ' ' || *ep == '\t')
 				ep--;
 			ep[1] = '\0';
@@ -1586,7 +1591,7 @@ char *
 roff_getarg(struct roff *r, char **cpp, int ln, int *pos)
 {
 	struct buf	 buf;
-	char	 	*cp, *start;
+	char		*cp, *start;
 	int		 newesc, pairs, quoted, white;
 
 	/* Quoting can only start with a new word. */
@@ -1811,8 +1816,10 @@ roff_parseln(struct roff *r, int ln, str
 		roff_addtbl(r->man, ln, r->tbl);
 		return e;
 	}
-	if ( ! ctl)
+	if ( ! ctl) {
+		r->options &= ~MPARSE_COMMENT;
 		return roff_parsetext(r, buf, pos, offs) | e;
+	}
 
 	/* Skip empty request lines. */
 
@@ -1835,6 +1842,7 @@ roff_parseln(struct roff *r, int ln, str
 
 	/* 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);
 
@@ -2002,6 +2010,10 @@ roff_cblock(ROFF_ARGS)
 
 }
 
+/*
+ * 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)
 {
@@ -2016,6 +2028,11 @@ roffnode_cleanscope(struct roff *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)
 {
@@ -2235,6 +2252,7 @@ roff_block_text(ROFF_ARGS)
 static int
 roff_cond_sub(ROFF_ARGS)
 {
+	struct roffnode	*bl;
 	char		*ep;
 	int		 endloop, irc, rr;
 	enum roff_tok	 t;
@@ -2276,15 +2294,39 @@ roff_cond_sub(ROFF_ARGS)
 		}
 	}
 
+	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.
 	 */
 
-	t = roff_parse(r, buf->buf, &pos, ln, ppos);
-	irc |= t != TOKEN_NONE && (rr || roffs[t].flags & ROFFMAC_STRUCT) ?
-	    (*roffs[t].proc)(r, t, buf, ln, ppos, pos, offs) :
-	    rr ? ROFF_CONT : ROFF_IGN;
+	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;
 }
 
@@ -3482,6 +3524,17 @@ roff_als(ROFF_ARGS)
 	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)
 {
@@ -3804,6 +3857,11 @@ roff_userdef(ROFF_ARGS)
 	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) {
@@ -3851,7 +3909,7 @@ roff_userdef(ROFF_ARGS)
 	buf->sz = strlen(buf->buf) + 1;
 	*offs = 0;
 
-	return buf->sz > 1 && buf->buf[buf->sz - 2] == '\n' ?
+	return buf->buf[buf->sz - 2] == '\n' ?
 	    ROFF_REPARSE | ROFF_USERCALL : ROFF_IGN | ROFF_APPEND;
 }
 
--- a/roff.h
+++ b/roff.h
@@ -1,7 +1,7 @@
 /*	$Id: roff.h,v 1.69 2019/03/04 13:01:57 schwarze Exp $	*/
 /*
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2013-2015, 2017-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2013-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
@@ -437,6 +437,7 @@ enum	roff_tok {
 	MDOC__Q,
 	MDOC__U,
 	MDOC_Ta,
+	MDOC_Tg,
 	MDOC_MAX,
 	MAN_TH,
 	MAN_SH,
--- a/roff_html.c
+++ b/roff_html.c
@@ -94,7 +94,7 @@ roff_html_pre_ft(ROFF_HTML_ARGS)
 	const char	*cp;
 
 	cp = n->child->string;
-	print_metaf(h, mandoc_font(cp, (int)strlen(cp)));
+	html_setfont(h, mandoc_font(cp, (int)strlen(cp)));
 }
 
 static void
--- a/tag.c
+++ b/tag.c
@@ -1,6 +1,6 @@
 /*	$Id: tag.c,v 1.21 2018/11/22 11:30:23 schwarze Exp $ */
 /*
- * Copyright (c) 2015, 2016, 2018 Ingo Schwarze <schwarze@openbsd.org>
+ * 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
@@ -18,9 +18,8 @@
 
 #include <sys/types.h>
 
-#if HAVE_ERR
-#include <err.h>
-#endif
+#include <assert.h>
+#include <errno.h>
 #include <limits.h>
 #include <signal.h>
 #include <stddef.h>
@@ -32,6 +31,7 @@
 
 #include "mandoc_aux.h"
 #include "mandoc_ohash.h"
+#include "mandoc.h"
 #include "tag.h"
 
 struct tag_entry {
@@ -54,7 +54,7 @@ static struct tag_files	 tag_files;
  * but for simplicity, create it anyway.
  */
 struct tag_files *
-tag_init(void)
+tag_init(char *tagname)
 {
 	struct sigaction	 sa;
 	int			 ofd;
@@ -62,6 +62,7 @@ tag_init(void)
 	ofd = -1;
 	tag_files.tfd = -1;
 	tag_files.tcpgid = -1;
+	tag_files.tagname = tagname;
 
 	/* Clean up when dying from a signal. */
 
@@ -83,8 +84,10 @@ tag_init(void)
 
 	/* Save the original standard output for use by the pager. */
 
-	if ((tag_files.ofd = dup(STDOUT_FILENO)) == -1)
+	if ((tag_files.ofd = dup(STDOUT_FILENO)) == -1) {
+		mandoc_msg(MANDOCERR_DUP, 0, 0, "%s", strerror(errno));
 		goto fail;
+	}
 
 	/* Create both temporary output files. */
 
@@ -92,12 +95,20 @@ tag_init(void)
 	    sizeof(tag_files.ofn));
 	(void)strlcpy(tag_files.tfn, "/tmp/man.XXXXXXXXXX",
 	    sizeof(tag_files.tfn));
-	if ((ofd = mkstemp(tag_files.ofn)) == -1)
+	if ((ofd = mkstemp(tag_files.ofn)) == -1) {
+		mandoc_msg(MANDOCERR_MKSTEMP, 0, 0,
+		    "%s: %s", tag_files.ofn, strerror(errno));
 		goto fail;
-	if ((tag_files.tfd = mkstemp(tag_files.tfn)) == -1)
+	}
+	if ((tag_files.tfd = mkstemp(tag_files.tfn)) == -1) {
+		mandoc_msg(MANDOCERR_MKSTEMP, 0, 0,
+		    "%s: %s", tag_files.tfn, strerror(errno));
 		goto fail;
-	if (dup2(ofd, STDOUT_FILENO) == -1)
+	}
+	if (dup2(ofd, STDOUT_FILENO) == -1) {
+		mandoc_msg(MANDOCERR_DUP, 0, 0, "%s", strerror(errno));
 		goto fail;
+	}
 	close(ofd);
 
 	/*
@@ -120,6 +131,7 @@ fail:
 	*tag_files.tfn = '\0';
 	tag_files.ofd = -1;
 	tag_files.tfd = -1;
+	tag_files.tagname = NULL;
 	return NULL;
 }
 
@@ -135,6 +147,7 @@ tag_put(const char *s, int prio, size_t
 	size_t			 len;
 	unsigned int		 slot;
 
+	assert(prio <= TAG_FALLBACK);
 	if (tag_files.tfd <= 0)
 		return;
 
@@ -142,17 +155,17 @@ tag_put(const char *s, int prio, size_t
 		s += 2;
 
 	/*
-	 * Skip whitespace and whatever follows it,
+	 * Skip whitespace and escapes and whatever follows,
 	 * and if there is any, downgrade the priority.
 	 */
 
-	len = strcspn(s, " \t");
+	len = strcspn(s, " \t\\");
 	if (len == 0)
 		return;
 
 	se = s + len;
-	if (*se != '\0')
-		prio = INT_MAX;
+	if (*se != '\0' && prio < TAG_WEAK)
+		prio = TAG_WEAK;
 
 	slot = ohash_qlookupi(&tag_data, s, &se);
 	entry = ohash_find(&tag_data, slot);
@@ -172,25 +185,25 @@ tag_put(const char *s, int prio, size_t
 
 		/*
 		 * Lower priority numbers take precedence,
-		 * but 0 is special.
-		 * A tag with priority 0 is only used
+		 * but TAG_FALLBACK is special.
+		 * A tag with priority TAG_FALLBACK is only used
 		 * if the tag occurs exactly once.
 		 */
 
-		if (prio == 0) {
-			if (entry->prio == 0)
-				entry->prio = -1;
+		if (prio == TAG_FALLBACK) {
+			if (entry->prio == TAG_FALLBACK)
+				entry->prio = TAG_DELETE;
 			return;
 		}
 
 		/* A better entry is already present, ignore the new one. */
 
-		if (entry->prio > 0 && entry->prio < prio)
+		if (entry->prio < prio)
 			return;
 
 		/* The existing entry is worse, clear it. */
 
-		if (entry->prio < 1 || entry->prio > prio)
+		if (entry->prio > prio)
 			entry->nlines = 0;
 	}
 
@@ -216,21 +229,27 @@ tag_write(void)
 	struct tag_entry	*entry;
 	size_t			 i;
 	unsigned int		 slot;
+	int			 empty;
 
 	if (tag_files.tfd <= 0)
 		return;
 	if (tag_files.tagname != NULL && ohash_find(&tag_data,
             ohash_qlookup(&tag_data, tag_files.tagname)) == NULL) {
-		warnx("%s: no such tag", tag_files.tagname);
+		mandoc_msg(MANDOCERR_TAG, 0, 0, "%s", tag_files.tagname);
 		tag_files.tagname = NULL;
 	}
-	stream = fdopen(tag_files.tfd, "w");
+	if ((stream = fdopen(tag_files.tfd, "w")) == NULL)
+		mandoc_msg(MANDOCERR_FDOPEN, 0, 0, "%s", strerror(errno));
+	empty = 1;
 	entry = ohash_first(&tag_data, &slot);
 	while (entry != NULL) {
-		if (stream != NULL && entry->prio >= 0)
-			for (i = 0; i < entry->nlines; i++)
+		if (stream != NULL && entry->prio < TAG_DELETE) {
+			for (i = 0; i < entry->nlines; i++) {
 				fprintf(stream, "%s %s %zu\n",
 				    entry->s, tag_files.ofn, entry->lines[i]);
+				empty = 0;
+			}
+		}
 		free(entry->lines);
 		free(entry);
 		entry = ohash_next(&tag_data, &slot);
@@ -241,6 +260,10 @@ tag_write(void)
 	else
 		close(tag_files.tfd);
 	tag_files.tfd = -1;
+	if (empty) {
+		unlink(tag_files.tfn);
+		*tag_files.tfn = '\0';
+	}
 }
 
 void
--- a/tag.h
+++ b/tag.h
@@ -1,6 +1,6 @@
 /*      $Id: tag.h,v 1.8 2018/11/22 11:30:23 schwarze Exp $    */
 /*
- * Copyright (c) 2015 Ingo Schwarze <schwarze@openbsd.org>
+ * 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
@@ -15,6 +15,17 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+/*
+ * 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. */
+
+
 struct	tag_files {
 	char	 ofn[20];
 	char	 tfn[20];
@@ -26,7 +37,7 @@ struct	tag_files {
 };
 
 
-struct tag_files *tag_init(void);
+struct tag_files *tag_init(char *);
 void	 tag_put(const char *, int, size_t);
 void	 tag_write(void);
 void	 tag_unlink(void);
--- a/tbl_data.c
+++ b/tbl_data.c
@@ -21,6 +21,7 @@
 
 #include <assert.h>
 #include <ctype.h>
+#include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -73,6 +74,7 @@ getdata(struct tbl_node *tbl, struct tbl
 		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;
--- a/tbl_html.c
+++ b/tbl_html.c
@@ -25,6 +25,7 @@
 #include <string.h>
 
 #include "mandoc.h"
+#include "roff.h"
 #include "tbl.h"
 #include "out.h"
 #include "html.h"
--- a/tbl_term.c
+++ b/tbl_term.c
@@ -1,7 +1,7 @@
 /*	$Id: tbl_term.c,v 1.68 2019/02/09 21:02:47 schwarze Exp $ */
 /*
  * Copyright (c) 2009, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2011-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * 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
@@ -46,7 +46,8 @@ static	void	tbl_fill_border(struct termp
 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 *, int);
+			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 *,
@@ -163,7 +164,7 @@ term_tbl(struct termp *tp, const struct
 	const struct tbl_cell	*cp, *cpn, *cpp, *cps;
 	const struct tbl_dat	*dp;
 	static size_t		 offset;
-	size_t		 	 save_offset;
+	size_t			 save_offset;
 	size_t			 coloff, tsz;
 	int			 hspans, ic, more;
 	int			 dvert, fc, horiz, lhori, rhori, uvert;
@@ -222,9 +223,9 @@ term_tbl(struct termp *tp, const struct
 
 		if (tp->enc == TERMENC_ASCII &&
 		    sp->opts->opts & TBL_OPT_DBOX)
-			tbl_hrule(tp, NULL, sp, 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, TBL_OPT_BOX);
+			tbl_hrule(tp, NULL, sp, sp, TBL_OPT_BOX);
 	}
 
 	/* Set up the columns. */
@@ -266,11 +267,11 @@ term_tbl(struct termp *tp, const struct
 				hspans--;
 				continue;
 			}
-			if (dp == NULL)
-				continue;
-			hspans = dp->hspans;
-			if (ic || sp->layout->first->pos != TBL_CELL_SPAN)
+			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. */
@@ -301,11 +302,11 @@ term_tbl(struct termp *tp, const struct
 			tp->tcol++;
 			tp->col = 0;
 			tbl_data(tp, sp->opts, cp, dp, tp->tbl.cols + ic);
-			if (dp == NULL)
-				continue;
-			hspans = dp->hspans;
-			if (cp->pos != TBL_CELL_SPAN)
+			if (dp != NULL &&
+			    (ic || sp->layout->first->pos != TBL_CELL_SPAN)) {
+				hspans = dp->hspans;
 				dp = dp->next;
+			}
 		}
 		break;
 	}
@@ -342,7 +343,7 @@ term_tbl(struct termp *tp, const struct
 
 		more = 0;
 		if (horiz)
-			tbl_hrule(tp, sp->prev, sp, 0);
+			tbl_hrule(tp, sp->prev, sp, sp->next, 0);
 		else {
 			cp = sp->layout->first;
 			cpn = sp->next == NULL ? NULL :
@@ -424,11 +425,10 @@ term_tbl(struct termp *tp, const struct
 					cp = cp->next;
 					continue;
 				}
-				if (dp != NULL) {
+				if (dp != NULL && (ic ||
+				    sp->layout->first->pos != TBL_CELL_SPAN)) {
 					hspans = dp->hspans;
-					if (ic || sp->layout->first->pos
-					    != TBL_CELL_SPAN)
-						dp = dp->next;
+					dp = dp->next;
 				}
 
 				/*
@@ -557,12 +557,12 @@ term_tbl(struct termp *tp, const struct
 	tp->tcol->rmargin = tp->maxrmargin;
 	if (sp->next == NULL) {
 		if (sp->opts->opts & (TBL_OPT_DBOX | TBL_OPT_BOX)) {
-			tbl_hrule(tp, sp, NULL, 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, NULL, TBL_OPT_DBOX);
+			tbl_hrule(tp, sp, sp, NULL, TBL_OPT_DBOX);
 			tp->skipvsp = 2;
 		}
 		assert(tp->tbl.cols);
@@ -571,7 +571,7 @@ term_tbl(struct termp *tp, const struct
 	} 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->next, TBL_OPT_ALLBOX);
+		tbl_hrule(tp, sp, sp, sp->next, TBL_OPT_ALLBOX);
 
 	tp->tcol->offset = save_offset;
 	tp->flags &= ~TERMP_NONOSPACE;
@@ -579,9 +579,10 @@ term_tbl(struct termp *tp, const struct
 
 static void
 tbl_hrule(struct termp *tp, const struct tbl_span *spp,
-    const struct tbl_span *spn, int flags)
+    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. */
@@ -592,6 +593,7 @@ tbl_hrule(struct termp *tp, const struct
 	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) {
@@ -600,11 +602,11 @@ tbl_hrule(struct termp *tp, const struct
 		else if (spn->next != NULL)
 			dpn = spn->next->first;
 	}
-	opts = spn == NULL ? spp->opts->opts : spn->opts->opts;
+	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 :
-	    spn->pos == TBL_SPAN_DHORIZ ? 2 : 1;
+	    sp->pos == TBL_SPAN_DHORIZ ? 2 : 1;
 
 	/* Print the left end of the line. */
 
@@ -619,14 +621,19 @@ tbl_hrule(struct termp *tp, const struct
 		    (spp == NULL || cpn == NULL ||
 		     cpn->pos != TBL_CELL_DOWN ? BRIGHT * hw : 0), 1);
 
+	col = tp->tbl.cols;
 	for (;;) {
-		col = tp->tbl.cols + (cpn == NULL ? cpp->col : cpn->col);
+		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 || strcmp(dpn->string, "\\^") != 0))
+		     (dpn == NULL || dpn->string == NULL ||
+		      strcmp(dpn->string, "\\^") != 0))
 		    ? hw : 0;
 		tbl_direct_border(tp, BHORIZ * lw,
 		    col->width + col->spacing / 2);
@@ -645,7 +652,10 @@ tbl_hrule(struct termp *tp, const struct
 					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;
@@ -655,8 +665,9 @@ tbl_hrule(struct termp *tp, const struct
 			cpn = cpn->next;
 			while (dpn != NULL && dpn->layout != cpn)
 				dpn = dpn->next;
-		}
-		if (cpp == NULL && cpn == NULL)
+		} 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. */
@@ -670,7 +681,8 @@ tbl_hrule(struct termp *tp, const struct
 
 		rw = cpp == NULL || cpn == NULL ||
 		    (cpn->pos != TBL_CELL_DOWN &&
-		     (dpn == NULL || strcmp(dpn->string, "\\^") != 0))
+		     (dpn == NULL || dpn->string == NULL ||
+		      strcmp(dpn->string, "\\^") != 0))
 		    ? hw : 0;
 
 		/* The line crossing at the end of this column. */
--- a/term.c
+++ b/term.c
@@ -281,6 +281,8 @@ term_fill(struct termp *p, size_t *nbr,
 			case ASCII_BREAK:
 				vn = vis;
 				break;
+			default:
+				abort();
 			}
 			/* Can break at the end of a word. */
 			if (breakline || vn > vtarget)
--- a/tree.c
+++ b/tree.c
@@ -1,7 +1,7 @@
 /*	$Id: tree.c,v 1.84 2019/01/01 05:56:34 schwarze Exp $ */
 /*
  * Copyright (c) 2008, 2009, 2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
- * Copyright (c) 2013-2015, 2017-2019 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2013-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
@@ -34,6 +34,7 @@
 #include "main.h"
 
 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);
@@ -374,11 +375,72 @@ print_box(const struct eqn_box *ep, int
 }
 
 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(' ');
 
@@ -392,31 +454,52 @@ print_span(const struct tbl_span *sp, in
 		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('-');
-				putchar(' ');
-				continue;
+				break;
 			case TBL_DATA_DHORIZ:
 			case TBL_DATA_NDHORIZ:
 				putchar('=');
-				putchar(' ');
-				continue;
+				break;
 			default:
+				putchar(dp->block ? '{' : '[');
+				if (dp->string != NULL)
+					fputs(dp->string, stdout);
+				putchar(dp->block ? '}' : ']');
 				break;
 			}
-			printf("[\"%s\"", dp->string ? dp->string : "");
 			if (dp->hspans)
 				printf(">%d", dp->hspans);
 			if (dp->vspans)
 				printf("v%d", dp->vspans);
-			if (dp->layout == NULL)
-				putchar('*');
-			else if (dp->layout->pos == TBL_CELL_DOWN)
-				putchar('^');
-			putchar(']');
 			putchar(' ');
 		}
 		break;
openSUSE Build Service is sponsored by