Skip to content
Permalink
master
Switch branches/tags
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"
struct tstack {
const struct lowdown_node *n; /* node in question */
size_t lines; /* times emitted */
};
struct term {
unsigned int opts; /* oflags from lowdown_cfg */
size_t col; /* output column from zero */
ssize_t last_blank; /* line breaks or -1 (start) */
struct tstack *stack; /* stack of nodes */
size_t stackmax; /* size of stack */
size_t stackpos; /* position in stack */
size_t maxcol; /* soft limit */
size_t hmargin; /* left of content */
size_t vmargin; /* before/after content */
struct lowdown_buf *tmp; /* for temporary allocations */
wchar_t *buf; /* buffer for counting wchar */
size_t bufsz; /* size of buf */
struct lowdown_buf **foots; /* footnotes */
size_t footsz; /* footnotes size */
int footoff; /* don't collect (tables) */
};
/*
* How to style the output on the screen.
*/
struct sty {
int italic; /* italic */
int strike; /* strikethrough */
int bold; /* bold */
int under; /* underline */
size_t bcolour; /* not inherited */
size_t colour; /* not inherited */
int override; /* don't inherit... */
#define OSTY_UNDER 0x01 /* underlining */
#define OSTY_BOLD 0x02 /* bold */
};
/*
* Prefixes to put before each line. This only applies to very specific
* circumstances.
*/
struct pfx {
const char *text;
size_t cols;
};
#include "term.h"
static const struct sty *stys[LOWDOWN__MAX] = {
NULL, /* LOWDOWN_ROOT */
&sty_blockcode, /* LOWDOWN_BLOCKCODE */
NULL, /* LOWDOWN_BLOCKQUOTE */
NULL, /* LOWDOWN_DEFINITION */
NULL, /* LOWDOWN_DEFINITION_TITLE */
NULL, /* LOWDOWN_DEFINITION_DATA */
&sty_header, /* LOWDOWN_HEADER */
&sty_hrule, /* LOWDOWN_HRULE */
NULL, /* LOWDOWN_LIST */
NULL, /* LOWDOWN_LISTITEM */
NULL, /* LOWDOWN_PARAGRAPH */
NULL, /* LOWDOWN_TABLE_BLOCK */
NULL, /* LOWDOWN_TABLE_HEADER */
NULL, /* LOWDOWN_TABLE_BODY */
NULL, /* LOWDOWN_TABLE_ROW */
NULL, /* LOWDOWN_TABLE_CELL */
&sty_blockhtml, /* LOWDOWN_BLOCKHTML */
&sty_autolink, /* LOWDOWN_LINK_AUTO */
&sty_codespan, /* LOWDOWN_CODESPAN */
&sty_d_emph, /* LOWDOWN_DOUBLE_EMPHASIS */
&sty_emph, /* LOWDOWN_EMPHASIS */
&sty_highlight, /* LOWDOWN_HIGHLIGHT */
&sty_img, /* LOWDOWN_IMAGE */
NULL, /* LOWDOWN_LINEBREAK */
&sty_link, /* LOWDOWN_LINK */
&sty_t_emph, /* LOWDOWN_TRIPLE_EMPHASIS */
&sty_strike, /* LOWDOWN_STRIKETHROUGH */
NULL, /* LOWDOWN_SUPERSCRIPT */
NULL, /* LOWDOWN_FOOTNOTE */
NULL, /* LOWDOWN_MATH_BLOCK */
&sty_rawhtml, /* LOWDOWN_RAW_HTML */
NULL, /* LOWDOWN_ENTITY */
NULL, /* LOWDOWN_NORMAL_TEXT */
NULL, /* LOWDOWN_DOC_HEADER */
NULL, /* LOWDOWN_META */
};
/*
* Whether the style is not empty (i.e., has style attributes).
*/
#define STY_NONEMPTY(_s) \
((_s)->colour || (_s)->bold || (_s)->italic || \
(_s)->under || (_s)->strike || (_s)->bcolour || \
(_s)->override)
/* Forward declaration. */
static int
rndr(struct lowdown_buf *, struct term *, const struct lowdown_node *);
/*
* Get the column width of a multi-byte sequence. The sequence should
* be self-contained, i.e., not straddle multi-byte borders, because the
* calculation for UTF-8 columns is local to this function: a split
* multi-byte sequence will fail to return the correct number of
* printable columns. If the sequence is bad, return the number of raw
* bytes to print. Return <0 on failure (memory), >=0 otherwise with
* the number of printable columns.
*/
static ssize_t
rndr_mbswidth(struct term *term, const char *buf, size_t sz)
{
size_t wsz, csz;
const char *cp;
void *pp;
mbstate_t mbs;
memset(&mbs, 0, sizeof(mbstate_t));
cp = buf;
wsz = mbsnrtowcs(NULL, &cp, sz, 0, &mbs);
if (wsz == (size_t)-1)
return sz;
if (term->bufsz < wsz) {
term->bufsz = wsz;
pp = reallocarray(term->buf, wsz, sizeof(wchar_t));
if (pp == NULL)
return -1;
term->buf = pp;
}
memset(&mbs, 0, sizeof(mbstate_t));
cp = buf;
mbsnrtowcs(term->buf, &cp, sz, wsz, &mbs);
csz = wcswidth(term->buf, wsz);
return csz == (size_t)-1 ? sz : csz;
}
/*
* Copy the buffer into "out", escaping along the width.
* Returns the number of actual printed columns, which in the case of
* multi-byte glyphs, may be less than the given bytes.
* Return <0 on failure (memory), >= 0 otherwise.
*/
static ssize_t
rndr_escape(struct term *term, struct lowdown_buf *out,
const char *buf, size_t sz)
{
size_t i, start = 0, cols = 0;
ssize_t ret;
/* Don't allow control characters through. */
for (i = 0; i < sz; i++)
if (iscntrl((unsigned char)buf[i])) {
ret = rndr_mbswidth
(term, buf + start, i - start);
if (ret < 0)
return -1;
cols += ret;
if (!hbuf_put(out, buf + start, i - start))
return -1;
start = i + 1;
}
/* Remaining bytes. */
if (start < sz) {
ret = rndr_mbswidth(term, buf + start, sz - start);
if (ret < 0)
return -1;
cols += ret;
if (!hbuf_put(out, buf + start, sz - start))
return -1;
}
return cols;
}
static void
rndr_free_footnotes(struct term *st)
{
size_t i;
for (i = 0; i < st->footsz; i++)
hbuf_free(st->foots[i]);
free(st->foots);
st->foots = NULL;
st->footsz = 0;
st->footoff = 0;
}
/*
* If there's an active style in "s" or s is NULL), then emit an
* unstyling escape sequence. Return zero on failure (memory), non-zero
* on success.
*/
static int
rndr_buf_unstyle(const struct term *term,
struct lowdown_buf *out, const struct sty *s)
{
if (term->opts & LOWDOWN_TERM_NOANSI)
return 1;
if (s != NULL && !STY_NONEMPTY(s))
return 1;
return HBUF_PUTSL(out, "\033[0m");
}
/*
* Output style "s" into "out" as an ANSI escape. If "s" does not have
* any style information or is NULL, output nothing. Return zero on
* failure (memory), non-zero on success.
*/
static int
rndr_buf_style(const struct term *term,
struct lowdown_buf *out, const struct sty *s)
{
int has = 0;
if (term->opts & LOWDOWN_TERM_NOANSI)
return 1;
if (s == NULL || !STY_NONEMPTY(s))
return 1;
if (!HBUF_PUTSL(out, "\033["))
return 0;
if (s->bold) {
if (!HBUF_PUTSL(out, "1"))
return 0;
has++;
}
if (s->under) {
if (has++ && !HBUF_PUTSL(out, ";"))
return 0;
if (!HBUF_PUTSL(out, "4"))
return 0;
}
if (s->italic) {
if (has++ && !HBUF_PUTSL(out, ";"))
return 0;
if (!HBUF_PUTSL(out, "3"))
return 0;
}
if (s->strike) {
if (has++ && !HBUF_PUTSL(out, ";"))
return 0;
if (!HBUF_PUTSL(out, "9"))
return 0;
}
if (s->bcolour && !(term->opts & LOWDOWN_TERM_NOCOLOUR) &&
((s->bcolour >= 40 && s->bcolour <= 47) ||
(s->bcolour >= 100 && s->bcolour <= 107))) {
if (has++ && !HBUF_PUTSL(out, ";"))
return 0;
if (!hbuf_printf(out, "%zu", s->bcolour))
return 0;
}
if (s->colour && !(term->opts & LOWDOWN_TERM_NOCOLOUR) &&
((s->colour >= 30 && s->colour <= 37) ||
(s->colour >= 90 && s->colour <= 97))) {
if (has++ && !HBUF_PUTSL(out, ";"))
return 0;
if (!hbuf_printf(out, "%zu", s->colour))
return 0;
}
return HBUF_PUTSL(out, "m");
}
/*
* Take the given style "from" and apply it to "to".
* This accumulates styles: unless an override has been set, it adds to
* the existing style in "to" instead of overriding it.
* The one exception is TODO colours, which override each other.
*/
static void
rndr_node_style_apply(struct sty *to, const struct sty *from)
{
if (from->italic)
to->italic = 1;
if (from->strike)
to->strike = 1;
if (from->bold)
to->bold = 1;
else if ((from->override & OSTY_BOLD))
to->bold = 0;
if (from->under)
to->under = 1;
else if ((from->override & OSTY_UNDER))
to->under = 0;
if (from->bcolour)
to->bcolour = from->bcolour;
if (from->colour)
to->colour = from->colour;
}
/*
* Apply the style for only the given node to the current style.
* This *augments* the current style: see rndr_node_style_apply().
* (This does not ascend to the parent node.)
*/
static void
rndr_node_style(struct sty *s, const struct lowdown_node *n)
{
/* The basic node itself. */
if (stys[n->type] != NULL)
rndr_node_style_apply(s, stys[n->type]);
/* Any special node situation that overrides. */
switch (n->type) {
case LOWDOWN_HEADER:
if (n->rndr_header.level > 0)
rndr_node_style_apply(s, &sty_header_n);
else
rndr_node_style_apply(s, &sty_header_1);
break;
default:
/* FIXME: crawl up nested? */
if (n->parent != NULL &&
n->parent->type == LOWDOWN_LINK)
rndr_node_style_apply(s, &sty_linkalt);
break;
}
if (n->chng == LOWDOWN_CHNG_INSERT)
rndr_node_style_apply(s, &sty_chng_ins);
if (n->chng == LOWDOWN_CHNG_DELETE)
rndr_node_style_apply(s, &sty_chng_del);
}
/*
* Bookkeep that we've put "len" characters into the current line.
*/
static void
rndr_buf_advance(struct term *term, size_t len)
{
term->col += len;
if (term->col && term->last_blank != 0)
term->last_blank = 0;
}
/*
* Return non-zero if "n" or any of its ancestors require resetting the
* output line mode, otherwise return zero.
* This applies to both block and inline styles.
*/
static int
rndr_buf_endstyle(const struct lowdown_node *n)
{
struct sty s;
if (n->parent != NULL)
if (rndr_buf_endstyle(n->parent))
return 1;
memset(&s, 0, sizeof(struct sty));
rndr_node_style(&s, n);
return STY_NONEMPTY(&s);
}
/*
* Unsets the current style context given "n" and an optional terminal
* style "osty", if applies. Return zero on failure (memory), non-zero
* on success.
*/
static int
rndr_buf_endwords(struct term *term, struct lowdown_buf *out,
const struct lowdown_node *n, const struct sty *osty)
{
if (rndr_buf_endstyle(n))
return rndr_buf_unstyle(term, out, NULL);
if (osty != NULL)
return rndr_buf_unstyle(term, out, osty);
return 1;
}
/*
* Like rndr_buf_endwords(), but also terminating the line itself.
* Return zero on failure (memory), non-zero on success.
*/
static int
rndr_buf_endline(struct term *term, struct lowdown_buf *out,
const struct lowdown_node *n, const struct sty *osty)
{
if (!rndr_buf_endwords(term, out, n, osty))
return 0;
/*
* We can legit be at col == 0 if, for example, we're in a
* literal context with a blank line.
* assert(term->col > 0);
* assert(term->last_blank == 0);
*/
term->col = 0;
term->last_blank = 1;
return HBUF_PUTSL(out, "\n");
}
/*
* Return the printed width of the number up to six digits (we're
* probably not going to have more list items than that).
*/
static size_t
rndr_numlen(size_t sz)
{
if (sz > 100000)
return 6;
if (sz > 10000)
return 5;
if (sz > 1000)
return 4;
if (sz > 100)
return 3;
if (sz > 10)
return 2;
return 1;
}
/*
* Output prefixes of the given node in the style further accumulated
* from the parent nodes. "Depth" is set to how deep we are, starting
* at -1 (the root).
* Return zero on failure (memory), non-zero on success.
*/
static int
rndr_buf_startline_prefixes(struct term *term,
struct sty *s, const struct lowdown_node *n,
struct lowdown_buf *out, size_t *depth)
{
struct sty sinner;
const struct pfx *pfx;
size_t i, emit, len;
int pstyle = 0;
enum hlist_fl fl;
if (n->parent != NULL &&
!rndr_buf_startline_prefixes(term, s, n->parent, out, depth))
return 0;
if (n->parent == NULL) {
assert(n->type == LOWDOWN_ROOT);
*depth = -1;
}
/*
* The "sinner" value is temporary for only this function.
* This allows us to set a temporary style mask that only
* applies to the prefix data.
* Otherwise "s" propogates to the subsequent line.
*/
rndr_node_style(s, n);
sinner = *s;
/*
* Look up the current node in the list of node's we're
* servicing so we can get how many times we've output the
* prefix. This is used for (e.g.) lists, where we only output
* the list prefix once. XXX: read backwards for faster perf?
*/
for (i = 0; i <= term->stackpos; i++)
if (term->stack[i].n == n)
break;
/*
* If we can't find the node, then we're in a "faked" context
* like footnotes within a table. Ignore this. XXX: is there a
* non-hacky way for this?
*/
if (i > term->stackpos)
return 1;
emit = term->stack[i].lines++;
/*
* If we're below the document root and not a header, that means
* we're in a body part. Emit the general body indentation.
*/
if (*depth == 0 && n->type != LOWDOWN_HEADER) {
if (!hbuf_puts(out, pfx_body.text))
return 0;
rndr_buf_advance(term, pfx_body.cols);
} else if (*depth == 0) {
if (!hbuf_puts(out, pfx_header.text))
return 0;
rndr_buf_advance(term, pfx_header.cols);
}
/*
* Output any prefixes.
* Any output must have rndr_buf_style() and set pstyle so that
* we close out the style afterward.
*/
switch (n->type) {
case LOWDOWN_BLOCKCODE:
rndr_node_style_apply(&sinner, &sty_bkcd_pfx);
if (!rndr_buf_style(term, out, &sinner))
return 0;
pstyle = 1;
if (!hbuf_puts(out, pfx_bkcd.text))
return 0;
rndr_buf_advance(term, pfx_bkcd.cols);
break;
case LOWDOWN_ROOT:
if (!rndr_buf_style(term, out, &sinner))
return 0;
pstyle = 1;
for (i = 0; i < term->hmargin; i++)
if (!HBUF_PUTSL(out, " "))
return 0;
break;
case LOWDOWN_BLOCKQUOTE:
rndr_node_style_apply(&sinner, &sty_bkqt_pfx);
if (!rndr_buf_style(term, out, &sinner))
return 0;
pstyle = 1;
if (!hbuf_puts(out, pfx_bkqt.text))
return 0;
rndr_buf_advance(term, pfx_bkqt.cols);
break;
case LOWDOWN_DEFINITION_DATA:
rndr_node_style_apply(&sinner, &sty_dli_pfx);
if (!rndr_buf_style(term, out, &sinner))
return 0;
pstyle = 1;
if (emit == 0) {
if (!hbuf_puts(out, pfx_dli_1.text))
return 0;
rndr_buf_advance(term, pfx_dli_1.cols);
} else {
if (!hbuf_puts(out, pfx_dli_n.text))
return 0;
rndr_buf_advance(term, pfx_dli_n.cols);
}
break;
case LOWDOWN_FOOTNOTE:
rndr_node_style_apply(&sinner, &sty_fdef_pfx);
if (!rndr_buf_style(term, out, &sinner))
return 0;
pstyle = 1;
if (emit == 0) {
if (!hbuf_printf(out, "%2zu. ",
term->footsz + 1))
return 0;
len = rndr_numlen(term->footsz + 1);
if (len + 2 > pfx_fdef_1.cols)
len += 2;
else
len = pfx_fdef_1.cols;
rndr_buf_advance(term, len);
} else {
if (!hbuf_puts(out, pfx_fdef_n.text))
return 0;
rndr_buf_advance(term, pfx_fdef_n.cols);
}
break;
case LOWDOWN_HEADER:
if (n->rndr_header.level == 0)
pfx = &pfx_header_1;
else
pfx = &pfx_header_n;
if (!rndr_buf_style(term, out, &sinner))
return 0;
pstyle = 1;
for (i = 0; i < n->rndr_header.level + 1; i++) {
if (!hbuf_puts(out, pfx->text))
return 0;
rndr_buf_advance(term, pfx->cols);
}
if (pfx->cols) {
if (!HBUF_PUTSL(out, " "))
return 0;
rndr_buf_advance(term, 1);
}
break;
case LOWDOWN_LISTITEM:
if (n->parent == NULL ||
n->parent->type == LOWDOWN_DEFINITION_DATA)
break;
/* Don't print list item prefix after first. */
if (emit) {
if (!hbuf_puts(out, pfx_li_n.text))
return 0;
rndr_buf_advance(term, pfx_li_n.cols);
break;
}
/* List item prefix depends upon type. */
fl = n->rndr_list.flags;
rndr_node_style_apply(&sinner, &sty_li_pfx);
if (!rndr_buf_style(term, out, &sinner))
return 0;
pstyle = 1;
if (fl & HLIST_FL_CHECKED)
pfx = &pfx_uli_c1;
else if (fl & HLIST_FL_UNCHECKED)
pfx = &pfx_uli_nc1;
else if (fl & HLIST_FL_UNORDERED)
pfx = &pfx_uli_1;
else
pfx = &pfx_oli_1;
if (pfx == &pfx_oli_1) {
if (!hbuf_printf(out, "%2zu. ",
n->rndr_listitem.num))
return 0;
len = rndr_numlen(n->rndr_listitem.num);
if (len + 2 > pfx->cols)
len += 2;
else
len = pfx->cols;
} else {
if (pfx->text != NULL &&
!hbuf_puts(out, pfx->text))
return 0;
len = pfx->cols;
}
rndr_buf_advance(term, len);
break;
default:
break;
}
if (pstyle && !rndr_buf_unstyle(term, out, &sinner))
return 0;
(*depth)++;
return 1;
}
/*
* Like rndr_buf_startwords(), but at the start of a line.
* This also outputs all line prefixes of the block context.
* Return zero on failure (memory), non-zero on success.
*/
static int
rndr_buf_startline(struct term *term, struct lowdown_buf *out,
const struct lowdown_node *n, const struct sty *osty)
{
struct sty s;
size_t depth = 0;
assert(term->last_blank);
assert(term->col == 0);
memset(&s, 0, sizeof(struct sty));
if (!rndr_buf_startline_prefixes(term, &s, n, out, &depth))
return 0;
if (osty != NULL)
rndr_node_style_apply(&s, osty);
return rndr_buf_style(term, out, &s);
}
/*
* Output optional number of newlines before or after content.
* Return zero on failure, non-zero on success.
*/
static int
rndr_buf_vspace(struct term *term, struct lowdown_buf *out,
const struct lowdown_node *n, size_t sz)
{
const struct lowdown_node *prev;
if (term->last_blank == -1)
return 1;
prev = n->parent == NULL ? NULL :
TAILQ_PREV(n, lowdown_nodeq, entries);
assert(sz > 0);
while ((size_t)term->last_blank < sz) {
if (term->col || prev == NULL) {
if (!HBUF_PUTSL(out, "\n"))
return 0;
} else {
if (!rndr_buf_startline
(term, out, n->parent, NULL))
return 0;
if (!rndr_buf_endline
(term, out, n->parent, NULL))
return 0;
}
term->last_blank++;
term->col = 0;
}
return 1;
}
/*
* Ascend to the root of the parse tree from rndr_buf_startwords(),
* accumulating styles as we do so.
*/
static void
rndr_buf_startwords_style(const struct lowdown_node *n, struct sty *s)
{
if (n->parent != NULL)
rndr_buf_startwords_style(n->parent, s);
rndr_node_style(s, n);
}
/*
* Accumulate and output the style at the start of one or more words.
* Should *not* be called on the start of a new line, which calls for
* rndr_buf_startline().
* Return zero on failure, non-zero on success.
*/
static int
rndr_buf_startwords(struct term *term, struct lowdown_buf *out,
const struct lowdown_node *n, const struct sty *osty)
{
struct sty s;
assert(!term->last_blank);
assert(term->col > 0);
memset(&s, 0, sizeof(struct sty));
rndr_buf_startwords_style(n, &s);
if (osty != NULL)
rndr_node_style_apply(&s, osty);
return rndr_buf_style(term, out, &s);
}
/*
* Return zero on failure, non-zero on success.
*/
static int
rndr_buf_literal(struct term *term, struct lowdown_buf *out,
const struct lowdown_node *n, const struct lowdown_buf *in,
const struct sty *osty)
{
size_t i = 0, len;
const char *start;
while (i < in->size) {
start = &in->data[i];
while (i < in->size && in->data[i] != '\n')
i++;
len = &in->data[i] - start;
i++;
if (!rndr_buf_startline(term, out, n, osty))
return 0;
/*
* No need to record the column width here because we're
* going to reset to zero anyway.
*/
if (rndr_escape(term, out, start, len) < 0)
return 0;
rndr_buf_advance(term, len);
if (!rndr_buf_endline(term, out, n, osty))
return 0;
}
return 1;
}
/*
* Emit text in "in" the current line with output "out".
* Use "n" and its ancestry to determine our context.
* Return zero on failure, non-zero on success.
*/
static int
rndr_buf(struct term *term, struct lowdown_buf *out,
const struct lowdown_node *n, const struct lowdown_buf *in,
const struct sty *osty)
{
size_t i = 0, len, cols;
ssize_t ret;
int needspace, begin = 1, end = 0;
const char *start;
const struct lowdown_node *nn;
for (nn = n; nn != NULL; nn = nn->parent)
if (nn->type == LOWDOWN_BLOCKCODE ||
nn->type == LOWDOWN_BLOCKHTML)
return rndr_buf_literal(term, out, n, in, osty);
/* Start each word by seeing if it has leading space. */
while (i < in->size) {
needspace = isspace((unsigned char)in->data[i]);
while (i < in->size &&
isspace((unsigned char)in->data[i]))
i++;
/* See how long it the coming word (may be 0). */
start = &in->data[i];
while (i < in->size &&
!isspace((unsigned char)in->data[i]))
i++;
len = &in->data[i] - start;
/*
* If we cross our maximum width and are preceded by a
* space, then break.
* (Leaving out the check for a space will cause
* adjacent text or punctuation to have a preceding
* newline.)
* This will also unset the current style.
*/
if ((needspace ||
(out->size && isspace
((unsigned char)out->data[out->size - 1]))) &&
term->col && term->col + len > term->maxcol) {
if (!rndr_buf_endline(term, out, n, osty))
return 0;
end = 0;
}
/*
* Either emit our new line prefix (only if we have a
* word that will follow!) or, if we need space, emit
* the spacing. In the first case, or if we have
* following text and are starting this node, emit our
* current style.
*/
if (term->last_blank && len) {
if (!rndr_buf_startline(term, out, n, osty))
return 0;
begin = 0;
end = 1;
} else if (!term->last_blank) {
if (begin && len) {
if (!rndr_buf_startwords
(term, out, n, osty))
return 0;
begin = 0;
end = 1;
}
if (needspace) {
if (!HBUF_PUTSL(out, " "))
return 0;
rndr_buf_advance(term, 1);
}
}
/* Emit the word itself. */
if ((ret = rndr_escape(term, out, start, len)) < 0)
return 0;
cols = ret;
rndr_buf_advance(term, cols);
}
if (end) {
assert(begin == 0);
if (!rndr_buf_endwords(term, out, n, osty))
return 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;
}
/*
* Adjust the stack of current nodes we're looking at.
*/
static int
rndr_stackpos_init(struct term *p, const struct lowdown_node *n)
{
void *pp;
if (p->stackpos >= p->stackmax) {
p->stackmax += 256;
pp = reallocarray(p->stack,
p->stackmax, sizeof(struct tstack));
if (pp == NULL)
return 0;
p->stack = pp;
}
memset(&p->stack[p->stackpos], 0, sizeof(struct tstack));
p->stack[p->stackpos].n = n;
return 1;
}
/*
* Return zero on failure (memory), non-zero on success.
*/
static int
rndr_table(struct lowdown_buf *ob, struct term *p,
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 col, i, j, maxcol, sz, footsz;
ssize_t last_blank;
unsigned int flags;
int rc = 0;
assert(n->type == LOWDOWN_TABLE_BLOCK);
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. We don't want to collect additional
* footnotes, as we're going to do so in the next iteration, and
* keep the current size (which will otherwise advance).
*/
assert(!p->footoff);
p->footoff = 1;
footsz = p->footsz;
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);
/*
* Simulate that we're starting within
* the line by unsetting last_blank,
* having a non-zero column, and an
* infinite maximum column to prevent
* line wrapping.
*/
maxcol = p->maxcol;
last_blank = p->last_blank;
col = p->col;
p->last_blank = 0;
p->maxcol = SIZE_MAX;
p->col = 1;
if (!rndr(celltmp, p, cell))
goto out;
if (widths[i] < p->col)
widths[i] = p->col;
p->last_blank = last_blank;
p->col = col;
p->maxcol = maxcol;
}
}
/* Restore footnotes. */
p->footsz = footsz;
assert(p->footoff);
p->footoff = 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);
maxcol = p->maxcol;
last_blank = p->last_blank;
col = p->col;
p->last_blank = 0;
p->maxcol = SIZE_MAX;
p->col = 1;
if (!rndr(celltmp, p, cell))
goto out;
assert(widths[i] >= p->col);
sz = widths[i] - p->col;
/*
* 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;
}
p->last_blank = last_blank;
p->col = col;
p->maxcol = maxcol;
if (TAILQ_NEXT(cell, entries) == NULL)
continue;
if (!rndr_buf_style(p, rowtmp, &sty_table) ||
!hbuf_printf(rowtmp, " %s ", ifx_table_col) ||
!rndr_buf_unstyle(p, rowtmp, &sty_table))
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.
*/
p->stackpos++;
if (!rndr_stackpos_init(p, n))
goto out;
if (!rndr_buf_startline(p, ob, n, NULL))
goto out;
if (!hbuf_putb(ob, rowtmp))
goto out;
rndr_buf_advance(p, 1);
if (!rndr_buf_endline(p, ob, n, NULL))
goto out;
if (!rndr_buf_vspace(p, ob, n, 1))
goto out;
p->stackpos--;
}
if (top->type == LOWDOWN_TABLE_HEADER) {
p->stackpos++;
if (!rndr_stackpos_init(p, n))
goto out;
if (!rndr_buf_startline(p, ob, n, &sty_table))
goto out;
for (i = 0; i < n->rndr_table.columns; i++) {
for (j = 0; j < widths[i]; j++)
if (!hbuf_puts(ob, ifx_table_row))
goto out;
if (i < n->rndr_table.columns - 1 &&
!hbuf_printf(ob, "%s%s",
ifx_table_col, ifx_table_row))
goto out;
}
rndr_buf_advance(p, 1);
if (!rndr_buf_endline(p, ob, n, &sty_table))
goto out;
if (!rndr_buf_vspace(p, ob, n, 1))
goto out;
p->stackpos--;
}
}
rc = 1;
out:
hbuf_free(celltmp);
hbuf_free(rowtmp);
free(widths);
return rc;
}
static int
rndr(struct lowdown_buf *ob, struct term *p,
const struct lowdown_node *n)
{
const struct lowdown_node *child, *nn;
struct lowdown_buf *metatmp;
void *pp;
int32_t entity;
size_t i, col, vs;
ssize_t last_blank;
/* Current nodes we're servicing. */
if (!rndr_stackpos_init(p, n))
return 0;
/*
* Vertical space before content. Vertical space (>1 space) is
* suppressed for normal blocks when in a non-block list, as the
* list item handles any spacing. Furthermore, definition list
* data also has its spaces suppressed because this is relegated
* to the title. The root gets the vertical margin as well.
*/
vs = 0;
switch (n->type) {
case LOWDOWN_ROOT:
for (i = 0; i < p->vmargin; i++)
if (!HBUF_PUTSL(ob, "\n"))
return 0;
p->last_blank = -1;
break;
case LOWDOWN_BLOCKCODE:
case LOWDOWN_BLOCKHTML:
case LOWDOWN_BLOCKQUOTE:
case LOWDOWN_DEFINITION:
case LOWDOWN_DEFINITION_TITLE:
case LOWDOWN_HEADER:
case LOWDOWN_LIST:
case LOWDOWN_TABLE_BLOCK:
case LOWDOWN_PARAGRAPH:
vs = 2;
for (nn = n->parent; nn != NULL; nn = nn->parent) {
if (nn->type != LOWDOWN_LISTITEM)
continue;
vs = (nn->rndr_listitem.flags & HLIST_FL_BLOCK) ? 2 : 1;
break;
}
break;
case LOWDOWN_MATH_BLOCK:
vs = n->rndr_math.blockmode ? 1 : 0;
break;
case LOWDOWN_DEFINITION_DATA:
case LOWDOWN_HRULE:
case LOWDOWN_LINEBREAK:
case LOWDOWN_META:
vs = 1;
break;
case LOWDOWN_LISTITEM:
vs = 1;
if (n->rndr_listitem.flags & HLIST_FL_BLOCK) {
for (nn = n->parent; nn != NULL; nn = nn->parent)
if (nn->type == LOWDOWN_LISTITEM ||
nn->type == LOWDOWN_DEFINITION_DATA)
break;
vs = nn == NULL ? 2 : 1;
}
break;
default:
break;
}
if (vs > 0 && !rndr_buf_vspace(p, ob, n, vs))
return 0;
/* Output leading content. */
switch (n->type) {
case LOWDOWN_SUPERSCRIPT:
hbuf_truncate(p->tmp);
if (!hbuf_puts(p->tmp, ifx_super) ||
!rndr_buf(p, ob, n, p->tmp, NULL))
return 0;
break;
case LOWDOWN_META:
if (!rndr_buf(p, ob, n,
&n->rndr_meta.key, &sty_meta_key))
return 0;
hbuf_truncate(p->tmp);
if (!hbuf_puts(p->tmp, ifx_meta_key) ||
!rndr_buf(p, ob, n, p->tmp, &sty_meta_key))
return 0;
break;
default:
break;
}
/* Descend into children. */
switch (n->type) {
case LOWDOWN_FOOTNOTE:
if (p->footoff) {
p->footsz++;
break;
}
last_blank = p->last_blank;
p->last_blank = -1;
col = p->col;
p->col = 0;
if ((metatmp = hbuf_new(128)) == NULL)
return 0;
TAILQ_FOREACH(child, &n->children, entries) {
p->stackpos++;
if (!rndr(metatmp, p, child))
return 0;
p->stackpos--;
}
p->last_blank = last_blank;
p->col = col;
pp = recallocarray(p->foots, p->footsz,
p->footsz + 1, sizeof(struct lowdown_buf *));
if (pp == NULL)
return 0;
p->foots = pp;
p->foots[p->footsz++] = metatmp;
break;
case LOWDOWN_TABLE_BLOCK:
if (!rndr_table(ob, p, n))
return 0;
break;
default:
TAILQ_FOREACH(child, &n->children, entries) {
p->stackpos++;
if (!rndr(ob, p, child))
return 0;
p->stackpos--;
}
break;
}
/* Output content. */
switch (n->type) {
case LOWDOWN_HRULE:
hbuf_truncate(p->tmp);
if (!hbuf_puts(p->tmp, ifx_hrule))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, NULL))
return 0;
break;
case LOWDOWN_FOOTNOTE:
hbuf_truncate(p->tmp);
if (!hbuf_printf(p->tmp, "%s%zu%s", ifx_fref_left,
p->footsz, ifx_fref_right))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, &sty_fref))
return 0;
break;
case LOWDOWN_RAW_HTML:
if (!rndr_buf(p, ob, n, &n->rndr_raw_html.text, NULL))
return 0;
break;
case LOWDOWN_MATH_BLOCK:
if (!rndr_buf(p, ob, n, &n->rndr_math.text, NULL))
return 0;
break;
case LOWDOWN_ENTITY:
entity = entity_find_iso(&n->rndr_entity.text);
if (entity > 0) {
hbuf_truncate(p->tmp);
if (!rndr_entity(p->tmp, entity))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, NULL))
return 0;
} else {
if (!rndr_buf(p, ob, n,
&n->rndr_entity.text, &sty_bad_ent))
return 0;
}
break;
case LOWDOWN_BLOCKCODE:
if (!rndr_buf(p, ob, n, &n->rndr_blockcode.text, NULL))
return 0;
break;
case LOWDOWN_BLOCKHTML:
if (!rndr_buf(p, ob, n, &n->rndr_blockhtml.text, NULL))
return 0;
break;
case LOWDOWN_CODESPAN:
if (!rndr_buf(p, ob, n, &n->rndr_codespan.text, NULL))
return 0;
break;
case LOWDOWN_LINK_AUTO:
if (p->opts & LOWDOWN_TERM_SHORTLINK) {
hbuf_truncate(p->tmp);
if (!hbuf_shortlink
(p->tmp, &n->rndr_autolink.link))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, NULL))
return 0;
} else {
if (!rndr_buf(p, ob, n,
&n->rndr_autolink.link, NULL))
return 0;
}
break;
case LOWDOWN_LINK:
if (p->opts & LOWDOWN_TERM_NOLINK)
break;
hbuf_truncate(p->tmp);
if (!HBUF_PUTSL(p->tmp, " "))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, NULL))
return 0;
if (p->opts & LOWDOWN_TERM_SHORTLINK) {
hbuf_truncate(p->tmp);
if (!hbuf_shortlink
(p->tmp, &n->rndr_link.link))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, NULL))
return 0;
} else {
if (!rndr_buf(p, ob, n,
&n->rndr_link.link, NULL))
return 0;
}
break;
case LOWDOWN_IMAGE:
if (!rndr_buf(p, ob, n, &n->rndr_image.alt, NULL))
return 0;
if (n->rndr_image.alt.size) {
hbuf_truncate(p->tmp);
if (!HBUF_PUTSL(p->tmp, " "))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, NULL))
return 0;
}
if (p->opts & LOWDOWN_TERM_NOLINK) {
hbuf_truncate(p->tmp);
if (!hbuf_puts(p->tmp, ifx_imgbox_left))
return 0;
if (!hbuf_puts(p->tmp, ifx_imgbox_right))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, &sty_imgbox))
return 0;
break;
}
hbuf_truncate(p->tmp);
if (!hbuf_puts(p->tmp, ifx_imgbox_left))
return 0;
if (!hbuf_puts(p->tmp, ifx_imgbox_sep))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, &sty_imgbox))
return 0;
if (p->opts & LOWDOWN_TERM_SHORTLINK) {
hbuf_truncate(p->tmp);
if (!hbuf_shortlink
(p->tmp, &n->rndr_image.link))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, &sty_imgurl))
return 0;
} else
if (!rndr_buf(p, ob, n,
&n->rndr_image.link, &sty_imgurl))
return 0;
hbuf_truncate(p->tmp);
if (!hbuf_puts(p->tmp, ifx_imgbox_right))
return 0;
if (!rndr_buf(p, ob, n, p->tmp, &sty_imgbox))
return 0;
break;
case LOWDOWN_NORMAL_TEXT:
if (!rndr_buf(p, ob, n,
&n->rndr_normal_text.text, NULL))
return 0;
break;
default:
break;
}
/* Trailing block spaces. */
if (n->type == LOWDOWN_ROOT) {
if (p->footsz) {
if (!rndr_buf_vspace(p, ob, n, 2))
return 0;
hbuf_truncate(p->tmp);
if (!hbuf_puts(p->tmp, pfx_body.text))
return 0;
if (!hbuf_puts(p->tmp, ifx_foot))
return 0;
if (!rndr_buf_literal(p, ob, n, p->tmp, &sty_foot))
return 0;
if (!rndr_buf_vspace(p, ob, n, 2))
return 0;
for (i = 0; i < p->footsz; i++) {
if (!hbuf_putb(ob, p->foots[i]))
return 0;
if (!HBUF_PUTSL(ob, "\n"))
return 0;
}
}
if (!rndr_buf_vspace(p, ob, n, 1))
return 0;
while (ob->size && ob->data[ob->size - 1] == '\n')
ob->size--;
if (!HBUF_PUTSL(ob, "\n"))
return 0;
/* Strip breaks but for the vmargin. */
for (i = 0; i < p->vmargin; i++)
if (!HBUF_PUTSL(ob, "\n"))
return 0;
}
return 1;
}
int
lowdown_term_rndr(struct lowdown_buf *ob,
void *arg, const struct lowdown_node *n)
{
struct term *st = arg;
int rc;
st->stackpos = 0;
rc = rndr(ob, st, n);
rndr_free_footnotes(st);
return rc;
}
void *
lowdown_term_new(const struct lowdown_opts *opts)
{
struct term *p;
if ((p = calloc(1, sizeof(struct term))) == NULL)
return NULL;
/* Give us 80 columns by default. */
if (opts != NULL) {
p->maxcol = opts->cols == 0 ? 80 : opts->cols;
p->hmargin = opts->hmargin;
p->vmargin = opts->vmargin;
p->opts = opts->oflags;
} else
p->maxcol = 80;
if ((p->tmp = hbuf_new(32)) == NULL) {
free(p);
return NULL;
}
return p;
}
void
lowdown_term_free(void *arg)
{
struct term *p = arg;
if (p == NULL)
return;
hbuf_free(p->tmp);
free(p->buf);
free(p->stack);
free(p);
}