#include "fsm.h" #include "table_stats.h" #include "line_index.h" #include "block_scan.h" #include "render.h" #include #ifdef __SSE2__ #include #elif defined(__ARM_NEON) || defined(__aarch64__) #include #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, ""); st->bold = 0; } if (st->italic) { fsm_append_lit(ctx, ""); st->italic = 0; } if (st->code) { fsm_append_lit(ctx, ""); st->code = 0; } if (st->strike) { fsm_append_lit(ctx, ""); 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 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, ""); html_escape(ctx, input + start, j - start); fsm_append_lit(ctx, ""); *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, "\"");"); } else { 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, ""); } *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, ""); 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, ""); st->bold = 0; } else { fsm_append_lit(ctx, ""); 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, ""); st->bold = 0; } else { fsm_append_lit(ctx, ""); st->bold = 1; } i += 2; continue; } goto lbl_emph; lbl_emph: if (st->italic) { fsm_append_lit(ctx, ""); st->italic = 0; } else { fsm_append_lit(ctx, ""); st->italic = 1; } i++; continue; lbl_tilde: if (i + 1 < end && input[i + 1] == '~') { if (st->strike) { fsm_append_lit(ctx, ""); st->strike = 0; } else { fsm_append_lit(ctx, ""); 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, ""); 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, "\n"); env->ul = 0; } if (env->ol) { fsm_append_lit(ctx, "\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, "

\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, "\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, "\n\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, "\n\n"); render_table_row_at(ctx, input, len, pos, le, cols, 1); fsm_append_lit(ctx, "\n\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, "

"); break; case 2: fsm_append_lit(ctx, "

"); break; case 3: fsm_append_lit(ctx, "

"); break; case 4: fsm_append_lit(ctx, "

"); break; case 5: fsm_append_lit(ctx, "

"); break; default: fsm_append_lit(ctx, "
"); 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, "
\n"); break; case 2: fsm_append_lit(ctx, "\n"); break; case 3: fsm_append_lit(ctx, "\n"); break; case 4: fsm_append_lit(ctx, "\n"); break; case 5: fsm_append_lit(ctx, "\n"); break; default: fsm_append_lit(ctx, "\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, "
\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, "

$$"); html_escape(ctx, input + li.content_start, li.content_end - li.content_start); fsm_append_lit(ctx, "$$

\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, "

"); break; case 2: fsm_append_lit(ctx, "

"); break; case 3: fsm_append_lit(ctx, "

"); break; case 4: fsm_append_lit(ctx, "

"); break; case 5: fsm_append_lit(ctx, "

"); break; default: fsm_append_lit(ctx, "
"); 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, "
\n"); break; case 2: fsm_append_lit(ctx, "\n"); break; case 3: fsm_append_lit(ctx, "\n"); break; case 4: fsm_append_lit(ctx, "\n"); break; case 5: fsm_append_lit(ctx, "\n"); break; default: fsm_append_lit(ctx, "\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, "
\n"); env->bq = 1; } fsm_append_lit(ctx, "

"); { 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, "

\n"); pos = block_skip_line(ctx, input, len, pos); break; case LK_UL: if (env->ol) { fsm_append_lit(ctx, "\n"); env->ol = 0; } if (!env->ul) { fsm_append_lit(ctx, "
    \n"); env->ul = 1; } state->para_hard_break = 0; fsm_append_lit(ctx, "
  • "); if (li.task == 1) fsm_append_lit(ctx, " "); else if (li.task == 2) fsm_append_lit(ctx, " "); { 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, "
  • \n"); pos = block_skip_line(ctx, input, len, pos); break; case LK_OL: if (env->ul) { fsm_append_lit(ctx, "
\n"); env->ul = 0; } if (!env->ol) { fsm_append_lit(ctx, "
    \n"); env->ol = 1; } state->para_hard_break = 0; fsm_append_lit(ctx, "
  1. "); { 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, "
  2. \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, "
\n"); env->bq = 0; } block_close_lists(env, ctx); if (!env->para) { fsm_append_lit(ctx, "

"); 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, "
\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, "

"); 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, ""); } else { fsm_append_lit(ctx, ""); } if (i < le && input[i] == '|') i++; } fsm_append_lit(ctx, "\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, "
 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, "
\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, "\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, "

$$"); 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, "$$

\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, "$$

\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, "$$

\n"); state->in_latex_block = 0; } return 0; }
"); render_table_cell(ctx, input, len, cs, i); fsm_append_lit(ctx, ""); render_table_cell(ctx, input, len, cs, i); fsm_append_lit(ctx, "