Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
560 lines
17 KiB
C
560 lines
17 KiB
C
#include "render_ansi.h"
|
|
#include "ansi_style.h"
|
|
#include "block_scan.h"
|
|
#include "table_stats.h"
|
|
#include "fsm.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
typedef struct {
|
|
uint8_t bold;
|
|
uint8_t italic;
|
|
uint8_t code;
|
|
uint8_t latex;
|
|
uint8_t strike;
|
|
} ansi_inline_t;
|
|
|
|
typedef struct {
|
|
uint8_t ul;
|
|
uint8_t ol;
|
|
uint8_t bq;
|
|
uint8_t para;
|
|
int ol_num;
|
|
} ansi_env_t;
|
|
|
|
static void ansi_s(sashidown_ctx_t *ctx, const char *s)
|
|
{
|
|
if (s && s[0])
|
|
fsm_append_str(ctx, s);
|
|
}
|
|
|
|
static void ansi_close_lists(ansi_env_t *env, sashidown_ctx_t *ctx)
|
|
{
|
|
(void)ctx;
|
|
env->ul = 0;
|
|
env->ol = 0;
|
|
env->ol_num = 0;
|
|
}
|
|
|
|
static void ansi_close_para(ansi_env_t *env, sashidown_ctx_t *ctx)
|
|
{
|
|
if (env->para) {
|
|
fsm_append(ctx, '\n');
|
|
env->para = 0;
|
|
}
|
|
}
|
|
|
|
static void ansi_close_all(ansi_env_t *env, sashidown_ctx_t *ctx)
|
|
{
|
|
ansi_close_lists(env, ctx);
|
|
ansi_close_para(env, ctx);
|
|
env->bq = 0;
|
|
}
|
|
|
|
static int ansi_render_link(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
|
const char *input, size_t len,
|
|
size_t *pos, int is_image)
|
|
{
|
|
size_t i = *pos + 1;
|
|
if (is_image) {
|
|
if (i >= len || input[i] != '[')
|
|
return 0;
|
|
i++;
|
|
}
|
|
size_t text_start = i;
|
|
i = block_scan_until_byte(input, len, i, ']');
|
|
if (i >= len || input[i] != ']') return 0;
|
|
size_t text_end = i;
|
|
i++;
|
|
if (i >= len || input[i] != '(') return 0;
|
|
i++;
|
|
size_t url_start = i;
|
|
i = block_scan_until_byte(input, len, i, ')');
|
|
if (i >= len) return 0;
|
|
|
|
if (is_image) {
|
|
ansi_s(ctx, th->image_on);
|
|
fsm_append_lit(ctx, "[img:");
|
|
fsm_append_len(ctx, input + text_start, text_end - text_start);
|
|
fsm_append_lit(ctx, "](");
|
|
ansi_s(ctx, th->url_on);
|
|
fsm_append_len(ctx, input + url_start, i - url_start);
|
|
ansi_s(ctx, th->url_off);
|
|
fsm_append(ctx, ')');
|
|
ansi_s(ctx, th->image_off);
|
|
} else {
|
|
ansi_s(ctx, th->link_on);
|
|
fsm_append_len(ctx, input + text_start, text_end - text_start);
|
|
ansi_s(ctx, th->link_off);
|
|
fsm_append_lit(ctx, " (");
|
|
ansi_s(ctx, th->url_on);
|
|
fsm_append_len(ctx, input + url_start, i - url_start);
|
|
ansi_s(ctx, th->url_off);
|
|
fsm_append(ctx, ')');
|
|
}
|
|
*pos = i + 1;
|
|
return 1;
|
|
}
|
|
|
|
static void ansi_render_inline(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
|
const char *input, size_t len,
|
|
size_t start, size_t end)
|
|
{
|
|
ansi_inline_t st = {0, 0, 0, 0, 0};
|
|
size_t i = start;
|
|
|
|
while (i < end) {
|
|
if (st.latex) {
|
|
size_t j = block_scan_until_byte(input, end, i, '$');
|
|
if (j > i)
|
|
fsm_append_len(ctx, input + i, j - i);
|
|
if (j < end) {
|
|
fsm_append(ctx, '$');
|
|
ansi_s(ctx, th->latex_off);
|
|
st.latex = 0;
|
|
i = j + 1;
|
|
} else {
|
|
i = j;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (st.code) {
|
|
size_t j = block_scan_until_byte(input, end, i, '`');
|
|
if (j > i)
|
|
fsm_append_len(ctx, input + i, j - i);
|
|
if (j < end) {
|
|
ansi_s(ctx, th->code_off);
|
|
st.code = 0;
|
|
i = j + 1;
|
|
} else {
|
|
i = j;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
unsigned char ch = (unsigned char)input[i];
|
|
|
|
if ((ch & 0x80) == 0) {
|
|
size_t j = block_scan_plain_ascii(input, end, i);
|
|
if (j > i) {
|
|
fsm_append_len(ctx, input + i, j - i);
|
|
i = j;
|
|
if (i >= end)
|
|
break;
|
|
ch = (unsigned char)input[i];
|
|
}
|
|
} else {
|
|
size_t j = block_scan_utf8_run(input, end, i);
|
|
if (j > i) {
|
|
fsm_append_len(ctx, input + i, j - i);
|
|
i = j;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (ch == '$' && i + 1 < end && input[i + 1] == '$') {
|
|
size_t j = block_scan_until_2byte(input, end, i + 2, '$', '$');
|
|
if (j < end)
|
|
j += 2;
|
|
ansi_s(ctx, th->latex_on);
|
|
fsm_append_len(ctx, input + i, j - i);
|
|
ansi_s(ctx, th->latex_off);
|
|
i = j;
|
|
continue;
|
|
}
|
|
|
|
if (ch == '$') {
|
|
ansi_s(ctx, th->latex_on);
|
|
fsm_append(ctx, '$');
|
|
st.latex = 1;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (ch == '!' && i + 1 < end && input[i + 1] == '[') {
|
|
if (ansi_render_link(ctx, th, input, len, &i, 1))
|
|
continue;
|
|
}
|
|
|
|
if (ch == '[') {
|
|
if (ansi_render_link(ctx, th, input, len, &i, 0))
|
|
continue;
|
|
}
|
|
|
|
if (ch == '\\' && i + 1 < end) {
|
|
unsigned char next = (unsigned char)input[i + 1];
|
|
if ((next >= 0x21 && next <= 0x2F) ||
|
|
(next >= 0x3A && next <= 0x40) ||
|
|
(next >= 0x5B && next <= 0x60) ||
|
|
(next >= 0x7B && next <= 0x7E)) {
|
|
fsm_append(ctx, (char)next);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ((ch == '*' || ch == '_') && i + 1 < end && input[i + 1] == (char)ch) {
|
|
ansi_s(ctx, st.bold ? th->strong_off : th->strong_on);
|
|
st.bold = !st.bold;
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
if (ch == '~' && i + 1 < end && input[i + 1] == '~') {
|
|
ansi_s(ctx, st.strike ? th->strike_off : th->strike_on);
|
|
st.strike = !st.strike;
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
if (ch == '*' || ch == '_') {
|
|
ansi_s(ctx, st.italic ? th->emph_off : th->emph_on);
|
|
st.italic = !st.italic;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (ch == '`') {
|
|
ansi_s(ctx, th->code_on);
|
|
st.code = 1;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
fsm_append(ctx, (char)ch);
|
|
i++;
|
|
}
|
|
|
|
if (st.bold) ansi_s(ctx, th->strong_off);
|
|
if (st.italic) ansi_s(ctx, th->emph_off);
|
|
if (st.code) ansi_s(ctx, th->code_off);
|
|
if (st.strike) ansi_s(ctx, th->strike_off);
|
|
if (st.latex) ansi_s(ctx, th->latex_off);
|
|
}
|
|
|
|
static void trim_cell(const char *input, size_t *start, size_t *end)
|
|
{
|
|
while (*start < *end && input[*start] == ' ')
|
|
(*start)++;
|
|
while (*end > *start && input[*end - 1] == ' ')
|
|
(*end)--;
|
|
}
|
|
|
|
static void ansi_table_row_at(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
|
const char *input, size_t len,
|
|
size_t pos, size_t le, int cols, int is_header)
|
|
{
|
|
size_t i = pos;
|
|
while (i < le && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
if (i < le && input[i] == '|')
|
|
i++;
|
|
|
|
for (int col = 0; col < cols; col++) {
|
|
size_t cs = i;
|
|
while (i < le && input[i] != '|') i++;
|
|
size_t ce = i;
|
|
trim_cell(input, &cs, &ce);
|
|
if (col)
|
|
ansi_s(ctx, th->sep);
|
|
if (is_header)
|
|
ansi_s(ctx, th->th_on);
|
|
ansi_render_inline(ctx, th, input, len, cs, ce);
|
|
if (is_header)
|
|
ansi_s(ctx, th->th_off);
|
|
if (i < le && input[i] == '|') i++;
|
|
}
|
|
fsm_append(ctx, '\n');
|
|
}
|
|
|
|
/* Scheme C (batch): header+sep commit, then body rows one-pass. Returns 1 if
|
|
* a table was consumed (*pos_io advanced past body). */
|
|
static int ansi_try_consume_table(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
|
ansi_env_t *env, const char *input, size_t len,
|
|
size_t pos, size_t le, size_t *pos_io)
|
|
{
|
|
int has_pipe = 0;
|
|
int cols = table_count_cols(input + pos, le - pos, &has_pipe);
|
|
if (!has_pipe)
|
|
return 0;
|
|
if (le >= len || input[le] != '\n')
|
|
return 0;
|
|
|
|
size_t next = le + 1;
|
|
if (next >= len)
|
|
return 0;
|
|
size_t nle = block_line_end(ctx, input, len, next);
|
|
if (!table_is_separator_row(input + next, nle - next)) {
|
|
block_line_seek(ctx, pos);
|
|
return 0;
|
|
}
|
|
|
|
ansi_close_all(env, ctx);
|
|
ansi_table_row_at(ctx, th, input, len, pos, le, cols, 1);
|
|
pos = block_skip_line(ctx, input, len, next);
|
|
|
|
while (pos < len) {
|
|
size_t ble = block_line_end(ctx, input, len, pos);
|
|
int empty = (pos == ble || (pos + 1 == ble && input[pos] == '\r'));
|
|
if (empty)
|
|
break;
|
|
size_t blen = ble - pos;
|
|
if (!memchr(input + pos, '|', blen) ||
|
|
table_is_separator_row(input + pos, blen))
|
|
break;
|
|
ansi_table_row_at(ctx, th, input, len, pos, ble, cols, 0);
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
}
|
|
fsm_append(ctx, '\n');
|
|
*pos_io = pos;
|
|
return 1;
|
|
}
|
|
|
|
static void ansi_render_codeblock(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
|
const char *input, size_t len,
|
|
size_t *pos, int fence_len, char fence_char)
|
|
{
|
|
*pos = block_skip_line(ctx, input, len, *pos);
|
|
ansi_s(ctx, th->fence_on);
|
|
while (*pos < len) {
|
|
int fl = 0;
|
|
char fc = 0;
|
|
if (block_is_fence(input, len, *pos, &fl, &fc) &&
|
|
fc == fence_char && fl >= fence_len) {
|
|
size_t is, ie;
|
|
block_fence_info_range(input, len, *pos, fl, fc, &is, &ie);
|
|
if (ie <= is) {
|
|
*pos = block_skip_line(ctx, input, len, *pos);
|
|
break;
|
|
}
|
|
}
|
|
size_t le = block_line_end(ctx, input, len, *pos);
|
|
fsm_append_lit(ctx, " ");
|
|
fsm_append_len(ctx, input + *pos, le - *pos);
|
|
fsm_append(ctx, '\n');
|
|
*pos = block_skip_line(ctx, input, len, *pos);
|
|
}
|
|
ansi_s(ctx, th->fence_off);
|
|
fsm_append(ctx, '\n');
|
|
}
|
|
|
|
static void ansi_render_latex_block(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
|
const char *input, size_t len,
|
|
size_t *pos)
|
|
{
|
|
size_t i = *pos + 2;
|
|
size_t start = i;
|
|
while (i + 1 < len) {
|
|
if (input[i] == '$' && input[i + 1] == '$') {
|
|
ansi_s(ctx, th->latex_on);
|
|
fsm_append_lit(ctx, "$$");
|
|
if (i > start)
|
|
fsm_append_len(ctx, input + start, i - start);
|
|
fsm_append_lit(ctx, "$$");
|
|
ansi_s(ctx, th->latex_off);
|
|
fsm_append(ctx, '\n');
|
|
i += 2;
|
|
if (i < len && input[i] == '\n') i++;
|
|
*pos = i;
|
|
return;
|
|
}
|
|
i++;
|
|
}
|
|
ansi_s(ctx, th->latex_on);
|
|
fsm_append_lit(ctx, "$$");
|
|
if (i > start)
|
|
fsm_append_len(ctx, input + start, (i < len ? i : len) - start);
|
|
fsm_append_lit(ctx, "$$");
|
|
ansi_s(ctx, th->latex_off);
|
|
fsm_append(ctx, '\n');
|
|
*pos = len;
|
|
}
|
|
|
|
static int sashidown_render_ansi_theme(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
const ansi_theme_t *th)
|
|
{
|
|
ansi_env_t env;
|
|
memset(&env, 0, sizeof(env));
|
|
size_t pos = 0;
|
|
|
|
fsm_write_begin(ctx);
|
|
|
|
while (pos < len) {
|
|
size_t le = block_line_end(ctx, input, len, pos);
|
|
if (pos == le || (pos + 1 == le && input[pos] == '\r')) {
|
|
ansi_close_all(&env, ctx);
|
|
if (env.bq)
|
|
env.bq = 0;
|
|
fsm_append(ctx, '\n');
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
continue;
|
|
}
|
|
|
|
if (memchr(input + pos, '|', le - pos) != NULL) {
|
|
size_t newt = 0;
|
|
if (ansi_try_consume_table(ctx, th, &env, input, len, pos, le, &newt)) {
|
|
pos = newt;
|
|
block_line_seek(ctx, pos);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
line_info_t li = block_classify_line(ctx, input, len, pos, le);
|
|
|
|
if (li.kind == LK_PARA) {
|
|
line_info_t sli;
|
|
size_t send = 0;
|
|
int sr = block_try_setext(ctx, input, len, pos, le, len, 1, &sli, &send);
|
|
if (sr > 0) {
|
|
int lvl = sli.heading_level;
|
|
if (lvl < 1) lvl = 1;
|
|
if (lvl > 6) lvl = 6;
|
|
ansi_close_all(&env, ctx);
|
|
ansi_s(ctx, th->heading[lvl]);
|
|
ansi_render_inline(ctx, th, input, len, sli.content_start, sli.content_end);
|
|
ansi_s(ctx, th->heading_end);
|
|
fsm_append(ctx, '\n');
|
|
if (lvl <= 2)
|
|
fsm_append(ctx, '\n');
|
|
pos = send;
|
|
block_line_seek(ctx, pos);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
switch (li.kind) {
|
|
case LK_EMPTY:
|
|
ansi_close_all(&env, ctx);
|
|
if (env.bq)
|
|
env.bq = 0;
|
|
fsm_append(ctx, '\n');
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_HR:
|
|
ansi_close_all(&env, ctx);
|
|
fsm_append_lit(ctx, "---\n\n");
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_FENCE:
|
|
ansi_close_all(&env, ctx);
|
|
ansi_render_codeblock(ctx, th, input, len, &pos, li.fence_len, li.fence_char);
|
|
break;
|
|
|
|
case LK_LATEX_LINE:
|
|
ansi_close_all(&env, ctx);
|
|
ansi_s(ctx, th->latex_on);
|
|
fsm_append_lit(ctx, "$$");
|
|
fsm_append_len(ctx, input + li.content_start,
|
|
li.content_end - li.content_start);
|
|
fsm_append_lit(ctx, "$$");
|
|
ansi_s(ctx, th->latex_off);
|
|
fsm_append(ctx, '\n');
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_LATEX_BLOCK:
|
|
ansi_close_all(&env, ctx);
|
|
ansi_render_latex_block(ctx, th, input, len, &pos);
|
|
break;
|
|
|
|
case LK_HEADING: {
|
|
int lvl = li.heading_level;
|
|
if (lvl < 1) lvl = 1;
|
|
if (lvl > 6) lvl = 6;
|
|
ansi_close_all(&env, ctx);
|
|
ansi_s(ctx, th->heading[lvl]);
|
|
ansi_render_inline(ctx, th, input, len, li.content_start, le);
|
|
ansi_s(ctx, th->heading_end);
|
|
fsm_append(ctx, '\n');
|
|
if (lvl <= 2)
|
|
fsm_append(ctx, '\n');
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
}
|
|
|
|
case LK_BQ:
|
|
ansi_close_lists(&env, ctx);
|
|
env.bq = 1;
|
|
ansi_s(ctx, th->bq_prefix);
|
|
ansi_render_inline(ctx, th, input, len, li.content_start, le);
|
|
fsm_append(ctx, '\n');
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_UL:
|
|
if (env.ol) { env.ol = 0; env.ol_num = 0; }
|
|
env.ul = 1;
|
|
ansi_s(ctx, th->bullet);
|
|
if (li.task == 1)
|
|
fsm_append_lit(ctx, "[ ] ");
|
|
else if (li.task == 2)
|
|
fsm_append_lit(ctx, "[x] ");
|
|
ansi_render_inline(ctx, th, input, len, li.content_start, le);
|
|
ansi_s(ctx, th->bullet_end);
|
|
fsm_append(ctx, '\n');
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_OL: {
|
|
int num = 0;
|
|
size_t cs = li.content_start;
|
|
block_is_ol(input, len, pos, &cs, &num);
|
|
if (env.ul) env.ul = 0;
|
|
env.ol = 1;
|
|
char buf[32];
|
|
int n = snprintf(buf, sizeof(buf), "%d. ", num > 0 ? num : ++env.ol_num);
|
|
ansi_s(ctx, th->ol_on);
|
|
if (n > 0)
|
|
fsm_append_len(ctx, buf, (size_t)n);
|
|
ansi_s(ctx, th->ol_off);
|
|
ansi_render_inline(ctx, th, input, len, li.content_start, le);
|
|
fsm_append(ctx, '\n');
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
}
|
|
|
|
case LK_PARA:
|
|
default:
|
|
if (env.bq) env.bq = 0;
|
|
ansi_close_lists(&env, ctx);
|
|
if (!env.para) {
|
|
env.para = 1;
|
|
} else {
|
|
fsm_append(ctx, '\n');
|
|
}
|
|
ansi_render_inline(ctx, th, input, len, pos, le);
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
if (pos >= len) {
|
|
ansi_close_para(&env, ctx);
|
|
} else {
|
|
size_t nle = block_line_end(ctx, input, len, pos);
|
|
line_info_t nli = block_classify_line(ctx, input, len, pos, nle);
|
|
if (nli.kind != LK_PARA)
|
|
ansi_close_para(&env, ctx);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (ctx->overflow) {
|
|
fsm_write_end(ctx);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
ansi_close_all(&env, ctx);
|
|
fsm_write_end(ctx);
|
|
return ctx->overflow ? -1 : 0;
|
|
}
|
|
|
|
int sashidown_render_ansi(sashidown_ctx_t *ctx, const char *input, size_t len)
|
|
{
|
|
return sashidown_render_ansi_theme(ctx, input, len, ansi_theme_color());
|
|
}
|
|
|
|
int sashidown_render_ansi_plain(sashidown_ctx_t *ctx, const char *input, size_t len)
|
|
{
|
|
return sashidown_render_ansi_theme(ctx, input, len, ansi_theme_plain());
|
|
}
|