Files
ligent/src/markdown.c
T
2026-10-04 13:50:33 +08:00

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);
}