Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions packages/editor/src/code/code-block-drawing-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest"
import {
createCodeDrawingNodeFromCodeBlock,
getCodeBlockText,
isMermaidCodeBlockLanguage,
} from "./code-block-drawing-utils"
import { CODE_DRAWING_KEY } from "./code-drawing-kit"

describe("code-block-drawing-utils", () => {
it("detects the Mermaid code block language case-insensitively", () => {
expect(isMermaidCodeBlockLanguage("mermaid")).toBe(true)
expect(isMermaidCodeBlockLanguage(" Mermaid ")).toBe(true)
expect(isMermaidCodeBlockLanguage("plantuml")).toBe(false)
})

it("joins code block lines into a single code string", () => {
const code = getCodeBlockText({
children: [
{ type: "code_line", children: [{ text: "flowchart TD" }] },
{ type: "code_line", children: [{ text: " A --> B" }] },
],
} as any)

expect(code).toBe("flowchart TD\n A --> B")
})

it("creates a Mermaid code drawing node from a code block", () => {
const node = createCodeDrawingNodeFromCodeBlock({
children: [
{ type: "code_line", children: [{ text: "sequenceDiagram" }] },
{ type: "code_line", children: [{ text: " Alice->>Bob: Hello" }] },
],
} as any)

expect(node).toEqual({
type: CODE_DRAWING_KEY,
data: {
code: "sequenceDiagram\n Alice->>Bob: Hello",
drawingMode: "Both",
drawingType: "Mermaid",
},
children: [{ text: "" }],
})
})
})
34 changes: 34 additions & 0 deletions packages/editor/src/code/code-block-drawing-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NodeApi, type TCodeBlockElement } from "platejs"
import { CODE_DRAWING_KEY } from "./code-drawing-kit"

export const MERMAID_CODE_BLOCK_LANGUAGE = "mermaid"

export function isMermaidCodeBlockLanguage(lang: string): boolean {
return lang.trim().toLowerCase() === MERMAID_CODE_BLOCK_LANGUAGE
}

export function getCodeBlockText(
codeBlock: Pick<TCodeBlockElement, "children">,
): string {
if (!Array.isArray(codeBlock.children)) {
return ""
}

return codeBlock.children
.map((child) => NodeApi.string(child as any))
.join("\n")
}

export function createCodeDrawingNodeFromCodeBlock(
codeBlock: Pick<TCodeBlockElement, "children">,
) {
return {
type: CODE_DRAWING_KEY,
data: {
code: getCodeBlockText(codeBlock),
drawingMode: "Both",
drawingType: "Mermaid",
},
children: [{ text: "" }],
}
}
22 changes: 18 additions & 4 deletions packages/editor/src/code/node-code-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import {
useReadOnly,
} from "platejs/react"
import { useEffect, useMemo, useState } from "react"
import {
createCodeDrawingNodeFromCodeBlock,
isMermaidCodeBlockLanguage,
} from "./code-block-drawing-utils"

export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
const { editor, element } = props
Expand Down Expand Up @@ -140,10 +144,20 @@ function CodeBlockCombobox() {
className="cursor-pointer"
value={language.value}
onSelect={(value) => {
editor.tf.setNodes<TCodeBlockElement>(
{ lang: value },
{ at: element },
)
if (isMermaidCodeBlockLanguage(value)) {
const path = editor.api.findPath(element)
if (path) {
editor.tf.replaceNodes(
createCodeDrawingNodeFromCodeBlock(element) as any,
{ at: path },
)
}
} else {
editor.tf.setNodes<TCodeBlockElement>(
{ lang: value },
{ at: element },
)
}
setSearchValue(value)
setOpen(false)
}}
Expand Down
86 changes: 86 additions & 0 deletions packages/editor/src/markdown/markdown-kit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { deserializeMd, serializeMd } from "@platejs/markdown"
import { createSlateEditor, KEYS } from "platejs"
import { describe, expect, it } from "vitest"
import { CODE_DRAWING_KEY } from "../code"
import { createNoteTitleBlock } from "../title"
import { MarkdownKit, MarkdownKitNoMdx } from "./markdown-kit"
import { OBSIDIAN_EMBED_KEY } from "./obsidian-embed-plugin"
Expand Down Expand Up @@ -392,6 +393,44 @@ describe("markdown-kit serialization", () => {
expect(markdown).toContain("\\end{equation}")
})

it("serializes Mermaid code drawings as fenced Mermaid blocks", async () => {
const editor = createMarkdownEditor()
const value = [
{
type: CODE_DRAWING_KEY,
data: {
code: "flowchart TD\n A --> B",
drawingType: "Mermaid",
},
children: [{ text: "" }],
},
]

const markdown = serializeMd(editor, { value })
expect(markdown).toContain("```mermaid")
expect(markdown).toContain("flowchart TD")
expect(markdown).toContain("A --> B")
})

it("serializes PlantUml code drawings as fenced plantuml blocks", async () => {
const editor = createMarkdownEditor()
const value = [
{
type: CODE_DRAWING_KEY,
data: {
code: "@startuml\nAlice -> Bob: Hello\n@enduml",
drawingType: "PlantUml",
},
children: [{ text: "" }],
},
]

const markdown = serializeMd(editor, { value })
expect(markdown).toContain("```plantuml")
expect(markdown).toContain("@startuml")
expect(markdown).toContain("Alice -> Bob: Hello")
})

it("serializes canonical tags rows as a yaml list", async () => {
const editor = createMarkdownEditor()
const value = [
Expand Down Expand Up @@ -591,6 +630,53 @@ describe("markdown-kit deserialization", () => {
})
})

it("deserializes Mermaid fenced code blocks into code drawings", async () => {
const editor = createMarkdownEditor()
const value = deserializeMd(
editor,
"```mermaid\nflowchart TD\n A --> B\n```",
)
const codeDrawingNode = findNodeByType(value as any[], CODE_DRAWING_KEY)

expect(codeDrawingNode).toMatchObject({
type: CODE_DRAWING_KEY,
data: {
code: "flowchart TD\n A --> B",
drawingType: "Mermaid",
},
})
})

it("deserializes plantuml fenced code blocks into PlantUml drawings", async () => {
const editor = createMarkdownEditor()
const value = deserializeMd(
editor,
"```plantuml\n@startuml\nAlice -> Bob: Hello\n@enduml\n```",
)
const codeDrawingNode = findNodeByType(value as any[], CODE_DRAWING_KEY)

expect(codeDrawingNode).toMatchObject({
type: CODE_DRAWING_KEY,
data: {
code: "@startuml\nAlice -> Bob: Hello\n@enduml",
drawingType: "PlantUml",
},
})
})

it("round-trips Mermaid fenced code blocks", async () => {
const editor = createMarkdownEditor()
const value = deserializeMd(
editor,
"```mermaid\nsequenceDiagram\n Alice->>Bob: Hello\n```",
)

const markdown = serializeMd(editor, { value: value as any })
expect(markdown).toContain("```mermaid")
expect(markdown).toContain("sequenceDiagram")
expect(markdown).toContain("Alice->>Bob: Hello")
})

it("deserializes Obsidian callouts into callout nodes", async () => {
const editor = createMarkdownEditor()
const value = deserializeMd(editor, "> [!warning] Watch out\n> Body")
Expand Down
94 changes: 94 additions & 0 deletions packages/editor/src/markdown/markdown-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import remarkFrontmatter from "remark-frontmatter"
import remarkGfm from "remark-gfm"
import remarkMath from "remark-math"
import YAML from "yaml"
import { CODE_DRAWING_KEY } from "../code/code-drawing-kit"
import type { FrontmatterRow as KVRow } from "../frontmatter"
import {
convertValueToType,
Expand Down Expand Up @@ -67,9 +68,11 @@ type MdastRoot = {
type MdastNode = {
type?: string
children?: MdastNode[]
lang?: string
value?: string
data?: {
alias?: string
drawingType?: string
hName?: string
hProperties?: Record<string, unknown>
path?: string
Expand All @@ -81,6 +84,16 @@ type SlateNodeWithChildren = {
[key: string]: unknown
}

const DRAWING_TYPE_BY_FENCE_LANGUAGE = {
flowchart: "Flowchart",
graphviz: "Graphviz",
mermaid: "Mermaid",
plantuml: "PlantUml",
} as const

type SupportedDrawingType =
(typeof DRAWING_TYPE_BY_FENCE_LANGUAGE)[keyof typeof DRAWING_TYPE_BY_FENCE_LANGUAGE]

function createRowId() {
return Math.random().toString(36).slice(2, 9)
}
Expand Down Expand Up @@ -244,6 +257,56 @@ function createEmbedNodeData(
}
}

function visitMdast(node: MdastNode, visitor: (node: MdastNode) => void) {
visitor(node)

for (const child of node.children ?? []) {
visitMdast(child, visitor)
}
}

function fenceLanguageToDrawingType(
lang: unknown,
): SupportedDrawingType | undefined {
if (typeof lang !== "string") {
return undefined
}

return DRAWING_TYPE_BY_FENCE_LANGUAGE[
lang.trim().toLowerCase() as keyof typeof DRAWING_TYPE_BY_FENCE_LANGUAGE
]
}

function drawingTypeToFenceLanguage(drawingType: unknown): string {
switch (drawingType) {
case "Flowchart":
return "flowchart"
case "Graphviz":
return "graphviz"
case "PlantUml":
return "plantuml"
default:
return "mermaid"
}
}
Comment thread
hhhjin marked this conversation as resolved.

export function remarkCodeDrawingBridge() {
return (tree: MdastRoot) => {
visitMdast(tree, (node) => {
if (node.type !== "code") return

const drawingType = fenceLanguageToDrawingType(node.lang)
if (!drawingType) return

node.type = "codeDrawing"
node.data = {
...node.data,
drawingType,
}
})
}
}

export type CreateMarkdownKitOptions = {
mdx?: boolean
}
Expand All @@ -259,6 +322,7 @@ export const createMarkdownKit = ({
remarkFrontmatter,
remarkWikiLink,
remarkCallout,
remarkCodeDrawingBridge,
remarkObsidianCalloutBridge,
] as any[]

Expand Down Expand Up @@ -335,6 +399,20 @@ export const createMarkdownKit = ({
}
},
},
[CODE_DRAWING_KEY]: {
serialize: (
node: SlateNodeWithChildren & {
data?: {
code?: string
drawingType?: string
}
},
) => ({
type: "code",
lang: drawingTypeToFenceLanguage(node.data?.drawingType),
value: node.data?.code ?? "",
}),
},
yaml: {
deserialize: (
mdastNode: MdRootContent,
Expand All @@ -352,6 +430,22 @@ export const createMarkdownKit = ({
}
},
},
codeDrawing: {
deserialize: (mdastNode: MdastNode) => {
const drawingType =
fenceLanguageToDrawingType(mdastNode.data?.drawingType) ??
"Mermaid"
Comment thread
hhhjin marked this conversation as resolved.

return {
type: CODE_DRAWING_KEY,
data: {
code: mdastNode.value ?? "",
drawingType,
},
children: [{ text: "" }],
}
},
},
[KEYS.link]: {
serialize: (
node: SlateNodeWithChildren & {
Expand Down
Loading