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:
Vendored
+16
@@ -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)
|
||||
Vendored
+64
@@ -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 */
|
||||
Vendored
+104
@@ -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;
|
||||
}
|
||||
Vendored
+48
@@ -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
|
||||
Vendored
+337
@@ -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;
|
||||
}
|
||||
Vendored
+196
@@ -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 */
|
||||
Vendored
+44
@@ -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);
|
||||
}
|
||||
Vendored
+110
@@ -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;
|
||||
}
|
||||
Vendored
+31
@@ -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 */
|
||||
Vendored
+184
@@ -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;
|
||||
}
|
||||
Vendored
+40
@@ -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 */
|
||||
Vendored
+1202
File diff suppressed because it is too large
Load Diff
Vendored
+90
@@ -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 */
|
||||
Vendored
+559
@@ -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());
|
||||
}
|
||||
Vendored
+11
@@ -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
|
||||
Vendored
+620
@@ -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;
|
||||
}
|
||||
Vendored
+8
@@ -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
|
||||
Vendored
+96
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+364
@@ -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;
|
||||
}
|
||||
Vendored
+137
@@ -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);
|
||||
}
|
||||
Vendored
+32
@@ -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 */
|
||||
Reference in New Issue
Block a user