From 1481c0648531c03ad8b53843797e3c269032f43c Mon Sep 17 00:00:00 2001
From: Ingo Schwarze <schwarze@openbsd.org>
Date: Fri, 5 May 2017 02:06:19 +0000
Subject: [PATCH] Move handling of the roff(7) .ft request from the man(7)
 modules to the new roff(7) modules.  As a side effect, mdoc(7) now handles
 .ft, too.  Of course, do not use that.

---
 Makefile                               |  4 +-
 Makefile.depend                        |  3 +
 man_html.c                             |  3 +-
 man_macro.c                            |  5 +-
 man_term.c                             | 39 +----------
 man_validate.c                         | 46 +------------
 mandoc_headers.3                       |  3 +-
 mdoc_html.c                            |  3 +-
 mdoc_man.c                             | 23 +++++--
 mdoc_markdown.c                        |  7 +-
 mdoc_term.c                            |  3 +-
 mdoc_validate.c                        |  5 +-
 regress/roff/ft/Makefile               |  6 +-
 regress/roff/ft/badargs-mdoc.in        | 18 ++++++
 regress/roff/ft/badargs-mdoc.out_ascii |  9 +++
 regress/roff/ft/badargs-mdoc.out_lint  |  2 +
 roff.c                                 | 36 ++++++++++-
 roff.h                                 |  6 +-
 roff_html.c                            |  5 +-
 roff_term.c                            | 30 ++++++++-
 roff_validate.c                        | 89 ++++++++++++++++++++++++++
 21 files changed, 235 insertions(+), 110 deletions(-)
 create mode 100644 regress/roff/ft/badargs-mdoc.in
 create mode 100644 regress/roff/ft/badargs-mdoc.out_ascii
 create mode 100644 regress/roff/ft/badargs-mdoc.out_lint
 create mode 100644 roff_validate.c

diff --git a/Makefile b/Makefile
index decfe75f..d2b9a8b5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-# $Id: Makefile,v 1.510 2017/05/04 22:16:09 schwarze Exp $
+# $Id: Makefile,v 1.511 2017/05/05 02:06:19 schwarze Exp $
 #
 # Copyright (c) 2010, 2011, 2012 Kristaps Dzonsons <kristaps@bsd.lv>
 # Copyright (c) 2011, 2013-2017 Ingo Schwarze <schwarze@openbsd.org>
@@ -112,6 +112,7 @@ SRCS		 = att.c \
 		   roff.c \
 		   roff_html.c \
 		   roff_term.c \
+		   roff_validate.c \
 		   soelim.c \
 		   st.c \
 		   tag.c \
@@ -212,6 +213,7 @@ LIBMDOC_OBJS	 = att.o \
 
 LIBROFF_OBJS	 = eqn.o \
 		   roff.o \
+		   roff_validate.o \
 		   tbl.o \
 		   tbl_data.o \
 		   tbl_layout.o \
diff --git a/Makefile.depend b/Makefile.depend
index b8e5363b..b75befd8 100644
--- a/Makefile.depend
+++ b/Makefile.depend
@@ -57,6 +57,9 @@ out.o: out.c config.h mandoc_aux.h mandoc.h out.h
 preconv.o: preconv.c config.h mandoc.h libmandoc.h
 read.o: read.c config.h mandoc_aux.h mandoc.h roff.h mdoc.h man.h libmandoc.h roff_int.h
 roff.o: roff.c config.h mandoc.h mandoc_aux.h mandoc_ohash.h compat_ohash.h roff.h libmandoc.h roff_int.h libroff.h predefs.in
+roff_html.o: roff_html.c roff.h out.h html.h
+roff_term.o: roff_term.c roff.h out.h term.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 roff.h mdoc.h libmdoc.h st.in
 tag.o: tag.c config.h mandoc_aux.h mandoc_ohash.h compat_ohash.h tag.h
diff --git a/man_html.c b/man_html.c
index d9fc43f2..e895698f 100644
--- a/man_html.c
+++ b/man_html.c
@@ -1,4 +1,4 @@
-/*	$Id: man_html.c,v 1.138 2017/05/04 22:16:09 schwarze Exp $ */
+/*	$Id: man_html.c,v 1.139 2017/05/05 02:06:19 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2012, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
  * Copyright (c) 2013, 2014, 2015, 2017 Ingo Schwarze <schwarze@openbsd.org>
@@ -102,7 +102,6 @@ static	const struct htmlman __mans[MAN_MAX - MAN_TH] = {
 	{ man_ign_pre, NULL }, /* PD */
 	{ man_ign_pre, NULL }, /* AT */
 	{ man_in_pre, NULL }, /* in */
-	{ man_ign_pre, NULL }, /* ft */
 	{ man_OP_pre, NULL }, /* OP */
 	{ NULL, NULL }, /* EX */
 	{ NULL, NULL }, /* EE */
diff --git a/man_macro.c b/man_macro.c
index 7d3a8d71..a2c3f68d 100644
--- a/man_macro.c
+++ b/man_macro.c
@@ -1,4 +1,4 @@
-/*	$Id: man_macro.c,v 1.117 2017/05/04 17:48:28 schwarze Exp $ */
+/*	$Id: man_macro.c,v 1.118 2017/05/05 02:06:19 schwarze Exp $ */
 /*
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
  * Copyright (c) 2012-2015, 2017 Ingo Schwarze <schwarze@openbsd.org>
@@ -71,7 +71,6 @@ const	struct man_macro __man_macros[MAN_MAX - MAN_TH] = {
 	{ in_line_eoln, MAN_NSCOPED }, /* PD */
 	{ in_line_eoln, 0 }, /* AT */
 	{ in_line_eoln, 0 }, /* in */
-	{ in_line_eoln, 0 }, /* ft */
 	{ in_line_eoln, 0 }, /* OP */
 	{ in_line_eoln, MAN_BSCOPE }, /* EX */
 	{ in_line_eoln, MAN_BSCOPE }, /* EE */
@@ -334,7 +333,7 @@ in_line_eoln(MACRO_PROT_ARGS)
 			break;
 		}
 		if (buf[*pos] != '\0' && man->last != n &&
-		    (tok == MAN_PD || tok == MAN_ft || tok == MAN_sp)) {
+		    (tok == MAN_PD || tok == MAN_sp)) {
 			mandoc_vmsg(MANDOCERR_ARG_EXCESS,
 			    man->parse, line, *pos, "%s ... %s",
 			    roff_name[tok], buf + *pos);
diff --git a/man_term.c b/man_term.c
index d6ddf762..ac04dd6f 100644
--- a/man_term.c
+++ b/man_term.c
@@ -1,4 +1,4 @@
-/*	$Id: man_term.c,v 1.194 2017/05/04 22:16:09 schwarze Exp $ */
+/*	$Id: man_term.c,v 1.195 2017/05/05 02:06:19 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
  * Copyright (c) 2010-2015, 2017 Ingo Schwarze <schwarze@openbsd.org>
@@ -80,7 +80,6 @@ static	int		  pre_SS(DECL_ARGS);
 static	int		  pre_TP(DECL_ARGS);
 static	int		  pre_UR(DECL_ARGS);
 static	int		  pre_alternate(DECL_ARGS);
-static	int		  pre_ft(DECL_ARGS);
 static	int		  pre_ign(DECL_ARGS);
 static	int		  pre_in(DECL_ARGS);
 static	int		  pre_literal(DECL_ARGS);
@@ -126,7 +125,6 @@ static	const struct termact __termacts[MAN_MAX - MAN_TH] = {
 	{ pre_PD, NULL, MAN_NOTEXT }, /* PD */
 	{ pre_ign, NULL, 0 }, /* AT */
 	{ pre_in, NULL, MAN_NOTEXT }, /* in */
-	{ pre_ft, NULL, MAN_NOTEXT }, /* ft */
 	{ pre_OP, NULL, 0 }, /* OP */
 	{ pre_literal, NULL, 0 }, /* EX */
 	{ pre_literal, NULL, 0 }, /* EE */
@@ -361,41 +359,6 @@ pre_OP(DECL_ARGS)
 	return 0;
 }
 
-static int
-pre_ft(DECL_ARGS)
-{
-	const char	*cp;
-
-	if (NULL == n->child) {
-		term_fontlast(p);
-		return 0;
-	}
-
-	cp = n->child->string;
-	switch (*cp) {
-	case '4':
-	case '3':
-	case 'B':
-		term_fontrepl(p, TERMFONT_BOLD);
-		break;
-	case '2':
-	case 'I':
-		term_fontrepl(p, TERMFONT_UNDER);
-		break;
-	case 'P':
-		term_fontlast(p);
-		break;
-	case '1':
-	case 'C':
-	case 'R':
-		term_fontrepl(p, TERMFONT_NONE);
-		break;
-	default:
-		break;
-	}
-	return 0;
-}
-
 static int
 pre_in(DECL_ARGS)
 {
diff --git a/man_validate.c b/man_validate.c
index c038b887..812e7743 100644
--- a/man_validate.c
+++ b/man_validate.c
@@ -48,7 +48,6 @@ static	void	  check_text(CHKARGS);
 static	void	  post_AT(CHKARGS);
 static	void	  post_IP(CHKARGS);
 static	void	  post_vs(CHKARGS);
-static	void	  post_ft(CHKARGS);
 static	void	  post_OP(CHKARGS);
 static	void	  post_TH(CHKARGS);
 static	void	  post_UC(CHKARGS);
@@ -85,7 +84,6 @@ static	const v_check __man_valids[MAN_MAX - MAN_TH] = {
 	NULL,       /* PD */
 	post_AT,    /* AT */
 	NULL,       /* in */
-	post_ft,    /* ft */
 	post_OP,    /* OP */
 	NULL,       /* EX */
 	NULL,       /* EE */
@@ -131,7 +129,8 @@ man_node_validate(struct roff_man *man)
 				post_vs(man, n);
 				break;
 			default:
-				abort();
+				roff_validate(man);
+				break;
 			}
 			break;
 		}
@@ -211,47 +210,6 @@ post_UR(CHKARGS)
 	check_part(man, n);
 }
 
-static void
-post_ft(CHKARGS)
-{
-	char	*cp;
-	int	 ok;
-
-	if (n->child == NULL)
-		return;
-
-	ok = 0;
-	cp = n->child->string;
-	switch (*cp) {
-	case '1':
-	case '2':
-	case '3':
-	case '4':
-	case 'I':
-	case 'P':
-	case 'R':
-		if ('\0' == cp[1])
-			ok = 1;
-		break;
-	case 'B':
-		if ('\0' == cp[1] || ('I' == cp[1] && '\0' == cp[2]))
-			ok = 1;
-		break;
-	case 'C':
-		if ('W' == cp[1] && '\0' == cp[2])
-			ok = 1;
-		break;
-	default:
-		break;
-	}
-
-	if (0 == ok) {
-		mandoc_vmsg(MANDOCERR_FT_BAD, man->parse,
-		    n->line, n->pos, "ft %s", cp);
-		*cp = '\0';
-	}
-}
-
 static void
 check_part(CHKARGS)
 {
diff --git a/mandoc_headers.3 b/mandoc_headers.3
index ecb730f2..f061332c 100644
--- a/mandoc_headers.3
+++ b/mandoc_headers.3
@@ -144,8 +144,9 @@ and the functions
 .Fn deroff ,
 .Fn roffhash_alloc ,
 .Fn roffhash_find ,
+.Fn roffhash_free ,
 and
-.Fn roffhash_free .
+.Fn roff_validate .
 .Pp
 Uses pointers to the types
 .Vt struct mdoc_arg
diff --git a/mdoc_html.c b/mdoc_html.c
index 38660bac..48e3b81c 100644
--- a/mdoc_html.c
+++ b/mdoc_html.c
@@ -1,4 +1,4 @@
-/*	$Id: mdoc_html.c,v 1.282 2017/05/04 22:16:09 schwarze Exp $ */
+/*	$Id: mdoc_html.c,v 1.283 2017/05/05 02:06:19 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
  * Copyright (c) 2014, 2015, 2016, 2017 Ingo Schwarze <schwarze@openbsd.org>
@@ -395,6 +395,7 @@ print_mdoc_node(MDOC_ARGS)
 		assert(h->tblt == NULL);
 		if (n->tok < ROFF_MAX) {
 			roff_html_pre(h, n);
+			child = 0;
 			break;
 		}
 		assert(n->tok >= MDOC_Dd && n->tok < MDOC_MAX);
diff --git a/mdoc_man.c b/mdoc_man.c
index 38fb57e6..20796006 100644
--- a/mdoc_man.c
+++ b/mdoc_man.c
@@ -1,4 +1,4 @@
-/*	$Id: mdoc_man.c,v 1.110 2017/05/04 17:48:29 schwarze Exp $ */
+/*	$Id: mdoc_man.c,v 1.111 2017/05/05 02:06:19 schwarze Exp $ */
 /*
  * Copyright (c) 2011-2017 Ingo Schwarze <schwarze@openbsd.org>
  *
@@ -94,6 +94,7 @@ static	int	  pre_fl(DECL_ARGS);
 static	int	  pre_fn(DECL_ARGS);
 static	int	  pre_fo(DECL_ARGS);
 static	int	  pre_ft(DECL_ARGS);
+static	int	  pre_Ft(DECL_ARGS);
 static	int	  pre_in(DECL_ARGS);
 static	int	  pre_it(DECL_ARGS);
 static	int	  pre_lk(DECL_ARGS);
@@ -148,14 +149,14 @@ static	const struct manact __manacts[MDOC_MAX - MDOC_Dd] = {
 	{ NULL, pre_fd, post_fd, NULL, NULL }, /* Fd */
 	{ NULL, pre_fl, post_fl, NULL, NULL }, /* Fl */
 	{ NULL, pre_fn, post_fn, NULL, NULL }, /* Fn */
-	{ NULL, pre_ft, post_font, NULL, NULL }, /* Ft */
+	{ NULL, pre_Ft, post_font, NULL, NULL }, /* Ft */
 	{ NULL, pre_sy, post_font, NULL, NULL }, /* Ic */
 	{ NULL, pre_in, post_in, NULL, NULL }, /* In */
 	{ NULL, pre_li, post_font, NULL, NULL }, /* Li */
 	{ cond_head, pre_enc, NULL, "\\- ", NULL }, /* Nd */
 	{ NULL, pre_nm, post_nm, NULL, NULL }, /* Nm */
 	{ cond_body, pre_enc, post_enc, "[", "]" }, /* Op */
-	{ NULL, pre_ft, post_font, NULL, NULL }, /* Ot */
+	{ NULL, pre_Ft, post_font, NULL, NULL }, /* Ot */
 	{ NULL, pre_em, post_font, NULL, NULL }, /* Pa */
 	{ NULL, pre_ex, NULL, NULL, NULL }, /* Rv */
 	{ NULL, NULL, NULL, NULL, NULL }, /* St */
@@ -654,7 +655,10 @@ print_node(DECL_ARGS)
 	} else if (n->tok < ROFF_MAX) {
 		switch (n->tok) {
 		case ROFF_br:
-			pre_br(meta, n);
+			do_sub = pre_br(meta, n);
+			break;
+		case ROFF_ft:
+			do_sub = pre_ft(meta, n);
 			break;
 		default:
 			abort();
@@ -1314,7 +1318,7 @@ post_fo(DECL_ARGS)
 }
 
 static int
-pre_ft(DECL_ARGS)
+pre_Ft(DECL_ARGS)
 {
 
 	pre_syn(n);
@@ -1322,6 +1326,15 @@ pre_ft(DECL_ARGS)
 	return 1;
 }
 
+static int
+pre_ft(DECL_ARGS)
+{
+	print_line(".ft", 0);
+	print_word(n->child->string);
+	outflags |= MMAN_nl;
+	return 0;
+}
+
 static int
 pre_in(DECL_ARGS)
 {
diff --git a/mdoc_markdown.c b/mdoc_markdown.c
index 5d80cfe3..15430c44 100644
--- a/mdoc_markdown.c
+++ b/mdoc_markdown.c
@@ -1,4 +1,4 @@
-/*	$Id: mdoc_markdown.c,v 1.18 2017/05/04 17:48:29 schwarze Exp $ */
+/*	$Id: mdoc_markdown.c,v 1.19 2017/05/05 02:06:19 schwarze Exp $ */
 /*
  * Copyright (c) 2017 Ingo Schwarze <schwarze@openbsd.org>
  *
@@ -323,7 +323,10 @@ md_node(struct roff_node *n)
 	} else if (n->tok < ROFF_MAX) {
 		switch (n->tok) {
 		case ROFF_br:
-			md_pre_br(n);
+			process_children = md_pre_br(n);
+			break;
+		case ROFF_ft:
+			process_children = 0;
 			break;
 		default:
 			abort();
diff --git a/mdoc_term.c b/mdoc_term.c
index 327ecea3..4718448e 100644
--- a/mdoc_term.c
+++ b/mdoc_term.c
@@ -1,4 +1,4 @@
-/*	$Id: mdoc_term.c,v 1.352 2017/05/04 22:16:09 schwarze Exp $ */
+/*	$Id: mdoc_term.c,v 1.353 2017/05/05 02:06:19 schwarze Exp $ */
 /*
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
  * Copyright (c) 2010, 2012-2017 Ingo Schwarze <schwarze@openbsd.org>
@@ -366,6 +366,7 @@ print_mdoc_node(DECL_ARGS)
 	default:
 		if (n->tok < ROFF_MAX) {
 			roff_term_pre(p, n);
+			chld = 0;
 			break;
 		}
 		assert(n->tok >= MDOC_Dd && n->tok < MDOC_MAX);
diff --git a/mdoc_validate.c b/mdoc_validate.c
index 75ec2e35..f57c4e54 100644
--- a/mdoc_validate.c
+++ b/mdoc_validate.c
@@ -1,4 +1,4 @@
-/*	$Id: mdoc_validate.c,v 1.323 2017/05/04 17:48:29 schwarze Exp $ */
+/*	$Id: mdoc_validate.c,v 1.324 2017/05/05 02:06:19 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
  * Copyright (c) 2010-2017 Ingo Schwarze <schwarze@openbsd.org>
@@ -332,7 +332,8 @@ mdoc_node_validate(struct roff_man *mdoc)
 				post_par(mdoc);
 				break;
 			default:
-				abort();
+				roff_validate(mdoc);
+				break;
 			}
 			break;
 		}
diff --git a/regress/roff/ft/Makefile b/regress/roff/ft/Makefile
index f57fe72b..5d2accbb 100644
--- a/regress/roff/ft/Makefile
+++ b/regress/roff/ft/Makefile
@@ -1,6 +1,8 @@
 # $OpenBSD: Makefile,v 1.1 2014/07/05 12:33:54 schwarze Exp $
 
-REGRESS_TARGETS	= badargs
-LINT_TARGETS	= badargs
+REGRESS_TARGETS	= badargs badargs-mdoc
+LINT_TARGETS	= badargs badargs-mdoc
+
+SKIP_TMAN	= badargs
 
 .include <bsd.regress.mk>
diff --git a/regress/roff/ft/badargs-mdoc.in b/regress/roff/ft/badargs-mdoc.in
new file mode 100644
index 00000000..06f463b5
--- /dev/null
+++ b/regress/roff/ft/badargs-mdoc.in
@@ -0,0 +1,18 @@
+.Dd May 5, 2017
+.Dt FT-BADARGS-MDOC 1
+.Os OpenBSD
+.Sh NAME
+.Nm ft-badargs-mdoc
+.Nd font request with bad arguments
+.Sh DESCRIPTION
+default font
+.ft B
+bold
+.ft foo
+still bold
+.ft I bogus
+italic
+.ft P
+back to bold
+.ft
+back to italic
diff --git a/regress/roff/ft/badargs-mdoc.out_ascii b/regress/roff/ft/badargs-mdoc.out_ascii
new file mode 100644
index 00000000..bd4eaa09
--- /dev/null
+++ b/regress/roff/ft/badargs-mdoc.out_ascii
@@ -0,0 +1,9 @@
+FT-BADARGS-MDOC(1)          General Commands Manual         FT-BADARGS-MDOC(1)
+
+NNAAMMEE
+     fftt--bbaaddaarrggss--mmddoocc - font request with bad arguments
+
+DDEESSCCRRIIPPTTIIOONN
+     default font bboolldd ssttiillll bboolldd _i_t_a_l_i_c bbaacckk ttoo bboolldd _b_a_c_k _t_o _i_t_a_l_i_c
+
+OpenBSD                           May 5, 2017                          OpenBSD
diff --git a/regress/roff/ft/badargs-mdoc.out_lint b/regress/roff/ft/badargs-mdoc.out_lint
new file mode 100644
index 00000000..d5f526e6
--- /dev/null
+++ b/regress/roff/ft/badargs-mdoc.out_lint
@@ -0,0 +1,2 @@
+mandoc: badargs-mdoc.in:13:7: ERROR: skipping excess arguments: ft ... bogus
+mandoc: badargs-mdoc.in:11:2: WARNING: unknown font, skipping request: ft foo
diff --git a/roff.c b/roff.c
index ba7aae26..654ee90e 100644
--- a/roff.c
+++ b/roff.c
@@ -1,4 +1,4 @@
-/*	$Id: roff.c,v 1.296 2017/05/04 17:48:29 schwarze Exp $ */
+/*	$Id: roff.c,v 1.297 2017/05/05 02:06:19 schwarze Exp $ */
 /*
  * Copyright (c) 2008-2012, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
  * Copyright (c) 2010-2015, 2017 Ingo Schwarze <schwarze@openbsd.org>
@@ -182,6 +182,7 @@ static	enum rofferr	 roff_line_ignore(ROFF_ARGS);
 static	void		 roff_man_alloc1(struct roff_man *);
 static	void		 roff_man_free1(struct roff_man *);
 static	enum rofferr	 roff_nr(ROFF_ARGS);
+static	enum rofferr	 roff_onearg(ROFF_ARGS);
 static	enum roff_tok	 roff_parse(struct roff *, char *, int *,
 				int, int);
 static	enum rofferr	 roff_parsetext(struct buf *, int, int *);
@@ -210,7 +211,7 @@ static	enum rofferr	 roff_userdef(ROFF_ARGS);
 #define	ROFFNUM_WHITE	(1 << 1)  /* Skip whitespace in roff_evalnum(). */
 
 const char *__roff_name[MAN_MAX + 1] = {
-	"br",		NULL,
+	"br",		"ft",		NULL,
 	"ab",		"ad",		"af",		"aln",
 	"als",		"am",		"am1",		"ami",
 	"ami1",		"as",		"as1",		"asciify",
@@ -309,7 +310,7 @@ const char *__roff_name[MAN_MAX + 1] = {
 	"B",		"I",		"IR",		"RI",
 	"sp",		"nf",		"fi",
 	"RE",		"RS",		"DT",		"UC",
-	"PD",		"AT",		"in",		"ft",
+	"PD",		"AT",		"in",
 	"OP",		"EX",		"EE",		"UR",
 	"UE",		"ll",		NULL
 };
@@ -317,6 +318,7 @@ const	char *const *roff_name = __roff_name;
 
 static	struct roffmac	 roffs[TOKEN_NONE] = {
 	{ roff_br, NULL, NULL, 0 },  /* br */
+	{ roff_onearg, NULL, NULL, 0 },  /* ft */
 	{ NULL, NULL, NULL, 0 },  /* ROFF_MAX */
 	{ roff_unsupp, NULL, NULL, 0 },  /* ab */
 	{ roff_line_ignore, NULL, NULL, 0 },  /* ad */
@@ -2766,6 +2768,34 @@ roff_TS(ROFF_ARGS)
 	return ROFF_IGN;
 }
 
+static enum rofferr
+roff_onearg(ROFF_ARGS)
+{
+	struct roff_node	*n;
+	char			*cp;
+
+	roff_elem_alloc(r->man, ln, ppos, tok);
+	n = r->man->last;
+
+	cp = buf->buf + pos;
+	if (*cp != '\0') {
+		while (*cp != '\0' && *cp != ' ')
+			cp++;
+		while (*cp == ' ')
+			*cp++ = '\0';
+		if (*cp != '\0')
+			mandoc_vmsg(MANDOCERR_ARG_EXCESS,
+			    r->parse, ln, cp - buf->buf,
+			    "%s ... %s", roff_name[tok], cp);
+		roff_word_alloc(r->man, ln, pos, buf->buf + pos);
+	}
+
+	n->flags |= NODE_LINE | NODE_VALID | NODE_ENDED;
+	r->man->last = n;
+	r->man->next = ROFF_NEXT_SIBLING;
+	return ROFF_IGN;
+}
+
 static enum rofferr
 roff_br(ROFF_ARGS)
 {
diff --git a/roff.h b/roff.h
index 59fd8ba9..928b97eb 100644
--- a/roff.h
+++ b/roff.h
@@ -1,4 +1,4 @@
-/*	$Id: roff.h,v 1.43 2017/05/04 17:48:29 schwarze Exp $	*/
+/*	$Id: roff.h,v 1.44 2017/05/05 02:06:19 schwarze Exp $	*/
 /*
  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
  * Copyright (c) 2013, 2014, 2015, 2017 Ingo Schwarze <schwarze@openbsd.org>
@@ -67,6 +67,7 @@ enum	roff_type {
 
 enum	roff_tok {
 	ROFF_br = 0,
+	ROFF_ft,
 	ROFF_MAX,
 	ROFF_ab,
 	ROFF_ad,
@@ -156,7 +157,6 @@ enum	roff_tok {
 	ROFF_fschar,
 	ROFF_fspacewidth,
 	ROFF_fspecial,
-	/* MAN_ft; ignored in mdoc(7) */
 	ROFF_ftr,
 	ROFF_fzoom,
 	ROFF_gcolor,
@@ -464,7 +464,6 @@ enum	roff_tok {
 	MAN_PD,
 	MAN_AT,
 	MAN_in,
-	MAN_ft,
 	MAN_OP,
 	MAN_EX,
 	MAN_EE,
@@ -576,3 +575,4 @@ void		 deroff(char **, const struct roff_node *);
 struct ohash	*roffhash_alloc(enum roff_tok, enum roff_tok);
 enum roff_tok	 roffhash_find(struct ohash *, const char *, size_t);
 void		 roffhash_free(struct ohash *);
+void		 roff_validate(struct roff_man *);
diff --git a/roff_html.c b/roff_html.c
index 533fb9b7..3e30aa14 100644
--- a/roff_html.c
+++ b/roff_html.c
@@ -17,6 +17,7 @@
 #include <sys/types.h>
 
 #include <assert.h>
+#include <stddef.h>
 
 #include "roff.h"
 #include "out.h"
@@ -30,6 +31,7 @@ static	void	  roff_html_pre_br(ROFF_HTML_ARGS);
 
 static	const roff_html_pre_fp roff_html_pre_acts[ROFF_MAX] = {
 	roff_html_pre_br,  /* br */
+	NULL,  /* ft */
 };
 
 
@@ -37,7 +39,8 @@ void
 roff_html_pre(struct html *h, const struct roff_node *n)
 {
 	assert(n->tok < ROFF_MAX);
-	(*roff_html_pre_acts[n->tok])(h, n);
+	if (roff_html_pre_acts[n->tok] != NULL)
+		(*roff_html_pre_acts[n->tok])(h, n);
 }
 
 static void
diff --git a/roff_term.c b/roff_term.c
index d96d9dd8..cd9fba37 100644
--- a/roff_term.c
+++ b/roff_term.c
@@ -1,6 +1,6 @@
 /*	$OpenBSD$ */
 /*
- * Copyright (c) 2017 Ingo Schwarze <schwarze@openbsd.org>
+ * Copyright (c) 2010, 2017 Ingo Schwarze <schwarze@openbsd.org>
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -27,9 +27,11 @@
 typedef	void	(*roff_term_pre_fp)(ROFF_TERM_ARGS);
 
 static	void	  roff_term_pre_br(ROFF_TERM_ARGS);
+static	void	  roff_term_pre_ft(ROFF_TERM_ARGS);
 
 static	const roff_term_pre_fp roff_term_pre_acts[ROFF_MAX] = {
 	roff_term_pre_br,  /* br */
+	roff_term_pre_ft,  /* ft */
 };
 
 
@@ -50,3 +52,29 @@ roff_term_pre_br(ROFF_TERM_ARGS)
 		p->flags &= ~(TERMP_NOBREAK | TERMP_BRIND);
 	}
 }
+
+static void
+roff_term_pre_ft(ROFF_TERM_ARGS)
+{
+	switch (*n->child->string) {
+	case '4':
+	case '3':
+	case 'B':
+		term_fontrepl(p, TERMFONT_BOLD);
+		break;
+	case '2':
+	case 'I':
+		term_fontrepl(p, TERMFONT_UNDER);
+		break;
+	case 'P':
+		term_fontlast(p);
+		break;
+	case '1':
+	case 'C':
+	case 'R':
+		term_fontrepl(p, TERMFONT_NONE);
+		break;
+	default:
+		break;
+	}
+}
diff --git a/roff_validate.c b/roff_validate.c
new file mode 100644
index 00000000..269de1c4
--- /dev/null
+++ b/roff_validate.c
@@ -0,0 +1,89 @@
+/*	$OpenBSD: roff_html.c,v 1.1 2017/05/04 22:07:44 schwarze Exp $ */
+/*
+ * Copyright (c) 2010, 2017 Ingo Schwarze <schwarze@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#include <sys/types.h>
+
+#include <assert.h>
+#include <stddef.h>
+
+#include "mandoc.h"
+#include "roff.h"
+#include "libmandoc.h"
+#include "roff_int.h"
+
+#define	ROFF_VALID_ARGS struct roff_man *man, struct roff_node *n
+
+typedef	void	(*roff_valid_fp)(ROFF_VALID_ARGS);
+
+static	void	  roff_valid_ft(ROFF_VALID_ARGS);
+
+static	const roff_valid_fp roff_valids[ROFF_MAX] = {
+	NULL,  /* br */
+	roff_valid_ft,  /* ft */
+};
+
+
+void
+roff_validate(struct roff_man *man)
+{
+	struct roff_node	*n;
+
+	n = man->last;
+	assert(n->tok < ROFF_MAX);
+	if (roff_valids[n->tok] != NULL)
+		(*roff_valids[n->tok])(man, n);
+}
+
+static void
+roff_valid_ft(ROFF_VALID_ARGS)
+{
+	char	*cp;
+
+	if (n->child == NULL) {
+		man->next = ROFF_NEXT_CHILD;
+		roff_word_alloc(man, n->line, n->pos, "P");
+		man->last = n;
+		return;
+	}
+
+	cp = n->child->string;
+	switch (*cp) {
+	case '1':
+	case '2':
+	case '3':
+	case '4':
+	case 'I':
+	case 'P':
+	case 'R':
+		if (cp[1] == '\0')
+			return;
+		break;
+	case 'B':
+		if (cp[1] == '\0' || (cp[1] == 'I' && cp[2] == '\0'))
+			return;
+		break;
+	case 'C':
+		if (cp[1] == 'W' && cp[2] == '\0')
+			return;
+		break;
+	default:
+		break;
+	}
+
+	mandoc_vmsg(MANDOCERR_FT_BAD, man->parse,
+	    n->line, n->pos, "ft %s", cp);
+	roff_node_delete(man, n);
+}
-- 
2.47.1