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