xref: /openbsd/usr.bin/mandoc/mdoc_html.c (revision 905646f0)
1 /* $OpenBSD: mdoc_html.c,v 1.216 2020/10/16 17:22:39 schwarze Exp $ */
2 /*
3  * Copyright (c) 2014-2020 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[16];
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 	print_otag_id(h, TAG_DIV, buf, n);
969 	return 1;
970 }
971 
972 static int
973 mdoc_pa_pre(MDOC_ARGS)
974 {
975 	print_otag(h, TAG_SPAN, "c", "Pa");
976 	return 1;
977 }
978 
979 static int
980 mdoc_ad_pre(MDOC_ARGS)
981 {
982 	print_otag(h, TAG_SPAN, "c", "Ad");
983 	return 1;
984 }
985 
986 static int
987 mdoc_an_pre(MDOC_ARGS)
988 {
989 	if (n->norm->An.auth == AUTH_split) {
990 		h->flags &= ~HTML_NOSPLIT;
991 		h->flags |= HTML_SPLIT;
992 		return 0;
993 	}
994 	if (n->norm->An.auth == AUTH_nosplit) {
995 		h->flags &= ~HTML_SPLIT;
996 		h->flags |= HTML_NOSPLIT;
997 		return 0;
998 	}
999 
1000 	if (h->flags & HTML_SPLIT)
1001 		print_otag(h, TAG_BR, "");
1002 
1003 	if (n->sec == SEC_AUTHORS && ! (h->flags & HTML_NOSPLIT))
1004 		h->flags |= HTML_SPLIT;
1005 
1006 	print_otag(h, TAG_SPAN, "c", "An");
1007 	return 1;
1008 }
1009 
1010 static int
1011 mdoc_cd_pre(MDOC_ARGS)
1012 {
1013 	synopsis_pre(h, n);
1014 	print_otag(h, TAG_CODE, "c", "Cd");
1015 	return 1;
1016 }
1017 
1018 static int
1019 mdoc_fa_pre(MDOC_ARGS)
1020 {
1021 	const struct roff_node	*nn;
1022 	struct tag		*t;
1023 
1024 	if (n->parent->tok != MDOC_Fo) {
1025 		print_otag(h, TAG_VAR, "c", "Fa");
1026 		return 1;
1027 	}
1028 	for (nn = n->child; nn != NULL; nn = nn->next) {
1029 		t = print_otag(h, TAG_VAR, "c", "Fa");
1030 		print_text(h, nn->string);
1031 		print_tagq(h, t);
1032 		if (nn->next != NULL) {
1033 			h->flags |= HTML_NOSPACE;
1034 			print_text(h, ",");
1035 		}
1036 	}
1037 	if (n->child != NULL &&
1038 	    (nn = roff_node_next(n)) != NULL &&
1039 	    nn->tok == MDOC_Fa) {
1040 		h->flags |= HTML_NOSPACE;
1041 		print_text(h, ",");
1042 	}
1043 	return 0;
1044 }
1045 
1046 static int
1047 mdoc_fd_pre(MDOC_ARGS)
1048 {
1049 	struct tag	*t;
1050 	char		*buf, *cp;
1051 
1052 	synopsis_pre(h, n);
1053 
1054 	if (NULL == (n = n->child))
1055 		return 0;
1056 
1057 	assert(n->type == ROFFT_TEXT);
1058 
1059 	if (strcmp(n->string, "#include")) {
1060 		print_otag(h, TAG_CODE, "c", "Fd");
1061 		return 1;
1062 	}
1063 
1064 	print_otag(h, TAG_CODE, "c", "In");
1065 	print_text(h, n->string);
1066 
1067 	if (NULL != (n = n->next)) {
1068 		assert(n->type == ROFFT_TEXT);
1069 
1070 		if (h->base_includes) {
1071 			cp = n->string;
1072 			if (*cp == '<' || *cp == '"')
1073 				cp++;
1074 			buf = mandoc_strdup(cp);
1075 			cp = strchr(buf, '\0') - 1;
1076 			if (cp >= buf && (*cp == '>' || *cp == '"'))
1077 				*cp = '\0';
1078 			t = print_otag(h, TAG_A, "chI", "In", buf);
1079 			free(buf);
1080 		} else
1081 			t = print_otag(h, TAG_A, "c", "In");
1082 
1083 		print_text(h, n->string);
1084 		print_tagq(h, t);
1085 
1086 		n = n->next;
1087 	}
1088 
1089 	for ( ; n; n = n->next) {
1090 		assert(n->type == ROFFT_TEXT);
1091 		print_text(h, n->string);
1092 	}
1093 
1094 	return 0;
1095 }
1096 
1097 static int
1098 mdoc_vt_pre(MDOC_ARGS)
1099 {
1100 	if (n->type == ROFFT_BLOCK) {
1101 		synopsis_pre(h, n);
1102 		return 1;
1103 	} else if (n->type == ROFFT_ELEM) {
1104 		synopsis_pre(h, n);
1105 	} else if (n->type == ROFFT_HEAD)
1106 		return 0;
1107 
1108 	print_otag(h, TAG_VAR, "c", "Vt");
1109 	return 1;
1110 }
1111 
1112 static int
1113 mdoc_ft_pre(MDOC_ARGS)
1114 {
1115 	synopsis_pre(h, n);
1116 	print_otag(h, TAG_VAR, "c", "Ft");
1117 	return 1;
1118 }
1119 
1120 static int
1121 mdoc_fn_pre(MDOC_ARGS)
1122 {
1123 	struct tag	*t;
1124 	char		 nbuf[BUFSIZ];
1125 	const char	*sp, *ep;
1126 	int		 sz, pretty;
1127 
1128 	pretty = NODE_SYNPRETTY & n->flags;
1129 	synopsis_pre(h, n);
1130 
1131 	/* Split apart into type and name. */
1132 	assert(n->child->string);
1133 	sp = n->child->string;
1134 
1135 	ep = strchr(sp, ' ');
1136 	if (NULL != ep) {
1137 		t = print_otag(h, TAG_VAR, "c", "Ft");
1138 
1139 		while (ep) {
1140 			sz = MIN((int)(ep - sp), BUFSIZ - 1);
1141 			(void)memcpy(nbuf, sp, (size_t)sz);
1142 			nbuf[sz] = '\0';
1143 			print_text(h, nbuf);
1144 			sp = ++ep;
1145 			ep = strchr(sp, ' ');
1146 		}
1147 		print_tagq(h, t);
1148 	}
1149 
1150 	t = print_otag_id(h, TAG_CODE, "Fn", n);
1151 
1152 	if (sp)
1153 		print_text(h, sp);
1154 
1155 	print_tagq(h, t);
1156 
1157 	h->flags |= HTML_NOSPACE;
1158 	print_text(h, "(");
1159 	h->flags |= HTML_NOSPACE;
1160 
1161 	for (n = n->child->next; n; n = n->next) {
1162 		if (NODE_SYNPRETTY & n->flags)
1163 			t = print_otag(h, TAG_VAR, "cs", "Fa",
1164 			    "white-space", "nowrap");
1165 		else
1166 			t = print_otag(h, TAG_VAR, "c", "Fa");
1167 		print_text(h, n->string);
1168 		print_tagq(h, t);
1169 		if (n->next) {
1170 			h->flags |= HTML_NOSPACE;
1171 			print_text(h, ",");
1172 		}
1173 	}
1174 
1175 	h->flags |= HTML_NOSPACE;
1176 	print_text(h, ")");
1177 
1178 	if (pretty) {
1179 		h->flags |= HTML_NOSPACE;
1180 		print_text(h, ";");
1181 	}
1182 
1183 	return 0;
1184 }
1185 
1186 static int
1187 mdoc_sm_pre(MDOC_ARGS)
1188 {
1189 
1190 	if (NULL == n->child)
1191 		h->flags ^= HTML_NONOSPACE;
1192 	else if (0 == strcmp("on", n->child->string))
1193 		h->flags &= ~HTML_NONOSPACE;
1194 	else
1195 		h->flags |= HTML_NONOSPACE;
1196 
1197 	if ( ! (HTML_NONOSPACE & h->flags))
1198 		h->flags &= ~HTML_NOSPACE;
1199 
1200 	return 0;
1201 }
1202 
1203 static int
1204 mdoc_skip_pre(MDOC_ARGS)
1205 {
1206 
1207 	return 0;
1208 }
1209 
1210 static int
1211 mdoc_pp_pre(MDOC_ARGS)
1212 {
1213 	char	*id;
1214 
1215 	if (n->flags & NODE_NOFILL) {
1216 		print_endline(h);
1217 		if (n->flags & NODE_ID)
1218 			mdoc_tg_pre(meta, n, h);
1219 		else {
1220 			h->col = 1;
1221 			print_endline(h);
1222 		}
1223 	} else {
1224 		html_close_paragraph(h);
1225 		id = n->flags & NODE_ID ? html_make_id(n, 1) : NULL;
1226 		print_otag(h, TAG_P, "ci", "Pp", id);
1227 		free(id);
1228 	}
1229 	return 0;
1230 }
1231 
1232 static int
1233 mdoc_lk_pre(MDOC_ARGS)
1234 {
1235 	const struct roff_node *link, *descr, *punct;
1236 	struct tag	*t;
1237 
1238 	if ((link = n->child) == NULL)
1239 		return 0;
1240 
1241 	/* Find beginning of trailing punctuation. */
1242 	punct = n->last;
1243 	while (punct != link && punct->flags & NODE_DELIMC)
1244 		punct = punct->prev;
1245 	punct = punct->next;
1246 
1247 	/* Link target and link text. */
1248 	descr = link->next;
1249 	if (descr == punct)
1250 		descr = link;  /* no text */
1251 	t = print_otag(h, TAG_A, "ch", "Lk", link->string);
1252 	do {
1253 		if (descr->flags & (NODE_DELIMC | NODE_DELIMO))
1254 			h->flags |= HTML_NOSPACE;
1255 		print_text(h, descr->string);
1256 		descr = descr->next;
1257 	} while (descr != punct);
1258 	print_tagq(h, t);
1259 
1260 	/* Trailing punctuation. */
1261 	while (punct != NULL) {
1262 		h->flags |= HTML_NOSPACE;
1263 		print_text(h, punct->string);
1264 		punct = punct->next;
1265 	}
1266 	return 0;
1267 }
1268 
1269 static int
1270 mdoc_mt_pre(MDOC_ARGS)
1271 {
1272 	struct tag	*t;
1273 	char		*cp;
1274 
1275 	for (n = n->child; n; n = n->next) {
1276 		assert(n->type == ROFFT_TEXT);
1277 		mandoc_asprintf(&cp, "mailto:%s", n->string);
1278 		t = print_otag(h, TAG_A, "ch", "Mt", cp);
1279 		print_text(h, n->string);
1280 		print_tagq(h, t);
1281 		free(cp);
1282 	}
1283 	return 0;
1284 }
1285 
1286 static int
1287 mdoc_fo_pre(MDOC_ARGS)
1288 {
1289 	struct tag	*t;
1290 
1291 	switch (n->type) {
1292 	case ROFFT_BLOCK:
1293 		synopsis_pre(h, n);
1294 		return 1;
1295 	case ROFFT_HEAD:
1296 		if (n->child != NULL) {
1297 			t = print_otag_id(h, TAG_CODE, "Fn", n);
1298 			print_text(h, n->child->string);
1299 			print_tagq(h, t);
1300 		}
1301 		return 0;
1302 	case ROFFT_BODY:
1303 		h->flags |= HTML_NOSPACE;
1304 		print_text(h, "(");
1305 		h->flags |= HTML_NOSPACE;
1306 		return 1;
1307 	default:
1308 		abort();
1309 	}
1310 }
1311 
1312 static void
1313 mdoc_fo_post(MDOC_ARGS)
1314 {
1315 	if (n->type != ROFFT_BODY)
1316 		return;
1317 	h->flags |= HTML_NOSPACE;
1318 	print_text(h, ")");
1319 	h->flags |= HTML_NOSPACE;
1320 	print_text(h, ";");
1321 }
1322 
1323 static int
1324 mdoc_in_pre(MDOC_ARGS)
1325 {
1326 	struct tag	*t;
1327 
1328 	synopsis_pre(h, n);
1329 	print_otag(h, TAG_CODE, "c", "In");
1330 
1331 	/*
1332 	 * The first argument of the `In' gets special treatment as
1333 	 * being a linked value.  Subsequent values are printed
1334 	 * afterward.  groff does similarly.  This also handles the case
1335 	 * of no children.
1336 	 */
1337 
1338 	if (NODE_SYNPRETTY & n->flags && NODE_LINE & n->flags)
1339 		print_text(h, "#include");
1340 
1341 	print_text(h, "<");
1342 	h->flags |= HTML_NOSPACE;
1343 
1344 	if (NULL != (n = n->child)) {
1345 		assert(n->type == ROFFT_TEXT);
1346 
1347 		if (h->base_includes)
1348 			t = print_otag(h, TAG_A, "chI", "In", n->string);
1349 		else
1350 			t = print_otag(h, TAG_A, "c", "In");
1351 		print_text(h, n->string);
1352 		print_tagq(h, t);
1353 
1354 		n = n->next;
1355 	}
1356 
1357 	h->flags |= HTML_NOSPACE;
1358 	print_text(h, ">");
1359 
1360 	for ( ; n; n = n->next) {
1361 		assert(n->type == ROFFT_TEXT);
1362 		print_text(h, n->string);
1363 	}
1364 	return 0;
1365 }
1366 
1367 static int
1368 mdoc_va_pre(MDOC_ARGS)
1369 {
1370 	print_otag(h, TAG_VAR, "c", "Va");
1371 	return 1;
1372 }
1373 
1374 static int
1375 mdoc_ap_pre(MDOC_ARGS)
1376 {
1377 	h->flags |= HTML_NOSPACE;
1378 	print_text(h, "\\(aq");
1379 	h->flags |= HTML_NOSPACE;
1380 	return 1;
1381 }
1382 
1383 static int
1384 mdoc_bf_pre(MDOC_ARGS)
1385 {
1386 	const char	*cattr;
1387 
1388 	switch (n->type) {
1389 	case ROFFT_BLOCK:
1390 		html_close_paragraph(h);
1391 		return 1;
1392 	case ROFFT_HEAD:
1393 		return 0;
1394 	case ROFFT_BODY:
1395 		break;
1396 	default:
1397 		abort();
1398 	}
1399 
1400 	if (FONT_Em == n->norm->Bf.font)
1401 		cattr = "Bf Em";
1402 	else if (FONT_Sy == n->norm->Bf.font)
1403 		cattr = "Bf Sy";
1404 	else if (FONT_Li == n->norm->Bf.font)
1405 		cattr = "Bf Li";
1406 	else
1407 		cattr = "Bf No";
1408 
1409 	/* Cannot use TAG_SPAN because it may contain blocks. */
1410 	print_otag(h, TAG_DIV, "c", cattr);
1411 	return 1;
1412 }
1413 
1414 static int
1415 mdoc_igndelim_pre(MDOC_ARGS)
1416 {
1417 	h->flags |= HTML_IGNDELIM;
1418 	return 1;
1419 }
1420 
1421 static void
1422 mdoc_pf_post(MDOC_ARGS)
1423 {
1424 	if ( ! (n->next == NULL || n->next->flags & NODE_LINE))
1425 		h->flags |= HTML_NOSPACE;
1426 }
1427 
1428 static int
1429 mdoc_rs_pre(MDOC_ARGS)
1430 {
1431 	switch (n->type) {
1432 	case ROFFT_BLOCK:
1433 		if (n->sec == SEC_SEE_ALSO)
1434 			html_close_paragraph(h);
1435 		break;
1436 	case ROFFT_HEAD:
1437 		return 0;
1438 	case ROFFT_BODY:
1439 		if (n->sec == SEC_SEE_ALSO)
1440 			print_otag(h, TAG_P, "c", "Pp");
1441 		print_otag(h, TAG_CITE, "c", "Rs");
1442 		break;
1443 	default:
1444 		abort();
1445 	}
1446 	return 1;
1447 }
1448 
1449 static int
1450 mdoc_no_pre(MDOC_ARGS)
1451 {
1452 	print_otag_id(h, TAG_SPAN, roff_name[n->tok], n);
1453 	return 1;
1454 }
1455 
1456 static int
1457 mdoc_sy_pre(MDOC_ARGS)
1458 {
1459 	print_otag_id(h, TAG_B, "Sy", n);
1460 	return 1;
1461 }
1462 
1463 static int
1464 mdoc_lb_pre(MDOC_ARGS)
1465 {
1466 	if (n->sec == SEC_LIBRARY &&
1467 	    n->flags & NODE_LINE &&
1468 	    roff_node_prev(n) != NULL)
1469 		print_otag(h, TAG_BR, "");
1470 
1471 	print_otag(h, TAG_SPAN, "c", "Lb");
1472 	return 1;
1473 }
1474 
1475 static int
1476 mdoc__x_pre(MDOC_ARGS)
1477 {
1478 	struct roff_node	*nn;
1479 	const char		*cattr;
1480 	enum htmltag		 t;
1481 
1482 	t = TAG_SPAN;
1483 
1484 	switch (n->tok) {
1485 	case MDOC__A:
1486 		cattr = "RsA";
1487 		if ((nn = roff_node_prev(n)) != NULL && nn->tok == MDOC__A &&
1488 		    ((nn = roff_node_next(n)) == NULL || nn->tok != MDOC__A))
1489 			print_text(h, "and");
1490 		break;
1491 	case MDOC__B:
1492 		t = TAG_I;
1493 		cattr = "RsB";
1494 		break;
1495 	case MDOC__C:
1496 		cattr = "RsC";
1497 		break;
1498 	case MDOC__D:
1499 		cattr = "RsD";
1500 		break;
1501 	case MDOC__I:
1502 		t = TAG_I;
1503 		cattr = "RsI";
1504 		break;
1505 	case MDOC__J:
1506 		t = TAG_I;
1507 		cattr = "RsJ";
1508 		break;
1509 	case MDOC__N:
1510 		cattr = "RsN";
1511 		break;
1512 	case MDOC__O:
1513 		cattr = "RsO";
1514 		break;
1515 	case MDOC__P:
1516 		cattr = "RsP";
1517 		break;
1518 	case MDOC__Q:
1519 		cattr = "RsQ";
1520 		break;
1521 	case MDOC__R:
1522 		cattr = "RsR";
1523 		break;
1524 	case MDOC__T:
1525 		cattr = "RsT";
1526 		break;
1527 	case MDOC__U:
1528 		print_otag(h, TAG_A, "ch", "RsU", n->child->string);
1529 		return 1;
1530 	case MDOC__V:
1531 		cattr = "RsV";
1532 		break;
1533 	default:
1534 		abort();
1535 	}
1536 
1537 	print_otag(h, t, "c", cattr);
1538 	return 1;
1539 }
1540 
1541 static void
1542 mdoc__x_post(MDOC_ARGS)
1543 {
1544 	struct roff_node *nn;
1545 
1546 	if (n->tok == MDOC__A &&
1547 	    (nn = roff_node_next(n)) != NULL && nn->tok == MDOC__A &&
1548 	    ((nn = roff_node_next(nn)) == NULL || nn->tok != MDOC__A) &&
1549 	    ((nn = roff_node_prev(n)) == NULL || nn->tok != MDOC__A))
1550 		return;
1551 
1552 	/* TODO: %U */
1553 
1554 	if (n->parent == NULL || n->parent->tok != MDOC_Rs)
1555 		return;
1556 
1557 	h->flags |= HTML_NOSPACE;
1558 	print_text(h, roff_node_next(n) ? "," : ".");
1559 }
1560 
1561 static int
1562 mdoc_bk_pre(MDOC_ARGS)
1563 {
1564 
1565 	switch (n->type) {
1566 	case ROFFT_BLOCK:
1567 		break;
1568 	case ROFFT_HEAD:
1569 		return 0;
1570 	case ROFFT_BODY:
1571 		if (n->parent->args != NULL || n->prev->child == NULL)
1572 			h->flags |= HTML_PREKEEP;
1573 		break;
1574 	default:
1575 		abort();
1576 	}
1577 
1578 	return 1;
1579 }
1580 
1581 static void
1582 mdoc_bk_post(MDOC_ARGS)
1583 {
1584 
1585 	if (n->type == ROFFT_BODY)
1586 		h->flags &= ~(HTML_KEEP | HTML_PREKEEP);
1587 }
1588 
1589 static int
1590 mdoc_quote_pre(MDOC_ARGS)
1591 {
1592 	if (n->type != ROFFT_BODY)
1593 		return 1;
1594 
1595 	switch (n->tok) {
1596 	case MDOC_Ao:
1597 	case MDOC_Aq:
1598 		print_text(h, n->child != NULL && n->child->next == NULL &&
1599 		    n->child->tok == MDOC_Mt ?  "<" : "\\(la");
1600 		break;
1601 	case MDOC_Bro:
1602 	case MDOC_Brq:
1603 		print_text(h, "\\(lC");
1604 		break;
1605 	case MDOC_Bo:
1606 	case MDOC_Bq:
1607 		print_text(h, "\\(lB");
1608 		break;
1609 	case MDOC_Oo:
1610 	case MDOC_Op:
1611 		print_text(h, "\\(lB");
1612 		/*
1613 		 * Give up on semantic markup for now.
1614 		 * We cannot use TAG_SPAN because .Oo may contain blocks.
1615 		 * We cannot use TAG_DIV because we might be in a
1616 		 * phrasing context (like .Dl or .Pp); we cannot
1617 		 * close out a .Pp at this point either because
1618 		 * that would break the line.
1619 		 */
1620 		/* XXX print_otag(h, TAG_???, "c", "Op"); */
1621 		break;
1622 	case MDOC_En:
1623 		if (NULL == n->norm->Es ||
1624 		    NULL == n->norm->Es->child)
1625 			return 1;
1626 		print_text(h, n->norm->Es->child->string);
1627 		break;
1628 	case MDOC_Do:
1629 	case MDOC_Dq:
1630 		print_text(h, "\\(lq");
1631 		break;
1632 	case MDOC_Qo:
1633 	case MDOC_Qq:
1634 		print_text(h, "\"");
1635 		break;
1636 	case MDOC_Po:
1637 	case MDOC_Pq:
1638 		print_text(h, "(");
1639 		break;
1640 	case MDOC_Ql:
1641 		print_text(h, "\\(oq");
1642 		h->flags |= HTML_NOSPACE;
1643 		print_otag(h, TAG_CODE, "c", "Li");
1644 		break;
1645 	case MDOC_So:
1646 	case MDOC_Sq:
1647 		print_text(h, "\\(oq");
1648 		break;
1649 	default:
1650 		abort();
1651 	}
1652 
1653 	h->flags |= HTML_NOSPACE;
1654 	return 1;
1655 }
1656 
1657 static void
1658 mdoc_quote_post(MDOC_ARGS)
1659 {
1660 
1661 	if (n->type != ROFFT_BODY && n->type != ROFFT_ELEM)
1662 		return;
1663 
1664 	h->flags |= HTML_NOSPACE;
1665 
1666 	switch (n->tok) {
1667 	case MDOC_Ao:
1668 	case MDOC_Aq:
1669 		print_text(h, n->child != NULL && n->child->next == NULL &&
1670 		    n->child->tok == MDOC_Mt ?  ">" : "\\(ra");
1671 		break;
1672 	case MDOC_Bro:
1673 	case MDOC_Brq:
1674 		print_text(h, "\\(rC");
1675 		break;
1676 	case MDOC_Oo:
1677 	case MDOC_Op:
1678 	case MDOC_Bo:
1679 	case MDOC_Bq:
1680 		print_text(h, "\\(rB");
1681 		break;
1682 	case MDOC_En:
1683 		if (n->norm->Es == NULL ||
1684 		    n->norm->Es->child == NULL ||
1685 		    n->norm->Es->child->next == NULL)
1686 			h->flags &= ~HTML_NOSPACE;
1687 		else
1688 			print_text(h, n->norm->Es->child->next->string);
1689 		break;
1690 	case MDOC_Do:
1691 	case MDOC_Dq:
1692 		print_text(h, "\\(rq");
1693 		break;
1694 	case MDOC_Qo:
1695 	case MDOC_Qq:
1696 		print_text(h, "\"");
1697 		break;
1698 	case MDOC_Po:
1699 	case MDOC_Pq:
1700 		print_text(h, ")");
1701 		break;
1702 	case MDOC_Ql:
1703 	case MDOC_So:
1704 	case MDOC_Sq:
1705 		print_text(h, "\\(cq");
1706 		break;
1707 	default:
1708 		abort();
1709 	}
1710 }
1711 
1712 static int
1713 mdoc_eo_pre(MDOC_ARGS)
1714 {
1715 
1716 	if (n->type != ROFFT_BODY)
1717 		return 1;
1718 
1719 	if (n->end == ENDBODY_NOT &&
1720 	    n->parent->head->child == NULL &&
1721 	    n->child != NULL &&
1722 	    n->child->end != ENDBODY_NOT)
1723 		print_text(h, "\\&");
1724 	else if (n->end != ENDBODY_NOT ? n->child != NULL :
1725 	    n->parent->head->child != NULL && (n->child != NULL ||
1726 	    (n->parent->tail != NULL && n->parent->tail->child != NULL)))
1727 		h->flags |= HTML_NOSPACE;
1728 	return 1;
1729 }
1730 
1731 static void
1732 mdoc_eo_post(MDOC_ARGS)
1733 {
1734 	int	 body, tail;
1735 
1736 	if (n->type != ROFFT_BODY)
1737 		return;
1738 
1739 	if (n->end != ENDBODY_NOT) {
1740 		h->flags &= ~HTML_NOSPACE;
1741 		return;
1742 	}
1743 
1744 	body = n->child != NULL || n->parent->head->child != NULL;
1745 	tail = n->parent->tail != NULL && n->parent->tail->child != NULL;
1746 
1747 	if (body && tail)
1748 		h->flags |= HTML_NOSPACE;
1749 	else if ( ! tail)
1750 		h->flags &= ~HTML_NOSPACE;
1751 }
1752 
1753 static int
1754 mdoc_abort_pre(MDOC_ARGS)
1755 {
1756 	abort();
1757 }
1758