markdown.tsx 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import ReactMarkdown from "react-markdown";
  2. import "katex/dist/katex.min.css";
  3. import RemarkMath from "remark-math";
  4. import RemarkBreaks from "remark-breaks";
  5. import RehypeKatex from "rehype-katex";
  6. import RemarkGfm from "remark-gfm";
  7. import RehypeHighlight from "rehype-highlight";
  8. import { useRef, useState, RefObject, useEffect } from "react";
  9. import { copyToClipboard } from "../utils";
  10. import LoadingIcon from "../icons/three-dots.svg";
  11. export function PreCode(props: { children: any }) {
  12. const ref = useRef<HTMLPreElement>(null);
  13. return (
  14. <pre ref={ref}>
  15. <span
  16. className="copy-code-button"
  17. onClick={() => {
  18. if (ref.current) {
  19. const code = ref.current.innerText;
  20. copyToClipboard(code);
  21. }
  22. }}
  23. ></span>
  24. {props.children}
  25. </pre>
  26. );
  27. }
  28. const useLazyLoad = (ref: RefObject<Element>): boolean => {
  29. const [isIntersecting, setIntersecting] = useState<boolean>(false);
  30. useEffect(() => {
  31. const observer = new IntersectionObserver(([entry]) => {
  32. if (entry.isIntersecting) {
  33. setIntersecting(true);
  34. observer.disconnect();
  35. }
  36. });
  37. if (ref.current) {
  38. observer.observe(ref.current);
  39. }
  40. return () => {
  41. observer.disconnect();
  42. };
  43. }, [ref]);
  44. return isIntersecting;
  45. };
  46. export function Markdown(
  47. props: {
  48. content: string;
  49. loading?: boolean;
  50. fontSize?: number;
  51. } & React.DOMAttributes<HTMLDivElement>,
  52. ) {
  53. const mdRef = useRef(null);
  54. const shouldRender = useLazyLoad(mdRef);
  55. const shouldLoading = props.loading || !shouldRender;
  56. return (
  57. <div
  58. className="markdown-body"
  59. style={{ fontSize: `${props.fontSize ?? 14}px` }}
  60. {...props}
  61. ref={mdRef}
  62. >
  63. {shouldLoading ? (
  64. <LoadingIcon />
  65. ) : (
  66. <ReactMarkdown
  67. remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
  68. rehypePlugins={[
  69. RehypeKatex,
  70. [
  71. RehypeHighlight,
  72. {
  73. detect: false,
  74. ignoreMissing: true,
  75. },
  76. ],
  77. ]}
  78. components={{
  79. pre: PreCode,
  80. }}
  81. linkTarget={"_blank"}
  82. >
  83. {props.content}
  84. </ReactMarkdown>
  85. )}
  86. </div>
  87. );
  88. }