term.c (36712B)
/* $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); }