feat: paste as mermaid if applicable (#8116)

This commit is contained in:
David Luzar 2024-06-11 19:19:22 +02:00 committed by GitHub
parent 63dee03ef0
commit 22b39277f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 179 additions and 27 deletions

View File

@ -49,6 +49,7 @@ import {
import type { PastedMixedContent } from "../clipboard";
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
import type { EXPORT_IMAGE_TYPES } from "../constants";
import { DEFAULT_FONT_SIZE } from "../constants";
import {
APP_NAME,
CURSOR_TYPE,
@ -435,6 +436,7 @@ import {
import { getShortcutFromShortcutName } from "../actions/shortcuts";
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
import { getVisibleSceneBounds } from "../element/bounds";
import { isMaybeMermaidDefinition } from "../mermaid";
const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!);
@ -3050,6 +3052,33 @@ class App extends React.Component<AppProps, AppState> {
retainSeed: isPlainPaste,
});
} else if (data.text) {
if (data.text && isMaybeMermaidDefinition(data.text)) {
const api = await import("@excalidraw/mermaid-to-excalidraw");
try {
const { elements: skeletonElements, files } =
await api.parseMermaidToExcalidraw(data.text, {
fontSize: DEFAULT_FONT_SIZE,
});
const elements = convertToExcalidrawElements(skeletonElements, {
regenerateIds: true,
});
this.addElementsFromPasteOrLibrary({
elements,
files,
position: "cursor",
});
return;
} catch (err: any) {
console.warn(
`parsing pasted text as mermaid definition failed: ${err.message}`,
);
}
}
const nonEmptyLines = normalizeEOL(data.text)
.split(/\n+/)
.map((s) => s.trim())

View File

@ -0,0 +1,32 @@
/** heuristically checks whether the text may be a mermaid diagram definition */
export const isMaybeMermaidDefinition = (text: string) => {
const chartTypes = [
"flowchart",
"sequenceDiagram",
"classDiagram",
"stateDiagram",
"stateDiagram-v2",
"erDiagram",
"journey",
"gantt",
"pie",
"quadrantChart",
"requirementDiagram",
"gitGraph",
"C4Context",
"mindmap",
"timeline",
"zenuml",
"sankey",
"xychart",
"block",
];
const re = new RegExp(
`^(?:%%{.*?}%%[\\s\\n]*)?\\b${chartTypes
.map((x) => `${x}(-beta)?`)
.join("|")}\\b`,
);
return re.test(text.trim());
};

View File

@ -1,28 +1,12 @@
import { act, render, waitFor } from "./test-utils";
import { Excalidraw } from "../index";
import React from "react";
import { expect, vi } from "vitest";
import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw";
import { expect } from "vitest";
import { getTextEditor, updateTextEditor } from "./queries/dom";
import { mockMermaidToExcalidraw } from "./helpers/mocks";
vi.mock("@excalidraw/mermaid-to-excalidraw", async (importActual) => {
const module = (await importActual()) as any;
return {
__esModule: true,
...module,
};
});
const parseMermaidToExcalidrawSpy = vi.spyOn(
MermaidToExcalidraw,
"parseMermaidToExcalidraw",
);
parseMermaidToExcalidrawSpy.mockImplementation(
async (
definition: string,
options?: MermaidToExcalidraw.MermaidOptions | undefined,
) => {
mockMermaidToExcalidraw({
mockRef: true,
parseMermaidToExcalidraw: async (definition) => {
const firstLine = definition.split("\n")[0];
return new Promise((resolve, reject) => {
if (firstLine === "flowchart TD") {
@ -88,12 +72,6 @@ parseMermaidToExcalidrawSpy.mockImplementation(
}
});
},
);
vi.spyOn(React, "useRef").mockReturnValue({
current: {
parseMermaidToExcalidraw: parseMermaidToExcalidrawSpy,
},
});
describe("Test <MermaidToExcalidraw/>", () => {

View File

@ -13,6 +13,7 @@ import type { NormalizedZoomValue } from "../types";
import { API } from "./helpers/api";
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
import { arrayToMap } from "../utils";
import { mockMermaidToExcalidraw } from "./helpers/mocks";
const { h } = window;
@ -435,3 +436,83 @@ describe("pasting & frames", () => {
});
});
});
describe("clipboard - pasting mermaid definition", () => {
beforeAll(() => {
mockMermaidToExcalidraw({
parseMermaidToExcalidraw: async (definition) => {
const lines = definition.split("\n");
return new Promise((resolve, reject) => {
if (lines.some((line) => line === "flowchart TD")) {
resolve({
elements: [
{
id: "rect1",
type: "rectangle",
groupIds: [],
x: 0,
y: 0,
width: 69.703125,
height: 44,
strokeWidth: 2,
label: {
groupIds: [],
text: "A",
fontSize: 20,
},
link: null,
},
],
});
} else {
reject(new Error("ERROR"));
}
});
},
});
});
it("should detect and paste as mermaid", async () => {
const text = "flowchart TD\nA";
pasteWithCtrlCmdV(text);
await waitFor(() => {
expect(h.elements.length).toEqual(2);
expect(h.elements).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "rectangle" }),
expect.objectContaining({ type: "text", text: "A" }),
]),
);
});
});
it("should support directives", async () => {
const text = "%%{init: { **config** } }%%\nflowchart TD\nA";
pasteWithCtrlCmdV(text);
await waitFor(() => {
expect(h.elements.length).toEqual(2);
expect(h.elements).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "rectangle" }),
expect.objectContaining({ type: "text", text: "A" }),
]),
);
});
});
it("should paste as normal text if invalid mermaid", async () => {
const text = "flowchart TD xx\nA";
pasteWithCtrlCmdV(text);
await waitFor(() => {
expect(h.elements.length).toEqual(2);
expect(h.elements).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "text", text: "flowchart TD xx" }),
expect.objectContaining({ type: "text", text: "A" }),
]),
);
});
});
});

View File

@ -0,0 +1,32 @@
import { vi } from "vitest";
import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw";
import type { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
import React from "react";
export const mockMermaidToExcalidraw = (opts: {
parseMermaidToExcalidraw: typeof parseMermaidToExcalidraw;
mockRef?: boolean;
}) => {
vi.mock("@excalidraw/mermaid-to-excalidraw", async (importActual) => {
const module = (await importActual()) as any;
return {
__esModule: true,
...module,
};
});
const parseMermaidToExcalidrawSpy = vi.spyOn(
MermaidToExcalidraw,
"parseMermaidToExcalidraw",
);
parseMermaidToExcalidrawSpy.mockImplementation(opts.parseMermaidToExcalidraw);
if (opts.mockRef) {
vi.spyOn(React, "useRef").mockReturnValue({
current: {
parseMermaidToExcalidraw: parseMermaidToExcalidrawSpy,
},
});
}
};