Files
Hermit/vendor/sashidown/src/render.c
T
WHCandCursor 1f1fcba7ad Initial CST-Agent: portable Linux/Termux/Windows coding agent.
Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 22:42:32 +08:00

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, "&amp;"); break;
case '<': fsm_append_lit(ctx, "&lt;"); break;
case '>': fsm_append_lit(ctx, "&gt;"); break;
case '"': fsm_append_lit(ctx, "&quot;"); 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, "&amp;"); break;
case '<': fsm_append_lit(ctx, "&lt;"); break;
case '>': fsm_append_lit(ctx, "&gt;"); break;
case '"': fsm_append_lit(ctx, "&quot;"); 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, "&amp;"); break;
case '<': fsm_append_lit(ctx, "&lt;"); break;
case '>': fsm_append_lit(ctx, "&gt;"); break;
case '"': fsm_append_lit(ctx, "&quot;"); 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, "&amp;"); break;
case '<': fsm_append_lit(ctx, "&lt;"); break;
case '>': fsm_append_lit(ctx, "&gt;"); 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;
}