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