mirror of https://github.com/usememos/memos
chore: implement node renderer components
parent
28c0549705
commit
5d677c3c57
@ -0,0 +1,18 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Blockquote: React.FC<Props> = ({ children }: Props) => {
|
||||||
|
return (
|
||||||
|
<blockquote>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Blockquote;
|
@ -0,0 +1,19 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bold: React.FC<Props> = ({ children }: Props) => {
|
||||||
|
return (
|
||||||
|
<strong>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</strong>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Bold;
|
@ -0,0 +1,14 @@
|
|||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BoldItalic: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return (
|
||||||
|
<strong>
|
||||||
|
<em>{content}</em>
|
||||||
|
</strong>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoldItalic;
|
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Code: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <code>{content}</code>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Code;
|
@ -0,0 +1,42 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import hljs from "highlight.js";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
language: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeBlock: React.FC<Props> = ({ language, content }: Props) => {
|
||||||
|
const formatedLanguage = language.toLowerCase() || "plaintext";
|
||||||
|
let highlightedCode = hljs.highlightAuto(content).value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const temp = hljs.highlight(content, {
|
||||||
|
language: formatedLanguage,
|
||||||
|
}).value;
|
||||||
|
highlightedCode = temp;
|
||||||
|
} catch (error) {
|
||||||
|
// Skip error and use default highlighted code.
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopyButtonClick = () => {
|
||||||
|
copy(content);
|
||||||
|
toast.success("Copied to clipboard!");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre className="group w-full my-1 p-3 rounded bg-gray-100 dark:bg-zinc-600 whitespace-pre-wrap relative">
|
||||||
|
<button
|
||||||
|
className="text-xs font-mono italic absolute top-0 right-0 px-2 leading-6 border btn-text rounded opacity-0 group-hover:opacity-60"
|
||||||
|
onClick={handleCopyButtonClick}
|
||||||
|
>
|
||||||
|
copy
|
||||||
|
</button>
|
||||||
|
<code className={classNames(`language-${formatedLanguage}`, "block")} dangerouslySetInnerHTML={{ __html: highlightedCode }}></code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeBlock;
|
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EscapingCharacter: React.FC<Props> = ({ symbol }: Props) => {
|
||||||
|
return <span>{symbol}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EscapingCharacter;
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
level: number;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Heading: React.FC<Props> = ({ level, children }: Props) => {
|
||||||
|
const Head = `h${level}` as keyof JSX.IntrinsicElements;
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</Head>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Heading;
|
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HorizontalRule: React.FC<Props> = () => {
|
||||||
|
return <hr />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HorizontalRule;
|
@ -0,0 +1,10 @@
|
|||||||
|
interface Props {
|
||||||
|
altText: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image: React.FC<Props> = ({ altText, url }: Props) => {
|
||||||
|
return <img alt={altText} src={url} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Image;
|
@ -0,0 +1,10 @@
|
|||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Italic: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <em>{content}</em>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Italic;
|
@ -0,0 +1,7 @@
|
|||||||
|
interface Props {}
|
||||||
|
|
||||||
|
const LineBreak: React.FC<Props> = () => {
|
||||||
|
return <br />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LineBreak;
|
@ -0,0 +1,14 @@
|
|||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Link: React.FC<Props> = ({ text, url }: Props) => {
|
||||||
|
return (
|
||||||
|
<a className="text-blue-600 dark:text-blue-400 cursor-pointer underline break-all hover:opacity-80 decoration-1" href={url}>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Link;
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
number: string;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderedList: React.FC<Props> = ({ number, children }: Props) => {
|
||||||
|
return (
|
||||||
|
<ol>
|
||||||
|
<li className="grid grid-cols-[24px_1fr] gap-1">
|
||||||
|
<div className="w-7 h-6 flex justify-center items-center">
|
||||||
|
<span className="opacity-80">{number}.</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderedList;
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Paragraph: React.FC<Props> = ({ children }: Props) => {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Paragraph;
|
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
BlockquoteNode,
|
||||||
|
BoldItalicNode,
|
||||||
|
BoldNode,
|
||||||
|
CodeBlockNode,
|
||||||
|
CodeNode,
|
||||||
|
EscapingCharacterNode,
|
||||||
|
HeadingNode,
|
||||||
|
HorizontalRuleNode,
|
||||||
|
ImageNode,
|
||||||
|
ItalicNode,
|
||||||
|
LinkNode,
|
||||||
|
Node,
|
||||||
|
NodeType,
|
||||||
|
OrderedListNode,
|
||||||
|
ParagraphNode,
|
||||||
|
StrikethroughNode,
|
||||||
|
TagNode,
|
||||||
|
TaskListNode,
|
||||||
|
TextNode,
|
||||||
|
UnorderedListNode,
|
||||||
|
} from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Blockquote from "./Blockquote";
|
||||||
|
import Bold from "./Bold";
|
||||||
|
import BoldItalic from "./BoldItalic";
|
||||||
|
import Code from "./Code";
|
||||||
|
import CodeBlock from "./CodeBlock";
|
||||||
|
import EscapingCharacter from "./EscapingCharacter";
|
||||||
|
import Heading from "./Heading";
|
||||||
|
import HorizontalRule from "./HorizontalRule";
|
||||||
|
import Image from "./Image";
|
||||||
|
import Italic from "./Italic";
|
||||||
|
import LineBreak from "./LineBreak";
|
||||||
|
import Link from "./Link";
|
||||||
|
import OrderedList from "./OrderedList";
|
||||||
|
import Paragraph from "./Paragraph";
|
||||||
|
import Strikethrough from "./Strikethrough";
|
||||||
|
import Tag from "./Tag";
|
||||||
|
import TaskList from "./TaskList";
|
||||||
|
import Text from "./Text";
|
||||||
|
import UnorderedList from "./UnorderedList";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
node: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Renderer: React.FC<Props> = ({ node }: Props) => {
|
||||||
|
switch (node.type) {
|
||||||
|
case NodeType.LINE_BREAK:
|
||||||
|
return <LineBreak />;
|
||||||
|
case NodeType.PARAGRAPH:
|
||||||
|
return <Paragraph {...(node.paragraphNode as ParagraphNode)} />;
|
||||||
|
case NodeType.CODE_BLOCK:
|
||||||
|
return <CodeBlock {...(node.codeBlockNode as CodeBlockNode)} />;
|
||||||
|
case NodeType.HEADING:
|
||||||
|
return <Heading {...(node.headingNode as HeadingNode)} />;
|
||||||
|
case NodeType.HORIZONTAL_RULE:
|
||||||
|
return <HorizontalRule {...(node.horizontalRuleNode as HorizontalRuleNode)} />;
|
||||||
|
case NodeType.BLOCKQUOTE:
|
||||||
|
return <Blockquote {...(node.blockquoteNode as BlockquoteNode)} />;
|
||||||
|
case NodeType.ORDERED_LIST:
|
||||||
|
return <OrderedList {...(node.orderedListNode as OrderedListNode)} />;
|
||||||
|
case NodeType.UNORDERED_LIST:
|
||||||
|
return <UnorderedList {...(node.unorderedListNode as UnorderedListNode)} />;
|
||||||
|
case NodeType.TASK_LIST:
|
||||||
|
return <TaskList {...(node.taskListNode as TaskListNode)} />;
|
||||||
|
case NodeType.TEXT:
|
||||||
|
return <Text {...(node.textNode as TextNode)} />;
|
||||||
|
case NodeType.BOLD:
|
||||||
|
return <Bold {...(node.boldNode as BoldNode)} />;
|
||||||
|
case NodeType.ITALIC:
|
||||||
|
return <Italic {...(node.italicNode as ItalicNode)} />;
|
||||||
|
case NodeType.BOLD_ITALIC:
|
||||||
|
return <BoldItalic {...(node.boldItalicNode as BoldItalicNode)} />;
|
||||||
|
case NodeType.CODE:
|
||||||
|
return <Code {...(node.codeNode as CodeNode)} />;
|
||||||
|
case NodeType.IMAGE:
|
||||||
|
return <Image {...(node.imageNode as ImageNode)} />;
|
||||||
|
case NodeType.LINK:
|
||||||
|
return <Link {...(node.linkNode as LinkNode)} />;
|
||||||
|
case NodeType.TAG:
|
||||||
|
return <Tag {...(node.tagNode as TagNode)} />;
|
||||||
|
case NodeType.STRIKETHROUGH:
|
||||||
|
return <Strikethrough {...(node.strikethroughNode as StrikethroughNode)} />;
|
||||||
|
case NodeType.ESCAPING_CHARACTER:
|
||||||
|
return <EscapingCharacter {...(node.escapingCharacterNode as EscapingCharacterNode)} />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Renderer;
|
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Strikethrough: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <del>{content}</del>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Strikethrough;
|
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tag: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <span className="inline-block w-auto text-blue-600 dark:text-blue-400">#{content}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tag;
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Checkbox } from "@mui/joy";
|
||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
complete: boolean;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskList: React.FC<Props> = ({ complete, children }: Props) => {
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
<li className="grid grid-cols-[24px_1fr] gap-1">
|
||||||
|
<div className="w-7 h-6 flex justify-center items-center">
|
||||||
|
<Checkbox size="sm" checked={complete} readOnly />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskList;
|
@ -0,0 +1,9 @@
|
|||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Text: React.FC<Props> = ({ content }: Props) => {
|
||||||
|
return <span>{content}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Text;
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
symbol: string;
|
||||||
|
children: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const UnorderedList: React.FC<Props> = ({ children }: Props) => {
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
<li className="grid grid-cols-[24px_1fr] gap-1">
|
||||||
|
<div className="w-7 h-6 flex justify-center items-center">
|
||||||
|
<span className="opacity-80">•</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<Renderer key={`${child.type}-${index}`} node={child} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnorderedList;
|
@ -0,0 +1,48 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { markdownServiceClient } from "@/grpcweb";
|
||||||
|
import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
className?: string;
|
||||||
|
onMemoContentClick?: (e: React.MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoContentV1: React.FC<Props> = (props: Props) => {
|
||||||
|
const { className, content, onMemoContentClick } = props;
|
||||||
|
const [nodes, setNodes] = useState<Node[]>([]);
|
||||||
|
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
markdownServiceClient
|
||||||
|
.parseMarkdown({
|
||||||
|
markdown: content,
|
||||||
|
})
|
||||||
|
.then(({ nodes }) => {
|
||||||
|
setNodes(nodes);
|
||||||
|
});
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
||||||
|
if (onMemoContentClick) {
|
||||||
|
onMemoContentClick(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300 ${className || ""}`}>
|
||||||
|
<div
|
||||||
|
ref={memoContentContainerRef}
|
||||||
|
className="w-full max-w-full word-break text-base leading-6 space-y-1"
|
||||||
|
onClick={handleMemoContentClick}
|
||||||
|
>
|
||||||
|
{nodes.map((node, index) => (
|
||||||
|
<Renderer key={`${node.type}-${index}`} node={node} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemoContentV1;
|
@ -0,0 +1 @@
|
|||||||
|
export interface RendererContext {}
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./context";
|
Loading…
Reference in New Issue