feat: Add mermaidjs integration (#3679)
* feat: Add mermaidjs integration (#3523) * Add mermaidjs to dependencies and CodeFenceNode * Fix diagram id for mermaidjs diagrams * Fix typescript compiler errors on mermaid integration * Fix id generation for mermaid diagrams * Refactor mermaidjs integration into prosemirror plugin * Remove unnecessary class attribute in mermaidjs integration * Change mermaidjs label to singular * Change decorator.inline to decorator.node for mermaid diagram id * Fix diagram toggle state * Add border and background to mermaid diagrams * Stop mermaidjs from overwriting fontFamily inside diagrams * Add stable diagramId to mermaid diagrams * Separate text for hide/show diagram Use uuid as diagramId, avoid storing in state Fix cursor on diagrams * fix: Base diagram visibility off presence of source * fix: More cases where our font-family is ignored * Disable HTML labels * fix: Button styling – not technically required but now we have a third button this felt all the more needed closes #3116 * named chunks * Upgrade mermaid 9.1.3 Co-authored-by: Jan Niklas Richter <5812215+ArcticXWolf@users.noreply.github.com>
This commit is contained in:
190
shared/editor/plugins/Mermaid.ts
Normal file
190
shared/editor/plugins/Mermaid.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { Node } from "prosemirror-model";
|
||||
import { Plugin, PluginKey, Transaction } from "prosemirror-state";
|
||||
import { findBlockNodes } from "prosemirror-utils";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
type MermaidState = {
|
||||
decorationSet: DecorationSet;
|
||||
diagramVisibility: Record<number, boolean>;
|
||||
};
|
||||
|
||||
function getNewState({
|
||||
doc,
|
||||
name,
|
||||
pluginState,
|
||||
}: {
|
||||
doc: Node;
|
||||
name: string;
|
||||
pluginState: MermaidState;
|
||||
}) {
|
||||
const decorations: Decoration[] = [];
|
||||
|
||||
// Find all blocks that represent Mermaid diagrams
|
||||
const blocks: { node: Node; pos: number }[] = findBlockNodes(doc).filter(
|
||||
(item) =>
|
||||
item.node.type.name === name && item.node.attrs.language === "mermaidjs"
|
||||
);
|
||||
|
||||
blocks.forEach((block) => {
|
||||
const diagramDecorationPos = block.pos + block.node.nodeSize;
|
||||
const existingDecorations = pluginState.decorationSet.find(
|
||||
block.pos,
|
||||
diagramDecorationPos
|
||||
);
|
||||
|
||||
// Attempt to find the existing diagramId from the decoration, or assign
|
||||
// a new one if none exists yet.
|
||||
let diagramId = existingDecorations[0]?.spec["diagramId"];
|
||||
if (diagramId === undefined) {
|
||||
diagramId = uuidv4();
|
||||
}
|
||||
|
||||
// Make the diagram visible by default if it contains source code
|
||||
if (pluginState.diagramVisibility[diagramId] === undefined) {
|
||||
pluginState.diagramVisibility[diagramId] = !!block.node.textContent;
|
||||
}
|
||||
|
||||
const diagramDecoration = Decoration.widget(
|
||||
block.pos + block.node.nodeSize,
|
||||
() => {
|
||||
const diagramWrapper = document.createElement("div");
|
||||
diagramWrapper.classList.add("mermaid-diagram-wrapper");
|
||||
|
||||
if (pluginState.diagramVisibility[diagramId] === false) {
|
||||
diagramWrapper.classList.add("diagram-hidden");
|
||||
}
|
||||
|
||||
import(
|
||||
/* webpackChunkName: "mermaid" */
|
||||
"mermaid"
|
||||
).then((module) => {
|
||||
module.default.initialize({
|
||||
startOnLoad: true,
|
||||
flowchart: {
|
||||
htmlLabels: false,
|
||||
},
|
||||
themeVariables: {
|
||||
fontFamily: "inherit",
|
||||
},
|
||||
});
|
||||
try {
|
||||
module.default.render(
|
||||
"mermaid-diagram-" + diagramId,
|
||||
block.node.textContent,
|
||||
(svgCode) => {
|
||||
diagramWrapper.innerHTML = svgCode;
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const errorNode = document.getElementById(
|
||||
"d" + "mermaid-diagram-" + diagramId
|
||||
);
|
||||
if (errorNode) {
|
||||
diagramWrapper.appendChild(errorNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return diagramWrapper;
|
||||
},
|
||||
{
|
||||
diagramId,
|
||||
}
|
||||
);
|
||||
|
||||
const attributes = { "data-diagram-id": "" + diagramId };
|
||||
if (pluginState.diagramVisibility[diagramId] !== false) {
|
||||
attributes["class"] = "code-hidden";
|
||||
}
|
||||
|
||||
const diagramIdDecoration = Decoration.node(
|
||||
block.pos,
|
||||
block.pos + block.node.nodeSize,
|
||||
attributes,
|
||||
{
|
||||
diagramId,
|
||||
}
|
||||
);
|
||||
|
||||
decorations.push(diagramDecoration);
|
||||
decorations.push(diagramIdDecoration);
|
||||
});
|
||||
|
||||
return {
|
||||
decorationSet: DecorationSet.create(doc, decorations),
|
||||
diagramVisibility: pluginState.diagramVisibility,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Mermaid({ name }: { name: string }) {
|
||||
let diagramShown = false;
|
||||
|
||||
return new Plugin({
|
||||
key: new PluginKey("mermaid"),
|
||||
state: {
|
||||
init: (_: Plugin, { doc }) => {
|
||||
const pluginState: MermaidState = {
|
||||
decorationSet: DecorationSet.create(doc, []),
|
||||
diagramVisibility: {},
|
||||
};
|
||||
return pluginState;
|
||||
},
|
||||
apply: (
|
||||
transaction: Transaction,
|
||||
pluginState: MermaidState,
|
||||
oldState,
|
||||
state
|
||||
) => {
|
||||
const nodeName = state.selection.$head.parent.type.name;
|
||||
const previousNodeName = oldState.selection.$head.parent.type.name;
|
||||
const codeBlockChanged =
|
||||
transaction.docChanged && [nodeName, previousNodeName].includes(name);
|
||||
const ySyncEdit = !!transaction.getMeta("y-sync$");
|
||||
const mermaidMeta = transaction.getMeta("mermaid");
|
||||
const diagramToggled = mermaidMeta?.toggleDiagram !== undefined;
|
||||
|
||||
if (diagramToggled) {
|
||||
pluginState.diagramVisibility[
|
||||
mermaidMeta.toggleDiagram
|
||||
] = !pluginState.diagramVisibility[mermaidMeta.toggleDiagram];
|
||||
}
|
||||
|
||||
if (!diagramShown || codeBlockChanged || diagramToggled || ySyncEdit) {
|
||||
diagramShown = true;
|
||||
return getNewState({
|
||||
doc: transaction.doc,
|
||||
name,
|
||||
pluginState,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
decorationSet: pluginState.decorationSet.map(
|
||||
transaction.mapping,
|
||||
transaction.doc
|
||||
),
|
||||
diagramVisibility: pluginState.diagramVisibility,
|
||||
};
|
||||
},
|
||||
},
|
||||
view: (view) => {
|
||||
if (!diagramShown) {
|
||||
// we don't draw diagrams on code blocks on the first render as part of mounting
|
||||
// as it's expensive (relative to the rest of the document). Instead let
|
||||
// it render without a diagram and then trigger a defered render of Mermaid
|
||||
// by updating the plugins metadata
|
||||
setTimeout(() => {
|
||||
view.dispatch(view.state.tr.setMeta("mermaid", { loaded: true }));
|
||||
}, 10);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state).decorationSet;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export const LANGUAGES = {
|
||||
java: "Java",
|
||||
javascript: "JavaScript",
|
||||
json: "JSON",
|
||||
mermaidjs: "Mermaid Diagram",
|
||||
perl: "Perl",
|
||||
php: "PHP",
|
||||
powershell: "Powershell",
|
||||
|
||||
Reference in New Issue
Block a user