Skip to content

Commit 7475ed7

Browse files
feat(editor): support markdown shortcuts for block types (#207) (#220)
Add MarkdownShortcutPlugin from @lexical/react with the existing MARKDOWN_TRANSFORMERS array. Typing standard markdown prefixes now converts to the corresponding block type inline: - # / ## / ### + space → headings - > + space → blockquote - --- → horizontal rule - - or * + space → bullet list - 1. + space → numbered list - ``` → code block Co-authored-by: Ona <no-reply@ona.com>
1 parent b2de345 commit 7475ed7

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

src/components/editor/editor.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ListTabIndentationPlugin } from "@/components/editor/list-tab-indentati
1313
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
1414
import { ClickableLinkPlugin } from "@lexical/react/LexicalClickableLinkPlugin";
1515
import { HorizontalRulePlugin } from "@lexical/react/LexicalHorizontalRulePlugin";
16+
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
1617
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
1718
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
1819
import { ListNode, ListItemNode } from "@lexical/list";
@@ -31,6 +32,7 @@ import { FloatingToolbarPlugin } from "@/components/editor/floating-toolbar-plug
3132
import { FloatingLinkEditorPlugin } from "@/components/editor/floating-link-editor-plugin";
3233
import { CodeHighlightPlugin } from "@/components/editor/code-highlight-plugin";
3334
import { DraggableBlockPlugin } from "@/components/editor/draggable-block-plugin";
35+
import { MARKDOWN_TRANSFORMERS } from "@/components/editor/markdown-utils";
3436
import { ImageNode } from "@/components/editor/image-node";
3537
import { ImagePlugin } from "@/components/editor/image-plugin";
3638
import { CalloutNode } from "@/components/editor/callout-node";
@@ -199,6 +201,7 @@ export function Editor({ pageId, initialContent, editorRef }: EditorProps) {
199201
<LinkPlugin validateUrl={validateUrl} />
200202
<ClickableLinkPlugin />
201203
<HorizontalRulePlugin />
204+
<MarkdownShortcutPlugin transformers={MARKDOWN_TRANSFORMERS} />
202205
<CodeHighlightPlugin />
203206
<ImagePlugin />
204207
<CalloutPlugin />
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { describe, it, expect } from "vitest";
2+
import { readFileSync } from "fs";
3+
import { resolve } from "path";
4+
import { MARKDOWN_TRANSFORMERS } from "./markdown-utils";
5+
6+
function readSource(relativePath: string): string {
7+
return readFileSync(resolve(__dirname, relativePath), "utf-8");
8+
}
9+
10+
describe("MarkdownShortcutPlugin integration", () => {
11+
const editorSource = readSource("./editor.tsx");
12+
13+
it("editor imports MarkdownShortcutPlugin", () => {
14+
expect(editorSource).toContain(
15+
'import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"'
16+
);
17+
});
18+
19+
it("editor renders MarkdownShortcutPlugin with MARKDOWN_TRANSFORMERS", () => {
20+
expect(editorSource).toContain(
21+
"<MarkdownShortcutPlugin transformers={MARKDOWN_TRANSFORMERS} />"
22+
);
23+
});
24+
25+
it("editor imports MARKDOWN_TRANSFORMERS from markdown-utils", () => {
26+
expect(editorSource).toContain(
27+
'import { MARKDOWN_TRANSFORMERS } from "@/components/editor/markdown-utils"'
28+
);
29+
});
30+
});
31+
32+
describe("MARKDOWN_TRANSFORMERS coverage", () => {
33+
// Verify the transformer array includes all required shortcut types
34+
const transformerTypes = MARKDOWN_TRANSFORMERS.map((t) => t.type);
35+
36+
it("includes element transformers for block-level shortcuts", () => {
37+
// HEADING, QUOTE, CODE, UNORDERED_LIST, ORDERED_LIST, CHECK_LIST,
38+
// HORIZONTAL_RULE are all element transformers
39+
const elementCount = transformerTypes.filter(
40+
(t) => t === "element" || t === "multiline-element"
41+
).length;
42+
expect(elementCount).toBeGreaterThanOrEqual(7);
43+
});
44+
45+
it("includes text-format transformers for inline shortcuts", () => {
46+
const textFormatCount = transformerTypes.filter(
47+
(t) => t === "text-format"
48+
).length;
49+
expect(textFormatCount).toBeGreaterThanOrEqual(4);
50+
});
51+
52+
it("includes text-match transformers for links", () => {
53+
const textMatchCount = transformerTypes.filter(
54+
(t) => t === "text-match"
55+
).length;
56+
expect(textMatchCount).toBeGreaterThanOrEqual(1);
57+
});
58+
59+
it("HEADING transformer matches # syntax", () => {
60+
const heading = MARKDOWN_TRANSFORMERS.find(
61+
(t) => t.type === "element" && "regExp" in t && t.regExp?.source.includes("#")
62+
);
63+
expect(heading).toBeDefined();
64+
});
65+
66+
it("QUOTE transformer matches > syntax", () => {
67+
const quote = MARKDOWN_TRANSFORMERS.find(
68+
(t) => t.type === "element" && "regExp" in t && t.regExp?.source.includes(">")
69+
);
70+
expect(quote).toBeDefined();
71+
});
72+
73+
it("HORIZONTAL_RULE transformer matches --- syntax", () => {
74+
const hr = MARKDOWN_TRANSFORMERS.find(
75+
(t) => t.type === "element" && "regExp" in t && t.regExp?.source.includes("---")
76+
);
77+
expect(hr).toBeDefined();
78+
});
79+
80+
it("UNORDERED_LIST transformer matches - or * syntax", () => {
81+
const ul = MARKDOWN_TRANSFORMERS.find(
82+
(t) =>
83+
t.type === "element" &&
84+
"regExp" in t &&
85+
(t.regExp?.source.includes("-") || t.regExp?.source.includes("*"))
86+
);
87+
expect(ul).toBeDefined();
88+
});
89+
90+
it("ORDERED_LIST transformer matches 1. syntax", () => {
91+
const ol = MARKDOWN_TRANSFORMERS.find(
92+
(t) =>
93+
t.type === "element" &&
94+
"regExp" in t &&
95+
t.regExp?.source.includes("\\d")
96+
);
97+
expect(ol).toBeDefined();
98+
});
99+
100+
it("includes a CODE multiline-element transformer for ``` syntax", () => {
101+
const code = MARKDOWN_TRANSFORMERS.find(
102+
(t) => t.type === "multiline-element"
103+
);
104+
expect(code).toBeDefined();
105+
});
106+
});

0 commit comments

Comments
 (0)