import { describe, expect, it } from "vitest"; import { code, codeBlock, findTrigger, inlineCode, insertInline, link, markdownLink, list, pasteLink, separator, table, uploadedImage, wrap, type Edit, type Selection } from "#lib/components/ui/markdownFormat.ts"; const select = (marked: string): Selection => { const start = marked.indexOf("«"); const end = marked.indexOf("»") - 1; return { value: marked.replace("«", "").replace("»", ""), start, end }; }; const result = ({ value }: Selection, edit: Edit) => { const next = value.slice(0, edit.from) + edit.insert + value.slice(edit.to); return `${next.slice(0, edit.selectionStart)}«${next.slice(edit.selectionStart, edit.selectionEnd)}»${next.slice(edit.selectionEnd)}`; }; const run = (marked: string, format: (selection: Selection) => Edit) => { const selection = select(marked); return result(selection, format(selection)); }; describe("wrap", () => { it("wraps the selection and keeps it selected", () => { expect(run("say «hello» there", (s) => wrap(s, "**"))).toBe("say **«hello»** there"); }); it("places the caret between the markers when nothing is selected", () => { expect(run("say «»", (s) => wrap(s, "_"))).toBe("say _«»_"); }); it("unwraps when the markers surround the selection", () => { expect(run("say **«hello»** there", (s) => wrap(s, "**"))).toBe("say «hello» there"); }); it("unwraps when the selection includes the markers", () => { expect(run("say «~~hello~~» there", (s) => wrap(s, "~~"))).toBe("say «hello» there"); }); }); describe("code", () => { it("uses backticks for a single line", () => { expect(run("run «make» now", code)).toBe("run `«make»` now"); }); it("uses a fence for several lines", () => { expect(run("«a\nb»", code)).toBe("```\n«a\nb»\n```"); }); }); describe("codeBlock", () => { it("pads the fence with blank lines", () => { expect(run("intro\n«x»\noutro", codeBlock)).toBe("intro\n\n```\n«x»\n```\n\noutro"); }); }); describe("link", () => { it("selects the url placeholder around selected text", () => { expect(run("see «docs»", link)).toBe("see [docs](«url»)"); }); it("puts a selected url in the destination", () => { expect(run("«https://tangled.org»", link)).toBe("[«»](https://tangled.org)"); }); }); describe("pasteLink", () => { const paste = (marked: string, pasted: string) => { const selection = select(marked); const edit = pasteLink(selection, pasted); return edit && result(selection, edit); }; it("links the selected text to a pasted url", () => { expect(paste("see «docs» here", " https://tangled.org\n")).toBe( "see [docs](https://tangled.org)«» here" ); }); it("leaves pastes without a selection alone", () => { expect(paste("see «»", "https://tangled.org")).toBeNull(); }); it("leaves pastes that are not a url alone", () => { expect(paste("«docs»", "tangled docs")).toBeNull(); }); it("replaces a selected url with the pasted one", () => { expect(paste("«https://a.org»", "https://tangled.org")).toBeNull(); }); it("leaves multi-line selections alone", () => { expect(paste("«one\ntwo»", "https://tangled.org")).toBeNull(); }); it("leaves a selected link destination alone", () => { expect(paste("![](«url»)", "https://tangled.org")).toBeNull(); expect(paste("[docs](«url»)", "https://tangled.org")).toBeNull(); }); }); describe("separator", () => { it("inserts a rule after the selection on its own paragraph", () => { expect(run("para«»", separator)).toBe("para\n\n---«»"); }); it("keeps following text a paragraph away", () => { expect(run("one«»\ntwo", separator)).toBe("one\n\n---«»\n\ntwo"); }); }); describe("uploadedImage", () => { const size = { width: 640, height: 480 }; const upload = (selection: Selection) => uploadedImage(selection, "/blob/x/bafy", size); it("inserts an img block with the selection as its alt", () => { expect(run("see «the cat» here", upload)).toBe( 'see \n\n«the cat»\n\n here' ); }); it("escapes the alt and keeps it on one line", () => { expect(run('«a "b" \n& d»', upload)).toBe( '«a "b" <c> & d»' ); }); it("falls back to a generic alt", () => { expect(run("«»", upload)).toBe( '«Image»' ); }); }); describe("table", () => { it("selects the first header cell", () => { expect(run("«»", table)).toBe( "| «Column» | Column |\n| ------ | ------ |\n| Cell | Cell |" ); }); }); describe("list", () => { it("prefixes every selected line", () => { expect(run("«one\ntwo»", (s) => list(s, "ordered"))).toBe("«1. one\n2. two»"); }); it("removes the prefix when every line already has it", () => { expect(run("«- [ ] one\n- [ ] two»", (s) => list(s, "task"))).toBe("«one\ntwo»"); }); it("keeps the caret on the same text for a single line", () => { expect(run("item«»", (s) => list(s, "bullet"))).toBe("- item«»"); }); it("does not treat a task item as a bullet", () => { expect(run("«- [ ] one»", (s) => list(s, "bullet"))).toBe("- «- [ ] one»"); }); }); describe("findTrigger", () => { it("finds a mention being typed", () => { expect(findTrigger("hi @ada.te", 10)).toEqual({ char: "@", query: "ada.te", from: 3, to: 10 }); }); it("finds a reference at the start of a line", () => { expect(findTrigger("one\n#12", 7)).toEqual({ char: "#", query: "12", from: 4, to: 7 }); }); it("ignores markers glued to other text", () => { expect(findTrigger("mail@ada", 8)).toBeNull(); expect(findTrigger("##", 2)).toBeNull(); }); it("stops at whitespace", () => { expect(findTrigger("# heading", 9)).toBeNull(); }); }); describe("insertInline", () => { const quote = (s: Selection) => insertInline(s, "Q"); it("puts text on an empty line as is", () => { expect(run("«»", quote)).toBe("Q«»"); expect(run("one\n«»", quote)).toBe("one\nQ«»"); }); it("starts a new line after text that ends its line", () => { expect(run("one«»", quote)).toBe("one\nQ«»"); expect(run("one «»\ntwo", quote)).toBe("one \nQ«»\ntwo"); }); it("keeps a space either side within a line", () => { expect(run("one«» two", quote)).toBe("one Q«» two"); expect(run("one «»two", quote)).toBe("one Q «»two"); }); it("replaces a selection as is", () => { expect(run("one «two» three", quote)).toBe("one Q«» three"); }); }); describe("inlineCode", () => { it("fences past the backticks inside", () => { expect(inlineCode("a.ts:3")).toBe("`a.ts:3`"); expect(inlineCode("a`b")).toBe("``a`b``"); expect(inlineCode("`a")).toBe("`` `a ``"); }); }); describe("markdownLink", () => { it("keeps parens in the url", () => { expect(markdownLink("x", "https://a.b/c(1)#d")).toBe("[x]()"); }); it("encodes what <> cannot hold", () => { expect(markdownLink("x", "https://a.b/")).toBe("[x]()"); }); });