123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- 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 LoadingIcon from "../icons/three-dots.svg";
- export function PreCode(props: { children: any }) {
- const ref = useRef<HTMLPreElement>(null);
- return (
- <pre ref={ref}>
- <span
- className="copy-code-button"
- onClick={() => {
- if (ref.current) {
- const code = ref.current.innerText;
- copyToClipboard(code);
- }
- }}
- ></span>
- {props.children}
- </pre>
- );
- }
- const useLazyLoad = (ref: RefObject<Element>): boolean => {
- const [isIntersecting, setIntersecting] = useState<boolean>(false);
- useEffect(() => {
- const observer = new IntersectionObserver(([entry]) => {
- if (entry.isIntersecting) {
- setIntersecting(true);
- observer.disconnect();
- }
- });
- if (ref.current) {
- observer.observe(ref.current);
- }
- return () => {
- observer.disconnect();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- return isIntersecting;
- };
- export function Markdown(
- props: {
- content: string;
- loading?: boolean;
- fontSize?: number;
- parentRef: RefObject<HTMLDivElement>;
- } & React.DOMAttributes<HTMLDivElement>,
- ) {
- const mdRef = useRef<HTMLDivElement>(null);
- const parent = props.parentRef.current;
- const md = mdRef.current;
- const rendered = useRef(false);
- const [counter, setCounter] = useState(0);
- useEffect(() => {
- // to triggr rerender
- setCounter(counter + 1);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.loading]);
- const inView =
- rendered.current ||
- (() => {
- if (parent && md) {
- const parentBounds = parent.getBoundingClientRect();
- const mdBounds = md.getBoundingClientRect();
- const isInRange = (x: number) =>
- x <= parentBounds.bottom && x >= parentBounds.top;
- const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
- if (inView) {
- rendered.current = true;
- }
- return inView;
- }
- })();
- const shouldLoading = props.loading || !inView;
- return (
- <div
- className="markdown-body"
- style={{ fontSize: `${props.fontSize ?? 14}px` }}
- ref={mdRef}
- onContextMenu={props.onContextMenu}
- onDoubleClickCapture={props.onDoubleClickCapture}
- >
- {shouldLoading ? (
- <LoadingIcon />
- ) : (
- <ReactMarkdown
- remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
- rehypePlugins={[
- RehypeKatex,
- [
- RehypeHighlight,
- {
- detect: false,
- ignoreMissing: true,
- },
- ],
- ]}
- components={{
- pre: PreCode,
- }}
- linkTarget={"_blank"}
- >
- {props.content}
- </ReactMarkdown>
- )}
- </div>
- );
- }
|