861 lines
26 KiB
C
861 lines
26 KiB
C
/* markdown.c - 流式 Markdown 渲染状态机
|
|
*
|
|
* 核心约束: SSE 的分片位置是任意的, 一个标记 (`**`、行首的 `#`、`[`)
|
|
* 完全可能被切成两片. 因此遇到"可能是标记前缀"的尾部时不能立即输出,
|
|
* 必须把这段字节留在缓冲区里等下一个分片凑齐后再判定.
|
|
*/
|
|
#include "markdown.h"
|
|
#include "sgr.h"
|
|
#include "util.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define MD_RESET C_OFF
|
|
#define MD_BOLD C_BOLD
|
|
#define MD_DIM C_DIM
|
|
#define MD_ITALIC C_ITALIC
|
|
#define MD_CODE C_CYAN
|
|
#define MD_BULLET C_CYAN
|
|
#define MD_LINK C_UNDERLINE C_CYAN
|
|
#define MD_H1 C_BOLD C_CYAN
|
|
#define MD_H2 C_BOLD C_MAGENTA
|
|
#define MD_H3 C_BOLD C_BLUE
|
|
#define MD_H4 C_BOLD
|
|
|
|
/* 等待更多数据 (当前字节可能是一个被切断的标记) */
|
|
#define MD_NEED_MORE ((size_t)-1)
|
|
|
|
/* 待处理缓冲上限: 遇到始终等不到结束标记的超长行时直接吐出, 保证内存有界 */
|
|
#define MD_MAX_PENDING (64 * 1024)
|
|
|
|
/* 候选表格行超过这个长度就不再当表格等下去, 避免长行被卡住 */
|
|
#define MD_MAX_TABLE_LINE 4096
|
|
|
|
/* 表格累积多少行后强制渲染 (防止超大表格一直不输出) */
|
|
#define MD_MAX_TABLE_ROWS 256
|
|
|
|
struct MdRenderer {
|
|
int enabled;
|
|
FILE *out;
|
|
StrBuf in; /* 跨分片的残留数据 */
|
|
|
|
int line_start; /* 下一个字符是否位于行首 (块级语法只在行首生效) */
|
|
int line_style; /* 本行处于行级样式 (标题)中, 换行时要复位 */
|
|
int last_ch; /* 上一个已消费的输入字节, 用于 _ 和 [ 的边界判断 */
|
|
|
|
int in_fence; /* 位于围栏代码块内 */
|
|
char fence_char;
|
|
int fence_len;
|
|
|
|
int in_bold;
|
|
int in_italic;
|
|
int in_code; /* 行内代码 */
|
|
int in_bracket; /* 位于 [链接文字] 内 */
|
|
int bracket_len;
|
|
|
|
int in_table; /* 正在累积一个表格 */
|
|
StrBuf table; /* 表格原始行 */
|
|
int table_rows;
|
|
|
|
/* 置位后表示"不会再有新数据", 所有挂起都必须就地解开.
|
|
* 这是"输出与分片方式无关"的关键: 挂起只允许发生在还有后续数据时,
|
|
* 到 EOF 一律按固定规则判定, 决策因此只依赖字节流本身. */
|
|
int eof;
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 输出原语 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void put(MdRenderer *r, const char *s, size_t n) {
|
|
if (n)
|
|
fwrite(s, 1, n, r->out);
|
|
}
|
|
|
|
static void putc_str(MdRenderer *r, const char *s) { fputs(s, r->out); }
|
|
|
|
static void style(MdRenderer *r, const char *code) {
|
|
if (r->enabled)
|
|
fputs(code, r->out);
|
|
}
|
|
|
|
/* 行尾清掉所有行内/行级样式, 避免染到下一行 */
|
|
static void close_line_styles(MdRenderer *r) {
|
|
if (r->in_bold || r->in_italic || r->in_code || r->in_bracket ||
|
|
r->line_style) {
|
|
style(r, MD_RESET);
|
|
r->in_bold = r->in_italic = r->in_code = 0;
|
|
r->in_bracket = 0;
|
|
r->bracket_len = 0;
|
|
r->line_style = 0;
|
|
}
|
|
}
|
|
|
|
/* 显示宽度相关工具已移到 util.c (行编辑器也要用) */
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 表格: 解析 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
typedef struct {
|
|
char **cell;
|
|
size_t n;
|
|
} TblRow;
|
|
|
|
typedef enum { ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT } TblAlign;
|
|
|
|
static void row_free(TblRow *row) {
|
|
for (size_t i = 0; i < row->n; i++)
|
|
free(row->cell[i]);
|
|
free(row->cell);
|
|
row->cell = NULL;
|
|
row->n = 0;
|
|
}
|
|
|
|
/* 按 '|' 拆分一行, 去掉首尾空白与首尾空单元格 */
|
|
static void row_split(const char *line, size_t n, TblRow *out) {
|
|
out->cell = NULL;
|
|
out->n = 0;
|
|
size_t cap = 0;
|
|
|
|
while (n > 0 && (line[0] == ' ' || line[0] == '\t')) {
|
|
line++;
|
|
n--;
|
|
}
|
|
while (n > 0 && (line[n - 1] == ' ' || line[n - 1] == '\t'))
|
|
n--;
|
|
if (n > 0 && line[0] == '|') {
|
|
line++;
|
|
n--;
|
|
}
|
|
if (n > 0 && line[n - 1] == '|')
|
|
n--;
|
|
|
|
size_t start = 0;
|
|
for (size_t i = 0; i <= n; i++) {
|
|
if (i != n && line[i] != '|')
|
|
continue;
|
|
const char *c = line + start;
|
|
size_t cl = i - start;
|
|
while (cl > 0 && (c[0] == ' ' || c[0] == '\t')) {
|
|
c++;
|
|
cl--;
|
|
}
|
|
while (cl > 0 && (c[cl - 1] == ' ' || c[cl - 1] == '\t'))
|
|
cl--;
|
|
if (out->n == cap) {
|
|
cap = cap ? cap * 2 : 8;
|
|
out->cell = xrealloc(out->cell, cap * sizeof(char *));
|
|
}
|
|
out->cell[out->n++] = xstrndup(c, cl);
|
|
start = i + 1;
|
|
}
|
|
}
|
|
|
|
/* 分隔单元格: :?-+:? */
|
|
static int is_delim_cell(const char *c, size_t n) {
|
|
size_t i = 0, dashes = 0;
|
|
if (i < n && c[i] == ':')
|
|
i++;
|
|
while (i < n && c[i] == '-') {
|
|
i++;
|
|
dashes++;
|
|
}
|
|
if (dashes == 0)
|
|
return 0;
|
|
if (i < n && c[i] == ':')
|
|
i++;
|
|
return i == n;
|
|
}
|
|
|
|
/* 表头行 + 分隔行是否构成合法表格:
|
|
* 两者单元格数必须相同, 且第二行每格都是 :?-+:?
|
|
* (这条校验同时挡掉了 "cat a | grep b" 跟着一行 "--------" 的误判) */
|
|
static int table_head_ok(const char *h, size_t hn, const char *d, size_t dn) {
|
|
TblRow hrow, drow;
|
|
row_split(h, hn, &hrow);
|
|
row_split(d, dn, &drow);
|
|
int ok = (hrow.n > 0) && (hrow.n == drow.n);
|
|
for (size_t i = 0; ok && i < drow.n; i++)
|
|
if (!is_delim_cell(drow.cell[i], strlen(drow.cell[i])))
|
|
ok = 0;
|
|
row_free(&hrow);
|
|
row_free(&drow);
|
|
return ok;
|
|
}
|
|
|
|
static int count_pipes(const char *s, size_t n) {
|
|
int c = 0;
|
|
for (size_t i = 0; i < n; i++)
|
|
if (s[i] == '|')
|
|
c++;
|
|
return c;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 表格: 渲染 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void emit_padded(MdRenderer *r, const char *cell, size_t width,
|
|
TblAlign al) {
|
|
size_t w = display_width(cell, strlen(cell));
|
|
size_t pad = width > w ? width - w : 0;
|
|
size_t left = 0, right = pad;
|
|
if (al == ALIGN_CENTER) {
|
|
left = pad / 2;
|
|
right = pad - left;
|
|
} else if (al == ALIGN_RIGHT) {
|
|
left = pad;
|
|
right = 0;
|
|
}
|
|
|
|
for (size_t i = 0; i < left; i++)
|
|
putc_str(r, " ");
|
|
putc_str(r, cell);
|
|
for (size_t i = 0; i < right; i++)
|
|
putc_str(r, " ");
|
|
}
|
|
|
|
static void emit_rule(MdRenderer *r, const size_t *w, size_t ncols,
|
|
const char *left, const char *mid, const char *right) {
|
|
style(r, MD_DIM);
|
|
putc_str(r, left);
|
|
for (size_t c = 0; c < ncols; c++) {
|
|
for (size_t k = 0; k < w[c] + 2; k++)
|
|
putc_str(r, "─");
|
|
if (c + 1 < ncols)
|
|
putc_str(r, mid);
|
|
}
|
|
putc_str(r, right);
|
|
style(r, MD_RESET);
|
|
putc_str(r, "\n");
|
|
}
|
|
|
|
static void emit_row(MdRenderer *r, const TblRow *row, const size_t *w,
|
|
const TblAlign *al, size_t ncols, int header) {
|
|
style(r, MD_DIM);
|
|
putc_str(r, "│");
|
|
for (size_t c = 0; c < ncols; c++) {
|
|
putc_str(r, " ");
|
|
style(r, MD_RESET);
|
|
if (header)
|
|
style(r, MD_BOLD);
|
|
emit_padded(r, c < row->n ? row->cell[c] : "", w[c], al[c]);
|
|
style(r, MD_RESET);
|
|
style(r, MD_DIM);
|
|
putc_str(r, " │");
|
|
}
|
|
style(r, MD_RESET);
|
|
putc_str(r, "\n");
|
|
}
|
|
|
|
/* 把 r->table 里累积的原始行渲染成带边框的表格 */
|
|
static void table_flush(MdRenderer *r) {
|
|
r->in_table = 0;
|
|
r->table_rows = 0;
|
|
if (r->table.len == 0)
|
|
return;
|
|
|
|
TblRow *rows = NULL;
|
|
size_t nrows = 0, cap = 0;
|
|
char *data = r->table.data;
|
|
size_t start = 0;
|
|
for (size_t i = 0; i <= r->table.len; i++) {
|
|
if (i != r->table.len && data[i] != '\n')
|
|
continue;
|
|
size_t ln = i - start;
|
|
if (ln > 0) {
|
|
if (nrows == cap) {
|
|
cap = cap ? cap * 2 : 8;
|
|
rows = xrealloc(rows, cap * sizeof(TblRow));
|
|
}
|
|
row_split(data + start, ln, &rows[nrows]);
|
|
nrows++;
|
|
}
|
|
start = i + 1;
|
|
}
|
|
r->table.len = 0;
|
|
|
|
if (nrows < 2) { /* 不成表, 原样吐出 */
|
|
for (size_t i = 0; i < nrows; i++) {
|
|
for (size_t c = 0; c < rows[i].n; c++) {
|
|
if (c)
|
|
putc_str(r, " | ");
|
|
putc_str(r, rows[i].cell[c]);
|
|
}
|
|
putc_str(r, "\n");
|
|
}
|
|
for (size_t i = 0; i < nrows; i++)
|
|
row_free(&rows[i]);
|
|
free(rows);
|
|
return;
|
|
}
|
|
|
|
size_t ncols = 0;
|
|
for (size_t i = 0; i < nrows; i++)
|
|
if (rows[i].n > ncols)
|
|
ncols = rows[i].n;
|
|
if (ncols == 0) {
|
|
free(rows);
|
|
return;
|
|
}
|
|
|
|
/* 对齐方式来自分隔行 (第 1 行) */
|
|
TblAlign *al = xmalloc(ncols * sizeof(TblAlign));
|
|
for (size_t c = 0; c < ncols; c++)
|
|
al[c] = ALIGN_LEFT;
|
|
for (size_t c = 0; c < rows[1].n && c < ncols; c++) {
|
|
const char *s = rows[1].cell[c];
|
|
size_t n = strlen(s);
|
|
int l = (n > 0 && s[0] == ':');
|
|
int rr = (n > 0 && s[n - 1] == ':');
|
|
if (l && rr)
|
|
al[c] = ALIGN_CENTER;
|
|
else if (rr)
|
|
al[c] = ALIGN_RIGHT;
|
|
}
|
|
|
|
/* 列宽由表头与所有数据行决定 (跳过第 1 行的分隔行) */
|
|
size_t *w = xcalloc(ncols, sizeof(size_t));
|
|
for (size_t i = 0; i < nrows; i++) {
|
|
if (i == 1)
|
|
continue;
|
|
for (size_t c = 0; c < rows[i].n && c < ncols; c++) {
|
|
size_t cw = display_width(rows[i].cell[c], strlen(rows[i].cell[c]));
|
|
if (cw > w[c])
|
|
w[c] = cw;
|
|
}
|
|
}
|
|
for (size_t c = 0; c < ncols; c++)
|
|
if (w[c] == 0)
|
|
w[c] = 1;
|
|
|
|
emit_rule(r, w, ncols, "┌", "┬", "┐");
|
|
emit_row(r, &rows[0], w, al, ncols, 1);
|
|
emit_rule(r, w, ncols, "├", "┼", "┤");
|
|
for (size_t i = 2; i < nrows; i++)
|
|
emit_row(r, &rows[i], w, al, ncols, 0);
|
|
emit_rule(r, w, ncols, "└", "┴", "┘");
|
|
|
|
for (size_t i = 0; i < nrows; i++)
|
|
row_free(&rows[i]);
|
|
free(rows);
|
|
free(w);
|
|
free(al);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 块级语法: 只在行首调用 */
|
|
/* 返回消耗的字节数; 0 表示"不是块级语法, 按普通字符处理";
|
|
* MD_NEED_MORE 表示需要更多数据才能判定 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static size_t scan_line(const char *s, size_t len, size_t i, int *has_nl) {
|
|
size_t j = i;
|
|
while (j < len && s[j] != '\n')
|
|
j++;
|
|
*has_nl = (j < len);
|
|
return j - i;
|
|
}
|
|
|
|
/* 规则表: 从上往下依次尝试.
|
|
* 关键约定: 每条规则只有在"确实消费了字节"时才 return;
|
|
* 判定为"不适用"时必须继续往下走, 绝不能中途 return 0 ——
|
|
* 否则会跳过后面所有规则 (早期版本正是因此让表格探测吃掉了列表规则). */
|
|
static size_t try_block(MdRenderer *r, size_t i) {
|
|
const char *s = r->in.data;
|
|
size_t len = r->in.len;
|
|
|
|
/* ---- 围栏代码块内部: 只认闭合行, 其余一律当代码内容 ---- */
|
|
if (r->in_fence) {
|
|
if (s[i] != r->fence_char)
|
|
return 0;
|
|
size_t run = 0;
|
|
while (i + run < len && s[i + run] == r->fence_char)
|
|
run++;
|
|
if (i + run >= len && !r->eof)
|
|
return MD_NEED_MORE;
|
|
if ((int)run < r->fence_len)
|
|
return 0;
|
|
|
|
size_t j = i + run;
|
|
while (j < len && (s[j] == ' ' || s[j] == '\t'))
|
|
j++;
|
|
if (j >= len && !r->eof)
|
|
return MD_NEED_MORE;
|
|
if (j < len && s[j] != '\n')
|
|
return 0; /* 围栏后还有内容, 不是闭合行 */
|
|
|
|
size_t emit = j - i;
|
|
put(r, s + i, emit);
|
|
if (j < len) {
|
|
put(r, "\n", 1);
|
|
emit++;
|
|
}
|
|
r->in_fence = 0;
|
|
r->line_start = 1;
|
|
return emit;
|
|
}
|
|
|
|
/* ---- 表格续行 ---- */
|
|
if (r->in_table) {
|
|
int has_nl;
|
|
size_t ln = scan_line(s, len, i, &has_nl);
|
|
if (!has_nl && !r->eof)
|
|
return MD_NEED_MORE;
|
|
if (ln > 0 && count_pipes(s + i, ln) >= 1) {
|
|
sb_append(&r->table, s + i, ln);
|
|
sb_putc(&r->table, '\n');
|
|
r->table_rows++;
|
|
if (r->table_rows >= MD_MAX_TABLE_ROWS ||
|
|
r->table.len > MD_MAX_PENDING) {
|
|
table_flush(r);
|
|
}
|
|
return ln + (has_nl ? 1 : 0);
|
|
}
|
|
table_flush(r);
|
|
/* 表格到此结束. 注意不 return: 当前行可能是个标题/列表,
|
|
* 要继续往下走让它被正常渲染. */
|
|
}
|
|
|
|
/* ---- 标题 # ~ ###### ---- */
|
|
if (s[i] == '#') {
|
|
size_t run = 0;
|
|
while (i + run < len && s[i + run] == '#')
|
|
run++;
|
|
if (i + run >= len && !r->eof) {
|
|
if (run <= 6)
|
|
return MD_NEED_MORE;
|
|
/* run > 6: 确定不是标题, 继续往下判断 */
|
|
} else if (run >= 1 && run <= 6 &&
|
|
(s[i + run] == ' ' || s[i + run] == '\t')) {
|
|
static const char *codes[6] = {MD_H1, MD_H2, MD_H3,
|
|
MD_H4, MD_H4, MD_H4};
|
|
style(r, codes[run - 1]);
|
|
put(r, s + i, run);
|
|
put(r, s + i + run, 1);
|
|
r->line_start = 0;
|
|
r->line_style = 1;
|
|
return run + 1;
|
|
}
|
|
}
|
|
|
|
/* ---- 引用 > ---- */
|
|
if (s[i] == '>') {
|
|
if (i + 1 >= len && !r->eof)
|
|
return MD_NEED_MORE;
|
|
size_t n = (i + 1 < len && s[i + 1] == ' ') ? 2 : 1;
|
|
style(r, MD_DIM);
|
|
putc_str(r, "│ ");
|
|
style(r, MD_RESET);
|
|
r->line_start = 0;
|
|
return n;
|
|
}
|
|
|
|
/* ---- 围栏代码块开启 ---- */
|
|
if (s[i] == '`') {
|
|
size_t run = 0;
|
|
while (i + run < len && s[i + run] == '`')
|
|
run++;
|
|
if (i + run >= len && !r->eof)
|
|
return MD_NEED_MORE;
|
|
if (run >= 3) {
|
|
size_t j = i + run;
|
|
while (j < len && s[j] != '\n')
|
|
j++;
|
|
int has_nl = (j < len);
|
|
r->in_fence = 1;
|
|
r->fence_char = '`';
|
|
r->fence_len = (int)run;
|
|
put(r, s + i, j - i);
|
|
if (has_nl) {
|
|
put(r, "\n", 1);
|
|
r->line_start = 1;
|
|
} else {
|
|
r->line_start = 0;
|
|
}
|
|
return (j - i) + (has_nl ? 1 : 0);
|
|
}
|
|
/* 少于 3 个反引号: 交给行内代码逻辑 */
|
|
}
|
|
|
|
/* ---- 无序列表 / 分隔线 ---- */
|
|
if (s[i] == '-' || s[i] == '*' || s[i] == '+') {
|
|
if (i + 1 >= len) {
|
|
if (!r->eof)
|
|
return MD_NEED_MORE; /* 可能是 "- " 列表或 "---" */
|
|
/* EOF 且只剩这一个字符: 当字面量 */
|
|
} else {
|
|
if (s[i + 1] == ' ') {
|
|
/* 保留模型输出的原始标记字符并上色, 不替换成 • */
|
|
style(r, MD_BULLET);
|
|
put(r, s + i, 1);
|
|
style(r, MD_RESET);
|
|
put(r, s + i + 1, 1);
|
|
r->line_start = 0;
|
|
return 2;
|
|
}
|
|
if (s[i] == '-') {
|
|
size_t run = 0;
|
|
while (i + run < len && s[i + run] == '-')
|
|
run++;
|
|
if (i + run >= len && !r->eof)
|
|
return MD_NEED_MORE;
|
|
if (run >= 3) {
|
|
size_t j = i + run;
|
|
while (j < len && (s[j] == ' ' || s[j] == '\t'))
|
|
j++;
|
|
if (j >= len && !r->eof)
|
|
return MD_NEED_MORE;
|
|
if (j >= len || s[j] == '\n') {
|
|
style(r, MD_DIM);
|
|
put(r, s + i, run);
|
|
style(r, MD_RESET);
|
|
if (j < len)
|
|
put(r, "\n", 1);
|
|
r->line_start = 1;
|
|
return (j - i) + (j < len ? 1 : 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ---- 表格起始: 表头行 + 分隔行. 放在最后, 因为它需要看两行 ---- */
|
|
{
|
|
int nl1;
|
|
size_t ln1 = scan_line(s, len, i, &nl1);
|
|
|
|
/* 表格必须顶格以 '|' 开头.
|
|
* 这个判断在行的第 0 个字节就能做出 —— 这是关键: 只要"是否挂起"
|
|
* 依赖行内更靠后的内容 (例如"整行是否含竖线"), 那么当分片边界
|
|
* 落在竖线之前时, 行的前半段已经被当成普通文本输出了, 再也无法回头,
|
|
* 结果就会随分片方式而变. */
|
|
if (s[i] == '|') {
|
|
if (!nl1 && !r->eof) {
|
|
if (ln1 > MD_MAX_TABLE_LINE)
|
|
return 0;
|
|
return MD_NEED_MORE;
|
|
}
|
|
size_t j = i + ln1 + (nl1 ? 1 : 0);
|
|
if (j >= len) {
|
|
if (r->eof)
|
|
return 0; /* 没有第二行, 必然不是表格 */
|
|
return MD_NEED_MORE;
|
|
}
|
|
int nl2;
|
|
size_t ln2 = scan_line(s, len, j, &nl2);
|
|
if (!nl2 && !r->eof) {
|
|
if (ln2 > MD_MAX_TABLE_LINE)
|
|
return 0;
|
|
return MD_NEED_MORE;
|
|
}
|
|
if (table_head_ok(s + i, ln1, s + j, ln2)) {
|
|
sb_append(&r->table, s + i, ln1);
|
|
sb_putc(&r->table, '\n');
|
|
sb_append(&r->table, s + j, ln2);
|
|
sb_putc(&r->table, '\n');
|
|
r->in_table = 1;
|
|
r->table_rows = 2;
|
|
return (j + ln2 + (nl2 ? 1 : 0)) - i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 主循环 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static int is_word_char(int c) {
|
|
/* 只把 ASCII 词字符算作"词内", 非 ASCII (中文等)不算.
|
|
* 这样 "中文[链接](url)" 能高亮, 而 "arr[i]" 不会. */
|
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') || c == '_';
|
|
}
|
|
|
|
static void feed_enabled(MdRenderer *r, const char *text, size_t len) {
|
|
sb_append(&r->in, text, len);
|
|
|
|
size_t i = 0;
|
|
while (i < r->in.len) {
|
|
const char *s = r->in.data;
|
|
size_t remain = r->in.len - i;
|
|
|
|
if (r->line_start && !r->in_code) {
|
|
size_t consumed = try_block(r, i);
|
|
if (consumed == MD_NEED_MORE)
|
|
break;
|
|
if (consumed > 0) {
|
|
i += consumed;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
char c = s[i];
|
|
|
|
/* ---- 换行 ---- */
|
|
if (c == '\n') {
|
|
close_line_styles(r);
|
|
put(r, "\n", 1);
|
|
r->line_start = 1;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
/* ---- 围栏代码块内部: 逐字原样输出 ---- */
|
|
if (r->in_fence) {
|
|
put(r, s + i, 1);
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
/* ---- 行内代码内部: 只有反引号有意义, 内容不做任何解析 ---- */
|
|
if (r->in_code) {
|
|
if (c == '`') {
|
|
style(r, MD_DIM);
|
|
put(r, "`", 1);
|
|
style(r, MD_RESET);
|
|
r->in_code = 0;
|
|
} else {
|
|
put(r, s + i, 1);
|
|
}
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
/* ---- 粗体 / 斜体 ---- */
|
|
if (c == '*') {
|
|
if (remain >= 2 && s[i + 1] == '*') {
|
|
if (r->in_bold) {
|
|
style(r, MD_RESET);
|
|
r->in_bold = 0;
|
|
} else {
|
|
style(r, MD_BOLD);
|
|
r->in_bold = 1;
|
|
}
|
|
r->line_start = 0;
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (remain < 2) {
|
|
/* 末尾的单个 '*': 可能是 '**' 的前半个, 必须等下一个分片.
|
|
* 只有到 EOF 才能确定后面没有第二个 '*', 此时按固定规则解开:
|
|
* 正在斜体就当作收尾, 否则当字面量. */
|
|
if (!r->eof)
|
|
break;
|
|
if (r->in_italic) {
|
|
style(r, MD_RESET);
|
|
r->in_italic = 0;
|
|
} else {
|
|
put(r, "*", 1);
|
|
}
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
if (r->in_italic) {
|
|
style(r, MD_RESET);
|
|
r->in_italic = 0;
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
char next = s[i + 1];
|
|
if (next != ' ' && next != '\t' && next != '\n') {
|
|
style(r, MD_ITALIC);
|
|
r->in_italic = 1;
|
|
} else {
|
|
put(r, "*", 1);
|
|
}
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
/* ---- _斜体_: 按 CommonMark 规则禁用词内强调, 避免误伤 foo_bar_baz
|
|
* ---- */
|
|
if (c == '_') {
|
|
int prev = (i > 0) ? (unsigned char)s[i - 1] : r->last_ch;
|
|
if (i + 1 >= r->in.len) {
|
|
if (!r->eof)
|
|
break; /* 需要看后一个字符 */
|
|
put(r, "_", 1); /* EOF: 没有后一个字符, 当字面量 */
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
int next = (unsigned char)s[i + 1];
|
|
|
|
/* 与另一个下划线相邻时一律当字面量:
|
|
* 这样 Python 的 __init__、__main__ 不会被拆成斜体 */
|
|
if (prev == '_' || next == '_') {
|
|
put(r, "_", 1);
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (r->in_italic) {
|
|
if (prev != ' ' && prev != '\t' && !is_word_char(next)) {
|
|
style(r, MD_RESET);
|
|
r->in_italic = 0;
|
|
} else {
|
|
put(r, "_", 1);
|
|
}
|
|
} else if (!is_word_char(prev) && next != ' ' && next != '\t' &&
|
|
next != '\n') {
|
|
style(r, MD_ITALIC);
|
|
r->in_italic = 1;
|
|
} else {
|
|
put(r, "_", 1);
|
|
}
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
/* ---- [链接文字]: 蓝色高亮, 其余部分 (含 (url))原样保留 ---- */
|
|
if (c == '[') {
|
|
if (remain < 2) {
|
|
if (!r->eof)
|
|
break; /* 需要看后一个字符 */
|
|
put(r, "[", 1); /* EOF: 当字面量 */
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
int prev = (i > 0) ? (unsigned char)s[i - 1] : r->last_ch;
|
|
int next = (unsigned char)s[i + 1];
|
|
if (!is_word_char(prev) && next != ' ' && next != '\t' &&
|
|
next != '\n') {
|
|
style(r, MD_LINK);
|
|
put(r, "[", 1);
|
|
r->in_bracket = 1;
|
|
r->bracket_len = 1;
|
|
} else {
|
|
put(r, "[", 1);
|
|
}
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (c == ']' && r->in_bracket) {
|
|
put(r, "]", 1);
|
|
style(r, MD_RESET);
|
|
r->in_bracket = 0;
|
|
r->bracket_len = 0;
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
/* ---- 行内代码开始 ---- */
|
|
if (c == '`') {
|
|
style(r, MD_DIM);
|
|
put(r, "`", 1);
|
|
style(r, MD_RESET);
|
|
style(r, MD_CODE);
|
|
r->in_code = 1;
|
|
r->line_start = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
put(r, s + i, 1);
|
|
r->line_start = 0;
|
|
i++;
|
|
}
|
|
|
|
if (i > 0)
|
|
r->last_ch = (unsigned char)r->in.data[i - 1];
|
|
|
|
/* 丢弃已处理的前缀, 只保留等待更多数据的尾部 */
|
|
if (i > 0) {
|
|
memmove(r->in.data, r->in.data + i, r->in.len - i);
|
|
r->in.len -= i;
|
|
r->in.data[r->in.len] = '\0';
|
|
}
|
|
|
|
/* 兜底: 等待中的残留过大就原样吐出, 保证内存有界 */
|
|
if (r->in.len > MD_MAX_PENDING) {
|
|
close_line_styles(r);
|
|
put(r, r->in.data, r->in.len);
|
|
r->in.len = 0;
|
|
r->line_start = 0;
|
|
}
|
|
}
|
|
|
|
void md_feed(MdRenderer *r, const char *text) {
|
|
if (!r || !text)
|
|
return;
|
|
size_t len = strlen(text);
|
|
if (!r->enabled) {
|
|
if (len)
|
|
fwrite(text, 1, len, r->out);
|
|
return;
|
|
}
|
|
feed_enabled(r, text, len);
|
|
}
|
|
|
|
void md_finish(MdRenderer *r) {
|
|
if (!r)
|
|
return;
|
|
if (r->enabled) {
|
|
/* 关键: 置 eof 后把残留重新过一遍状态机, 让所有挂起按固定规则解开.
|
|
* 这样"一次性喂"与"分片喂"在结尾处的结果必然一致. */
|
|
r->eof = 1;
|
|
feed_enabled(r, "", 0);
|
|
if (r->in_table)
|
|
table_flush(r);
|
|
if (r->in.len > 0) { /* 兜底: 正常不该走到 */
|
|
close_line_styles(r);
|
|
put(r, r->in.data, r->in.len);
|
|
r->in.len = 0;
|
|
}
|
|
close_line_styles(r);
|
|
r->in_fence = 0;
|
|
r->in_table = 0;
|
|
r->eof = 0;
|
|
}
|
|
fflush(r->out);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 生命周期 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
MdRenderer *md_new(int enabled) {
|
|
MdRenderer *r = xcalloc(1, sizeof *r);
|
|
r->enabled = enabled ? 1 : 0;
|
|
r->out = stdout;
|
|
sb_init(&r->in);
|
|
sb_init(&r->table);
|
|
r->line_start = 1;
|
|
return r;
|
|
}
|
|
|
|
void md_set_enabled(MdRenderer *r, int enabled) {
|
|
if (r)
|
|
r->enabled = enabled ? 1 : 0;
|
|
}
|
|
|
|
void md_set_out(MdRenderer *r, FILE *out) {
|
|
if (r && out)
|
|
r->out = out;
|
|
}
|
|
|
|
void md_free(MdRenderer *r) {
|
|
if (!r)
|
|
return;
|
|
sb_free(&r->in);
|
|
sb_free(&r->table);
|
|
free(r);
|
|
}
|