diff --git a/api/v2/markdown_service.go b/api/v2/markdown_service.go index 7308228a..12c0229d 100644 --- a/api/v2/markdown_service.go +++ b/api/v2/markdown_service.go @@ -95,6 +95,73 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node { return node } +func convertToASTNodes(nodes []*apiv2pb.Node) []ast.Node { + rawNodes := []ast.Node{} + for _, node := range nodes { + rawNode := convertToASTNode(node) + rawNodes = append(rawNodes, rawNode) + } + return rawNodes +} + +func convertToASTNode(node *apiv2pb.Node) ast.Node { + switch n := node.Node.(type) { + case *apiv2pb.Node_LineBreakNode: + return &ast.LineBreak{} + case *apiv2pb.Node_ParagraphNode: + children := convertToASTNodes(n.ParagraphNode.Children) + return &ast.Paragraph{Children: children} + case *apiv2pb.Node_CodeBlockNode: + return &ast.CodeBlock{Language: n.CodeBlockNode.Language, Content: n.CodeBlockNode.Content} + case *apiv2pb.Node_HeadingNode: + children := convertToASTNodes(n.HeadingNode.Children) + return &ast.Heading{Level: int(n.HeadingNode.Level), Children: children} + case *apiv2pb.Node_HorizontalRuleNode: + return &ast.HorizontalRule{Symbol: n.HorizontalRuleNode.Symbol} + case *apiv2pb.Node_BlockquoteNode: + children := convertToASTNodes(n.BlockquoteNode.Children) + return &ast.Blockquote{Children: children} + case *apiv2pb.Node_OrderedListNode: + children := convertToASTNodes(n.OrderedListNode.Children) + return &ast.OrderedList{Number: n.OrderedListNode.Number, Children: children} + case *apiv2pb.Node_UnorderedListNode: + children := convertToASTNodes(n.UnorderedListNode.Children) + return &ast.UnorderedList{Symbol: n.UnorderedListNode.Symbol, Children: children} + case *apiv2pb.Node_TaskListNode: + children := convertToASTNodes(n.TaskListNode.Children) + return &ast.TaskList{Symbol: n.TaskListNode.Symbol, Complete: n.TaskListNode.Complete, Children: children} + case *apiv2pb.Node_MathBlockNode: + return &ast.MathBlock{Content: n.MathBlockNode.Content} + case *apiv2pb.Node_TextNode: + return &ast.Text{Content: n.TextNode.Content} + case *apiv2pb.Node_BoldNode: + children := convertToASTNodes(n.BoldNode.Children) + return &ast.Bold{Symbol: n.BoldNode.Symbol, Children: children} + case *apiv2pb.Node_ItalicNode: + return &ast.Italic{Symbol: n.ItalicNode.Symbol, Content: n.ItalicNode.Content} + case *apiv2pb.Node_BoldItalicNode: + return &ast.BoldItalic{Symbol: n.BoldItalicNode.Symbol, Content: n.BoldItalicNode.Content} + case *apiv2pb.Node_CodeNode: + return &ast.Code{Content: n.CodeNode.Content} + case *apiv2pb.Node_ImageNode: + return &ast.Image{AltText: n.ImageNode.AltText, URL: n.ImageNode.Url} + case *apiv2pb.Node_LinkNode: + return &ast.Link{Text: n.LinkNode.Text, URL: n.LinkNode.Url} + case *apiv2pb.Node_AutoLinkNode: + return &ast.AutoLink{URL: n.AutoLinkNode.Url} + case *apiv2pb.Node_TagNode: + return &ast.Tag{Content: n.TagNode.Content} + case *apiv2pb.Node_StrikethroughNode: + return &ast.Strikethrough{Content: n.StrikethroughNode.Content} + case *apiv2pb.Node_EscapingCharacterNode: + return &ast.EscapingCharacter{Symbol: n.EscapingCharacterNode.Symbol} + case *apiv2pb.Node_MathNode: + return &ast.Math{Content: n.MathNode.Content} + default: + return &ast.Text{} + } +} + func traverseASTNodes(nodes []ast.Node, fn func(ast.Node)) { for _, node := range nodes { fn(node) diff --git a/api/v2/memo_service.go b/api/v2/memo_service.go index 3655e77c..0148f058 100644 --- a/api/v2/memo_service.go +++ b/api/v2/memo_service.go @@ -19,6 +19,7 @@ import ( "github.com/usememos/memos/plugin/gomark/ast" "github.com/usememos/memos/plugin/gomark/parser" "github.com/usememos/memos/plugin/gomark/parser/tokenizer" + "github.com/usememos/memos/plugin/gomark/restore" "github.com/usememos/memos/plugin/webhook" apiv2pb "github.com/usememos/memos/proto/gen/api/v2" storepb "github.com/usememos/memos/proto/gen/store" @@ -232,6 +233,10 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe } } }) + } else if path == "nodes" { + nodes := convertToASTNodes(request.Memo.Nodes) + content := restore.Restore(nodes) + update.Content = &content } else if path == "visibility" { visibility := convertVisibilityToStore(request.Memo.Visibility) update.Visibility = &visibility diff --git a/plugin/gomark/ast/utils.go b/plugin/gomark/ast/utils.go new file mode 100644 index 00000000..58e91e19 --- /dev/null +++ b/plugin/gomark/ast/utils.go @@ -0,0 +1,23 @@ +package ast + +func FindPrevSiblingExceptLineBreak(node Node) Node { + if node == nil { + return nil + } + prev := node.PrevSibling() + if prev != nil && prev.Type() == LineBreakNode { + return FindPrevSiblingExceptLineBreak(prev) + } + return prev +} + +func FindNextSiblingExceptLineBreak(node Node) Node { + if node == nil { + return nil + } + next := node.NextSibling() + if next != nil && next.Type() == LineBreakNode { + return FindNextSiblingExceptLineBreak(next) + } + return next +} diff --git a/plugin/gomark/parser/parser.go b/plugin/gomark/parser/parser.go index 8d0f8a4a..bf67a46f 100644 --- a/plugin/gomark/parser/parser.go +++ b/plugin/gomark/parser/parser.go @@ -49,7 +49,6 @@ func ParseBlock(tokens []*tokenizer.Token) ([]ast.Node, error) { func ParseBlockWithParsers(tokens []*tokenizer.Token, blockParsers []BlockParser) ([]ast.Node, error) { nodes := []ast.Node{} var prevNode ast.Node - var skipNextLineBreakFlag bool for len(tokens) > 0 { for _, blockParser := range blockParsers { size, matched := blockParser.Match(tokens) @@ -59,21 +58,12 @@ func ParseBlockWithParsers(tokens []*tokenizer.Token, blockParsers []BlockParser return nil, errors.New("parse error") } - if node.Type() == ast.LineBreakNode && skipNextLineBreakFlag { - if prevNode != nil && ast.IsBlockNode(prevNode) { - tokens = tokens[size:] - skipNextLineBreakFlag = false - break - } - } - tokens = tokens[size:] if prevNode != nil { prevNode.SetNextSibling(node) node.SetPrevSibling(prevNode) } prevNode = node - skipNextLineBreakFlag = true nodes = append(nodes, node) break } diff --git a/plugin/gomark/parser/parser_test.go b/plugin/gomark/parser/parser_test.go index 197da255..817592d2 100644 --- a/plugin/gomark/parser/parser_test.go +++ b/plugin/gomark/parser/parser_test.go @@ -96,6 +96,7 @@ func TestParser(t *testing.T) { }, }, }, + &ast.LineBreak{}, &ast.Paragraph{ Children: []ast.Node{ &ast.Text{ @@ -126,6 +127,7 @@ func TestParser(t *testing.T) { }, }, }, + &ast.LineBreak{}, &ast.CodeBlock{ Language: "javascript", Content: "console.log(\"Hello world!\");", @@ -143,6 +145,7 @@ func TestParser(t *testing.T) { }, }, &ast.LineBreak{}, + &ast.LineBreak{}, &ast.Paragraph{ Children: []ast.Node{ &ast.Text{ @@ -163,6 +166,7 @@ func TestParser(t *testing.T) { }, }, }, + &ast.LineBreak{}, &ast.TaskList{ Symbol: tokenizer.Hyphen, Complete: false, @@ -186,6 +190,7 @@ func TestParser(t *testing.T) { }, }, }, + &ast.LineBreak{}, &ast.TaskList{ Symbol: tokenizer.Hyphen, Complete: true, diff --git a/plugin/gomark/renderer/html/html.go b/plugin/gomark/renderer/html/html.go index 4a4fd8be..538b6f18 100644 --- a/plugin/gomark/renderer/html/html.go +++ b/plugin/gomark/renderer/html/html.go @@ -72,8 +72,19 @@ func (r *HTMLRenderer) RenderNode(node ast.Node) { // RenderNodes renders a slice of AST nodes to HTML. func (r *HTMLRenderer) RenderNodes(nodes []ast.Node) { + var prevNode ast.Node + var skipNextLineBreakFlag bool for _, node := range nodes { + if node.Type() == ast.LineBreakNode && skipNextLineBreakFlag { + if prevNode != nil && ast.IsBlockNode(prevNode) { + skipNextLineBreakFlag = false + continue + } + } + r.RenderNode(node) + prevNode = node + skipNextLineBreakFlag = true } } @@ -111,7 +122,7 @@ func (r *HTMLRenderer) renderHorizontalRule(_ *ast.HorizontalRule) { } func (r *HTMLRenderer) renderBlockquote(node *ast.Blockquote) { - prevSibling, nextSibling := node.PrevSibling(), node.NextSibling() + prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node) if prevSibling == nil || prevSibling.Type() != ast.BlockquoteNode { r.output.WriteString("
") } @@ -122,7 +133,7 @@ func (r *HTMLRenderer) renderBlockquote(node *ast.Blockquote) { } func (r *HTMLRenderer) renderTaskList(node *ast.TaskList) { - prevSibling, nextSibling := node.PrevSibling(), node.NextSibling() + prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node) if prevSibling == nil || prevSibling.Type() != ast.TaskListNode { r.output.WriteString("") } @@ -140,7 +151,7 @@ func (r *HTMLRenderer) renderTaskList(node *ast.TaskList) { } func (r *HTMLRenderer) renderUnorderedList(node *ast.UnorderedList) { - prevSibling, nextSibling := node.PrevSibling(), node.NextSibling() + prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node) if prevSibling == nil || prevSibling.Type() != ast.UnorderedListNode { r.output.WriteString("
") } @@ -153,7 +164,7 @@ func (r *HTMLRenderer) renderUnorderedList(node *ast.UnorderedList) { } func (r *HTMLRenderer) renderOrderedList(node *ast.OrderedList) { - prevSibling, nextSibling := node.PrevSibling(), node.NextSibling() + prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node) if prevSibling == nil || prevSibling.Type() != ast.OrderedListNode { r.output.WriteString("
") } diff --git a/plugin/gomark/renderer/string/string.go b/plugin/gomark/renderer/string/string.go index 8e80a088..546d5e83 100644 --- a/plugin/gomark/renderer/string/string.go +++ b/plugin/gomark/renderer/string/string.go @@ -72,8 +72,19 @@ func (r *StringRenderer) RenderNode(node ast.Node) { // RenderNodes renders a slice of AST nodes to raw string. func (r *StringRenderer) RenderNodes(nodes []ast.Node) { + var prevNode ast.Node + var skipNextLineBreakFlag bool for _, node := range nodes { + if node.Type() == ast.LineBreakNode && skipNextLineBreakFlag { + if prevNode != nil && ast.IsBlockNode(prevNode) { + skipNextLineBreakFlag = false + continue + } + } + r.RenderNode(node) + prevNode = node + skipNextLineBreakFlag = true } } diff --git a/web/src/components/MemoContent/Renderer.tsx b/web/src/components/MemoContent/Renderer.tsx index 2eb0062f..b8357026 100644 --- a/web/src/components/MemoContent/Renderer.tsx +++ b/web/src/components/MemoContent/Renderer.tsx @@ -44,10 +44,11 @@ import Text from "./Text"; import UnorderedList from "./UnorderedList"; interface Props { + index: string; node: Node; } -const Renderer: React.FC
= ({ node }: Props) => { +const Renderer: React.FC = ({ index, node }: Props) => { switch (node.type) { case NodeType.LINE_BREAK: return ; @@ -66,7 +67,7 @@ const Renderer: React.FC = ({ node }: Props) => { case NodeType.UNORDERED_LIST: return ; case NodeType.TASK_LIST: - return ; + return ; case NodeType.MATH_BLOCK: return ; case NodeType.TEXT: diff --git a/web/src/components/MemoContent/TaskList.tsx b/web/src/components/MemoContent/TaskList.tsx index 7774bfe6..e2c36b3f 100644 --- a/web/src/components/MemoContent/TaskList.tsx +++ b/web/src/components/MemoContent/TaskList.tsx @@ -1,23 +1,51 @@ import { Checkbox } from "@mui/joy"; -import { Node } from "@/types/proto/api/v2/markdown_service"; +import { useContext } from "react"; +import { useMemoStore } from "@/store/v1"; +import { Node, NodeType } from "@/types/proto/api/v2/markdown_service"; import Renderer from "./Renderer"; +import { RendererContext } from "./types"; interface Props { + index: string; symbol: string; complete: boolean; children: Node[]; } -const TaskList: React.FC = ({ complete, children }: Props) => { +const TaskList: React.FC = ({ index, complete, children }: Props) => { + const context = useContext(RendererContext); + const memoStore = useMemoStore(); + + const handleCheckboxChange = async (on: boolean) => { + const nodeIndex = Number(index); + if (isNaN(nodeIndex)) { + return; + } + + const node = context.nodes[nodeIndex]; + if (node.type !== NodeType.TASK_LIST || !node.taskListNode) { + return; + } + + node.taskListNode!.complete = on; + await memoStore.updateMemo( + { + id: context.memoId, + nodes: context.nodes, + }, + ["nodes"] + ); + }; + return (
- diff --git a/web/src/components/MemoContent/index.tsx b/web/src/components/MemoContent/index.tsx index a3f11487..764bf9cb 100644 --- a/web/src/components/MemoContent/index.tsx +++ b/web/src/components/MemoContent/index.tsx @@ -1,16 +1,24 @@ import { useRef } from "react"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { useMemoStore } from "@/store/v1"; import { Node } from "@/types/proto/api/v2/markdown_service"; import Renderer from "./Renderer"; +import { RendererContext } from "./types"; interface Props { + memoId: number; nodes: Node[]; + readonly?: boolean; className?: string; onMemoContentClick?: (e: React.MouseEvent) => void; } const MemoContent: React.FC
-+ handleCheckboxChange(e.target.checked)} /> - {children.map((child, index) => ( -+ {children.map((child, subIndex) => ( + ))} = (props: Props) => { - const { className, onMemoContentClick } = props; + const { className, memoId, nodes, onMemoContentClick } = props; + const currentUser = useCurrentUser(); + const memoStore = useMemoStore(); const memoContentContainerRef = useRef (null); + const allowEdit = currentUser?.id === memoStore.getMemoById(memoId)?.creatorId && !props.readonly; const handleMemoContentClick = async (e: React.MouseEvent) => { if (onMemoContentClick) { @@ -19,17 +27,25 @@ const MemoContent: React.FC = (props: Props) => { }; return ( - --- {props.nodes.map((node, index) => ( -+ ); }; diff --git a/web/src/components/MemoContent/types/context.ts b/web/src/components/MemoContent/types/context.ts index da10b091..cf678c6c 100644 --- a/web/src/components/MemoContent/types/context.ts +++ b/web/src/components/MemoContent/types/context.ts @@ -1 +1,14 @@ -export interface RendererContext {} +import { createContext } from "react"; +import { UNKNOWN_ID } from "@/helpers/consts"; +import { Node } from "@/types/proto/api/v2/markdown_service"; + +interface Context { + memoId: number; + nodes: Node[]; + readonly?: boolean; +} + +export const RendererContext = createContext- ))} + + +-+ {nodes.map((node, index) => ( ++ ))} + ({ + memoId: UNKNOWN_ID, + nodes: [], +}); diff --git a/web/src/components/MemoView.tsx b/web/src/components/MemoView.tsx index 2a31560a..02f05b13 100644 --- a/web/src/components/MemoView.tsx +++ b/web/src/components/MemoView.tsx @@ -251,7 +251,7 @@ const MemoView: React.FC = (props: Props) => { )} + diff --git a/web/src/components/ShareMemoDialog.tsx b/web/src/components/ShareMemoDialog.tsx index 4514cf8f..a5fc4f7c 100644 --- a/web/src/components/ShareMemoDialog.tsx +++ b/web/src/components/ShareMemoDialog.tsx @@ -100,7 +100,7 @@ const ShareMemoDialog: React.FC = (props: Props) => { > {getDateTimeString(memo.displayTime)} -+ diff --git a/web/src/components/TimelineMemo.tsx b/web/src/components/TimelineMemo.tsx index 3b5624f6..33074d16 100644 --- a/web/src/components/TimelineMemo.tsx +++ b/web/src/components/TimelineMemo.tsx @@ -18,7 +18,7 @@ const TimelineMemo = (props: Props) => {diff --git a/web/src/pages/Archived.tsx b/web/src/pages/Archived.tsx index 4562ebb6..b25a0182 100644 --- a/web/src/pages/Archived.tsx +++ b/web/src/pages/Archived.tsx @@ -105,7 +105,7 @@ const Archived = () => { -{getTimeString(memo.displayTime)}-+ + ))} diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 21b59e58..7548a48f 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -139,7 +139,7 @@ const MemoDetail = () => { )} - +