diff --git a/src/editor/plugins/autocomplete/transformers/index.ts b/src/editor/plugins/autocomplete/transformers/index.ts index 7b57b2b..f9235ae 100644 --- a/src/editor/plugins/autocomplete/transformers/index.ts +++ b/src/editor/plugins/autocomplete/transformers/index.ts @@ -4,12 +4,14 @@ import arrows from "./arrows"; import blockquote from "./blockquote"; import heading from "./heading"; import bullet_list from "./bullet_list"; +import link from "./link"; const transformers: { [key: string]: Transformer } = { arrows, heading, blockquote, bullet_list, + link, // }; diff --git a/src/editor/plugins/autocomplete/transformers/link.test.ts b/src/editor/plugins/autocomplete/transformers/link.test.ts new file mode 100644 index 0000000..77cd1dd --- /dev/null +++ b/src/editor/plugins/autocomplete/transformers/link.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import linkTransformer from "./link"; + +describe("transformer.link", () => { + describe("activate", () => { + it("activates for valid markdown link", () => { + const text = "[Example](https://example.com)"; + const props = linkTransformer.activate(text); + expect(props).toEqual({ + title: "Example", + url: "https://example.com", + matchLength: text.length, + }); + }); + + it("returns undefined for non-link text", () => { + const props = linkTransformer.activate("not a link"); + expect(props).toBeUndefined(); + }); + + it("only uses first link in string", () => { + const props = linkTransformer.activate( + "foo [One](https://one.com) bar [Two](https://two.com)", + ); + expect(props?.title).toBe("One"); + expect(props?.url).toBe("https://one.com"); + }); + }); +}); diff --git a/src/editor/plugins/autocomplete/transformers/link.ts b/src/editor/plugins/autocomplete/transformers/link.ts new file mode 100644 index 0000000..0e961aa --- /dev/null +++ b/src/editor/plugins/autocomplete/transformers/link.ts @@ -0,0 +1,51 @@ +import { TextSelection } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; +import { schema } from "prosemirror-markdown"; + +import type { activator, transformer, Transformer } from "../types"; + +const reLink = /\[([^\]]+)\]\(([^)]+)\)/; + +interface Props { + title: string; + url: string; + matchLength: number; +} + +const activate: activator = (text: string): undefined | Props => { + const match = reLink.exec(text); + if (match) { + return { + title: match[1], + url: match[2], + matchLength: match[0].length, + }; + } + return undefined; +}; + +const transform: transformer = ( + view: EditorView, + _: string, + { title, url, matchLength }: Props, +): boolean => { + const node = schema.text(title, [schema.marks.link.create({ href: url })]); + + const { $cursor } = view.state.selection as TextSelection; + if (!$cursor) return false; + view.dispatch( + view.state.tr + .replaceRangeWith($cursor.pos - matchLength, $cursor.pos, node) + .insertText(" ") + .scrollIntoView(), + ); + + return true; +}; + +const _transformer: Transformer = { + activate, + transform, +}; + +export default _transformer;