Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
1203 lines
40 KiB
C
1203 lines
40 KiB
C
#include "fsm.h"
|
|
#include "table_stats.h"
|
|
#include "line_index.h"
|
|
#include "block_scan.h"
|
|
#include "render.h"
|
|
#include <string.h>
|
|
#ifdef __SSE2__
|
|
#include <emmintrin.h>
|
|
#elif defined(__ARM_NEON) || defined(__aarch64__)
|
|
#include <arm_neon.h>
|
|
#endif
|
|
|
|
static void html_escape(sashidown_ctx_t *ctx, const char *s, size_t n)
|
|
{
|
|
size_t i = 0;
|
|
#ifdef __SSE2__
|
|
const __m128i m_amp = _mm_set1_epi8('&');
|
|
const __m128i m_lt = _mm_set1_epi8('<');
|
|
const __m128i m_gt = _mm_set1_epi8('>');
|
|
const __m128i m_quot = _mm_set1_epi8('"');
|
|
while (i + 16 <= n) {
|
|
__m128i v = _mm_loadu_si128((const __m128i *)(s + i));
|
|
__m128i hit = _mm_cmpeq_epi8(v, m_amp);
|
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_lt));
|
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_gt));
|
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_quot));
|
|
int mask = _mm_movemask_epi8(hit);
|
|
if (mask) {
|
|
size_t pos = (unsigned)__builtin_ctz(mask);
|
|
if (pos > 0)
|
|
fsm_append_len(ctx, s + i, pos);
|
|
i += pos;
|
|
break;
|
|
}
|
|
fsm_append_len(ctx, s + i, 16);
|
|
i += 16;
|
|
}
|
|
#elif defined(__ARM_NEON) || defined(__aarch64__)
|
|
const uint8x16_t m_amp = vdupq_n_u8('&');
|
|
const uint8x16_t m_lt = vdupq_n_u8('<');
|
|
const uint8x16_t m_gt = vdupq_n_u8('>');
|
|
const uint8x16_t m_quot = vdupq_n_u8('"');
|
|
while (i + 16 <= n) {
|
|
uint8x16_t v = vld1q_u8((const uint8_t *)(s + i));
|
|
uint8x16_t hit = vceqq_u8(v, m_amp);
|
|
hit = vorrq_u8(hit, vceqq_u8(v, m_lt));
|
|
hit = vorrq_u8(hit, vceqq_u8(v, m_gt));
|
|
hit = vorrq_u8(hit, vceqq_u8(v, m_quot));
|
|
if (vmaxvq_u8(hit)) {
|
|
uint8_t lanes[16];
|
|
vst1q_u8(lanes, hit);
|
|
size_t pos = 0;
|
|
while (pos < 16 && !lanes[pos]) pos++;
|
|
if (pos > 0)
|
|
fsm_append_len(ctx, s + i, pos);
|
|
i += pos;
|
|
break;
|
|
}
|
|
fsm_append_len(ctx, s + i, 16);
|
|
i += 16;
|
|
}
|
|
#endif
|
|
while (i < n) {
|
|
size_t j = i;
|
|
while (j < n) {
|
|
unsigned char c = (unsigned char)s[j];
|
|
if (unlikely(CH_HTML_ESCAPE[c]))
|
|
break;
|
|
j++;
|
|
}
|
|
if (j > i)
|
|
fsm_append_len(ctx, s + i, j - i);
|
|
if (j >= n)
|
|
break;
|
|
switch (s[j]) {
|
|
case '&': fsm_append_lit(ctx, "&"); break;
|
|
case '<': fsm_append_lit(ctx, "<"); break;
|
|
case '>': fsm_append_lit(ctx, ">"); break;
|
|
case '"': fsm_append_lit(ctx, """); break;
|
|
default: fsm_append(ctx, s[j]); break;
|
|
}
|
|
i = j + 1;
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
uint8_t bold;
|
|
uint8_t italic;
|
|
uint8_t code;
|
|
uint8_t latex;
|
|
uint8_t strike;
|
|
} inline_state_t;
|
|
|
|
static int render_inline_range(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
size_t start, size_t end, int escape_html,
|
|
inline_state_t *st, int flush);
|
|
|
|
static void inline_flush_open(sashidown_ctx_t *ctx, inline_state_t *st)
|
|
{
|
|
if (st->bold) { fsm_append_lit(ctx, "</strong>"); st->bold = 0; }
|
|
if (st->italic) { fsm_append_lit(ctx, "</em>"); st->italic = 0; }
|
|
if (st->code) { fsm_append_lit(ctx, "</code>"); st->code = 0; }
|
|
if (st->strike) { fsm_append_lit(ctx, "</del>"); st->strike = 0; }
|
|
}
|
|
|
|
static void inline_reset(inline_state_t *st)
|
|
{
|
|
st->bold = st->italic = st->code = st->latex = st->strike = 0;
|
|
}
|
|
|
|
static void inline_load_from_state(sashidown_render_state_t *rs, inline_state_t *st)
|
|
{
|
|
st->bold = rs->inline_bold;
|
|
st->italic = rs->inline_italic;
|
|
st->code = rs->inline_code;
|
|
st->latex = rs->inline_latex;
|
|
st->strike = rs->inline_strike;
|
|
}
|
|
|
|
static void inline_store_to_state(sashidown_render_state_t *rs, const inline_state_t *st)
|
|
{
|
|
rs->inline_bold = st->bold;
|
|
rs->inline_italic = st->italic;
|
|
rs->inline_code = st->code;
|
|
rs->inline_latex = st->latex;
|
|
rs->inline_strike = st->strike;
|
|
}
|
|
|
|
static void url_attr_escape(sashidown_ctx_t *ctx, const char *s, size_t n)
|
|
{
|
|
static const char HEX[] = "0123456789ABCDEF";
|
|
for (size_t i = 0; i < n; i++) {
|
|
unsigned char c = (unsigned char)s[i];
|
|
if (c >= 0x80) {
|
|
char hex[3] = {'%', HEX[c >> 4], HEX[c & 0xF]};
|
|
fsm_append_len(ctx, hex, 3);
|
|
} else if (unlikely(CH_HTML_ESCAPE[c])) {
|
|
switch (c) {
|
|
case '&': fsm_append_lit(ctx, "&"); break;
|
|
case '<': fsm_append_lit(ctx, "<"); break;
|
|
case '>': fsm_append_lit(ctx, ">"); break;
|
|
case '"': fsm_append_lit(ctx, """); break;
|
|
default: fsm_append(ctx, (char)c); break;
|
|
}
|
|
} else {
|
|
fsm_append(ctx, (char)c);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int render_autolink(sashidown_ctx_t *ctx, const char *input, size_t end,
|
|
size_t *pos)
|
|
{
|
|
size_t i = *pos;
|
|
if (i >= end || input[i] != '<')
|
|
return 0;
|
|
size_t start = i + 1;
|
|
if (start + 7 > end) /* http:// minimum-ish */
|
|
return 0;
|
|
int is_url = (start + 7 <= end && memcmp(input + start, "http://", 7) == 0) ||
|
|
(start + 8 <= end && memcmp(input + start, "https://", 8) == 0) ||
|
|
(start + 7 <= end && memcmp(input + start, "mailto:", 7) == 0) ||
|
|
(start + 6 <= end && memcmp(input + start, "ftp://", 6) == 0);
|
|
/* also allow bare <foo@bar> mailto style: contains @ and no spaces */
|
|
size_t j = start;
|
|
while (j < end && input[j] != '>' && input[j] != ' ' &&
|
|
input[j] != '\n' && input[j] != '\r')
|
|
j++;
|
|
if (j >= end || input[j] != '>' || j == start)
|
|
return 0;
|
|
if (!is_url) {
|
|
int has_at = 0;
|
|
for (size_t k = start; k < j; k++) {
|
|
if (input[k] == '@') has_at = 1;
|
|
if (input[k] == '<' ) return 0;
|
|
}
|
|
if (!has_at)
|
|
return 0;
|
|
}
|
|
fsm_append_lit(ctx, "<a href=\"");
|
|
url_attr_escape(ctx, input + start, j - start);
|
|
fsm_append_lit(ctx, "\">");
|
|
html_escape(ctx, input + start, j - start);
|
|
fsm_append_lit(ctx, "</a>");
|
|
*pos = j + 1;
|
|
return 1;
|
|
}
|
|
|
|
static int render_link(sashidown_ctx_t *ctx, 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++;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
|
|
size_t url_start = i;
|
|
size_t url_end;
|
|
if (i < len && input[i] == '<') {
|
|
url_start = ++i;
|
|
i = block_scan_until_byte(input, len, i, '>');
|
|
if (i >= len) return 0;
|
|
url_end = i;
|
|
i++;
|
|
} else {
|
|
while (i < len && input[i] != ')' && input[i] != ' ' &&
|
|
input[i] != '\t' && input[i] != '\n' && input[i] != '"')
|
|
i++;
|
|
url_end = i;
|
|
}
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
|
|
size_t title_start = 0, title_end = 0;
|
|
int has_title = 0;
|
|
if (i < len && input[i] == '"') {
|
|
title_start = ++i;
|
|
i = block_scan_until_byte(input, len, i, '"');
|
|
if (i >= len) return 0;
|
|
title_end = i;
|
|
has_title = 1;
|
|
i++;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
}
|
|
if (i >= len || input[i] != ')') return 0;
|
|
|
|
if (is_image) {
|
|
fsm_append_lit(ctx, "<img src=\"");
|
|
url_attr_escape(ctx, input + url_start, url_end - url_start);
|
|
fsm_append_lit(ctx, "\" alt=\"");
|
|
html_escape(ctx, input + text_start, text_end - text_start);
|
|
fsm_append(ctx, '"');
|
|
if (has_title) {
|
|
fsm_append_lit(ctx, " title=\"");
|
|
html_escape(ctx, input + title_start, title_end - title_start);
|
|
fsm_append(ctx, '"');
|
|
}
|
|
fsm_append_lit(ctx, " />");
|
|
} else {
|
|
fsm_append_lit(ctx, "<a href=\"");
|
|
url_attr_escape(ctx, input + url_start, url_end - url_start);
|
|
fsm_append(ctx, '"');
|
|
if (has_title) {
|
|
fsm_append_lit(ctx, " title=\"");
|
|
html_escape(ctx, input + title_start, title_end - title_start);
|
|
fsm_append(ctx, '"');
|
|
}
|
|
fsm_append_lit(ctx, ">");
|
|
{
|
|
inline_state_t link_st = {0, 0, 0, 0, 0};
|
|
render_inline_range(ctx, input, len, text_start, text_end, 1, &link_st, 1);
|
|
}
|
|
fsm_append_lit(ctx, "</a>");
|
|
}
|
|
*pos = i + 1;
|
|
return 1;
|
|
}
|
|
|
|
static int render_inline_range(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
size_t start, size_t end, int escape_html,
|
|
inline_state_t *st, int flush)
|
|
{
|
|
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, '$');
|
|
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) {
|
|
fsm_append_lit(ctx, "</code>");
|
|
st->code = 0;
|
|
i = j + 1;
|
|
} else {
|
|
i = j;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
unsigned char ch = (unsigned char)input[i];
|
|
|
|
if (likely((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;
|
|
}
|
|
}
|
|
|
|
{
|
|
static const void *dispatch[256] = {
|
|
['$'] = &&lbl_dollar,
|
|
['!'] = &&lbl_bang,
|
|
['['] = &&lbl_bracket,
|
|
['*'] = &&lbl_star,
|
|
['_'] = &&lbl_underscore,
|
|
['`'] = &&lbl_backtick,
|
|
['~'] = &&lbl_tilde,
|
|
['\\'] = &&lbl_escape,
|
|
['&'] = &&lbl_html,
|
|
['<'] = &&lbl_lt,
|
|
['>'] = &&lbl_html,
|
|
};
|
|
const void *target = dispatch[ch];
|
|
if (target) goto *target;
|
|
goto lbl_default;
|
|
|
|
lbl_dollar:
|
|
if (i + 1 < end && input[i + 1] == '$') {
|
|
size_t j = block_scan_until_2byte(input, end, i + 2, '$', '$');
|
|
if (j < end) j += 2;
|
|
fsm_append_len(ctx, input + i, j - i);
|
|
i = j;
|
|
continue;
|
|
}
|
|
fsm_append(ctx, '$');
|
|
st->latex = 1;
|
|
i++;
|
|
continue;
|
|
|
|
lbl_bang:
|
|
if (i + 1 < end && input[i + 1] == '[') {
|
|
if (render_link(ctx, input, len, &i, 1))
|
|
continue;
|
|
}
|
|
goto lbl_default;
|
|
|
|
lbl_bracket:
|
|
if (render_link(ctx, input, len, &i, 0))
|
|
continue;
|
|
goto lbl_default;
|
|
|
|
lbl_star:
|
|
if (i + 1 < end && input[i + 1] == '*') {
|
|
if (st->bold) {
|
|
fsm_append_lit(ctx, "</strong>");
|
|
st->bold = 0;
|
|
} else {
|
|
fsm_append_lit(ctx, "<strong>");
|
|
st->bold = 1;
|
|
}
|
|
i += 2;
|
|
continue;
|
|
}
|
|
/* Inside **…, a lone * is literal (e.g. CCF A*). */
|
|
if (st->bold) {
|
|
fsm_append(ctx, '*');
|
|
i++;
|
|
continue;
|
|
}
|
|
goto lbl_emph;
|
|
|
|
lbl_underscore:
|
|
if (i + 1 < end && input[i + 1] == '_') {
|
|
if (st->bold) {
|
|
fsm_append_lit(ctx, "</strong>");
|
|
st->bold = 0;
|
|
} else {
|
|
fsm_append_lit(ctx, "<strong>");
|
|
st->bold = 1;
|
|
}
|
|
i += 2;
|
|
continue;
|
|
}
|
|
goto lbl_emph;
|
|
|
|
lbl_emph:
|
|
if (st->italic) {
|
|
fsm_append_lit(ctx, "</em>");
|
|
st->italic = 0;
|
|
} else {
|
|
fsm_append_lit(ctx, "<em>");
|
|
st->italic = 1;
|
|
}
|
|
i++;
|
|
continue;
|
|
|
|
lbl_tilde:
|
|
if (i + 1 < end && input[i + 1] == '~') {
|
|
if (st->strike) {
|
|
fsm_append_lit(ctx, "</del>");
|
|
st->strike = 0;
|
|
} else {
|
|
fsm_append_lit(ctx, "<del>");
|
|
st->strike = 1;
|
|
}
|
|
i += 2;
|
|
continue;
|
|
}
|
|
goto lbl_default;
|
|
|
|
lbl_escape:
|
|
if (i + 1 < end) {
|
|
unsigned char next = (unsigned char)input[i + 1];
|
|
/* CommonMark: any ASCII punctuation can be escaped */
|
|
if ((next >= 0x21 && next <= 0x2F) ||
|
|
(next >= 0x3A && next <= 0x40) ||
|
|
(next >= 0x5B && next <= 0x60) ||
|
|
(next >= 0x7B && next <= 0x7E)) {
|
|
if (escape_html && CH_HTML_ESCAPE[next]) {
|
|
switch (next) {
|
|
case '&': fsm_append_lit(ctx, "&"); break;
|
|
case '<': fsm_append_lit(ctx, "<"); break;
|
|
case '>': fsm_append_lit(ctx, ">"); break;
|
|
case '"': fsm_append_lit(ctx, """); break;
|
|
default: fsm_append(ctx, (char)next); break;
|
|
}
|
|
} else {
|
|
fsm_append(ctx, (char)next);
|
|
}
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
fsm_append(ctx, '\\');
|
|
i++;
|
|
continue;
|
|
|
|
lbl_backtick:
|
|
fsm_append_lit(ctx, "<code>");
|
|
st->code = 1;
|
|
i++;
|
|
continue;
|
|
|
|
lbl_lt:
|
|
if (render_autolink(ctx, input, end, &i))
|
|
continue;
|
|
/* fallthrough to html escape */
|
|
lbl_html:
|
|
if (escape_html) {
|
|
switch (ch) {
|
|
case '&': fsm_append_lit(ctx, "&"); break;
|
|
case '<': fsm_append_lit(ctx, "<"); break;
|
|
case '>': fsm_append_lit(ctx, ">"); break;
|
|
default: fsm_append(ctx, (char)ch); break;
|
|
}
|
|
i++;
|
|
continue;
|
|
}
|
|
/* fallthrough */
|
|
|
|
lbl_default:
|
|
fsm_append(ctx, (char)ch);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (flush)
|
|
inline_flush_open(ctx, st);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void render_table_row_at(sashidown_ctx_t *ctx, const char *input,
|
|
size_t len, size_t pos, size_t le,
|
|
int cols, int is_header);
|
|
static void table_close(sashidown_render_state_t *state, sashidown_ctx_t *ctx);
|
|
static int render_table_body_continue(sashidown_ctx_t *ctx,
|
|
sashidown_render_state_t *state,
|
|
const char *input, size_t len,
|
|
size_t *pos_io, size_t stop, int is_final);
|
|
static int try_open_table(sashidown_ctx_t *ctx, sashidown_render_state_t *state,
|
|
block_env_t *env, const char *input, size_t len,
|
|
size_t pos, size_t le, size_t stop, int is_final,
|
|
size_t *pos_out);
|
|
static int render_codeblock(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
size_t *pos, const line_info_t *open_li,
|
|
size_t stop, int is_final,
|
|
sashidown_render_state_t *state);
|
|
static int render_latex_block_multiline(sashidown_ctx_t *ctx, const char *input,
|
|
size_t len, size_t *pos, size_t stop,
|
|
int is_final, sashidown_render_state_t *state);
|
|
|
|
static int block_line_is_blank(const char *input, size_t len, size_t pos, size_t le)
|
|
{
|
|
(void)len;
|
|
return pos == le || (pos + 1 == le && input[pos] == '\r');
|
|
}
|
|
|
|
static int block_peek_continues_list(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
size_t pos, size_t limit)
|
|
{
|
|
while (pos < limit) {
|
|
size_t le = block_line_end(ctx, input, len, pos);
|
|
if (block_line_is_blank(input, len, pos, le)) {
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
continue;
|
|
}
|
|
line_info_t nli = block_classify_line(ctx, input, len, pos, le);
|
|
return nli.kind == LK_UL || nli.kind == LK_OL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void block_close_lists(block_env_t *env, sashidown_ctx_t *ctx)
|
|
{
|
|
if (env->ul) { fsm_append_lit(ctx, "</ul>\n"); env->ul = 0; }
|
|
if (env->ol) { fsm_append_lit(ctx, "</ol>\n"); env->ol = 0; }
|
|
}
|
|
|
|
static void block_close_para(block_env_t *env, sashidown_ctx_t *ctx,
|
|
sashidown_render_state_t *rs)
|
|
{
|
|
if (env->para) {
|
|
inline_state_t ist;
|
|
inline_load_from_state(rs, &ist);
|
|
inline_flush_open(ctx, &ist);
|
|
fsm_append_lit(ctx, "</p>\n");
|
|
env->para = 0;
|
|
inline_reset(&ist);
|
|
inline_store_to_state(rs, &ist);
|
|
}
|
|
}
|
|
|
|
static void block_close_all(block_env_t *env, sashidown_ctx_t *ctx,
|
|
sashidown_render_state_t *rs)
|
|
{
|
|
if (!env->ul && !env->ol && !env->para && !env->bq) return;
|
|
block_close_lists(env, ctx);
|
|
block_close_para(env, ctx, rs);
|
|
if (env->bq) { fsm_append_lit(ctx, "</blockquote>\n"); env->bq = 0; }
|
|
}
|
|
|
|
static void table_close(sashidown_render_state_t *state, sashidown_ctx_t *ctx)
|
|
{
|
|
if (!state->in_table)
|
|
return;
|
|
fsm_append_lit(ctx, "</tbody>\n</table>\n");
|
|
state->in_table = 0;
|
|
state->table_cols = 0;
|
|
}
|
|
|
|
void sashidown_render_state_init(sashidown_render_state_t *state)
|
|
{
|
|
if (state)
|
|
memset(state, 0, sizeof(*state));
|
|
}
|
|
|
|
/* Continue an open table: emit body rows until a terminator line (left unconsumed)
|
|
* or soft-stop. Returns 1 if still in_table waiting for more input. */
|
|
static int render_table_body_continue(sashidown_ctx_t *ctx,
|
|
sashidown_render_state_t *state,
|
|
const char *input, size_t len,
|
|
size_t *pos_io, size_t stop, int is_final)
|
|
{
|
|
size_t pos = *pos_io;
|
|
size_t limit = is_final ? len : stop;
|
|
int cols = state->table_cols;
|
|
|
|
while (pos < limit) {
|
|
size_t le = block_line_end(ctx, input, len, pos);
|
|
int empty = (pos == le || (pos + 1 == le && input[pos] == '\r'));
|
|
if (empty) {
|
|
table_close(state, ctx);
|
|
*pos_io = pos;
|
|
return 0;
|
|
}
|
|
|
|
size_t llen = le - pos;
|
|
if (!memchr(input + pos, '|', llen) ||
|
|
table_is_separator_row(input + pos, llen)) {
|
|
table_close(state, ctx);
|
|
*pos_io = pos;
|
|
return 0;
|
|
}
|
|
|
|
/* Line must lie fully inside the soft window (newline included). */
|
|
if (!is_final) {
|
|
int complete = (le < len && input[le] == '\n' && le + 1 <= stop);
|
|
if (!complete) {
|
|
*pos_io = pos;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
render_table_row_at(ctx, input, len, pos, le, cols, 0);
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
if (ctx->overflow) {
|
|
*pos_io = pos;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
*pos_io = pos;
|
|
if (is_final) {
|
|
table_close(state, ctx);
|
|
return 0;
|
|
}
|
|
return state->in_table ? 1 : 0;
|
|
}
|
|
|
|
/* Try to open a table at pos (line has '|').
|
|
* Returns 1 if opened (pos_out after sep), 0 if not a table,
|
|
* -1 if need more input (defer, pos unchanged). */
|
|
static int try_open_table(sashidown_ctx_t *ctx, sashidown_render_state_t *state,
|
|
block_env_t *env, const char *input, size_t len,
|
|
size_t pos, size_t le, size_t stop, int is_final,
|
|
size_t *pos_out)
|
|
{
|
|
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 is_final ? 0 : -1;
|
|
|
|
size_t next = le + 1;
|
|
if (next >= len)
|
|
return is_final ? 0 : -1;
|
|
if (!is_final && next >= stop)
|
|
return -1;
|
|
|
|
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;
|
|
}
|
|
|
|
if (!is_final && nle >= stop && (nle >= len || input[nle] != '\n')) {
|
|
block_line_seek(ctx, pos);
|
|
return -1;
|
|
}
|
|
|
|
block_close_all(env, ctx, state);
|
|
state->pending_valid = 0;
|
|
state->para_hard_break = 0;
|
|
|
|
fsm_append_lit(ctx, "<table>\n<thead>\n");
|
|
render_table_row_at(ctx, input, len, pos, le, cols, 1);
|
|
fsm_append_lit(ctx, "</thead>\n<tbody>\n");
|
|
if (ctx->overflow) {
|
|
/* Partial tags may be in the attempt buffer; stream rewinds state.
|
|
* Do not commit in_table or advance pos. */
|
|
block_line_seek(ctx, pos);
|
|
*pos_out = pos;
|
|
return 1;
|
|
}
|
|
|
|
state->in_table = 1;
|
|
state->table_cols = cols;
|
|
*pos_out = block_skip_line(ctx, input, len, next);
|
|
return 1;
|
|
}
|
|
|
|
int sashidown_render_incremental(
|
|
sashidown_ctx_t *restrict ctx,
|
|
sashidown_render_state_t *restrict state,
|
|
const char *restrict input, size_t len,
|
|
size_t *pos_io,
|
|
size_t stop,
|
|
int is_final)
|
|
{
|
|
size_t pos = *pos_io;
|
|
block_env_t *env = &state->block;
|
|
|
|
if (state->in_fence) {
|
|
render_codeblock(ctx, input, len, &pos, NULL, stop, is_final, state);
|
|
*pos_io = pos;
|
|
if (ctx->overflow)
|
|
return -1;
|
|
if (state->in_fence && !is_final && pos >= stop)
|
|
return 0;
|
|
}
|
|
|
|
if (state->in_latex_block) {
|
|
render_latex_block_multiline(ctx, input, len, &pos, stop, is_final, state);
|
|
*pos_io = pos;
|
|
if (ctx->overflow)
|
|
return -1;
|
|
if (state->in_latex_block && !is_final && pos >= stop)
|
|
return 0;
|
|
}
|
|
|
|
if (state->in_table) {
|
|
int waiting = render_table_body_continue(ctx, state, input, len,
|
|
&pos, stop, is_final);
|
|
*pos_io = pos;
|
|
if (ctx->overflow)
|
|
return -1;
|
|
if (waiting)
|
|
return 0;
|
|
/* Terminator line left at pos — fall through to normal dispatch. */
|
|
}
|
|
|
|
size_t limit = is_final ? len : stop;
|
|
|
|
while (pos < limit) {
|
|
size_t le = 0;
|
|
line_info_t li;
|
|
int have_li = 0;
|
|
|
|
if (state->pending_valid && state->pending_pos == pos) {
|
|
li = state->pending_li;
|
|
le = state->pending_le;
|
|
state->pending_valid = 0;
|
|
have_li = 1;
|
|
}
|
|
|
|
if (!have_li) {
|
|
le = block_line_end(ctx, input, len, pos);
|
|
/* Empty lines: no table/setext probes. */
|
|
if (block_line_is_blank(input, len, pos, le)) {
|
|
size_t after = block_skip_line(ctx, input, len, pos);
|
|
if (!is_final && (env->ul || env->ol) &&
|
|
block_peek_continues_list(ctx, input, len, after, limit)) {
|
|
pos = after;
|
|
continue;
|
|
}
|
|
block_close_all(env, ctx, state);
|
|
state->para_hard_break = 0;
|
|
pos = after;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* Table open (scheme C): commit on header+sep, then body streams. */
|
|
if (memchr(input + pos, '|', le - pos) != NULL) {
|
|
size_t newt = 0;
|
|
int tr = try_open_table(ctx, state, env, input, len, pos, le,
|
|
stop, is_final, &newt);
|
|
if (tr < 0)
|
|
break;
|
|
if (tr > 0) {
|
|
if (ctx->overflow)
|
|
goto done;
|
|
pos = newt;
|
|
block_line_seek(ctx, pos);
|
|
if (state->in_table) {
|
|
int waiting = render_table_body_continue(ctx, state, input, len,
|
|
&pos, stop, is_final);
|
|
if (ctx->overflow)
|
|
goto done;
|
|
if (waiting)
|
|
goto done;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!have_li) {
|
|
li = block_classify_line(ctx, input, len, pos, le);
|
|
have_li = 1;
|
|
}
|
|
|
|
/* Setext: only paragraph lines; peek next line without dirtying cursor. */
|
|
if (li.kind == LK_PARA) {
|
|
line_info_t sli;
|
|
size_t send = 0;
|
|
int sr = block_try_setext(ctx, input, len, pos, le, stop, is_final,
|
|
&sli, &send);
|
|
if (sr < 0)
|
|
break;
|
|
if (sr > 0) {
|
|
block_close_all(env, ctx, state);
|
|
state->pending_valid = 0;
|
|
state->para_hard_break = 0;
|
|
{
|
|
int lvl = sli.heading_level;
|
|
switch (lvl) {
|
|
case 1: fsm_append_lit(ctx, "<h1>"); break;
|
|
case 2: fsm_append_lit(ctx, "<h2>"); break;
|
|
case 3: fsm_append_lit(ctx, "<h3>"); break;
|
|
case 4: fsm_append_lit(ctx, "<h4>"); break;
|
|
case 5: fsm_append_lit(ctx, "<h5>"); break;
|
|
default: fsm_append_lit(ctx, "<h6>"); break;
|
|
}
|
|
{
|
|
inline_state_t ist = {0, 0, 0, 0, 0};
|
|
render_inline_range(ctx, input, len, sli.content_start,
|
|
sli.content_end, 1, &ist, 1);
|
|
}
|
|
switch (lvl) {
|
|
case 1: fsm_append_lit(ctx, "</h1>\n"); break;
|
|
case 2: fsm_append_lit(ctx, "</h2>\n"); break;
|
|
case 3: fsm_append_lit(ctx, "</h3>\n"); break;
|
|
case 4: fsm_append_lit(ctx, "</h4>\n"); break;
|
|
case 5: fsm_append_lit(ctx, "</h5>\n"); break;
|
|
default: fsm_append_lit(ctx, "</h6>\n"); break;
|
|
}
|
|
}
|
|
pos = send;
|
|
block_line_seek(ctx, pos);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
switch (li.kind) {
|
|
case LK_EMPTY: {
|
|
size_t after = block_skip_line(ctx, input, len, pos);
|
|
if (!is_final && (env->ul || env->ol) &&
|
|
block_peek_continues_list(ctx, input, len, after, limit)) {
|
|
pos = after;
|
|
break;
|
|
}
|
|
block_close_all(env, ctx, state);
|
|
state->para_hard_break = 0;
|
|
pos = after;
|
|
break;
|
|
}
|
|
|
|
case LK_HR:
|
|
block_close_all(env, ctx, state);
|
|
state->para_hard_break = 0;
|
|
fsm_append_lit(ctx, "<hr />\n");
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_FENCE:
|
|
block_close_all(env, ctx, state);
|
|
state->para_hard_break = 0;
|
|
render_codeblock(ctx, input, len, &pos, &li, stop, is_final, state);
|
|
if (state->in_fence && !is_final && pos >= stop)
|
|
goto done;
|
|
break;
|
|
|
|
case LK_LATEX_LINE:
|
|
block_close_all(env, ctx, state);
|
|
state->para_hard_break = 0;
|
|
fsm_append_lit(ctx, "<p>$$");
|
|
html_escape(ctx, input + li.content_start,
|
|
li.content_end - li.content_start);
|
|
fsm_append_lit(ctx, "$$</p>\n");
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_LATEX_BLOCK:
|
|
block_close_all(env, ctx, state);
|
|
state->para_hard_break = 0;
|
|
render_latex_block_multiline(ctx, input, len, &pos, stop, is_final, state);
|
|
if (state->in_latex_block && !is_final && pos >= stop)
|
|
goto done;
|
|
break;
|
|
|
|
case LK_HEADING:
|
|
block_close_all(env, ctx, state);
|
|
state->para_hard_break = 0;
|
|
{
|
|
int lvl = li.heading_level;
|
|
size_t hend = (li.content_end > li.content_start) ? li.content_end : le;
|
|
switch (lvl) {
|
|
case 1: fsm_append_lit(ctx, "<h1>"); break;
|
|
case 2: fsm_append_lit(ctx, "<h2>"); break;
|
|
case 3: fsm_append_lit(ctx, "<h3>"); break;
|
|
case 4: fsm_append_lit(ctx, "<h4>"); break;
|
|
case 5: fsm_append_lit(ctx, "<h5>"); break;
|
|
default: fsm_append_lit(ctx, "<h6>"); break;
|
|
}
|
|
{
|
|
inline_state_t ist = {0, 0, 0, 0, 0};
|
|
render_inline_range(ctx, input, len, li.content_start, hend, 1, &ist, 1);
|
|
}
|
|
switch (lvl) {
|
|
case 1: fsm_append_lit(ctx, "</h1>\n"); break;
|
|
case 2: fsm_append_lit(ctx, "</h2>\n"); break;
|
|
case 3: fsm_append_lit(ctx, "</h3>\n"); break;
|
|
case 4: fsm_append_lit(ctx, "</h4>\n"); break;
|
|
case 5: fsm_append_lit(ctx, "</h5>\n"); break;
|
|
default: fsm_append_lit(ctx, "</h6>\n"); break;
|
|
}
|
|
}
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_BQ:
|
|
block_close_lists(env, ctx);
|
|
state->para_hard_break = 0;
|
|
if (!env->bq) { fsm_append_lit(ctx, "<blockquote>\n"); env->bq = 1; }
|
|
fsm_append_lit(ctx, "<p>");
|
|
{
|
|
inline_state_t ist = {0, 0, 0, 0, 0};
|
|
render_inline_range(ctx, input, len, li.content_start, le, 1, &ist, 1);
|
|
}
|
|
fsm_append_lit(ctx, "</p>\n");
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_UL:
|
|
if (env->ol) { fsm_append_lit(ctx, "</ol>\n"); env->ol = 0; }
|
|
if (!env->ul) { fsm_append_lit(ctx, "<ul>\n"); env->ul = 1; }
|
|
state->para_hard_break = 0;
|
|
fsm_append_lit(ctx, "<li>");
|
|
if (li.task == 1)
|
|
fsm_append_lit(ctx, "<input type=\"checkbox\" disabled=\"\" /> ");
|
|
else if (li.task == 2)
|
|
fsm_append_lit(ctx, "<input type=\"checkbox\" disabled=\"\" checked=\"\" /> ");
|
|
{
|
|
inline_state_t ist = {0, 0, 0, 0, 0};
|
|
render_inline_range(ctx, input, len, li.content_start, le, 1, &ist, 1);
|
|
}
|
|
fsm_append_lit(ctx, "</li>\n");
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_OL:
|
|
if (env->ul) { fsm_append_lit(ctx, "</ul>\n"); env->ul = 0; }
|
|
if (!env->ol) { fsm_append_lit(ctx, "<ol>\n"); env->ol = 1; }
|
|
state->para_hard_break = 0;
|
|
fsm_append_lit(ctx, "<li>");
|
|
{
|
|
inline_state_t ist = {0, 0, 0, 0, 0};
|
|
render_inline_range(ctx, input, len, li.content_start, le, 1, &ist, 1);
|
|
}
|
|
fsm_append_lit(ctx, "</li>\n");
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
break;
|
|
|
|
case LK_PARA:
|
|
default: {
|
|
size_t content_end = le;
|
|
int hard = 0;
|
|
if (content_end > pos && input[content_end - 1] == '\r')
|
|
content_end--;
|
|
if (content_end >= pos + 1 && input[content_end - 1] == '\\') {
|
|
hard = 1;
|
|
content_end--;
|
|
} else if (content_end >= pos + 2 &&
|
|
input[content_end - 1] == ' ' &&
|
|
input[content_end - 2] == ' ') {
|
|
hard = 1;
|
|
content_end -= 2;
|
|
}
|
|
|
|
if (env->bq) { fsm_append_lit(ctx, "</blockquote>\n"); env->bq = 0; }
|
|
block_close_lists(env, ctx);
|
|
if (!env->para) {
|
|
fsm_append_lit(ctx, "<p>");
|
|
env->para = 1;
|
|
} else if (!state->para_hard_break) {
|
|
fsm_append(ctx, '\n');
|
|
}
|
|
{
|
|
inline_state_t ist;
|
|
inline_load_from_state(state, &ist);
|
|
render_inline_range(ctx, input, len, pos, content_end, 1, &ist, 0);
|
|
inline_store_to_state(state, &ist);
|
|
}
|
|
if (hard) {
|
|
fsm_append_lit(ctx, "<br />\n");
|
|
state->para_hard_break = 1;
|
|
} else {
|
|
state->para_hard_break = 0;
|
|
}
|
|
pos = block_skip_line(ctx, input, len, pos);
|
|
if (pos >= limit) {
|
|
if (is_final)
|
|
block_close_para(env, ctx, state);
|
|
} else {
|
|
size_t nle = block_line_end(ctx, input, len, pos);
|
|
line_info_t nli = block_classify_line(ctx, input, len, pos, nle);
|
|
state->pending_valid = 1;
|
|
state->pending_pos = pos;
|
|
state->pending_le = nle;
|
|
state->pending_li = nli;
|
|
if (nli.kind != LK_PARA)
|
|
block_close_para(env, ctx, state);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
done:
|
|
if (is_final) {
|
|
table_close(state, ctx);
|
|
block_close_all(env, ctx, state);
|
|
}
|
|
|
|
*pos_io = pos;
|
|
return ctx->overflow ? -1 : 0;
|
|
}
|
|
|
|
int sashidown_render(sashidown_ctx_t *ctx, const char *input, size_t len)
|
|
{
|
|
sashidown_render_state_t state;
|
|
size_t pos = 0;
|
|
|
|
sashidown_render_state_init(&state);
|
|
fsm_write_begin(ctx);
|
|
int r = sashidown_render_incremental(ctx, &state, input, len, &pos, len, 1);
|
|
fsm_write_end(ctx);
|
|
return r;
|
|
}
|
|
|
|
static void trim_cell_bounds(const char *input, size_t *start, size_t *end)
|
|
{
|
|
while (*start < *end && input[*start] == ' ')
|
|
(*start)++;
|
|
while (*end > *start && input[*end - 1] == ' ')
|
|
(*end)--;
|
|
}
|
|
|
|
static void render_table_cell(sashidown_ctx_t *ctx, const char *input,
|
|
size_t len, size_t abs_start, size_t abs_end)
|
|
{
|
|
trim_cell_bounds(input, &abs_start, &abs_end);
|
|
if (abs_end <= abs_start)
|
|
return;
|
|
/* One pass: plain SIMD scan + memcpy / inline FSM (no pre-probe). */
|
|
{
|
|
inline_state_t ist = {0, 0, 0, 0, 0};
|
|
render_inline_range(ctx, input, len, abs_start, abs_end, 1, &ist, 1);
|
|
}
|
|
}
|
|
|
|
static void render_table_row_at(sashidown_ctx_t *ctx, const char *input,
|
|
size_t len, size_t pos, size_t le,
|
|
int cols, int is_header)
|
|
{
|
|
fsm_append_lit(ctx, "<tr>");
|
|
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++;
|
|
if (is_header) {
|
|
fsm_append_lit(ctx, "<th>");
|
|
render_table_cell(ctx, input, len, cs, i);
|
|
fsm_append_lit(ctx, "</th>");
|
|
} else {
|
|
fsm_append_lit(ctx, "<td>");
|
|
render_table_cell(ctx, input, len, cs, i);
|
|
fsm_append_lit(ctx, "</td>");
|
|
}
|
|
if (i < le && input[i] == '|') i++;
|
|
}
|
|
fsm_append_lit(ctx, "</tr>\n");
|
|
}
|
|
|
|
static int render_codeblock(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
size_t *pos, const line_info_t *open_li,
|
|
size_t stop, int is_final,
|
|
sashidown_render_state_t *state)
|
|
{
|
|
if (!state->in_fence) {
|
|
int fence_len = open_li ? open_li->fence_len : state->fence_len;
|
|
char fence_char = open_li ? open_li->fence_char : state->fence_char;
|
|
size_t info_start = open_li ? open_li->info_start : 0;
|
|
size_t info_end = open_li ? open_li->info_end : 0;
|
|
|
|
fsm_append_lit(ctx, "<pre><code");
|
|
if (info_end > info_start) {
|
|
size_t s = info_start, e = info_start;
|
|
while (e < info_end && input[e] != ' ' && input[e] != '\t')
|
|
e++;
|
|
if (e > s) {
|
|
fsm_append_lit(ctx, " class=\"language-");
|
|
html_escape(ctx, input + s, e - s);
|
|
fsm_append_lit(ctx, "\"");
|
|
}
|
|
}
|
|
fsm_append(ctx, '>');
|
|
state->in_fence = 1;
|
|
state->fence_len = fence_len;
|
|
state->fence_char = fence_char;
|
|
state->fence_info_start = info_start;
|
|
state->fence_info_end = info_end;
|
|
*pos = block_skip_line(ctx, input, len, *pos);
|
|
}
|
|
|
|
size_t limit = is_final ? len : stop;
|
|
|
|
while (*pos < len) {
|
|
if (*pos >= limit && !is_final) {
|
|
return 0;
|
|
}
|
|
|
|
int fl = 0;
|
|
char fc = 0;
|
|
if (block_is_fence(input, len, *pos, &fl, &fc) &&
|
|
fc == state->fence_char && fl >= state->fence_len) {
|
|
/* Closing fence: info string must be empty (CommonMark). */
|
|
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);
|
|
fsm_append_lit(ctx, "</code></pre>\n");
|
|
state->in_fence = 0;
|
|
return 0;
|
|
}
|
|
}
|
|
size_t le = block_line_end(ctx, input, len, *pos);
|
|
html_escape(ctx, input + *pos, le - *pos);
|
|
if (le < len) fsm_append(ctx, '\n');
|
|
*pos = block_skip_line(ctx, input, len, *pos);
|
|
}
|
|
|
|
if (is_final && state->in_fence) {
|
|
fsm_append_lit(ctx, "</code></pre>\n");
|
|
state->in_fence = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int render_latex_block_multiline(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
size_t *pos, size_t stop, int is_final,
|
|
sashidown_render_state_t *state)
|
|
{
|
|
size_t i;
|
|
if (!state->in_latex_block) {
|
|
fsm_append_lit(ctx, "<p>$$");
|
|
state->in_latex_block = 1;
|
|
i = *pos + 2;
|
|
} else {
|
|
i = *pos;
|
|
}
|
|
|
|
size_t start = i;
|
|
size_t limit = is_final ? len : stop;
|
|
|
|
while (i < len) {
|
|
if (!is_final && i >= limit) {
|
|
if (i > start)
|
|
fsm_append_len(ctx, input + start, i - start);
|
|
*pos = i;
|
|
return 0;
|
|
}
|
|
size_t scan_end = is_final ? len : limit;
|
|
if (i >= scan_end) {
|
|
if (i > start)
|
|
fsm_append_len(ctx, input + start, i - start);
|
|
*pos = i;
|
|
return 0;
|
|
}
|
|
const char *hitp = memchr(input + i, '$', scan_end - i);
|
|
if (!hitp) {
|
|
if (scan_end > start)
|
|
fsm_append_len(ctx, input + start, scan_end - start);
|
|
*pos = scan_end;
|
|
if (is_final && state->in_latex_block) {
|
|
fsm_append_lit(ctx, "$$</p>\n");
|
|
state->in_latex_block = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
i = (size_t)(hitp - input);
|
|
if (i + 1 < len && input[i + 1] == '$') {
|
|
/* Closing $$ must lie fully before soft-stop when incremental. */
|
|
if (!is_final && i + 1 >= limit) {
|
|
if (limit > start)
|
|
fsm_append_len(ctx, input + start, limit - start);
|
|
*pos = limit;
|
|
return 0;
|
|
}
|
|
if (i > start)
|
|
fsm_append_len(ctx, input + start, i - start);
|
|
fsm_append_lit(ctx, "$$</p>\n");
|
|
i += 2;
|
|
if (i < len && input[i] == '\n')
|
|
i++;
|
|
*pos = i;
|
|
state->in_latex_block = 0;
|
|
return 0;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
if (i > start)
|
|
fsm_append_len(ctx, input + start, i - start);
|
|
*pos = i;
|
|
|
|
if (is_final && state->in_latex_block) {
|
|
fsm_append_lit(ctx, "$$</p>\n");
|
|
state->in_latex_block = 0;
|
|
}
|
|
return 0;
|
|
}
|