feat: support mermaid
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
1364424a6d
commit
5ad9213fc8
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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>`
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue