markdown.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  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. import React from "react";
  12. export function PreCode(props: { children: any }) {
  13. const ref = useRef<HTMLPreElement>(null);
  14. return (
  15. <pre ref={ref}>
  16. <span
  17. className="copy-code-button"
  18. onClick={() => {
  19. if (ref.current) {
  20. const code = ref.current.innerText;
  21. copyToClipboard(code);
  22. }
  23. }}
  24. ></span>
  25. {props.children}
  26. </pre>
  27. );
  28. }
  29. function _MarkDownContent(props: { content: string }) {
  30. return (
  31. <ReactMarkdown
  32. remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
  33. rehypePlugins={[
  34. RehypeKatex,
  35. [
  36. RehypeHighlight,
  37. {
  38. detect: false,
  39. ignoreMissing: true,
  40. },
  41. ],
  42. ]}
  43. components={{
  44. pre: PreCode,
  45. a: (aProps) => {
  46. const href = aProps.href || "";
  47. const isInternal = /^\/#/i.test(href);
  48. const target = isInternal ? "_self" : aProps.target ?? "_blank";
  49. return <a {...aProps} target={target} />;
  50. },
  51. }}
  52. >
  53. {props.content}
  54. </ReactMarkdown>
  55. );
  56. }
  57. export const MarkdownContent = React.memo(_MarkDownContent);
  58. export function Markdown(
  59. props: {
  60. content: string;
  61. loading?: boolean;
  62. fontSize?: number;
  63. parentRef: RefObject<HTMLDivElement>;
  64. defaultShow?: boolean;
  65. } & React.DOMAttributes<HTMLDivElement>,
  66. ) {
  67. const mdRef = useRef<HTMLDivElement>(null);
  68. const renderedHeight = useRef(0);
  69. const inView = useRef(!!props.defaultShow);
  70. const parent = props.parentRef.current;
  71. const md = mdRef.current;
  72. const checkInView = () => {
  73. if (parent && md) {
  74. const parentBounds = parent.getBoundingClientRect();
  75. const twoScreenHeight = Math.max(500, parentBounds.height * 2);
  76. const mdBounds = md.getBoundingClientRect();
  77. const isInRange = (x: number) =>
  78. x <= parentBounds.bottom + twoScreenHeight &&
  79. x >= parentBounds.top - twoScreenHeight;
  80. inView.current = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
  81. }
  82. if (inView.current && md) {
  83. renderedHeight.current = Math.max(
  84. renderedHeight.current,
  85. md.getBoundingClientRect().height,
  86. );
  87. }
  88. };
  89. checkInView();
  90. return (
  91. <div
  92. className="markdown-body"
  93. style={{
  94. fontSize: `${props.fontSize ?? 14}px`,
  95. height:
  96. !inView.current && renderedHeight.current > 0
  97. ? renderedHeight.current
  98. : "auto",
  99. }}
  100. ref={mdRef}
  101. onContextMenu={props.onContextMenu}
  102. onDoubleClickCapture={props.onDoubleClickCapture}
  103. >
  104. {inView.current &&
  105. (props.loading ? (
  106. <LoadingIcon />
  107. ) : (
  108. <MarkdownContent content={props.content} />
  109. ))}
  110. </div>
  111. );
  112. }