1import { describe, expect, test } from "bun:test";
2import { markdownDialect, markdownRuns } from "../src/markdown.ts";
3import { applyRuns } from "../src/text.ts";
4
5const edit = (md: string, before: string[], after: string[], options = {}) =>
6 applyRuns(
7 md,
8 markdownRuns(md, 0, md.length, options),
9 markdownDialect(options),
10 {
11 before,
12 after,
13 },
14 );
15
16describe("markdownRuns", () => {
17 test("splits at inline syntax the way it renders", () => {
18 const md = "Use `foo` with **bold** and [a link](/x) or _em_.";
19 const runs = markdownRuns(md, 0, md.length).map((r) =>
20 md.slice(r.start, r.end),
21 );
22 expect(runs.filter(Boolean)).toEqual([
23 "Use ",
24 "foo",
25 " with ",
26 "bold",
27 " and ",
28 "a link",
29 " or ",
30 "em",
31 ".",
32 ]);
33 });
34
35 test("keeps intraword underscores and lone asterisks as text", () => {
36 const md = "snake_case_name and 5 * 3";
37 const runs = markdownRuns(md, 0, md.length).filter((r) => r.end > r.start);
38 expect(runs).toHaveLength(1);
39 });
40
41 test("treats bare URLs as generated text", () => {
42 const md = "see https://alchemy.run/docs. now";
43 const runs = markdownRuns(md, 0, md.length);
44 expect(runs.find((r) => r.kind === "locked")?.expected).toBe(
45 "https://alchemy.run/docs",
46 );
47 });
48});
49
50describe("markdown edits", () => {
51 test("rewrites only the changed words, keeping syntax", () => {
52 const md = "Use `foo` with **bold** text.";
53 const out = edit(
54 md,
55 ["Use ", "foo", " with ", "bold", " text."],
56 ["Use ", "foo", " with ", "bold", " prose."],
57 );
58 expect(out).toBe("Use `foo` with **bold** prose.");
59 });
60
61 test("matches smart punctuation against straight source", () => {
62 const md = 'It\'s "quoted" -- fine...';
63 const rendered = ["It’s “quoted” – fine…"];
64 const out = edit(md, rendered, ["It’s “quoted” – great…"]);
65 expect(out).toBe('It\'s "quoted" -- great...');
66 });
67
68 test("escapes markdown syntax in typed text", () => {
69 const md = "plain words";
70 const out = edit(md, ["plain words"], ["plain *starred* [x] words"]);
71 expect(out).toBe("plain \\*starred\\* \\[x\\] words");
72 });
73
74 test("escapes block syntax at the start of a line", () => {
75 const md = "first\nsecond";
76 const out = edit(md, ["first\nsecond"], ["first\n#second"]);
77 expect(out).toBe("first\n\\#second");
78 });
79
80 test("never writes */ into prose", () => {
81 const md = "a b";
82 const out = edit(md, ["a b"], ["a */ b"]);
83 expect(out).not.toContain("*/");
84 });
85
86 test("honors a line prefix (JSDoc continuation lines)", () => {
87 const md = "An S3 bucket for\n * storing objects.";
88 const options = { linePrefix: "[ \\t]*\\*(?!/)[ \\t]?" };
89 const out = edit(
90 md,
91 ["An S3 bucket for\nstoring objects."],
92 ["An S3 bucket for\nstoring files."],
93 options,
94 );
95 expect(out).toBe("An S3 bucket for\n * storing files.");
96 });
97
98 test("rejects edits to generated text", () => {
99 const md = "see https://alchemy.run now";
100 expect(() =>
101 edit(
102 md,
103 ["see ", "https://alchemy.run", " now"],
104 ["see ", "https://x.dev", " now"],
105 ),
106 ).toThrow(/generated/);
107 });
108
109 test("edits inside a code span", () => {
110 const md = "run `bun dev` first";
111 const out = edit(
112 md,
113 ["run ", "bun dev", " first"],
114 ["run ", "bun start", " first"],
115 );
116 expect(out).toBe("run `bun start` first");
117 });
118});