xref: /freebsd/contrib/mandoc/html.c (revision 7fdf597e96a02165cfe22ff357b857d5fa15ed8a)
1 /* $Id: html.c,v 1.279 2022/08/09 11:23:11 schwarze Exp $ */
2 /*
3  * Copyright (c) 2008-2011, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
4  * Copyright (c) 2011-2015, 2017-2021 Ingo Schwarze <schwarze@openbsd.org>
5  * Copyright (c) 2022 Anna Vyalkova <cyber@sysrq.in>
6  *
7  * Permission to use, copy, modify, and distribute this software for any
8  * purpose with or without fee is hereby granted, provided that the above
9  * copyright notice and this permission notice appear in all copies.
10  *
11  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
12  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
14  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18  *
19  * Common functions for mandoc(1) HTML formatters.
20  * For use by individual formatters and by the main program.
21  */
22 #include "config.h"
23 
24 #include <sys/types.h>
25 #include <sys/stat.h>
26 
27 #include <assert.h>
28 #include <ctype.h>
29 #include <stdarg.h>
30 #include <stddef.h>
31 #include <stdio.h>
32 #include <stdint.h>
33 #include <stdlib.h>
34 #include <string.h>
35 #include <unistd.h>
36 
37 #include "mandoc_aux.h"
38 #include "mandoc_ohash.h"
39 #include "mandoc.h"
40 #include "roff.h"
41 #include "out.h"
42 #include "html.h"
43 #include "manconf.h"
44 #include "main.h"
45 
46 struct	htmldata {
47 	const char	 *name;
48 	int		  flags;
49 #define	HTML_INPHRASE	 (1 << 0)  /* Can appear in phrasing context. */
50 #define	HTML_TOPHRASE	 (1 << 1)  /* Establishes phrasing context. */
51 #define	HTML_NOSTACK	 (1 << 2)  /* Does not have an end tag. */
52 #define	HTML_NLBEFORE	 (1 << 3)  /* Output line break before opening. */
53 #define	HTML_NLBEGIN	 (1 << 4)  /* Output line break after opening. */
54 #define	HTML_NLEND	 (1 << 5)  /* Output line break before closing. */
55 #define	HTML_NLAFTER	 (1 << 6)  /* Output line break after closing. */
56 #define	HTML_NLAROUND	 (HTML_NLBEFORE | HTML_NLAFTER)
57 #define	HTML_NLINSIDE	 (HTML_NLBEGIN | HTML_NLEND)
58 #define	HTML_NLALL	 (HTML_NLAROUND | HTML_NLINSIDE)
59 #define	HTML_INDENT	 (1 << 7)  /* Indent content by two spaces. */
60 #define	HTML_NOINDENT	 (1 << 8)  /* Exception: never indent content. */
61 };
62 
63 static	const struct htmldata htmltags[TAG_MAX] = {
64 	{"html",	HTML_NLALL},
65 	{"head",	HTML_NLALL | HTML_INDENT},
66 	{"meta",	HTML_NOSTACK | HTML_NLALL},
67 	{"link",	HTML_NOSTACK | HTML_NLALL},
68 	{"style",	HTML_NLALL | HTML_INDENT},
69 	{"title",	HTML_NLAROUND},
70 	{"body",	HTML_NLALL},
71 	{"main",	HTML_NLALL},
72 	{"div",		HTML_NLAROUND},
73 	{"section",	HTML_NLALL},
74 	{"nav",		HTML_NLALL},
75 	{"table",	HTML_NLALL | HTML_INDENT},
76 	{"tr",		HTML_NLALL | HTML_INDENT},
77 	{"td",		HTML_NLAROUND},
78 	{"li",		HTML_NLAROUND | HTML_INDENT},
79 	{"ul",		HTML_NLALL | HTML_INDENT},
80 	{"ol",		HTML_NLALL | HTML_INDENT},
81 	{"dl",		HTML_NLALL | HTML_INDENT},
82 	{"dt",		HTML_NLAROUND},
83 	{"dd",		HTML_NLAROUND | HTML_INDENT},
84 	{"h2",		HTML_TOPHRASE | HTML_NLAROUND},
85 	{"h3",		HTML_TOPHRASE | HTML_NLAROUND},
86 	{"p",		HTML_TOPHRASE | HTML_NLAROUND | HTML_INDENT},
87 	{"pre",		HTML_TOPHRASE | HTML_NLAROUND | HTML_NOINDENT},
88 	{"a",		HTML_INPHRASE | HTML_TOPHRASE},
89 	{"b",		HTML_INPHRASE | HTML_TOPHRASE},
90 	{"cite",	HTML_INPHRASE | HTML_TOPHRASE},
91 	{"code",	HTML_INPHRASE | HTML_TOPHRASE},
92 	{"i",		HTML_INPHRASE | HTML_TOPHRASE},
93 	{"small",	HTML_INPHRASE | HTML_TOPHRASE},
94 	{"span",	HTML_INPHRASE | HTML_TOPHRASE},
95 	{"var",		HTML_INPHRASE | HTML_TOPHRASE},
96 	{"br",		HTML_INPHRASE | HTML_NOSTACK | HTML_NLALL},
97 	{"hr",		HTML_INPHRASE | HTML_NOSTACK},
98 	{"mark",	HTML_INPHRASE },
99 	{"math",	HTML_INPHRASE | HTML_NLALL | HTML_INDENT},
100 	{"mrow",	0},
101 	{"mi",		0},
102 	{"mn",		0},
103 	{"mo",		0},
104 	{"msup",	0},
105 	{"msub",	0},
106 	{"msubsup",	0},
107 	{"mfrac",	0},
108 	{"msqrt",	0},
109 	{"mfenced",	0},
110 	{"mtable",	0},
111 	{"mtr",		0},
112 	{"mtd",		0},
113 	{"munderover",	0},
114 	{"munder",	0},
115 	{"mover",	0},
116 };
117 
118 /* Avoid duplicate HTML id= attributes. */
119 
120 struct	id_entry {
121 	int	 ord;	/* Ordinal number of the latest occurrence. */
122 	char	 id[];	/* The id= attribute without any ordinal suffix. */
123 };
124 static	struct ohash	 id_unique;
125 
126 static	void	 html_reset_internal(struct html *);
127 static	void	 print_byte(struct html *, char);
128 static	void	 print_endword(struct html *);
129 static	void	 print_indent(struct html *);
130 static	void	 print_word(struct html *, const char *);
131 
132 static	void	 print_ctag(struct html *, struct tag *);
133 static	int	 print_escape(struct html *, char);
134 static	int	 print_encode(struct html *, const char *, const char *, int);
135 static	void	 print_href(struct html *, const char *, const char *, int);
136 static	void	 print_metaf(struct html *);
137 
138 
139 void *
140 html_alloc(const struct manoutput *outopts)
141 {
142 	struct html	*h;
143 
144 	h = mandoc_calloc(1, sizeof(struct html));
145 
146 	h->tag = NULL;
147 	h->metac = h->metal = ESCAPE_FONTROMAN;
148 	h->style = outopts->style;
149 	if ((h->base_man1 = outopts->man) == NULL)
150 		h->base_man2 = NULL;
151 	else if ((h->base_man2 = strchr(h->base_man1, ';')) != NULL)
152 		*h->base_man2++ = '\0';
153 	h->base_includes = outopts->includes;
154 	if (outopts->fragment)
155 		h->oflags |= HTML_FRAGMENT;
156 	if (outopts->toc)
157 		h->oflags |= HTML_TOC;
158 
159 	mandoc_ohash_init(&id_unique, 4, offsetof(struct id_entry, id));
160 
161 	return h;
162 }
163 
164 static void
165 html_reset_internal(struct html *h)
166 {
167 	struct tag	*tag;
168 	struct id_entry	*entry;
169 	unsigned int	 slot;
170 
171 	while ((tag = h->tag) != NULL) {
172 		h->tag = tag->next;
173 		free(tag);
174 	}
175 	entry = ohash_first(&id_unique, &slot);
176 	while (entry != NULL) {
177 		free(entry);
178 		entry = ohash_next(&id_unique, &slot);
179 	}
180 	ohash_delete(&id_unique);
181 }
182 
183 void
184 html_reset(void *p)
185 {
186 	html_reset_internal(p);
187 	mandoc_ohash_init(&id_unique, 4, offsetof(struct id_entry, id));
188 }
189 
190 void
191 html_free(void *p)
192 {
193 	html_reset_internal(p);
194 	free(p);
195 }
196 
197 void
198 print_gen_head(struct html *h)
199 {
200 	struct tag	*t;
201 
202 	print_otag(h, TAG_META, "?", "charset", "utf-8");
203 	print_otag(h, TAG_META, "??", "name", "viewport",
204 	    "content", "width=device-width, initial-scale=1.0");
205 	if (h->style != NULL) {
206 		print_otag(h, TAG_LINK, "?h??", "rel", "stylesheet",
207 		    h->style, "type", "text/css", "media", "all");
208 		return;
209 	}
210 
211 	/*
212 	 * Print a minimal embedded style sheet.
213 	 */
214 
215 	t = print_otag(h, TAG_STYLE, "");
216 	print_text(h, "table.head, table.foot { width: 100%; }");
217 	print_endline(h);
218 	print_text(h, "td.head-rtitle, td.foot-os { text-align: right; }");
219 	print_endline(h);
220 	print_text(h, "td.head-vol { text-align: center; }");
221 	print_endline(h);
222 	print_text(h, ".Nd, .Bf, .Op { display: inline; }");
223 	print_endline(h);
224 	print_text(h, ".Pa, .Ad { font-style: italic; }");
225 	print_endline(h);
226 	print_text(h, ".Ms { font-weight: bold; }");
227 	print_endline(h);
228 	print_text(h, ".Bl-diag ");
229 	print_byte(h, '>');
230 	print_text(h, " dt { font-weight: bold; }");
231 	print_endline(h);
232 	print_text(h, "code.Nm, .Fl, .Cm, .Ic, code.In, .Fd, .Fn, .Cd "
233 	    "{ font-weight: bold; font-family: inherit; }");
234 	print_tagq(h, t);
235 }
236 
237 int
238 html_setfont(struct html *h, enum mandoc_esc font)
239 {
240 	switch (font) {
241 	case ESCAPE_FONTPREV:
242 		font = h->metal;
243 		break;
244 	case ESCAPE_FONTITALIC:
245 	case ESCAPE_FONTBOLD:
246 	case ESCAPE_FONTBI:
247 	case ESCAPE_FONTROMAN:
248 	case ESCAPE_FONTCR:
249 	case ESCAPE_FONTCB:
250 	case ESCAPE_FONTCI:
251 		break;
252 	case ESCAPE_FONT:
253 		font = ESCAPE_FONTROMAN;
254 		break;
255 	default:
256 		return 0;
257 	}
258 	h->metal = h->metac;
259 	h->metac = font;
260 	return 1;
261 }
262 
263 static void
264 print_metaf(struct html *h)
265 {
266 	if (h->metaf) {
267 		print_tagq(h, h->metaf);
268 		h->metaf = NULL;
269 	}
270 	switch (h->metac) {
271 	case ESCAPE_FONTITALIC:
272 		h->metaf = print_otag(h, TAG_I, "");
273 		break;
274 	case ESCAPE_FONTBOLD:
275 		h->metaf = print_otag(h, TAG_B, "");
276 		break;
277 	case ESCAPE_FONTBI:
278 		h->metaf = print_otag(h, TAG_B, "");
279 		print_otag(h, TAG_I, "");
280 		break;
281 	case ESCAPE_FONTCR:
282 		h->metaf = print_otag(h, TAG_SPAN, "c", "Li");
283 		break;
284 	case ESCAPE_FONTCB:
285 		h->metaf = print_otag(h, TAG_SPAN, "c", "Li");
286 		print_otag(h, TAG_B, "");
287 		break;
288 	case ESCAPE_FONTCI:
289 		h->metaf = print_otag(h, TAG_SPAN, "c", "Li");
290 		print_otag(h, TAG_I, "");
291 		break;
292 	default:
293 		break;
294 	}
295 }
296 
297 void
298 html_close_paragraph(struct html *h)
299 {
300 	struct tag	*this, *next;
301 	int		 flags;
302 
303 	this = h->tag;
304 	for (;;) {
305 		next = this->next;
306 		flags = htmltags[this->tag].flags;
307 		if (flags & (HTML_INPHRASE | HTML_TOPHRASE))
308 			print_ctag(h, this);
309 		if ((flags & HTML_INPHRASE) == 0)
310 			break;
311 		this = next;
312 	}
313 }
314 
315 /*
316  * ROFF_nf switches to no-fill mode, ROFF_fi to fill mode.
317  * TOKEN_NONE does not switch.  The old mode is returned.
318  */
319 enum roff_tok
320 html_fillmode(struct html *h, enum roff_tok want)
321 {
322 	struct tag	*t;
323 	enum roff_tok	 had;
324 
325 	for (t = h->tag; t != NULL; t = t->next)
326 		if (t->tag == TAG_PRE)
327 			break;
328 
329 	had = t == NULL ? ROFF_fi : ROFF_nf;
330 
331 	if (want != had) {
332 		switch (want) {
333 		case ROFF_fi:
334 			print_tagq(h, t);
335 			break;
336 		case ROFF_nf:
337 			html_close_paragraph(h);
338 			print_otag(h, TAG_PRE, "");
339 			break;
340 		case TOKEN_NONE:
341 			break;
342 		default:
343 			abort();
344 		}
345 	}
346 	return had;
347 }
348 
349 /*
350  * Allocate a string to be used for the "id=" attribute of an HTML
351  * element and/or as a segment identifier for a URI in an <a> element.
352  * The function may fail and return NULL if the node lacks text data
353  * to create the attribute from.
354  * The caller is responsible for free(3)ing the returned string.
355  *
356  * If the "unique" argument is non-zero, the "id_unique" ohash table
357  * is used for de-duplication.  If the "unique" argument is 1,
358  * it is the first time the function is called for this tag and
359  * location, so if an ordinal suffix is needed, it is incremented.
360  * If the "unique" argument is 2, it is the second time the function
361  * is called for this tag and location, so the ordinal suffix
362  * remains unchanged.
363  */
364 char *
365 html_make_id(const struct roff_node *n, int unique)
366 {
367 	const struct roff_node	*nch;
368 	struct id_entry		*entry;
369 	char			*buf, *cp;
370 	size_t			 len;
371 	unsigned int		 slot;
372 
373 	if (n->tag != NULL)
374 		buf = mandoc_strdup(n->tag);
375 	else {
376 		switch (n->tok) {
377 		case MDOC_Sh:
378 		case MDOC_Ss:
379 		case MDOC_Sx:
380 		case MAN_SH:
381 		case MAN_SS:
382 			for (nch = n->child; nch != NULL; nch = nch->next)
383 				if (nch->type != ROFFT_TEXT)
384 					return NULL;
385 			buf = NULL;
386 			deroff(&buf, n);
387 			if (buf == NULL)
388 				return NULL;
389 			break;
390 		default:
391 			if (n->child == NULL || n->child->type != ROFFT_TEXT)
392 				return NULL;
393 			buf = mandoc_strdup(n->child->string);
394 			break;
395 		}
396 	}
397 
398 	/*
399 	 * In ID attributes, only use ASCII characters that are
400 	 * permitted in URL-fragment strings according to the
401 	 * explicit list at:
402 	 * https://url.spec.whatwg.org/#url-fragment-string
403 	 * In addition, reserve '~' for ordinal suffixes.
404 	 */
405 
406 	for (cp = buf; *cp != '\0'; cp++) {
407 		if (*cp == ASCII_HYPH)
408 			*cp = '-';
409 		else if (isalnum((unsigned char)*cp) == 0 &&
410 		    strchr("!$&'()*+,-./:;=?@_", *cp) == NULL)
411 			*cp = '_';
412 	}
413 
414 	if (unique == 0)
415 		return buf;
416 
417 	/* Avoid duplicate HTML id= attributes. */
418 
419 	slot = ohash_qlookup(&id_unique, buf);
420 	if ((entry = ohash_find(&id_unique, slot)) == NULL) {
421 		len = strlen(buf) + 1;
422 		entry = mandoc_malloc(sizeof(*entry) + len);
423 		entry->ord = 1;
424 		memcpy(entry->id, buf, len);
425 		ohash_insert(&id_unique, slot, entry);
426 	} else if (unique == 1)
427 		entry->ord++;
428 
429 	if (entry->ord > 1) {
430 		cp = buf;
431 		mandoc_asprintf(&buf, "%s~%d", cp, entry->ord);
432 		free(cp);
433 	}
434 	return buf;
435 }
436 
437 static int
438 print_escape(struct html *h, char c)
439 {
440 
441 	switch (c) {
442 	case '<':
443 		print_word(h, "&lt;");
444 		break;
445 	case '>':
446 		print_word(h, "&gt;");
447 		break;
448 	case '&':
449 		print_word(h, "&amp;");
450 		break;
451 	case '"':
452 		print_word(h, "&quot;");
453 		break;
454 	case ASCII_NBRSP:
455 		print_word(h, "&nbsp;");
456 		break;
457 	case ASCII_HYPH:
458 		print_byte(h, '-');
459 		break;
460 	case ASCII_BREAK:
461 		break;
462 	default:
463 		return 0;
464 	}
465 	return 1;
466 }
467 
468 static int
469 print_encode(struct html *h, const char *p, const char *pend, int norecurse)
470 {
471 	char		 numbuf[16];
472 	const char	*seq;
473 	size_t		 sz;
474 	int		 c, len, breakline, nospace;
475 	enum mandoc_esc	 esc;
476 	static const char rejs[10] = { ' ', '\\', '<', '>', '&', '"',
477 		ASCII_NBRSP, ASCII_HYPH, ASCII_BREAK, '\0' };
478 
479 	if (pend == NULL)
480 		pend = strchr(p, '\0');
481 
482 	breakline = 0;
483 	nospace = 0;
484 
485 	while (p < pend) {
486 		if (HTML_SKIPCHAR & h->flags && '\\' != *p) {
487 			h->flags &= ~HTML_SKIPCHAR;
488 			p++;
489 			continue;
490 		}
491 
492 		for (sz = strcspn(p, rejs); sz-- && p < pend; p++)
493 			print_byte(h, *p);
494 
495 		if (breakline &&
496 		    (p >= pend || *p == ' ' || *p == ASCII_NBRSP)) {
497 			print_otag(h, TAG_BR, "");
498 			breakline = 0;
499 			while (p < pend && (*p == ' ' || *p == ASCII_NBRSP))
500 				p++;
501 			continue;
502 		}
503 
504 		if (p >= pend)
505 			break;
506 
507 		if (*p == ' ') {
508 			print_endword(h);
509 			p++;
510 			continue;
511 		}
512 
513 		if (print_escape(h, *p++))
514 			continue;
515 
516 		esc = mandoc_escape(&p, &seq, &len);
517 		switch (esc) {
518 		case ESCAPE_FONT:
519 		case ESCAPE_FONTPREV:
520 		case ESCAPE_FONTBOLD:
521 		case ESCAPE_FONTITALIC:
522 		case ESCAPE_FONTBI:
523 		case ESCAPE_FONTROMAN:
524 		case ESCAPE_FONTCR:
525 		case ESCAPE_FONTCB:
526 		case ESCAPE_FONTCI:
527 			if (0 == norecurse) {
528 				h->flags |= HTML_NOSPACE;
529 				if (html_setfont(h, esc))
530 					print_metaf(h);
531 				h->flags &= ~HTML_NOSPACE;
532 			}
533 			continue;
534 		case ESCAPE_SKIPCHAR:
535 			h->flags |= HTML_SKIPCHAR;
536 			continue;
537 		case ESCAPE_ERROR:
538 			continue;
539 		default:
540 			break;
541 		}
542 
543 		if (h->flags & HTML_SKIPCHAR) {
544 			h->flags &= ~HTML_SKIPCHAR;
545 			continue;
546 		}
547 
548 		switch (esc) {
549 		case ESCAPE_UNICODE:
550 			/* Skip past "u" header. */
551 			c = mchars_num2uc(seq + 1, len - 1);
552 			break;
553 		case ESCAPE_NUMBERED:
554 			c = mchars_num2char(seq, len);
555 			if (c < 0)
556 				continue;
557 			break;
558 		case ESCAPE_SPECIAL:
559 			c = mchars_spec2cp(seq, len);
560 			if (c <= 0)
561 				continue;
562 			break;
563 		case ESCAPE_UNDEF:
564 			c = *seq;
565 			break;
566 		case ESCAPE_DEVICE:
567 			print_word(h, "html");
568 			continue;
569 		case ESCAPE_BREAK:
570 			breakline = 1;
571 			continue;
572 		case ESCAPE_NOSPACE:
573 			if ('\0' == *p)
574 				nospace = 1;
575 			continue;
576 		case ESCAPE_OVERSTRIKE:
577 			if (len == 0)
578 				continue;
579 			c = seq[len - 1];
580 			break;
581 		default:
582 			continue;
583 		}
584 		if ((c < 0x20 && c != 0x09) ||
585 		    (c > 0x7E && c < 0xA0))
586 			c = 0xFFFD;
587 		if (c > 0x7E) {
588 			(void)snprintf(numbuf, sizeof(numbuf), "&#x%.4X;", c);
589 			print_word(h, numbuf);
590 		} else if (print_escape(h, c) == 0)
591 			print_byte(h, c);
592 	}
593 
594 	return nospace;
595 }
596 
597 static void
598 print_href(struct html *h, const char *name, const char *sec, int man)
599 {
600 	struct stat	 sb;
601 	const char	*p, *pp;
602 	char		*filename;
603 
604 	if (man) {
605 		pp = h->base_man1;
606 		if (h->base_man2 != NULL) {
607 			mandoc_asprintf(&filename, "%s.%s", name, sec);
608 			if (stat(filename, &sb) == -1)
609 				pp = h->base_man2;
610 			free(filename);
611 		}
612 	} else
613 		pp = h->base_includes;
614 
615 	while ((p = strchr(pp, '%')) != NULL) {
616 		print_encode(h, pp, p, 1);
617 		if (man && p[1] == 'S') {
618 			if (sec == NULL)
619 				print_byte(h, '1');
620 			else
621 				print_encode(h, sec, NULL, 1);
622 		} else if ((man && p[1] == 'N') ||
623 		    (man == 0 && p[1] == 'I'))
624 			print_encode(h, name, NULL, 1);
625 		else
626 			print_encode(h, p, p + 2, 1);
627 		pp = p + 2;
628 	}
629 	if (*pp != '\0')
630 		print_encode(h, pp, NULL, 1);
631 }
632 
633 struct tag *
634 print_otag(struct html *h, enum htmltag tag, const char *fmt, ...)
635 {
636 	va_list		 ap;
637 	struct tag	*t;
638 	const char	*attr;
639 	char		*arg1, *arg2;
640 	int		 style_written, tflags;
641 
642 	tflags = htmltags[tag].flags;
643 
644 	/* Flow content is not allowed in phrasing context. */
645 
646 	if ((tflags & HTML_INPHRASE) == 0) {
647 		for (t = h->tag; t != NULL; t = t->next) {
648 			if (t->closed)
649 				continue;
650 			assert((htmltags[t->tag].flags & HTML_TOPHRASE) == 0);
651 			break;
652 		}
653 
654 	/*
655 	 * Always wrap phrasing elements in a paragraph
656 	 * unless already contained in some flow container;
657 	 * never put them directly into a section.
658 	 */
659 
660 	} else if (tflags & HTML_TOPHRASE && h->tag->tag == TAG_SECTION)
661 		print_otag(h, TAG_P, "c", "Pp");
662 
663 	/* Push this tag onto the stack of open scopes. */
664 
665 	if ((tflags & HTML_NOSTACK) == 0) {
666 		t = mandoc_malloc(sizeof(struct tag));
667 		t->tag = tag;
668 		t->next = h->tag;
669 		t->refcnt = 0;
670 		t->closed = 0;
671 		h->tag = t;
672 	} else
673 		t = NULL;
674 
675 	if (tflags & HTML_NLBEFORE)
676 		print_endline(h);
677 	if (h->col == 0)
678 		print_indent(h);
679 	else if ((h->flags & HTML_NOSPACE) == 0) {
680 		if (h->flags & HTML_KEEP)
681 			print_word(h, "&#x00A0;");
682 		else {
683 			if (h->flags & HTML_PREKEEP)
684 				h->flags |= HTML_KEEP;
685 			print_endword(h);
686 		}
687 	}
688 
689 	if ( ! (h->flags & HTML_NONOSPACE))
690 		h->flags &= ~HTML_NOSPACE;
691 	else
692 		h->flags |= HTML_NOSPACE;
693 
694 	/* Print out the tag name and attributes. */
695 
696 	print_byte(h, '<');
697 	print_word(h, htmltags[tag].name);
698 
699 	va_start(ap, fmt);
700 
701 	while (*fmt != '\0' && *fmt != 's') {
702 
703 		/* Parse attributes and arguments. */
704 
705 		arg1 = va_arg(ap, char *);
706 		arg2 = NULL;
707 		switch (*fmt++) {
708 		case 'c':
709 			attr = "class";
710 			break;
711 		case 'h':
712 			attr = "href";
713 			break;
714 		case 'i':
715 			attr = "id";
716 			break;
717 		case 'r':
718 			attr = "role";
719 			break;
720 		case '?':
721 			attr = arg1;
722 			arg1 = va_arg(ap, char *);
723 			break;
724 		default:
725 			abort();
726 		}
727 		if (*fmt == 'M')
728 			arg2 = va_arg(ap, char *);
729 		if (arg1 == NULL)
730 			continue;
731 
732 		/* Print the attributes. */
733 
734 		print_byte(h, ' ');
735 		print_word(h, attr);
736 		print_byte(h, '=');
737 		print_byte(h, '"');
738 		switch (*fmt) {
739 		case 'I':
740 			print_href(h, arg1, NULL, 0);
741 			fmt++;
742 			break;
743 		case 'M':
744 			print_href(h, arg1, arg2, 1);
745 			fmt++;
746 			break;
747 		case 'R':
748 			print_byte(h, '#');
749 			print_encode(h, arg1, NULL, 1);
750 			fmt++;
751 			break;
752 		default:
753 			print_encode(h, arg1, NULL, 1);
754 			break;
755 		}
756 		print_byte(h, '"');
757 	}
758 
759 	style_written = 0;
760 	while (*fmt++ == 's') {
761 		arg1 = va_arg(ap, char *);
762 		arg2 = va_arg(ap, char *);
763 		if (arg2 == NULL)
764 			continue;
765 		print_byte(h, ' ');
766 		if (style_written == 0) {
767 			print_word(h, "style=\"");
768 			style_written = 1;
769 		}
770 		print_word(h, arg1);
771 		print_byte(h, ':');
772 		print_byte(h, ' ');
773 		print_word(h, arg2);
774 		print_byte(h, ';');
775 	}
776 	if (style_written)
777 		print_byte(h, '"');
778 
779 	va_end(ap);
780 
781 	/* Accommodate for "well-formed" singleton escaping. */
782 
783 	if (htmltags[tag].flags & HTML_NOSTACK)
784 		print_byte(h, '/');
785 
786 	print_byte(h, '>');
787 
788 	if (tflags & HTML_NLBEGIN)
789 		print_endline(h);
790 	else
791 		h->flags |= HTML_NOSPACE;
792 
793 	if (tflags & HTML_INDENT)
794 		h->indent++;
795 	if (tflags & HTML_NOINDENT)
796 		h->noindent++;
797 
798 	return t;
799 }
800 
801 /*
802  * Print an element with an optional "id=" attribute.
803  * If the element has phrasing content and an "id=" attribute,
804  * also add a permalink: outside if it can be in phrasing context,
805  * inside otherwise.
806  */
807 struct tag *
808 print_otag_id(struct html *h, enum htmltag elemtype, const char *cattr,
809     struct roff_node *n)
810 {
811 	struct roff_node *nch;
812 	struct tag	*ret, *t;
813 	char		*id, *href;
814 
815 	ret = NULL;
816 	id = href = NULL;
817 	if (n->flags & NODE_ID)
818 		id = html_make_id(n, 1);
819 	if (n->flags & NODE_HREF)
820 		href = id == NULL ? html_make_id(n, 2) : id;
821 	if (href != NULL && htmltags[elemtype].flags & HTML_INPHRASE)
822 		ret = print_otag(h, TAG_A, "chR", "permalink", href);
823 	t = print_otag(h, elemtype, "ci", cattr, id);
824 	if (ret == NULL) {
825 		ret = t;
826 		if (href != NULL && (nch = n->child) != NULL) {
827 			/* man(7) is safe, it tags phrasing content only. */
828 			if (n->tok > MDOC_MAX ||
829 			    htmltags[elemtype].flags & HTML_TOPHRASE)
830 				nch = NULL;
831 			else  /* For mdoc(7), beware of nested blocks. */
832 				while (nch != NULL && nch->type == ROFFT_TEXT)
833 					nch = nch->next;
834 			if (nch == NULL)
835 				print_otag(h, TAG_A, "chR", "permalink", href);
836 		}
837 	}
838 	free(id);
839 	if (id == NULL)
840 		free(href);
841 	return ret;
842 }
843 
844 static void
845 print_ctag(struct html *h, struct tag *tag)
846 {
847 	int	 tflags;
848 
849 	if (tag->closed == 0) {
850 		tag->closed = 1;
851 		if (tag == h->metaf)
852 			h->metaf = NULL;
853 		if (tag == h->tblt)
854 			h->tblt = NULL;
855 
856 		tflags = htmltags[tag->tag].flags;
857 		if (tflags & HTML_INDENT)
858 			h->indent--;
859 		if (tflags & HTML_NOINDENT)
860 			h->noindent--;
861 		if (tflags & HTML_NLEND)
862 			print_endline(h);
863 		print_indent(h);
864 		print_byte(h, '<');
865 		print_byte(h, '/');
866 		print_word(h, htmltags[tag->tag].name);
867 		print_byte(h, '>');
868 		if (tflags & HTML_NLAFTER)
869 			print_endline(h);
870 	}
871 	if (tag->refcnt == 0) {
872 		h->tag = tag->next;
873 		free(tag);
874 	}
875 }
876 
877 void
878 print_gen_decls(struct html *h)
879 {
880 	print_word(h, "<!DOCTYPE html>");
881 	print_endline(h);
882 }
883 
884 void
885 print_gen_comment(struct html *h, struct roff_node *n)
886 {
887 	int	 wantblank;
888 
889 	print_word(h, "<!-- This is an automatically generated file."
890 	    "  Do not edit.");
891 	h->indent = 1;
892 	wantblank = 0;
893 	while (n != NULL && n->type == ROFFT_COMMENT) {
894 		if (strstr(n->string, "-->") == NULL &&
895 		    (wantblank || *n->string != '\0')) {
896 			print_endline(h);
897 			print_indent(h);
898 			print_word(h, n->string);
899 			wantblank = *n->string != '\0';
900 		}
901 		n = n->next;
902 	}
903 	if (wantblank)
904 		print_endline(h);
905 	print_word(h, " -->");
906 	print_endline(h);
907 	h->indent = 0;
908 }
909 
910 void
911 print_text(struct html *h, const char *word)
912 {
913 	print_tagged_text(h, word, NULL);
914 }
915 
916 void
917 print_tagged_text(struct html *h, const char *word, struct roff_node *n)
918 {
919 	struct tag	*t;
920 	char		*href;
921 
922 	/*
923 	 * Always wrap text in a paragraph unless already contained in
924 	 * some flow container; never put it directly into a section.
925 	 */
926 
927 	if (h->tag->tag == TAG_SECTION)
928 		print_otag(h, TAG_P, "c", "Pp");
929 
930 	/* Output whitespace before this text? */
931 
932 	if (h->col && (h->flags & HTML_NOSPACE) == 0) {
933 		if ( ! (HTML_KEEP & h->flags)) {
934 			if (HTML_PREKEEP & h->flags)
935 				h->flags |= HTML_KEEP;
936 			print_endword(h);
937 		} else
938 			print_word(h, "&#x00A0;");
939 	}
940 
941 	/*
942 	 * Optionally switch fonts, optionally write a permalink, then
943 	 * print the text, optionally surrounded by HTML whitespace.
944 	 */
945 
946 	assert(h->metaf == NULL);
947 	print_metaf(h);
948 	print_indent(h);
949 
950 	if (n != NULL && (href = html_make_id(n, 2)) != NULL) {
951 		t = print_otag(h, TAG_A, "chR", "permalink", href);
952 		free(href);
953 	} else
954 		t = NULL;
955 
956 	if ( ! print_encode(h, word, NULL, 0)) {
957 		if ( ! (h->flags & HTML_NONOSPACE))
958 			h->flags &= ~HTML_NOSPACE;
959 		h->flags &= ~HTML_NONEWLINE;
960 	} else
961 		h->flags |= HTML_NOSPACE | HTML_NONEWLINE;
962 
963 	if (h->metaf != NULL) {
964 		print_tagq(h, h->metaf);
965 		h->metaf = NULL;
966 	} else if (t != NULL)
967 		print_tagq(h, t);
968 
969 	h->flags &= ~HTML_IGNDELIM;
970 }
971 
972 void
973 print_tagq(struct html *h, const struct tag *until)
974 {
975 	struct tag	*this, *next;
976 
977 	for (this = h->tag; this != NULL; this = next) {
978 		next = this == until ? NULL : this->next;
979 		print_ctag(h, this);
980 	}
981 }
982 
983 /*
984  * Close out all open elements up to but excluding suntil.
985  * Note that a paragraph just inside stays open together with it
986  * because paragraphs include subsequent phrasing content.
987  */
988 void
989 print_stagq(struct html *h, const struct tag *suntil)
990 {
991 	struct tag	*this, *next;
992 
993 	for (this = h->tag; this != NULL; this = next) {
994 		next = this->next;
995 		if (this == suntil || (next == suntil &&
996 		    (this->tag == TAG_P || this->tag == TAG_PRE)))
997 			break;
998 		print_ctag(h, this);
999 	}
1000 }
1001 
1002 
1003 /***********************************************************************
1004  * Low level output functions.
1005  * They implement line breaking using a short static buffer.
1006  ***********************************************************************/
1007 
1008 /*
1009  * Buffer one HTML output byte.
1010  * If the buffer is full, flush and deactivate it and start a new line.
1011  * If the buffer is inactive, print directly.
1012  */
1013 static void
1014 print_byte(struct html *h, char c)
1015 {
1016 	if ((h->flags & HTML_BUFFER) == 0) {
1017 		putchar(c);
1018 		h->col++;
1019 		return;
1020 	}
1021 
1022 	if (h->col + h->bufcol < sizeof(h->buf)) {
1023 		h->buf[h->bufcol++] = c;
1024 		return;
1025 	}
1026 
1027 	putchar('\n');
1028 	h->col = 0;
1029 	print_indent(h);
1030 	putchar(' ');
1031 	putchar(' ');
1032 	fwrite(h->buf, h->bufcol, 1, stdout);
1033 	putchar(c);
1034 	h->col = (h->indent + 1) * 2 + h->bufcol + 1;
1035 	h->bufcol = 0;
1036 	h->flags &= ~HTML_BUFFER;
1037 }
1038 
1039 /*
1040  * If something was printed on the current output line, end it.
1041  * Not to be called right after print_indent().
1042  */
1043 void
1044 print_endline(struct html *h)
1045 {
1046 	if (h->col == 0)
1047 		return;
1048 
1049 	if (h->bufcol) {
1050 		putchar(' ');
1051 		fwrite(h->buf, h->bufcol, 1, stdout);
1052 		h->bufcol = 0;
1053 	}
1054 	putchar('\n');
1055 	h->col = 0;
1056 	h->flags |= HTML_NOSPACE;
1057 	h->flags &= ~HTML_BUFFER;
1058 }
1059 
1060 /*
1061  * Flush the HTML output buffer.
1062  * If it is inactive, activate it.
1063  */
1064 static void
1065 print_endword(struct html *h)
1066 {
1067 	if (h->noindent) {
1068 		print_byte(h, ' ');
1069 		return;
1070 	}
1071 
1072 	if ((h->flags & HTML_BUFFER) == 0) {
1073 		h->col++;
1074 		h->flags |= HTML_BUFFER;
1075 	} else if (h->bufcol) {
1076 		putchar(' ');
1077 		fwrite(h->buf, h->bufcol, 1, stdout);
1078 		h->col += h->bufcol + 1;
1079 	}
1080 	h->bufcol = 0;
1081 }
1082 
1083 /*
1084  * If at the beginning of a new output line,
1085  * perform indentation and mark the line as containing output.
1086  * Make sure to really produce some output right afterwards,
1087  * but do not use print_otag() for producing it.
1088  */
1089 static void
1090 print_indent(struct html *h)
1091 {
1092 	size_t	 i;
1093 
1094 	if (h->col || h->noindent)
1095 		return;
1096 
1097 	h->col = h->indent * 2;
1098 	for (i = 0; i < h->col; i++)
1099 		putchar(' ');
1100 }
1101 
1102 /*
1103  * Print or buffer some characters
1104  * depending on the current HTML output buffer state.
1105  */
1106 static void
1107 print_word(struct html *h, const char *cp)
1108 {
1109 	while (*cp != '\0')
1110 		print_byte(h, *cp++);
1111 }
1112