Files
ligent/tools/md_demo.c
T
2026-10-03 14:13:21 +08:00

306 lines
10 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* md_demo.c - Markdown 渲染效果演示
*
* 一个独立的小程序:把内置示例(或你指定的文件 / stdin)喂进 ligent 的
* 流式 Markdown 渲染器,直观看到每种语法的渲染结果。
*
* 编译: make md_demo
* 运行: ./build/md_demo
*/
#include "markdown.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
/* ------------------------------------------------------------------ */
/* 内置示例:把支持的语法和边界情况都覆盖一遍 */
/* ------------------------------------------------------------------ */
static const char *SAMPLE =
"# ligent 渲染演示\n"
"\n"
"## 行内样式\n"
"\n"
"这是 **粗体**、*斜体*、_下划线斜体_ 和 `行内代码` 的混排效果。\n"
"标识符不会被误伤:foo_bar_baz、__init__、arr[i]、a_b_c 都保持原样。\n"
"链接只高亮方括号:[官方文档](https://example.com/doc) 后面还有普通文字。\n"
"图片保留原文:![架构图](./arch.png)\n"
"\n"
"## 列表与引用\n"
"\n"
"- 减号列表项\n"
"- 第二项\n"
"* 星号也能当列表\n"
"+ 加号也可以\n"
"\n"
"> 这是一段引用,前缀是暗色的竖线。\n"
"> 引用可以有多行。\n"
"\n"
"---\n"
"\n"
"## 代码块(整块不渲染)\n"
"\n"
"```c\n"
"#include <stdio.h>\n"
"int main(void) {\n"
" // **这里不会加粗**,[方括号] 也不会高亮\n"
" printf(\"hello %s\\n\", \"world\");\n"
" return 0;\n"
"}\n"
"```\n"
"\n"
"行内代码同理:`a ** b` 里的星号不会被当成粗体标记。\n"
"\n"
"## 表格\n"
"\n"
"| 语言 | 类型系统 | 典型用途 | 份额 |\n"
"|:-----|:--------:|----------|-----:|\n"
"| C | 静态弱类型 | 系统编程 | 11.4% |\n"
"| Rust | 静态强类型 | 系统编程 | 3.2% |\n"
"| Python | 动态强类型 | 脚本 / AI | 23.1% |\n"
"\n"
"| 单列表格 |\n"
"|---|\n"
"| 也能渲染 |\n"
"\n"
"## 边界情况\n"
"\n"
"乘法星号原样保留:2 * 3 * 4 = 24\n"
"管道命令不会被误判成表格:ls -la | grep foo\n"
"未闭合的 **粗体标记 会在行尾被复位\n"
"#井号后没有空格就不是标题\n"
"####### 七个井号也不是标题\n"
"\n";
/* ------------------------------------------------------------------ */
/* 渲染到内存(用于 --compare 自检) */
/* ------------------------------------------------------------------ */
static char *render_to_mem(const char *src, int enabled, size_t chunk)
{
char *buf = NULL;
size_t size = 0;
FILE *f = open_memstream(&buf, &size);
if (!f) return NULL;
MdRenderer *r = md_new(enabled);
md_set_out(r, f);
if (chunk == 0) {
md_feed(r, src);
} else {
size_t len = strlen(src);
for (size_t i = 0; i < len; i += chunk) {
size_t n = len - i < chunk ? len - i : chunk;
char *tmp = xstrndup(src + i, n);
md_feed(r, tmp);
free(tmp);
}
}
md_finish(r);
md_free(r);
fclose(f);
return buf;
}
/* ------------------------------------------------------------------ */
/* 渲染到终端(可带分片与延时,模拟 SSE 流式) */
/* ------------------------------------------------------------------ */
static void render_to_tty(const char *src, int enabled,
size_t chunk, long delay_ms)
{
MdRenderer *r = md_new(enabled);
md_set_out(r, stdout);
size_t len = strlen(src);
if (chunk == 0) chunk = len ? len : 1;
struct timespec ts;
ts.tv_sec = delay_ms / 1000;
ts.tv_nsec = (delay_ms % 1000) * 1000000L;
for (size_t i = 0; i < len; i += chunk) {
size_t n = len - i < chunk ? len - i : chunk;
char *tmp = xstrndup(src + i, n);
md_feed(r, tmp);
free(tmp);
fflush(stdout);
if (delay_ms > 0) nanosleep(&ts, NULL);
}
md_finish(r);
md_free(r);
}
/* ------------------------------------------------------------------ */
/* 参数 */
/* ------------------------------------------------------------------ */
static void usage(FILE *out)
{
fputs(
"md_demo - 演示 ligent 的流式 Markdown 渲染效果\n"
"\n"
"用法: md_demo [选项] [文件...]\n"
"\n"
"不指定文件时使用内置示例;文件名为 - 或 stdin 不是终端时从标准输入读取。\n"
"\n"
"选项:\n"
" -p, --plain 关闭渲染,输出原始 Markdown(对照用)\n"
" --color 强制开启渲染(默认仅在输出到终端时开启)\n"
" -c, --chunk N 按每 N 字节一片喂入(默认 0 = 一次性喂完)\n"
" -d, --delay MS 每片之间延时毫秒(默认 0)\n"
" -s, --stream 流式预设:等价于 -c 5 -d 12\n"
" --compare 自检:把同一段文本按多种分片方式渲染,比对结果是否一致\n"
" -h, --help 显示本帮助\n"
"\n"
"例子:\n"
" md_demo 看渲染效果\n"
" md_demo --plain 看原始 Markdown,对照差异\n"
" md_demo -c 5 -d 12 模拟 SSE 流式(标记会被随机切断)\n"
" md_demo --compare 验证分片不变性\n"
" md_demo README.md 渲染任意 Markdown 文件\n"
" echo '# hi **there**' | md_demo\n",
out);
}
static char *read_stream(FILE *f)
{
StrBuf sb;
sb_init(&sb);
char buf[8192];
size_t n;
while ((n = fread(buf, 1, sizeof buf, f)) > 0)
sb_append(&sb, buf, n);
return sb_detach(&sb);
}
int main(int argc, char **argv)
{
int plain = 0, force_color = 0, compare = 0;
long delay_ms = 0;
size_t chunk = 0;
const char *file = NULL;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
usage(stdout);
return 0;
} else if (strcmp(a, "-p") == 0 || strcmp(a, "--plain") == 0) {
plain = 1;
} else if (strcmp(a, "--color") == 0) {
force_color = 1;
} else if (strcmp(a, "-c") == 0 || strcmp(a, "--chunk") == 0) {
if (++i >= argc) { fputs("缺少 -c 的参数\n", stderr); return 2; }
chunk = (size_t)strtoul(argv[i], NULL, 10);
} else if (strcmp(a, "-d") == 0 || strcmp(a, "--delay") == 0) {
if (++i >= argc) { fputs("缺少 -d 的参数\n", stderr); return 2; }
delay_ms = strtol(argv[i], NULL, 10);
} else if (strcmp(a, "-s") == 0 || strcmp(a, "--stream") == 0) {
chunk = 5;
delay_ms = 12;
} else if (strcmp(a, "--compare") == 0) {
compare = 1;
} else if (a[0] == '-' && a[1] != '\0' && strcmp(a, "-") != 0) {
fprintf(stderr, "未知选项 %s(--help 查看用法)\n", a);
return 2;
} else {
file = a;
}
}
/* 取输入 */
char *src = NULL;
const char *source_name = "内置示例";
if (file && strcmp(file, "-") != 0) {
size_t len = 0;
src = read_entire_file(file, &len);
if (!src) {
fprintf(stderr, "无法读取文件 %s\n", file);
return 1;
}
source_name = file;
} else if (file || !isatty(fileno(stdin))) {
src = read_stream(stdin);
source_name = "标准输入";
if (src[0] == '\0') {
/* stdin 是空管道(例如被 CI / make 调用)时退回内置示例 */
free(src);
src = xstrdup(SAMPLE);
source_name = "内置示例(stdin 为空)";
}
} else {
src = xstrdup(SAMPLE);
}
int enabled = plain ? 0 : (force_color ? 1 : (isatty(fileno(stdout)) ? 1 : 0));
if (compare) {
/* 分片不变性自检:不同分片大小必须得到完全相同的结果 */
puts("分片不变性自检(同一段文本,不同分片方式,输出必须逐字节一致)");
printf("输入:%s(%zu 字节)\n\n", source_name, strlen(src));
char *whole = render_to_mem(src, 1, 0);
size_t sizes[] = { 1, 2, 3, 5, 7, 13, 64 };
int all_ok = 1;
for (size_t k = 0; k < sizeof sizes / sizeof sizes[0]; k++) {
char *part = render_to_mem(src, 1, sizes[k]);
int ok = part && whole && strcmp(whole, part) == 0;
printf(" %-5zu 字节/片 %s\n", sizes[k], ok ? "一致 ✓" : "不一致 ✗");
if (!ok) all_ok = 0;
free(part);
}
/* 随机分片(模拟真实 SSE 的抖动) */
srand(12345);
char *buf = NULL;
size_t bsize = 0;
FILE *f = open_memstream(&buf, &bsize);
MdRenderer *r = md_new(1);
md_set_out(r, f);
size_t len = strlen(src), i = 0;
while (i < len) {
size_t n = (size_t)(rand() % 11) + 1;
if (i + n > len) n = len - i;
char *tmp = xstrndup(src + i, n);
md_feed(r, tmp);
free(tmp);
i += n;
}
md_finish(r);
md_free(r);
fclose(f);
int ok = buf && whole && strcmp(whole, buf) == 0;
printf(" %-5s %s\n", "随机", ok ? "一致 ✓" : "不一致 ✗");
if (!ok) all_ok = 0;
free(buf);
free(whole);
printf("\n结论:%s\n", all_ok ? "全部一致,标记跨分片处理正确。"
: "存在不一致,渲染器有 bug。");
free(src);
return all_ok ? 0 : 1;
}
if (isatty(fileno(stdout))) {
fprintf(stderr, "── 输入:%s 渲染:%s%s ──\n\n",
source_name, enabled ? "开" : "关",
chunk ? "(分片模式)" : "");
}
render_to_tty(src, enabled, chunk, delay_ms);
if (isatty(fileno(stdout))) {
fprintf(stderr, "\n── 演示结束 ──\n");
if (enabled && chunk == 0)
fprintf(stderr, "试试 `md_demo -s` 看流式分片下标记被切断时的表现,"
"或 `md_demo -p` 对照原始 Markdown。\n");
}
free(src);
return 0;
}