feat: support markdown
This commit is contained in:
parent
bd230ddd4f
commit
85d144a1e9
34 changed files with 2350 additions and 4 deletions
69
ui/components/Markdown/renderer/inner/CodeRendererInner.tsx
Normal file
69
ui/components/Markdown/renderer/inner/CodeRendererInner.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { css, cx } from "@emotion/css";
|
||||
import CodeHighlighter from "@yozora/react-code-highlighter";
|
||||
import React from "react";
|
||||
import { astClasses } from "../../context";
|
||||
import { CopyButton } from "./CopyButton";
|
||||
|
||||
interface IProps {
|
||||
darken: boolean;
|
||||
lang: string;
|
||||
value: string;
|
||||
preferCodeWrap: boolean;
|
||||
showCodeLineno: boolean;
|
||||
pendingText?: string;
|
||||
copyingText?: string;
|
||||
copiedText?: string;
|
||||
failedText?: string;
|
||||
}
|
||||
|
||||
export class CodeRendererInner extends React.PureComponent<IProps> {
|
||||
public override render(): React.ReactElement {
|
||||
const { calcContentForCopy } = this;
|
||||
const { darken, lang, value, preferCodeWrap, showCodeLineno } = this.props;
|
||||
|
||||
return (
|
||||
<code className={codeCls} data-wrap={preferCodeWrap}>
|
||||
<CodeHighlighter
|
||||
lang={lang}
|
||||
value={value}
|
||||
collapsed={false}
|
||||
showLineNo={showCodeLineno && !preferCodeWrap}
|
||||
darken={darken}
|
||||
/>
|
||||
<div className={copyBtnCls}>
|
||||
<CopyButton calcContentForCopy={calcContentForCopy} />
|
||||
</div>
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
protected calcContentForCopy = (): string => {
|
||||
return this.props.value;
|
||||
};
|
||||
}
|
||||
|
||||
const copyBtnCls = css({
|
||||
position: "absolute",
|
||||
right: "4px",
|
||||
top: "4px",
|
||||
display: "none",
|
||||
});
|
||||
|
||||
const codeCls = cx(
|
||||
astClasses.code,
|
||||
css({
|
||||
position: "relative",
|
||||
display: "block",
|
||||
boxSizing: "border-box",
|
||||
borderRadius: "4px",
|
||||
margin: "0px 0px 1.25em 0px",
|
||||
backgroundColor: "var(--colorBgCode)",
|
||||
[`&:hover > .${copyBtnCls}`]: {
|
||||
display: "inline-block",
|
||||
},
|
||||
[`&&[data-wrap="true"] > div`]: {
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "keep-all",
|
||||
},
|
||||
}),
|
||||
);
|
69
ui/components/Markdown/renderer/inner/CopyButton.tsx
Normal file
69
ui/components/Markdown/renderer/inner/CopyButton.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { css, cx } from "@emotion/css";
|
||||
import { Copy as CopyIcon, ClipboardPaste as CopiedIcon } from "lucide-react";
|
||||
import copy from "copy-to-clipboard";
|
||||
import React from "react";
|
||||
|
||||
export enum CopyStatus {
|
||||
PENDING = 0,
|
||||
COPYING = 1,
|
||||
COPIED = 2,
|
||||
FAILED = 3,
|
||||
}
|
||||
|
||||
export interface ICopyButtonProps {
|
||||
delay?: number;
|
||||
className?: string;
|
||||
calcContentForCopy: () => string;
|
||||
}
|
||||
|
||||
export const CopyButton: React.FC<ICopyButtonProps> = props => {
|
||||
const { className, delay = 1500, calcContentForCopy } = props;
|
||||
const [status, setStatus] = React.useState<CopyStatus>(CopyStatus.PENDING);
|
||||
const disabled: boolean = status !== CopyStatus.PENDING;
|
||||
|
||||
const onCopy = () => {
|
||||
if (status === CopyStatus.PENDING) {
|
||||
setStatus(CopyStatus.COPYING);
|
||||
try {
|
||||
const contentForCopy: string = calcContentForCopy();
|
||||
copy(contentForCopy);
|
||||
setStatus(CopyStatus.COPIED);
|
||||
} catch (_error) {
|
||||
setStatus(CopyStatus.FAILED);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect((): (() => void) | undefined => {
|
||||
if (status === CopyStatus.COPIED || status === CopyStatus.FAILED) {
|
||||
const timer = setTimeout(() => setStatus(CopyStatus.PENDING), delay);
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [status, delay]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cx(
|
||||
classes.copyButton,
|
||||
className,
|
||||
"bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2",
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onCopy}
|
||||
>
|
||||
status === CopyStatus.PENDING ? <CopyIcon size={24} /> : <CopiedIcon size={24} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const classes = {
|
||||
copyButton: css({
|
||||
cursor: "pointer",
|
||||
}),
|
||||
};
|
59
ui/components/Markdown/renderer/inner/ImageRendererInner.tsx
Normal file
59
ui/components/Markdown/renderer/inner/ImageRendererInner.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { css } from "@emotion/css";
|
||||
import React from "react";
|
||||
|
||||
interface IProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
title: string | undefined;
|
||||
srcSet: string | undefined;
|
||||
sizes: string | undefined;
|
||||
loading: "eager" | "lazy" | undefined;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export class ImageRendererInner extends React.Component<IProps> {
|
||||
public override shouldComponentUpdate(nextProps: IProps): boolean {
|
||||
const props = this.props;
|
||||
return (
|
||||
props.src !== nextProps.src ||
|
||||
props.alt !== nextProps.alt ||
|
||||
props.title !== nextProps.title ||
|
||||
props.srcSet !== nextProps.srcSet ||
|
||||
props.sizes !== nextProps.sizes ||
|
||||
props.loading !== nextProps.loading ||
|
||||
props.className !== nextProps.className
|
||||
);
|
||||
}
|
||||
|
||||
public override render(): React.ReactElement {
|
||||
const { src, alt, title, srcSet, sizes, loading, className } = this.props;
|
||||
return (
|
||||
<figure className={`${className} ${cls}`}>
|
||||
<img alt={alt} src={src} title={title} srcSet={srcSet} sizes={sizes} loading={loading} />
|
||||
{title && <figcaption>{title}</figcaption>}
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cls = css({
|
||||
boxSizing: "border-box",
|
||||
maxWidth: "80%", // Prevent images from overflowing the container.
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
margin: 0,
|
||||
"> img": {
|
||||
flex: "1 0 auto",
|
||||
boxSizing: "border-box",
|
||||
maxWidth: "100%",
|
||||
border: "1px solid var(--colorBorderImage)",
|
||||
boxShadow: "0 0 20px 1px rgba(126, 125, 150, 0.6)",
|
||||
},
|
||||
"> figcaption": {
|
||||
textAlign: "center",
|
||||
fontStyle: "italic",
|
||||
fontSize: "1em",
|
||||
color: "var(--colorImageTitle)",
|
||||
},
|
||||
});
|
48
ui/components/Markdown/renderer/inner/LinkRendererInner.tsx
Normal file
48
ui/components/Markdown/renderer/inner/LinkRendererInner.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { css, cx } from "@emotion/css";
|
||||
import type { Node } from "@yozora/ast";
|
||||
import React from "react";
|
||||
import { NodesRenderer } from "../../NodesRenderer";
|
||||
|
||||
interface IProps {
|
||||
url: string;
|
||||
title: string | undefined;
|
||||
childNodes: Node[] | undefined;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export class LinkRendererInner extends React.Component<IProps> {
|
||||
public override shouldComponentUpdate(nextProps: Readonly<IProps>): boolean {
|
||||
const props = this.props;
|
||||
return (
|
||||
props.url !== nextProps.url ||
|
||||
props.title !== nextProps.title ||
|
||||
props.childNodes !== nextProps.childNodes ||
|
||||
props.className !== nextProps.className
|
||||
);
|
||||
}
|
||||
|
||||
public override render(): React.ReactElement {
|
||||
const { url, title, childNodes, className } = this.props;
|
||||
return (
|
||||
<a className={cx(cls, className)} href={url} title={title} rel="noopener, noreferrer" target="_blank">
|
||||
<NodesRenderer nodes={childNodes} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cls = css({
|
||||
padding: "0.2rem 0",
|
||||
color: "var(--colorLink)",
|
||||
textDecoration: "none",
|
||||
"&:active": {
|
||||
color: "var(--colorLinkActive)",
|
||||
},
|
||||
"&&:hover": {
|
||||
color: "var(--colorLinkHover)",
|
||||
textDecoration: "underline",
|
||||
},
|
||||
"&:visited": {
|
||||
color: "var(--colorLinkVisited)",
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue