Skip to content

Commit a61ed50

Browse files
committed
fix #7733 -- mermaid in rendered slate view
- i am NOT going to implement the "only show editor" on focus request unless mermaid is suddenly really popular when used this way, since that is a hellish nightmare... due to the subtle issues of focused between codemirror and slate.
1 parent 54bd10a commit a61ed50

File tree

3 files changed

+185
-133
lines changed

3 files changed

+185
-133
lines changed

src/packages/frontend/editors/slate/elements/code-block/editable.tsx

Lines changed: 107 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { getHistory, isPreviousSiblingCodeBlock } from "./history";
1818
import InsertBar from "./insert-bar";
1919
import { useFileContext } from "@cocalc/frontend/lib/file-context";
2020
import { isEqual } from "lodash";
21+
import Mermaid from "./mermaid";
2122

2223
function Element({ attributes, children, element }: RenderElementProps) {
2324
if (element.type != "code_block") {
@@ -29,9 +30,7 @@ function Element({ attributes, children, element }: RenderElementProps) {
2930
const [info, setInfo] = useState<string>(element.info ?? "");
3031
const infoFocusedRef = useRef<boolean>(false);
3132
const [output, setOutput] = useState<null | ReactNode>(null);
32-
3333
const runRef = useRef<RunFunction | null>(null);
34-
3534
const setElement = useSetElement(editor, element);
3635
// textIndent: 0 is needed due to task lists -- see https://github.com/sagemathinc/cocalc/issues/6074
3736
const { change } = useChange();
@@ -64,108 +63,115 @@ function Element({ attributes, children, element }: RenderElementProps) {
6463
above={true}
6564
/>
6665
)}
67-
<SlateCodeMirror
68-
options={{ lineWrapping: true }}
69-
value={element.value}
70-
info={infoToMode(element.info, { value: element.value })}
71-
onChange={(value) => {
72-
setElement({ value });
73-
}}
74-
onFocus={async () => {
75-
await delay(1); // must be a little longer than the onBlur below.
76-
if (!isMountedRef.current) return;
77-
}}
78-
onBlur={async () => {
79-
await delay(0);
80-
if (!isMountedRef.current) return;
81-
}}
82-
onShiftEnter={() => {
83-
runRef.current?.();
84-
}}
85-
addonBefore={
86-
<div
87-
style={{
88-
borderBottom: "1px solid #ccc",
89-
padding: "3px",
90-
display: "flex",
91-
background: "#f8f8f8",
66+
<div style={{ display: "flex", flexDirection: "column" }}>
67+
<div style={{ flex: 1 }}>
68+
<SlateCodeMirror
69+
options={{ lineWrapping: true }}
70+
value={element.value}
71+
info={infoToMode(element.info, { value: element.value })}
72+
onChange={(value) => {
73+
setElement({ value });
9274
}}
93-
>
94-
<div style={{ flex: 1 }}></div>
95-
{element.fence && (
96-
<Input
97-
size="small"
98-
onKeyDown={(e) => {
99-
if (e.keyCode == 13 && e.shiftKey) {
100-
runRef.current?.();
101-
} else if (e.keyCode == 40) {
102-
// down arrow and 38 is up. TODO
103-
}
104-
}}
75+
onFocus={async () => {
76+
await delay(1); // must be a little longer than the onBlur below.
77+
if (!isMountedRef.current) return;
78+
}}
79+
onBlur={async () => {
80+
await delay(0);
81+
if (!isMountedRef.current) return;
82+
}}
83+
onShiftEnter={() => {
84+
runRef.current?.();
85+
}}
86+
addonBefore={
87+
<div
10588
style={{
106-
flex: 1,
107-
color: "#666",
108-
minWidth: "100px",
109-
maxWidth: "300px",
110-
margin: "0 5px",
89+
borderBottom: "1px solid #ccc",
90+
padding: "3px",
91+
display: "flex",
92+
background: "#f8f8f8",
11193
}}
112-
placeholder="Info string (py, r, jl, tex, md, etc.)..."
113-
value={info}
114-
onFocus={() => {
115-
infoFocusedRef.current = true;
116-
editor.setIgnoreSelection(true);
117-
}}
118-
onBlur={() => {
119-
infoFocusedRef.current = false;
120-
editor.setIgnoreSelection(false);
121-
}}
122-
onChange={(e) => {
123-
const info = e.target.value;
124-
setInfo(info);
125-
setElement({ info });
126-
}}
127-
/>
128-
)}
129-
{!disableMarkdownCodebar && (
130-
<ActionButtons
131-
auto
132-
size="small"
133-
input={element.value}
134-
history={history}
135-
setOutput={setOutput}
136-
output={output}
137-
info={info}
138-
runRef={runRef}
139-
setInfo={(info) => {
140-
setElement({ info });
141-
}}
142-
/>
143-
)}
144-
</div>
145-
}
146-
addonAfter={
147-
disableMarkdownCodebar || output == null ? null : (
148-
<div
149-
onMouseDown={() => {
150-
editor.setIgnoreSelection(true);
151-
}}
152-
onMouseUp={() => {
153-
// Re-enable slate listing for selection changes again in next render loop.
154-
setTimeout(() => {
155-
editor.setIgnoreSelection(false);
156-
}, 0);
157-
}}
158-
style={{
159-
borderTop: "1px dashed #ccc",
160-
background: "white",
161-
padding: "5px 0 5px 30px",
162-
}}
163-
>
164-
{output}
165-
</div>
166-
)
167-
}
168-
/>
94+
>
95+
<div style={{ flex: 1 }}></div>
96+
{element.fence && (
97+
<Input
98+
size="small"
99+
onKeyDown={(e) => {
100+
if (e.keyCode == 13 && e.shiftKey) {
101+
runRef.current?.();
102+
} else if (e.keyCode == 40) {
103+
// down arrow and 38 is up. TODO
104+
}
105+
}}
106+
style={{
107+
flex: 1,
108+
color: "#666",
109+
minWidth: "100px",
110+
maxWidth: "300px",
111+
margin: "0 5px",
112+
}}
113+
placeholder="Info string (py, r, jl, tex, md, etc.)..."
114+
value={info}
115+
onFocus={() => {
116+
infoFocusedRef.current = true;
117+
editor.setIgnoreSelection(true);
118+
}}
119+
onBlur={() => {
120+
infoFocusedRef.current = false;
121+
editor.setIgnoreSelection(false);
122+
}}
123+
onChange={(e) => {
124+
const info = e.target.value;
125+
setInfo(info);
126+
setElement({ info });
127+
}}
128+
/>
129+
)}
130+
{!disableMarkdownCodebar && (
131+
<ActionButtons
132+
auto
133+
size="small"
134+
input={element.value}
135+
history={history}
136+
setOutput={setOutput}
137+
output={output}
138+
info={info}
139+
runRef={runRef}
140+
setInfo={(info) => {
141+
setElement({ info });
142+
}}
143+
/>
144+
)}
145+
</div>
146+
}
147+
addonAfter={
148+
disableMarkdownCodebar || output == null ? null : (
149+
<div
150+
onMouseDown={() => {
151+
editor.setIgnoreSelection(true);
152+
}}
153+
onMouseUp={() => {
154+
// Re-enable slate listing for selection changes again in next render loop.
155+
setTimeout(() => {
156+
editor.setIgnoreSelection(false);
157+
}, 0);
158+
}}
159+
style={{
160+
borderTop: "1px dashed #ccc",
161+
background: "white",
162+
padding: "5px 0 5px 30px",
163+
}}
164+
>
165+
{output}
166+
</div>
167+
)
168+
}
169+
/>
170+
</div>
171+
{element.info == "mermaid" && (
172+
<Mermaid style={{ flex: 1 }} value={element.value} />
173+
)}
174+
</div>
169175
<InsertBar
170176
editor={editor}
171177
element={element}

src/packages/frontend/editors/slate/elements/code-block/index.tsx

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { DARK_GREY_BORDER } from "../../util";
1616
import { useFileContext } from "@cocalc/frontend/lib/file-context";
1717
import { Icon } from "@cocalc/frontend/components/icon";
1818
import { isEqual } from "lodash";
19-
import ShowError from "@cocalc/frontend/components/error";
19+
import Mermaid from "./mermaid";
2020

2121
export interface CodeBlock extends SlateElement {
2222
type: "code_block";
@@ -26,7 +26,7 @@ export interface CodeBlock extends SlateElement {
2626
info: string;
2727
}
2828

29-
const StaticElement: React.FC<RenderElementProps> = ({
29+
export const StaticElement: React.FC<RenderElementProps> = ({
3030
attributes,
3131
element,
3232
}) => {
@@ -48,7 +48,6 @@ const StaticElement: React.FC<RenderElementProps> = ({
4848

4949
const [newValue, setNewValue] = useState<string | null>(null);
5050
const runRef = useRef<any>(null);
51-
const mermaidRef = useRef<any>(null);
5251

5352
const [output, setOutput] = useState<null | ReactNode>(null);
5453

@@ -90,38 +89,11 @@ const StaticElement: React.FC<RenderElementProps> = ({
9089
}, 1);
9190
};
9291

93-
const isMermaid = temporaryInfo ?? element.info == "mermaid";
94-
const [mermaidError, setMermaidError] = useState<string>("");
95-
96-
useEffect(() => {
97-
const elt = mermaidRef.current;
98-
if (!isMermaid || !elt) {
99-
return;
100-
}
101-
(async () => {
102-
try {
103-
setMermaidError("");
104-
const mermaid = (await import("mermaid")).default;
105-
mermaid.initialize({
106-
startOnLoad: false,
107-
});
108-
elt.removeAttribute("data-processed");
109-
await mermaid.run({
110-
nodes: [elt],
111-
});
112-
} catch (err) {
113-
setMermaidError(err.str ?? `${err}`);
114-
}
115-
})();
116-
}, [isMermaid, newValue ?? element.value]);
117-
92+
const isMermaid = element.info == "mermaid";
11893
if (isMermaid) {
11994
return (
12095
<div {...attributes} style={{ marginBottom: "1em", textIndent: 0 }}>
121-
<pre className="mermaid" ref={mermaidRef}>
122-
{newValue ?? element.value}
123-
</pre>
124-
<ShowError error={mermaidError} setError={setMermaidError} />
96+
<Mermaid value={newValue ?? element.value} />
12597
</div>
12698
);
12799
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3+
* License: MS-RSL – see LICENSE.md for details
4+
*/
5+
6+
import { useEffect, useRef, useState } from "react";
7+
import ShowError from "@cocalc/frontend/components/error";
8+
import { delay } from "awaiting";
9+
import { uuid, replace_all } from "@cocalc/util/misc";
10+
11+
interface Props {
12+
value: string;
13+
style?;
14+
}
15+
16+
export default function Mermaid({ value, style }: Props) {
17+
const mermaidRef = useRef<any>(null);
18+
const [mermaidError, setMermaidError] = useState<string>("");
19+
const processingRef = useRef<boolean>(false);
20+
const [id] = useState<string>("a" + replace_all(uuid(), "-", ""));
21+
22+
const waitUntilNotProcessing = async () => {
23+
let d = 1;
24+
while (processingRef.current) {
25+
// value changed *while* processing.
26+
await delay(d);
27+
d *= 1.2;
28+
}
29+
};
30+
31+
useEffect(() => {
32+
const elt = mermaidRef.current;
33+
if (!elt) {
34+
return;
35+
}
36+
if (!value.trim()) {
37+
elt.innerHTML = "";
38+
return;
39+
}
40+
(async () => {
41+
try {
42+
await waitUntilNotProcessing();
43+
processingRef.current = true;
44+
setMermaidError("");
45+
const mermaid = await getMermaid();
46+
const { svg } = await mermaid.render(id, value);
47+
elt.innerHTML = svg;
48+
} catch (err) {
49+
setMermaidError(err.str ?? `${err}`);
50+
} finally {
51+
processingRef.current = false;
52+
}
53+
})();
54+
}, [value]);
55+
56+
return (
57+
<div style={style}>
58+
<pre ref={mermaidRef}></pre>
59+
<ShowError error={mermaidError} setError={setMermaidError} />
60+
</div>
61+
);
62+
}
63+
64+
let initialized = false;
65+
async function getMermaid() {
66+
const mermaid = (await import("mermaid")).default;
67+
if (!initialized) {
68+
mermaid.initialize({
69+
startOnLoad: false,
70+
});
71+
initialized = true;
72+
}
73+
return mermaid;
74+
}

0 commit comments

Comments
 (0)