Skip to content

Commit

Permalink
feat: add remarkMermaid plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
ndom91 committed Oct 7, 2024
1 parent 9f74742 commit 9fa1b63
Show file tree
Hide file tree
Showing 5 changed files with 919 additions and 3 deletions.
86 changes: 86 additions & 0 deletions app/components/mermaid/Mermaid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use client"

import { MutableRefObject, ReactElement, useEffect, useId, useRef, useState } from "react"
import { MermaidConfig } from "mermaid"

function useIsVisible(ref: MutableRefObject<HTMLElement>) {
const [isIntersecting, setIsIntersecting] = useState(false)

useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
// disconnect after once visible to avoid re-rendering of chart when `isIntersecting` will
// be changed to true/false
observer.disconnect()
setIsIntersecting(true)
}
})

observer.observe(ref.current)
return () => {
observer.disconnect()
}
}, [ref])

return isIntersecting
}

export function Mermaid({ chart }: { chart: string }): ReactElement {
const id = useId()
const [svg, setSvg] = useState("")
const containerRef = useRef<HTMLDivElement>(null!)
const isVisible = useIsVisible(containerRef)

useEffect(() => {
// Fix when inside element with `display: hidden` https://github.com/shuding/nextra/issues/3291
if (!isVisible) {
return
}
const htmlElement = document.documentElement
const observer = new MutationObserver(renderChart)
observer.observe(htmlElement, { attributes: true })
renderChart()

return () => {
observer.disconnect()
}

// Switching themes taken from https://github.com/mermaid-js/mermaid/blob/1b40f552b20df4ab99a986dd58c9d254b3bfd7bc/packages/mermaid/src/docs/.vitepress/theme/Mermaid.vue#L53
async function renderChart() {
const isDarkTheme =
htmlElement.classList.contains("dark") ||
htmlElement.attributes.getNamedItem("data-theme")?.value === "dark"

const mermaidConfig: MermaidConfig = {
securityLevel: "loose",
fontFamily: "inherit",
themeCSS: "margin: 1.5rem auto 0;",
theme: isDarkTheme ? "dark" : "default",
themeVariables: {
background: isDarkTheme ? "#97eae5" : "#003366",
primaryColor: isDarkTheme ? "#97eae5" : "#003366",
git0: "#97eae5",
git1: "#DC606B",
git2: "#DC9B14"
}
}

const { default: mermaid } = await import("mermaid")

try {
mermaid.initialize(mermaidConfig)
const { svg } = await mermaid.render(
// strip invalid characters for `id` attribute
id.replaceAll(":", ""),
chart.replaceAll("\\n", "\n"),
containerRef.current
)
setSvg(svg)
} catch (error) {
console.error("Error while rendering mermaid", error)
}
}
}, [chart, isVisible])

return <div ref={containerRef} dangerouslySetInnerHTML={{ __html: svg }} />
}
53 changes: 53 additions & 0 deletions app/components/mermaid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Code, Root, RootContent } from "mdast"
import { Plugin } from "unified"
import { visit } from "unist-util-visit"

const COMPONENT_NAME = "Mermaid"

const MERMAID_IMPORT_AST = {
type: "mdxjsEsm",
data: {
estree: {
body: [
{
type: "ImportDeclaration",
specifiers: [
{
type: "ImportSpecifier",
imported: { type: "Identifier", name: COMPONENT_NAME },
local: { type: "Identifier", name: COMPONENT_NAME }
}
],
source: { type: "Literal", value: "@/components/mermaid/Mermaid" }
}
]
}
}
} as RootContent

export const remarkMermaid: Plugin<[], Root> = () => (ast, _file, done) => {
// eslint-disable-next-line
const codeblocks: any[] = []
visit(ast, { type: "code", lang: "mermaid" }, (node: Code, index, parent) => {
codeblocks.push([node, index, parent])
})

if (codeblocks.length !== 0) {
for (const [node, index, parent] of codeblocks) {
parent.children.splice(index, 1, {
type: "mdxJsxFlowElement",
name: COMPONENT_NAME,
attributes: [
{
type: "mdxJsxAttribute",
name: "chart",
value: node.value.replaceAll("\n", "\\n")
}
]
})
}
ast.children.unshift(MERMAID_IMPORT_AST)
}

done()
}
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"shiki": "^1.16.1",
"tailwind-merge": "^2.3.0"
"tailwind-merge": "^2.3.0",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.7.0",
"@next/eslint-plugin-next": "^14.2.7",
"@types/mdast": "^4.0.4",
"@types/mdx": "^2.0.13",
"@types/node": "20.14.8",
"@types/react": "^18.3.3",
Expand All @@ -40,6 +42,7 @@
"eslint-plugin-prettier": "^5.1.3",
"globals": "^15.8.0",
"lint-staged": "^15.2.7",
"mermaid": "^11.3.0",
"open-props": "^1.7.4",
"postcss": "^8.4.38",
"postcss-nesting": "^12.1.5",
Expand All @@ -48,7 +51,8 @@
"remark-youtube": "^1.3.2",
"simple-git-hooks": "^2.11.1",
"tailwindcss": "^3.4.4",
"typescript": "^5.5.2"
"typescript": "^5.5.2",
"unified": "^11.0.5"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
Expand Down
Loading

0 comments on commit 9fa1b63

Please sign in to comment.