import type { Root } from "mdast";
import { renderToStaticMarkup } from "react-dom/server";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import remarkGfm from "remark-gfm";
import type { Plugin } from "unified";
import { describe, expect, it } from "vite-plus/test";
import { remarkCodexDirectives } from "@t3tools/client-runtime/codex-markdown-directives";
import { remarkGithubAlerts } from "./markdown-github-alerts";
import { createIncrementalMarkdownPlugin } from "./markdown-incremental";
import { remarkNormalizeListItemIndentation } from "./markdown-list-indentation";
function render(source: string, incremental?: Plugin<[], Root>, parsedSources?: string[]) {
let tree: Root | undefined;
const observeParsing: Plugin<[], Root> = function () {
const original = this.parser;
if (original) {
this.parser = (text, file) => {
parsedSources?.push(text);
return original(text, file);
};
}
};
const capture: Plugin<[], Root> = () => (root) => {
tree = structuredClone(root);
};
const html = renderToStaticMarkup(
{source}
,
);
return { html, tree };
}
const prefix = "# Before\n\n```ts\nconst values = [1, 2];\n```\n\n";
describe("incremental Markdown parsing", () => {
it("keeps the document prefix cached when list recovery parses contain fences", () => {
const source =
prefix +
"- first block\n\n ```ts\n const nested = 1;\n ```\n\n tail";
const incremental = createIncrementalMarkdownPlugin();
const parsedSources: string[] = [];
expect(render(source, incremental, parsedSources)).toEqual(render(source));
parsedSources.length = 0;
const next = source + " more";
expect(render(next, incremental, parsedSources)).toEqual(render(next));
expect(parsedSources).not.toContain(next);
expect(parsedSources.some((text) => text.startsWith("t3-markdown-inline-prefix:"))).toBe(true);
});
it.each([
"a\n===\n\nb\n---\n",
"- first\n\n continued\n\n- next\n",
"> quoted\n>\n> ```js\n> abc\n> ```\n\nend",
"
\nhello\n\n
\n\nend",
"[ref]\n\n[ref]: /later",
"a[^x]\n\n[^x]: note",
"a | b\n--|--\na | b\n",
"```\na\n```\n\nnext\n\n~~~\nb\n~~~\n\nmore",
"\n\n\tcode\n\nmore",
"text *bold*",
"> [!NOTE]\n> alert\n\n- [ ] task",
"\uFEFFtext after a byte-order mark",
])("preserves the parse tree, positions, and HTML while streaming %j", (tail) => {
const source = prefix + tail;
const incremental = createIncrementalMarkdownPlugin();
for (let end = 0; end <= source.length; end++) {
const text = source.slice(0, end);
expect(render(text, incremental), `prefix ${end}`).toEqual(render(text));
}
});
it.each(["\r\n", "\r"])("preserves partial %j line endings", (newline) => {
const source = (prefix + "next\n\n```\nlast\n```\n\nend").replaceAll("\n", newline);
const incremental = createIncrementalMarkdownPlugin();
for (let end = 0; end <= source.length; end++) {
const text = source.slice(0, end);
expect(render(text, incremental)).toEqual(render(text));
}
});
it("updates earlier references when definitions arrive after the cached prefix", () => {
const before = "[later] and footnote[^note]\n\n" + prefix;
const incremental = createIncrementalMarkdownPlugin();
for (const tail of ["text", "[later]: /target", "[later]: /target\n\n[^note]: a note"]) {
expect(render(before + tail, incremental)).toEqual(render(before + tail));
}
});
it("handles edits, replacements, and repeated renders without leaking transformed nodes", () => {
const incremental = createIncrementalMarkdownPlugin();
const documents = [
prefix + "- first\n - second",
prefix + "> [!NOTE]\n> transformed alert",
prefix + "plain text",
"replacement without fences",
prefix.replace("Before", "Edited") + "edited prefix",
prefix + "plain text",
prefix + "plain text",
];
for (const document of documents) {
expect(render(document, incremental)).toEqual(render(document));
}
});
it("does not freeze unclosed, nested, indented, or mismatched fences", () => {
const prefixes = [
"```\nopen\n\n",
"````\n```\n\n",
"> ```\n> code\n> ```\n\n",
"- ```\n code\n ```\n\n",
" ```\n code\n ```\n\n",
"