xref: /freebsd/contrib/mandoc/mdoc_html.c (revision 7fdf597e96a02165cfe22ff357b857d5fa15ed8a)
1 /* $Id: mdoc_html.c,v 1.350 2022/07/06 16:05:40 schwarze Exp $ */
2 /*
3  * Copyright (c) 2014-2022 Ingo Schwarze <schwarze@openbsd.org>
4  * Copyright (c) 2008-2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
5  * Copyright (c) 2022 Anna Vyalkova <cyber@sysrq.in>
6  *
7  * Permission to use, copy, modify, and distribute this software for any
8  * purpose with or without fee is hereby granted, provided that the above
9  * copyright notice and this permission notice appear in all copies.
10  *
11  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
12  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
14  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18  *
19  * HTML formatter for mdoc(7) used by mandoc(1).
20  */
21 #include "config.h"
22 
23 #include <sys/types.h>
24 
25 #include <assert.h>
26 #include <ctype.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <unistd.h>
31 
32 #include "mandoc_aux.h"
33 #include "mandoc.h"
34 #include "roff.h"
35 #include "mdoc.h"
36 #include "out.h"
37 #include "html.h"
38 #include "main.h"
39 
40 #define	MDOC_ARGS	  const struct roff_meta *meta, \
41 			  struct roff_node *n, \
42 			  struct html *h
43 
44 #ifndef MIN
45 #define	MIN(a,b)	((/*CONSTCOND*/(a)<(b))?(a):(b))
46 #endif
47 
48 struct	mdoc_html_act {
49 	int		(*pre)(MDOC_ARGS);
50 	void		(*post)(MDOC_ARGS);
51 };
52 
53 static	void		  print_mdoc_head(const struct roff_meta *,
54 				struct html *);
55 static	void		  print_mdoc_node(MDOC_ARGS);
56 static	void		  print_mdoc_nodelist(MDOC_ARGS);
57 static	void		  synopsis_pre(struct html *, struct roff_node *);
58 
59 static	void		  mdoc_root_post(const struct roff_meta *,
60 				struct html *);
61 static	int		  mdoc_root_pre(const struct roff_meta *,
62 				struct html *);
63 
64 static	void		  mdoc__x_post(MDOC_ARGS);
65 static	int		  mdoc__x_pre(MDOC_ARGS);
66 static	int		  mdoc_abort_pre(MDOC_ARGS);
67 static	int		  mdoc_ad_pre(MDOC_ARGS);
68 static	int		  mdoc_an_pre(MDOC_ARGS);
69 static	int		  mdoc_ap_pre(MDOC_ARGS);
70 static	int		  mdoc_ar_pre(MDOC_ARGS);
71 static	int		  mdoc_bd_pre(MDOC_ARGS);
72 static	int		  mdoc_bf_pre(MDOC_ARGS);
73 static	void		  mdoc_bk_post(MDOC_ARGS);
74 static	int		  mdoc_bk_pre(MDOC_ARGS);
75 static	int		  mdoc_bl_pre(MDOC_ARGS);
76 static	int		  mdoc_cd_pre(MDOC_ARGS);
77 static	int		  mdoc_code_pre(MDOC_ARGS);
78 static	int		  mdoc_d1_pre(MDOC_ARGS);
79 static	int		  mdoc_fa_pre(MDOC_ARGS);
80 static	int		  mdoc_fd_pre(MDOC_ARGS);
81 static	int		  mdoc_fl_pre(MDOC_ARGS);
82 static	int		  mdoc_fn_pre(MDOC_ARGS);
83 static	int		  mdoc_ft_pre(MDOC_ARGS);
84 static	int		  mdoc_em_pre(MDOC_ARGS);
85 static	void		  mdoc_eo_post(MDOC_ARGS);
86 static	int		  mdoc_eo_pre(MDOC_ARGS);
87 static	int		  mdoc_ex_pre(MDOC_ARGS);
88 static	void		  mdoc_fo_post(MDOC_ARGS);
89 static	int		  mdoc_fo_pre(MDOC_ARGS);
90 static	int		  mdoc_igndelim_pre(MDOC_ARGS);
91 static	int		  mdoc_in_pre(MDOC_ARGS);
92 static	int		  mdoc_it_pre(MDOC_ARGS);
93 static	int		  mdoc_lb_pre(MDOC_ARGS);
94 static	int		  mdoc_lk_pre(MDOC_ARGS);
95 static	int		  mdoc_mt_pre(MDOC_ARGS);
96 static	int		  mdoc_nd_pre(MDOC_ARGS);
97 static	int		  mdoc_nm_pre(MDOC_ARGS);
98 static	int		  mdoc_no_pre(MDOC_ARGS);
99 static	int		  mdoc_ns_pre(MDOC_ARGS);
100 static	int		  mdoc_pa_pre(MDOC_ARGS);
101 static	void		  mdoc_pf_post(MDOC_ARGS);
102 static	int		  mdoc_pp_pre(MDOC_ARGS);
103 static	void		  mdoc_quote_post(MDOC_ARGS);
104 static	int		  mdoc_quote_pre(MDOC_ARGS);
105 static	int		  mdoc_rs_pre(MDOC_ARGS);
106 static	int		  mdoc_sh_pre(MDOC_ARGS);
107 static	int		  mdoc_skip_pre(MDOC_ARGS);
108 static	int		  mdoc_sm_pre(MDOC_ARGS);
109 static	int		  mdoc_ss_pre(MDOC_ARGS);
110 static	int		  mdoc_st_pre(MDOC_ARGS);
111 static	int		  mdoc_sx_pre(MDOC_ARGS);
112 static	int		  mdoc_sy_pre(MDOC_ARGS);
113 static	int		  mdoc_tg_pre(MDOC_ARGS);
114 static	int		  mdoc_va_pre(MDOC_ARGS);
115 static	int		  mdoc_vt_pre(MDOC_ARGS);
116 static	int		  mdoc_xr_pre(MDOC_ARGS);
117 static	int		  mdoc_xx_pre(MDOC_ARGS);
118 
119 static const struct mdoc_html_act mdoc_html_acts[MDOC_MAX - MDOC_Dd] = {
120 	{NULL, NULL}, /* Dd */
121 	{NULL, NULL}, /* Dt */
122 	{NULL, NULL}, /* Os */
123 	{mdoc_sh_pre, NULL }, /* Sh */
124 	{mdoc_ss_pre, NULL }, /* Ss */
125 	{mdoc_pp_pre, NULL}, /* Pp */
126 	{mdoc_d1_pre, NULL}, /* D1 */
127 	{mdoc_d1_pre, NULL}, /* Dl */
128 	{mdoc_bd_pre, NULL}, /* Bd */
129 	{NULL, NULL}, /* Ed */
130 	{mdoc_bl_pre, NULL}, /* Bl */
131 	{NULL, NULL}, /* El */
132 	{mdoc_it_pre, NULL}, /* It */
133 	{mdoc_ad_pre, NULL}, /* Ad */
134 	{mdoc_an_pre, NULL}, /* An */
135 	{mdoc_ap_pre, NULL}, /* Ap */
136 	{mdoc_ar_pre, NULL}, /* Ar */
137 	{mdoc_cd_pre, NULL}, /* Cd */
138 	{mdoc_code_pre, NULL}, /* Cm */
139 	{mdoc_code_pre, NULL}, /* Dv */
140 	{mdoc_code_pre, NULL}, /* Er */
141 	{mdoc_code_pre, NULL}, /* Ev */
142 	{mdoc_ex_pre, NULL}, /* Ex */
143 	{mdoc_fa_pre, NULL}, /* Fa */
144 	{mdoc_fd_pre, NULL}, /* Fd */
145 	{mdoc_fl_pre, NULL}, /* Fl */
146 	{mdoc_fn_pre, NULL}, /* Fn */
147 	{mdoc_ft_pre, NULL}, /* Ft */
148 	{mdoc_code_pre, NULL}, /* Ic */
149 	{mdoc_in_pre, NULL}, /* In */
150 	{mdoc_code_pre, NULL}, /* Li */
151 	{mdoc_nd_pre, NULL}, /* Nd */
152 	{mdoc_nm_pre, NULL}, /* Nm */
153 	{mdoc_quote_pre, mdoc_quote_post}, /* Op */
154 	{mdoc_abort_pre, NULL}, /* Ot */
155 	{mdoc_pa_pre, NULL}, /* Pa */
156 	{mdoc_ex_pre, NULL}, /* Rv */
157 	{mdoc_st_pre, NULL}, /* St */
158 	{mdoc_va_pre, NULL}, /* Va */
159 	{mdoc_vt_pre, NULL}, /* Vt */
160 	{mdoc_xr_pre, NULL}, /* Xr */
161 	{mdoc__x_pre, mdoc__x_post}, /* %A */
162 	{mdoc__x_pre, mdoc__x_post}, /* %B */
163 	{mdoc__x_pre, mdoc__x_post}, /* %D */
164 	{mdoc__x_pre, mdoc__x_post}, /* %I */
165 	{mdoc__x_pre, mdoc__x_post}, /* %J */
166 	{mdoc__x_pre, mdoc__x_post}, /* %N */
167 	{mdoc__x_pre, mdoc__x_post}, /* %O */
168 	{mdoc__x_pre, mdoc__x_post}, /* %P */
169 	{mdoc__x_pre, mdoc__x_post}, /* %R */
170 	{mdoc__x_pre, mdoc__x_post}, /* %T */
171 	{mdoc__x_pre, mdoc__x_post}, /* %V */
172 	{NULL, NULL}, /* Ac */
173 	{mdoc_quote_pre, mdoc_quote_post}, /* Ao */
174 	{mdoc_quote_pre, mdoc_quote_post}, /* Aq */
175 	{mdoc_xx_pre, NULL}, /* At */
176 	{NULL, NULL}, /* Bc */
177 	{mdoc_bf_pre, NULL}, /* Bf */
178 	{mdoc_quote_pre, mdoc_quote_post}, /* Bo */
179 	{mdoc_quote_pre, mdoc_quote_post}, /* Bq */
180 	{mdoc_xx_pre, NULL}, /* Bsx */
181 	{mdoc_xx_pre, NULL}, /* Bx */
182 	{mdoc_skip_pre, NULL}, /* Db */
183 	{NULL, NULL}, /* Dc */
184 	{mdoc_quote_pre, mdoc_quote_post}, /* Do */
185 	{mdoc_quote_pre, mdoc_quote_post}, /* Dq */
186 	{NULL, NULL}, /* Ec */ /* FIXME: no space */
187 	{NULL, NULL}, /* Ef */
188 	{mdoc_em_pre, NULL}, /* Em */
189 	{mdoc_eo_pre, mdoc_eo_post}, /* Eo */
190 	{mdoc_xx_pre, NULL}, /* Fx */
191 	{mdoc_no_pre, NULL}, /* Ms */
192 	{mdoc_no_pre, NULL}, /* No */
193 	{mdoc_ns_pre, NULL}, /* Ns */
194 	{mdoc_xx_pre, NULL}, /* Nx */
195 	{mdoc_xx_pre, NULL}, /* Ox */
196 	{NULL, NULL}, /* Pc */
197 	{mdoc_igndelim_pre, mdoc_pf_post}, /* Pf */
198 	{mdoc_quote_pre, mdoc_quote_post}, /* Po */
199 	{mdoc_quote_pre, mdoc_quote_post}, /* Pq */
200 	{NULL, NULL}, /* Qc */
201 	{mdoc_quote_pre, mdoc_quote_post}, /* Ql */
202 	{mdoc_quote_pre, mdoc_quote_post}, /* Qo */
203 	{mdoc_quote_pre, mdoc_quote_post}, /* Qq */
204 	{NULL, NULL}, /* Re */
205 	{mdoc_rs_pre, NULL}, /* Rs */
206 	{NULL, NULL}, /* Sc */
207 	{mdoc_quote_pre, mdoc_quote_post}, /* So */
208 	{mdoc_quote_pre, mdoc_quote_post}, /* Sq */
209 	{mdoc_sm_pre, NULL}, /* Sm */
210 	{mdoc_sx_pre, NULL}, /* Sx */
211 	{mdoc_sy_pre, NULL}, /* Sy */
212 	{NULL, NULL}, /* Tn */
213 	{mdoc_xx_pre, NULL}, /* Ux */
214 	{NULL, NULL}, /* Xc */
215 	{NULL, NULL}, /* Xo */
216 	{mdoc_fo_pre, mdoc_fo_post}, /* Fo */
217 	{NULL, NULL}, /* Fc */
218 	{mdoc_quote_pre, mdoc_quote_post}, /* Oo */
219 	{NULL, NULL}, /* Oc */
220 	{mdoc_bk_pre, mdoc_bk_post}, /* Bk */
221 	{NULL, NULL}, /* Ek */
222 	{NULL, NULL}, /* Bt */
223 	{NULL, NULL}, /* Hf */
224 	{mdoc_em_pre, NULL}, /* Fr */
225 	{NULL, NULL}, /* Ud */
226 	{mdoc_lb_pre, NULL}, /* Lb */
227 	{mdoc_abort_pre, NULL}, /* Lp */
228 	{mdoc_lk_pre, NULL}, /* Lk */
229 	{mdoc_mt_pre, NULL}, /* Mt */
230 	{mdoc_quote_pre, mdoc_quote_post}, /* Brq */
231 	{mdoc_quote_pre, mdoc_quote_post}, /* Bro */
232 	{NULL, NULL}, /* Brc */
233 	{mdoc__x_pre, mdoc__x_post}, /* %C */
234 	{mdoc_skip_pre, NULL}, /* Es */
235 	{mdoc_quote_pre, mdoc_quote_post}, /* En */
236 	{mdoc_xx_pre, NULL}, /* Dx */
237 	{mdoc__x_pre, mdoc__x_post}, /* %Q */
238 	{mdoc__x_pre, mdoc__x_post}, /* %U */
239 	{NULL, NULL}, /* Ta */
240 	{mdoc_tg_pre, NULL}, /* Tg */
241 };
242 
243 
244 /*
245  * See the same function in mdoc_term.c for documentation.
246  */
247 static void
248 synopsis_pre(struct html *h, struct roff_node *n)
249 {
250 	struct roff_node *np;
251 
252 	if ((n->flags & NODE_SYNPRETTY) == 0 ||
253 	    (np = roff_node_prev(n)) == NULL)
254 		return;
255 
256 	if (np->tok == n->tok &&
257 	    MDOC_Fo != n->tok &&
258 	    MDOC_Ft != n->tok &&
259 	    MDOC_Fn != n->tok) {
260 		print_otag(h, TAG_BR, "");
261 		return;
262 	}
263 
264 	switch (np->tok) {
265 	case MDOC_Fd:
266 	case MDOC_Fn:
267 	case MDOC_Fo:
268 	case MDOC_In:
269 	case MDOC_Vt:
270 		break;
271 	case MDOC_Ft:
272 		if (n->tok != MDOC_Fn && n->tok != MDOC_Fo)
273 			break;
274 		/* FALLTHROUGH */
275 	default:
276 		print_otag(h, TAG_BR, "");
277 		return;
278 	}
279 	html_close_paragraph(h);
280 	print_otag(h, TAG_P, "c", "Pp");
281 }
282 
283 void
284 html_mdoc(void *arg, const struct roff_meta *mdoc)
285 {
286 	struct html		*h;
287 	struct roff_node	*n;
288 	struct tag		*t;
289 
290 	h = (struct html *)arg;
291 	n = mdoc->first->child;
292 
293 	if ((h->oflags & HTML_FRAGMENT) == 0) {
294 		print_gen_decls(h);
295 		print_otag(h, TAG_HTML, "");
296 		t = print_otag(h, TAG_HEAD, "");
297 		print_mdoc_head(mdoc, h);
298 		print_tagq(h, t);
299 		if (n != NULL && n->type == ROFFT_COMMENT)
300 			print_gen_comment(h, n);
301 		print_otag(h, TAG_BODY, "");
302 	}
303 
304 	mdoc_root_pre(mdoc, h);
305 	t = print_otag(h, TAG_MAIN, "c", "manual-text");
306 	print_mdoc_nodelist(mdoc, n, h);
307 	print_tagq(h, t);
308 	mdoc_root_post(mdoc, h);
309 	print_tagq(h, NULL);
310 }
311 
312 static void
313 print_mdoc_head(const struct roff_meta *meta, struct html *h)
314 {
315 	char	*cp;
316 
317 	print_gen_head(h);
318 
319 	if (meta->arch != NULL && meta->msec != NULL)
320 		mandoc_asprintf(&cp, "%s(%s) (%s)", meta->title,
321 		    meta->msec, meta->arch);
322 	else if (meta->msec != NULL)
323 		mandoc_asprintf(&cp, "%s(%s)", meta->title, meta->msec);
324 	else if (meta->arch != NULL)
325 		mandoc_asprintf(&cp, "%s (%s)", meta->title, meta->arch);
326 	else
327 		cp = mandoc_strdup(meta->title);
328 
329 	print_otag(h, TAG_TITLE, "");
330 	print_text(h, cp);
331 	free(cp);
332 }
333 
334 static void
335 print_mdoc_nodelist(MDOC_ARGS)
336 {
337 
338 	while (n != NULL) {
339 		print_mdoc_node(meta, n, h);
340 		n = n->next;
341 	}
342 }
343 
344 static void
345 print_mdoc_node(MDOC_ARGS)
346 {
347 	struct tag	*t;
348 	int		 child;
349 
350 	if (n->type == ROFFT_COMMENT || n->flags & NODE_NOPRT)
351 		return;
352 
353 	if ((n->flags & NODE_NOFILL) == 0)
354 		html_fillmode(h, ROFF_fi);
355 	else if (html_fillmode(h, ROFF_nf) == ROFF_nf &&
356 	    n->tok != ROFF_fi && n->flags & NODE_LINE)
357 		print_endline(h);
358 
359 	child = 1;
360 	n->flags &= ~NODE_ENDED;
361 	switch (n->type) {
362 	case ROFFT_TEXT:
363 		if (n->flags & NODE_LINE) {
364 			switch (*n->string) {
365 			case '\0':
366 				h->col = 1;
367 				print_endline(h);
368 				return;
369 			case ' ':
370 				if ((h->flags & HTML_NONEWLINE) == 0 &&
371 				    (n->flags & NODE_NOFILL) == 0)
372 					print_otag(h, TAG_BR, "");
373 				break;
374 			default:
375 				break;
376 			}
377 		}
378 		t = h->tag;
379 		t->refcnt++;
380 		if (n->flags & NODE_DELIMC)
381 			h->flags |= HTML_NOSPACE;
382 		if (n->flags & NODE_HREF)
383 			print_tagged_text(h, n->string, n);
384 		else
385 			print_text(h, n->string);
386 		if (n->flags & NODE_DELIMO)
387 			h->flags |= HTML_NOSPACE;
388 		break;
389 	case ROFFT_EQN:
390 		t = h->tag;
391 		t->refcnt++;
392 		print_eqn(h, n->eqn);
393 		break;
394 	case ROFFT_TBL:
395 		/*
396 		 * This will take care of initialising all of the table
397 		 * state data for the first table, then tearing it down
398 		 * for the last one.
399 		 */
400 		print_tbl(h, n->span);
401 		return;
402 	default:
403 		/*
404 		 * Close out the current table, if it's open, and unset
405 		 * the "meta" table state.  This will be reopened on the
406 		 * next table element.
407 		 */
408 		if (h->tblt != NULL)
409 			print_tblclose(h);
410 		assert(h->tblt == NULL);
411 		t = h->tag;
412 		t->refcnt++;
413 		if (n->tok < ROFF_MAX) {
414 			roff_html_pre(h, n);
415 			t->refcnt--;
416 			print_stagq(h, t);
417 			return;
418 		}
419 		assert(n->tok >= MDOC_Dd && n->tok < MDOC_MAX);
420 		if (mdoc_html_acts[n->tok - MDOC_Dd].pre != NULL &&
421 		    (n->end == ENDBODY_NOT || n->child != NULL))
422 			child = (*mdoc_html_acts[n->tok - MDOC_Dd].pre)(meta,
423 			    n, h);
424 		break;
425 	}
426 
427 	if (h->flags & HTML_KEEP && n->flags & NODE_LINE) {
428 		h->flags &= ~HTML_KEEP;
429 		h->flags |= HTML_PREKEEP;
430 	}
431 
432 	if (child && n->child != NULL)
433 		print_mdoc_nodelist(meta, n->child, h);
434 
435 	t->refcnt--;
436 	print_stagq(h, t);
437 
438 	switch (n->type) {
439 	case ROFFT_TEXT:
440 	case ROFFT_EQN:
441 		break;
442 	default:
443 		if (mdoc_html_acts[n->tok - MDOC_Dd].post == NULL ||
444 		    n->flags & NODE_ENDED)
445 			break;
446 		(*mdoc_html_acts[n->tok - MDOC_Dd].post)(meta, n, h);
447 		if (n->end != ENDBODY_NOT)
448 			n->body->flags |= NODE_ENDED;
449 		break;
450 	}
451 }
452 
453 static void
454 mdoc_root_post(const struct roff_meta *meta, struct html *h)
455 {
456 	struct tag	*t;
457 
458 	t = print_otag(h, TAG_DIV, "cr?", "foot", "doc-pagefooter",
459 	    "aria-label", "Manual footer line");
460 
461 	print_otag(h, TAG_SPAN, "c", "foot-left");
462 	print_stagq(h, t);
463 
464 	print_otag(h, TAG_SPAN, "c", "foot-date");
465 	print_text(h, meta->date);
466 	print_stagq(h, t);
467 
468 	print_otag(h, TAG_SPAN, "c", "foot-os");
469 	print_text(h, meta->os);
470 	print_tagq(h, t);
471 }
472 
473 static int
474 mdoc_root_pre(const struct roff_meta *meta, struct html *h)
475 {
476 	struct tag	*t;
477 	char		*volume, *title;
478 
479 	if (NULL == meta->arch)
480 		volume = mandoc_strdup(meta->vol);
481 	else
482 		mandoc_asprintf(&volume, "%s (%s)",
483 		    meta->vol, meta->arch);
484 
485 	if (NULL == meta->msec)
486 		title = mandoc_strdup(meta->title);
487 	else
488 		mandoc_asprintf(&title, "%s(%s)",
489 		    meta->title, meta->msec);
490 
491 	t = print_otag(h, TAG_DIV, "cr?", "head", "doc-pageheader",
492 	    "aria-label", "Manual header line");
493 
494 	print_otag(h, TAG_SPAN, "c", "head-ltitle");
495 	print_text(h, title);
496 	print_stagq(h, t);
497 
498 	print_otag(h, TAG_SPAN, "c", "head-vol");
499 	print_text(h, volume);
500 	print_stagq(h, t);
501 
502 	print_otag(h, TAG_SPAN, "c", "head-rtitle");
503 	print_text(h, title);
504 	print_tagq(h, t);
505 
506 	free(title);
507 	free(volume);
508 	return 1;
509 }
510 
511 static int
512 mdoc_code_pre(MDOC_ARGS)
513 {
514 	print_otag_id(h, TAG_CODE, roff_name[n->tok], n);
515 	return 1;
516 }
517 
518 static int
519 mdoc_sh_pre(MDOC_ARGS)
520 {
521 	struct roff_node	*sn, *subn;
522 	struct tag		*t, *tnav, *tsec, *tsub;
523 	char			*id;
524 	int			 sc;
525 
526 	switch (n->type) {
527 	case ROFFT_BLOCK:
528 		html_close_paragraph(h);
529 		if ((h->oflags & HTML_TOC) == 0 ||
530 		    h->flags & HTML_TOCDONE ||
531 		    n->sec <= SEC_SYNOPSIS) {
532 			print_otag(h, TAG_SECTION, "c", "Sh");
533 			break;
534 		}
535 		h->flags |= HTML_TOCDONE;
536 		sc = 0;
537 		for (sn = n->next; sn != NULL; sn = sn->next)
538 			if (sn->sec == SEC_CUSTOM)
539 				if (++sc == 2)
540 					break;
541 		if (sc < 2)
542 			break;
543 		tnav = print_otag(h, TAG_NAV, "r", "doc-toc");
544 		t = print_otag(h, TAG_H2, "c", "Sh");
545 		print_text(h, "TABLE OF CONTENTS");
546 		print_tagq(h, t);
547 		t = print_otag(h, TAG_UL, "c", "Bl-compact");
548 		for (sn = n; sn != NULL; sn = sn->next) {
549 			tsec = print_otag(h, TAG_LI, "");
550 			id = html_make_id(sn->head, 0);
551 			tsub = print_otag(h, TAG_A, "hR", id);
552 			free(id);
553 			print_mdoc_nodelist(meta, sn->head->child, h);
554 			print_tagq(h, tsub);
555 			tsub = NULL;
556 			for (subn = sn->body->child; subn != NULL;
557 			    subn = subn->next) {
558 				if (subn->tok != MDOC_Ss)
559 					continue;
560 				id = html_make_id(subn->head, 0);
561 				if (id == NULL)
562 					continue;
563 				if (tsub == NULL)
564 					print_otag(h, TAG_UL,
565 					    "c", "Bl-compact");
566 				tsub = print_otag(h, TAG_LI, "");
567 				print_otag(h, TAG_A, "hR", id);
568 				free(id);
569 				print_mdoc_nodelist(meta,
570 				    subn->head->child, h);
571 				print_tagq(h, tsub);
572 			}
573 			print_tagq(h, tsec);
574 		}
575 		print_tagq(h, tnav);
576 		print_otag(h, TAG_SECTION, "c", "Sh");
577 		break;
578 	case ROFFT_HEAD:
579 		print_otag_id(h, TAG_H2, "Sh", n);
580 		break;
581 	case ROFFT_BODY:
582 		if (n->sec == SEC_AUTHORS)
583 			h->flags &= ~(HTML_SPLIT|HTML_NOSPLIT);
584 		break;
585 	default:
586 		break;
587 	}
588 	return 1;
589 }
590 
591 static int
592 mdoc_ss_pre(MDOC_ARGS)
593 {
594 	switch (n->type) {
595 	case ROFFT_BLOCK:
596 		html_close_paragraph(h);
597 		print_otag(h, TAG_SECTION, "c", "Ss");
598 		break;
599 	case ROFFT_HEAD:
600 		print_otag_id(h, TAG_H3, "Ss", n);
601 		break;
602 	case ROFFT_BODY:
603 		break;
604 	default:
605 		abort();
606 	}
607 	return 1;
608 }
609 
610 static int
611 mdoc_fl_pre(MDOC_ARGS)
612 {
613 	struct roff_node	*nn;
614 
615 	print_otag_id(h, TAG_CODE, "Fl", n);
616 	print_text(h, "\\-");
617 	if (n->child != NULL ||
618 	    ((nn = roff_node_next(n)) != NULL &&
619 	     nn->type != ROFFT_TEXT &&
620 	     (nn->flags & NODE_LINE) == 0))
621 		h->flags |= HTML_NOSPACE;
622 
623 	return 1;
624 }
625 
626 static int
627 mdoc_nd_pre(MDOC_ARGS)
628 {
629 	switch (n->type) {
630 	case ROFFT_BLOCK:
631 		return 1;
632 	case ROFFT_HEAD:
633 		return 0;
634 	case ROFFT_BODY:
635 		break;
636 	default:
637 		abort();
638 	}
639 	print_text(h, "\\(em");
640 	print_otag(h, TAG_SPAN, "cr", "Nd", "doc-subtitle");
641 	return 1;
642 }
643 
644 static int
645 mdoc_nm_pre(MDOC_ARGS)
646 {
647 	switch (n->type) {
648 	case ROFFT_BLOCK:
649 		break;
650 	case ROFFT_HEAD:
651 		print_otag(h, TAG_TD, "");
652 		/* FALLTHROUGH */
653 	case ROFFT_ELEM:
654 		print_otag(h, TAG_CODE, "c", "Nm");
655 		return 1;
656 	case ROFFT_BODY:
657 		print_otag(h, TAG_TD, "");
658 		return 1;
659 	default:
660 		abort();
661 	}
662 	html_close_paragraph(h);
663 	synopsis_pre(h, n);
664 	print_otag(h, TAG_TABLE, "c", "Nm");
665 	print_otag(h, TAG_TR, "");
666 	return 1;
667 }
668 
669 static int
670 mdoc_xr_pre(MDOC_ARGS)
671 {
672 	char	*name, *section, *label;
673 
674 	if (n->child == NULL)
675 		return 0;
676 
677 	name = n->child->string;
678 	if (n->child->next != NULL) {
679 		section = n->child->next->string;
680 		mandoc_asprintf(&label, "%s, section %s", name, section);
681 	} else
682 		section = label = NULL;
683 
684 	if (h->base_man1)
685 		print_otag(h, TAG_A, "chM?", "Xr",
686 		    name, section, "aria-label", label);
687 	else
688 		print_otag(h, TAG_A, "c?", "Xr", "aria-label", label);
689 
690 	free(label);
691 	print_text(h, name);
692 
693 	if (section == NULL)
694 		return 0;
695 
696 	h->flags |= HTML_NOSPACE;
697 	print_text(h, "(");
698 	h->flags |= HTML_NOSPACE;
699 	print_text(h, section);
700 	h->flags |= HTML_NOSPACE;
701 	print_text(h, ")");
702 	return 0;
703 }
704 
705 static int
706 mdoc_tg_pre(MDOC_ARGS)
707 {
708 	char	*id;
709 
710 	if ((id = html_make_id(n, 1)) != NULL) {
711 		print_tagq(h, print_otag(h, TAG_MARK, "i", id));
712 		free(id);
713 	}
714 	return 0;
715 }
716 
717 static int
718 mdoc_ns_pre(MDOC_ARGS)
719 {
720 
721 	if ( ! (NODE_LINE & n->flags))
722 		h->flags |= HTML_NOSPACE;
723 	return 1;
724 }
725 
726 static int
727 mdoc_ar_pre(MDOC_ARGS)
728 {
729 	print_otag(h, TAG_VAR, "c", "Ar");
730 	return 1;
731 }
732 
733 static int
734 mdoc_xx_pre(MDOC_ARGS)
735 {
736 	print_otag(h, TAG_SPAN, "c", "Ux");
737 	return 1;
738 }
739 
740 static int
741 mdoc_it_pre(MDOC_ARGS)
742 {
743 	const struct roff_node	*bl;
744 	enum mdoc_list		 type;
745 
746 	bl = n->parent;
747 	while (bl->tok != MDOC_Bl)
748 		bl = bl->parent;
749 	type = bl->norm->Bl.type;
750 
751 	switch (type) {
752 	case LIST_bullet:
753 	case LIST_dash:
754 	case LIST_hyphen:
755 	case LIST_item:
756 	case LIST_enum:
757 		switch (n->type) {
758 		case ROFFT_HEAD:
759 			return 0;
760 		case ROFFT_BODY:
761 			print_otag_id(h, TAG_LI, NULL, n);
762 			break;
763 		default:
764 			break;
765 		}
766 		break;
767 	case LIST_diag:
768 	case LIST_hang:
769 	case LIST_inset:
770 	case LIST_ohang:
771 		switch (n->type) {
772 		case ROFFT_HEAD:
773 			print_otag_id(h, TAG_DT, NULL, n);
774 			break;
775 		case ROFFT_BODY:
776 			print_otag(h, TAG_DD, "");
777 			break;
778 		default:
779 			break;
780 		}
781 		break;
782 	case LIST_tag:
783 		switch (n->type) {
784 		case ROFFT_HEAD:
785 			print_otag_id(h, TAG_DT, NULL, n);
786 			break;
787 		case ROFFT_BODY:
788 			if (n->child == NULL) {
789 				print_otag(h, TAG_DD, "s", "width", "auto");
790 				print_text(h, "\\ ");
791 			} else
792 				print_otag(h, TAG_DD, "");
793 			break;
794 		default:
795 			break;
796 		}
797 		break;
798 	case LIST_column:
799 		switch (n->type) {
800 		case ROFFT_HEAD:
801 			break;
802 		case ROFFT_BODY:
803 			print_otag(h, TAG_TD, "");
804 			break;
805 		default:
806 			print_otag_id(h, TAG_TR, NULL, n);
807 		}
808 	default:
809 		break;
810 	}
811 
812 	return 1;
813 }
814 
815 static int
816 mdoc_bl_pre(MDOC_ARGS)
817 {
818 	char		 cattr[32];
819 	struct mdoc_bl	*bl;
820 	enum htmltag	 elemtype;
821 
822 	switch (n->type) {
823 	case ROFFT_BLOCK:
824 		html_close_paragraph(h);
825 		break;
826 	case ROFFT_HEAD:
827 		return 0;
828 	case ROFFT_BODY:
829 		return 1;
830 	default:
831 		abort();
832 	}
833 
834 	bl = &n->norm->Bl;
835 	switch (bl->type) {
836 	case LIST_bullet:
837 		elemtype = TAG_UL;
838 		(void)strlcpy(cattr, "Bl-bullet", sizeof(cattr));
839 		break;
840 	case LIST_dash:
841 	case LIST_hyphen:
842 		elemtype = TAG_UL;
843 		(void)strlcpy(cattr, "Bl-dash", sizeof(cattr));
844 		break;
845 	case LIST_item:
846 		elemtype = TAG_UL;
847 		(void)strlcpy(cattr, "Bl-item", sizeof(cattr));
848 		break;
849 	case LIST_enum:
850 		elemtype = TAG_OL;
851 		(void)strlcpy(cattr, "Bl-enum", sizeof(cattr));
852 		break;
853 	case LIST_diag:
854 		elemtype = TAG_DL;
855 		(void)strlcpy(cattr, "Bl-diag", sizeof(cattr));
856 		break;
857 	case LIST_hang:
858 		elemtype = TAG_DL;
859 		(void)strlcpy(cattr, "Bl-hang", sizeof(cattr));
860 		break;
861 	case LIST_inset:
862 		elemtype = TAG_DL;
863 		(void)strlcpy(cattr, "Bl-inset", sizeof(cattr));
864 		break;
865 	case LIST_ohang:
866 		elemtype = TAG_DL;
867 		(void)strlcpy(cattr, "Bl-ohang", sizeof(cattr));
868 		break;
869 	case LIST_tag:
870 		if (bl->offs)
871 			print_otag(h, TAG_DIV, "c", "Bd-indent");
872 		print_otag_id(h, TAG_DL,
873 		    bl->comp ? "Bl-tag Bl-compact" : "Bl-tag", n->body);
874 		return 1;
875 	case LIST_column:
876 		elemtype = TAG_TABLE;
877 		(void)strlcpy(cattr, "Bl-column", sizeof(cattr));
878 		break;
879 	default:
880 		abort();
881 	}
882 	if (bl->offs != NULL)
883 		(void)strlcat(cattr, " Bd-indent", sizeof(cattr));
884 	if (bl->comp)
885 		(void)strlcat(cattr, " Bl-compact", sizeof(cattr));
886 	print_otag_id(h, elemtype, cattr, n->body);
887 	return 1;
888 }
889 
890 static int
891 mdoc_ex_pre(MDOC_ARGS)
892 {
893 	if (roff_node_prev(n) != NULL)
894 		print_otag(h, TAG_BR, "");
895 	return 1;
896 }
897 
898 static int
899 mdoc_st_pre(MDOC_ARGS)
900 {
901 	print_otag(h, TAG_SPAN, "c", "St");
902 	return 1;
903 }
904 
905 static int
906 mdoc_em_pre(MDOC_ARGS)
907 {
908 	print_otag_id(h, TAG_I, "Em", n);
909 	return 1;
910 }
911 
912 static int
913 mdoc_d1_pre(MDOC_ARGS)
914 {
915 	switch (n->type) {
916 	case ROFFT_BLOCK:
917 		html_close_paragraph(h);
918 		return 1;
919 	case ROFFT_HEAD:
920 		return 0;
921 	case ROFFT_BODY:
922 		break;
923 	default:
924 		abort();
925 	}
926 	print_otag_id(h, TAG_DIV, "Bd Bd-indent", n);
927 	if (n->tok == MDOC_Dl)
928 		print_otag(h, TAG_CODE, "c", "Li");
929 	return 1;
930 }
931 
932 static int
933 mdoc_sx_pre(MDOC_ARGS)
934 {
935 	char	*id;
936 
937 	id = html_make_id(n, 0);
938 	print_otag(h, TAG_A, "chR", "Sx", id);
939 	free(id);
940 	return 1;
941 }
942 
943 static int
944 mdoc_bd_pre(MDOC_ARGS)
945 {
946 	char			 buf[20];
947 	struct roff_node	*nn;
948 	int			 comp;
949 
950 	switch (n->type) {
951 	case ROFFT_BLOCK:
952 		html_close_paragraph(h);
953 		return 1;
954 	case ROFFT_HEAD:
955 		return 0;
956 	case ROFFT_BODY:
957 		break;
958 	default:
959 		abort();
960 	}
961 
962 	/* Handle preceding whitespace. */
963 
964 	comp = n->norm->Bd.comp;
965 	for (nn = n; nn != NULL && comp == 0; nn = nn->parent) {
966 		if (nn->type != ROFFT_BLOCK)
967 			continue;
968 		if (nn->tok == MDOC_Sh || nn->tok == MDOC_Ss)
969 			comp = 1;
970 		if (roff_node_prev(nn) != NULL)
971 			break;
972 	}
973 	(void)strlcpy(buf, "Bd", sizeof(buf));
974 	if (comp == 0)
975 		(void)strlcat(buf, " Pp", sizeof(buf));
976 
977 	/* Handle the -offset argument. */
978 
979 	if (n->norm->Bd.offs != NULL &&
980 	    strcmp(n->norm->Bd.offs, "left") != 0)
981 		(void)strlcat(buf, " Bd-indent", sizeof(buf));
982 
983 	if (n->norm->Bd.type == DISP_literal)
984 		(void)strlcat(buf, " Li", sizeof(buf));
985 
986 	print_otag_id(h, TAG_DIV, buf, n);
987 	return 1;
988 }
989 
990 static int
991 mdoc_pa_pre(MDOC_ARGS)
992 {
993 	print_otag(h, TAG_SPAN, "c", "Pa");
994 	return 1;
995 }
996 
997 static int
998 mdoc_ad_pre(MDOC_ARGS)
999 {
1000 	print_otag(h, TAG_SPAN, "c", "Ad");
1001 	return 1;
1002 }
1003 
1004 static int
1005 mdoc_an_pre(MDOC_ARGS)
1006 {
1007 	if (n->norm->An.auth == AUTH_split) {
1008 		h->flags &= ~HTML_NOSPLIT;
1009 		h->flags |= HTML_SPLIT;
1010 		return 0;
1011 	}
1012 	if (n->norm->An.auth == AUTH_nosplit) {
1013 		h->flags &= ~HTML_SPLIT;
1014 		h->flags |= HTML_NOSPLIT;
1015 		return 0;
1016 	}
1017 
1018 	if (h->flags & HTML_SPLIT)
1019 		print_otag(h, TAG_BR, "");
1020 
1021 	if (n->sec == SEC_AUTHORS && ! (h->flags & HTML_NOSPLIT))
1022 		h->flags |= HTML_SPLIT;
1023 
1024 	print_otag(h, TAG_SPAN, "c", "An");
1025 	return 1;
1026 }
1027 
1028 static int
1029 mdoc_cd_pre(MDOC_ARGS)
1030 {
1031 	synopsis_pre(h, n);
1032 	print_otag(h, TAG_CODE, "c", "Cd");
1033 	return 1;
1034 }
1035 
1036 static int
1037 mdoc_fa_pre(MDOC_ARGS)
1038 {
1039 	const struct roff_node	*nn;
1040 	struct tag		*t;
1041 
1042 	if (n->parent->tok != MDOC_Fo) {
1043 		print_otag(h, TAG_VAR, "c", "Fa");
1044 		return 1;
1045 	}
1046 	for (nn = n->child; nn != NULL; nn = nn->next) {
1047 		t = print_otag(h, TAG_VAR, "c", "Fa");
1048 		print_text(h, nn->string);
1049 		print_tagq(h, t);
1050 		if (nn->next != NULL) {
1051 			h->flags |= HTML_NOSPACE;
1052 			print_text(h, ",");
1053 		}
1054 	}
1055 	if (n->child != NULL &&
1056 	    (nn = roff_node_next(n)) != NULL &&
1057 	    nn->tok == MDOC_Fa) {
1058 		h->flags |= HTML_NOSPACE;
1059 		print_text(h, ",");
1060 	}
1061 	return 0;
1062 }
1063 
1064 static int
1065 mdoc_fd_pre(MDOC_ARGS)
1066 {
1067 	struct tag	*t;
1068 	char		*buf, *cp;
1069 
1070 	synopsis_pre(h, n);
1071 
1072 	if (NULL == (n = n->child))
1073 		return 0;
1074 
1075 	assert(n->type == ROFFT_TEXT);
1076 
1077 	if (strcmp(n->string, "#include")) {
1078 		print_otag(h, TAG_CODE, "c", "Fd");
1079 		return 1;
1080 	}
1081 
1082 	print_otag(h, TAG_CODE, "c", "In");
1083 	print_text(h, n->string);
1084 
1085 	if (NULL != (n = n->next)) {
1086 		assert(n->type == ROFFT_TEXT);
1087 
1088 		if (h->base_includes) {
1089 			cp = n->string;
1090 			if (*cp == '<' || *cp == '"')
1091 				cp++;
1092 			buf = mandoc_strdup(cp);
1093 			cp = strchr(buf, '\0') - 1;
1094 			if (cp >= buf && (*cp == '>' || *cp == '"'))
1095 				*cp = '\0';
1096 			t = print_otag(h, TAG_A, "chI", "In", buf);
1097 			free(buf);
1098 		} else
1099 			t = print_otag(h, TAG_A, "c", "In");
1100 
1101 		print_text(h, n->string);
1102 		print_tagq(h, t);
1103 
1104 		n = n->next;
1105 	}
1106 
1107 	for ( ; n; n = n->next) {
1108 		assert(n->type == ROFFT_TEXT);
1109 		print_text(h, n->string);
1110 	}
1111 
1112 	return 0;
1113 }
1114 
1115 static int
1116 mdoc_vt_pre(MDOC_ARGS)
1117 {
1118 	if (n->type == ROFFT_BLOCK) {
1119 		synopsis_pre(h, n);
1120 		return 1;
1121 	} else if (n->type == ROFFT_ELEM) {
1122 		synopsis_pre(h, n);
1123 	} else if (n->type == ROFFT_HEAD)
1124 		return 0;
1125 
1126 	print_otag(h, TAG_VAR, "c", "Vt");
1127 	return 1;
1128 }
1129 
1130 static int
1131 mdoc_ft_pre(MDOC_ARGS)
1132 {
1133 	synopsis_pre(h, n);
1134 	print_otag(h, TAG_VAR, "c", "Ft");
1135 	return 1;
1136 }
1137 
1138 static int
1139 mdoc_fn_pre(MDOC_ARGS)
1140 {
1141 	struct tag	*t;
1142 	char		 nbuf[BUFSIZ];
1143 	const char	*sp, *ep;
1144 	int		 sz, pretty;
1145 
1146 	pretty = NODE_SYNPRETTY & n->flags;
1147 	synopsis_pre(h, n);
1148 
1149 	/* Split apart into type and name. */
1150 	assert(n->child->string);
1151 	sp = n->child->string;
1152 
1153 	ep = strchr(sp, ' ');
1154 	if (NULL != ep) {
1155 		t = print_otag(h, TAG_VAR, "c", "Ft");
1156 
1157 		while (ep) {
1158 			sz = MIN((int)(ep - sp), BUFSIZ - 1);
1159 			(void)memcpy(nbuf, sp, (size_t)sz);
1160 			nbuf[sz] = '\0';
1161 			print_text(h, nbuf);
1162 			sp = ++ep;
1163 			ep = strchr(sp, ' ');
1164 		}
1165 		print_tagq(h, t);
1166 	}
1167 
1168 	t = print_otag_id(h, TAG_CODE, "Fn", n);
1169 
1170 	if (sp)
1171 		print_text(h, sp);
1172 
1173 	print_tagq(h, t);
1174 
1175 	h->flags |= HTML_NOSPACE;
1176 	print_text(h, "(");
1177 	h->flags |= HTML_NOSPACE;
1178 
1179 	for (n = n->child->next; n; n = n->next) {
1180 		if (NODE_SYNPRETTY & n->flags)
1181 			t = print_otag(h, TAG_VAR, "cs", "Fa",
1182 			    "white-space", "nowrap");
1183 		else
1184 			t = print_otag(h, TAG_VAR, "c", "Fa");
1185 		print_text(h, n->string);
1186 		print_tagq(h, t);
1187 		if (n->next) {
1188 			h->flags |= HTML_NOSPACE;
1189 			print_text(h, ",");
1190 		}
1191 	}
1192 
1193 	h->flags |= HTML_NOSPACE;
1194 	print_text(h, ")");
1195 
1196 	if (pretty) {
1197 		h->flags |= HTML_NOSPACE;
1198 		print_text(h, ";");
1199 	}
1200 
1201 	return 0;
1202 }
1203 
1204 static int
1205 mdoc_sm_pre(MDOC_ARGS)
1206 {
1207 
1208 	if (NULL == n->child)
1209 		h->flags ^= HTML_NONOSPACE;
1210 	else if (0 == strcmp("on", n->child->string))
1211 		h->flags &= ~HTML_NONOSPACE;
1212 	else
1213 		h->flags |= HTML_NONOSPACE;
1214 
1215 	if ( ! (HTML_NONOSPACE & h->flags))
1216 		h->flags &= ~HTML_NOSPACE;
1217 
1218 	return 0;
1219 }
1220 
1221 static int
1222 mdoc_skip_pre(MDOC_ARGS)
1223 {
1224 
1225 	return 0;
1226 }
1227 
1228 static int
1229 mdoc_pp_pre(MDOC_ARGS)
1230 {
1231 	char	*id;
1232 
1233 	if (n->flags & NODE_NOFILL) {
1234 		print_endline(h);
1235 		if (n->flags & NODE_ID)
1236 			mdoc_tg_pre(meta, n, h);
1237 		else {
1238 			h->col = 1;
1239 			print_endline(h);
1240 		}
1241 	} else {
1242 		html_close_paragraph(h);
1243 		id = n->flags & NODE_ID ? html_make_id(n, 1) : NULL;
1244 		print_otag(h, TAG_P, "ci", "Pp", id);
1245 		free(id);
1246 	}
1247 	return 0;
1248 }
1249 
1250 static int
1251 mdoc_lk_pre(MDOC_ARGS)
1252 {
1253 	const struct roff_node *link, *descr, *punct;
1254 	struct tag	*t;
1255 
1256 	if ((link = n->child) == NULL)
1257 		return 0;
1258 
1259 	/* Find beginning of trailing punctuation. */
1260 	punct = n->last;
1261 	while (punct != link && punct->flags & NODE_DELIMC)
1262 		punct = punct->prev;
1263 	punct = punct->next;
1264 
1265 	/* Link target and link text. */
1266 	descr = link->next;
1267 	if (descr == punct)
1268 		descr = link;  /* no text */
1269 	t = print_otag(h, TAG_A, "ch", "Lk", link->string);
1270 	do {
1271 		if (descr->flags & (NODE_DELIMC | NODE_DELIMO))
1272 			h->flags |= HTML_NOSPACE;
1273 		print_text(h, descr->string);
1274 		descr = descr->next;
1275 	} while (descr != punct);
1276 	print_tagq(h, t);
1277 
1278 	/* Trailing punctuation. */
1279 	while (punct != NULL) {
1280 		h->flags |= HTML_NOSPACE;
1281 		print_text(h, punct->string);
1282 		punct = punct->next;
1283 	}
1284 	return 0;
1285 }
1286 
1287 static int
1288 mdoc_mt_pre(MDOC_ARGS)
1289 {
1290 	struct tag	*t;
1291 	char		*cp;
1292 
1293 	for (n = n->child; n; n = n->next) {
1294 		assert(n->type == ROFFT_TEXT);
1295 		mandoc_asprintf(&cp, "mailto:%s", n->string);
1296 		t = print_otag(h, TAG_A, "ch", "Mt", cp);
1297 		print_text(h, n->string);
1298 		print_tagq(h, t);
1299 		free(cp);
1300 	}
1301 	return 0;
1302 }
1303 
1304 static int
1305 mdoc_fo_pre(MDOC_ARGS)
1306 {
1307 	struct tag	*t;
1308 
1309 	switch (n->type) {
1310 	case ROFFT_BLOCK:
1311 		synopsis_pre(h, n);
1312 		return 1;
1313 	case ROFFT_HEAD:
1314 		if (n->child != NULL) {
1315 			t = print_otag_id(h, TAG_CODE, "Fn", n);
1316 			print_text(h, n->child->string);
1317 			print_tagq(h, t);
1318 		}
1319 		return 0;
1320 	case ROFFT_BODY:
1321 		h->flags |= HTML_NOSPACE;
1322 		print_text(h, "(");
1323 		h->flags |= HTML_NOSPACE;
1324 		return 1;
1325 	default:
1326 		abort();
1327 	}
1328 }
1329 
1330 static void
1331 mdoc_fo_post(MDOC_ARGS)
1332 {
1333 	if (n->type != ROFFT_BODY)
1334 		return;
1335 	h->flags |= HTML_NOSPACE;
1336 	print_text(h, ")");
1337 	h->flags |= HTML_NOSPACE;
1338 	print_text(h, ";");
1339 }
1340 
1341 static int
1342 mdoc_in_pre(MDOC_ARGS)
1343 {
1344 	struct tag	*t;
1345 
1346 	synopsis_pre(h, n);
1347 	print_otag(h, TAG_CODE, "c", "In");
1348 
1349 	/*
1350 	 * The first argument of the `In' gets special treatment as
1351 	 * being a linked value.  Subsequent values are printed
1352 	 * afterward.  groff does similarly.  This also handles the case
1353 	 * of no children.
1354 	 */
1355 
1356 	if (NODE_SYNPRETTY & n->flags && NODE_LINE & n->flags)
1357 		print_text(h, "#include");
1358 
1359 	print_text(h, "<");
1360 	h->flags |= HTML_NOSPACE;
1361 
1362 	if (NULL != (n = n->child)) {
1363 		assert(n->type == ROFFT_TEXT);
1364 
1365 		if (h->base_includes)
1366 			t = print_otag(h, TAG_A, "chI", "In", n->string);
1367 		else
1368 			t = print_otag(h, TAG_A, "c", "In");
1369 		print_text(h, n->string);
1370 		print_tagq(h, t);
1371 
1372 		n = n->next;
1373 	}
1374 
1375 	h->flags |= HTML_NOSPACE;
1376 	print_text(h, ">");
1377 
1378 	for ( ; n; n = n->next) {
1379 		assert(n->type == ROFFT_TEXT);
1380 		print_text(h, n->string);
1381 	}
1382 	return 0;
1383 }
1384 
1385 static int
1386 mdoc_va_pre(MDOC_ARGS)
1387 {
1388 	print_otag(h, TAG_VAR, "c", "Va");
1389 	return 1;
1390 }
1391 
1392 static int
1393 mdoc_ap_pre(MDOC_ARGS)
1394 {
1395 	h->flags |= HTML_NOSPACE;
1396 	print_text(h, "\\(aq");
1397 	h->flags |= HTML_NOSPACE;
1398 	return 1;
1399 }
1400 
1401 static int
1402 mdoc_bf_pre(MDOC_ARGS)
1403 {
1404 	const char	*cattr;
1405 
1406 	switch (n->type) {
1407 	case ROFFT_BLOCK:
1408 		html_close_paragraph(h);
1409 		return 1;
1410 	case ROFFT_HEAD:
1411 		return 0;
1412 	case ROFFT_BODY:
1413 		break;
1414 	default:
1415 		abort();
1416 	}
1417 
1418 	if (FONT_Em == n->norm->Bf.font)
1419 		cattr = "Bf Em";
1420 	else if (FONT_Sy == n->norm->Bf.font)
1421 		cattr = "Bf Sy";
1422 	else if (FONT_Li == n->norm->Bf.font)
1423 		cattr = "Bf Li";
1424 	else
1425 		cattr = "Bf No";
1426 
1427 	/* Cannot use TAG_SPAN because it may contain blocks. */
1428 	print_otag(h, TAG_DIV, "c", cattr);
1429 	return 1;
1430 }
1431 
1432 static int
1433 mdoc_igndelim_pre(MDOC_ARGS)
1434 {
1435 	h->flags |= HTML_IGNDELIM;
1436 	return 1;
1437 }
1438 
1439 static void
1440 mdoc_pf_post(MDOC_ARGS)
1441 {
1442 	if ( ! (n->next == NULL || n->next->flags & NODE_LINE))
1443 		h->flags |= HTML_NOSPACE;
1444 }
1445 
1446 static int
1447 mdoc_rs_pre(MDOC_ARGS)
1448 {
1449 	switch (n->type) {
1450 	case ROFFT_BLOCK:
1451 		if (n->sec == SEC_SEE_ALSO)
1452 			html_close_paragraph(h);
1453 		break;
1454 	case ROFFT_HEAD:
1455 		return 0;
1456 	case ROFFT_BODY:
1457 		if (n->sec == SEC_SEE_ALSO)
1458 			print_otag(h, TAG_P, "c", "Pp");
1459 		print_otag(h, TAG_CITE, "c", "Rs");
1460 		break;
1461 	default:
1462 		abort();
1463 	}
1464 	return 1;
1465 }
1466 
1467 static int
1468 mdoc_no_pre(MDOC_ARGS)
1469 {
1470 	print_otag_id(h, TAG_SPAN, roff_name[n->tok], n);
1471 	return 1;
1472 }
1473 
1474 static int
1475 mdoc_sy_pre(MDOC_ARGS)
1476 {
1477 	print_otag_id(h, TAG_B, "Sy", n);
1478 	return 1;
1479 }
1480 
1481 static int
1482 mdoc_lb_pre(MDOC_ARGS)
1483 {
1484 	if (n->sec == SEC_LIBRARY &&
1485 	    n->flags & NODE_LINE &&
1486 	    roff_node_prev(n) != NULL)
1487 		print_otag(h, TAG_BR, "");
1488 
1489 	print_otag(h, TAG_SPAN, "c", "Lb");
1490 	return 1;
1491 }
1492 
1493 static int
1494 mdoc__x_pre(MDOC_ARGS)
1495 {
1496 	struct roff_node	*nn;
1497 	const char		*cattr;
1498 	enum htmltag		 t;
1499 
1500 	t = TAG_SPAN;
1501 
1502 	switch (n->tok) {
1503 	case MDOC__A:
1504 		cattr = "RsA";
1505 		if ((nn = roff_node_prev(n)) != NULL && nn->tok == MDOC__A &&
1506 		    ((nn = roff_node_next(n)) == NULL || nn->tok != MDOC__A))
1507 			print_text(h, "and");
1508 		break;
1509 	case MDOC__B:
1510 		t = TAG_I;
1511 		cattr = "RsB";
1512 		break;
1513 	case MDOC__C:
1514 		cattr = "RsC";
1515 		break;
1516 	case MDOC__D:
1517 		cattr = "RsD";
1518 		break;
1519 	case MDOC__I:
1520 		t = TAG_I;
1521 		cattr = "RsI";
1522 		break;
1523 	case MDOC__J:
1524 		t = TAG_I;
1525 		cattr = "RsJ";
1526 		break;
1527 	case MDOC__N:
1528 		cattr = "RsN";
1529 		break;
1530 	case MDOC__O:
1531 		cattr = "RsO";
1532 		break;
1533 	case MDOC__P:
1534 		cattr = "RsP";
1535 		break;
1536 	case MDOC__Q:
1537 		cattr = "RsQ";
1538 		break;
1539 	case MDOC__R:
1540 		cattr = "RsR";
1541 		break;
1542 	case MDOC__T:
1543 		cattr = "RsT";
1544 		break;
1545 	case MDOC__U:
1546 		print_otag(h, TAG_A, "ch", "RsU", n->child->string);
1547 		return 1;
1548 	case MDOC__V:
1549 		cattr = "RsV";
1550 		break;
1551 	default:
1552 		abort();
1553 	}
1554 
1555 	print_otag(h, t, "c", cattr);
1556 	return 1;
1557 }
1558 
1559 static void
1560 mdoc__x_post(MDOC_ARGS)
1561 {
1562 	struct roff_node *nn;
1563 
1564 	if (n->tok == MDOC__A &&
1565 	    (nn = roff_node_next(n)) != NULL && nn->tok == MDOC__A &&
1566 	    ((nn = roff_node_next(nn)) == NULL || nn->tok != MDOC__A) &&
1567 	    ((nn = roff_node_prev(n)) == NULL || nn->tok != MDOC__A))
1568 		return;
1569 
1570 	/* TODO: %U */
1571 
1572 	if (n->parent == NULL || n->parent->tok != MDOC_Rs)
1573 		return;
1574 
1575 	h->flags |= HTML_NOSPACE;
1576 	print_text(h, roff_node_next(n) ? "," : ".");
1577 }
1578 
1579 static int
1580 mdoc_bk_pre(MDOC_ARGS)
1581 {
1582 
1583 	switch (n->type) {
1584 	case ROFFT_BLOCK:
1585 		break;
1586 	case ROFFT_HEAD:
1587 		return 0;
1588 	case ROFFT_BODY:
1589 		if (n->parent->args != NULL || n->prev->child == NULL)
1590 			h->flags |= HTML_PREKEEP;
1591 		break;
1592 	default:
1593 		abort();
1594 	}
1595 
1596 	return 1;
1597 }
1598 
1599 static void
1600 mdoc_bk_post(MDOC_ARGS)
1601 {
1602 
1603 	if (n->type == ROFFT_BODY)
1604 		h->flags &= ~(HTML_KEEP | HTML_PREKEEP);
1605 }
1606 
1607 static int
1608 mdoc_quote_pre(MDOC_ARGS)
1609 {
1610 	if (n->type != ROFFT_BODY)
1611 		return 1;
1612 
1613 	switch (n->tok) {
1614 	case MDOC_Ao:
1615 	case MDOC_Aq:
1616 		print_text(h, n->child != NULL && n->child->next == NULL &&
1617 		    n->child->tok == MDOC_Mt ?  "<" : "\\(la");
1618 		break;
1619 	case MDOC_Bro:
1620 	case MDOC_Brq:
1621 		print_text(h, "\\(lC");
1622 		break;
1623 	case MDOC_Bo:
1624 	case MDOC_Bq:
1625 		print_text(h, "\\(lB");
1626 		break;
1627 	case MDOC_Oo:
1628 	case MDOC_Op:
1629 		print_text(h, "\\(lB");
1630 		/*
1631 		 * Give up on semantic markup for now.
1632 		 * We cannot use TAG_SPAN because .Oo may contain blocks.
1633 		 * We cannot use TAG_DIV because we might be in a
1634 		 * phrasing context (like .Dl or .Pp); we cannot
1635 		 * close out a .Pp at this point either because
1636 		 * that would break the line.
1637 		 */
1638 		/* XXX print_otag(h, TAG_???, "c", "Op"); */
1639 		break;
1640 	case MDOC_En:
1641 		if (NULL == n->norm->Es ||
1642 		    NULL == n->norm->Es->child)
1643 			return 1;
1644 		print_text(h, n->norm->Es->child->string);
1645 		break;
1646 	case MDOC_Do:
1647 	case MDOC_Dq:
1648 		print_text(h, "\\(lq");
1649 		break;
1650 	case MDOC_Qo:
1651 	case MDOC_Qq:
1652 		print_text(h, "\"");
1653 		break;
1654 	case MDOC_Po:
1655 	case MDOC_Pq:
1656 		print_text(h, "(");
1657 		break;
1658 	case MDOC_Ql:
1659 		print_text(h, "\\(oq");
1660 		h->flags |= HTML_NOSPACE;
1661 		print_otag(h, TAG_CODE, "c", "Li");
1662 		break;
1663 	case MDOC_So:
1664 	case MDOC_Sq:
1665 		print_text(h, "\\(oq");
1666 		break;
1667 	default:
1668 		abort();
1669 	}
1670 
1671 	h->flags |= HTML_NOSPACE;
1672 	return 1;
1673 }
1674 
1675 static void
1676 mdoc_quote_post(MDOC_ARGS)
1677 {
1678 
1679 	if (n->type != ROFFT_BODY && n->type != ROFFT_ELEM)
1680 		return;
1681 
1682 	h->flags |= HTML_NOSPACE;
1683 
1684 	switch (n->tok) {
1685 	case MDOC_Ao:
1686 	case MDOC_Aq:
1687 		print_text(h, n->child != NULL && n->child->next == NULL &&
1688 		    n->child->tok == MDOC_Mt ?  ">" : "\\(ra");
1689 		break;
1690 	case MDOC_Bro:
1691 	case MDOC_Brq:
1692 		print_text(h, "\\(rC");
1693 		break;
1694 	case MDOC_Oo:
1695 	case MDOC_Op:
1696 	case MDOC_Bo:
1697 	case MDOC_Bq:
1698 		print_text(h, "\\(rB");
1699 		break;
1700 	case MDOC_En:
1701 		if (n->norm->Es == NULL ||
1702 		    n->norm->Es->child == NULL ||
1703 		    n->norm->Es->child->next == NULL)
1704 			h->flags &= ~HTML_NOSPACE;
1705 		else
1706 			print_text(h, n->norm->Es->child->next->string);
1707 		break;
1708 	case MDOC_Do:
1709 	case MDOC_Dq:
1710 		print_text(h, "\\(rq");
1711 		break;
1712 	case MDOC_Qo:
1713 	case MDOC_Qq:
1714 		print_text(h, "\"");
1715 		break;
1716 	case MDOC_Po:
1717 	case MDOC_Pq:
1718 		print_text(h, ")");
1719 		break;
1720 	case MDOC_Ql:
1721 	case MDOC_So:
1722 	case MDOC_Sq:
1723 		print_text(h, "\\(cq");
1724 		break;
1725 	default:
1726 		abort();
1727 	}
1728 }
1729 
1730 static int
1731 mdoc_eo_pre(MDOC_ARGS)
1732 {
1733 
1734 	if (n->type != ROFFT_BODY)
1735 		return 1;
1736 
1737 	if (n->end == ENDBODY_NOT &&
1738 	    n->parent->head->child == NULL &&
1739 	    n->child != NULL &&
1740 	    n->child->end != ENDBODY_NOT)
1741 		print_text(h, "\\&");
1742 	else if (n->end != ENDBODY_NOT ? n->child != NULL :
1743 	    n->parent->head->child != NULL && (n->child != NULL ||
1744 	    (n->parent->tail != NULL && n->parent->tail->child != NULL)))
1745 		h->flags |= HTML_NOSPACE;
1746 	return 1;
1747 }
1748 
1749 static void
1750 mdoc_eo_post(MDOC_ARGS)
1751 {
1752 	int	 body, tail;
1753 
1754 	if (n->type != ROFFT_BODY)
1755 		return;
1756 
1757 	if (n->end != ENDBODY_NOT) {
1758 		h->flags &= ~HTML_NOSPACE;
1759 		return;
1760 	}
1761 
1762 	body = n->child != NULL || n->parent->head->child != NULL;
1763 	tail = n->parent->tail != NULL && n->parent->tail->child != NULL;
1764 
1765 	if (body && tail)
1766 		h->flags |= HTML_NOSPACE;
1767 	else if ( ! tail)
1768 		h->flags &= ~HTML_NOSPACE;
1769 }
1770 
1771 static int
1772 mdoc_abort_pre(MDOC_ARGS)
1773 {
1774 	abort();
1775 }
1776