mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-13 00:02:30 +10:00
feat: mermaid diagram integration (#202)
This commit is contained in:
@ -33,6 +33,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"katex": "^0.16.10",
|
||||
"lowlight": "^3.1.0",
|
||||
"mermaid": "^11.0.1",
|
||||
"react": "^18.3.1",
|
||||
"react-arborist": "^3.4.0",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@ -1,17 +1,34 @@
|
||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { ActionIcon, CopyButton, Group, Select, Tooltip } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import classes from "./code-block.module.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import { useHover } from "@mantine/hooks";
|
||||
import MermaidView from "@/features/editor/components/code-block/mermaid-view.tsx";
|
||||
import classes from "./code-block.module.css";
|
||||
|
||||
export default function CodeBlockView(props: NodeViewProps) {
|
||||
const { node, updateAttributes, extension, editor, selected } = props;
|
||||
const { node, updateAttributes, extension, editor, getPos } = props;
|
||||
const { language } = node.attrs;
|
||||
const [languageValue, setLanguageValue] = useState<string | null>(
|
||||
language || null,
|
||||
);
|
||||
const { hovered, ref } = useHover();
|
||||
const [isSelected, setIsSelected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const updateSelection = () => {
|
||||
const { state } = editor;
|
||||
const { from, to } = state.selection;
|
||||
// Check if the selection intersects with the node's range
|
||||
const isNodeSelected =
|
||||
(from >= getPos() && from < getPos() + node.nodeSize) ||
|
||||
(to > getPos() && to <= getPos() + node.nodeSize);
|
||||
setIsSelected(isNodeSelected);
|
||||
};
|
||||
|
||||
editor.on("selectionUpdate", updateSelection);
|
||||
return () => {
|
||||
editor.off("selectionUpdate", updateSelection);
|
||||
};
|
||||
}, [editor, getPos(), node.nodeSize]);
|
||||
|
||||
function changeLanguage(language: string) {
|
||||
setLanguageValue(language);
|
||||
@ -21,10 +38,10 @@ export default function CodeBlockView(props: NodeViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="codeBlock" ref={ref}>
|
||||
<Group justify="flex-end">
|
||||
<NodeViewWrapper className="codeBlock">
|
||||
<Group justify="flex-end" contentEditable={false}>
|
||||
<Select
|
||||
placeholder="Auto"
|
||||
placeholder="auto"
|
||||
checkIconPosition="right"
|
||||
data={extension.options.lowlight.listLanguages()}
|
||||
value={languageValue}
|
||||
@ -54,9 +71,18 @@ export default function CodeBlockView(props: NodeViewProps) {
|
||||
</CopyButton>
|
||||
</Group>
|
||||
|
||||
<pre spellCheck="false">
|
||||
<pre
|
||||
spellCheck="false"
|
||||
hidden={
|
||||
((language === "mermaid" && !editor.isEditable) ||
|
||||
(language === "mermaid" && !isSelected)) &&
|
||||
node.textContent.length > 0
|
||||
}
|
||||
>
|
||||
<NodeViewContent as="code" className={`language-${language}`} />
|
||||
</pre>
|
||||
|
||||
{language === "mermaid" && <MermaidView props={props} />}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,3 +2,17 @@
|
||||
height: 25px;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mermaid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
import { NodeViewProps } from "@tiptap/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import mermaid from "mermaid";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import classes from "./code-block.module.css";
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
suppressErrorRendering: true,
|
||||
});
|
||||
|
||||
interface MermaidViewProps {
|
||||
props: NodeViewProps;
|
||||
}
|
||||
|
||||
export default function MermaidView({ props }: MermaidViewProps) {
|
||||
const { node } = props;
|
||||
const [preview, setPreview] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const id = `mermaid-${uuidv4()}`;
|
||||
|
||||
if (node.textContent.length > 0) {
|
||||
mermaid
|
||||
.render(id, node.textContent)
|
||||
.then((item) => {
|
||||
setPreview(item.svg);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (props.editor.isEditable) {
|
||||
setPreview(
|
||||
`<div class="${classes.error}">Mermaid diagram error: ${err}</div>`,
|
||||
);
|
||||
} else {
|
||||
setPreview(
|
||||
`<div class="${classes.error}">Invalid Mermaid Diagram</div>`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [node.textContent]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes.mermaid}
|
||||
contentEditable={false}
|
||||
dangerouslySetInnerHTML={{ __html: preview }}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
@ -7,6 +7,7 @@ import {
|
||||
IconH2,
|
||||
IconH3,
|
||||
IconInfoCircle,
|
||||
IconLetterY,
|
||||
IconList,
|
||||
IconListNumbers,
|
||||
IconMath,
|
||||
@ -253,6 +254,19 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
editor.chain().focus().deleteRange(range).setMathBlock().run(),
|
||||
},
|
||||
{
|
||||
title: "Mermaid diagram",
|
||||
description: "Insert mermaid diagram",
|
||||
searchTerms: ["mermaid", "diagram", "chart"],
|
||||
icon: IconLetterY,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setCodeBlock({ language: "mermaid" })
|
||||
.run(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ import { TextStyle } from "@tiptap/extension-text-style";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import Table from "@tiptap/extension-table";
|
||||
import TableHeader from "@tiptap/extension-table-header";
|
||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||
import SlashCommand from "@/features/editor/extensions/slash-command";
|
||||
import { Collaboration } from "@tiptap/extension-collaboration";
|
||||
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor";
|
||||
@ -32,6 +31,7 @@ import {
|
||||
TiptapVideo,
|
||||
LinkExtension,
|
||||
Selection,
|
||||
CustomCodeBlock,
|
||||
} from "@docmost/editor-ext";
|
||||
import {
|
||||
randomElement,
|
||||
@ -46,7 +46,6 @@ import ImageView from "@/features/editor/components/image/image-view.tsx";
|
||||
import CalloutView from "@/features/editor/components/callout/callout-view.tsx";
|
||||
import { common, createLowlight } from "lowlight";
|
||||
import VideoView from "@/features/editor/components/video/video-view.tsx";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx";
|
||||
import plaintext from "highlight.js/lib/languages/plaintext";
|
||||
|
||||
@ -139,11 +138,8 @@ export const mainExtensions = [
|
||||
Callout.configure({
|
||||
view: CalloutView,
|
||||
}),
|
||||
CodeBlockLowlight.extend({
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CodeBlockView);
|
||||
},
|
||||
}).configure({
|
||||
CustomCodeBlock.configure({
|
||||
view: CodeBlockView,
|
||||
lowlight,
|
||||
HTMLAttributes: {
|
||||
spellcheck: false,
|
||||
|
||||
@ -15,6 +15,7 @@ import TableHeader from '@tiptap/extension-table-header';
|
||||
import {
|
||||
Callout,
|
||||
Comment,
|
||||
CustomCodeBlock,
|
||||
Details,
|
||||
DetailsContent,
|
||||
DetailsSummary,
|
||||
@ -35,7 +36,9 @@ import { generateHTML } from '../common/helpers/prosemirror/html';
|
||||
import { generateJSON } from '@tiptap/html';
|
||||
|
||||
export const tiptapExtensions = [
|
||||
StarterKit,
|
||||
StarterKit.configure({
|
||||
codeBlock: false
|
||||
}),
|
||||
Comment,
|
||||
TextAlign,
|
||||
TaskList,
|
||||
@ -62,6 +65,7 @@ export const tiptapExtensions = [
|
||||
TiptapImage,
|
||||
TiptapVideo,
|
||||
Callout,
|
||||
CustomCodeBlock
|
||||
] as any;
|
||||
|
||||
export function jsonToHtml(tiptapJson: any) {
|
||||
|
||||
Reference in New Issue
Block a user