diff --git a/1.14.5-master.patch b/1.14.5-master.patch new file mode 100644 index 0000000..9212767 --- /dev/null +++ b/1.14.5-master.patch @@ -0,0 +1,7696 @@ +--- 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 rather than
++ * 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 "wordword" Ta word Macro ++ * -T html: remove some spurious line breaks, in particular inside
++    --- 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  to the max-width given for .
+   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 
+- * Copyright (c) 2014, 2015, 2016, 2017, 2018 Ingo Schwarze 
++ * Copyright (c) 2014-2019 Ingo Schwarze 
+  *
+  * Permission to use, copy, modify, and distribute this software for any
+  * purpose with or without fee is hereby granted, provided that the above
+@@ -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("
\n" ++ printf("\n" + "
\n" + " Manual Page Search Parameters\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("

"); +- puts(msg); ++ puts(user_msg); + puts("

"); + 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 , ++# which may require this line: ++CC=gcc ++ ++# IBM AIX may need: ++CC=xlc ++ + # For -Tutf8 and -Tlocale operation, mandoc(1) requires + # providing setlocale(3) and 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 , +-# 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 +- * Copyright (c) 2014, 2015, 2017, 2018 Ingo Schwarze ++ * Copyright (c) 2014,2015,2017,2018,2020 Ingo Schwarze + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -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 + + #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, " "); + } + +- 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 +- * Copyright (c) 2013,2014,2015,2017,2018 Ingo Schwarze ++ * Copyright (c) 2013-2015,2017,2018,2020 Ingo Schwarze + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -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 +- * Copyright (c) 2010-2012, 2014-2019 Ingo Schwarze ++ * Copyright (c) 2010-2012, 2014-2020 Ingo Schwarze + * Copyright (c) 2010 Joerg Sonnenberger + * + * Permission to use, copy, modify, and distribute this software for any +@@ -21,6 +21,7 @@ + #include + #include + #include /* MACHINE */ ++#include + #include + + #include +@@ -31,6 +32,7 @@ + #include + #include + #include ++#include + #if HAVE_SANDBOX_INIT + #include + #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 = ""; +- 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 = ""; ++ ++ 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 +-.\" Copyright (c) 2010, 2011, 2014-2018 Ingo Schwarze ++.\" Copyright (c) 2010, 2011, 2014-2020 Ingo Schwarze + .\" + .\" Redistribution and use in source and binary forms, with or without + .\" modification, are permitted provided that the following conditions +@@ -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 +- * Copyright (c) 2010-2015, 2017-2019 Ingo Schwarze ++ * Copyright (c) 2010-2015, 2017-2020 Ingo Schwarze + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -27,10 +27,12 @@ + #include + + #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 +- * Copyright (c) 2010, 2012-2018 Ingo Schwarze ++ * Copyright (c) 2010, 2012-2020 Ingo Schwarze + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -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 +-.\" Copyright (c) 2012, 2014-2018 Ingo Schwarze ++.\" Copyright (c) 2012, 2014-2019 Ingo Schwarze + .\" + .\" Permission to use, copy, modify, and distribute this software for any + .\" purpose with or without fee is hereby granted, provided that the above +@@ -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 +- * Copyright (c) 2011-2015, 2017, 2018 Ingo Schwarze ++ * Copyright (c) 2011-2015, 2017-2020 Ingo Schwarze + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -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 +- * Copyright (c) 2012-2018 Ingo Schwarze ++ * Copyright (c) 2012-2020 Ingo Schwarze + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -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 ++.\" ++.\" 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 +- * Copyright (c) 2014,2015,2016,2017,2018 Ingo Schwarze ++ * Copyright (c) 2014-2020 Ingo Schwarze + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -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 +- * Copyright (c) 2011-2018 Ingo Schwarze ++ * Copyright (c) 2011-2020 Ingo Schwarze + * Copyright (c) 2016 Ed Maste + * + * 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 ++ * Copyright (c) 2011,2014,2015,2017-2019 Ingo Schwarze + * Copyright (c) 2011 Kristaps Dzonsons + * + * Permission to use, copy, modify, and distribute this software for any +@@ -21,20 +21,19 @@ + #include + + #include +-#if HAVE_ERR +-#include +-#endif ++#include + #include + #include + #include + #include + + #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 +-.\" Copyright (c) 2010, 2011, 2013-2018 Ingo Schwarze ++.\" Copyright (c) 2010, 2011, 2013-2020 Ingo Schwarze + .\" + .\" Permission to use, copy, modify, and distribute this software for any + .\" purpose with or without fee is hereby granted, provided that the above +@@ -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 +- * Copyright (c) 2012, 2014-2018 Ingo Schwarze ++ * Copyright (c) 2012, 2014-2019 Ingo Schwarze + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -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 +- * Copyright (c) 2014-2019 Ingo Schwarze ++ * Copyright (c) 2014-2020 Ingo Schwarze + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -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
) 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 
+- * Copyright (c) 2010, 2012-2019 Ingo Schwarze 
++ * Copyright (c) 2010, 2012-2020 Ingo Schwarze 
+  *
+  * Permission to use, copy, modify, and distribute this software for any
+  * purpose with or without fee is hereby granted, provided that the above
+@@ -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 
+- * Copyright (c) 2010, 2012-2019 Ingo Schwarze 
++ * Copyright (c) 2010, 2012-2020 Ingo Schwarze 
+  * Copyright (c) 2013 Franco Fichtner 
+  *
+  * 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 
+- * Copyright (c) 2010-2019 Ingo Schwarze 
++ * Copyright (c) 2010-2020 Ingo Schwarze 
+  * Copyright (c) 2010 Joerg Sonnenberger 
+  *
+  * 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 \& 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
+ """	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: >.<
+ just right: >+<
+ too long: >..<
+ too large: >..<
+ trailing garbage: ><
+ not unicode: >_.↑.⇑<
+-ENDTEST
+--- a/regress/char/unicode/latin1.out_html
++++ b/regress/char/unicode/latin1.out_html
+@@ -1,4 +1,3 @@
+-BEGINTEST
+ ¡¡	INVERTED EXCLAMATION MARK
+ ¢¢	CENT SIGN
+ ££	POUND SIGN
+@@ -93,4 +92,3 @@ BEGINTEST
+ ýý	LATIN SMALL LETTER Y WITH ACUTE
+ þþ	LATIN SMALL LETTER THORN
+ ÿÿ	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
+ ¯¯	MACRON
+-ENDTEST
+--- a/regress/char/unicode/named.out_html
++++ b/regress/char/unicode/named.out_html
+@@ -1,4 +1,3 @@
+-BEGINTEST
+ ıı	LATIN SMALL LETTER DOTLESS I
+ IJIJ	LATIN CAPITAL LIGATURE IJ
+ ijij	LATIN SMALL LIGATURE IJ
+@@ -167,4 +166,3 @@ BEGINTEST
+ ♦♦	BLACK DIAMOND SUIT
+ ⟨⟨	MATHEMATICAL LEFT ANGLE BRACKET
+ ⟩⟩	MATHEMATICAL RIGHT ANGLE BRACKET
+-ENDTEST
+--- a/regress/char/unicode/namediff.out_html
++++ b/regress/char/unicode/namediff.out_html
+@@ -1,4 +1,3 @@
+-BEGINTEST
+ ‾‾ OVERLINE
+ ℏℏℏ PLANCK CONSTANT OVER TWO PI
+ ↕↕ UP DOWN ARROW
+@@ -22,4 +21,3 @@ BEGINTEST
+ ⎫⎫ RIGHT CURLY BRACKET UPPER HOOK
+ ⎬⎬ RIGHT CURLY BRACKET MIDDLE PIECE
+ ⎭⎭ 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
+ ��	<control> NULL
+ ��	<control> START OF HEADING
+ ��	<control> BELL
+@@ -34,4 +33,3 @@ BEGINTEST
+ 􏿽	<Plane 16 Private Use, Last>
+ 􏿾	<undefined>
+ 􏿿	<undefined>
+-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 
+--- 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 
++inline [beta]
+ .EQ
+ delim offgamma
+ .EN
+-inline 
++inline [delta]
+ .EQ
+ delim onepsilon
+ .EN
+-inline 
++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  inline   inline  
+-     inline  final text
++     initial text  inline   inline [delta] 
++     inline  inline $eta$ inline  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 @@
+ unquoted
+-words:sincostan
+-seccsc
+-asinacosatanasec
+-acscsinh
+-coshtanhcotharc
+-maxminlimloglnexpReImandiffordetquoted
+-words:
+-sincos
+-tanseccscasinacosatanasecacscsinhcoshtanhcotharcmaxminlimloglnexpReImandiffordetfont
+-operations:
+-sinsinsuperstring:sinuscomposite
+-word:
+-tan=sin/
+-cos
++words:sincostanseccscasinacosatanasecacscsinhcoshtanhcotharcmaxminlimloglnexpReImandiffordetquoted
++words:sincostanseccscasinacosatanasecacscsinhcoshtanhcotharcmaxminlimloglnexpReImandiffordetfont
++operations:sinsinsuperstring:sinuscomposite
++word:tan=sin/cos
+--- a/regress/eqn/nullary/symbol.out_html
++++ b/regress/eqn/nullary/symbol.out_html
+@@ -1,6 +1,4 @@
+ unquoted
+ words:εquoted
+-words:
+-epsilonprimecomposite
+-word:
+-epsilon-prime
++words:epsilonprimecomposite
++word:epsilon-prime
+--- a/regress/man/HP/literal.out_html
++++ b/regress/man/HP/literal.out_html
+@@ -1,4 +1,3 @@
+-BEGINTEST before hanged paragraph
+ 

tag indented text

+

regular paragraph

+
+@@ -17,4 +16,3 @@ paragraph
+ 
+ regular text +
+-ENDTEST +--- a/regress/man/IP/literal.out_html ++++ b/regress/man/IP/literal.out_html +@@ -1,4 +1,3 @@ +-BEGINTEST before indentation +
+
tag
+
indented regular text
+@@ -27,7 +26,7 @@ regular text +
+

+-regular text ++

regular text

+
+ literal
+ text
+@@ -47,7 +46,7 @@ text
+ 
+

+-regular text ++

regular text

+
+
tag
+
indented regular text +@@ -65,4 +64,3 @@ paragraph +
+ regular text +
+-ENDTEST +--- a/regress/man/RS/literal.out_html ++++ b/regress/man/RS/literal.out_html +@@ -1,6 +1,5 @@ +-BEGINTEST +-
+-initial regular text ++
++ initial regular text

+
+ literal text
+ before display
+@@ -15,6 +14,5 @@ This is a very long line that would wrap
+ literal text
+ after display
+ 
+-final regular text +-
+-ENDTEST ++

final regular text ++
+--- a/regress/man/RS/paragraph.out_html ++++ b/regress/man/RS/paragraph.out_html +@@ -1,8 +1,6 @@ +-BEGINTEST before paragraph +

regular paragraph

+
indented paragraph +

nested paragraph

+
+ regular text after display +
+-ENDTEST +--- a/regress/man/SH/paragraph.out_html ++++ b/regress/man/SH/paragraph.out_html +@@ -1,10 +1,8 @@ +-BEGINTEST +
+
+

+-This text immediately follows a section header. ++

This text immediately follows a section header.

+

This is a paragraph.

+
+
+

+-ENDTEST +--- a/regress/man/SS/paragraph.out_html ++++ b/regress/man/SS/paragraph.out_html +@@ -1,11 +1,9 @@ +-BEGINTEST +
+

+-This text immediately follows a subsection header. ++

This text immediately follows a subsection header.

+

This is a paragraph.

+
+
+

+-ENDTEST +--- a/regress/man/SY/literal.out_html ++++ b/regress/man/SY/literal.out_html +@@ -1,6 +1,5 @@ +-BEGINTEST +-
+-initial regular text ++
++ initial regular text

+ + + +@@ -26,6 +25,5 @@ before display + literal text + after display + +-final regular text +-
+-ENDTEST ++

final regular text ++
+--- 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 +

+
tag
+
regular indented text
+@@ -24,4 +23,3 @@ paragraph + + regular text +
+-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 +--- a/regress/mdoc/Bd/nf.out_html ++++ b/regress/mdoc/Bd/nf.out_html +@@ -1,9 +1,8 @@ +-BEGINTEST initial text +
+ after .nf
+ request
+ 
+-after .fi request ++

after .fi request

+
+
+ in unfilled
+@@ -19,4 +18,3 @@ in filled block
+ 
+ after end of filled block +
+-ENDTEST +--- a/regress/mdoc/Bd/paragraph.out_html ++++ b/regress/mdoc/Bd/paragraph.out_html +@@ -1,4 +1,3 @@ +-BEGINTEST initial text +

normal paragraph

+
filled display +

paragraph in display

+@@ -16,4 +15,3 @@ paragraph +
+ again back to normal +
+-ENDTEST +--- a/regress/mdoc/Bf/paragraph.out_html ++++ b/regress/mdoc/Bf/paragraph.out_html +@@ -1,6 +1,4 @@ +-BEGINTEST +

normal text

+
literal text +

literal paragraph

+
+-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 +

preceding paragraph

+
spacing in and around one-line displays
+ empty display: +
+ following text +
+-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 +--- 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 \*‌* ++**(\*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 +-
+-initial reference: author name, +- book title. ++
++ initial reference: author name, ++ book title.

+

in a paragraph: another + author, another book.

+ +
+

+-initial reference: ++

initial reference:

+

author name, + book title.

+

in a paragraph:

+

another author, + another book.

+
+-ENDTEST
+--- a/regress/mdoc/Sh/paragraph.out_html
++++ b/regress/mdoc/Sh/paragraph.out_html
+@@ -1,11 +1,9 @@
+-BEGINTEST
+ 

descriptive text

+
+

+-initial subsection text ++

initial subsection text

+

subsection paragraph

+
+
+
+

+-ENDTEST +--- a/regress/regress.pl ++++ b/regress/regress.pl +@@ -86,8 +86,12 @@ sub syshtml ($@) { + if (!$state && s/.*//) { + $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: bolditalicbolditalicroman + letters: bolditalicbackbolditalicroman + multiletter: boldemptyitalicbackbolditalicroman + typewriter: romanboldromanitalicroman +-ENDTEST +--- a/regress/roff/ft/badargs.out_html ++++ b/regress/roff/ft/badargs.out_html +@@ -1,9 +1,6 @@ +-BEGINTEST +-
+-default font italic bold italic +- typeqriter +- roman bold +- italic bold still bold +- italic back to bold back to italic +-
+-ENDTEST ++
++ default font italic bold italic ++ typeqriter roman bold ++ italic bold still bold italic back to ++ bold back to italic ++
+--- a/regress/roff/sp/fill-man.out_html ++++ b/regress/roff/sp/fill-man.out_html +@@ -1,4 +1,3 @@ +-BEGINTEST in fill mode: +

switch to no-fill mode:

+
+ in no-fill mode:
+@@ -6,4 +5,3 @@ in no-fill mode:
+ back to
+ fill mode:
+ 
+-ENDTEST +--- a/regress/roff/string/dotT.out_html ++++ b/regress/roff/string/dotT.out_html +@@ -1,5 +1,3 @@ +-BEGINTEST +

We are using the html device.

+

The device name can be overridden.

+
+-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 
++
++
++
++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 
+- * Copyright (c) 2010-2015, 2017-2019 Ingo Schwarze 
++ * Copyright (c) 2010-2015, 2017-2020 Ingo Schwarze 
+  *
+  * Permission to use, copy, modify, and distribute this software for any
+  * purpose with or without fee is hereby granted, provided that the above
+@@ -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 
+- * Copyright (c) 2013-2015, 2017-2019 Ingo Schwarze 
++ * Copyright (c) 2013-2015, 2017-2020 Ingo Schwarze 
+  *
+  * Permission to use, copy, modify, and distribute this software for any
+  * purpose with or without fee is hereby granted, provided that the above
+@@ -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 
++ * Copyright (c) 2015,2016,2018,2019,2020 Ingo Schwarze 
+  *
+  * Permission to use, copy, modify, and distribute this software for any
+  * purpose with or without fee is hereby granted, provided that the above
+@@ -18,9 +18,8 @@
+ 
+ #include 
+ 
+-#if HAVE_ERR
+-#include 
+-#endif
++#include 
++#include 
+ #include 
+ #include 
+ #include 
+@@ -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 
++ * Copyright (c) 2015, 2018, 2019, 2020 Ingo Schwarze 
+  *
+  * Permission to use, copy, modify, and distribute this software for any
+  * purpose with or without fee is hereby granted, provided that the above
+@@ -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 
+ #include 
++#include 
+ #include 
+ #include 
+ #include 
+@@ -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 
+ 
+ #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 
+- * Copyright (c) 2011-2019 Ingo Schwarze 
++ * Copyright (c) 2011-2020 Ingo Schwarze 
+  *
+  * Permission to use, copy, modify, and distribute this software for any
+  * purpose with or without fee is hereby granted, provided that the above
+@@ -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 
+- * Copyright (c) 2013-2015, 2017-2019 Ingo Schwarze 
++ * Copyright (c) 2013-2015, 2017-2020 Ingo Schwarze 
+  *
+  * Permission to use, copy, modify, and distribute this software for any
+  * purpose with or without fee is hereby granted, provided that the above
+@@ -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;
diff --git a/mandoc.changes b/mandoc.changes
index 5373059..9f2c3f2 100644
--- a/mandoc.changes
+++ b/mandoc.changes
@@ -1,3 +1,10 @@
+-------------------------------------------------------------------
+Mon Feb 10 18:46:58 CET 2020 - Matej Cepl 
+
+- Add 1.14.5-master.patch containing among many other things fix
+  allowing running ``man -w`` to get manpath.
+  (gh#neovim/neovim#11794)
+
 -------------------------------------------------------------------
 Fri Jan 31 21:50:30 UTC 2020 - Matej Cepl 
 
diff --git a/mandoc.spec b/mandoc.spec
index 105536f..eb77bd4 100644
--- a/mandoc.spec
+++ b/mandoc.spec
@@ -24,6 +24,9 @@ License:        ISC
 Group:          Productivity/Publishing/Troff
 URL:            http://mandoc.bsd.lv/
 Source:         http://mandoc.bsd.lv/snapshots/mandoc-%{version}.tar.gz
+# PATCH-FEATURE-UPSTREAM empty_w-manpath.patch gh#neovim/neovim#11794 mcepl@suse.com
+# Add man -w producing manpath (among many other things)
+Patch0:         1.14.5-master.patch
 BuildRequires:  zlib-devel
 Provides:       man = %{version}
 Conflicts:      man
@@ -42,6 +45,7 @@ For general information, see .
 
 %prep
 %setup -q
+%autopatch -p1
 
 %build
 %{?!make_build:%define make_build make %{?_smp_mflags} V=1 VERBOSE=1}
command