/* md_demo.c - Markdown 渲染效果演示 * * 一个独立的小程序: 把内置示例 (或你指定的文件 / stdin)喂进 ligent 的 * 流式 Markdown 渲染器, 直观看到每种语法的渲染结果. * * 编译: make md_demo * 运行: ./build/md_demo */ #include "markdown.h" #include "util.h" #include #include #include #include #include /* ------------------------------------------------------------------ */ /* 内置示例: 把支持的语法和边界情况都覆盖一遍 */ /* ------------------------------------------------------------------ */ 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 \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; }