xref: /freebsd/contrib/mandoc/mdoc_html.c (revision b5daf675efc746611c7cfcd1fa474b8905064c4b)
1 /* $Id: mdoc_html.c,v 1.354 2025/06/26 17:06:34 schwarze Exp $ */
2 /*
3  * Copyright (c) 2014-2022, 2025 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 	char		*title;
458 
459 	assert(meta->title != NULL);
460 	if (meta->msec == NULL)
461 		title = mandoc_strdup(meta->title);
462 	else
463 		mandoc_asprintf(&title, "%s(%s)", meta->title, meta->msec);
464 
465 	t = print_otag(h, TAG_DIV, "cr?", "foot", "doc-pagefooter",
466 	    "aria-label", "Manual footer line");
467 
468 	print_otag(h, TAG_SPAN, "c", "foot-left");
469 	print_text(h, meta->os);
470 	print_stagq(h, t);
471 
472 	print_otag(h, TAG_SPAN, "c", "foot-date");
473 	print_text(h, meta->date);
474 	print_stagq(h, t);
475 
476 	print_otag(h, TAG_SPAN, "c", "foot-right");
477 	print_text(h, title);
478 	print_tagq(h, t);
479 	free(title);
480 }
481 
482 static int
483 mdoc_root_pre(const struct roff_meta *meta, struct html *h)
484 {
485 	struct tag	*t;
486 	char		*volume, *title;
487 
488 	if (NULL == meta->arch)
489 		volume = mandoc_strdup(meta->vol);
490 	else
491 		mandoc_asprintf(&volume, "%s (%s)",
492 		    meta->vol, meta->arch);
493 
494 	if (NULL == meta->msec)
495 		title = mandoc_strdup(meta->title);
496 	else
497 		mandoc_asprintf(&title, "%s(%s)",
498 		    meta->title, meta->msec);
499 
500 	t = print_otag(h, TAG_DIV, "cr?", "head", "doc-pageheader",
501 	    "aria-label", "Manual header line");
502 
503 	print_otag(h, TAG_SPAN, "c", "head-ltitle");
504 	print_text(h, title);
505 	print_stagq(h, t);
506 
507 	print_otag(h, TAG_SPAN, "c", "head-vol");
508 	print_text(h, volume);
509 	print_stagq(h, t);
510 
511 	print_otag(h, TAG_SPAN, "c", "head-rtitle");
512 	print_text(h, title);
513 	print_tagq(h, t);
514 
515 	free(title);
516 	free(volume);
517 	return 1;
518 }
519 
520 static int
521 mdoc_code_pre(MDOC_ARGS)
522 {
523 	print_otag_id(h, TAG_CODE, roff_name[n->tok], n);
524 	return 1;
525 }
526 
527 static int
528 mdoc_sh_pre(MDOC_ARGS)
529 {
530 	struct roff_node	*sn, *subn;
531 	struct tag		*t, *tnav, *tsec, *tsub;
532 	char			*id;
533 	int			 sc;
534 
535 	switch (n->type) {
536 	case ROFFT_BLOCK:
537 		html_close_paragraph(h);
538 		if ((h->oflags & HTML_TOC) == 0 ||
539 		    h->flags & HTML_TOCDONE ||
540 		    n->sec <= SEC_SYNOPSIS) {
541 			print_otag(h, TAG_SECTION, "c", "Sh");
542 			break;
543 		}
544 		h->flags |= HTML_TOCDONE;
545 		sc = 0;
546 		for (sn = n->next; sn != NULL; sn = sn->next)
547 			if (sn->sec == SEC_CUSTOM)
548 				if (++sc == 2)
549 					break;
550 		if (sc < 2)
551 			break;
552 		tnav = print_otag(h, TAG_NAV, "r", "doc-toc");
553 		t = print_otag(h, TAG_H2, "c", "Sh");
554 		print_text(h, "TABLE OF CONTENTS");
555 		print_tagq(h, t);
556 		t = print_otag(h, TAG_UL, "c", "Bl-compact");
557 		for (sn = n; sn != NULL; sn = sn->next) {
558 			tsec = print_otag(h, TAG_LI, "");
559 			id = html_make_id(sn->head, 0);
560 			tsub = print_otag(h, TAG_A, "hR", id);
561 			free(id);
562 			print_mdoc_nodelist(meta, sn->head->child, h);
563 			print_tagq(h, tsub);
564 			tsub = NULL;
565 			for (subn = sn->body->child; subn != NULL;
566 			    subn = subn->next) {
567 				if (subn->tok != MDOC_Ss)
568 					continue;
569 				id = html_make_id(subn->head, 0);
570 				if (id == NULL)
571 					continue;
572 				if (tsub == NULL)
573 					print_otag(h, TAG_UL,
574 					    "c", "Bl-compact");
575 				tsub = print_otag(h, TAG_LI, "");
576 				print_otag(h, TAG_A, "hR", id);
577 				free(id);
578 				print_mdoc_nodelist(meta,
579 				    subn->head->child, h);
580 				print_tagq(h, tsub);
581 			}
582 			print_tagq(h, tsec);
583 		}
584 		print_tagq(h, tnav);
585 		print_otag(h, TAG_SECTION, "c", "Sh");
586 		break;
587 	case ROFFT_HEAD:
588 		print_otag_id(h, TAG_H2, "Sh", n);
589 		break;
590 	case ROFFT_BODY:
591 		if (n->sec == SEC_AUTHORS)
592 			h->flags &= ~(HTML_SPLIT|HTML_NOSPLIT);
593 		break;
594 	default:
595 		break;
596 	}
597 	return 1;
598 }
599 
600 static int
601 mdoc_ss_pre(MDOC_ARGS)
602 {
603 	switch (n->type) {
604 	case ROFFT_BLOCK:
605 		html_close_paragraph(h);
606 		print_otag(h, TAG_SECTION, "c", "Ss");
607 		break;
608 	case ROFFT_HEAD:
609 		print_otag_id(h, TAG_H3, "Ss", n);
610 		break;
611 	case ROFFT_BODY:
612 		break;
613 	default:
614 		abort();
615 	}
616 	return 1;
617 }
618 
619 static int
620 mdoc_fl_pre(MDOC_ARGS)
621 {
622 	struct roff_node	*nn;
623 
624 	print_otag_id(h, TAG_CODE, "Fl", n);
625 	print_text(h, "\\-");
626 	if (n->child != NULL ||
627 	    ((nn = roff_node_next(n)) != NULL &&
628 	     nn->type != ROFFT_TEXT &&
629 	     (nn->flags & NODE_LINE) == 0))
630 		h->flags |= HTML_NOSPACE;
631 
632 	return 1;
633 }
634 
635 static int
636 mdoc_nd_pre(MDOC_ARGS)
637 {
638 	switch (n->type) {
639 	case ROFFT_BLOCK:
640 		return 1;
641 	case ROFFT_HEAD:
642 		return 0;
643 	case ROFFT_BODY:
644 		break;
645 	default:
646 		abort();
647 	}
648 	print_text(h, "\\(em");
649 	print_otag(h, TAG_SPAN, "cr", "Nd", "doc-subtitle");
650 	return 1;
651 }
652 
653 static int
654 mdoc_nm_pre(MDOC_ARGS)
655 {
656 	switch (n->type) {
657 	case ROFFT_BLOCK:
658 		break;
659 	case ROFFT_HEAD:
660 		print_otag(h, TAG_TD, "");
661 		/* FALLTHROUGH */
662 	case ROFFT_ELEM:
663 		print_otag(h, TAG_CODE, "c", "Nm");
664 		return 1;
665 	case ROFFT_BODY:
666 		print_otag(h, TAG_TD, "");
667 		return 1;
668 	default:
669 		abort();
670 	}
671 	html_close_paragraph(h);
672 	synopsis_pre(h, n);
673 	print_otag(h, TAG_TABLE, "c", "Nm");
674 	print_otag(h, TAG_TR, "");
675 	return 1;
676 }
677 
678 static int
679 mdoc_xr_pre(MDOC_ARGS)
680 {
681 	char	*name, *section, *label;
682 
683 	if (n->child == NULL)
684 		return 0;
685 
686 	name = n->child->string;
687 	if (n->child->next != NULL) {
688 		section = n->child->next->string;
689 		mandoc_asprintf(&label, "%s, section %s", name, section);
690 	} else
691 		section = label = NULL;
692 
693 	if (h->base_man1)
694 		print_otag(h, TAG_A, "chM?", "Xr",
695 		    name, section, "aria-label", label);
696 	else
697 		print_otag(h, TAG_A, "c?", "Xr", "aria-label", label);
698 
699 	free(label);
700 	print_text(h, name);
701 
702 	if (section == NULL)
703 		return 0;
704 
705 	h->flags |= HTML_NOSPACE;
706 	print_text(h, "(");
707 	h->flags |= HTML_NOSPACE;
708 	print_text(h, section);
709 	h->flags |= HTML_NOSPACE;
710 	print_text(h, ")");
711 	return 0;
712 }
713 
714 static int
715 mdoc_tg_pre(MDOC_ARGS)
716 {
717 	char	*id;
718 
719 	if ((id = html_make_id(n, 1)) != NULL) {
720 		print_tagq(h, print_otag(h, TAG_MARK, "i", id));
721 		free(id);
722 	}
723 	return 0;
724 }
725 
726 static int
727 mdoc_ns_pre(MDOC_ARGS)
728 {
729 
730 	if ( ! (NODE_LINE & n->flags))
731 		h->flags |= HTML_NOSPACE;
732 	return 1;
733 }
734 
735 static int
736 mdoc_ar_pre(MDOC_ARGS)
737 {
738 	print_otag(h, TAG_VAR, "c", "Ar");
739 	return 1;
740 }
741 
742 static int
743 mdoc_xx_pre(MDOC_ARGS)
744 {
745 	print_otag(h, TAG_SPAN, "c", "Ux");
746 	return 1;
747 }
748 
749 static int
750 mdoc_it_pre(MDOC_ARGS)
751 {
752 	const struct roff_node	*bl;
753 	enum mdoc_list		 type;
754 
755 	bl = n->parent;
756 	while (bl->tok != MDOC_Bl)
757 		bl = bl->parent;
758 	type = bl->norm->Bl.type;
759 
760 	switch (type) {
761 	case LIST_bullet:
762 	case LIST_dash:
763 	case LIST_hyphen:
764 	case LIST_item:
765 	case LIST_enum:
766 		switch (n->type) {
767 		case ROFFT_HEAD:
768 			return 0;
769 		case ROFFT_BODY:
770 			print_otag_id(h, TAG_LI, NULL, n);
771 			break;
772 		default:
773 			break;
774 		}
775 		break;
776 	case LIST_diag:
777 	case LIST_hang:
778 	case LIST_inset:
779 	case LIST_ohang:
780 		switch (n->type) {
781 		case ROFFT_HEAD:
782 			print_otag_id(h, TAG_DT, NULL, n);
783 			break;
784 		case ROFFT_BODY:
785 			print_otag(h, TAG_DD, "");
786 			break;
787 		default:
788 			break;
789 		}
790 		break;
791 	case LIST_tag:
792 		switch (n->type) {
793 		case ROFFT_HEAD:
794 			print_otag_id(h, TAG_DT, NULL, n);
795 			break;
796 		case ROFFT_BODY:
797 			if (n->child == NULL) {
798 				print_otag(h, TAG_DD, "s", "width", "auto");
799 				print_text(h, "\\ ");
800 			} else
801 				print_otag(h, TAG_DD, "");
802 			break;
803 		default:
804 			break;
805 		}
806 		break;
807 	case LIST_column:
808 		switch (n->type) {
809 		case ROFFT_HEAD:
810 			break;
811 		case ROFFT_BODY:
812 			print_otag(h, TAG_TD, "");
813 			break;
814 		default:
815 			print_otag_id(h, TAG_TR, NULL, n);
816 		}
817 	default:
818 		break;
819 	}
820 
821 	return 1;
822 }
823 
824 static int
825 mdoc_bl_pre(MDOC_ARGS)
826 {
827 	char		 cattr[32];
828 	struct mdoc_bl	*bl;
829 	enum htmltag	 elemtype;
830 
831 	switch (n->type) {
832 	case ROFFT_BLOCK:
833 		html_close_paragraph(h);
834 		break;
835 	case ROFFT_HEAD:
836 		return 0;
837 	case ROFFT_BODY:
838 		return 1;
839 	default:
840 		abort();
841 	}
842 
843 	bl = &n->norm->Bl;
844 	switch (bl->type) {
845 	case LIST_bullet:
846 		elemtype = TAG_UL;
847 		(void)strlcpy(cattr, "Bl-bullet", sizeof(cattr));
848 		break;
849 	case LIST_dash:
850 	case LIST_hyphen:
851 		elemtype = TAG_UL;
852 		(void)strlcpy(cattr, "Bl-dash", sizeof(cattr));
853 		break;
854 	case LIST_item:
855 		elemtype = TAG_UL;
856 		(void)strlcpy(cattr, "Bl-item", sizeof(cattr));
857 		break;
858 	case LIST_enum:
859 		elemtype = TAG_OL;
860 		(void)strlcpy(cattr, "Bl-enum", sizeof(cattr));
861 		break;
862 	case LIST_diag:
863 		elemtype = TAG_DL;
864 		(void)strlcpy(cattr, "Bl-diag", sizeof(cattr));
865 		break;
866 	case LIST_hang:
867 		elemtype = TAG_DL;
868 		(void)strlcpy(cattr, "Bl-hang", sizeof(cattr));
869 		break;
870 	case LIST_inset:
871 		elemtype = TAG_DL;
872 		(void)strlcpy(cattr, "Bl-inset", sizeof(cattr));
873 		break;
874 	case LIST_ohang:
875 		elemtype = TAG_DL;
876 		(void)strlcpy(cattr, "Bl-ohang", sizeof(cattr));
877 		break;
878 	case LIST_tag:
879 		if (bl->offs)
880 			print_otag(h, TAG_DIV, "c", "Bd-indent");
881 		print_otag_id(h, TAG_DL,
882 		    bl->comp ? "Bl-tag Bl-compact" : "Bl-tag", n->body);
883 		return 1;
884 	case LIST_column:
885 		elemtype = TAG_TABLE;
886 		(void)strlcpy(cattr, "Bl-column", sizeof(cattr));
887 		break;
888 	default:
889 		abort();
890 	}
891 	if (bl->offs != NULL)
892 		(void)strlcat(cattr, " Bd-indent", sizeof(cattr));
893 	if (bl->comp)
894 		(void)strlcat(cattr, " Bl-compact", sizeof(cattr));
895 	print_otag_id(h, elemtype, cattr, n->body);
896 	return 1;
897 }
898 
899 static int
900 mdoc_ex_pre(MDOC_ARGS)
901 {
902 	if (roff_node_prev(n) != NULL)
903 		print_otag(h, TAG_BR, "");
904 	return 1;
905 }
906 
907 static int
908 mdoc_st_pre(MDOC_ARGS)
909 {
910 	print_otag(h, TAG_SPAN, "c", "St");
911 	return 1;
912 }
913 
914 static int
915 mdoc_em_pre(MDOC_ARGS)
916 {
917 	print_otag_id(h, TAG_I, "Em", n);
918 	return 1;
919 }
920 
921 static int
922 mdoc_d1_pre(MDOC_ARGS)
923 {
924 	switch (n->type) {
925 	case ROFFT_BLOCK:
926 		html_close_paragraph(h);
927 		return 1;
928 	case ROFFT_HEAD:
929 		return 0;
930 	case ROFFT_BODY:
931 		break;
932 	default:
933 		abort();
934 	}
935 	print_otag_id(h, TAG_DIV, "Bd Bd-indent", n);
936 	if (n->tok == MDOC_Dl)
937 		print_otag(h, TAG_CODE, "c", "Li");
938 	return 1;
939 }
940 
941 static int
942 mdoc_sx_pre(MDOC_ARGS)
943 {
944 	char	*id;
945 
946 	id = html_make_id(n, 0);
947 	print_otag(h, TAG_A, "chR", "Sx", id);
948 	free(id);
949 	return 1;
950 }
951 
952 static int
953 mdoc_bd_pre(MDOC_ARGS)
954 {
955 	char			 buf[20];
956 	struct roff_node	*nn;
957 	int			 comp;
958 
959 	switch (n->type) {
960 	case ROFFT_BLOCK:
961 		html_close_paragraph(h);
962 		return 1;
963 	case ROFFT_HEAD:
964 		return 0;
965 	case ROFFT_BODY:
966 		break;
967 	default:
968 		abort();
969 	}
970 
971 	/* Handle preceding whitespace. */
972 
973 	comp = n->norm->Bd.comp;
974 	for (nn = n; nn != NULL && comp == 0; nn = nn->parent) {
975 		if (nn->type != ROFFT_BLOCK)
976 			continue;
977 		if (nn->tok == MDOC_Sh || nn->tok == MDOC_Ss)
978 			comp = 1;
979 		if (roff_node_prev(nn) != NULL)
980 			break;
981 	}
982 	(void)strlcpy(buf, "Bd", sizeof(buf));
983 	if (comp == 0)
984 		(void)strlcat(buf, " Pp", sizeof(buf));
985 
986 	/* Handle the -offset argument. */
987 
988 	if (n->norm->Bd.offs != NULL &&
989 	    strcmp(n->norm->Bd.offs, "left") != 0)
990 		(void)strlcat(buf, " Bd-indent", sizeof(buf));
991 
992 	if (n->norm->Bd.type == DISP_literal)
993 		(void)strlcat(buf, " Li", sizeof(buf));
994 
995 	print_otag_id(h, TAG_DIV, buf, n);
996 	return 1;
997 }
998 
999 static int
1000 mdoc_pa_pre(MDOC_ARGS)
1001 {
1002 	print_otag(h, TAG_SPAN, "c", "Pa");
1003 	return 1;
1004 }
1005 
1006 static int
1007 mdoc_ad_pre(MDOC_ARGS)
1008 {
1009 	print_otag(h, TAG_SPAN, "c", "Ad");
1010 	return 1;
1011 }
1012 
1013 static int
1014 mdoc_an_pre(MDOC_ARGS)
1015 {
1016 	if (n->norm->An.auth == AUTH_split) {
1017 		h->flags &= ~HTML_NOSPLIT;
1018 		h->flags |= HTML_SPLIT;
1019 		return 0;
1020 	}
1021 	if (n->norm->An.auth == AUTH_nosplit) {
1022 		h->flags &= ~HTML_SPLIT;
1023 		h->flags |= HTML_NOSPLIT;
1024 		return 0;
1025 	}
1026 
1027 	if (h->flags & HTML_SPLIT)
1028 		print_otag(h, TAG_BR, "");
1029 
1030 	if (n->sec == SEC_AUTHORS && ! (h->flags & HTML_NOSPLIT))
1031 		h->flags |= HTML_SPLIT;
1032 
1033 	print_otag(h, TAG_SPAN, "c", "An");
1034 	return 1;
1035 }
1036 
1037 static int
1038 mdoc_cd_pre(MDOC_ARGS)
1039 {
1040 	synopsis_pre(h, n);
1041 	print_otag(h, TAG_CODE, "c", "Cd");
1042 	return 1;
1043 }
1044 
1045 static int
1046 mdoc_fa_pre(MDOC_ARGS)
1047 {
1048 	const struct roff_node	*nn;
1049 	struct tag		*t;
1050 
1051 	if (n->parent->tok != MDOC_Fo) {
1052 		print_otag(h, TAG_VAR, "c", "Fa");
1053 		return 1;
1054 	}
1055 	for (nn = n->child; nn != NULL; nn = nn->next) {
1056 		t = print_otag(h, TAG_VAR, "c", "Fa");
1057 		print_text(h, nn->string);
1058 		print_tagq(h, t);
1059 		if (nn->next != NULL) {
1060 			h->flags |= HTML_NOSPACE;
1061 			print_text(h, ",");
1062 		}
1063 	}
1064 	if (n->child != NULL &&
1065 	    (nn = roff_node_next(n)) != NULL &&
1066 	    nn->tok == MDOC_Fa) {
1067 		h->flags |= HTML_NOSPACE;
1068 		print_text(h, ",");
1069 	}
1070 	return 0;
1071 }
1072 
1073 static int
1074 mdoc_fd_pre(MDOC_ARGS)
1075 {
1076 	struct tag	*t;
1077 	char		*buf, *cp;
1078 
1079 	synopsis_pre(h, n);
1080 
1081 	if (NULL == (n = n->child))
1082 		return 0;
1083 
1084 	assert(n->type == ROFFT_TEXT);
1085 
1086 	if (strcmp(n->string, "#include")) {
1087 		print_otag(h, TAG_CODE, "c", "Fd");
1088 		return 1;
1089 	}
1090 
1091 	print_otag(h, TAG_CODE, "c", "In");
1092 	print_text(h, n->string);
1093 
1094 	if (NULL != (n = n->next)) {
1095 		assert(n->type == ROFFT_TEXT);
1096 
1097 		if (h->base_includes) {
1098 			cp = n->string;
1099 			if (*cp == '<' || *cp == '"')
1100 				cp++;
1101 			buf = mandoc_strdup(cp);
1102 			cp = strchr(buf, '\0') - 1;
1103 			if (cp >= buf && (*cp == '>' || *cp == '"'))
1104 				*cp = '\0';
1105 			t = print_otag(h, TAG_A, "chI", "In", buf);
1106 			free(buf);
1107 		} else
1108 			t = print_otag(h, TAG_A, "c", "In");
1109 
1110 		print_text(h, n->string);
1111 		print_tagq(h, t);
1112 
1113 		n = n->next;
1114 	}
1115 
1116 	for ( ; n; n = n->next) {
1117 		assert(n->type == ROFFT_TEXT);
1118 		print_text(h, n->string);
1119 	}
1120 
1121 	return 0;
1122 }
1123 
1124 static int
1125 mdoc_vt_pre(MDOC_ARGS)
1126 {
1127 	if (n->type == ROFFT_BLOCK) {
1128 		synopsis_pre(h, n);
1129 		return 1;
1130 	} else if (n->type == ROFFT_ELEM) {
1131 		synopsis_pre(h, n);
1132 	} else if (n->type == ROFFT_HEAD)
1133 		return 0;
1134 
1135 	print_otag(h, TAG_VAR, "c", "Vt");
1136 	return 1;
1137 }
1138 
1139 static int
1140 mdoc_ft_pre(MDOC_ARGS)
1141 {
1142 	synopsis_pre(h, n);
1143 	print_otag(h, TAG_VAR, "c", "Ft");
1144 	return 1;
1145 }
1146 
1147 static int
1148 mdoc_fn_pre(MDOC_ARGS)
1149 {
1150 	struct tag	*t;
1151 	char		 nbuf[BUFSIZ];
1152 	const char	*sp, *ep;
1153 	int		 sz, pretty;
1154 
1155 	pretty = NODE_SYNPRETTY & n->flags;
1156 	synopsis_pre(h, n);
1157 
1158 	/* Split apart into type and name. */
1159 	assert(n->child->string);
1160 	sp = n->child->string;
1161 
1162 	ep = strchr(sp, ' ');
1163 	if (NULL != ep) {
1164 		t = print_otag(h, TAG_VAR, "c", "Ft");
1165 
1166 		while (ep) {
1167 			sz = MIN((int)(ep - sp), BUFSIZ - 1);
1168 			(void)memcpy(nbuf, sp, (size_t)sz);
1169 			nbuf[sz] = '\0';
1170 			print_text(h, nbuf);
1171 			sp = ++ep;
1172 			ep = strchr(sp, ' ');
1173 		}
1174 		print_tagq(h, t);
1175 	}
1176 
1177 	t = print_otag_id(h, TAG_CODE, "Fn", n);
1178 
1179 	if (sp)
1180 		print_text(h, sp);
1181 
1182 	print_tagq(h, t);
1183 
1184 	h->flags |= HTML_NOSPACE;
1185 	print_text(h, "(");
1186 	h->flags |= HTML_NOSPACE;
1187 
1188 	for (n = n->child->next; n; n = n->next) {
1189 		if (NODE_SYNPRETTY & n->flags)
1190 			t = print_otag(h, TAG_VAR, "cs", "Fa",
1191 			    "white-space", "nowrap");
1192 		else
1193 			t = print_otag(h, TAG_VAR, "c", "Fa");
1194 		print_text(h, n->string);
1195 		print_tagq(h, t);
1196 		if (n->next) {
1197 			h->flags |= HTML_NOSPACE;
1198 			print_text(h, ",");
1199 		}
1200 	}
1201 
1202 	h->flags |= HTML_NOSPACE;
1203 	print_text(h, ")");
1204 
1205 	if (pretty) {
1206 		h->flags |= HTML_NOSPACE;
1207 		print_text(h, ";");
1208 	}
1209 
1210 	return 0;
1211 }
1212 
1213 static int
1214 mdoc_sm_pre(MDOC_ARGS)
1215 {
1216 
1217 	if (NULL == n->child)
1218 		h->flags ^= HTML_NONOSPACE;
1219 	else if (0 == strcmp("on", n->child->string))
1220 		h->flags &= ~HTML_NONOSPACE;
1221 	else
1222 		h->flags |= HTML_NONOSPACE;
1223 
1224 	if ( ! (HTML_NONOSPACE & h->flags))
1225 		h->flags &= ~HTML_NOSPACE;
1226 
1227 	return 0;
1228 }
1229 
1230 static int
1231 mdoc_skip_pre(MDOC_ARGS)
1232 {
1233 
1234 	return 0;
1235 }
1236 
1237 static int
1238 mdoc_pp_pre(MDOC_ARGS)
1239 {
1240 	char	*id;
1241 
1242 	if (n->flags & NODE_NOFILL) {
1243 		print_endline(h);
1244 		if (n->flags & NODE_ID)
1245 			mdoc_tg_pre(meta, n, h);
1246 		else {
1247 			h->col = 1;
1248 			print_endline(h);
1249 		}
1250 	} else {
1251 		html_close_paragraph(h);
1252 		id = n->flags & NODE_ID ? html_make_id(n, 1) : NULL;
1253 		print_otag(h, TAG_P, "ci", "Pp", id);
1254 		free(id);
1255 	}
1256 	return 0;
1257 }
1258 
1259 static int
1260 mdoc_lk_pre(MDOC_ARGS)
1261 {
1262 	const struct roff_node *link, *descr, *punct;
1263 	struct tag	*t;
1264 
1265 	if ((link = n->child) == NULL)
1266 		return 0;
1267 
1268 	/* Find beginning of trailing punctuation. */
1269 	punct = n->last;
1270 	while (punct != link && punct->flags & NODE_DELIMC)
1271 		punct = punct->prev;
1272 	punct = punct->next;
1273 
1274 	/* Link target and link text. */
1275 	descr = link->next;
1276 	if (descr == punct)
1277 		descr = link;  /* no text */
1278 	t = print_otag(h, TAG_A, "ch", "Lk", link->string);
1279 	do {
1280 		if (descr->flags & (NODE_DELIMC | NODE_DELIMO))
1281 			h->flags |= HTML_NOSPACE;
1282 		print_text(h, descr->string);
1283 		descr = descr->next;
1284 	} while (descr != punct);
1285 	print_tagq(h, t);
1286 
1287 	/* Trailing punctuation. */
1288 	while (punct != NULL) {
1289 		h->flags |= HTML_NOSPACE;
1290 		print_text(h, punct->string);
1291 		punct = punct->next;
1292 	}
1293 	return 0;
1294 }
1295 
1296 static int
1297 mdoc_mt_pre(MDOC_ARGS)
1298 {
1299 	struct tag	*t;
1300 	char		*cp;
1301 
1302 	for (n = n->child; n; n = n->next) {
1303 		assert(n->type == ROFFT_TEXT);
1304 		mandoc_asprintf(&cp, "mailto:%s", n->string);
1305 		t = print_otag(h, TAG_A, "ch", "Mt", cp);
1306 		print_text(h, n->string);
1307 		print_tagq(h, t);
1308 		free(cp);
1309 	}
1310 	return 0;
1311 }
1312 
1313 static int
1314 mdoc_fo_pre(MDOC_ARGS)
1315 {
1316 	struct tag	*t;
1317 
1318 	switch (n->type) {
1319 	case ROFFT_BLOCK:
1320 		synopsis_pre(h, n);
1321 		return 1;
1322 	case ROFFT_HEAD:
1323 		if (n->child != NULL) {
1324 			t = print_otag_id(h, TAG_CODE, "Fn", n);
1325 			print_text(h, n->child->string);
1326 			print_tagq(h, t);
1327 		}
1328 		return 0;
1329 	case ROFFT_BODY:
1330 		h->flags |= HTML_NOSPACE;
1331 		print_text(h, "(");
1332 		h->flags |= HTML_NOSPACE;
1333 		return 1;
1334 	default:
1335 		abort();
1336 	}
1337 }
1338 
1339 static void
1340 mdoc_fo_post(MDOC_ARGS)
1341 {
1342 	if (n->type != ROFFT_BODY)
1343 		return;
1344 	h->flags |= HTML_NOSPACE;
1345 	print_text(h, ")");
1346 	h->flags |= HTML_NOSPACE;
1347 	print_text(h, ";");
1348 }
1349 
1350 static int
1351 mdoc_in_pre(MDOC_ARGS)
1352 {
1353 	struct tag	*t;
1354 
1355 	synopsis_pre(h, n);
1356 	print_otag(h, TAG_CODE, "c", "In");
1357 
1358 	/*
1359 	 * The first argument of the `In' gets special treatment as
1360 	 * being a linked value.  Subsequent values are printed
1361 	 * afterward.  groff does similarly.  This also handles the case
1362 	 * of no children.
1363 	 */
1364 
1365 	if (NODE_SYNPRETTY & n->flags && NODE_LINE & n->flags)
1366 		print_text(h, "#include");
1367 
1368 	print_text(h, "<");
1369 	h->flags |= HTML_NOSPACE;
1370 
1371 	if (NULL != (n = n->child)) {
1372 		assert(n->type == ROFFT_TEXT);
1373 
1374 		if (h->base_includes)
1375 			t = print_otag(h, TAG_A, "chI", "In", n->string);
1376 		else
1377 			t = print_otag(h, TAG_A, "c", "In");
1378 		print_text(h, n->string);
1379 		print_tagq(h, t);
1380 
1381 		n = n->next;
1382 	}
1383 
1384 	h->flags |= HTML_NOSPACE;
1385 	print_text(h, ">");
1386 
1387 	for ( ; n; n = n->next) {
1388 		assert(n->type == ROFFT_TEXT);
1389 		print_text(h, n->string);
1390 	}
1391 	return 0;
1392 }
1393 
1394 static int
1395 mdoc_va_pre(MDOC_ARGS)
1396 {
1397 	print_otag(h, TAG_VAR, "c", "Va");
1398 	return 1;
1399 }
1400 
1401 static int
1402 mdoc_ap_pre(MDOC_ARGS)
1403 {
1404 	h->flags |= HTML_NOSPACE;
1405 	print_text(h, "\\(aq");
1406 	h->flags |= HTML_NOSPACE;
1407 	return 1;
1408 }
1409 
1410 static int
1411 mdoc_bf_pre(MDOC_ARGS)
1412 {
1413 	const char	*cattr;
1414 
1415 	switch (n->type) {
1416 	case ROFFT_BLOCK:
1417 		html_close_paragraph(h);
1418 		return 1;
1419 	case ROFFT_HEAD:
1420 		return 0;
1421 	case ROFFT_BODY:
1422 		break;
1423 	default:
1424 		abort();
1425 	}
1426 
1427 	if (FONT_Em == n->norm->Bf.font)
1428 		cattr = "Bf Em";
1429 	else if (FONT_Sy == n->norm->Bf.font)
1430 		cattr = "Bf Sy";
1431 	else if (FONT_Li == n->norm->Bf.font)
1432 		cattr = "Bf Li";
1433 	else
1434 		cattr = "Bf No";
1435 
1436 	/* Cannot use TAG_SPAN because it may contain blocks. */
1437 	print_otag(h, TAG_DIV, "c", cattr);
1438 	return 1;
1439 }
1440 
1441 static int
1442 mdoc_igndelim_pre(MDOC_ARGS)
1443 {
1444 	h->flags |= HTML_IGNDELIM;
1445 	return 1;
1446 }
1447 
1448 static void
1449 mdoc_pf_post(MDOC_ARGS)
1450 {
1451 	if ( ! (n->next == NULL || n->next->flags & NODE_LINE))
1452 		h->flags |= HTML_NOSPACE;
1453 }
1454 
1455 static int
1456 mdoc_rs_pre(MDOC_ARGS)
1457 {
1458 	switch (n->type) {
1459 	case ROFFT_BLOCK:
1460 		if (n->sec == SEC_SEE_ALSO)
1461 			html_close_paragraph(h);
1462 		break;
1463 	case ROFFT_HEAD:
1464 		return 0;
1465 	case ROFFT_BODY:
1466 		if (n->sec == SEC_SEE_ALSO)
1467 			print_otag(h, TAG_P, "c", "Pp");
1468 		print_otag(h, TAG_SPAN, "c", "Rs");
1469 		break;
1470 	default:
1471 		abort();
1472 	}
1473 	return 1;
1474 }
1475 
1476 static int
1477 mdoc_no_pre(MDOC_ARGS)
1478 {
1479 	print_otag_id(h, TAG_SPAN, roff_name[n->tok], n);
1480 	return 1;
1481 }
1482 
1483 static int
1484 mdoc_sy_pre(MDOC_ARGS)
1485 {
1486 	print_otag_id(h, TAG_B, "Sy", n);
1487 	return 1;
1488 }
1489 
1490 static int
1491 mdoc_lb_pre(MDOC_ARGS)
1492 {
1493 	if (n->sec == SEC_LIBRARY &&
1494 	    n->flags & NODE_LINE &&
1495 	    roff_node_prev(n) != NULL)
1496 		print_otag(h, TAG_BR, "");
1497 
1498 	print_otag(h, TAG_SPAN, "c", "Lb");
1499 	return 1;
1500 }
1501 
1502 static int
1503 mdoc__x_pre(MDOC_ARGS)
1504 {
1505 	struct roff_node	*nn;
1506 	const unsigned char	*cp;
1507 	const char		*cattr, *arg;
1508 	char			*url;
1509 	enum htmltag		 t;
1510 
1511 	t = TAG_SPAN;
1512 	arg = n->child->string;
1513 
1514 	switch (n->tok) {
1515 	case MDOC__A:
1516 		cattr = "RsA";
1517 		if ((nn = roff_node_prev(n)) != NULL && nn->tok == MDOC__A &&
1518 		    ((nn = roff_node_next(n)) == NULL || nn->tok != MDOC__A))
1519 			print_text(h, "and");
1520 		break;
1521 	case MDOC__B:
1522 		t = TAG_CITE;
1523 		cattr = "RsB";
1524 		break;
1525 	case MDOC__C:
1526 		cattr = "RsC";
1527 		break;
1528 	case MDOC__D:
1529 		cattr = "RsD";
1530 		break;
1531 	case MDOC__I:
1532 		t = TAG_I;
1533 		cattr = "RsI";
1534 		break;
1535 	case MDOC__J:
1536 		t = TAG_I;
1537 		cattr = "RsJ";
1538 		break;
1539 	case MDOC__N:
1540 		cattr = "RsN";
1541 		break;
1542 	case MDOC__O:
1543 		cattr = "RsO";
1544 		break;
1545 	case MDOC__P:
1546 		cattr = "RsP";
1547 		break;
1548 	case MDOC__Q:
1549 		cattr = "RsQ";
1550 		break;
1551 	case MDOC__R:
1552 		if (strncmp(arg, "RFC ", 4) == 0) {
1553 			cp = arg += 4;
1554 			while (isdigit(*cp))
1555 				cp++;
1556 			if (*cp == '\0') {
1557 				mandoc_asprintf(&url, "https://www.rfc-"
1558 				    "editor.org/rfc/rfc%s.html", arg);
1559 				print_otag(h, TAG_A, "ch", "RsR", url);
1560 				free(url);
1561 				return 1;
1562 			}
1563 		}
1564 		cattr = "RsR";
1565 		break;
1566 	case MDOC__T:
1567 		t = TAG_CITE;
1568 		if (n->parent != NULL && n->parent->tok == MDOC_Rs &&
1569 		    n->parent->norm->Rs.quote_T) {
1570 			print_text(h, "\\(lq");
1571 			h->flags |= HTML_NOSPACE;
1572 			cattr = "RsT";
1573 		} else
1574 			cattr = "RsB";
1575 		break;
1576 	case MDOC__U:
1577 		print_otag(h, TAG_A, "ch", "RsU", arg);
1578 		return 1;
1579 	case MDOC__V:
1580 		cattr = "RsV";
1581 		break;
1582 	default:
1583 		abort();
1584 	}
1585 
1586 	print_otag(h, t, "c", cattr);
1587 	return 1;
1588 }
1589 
1590 static void
1591 mdoc__x_post(MDOC_ARGS)
1592 {
1593 	struct roff_node *nn;
1594 
1595 	switch (n->tok) {
1596 	case MDOC__A:
1597 		if ((nn = roff_node_next(n)) != NULL && nn->tok == MDOC__A &&
1598 		    ((nn = roff_node_next(nn)) == NULL || nn->tok != MDOC__A) &&
1599 		    ((nn = roff_node_prev(n)) == NULL || nn->tok != MDOC__A))
1600 			return;
1601 		break;
1602 	case MDOC__T:
1603 		if (n->parent != NULL && n->parent->tok == MDOC_Rs &&
1604 		    n->parent->norm->Rs.quote_T) {
1605 			h->flags |= HTML_NOSPACE;
1606 			print_text(h, "\\(rq");
1607 		}
1608 		break;
1609 	default:
1610 		break;
1611 	}
1612 	if (n->parent == NULL || n->parent->tok != MDOC_Rs)
1613 		return;
1614 
1615 	h->flags |= HTML_NOSPACE;
1616 	print_text(h, roff_node_next(n) ? "," : ".");
1617 }
1618 
1619 static int
1620 mdoc_bk_pre(MDOC_ARGS)
1621 {
1622 
1623 	switch (n->type) {
1624 	case ROFFT_BLOCK:
1625 		break;
1626 	case ROFFT_HEAD:
1627 		return 0;
1628 	case ROFFT_BODY:
1629 		if (n->parent->args != NULL || n->prev->child == NULL)
1630 			h->flags |= HTML_PREKEEP;
1631 		break;
1632 	default:
1633 		abort();
1634 	}
1635 
1636 	return 1;
1637 }
1638 
1639 static void
1640 mdoc_bk_post(MDOC_ARGS)
1641 {
1642 
1643 	if (n->type == ROFFT_BODY)
1644 		h->flags &= ~(HTML_KEEP | HTML_PREKEEP);
1645 }
1646 
1647 static int
1648 mdoc_quote_pre(MDOC_ARGS)
1649 {
1650 	if (n->type != ROFFT_BODY)
1651 		return 1;
1652 
1653 	switch (n->tok) {
1654 	case MDOC_Ao:
1655 	case MDOC_Aq:
1656 		print_text(h, n->child != NULL && n->child->next == NULL &&
1657 		    n->child->tok == MDOC_Mt ?  "<" : "\\(la");
1658 		break;
1659 	case MDOC_Bro:
1660 	case MDOC_Brq:
1661 		print_text(h, "\\(lC");
1662 		break;
1663 	case MDOC_Bo:
1664 	case MDOC_Bq:
1665 		print_text(h, "\\(lB");
1666 		break;
1667 	case MDOC_Oo:
1668 	case MDOC_Op:
1669 		print_text(h, "\\(lB");
1670 		/*
1671 		 * Give up on semantic markup for now.
1672 		 * We cannot use TAG_SPAN because .Oo may contain blocks.
1673 		 * We cannot use TAG_DIV because we might be in a
1674 		 * phrasing context (like .Dl or .Pp); we cannot
1675 		 * close out a .Pp at this point either because
1676 		 * that would break the line.
1677 		 */
1678 		/* XXX print_otag(h, TAG_???, "c", "Op"); */
1679 		break;
1680 	case MDOC_En:
1681 		if (NULL == n->norm->Es ||
1682 		    NULL == n->norm->Es->child)
1683 			return 1;
1684 		print_text(h, n->norm->Es->child->string);
1685 		break;
1686 	case MDOC_Do:
1687 	case MDOC_Dq:
1688 		print_text(h, "\\(lq");
1689 		break;
1690 	case MDOC_Qo:
1691 	case MDOC_Qq:
1692 		print_text(h, "\"");
1693 		break;
1694 	case MDOC_Po:
1695 	case MDOC_Pq:
1696 		print_text(h, "(");
1697 		break;
1698 	case MDOC_Ql:
1699 		print_text(h, "\\(oq");
1700 		h->flags |= HTML_NOSPACE;
1701 		print_otag(h, TAG_CODE, "c", "Li");
1702 		break;
1703 	case MDOC_So:
1704 	case MDOC_Sq:
1705 		print_text(h, "\\(oq");
1706 		break;
1707 	default:
1708 		abort();
1709 	}
1710 
1711 	h->flags |= HTML_NOSPACE;
1712 	return 1;
1713 }
1714 
1715 static void
1716 mdoc_quote_post(MDOC_ARGS)
1717 {
1718 
1719 	if (n->type != ROFFT_BODY && n->type != ROFFT_ELEM)
1720 		return;
1721 
1722 	h->flags |= HTML_NOSPACE;
1723 
1724 	switch (n->tok) {
1725 	case MDOC_Ao:
1726 	case MDOC_Aq:
1727 		print_text(h, n->child != NULL && n->child->next == NULL &&
1728 		    n->child->tok == MDOC_Mt ?  ">" : "\\(ra");
1729 		break;
1730 	case MDOC_Bro:
1731 	case MDOC_Brq:
1732 		print_text(h, "\\(rC");
1733 		break;
1734 	case MDOC_Oo:
1735 	case MDOC_Op:
1736 	case MDOC_Bo:
1737 	case MDOC_Bq:
1738 		print_text(h, "\\(rB");
1739 		break;
1740 	case MDOC_En:
1741 		if (n->norm->Es == NULL ||
1742 		    n->norm->Es->child == NULL ||
1743 		    n->norm->Es->child->next == NULL)
1744 			h->flags &= ~HTML_NOSPACE;
1745 		else
1746 			print_text(h, n->norm->Es->child->next->string);
1747 		break;
1748 	case MDOC_Do:
1749 	case MDOC_Dq:
1750 		print_text(h, "\\(rq");
1751 		break;
1752 	case MDOC_Qo:
1753 	case MDOC_Qq:
1754 		print_text(h, "\"");
1755 		break;
1756 	case MDOC_Po:
1757 	case MDOC_Pq:
1758 		print_text(h, ")");
1759 		break;
1760 	case MDOC_Ql:
1761 	case MDOC_So:
1762 	case MDOC_Sq:
1763 		print_text(h, "\\(cq");
1764 		break;
1765 	default:
1766 		abort();
1767 	}
1768 }
1769 
1770 static int
1771 mdoc_eo_pre(MDOC_ARGS)
1772 {
1773 
1774 	if (n->type != ROFFT_BODY)
1775 		return 1;
1776 
1777 	if (n->end == ENDBODY_NOT &&
1778 	    n->parent->head->child == NULL &&
1779 	    n->child != NULL &&
1780 	    n->child->end != ENDBODY_NOT)
1781 		print_text(h, "\\&");
1782 	else if (n->end != ENDBODY_NOT ? n->child != NULL :
1783 	    n->parent->head->child != NULL && (n->child != NULL ||
1784 	    (n->parent->tail != NULL && n->parent->tail->child != NULL)))
1785 		h->flags |= HTML_NOSPACE;
1786 	return 1;
1787 }
1788 
1789 static void
1790 mdoc_eo_post(MDOC_ARGS)
1791 {
1792 	int	 body, tail;
1793 
1794 	if (n->type != ROFFT_BODY)
1795 		return;
1796 
1797 	if (n->end != ENDBODY_NOT) {
1798 		h->flags &= ~HTML_NOSPACE;
1799 		return;
1800 	}
1801 
1802 	body = n->child != NULL || n->parent->head->child != NULL;
1803 	tail = n->parent->tail != NULL && n->parent->tail->child != NULL;
1804 
1805 	if (body && tail)
1806 		h->flags |= HTML_NOSPACE;
1807 	else if ( ! tail)
1808 		h->flags &= ~HTML_NOSPACE;
1809 }
1810 
1811 static int
1812 mdoc_abort_pre(MDOC_ARGS)
1813 {
1814 	abort();
1815 }
1816