feat: support mermaid

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2023-04-10 23:07:23 +08:00
parent 1364424a6d
commit 5ad9213fc8
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
5 changed files with 98 additions and 1 deletions

View File

@ -2,7 +2,8 @@ import { cn } from "~/lib/utils"
import { useCodeCopy } from "~/hooks/useCodeCopy"
import { renderPageContent } from "~/markdown"
import { PostToc } from "~/components/site/PostToc"
import { MutableRefObject, useMemo } from "react"
import { MutableRefObject, useMemo, useRef } from "react"
import { useMermaid } from "~/hooks/useMermaid"
export const PageContent: React.FC<{
content?: string
@ -21,7 +22,9 @@ export const PageContent: React.FC<{
onMouseEnter,
parsedContent,
}) => {
const $articleRef = useRef<HTMLDivElement>(null)
useCodeCopy()
useMermaid($articleRef)
const inParsedContent = useMemo(() => {
if (parsedContent) {
@ -39,6 +42,7 @@ export const PageContent: React.FC<{
className={cn("relative", className)}
onMouseEnter={onMouseEnter}
onScroll={(e) => onScroll?.((e.target as any)?.scrollTop)}
ref={$articleRef}
>
<div className="xlog-post-content prose" ref={inputRef}>
{inParsedContent?.element}

32
src/hooks/useMermaid.ts Normal file
View File

@ -0,0 +1,32 @@
import React, { useEffect, useLayoutEffect } from "react"
import { loadScript } from "~/lib/load-script"
declare global {
interface Window {
mermaid: any
}
}
export const useMermaid = (
contentRef: React.MutableRefObject<HTMLElement | null>,
) => {
useEffect(() => {
if (contentRef.current) {
const mermaidEls = contentRef.current.querySelectorAll(".mermaid")
if (mermaidEls.length === 0) return
loadScript(
"https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/mermaid/8.9.0/mermaid.min.js",
).then(() => {
if (window.mermaid) {
window.mermaid.initialize({
theme: "default",
startOnLoad: false,
})
window.mermaid.init(undefined, ".mermaid")
}
})
}
}, [contentRef])
}

45
src/lib/load-script.ts Normal file
View File

@ -0,0 +1,45 @@
const isLoadScriptMap: Record<string, "loading" | "loaded"> = {}
const loadingQueueMap: Record<string, [Function, Function][]> = {}
export function loadScript(url: string) {
return new Promise((resolve, reject) => {
const status = isLoadScriptMap[url]
if (status === "loaded") {
return resolve(null)
} else if (status === "loading") {
loadingQueueMap[url] = !loadingQueueMap[url]
? [[resolve, reject]]
: [...loadingQueueMap[url], [resolve, reject]]
return
}
const script = document.createElement("script")
script.src = url
script.crossOrigin = "anonymous"
isLoadScriptMap[url] = "loading"
script.onload = function () {
isLoadScriptMap[url] = "loaded"
resolve(null)
if (loadingQueueMap[url]) {
loadingQueueMap[url].forEach(([resolve, reject]) => {
resolve(null)
})
delete loadingQueueMap[url]
}
}
script.onerror = function (e) {
// this.onload = null here is necessary
// because even IE9 works not like others
this.onerror = this.onload = null
delete isLoadScriptMap[url]
loadingQueueMap[url].forEach(([resolve, reject]) => {
reject(e)
})
delete loadingQueueMap[url]
reject(e)
}
document.head.appendChild(script)
})
}

View File

@ -31,6 +31,7 @@ import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex"
import rehypeInferDescriptionMeta from "rehype-infer-description-meta"
import remarkBreaks from "remark-breaks"
import { remarkMermaid } from "./remark-mermaid"
export type MarkdownEnv = {
excerpt: string
@ -98,6 +99,7 @@ export const renderPageContent = (
.use(remarkDirective)
.use(remarkDirectiveRehype)
.use(remarkYoutube)
.use(remarkMermaid)
.use(remarkMath)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeKatex) // There may be $ symbol parsing errors
@ -114,6 +116,7 @@ export const renderPageContent = (
.use(rehypeWrapCode)
.use(rehypeInferDescriptionMeta)
.use(rehypeSlug)
.use(rehypeAutolinkHeadings, {
properties: {
className: ["xlog-anchor"],

View File

@ -0,0 +1,13 @@
import { Plugin } from "unified"
import { Root } from "remark-gfm"
import { visit } from "unist-util-visit"
export const remarkMermaid: Plugin<Array<void>, Root> = () => (tree, file) => {
visit(tree, (node) => {
if (node.type === "code" && node.lang === "mermaid") {
// @ts-ignore
node.type = "html"
node.value = `<div class="mermaid">${node.value}</div>`
}
})
}