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

gemini.c (25386B)


/*	$Id$ */
/*
 * Copyright (c) 2020--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 <time.h>
#include <wchar.h>

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

/*
 * A standalone link is one that lives in its own paragraph.
 */
#define	IS_STANDALONE_LINK(_n, _prev) \
	((_n)->parent != NULL && \
	 (_n)->parent->type == LOWDOWN_PARAGRAPH && \
	 (_n)->parent->parent != NULL && \
	 (_n)->parent->parent->type == LOWDOWN_ROOT && \
	 (_prev) == NULL && \
	 TAILQ_NEXT((_n), entries) == NULL)

/*
 * A link queued for display.
 * This only happens when using footnote or endnote links.
 */
struct link {
	const struct lowdown_node	*n; /* node needing link */
	size_t				 id; /* link-%zu */
	TAILQ_ENTRY(link)		 entries;
};

TAILQ_HEAD(linkq, link);

struct gemini {
	unsigned int		  flags; /* output flags */
	ssize_t			  last_blank; /* line breaks or -1 (start) */
	struct lowdown_buf	 *tmp; /* for temporary allocations */
	size_t			  nolinkqsz; /* if >0, don't record links */
	int			  nolinkflush; /* if TRUE, don't flush links */
	struct linkq		  linkq; /* link queue */
	size_t			  linkqsz; /* position in link queue */
	wchar_t			 *buf; /* buffer for counting wchar */
	size_t			  bufsz; /* size of buf */
	ssize_t			  headers_offs; /* header offset */
	struct lowdown_buf	**foots; /* footnotes */
	size_t			  footsz; /* footnotes size  */
};

/*
 * Forward declaration.
 */
static int
rndr(struct lowdown_buf *, struct lowdown_metaq *, 
	struct gemini *, const struct lowdown_node *);

static void
link_freeq(struct linkq *q)
{
	struct link	*l;

	while ((l = TAILQ_FIRST(q)) != NULL) {
		TAILQ_REMOVE(q, l, entries);
		free(l);
	}
}

static int
rndr_link_ref(const struct gemini *st,
	struct lowdown_buf *out, size_t ref, int nl)
{
	char		 buf[32], c;
	size_t		 sz = 0, i;

	assert(ref);

	if (st->flags & LOWDOWN_GEMINI_LINK_NOREF)
		return hbuf_printf(out, "%s", nl ? "\n" : "");

	buf[0] = '\0';
	if (st->flags & LOWDOWN_GEMINI_LINK_ROMAN) {
		while(ref)
			if (ref >= 1000) {
				strlcat(buf, "m", sizeof(buf));
				ref -= 1000;
			} else if (ref >= 900) {
				strlcat(buf, "cm", sizeof(buf));
				ref -= 900;
			} else if (ref >= 500) {
				strlcat(buf, "d", sizeof(buf));
				ref -= 500;
			} else if (ref >= 400) {
				strlcat(buf, "cd", sizeof(buf));
				ref -= 400;
			} else if (ref >= 100) {
				strlcat(buf, "c", sizeof(buf));
				ref -= 100;
			} else if (ref >= 90) {
				strlcat(buf, "xc", sizeof(buf));
				ref -= 90;
			} else if (ref >= 50) {
				strlcat(buf, "l", sizeof(buf));
				ref -= 50;
			} else if (ref >= 40) {
				strlcat(buf, "xl", sizeof(buf));
				ref -= 40;
			} else if (ref >= 10) {
				strlcat(buf, "x", sizeof(buf));
				ref -= 10;
			} else if (ref >= 9) {
				strlcat(buf, "ix", sizeof(buf));
				ref -= 9;
			} else if (ref >= 5) {
				strlcat(buf, "v", sizeof(buf));
				ref -= 5;
			} else if (ref >= 4) {
				strlcat(buf, "iv", sizeof(buf));
				ref -= 4;
			} else if (ref >= 1) {
				strlcat(buf, "i", sizeof(buf));
				ref -= 1;
			}
	} else {
		while (ref && sz < sizeof(buf) - 1) {
			buf[sz++] = 'a' + (ref - 1) % 26;
			ref = (ref - 1) / 26;
		}
		buf[sz] = '\0';
		for (i = 0; i < sz; i++, sz--) {
			c = buf[i];
			buf[i] = buf[sz - 1];
			buf[sz - 1] = c;
		}
	}

	return hbuf_printf(out, "%s[%s]%s", 
		nl ? " " : "", buf, nl ? "\n" : "");
}

/*
 * Convert newlines to spaces, elide control characters.
 * If a newline follows a period, it's converted to two spaces.
 * Return zero on failure (memory), non-zero on success.
 */
static int
rndr_escape(struct lowdown_buf *out, const char *buf, size_t sz)
{
	size_t	 i, start = 0;

	for (i = 0; i < sz; i++) {
		if (buf[i] == '\n') {
			if (!hbuf_put(out, buf + start, i - start))
				return 0;
			if (out->size && 
			    out->data[out->size - 1] == '.' &&
			    !hbuf_putc(out, ' '))
				return 0;
			if (!hbuf_putc(out, ' '))
				return 0;
			start = i + 1;
		} else if (iscntrl((unsigned char)buf[i])) {
			if (!hbuf_put(out, buf + start, i - start))
				return 0;
			start = i + 1;
		}
	}

	if (start < sz &&
	    !hbuf_put(out, buf + start, sz - start))
		return 0;

	return 1;
}

/*
 * Output optional number of newlines before or after content.
 * Return zero on failure (memory), non-zero on success.
 */
static int
rndr_buf_vspace(struct gemini *st, struct lowdown_buf *out, size_t sz)
{

	if (st->last_blank >= 0)
		while ((size_t)st->last_blank < sz) {
			if (!HBUF_PUTSL(out, "\n"))
				return 0;
			st->last_blank++;
		}

	return 1;
}

/*
 * Emit text in "in" the current line with output "out".
 * Return zero on failure (memory), non-zero on success.
 */
static int
rndr_buf(struct gemini *st, struct lowdown_buf *out, 
	const struct lowdown_node *n, const struct lowdown_buf *in)
{
	const struct lowdown_node	*nn;
	size_t				 i = 0;

	for (nn = n; nn != NULL; nn = nn->parent)
		if (nn->type == LOWDOWN_BLOCKCODE ||
	  	    nn->type == LOWDOWN_BLOCKHTML) {
			st->last_blank = 1;
			return hbuf_putb(out, in);
		}

	/* 
	 * If we last printed some space and we're not in literal mode,
	 * suppress any leading blanks.
	 * This is only likely to happen around links.
	 */

	assert(in != NULL);
	if (st->last_blank != 0)
		for ( ; i < in->size; i++)
			if (!isspace((unsigned char)in->data[i]))
				break;

	if (!rndr_escape(out, in->data + i, in->size - i))
		return 0;
	if (in->size && st->last_blank != 0)
		st->last_blank = 0;
	return 1;
}

/*
 * Output the unicode entry "val", which must be strictly greater than
 * zero, as a UTF-8 sequence.
 * This does no error checking.
 * Return zero on failure (memory), non-zero on success.
 */
static int
rndr_entity(struct lowdown_buf *buf, int32_t val)
{

	assert(val > 0);
	if (val < 0x80)
		return hbuf_putc(buf, val);
       	if (val < 0x800)
		return hbuf_putc(buf, 192 + val / 64) && 
			hbuf_putc(buf, 128 + val % 64);
       	if (val - 0xd800u < 0x800) 
		return 1;
       	if (val < 0x10000)
		return hbuf_putc(buf, 224 + val / 4096) &&
			hbuf_putc(buf, 128 + val / 64 % 64) &&
			hbuf_putc(buf, 128 + val % 64);
       	if (val < 0x110000)
		return hbuf_putc(buf, 240 + val / 262144) &&
			hbuf_putc(buf, 128 + val / 4096 % 64) &&
			hbuf_putc(buf, 128 + val / 64 % 64) &&
			hbuf_putc(buf, 128 + val % 64);
	return 1;
}

static int
rndr_doc_header(struct gemini *st, struct lowdown_buf *out,
	const struct lowdown_metaq *mq)
{
	const struct lowdown_meta	*m;

	if (!(st->flags & LOWDOWN_GEMINI_METADATA))
		return 1;
	TAILQ_FOREACH(m, mq, entries) {
		if (!rndr_escape(out, m->key, strlen(m->key)))
			return 0;
		if (!HBUF_PUTSL(out, ": "))
			return 0;
		if (!rndr_escape(out, m->value, strlen(m->value)))
			return 0;
		st->last_blank = 0;
		if (!rndr_buf_vspace(st, out, 1))
			return 0;
	}
	return 1;
}

/*
 * Render the key and value, then store the results in our "mq"
 * conditional to it existing.
 * Return zero on failure (memory), non-zero on success.
 */
static int
rndr_meta(struct gemini *st, 
	const struct lowdown_node *n, struct lowdown_metaq *mq)
{
	ssize_t				 last_blank;
	struct lowdown_buf		*tmp = NULL;
	struct lowdown_meta		*m;
	const struct lowdown_node	*child;
	ssize_t				 val;
	const char			*ep;

	/*
	 * Manually render the children of the meta into a
	 * buffer and use that as our value.  Start by zeroing
	 * our terminal position and using another output buffer
	 * (st->tmp would be clobbered by children).
	 */

	last_blank = st->last_blank;
	st->last_blank = -1;

	if ((tmp = hbuf_new(128)) == NULL)
		goto err;
	if ((m = calloc(1, sizeof(struct lowdown_meta))) == NULL)
		goto err;
	TAILQ_INSERT_TAIL(mq, m, entries);

	m->key = strndup(n->rndr_meta.key.data,
		n->rndr_meta.key.size);
	if (m->key == NULL)
		goto err;

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

	m->value = strndup(tmp->data, tmp->size);
	if (m->value == NULL)
		goto err;

	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;
	}

	hbuf_free(tmp);
	st->last_blank = last_blank;
	return 1;
err:
	hbuf_free(tmp);
	return 0;
}

/*
 * Return zero on failure (memory), non-zero on success.
 */
static int
rndr_flush_linkq(struct gemini *st, struct lowdown_buf *out)
{
	struct link	*l;
	int		 rc;

	assert(st->nolinkqsz == 0);

	while ((l = TAILQ_FIRST(&st->linkq)) != NULL) {
		TAILQ_REMOVE(&st->linkq, l, entries);
		if (!HBUF_PUTSL(out, "=> "))
			return 0;
		if (l->n->type == LOWDOWN_LINK)
			rc = hbuf_putb(out, &l->n->rndr_link.link);
		else if (l->n->type == LOWDOWN_LINK_AUTO)
			rc = hbuf_putb(out, &l->n->rndr_autolink.link);
		else if (l->n->type == LOWDOWN_IMAGE)
			rc = hbuf_putb(out, &l->n->rndr_image.link);
		else
			rc = 1;
		if (!rc)
			return 0;
		if (!rndr_link_ref(st, out, l->id, 1))
			return 0;
		st->last_blank = 1;
		free(l);
	}

	st->linkqsz = 0;
	return 1;
}

/*
 * Get the column width of a multi-byte sequence.
 * If the sequence is bad, return the number of raw bytes to print.
 * Return <0 on failure (memory), >=0 otherwise.
 */
static ssize_t
rndr_mbswidth(struct gemini *st, const struct lowdown_buf *in)
{
	size_t	 	 wsz, csz;
	const char	*cp;
	void		*pp;
	mbstate_t	 mbs;

	memset(&mbs, 0, sizeof(mbstate_t));
	cp = in->data;
	wsz = mbsnrtowcs(NULL, &cp, in->size, 0, &mbs);
	if (wsz == (size_t)-1)
		return in->size;

	if (st->bufsz < wsz) {
		st->bufsz = wsz;
		pp = reallocarray(st->buf, wsz, sizeof(wchar_t));
		if (pp == NULL)
			return -1;
		st->buf = pp;
	}

	memset(&mbs, 0, sizeof(mbstate_t));
	cp = in->data;
	mbsnrtowcs(st->buf, &cp, in->size, wsz, &mbs);
	csz = wcswidth(st->buf, wsz);
	return csz == (size_t)-1 ? in->size : csz;
}

/*
 * Return zero on failure (memory), non-zero on success.
 */
static int
rndr_table(struct lowdown_buf *ob, struct gemini *st, 
	const struct lowdown_node *n)
{
	size_t				*widths = NULL;
	const struct lowdown_node	*row, *top, *cell;
	struct lowdown_buf		*celltmp = NULL, 
					*rowtmp = NULL;
	size_t				 i, j, sz;
	ssize_t			 	 last_blank, ssz;
	unsigned int			 flags, oflags;
	int				 rc = 0;

	assert(n->type == LOWDOWN_TABLE_BLOCK);

	/*
	 * Temporarily make us not use in-line links.
	 * This is obviously because tables and inline links don't work
	 * well together.
	 */

	oflags = st->flags;
	if (st->flags & LOWDOWN_GEMINI_LINK_IN)
		st->flags &= ~LOWDOWN_GEMINI_LINK_IN;

	widths = calloc(n->rndr_table.columns, sizeof(size_t));
	if (widths == NULL)
		goto out;

	if ((rowtmp = hbuf_new(128)) == NULL ||
	    (celltmp = hbuf_new(128)) == NULL)
		goto out;

	/*
	 * Begin by counting the number of printable columns in each
	 * column in each row.  Don't let us accumulate any links, as
	 * we're going to re-run this after.
	 */

	st->nolinkqsz = st->linkqsz + 1;
	TAILQ_FOREACH(top, &n->children, entries) {
		assert(top->type == LOWDOWN_TABLE_HEADER ||
			top->type == LOWDOWN_TABLE_BODY);
		TAILQ_FOREACH(row, &top->children, entries)
			TAILQ_FOREACH(cell, &row->children, entries) {
				i = cell->rndr_table_cell.col;
				assert(i < n->rndr_table.columns);
				hbuf_truncate(celltmp);
				last_blank = st->last_blank;
				st->last_blank = 0;
				if (!rndr(celltmp, NULL, st, cell))
					goto out;
				ssz = rndr_mbswidth(st, celltmp);
				if (ssz < 0)
					goto out;
				if (widths[i] < (size_t)ssz)
					widths[i] = (size_t)ssz;
				st->last_blank = last_blank;
			}
	}
	st->nolinkqsz = 0;

	/* Now actually print, row-by-row into the output. */

	TAILQ_FOREACH(top, &n->children, entries) {
		assert(top->type == LOWDOWN_TABLE_HEADER ||
			top->type == LOWDOWN_TABLE_BODY);
		TAILQ_FOREACH(row, &top->children, entries) {
			hbuf_truncate(rowtmp);
			TAILQ_FOREACH(cell, &row->children, entries) {
				i = cell->rndr_table_cell.col;
				hbuf_truncate(celltmp);
				last_blank = st->last_blank;
				st->last_blank = 0;
				if (!rndr(celltmp, NULL, st, cell))
					goto out;
				ssz = rndr_mbswidth(st, celltmp);
				if (ssz < 0)
					goto out;
				assert(widths[i] >= (size_t)ssz);
				sz = widths[i] - (size_t)ssz;

				/* 
				 * Alignment is either beginning,
				 * ending, or splitting the remaining
				 * spaces around the word.
				 * Be careful about uneven splitting in
				 * the case of centre.
				 */

				flags = cell->rndr_table_cell.flags & 
					HTBL_FL_ALIGNMASK;
				if (flags == HTBL_FL_ALIGN_RIGHT)
					for (j = 0; j < sz; j++)
						if (!HBUF_PUTSL(rowtmp, " "))
							goto out;
				if (flags == HTBL_FL_ALIGN_CENTER)
					for (j = 0; j < sz / 2; j++)
						if (!HBUF_PUTSL(rowtmp, " "))
							goto out;
				if (!hbuf_putb(rowtmp, celltmp))
					goto out;
				if (flags == 0 ||
				    flags == HTBL_FL_ALIGN_LEFT)
					for (j = 0; j < sz; j++)
						if (!HBUF_PUTSL(rowtmp, " "))
							goto out;
				if (flags == HTBL_FL_ALIGN_CENTER) {
					sz = (sz % 2) ? 
						(sz / 2) + 1 : (sz / 2);
					for (j = 0; j < sz; j++)
						if (!HBUF_PUTSL(rowtmp, " "))
							goto out;
				}

				st->last_blank = last_blank;
				if (TAILQ_NEXT(cell, entries) != NULL &&
				    !HBUF_PUTSL(rowtmp, " | "))
					goto out;
			}

			/* 
			 * Some magic here.
			 * First, emulate rndr() by setting the
			 * stackpos to the table, which is required for
			 * checking the line start.
			 * Then directly print, as we've already escaped
			 * all characters, and have embedded escapes of
			 * our own.  Then end the line.
			 */

			if (!hbuf_putb(ob, rowtmp))
				goto out;
			st->last_blank = 0;
			if (!rndr_buf_vspace(st, ob, 1))
				goto out;
		}

		if (top->type == LOWDOWN_TABLE_HEADER) {
			for (i = 0; i < n->rndr_table.columns; i++) {
				for (j = 0; j <= widths[i]; j++)
					if (!HBUF_PUTSL(ob, "-"))
						goto out;
				if (i < n->rndr_table.columns - 1 &&
				    !HBUF_PUTSL(ob, "|-"))
					goto out;
			}
			st->last_blank = 0;
			if (!rndr_buf_vspace(st, ob, 1))
				goto out;
		}
	}

	rc = 1;
out:
	hbuf_free(celltmp);
	hbuf_free(rowtmp);
	free(widths);
	st->flags = oflags;
	return rc;
}

/*
 * Return zero on failure (memory), non-zero on success.
 */
static int
rndr(struct lowdown_buf *ob, struct lowdown_metaq *mq,
	struct gemini *st, const struct lowdown_node *n)
{
	const struct lowdown_node	*child, *prev;
	struct link			*l;
	void				*pp;
	struct lowdown_buf		*tmpbuf;
	size_t				 i;
	ssize_t				 level;
	int32_t				 entity;
	int				 rc;
	
	prev = n->parent == NULL ? NULL :
		TAILQ_PREV(n, lowdown_nodeq, entries);
	
	/* Vertical space before content. */

	switch (n->type) {
	case LOWDOWN_ROOT:
		st->last_blank = -1;
		break;
	case LOWDOWN_BLOCKCODE:
	case LOWDOWN_BLOCKHTML:
	case LOWDOWN_BLOCKQUOTE:
	case LOWDOWN_DEFINITION:
	case LOWDOWN_HEADER:
	case LOWDOWN_LIST:
	case LOWDOWN_PARAGRAPH:
	case LOWDOWN_TABLE_BLOCK:
		/*
		 * Blocks in a definition list get special treatment
		 * because we only put one newline between the title and
		 * the data regardless of its contents.
		 */

		if (n->parent != NULL && 
		    n->parent->type == LOWDOWN_LISTITEM &&
		    n->parent->parent != NULL &&
		    n->parent->parent->type == 
		      LOWDOWN_DEFINITION_DATA &&
		    prev == NULL) {
			if (!rndr_buf_vspace(st, ob, 1))
				return 0;
		} else {
			if (!rndr_buf_vspace(st, ob, 2))
				return 0;
		}
		break;
	case LOWDOWN_MATH_BLOCK:
		if (n->rndr_math.blockmode &&
		    !rndr_buf_vspace(st, ob, 1))
			return 0;
		break;
	case LOWDOWN_DEFINITION_DATA:
		/* 
		 * Vertical space if previous block-mode data. 
		 */

		if (n->parent != NULL &&
		    n->parent->type == LOWDOWN_DEFINITION &&
		    (n->parent->rndr_definition.flags &
		     HLIST_FL_BLOCK) &&
		    prev != NULL &&
		    prev->type == LOWDOWN_DEFINITION_DATA) {
			if (!rndr_buf_vspace(st, ob, 2))
				return 0;
		} else {
			if (!rndr_buf_vspace(st, ob, 1))
				return 0;
		}
		break;
	case LOWDOWN_DEFINITION_TITLE:
	case LOWDOWN_HRULE:
	case LOWDOWN_LINEBREAK:
	case LOWDOWN_LISTITEM:
	case LOWDOWN_META:
	case LOWDOWN_TABLE_ROW:
		if (!rndr_buf_vspace(st, ob, 1))
			return 0;
		break;
	case LOWDOWN_IMAGE:
	case LOWDOWN_LINK:
	case LOWDOWN_LINK_AUTO:
		if ((st->flags & LOWDOWN_GEMINI_LINK_IN) &&
		    !rndr_buf_vspace(st, ob, 1))
			return 0;
		break;
	default:
		break;
	}

	/* Output leading content. */

	hbuf_truncate(st->tmp);

	switch (n->type) {
	case LOWDOWN_TABLE_BLOCK:
	case LOWDOWN_BLOCKCODE:
	case LOWDOWN_BLOCKHTML:
		if (!HBUF_PUTSL(st->tmp, "```"))
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		if (!rndr_buf_vspace(st, ob, 1))
			return 0;
		break;
	case LOWDOWN_BLOCKQUOTE:
		if (!HBUF_PUTSL(st->tmp, "> "))
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		st->last_blank = -1;
		break;
	case LOWDOWN_HEADER:
		level = (ssize_t)n->rndr_header.level +
			st->headers_offs;
		if (level < 1)
			level = 1;
		for (i = 0; i < (size_t)level; i++)
			if (!HBUF_PUTSL(st->tmp, "#"))
				return 0;
		if (!HBUF_PUTSL(st->tmp, " "))
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		st->last_blank = -1;
		break;
	case LOWDOWN_IMAGE:
	case LOWDOWN_LINK:
	case LOWDOWN_LINK_AUTO:
		if (!(IS_STANDALONE_LINK(n, prev) ||
		     (st->flags & LOWDOWN_GEMINI_LINK_IN)))
			break;
		if (!HBUF_PUTSL(st->tmp, "=> "))
			return 0;
		rc = 1;
		if (n->type == LOWDOWN_LINK_AUTO)
			rc = hbuf_putb(st->tmp, &n->rndr_autolink.link);
		else if (n->type == LOWDOWN_LINK)
			rc = hbuf_putb(st->tmp, &n->rndr_link.link);
		else if (n->type == LOWDOWN_IMAGE)
			rc = hbuf_putb(st->tmp, &n->rndr_image.link);
		if (!rc)
			return 0;
		if (!HBUF_PUTSL(st->tmp, " "))
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		st->last_blank = -1;
		break;
	case LOWDOWN_LISTITEM:
		rc = 1;
		if (n->rndr_listitem.flags & HLIST_FL_DEF)
			rc = HBUF_PUTSL(st->tmp, ": ");
		else if (n->rndr_listitem.flags & HLIST_FL_CHECKED)
			rc = HBUF_PUTSL(st->tmp, "☑ ");
		else if (n->rndr_listitem.flags & HLIST_FL_UNCHECKED)
			rc = HBUF_PUTSL(st->tmp, "☐ ");
		else if (n->rndr_listitem.flags & HLIST_FL_UNORDERED)
			rc = HBUF_PUTSL(st->tmp, "* ");
		else
			rc = hbuf_printf(st->tmp, "%zu. ", 
				n->rndr_listitem.num);
		if (!rc)
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		st->last_blank = -1;
		break;
	case LOWDOWN_SUPERSCRIPT:
		if (!HBUF_PUTSL(st->tmp, "^"))
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		break;
	default:
		break;
	}

	/* Descend into children. */

	switch (n->type) {
	case LOWDOWN_TABLE_BLOCK:
		if (!rndr_table(ob, st, n))
			return 0;
		break;
	case LOWDOWN_META:
		if (n->chng != LOWDOWN_CHNG_DELETE &&
		    !rndr_meta(st, n, mq))
			return 0;
		break;
	case LOWDOWN_FOOTNOTE:
		if ((tmpbuf = hbuf_new(32)) == NULL)
			return 0;
		if (!hbuf_printf(tmpbuf, "[%zu] ", st->footsz + 1))
			return 0;
		st->last_blank = -1;
		st->nolinkflush = 1;
		TAILQ_FOREACH(child, &n->children, entries)
			if (!rndr(tmpbuf, mq, st, child))
				return 0;
		st->nolinkflush = 0;
		pp = reallocarray(st->foots,
			st->footsz + 1,
			sizeof(struct lowdown_buf *));
		if (pp == NULL)
			return 0;
		st->foots = pp;
		st->foots[st->footsz++] = tmpbuf;
		break;
	default:
		TAILQ_FOREACH(child, &n->children, entries)
			if (!rndr(ob, mq, st, child))
				return 0;
		break;
	}

	/* Output non-child or trailing content. */

	hbuf_truncate(st->tmp);

	switch (n->type) {
	case LOWDOWN_HRULE:
		if (!HBUF_PUTSL(st->tmp, "~~~~~~~~"))
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		break;
	case LOWDOWN_FOOTNOTE:
		if (!hbuf_printf(st->tmp, "[%zu]", st->footsz))
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		break;
	case LOWDOWN_RAW_HTML:
		if (!rndr_buf(st, ob, n, &n->rndr_raw_html.text))
			return 0;
		break;
	case LOWDOWN_MATH_BLOCK:
		if (!rndr_buf(st, ob, n, &n->rndr_math.text))
			return 0;
		break;
	case LOWDOWN_ENTITY:
		entity = entity_find_iso(&n->rndr_entity.text);
		if (entity > 0) {
			if (!rndr_entity(st->tmp, entity))
				return 0;
			if (!rndr_buf(st, ob, n, st->tmp))
				return 0;
		} else {
			if (!rndr_buf(st, ob, n, &n->rndr_entity.text))
				return 0;
		}
		break;
	case LOWDOWN_BLOCKCODE:
		if (!rndr_buf(st, ob, n, &n->rndr_blockcode.text))
			return 0;
		break;
	case LOWDOWN_BLOCKHTML:
		if (!rndr_buf(st, ob, n, &n->rndr_blockhtml.text))
			return 0;
		break;
	case LOWDOWN_CODESPAN:
		if (!rndr_buf(st, ob, n, &n->rndr_codespan.text))
			return 0;
		break;
	case LOWDOWN_IMAGE:
		if (!rndr_buf(st, ob, n, &n->rndr_image.alt))
			return 0;
		/* FALLTHROUGH */
	case LOWDOWN_LINK:
	case LOWDOWN_LINK_AUTO:
		if (IS_STANDALONE_LINK(n, prev) ||
		    (st->flags & LOWDOWN_GEMINI_LINK_IN))
			break;
		if (st->nolinkqsz == 0) {
			if ((l = calloc(1, sizeof(struct link))) == NULL)
				return 0;
			l->n = n;
			l->id = ++st->linkqsz;
			TAILQ_INSERT_TAIL(&st->linkq, l, entries);
			i = l->id;
		} else
			i = st->nolinkqsz++;
		if (!rndr_link_ref(st, st->tmp, i, 0))
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		break;
	case LOWDOWN_NORMAL_TEXT:
		if (!rndr_buf(st, ob, n, &n->rndr_normal_text.text))
			return 0;
		break;
	case LOWDOWN_ROOT:
		if (!TAILQ_EMPTY(&st->linkq) &&
		    (st->flags & LOWDOWN_GEMINI_LINK_END)) {
			if (!rndr_buf_vspace(st, ob, 2))
				return 0;
			if (!rndr_flush_linkq(st, ob))
				return 0;
		}
		if (st->footsz == 0)
			break;
		if (!HBUF_PUTSL(ob, "~~~~~~~~\n\n"))
			return 0;
		for (i = 0; i < st->footsz; i++) {
			if (!hbuf_putb(ob, st->foots[i]))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		break;
	case LOWDOWN_DOC_HEADER:
		if (!rndr_doc_header(st, ob, mq))
			return 0;
		break;
	default:
		break;
	}

	/* Trailing block spaces. */

	hbuf_truncate(st->tmp);

	switch (n->type) {
	case LOWDOWN_TABLE_BLOCK:
	case LOWDOWN_BLOCKCODE:
	case LOWDOWN_BLOCKHTML:
		if (!HBUF_PUTSL(st->tmp, "```"))
			return 0;
		if (!rndr_buf(st, ob, n, st->tmp))
			return 0;
		st->last_blank = 0;
		if (!rndr_buf_vspace(st, ob, 2))
			return 0;
		break;
	case LOWDOWN_DOC_HEADER:
		if ((st->flags & LOWDOWN_STANDALONE) &&
		    !rndr_buf_vspace(st, ob, 2))
			return 0;
		break;
	case LOWDOWN_BLOCKQUOTE:
	case LOWDOWN_DEFINITION:
	case LOWDOWN_HEADER:
	case LOWDOWN_LIST:
	case LOWDOWN_PARAGRAPH:
		if (!rndr_buf_vspace(st, ob, 2))
			return 0;
		break;
	case LOWDOWN_MATH_BLOCK:
		if (n->rndr_math.blockmode &&
		    !rndr_buf_vspace(st, ob, 1))
			return 0;
		break;
	case LOWDOWN_DEFINITION_DATA:
	case LOWDOWN_DEFINITION_TITLE:
	case LOWDOWN_HRULE:
	case LOWDOWN_LISTITEM:
	case LOWDOWN_META:
	case LOWDOWN_TABLE_ROW:
		if (!rndr_buf_vspace(st, ob, 1))
			return 0;
		break;
	case LOWDOWN_IMAGE:
	case LOWDOWN_LINK:
	case LOWDOWN_LINK_AUTO:
		if (IS_STANDALONE_LINK(n, prev) ||
		    (st->flags & LOWDOWN_GEMINI_LINK_IN))
			if (!rndr_buf_vspace(st, ob, 1))
				return 0;
		break;
	case LOWDOWN_ROOT:
		/*
		 * Special case: snip any trailing newlines that may
		 * have been printed as trailing vertical space.
		 * This tidies up the output.
		 */

		if (!rndr_buf_vspace(st, ob, 1))
			return 0;
		while (ob->size && ob->data[ob->size - 1] == '\n')
			ob->size--;
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		break;
	default:
		break;
	}

	if (!st->nolinkflush &&
	    st->nolinkqsz == 0 && st->last_blank > 1 && 
	    !TAILQ_EMPTY(&st->linkq) && 
	    !(st->flags & LOWDOWN_GEMINI_LINK_END)) {
		if (!rndr_flush_linkq(st, ob))
			return 0;
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		st->last_blank = 2;
	}

	return 1;
}

int
lowdown_gemini_rndr(struct lowdown_buf *ob,
	void *arg, const struct lowdown_node *n)
{
	struct gemini		*st = arg;
	int			 rc;
	size_t			 i;
	struct lowdown_metaq	 metaq;

	TAILQ_INIT(&metaq);
	st->last_blank = 0;
	st->headers_offs = 1;

	rc = rndr(ob, &metaq, st, n);

	link_freeq(&st->linkq);
	st->linkqsz = 0;
	st->nolinkqsz = 0;

	for (i = 0; i < st->footsz; i++)
		hbuf_free(st->foots[i]);

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

void *
lowdown_gemini_new(const struct lowdown_opts *opts)
{
	struct gemini	*p;

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

	TAILQ_INIT(&p->linkq);
	p->flags = opts != NULL ? opts->oflags : 0;

	/* Only use one kind of flag output. */

	if ((p->flags & LOWDOWN_GEMINI_LINK_IN) &&
	    (p->flags & LOWDOWN_GEMINI_LINK_END))
		p->flags &= ~LOWDOWN_GEMINI_LINK_IN;

	if ((p->tmp = hbuf_new(32)) == NULL) {
		free(p);
		return NULL;
	}

	return p;
}

void
lowdown_gemini_free(void *arg)
{
	struct gemini	*p = arg;
	
	if (p == NULL)
		return;

	hbuf_free(p->tmp);
	free(p->buf);
	free(p);
}