Back (Current repo: lowdown)

fork of lowdown from https://kristaps.bsd.lv
To clone this repository:
git clone https://git.viktor1993.net/lowdown.git
Log | Download | Files | Refs | LICENSE

nroff.c (44252B)


/*	$Id$ */
/*
 * Copyright (c) 2008, Natacha Porté
 * Copyright (c) 2011, Vicent Martí
 * Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors
 * Copyright (c) 2016--2021 Kristaps Dzonsons <kristaps@bsd.lv>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include "config.h"

#if HAVE_SYS_QUEUE
# include <sys/queue.h>
#endif

#include <assert.h>
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "lowdown.h"
#include "extern.h"

enum	nfont {
	NFONT_ITALIC = 0, /* italic */
	NFONT_BOLD, /* bold */
	NFONT_FIXED, /* fixed-width */
	NFONT__MAX
};

struct 	nroff {
	struct hentryq	 	  headers_used; /* headers we've seen */
	int			  man; /* whether man(7) */
	int			  post_para; /* for choosing PP/LP */
	unsigned int		  flags; /* output flags */
	ssize_t			  headers_offs; /* header offset */
	enum nfont		  fonts[NFONT__MAX]; /* see bqueue_font() */
	struct bnodeq		**foots; /* footnotes */
	size_t			  footsz; /* footnote size */
};

enum	bscope {
	BSCOPE_BLOCK = 0,
	BSCOPE_SPAN,
	BSCOPE_PDFHREF,
	BSCOPE_LITERAL,
	BSCOPE_FONT,
	BSCOPE_COLOUR
};

/*
 * Instead of writing directly into the output buffer, we write
 * temporarily into bnodes, which are converted into output.  These
 * nodes are aware of whether they need surrounding newlines.
 */
struct	bnode {
	char				*nbuf; /* (safe) 1st data */
	char				*buf; /* (unsafe) 2nd data */
	char				*nargs; /* (safe) 1st args */
	char				*args; /* (unsafe) 2nd args */
	int				 close; /* BNODE_COLOUR/FONT */
	int				 tblhack; /* BSCOPE_SPAN */
	enum bscope			 scope; /* scope */
	unsigned int			 font; /* if BNODE_FONT */
#define	BFONT_ITALIC			 0x01
#define	BFONT_BOLD			 0x02
#define	BFONT_FIXED			 0x04
	unsigned int			 colour; /* if BNODE_COLOUR */
#define	BFONT_BLUE			 0x01
#define	BFONT_RED			 0x02
	TAILQ_ENTRY(bnode)		 entries;
};

TAILQ_HEAD(bnodeq, bnode);

/*
 * Escape unsafe text into roff output such that no roff fetaures are
 * invoked by the text (macros, escapes, etc.).
 * If "oneline" is non-zero, newlines are replaced with spaces.
 * If "literal", doesn't strip leading space.
 * Return zero on failure, non-zero on success.
 */
static int
hesc_nroff(struct lowdown_buf *ob, const char *data, 
	size_t size, int oneline, int literal, int esc)
{
	size_t	 	i = 0;
	unsigned char	ch;

	if (size == 0)
		return 1;

	if (!esc && oneline) {
		assert(!literal);
		for (i = 0; i < size; i++) {
			ch = data[i] == '\n' ? ' ' : data[i];
			if (!hbuf_putc(ob, ch))
				return 0;
			if (ch != ' ')
				continue;
			while (i < size && isspace((unsigned char)data[i]))
				i++;
			i--;
		}
		return 1;
	} else if (!esc)
		return hbuf_put(ob, data, size);

	/* Strip leading whitespace. */

	if (!literal && ob->size > 0 && ob->data[ob->size - 1] == '\n')
		while (i < size && (data[i] == ' ' || data[i] == '\n'))
			i++;

	/*
	 * According to mandoc_char(7), we need to escape the backtick,
	 * single apostrophe, and tilde or else they'll be considered as
	 * special Unicode output.
	 * Slashes need to be escaped too.
	 * We also escape double-quotes because this text might be used
	 * within quoted macro arguments.
	 */

	for ( ; i < size; i++)
		switch (data[i]) {
		case '^':
			if (!HBUF_PUTSL(ob, "\\(ha"))
				return 0;
			break;
		case '~':
			if (!HBUF_PUTSL(ob, "\\(ti"))
				return 0;
			break;
		case '`':
			if (!HBUF_PUTSL(ob, "\\(ga"))
				return 0;
			break;
		case '"':
			if (!HBUF_PUTSL(ob, "\\(dq"))
				return 0;
			break;
		case '\n':
			if (!hbuf_putc(ob, oneline ? ' ' : '\n'))
				return 0;
			if (literal)
				break;

			/* Prevent leading spaces on the line. */

			while (i + 1 < size && 
			       (data[i + 1] == ' ' || 
				data[i + 1] == '\n'))
				i++;
			break;
		case '\\':
			if (!HBUF_PUTSL(ob, "\\e"))
				return 0;
			break;
		case '\'':
		case '.':
			if (!oneline &&
			    ob->size > 0 && 
			    ob->data[ob->size - 1] == '\n' &&
			    !HBUF_PUTSL(ob, "\\&"))
				return 0;
			/* FALLTHROUGH */
		default:
			if (!hbuf_putc(ob, data[i]))
				return 0;
			break;
		}

	return 1;
}

static const char *
nstate_colour_buf(unsigned int ft)
{
	static char 	 fonts[10];

	fonts[0] = '\0';
	if (ft == BFONT_BLUE)
		strlcat(fonts, "blue", sizeof(fonts));
	else if (ft == BFONT_RED)
		strlcat(fonts, "red", sizeof(fonts));
	else
		strlcat(fonts, "black", sizeof(fonts));
	return fonts;
}

/*
 * For compatibility with traditional troff, return non-block font code
 * using the correct sequence of \fX, \f(xx, and \f[xxx].
 */
static const char *
nstate_font_buf(unsigned int ft, int blk)
{
	static char 	 fonts[10];
	char		*cp = fonts;
	size_t		 len = 0;

	if (ft & BFONT_FIXED)
		len++;
	if (ft & BFONT_BOLD)
		len++;
	if (ft & BFONT_ITALIC)
		len++;
	if (ft == 0)
		len++;

	if (!blk && len == 3)
		(*cp++) = '[';
	else if (!blk && len == 2)
		(*cp++) = '(';

	if (ft & BFONT_FIXED)
		(*cp++) = 'C';
	if (ft & BFONT_BOLD)
		(*cp++) = 'B';
	if (ft & BFONT_ITALIC)
		(*cp++) = 'I';
	if (ft == 0)
		(*cp++) = 'R';

	if (!blk && len == 3)
		(*cp++) = ']';

	(*cp++) = '\0';
	return fonts;
}

static int
bqueue_colour(struct bnodeq *bq, enum lowdown_chng chng, int close)
{
	struct bnode	*bn;

	if ((bn = calloc(1, sizeof(struct bnode))) == NULL)
		return 0;
	TAILQ_INSERT_TAIL(bq, bn, entries);
	bn->scope = BSCOPE_COLOUR;
	bn->close = close;
	bn->colour = close ? 0 :
		chng == LOWDOWN_CHNG_INSERT ? 
		BFONT_BLUE : BFONT_RED;
	return 1;
}

static int
bqueue_font(const struct nroff *st, struct bnodeq *bq, int close)
{
	struct bnode	*bn;

	if ((bn = calloc(1, sizeof(struct bnode))) == NULL)
		return 0;
	TAILQ_INSERT_TAIL(bq, bn, entries);
	bn->scope = BSCOPE_FONT;
	bn->close = close;
	if (st->fonts[NFONT_FIXED])
		bn->font |= BFONT_FIXED;
	if (st->fonts[NFONT_BOLD])
		bn->font |= BFONT_BOLD;
	if (st->fonts[NFONT_ITALIC])
		bn->font |= BFONT_ITALIC;
	return 1;
}

static struct bnode *
bqueue_node(struct bnodeq *bq, enum bscope scope, const char *text)
{
	struct bnode	*bn;

	if ((bn = calloc(1, sizeof(struct bnode))) == NULL)
		return NULL;
	bn->scope = scope;
	if (text != NULL && (bn->nbuf = strdup(text)) == NULL) {
		free(bn);
		return NULL;
	}
	TAILQ_INSERT_TAIL(bq, bn, entries);
	return bn;
}

static struct bnode *
bqueue_span(struct bnodeq *bq, const char *text)
{

	return bqueue_node(bq, BSCOPE_SPAN, text);
}

static struct bnode *
bqueue_block(struct bnodeq *bq, const char *text)
{

	return bqueue_node(bq, BSCOPE_BLOCK, text);
}

static struct bnode *
bqueue_semiblock(struct bnodeq *bq, const char *text)
{

	return bqueue_node(bq, BSCOPE_PDFHREF, text);
}

static void
bnode_free(struct bnode *bn)
{

	free(bn->args);
	free(bn->nargs);
	free(bn->nbuf);
	free(bn->buf);
	free(bn);
}

static void
bqueue_free(struct bnodeq *bq)
{
	struct bnode	*bn;

	while ((bn = TAILQ_FIRST(bq)) != NULL) {
		TAILQ_REMOVE(bq, bn, entries);
		bnode_free(bn);
	}
}

static void
bqueue_strip_paras(struct bnodeq *bq)
{
	struct bnode	*bn;

	while ((bn = TAILQ_FIRST(bq)) != NULL) {
		if (bn->scope != BSCOPE_BLOCK || bn->nbuf == NULL)
			break;
		if (strcmp(bn->nbuf, ".PP") &&
		    strcmp(bn->nbuf, ".IP") &&
		    strcmp(bn->nbuf, ".LP"))
			break;
		TAILQ_REMOVE(bq, bn, entries);
		bnode_free(bn);
	}
}

static int
bqueue_flush(struct lowdown_buf *ob, const struct bnodeq *bq, int esc)
{
	const struct bnode	*bn, *chk, *next;
	const char		*cp;
	int		 	 nextblk;

	TAILQ_FOREACH(bn, bq, entries) {
		nextblk = 0;

		if (bn->scope == BSCOPE_PDFHREF &&
		    ob->size > 0 && 
		    ob->data[ob->size - 1] != '\n' &&
		    !hbuf_puts(ob, "\\c"))
			return 0;

		/* 
		 * Block scopes start with a newline.
		 * Also have colours use their own block, as otherwise
		 * (bugs in groff?) inline colour selection after a
		 * hyperlink macro causes line breaks.
		 * Besides, having spaces around changing colour, which
		 * indicates differences, improves readability.
		 */

		if (bn->scope == BSCOPE_BLOCK ||
		    bn->scope == BSCOPE_PDFHREF ||
		    bn->scope == BSCOPE_COLOUR) {
			if (ob->size > 0 && 
			    ob->data[ob->size - 1] != '\n' &&
			    !hbuf_putc(ob, '\n'))
				return 0;
			nextblk = 1;
		}

		/* 
		 * Fonts can either be macros or inline depending upon
		 * where they set relative to a macro block.
		 */

		if (bn->scope == BSCOPE_FONT) {
			chk = bn->close ?
				TAILQ_PREV(bn, bnodeq, entries) :
				TAILQ_NEXT(bn, entries);
			if (chk != NULL && 
			    (chk->scope == BSCOPE_PDFHREF ||
			     chk->scope == BSCOPE_BLOCK)) {
				if (ob->size > 0 && 
				    ob->data[ob->size - 1] != '\n' &&
				    !hbuf_putc(ob, '\n'))
					return 0;
				nextblk = 1;
			}
		}

		/* Print font and colour escapes. */

		if (bn->scope == BSCOPE_FONT && nextblk) {
			if (!hbuf_printf(ob, ".ft %s",
			    nstate_font_buf(bn->font, nextblk)))
				return 0;
		} else if (bn->scope == BSCOPE_FONT) {
			if (!hbuf_printf(ob, "\\f%s", 
			    nstate_font_buf(bn->font, nextblk)))
				return 0;
		} else if (bn->scope == BSCOPE_COLOUR) {
			assert(nextblk);
			if (!hbuf_printf(ob, ".gcolor %s", 
			    nstate_colour_buf(bn->colour)))
				return 0;
		}

		/* 
		 * A "tblhack" is used by a span macro to indicate
		 * that it should start its own line, but that data
		 * continues to flow after it.  This is only used in
		 * tables with T}, at this point.
		 */

		if (bn->scope == BSCOPE_SPAN && bn->tblhack &&
		    ob->size > 0 && ob->data[ob->size - 1] != '\n')
			if (!hbuf_putc(ob, '\n'))
				return 0;

		/*
		 * If we're a span, double-check to see whether we
		 * should introduce a line with an escape.
		 */

		if (bn->scope == BSCOPE_SPAN &&
		    bn->nbuf != NULL && ob->size > 0 &&
		    ob->data[ob->size - 1] == '\n' &&
		    (bn->nbuf[0] == '.' || bn->nbuf[0] == '\'') &&
		    !HBUF_PUTSL(ob, "\\&"))
			return 0;

		/* Safe data need not be escaped. */

		if (bn->nbuf != NULL && !hbuf_puts(ob, bn->nbuf))
			return 0;

		/* Unsafe data must be escaped. */

		if (bn->scope == BSCOPE_LITERAL) {
			assert(bn->buf != NULL);
			if (!hesc_nroff(ob, bn->buf,
			    strlen(bn->buf), 0, 1, esc))
				return 0;
		} else if (bn->buf != NULL)
			if (!hesc_nroff(ob, bn->buf,
			    strlen(bn->buf), 0, 0, esc))
				return 0;

		if (bn->scope == BSCOPE_PDFHREF &&
		    (next = TAILQ_NEXT(bn, entries)) != NULL &&
		    next->scope == BSCOPE_SPAN &&
		    next->buf != NULL &&
		    next->buf[0] != ' ' &&
		    next->buf[0] != '\n' &&
		    !HBUF_PUTSL(ob, " -A \"\\c\""))
			return 0;

		/* 
		 * Macro arguments follow after space.  For links, these
		 * must all be printed on the same line.
		 */

		if (bn->nargs != NULL &&
		    (bn->scope == BSCOPE_BLOCK ||
		     bn->scope == BSCOPE_PDFHREF)) {
			assert(nextblk);
			if (!hbuf_putc(ob, ' '))
				return 0;
			for (cp = bn->nargs; *cp != '\0'; cp++)
				if (!hbuf_putc(ob, 
				    *cp == '\n' ? ' ' : *cp))
					return 0;
		}

		if (bn->args != NULL) {
			assert(nextblk);
			assert(bn->scope == BSCOPE_BLOCK ||
				bn->scope == BSCOPE_PDFHREF);
			if (!hbuf_putc(ob, ' '))
				return 0;
			if (!hesc_nroff(ob, bn->args,
			    strlen(bn->args), 1, 0, esc))
				return 0;
		}

		/* Finally, trailing newline. */

		if (nextblk && ob->size > 0 && 
		    ob->data[ob->size - 1] != '\n' &&
		    !hbuf_putc(ob, '\n'))
			return 0;
	}

	return 1;
}

/*
 * Convert a link into a short-link and place the escaped output into a
 * returned string.
 * Returns NULL on memory allocation failure.
 */
static char *
hbuf2shortlink(const struct lowdown_buf *link)
{
	struct lowdown_buf	*tmp = NULL, *slink = NULL;
	char			*ret = NULL;

	if ((tmp = hbuf_new(32)) == NULL)
		goto out;
	if ((slink = hbuf_new(32)) == NULL)
		goto out;
	if (!hbuf_shortlink(tmp, link))
		goto out;
	if (!hesc_nroff(slink, tmp->data, tmp->size, 1, 0, 1))
		goto out;
	ret = strndup(slink->data, slink->size);
out:
	hbuf_free(tmp);
	hbuf_free(slink);
	return ret;
}

/*
 * Manage hypertext linking with the groff "pdfhref" macro or simply
 * using italics.  XXX: use italics because the UR/UE macro doesn't
 * support leading un-spaced content, so "x[foo](https://foo.com)y"
 * wouldn't work.  Until a solution is found, let's just italicise the
 * link text (or link, if no text is found).  Return FALSE on error
 * (memory), TRUE on success.
 */
static int
putlink(struct bnodeq *obq, struct nroff *st, 
	const struct lowdown_buf *link, 
	const struct lowdown_buf *id, 
	struct bnodeq *bq, enum halink_type type)
{
	struct lowdown_buf	*ob = NULL;
	struct bnode		*bn;
	size_t			 i;
	int			 rc = 0, local = 0;

	/*
	 * For -Tman or without .pdfhref, format the link as-is, with
	 * text then link, or use the various shorteners.
	 */

	if (st->man || !(st->flags & LOWDOWN_NROFF_GROFF)) {
		if (bq == NULL) {
			st->fonts[NFONT_ITALIC]++;
			if (!bqueue_font(st, obq, 0))
				goto out;
			if ((bn = bqueue_span(obq, NULL)) == NULL)
				goto out;
			if (st->flags & LOWDOWN_NROFF_SHORTLINK) {
				bn->nbuf = hbuf2shortlink(link);
				if (bn->nbuf == NULL)
					goto out;
			} else {
				bn->buf = strndup(link->data, link->size);
				if (bn->buf == NULL)
					goto out;
			}
			st->fonts[NFONT_ITALIC]--;
			if (!bqueue_font(st, obq, 1))
				goto out;
			rc = 1;
			goto out;
		}
		st->fonts[NFONT_BOLD]++;
		if (!bqueue_font(st, obq, 0))
			goto out;
		TAILQ_CONCAT(obq, bq, entries);
		st->fonts[NFONT_BOLD]--;
		if (!bqueue_font(st, obq, 1))
			goto out;
		if (st->flags & LOWDOWN_NROFF_NOLINK) {
			rc = 1;
			goto out;
		}
		if (bqueue_span(obq, " <") == NULL)
			goto out;
		st->fonts[NFONT_ITALIC]++;
		if (!bqueue_font(st, obq, 0))
			goto out;
		if ((bn = bqueue_span(obq, NULL)) == NULL)
			goto out;
		if (st->flags & LOWDOWN_NROFF_SHORTLINK) {
			bn->nbuf = hbuf2shortlink(link);
			if (bn->nbuf == NULL)
				goto out;
		} else {
 			bn->buf = strndup(link->data, link->size);
			if (bn->buf == NULL)
				goto out;
		}
		st->fonts[NFONT_ITALIC]--;
		if (!bqueue_font(st, obq, 1))
			goto out;
		if (bqueue_span(obq, ">") == NULL)
			goto out;
		rc = 1;
		goto out;
	}

	/* Otherwise, use .pdfhref. */

	if ((ob = hbuf_new(32)) == NULL)
		goto out;

	/* Encode the URL. */

	local = type != HALINK_EMAIL &&
		link->size && link->data[0] == '#';

	if (!HBUF_PUTSL(ob, "-D "))
		goto out;
	if (type == HALINK_EMAIL && !HBUF_PUTSL(ob, "mailto:"))
		goto out;
	for (i = local ? 1 : 0; i < link->size; i++) {
		if (!isprint((unsigned char)link->data[i]) ||
		    strchr("<>\\^`{|}\"", link->data[i]) != NULL) {
			if (!hbuf_printf(ob, "%%%.2X", link->data[i]))
				goto out;
		} else if (!hbuf_putc(ob, link->data[i]))
			goto out;
	}

	if (!HBUF_PUTSL(ob, " -- "))
		goto out;
	if (bq == NULL && !hbuf_putb(ob, link))
		goto out;
	else if (bq != NULL && !bqueue_flush(ob, bq, 1))
		goto out;

	/*
	 * If we have an ID, emit it before the link part.  This is
	 * important because this isn't printed, so using "-A \c" will
	 * have no effect, so that's used on the subsequent link.
	 */

	if (id != NULL && id->size > 0) {
		bn = bqueue_semiblock(obq, ".pdfhref M");
		if (bn == NULL)
			goto out;
		bn->args = strndup(id->data, id->size);
		if (bn->args == NULL)
			goto out;
	}

	/* Finally, emit the link contents. */

	bn = local ? 
		bqueue_semiblock(obq, ".pdfhref L") :
		bqueue_semiblock(obq, ".pdfhref W");
	if (bn == NULL)
		goto out;
	if ((bn->nargs = strndup(ob->data, ob->size)) == NULL)
		goto out;

	rc = 1;
out:
	hbuf_free(ob);
	return rc;
}

/*
 * Return FALSE on failure, TRUE on success.
 */
static int
rndr_autolink(struct nroff *st, struct bnodeq *obq,
	const struct rndr_autolink *param)
{

	return putlink(obq, st, &param->link, NULL, NULL, param->type);
}

static int
rndr_blockcode(const struct nroff *st, struct bnodeq *obq,
	const struct rndr_blockcode *param)
{
	struct bnode	*bn;

	/*
	 * XXX: intentionally don't use LD/DE because it introduces
	 * vertical space.  This means that subsequent blocks
	 * (paragraphs, etc.) will have a double-newline.
	 */

	if (bqueue_block(obq, ".LP") == NULL)
		return 0;

	if (st->man && (st->flags & LOWDOWN_NROFF_GROFF)) {
		if (bqueue_block(obq, ".EX") == NULL)
			return 0;
	} else {
		if (bqueue_block(obq, ".nf") == NULL)
			return 0;
		if (bqueue_block(obq, ".ft CR") == NULL)
			return 0;
	}

	if ((bn = calloc(1, sizeof(struct bnode))) == NULL)
		return 0;
	TAILQ_INSERT_TAIL(obq, bn, entries);
	bn->scope = BSCOPE_LITERAL;
	bn->buf = strndup(param->text.data, param->text.size);
	if (bn->buf == NULL)
		return 0;

	if (st->man && (st->flags & LOWDOWN_NROFF_GROFF))
		return bqueue_block(obq, ".EE") != NULL;

	if (bqueue_block(obq, ".ft") == NULL)
		return 0;
	return bqueue_block(obq, ".fi") != NULL;
}

static int
rndr_definition_title(struct bnodeq *obq, struct bnodeq *bq)
{

	if (bqueue_block(obq, ".LP") == NULL)
		return 0;
	TAILQ_CONCAT(obq, bq, entries);
	return 1;
}

static int
rndr_definition_data(struct bnodeq *obq, struct bnodeq *bq)
{
	/*
	 * The IP creates an empty vertical space til I figure out a
	 * better way to do hanging lists, so account for it by backing
	 * up first.
	 *
	 * XXX: this produces different results on mandoc and groff as
	 * of 2022-02-19: mandoc backs up one space, while groff backs
	 * up two.  The groff behaviour is what we want, so that the
	 * text is flush on the next line, but this is good enough.
	 */

	if (bqueue_block(obq, ".if n \\\n.sp -1v") == NULL)
		return 0;
	if (bqueue_block(obq, ".if t \\\n.sp -0.25v\n") == NULL)
		return 0;
	if (bqueue_block(obq, ".IP \"\" \\*(PI") == NULL)
		return 0;

	/* Strip out leading paragraphs. */

	bqueue_strip_paras(bq);
	TAILQ_CONCAT(obq, bq, entries);
	return 1;
}

static int
rndr_list(struct nroff *st, struct bnodeq *obq, 
	const struct lowdown_node *n, struct bnodeq *bq)
{
	/* 
	 * If we have a nested list, we need to use RS/RE to indent the
	 * nested component.  Otherwise the "IP" used for the titles and
	 * contained paragraphs won't indent properly.
	 */

	for (n = n->parent; n != NULL; n = n->parent)
		if (n->type == LOWDOWN_LISTITEM)
			break;

	if (n != NULL && bqueue_block(obq, ".RS") == NULL)
		return 0;
	TAILQ_CONCAT(obq, bq, entries);
	if (n != NULL && bqueue_block(obq, ".RE") == NULL)
		return 0;

	st->post_para = 1;
	return 1;
}

static int
rndr_blockquote(struct nroff *st, 
	struct bnodeq *obq, struct bnodeq *bq)
{

	if (bqueue_block(obq, ".RS") == NULL)
		return 0;
	TAILQ_CONCAT(obq, bq, entries);
	st->post_para = 1;
	return bqueue_block(obq, ".RE") != NULL;
}

static int
rndr_codespan(struct bnodeq *obq, const struct rndr_codespan *param)
{
	struct bnode	*bn;

	if ((bn = bqueue_span(obq, NULL)) == NULL)
		return 0;
	bn->buf = strndup(param->text.data, param->text.size);
	return bn->buf != NULL;
}

static int
rndr_linebreak(struct bnodeq *obq)
{

	return bqueue_block(obq, ".br") != NULL;
}

static int
rndr_header(struct nroff *st, struct bnodeq *obq,
	struct bnodeq *bq, const struct lowdown_node *n)
{
	ssize_t				 level;
	struct bnode			*bn;
	struct lowdown_buf		*buf = NULL;
	const struct lowdown_buf	*nbuf;
	int			 	 rc = 0;

	level = (ssize_t)n->rndr_header.level + st->headers_offs;
	if (level < 1)
		level = 1;

	/*
	 * For man(7), we use SH for the first-level section, SS for
	 * other sections.  TODO: use PP then italics or something for
	 * third-level etc.
	 */

	if (st->man) {
		bn = level == 1 ?
			bqueue_block(obq, ".SH") :
			bqueue_block(obq, ".SS");
		if (bn == NULL)
			return 0;
		TAILQ_CONCAT(obq, bq, entries);
		st->post_para = 1;
		return 1;
	} 

	/*
	 * If we're using ms(7) w/groff extensions and w/o numbering,
	 * used the numbered version of the SH macro.
	 * If we're numbered ms(7), use NH.
	 */

	bn = (st->flags & LOWDOWN_NROFF_NUMBERED) ?
		bqueue_block(obq, ".NH") : bqueue_block(obq, ".SH");
	if (bn == NULL)
		goto out;

	if ((st->flags & LOWDOWN_NROFF_NUMBERED) ||
	    (st->flags & LOWDOWN_NROFF_GROFF)) 
		if (asprintf(&bn->nargs, "%zd", level) == -1) {
			bn->nargs = NULL;
			goto out;
		}

	TAILQ_CONCAT(obq, bq, entries);
	st->post_para = 1;

	/*
	 * Used in -mspdf output for creating a TOC and intra-document
	 * linking.
	 */

	if (st->flags & LOWDOWN_NROFF_GROFF) {
		if ((buf = hbuf_new(32)) == NULL)
			goto out;
		if (!hbuf_extract_text(buf, n))
			goto out;

		if ((bn = bqueue_block(obq, ".pdfhref")) == NULL)
			goto out;
		if (asprintf(&bn->nargs, "O %zd", level) == -1) {
			bn->nargs = NULL;
			goto out;
		}

		/*
		 * No need to quote: quotes will be converted by
		 * escaping into roff.
		 */

		bn->args = strndup(buf->data, buf->size);
		if (bn->args == NULL)
			goto out;

		if ((bn = bqueue_block(obq, ".pdfhref M")) == NULL)
			goto out;

		/*
		 * If the identifier comes from the user, we need to
		 * escape it accordingly; otherwise, use it directly as
		 * the hbuf_id() function will take care of it.
		 */

		if (n->rndr_header.attr_id.size) {
			bn->args = strndup
				(n->rndr_header.attr_id.data,
				 n->rndr_header.attr_id.size);
			if (bn->args == NULL)
				goto out;
		} else {
			nbuf = hbuf_id(buf, NULL, &st->headers_used);
			if (nbuf == NULL)
				goto out;
			bn->nargs = strndup(nbuf->data, nbuf->size);
			if (bn->nargs == NULL)
				goto out;
		}
	}

	rc = 1;
out:
	hbuf_free(buf);
	return rc;
}

/*
 * Return FALSE on failure, TRUE on success.
 */
static ssize_t
rndr_link(struct nroff *st, struct bnodeq *obq, struct bnodeq *bq,
	const struct rndr_link *param)
{

	return putlink(obq, st, &param->link,
		&param->attr_id, bq, HALINK_NORMAL);
}

static int
rndr_listitem(struct bnodeq *obq, const struct lowdown_node *n,
	struct bnodeq *bq, const struct rndr_listitem *param)
{
	struct bnode	*bn;
	const char	*box;

	if (param->flags & HLIST_FL_ORDERED) {
		if ((bn = bqueue_block(obq, ".IP")) == NULL)
			return 0;
		if (asprintf(&bn->nargs, 
		    "\"%zu.  \"", param->num) == -1)
			return 0;
	} else if (param->flags & HLIST_FL_UNORDERED) {
		if (param->flags & HLIST_FL_CHECKED)
			box = "[u2611]";
		else if (param->flags & HLIST_FL_UNCHECKED)
			box = "[u2610]";
		else
			box = "(bu";
		if ((bn = bqueue_block(obq, ".IP")) == NULL)
			return 0;
		if (asprintf(&bn->nargs, "\"\\%s\" 2", box) == -1)
			return 0;
	}

	/* Strip out all leading redundant paragraphs. */

	bqueue_strip_paras(bq);
	TAILQ_CONCAT(obq, bq, entries);

	/* 
	 * Suppress trailing space if we're not in a block and there's a
	 * list item that comes after us (i.e., anything after us).
	 */

	if ((n->rndr_listitem.flags & HLIST_FL_BLOCK) ||
	    (n->rndr_listitem.flags & HLIST_FL_DEF))
		return 1;

	if (TAILQ_NEXT(n, entries) != NULL) {
		if (bqueue_block(obq, ".if n \\\n.sp -1") == NULL)
			return 0;
		if (bqueue_block(obq, ".if t \\\n.sp -0.25v\n") == NULL)
			return 0;
	}

	return 1;
}

static int
rndr_paragraph(struct nroff *st, const struct lowdown_node *n,
	struct bnodeq *obq, struct bnodeq *nbq)
{
	struct bnode	*bn;

	/* 
	 * Subsequent paragraphs get a PP for the indentation; otherwise, use
	 * LP and forego the indentation.  If we're in a list item, make sure
	 * that we don't reset our text indent by using an "IP".
	 */

	for ( ; n != NULL; n = n->parent)
		if (n->type == LOWDOWN_LISTITEM)
			break;
	if (n != NULL)
		bn = bqueue_block(obq, ".IP");
	else if (st->post_para)
		bn = bqueue_block(obq, ".LP");
	else
		bn = bqueue_block(obq, ".PP");
	if (bn == NULL)
		return 0;

	TAILQ_CONCAT(obq, nbq, entries);
	st->post_para = 0;
	return 1;
}

static int
rndr_raw_block(const struct nroff *st,
	struct bnodeq *obq, const struct rndr_blockhtml *param)
{
	struct bnode	*bn;

	if (st->flags & LOWDOWN_NROFF_SKIP_HTML)
		return 1;
	if ((bn = calloc(1, sizeof(struct bnode))) == NULL)
		return 0;
	TAILQ_INSERT_TAIL(obq, bn, entries);
	bn->scope = BSCOPE_LITERAL;
	bn->buf = strndup(param->text.data, param->text.size);
	return bn->buf != NULL;
}

static int
rndr_hrule(struct nroff *st, struct bnodeq *obq)
{

	/* The LP is to reset the margins. */

	if (bqueue_block(obq, ".LP") == NULL)
		return 0;

	/* Set post_para so we get a following LP not PP. */

	st->post_para = 1;

	if (st->man)
		return bqueue_block(obq, "\\l\'2i'") != NULL;

	return bqueue_block(obq,
	    ".ie d HR \\{\\\n"
	    ".HR\n"
	    "\\}\n"
	    ".el \\{\\\n"
	    ".sp 1v\n"
	    "\\l'\\n(.lu'\n"
	    ".sp 1v\n"
	    ".\\}") != NULL;
}

static int
rndr_image(struct nroff *st, struct bnodeq *obq, 
	const struct rndr_image *param)
{
	const char	*cp;
	size_t		 sz;
	struct bnode	*bn;

	if (!st->man) {
		cp = memrchr(param->link.data, '.', param->link.size);
		if (cp != NULL) {
			cp++;
			sz = param->link.size - (cp - param->link.data);
			if ((sz == 2 && memcmp(cp, "ps", 2) == 0) ||
			    (sz == 3 && memcmp(cp, "eps", 3) == 0)) {
				bn = bqueue_block(obq, ".PSPIC");
				if (bn == NULL)
					return 0;
				bn->args = strndup(param->link.data,
					param->link.size);
				return bn->args != NULL;
			}
		}
	}

	/* In -Tman, we have no images: treat as a link. */

	st->fonts[NFONT_BOLD]++;
	if (!bqueue_font(st, obq, 0))
		return 0;
	if ((bn = bqueue_span(obq, NULL)) == NULL)
		return 0;
	bn->buf = strndup(param->alt.data, param->alt.size);
	if (bn->buf == NULL)
		return 0;
	st->fonts[NFONT_BOLD]--;
	if (!bqueue_font(st, obq, 1))
		return 0;
	if (st->flags & LOWDOWN_NROFF_NOLINK)
		return bqueue_span(obq, " (Image)") != NULL;
	if (bqueue_span(obq, " (Image: ") == NULL)
		return 0;
	st->fonts[NFONT_ITALIC]++;
	if (!bqueue_font(st, obq, 0))
		return 0;
	if ((bn = bqueue_span(obq, NULL)) == NULL)
		return 0;
	if (st->flags & LOWDOWN_NROFF_SHORTLINK) {
		bn->nbuf = hbuf2shortlink(&param->link);
		if (bn->nbuf == NULL)
			return 0;
	} else {
		bn->buf = strndup(param->link.data, param->link.size);
		if (bn->buf == NULL)
			return 0;
	}
	st->fonts[NFONT_ITALIC]--;
	if (!bqueue_font(st, obq, 1))
		return 0;
	return bqueue_span(obq, ")") != NULL;
}

static int
rndr_raw_html(const struct nroff *st,
	struct bnodeq *obq, const struct rndr_raw_html *param)
{
	struct bnode	*bn;

	if (st->flags & LOWDOWN_NROFF_SKIP_HTML)
		return 1;
	if ((bn = calloc(1, sizeof(struct bnode))) == NULL)
		return 0;
	TAILQ_INSERT_TAIL(obq, bn, entries);
	bn->scope = BSCOPE_LITERAL;
	bn->buf = strndup(param->text.data, param->text.size);
	return bn->buf != NULL;
}

static int
rndr_table(struct nroff *st, struct bnodeq *obq, struct bnodeq *bq)
{
	const char	*macro;

	macro = st->man || !(st->flags & LOWDOWN_NROFF_GROFF) ?
		".TS" : ".TS H";
	if (bqueue_block(obq, macro) == NULL)
		return 0;
	if (bqueue_block(obq, "tab(|) expand allbox;") == NULL)
		return 0;
	TAILQ_CONCAT(obq, bq, entries);
	st->post_para = 1;
	return bqueue_block(obq, ".TE") != NULL;
}

static int
rndr_table_header(const struct nroff *st, struct bnodeq *obq,
	struct bnodeq *bq, const struct rndr_table_header *param)
{
	size_t		 	 i;
	struct lowdown_buf	*ob;
	struct bnode		*bn;
	int			 rc = 0;

	if ((ob = hbuf_new(32)) == NULL)
		return 0;

	/* 
	 * This specifies the header layout.
	 * We make the header bold, but this is arbitrary.
	 */

	if ((bn = bqueue_block(obq, NULL)) == NULL)
		goto out;
	for (i = 0; i < param->columns; i++) {
		if (i > 0 && !HBUF_PUTSL(ob, " "))
			goto out;
		switch (param->flags[i] & HTBL_FL_ALIGNMASK) {
		case HTBL_FL_ALIGN_CENTER:
			if (!HBUF_PUTSL(ob, "cb"))
				goto out;
			break;
		case HTBL_FL_ALIGN_RIGHT:
			if (!HBUF_PUTSL(ob, "rb"))
				goto out;
			break;
		default:
			if (!HBUF_PUTSL(ob, "lb"))
				goto out;
			break;
		}
	}
	if ((bn->nbuf = strndup(ob->data, ob->size)) == NULL)
		goto out;

	/* Now the body layout. */

	hbuf_truncate(ob);
	if ((bn = bqueue_block(obq, NULL)) == NULL)
		goto out;
	for (i = 0; i < param->columns; i++) {
		if (i > 0 && !HBUF_PUTSL(ob, " "))
			goto out;
		switch (param->flags[i] & HTBL_FL_ALIGNMASK) {
		case HTBL_FL_ALIGN_CENTER:
			if (!HBUF_PUTSL(ob, "c"))
				goto out;
			break;
		case HTBL_FL_ALIGN_RIGHT:
			if (!HBUF_PUTSL(ob, "r"))
				goto out;
			break;
		default:
			if (!HBUF_PUTSL(ob, "l"))
				goto out;
			break;
		}
	}
	if (!hbuf_putc(ob, '.'))
		goto out;
	if ((bn->nbuf = strndup(ob->data, ob->size)) == NULL)
		goto out;

	TAILQ_CONCAT(obq, bq, entries);

	if (!st->man && (st->flags & LOWDOWN_NROFF_GROFF) &&
	    bqueue_block(obq, ".TH") == NULL)
		goto out;

	rc = 1;
out:
	hbuf_free(ob);
	return rc;
}

static int
rndr_table_row(struct bnodeq *obq, struct bnodeq *bq)
{

	TAILQ_CONCAT(obq, bq, entries);
	return bqueue_block(obq, NULL) != NULL;
}

static int
rndr_table_cell(struct bnodeq *obq, struct bnodeq *bq,
	const struct rndr_table_cell *param)
{
	struct bnode	*bn;

	if (param->col > 0 && bqueue_span(obq, "|") == NULL)
		return 0;
	if (bqueue_span(obq, "T{\n") == NULL)
		return 0;
	TAILQ_CONCAT(obq, bq, entries);
	if ((bn = bqueue_span(obq, "T}")) == NULL)
		return 0;
	bn->tblhack = 1;
	return 1;
}

static int
rndr_superscript(struct bnodeq *obq, struct bnodeq *bq)
{

	if (bqueue_span(obq, "\\u\\s-3") == NULL)
		return 0;
	TAILQ_CONCAT(obq, bq, entries);
	return bqueue_span(obq, "\\s+3\\d") != NULL;
}

static int
rndr_footnote_def(const struct nroff *st, struct bnodeq *obq,
	struct bnodeq *bq, size_t num)
{
	struct bnode	*bn;

	/* 
	 * Use groff_ms(7)-style footnotes.
	 * We know that the definitions are delivered in the same order
	 * as the footnotes are made, so we can use the automatic
	 * ordering facilities.
	 */

	if (!st->man) {
		if (bqueue_block(obq, ".FS") == NULL)
			return 0;
		bqueue_strip_paras(bq);
		TAILQ_CONCAT(obq, bq, entries);
		return bqueue_block(obq, ".FE") != NULL;
	}

	/*
	 * For man(7), just print as normal, with a leading footnote
	 * number in italics and superscripted.
	 */

	if (bqueue_block(obq, ".LP") == NULL)
		return 0;
	if ((bn = bqueue_span(obq, NULL)) == NULL)
		return 0;
	if (asprintf(&bn->nbuf, 
	    "\\0\\fI\\u\\s-3%zu\\s+3\\d\\fP\\0", num) == -1) {
		bn->nbuf = NULL;
		return 0;
	}
	bqueue_strip_paras(bq);
	TAILQ_CONCAT(obq, bq, entries);
	return 1;
}

static int
rndr_footnotes(const struct nroff *st, struct bnodeq *obq)
{
	size_t	 i;

	if (st->footsz == 0)
		return 1;

	if (st->man) {
		if (bqueue_block(obq, ".LP") == NULL)
			return 0;
		if (bqueue_block(obq, ".sp 3") == NULL)
			return 0;
		if (bqueue_block(obq, "\\l\'2i'") == NULL)
			return 0;
	}

	for (i = 0; i < st->footsz; i++)
		if (!rndr_footnote_def(st, obq, st->foots[i], i + 1))
			return 0;

	return 1;
}

static int
rndr_footnote_ref(struct nroff *st, struct bnodeq *obq,
	struct bnodeq *bq)
{
	struct bnode	*bn;
	void		*pp;
	size_t		 num = st->footsz;

	/* 
	 * Use groff_ms(7)-style automatic footnoting, else just put a
	 * reference number in small superscripts.
	 */

	if ((bn = bqueue_span(obq, NULL)) == NULL)
		return 0;

	if (!st->man)
		bn->nbuf = strdup("\\**");
	else if (asprintf(&bn->nbuf, 
		 "\\u\\s-3%zu\\s+3\\d", num + 1) == -1)
		bn->nbuf = NULL;

	if (bn->nbuf == NULL)
		return 0;

	/*
	 * For -Tman, queue the footnote for printing at the end of the
	 * document.  For -Tms, emit it now in a FS/FE block.
	 */

	if (st->man) {
		pp = recallocarray(st->foots, st->footsz,
			st->footsz + 1, sizeof(struct bnodeq *));
		if (pp == NULL)
			return 0;
		st->foots = pp;
		st->foots[st->footsz++] = malloc(sizeof(struct bnodeq));
		if (st->foots[num] == NULL)
			return 0;
		TAILQ_INIT(st->foots[num]);
		TAILQ_CONCAT(st->foots[num], bq, entries);
		return 1;
	} else {
		if (bqueue_block(obq, ".FS") == NULL)
			return 0;
		bqueue_strip_paras(bq);
		TAILQ_CONCAT(obq, bq, entries);
		return bqueue_block(obq, ".FE") != NULL;
	}
}

static int
rndr_entity(const struct nroff *st,
	struct bnodeq *obq, const struct rndr_entity *param)
{
	char		 buf[32];
	const char	*ent;
	struct bnode	*bn;
	int32_t		 iso;
	size_t		 sz;

	/*
	 * Handle named entities if "ent" is non-NULL, use unicode
	 * escapes for values above 126, and just the regular character
	 * if within the ASCII set.
	 */

	if ((ent = entity_find_nroff(&param->text, &iso)) != NULL) {
		sz = strlen(ent);
		if (sz == 1)
			snprintf(buf, sizeof(buf), "\\%s", ent);
		else if (sz == 2)
			snprintf(buf, sizeof(buf), "\\(%s", ent);
		else
			snprintf(buf, sizeof(buf), "\\[%s]", ent);
		return bqueue_span(obq, buf) != NULL;
	} else if (iso > 0 && iso > 126) {
		if (st->flags & LOWDOWN_NROFF_GROFF)
			snprintf(buf, sizeof(buf), "\\[u%.4llX]", 
				(unsigned long long)iso);
		else
			snprintf(buf, sizeof(buf), "\\U\'%.4llX\'", 
				(unsigned long long)iso);
		return bqueue_span(obq, buf) != NULL;
	} else if (iso > 0) {
		snprintf(buf, sizeof(buf), "%c", iso);
		return bqueue_span(obq, buf) != NULL;
	}

	if ((bn = bqueue_span(obq, NULL)) == NULL)
		return 0;
	bn->buf = strndup(param->text.data, param->text.size);
	return bn->buf != NULL;
}

/*
 * Split "b" at sequential white-space, outputting the results in the
 * line-based "env" macro.  The content in "b" has already been escaped.
 */
static int
rndr_meta_multi(struct bnodeq *obq, const char *b, const char *env)
{
	const char	*start;
	size_t		 sz, i, bsz;
	struct bnode	*bn;
	char		 macro[32];

	if (b == NULL)
		return 1;

	assert(strlen(env) < sizeof(macro) - 1);
	snprintf(macro, sizeof(macro), ".%s", env);
	bsz = strlen(b);

	for (i = 0; i < bsz; i++) {
		while (i < bsz &&
		       isspace((unsigned char)b[i]))
			i++;
		if (i == bsz)
			continue;
		start = &b[i];

		for (; i < bsz; i++)
			if (i < bsz - 1 &&
			    isspace((unsigned char)b[i]) &&
			    isspace((unsigned char)b[i + 1]))
				break;
		if ((sz = &b[i] - start) == 0)
			continue;

		if (bqueue_block(obq, macro) == NULL)
			return 0;
		if ((bn = bqueue_span(obq, NULL)) == NULL)
			return 0;
		if ((bn->nbuf = strndup(start, sz)) == NULL)
			return 0;
	}

	return 1;
}

/*
 * Fill "mq" by serialising child nodes into strings.  The serialised
 * strings are escaped.
 */
static int
rndr_meta(struct nroff *st, const struct bnodeq *bq, 
	struct lowdown_metaq *mq, const struct rndr_meta *params)
{
	struct lowdown_meta	*m;
	struct lowdown_buf	*ob;
	ssize_t			 val;
	const char		*ep;

	if ((m = calloc(1, sizeof(struct lowdown_meta))) == NULL)
		return 0;
	TAILQ_INSERT_TAIL(mq, m, entries);

	m->key = strndup(params->key.data, params->key.size);
	if (m->key == NULL)
		return 0;

	if ((ob = hbuf_new(32)) == NULL)
		return 0;
	if (!bqueue_flush(ob, bq, 1)) {
		hbuf_free(ob);
		return 0;
	}
	m->value = strndup(ob->data, ob->size);
	hbuf_free(ob);
	if (m->value == NULL)
		return 0;

	if (strcmp(m->key, "shiftheadinglevelby") == 0) {
		val = (ssize_t)strtonum
			(m->value, -100, 100, &ep);
		if (ep == NULL)
			st->headers_offs = val + 1;
	} else if (strcmp(m->key, "baseheaderlevel") == 0) {
		val = (ssize_t)strtonum
			(m->value, 1, 100, &ep);
		if (ep == NULL)
			st->headers_offs = val;
	}

	return 1;
}

static int
rndr_doc_header(const struct nroff *st,
	struct bnodeq *obq, const struct lowdown_metaq *mq)
{
	struct lowdown_buf		*ob = NULL;
	struct bnode			*bn;
	const struct lowdown_meta	*m;
	int				 rc = 0;
	const char			*author = NULL, *title = NULL,
					*affil = NULL, *date = NULL,
					*copy = NULL, *sec = NULL,
					*rcsauthor = NULL, *rcsdate = NULL,
					*source = NULL, *volume = NULL;

	if (!(st->flags & LOWDOWN_STANDALONE))
		return 1;

	TAILQ_FOREACH(m, mq, entries)
		if (strcasecmp(m->key, "author") == 0)
			author = m->value;
		else if (strcasecmp(m->key, "copyright") == 0)
			copy = m->value;
		else if (strcasecmp(m->key, "affiliation") == 0)
			affil = m->value;
		else if (strcasecmp(m->key, "date") == 0)
			date = m->value;
		else if (strcasecmp(m->key, "rcsauthor") == 0)
			rcsauthor = rcsauthor2str(m->value);
		else if (strcasecmp(m->key, "rcsdate") == 0)
			rcsdate = rcsdate2str(m->value);
		else if (strcasecmp(m->key, "title") == 0)
			title = m->value;
		else if (strcasecmp(m->key, "section") == 0)
			sec = m->value;
		else if (strcasecmp(m->key, "source") == 0)
			source = m->value;
		else if (strcasecmp(m->key, "volume") == 0)
			volume = m->value;

	/* Overrides. */

	if (title == NULL)
		title = "Untitled article";
	if (sec == NULL)
		sec = "7";
	if (rcsdate != NULL)
		date = rcsdate;
	if (rcsauthor != NULL)
		author = rcsauthor;

	bn = bqueue_block(obq, 
		".\\\" -*- mode: troff; coding: utf-8 -*-");
	if (bn == NULL)
		goto out;

	if (!st->man) {
		if (copy != NULL) {
			bn = bqueue_block(obq,
				".ds LF Copyright \\(co");
			if (bn == NULL)
				goto out;
			if ((bn->nargs = strdup(copy)) == NULL)
				goto out;
		}
		if (date != NULL) {
			if (copy != NULL)
				bn = bqueue_block(obq, ".ds RF");
			else
				bn = bqueue_block(obq, ".DA");
			if (bn == NULL)
				goto out;
			if ((bn->nargs = strdup(date)) == NULL)
				goto out;
		}

		if (bqueue_block(obq, ".TL") == NULL)
			goto out;
		if ((bn = bqueue_span(obq, NULL)) == NULL)
			goto out;
		if ((bn->nbuf = strdup(title)) == NULL)
			goto out;
		if (!rndr_meta_multi(obq, author, "AU"))
			goto out;
		if (!rndr_meta_multi(obq, affil, "AI"))
			goto out;
	} else {
		if ((ob = hbuf_new(32)) == NULL)
			goto out;

		/*
		 * The syntax of this macro, according to man(7), is 
		 * TH name section date [source [volume]].
		 */

		if ((bn = bqueue_block(obq, ".TH")) == NULL)
			goto out;

		if (!hbuf_putc(ob, '"') ||
		    !hesc_nroff(ob, title, strlen(title), 1, 0, 0) ||
		    !HBUF_PUTSL(ob, "\" \"") ||
		    !hesc_nroff(ob, sec, strlen(sec), 1, 0, 0) ||
		    !hbuf_putc(ob, '"'))
			goto out;

		/*
		 * We may not have a date (or it may be empty), in which
		 * case man(7) says the current date is used.
		 */

		if (!HBUF_PUTSL(ob, " \""))
			goto out;
		if (date != NULL &&
		    !hesc_nroff(ob, date, strlen(date), 1, 0, 0))
			goto out;
		if (!HBUF_PUTSL(ob, "\""))
			goto out;

		/*
		 * Don't print these unless necessary, as the latter
		 * overrides the default system printing for the
		 * section.
		 */

		if (source != NULL || volume != NULL) {
			if (!HBUF_PUTSL(ob, " \""))
				goto out;
			if (source != NULL && !hesc_nroff
			    (ob, source, strlen(source), 1, 0, 0))
				goto out;
			if (!HBUF_PUTSL(ob, "\""))
				goto out;
			if (!HBUF_PUTSL(ob, " \""))
				goto out;
			if (volume != NULL && !hesc_nroff
			    (ob, volume, strlen(volume), 1, 0, 0))
				goto out;
			if (!HBUF_PUTSL(ob, "\""))
				goto out;
		}
		if ((bn->nargs = strndup(ob->data, ob->size)) == NULL)
			goto out;
	}

	rc = 1;
out:
	hbuf_free(ob);
	return rc;
}

/*
 * Actually render the node "n" and all of its children into the output
 * buffer "ob", chopping "chop" from the current node if specified.
 * Return what (if anything) we should chop from the next node or <0 on
 * failure.
 */
static int
rndr(struct lowdown_metaq *mq, struct nroff *st,
	const struct lowdown_node *n, struct bnodeq *obq)
{
	const struct lowdown_node	*child;
	int				 rc = 1;
	enum nfont			 fonts[NFONT__MAX];
	struct bnodeq			 tmpbq;
	struct bnode			*bn;

	TAILQ_INIT(&tmpbq);

	if ((n->chng == LOWDOWN_CHNG_INSERT ||
	     n->chng == LOWDOWN_CHNG_DELETE) &&
	    !bqueue_colour(obq, n->chng, 0))
		goto out;

	/*
	 * Font management.
	 * roff doesn't handle its own font stack, so we can't set fonts
	 * and step out of them in a nested way.
	 */

	memcpy(fonts, st->fonts, sizeof(fonts));

	switch (n->type) {
	case LOWDOWN_CODESPAN:
		st->fonts[NFONT_FIXED]++;
		if (!bqueue_font(st, obq, 0))
			goto out;
		break;
	case LOWDOWN_EMPHASIS:
		st->fonts[NFONT_ITALIC]++;
		if (!bqueue_font(st, obq, 0))
			goto out;
		break;
	case LOWDOWN_HIGHLIGHT:
	case LOWDOWN_DOUBLE_EMPHASIS:
		st->fonts[NFONT_BOLD]++;
		if (!bqueue_font(st, obq, 0))
			goto out;
		break;
	case LOWDOWN_TRIPLE_EMPHASIS:
		st->fonts[NFONT_ITALIC]++;
		st->fonts[NFONT_BOLD]++;
		if (!bqueue_font(st, obq, 0))
			goto out;
		break;
	default:
		break;
	}

	TAILQ_FOREACH(child, &n->children, entries)
		if (!rndr(mq, st, child, &tmpbq))
			goto out;

	switch (n->type) {
	case LOWDOWN_BLOCKCODE:
		rc = rndr_blockcode(st, obq, &n->rndr_blockcode);
		break;
	case LOWDOWN_BLOCKQUOTE:
		rc = rndr_blockquote(st, obq, &tmpbq);
		break;
	case LOWDOWN_DEFINITION:
		rc = rndr_list(st, obq, n, &tmpbq);
		break;
	case LOWDOWN_DEFINITION_DATA:
		rc = rndr_definition_data(obq, &tmpbq);
		break;
	case LOWDOWN_DEFINITION_TITLE:
		rc = rndr_definition_title(obq, &tmpbq);
		break;
	case LOWDOWN_DOC_HEADER:
		rc = rndr_doc_header(st, obq, mq);
		break;
	case LOWDOWN_META:
		if (n->chng != LOWDOWN_CHNG_DELETE)
			rc = rndr_meta(st, &tmpbq, mq, &n->rndr_meta);
		break;
	case LOWDOWN_HEADER:
		rc = rndr_header(st, obq, &tmpbq, n);
		break;
	case LOWDOWN_HRULE:
		rc = rndr_hrule(st, obq);
		break;
	case LOWDOWN_LIST:
		rc = rndr_list(st, obq, n, &tmpbq);
		break;
	case LOWDOWN_LISTITEM:
		rc = rndr_listitem(obq, n, &tmpbq, &n->rndr_listitem);
		break;
	case LOWDOWN_PARAGRAPH:
		rc = rndr_paragraph(st, n, obq, &tmpbq);
		break;
	case LOWDOWN_TABLE_BLOCK:
		rc = rndr_table(st, obq, &tmpbq);
		break;
	case LOWDOWN_TABLE_HEADER:
		rc = rndr_table_header(st, 
			obq, &tmpbq, &n->rndr_table_header);
		break;
	case LOWDOWN_TABLE_ROW:
		rc = rndr_table_row(obq, &tmpbq);
		break;
	case LOWDOWN_TABLE_CELL:
		rc = rndr_table_cell(obq, &tmpbq, &n->rndr_table_cell);
		break;
	case LOWDOWN_ROOT:
		TAILQ_CONCAT(obq, &tmpbq, entries);
		rc = rndr_footnotes(st, obq);
		break;
	case LOWDOWN_BLOCKHTML:
		rc = rndr_raw_block(st, obq, &n->rndr_blockhtml);
		break;
	case LOWDOWN_LINK_AUTO:
		rc = rndr_autolink(st, obq, &n->rndr_autolink);
		break;
	case LOWDOWN_CODESPAN:
		rc = rndr_codespan(obq, &n->rndr_codespan);
		break;
	case LOWDOWN_IMAGE:
		rc = rndr_image(st, obq, &n->rndr_image);
		break;
	case LOWDOWN_LINEBREAK:
		rc = rndr_linebreak(obq);
		break;
	case LOWDOWN_LINK:
		rc = rndr_link(st, obq, &tmpbq, &n->rndr_link);
		break;
	case LOWDOWN_SUPERSCRIPT:
		rc = rndr_superscript(obq, &tmpbq);
		break;
	case LOWDOWN_FOOTNOTE:
		rc = rndr_footnote_ref(st, obq, &tmpbq);
		break;
	case LOWDOWN_RAW_HTML:
		rc = rndr_raw_html(st, obq, &n->rndr_raw_html);
		break;
	case LOWDOWN_NORMAL_TEXT:
		if ((bn = bqueue_span(obq, NULL)) == NULL)
			goto out;
		bn->buf = strndup
			(n->rndr_normal_text.text.data,
			 n->rndr_normal_text.text.size);
		if (bn->buf == NULL)
			goto out;
		break;
	case LOWDOWN_ENTITY:
		rc = rndr_entity(st, obq, &n->rndr_entity);
		break;
	default:
		TAILQ_CONCAT(obq, &tmpbq, entries);
		break;
	}

	if (!rc)
		goto out;

	/* Restore the font stack. */

	switch (n->type) {
	case LOWDOWN_CODESPAN:
	case LOWDOWN_EMPHASIS:
	case LOWDOWN_HIGHLIGHT:
	case LOWDOWN_DOUBLE_EMPHASIS:
	case LOWDOWN_TRIPLE_EMPHASIS:
		memcpy(st->fonts, fonts, sizeof(fonts));
		if (!bqueue_font(st, obq, 1)) {
			rc = 0;
			goto out;
		}
		break;
	default:
		break;
	}

	if ((n->chng == LOWDOWN_CHNG_INSERT ||
	     n->chng == LOWDOWN_CHNG_DELETE) &&
	    !bqueue_colour(obq, n->chng, 1)) {
		rc = 0;
		goto out;
	}

	rc = 1;
out:
	bqueue_free(&tmpbq);
	return rc;
}

int
lowdown_nroff_rndr(struct lowdown_buf *ob,
	void *arg, const struct lowdown_node *n)
{
	struct nroff		*st = arg;
	struct lowdown_metaq	 metaq;
	int			 rc = 0;
	struct bnodeq		 bq;
	size_t			 i;

	TAILQ_INIT(&metaq);
	TAILQ_INIT(&bq);
	TAILQ_INIT(&st->headers_used);

	memset(st->fonts, 0, sizeof(st->fonts));
	st->headers_offs = 1;
	st->post_para = 0;

	if (rndr(&metaq, st, n, &bq)) {
		if (!bqueue_flush(ob, &bq, 1))
			goto out;
		if (ob->size && ob->data[ob->size - 1] != '\n' &&
		    !hbuf_putc(ob, '\n'))
			goto out;
		rc = 1;
	}

out:
	for (i = 0; i < st->footsz; i++) {
		bqueue_free(st->foots[i]);
		free(st->foots[i]);
	}

	free(st->foots);
	st->footsz = 0;
	st->foots = NULL;
	lowdown_metaq_free(&metaq);
	bqueue_free(&bq);
	hentryq_clear(&st->headers_used);
	return rc;
}

void *
lowdown_nroff_new(const struct lowdown_opts *opts)
{
	struct nroff 	*p;

	if ((p = calloc(1, sizeof(struct nroff))) == NULL)
		return NULL;

	p->flags = opts != NULL ? opts->oflags : 0;
	p->man = opts != NULL && opts->type == LOWDOWN_MAN;
	return p;
}

void
lowdown_nroff_free(void *arg)
{

	/* No need to check NULL: pass directly to free(). */

	free(arg);
}