Skip to content

Commit

Permalink
feat: transform playground in react
Browse files Browse the repository at this point in the history
  • Loading branch information
igorwessel committed Apr 16, 2024
1 parent 20a6a25 commit 57a65b3
Show file tree
Hide file tree
Showing 11 changed files with 391 additions and 364 deletions.
86 changes: 86 additions & 0 deletions playground/CustomTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from "react";

import { useExcalidraw } from "./ExcalidrawWrapper.tsx";
import { Mermaid } from "./Mermaid.tsx";

function CustomTest() {
const excalidraw = useExcalidraw();
const [parsedMermaid, setParsedMermaid] = React.useState<{
data: string | null;
error: string | null;
definition: string | null;
}>({
data: null,
error: null,
definition: null,
});

return (
<>
<form
onSubmit={async (event) => {
event.preventDefault();

const data = new FormData(event.target as HTMLFormElement);
const mermaidSyntax = data.get("mermaid-input") as string;

if (!mermaidSyntax) {
return;
}

try {
setParsedMermaid({
data: null,
definition: null,
error: null,
});

const { mermaid } = await excalidraw.translateMermaidToExcalidraw(
mermaidSyntax
);

setParsedMermaid({
data: JSON.stringify(mermaid, null, 2),
definition: mermaidSyntax,
error: null,
});
} catch (error) {
setParsedMermaid({
data: null,
definition: null,
error: String(error),
});
}
}}
>
<textarea
id="mermaid-input"
rows={10}
cols={50}
name="mermaid-input"
style={{ marginTop: "1rem" }}
placeholder="Input Mermaid Syntax"
/>
<br />
<button type="submit" id="render-excalidraw-btn">
{"Render to Excalidraw"}
</button>
</form>

{parsedMermaid.definition && (
<Mermaid definition={parsedMermaid.definition} id="custom-diagram" />
)}

{parsedMermaid.data && (
<details id="parsed-data-details">
<summary>{"Parsed data from parseMermaid"}</summary>
<pre id="custom-parsed-data">{parsedMermaid.data}</pre>
</details>
)}

{parsedMermaid.error && <div id="error">{parsedMermaid.error}</div>}
</>
);
}

export default CustomTest;
125 changes: 101 additions & 24 deletions playground/ExcalidrawWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback, useContext, useMemo } from "react";
import {
Excalidraw,
convertToExcalidrawElements,
Expand All @@ -8,36 +8,113 @@ import {
ExcalidrawImperativeAPI,
} from "@excalidraw/excalidraw/types/types.js";
import { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/types/data/transform.js";
import { parseMermaid } from "../src/parseMermaid";
import { graphToExcalidraw } from "../src/graphToExcalidraw";
import { DEFAULT_FONT_SIZE } from "../src/constants";

interface ExcalidrawWrapperProps {
elements: ExcalidrawElementSkeleton[];
files?: BinaryFiles;
}
const ExcalidrawContext = React.createContext<{
excalidrawAPI?: ExcalidrawImperativeAPI;
addFiles: (files: BinaryFiles) => void;
updateElements: (elements: ExcalidrawElementSkeleton[]) => void;
translateMermaidToExcalidraw: (mermaidSyntax: any) => Promise<{
mermaid: ReturnType<typeof parseMermaid>;
excalidraw: { elements: ExcalidrawElementSkeleton[]; files: BinaryFiles };
}>;
setApi: (api: ExcalidrawImperativeAPI) => void;
} | null>(null);

const ExcalidrawWrapper = (props: ExcalidrawWrapperProps) => {
const [excalidrawAPI, setExcalidrawAPI] =
React.useState<ExcalidrawImperativeAPI | null>(null);
export const useExcalidraw = () => {
const context = useContext(ExcalidrawContext);

React.useEffect(() => {
if (!props.elements || !excalidrawAPI) {
return;
}
if (!context) {
throw new Error("useExcalidraw must be used within a ExcalidrawProvider");
}

return context;
};

export const ExcalidrawProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const excalidrawAPI = React.useRef<ExcalidrawImperativeAPI>();

const updateElements = useCallback(
(elements: ExcalidrawElementSkeleton[]) => {
if (!excalidrawAPI.current) {
return;
}

excalidrawAPI.updateScene({
elements: convertToExcalidrawElements(props.elements),
});
excalidrawAPI.scrollToContent(excalidrawAPI.getSceneElements(), {
fitToContent: true,
});
}, [props.elements]);
excalidrawAPI.current.updateScene({
elements: convertToExcalidrawElements(elements),
});

React.useEffect(() => {
if (!props.files || !excalidrawAPI) {
excalidrawAPI.current.scrollToContent(
excalidrawAPI.current.getSceneElements(),
{
fitToContent: true,
}
);
},
[]
);

const addFiles = useCallback((files: BinaryFiles) => {
if (!excalidrawAPI.current) {
return;
}

excalidrawAPI.addFiles(Object.values(props.files));
}, [props.files]);
excalidrawAPI.current.addFiles(Object.values(files));
}, []);

const setApi = useCallback((api: ExcalidrawImperativeAPI) => {
excalidrawAPI.current = api;
}, []);

const translateMermaidToExcalidraw = useCallback(
async (mermaidSyntax: string) => {
if (!excalidrawAPI.current) {
return;
}

const mermaid = await parseMermaid(mermaidSyntax);

const { elements, files } = graphToExcalidraw(mermaid, {
fontSize: DEFAULT_FONT_SIZE,
});

updateElements(elements);

if (files) {
addFiles(files);
}

return { mermaid, excalidraw: { elements, files } };
},
[updateElements, addFiles]
);

const context = useMemo(
() => ({
excalidrawAPI: excalidrawAPI.current,
addFiles,
updateElements,
setApi,
translateMermaidToExcalidraw,
}),
[addFiles, updateElements, setApi, translateMermaidToExcalidraw]
);

return (
<ExcalidrawContext.Provider value={context}>
{children}
</ExcalidrawContext.Provider>
);
};

const ExcalidrawWrapper = () => {
const excalidraw = useExcalidraw();

return (
<div className="excalidraw-wrapper">
Expand All @@ -48,7 +125,7 @@ const ExcalidrawWrapper = (props: ExcalidrawWrapperProps) => {
currentItemFontFamily: 1,
},
}}
excalidrawAPI={(api) => setExcalidrawAPI(api)}
excalidrawAPI={excalidraw?.setApi}
/>
</div>
);
Expand Down
32 changes: 32 additions & 0 deletions playground/Mermaid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import mermaid from "mermaid";
import React from "react";

interface MermaidProps {
id: string;
definition: string;
}

export function Mermaid({ definition, id }: MermaidProps) {
const [svg, setSvg] = React.useState("");
const [, startTransition] = React.useTransition();

React.useEffect(() => {
const render = async (id: string, definition: string) => {
startTransition(() => {
mermaid.render(`mermaid-diagram-${id}`, definition).then(({ svg }) => {
setSvg(svg);
});
});
};

render(id, definition);
}, [definition, id]);

return (
<div
style={{ width: "50%" }}
className="mermaid"
dangerouslySetInnerHTML={{ __html: svg }}
/>
);
}
106 changes: 106 additions & 0 deletions playground/Testcases.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from "react";

import { FLOWCHART_DIAGRAM_TESTCASES } from "./testcases/flowchart";
import { SEQUENCE_DIAGRAM_TESTCASES } from "./testcases/sequence.ts";
import { CLASS_DIAGRAM_TESTCASES } from "./testcases/class.ts";
import { UNSUPPORTED_DIAGRAM_TESTCASES } from "./testcases/unsupported.ts";
import { useExcalidraw } from "./ExcalidrawWrapper.tsx";
import { Mermaid } from "./Mermaid";

interface TestCaseProps {
name: string;
baseId: string;
testcases: { name: string; definition: string }[];
}

function Testcase({ name, baseId, testcases }: TestCaseProps) {
const excalidraw = useExcalidraw();
const activeTestcase = React.useRef<number>();

React.useEffect(() => {
const testcase = activeTestcase.current;

if (testcase !== undefined) {
const { definition } = testcases[testcase];

excalidraw.translateMermaidToExcalidraw(definition);
}
}, [excalidraw.translateMermaidToExcalidraw, testcases]);

return (
<>
<h2>{`${name} Diagrams`}</h2>
<details>
<summary>{`${name} Examples`}</summary>
<div id={`${baseId}-container`}>
{testcases.map(({ name, definition }, index) => {
const id = `${baseId}-${index}`;

return (
<div key={id}>
<h2 style={{ marginTop: "50px", color: "#f06595" }}>{name}</h2>

<pre
style={{
fontSize: "16px",
fontWeight: "600",
fontStyle: "italic",
background: "#eeeef1",
whiteSpace: "pre-wrap",
width: "45vw",
padding: "5px",
}}
>
{definition}
</pre>

<button
onClick={() => {
excalidraw.translateMermaidToExcalidraw(definition);
activeTestcase.current = index;
}}
>
{"Render to Excalidraw"}
</button>

<Mermaid definition={definition} id={id} />
</div>
);
})}
</div>
</details>
</>
);
}

function Testcases() {
return (
<>
<Testcase
name="Flowchart"
baseId="flowchart"
testcases={FLOWCHART_DIAGRAM_TESTCASES}
/>

<Testcase
name="Sequence"
baseId="sequence"
testcases={SEQUENCE_DIAGRAM_TESTCASES}
/>

<Testcase
name="Class"
baseId="class"
testcases={CLASS_DIAGRAM_TESTCASES}
/>

<Testcase
name="Unsupported"
baseId="unsupported"
testcases={UNSUPPORTED_DIAGRAM_TESTCASES}
/>
</>
);
}

export default Testcases;
Loading

0 comments on commit 57a65b3

Please sign in to comment.