mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: paste as mermaid if applicable (#8116)
This commit is contained in:
parent
63dee03ef0
commit
22b39277f5
5 changed files with 179 additions and 27 deletions
|
@ -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())
|
||||
|
|
32
packages/excalidraw/mermaid.ts
Normal file
32
packages/excalidraw/mermaid.ts
Normal 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());
|
||||
};
|
|
@ -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/>", () => {
|
||||
|
|
|
@ -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" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
32
packages/excalidraw/tests/helpers/mocks.ts
Normal file
32
packages/excalidraw/tests/helpers/mocks.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue