markdown.tsx 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  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. export function Markdown(
  29. props: {
  30. content: string;
  31. loading?: boolean;
  32. fontSize?: number;
  33. parentRef: RefObject<HTMLDivElement>;
  34. } & React.DOMAttributes<HTMLDivElement>,
  35. ) {
  36. const mdRef = useRef<HTMLDivElement>(null);
  37. const parent = props.parentRef.current;
  38. const md = mdRef.current;
  39. const rendered = useRef(true); // disable lazy loading for bad ux
  40. const [counter, setCounter] = useState(0);
  41. useEffect(() => {
  42. // to triggr rerender
  43. setCounter(counter + 1);
  44. // eslint-disable-next-line react-hooks/exhaustive-deps
  45. }, [props.loading]);
  46. const inView =
  47. rendered.current ||
  48. (() => {
  49. if (parent && md) {
  50. const parentBounds = parent.getBoundingClientRect();
  51. const mdBounds = md.getBoundingClientRect();
  52. const isInRange = (x: number) =>
  53. x <= parentBounds.bottom && x >= parentBounds.top;
  54. const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
  55. if (inView) {
  56. rendered.current = true;
  57. }
  58. return inView;
  59. }
  60. })();
  61. const shouldLoading = props.loading || !inView;
  62. return (
  63. <div
  64. className="markdown-body"
  65. style={{ fontSize: `${props.fontSize ?? 14}px` }}
  66. ref={mdRef}
  67. onContextMenu={props.onContextMenu}
  68. onDoubleClickCapture={props.onDoubleClickCapture}
  69. >
  70. {shouldLoading ? (
  71. <LoadingIcon />
  72. ) : (
  73. <ReactMarkdown
  74. remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
  75. rehypePlugins={[
  76. RehypeKatex,
  77. [
  78. RehypeHighlight,
  79. {
  80. detect: false,
  81. ignoreMissing: true,
  82. },
  83. ],
  84. ]}
  85. components={{
  86. pre: PreCode,
  87. }}
  88. linkTarget={"_blank"}
  89. >
  90. {props.content}
  91. </ReactMarkdown>
  92. )}
  93. </div>
  94. );
  95. }