Spaces:
Paused
Paused
File size: 6,217 Bytes
fae625a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | import { describe, expect, test } from "vitest";
import { parseMarkdownIntoBlocks } from "./parseBlocks";
describe("parseMarkdownIntoBlocks", () => {
describe("basic splitting", () => {
test("splits paragraphs into separate blocks", () => {
const blocks = parseMarkdownIntoBlocks("First paragraph.\n\nSecond paragraph.");
const content = blocks.filter((b) => b.trim().length > 0);
expect(content).toHaveLength(2);
expect(content[0]).toContain("First paragraph.");
expect(content[1]).toContain("Second paragraph.");
});
test("reassembles the original document exactly", () => {
const markdown = "# Title\n\nSome text with **bold**.\n\n```js\nconst x = 1;\n```\n\nEnd.";
const blocks = parseMarkdownIntoBlocks(markdown);
expect(blocks.join("")).toBe(markdown);
});
});
describe("footnote detection", () => {
test("returns a single block when real footnotes are present", () => {
const markdown = "Here is a footnote[^1].\n\n[^1]: This is the footnote content.\n";
expect(parseMarkdownIntoBlocks(markdown)).toHaveLength(1);
});
test("does not treat regex negated character classes as footnotes", () => {
const markdown = [
"# Regex Examples",
"",
"Here are some useful regex patterns.",
"",
"```perl",
'https?://[^\\s<>"{}|\\\\^`\\[\\]]+',
"```",
"",
"More text after the code block.",
"",
].join("\n");
expect(parseMarkdownIntoBlocks(markdown).length).toBeGreaterThan(1);
});
test("does not match short regex classes like [^>] as footnotes", () => {
const markdown = [
"# Parser Code",
"",
"Some explanation.",
"",
"```js",
"const regex = /[^>]+/;",
"const other = /[^)]/;",
"```",
"",
"End of document.",
"",
].join("\n");
expect(parseMarkdownIntoBlocks(markdown).length).toBeGreaterThan(1);
});
});
describe("HTML block handling", () => {
test("void elements like <br> do not swallow subsequent blocks", () => {
const markdown = "First paragraph.\n\n<br>\n\nSecond paragraph.\n\nThird paragraph.";
const blocks = parseMarkdownIntoBlocks(markdown);
const second = blocks.find((b) => b.includes("Second paragraph."));
expect(second).toBeTruthy();
expect(second).not.toContain("Third paragraph.");
});
test("self-closing tags like <div /> do not swallow subsequent blocks", () => {
const markdown = "<div />\n\nNext paragraph.\n\nAnother paragraph.";
const blocks = parseMarkdownIntoBlocks(markdown);
const next = blocks.find((b) => b.includes("Next paragraph."));
expect(next).toBeTruthy();
expect(next).not.toContain("Another paragraph.");
});
test("keeps an unclosed HTML element in one block until it closes", () => {
const markdown = "<div>\nFirst part\n\nMiddle part\n\n</div>\n\nAfter.";
const blocks = parseMarkdownIntoBlocks(markdown);
const htmlBlock = blocks.find((b) => b.includes("<div>"));
expect(htmlBlock).toContain("Middle part");
expect(htmlBlock).toContain("</div>");
expect(htmlBlock).not.toContain("After.");
});
test("nested same-tag elements are not closed prematurely", () => {
const markdown = "<div>\nA\n\n<div>\nB\n</div>\n\nC\n</div>\n\nAfter.";
const blocks = parseMarkdownIntoBlocks(markdown);
const htmlBlock = blocks.find((b) => b.includes("<div>"));
expect(htmlBlock).toContain("A");
expect(htmlBlock).toContain("B");
expect(htmlBlock).toContain("C");
expect(htmlBlock).not.toContain("After.");
const after = blocks.find((b) => b.includes("After."));
expect(after).toBeTruthy();
});
});
describe("math block handling", () => {
test("keeps a math block with its closing $$ in one block", () => {
const markdown = "Some text.\n\n$$\nx = y + z\n$$\n\nMore text.";
const blocks = parseMarkdownIntoBlocks(markdown);
const mathBlock = blocks.find((b) => b.includes("$$") && b.includes("x = y"));
expect(mathBlock).toBeTruthy();
expect((mathBlock?.match(/\$\$/g) || []).length).toBe(2);
});
test("merges math blocks split by setext-like lines (= on its own line)", () => {
const markdown = "Before.\n\n$$\nE\n=\nmc^2\n$$\n\nAfter.";
const blocks = parseMarkdownIntoBlocks(markdown);
const mathBlock = blocks.find((b) => b.includes("mc^2"));
expect(mathBlock).toBeTruthy();
expect((mathBlock?.match(/\$\$/g) || []).length).toBe(2);
const after = blocks.find((b) => b.includes("After."));
expect(after).toBeTruthy();
expect(after).not.toContain("mc^2");
});
test("does not treat $$ inside code blocks as math delimiters", () => {
const markdown = "```bash\n# Process tree\npstree -p $$\necho $$\n```\n\nSome text after.";
const blocks = parseMarkdownIntoBlocks(markdown);
expect(blocks.length).toBeGreaterThan(1);
const codeBlock = blocks.find((b) => b.includes("```"));
expect(codeBlock).toContain("pstree -p $$");
expect(codeBlock).not.toContain("Some text after.");
expect(blocks.find((b) => b.trim() === "$$")).toBeUndefined();
});
test("handles a code block followed by a math block", () => {
const markdown = "```bash\necho $$\n```\n\n$$\nmath here\n$$";
const blocks = parseMarkdownIntoBlocks(markdown);
const codeBlock = blocks.find((b) => b.includes("```") && b.includes("echo $$"));
expect(codeBlock).toBeTruthy();
const mathBlock = blocks.find((b) => b.trim().startsWith("$$") && b.includes("math here"));
expect(mathBlock).toBeTruthy();
});
});
describe("streaming stability", () => {
test("completed blocks stay byte-identical as the document grows", () => {
const doc = [
"Paragraph one with some text.",
"Paragraph two with **bold** and `code`.",
"```js\nconst x = 1;\nconst y = 2;\n```",
"A final paragraph at the end.",
].join("\n\n");
const finalBlocks = parseMarkdownIntoBlocks(doc);
for (let i = 1; i <= doc.length; i++) {
const partialBlocks = parseMarkdownIntoBlocks(doc.slice(0, i));
// All blocks except the in-flight last one must already match the final
// parse, otherwise memoization breaks and blocks re-render while streaming.
for (let b = 0; b < partialBlocks.length - 1; b++) {
expect(partialBlocks[b]).toBe(finalBlocks[b]);
}
}
});
});
});
|