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( <ReactMarkdown remarkPlugins={[ observeParsing, capture, remarkGfm, remarkGithubAlerts, remarkNormalizeListItemIndentation, remarkCodexDirectives, ...(incremental ? [incremental] : []), ]} rehypePlugins={[rehypeRaw, rehypeSanitize]} > {source} </ReactMarkdown>, ); 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", "<div>\nhello\n\n</div>\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 <https://example.com> *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", "<script>\n```\ncode\n```\n\n", ]; for (const start of prefixes) { const incremental = createIncrementalMarkdownPlugin(); for (const tail of ["", "text", "\n```\n", "\n```\n\nnext"]) { expect(render(start + tail, incremental)).toEqual(render(start + tail)); } } });});
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(
<ReactMarkdown
remarkPlugins={[
observeParsing,
capture,
remarkGfm,
remarkGithubAlerts,
remarkNormalizeListItemIndentation,
remarkCodexDirectives,
...(incremental ? [incremental] : []),
]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
>
{source}
</ReactMarkdown>,
);
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",
"<div>\nhello\n\n</div>\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 <https://example.com> *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",
"<script>\n```\ncode\n```\n\n",
];
for (const start of prefixes) {
const incremental = createIncrementalMarkdownPlugin();
for (const tail of ["", "text", "\n```\n", "\n```\n\nnext"]) {
expect(render(start + tail, incremental)).toEqual(render(start + tail));
}
}
});
});