| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 | import ReactMarkdown from "react-markdown";import "katex/dist/katex.min.css";import RemarkMath from "remark-math";import RemarkBreaks from "remark-breaks";import RehypeKatex from "rehype-katex";import RemarkGfm from "remark-gfm";import RehypeHighlight from "rehype-highlight";import { useRef, useState, RefObject, useEffect } from "react";import { copyToClipboard } from "../utils";import mermaid from "mermaid";import LoadingIcon from "../icons/three-dots.svg";import React from "react";export function Mermaid(props: { code: string; onError: () => void }) {  const ref = useRef<HTMLDivElement>(null);  useEffect(() => {    if (props.code && ref.current) {      mermaid        .run({          nodes: [ref.current],        })        .catch((e) => {          props.onError();          console.error("[Mermaid] ", e.message);        });    }    // eslint-disable-next-line react-hooks/exhaustive-deps  }, [props.code]);  function viewSvgInNewWindow() {    const svg = ref.current?.querySelector("svg");    if (!svg) return;    const text = new XMLSerializer().serializeToString(svg);    const blob = new Blob([text], { type: "image/svg+xml" });    const url = URL.createObjectURL(blob);    const win = window.open(url);    if (win) {      win.onload = () => URL.revokeObjectURL(url);    }  }  return (    <div      className="no-dark"      style={{ cursor: "pointer", overflow: "auto" }}      ref={ref}      onClick={() => viewSvgInNewWindow()}    >      {props.code}    </div>  );}export function PreCode(props: { children: any }) {  const ref = useRef<HTMLPreElement>(null);  const [mermaidCode, setMermaidCode] = useState("");  useEffect(() => {    if (!ref.current) return;    const mermaidDom = ref.current.querySelector("code.language-mermaid");    if (mermaidDom) {      setMermaidCode((mermaidDom as HTMLElement).innerText);    }  }, [props.children]);  if (mermaidCode) {    return <Mermaid code={mermaidCode} onError={() => setMermaidCode("")} />;  }  return (    <pre ref={ref}>      <span        className="copy-code-button"        onClick={() => {          if (ref.current) {            const code = ref.current.innerText;            copyToClipboard(code);          }        }}      ></span>      {props.children}    </pre>  );}function _MarkDownContent(props: { content: string }) {  return (    <ReactMarkdown      remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}      rehypePlugins={[        RehypeKatex,        [          RehypeHighlight,          {            detect: false,            ignoreMissing: true,          },        ],      ]}      components={{        pre: PreCode,        a: (aProps) => {          const href = aProps.href || "";          const isInternal = /^\/#/i.test(href);          const target = isInternal ? "_self" : aProps.target ?? "_blank";          return <a {...aProps} target={target} />;        },      }}    >      {props.content}    </ReactMarkdown>  );}export const MarkdownContent = React.memo(_MarkDownContent);export function Markdown(  props: {    content: string;    loading?: boolean;    fontSize?: number;    parentRef: RefObject<HTMLDivElement>;    defaultShow?: boolean;  } & React.DOMAttributes<HTMLDivElement>,) {  const mdRef = useRef<HTMLDivElement>(null);  const renderedHeight = useRef(0);  const inView = useRef(!!props.defaultShow);  const parent = props.parentRef.current;  const md = mdRef.current;  const checkInView = () => {    if (parent && md) {      const parentBounds = parent.getBoundingClientRect();      const twoScreenHeight = Math.max(500, parentBounds.height * 2);      const mdBounds = md.getBoundingClientRect();      const parentTop = parentBounds.top - twoScreenHeight;      const parentBottom = parentBounds.bottom + twoScreenHeight;      const isOverlap =        Math.max(parentTop, mdBounds.top) <=        Math.min(parentBottom, mdBounds.bottom);      inView.current = isOverlap;    }    if (inView.current && md) {      renderedHeight.current = Math.max(        renderedHeight.current,        md.getBoundingClientRect().height,      );    }  };  setTimeout(() => checkInView(), 1);  return (    <div      className="markdown-body"      style={{        fontSize: `${props.fontSize ?? 14}px`,        height:          !inView.current && renderedHeight.current > 0            ? renderedHeight.current            : "auto",      }}      ref={mdRef}      onContextMenu={props.onContextMenu}      onDoubleClickCapture={props.onDoubleClickCapture}    >      {inView.current &&        (props.loading ? (          <LoadingIcon />        ) : (          <MarkdownContent content={props.content} />        ))}    </div>  );}
 |