diff --git a/src/components/common/PageContent.tsx b/src/components/common/PageContent.tsx index 6c1fcc05..08190988 100644 --- a/src/components/common/PageContent.tsx +++ b/src/components/common/PageContent.tsx @@ -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(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} >
{inParsedContent?.element} diff --git a/src/hooks/useMermaid.ts b/src/hooks/useMermaid.ts new file mode 100644 index 00000000..855f10f9 --- /dev/null +++ b/src/hooks/useMermaid.ts @@ -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, +) => { + 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]) +} diff --git a/src/lib/load-script.ts b/src/lib/load-script.ts new file mode 100644 index 00000000..8d7fc4cd --- /dev/null +++ b/src/lib/load-script.ts @@ -0,0 +1,45 @@ +const isLoadScriptMap: Record = {} +const loadingQueueMap: Record = {} +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) + }) +} diff --git a/src/markdown/index.ts b/src/markdown/index.ts index b21bfa0d..7fa4397c 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -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"], diff --git a/src/markdown/remark-mermaid.ts b/src/markdown/remark-mermaid.ts new file mode 100644 index 00000000..57ae0bc4 --- /dev/null +++ b/src/markdown/remark-mermaid.ts @@ -0,0 +1,13 @@ +import { Plugin } from "unified" +import { Root } from "remark-gfm" +import { visit } from "unist-util-visit" + +export const remarkMermaid: Plugin, Root> = () => (tree, file) => { + visit(tree, (node) => { + if (node.type === "code" && node.lang === "mermaid") { + // @ts-ignore + node.type = "html" + node.value = `
${node.value}
` + } + }) +}