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