Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
/* $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);
}