| 1 | import { describe, expect, test } from "bun:test"; |
| 2 | import { markdownDialect, markdownRuns } from "../src/markdown.ts"; |
| 3 | import { applyRuns } from "../src/text.ts"; |
| 4 | |
| 5 | const 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 | |
| 16 | describe("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 | |
| 50 | describe("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 | }); |