xref: /openbsd-src/usr.bin/mandoc/mdoc_html.c (revision d4741794dd2f512d997014f8bd85fbb24d935059)
1 /*	$OpenBSD: mdoc_html.c,v 1.122 2017/01/11 17:39:45 schwarze Exp $ */
2 /*
3  * Copyright (c) 2008-2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
4  * Copyright (c) 2014, 2015, 2016, 2017 Ingo Schwarze <schwarze@openbsd.org>
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 #include <sys/types.h>
19 
20 #include <assert.h>
21 #include <ctype.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <unistd.h>
26 
27 #include "mandoc_aux.h"
28 #include "roff.h"
29 #include "mdoc.h"
30 #include "out.h"
31 #include "html.h"
32 #include "main.h"
33 
34 #define	INDENT		 5
35 
36 #define	MDOC_ARGS	  const struct roff_meta *meta, \
37 			  struct roff_node *n, \
38 			  struct html *h
39 
40 #ifndef MIN
41 #define	MIN(a,b)	((/*CONSTCOND*/(a)<(b))?(a):(b))
42 #endif
43 
44 struct	htmlmdoc {
45 	int		(*pre)(MDOC_ARGS);
46 	void		(*post)(MDOC_ARGS);
47 };
48 
49 static	void		  print_mdoc_head(MDOC_ARGS);
50 static	void		  print_mdoc_node(MDOC_ARGS);
51 static	void		  print_mdoc_nodelist(MDOC_ARGS);
52 static	void		  synopsis_pre(struct html *,
53 				const struct roff_node *);
54 
55 static	void		  a2width(const char *, struct roffsu *);
56 
57 static	void		  mdoc_root_post(MDOC_ARGS);
58 static	int		  mdoc_root_pre(MDOC_ARGS);
59 
60 static	void		  mdoc__x_post(MDOC_ARGS);
61 static	int		  mdoc__x_pre(MDOC_ARGS);
62 static	int		  mdoc_ad_pre(MDOC_ARGS);
63 static	int		  mdoc_an_pre(MDOC_ARGS);
64 static	int		  mdoc_ap_pre(MDOC_ARGS);
65 static	int		  mdoc_ar_pre(MDOC_ARGS);
66 static	int		  mdoc_bd_pre(MDOC_ARGS);
67 static	int		  mdoc_bf_pre(MDOC_ARGS);
68 static	void		  mdoc_bk_post(MDOC_ARGS);
69 static	int		  mdoc_bk_pre(MDOC_ARGS);
70 static	int		  mdoc_bl_pre(MDOC_ARGS);
71 static	int		  mdoc_cd_pre(MDOC_ARGS);
72 static	int		  mdoc_d1_pre(MDOC_ARGS);
73 static	int		  mdoc_dv_pre(MDOC_ARGS);
74 static	int		  mdoc_fa_pre(MDOC_ARGS);
75 static	int		  mdoc_fd_pre(MDOC_ARGS);
76 static	int		  mdoc_fl_pre(MDOC_ARGS);
77 static	int		  mdoc_fn_pre(MDOC_ARGS);
78 static	int		  mdoc_ft_pre(MDOC_ARGS);
79 static	int		  mdoc_em_pre(MDOC_ARGS);
80 static	void		  mdoc_eo_post(MDOC_ARGS);
81 static	int		  mdoc_eo_pre(MDOC_ARGS);
82 static	int		  mdoc_er_pre(MDOC_ARGS);
83 static	int		  mdoc_ev_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_ic_pre(MDOC_ARGS);
88 static	int		  mdoc_igndelim_pre(MDOC_ARGS);
89 static	int		  mdoc_in_pre(MDOC_ARGS);
90 static	int		  mdoc_it_pre(MDOC_ARGS);
91 static	int		  mdoc_lb_pre(MDOC_ARGS);
92 static	int		  mdoc_li_pre(MDOC_ARGS);
93 static	int		  mdoc_lk_pre(MDOC_ARGS);
94 static	int		  mdoc_mt_pre(MDOC_ARGS);
95 static	int		  mdoc_ms_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_sp_pre(MDOC_ARGS);
110 static	int		  mdoc_ss_pre(MDOC_ARGS);
111 static	int		  mdoc_sx_pre(MDOC_ARGS);
112 static	int		  mdoc_sy_pre(MDOC_ARGS);
113 static	int		  mdoc_va_pre(MDOC_ARGS);
114 static	int		  mdoc_vt_pre(MDOC_ARGS);
115 static	int		  mdoc_xr_pre(MDOC_ARGS);
116 static	int		  mdoc_xx_pre(MDOC_ARGS);
117 
118 static	const struct htmlmdoc mdocs[MDOC_MAX] = {
119 	{mdoc_ap_pre, NULL}, /* Ap */
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_ar_pre, NULL}, /* Ar */
136 	{mdoc_cd_pre, NULL}, /* Cd */
137 	{mdoc_fl_pre, NULL}, /* Cm */
138 	{mdoc_dv_pre, NULL}, /* Dv */
139 	{mdoc_er_pre, NULL}, /* Er */
140 	{mdoc_ev_pre, NULL}, /* Ev */
141 	{mdoc_ex_pre, NULL}, /* Ex */
142 	{mdoc_fa_pre, NULL}, /* Fa */
143 	{mdoc_fd_pre, NULL}, /* Fd */
144 	{mdoc_fl_pre, NULL}, /* Fl */
145 	{mdoc_fn_pre, NULL}, /* Fn */
146 	{mdoc_ft_pre, NULL}, /* Ft */
147 	{mdoc_ic_pre, NULL}, /* Ic */
148 	{mdoc_in_pre, NULL}, /* In */
149 	{mdoc_li_pre, NULL}, /* Li */
150 	{mdoc_nd_pre, NULL}, /* Nd */
151 	{mdoc_nm_pre, NULL}, /* Nm */
152 	{mdoc_quote_pre, mdoc_quote_post}, /* Op */
153 	{mdoc_ft_pre, NULL}, /* Ot */
154 	{mdoc_pa_pre, NULL}, /* Pa */
155 	{mdoc_ex_pre, NULL}, /* Rv */
156 	{NULL, NULL}, /* St */
157 	{mdoc_va_pre, NULL}, /* Va */
158 	{mdoc_vt_pre, NULL}, /* Vt */
159 	{mdoc_xr_pre, NULL}, /* Xr */
160 	{mdoc__x_pre, mdoc__x_post}, /* %A */
161 	{mdoc__x_pre, mdoc__x_post}, /* %B */
162 	{mdoc__x_pre, mdoc__x_post}, /* %D */
163 	{mdoc__x_pre, mdoc__x_post}, /* %I */
164 	{mdoc__x_pre, mdoc__x_post}, /* %J */
165 	{mdoc__x_pre, mdoc__x_post}, /* %N */
166 	{mdoc__x_pre, mdoc__x_post}, /* %O */
167 	{mdoc__x_pre, mdoc__x_post}, /* %P */
168 	{mdoc__x_pre, mdoc__x_post}, /* %R */
169 	{mdoc__x_pre, mdoc__x_post}, /* %T */
170 	{mdoc__x_pre, mdoc__x_post}, /* %V */
171 	{NULL, NULL}, /* Ac */
172 	{mdoc_quote_pre, mdoc_quote_post}, /* Ao */
173 	{mdoc_quote_pre, mdoc_quote_post}, /* Aq */
174 	{NULL, NULL}, /* At */
175 	{NULL, NULL}, /* Bc */
176 	{mdoc_bf_pre, NULL}, /* Bf */
177 	{mdoc_quote_pre, mdoc_quote_post}, /* Bo */
178 	{mdoc_quote_pre, mdoc_quote_post}, /* Bq */
179 	{mdoc_xx_pre, NULL}, /* Bsx */
180 	{mdoc_xx_pre, NULL}, /* Bx */
181 	{mdoc_skip_pre, NULL}, /* Db */
182 	{NULL, NULL}, /* Dc */
183 	{mdoc_quote_pre, mdoc_quote_post}, /* Do */
184 	{mdoc_quote_pre, mdoc_quote_post}, /* Dq */
185 	{NULL, NULL}, /* Ec */ /* FIXME: no space */
186 	{NULL, NULL}, /* Ef */
187 	{mdoc_em_pre, NULL}, /* Em */
188 	{mdoc_eo_pre, mdoc_eo_post}, /* Eo */
189 	{mdoc_xx_pre, NULL}, /* Fx */
190 	{mdoc_ms_pre, NULL}, /* Ms */
191 	{mdoc_no_pre, NULL}, /* No */
192 	{mdoc_ns_pre, NULL}, /* Ns */
193 	{mdoc_xx_pre, NULL}, /* Nx */
194 	{mdoc_xx_pre, NULL}, /* Ox */
195 	{NULL, NULL}, /* Pc */
196 	{mdoc_igndelim_pre, mdoc_pf_post}, /* Pf */
197 	{mdoc_quote_pre, mdoc_quote_post}, /* Po */
198 	{mdoc_quote_pre, mdoc_quote_post}, /* Pq */
199 	{NULL, NULL}, /* Qc */
200 	{mdoc_quote_pre, mdoc_quote_post}, /* Ql */
201 	{mdoc_quote_pre, mdoc_quote_post}, /* Qo */
202 	{mdoc_quote_pre, mdoc_quote_post}, /* Qq */
203 	{NULL, NULL}, /* Re */
204 	{mdoc_rs_pre, NULL}, /* Rs */
205 	{NULL, NULL}, /* Sc */
206 	{mdoc_quote_pre, mdoc_quote_post}, /* So */
207 	{mdoc_quote_pre, mdoc_quote_post}, /* Sq */
208 	{mdoc_sm_pre, NULL}, /* Sm */
209 	{mdoc_sx_pre, NULL}, /* Sx */
210 	{mdoc_sy_pre, NULL}, /* Sy */
211 	{NULL, NULL}, /* Tn */
212 	{mdoc_xx_pre, NULL}, /* Ux */
213 	{NULL, NULL}, /* Xc */
214 	{NULL, NULL}, /* Xo */
215 	{mdoc_fo_pre, mdoc_fo_post}, /* Fo */
216 	{NULL, NULL}, /* Fc */
217 	{mdoc_quote_pre, mdoc_quote_post}, /* Oo */
218 	{NULL, NULL}, /* Oc */
219 	{mdoc_bk_pre, mdoc_bk_post}, /* Bk */
220 	{NULL, NULL}, /* Ek */
221 	{NULL, NULL}, /* Bt */
222 	{NULL, NULL}, /* Hf */
223 	{mdoc_em_pre, NULL}, /* Fr */
224 	{NULL, NULL}, /* Ud */
225 	{mdoc_lb_pre, NULL}, /* Lb */
226 	{mdoc_pp_pre, NULL}, /* Lp */
227 	{mdoc_lk_pre, NULL}, /* Lk */
228 	{mdoc_mt_pre, NULL}, /* Mt */
229 	{mdoc_quote_pre, mdoc_quote_post}, /* Brq */
230 	{mdoc_quote_pre, mdoc_quote_post}, /* Bro */
231 	{NULL, NULL}, /* Brc */
232 	{mdoc__x_pre, mdoc__x_post}, /* %C */
233 	{mdoc_skip_pre, NULL}, /* Es */
234 	{mdoc_quote_pre, mdoc_quote_post}, /* En */
235 	{mdoc_xx_pre, NULL}, /* Dx */
236 	{mdoc__x_pre, mdoc__x_post}, /* %Q */
237 	{mdoc_sp_pre, NULL}, /* br */
238 	{mdoc_sp_pre, NULL}, /* sp */
239 	{mdoc__x_pre, mdoc__x_post}, /* %U */
240 	{NULL, NULL}, /* Ta */
241 	{mdoc_skip_pre, NULL}, /* ll */
242 };
243 
244 static	const char * const lists[LIST_MAX] = {
245 	NULL,
246 	"list-bul",
247 	"list-col",
248 	"list-dash",
249 	"list-diag",
250 	"list-enum",
251 	"list-hang",
252 	"list-hyph",
253 	"list-inset",
254 	"list-item",
255 	"list-ohang",
256 	"list-tag"
257 };
258 
259 
260 /*
261  * Calculate the scaling unit passed in a `-width' argument.  This uses
262  * either a native scaling unit (e.g., 1i, 2m) or the string length of
263  * the value.
264  */
265 static void
266 a2width(const char *p, struct roffsu *su)
267 {
268 
269 	if (a2roffsu(p, su, SCALE_MAX) < 2) {
270 		su->unit = SCALE_EN;
271 		su->scale = html_strlen(p);
272 	} else if (su->scale < 0.0)
273 		su->scale = 0.0;
274 }
275 
276 /*
277  * See the same function in mdoc_term.c for documentation.
278  */
279 static void
280 synopsis_pre(struct html *h, const struct roff_node *n)
281 {
282 
283 	if (NULL == n->prev || ! (NODE_SYNPRETTY & n->flags))
284 		return;
285 
286 	if (n->prev->tok == n->tok &&
287 	    MDOC_Fo != n->tok &&
288 	    MDOC_Ft != n->tok &&
289 	    MDOC_Fn != n->tok) {
290 		print_otag(h, TAG_BR, 0, NULL);
291 		return;
292 	}
293 
294 	switch (n->prev->tok) {
295 	case MDOC_Fd:
296 	case MDOC_Fn:
297 	case MDOC_Fo:
298 	case MDOC_In:
299 	case MDOC_Vt:
300 		print_paragraph(h);
301 		break;
302 	case MDOC_Ft:
303 		if (MDOC_Fn != n->tok && MDOC_Fo != n->tok) {
304 			print_paragraph(h);
305 			break;
306 		}
307 		/* FALLTHROUGH */
308 	default:
309 		print_otag(h, TAG_BR, 0, NULL);
310 		break;
311 	}
312 }
313 
314 void
315 html_mdoc(void *arg, const struct roff_man *mdoc)
316 {
317 	struct htmlpair	 tag;
318 	struct html	*h;
319 	struct tag	*t, *tt;
320 
321 	PAIR_CLASS_INIT(&tag, "mandoc");
322 	h = (struct html *)arg;
323 
324 	if ( ! (HTML_FRAGMENT & h->oflags)) {
325 		print_gen_decls(h);
326 		t = print_otag(h, TAG_HTML, 0, NULL);
327 		tt = print_otag(h, TAG_HEAD, 0, NULL);
328 		print_mdoc_head(&mdoc->meta, mdoc->first->child, h);
329 		print_tagq(h, tt);
330 		print_otag(h, TAG_BODY, 0, NULL);
331 		print_otag(h, TAG_DIV, 1, &tag);
332 	} else
333 		t = print_otag(h, TAG_DIV, 1, &tag);
334 
335 	mdoc_root_pre(&mdoc->meta, mdoc->first->child, h);
336 	print_mdoc_nodelist(&mdoc->meta, mdoc->first->child, h);
337 	mdoc_root_post(&mdoc->meta, mdoc->first->child, h);
338 	print_tagq(h, t);
339 	putchar('\n');
340 }
341 
342 static void
343 print_mdoc_head(MDOC_ARGS)
344 {
345 
346 	print_gen_head(h);
347 	bufinit(h);
348 	bufcat(h, meta->title);
349 	if (meta->msec)
350 		bufcat_fmt(h, "(%s)", meta->msec);
351 	if (meta->arch)
352 		bufcat_fmt(h, " (%s)", meta->arch);
353 
354 	print_otag(h, TAG_TITLE, 0, NULL);
355 	print_text(h, h->buf);
356 }
357 
358 static void
359 print_mdoc_nodelist(MDOC_ARGS)
360 {
361 
362 	while (n != NULL) {
363 		print_mdoc_node(meta, n, h);
364 		n = n->next;
365 	}
366 }
367 
368 static void
369 print_mdoc_node(MDOC_ARGS)
370 {
371 	int		 child;
372 	struct tag	*t;
373 
374 	if (n->flags & NODE_NOPRT)
375 		return;
376 
377 	child = 1;
378 	t = h->tags.head;
379 	n->flags &= ~NODE_ENDED;
380 
381 	switch (n->type) {
382 	case ROFFT_TEXT:
383 		/* No tables in this mode... */
384 		assert(NULL == h->tblt);
385 
386 		/*
387 		 * Make sure that if we're in a literal mode already
388 		 * (i.e., within a <PRE>) don't print the newline.
389 		 */
390 		if (' ' == *n->string && NODE_LINE & n->flags)
391 			if ( ! (HTML_LITERAL & h->flags))
392 				print_otag(h, TAG_BR, 0, NULL);
393 		if (NODE_DELIMC & n->flags)
394 			h->flags |= HTML_NOSPACE;
395 		print_text(h, n->string);
396 		if (NODE_DELIMO & n->flags)
397 			h->flags |= HTML_NOSPACE;
398 		return;
399 	case ROFFT_EQN:
400 		if (n->flags & NODE_LINE)
401 			putchar('\n');
402 		print_eqn(h, n->eqn);
403 		break;
404 	case ROFFT_TBL:
405 		/*
406 		 * This will take care of initialising all of the table
407 		 * state data for the first table, then tearing it down
408 		 * for the last one.
409 		 */
410 		print_tbl(h, n->span);
411 		return;
412 	default:
413 		/*
414 		 * Close out the current table, if it's open, and unset
415 		 * the "meta" table state.  This will be reopened on the
416 		 * next table element.
417 		 */
418 		if (h->tblt != NULL) {
419 			print_tblclose(h);
420 			t = h->tags.head;
421 		}
422 		assert(h->tblt == NULL);
423 		if (mdocs[n->tok].pre && (n->end == ENDBODY_NOT || n->child))
424 			child = (*mdocs[n->tok].pre)(meta, n, h);
425 		break;
426 	}
427 
428 	if (h->flags & HTML_KEEP && n->flags & NODE_LINE) {
429 		h->flags &= ~HTML_KEEP;
430 		h->flags |= HTML_PREKEEP;
431 	}
432 
433 	if (child && n->child)
434 		print_mdoc_nodelist(meta, n->child, h);
435 
436 	print_stagq(h, t);
437 
438 	switch (n->type) {
439 	case ROFFT_EQN:
440 		break;
441 	default:
442 		if ( ! mdocs[n->tok].post || n->flags & NODE_ENDED)
443 			break;
444 		(*mdocs[n->tok].post)(meta, n, h);
445 		if (n->end != ENDBODY_NOT)
446 			n->body->flags |= NODE_ENDED;
447 		if (n->end == ENDBODY_NOSPACE)
448 			h->flags |= HTML_NOSPACE;
449 		break;
450 	}
451 }
452 
453 static void
454 mdoc_root_post(MDOC_ARGS)
455 {
456 	struct htmlpair	 tag;
457 	struct tag	*t, *tt;
458 
459 	PAIR_CLASS_INIT(&tag, "foot");
460 	t = print_otag(h, TAG_TABLE, 1, &tag);
461 
462 	print_otag(h, TAG_TBODY, 0, NULL);
463 
464 	tt = print_otag(h, TAG_TR, 0, NULL);
465 
466 	PAIR_CLASS_INIT(&tag, "foot-date");
467 	print_otag(h, TAG_TD, 1, &tag);
468 	print_text(h, meta->date);
469 	print_stagq(h, tt);
470 
471 	PAIR_CLASS_INIT(&tag, "foot-os");
472 	print_otag(h, TAG_TD, 1, &tag);
473 	print_text(h, meta->os);
474 	print_tagq(h, t);
475 }
476 
477 static int
478 mdoc_root_pre(MDOC_ARGS)
479 {
480 	struct htmlpair	 tag;
481 	struct tag	*t, *tt;
482 	char		*volume, *title;
483 
484 	if (NULL == meta->arch)
485 		volume = mandoc_strdup(meta->vol);
486 	else
487 		mandoc_asprintf(&volume, "%s (%s)",
488 		    meta->vol, meta->arch);
489 
490 	if (NULL == meta->msec)
491 		title = mandoc_strdup(meta->title);
492 	else
493 		mandoc_asprintf(&title, "%s(%s)",
494 		    meta->title, meta->msec);
495 
496 	PAIR_CLASS_INIT(&tag, "head");
497 	t = print_otag(h, TAG_TABLE, 1, &tag);
498 
499 	print_otag(h, TAG_TBODY, 0, NULL);
500 
501 	tt = print_otag(h, TAG_TR, 0, NULL);
502 
503 	PAIR_CLASS_INIT(&tag, "head-ltitle");
504 	print_otag(h, TAG_TD, 1, &tag);
505 	print_text(h, title);
506 	print_stagq(h, tt);
507 
508 	PAIR_CLASS_INIT(&tag, "head-vol");
509 	print_otag(h, TAG_TD, 1, &tag);
510 	print_text(h, volume);
511 	print_stagq(h, tt);
512 
513 	PAIR_CLASS_INIT(&tag, "head-rtitle");
514 	print_otag(h, TAG_TD, 1, &tag);
515 	print_text(h, title);
516 	print_tagq(h, t);
517 
518 	free(title);
519 	free(volume);
520 	return 1;
521 }
522 
523 static int
524 mdoc_sh_pre(MDOC_ARGS)
525 {
526 	struct htmlpair	 tag;
527 
528 	switch (n->type) {
529 	case ROFFT_BLOCK:
530 		PAIR_CLASS_INIT(&tag, "section");
531 		print_otag(h, TAG_DIV, 1, &tag);
532 		return 1;
533 	case ROFFT_BODY:
534 		if (n->sec == SEC_AUTHORS)
535 			h->flags &= ~(HTML_SPLIT|HTML_NOSPLIT);
536 		return 1;
537 	default:
538 		break;
539 	}
540 
541 	bufinit(h);
542 
543 	for (n = n->child; n != NULL && n->type == ROFFT_TEXT; ) {
544 		bufcat_id(h, n->string);
545 		if (NULL != (n = n->next))
546 			bufcat_id(h, " ");
547 	}
548 
549 	if (NULL == n) {
550 		PAIR_ID_INIT(&tag, h->buf);
551 		print_otag(h, TAG_H1, 1, &tag);
552 	} else
553 		print_otag(h, TAG_H1, 0, NULL);
554 
555 	return 1;
556 }
557 
558 static int
559 mdoc_ss_pre(MDOC_ARGS)
560 {
561 	struct htmlpair	 tag;
562 
563 	if (n->type == ROFFT_BLOCK) {
564 		PAIR_CLASS_INIT(&tag, "subsection");
565 		print_otag(h, TAG_DIV, 1, &tag);
566 		return 1;
567 	} else if (n->type == ROFFT_BODY)
568 		return 1;
569 
570 	bufinit(h);
571 
572 	for (n = n->child; n != NULL && n->type == ROFFT_TEXT; ) {
573 		bufcat_id(h, n->string);
574 		if (NULL != (n = n->next))
575 			bufcat_id(h, " ");
576 	}
577 
578 	if (NULL == n) {
579 		PAIR_ID_INIT(&tag, h->buf);
580 		print_otag(h, TAG_H2, 1, &tag);
581 	} else
582 		print_otag(h, TAG_H2, 0, NULL);
583 
584 	return 1;
585 }
586 
587 static int
588 mdoc_fl_pre(MDOC_ARGS)
589 {
590 	struct htmlpair	 tag;
591 
592 	PAIR_CLASS_INIT(&tag, "flag");
593 	print_otag(h, TAG_B, 1, &tag);
594 
595 	/* `Cm' has no leading hyphen. */
596 
597 	if (MDOC_Cm == n->tok)
598 		return 1;
599 
600 	print_text(h, "\\-");
601 
602 	if (!(n->child == NULL &&
603 	    (n->next == NULL ||
604 	     n->next->type == ROFFT_TEXT ||
605 	     n->next->flags & NODE_LINE)))
606 		h->flags |= HTML_NOSPACE;
607 
608 	return 1;
609 }
610 
611 static int
612 mdoc_nd_pre(MDOC_ARGS)
613 {
614 	struct htmlpair	 tag;
615 
616 	if (n->type != ROFFT_BODY)
617 		return 1;
618 
619 	/* XXX: this tag in theory can contain block elements. */
620 
621 	print_text(h, "\\(em");
622 	PAIR_CLASS_INIT(&tag, "desc");
623 	print_otag(h, TAG_SPAN, 1, &tag);
624 	return 1;
625 }
626 
627 static int
628 mdoc_nm_pre(MDOC_ARGS)
629 {
630 	struct htmlpair	 tag;
631 	struct roffsu	 su;
632 	int		 len;
633 
634 	switch (n->type) {
635 	case ROFFT_HEAD:
636 		print_otag(h, TAG_TD, 0, NULL);
637 		/* FALLTHROUGH */
638 	case ROFFT_ELEM:
639 		PAIR_CLASS_INIT(&tag, "name");
640 		print_otag(h, TAG_B, 1, &tag);
641 		if (n->child == NULL && meta->name != NULL)
642 			print_text(h, meta->name);
643 		return 1;
644 	case ROFFT_BODY:
645 		print_otag(h, TAG_TD, 0, NULL);
646 		return 1;
647 	default:
648 		break;
649 	}
650 
651 	synopsis_pre(h, n);
652 	PAIR_CLASS_INIT(&tag, "synopsis");
653 	print_otag(h, TAG_TABLE, 1, &tag);
654 
655 	for (len = 0, n = n->head->child; n; n = n->next)
656 		if (n->type == ROFFT_TEXT)
657 			len += html_strlen(n->string);
658 
659 	if (len == 0 && meta->name != NULL)
660 		len = html_strlen(meta->name);
661 
662 	SCALE_HS_INIT(&su, len);
663 	bufinit(h);
664 	bufcat_su(h, "width", &su);
665 	PAIR_STYLE_INIT(&tag, h);
666 	print_otag(h, TAG_COL, 1, &tag);
667 	print_otag(h, TAG_COL, 0, NULL);
668 	print_otag(h, TAG_TBODY, 0, NULL);
669 	print_otag(h, TAG_TR, 0, NULL);
670 	return 1;
671 }
672 
673 static int
674 mdoc_xr_pre(MDOC_ARGS)
675 {
676 	struct htmlpair	 tag[2];
677 
678 	if (NULL == n->child)
679 		return 0;
680 
681 	PAIR_CLASS_INIT(&tag[0], "link-man");
682 
683 	if (h->base_man) {
684 		buffmt_man(h, n->child->string,
685 		    n->child->next ?
686 		    n->child->next->string : NULL);
687 		PAIR_HREF_INIT(&tag[1], h->buf);
688 		print_otag(h, TAG_A, 2, tag);
689 	} else
690 		print_otag(h, TAG_A, 1, tag);
691 
692 	n = n->child;
693 	print_text(h, n->string);
694 
695 	if (NULL == (n = n->next))
696 		return 0;
697 
698 	h->flags |= HTML_NOSPACE;
699 	print_text(h, "(");
700 	h->flags |= HTML_NOSPACE;
701 	print_text(h, n->string);
702 	h->flags |= HTML_NOSPACE;
703 	print_text(h, ")");
704 	return 0;
705 }
706 
707 static int
708 mdoc_ns_pre(MDOC_ARGS)
709 {
710 
711 	if ( ! (NODE_LINE & n->flags))
712 		h->flags |= HTML_NOSPACE;
713 	return 1;
714 }
715 
716 static int
717 mdoc_ar_pre(MDOC_ARGS)
718 {
719 	struct htmlpair tag;
720 
721 	PAIR_CLASS_INIT(&tag, "arg");
722 	print_otag(h, TAG_I, 1, &tag);
723 	return 1;
724 }
725 
726 static int
727 mdoc_xx_pre(MDOC_ARGS)
728 {
729 	struct htmlpair	 tag;
730 
731 	PAIR_CLASS_INIT(&tag, "unix");
732 	print_otag(h, TAG_SPAN, 1, &tag);
733 	return 1;
734 }
735 
736 static int
737 mdoc_it_pre(MDOC_ARGS)
738 {
739 	struct roffsu	 su;
740 	enum mdoc_list	 type;
741 	struct htmlpair	 tag[2];
742 	const struct roff_node *bl;
743 
744 	bl = n->parent;
745 	while (bl && MDOC_Bl != bl->tok)
746 		bl = bl->parent;
747 
748 	assert(bl);
749 
750 	type = bl->norm->Bl.type;
751 
752 	assert(lists[type]);
753 	PAIR_CLASS_INIT(&tag[0], lists[type]);
754 
755 	bufinit(h);
756 
757 	if (n->type == ROFFT_HEAD) {
758 		switch (type) {
759 		case LIST_bullet:
760 		case LIST_dash:
761 		case LIST_item:
762 		case LIST_hyphen:
763 		case LIST_enum:
764 			return 0;
765 		case LIST_diag:
766 		case LIST_hang:
767 		case LIST_inset:
768 		case LIST_ohang:
769 		case LIST_tag:
770 			SCALE_VS_INIT(&su, ! bl->norm->Bl.comp);
771 			bufcat_su(h, "margin-top", &su);
772 			PAIR_STYLE_INIT(&tag[1], h);
773 			print_otag(h, TAG_DT, 2, tag);
774 			if (LIST_diag != type)
775 				break;
776 			PAIR_CLASS_INIT(&tag[0], "diag");
777 			print_otag(h, TAG_B, 1, tag);
778 			break;
779 		case LIST_column:
780 			break;
781 		default:
782 			break;
783 		}
784 	} else if (n->type == ROFFT_BODY) {
785 		switch (type) {
786 		case LIST_bullet:
787 		case LIST_hyphen:
788 		case LIST_dash:
789 		case LIST_enum:
790 		case LIST_item:
791 			SCALE_VS_INIT(&su, ! bl->norm->Bl.comp);
792 			bufcat_su(h, "margin-top", &su);
793 			PAIR_STYLE_INIT(&tag[1], h);
794 			print_otag(h, TAG_LI, 2, tag);
795 			break;
796 		case LIST_diag:
797 		case LIST_hang:
798 		case LIST_inset:
799 		case LIST_ohang:
800 		case LIST_tag:
801 			if (NULL == bl->norm->Bl.width) {
802 				print_otag(h, TAG_DD, 1, tag);
803 				break;
804 			}
805 			a2width(bl->norm->Bl.width, &su);
806 			bufcat_su(h, "margin-left", &su);
807 			PAIR_STYLE_INIT(&tag[1], h);
808 			print_otag(h, TAG_DD, 2, tag);
809 			break;
810 		case LIST_column:
811 			SCALE_VS_INIT(&su, ! bl->norm->Bl.comp);
812 			bufcat_su(h, "margin-top", &su);
813 			PAIR_STYLE_INIT(&tag[1], h);
814 			print_otag(h, TAG_TD, 2, tag);
815 			break;
816 		default:
817 			break;
818 		}
819 	} else {
820 		switch (type) {
821 		case LIST_column:
822 			print_otag(h, TAG_TR, 1, tag);
823 			break;
824 		default:
825 			break;
826 		}
827 	}
828 
829 	return 1;
830 }
831 
832 static int
833 mdoc_bl_pre(MDOC_ARGS)
834 {
835 	int		 i;
836 	struct htmlpair	 tag[3];
837 	struct roffsu	 su;
838 	char		 buf[BUFSIZ];
839 
840 	if (n->type == ROFFT_BODY) {
841 		if (LIST_column == n->norm->Bl.type)
842 			print_otag(h, TAG_TBODY, 0, NULL);
843 		return 1;
844 	}
845 
846 	if (n->type == ROFFT_HEAD) {
847 		if (LIST_column != n->norm->Bl.type)
848 			return 0;
849 
850 		/*
851 		 * For each column, print out the <COL> tag with our
852 		 * suggested width.  The last column gets min-width, as
853 		 * in terminal mode it auto-sizes to the width of the
854 		 * screen and we want to preserve that behaviour.
855 		 */
856 
857 		for (i = 0; i < (int)n->norm->Bl.ncols; i++) {
858 			bufinit(h);
859 			a2width(n->norm->Bl.cols[i], &su);
860 			if (i < (int)n->norm->Bl.ncols - 1)
861 				bufcat_su(h, "width", &su);
862 			else
863 				bufcat_su(h, "min-width", &su);
864 			PAIR_STYLE_INIT(&tag[0], h);
865 			print_otag(h, TAG_COL, 1, tag);
866 		}
867 
868 		return 0;
869 	}
870 
871 	SCALE_VS_INIT(&su, 0);
872 	bufinit(h);
873 	bufcat_su(h, "margin-top", &su);
874 	bufcat_su(h, "margin-bottom", &su);
875 	PAIR_STYLE_INIT(&tag[0], h);
876 
877 	assert(lists[n->norm->Bl.type]);
878 	(void)strlcpy(buf, "list ", BUFSIZ);
879 	(void)strlcat(buf, lists[n->norm->Bl.type], BUFSIZ);
880 	PAIR_INIT(&tag[1], ATTR_CLASS, buf);
881 
882 	/* Set the block's left-hand margin. */
883 
884 	if (n->norm->Bl.offs) {
885 		a2width(n->norm->Bl.offs, &su);
886 		bufcat_su(h, "margin-left", &su);
887 	}
888 
889 	switch (n->norm->Bl.type) {
890 	case LIST_bullet:
891 	case LIST_dash:
892 	case LIST_hyphen:
893 	case LIST_item:
894 		print_otag(h, TAG_UL, 2, tag);
895 		break;
896 	case LIST_enum:
897 		print_otag(h, TAG_OL, 2, tag);
898 		break;
899 	case LIST_diag:
900 	case LIST_hang:
901 	case LIST_inset:
902 	case LIST_ohang:
903 	case LIST_tag:
904 		print_otag(h, TAG_DL, 2, tag);
905 		break;
906 	case LIST_column:
907 		print_otag(h, TAG_TABLE, 2, tag);
908 		break;
909 	default:
910 		abort();
911 	}
912 
913 	return 1;
914 }
915 
916 static int
917 mdoc_ex_pre(MDOC_ARGS)
918 {
919 	if (n->prev)
920 		print_otag(h, TAG_BR, 0, NULL);
921 	return 1;
922 }
923 
924 static int
925 mdoc_em_pre(MDOC_ARGS)
926 {
927 	struct htmlpair	tag;
928 
929 	PAIR_CLASS_INIT(&tag, "emph");
930 	print_otag(h, TAG_SPAN, 1, &tag);
931 	return 1;
932 }
933 
934 static int
935 mdoc_d1_pre(MDOC_ARGS)
936 {
937 	struct htmlpair	 tag[2];
938 	struct roffsu	 su;
939 
940 	if (n->type != ROFFT_BLOCK)
941 		return 1;
942 
943 	SCALE_VS_INIT(&su, 0);
944 	bufinit(h);
945 	bufcat_su(h, "margin-top", &su);
946 	bufcat_su(h, "margin-bottom", &su);
947 	PAIR_STYLE_INIT(&tag[0], h);
948 	print_otag(h, TAG_BLOCKQUOTE, 1, tag);
949 
950 	/* BLOCKQUOTE needs a block body. */
951 
952 	PAIR_CLASS_INIT(&tag[0], "display");
953 	print_otag(h, TAG_DIV, 1, tag);
954 
955 	if (MDOC_Dl == n->tok) {
956 		PAIR_CLASS_INIT(&tag[0], "lit");
957 		print_otag(h, TAG_CODE, 1, tag);
958 	}
959 
960 	return 1;
961 }
962 
963 static int
964 mdoc_sx_pre(MDOC_ARGS)
965 {
966 	struct htmlpair	 tag[2];
967 
968 	bufinit(h);
969 	bufcat(h, "#");
970 
971 	for (n = n->child; n; ) {
972 		bufcat_id(h, n->string);
973 		if (NULL != (n = n->next))
974 			bufcat_id(h, " ");
975 	}
976 
977 	PAIR_CLASS_INIT(&tag[0], "link-sec");
978 	PAIR_HREF_INIT(&tag[1], h->buf);
979 
980 	print_otag(h, TAG_I, 1, tag);
981 	print_otag(h, TAG_A, 2, tag);
982 	return 1;
983 }
984 
985 static int
986 mdoc_bd_pre(MDOC_ARGS)
987 {
988 	struct htmlpair		 tag[2];
989 	int			 comp, sv;
990 	struct roff_node	*nn;
991 	struct roffsu		 su;
992 
993 	if (n->type == ROFFT_HEAD)
994 		return 0;
995 
996 	if (n->type == ROFFT_BLOCK) {
997 		comp = n->norm->Bd.comp;
998 		for (nn = n; nn && ! comp; nn = nn->parent) {
999 			if (nn->type != ROFFT_BLOCK)
1000 				continue;
1001 			if (MDOC_Ss == nn->tok || MDOC_Sh == nn->tok)
1002 				comp = 1;
1003 			if (nn->prev)
1004 				break;
1005 		}
1006 		if ( ! comp)
1007 			print_paragraph(h);
1008 		return 1;
1009 	}
1010 
1011 	/* Handle the -offset argument. */
1012 
1013 	if (n->norm->Bd.offs == NULL ||
1014 	    ! strcmp(n->norm->Bd.offs, "left"))
1015 		SCALE_HS_INIT(&su, 0);
1016 	else if ( ! strcmp(n->norm->Bd.offs, "indent"))
1017 		SCALE_HS_INIT(&su, INDENT);
1018 	else if ( ! strcmp(n->norm->Bd.offs, "indent-two"))
1019 		SCALE_HS_INIT(&su, INDENT * 2);
1020 	else
1021 		a2width(n->norm->Bd.offs, &su);
1022 
1023 	bufinit(h);
1024 	bufcat_su(h, "margin-left", &su);
1025 	PAIR_STYLE_INIT(&tag[0], h);
1026 
1027 	PAIR_CLASS_INIT(&tag[1], "display");
1028 	print_otag(h, TAG_DIV, 2, tag);
1029 
1030 	if (n->norm->Bd.type != DISP_unfilled &&
1031 	    n->norm->Bd.type != DISP_literal)
1032 		return 1;
1033 
1034 	PAIR_CLASS_INIT(&tag[0], "lit");
1035 	print_otag(h, TAG_PRE, 1, tag);
1036 
1037 	/* This can be recursive: save & set our literal state. */
1038 
1039 	sv = h->flags & HTML_LITERAL;
1040 	h->flags |= HTML_LITERAL;
1041 
1042 	for (nn = n->child; nn; nn = nn->next) {
1043 		print_mdoc_node(meta, nn, h);
1044 		/*
1045 		 * If the printed node flushes its own line, then we
1046 		 * needn't do it here as well.  This is hacky, but the
1047 		 * notion of selective eoln whitespace is pretty dumb
1048 		 * anyway, so don't sweat it.
1049 		 */
1050 		switch (nn->tok) {
1051 		case MDOC_Sm:
1052 		case MDOC_br:
1053 		case MDOC_sp:
1054 		case MDOC_Bl:
1055 		case MDOC_D1:
1056 		case MDOC_Dl:
1057 		case MDOC_Lp:
1058 		case MDOC_Pp:
1059 			continue;
1060 		default:
1061 			break;
1062 		}
1063 		if (h->flags & HTML_NONEWLINE ||
1064 		    (nn->next && ! (nn->next->flags & NODE_LINE)))
1065 			continue;
1066 		else if (nn->next)
1067 			print_text(h, "\n");
1068 
1069 		h->flags |= HTML_NOSPACE;
1070 	}
1071 
1072 	if (0 == sv)
1073 		h->flags &= ~HTML_LITERAL;
1074 
1075 	return 0;
1076 }
1077 
1078 static int
1079 mdoc_pa_pre(MDOC_ARGS)
1080 {
1081 	struct htmlpair	tag;
1082 
1083 	PAIR_CLASS_INIT(&tag, "file");
1084 	print_otag(h, TAG_I, 1, &tag);
1085 	return 1;
1086 }
1087 
1088 static int
1089 mdoc_ad_pre(MDOC_ARGS)
1090 {
1091 	struct htmlpair	tag;
1092 
1093 	PAIR_CLASS_INIT(&tag, "addr");
1094 	print_otag(h, TAG_I, 1, &tag);
1095 	return 1;
1096 }
1097 
1098 static int
1099 mdoc_an_pre(MDOC_ARGS)
1100 {
1101 	struct htmlpair	tag;
1102 
1103 	if (n->norm->An.auth == AUTH_split) {
1104 		h->flags &= ~HTML_NOSPLIT;
1105 		h->flags |= HTML_SPLIT;
1106 		return 0;
1107 	}
1108 	if (n->norm->An.auth == AUTH_nosplit) {
1109 		h->flags &= ~HTML_SPLIT;
1110 		h->flags |= HTML_NOSPLIT;
1111 		return 0;
1112 	}
1113 
1114 	if (h->flags & HTML_SPLIT)
1115 		print_otag(h, TAG_BR, 0, NULL);
1116 
1117 	if (n->sec == SEC_AUTHORS && ! (h->flags & HTML_NOSPLIT))
1118 		h->flags |= HTML_SPLIT;
1119 
1120 	PAIR_CLASS_INIT(&tag, "author");
1121 	print_otag(h, TAG_SPAN, 1, &tag);
1122 	return 1;
1123 }
1124 
1125 static int
1126 mdoc_cd_pre(MDOC_ARGS)
1127 {
1128 	struct htmlpair	tag;
1129 
1130 	synopsis_pre(h, n);
1131 	PAIR_CLASS_INIT(&tag, "config");
1132 	print_otag(h, TAG_B, 1, &tag);
1133 	return 1;
1134 }
1135 
1136 static int
1137 mdoc_dv_pre(MDOC_ARGS)
1138 {
1139 	struct htmlpair	tag;
1140 
1141 	PAIR_CLASS_INIT(&tag, "define");
1142 	print_otag(h, TAG_SPAN, 1, &tag);
1143 	return 1;
1144 }
1145 
1146 static int
1147 mdoc_ev_pre(MDOC_ARGS)
1148 {
1149 	struct htmlpair	tag;
1150 
1151 	PAIR_CLASS_INIT(&tag, "env");
1152 	print_otag(h, TAG_SPAN, 1, &tag);
1153 	return 1;
1154 }
1155 
1156 static int
1157 mdoc_er_pre(MDOC_ARGS)
1158 {
1159 	struct htmlpair	tag;
1160 
1161 	PAIR_CLASS_INIT(&tag, "errno");
1162 	print_otag(h, TAG_SPAN, 1, &tag);
1163 	return 1;
1164 }
1165 
1166 static int
1167 mdoc_fa_pre(MDOC_ARGS)
1168 {
1169 	const struct roff_node	*nn;
1170 	struct htmlpair		 tag;
1171 	struct tag		*t;
1172 
1173 	PAIR_CLASS_INIT(&tag, "farg");
1174 	if (n->parent->tok != MDOC_Fo) {
1175 		print_otag(h, TAG_I, 1, &tag);
1176 		return 1;
1177 	}
1178 
1179 	for (nn = n->child; nn; nn = nn->next) {
1180 		t = print_otag(h, TAG_I, 1, &tag);
1181 		print_text(h, nn->string);
1182 		print_tagq(h, t);
1183 		if (nn->next) {
1184 			h->flags |= HTML_NOSPACE;
1185 			print_text(h, ",");
1186 		}
1187 	}
1188 
1189 	if (n->child && n->next && n->next->tok == MDOC_Fa) {
1190 		h->flags |= HTML_NOSPACE;
1191 		print_text(h, ",");
1192 	}
1193 
1194 	return 0;
1195 }
1196 
1197 static int
1198 mdoc_fd_pre(MDOC_ARGS)
1199 {
1200 	struct htmlpair	 tag[2];
1201 	char		 buf[BUFSIZ];
1202 	size_t		 sz;
1203 	int		 i;
1204 	struct tag	*t;
1205 
1206 	synopsis_pre(h, n);
1207 
1208 	if (NULL == (n = n->child))
1209 		return 0;
1210 
1211 	assert(n->type == ROFFT_TEXT);
1212 
1213 	if (strcmp(n->string, "#include")) {
1214 		PAIR_CLASS_INIT(&tag[0], "macro");
1215 		print_otag(h, TAG_B, 1, tag);
1216 		return 1;
1217 	}
1218 
1219 	PAIR_CLASS_INIT(&tag[0], "includes");
1220 	print_otag(h, TAG_B, 1, tag);
1221 	print_text(h, n->string);
1222 
1223 	if (NULL != (n = n->next)) {
1224 		assert(n->type == ROFFT_TEXT);
1225 
1226 		/*
1227 		 * XXX This is broken and not easy to fix.
1228 		 * When using -Oincludes, truncation may occur.
1229 		 * Dynamic allocation wouldn't help because
1230 		 * passing long strings to buffmt_includes()
1231 		 * does not work either.
1232 		 */
1233 
1234 		strlcpy(buf, '<' == *n->string || '"' == *n->string ?
1235 		    n->string + 1 : n->string, BUFSIZ);
1236 
1237 		sz = strlen(buf);
1238 		if (sz && ('>' == buf[sz - 1] || '"' == buf[sz - 1]))
1239 			buf[sz - 1] = '\0';
1240 
1241 		PAIR_CLASS_INIT(&tag[0], "link-includes");
1242 
1243 		i = 1;
1244 		if (h->base_includes) {
1245 			buffmt_includes(h, buf);
1246 			PAIR_HREF_INIT(&tag[i], h->buf);
1247 			i++;
1248 		}
1249 
1250 		t = print_otag(h, TAG_A, i, tag);
1251 		print_text(h, n->string);
1252 		print_tagq(h, t);
1253 
1254 		n = n->next;
1255 	}
1256 
1257 	for ( ; n; n = n->next) {
1258 		assert(n->type == ROFFT_TEXT);
1259 		print_text(h, n->string);
1260 	}
1261 
1262 	return 0;
1263 }
1264 
1265 static int
1266 mdoc_vt_pre(MDOC_ARGS)
1267 {
1268 	struct htmlpair	 tag;
1269 
1270 	if (n->type == ROFFT_BLOCK) {
1271 		synopsis_pre(h, n);
1272 		return 1;
1273 	} else if (n->type == ROFFT_ELEM) {
1274 		synopsis_pre(h, n);
1275 	} else if (n->type == ROFFT_HEAD)
1276 		return 0;
1277 
1278 	PAIR_CLASS_INIT(&tag, "type");
1279 	print_otag(h, TAG_SPAN, 1, &tag);
1280 	return 1;
1281 }
1282 
1283 static int
1284 mdoc_ft_pre(MDOC_ARGS)
1285 {
1286 	struct htmlpair	 tag;
1287 
1288 	synopsis_pre(h, n);
1289 	PAIR_CLASS_INIT(&tag, "ftype");
1290 	print_otag(h, TAG_I, 1, &tag);
1291 	return 1;
1292 }
1293 
1294 static int
1295 mdoc_fn_pre(MDOC_ARGS)
1296 {
1297 	struct tag	*t;
1298 	struct htmlpair	 tag[2];
1299 	char		 nbuf[BUFSIZ];
1300 	const char	*sp, *ep;
1301 	int		 sz, i, pretty;
1302 
1303 	pretty = NODE_SYNPRETTY & n->flags;
1304 	synopsis_pre(h, n);
1305 
1306 	/* Split apart into type and name. */
1307 	assert(n->child->string);
1308 	sp = n->child->string;
1309 
1310 	ep = strchr(sp, ' ');
1311 	if (NULL != ep) {
1312 		PAIR_CLASS_INIT(&tag[0], "ftype");
1313 		t = print_otag(h, TAG_I, 1, tag);
1314 
1315 		while (ep) {
1316 			sz = MIN((int)(ep - sp), BUFSIZ - 1);
1317 			(void)memcpy(nbuf, sp, (size_t)sz);
1318 			nbuf[sz] = '\0';
1319 			print_text(h, nbuf);
1320 			sp = ++ep;
1321 			ep = strchr(sp, ' ');
1322 		}
1323 		print_tagq(h, t);
1324 	}
1325 
1326 	PAIR_CLASS_INIT(&tag[0], "fname");
1327 
1328 	/*
1329 	 * FIXME: only refer to IDs that we know exist.
1330 	 */
1331 
1332 #if 0
1333 	if (NODE_SYNPRETTY & n->flags) {
1334 		nbuf[0] = '\0';
1335 		html_idcat(nbuf, sp, BUFSIZ);
1336 		PAIR_ID_INIT(&tag[1], nbuf);
1337 	} else {
1338 		strlcpy(nbuf, "#", BUFSIZ);
1339 		html_idcat(nbuf, sp, BUFSIZ);
1340 		PAIR_HREF_INIT(&tag[1], nbuf);
1341 	}
1342 #endif
1343 
1344 	t = print_otag(h, TAG_B, 1, tag);
1345 
1346 	if (sp)
1347 		print_text(h, sp);
1348 
1349 	print_tagq(h, t);
1350 
1351 	h->flags |= HTML_NOSPACE;
1352 	print_text(h, "(");
1353 	h->flags |= HTML_NOSPACE;
1354 
1355 	PAIR_CLASS_INIT(&tag[0], "farg");
1356 	bufinit(h);
1357 	bufcat_style(h, "white-space", "nowrap");
1358 	PAIR_STYLE_INIT(&tag[1], h);
1359 
1360 	for (n = n->child->next; n; n = n->next) {
1361 		i = 1;
1362 		if (NODE_SYNPRETTY & n->flags)
1363 			i = 2;
1364 		t = print_otag(h, TAG_I, i, tag);
1365 		print_text(h, n->string);
1366 		print_tagq(h, t);
1367 		if (n->next) {
1368 			h->flags |= HTML_NOSPACE;
1369 			print_text(h, ",");
1370 		}
1371 	}
1372 
1373 	h->flags |= HTML_NOSPACE;
1374 	print_text(h, ")");
1375 
1376 	if (pretty) {
1377 		h->flags |= HTML_NOSPACE;
1378 		print_text(h, ";");
1379 	}
1380 
1381 	return 0;
1382 }
1383 
1384 static int
1385 mdoc_sm_pre(MDOC_ARGS)
1386 {
1387 
1388 	if (NULL == n->child)
1389 		h->flags ^= HTML_NONOSPACE;
1390 	else if (0 == strcmp("on", n->child->string))
1391 		h->flags &= ~HTML_NONOSPACE;
1392 	else
1393 		h->flags |= HTML_NONOSPACE;
1394 
1395 	if ( ! (HTML_NONOSPACE & h->flags))
1396 		h->flags &= ~HTML_NOSPACE;
1397 
1398 	return 0;
1399 }
1400 
1401 static int
1402 mdoc_skip_pre(MDOC_ARGS)
1403 {
1404 
1405 	return 0;
1406 }
1407 
1408 static int
1409 mdoc_pp_pre(MDOC_ARGS)
1410 {
1411 
1412 	print_paragraph(h);
1413 	return 0;
1414 }
1415 
1416 static int
1417 mdoc_sp_pre(MDOC_ARGS)
1418 {
1419 	struct roffsu	 su;
1420 	struct htmlpair	 tag;
1421 
1422 	SCALE_VS_INIT(&su, 1);
1423 
1424 	if (MDOC_sp == n->tok) {
1425 		if (NULL != (n = n->child)) {
1426 			if ( ! a2roffsu(n->string, &su, SCALE_VS))
1427 				su.scale = 1.0;
1428 			else if (su.scale < 0.0)
1429 				su.scale = 0.0;
1430 		}
1431 	} else
1432 		su.scale = 0.0;
1433 
1434 	bufinit(h);
1435 	bufcat_su(h, "height", &su);
1436 	PAIR_STYLE_INIT(&tag, h);
1437 	print_otag(h, TAG_DIV, 1, &tag);
1438 
1439 	/* So the div isn't empty: */
1440 	print_text(h, "\\~");
1441 
1442 	return 0;
1443 
1444 }
1445 
1446 static int
1447 mdoc_lk_pre(MDOC_ARGS)
1448 {
1449 	struct htmlpair	 tag[2];
1450 
1451 	if (NULL == (n = n->child))
1452 		return 0;
1453 
1454 	assert(n->type == ROFFT_TEXT);
1455 
1456 	PAIR_CLASS_INIT(&tag[0], "link-ext");
1457 	PAIR_HREF_INIT(&tag[1], n->string);
1458 
1459 	print_otag(h, TAG_A, 2, tag);
1460 
1461 	if (NULL == n->next)
1462 		print_text(h, n->string);
1463 
1464 	for (n = n->next; n; n = n->next)
1465 		print_text(h, n->string);
1466 
1467 	return 0;
1468 }
1469 
1470 static int
1471 mdoc_mt_pre(MDOC_ARGS)
1472 {
1473 	struct htmlpair	 tag[2];
1474 	struct tag	*t;
1475 
1476 	PAIR_CLASS_INIT(&tag[0], "link-mail");
1477 
1478 	for (n = n->child; n; n = n->next) {
1479 		assert(n->type == ROFFT_TEXT);
1480 
1481 		bufinit(h);
1482 		bufcat(h, "mailto:");
1483 		bufcat(h, n->string);
1484 
1485 		PAIR_HREF_INIT(&tag[1], h->buf);
1486 		t = print_otag(h, TAG_A, 2, tag);
1487 		print_text(h, n->string);
1488 		print_tagq(h, t);
1489 	}
1490 
1491 	return 0;
1492 }
1493 
1494 static int
1495 mdoc_fo_pre(MDOC_ARGS)
1496 {
1497 	struct htmlpair	 tag;
1498 	struct tag	*t;
1499 
1500 	if (n->type == ROFFT_BODY) {
1501 		h->flags |= HTML_NOSPACE;
1502 		print_text(h, "(");
1503 		h->flags |= HTML_NOSPACE;
1504 		return 1;
1505 	} else if (n->type == ROFFT_BLOCK) {
1506 		synopsis_pre(h, n);
1507 		return 1;
1508 	}
1509 
1510 	if (n->child == NULL)
1511 		return 0;
1512 
1513 	assert(n->child->string);
1514 	PAIR_CLASS_INIT(&tag, "fname");
1515 	t = print_otag(h, TAG_B, 1, &tag);
1516 	print_text(h, n->child->string);
1517 	print_tagq(h, t);
1518 	return 0;
1519 }
1520 
1521 static void
1522 mdoc_fo_post(MDOC_ARGS)
1523 {
1524 
1525 	if (n->type != ROFFT_BODY)
1526 		return;
1527 	h->flags |= HTML_NOSPACE;
1528 	print_text(h, ")");
1529 	h->flags |= HTML_NOSPACE;
1530 	print_text(h, ";");
1531 }
1532 
1533 static int
1534 mdoc_in_pre(MDOC_ARGS)
1535 {
1536 	struct tag	*t;
1537 	struct htmlpair	 tag[2];
1538 	int		 i;
1539 
1540 	synopsis_pre(h, n);
1541 
1542 	PAIR_CLASS_INIT(&tag[0], "includes");
1543 	print_otag(h, TAG_B, 1, tag);
1544 
1545 	/*
1546 	 * The first argument of the `In' gets special treatment as
1547 	 * being a linked value.  Subsequent values are printed
1548 	 * afterward.  groff does similarly.  This also handles the case
1549 	 * of no children.
1550 	 */
1551 
1552 	if (NODE_SYNPRETTY & n->flags && NODE_LINE & n->flags)
1553 		print_text(h, "#include");
1554 
1555 	print_text(h, "<");
1556 	h->flags |= HTML_NOSPACE;
1557 
1558 	if (NULL != (n = n->child)) {
1559 		assert(n->type == ROFFT_TEXT);
1560 
1561 		PAIR_CLASS_INIT(&tag[0], "link-includes");
1562 
1563 		i = 1;
1564 		if (h->base_includes) {
1565 			buffmt_includes(h, n->string);
1566 			PAIR_HREF_INIT(&tag[i], h->buf);
1567 			i++;
1568 		}
1569 
1570 		t = print_otag(h, TAG_A, i, tag);
1571 		print_text(h, n->string);
1572 		print_tagq(h, t);
1573 
1574 		n = n->next;
1575 	}
1576 
1577 	h->flags |= HTML_NOSPACE;
1578 	print_text(h, ">");
1579 
1580 	for ( ; n; n = n->next) {
1581 		assert(n->type == ROFFT_TEXT);
1582 		print_text(h, n->string);
1583 	}
1584 
1585 	return 0;
1586 }
1587 
1588 static int
1589 mdoc_ic_pre(MDOC_ARGS)
1590 {
1591 	struct htmlpair	tag;
1592 
1593 	PAIR_CLASS_INIT(&tag, "cmd");
1594 	print_otag(h, TAG_B, 1, &tag);
1595 	return 1;
1596 }
1597 
1598 static int
1599 mdoc_va_pre(MDOC_ARGS)
1600 {
1601 	struct htmlpair	tag;
1602 
1603 	PAIR_CLASS_INIT(&tag, "var");
1604 	print_otag(h, TAG_B, 1, &tag);
1605 	return 1;
1606 }
1607 
1608 static int
1609 mdoc_ap_pre(MDOC_ARGS)
1610 {
1611 
1612 	h->flags |= HTML_NOSPACE;
1613 	print_text(h, "\\(aq");
1614 	h->flags |= HTML_NOSPACE;
1615 	return 1;
1616 }
1617 
1618 static int
1619 mdoc_bf_pre(MDOC_ARGS)
1620 {
1621 	struct htmlpair	 tag[2];
1622 	struct roffsu	 su;
1623 
1624 	if (n->type == ROFFT_HEAD)
1625 		return 0;
1626 	else if (n->type != ROFFT_BODY)
1627 		return 1;
1628 
1629 	if (FONT_Em == n->norm->Bf.font)
1630 		PAIR_CLASS_INIT(&tag[0], "emph");
1631 	else if (FONT_Sy == n->norm->Bf.font)
1632 		PAIR_CLASS_INIT(&tag[0], "symb");
1633 	else if (FONT_Li == n->norm->Bf.font)
1634 		PAIR_CLASS_INIT(&tag[0], "lit");
1635 	else
1636 		PAIR_CLASS_INIT(&tag[0], "none");
1637 
1638 	/*
1639 	 * We want this to be inline-formatted, but needs to be div to
1640 	 * accept block children.
1641 	 */
1642 	bufinit(h);
1643 	bufcat_style(h, "display", "inline");
1644 	SCALE_HS_INIT(&su, 1);
1645 	/* Needs a left-margin for spacing. */
1646 	bufcat_su(h, "margin-left", &su);
1647 	PAIR_STYLE_INIT(&tag[1], h);
1648 	print_otag(h, TAG_DIV, 2, tag);
1649 	return 1;
1650 }
1651 
1652 static int
1653 mdoc_ms_pre(MDOC_ARGS)
1654 {
1655 	struct htmlpair	tag;
1656 
1657 	PAIR_CLASS_INIT(&tag, "symb");
1658 	print_otag(h, TAG_SPAN, 1, &tag);
1659 	return 1;
1660 }
1661 
1662 static int
1663 mdoc_igndelim_pre(MDOC_ARGS)
1664 {
1665 
1666 	h->flags |= HTML_IGNDELIM;
1667 	return 1;
1668 }
1669 
1670 static void
1671 mdoc_pf_post(MDOC_ARGS)
1672 {
1673 
1674 	if ( ! (n->next == NULL || n->next->flags & NODE_LINE))
1675 		h->flags |= HTML_NOSPACE;
1676 }
1677 
1678 static int
1679 mdoc_rs_pre(MDOC_ARGS)
1680 {
1681 	struct htmlpair	 tag;
1682 
1683 	if (n->type != ROFFT_BLOCK)
1684 		return 1;
1685 
1686 	if (n->prev && SEC_SEE_ALSO == n->sec)
1687 		print_paragraph(h);
1688 
1689 	PAIR_CLASS_INIT(&tag, "ref");
1690 	print_otag(h, TAG_SPAN, 1, &tag);
1691 	return 1;
1692 }
1693 
1694 static int
1695 mdoc_no_pre(MDOC_ARGS)
1696 {
1697 	struct htmlpair	tag;
1698 
1699 	PAIR_CLASS_INIT(&tag, "none");
1700 	print_otag(h, TAG_SPAN, 1, &tag);
1701 	return 1;
1702 }
1703 
1704 static int
1705 mdoc_li_pre(MDOC_ARGS)
1706 {
1707 	struct htmlpair	tag;
1708 
1709 	PAIR_CLASS_INIT(&tag, "lit");
1710 	print_otag(h, TAG_CODE, 1, &tag);
1711 	return 1;
1712 }
1713 
1714 static int
1715 mdoc_sy_pre(MDOC_ARGS)
1716 {
1717 	struct htmlpair	tag;
1718 
1719 	PAIR_CLASS_INIT(&tag, "symb");
1720 	print_otag(h, TAG_SPAN, 1, &tag);
1721 	return 1;
1722 }
1723 
1724 static int
1725 mdoc_lb_pre(MDOC_ARGS)
1726 {
1727 	struct htmlpair	tag;
1728 
1729 	if (SEC_LIBRARY == n->sec && NODE_LINE & n->flags && n->prev)
1730 		print_otag(h, TAG_BR, 0, NULL);
1731 
1732 	PAIR_CLASS_INIT(&tag, "lib");
1733 	print_otag(h, TAG_SPAN, 1, &tag);
1734 	return 1;
1735 }
1736 
1737 static int
1738 mdoc__x_pre(MDOC_ARGS)
1739 {
1740 	struct htmlpair	tag[2];
1741 	enum htmltag	t;
1742 
1743 	t = TAG_SPAN;
1744 
1745 	switch (n->tok) {
1746 	case MDOC__A:
1747 		PAIR_CLASS_INIT(&tag[0], "ref-auth");
1748 		if (n->prev && MDOC__A == n->prev->tok)
1749 			if (NULL == n->next || MDOC__A != n->next->tok)
1750 				print_text(h, "and");
1751 		break;
1752 	case MDOC__B:
1753 		PAIR_CLASS_INIT(&tag[0], "ref-book");
1754 		t = TAG_I;
1755 		break;
1756 	case MDOC__C:
1757 		PAIR_CLASS_INIT(&tag[0], "ref-city");
1758 		break;
1759 	case MDOC__D:
1760 		PAIR_CLASS_INIT(&tag[0], "ref-date");
1761 		break;
1762 	case MDOC__I:
1763 		PAIR_CLASS_INIT(&tag[0], "ref-issue");
1764 		t = TAG_I;
1765 		break;
1766 	case MDOC__J:
1767 		PAIR_CLASS_INIT(&tag[0], "ref-jrnl");
1768 		t = TAG_I;
1769 		break;
1770 	case MDOC__N:
1771 		PAIR_CLASS_INIT(&tag[0], "ref-num");
1772 		break;
1773 	case MDOC__O:
1774 		PAIR_CLASS_INIT(&tag[0], "ref-opt");
1775 		break;
1776 	case MDOC__P:
1777 		PAIR_CLASS_INIT(&tag[0], "ref-page");
1778 		break;
1779 	case MDOC__Q:
1780 		PAIR_CLASS_INIT(&tag[0], "ref-corp");
1781 		break;
1782 	case MDOC__R:
1783 		PAIR_CLASS_INIT(&tag[0], "ref-rep");
1784 		break;
1785 	case MDOC__T:
1786 		PAIR_CLASS_INIT(&tag[0], "ref-title");
1787 		break;
1788 	case MDOC__U:
1789 		PAIR_CLASS_INIT(&tag[0], "link-ref");
1790 		break;
1791 	case MDOC__V:
1792 		PAIR_CLASS_INIT(&tag[0], "ref-vol");
1793 		break;
1794 	default:
1795 		abort();
1796 	}
1797 
1798 	if (MDOC__U != n->tok) {
1799 		print_otag(h, t, 1, tag);
1800 		return 1;
1801 	}
1802 
1803 	PAIR_HREF_INIT(&tag[1], n->child->string);
1804 	print_otag(h, TAG_A, 2, tag);
1805 
1806 	return 1;
1807 }
1808 
1809 static void
1810 mdoc__x_post(MDOC_ARGS)
1811 {
1812 
1813 	if (MDOC__A == n->tok && n->next && MDOC__A == n->next->tok)
1814 		if (NULL == n->next->next || MDOC__A != n->next->next->tok)
1815 			if (NULL == n->prev || MDOC__A != n->prev->tok)
1816 				return;
1817 
1818 	/* TODO: %U */
1819 
1820 	if (NULL == n->parent || MDOC_Rs != n->parent->tok)
1821 		return;
1822 
1823 	h->flags |= HTML_NOSPACE;
1824 	print_text(h, n->next ? "," : ".");
1825 }
1826 
1827 static int
1828 mdoc_bk_pre(MDOC_ARGS)
1829 {
1830 
1831 	switch (n->type) {
1832 	case ROFFT_BLOCK:
1833 		break;
1834 	case ROFFT_HEAD:
1835 		return 0;
1836 	case ROFFT_BODY:
1837 		if (n->parent->args != NULL || n->prev->child == NULL)
1838 			h->flags |= HTML_PREKEEP;
1839 		break;
1840 	default:
1841 		abort();
1842 	}
1843 
1844 	return 1;
1845 }
1846 
1847 static void
1848 mdoc_bk_post(MDOC_ARGS)
1849 {
1850 
1851 	if (n->type == ROFFT_BODY)
1852 		h->flags &= ~(HTML_KEEP | HTML_PREKEEP);
1853 }
1854 
1855 static int
1856 mdoc_quote_pre(MDOC_ARGS)
1857 {
1858 	struct htmlpair	tag;
1859 
1860 	if (n->type != ROFFT_BODY)
1861 		return 1;
1862 
1863 	switch (n->tok) {
1864 	case MDOC_Ao:
1865 	case MDOC_Aq:
1866 		print_text(h, n->child != NULL && n->child->next == NULL &&
1867 		    n->child->tok == MDOC_Mt ?  "<" : "\\(la");
1868 		break;
1869 	case MDOC_Bro:
1870 	case MDOC_Brq:
1871 		print_text(h, "\\(lC");
1872 		break;
1873 	case MDOC_Bo:
1874 	case MDOC_Bq:
1875 		print_text(h, "\\(lB");
1876 		break;
1877 	case MDOC_Oo:
1878 	case MDOC_Op:
1879 		print_text(h, "\\(lB");
1880 		h->flags |= HTML_NOSPACE;
1881 		PAIR_CLASS_INIT(&tag, "opt");
1882 		print_otag(h, TAG_SPAN, 1, &tag);
1883 		break;
1884 	case MDOC_En:
1885 		if (NULL == n->norm->Es ||
1886 		    NULL == n->norm->Es->child)
1887 			return 1;
1888 		print_text(h, n->norm->Es->child->string);
1889 		break;
1890 	case MDOC_Do:
1891 	case MDOC_Dq:
1892 	case MDOC_Qo:
1893 	case MDOC_Qq:
1894 		print_text(h, "\\(lq");
1895 		break;
1896 	case MDOC_Po:
1897 	case MDOC_Pq:
1898 		print_text(h, "(");
1899 		break;
1900 	case MDOC_Ql:
1901 		print_text(h, "\\(oq");
1902 		h->flags |= HTML_NOSPACE;
1903 		PAIR_CLASS_INIT(&tag, "lit");
1904 		print_otag(h, TAG_CODE, 1, &tag);
1905 		break;
1906 	case MDOC_So:
1907 	case MDOC_Sq:
1908 		print_text(h, "\\(oq");
1909 		break;
1910 	default:
1911 		abort();
1912 	}
1913 
1914 	h->flags |= HTML_NOSPACE;
1915 	return 1;
1916 }
1917 
1918 static void
1919 mdoc_quote_post(MDOC_ARGS)
1920 {
1921 
1922 	if (n->type != ROFFT_BODY && n->type != ROFFT_ELEM)
1923 		return;
1924 
1925 	h->flags |= HTML_NOSPACE;
1926 
1927 	switch (n->tok) {
1928 	case MDOC_Ao:
1929 	case MDOC_Aq:
1930 		print_text(h, n->child != NULL && n->child->next == NULL &&
1931 		    n->child->tok == MDOC_Mt ?  ">" : "\\(ra");
1932 		break;
1933 	case MDOC_Bro:
1934 	case MDOC_Brq:
1935 		print_text(h, "\\(rC");
1936 		break;
1937 	case MDOC_Oo:
1938 	case MDOC_Op:
1939 	case MDOC_Bo:
1940 	case MDOC_Bq:
1941 		print_text(h, "\\(rB");
1942 		break;
1943 	case MDOC_En:
1944 		if (n->norm->Es == NULL ||
1945 		    n->norm->Es->child == NULL ||
1946 		    n->norm->Es->child->next == NULL)
1947 			h->flags &= ~HTML_NOSPACE;
1948 		else
1949 			print_text(h, n->norm->Es->child->next->string);
1950 		break;
1951 	case MDOC_Qo:
1952 	case MDOC_Qq:
1953 	case MDOC_Do:
1954 	case MDOC_Dq:
1955 		print_text(h, "\\(rq");
1956 		break;
1957 	case MDOC_Po:
1958 	case MDOC_Pq:
1959 		print_text(h, ")");
1960 		break;
1961 	case MDOC_Ql:
1962 	case MDOC_So:
1963 	case MDOC_Sq:
1964 		print_text(h, "\\(cq");
1965 		break;
1966 	default:
1967 		abort();
1968 	}
1969 }
1970 
1971 static int
1972 mdoc_eo_pre(MDOC_ARGS)
1973 {
1974 
1975 	if (n->type != ROFFT_BODY)
1976 		return 1;
1977 
1978 	if (n->end == ENDBODY_NOT &&
1979 	    n->parent->head->child == NULL &&
1980 	    n->child != NULL &&
1981 	    n->child->end != ENDBODY_NOT)
1982 		print_text(h, "\\&");
1983 	else if (n->end != ENDBODY_NOT ? n->child != NULL :
1984 	    n->parent->head->child != NULL && (n->child != NULL ||
1985 	    (n->parent->tail != NULL && n->parent->tail->child != NULL)))
1986 		h->flags |= HTML_NOSPACE;
1987 	return 1;
1988 }
1989 
1990 static void
1991 mdoc_eo_post(MDOC_ARGS)
1992 {
1993 	int	 body, tail;
1994 
1995 	if (n->type != ROFFT_BODY)
1996 		return;
1997 
1998 	if (n->end != ENDBODY_NOT) {
1999 		h->flags &= ~HTML_NOSPACE;
2000 		return;
2001 	}
2002 
2003 	body = n->child != NULL || n->parent->head->child != NULL;
2004 	tail = n->parent->tail != NULL && n->parent->tail->child != NULL;
2005 
2006 	if (body && tail)
2007 		h->flags |= HTML_NOSPACE;
2008 	else if ( ! tail)
2009 		h->flags &= ~HTML_NOSPACE;
2010 }
2011