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("", "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
\n\n here'
);
});
it("escapes the alt and keeps it on one line", () => {
expect(run('«a "b" \n& d»', upload)).toBe(
'
'
);
});
it("falls back to a generic alt", () => {
expect(run("«»", upload)).toBe(
'
'
);
});
});
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]()");
});
});