forked from XiaoMo/ChatGPT-Next-Web
aeda7520fe
Resolved Markdown Issue This pull request also resolves an issue where internal links were not redirecting properly in markdown, and optimizes the behavior for external links to open in a new window.
124 lines
3.2 KiB
TypeScript
124 lines
3.2 KiB
TypeScript
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";
|
|
import React from "react";
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
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 isInRange = (x: number) =>
|
|
x <= parentBounds.bottom + twoScreenHeight &&
|
|
x >= parentBounds.top - twoScreenHeight;
|
|
inView.current = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
|
|
}
|
|
|
|
if (inView.current && md) {
|
|
renderedHeight.current = Math.max(
|
|
renderedHeight.current,
|
|
md.getBoundingClientRect().height,
|
|
);
|
|
}
|
|
};
|
|
|
|
checkInView();
|
|
|
|
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>
|
|
);
|
|
}
|