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 type { PastedMixedContent } from "../clipboard";
|
||||||
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
|
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
|
||||||
import type { EXPORT_IMAGE_TYPES } from "../constants";
|
import type { EXPORT_IMAGE_TYPES } from "../constants";
|
||||||
|
import { DEFAULT_FONT_SIZE } from "../constants";
|
||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
CURSOR_TYPE,
|
CURSOR_TYPE,
|
||||||
|
@ -435,6 +436,7 @@ import {
|
||||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||||
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
||||||
import { getVisibleSceneBounds } from "../element/bounds";
|
import { getVisibleSceneBounds } from "../element/bounds";
|
||||||
|
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
|
@ -3050,6 +3052,33 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
retainSeed: isPlainPaste,
|
retainSeed: isPlainPaste,
|
||||||
});
|
});
|
||||||
} else if (data.text) {
|
} 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)
|
const nonEmptyLines = normalizeEOL(data.text)
|
||||||
.split(/\n+/)
|
.split(/\n+/)
|
||||||
.map((s) => s.trim())
|
.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 { act, render, waitFor } from "./test-utils";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import React from "react";
|
import { expect } from "vitest";
|
||||||
import { expect, vi } from "vitest";
|
|
||||||
import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw";
|
|
||||||
import { getTextEditor, updateTextEditor } from "./queries/dom";
|
import { getTextEditor, updateTextEditor } from "./queries/dom";
|
||||||
|
import { mockMermaidToExcalidraw } from "./helpers/mocks";
|
||||||
|
|
||||||
vi.mock("@excalidraw/mermaid-to-excalidraw", async (importActual) => {
|
mockMermaidToExcalidraw({
|
||||||
const module = (await importActual()) as any;
|
mockRef: true,
|
||||||
|
parseMermaidToExcalidraw: async (definition) => {
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
...module,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const parseMermaidToExcalidrawSpy = vi.spyOn(
|
|
||||||
MermaidToExcalidraw,
|
|
||||||
"parseMermaidToExcalidraw",
|
|
||||||
);
|
|
||||||
|
|
||||||
parseMermaidToExcalidrawSpy.mockImplementation(
|
|
||||||
async (
|
|
||||||
definition: string,
|
|
||||||
options?: MermaidToExcalidraw.MermaidOptions | undefined,
|
|
||||||
) => {
|
|
||||||
const firstLine = definition.split("\n")[0];
|
const firstLine = definition.split("\n")[0];
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (firstLine === "flowchart TD") {
|
if (firstLine === "flowchart TD") {
|
||||||
|
@ -88,12 +72,6 @@ parseMermaidToExcalidrawSpy.mockImplementation(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
|
||||||
|
|
||||||
vi.spyOn(React, "useRef").mockReturnValue({
|
|
||||||
current: {
|
|
||||||
parseMermaidToExcalidraw: parseMermaidToExcalidrawSpy,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test <MermaidToExcalidraw/>", () => {
|
describe("Test <MermaidToExcalidraw/>", () => {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type { NormalizedZoomValue } from "../types";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
|
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
|
import { mockMermaidToExcalidraw } from "./helpers/mocks";
|
||||||
|
|
||||||
const { h } = window;
|
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