feat: support markdown

This commit is contained in:
guanghechen 2024-07-10 16:17:23 +08:00
parent bd230ddd4f
commit 85d144a1e9
34 changed files with 2350 additions and 4 deletions

View 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",
},
}),
);

View 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",
}),
};

View 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)",
},
});

View 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)",
},
});