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>
This commit is contained in:
WHC
2026-09-07 22:42:32 +08:00
co-authored by Cursor
commit 1f1fcba7ad
75 changed files with 14940 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
CC ?= gcc
CFLAGS ?= -std=c11 -Wall -Wextra -O2 -Iinclude -Isrc
SRCS = src/fsm.c src/table_stats.c src/line_index.c src/block_scan.c \
src/ansi_style.c src/render.c src/render_ansi.c src/render_json.c \
src/sashidown.c src/compat_cmark.c src/stream.c
OBJS = $(SRCS:.c=.o)
LIB = libsashidown.a
.PHONY: all clean
all: $(LIB)
$(LIB): $(OBJS)
$(AR) rcs $@ $(OBJS)
src/%.o: src/%.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f $(OBJS) $(LIB)
+64
View File
@@ -0,0 +1,64 @@
#ifndef SASHIDOWN_H
#define SASHIDOWN_H
#include <stddef.h>
#define SASHIDOWN_OUTPUT_DEFAULT 65536
typedef struct {
const char *input;
size_t input_len;
char *output;
size_t output_cap;
size_t output_len;
char *write_ptr;
char *write_end;
void *line_ix;
int overflow; /* set when output buffer is exhausted */
} sashidown_ctx_t;
/* Parse markdown to HTML. Returns 0 on success, -1 on error
* (null args, or output buffer too small — never silently truncates). */
int sashidown_parse(const char *input, size_t len,
char *output, size_t cap, size_t *out_len);
typedef enum {
SASHIDOWN_FMT_HTML = 0,
SASHIDOWN_FMT_ANSI = 1, /* built-in colorful terminal theme */
SASHIDOWN_FMT_JSON = 2,
SASHIDOWN_FMT_ANSI_PLAIN = 3, /* monochrome / SGR attributes only */
} sashidown_format_t;
/* Single-pass convert. SASHIDOWN_FMT_HTML delegates to sashidown_parse.
* Returns 0 on success, -1 on error (null args, bad format, or buffer too small). */
int sashidown_convert(const char *input, size_t len,
char *output, size_t cap, size_t *out_len,
sashidown_format_t format);
/* Allocating wrapper (thread-safe). Caller must free via sashidown_free. */
char *cmark_markdown_to_html(const char *text, int len);
void sashidown_free(void *ptr);
/* --- Streaming API (AI conversation / incremental input) --- */
typedef struct sashidown_stream sashidown_stream_t;
/* Called for each HTML chunk; return non-zero to abort feeding. */
typedef int (*sashidown_stream_cb)(const char *html, size_t len, void *user);
typedef struct {
sashidown_stream_cb on_html;
void *user;
size_t output_buf_size; /* default 8192 if 0; grows on overflow */
} sashidown_stream_opts_t;
sashidown_stream_t *sashidown_stream_create(const sashidown_stream_opts_t *opts);
int sashidown_stream_feed(sashidown_stream_t *s, const char *chunk, size_t len);
int sashidown_stream_finish(sashidown_stream_t *s);
void sashidown_stream_destroy(sashidown_stream_t *s);
/* Bytes held past the confirmation frontier (input_len - render_pos).
* Unconfirmed suffix under uncertain future input; O(1) query. */
size_t sashidown_stream_pending(const sashidown_stream_t *s);
#endif /* SASHIDOWN_H */
+104
View File
@@ -0,0 +1,104 @@
#include "ansi_style.h"
#include <stddef.h>
/* Default colorful theme — 16-color SGR, widely supported. */
static const ansi_theme_t THEME_COLOR = {
.reset = "\033[0m",
.heading = {
NULL,
"\033[1;96m", /* h1 bright cyan */
"\033[1;94m", /* h2 bright blue */
"\033[1;92m", /* h3 bright green */
"\033[1;95m", /* h4 bright magenta */
"\033[1;93m", /* h5 bright yellow */
"\033[1;91m", /* h6 bright red */
},
.heading_end = "\033[0m",
.strong_on = "\033[1m",
.strong_off = "\033[22m",
.emph_on = "\033[3;33m", /* italic yellow */
.emph_off = "\033[23;39m",
.strike_on = "\033[9m",
.strike_off = "\033[29m",
.code_on = "\033[1;36m", /* bright cyan */
.code_off = "\033[22;39m",
.fence_on = "\033[32m", /* green */
.fence_off = "\033[0m",
.link_on = "\033[4;94m", /* underline bright blue */
.link_off = "\033[24;39m",
.url_on = "\033[2;36m", /* dim cyan */
.url_off = "\033[22;39m",
.image_on = "\033[95m",
.image_off = "\033[39m",
.bullet = "\033[93m• \033[0m",
.bullet_end = "",
.ol_on = "\033[93m",
.ol_off = "\033[0m",
.bq_prefix = "\033[2;35m│ \033[0m",
.th_on = "\033[1;96m",
.th_off = "\033[0m",
.sep = "\033[2m │ \033[0m",
.latex_on = "\033[95m",
.latex_off = "\033[39m",
};
/* Monochrome / attribute-only (previous behavior). */
static const ansi_theme_t THEME_PLAIN = {
.reset = "\033[0m",
.heading = {
NULL,
"\033[1m", "\033[1m", "\033[1m",
"\033[1m", "\033[1m", "\033[1m",
},
.heading_end = "\033[0m",
.strong_on = "\033[1m",
.strong_off = "\033[22m",
.emph_on = "\033[3m",
.emph_off = "\033[23m",
.strike_on = "\033[9m",
.strike_off = "\033[29m",
.code_on = "\033[7m",
.code_off = "\033[27m",
.fence_on = "\033[2m",
.fence_off = "\033[0m",
.link_on = "",
.link_off = "",
.url_on = "",
.url_off = "",
.image_on = "",
.image_off = "",
.bullet = "• ",
.bullet_end = "",
.ol_on = "",
.ol_off = "",
.bq_prefix = "\033[2m| \033[0m",
.th_on = "\033[1m",
.th_off = "\033[22m",
.sep = " | ",
.latex_on = "",
.latex_off = "",
};
const ansi_theme_t *ansi_theme_color(void)
{
return &THEME_COLOR;
}
const ansi_theme_t *ansi_theme_plain(void)
{
return &THEME_PLAIN;
}
+48
View File
@@ -0,0 +1,48 @@
#ifndef SASHIDOWN_ANSI_STYLE_H
#define SASHIDOWN_ANSI_STYLE_H
/* Built-in ANSI role styles (SGR). Strings are compile-time literals. */
typedef struct {
const char *reset;
const char *heading[7]; /* [1..6] used; [0] unused */
const char *heading_end;
const char *strong_on;
const char *strong_off;
const char *emph_on;
const char *emph_off;
const char *strike_on;
const char *strike_off;
const char *code_on;
const char *code_off;
const char *fence_on;
const char *fence_off;
const char *link_on;
const char *link_off;
const char *url_on;
const char *url_off;
const char *image_on;
const char *image_off;
const char *bullet;
const char *bullet_end;
const char *ol_on;
const char *ol_off;
const char *bq_prefix;
const char *th_on;
const char *th_off;
const char *sep; /* table column separator */
const char *latex_on;
const char *latex_off;
} ansi_theme_t;
const ansi_theme_t *ansi_theme_color(void);
const ansi_theme_t *ansi_theme_plain(void);
#endif
+337
View File
@@ -0,0 +1,337 @@
#include "block_scan.h"
#include <string.h>
int block_count_heading(const char *input, size_t len, size_t pos, size_t *content_start)
{
size_t i = pos;
int level = 0;
while (i < len && input[i] == '#') {
level++;
i++;
}
if (level == 0 || level > 6) return 0;
if (i < len && input[i] != ' ' && input[i] != '\n') return 0;
while (i < len && input[i] == ' ') i++;
*content_start = i;
return level;
}
int block_is_fence(const char *input, size_t len, size_t pos,
int *fence_len, char *fence_char)
{
size_t i = pos;
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
if (i + 2 >= len) return 0;
char ch = input[i];
if (ch != '`' && ch != '~') return 0;
if (input[i + 1] != ch || input[i + 2] != ch) return 0;
int fl = 3;
i += 3;
while (i < len && input[i] == ch) { fl++; i++; }
*fence_len = fl;
if (fence_char)
*fence_char = ch;
return 1;
}
void block_fence_info_range(const char *input, size_t len, size_t pos,
int fence_len, char fence_char,
size_t *info_start, size_t *info_end)
{
size_t i = pos;
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
i += (size_t)fence_len;
(void)fence_char;
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
*info_start = i;
size_t end = i;
while (end < len && input[end] != '\n' && input[end] != '\r')
end++;
while (end > *info_start &&
(input[end - 1] == ' ' || input[end - 1] == '\t'))
end--;
*info_end = end;
}
int block_is_thematic_break(const char *input, size_t pos, size_t le)
{
size_t i = pos;
while (i < le && (input[i] == ' ' || input[i] == '\t')) i++;
if (i >= le) return 0;
char marker = input[i];
if (marker != '*' && marker != '-' && marker != '_') return 0;
int count = 0;
for (; i < le; i++) {
char c = input[i];
if (c == marker) {
count++;
} else if (c == ' ' || c == '\t') {
continue;
} else if (c == '\r') {
continue;
} else {
return 0;
}
}
return count >= 3;
}
int block_is_setext_underline(const char *input, size_t pos, size_t le, int *level)
{
size_t i = pos;
while (i < le && (input[i] == ' ' || input[i] == '\t')) i++;
if (i >= le) return 0;
char marker = input[i];
if (marker != '=' && marker != '-') return 0;
size_t start = i;
while (i < le && input[i] == marker) i++;
if (i == start) return 0;
while (i < le && (input[i] == ' ' || input[i] == '\t' || input[i] == '\r'))
i++;
if (i < le) return 0;
/* Thematic-break-looking '---' is still a valid setext underline when
* paired with a preceding text line (caller decides context). */
if (level)
*level = (marker == '=') ? 1 : 2;
return 1;
}
int block_is_blockquote(const char *input, size_t len, size_t pos, size_t *content_start)
{
size_t i = pos;
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
if (i >= len || input[i] != '>') return 0;
i++;
if (i < len && input[i] == ' ') i++;
*content_start = i;
return 1;
}
int block_is_ul(const char *input, size_t len, size_t pos, size_t *content_start)
{
size_t i = pos;
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
if (i >= len) return 0;
if (input[i] != '-' && input[i] != '*' && input[i] != '+') return 0;
if (i + 1 < len && input[i + 1] != ' ') return 0;
i += 2;
*content_start = i;
return 1;
}
int block_parse_task_marker(const char *input, size_t len, size_t content_start,
int *task, size_t *text_start)
{
size_t i = content_start;
if (i + 3 > len) return 0;
if (input[i] != '[') return 0;
char mark = input[i + 1];
if (input[i + 2] != ']') return 0;
if (mark != ' ' && mark != 'x' && mark != 'X') return 0;
i += 3;
if (i < len && input[i] == ' ') i++;
*task = (mark == ' ') ? 1 : 2;
*text_start = i;
return 1;
}
int block_is_ol(const char *input, size_t len, size_t pos, size_t *content_start, int *num)
{
size_t i = pos;
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
size_t num_start = i;
{ unsigned char c = (unsigned char)input[i];
if (i >= len || c < '0' || c > '9') return 0; }
while (i < len) {
unsigned char c = (unsigned char)input[i];
if (c < '0' || c > '9') break;
i++;
}
if (i >= len || input[i] != '.') return 0;
if (i + 1 < len && input[i + 1] != ' ') return 0;
*num = 0;
for (size_t j = num_start; j < i; j++)
*num = *num * 10 + (input[j] - '0');
i += 2;
*content_start = i;
return 1;
}
int block_line_starts_table(const char *input, size_t len, size_t pos)
{
size_t i = pos;
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
if (i >= len) return 0;
/* GFM: leading '|' OR a pipe somewhere on the line with a separator below.
* Cheap reject: must contain '|' before newline. */
const char *nl = memchr(input + i, '\n', len - i);
size_t line_len = nl ? (size_t)(nl - (input + i)) : (len - i);
return memchr(input + i, '|', line_len) != NULL;
}
int block_is_latex_line(sashidown_ctx_t *ctx, const char *input, size_t len,
size_t pos, size_t *content_start, size_t *content_end)
{
size_t i = pos;
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
if (i + 1 >= len || input[i] != '$' || input[i + 1] != '$') return 0;
i += 2;
*content_start = i;
size_t le = block_line_end(ctx, input, len, pos);
if (i + 1 < le && input[le - 2] == '$' && input[le - 1] == '$') {
*content_end = le - 2;
return 1;
}
return 0;
}
int block_try_setext(sashidown_ctx_t *ctx, const char *input, size_t len,
size_t pos, size_t le, size_t stop, int is_final,
line_info_t *info, size_t *end_pos)
{
if (pos == le || (pos + 1 == le && input[pos] == '\r'))
return 0;
/* Next line starts after the newline at `le` (line_end points at '\n'). */
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 (next >= stop && !is_final)
return -1;
/* Cheap reject before full underline scan. */
size_t u = next;
while (u < len && (input[u] == ' ' || input[u] == '\t'))
u++;
if (u >= len || (input[u] != '=' && input[u] != '-'))
return 0;
size_t nle = block_line_end(ctx, input, len, next);
int level = 0;
if (!block_is_setext_underline(input, next, nle, &level))
return 0;
info->kind = LK_HEADING;
info->heading_level = level;
info->content_start = pos;
while (info->content_start < le &&
(input[info->content_start] == ' ' || input[info->content_start] == '\t'))
info->content_start++;
info->content_end = le;
while (info->content_end > info->content_start &&
(input[info->content_end - 1] == ' ' ||
input[info->content_end - 1] == '\t' ||
input[info->content_end - 1] == '\r'))
info->content_end--;
info->fence_len = 0;
info->fence_char = 0;
info->info_start = 0;
info->info_end = 0;
info->task = 0;
*end_pos = block_skip_line(ctx, input, len, next);
return 1;
}
line_info_t block_classify_line(sashidown_ctx_t *ctx, const char *input, size_t len,
size_t pos, size_t le)
{
line_info_t info;
memset(&info, 0, sizeof(info));
info.kind = LK_PARA;
info.content_start = pos;
info.content_end = le;
if (pos == le || (pos + 1 == le && input[pos] == '\r')) {
info.kind = LK_EMPTY;
return info;
}
size_t i = pos;
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
unsigned char first = (i < len) ? (unsigned char)input[i] : 0;
/* Thematic breaks only possible for - / * / _ lines. */
if ((first == '*' || first == '-' || first == '_') &&
block_is_thematic_break(input, pos, le)) {
info.kind = LK_HR;
return info;
}
switch (first) {
case '#':
info.heading_level = block_count_heading(input, len, pos, &info.content_start);
if (info.heading_level > 0) {
info.kind = LK_HEADING;
info.content_end = le;
return info;
}
break;
case '`':
case '~':
if (block_is_fence(input, len, pos, &info.fence_len, &info.fence_char)) {
info.kind = LK_FENCE;
block_fence_info_range(input, len, pos, info.fence_len, info.fence_char,
&info.info_start, &info.info_end);
return info;
}
break;
case '>':
if (block_is_blockquote(input, len, pos, &info.content_start)) {
info.kind = LK_BQ;
return info;
}
break;
case '-': case '*': case '+':
if (block_is_ul(input, len, pos, &info.content_start)) {
info.kind = LK_UL;
size_t text = info.content_start;
int task = 0;
if (block_parse_task_marker(input, le, info.content_start, &task, &text)) {
info.task = task;
info.content_start = text;
}
return info;
}
break;
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
{
int ol_num = 0;
if (block_is_ol(input, len, pos, &info.content_start, &ol_num)) {
info.kind = LK_OL;
return info;
}
break;
}
case '$':
{
size_t cs, ce;
if (i + 1 < len && input[i] == '$' && input[i + 1] == '$') {
if (i == pos) {
info.kind = LK_LATEX_BLOCK;
return info;
}
if (block_is_latex_line(ctx, input, len, pos, &cs, &ce)) {
info.kind = LK_LATEX_LINE;
info.content_start = cs;
info.content_end = ce;
return info;
}
}
break;
}
default:
break;
}
info.kind = LK_PARA;
return info;
}
+196
View File
@@ -0,0 +1,196 @@
#ifndef SASHIDOWN_BLOCK_SCAN_H
#define SASHIDOWN_BLOCK_SCAN_H
#include "sashidown.h"
#include "render.h"
#include "fsm.h"
#include "line_index.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#ifdef __SSE2__
#include <emmintrin.h>
#elif defined(__ARM_NEON) || defined(__aarch64__)
#include <arm_neon.h>
#endif
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Woverride-init"
static inline size_t block_utf8_seq_len(unsigned char ch)
{
static const unsigned char tbl[256] = {
[0x00 ... 0xFF] = 1,
[0xC0 ... 0xDF] = 2,
[0xE0 ... 0xEF] = 3,
[0xF0 ... 0xF7] = 4,
};
return tbl[ch];
}
#pragma GCC diagnostic pop
static inline size_t block_scan_plain_ascii(const char *restrict input, size_t end, size_t i)
{
#ifdef __SSE2__
const __m128i m_star = _mm_set1_epi8('*');
const __m128i m_us = _mm_set1_epi8('_');
const __m128i m_tick = _mm_set1_epi8('`');
const __m128i m_dol = _mm_set1_epi8('$');
const __m128i m_bang = _mm_set1_epi8('!');
const __m128i m_lb = _mm_set1_epi8('[');
const __m128i m_amp = _mm_set1_epi8('&');
const __m128i m_lt = _mm_set1_epi8('<');
const __m128i m_gt = _mm_set1_epi8('>');
const __m128i m_bs = _mm_set1_epi8('\\');
const __m128i m_tilde = _mm_set1_epi8('~');
while (i + 16 <= end) {
__m128i v = _mm_loadu_si128((const __m128i *)(input + i));
if (_mm_movemask_epi8(v))
break; /* high bit => UTF-8 */
__m128i hit = _mm_cmpeq_epi8(v, m_star);
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_us));
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_tick));
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_dol));
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_bang));
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_lb));
hit = _mm_or_si128(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_bs));
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_tilde));
int mask = _mm_movemask_epi8(hit);
if (mask)
return i + (unsigned)__builtin_ctz(mask);
i += 16;
}
#elif defined(__ARM_NEON) || defined(__aarch64__)
while (i + 16 <= end) {
uint8x16_t v = vld1q_u8((const uint8_t *)(input + i));
if (vmaxvq_u8(vandq_u8(v, vdupq_n_u8(0x80))))
break;
uint8x16_t hit = vceqq_u8(v, vdupq_n_u8('*'));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('_')));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('`')));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('$')));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('!')));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('[')));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('&')));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('<')));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('>')));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('\\')));
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('~')));
if (vmaxvq_u8(hit)) {
uint8_t lanes[16];
vst1q_u8(lanes, hit);
size_t pos = 0;
while (pos < 16 && !lanes[pos]) pos++;
return i + pos;
}
i += 16;
}
#endif
size_t j = i;
while (j + 4 <= end) {
uint32_t v;
memcpy(&v, input + j, 4);
if (v & 0x80808080)
break;
if (CH_SCAN_STOP[(v & 0xFF)] ||
CH_SCAN_STOP[(v >> 8) & 0xFF] ||
CH_SCAN_STOP[(v >> 16) & 0xFF] ||
CH_SCAN_STOP[(v >> 24) & 0xFF])
break;
j += 4;
}
while (j < end) {
unsigned char c = (unsigned char)input[j];
if ((c & 0x80))
break;
if (unlikely(CH_SCAN_STOP[c]))
break;
j++;
}
return j;
}
static inline size_t block_scan_utf8_run(const char *input, size_t end, size_t i)
{
size_t j = i;
while (j < end) {
unsigned char c = (unsigned char)input[j];
if (likely((c & 0x80) == 0))
break;
size_t seq = block_utf8_seq_len(c);
if (j + seq > end)
break;
j += seq;
}
return j;
}
static inline size_t block_scan_until_byte(const char *input, size_t end, size_t i, char delim)
{
if (i >= end)
return i;
const char *hit = memchr(input + i, delim, end - i);
return hit ? (size_t)(hit - input) : end;
}
static inline size_t block_scan_until_2byte(const char *input, size_t end, size_t start,
char c1, char c2)
{
size_t i = start;
while (i + 1 < end) {
const char *hit = memchr(input + i, c1, end - i);
if (!hit) return end;
i = (size_t)(hit - input);
if (i + 1 < end && input[i + 1] == c2)
return i;
i++;
}
return end;
}
static inline size_t block_line_end(sashidown_ctx_t *ctx, const char *input, size_t len, size_t pos)
{
return line_end_ix((line_index_t *)ctx->line_ix, input, len, pos);
}
static inline size_t block_skip_line(sashidown_ctx_t *ctx, const char *input, size_t len, size_t pos)
{
return skip_line_ix((line_index_t *)ctx->line_ix, input, len, pos);
}
static inline void block_line_seek(sashidown_ctx_t *ctx, size_t pos)
{
line_index_seek((line_index_t *)ctx->line_ix, pos);
}
int block_count_heading(const char *input, size_t len, size_t pos, size_t *content_start);
int block_is_fence(const char *input, size_t len, size_t pos,
int *fence_len, char *fence_char);
void block_fence_info_range(const char *input, size_t len, size_t pos,
int fence_len, char fence_char,
size_t *info_start, size_t *info_end);
int block_is_thematic_break(const char *input, size_t pos, size_t le);
int block_is_setext_underline(const char *input, size_t pos, size_t le, int *level);
int block_is_blockquote(const char *input, size_t len, size_t pos, size_t *content_start);
int block_is_ul(const char *input, size_t len, size_t pos, size_t *content_start);
int block_is_ol(const char *input, size_t len, size_t pos, size_t *content_start, int *num);
int block_parse_task_marker(const char *input, size_t len, size_t content_start,
int *task, size_t *text_start);
int block_line_starts_table(const char *input, size_t len, size_t pos);
int block_is_latex_line(sashidown_ctx_t *ctx, const char *input, size_t len,
size_t pos, size_t *content_start, size_t *content_end);
/* Peek next-line setext underline. Does NOT advance line_index on failure.
* `le` must be block_line_end(pos). Caller should only invoke for LK_PARA lines.
* Returns 1 = heading, 0 = no, -1 = defer (need next line). */
int block_try_setext(sashidown_ctx_t *ctx, const char *input, size_t len,
size_t pos, size_t le, size_t stop, int is_final,
line_info_t *info, size_t *end_pos);
line_info_t block_classify_line(sashidown_ctx_t *ctx, const char *input, size_t len,
size_t pos, size_t le);
#endif /* SASHIDOWN_BLOCK_SCAN_H */
+44
View File
@@ -0,0 +1,44 @@
#include "sashidown.h"
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
char *cmark_markdown_to_html(const char *text, int len)
{
if (!text)
return NULL;
if (len < 0)
len = (int)strlen(text);
size_t in_len = (size_t)len;
/* HTML typically expands; grow until parse fits (thread-safe, no static buf). */
size_t cap = in_len * 4 + 256;
if (cap < 1024)
cap = 1024;
for (;;) {
char *buf = (char *)malloc(cap);
if (!buf)
return NULL;
size_t out_len = 0;
if (sashidown_parse(text, in_len, buf, cap, &out_len) == 0) {
char *fit = (char *)realloc(buf, out_len + 1);
return fit ? fit : buf;
}
free(buf);
if (cap > SIZE_MAX / 2)
return NULL;
size_t next = cap * 2;
/* Bail if expansion is absurd relative to input. */
if (next > in_len * 64 + 4096)
return NULL;
cap = next;
}
}
void sashidown_free(void *ptr)
{
free(ptr);
}
+110
View File
@@ -0,0 +1,110 @@
#include "fsm.h"
#include <string.h>
const uint8_t CH_HTML_ESCAPE[256] = {
['&'] = 1, ['<'] = 1, ['>'] = 1, ['"'] = 1,
};
const uint8_t CH_JSON_ESCAPE[256] = {
['"'] = 1, ['\\'] = 1,
[0x00] = 1, [0x01] = 1, [0x02] = 1, [0x03] = 1,
[0x04] = 1, [0x05] = 1, [0x06] = 1, [0x07] = 1,
[0x08] = 1, [0x09] = 1, [0x0A] = 1, [0x0B] = 1,
[0x0C] = 1, [0x0D] = 1, [0x0E] = 1, [0x0F] = 1,
[0x10] = 1, [0x11] = 1, [0x12] = 1, [0x13] = 1,
[0x14] = 1, [0x15] = 1, [0x16] = 1, [0x17] = 1,
[0x18] = 1, [0x19] = 1, [0x1A] = 1, [0x1B] = 1,
[0x1C] = 1, [0x1D] = 1, [0x1E] = 1, [0x1F] = 1,
};
/* Inline special + HTML escape; combine with (c & 0x80) in the scan loop. */
const uint8_t CH_SCAN_STOP[256] = {
['*'] = 1, ['_'] = 1, ['`'] = 1, ['$'] = 1, ['!'] = 1, ['['] = 1,
['&'] = 1, ['<'] = 1, ['>'] = 1, ['\\'] = 1, ['~'] = 1,
};
void fsm_write_begin(sashidown_ctx_t *ctx)
{
ctx->overflow = 0;
ctx->write_ptr = ctx->output + ctx->output_len;
ctx->write_end = ctx->output + ctx->output_cap - 1;
}
void fsm_write_end(sashidown_ctx_t *ctx)
{
if (ctx->write_ptr)
ctx->output_len = (size_t)(ctx->write_ptr - ctx->output);
ctx->write_ptr = NULL;
ctx->write_end = NULL;
}
int fsm_append(sashidown_ctx_t *ctx, char c)
{
if (unlikely(ctx->overflow))
return -1;
if (ctx->write_ptr) {
if (unlikely(ctx->write_ptr >= ctx->write_end)) {
ctx->overflow = 1;
return -1;
}
*ctx->write_ptr++ = c;
return 0;
}
if (unlikely(ctx->output_len + 1 >= ctx->output_cap)) {
ctx->overflow = 1;
return -1;
}
ctx->output[ctx->output_len++] = c;
return 0;
}
int fsm_append_len(sashidown_ctx_t *restrict ctx, const char *restrict s, size_t n)
{
if (n == 0)
return 0;
if (unlikely(ctx->overflow))
return -1;
if (ctx->write_ptr) {
if (unlikely(ctx->write_ptr + n > ctx->write_end)) {
ctx->overflow = 1;
return -1;
}
memcpy(ctx->write_ptr, s, n);
ctx->write_ptr += n;
return 0;
}
if (unlikely(ctx->output_len + n >= ctx->output_cap)) {
ctx->overflow = 1;
return -1;
}
memcpy(ctx->output + ctx->output_len, s, n);
ctx->output_len += n;
return 0;
}
int fsm_append_str(sashidown_ctx_t *ctx, const char *s)
{
return fsm_append_len(ctx, s, strlen(s));
}
void fsm_append_fast(sashidown_ctx_t *ctx, char c)
{
if (unlikely(ctx->overflow)) return;
if (unlikely(ctx->write_ptr >= ctx->write_end)) {
ctx->overflow = 1;
return;
}
*ctx->write_ptr++ = c;
}
void fsm_append_len_fast(sashidown_ctx_t *restrict ctx, const char *restrict s, size_t n)
{
if (n == 0) return;
if (unlikely(ctx->overflow)) return;
if (unlikely(ctx->write_ptr + n > ctx->write_end)) {
ctx->overflow = 1;
return;
}
memcpy(ctx->write_ptr, s, n);
ctx->write_ptr += n;
}
+31
View File
@@ -0,0 +1,31 @@
#ifndef MD_FSM_H
#define MD_FSM_H
#include <stdint.h>
#include <stddef.h>
#include "sashidown.h"
#ifndef likely
#define likely(x) __builtin_expect(!!(x), 1)
#endif
#ifndef unlikely
#define unlikely(x) __builtin_expect(!!(x), 0)
#endif
extern const uint8_t CH_HTML_ESCAPE[256];
extern const uint8_t CH_SCAN_STOP[256];
extern const uint8_t CH_JSON_ESCAPE[256];
int fsm_append(sashidown_ctx_t *ctx, char c);
int fsm_append_str(sashidown_ctx_t *ctx, const char *s);
int fsm_append_len(sashidown_ctx_t *restrict ctx, const char *restrict s, size_t n);
void fsm_append_fast(sashidown_ctx_t *ctx, char c);
void fsm_append_len_fast(sashidown_ctx_t *restrict ctx, const char *restrict s, size_t n);
void fsm_write_begin(sashidown_ctx_t *ctx);
void fsm_write_end(sashidown_ctx_t *ctx);
#define fsm_append_lit(ctx, s) fsm_append_len((ctx), (s), sizeof(s) - 1)
#endif /* MD_FSM_H */
+184
View File
@@ -0,0 +1,184 @@
#include "line_index.h"
#include <stdlib.h>
#include <string.h>
#define LINE_IX_DYNAMIC_INIT 256
static size_t scan_line_end(const char *input, size_t len, size_t pos)
{
const char *hit = memchr(input + pos, '\n', len - pos);
return hit ? (size_t)(hit - input) : len;
}
static size_t scan_skip_line(const char *input, size_t len, size_t pos)
{
pos = scan_line_end(input, len, pos);
if (pos < len)
pos++;
return pos;
}
static int line_index_ensure(line_index_t *ix, uint32_t need_slots)
{
if (need_slots <= ix->cap)
return 0;
if (!ix->owned) {
ix->valid = 0;
return -1;
}
uint32_t cap = ix->cap ? ix->cap : LINE_IX_DYNAMIC_INIT;
while (cap < need_slots)
cap *= 2;
uint32_t *p = (uint32_t *)realloc(ix->off, (size_t)cap * sizeof(uint32_t));
if (!p) {
ix->valid = 0;
return -1;
}
ix->off = p;
ix->cap = cap;
return 0;
}
int line_index_init_dynamic(line_index_t *ix, uint32_t initial_cap)
{
if (!ix)
return -1;
if (initial_cap < 2)
initial_cap = LINE_IX_DYNAMIC_INIT;
uint32_t *off = (uint32_t *)malloc((size_t)initial_cap * sizeof(uint32_t));
if (!off)
return -1;
ix->off = off;
ix->cap = initial_cap;
ix->n = 1;
ix->off[0] = 0;
ix->off[1] = 0;
ix->cur = 0;
ix->valid = 1;
ix->owned = 1;
return 0;
}
void line_index_free(line_index_t *ix)
{
if (!ix)
return;
if (ix->owned) {
free(ix->off);
ix->off = NULL;
ix->owned = 0;
ix->cap = 0;
}
}
void line_index_build(const char *input, size_t len, line_index_t *ix)
{
ix->n = 1;
ix->off[0] = 0;
ix->cur = 0;
ix->valid = 1;
const char *start = input;
const char *end = input + len;
const char *hit;
while ((hit = memchr(start, '\n', (size_t)(end - start))) != NULL) {
if (ix->n + 1 >= ix->cap) {
if (line_index_ensure(ix, ix->n + 2) != 0)
return;
}
ix->off[ix->n++] = (uint32_t)(hit - input) + 1;
start = hit + 1;
}
if (ix->n >= ix->cap) {
if (line_index_ensure(ix, ix->n + 1) != 0)
return;
}
ix->off[ix->n] = (uint32_t)len;
}
size_t line_index_extend(line_index_t *ix, const char *input,
size_t old_len, size_t new_len)
{
if (!ix || !ix->valid || new_len <= old_len)
return 0;
size_t last_nl = 0;
const char *start = input + old_len;
const char *end = input + new_len;
const char *hit;
while ((hit = memchr(start, '\n', (size_t)(end - start))) != NULL) {
if (ix->n + 1 >= ix->cap) {
if (line_index_ensure(ix, ix->n + 2) != 0)
return last_nl;
}
last_nl = (size_t)(hit - input) + 1;
ix->off[ix->n++] = (uint32_t)last_nl;
start = hit + 1;
}
if (ix->n >= ix->cap) {
if (line_index_ensure(ix, ix->n + 1) != 0)
return last_nl;
}
ix->off[ix->n] = (uint32_t)new_len;
return last_nl;
}
static uint32_t line_no(const line_index_t *ix, size_t pos)
{
uint32_t lo = 0;
uint32_t hi = ix->n;
while (lo + 1 < hi) {
uint32_t mid = (lo + hi) / 2;
if (ix->off[mid] <= pos)
lo = mid;
else
hi = mid;
}
return lo;
}
void line_index_seek(line_index_t *ix, size_t pos)
{
if (!ix || !ix->valid)
return;
ix->cur = line_no(ix, pos);
}
static uint32_t resolve_line(line_index_t *ix, size_t pos)
{
/* Sequential hot path: render walks lines in order. */
if (ix->cur < ix->n && ix->off[ix->cur] == (uint32_t)pos)
return ix->cur;
if (ix->cur + 1 < ix->n && ix->off[ix->cur + 1] == (uint32_t)pos) {
ix->cur++;
return ix->cur;
}
ix->cur = line_no(ix, pos);
return ix->cur;
}
size_t line_end_ix(line_index_t *ix, const char *input, size_t len, size_t pos)
{
if (!ix || !ix->valid)
return scan_line_end(input, len, pos);
uint32_t k = resolve_line(ix, pos);
size_t next = (size_t)ix->off[k + 1];
if (next > 0 && next <= len && input[next - 1] == '\n')
return next - 1;
return next <= len ? next : len;
}
size_t skip_line_ix(line_index_t *ix, const char *input, size_t len, size_t pos)
{
if (!ix || !ix->valid)
return scan_skip_line(input, len, pos);
uint32_t k = resolve_line(ix, pos);
size_t next = (size_t)ix->off[k + 1];
if (next > len)
return len;
/* Advance cursor to the next line start (may equal n at EOF). */
ix->cur = k + 1;
return next;
}
+40
View File
@@ -0,0 +1,40 @@
#ifndef MD_LINE_INDEX_H
#define MD_LINE_INDEX_H
#include <stddef.h>
#include <stdint.h>
#define SASHIDOWN_MAX_LINE_INDEX 16384
typedef struct {
uint32_t *off; /* line-start offsets; off[n] is end sentinel */
uint32_t n; /* number of line starts (off[0]..off[n-1]) */
uint32_t cur; /* sequential cursor: line index for last lookup */
uint32_t cap; /* slots in off[] (must be >= n+1 when valid) */
int valid; /* 0 if doc exceeded index capacity / OOM */
int owned; /* 1 if off was malloc'd (stream path) */
} line_index_t;
/* Stack-backed index for zero-malloc parse/convert hot path. */
#define LINE_INDEX_STACK(name) \
uint32_t name##_off_[SASHIDOWN_MAX_LINE_INDEX + 1]; \
line_index_t name = { \
.off = name##_off_, .n = 0, .cur = 0, \
.cap = SASHIDOWN_MAX_LINE_INDEX + 1, .valid = 1, .owned = 0 \
}
int line_index_init_dynamic(line_index_t *ix, uint32_t initial_cap);
void line_index_free(line_index_t *ix);
void line_index_build(const char *input, size_t len, line_index_t *ix);
/* Extend for input[old_len..new_len). Returns absolute offset after the last
* newline in that range, or 0 if none. */
size_t line_index_extend(line_index_t *ix, const char *input,
size_t old_len, size_t new_len);
void line_index_seek(line_index_t *ix, size_t pos);
size_t line_end_ix(line_index_t *ix, const char *input, size_t len, size_t pos);
size_t skip_line_ix(line_index_t *ix, const char *input, size_t len, size_t pos);
#endif /* MD_LINE_INDEX_H */
+1202
View File
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
#ifndef MD_RENDER_H
#define MD_RENDER_H
#include "sashidown.h"
#include <stddef.h>
#include <stdint.h>
typedef enum {
LK_EMPTY,
LK_FENCE,
LK_HEADING,
LK_HR,
LK_BQ,
LK_UL,
LK_OL,
LK_LATEX_LINE,
LK_LATEX_BLOCK,
LK_PARA,
} line_kind_t;
typedef struct {
line_kind_t kind;
size_t content_start;
size_t content_end;
int heading_level;
int fence_len;
char fence_char; /* '`' or '~' */
size_t info_start; /* fence info string */
size_t info_end;
int task; /* 0=none, 1=unchecked, 2=checked */
} line_info_t;
/*
* [CST State Q] — open-block environment registers (finite CST state).
* Tracks which block containers are currently open so the transducer can
* emit close tags without an AST. Constant size; independent of |B|.
*/
typedef struct {
uint8_t ul;
uint8_t ol;
uint8_t bq;
uint8_t para;
} block_env_t;
/*
* [CST State Q] — full confirmation-state transducer state.
* Holds block_env plus soft-window / delayed-commit registers (fence,
* LaTeX, pending line, table gate). Maps to CST state Q in the paper:
* Scan(B[0..stop)) advances Q and emits only confirmed Y.
*/
typedef struct {
block_env_t block; /* [CST State Q] nested open-block bits */
int pending_valid;
size_t pending_pos;
size_t pending_le;
line_info_t pending_li;
int in_fence;
int fence_len;
char fence_char;
size_t fence_info_start;
size_t fence_info_end;
int in_latex_block;
size_t latex_start;
uint8_t para_hard_break;
/* Streaming table (scheme C): after header+sep committed, body rows
* emit immediately; close on empty / non-pipe / final. */
int in_table;
int table_cols;
/* Inline emphasis/code spans an open <p> across soft line breaks. */
uint8_t inline_bold;
uint8_t inline_italic;
uint8_t inline_code;
uint8_t inline_latex;
uint8_t inline_strike;
} sashidown_render_state_t;
void sashidown_render_state_init(sashidown_render_state_t *state);
/* Render input[start..stop) or full len when is_final. Updates *pos_io. */
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);
int sashidown_render(sashidown_ctx_t *ctx, const char *input, size_t len);
#endif /* MD_RENDER_H */
+559
View File
@@ -0,0 +1,559 @@
#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());
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef SASHIDOWN_RENDER_ANSI_H
#define SASHIDOWN_RENDER_ANSI_H
#include "sashidown.h"
/* Default: built-in colorful theme. */
int sashidown_render_ansi(sashidown_ctx_t *ctx, const char *input, size_t len);
/* Monochrome / attribute-only theme. */
int sashidown_render_ansi_plain(sashidown_ctx_t *ctx, const char *input, size_t len);
#endif
+620
View File
@@ -0,0 +1,620 @@
#include "render_json.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;
} json_inline_t;
typedef struct {
uint8_t ul;
uint8_t ol;
uint8_t bq;
uint8_t para;
int need_comma;
} json_env_t;
static void json_comma(sashidown_ctx_t *ctx, json_env_t *env)
{
if (env->need_comma)
fsm_append(ctx, ',');
env->need_comma = 1;
}
static void json_escape(sashidown_ctx_t *ctx, const char *s, size_t n)
{
size_t i = 0;
while (i < n) {
size_t j = i;
while (j < n) {
if (CH_JSON_ESCAPE[(unsigned char)s[j]])
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 '\n': fsm_append_lit(ctx, "\\n"); break;
case '\r': fsm_append_lit(ctx, "\\r"); break;
case '\t': fsm_append_lit(ctx, "\\t"); break;
default: {
char buf[7];
int m = snprintf(buf, sizeof(buf), "\\u%04x", (unsigned char)s[j]);
if (m > 0)
fsm_append_len(ctx, buf, (size_t)m);
break;
}
}
i = j + 1;
}
}
static void json_event_simple(sashidown_ctx_t *ctx, json_env_t *env, const char *t)
{
json_comma(ctx, env);
fsm_append_lit(ctx, "{\"t\":\"");
fsm_append_str(ctx, t);
fsm_append_lit(ctx, "\"}");
}
static void json_event_text(sashidown_ctx_t *ctx, json_env_t *env,
const char *s, size_t n)
{
if (n == 0)
return;
json_comma(ctx, env);
fsm_append_lit(ctx, "{\"t\":\"text\",\"v\":\"");
json_escape(ctx, s, n);
fsm_append_lit(ctx, "\"}");
}
static void json_event_heading(sashidown_ctx_t *ctx, json_env_t *env, int level)
{
json_comma(ctx, env);
char buf[48];
int n = snprintf(buf, sizeof(buf), "{\"t\":\"heading\",\"level\":%d}", level);
if (n > 0)
fsm_append_len(ctx, buf, (size_t)n);
}
static void json_close_lists(sashidown_ctx_t *ctx, json_env_t *env)
{
if (env->ul) {
json_event_simple(ctx, env, "ul_end");
env->ul = 0;
}
if (env->ol) {
json_event_simple(ctx, env, "ol_end");
env->ol = 0;
}
}
static void json_close_para(sashidown_ctx_t *ctx, json_env_t *env)
{
if (env->para) {
json_event_simple(ctx, env, "para_end");
env->para = 0;
}
}
static void json_close_bq(sashidown_ctx_t *ctx, json_env_t *env)
{
if (env->bq) {
json_event_simple(ctx, env, "bq_end");
env->bq = 0;
}
}
static void json_close_all(sashidown_ctx_t *ctx, json_env_t *env)
{
json_close_lists(ctx, env);
json_close_para(ctx, env);
json_close_bq(ctx, env);
}
static int json_render_link(sashidown_ctx_t *ctx, json_env_t *env,
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;
json_comma(ctx, env);
if (is_image)
fsm_append_lit(ctx, "{\"t\":\"image\",\"alt\":\"");
else
fsm_append_lit(ctx, "{\"t\":\"link\",\"text\":\"");
json_escape(ctx, input + text_start, text_end - text_start);
fsm_append_lit(ctx, "\",\"href\":\"");
json_escape(ctx, input + url_start, i - url_start);
fsm_append_lit(ctx, "\"}");
*pos = i + 1;
return 1;
}
static void json_flush_text(sashidown_ctx_t *ctx, json_env_t *env,
const char *input, size_t *text_start, size_t i)
{
if (i > *text_start)
json_event_text(ctx, env, input + *text_start, i - *text_start);
*text_start = i;
}
static void json_render_inline(sashidown_ctx_t *ctx, json_env_t *env,
const char *input, size_t len,
size_t start, size_t end)
{
json_inline_t st = {0, 0, 0, 0, 0};
size_t i = start;
size_t text_start = start;
while (i < end) {
if (st.latex) {
size_t j = block_scan_until_byte(input, end, i, '$');
if (j < end) {
json_flush_text(ctx, env, input, &text_start, i);
/* include opening $ already emitted into latex stream via text */
json_event_text(ctx, env, input + i - 1, (j + 1) - (i - 1));
st.latex = 0;
i = j + 1;
text_start = i;
} else {
i = j;
}
continue;
}
if (st.code) {
size_t j = block_scan_until_byte(input, end, i, '`');
if (j > i)
json_event_text(ctx, env, input + i, j - i);
if (j < end) {
json_event_simple(ctx, env, "code_off");
st.code = 0;
i = j + 1;
text_start = i;
} else {
i = j;
text_start = i;
}
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) {
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) {
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;
json_flush_text(ctx, env, input, &text_start, i);
json_event_text(ctx, env, input + i, j - i);
i = j;
text_start = i;
continue;
}
if (ch == '$') {
json_flush_text(ctx, env, input, &text_start, i);
st.latex = 1;
i++;
text_start = i;
continue;
}
if (ch == '!' && i + 1 < end && input[i + 1] == '[') {
size_t save = i;
json_flush_text(ctx, env, input, &text_start, i);
if (json_render_link(ctx, env, input, len, &i, 1)) {
text_start = i;
continue;
}
i = save;
}
if (ch == '[') {
size_t save = i;
json_flush_text(ctx, env, input, &text_start, i);
if (json_render_link(ctx, env, input, len, &i, 0)) {
text_start = i;
continue;
}
i = save;
}
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)) {
json_flush_text(ctx, env, input, &text_start, i);
json_event_text(ctx, env, input + i + 1, 1);
i += 2;
text_start = i;
continue;
}
}
if ((ch == '*' || ch == '_') && i + 1 < end && input[i + 1] == (char)ch) {
json_flush_text(ctx, env, input, &text_start, i);
json_event_simple(ctx, env, st.bold ? "strong_off" : "strong_on");
st.bold = !st.bold;
i += 2;
text_start = i;
continue;
}
if (ch == '~' && i + 1 < end && input[i + 1] == '~') {
json_flush_text(ctx, env, input, &text_start, i);
json_event_simple(ctx, env, st.strike ? "strike_off" : "strike_on");
st.strike = !st.strike;
i += 2;
text_start = i;
continue;
}
if (ch == '*' || ch == '_') {
json_flush_text(ctx, env, input, &text_start, i);
json_event_simple(ctx, env, st.italic ? "emph_off" : "emph_on");
st.italic = !st.italic;
i++;
text_start = i;
continue;
}
if (ch == '`') {
json_flush_text(ctx, env, input, &text_start, i);
json_event_simple(ctx, env, "code_on");
st.code = 1;
i++;
text_start = i;
continue;
}
/* special char that was a scan stop — keep accumulating as text */
i++;
}
json_flush_text(ctx, env, input, &text_start, i);
if (st.bold) json_event_simple(ctx, env, "strong_off");
if (st.italic) json_event_simple(ctx, env, "emph_off");
if (st.code) json_event_simple(ctx, env, "code_off");
if (st.strike) json_event_simple(ctx, env, "strike_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 json_table_row_at(sashidown_ctx_t *ctx, json_env_t *env,
const char *input, size_t len,
size_t pos, size_t le, int cols, int is_header)
{
json_event_simple(ctx, env, is_header ? "tr_header" : "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++;
size_t ce = i;
trim_cell(input, &cs, &ce);
json_event_simple(ctx, env, is_header ? "th" : "td");
json_render_inline(ctx, env, input, len, cs, ce);
json_event_simple(ctx, env, is_header ? "th_end" : "td_end");
if (i < le && input[i] == '|') i++;
}
json_event_simple(ctx, env, "tr_end");
}
/* Scheme C (batch): header+sep commit, body one-pass. */
static int json_try_consume_table(sashidown_ctx_t *ctx, json_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;
}
json_close_all(ctx, env);
json_event_simple(ctx, env, "table");
json_table_row_at(ctx, env, 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;
json_table_row_at(ctx, env, input, len, pos, ble, cols, 0);
pos = block_skip_line(ctx, input, len, pos);
}
json_event_simple(ctx, env, "table_end");
*pos_io = pos;
return 1;
}
static void json_render_codeblock(sashidown_ctx_t *ctx, json_env_t *env,
const char *input, size_t len,
size_t *pos, int fence_len, char fence_char)
{
json_event_simple(ctx, env, "fence");
*pos = block_skip_line(ctx, input, len, *pos);
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);
json_event_text(ctx, env, input + *pos, le - *pos);
if (le < len)
json_event_text(ctx, env, "\n", 1);
*pos = block_skip_line(ctx, input, len, *pos);
}
json_event_simple(ctx, env, "fence_end");
}
static void json_render_latex_block(sashidown_ctx_t *ctx, json_env_t *env,
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] == '$') {
json_comma(ctx, env);
fsm_append_lit(ctx, "{\"t\":\"latex_block\",\"v\":\"");
json_escape(ctx, input + start, i - start);
fsm_append_lit(ctx, "\"}");
i += 2;
if (i < len && input[i] == '\n') i++;
*pos = i;
return;
}
i++;
}
json_comma(ctx, env);
fsm_append_lit(ctx, "{\"t\":\"latex_block\",\"v\":\"");
json_escape(ctx, input + start, (i < len ? i : len) - start);
fsm_append_lit(ctx, "\"}");
*pos = len;
}
int sashidown_render_json(sashidown_ctx_t *ctx, const char *input, size_t len)
{
json_env_t env;
memset(&env, 0, sizeof(env));
size_t pos = 0;
fsm_write_begin(ctx);
fsm_append(ctx, '[');
while (pos < len) {
size_t le = block_line_end(ctx, input, len, pos);
if (pos == le || (pos + 1 == le && input[pos] == '\r')) {
json_close_all(ctx, &env);
pos = block_skip_line(ctx, input, len, pos);
continue;
}
if (memchr(input + pos, '|', le - pos) != NULL) {
size_t newt = 0;
if (json_try_consume_table(ctx, &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) {
json_close_all(ctx, &env);
json_event_heading(ctx, &env, sli.heading_level);
json_render_inline(ctx, &env, input, len, sli.content_start, sli.content_end);
json_event_simple(ctx, &env, "heading_end");
pos = send;
block_line_seek(ctx, pos);
continue;
}
}
switch (li.kind) {
case LK_EMPTY:
json_close_all(ctx, &env);
pos = block_skip_line(ctx, input, len, pos);
break;
case LK_HR:
json_close_all(ctx, &env);
json_event_simple(ctx, &env, "hr");
pos = block_skip_line(ctx, input, len, pos);
break;
case LK_FENCE:
json_close_all(ctx, &env);
json_render_codeblock(ctx, &env, input, len, &pos, li.fence_len, li.fence_char);
break;
case LK_LATEX_LINE:
json_close_all(ctx, &env);
json_comma(ctx, &env);
fsm_append_lit(ctx, "{\"t\":\"latex_block\",\"v\":\"");
json_escape(ctx, input + li.content_start,
li.content_end - li.content_start);
fsm_append_lit(ctx, "\"}");
pos = block_skip_line(ctx, input, len, pos);
break;
case LK_LATEX_BLOCK:
json_close_all(ctx, &env);
json_render_latex_block(ctx, &env, input, len, &pos);
break;
case LK_HEADING:
json_close_all(ctx, &env);
json_event_heading(ctx, &env, li.heading_level);
json_render_inline(ctx, &env, input, len, li.content_start, le);
json_event_simple(ctx, &env, "heading_end");
pos = block_skip_line(ctx, input, len, pos);
break;
case LK_BQ:
json_close_lists(ctx, &env);
json_close_para(ctx, &env);
if (!env.bq) {
json_event_simple(ctx, &env, "bq");
env.bq = 1;
}
json_event_simple(ctx, &env, "para");
json_render_inline(ctx, &env, input, len, li.content_start, le);
json_event_simple(ctx, &env, "para_end");
pos = block_skip_line(ctx, input, len, pos);
break;
case LK_UL:
json_close_para(ctx, &env);
json_close_bq(ctx, &env);
if (env.ol) {
json_event_simple(ctx, &env, "ol_end");
env.ol = 0;
}
if (!env.ul) {
json_event_simple(ctx, &env, "ul");
env.ul = 1;
}
json_event_simple(ctx, &env, "li");
if (li.task == 1)
json_event_simple(ctx, &env, "task_unchecked");
else if (li.task == 2)
json_event_simple(ctx, &env, "task_checked");
json_render_inline(ctx, &env, input, len, li.content_start, le);
json_event_simple(ctx, &env, "li_end");
pos = block_skip_line(ctx, input, len, pos);
break;
case LK_OL:
json_close_para(ctx, &env);
json_close_bq(ctx, &env);
if (env.ul) {
json_event_simple(ctx, &env, "ul_end");
env.ul = 0;
}
if (!env.ol) {
json_event_simple(ctx, &env, "ol");
env.ol = 1;
}
json_event_simple(ctx, &env, "li");
json_render_inline(ctx, &env, input, len, li.content_start, le);
json_event_simple(ctx, &env, "li_end");
pos = block_skip_line(ctx, input, len, pos);
break;
case LK_PARA:
default:
json_close_bq(ctx, &env);
json_close_lists(ctx, &env);
if (!env.para) {
json_event_simple(ctx, &env, "para");
env.para = 1;
} else {
json_event_text(ctx, &env, "\n", 1);
}
json_render_inline(ctx, &env, input, len, pos, le);
pos = block_skip_line(ctx, input, len, pos);
if (pos >= len) {
json_close_para(ctx, &env);
} 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)
json_close_para(ctx, &env);
}
break;
}
if (ctx->overflow) {
fsm_write_end(ctx);
return -1;
}
}
json_close_all(ctx, &env);
fsm_append(ctx, ']');
fsm_write_end(ctx);
return ctx->overflow ? -1 : 0;
}
+8
View File
@@ -0,0 +1,8 @@
#ifndef SASHIDOWN_RENDER_JSON_H
#define SASHIDOWN_RENDER_JSON_H
#include "sashidown.h"
int sashidown_render_json(sashidown_ctx_t *ctx, const char *input, size_t len);
#endif
+96
View File
@@ -0,0 +1,96 @@
#include "sashidown.h"
#include "line_index.h"
#include "fsm.h"
#include "render.h"
#include "render_ansi.h"
#include "render_json.h"
#include <string.h>
int sashidown_parse(const char *input, size_t len,
char *output, size_t cap, size_t *out_len)
{
if (!input || !output || cap == 0)
return -1;
LINE_INDEX_STACK(ix);
line_index_build(input, len, &ix);
sashidown_ctx_t ctx = {
.input = input,
.input_len = len,
.output = output,
.output_cap = cap,
.output_len = 0,
.write_ptr = NULL,
.write_end = NULL,
.line_ix = &ix,
.overflow = 0,
};
if (sashidown_render(&ctx, input, len) != 0) {
fsm_write_end(&ctx);
return -1;
}
if (ctx.overflow || ctx.output_len + 1 > cap)
return -1;
output[ctx.output_len] = '\0';
if (out_len)
*out_len = ctx.output_len;
return 0;
}
static int convert_with_render(const char *input, size_t len,
char *output, size_t cap, size_t *out_len,
int (*render_fn)(sashidown_ctx_t *, const char *, size_t))
{
if (!input || !output || cap == 0)
return -1;
LINE_INDEX_STACK(ix);
line_index_build(input, len, &ix);
sashidown_ctx_t ctx = {
.input = input,
.input_len = len,
.output = output,
.output_cap = cap,
.output_len = 0,
.write_ptr = NULL,
.write_end = NULL,
.line_ix = &ix,
.overflow = 0,
};
if (render_fn(&ctx, input, len) != 0) {
fsm_write_end(&ctx);
return -1;
}
if (ctx.overflow || ctx.output_len + 1 > cap)
return -1;
output[ctx.output_len] = '\0';
if (out_len)
*out_len = ctx.output_len;
return 0;
}
int sashidown_convert(const char *input, size_t len,
char *output, size_t cap, size_t *out_len,
sashidown_format_t format)
{
switch (format) {
case SASHIDOWN_FMT_HTML:
return sashidown_parse(input, len, output, cap, out_len);
case SASHIDOWN_FMT_ANSI:
return convert_with_render(input, len, output, cap, out_len, sashidown_render_ansi);
case SASHIDOWN_FMT_ANSI_PLAIN:
return convert_with_render(input, len, output, cap, out_len, sashidown_render_ansi_plain);
case SASHIDOWN_FMT_JSON:
return convert_with_render(input, len, output, cap, out_len, sashidown_render_json);
default:
return -1;
}
}
+364
View File
@@ -0,0 +1,364 @@
#include "sashidown.h"
#include "line_index.h"
#include "fsm.h"
#include "render.h"
#include <stdlib.h>
#include <string.h>
#define STREAM_INPUT_INIT 4096
#define STREAM_OUTPUT_INIT 8192
#define STREAM_OUTPUT_MAX (64u * 1024u * 1024u)
#define STREAM_LINE_IX_INIT 256
struct sashidown_stream {
char *input_buf;
size_t input_len;
size_t input_cap;
size_t indexed_len; /* bytes already in line_ix */
size_t render_pos;
size_t last_complete_nl; /* [0, last_complete_nl) ends on line boundary */
sashidown_render_state_t render_state;
line_index_t line_ix;
char *output_buf;
size_t output_cap;
sashidown_ctx_t render_ctx;
sashidown_stream_cb on_html;
void *user;
int finished;
int aborted;
};
static int stream_grow_input(sashidown_stream_t *s, size_t need)
{
if (need <= s->input_cap)
return 0;
size_t cap = s->input_cap ? s->input_cap : STREAM_INPUT_INIT;
while (cap < need)
cap *= 2;
char *p = (char *)realloc(s->input_buf, cap);
if (!p)
return -1;
s->input_buf = p;
s->input_cap = cap;
return 0;
}
static int stream_grow_output(sashidown_stream_t *s, size_t need)
{
if (need <= s->output_cap)
return 0;
size_t cap = s->output_cap ? s->output_cap : STREAM_OUTPUT_INIT;
while (cap < need)
cap *= 2;
if (cap > STREAM_OUTPUT_MAX)
cap = STREAM_OUTPUT_MAX;
if (cap <= s->output_cap)
return -1;
char *p = (char *)realloc(s->output_buf, cap);
if (!p)
return -1;
s->output_buf = p;
s->output_cap = cap;
return 0;
}
static int stream_flush_html(sashidown_stream_t *s)
{
sashidown_ctx_t *ctx = &s->render_ctx;
if (ctx->output_len == 0)
return 0;
if (!s->on_html)
return 0;
int r = s->on_html(ctx->output, ctx->output_len, s->user);
if (r != 0) {
s->aborted = 1;
return -1;
}
ctx->output_len = 0;
return 0;
}
/* Sync line index through input_len; update last_complete_nl. */
static void stream_catchup_index(sashidown_stream_t *s)
{
if (s->indexed_len >= s->input_len)
return;
if (s->indexed_len == 0) {
line_index_build(s->input_buf, s->input_len, &s->line_ix);
size_t last = 0;
if (s->line_ix.valid && s->line_ix.n > 1) {
last = (size_t)s->line_ix.off[s->line_ix.n - 1];
} else {
const char *p = s->input_buf;
const char *end = s->input_buf + s->input_len;
const char *hit;
while ((hit = memchr(p, '\n', (size_t)(end - p))) != NULL) {
last = (size_t)(hit - s->input_buf) + 1;
p = hit + 1;
}
}
if (last)
s->last_complete_nl = last;
} else {
size_t nl = line_index_extend(&s->line_ix, s->input_buf,
s->indexed_len, s->input_len);
if (nl)
s->last_complete_nl = nl;
}
s->indexed_len = s->input_len;
}
/* 1 = overflow (state restored), 0 = ok, -1 = error */
static int stream_render_attempt(sashidown_stream_t *s, size_t stop, int is_final)
{
sashidown_ctx_t *ctx = &s->render_ctx;
size_t saved_pos = s->render_pos;
sashidown_render_state_t saved_state = s->render_state;
ctx->output_len = 0;
ctx->overflow = 0;
fsm_write_begin(ctx);
int r = sashidown_render_incremental(ctx, &s->render_state,
s->input_buf, s->input_len,
&s->render_pos, stop, is_final);
fsm_write_end(ctx);
if (ctx->overflow) {
s->render_pos = saved_pos;
s->render_state = saved_state;
return 1;
}
if (r != 0)
return -1;
return stream_flush_html(s);
}
static size_t stream_soft_stop(sashidown_stream_t *s, size_t from, size_t target)
{
if (from >= target)
return target;
/* Keep each attempt roughly within half the output buffer. */
size_t soft = from + s->output_cap / 2;
if (soft >= target)
return target;
const char *base = s->input_buf + from;
size_t lim = soft - from;
const char *p = base;
const char *end = base + lim;
const char *last = NULL;
const char *hit;
while ((hit = memchr(p, '\n', (size_t)(end - p))) != NULL) {
last = hit;
p = hit + 1;
}
if (last)
return (size_t)(last - s->input_buf) + 1;
/* No newline in soft window — take the next one so we still make progress. */
hit = memchr(s->input_buf + from, '\n', target - from);
if (hit)
return (size_t)(hit - s->input_buf) + 1;
return target;
}
static size_t stream_next_line_stop(sashidown_stream_t *s, size_t from, size_t limit)
{
if (from >= limit)
return limit;
const char *hit = memchr(s->input_buf + from, '\n', limit - from);
if (hit)
return (size_t)(hit - s->input_buf) + 1;
return limit;
}
/* Render up to pass_stop (is_final as given). Grow/shrink on overflow.
* Returns 0 on success, -1 on error. */
static int stream_render_window(sashidown_stream_t *s, size_t pass_stop, int is_final)
{
sashidown_ctx_t *ctx = &s->render_ctx;
for (;;) {
int ar = stream_render_attempt(s, pass_stop, is_final);
if (ar == 0)
return 0;
if (ar < 0)
return -1;
if (stream_grow_output(s, s->output_cap * 2) == 0) {
ctx->output = s->output_buf;
ctx->output_cap = s->output_cap;
continue;
}
size_t one = stream_next_line_stop(s, s->render_pos, pass_stop);
if (one <= s->render_pos || one >= pass_stop)
return -1;
pass_stop = one;
is_final = 0;
}
}
static int stream_render_pass(sashidown_stream_t *s, size_t stop, int is_final)
{
if (s->aborted)
return -1;
if (!is_final && stop <= s->render_pos)
return 0;
sashidown_ctx_t *ctx = &s->render_ctx;
ctx->input = s->input_buf;
ctx->input_len = s->input_len;
size_t target = is_final ? s->input_len : stop;
/* Common case: one or a few lines already fit in the output buffer. */
if (target - s->render_pos <= s->output_cap / 4) {
int pass_final = is_final && (target >= s->input_len);
size_t before = s->render_pos;
if (stream_render_window(s, target, pass_final) != 0)
return -1;
if (s->render_pos > before || pass_final)
return (is_final && !pass_final)
? stream_render_window(s, s->input_len, 1)
: 0;
if (!is_final)
return 0; /* deferred (incomplete table, etc.) */
/* fall through to spanned finalization */
}
while (s->render_pos < target) {
size_t before = s->render_pos;
size_t pass_stop = stream_soft_stop(s, s->render_pos, target);
int pass_final = is_final && (pass_stop >= s->input_len);
if (stream_render_window(s, pass_stop, pass_final) != 0)
return -1;
if (s->render_pos > before) {
if (pass_final)
return 0;
continue;
}
/* No progress: soft window may have deferred a table. Retry full target. */
if (pass_stop < target) {
pass_final = is_final && (target >= s->input_len);
if (stream_render_window(s, target, pass_final) != 0)
return -1;
if (s->render_pos > before) {
if (pass_final)
return 0;
continue;
}
}
/* Still stuck — waiting for more input (incomplete table, etc.). */
if (!is_final)
return 0;
return -1;
}
if (!is_final)
return 0;
/* EOF reached without a finalizing pass — close open blocks. */
return stream_render_window(s, s->input_len, 1);
}
sashidown_stream_t *sashidown_stream_create(const sashidown_stream_opts_t *opts)
{
if (!opts || !opts->on_html)
return NULL;
sashidown_stream_t *s = (sashidown_stream_t *)calloc(1, sizeof(*s));
if (!s)
return NULL;
size_t out_cap = opts->output_buf_size ? opts->output_buf_size : STREAM_OUTPUT_INIT;
if (out_cap > STREAM_OUTPUT_MAX)
out_cap = STREAM_OUTPUT_MAX;
s->output_cap = out_cap;
s->output_buf = (char *)malloc(s->output_cap);
if (!s->output_buf) {
free(s);
return NULL;
}
if (line_index_init_dynamic(&s->line_ix, STREAM_LINE_IX_INIT) != 0) {
free(s->output_buf);
free(s);
return NULL;
}
s->on_html = opts->on_html;
s->user = opts->user;
sashidown_render_state_init(&s->render_state);
s->render_ctx.output = s->output_buf;
s->render_ctx.output_cap = s->output_cap;
s->render_ctx.line_ix = &s->line_ix;
return s;
}
int sashidown_stream_feed(sashidown_stream_t *s, const char *chunk, size_t len)
{
if (!s || s->aborted)
return -1;
if (s->finished)
return 0;
if (!chunk && len > 0)
return -1;
if (len == 0)
return 0;
int has_nl = (memchr(chunk, '\n', len) != NULL);
if (stream_grow_input(s, s->input_len + len) != 0)
return -1;
memcpy(s->input_buf + s->input_len, chunk, len);
s->input_len += len;
/* Tiny-chunk fast path: no newline → defer index + render. */
if (!has_nl)
return 0;
stream_catchup_index(s);
if (s->last_complete_nl <= s->render_pos)
return 0;
return stream_render_pass(s, s->last_complete_nl, 0);
}
int sashidown_stream_finish(sashidown_stream_t *s)
{
if (!s || s->aborted)
return -1;
if (s->finished)
return 0;
stream_catchup_index(s);
int r = stream_render_pass(s, s->input_len, 1);
s->finished = 1;
return r;
}
void sashidown_stream_destroy(sashidown_stream_t *s)
{
if (!s)
return;
free(s->input_buf);
free(s->output_buf);
line_index_free(&s->line_ix);
free(s);
}
size_t sashidown_stream_pending(const sashidown_stream_t *s)
{
if (!s || s->input_len <= s->render_pos)
return 0;
return s->input_len - s->render_pos;
}
+137
View File
@@ -0,0 +1,137 @@
#include "table_stats.h"
#include "line_index.h"
#include <string.h>
int table_is_separator_row(const char *line, size_t len)
{
int pipes = 0;
int dashes = 0;
for (size_t i = 0; i < len; i++) {
char c = line[i];
if (c == '|') pipes++;
else if (c == '-' || c == ':' || c == ' ') dashes++;
else if (c != '\r') return 0;
}
return pipes >= 1 && dashes > 0;
}
/* GFM: leading/trailing '|' are delimiters, not empty cells. */
int table_count_cols(const char *line, size_t len, int *has_pipe)
{
if (has_pipe)
*has_pipe = 0;
while (len > 0 && line[len - 1] == '\r')
len--;
size_t start = 0;
size_t end = len;
while (start < end && (line[start] == ' ' || line[start] == '\t'))
start++;
if (start < end && line[start] == '|') {
start++;
if (has_pipe)
*has_pipe = 1;
}
while (end > start && (line[end - 1] == ' ' || line[end - 1] == '\t'))
end--;
if (end > start && line[end - 1] == '|') {
end--;
if (has_pipe)
*has_pipe = 1;
}
if (start >= end)
return 1;
int cols = 1;
for (size_t i = start; i < end; i++) {
if (line[i] == '|') {
cols++;
if (has_pipe)
*has_pipe = 1;
}
}
return cols > SASHIDOWN_MAX_COLS ? SASHIDOWN_MAX_COLS : cols;
}
static size_t te_line_end(sashidown_ctx_t *ctx, const char *input, size_t len, size_t pos)
{
if (ctx && ctx->line_ix)
return line_end_ix((line_index_t *)ctx->line_ix, input, len, pos);
const char *hit = memchr(input + pos, '\n', len - pos);
return hit ? (size_t)(hit - input) : len;
}
static size_t te_skip_line(sashidown_ctx_t *ctx, const char *input, size_t len, size_t pos)
{
if (ctx && ctx->line_ix)
return skip_line_ix((line_index_t *)ctx->line_ix, input, len, pos);
pos = te_line_end(NULL, input, len, pos);
if (pos < len)
pos++;
return pos;
}
static int line_has_pipe_raw(const char *line, size_t len)
{
return memchr(line, '|', len) != NULL;
}
static int try_parse_table(sashidown_ctx_t *ctx, const char *input, size_t len,
size_t line_start, table_entry_t *e)
{
size_t le = te_line_end(ctx, input, len, line_start);
int has_pipe = 0;
int cols = table_count_cols(input + line_start, le - line_start, &has_pipe);
if (!has_pipe)
return 0;
size_t sep = te_skip_line(ctx, input, len, line_start);
if (sep >= len) {
if (ctx && ctx->line_ix)
line_index_seek((line_index_t *)ctx->line_ix, line_start);
return 0;
}
size_t sep_le = te_line_end(ctx, input, len, sep);
if (!table_is_separator_row(input + sep, sep_le - sep)) {
if (ctx && ctx->line_ix)
line_index_seek((line_index_t *)ctx->line_ix, line_start);
return 0;
}
size_t pos = te_skip_line(ctx, input, len, sep);
e->start = line_start;
e->body_start = pos;
e->cols = cols; /* GFM: header determines column count */
e->body_rows = 0;
e->valid = 1;
while (pos < len) {
le = te_line_end(ctx, input, len, pos);
size_t line_len = le - pos;
if (!line_has_pipe_raw(input + pos, line_len))
break;
if (table_is_separator_row(input + pos, line_len))
break;
e->body_rows++;
pos = te_skip_line(ctx, input, len, pos);
}
e->end = pos;
return 1;
}
int table_try_parse_at(sashidown_ctx_t *ctx, const char *input, size_t len,
size_t pos, table_entry_t *e)
{
e->start = 0;
e->end = 0;
e->body_start = 0;
e->cols = 0;
e->body_rows = 0;
e->valid = 0;
return try_parse_table(ctx, input, len, pos, e);
}
+32
View File
@@ -0,0 +1,32 @@
#ifndef MD_TABLE_STATS_H
#define MD_TABLE_STATS_H
#include "sashidown.h"
#include <stddef.h>
#include <stdint.h>
#define SASHIDOWN_MAX_COLS 32
/*
* [CST Pending Window Metadata] — lightweight table descriptor for the
* delayed-commitment pending window. Records header/separator/body bounds
* without caching cells; render rescans once after the separator enters
* the confirmation frontier. No per-cell heap; O(1) registers only.
*/
typedef struct {
size_t start; /* header line start */
size_t end; /* first byte after the table */
size_t body_start; /* first body row (after separator) */
int cols; /* column count from header (GFM) */
int body_rows;
int valid;
} table_entry_t;
int table_is_separator_row(const char *line, size_t len);
int table_count_cols(const char *line, size_t len, int *has_pipe);
/* ctx may be NULL (falls back to memchr); when non-NULL reuses line_index. */
int table_try_parse_at(sashidown_ctx_t *ctx, const char *input, size_t len,
size_t pos, table_entry_t *e);
#endif /* MD_TABLE_STATS_H */