From 5d677c3c578d6052fd9c17b3e81722b16edddbce Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 17 Dec 2023 10:49:49 +0800 Subject: [PATCH] chore: implement node renderer components --- proto/api/v2/markdown_service.proto | 1 - proto/gen/api/v2/README.md | 2 +- proto/gen/api/v2/markdown_service.pb.go | 1 - .../components/MemoContentV1/Blockquote.tsx | 18 ++++ web/src/components/MemoContentV1/Bold.tsx | 19 ++++ .../components/MemoContentV1/BoldItalic.tsx | 14 +++ web/src/components/MemoContentV1/Code.tsx | 9 ++ .../components/MemoContentV1/CodeBlock.tsx | 42 +++++++++ .../MemoContentV1/EscapingCharacter.tsx | 9 ++ web/src/components/MemoContentV1/Heading.tsx | 20 ++++ .../MemoContentV1/HorizontalRule.tsx | 9 ++ web/src/components/MemoContentV1/Image.tsx | 10 ++ web/src/components/MemoContentV1/Italic.tsx | 10 ++ .../components/MemoContentV1/LineBreak.tsx | 7 ++ web/src/components/MemoContentV1/Link.tsx | 14 +++ .../components/MemoContentV1/OrderedList.tsx | 26 ++++++ .../components/MemoContentV1/Paragraph.tsx | 18 ++++ web/src/components/MemoContentV1/Renderer.tsx | 92 +++++++++++++++++++ .../MemoContentV1/Strikethrough.tsx | 9 ++ web/src/components/MemoContentV1/Tag.tsx | 9 ++ web/src/components/MemoContentV1/TaskList.tsx | 28 ++++++ web/src/components/MemoContentV1/Text.tsx | 9 ++ .../MemoContentV1/UnorderedList.tsx | 26 ++++++ web/src/components/MemoContentV1/index.tsx | 48 ++++++++++ .../components/MemoContentV1/types/context.ts | 1 + .../components/MemoContentV1/types/index.ts | 1 + web/src/components/MemoEditor/index.tsx | 13 +-- web/src/grpcweb.ts | 3 + web/src/less/memo-content.less | 10 -- web/src/pages/MemoDetail.tsx | 4 +- 30 files changed, 456 insertions(+), 26 deletions(-) create mode 100644 web/src/components/MemoContentV1/Blockquote.tsx create mode 100644 web/src/components/MemoContentV1/Bold.tsx create mode 100644 web/src/components/MemoContentV1/BoldItalic.tsx create mode 100644 web/src/components/MemoContentV1/Code.tsx create mode 100644 web/src/components/MemoContentV1/CodeBlock.tsx create mode 100644 web/src/components/MemoContentV1/EscapingCharacter.tsx create mode 100644 web/src/components/MemoContentV1/Heading.tsx create mode 100644 web/src/components/MemoContentV1/HorizontalRule.tsx create mode 100644 web/src/components/MemoContentV1/Image.tsx create mode 100644 web/src/components/MemoContentV1/Italic.tsx create mode 100644 web/src/components/MemoContentV1/LineBreak.tsx create mode 100644 web/src/components/MemoContentV1/Link.tsx create mode 100644 web/src/components/MemoContentV1/OrderedList.tsx create mode 100644 web/src/components/MemoContentV1/Paragraph.tsx create mode 100644 web/src/components/MemoContentV1/Renderer.tsx create mode 100644 web/src/components/MemoContentV1/Strikethrough.tsx create mode 100644 web/src/components/MemoContentV1/Tag.tsx create mode 100644 web/src/components/MemoContentV1/TaskList.tsx create mode 100644 web/src/components/MemoContentV1/Text.tsx create mode 100644 web/src/components/MemoContentV1/UnorderedList.tsx create mode 100644 web/src/components/MemoContentV1/index.tsx create mode 100644 web/src/components/MemoContentV1/types/context.ts create mode 100644 web/src/components/MemoContentV1/types/index.ts diff --git a/proto/api/v2/markdown_service.proto b/proto/api/v2/markdown_service.proto index 1e0b20e09..3ce0bf286 100644 --- a/proto/api/v2/markdown_service.proto +++ b/proto/api/v2/markdown_service.proto @@ -46,7 +46,6 @@ enum NodeType { ESCAPING_CHARACTER = 19; } -// Define the Node message. message Node { NodeType type = 1; oneof node { diff --git a/proto/gen/api/v2/README.md b/proto/gen/api/v2/README.md index 86bf211aa..4bbb764da 100644 --- a/proto/gen/api/v2/README.md +++ b/proto/gen/api/v2/README.md @@ -1092,7 +1092,7 @@ ### Node -Define the Node message. + | Field | Type | Label | Description | diff --git a/proto/gen/api/v2/markdown_service.pb.go b/proto/gen/api/v2/markdown_service.pb.go index f75e10c0e..16b93436b 100644 --- a/proto/gen/api/v2/markdown_service.pb.go +++ b/proto/gen/api/v2/markdown_service.pb.go @@ -215,7 +215,6 @@ func (x *ParseMarkdownResponse) GetNodes() []*Node { return nil } -// Define the Node message. type Node struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache diff --git a/web/src/components/MemoContentV1/Blockquote.tsx b/web/src/components/MemoContentV1/Blockquote.tsx new file mode 100644 index 000000000..7d5065c02 --- /dev/null +++ b/web/src/components/MemoContentV1/Blockquote.tsx @@ -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 = ({ children }: Props) => { + return ( +
+ {children.map((child, index) => ( + + ))} +
+ ); +}; + +export default Blockquote; diff --git a/web/src/components/MemoContentV1/Bold.tsx b/web/src/components/MemoContentV1/Bold.tsx new file mode 100644 index 000000000..3919f0f7a --- /dev/null +++ b/web/src/components/MemoContentV1/Bold.tsx @@ -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 = ({ children }: Props) => { + return ( + + {children.map((child, index) => ( + + ))} + + ); +}; + +export default Bold; diff --git a/web/src/components/MemoContentV1/BoldItalic.tsx b/web/src/components/MemoContentV1/BoldItalic.tsx new file mode 100644 index 000000000..d59bc8bb0 --- /dev/null +++ b/web/src/components/MemoContentV1/BoldItalic.tsx @@ -0,0 +1,14 @@ +interface Props { + symbol: string; + content: string; +} + +const BoldItalic: React.FC = ({ content }: Props) => { + return ( + + {content} + + ); +}; + +export default BoldItalic; diff --git a/web/src/components/MemoContentV1/Code.tsx b/web/src/components/MemoContentV1/Code.tsx new file mode 100644 index 000000000..59a115f13 --- /dev/null +++ b/web/src/components/MemoContentV1/Code.tsx @@ -0,0 +1,9 @@ +interface Props { + content: string; +} + +const Code: React.FC = ({ content }: Props) => { + return {content}; +}; + +export default Code; diff --git a/web/src/components/MemoContentV1/CodeBlock.tsx b/web/src/components/MemoContentV1/CodeBlock.tsx new file mode 100644 index 000000000..ce08e4d50 --- /dev/null +++ b/web/src/components/MemoContentV1/CodeBlock.tsx @@ -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 = ({ 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 ( +
+      
+      
+    
+ ); +}; + +export default CodeBlock; diff --git a/web/src/components/MemoContentV1/EscapingCharacter.tsx b/web/src/components/MemoContentV1/EscapingCharacter.tsx new file mode 100644 index 000000000..ffd567250 --- /dev/null +++ b/web/src/components/MemoContentV1/EscapingCharacter.tsx @@ -0,0 +1,9 @@ +interface Props { + symbol: string; +} + +const EscapingCharacter: React.FC = ({ symbol }: Props) => { + return {symbol}; +}; + +export default EscapingCharacter; diff --git a/web/src/components/MemoContentV1/Heading.tsx b/web/src/components/MemoContentV1/Heading.tsx new file mode 100644 index 000000000..c9649c5e4 --- /dev/null +++ b/web/src/components/MemoContentV1/Heading.tsx @@ -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 = ({ level, children }: Props) => { + const Head = `h${level}` as keyof JSX.IntrinsicElements; + return ( + + {children.map((child, index) => ( + + ))} + + ); +}; + +export default Heading; diff --git a/web/src/components/MemoContentV1/HorizontalRule.tsx b/web/src/components/MemoContentV1/HorizontalRule.tsx new file mode 100644 index 000000000..908d049fa --- /dev/null +++ b/web/src/components/MemoContentV1/HorizontalRule.tsx @@ -0,0 +1,9 @@ +interface Props { + symbol: string; +} + +const HorizontalRule: React.FC = () => { + return
; +}; + +export default HorizontalRule; diff --git a/web/src/components/MemoContentV1/Image.tsx b/web/src/components/MemoContentV1/Image.tsx new file mode 100644 index 000000000..7957b58bc --- /dev/null +++ b/web/src/components/MemoContentV1/Image.tsx @@ -0,0 +1,10 @@ +interface Props { + altText: string; + url: string; +} + +const Image: React.FC = ({ altText, url }: Props) => { + return {altText}; +}; + +export default Image; diff --git a/web/src/components/MemoContentV1/Italic.tsx b/web/src/components/MemoContentV1/Italic.tsx new file mode 100644 index 000000000..ccd2be9cd --- /dev/null +++ b/web/src/components/MemoContentV1/Italic.tsx @@ -0,0 +1,10 @@ +interface Props { + symbol: string; + content: string; +} + +const Italic: React.FC = ({ content }: Props) => { + return {content}; +}; + +export default Italic; diff --git a/web/src/components/MemoContentV1/LineBreak.tsx b/web/src/components/MemoContentV1/LineBreak.tsx new file mode 100644 index 000000000..31d1deae6 --- /dev/null +++ b/web/src/components/MemoContentV1/LineBreak.tsx @@ -0,0 +1,7 @@ +interface Props {} + +const LineBreak: React.FC = () => { + return
; +}; + +export default LineBreak; diff --git a/web/src/components/MemoContentV1/Link.tsx b/web/src/components/MemoContentV1/Link.tsx new file mode 100644 index 000000000..4298ef95c --- /dev/null +++ b/web/src/components/MemoContentV1/Link.tsx @@ -0,0 +1,14 @@ +interface Props { + text: string; + url: string; +} + +const Link: React.FC = ({ text, url }: Props) => { + return ( + + {text} + + ); +}; + +export default Link; diff --git a/web/src/components/MemoContentV1/OrderedList.tsx b/web/src/components/MemoContentV1/OrderedList.tsx new file mode 100644 index 000000000..fc22c0611 --- /dev/null +++ b/web/src/components/MemoContentV1/OrderedList.tsx @@ -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 = ({ number, children }: Props) => { + return ( +
    +
  1. +
    + {number}. +
    +
    + {children.map((child, index) => ( + + ))} +
    +
  2. +
+ ); +}; + +export default OrderedList; diff --git a/web/src/components/MemoContentV1/Paragraph.tsx b/web/src/components/MemoContentV1/Paragraph.tsx new file mode 100644 index 000000000..bda04b792 --- /dev/null +++ b/web/src/components/MemoContentV1/Paragraph.tsx @@ -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 = ({ children }: Props) => { + return ( +

+ {children.map((child, index) => ( + + ))} +

+ ); +}; + +export default Paragraph; diff --git a/web/src/components/MemoContentV1/Renderer.tsx b/web/src/components/MemoContentV1/Renderer.tsx new file mode 100644 index 000000000..83eee7a6b --- /dev/null +++ b/web/src/components/MemoContentV1/Renderer.tsx @@ -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 = ({ node }: Props) => { + switch (node.type) { + case NodeType.LINE_BREAK: + return ; + case NodeType.PARAGRAPH: + return ; + case NodeType.CODE_BLOCK: + return ; + case NodeType.HEADING: + return ; + case NodeType.HORIZONTAL_RULE: + return ; + case NodeType.BLOCKQUOTE: + return
; + case NodeType.ORDERED_LIST: + return ; + case NodeType.UNORDERED_LIST: + return ; + case NodeType.TASK_LIST: + return ; + case NodeType.TEXT: + return ; + case NodeType.BOLD: + return ; + case NodeType.ITALIC: + return ; + case NodeType.BOLD_ITALIC: + return ; + case NodeType.CODE: + return ; + case NodeType.IMAGE: + return ; + case NodeType.LINK: + return ; + case NodeType.TAG: + return ; + case NodeType.STRIKETHROUGH: + return ; + case NodeType.ESCAPING_CHARACTER: + return ; + default: + return null; + } +}; + +export default Renderer; diff --git a/web/src/components/MemoContentV1/Strikethrough.tsx b/web/src/components/MemoContentV1/Strikethrough.tsx new file mode 100644 index 000000000..0bfcde6d8 --- /dev/null +++ b/web/src/components/MemoContentV1/Strikethrough.tsx @@ -0,0 +1,9 @@ +interface Props { + content: string; +} + +const Strikethrough: React.FC = ({ content }: Props) => { + return {content}; +}; + +export default Strikethrough; diff --git a/web/src/components/MemoContentV1/Tag.tsx b/web/src/components/MemoContentV1/Tag.tsx new file mode 100644 index 000000000..2534a815e --- /dev/null +++ b/web/src/components/MemoContentV1/Tag.tsx @@ -0,0 +1,9 @@ +interface Props { + content: string; +} + +const Tag: React.FC = ({ content }: Props) => { + return #{content}; +}; + +export default Tag; diff --git a/web/src/components/MemoContentV1/TaskList.tsx b/web/src/components/MemoContentV1/TaskList.tsx new file mode 100644 index 000000000..7774bfe65 --- /dev/null +++ b/web/src/components/MemoContentV1/TaskList.tsx @@ -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 = ({ complete, children }: Props) => { + return ( +
    +
  • +
    + +
    +
    + {children.map((child, index) => ( + + ))} +
    +
  • +
+ ); +}; + +export default TaskList; diff --git a/web/src/components/MemoContentV1/Text.tsx b/web/src/components/MemoContentV1/Text.tsx new file mode 100644 index 000000000..3cacdffeb --- /dev/null +++ b/web/src/components/MemoContentV1/Text.tsx @@ -0,0 +1,9 @@ +interface Props { + content: string; +} + +const Text: React.FC = ({ content }: Props) => { + return {content}; +}; + +export default Text; diff --git a/web/src/components/MemoContentV1/UnorderedList.tsx b/web/src/components/MemoContentV1/UnorderedList.tsx new file mode 100644 index 000000000..e26d2e3e6 --- /dev/null +++ b/web/src/components/MemoContentV1/UnorderedList.tsx @@ -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 = ({ children }: Props) => { + return ( +
    +
  • +
    + +
    +
    + {children.map((child, index) => ( + + ))} +
    +
  • +
+ ); +}; + +export default UnorderedList; diff --git a/web/src/components/MemoContentV1/index.tsx b/web/src/components/MemoContentV1/index.tsx new file mode 100644 index 000000000..7eecf75c1 --- /dev/null +++ b/web/src/components/MemoContentV1/index.tsx @@ -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) => { + const { className, content, onMemoContentClick } = props; + const [nodes, setNodes] = useState([]); + const memoContentContainerRef = useRef(null); + + useEffect(() => { + markdownServiceClient + .parseMarkdown({ + markdown: content, + }) + .then(({ nodes }) => { + setNodes(nodes); + }); + }, [content]); + + const handleMemoContentClick = async (e: React.MouseEvent) => { + if (onMemoContentClick) { + onMemoContentClick(e); + } + }; + + return ( +
+
+ {nodes.map((node, index) => ( + + ))} +
+
+ ); +}; + +export default MemoContentV1; diff --git a/web/src/components/MemoContentV1/types/context.ts b/web/src/components/MemoContentV1/types/context.ts new file mode 100644 index 000000000..da10b091f --- /dev/null +++ b/web/src/components/MemoContentV1/types/context.ts @@ -0,0 +1 @@ +export interface RendererContext {} diff --git a/web/src/components/MemoContentV1/types/index.ts b/web/src/components/MemoContentV1/types/index.ts new file mode 100644 index 000000000..2edd280c7 --- /dev/null +++ b/web/src/components/MemoContentV1/types/index.ts @@ -0,0 +1 @@ +export * from "./context"; diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 3c9ec46aa..b6b7ff368 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -1,13 +1,12 @@ import { Select, Option, Button, IconButton, Divider } from "@mui/joy"; -import { isNumber, last, uniq, uniqBy } from "lodash-es"; +import { isNumber, last, uniqBy } from "lodash-es"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import useLocalStorage from "react-use/lib/useLocalStorage"; import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { getMatchedNodes } from "@/labs/marked"; -import { useGlobalStore, useMemoStore, useResourceStore, useTagStore } from "@/store/module"; +import { useGlobalStore, useMemoStore, useResourceStore } from "@/store/module"; import { useUserV1Store } from "@/store/v1"; import { Resource } from "@/types/proto/api/v2/resource_service"; import { UserSetting, User_Role } from "@/types/proto/api/v2/user_service"; @@ -52,7 +51,6 @@ const MemoEditor = (props: Props) => { } = useGlobalStore(); const userV1Store = useUserV1Store(); const memoStore = useMemoStore(); - const tagStore = useTagStore(); const resourceStore = useResourceStore(); const currentUser = useCurrentUser(); const [state, setState] = useState({ @@ -335,13 +333,6 @@ const MemoEditor = (props: Props) => { }; }); - // Upsert tag with the content. - const matchedNodes = getMatchedNodes(content); - const tagNameList = uniq(matchedNodes.filter((node) => node.parserName === "tag").map((node) => node.matchedContent.slice(1))); - for (const tagName of tagNameList) { - await tagStore.upsertTag(tagName); - } - setState((prevState) => ({ ...prevState, resourceList: [], diff --git a/web/src/grpcweb.ts b/web/src/grpcweb.ts index 975184b72..33ad6692b 100644 --- a/web/src/grpcweb.ts +++ b/web/src/grpcweb.ts @@ -2,6 +2,7 @@ import { createChannel, createClientFactory, FetchTransport } from "nice-grpc-we import { ActivityServiceDefinition } from "./types/proto/api/v2/activity_service"; import { AuthServiceDefinition } from "./types/proto/api/v2/auth_service"; import { InboxServiceDefinition } from "./types/proto/api/v2/inbox_service"; +import { MarkdownServiceDefinition } from "./types/proto/api/v2/markdown_service"; import { MemoServiceDefinition } from "./types/proto/api/v2/memo_service"; import { ResourceServiceDefinition } from "./types/proto/api/v2/resource_service"; import { SystemServiceDefinition } from "./types/proto/api/v2/system_service"; @@ -35,3 +36,5 @@ export const inboxServiceClient = clientFactory.create(InboxServiceDefinition, c export const activityServiceClient = clientFactory.create(ActivityServiceDefinition, channel); export const webhookServiceClient = clientFactory.create(WebhookServiceDefinition, channel); + +export const markdownServiceClient = clientFactory.create(MarkdownServiceDefinition, channel); diff --git a/web/src/less/memo-content.less b/web/src/less/memo-content.less index ef21e40dc..d01521bde 100644 --- a/web/src/less/memo-content.less +++ b/web/src/less/memo-content.less @@ -69,16 +69,6 @@ code { @apply block; } - - &:hover { - .codeblock-copy-btn { - @apply flex; - } - } - - .codeblock-copy-btn { - @apply btn-normal absolute hidden top-2 right-2 border-solid border-2; - } } code { diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 25ff9f28c..4f1d15bdf 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -6,7 +6,7 @@ import { Link, useParams } from "react-router-dom"; import FloatingNavButton from "@/components/FloatingNavButton"; import Icon from "@/components/Icon"; import Memo from "@/components/Memo"; -import MemoContent from "@/components/MemoContent"; +import MemoContentV1 from "@/components/MemoContentV1"; import MemoEditor from "@/components/MemoEditor"; import showMemoEditorDialog from "@/components/MemoEditor/MemoEditorDialog"; import MemoRelationListView from "@/components/MemoRelationListView"; @@ -133,7 +133,7 @@ const MemoDetail = () => {
{getDateTimeString(memo.displayTs)}
- +