Skip to content

Commit 454cd4e

Browse files
committed
feat: implement switchable task list node
1 parent 6320d04 commit 454cd4e

File tree

16 files changed

+208
-38
lines changed

16 files changed

+208
-38
lines changed

api/v2/markdown_service.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,73 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node {
9595
return node
9696
}
9797

98+
func convertToASTNodes(nodes []*apiv2pb.Node) []ast.Node {
99+
rawNodes := []ast.Node{}
100+
for _, node := range nodes {
101+
rawNode := convertToASTNode(node)
102+
rawNodes = append(rawNodes, rawNode)
103+
}
104+
return rawNodes
105+
}
106+
107+
func convertToASTNode(node *apiv2pb.Node) ast.Node {
108+
switch n := node.Node.(type) {
109+
case *apiv2pb.Node_LineBreakNode:
110+
return &ast.LineBreak{}
111+
case *apiv2pb.Node_ParagraphNode:
112+
children := convertToASTNodes(n.ParagraphNode.Children)
113+
return &ast.Paragraph{Children: children}
114+
case *apiv2pb.Node_CodeBlockNode:
115+
return &ast.CodeBlock{Language: n.CodeBlockNode.Language, Content: n.CodeBlockNode.Content}
116+
case *apiv2pb.Node_HeadingNode:
117+
children := convertToASTNodes(n.HeadingNode.Children)
118+
return &ast.Heading{Level: int(n.HeadingNode.Level), Children: children}
119+
case *apiv2pb.Node_HorizontalRuleNode:
120+
return &ast.HorizontalRule{Symbol: n.HorizontalRuleNode.Symbol}
121+
case *apiv2pb.Node_BlockquoteNode:
122+
children := convertToASTNodes(n.BlockquoteNode.Children)
123+
return &ast.Blockquote{Children: children}
124+
case *apiv2pb.Node_OrderedListNode:
125+
children := convertToASTNodes(n.OrderedListNode.Children)
126+
return &ast.OrderedList{Number: n.OrderedListNode.Number, Children: children}
127+
case *apiv2pb.Node_UnorderedListNode:
128+
children := convertToASTNodes(n.UnorderedListNode.Children)
129+
return &ast.UnorderedList{Symbol: n.UnorderedListNode.Symbol, Children: children}
130+
case *apiv2pb.Node_TaskListNode:
131+
children := convertToASTNodes(n.TaskListNode.Children)
132+
return &ast.TaskList{Symbol: n.TaskListNode.Symbol, Complete: n.TaskListNode.Complete, Children: children}
133+
case *apiv2pb.Node_MathBlockNode:
134+
return &ast.MathBlock{Content: n.MathBlockNode.Content}
135+
case *apiv2pb.Node_TextNode:
136+
return &ast.Text{Content: n.TextNode.Content}
137+
case *apiv2pb.Node_BoldNode:
138+
children := convertToASTNodes(n.BoldNode.Children)
139+
return &ast.Bold{Symbol: n.BoldNode.Symbol, Children: children}
140+
case *apiv2pb.Node_ItalicNode:
141+
return &ast.Italic{Symbol: n.ItalicNode.Symbol, Content: n.ItalicNode.Content}
142+
case *apiv2pb.Node_BoldItalicNode:
143+
return &ast.BoldItalic{Symbol: n.BoldItalicNode.Symbol, Content: n.BoldItalicNode.Content}
144+
case *apiv2pb.Node_CodeNode:
145+
return &ast.Code{Content: n.CodeNode.Content}
146+
case *apiv2pb.Node_ImageNode:
147+
return &ast.Image{AltText: n.ImageNode.AltText, URL: n.ImageNode.Url}
148+
case *apiv2pb.Node_LinkNode:
149+
return &ast.Link{Text: n.LinkNode.Text, URL: n.LinkNode.Url}
150+
case *apiv2pb.Node_AutoLinkNode:
151+
return &ast.AutoLink{URL: n.AutoLinkNode.Url}
152+
case *apiv2pb.Node_TagNode:
153+
return &ast.Tag{Content: n.TagNode.Content}
154+
case *apiv2pb.Node_StrikethroughNode:
155+
return &ast.Strikethrough{Content: n.StrikethroughNode.Content}
156+
case *apiv2pb.Node_EscapingCharacterNode:
157+
return &ast.EscapingCharacter{Symbol: n.EscapingCharacterNode.Symbol}
158+
case *apiv2pb.Node_MathNode:
159+
return &ast.Math{Content: n.MathNode.Content}
160+
default:
161+
return &ast.Text{}
162+
}
163+
}
164+
98165
func traverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
99166
for _, node := range nodes {
100167
fn(node)

api/v2/memo_service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/usememos/memos/plugin/gomark/ast"
2020
"github.com/usememos/memos/plugin/gomark/parser"
2121
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
22+
"github.com/usememos/memos/plugin/gomark/restore"
2223
"github.com/usememos/memos/plugin/webhook"
2324
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
2425
storepb "github.com/usememos/memos/proto/gen/store"
@@ -232,6 +233,10 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
232233
}
233234
}
234235
})
236+
} else if path == "nodes" {
237+
nodes := convertToASTNodes(request.Memo.Nodes)
238+
content := restore.Restore(nodes)
239+
update.Content = &content
235240
} else if path == "visibility" {
236241
visibility := convertVisibilityToStore(request.Memo.Visibility)
237242
update.Visibility = &visibility

plugin/gomark/ast/utils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ast
2+
3+
func FindPrevSiblingExceptLineBreak(node Node) Node {
4+
if node == nil {
5+
return nil
6+
}
7+
prev := node.PrevSibling()
8+
if prev != nil && prev.Type() == LineBreakNode {
9+
return FindPrevSiblingExceptLineBreak(prev)
10+
}
11+
return prev
12+
}
13+
14+
func FindNextSiblingExceptLineBreak(node Node) Node {
15+
if node == nil {
16+
return nil
17+
}
18+
next := node.NextSibling()
19+
if next != nil && next.Type() == LineBreakNode {
20+
return FindNextSiblingExceptLineBreak(next)
21+
}
22+
return next
23+
}

plugin/gomark/parser/parser.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ func ParseBlock(tokens []*tokenizer.Token) ([]ast.Node, error) {
4949
func ParseBlockWithParsers(tokens []*tokenizer.Token, blockParsers []BlockParser) ([]ast.Node, error) {
5050
nodes := []ast.Node{}
5151
var prevNode ast.Node
52-
var skipNextLineBreakFlag bool
5352
for len(tokens) > 0 {
5453
for _, blockParser := range blockParsers {
5554
size, matched := blockParser.Match(tokens)
@@ -59,21 +58,12 @@ func ParseBlockWithParsers(tokens []*tokenizer.Token, blockParsers []BlockParser
5958
return nil, errors.New("parse error")
6059
}
6160

62-
if node.Type() == ast.LineBreakNode && skipNextLineBreakFlag {
63-
if prevNode != nil && ast.IsBlockNode(prevNode) {
64-
tokens = tokens[size:]
65-
skipNextLineBreakFlag = false
66-
break
67-
}
68-
}
69-
7061
tokens = tokens[size:]
7162
if prevNode != nil {
7263
prevNode.SetNextSibling(node)
7364
node.SetPrevSibling(prevNode)
7465
}
7566
prevNode = node
76-
skipNextLineBreakFlag = true
7767
nodes = append(nodes, node)
7868
break
7969
}

plugin/gomark/parser/parser_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ func TestParser(t *testing.T) {
9696
},
9797
},
9898
},
99+
&ast.LineBreak{},
99100
&ast.Paragraph{
100101
Children: []ast.Node{
101102
&ast.Text{
@@ -126,6 +127,7 @@ func TestParser(t *testing.T) {
126127
},
127128
},
128129
},
130+
&ast.LineBreak{},
129131
&ast.CodeBlock{
130132
Language: "javascript",
131133
Content: "console.log(\"Hello world!\");",
@@ -143,6 +145,7 @@ func TestParser(t *testing.T) {
143145
},
144146
},
145147
&ast.LineBreak{},
148+
&ast.LineBreak{},
146149
&ast.Paragraph{
147150
Children: []ast.Node{
148151
&ast.Text{
@@ -163,6 +166,7 @@ func TestParser(t *testing.T) {
163166
},
164167
},
165168
},
169+
&ast.LineBreak{},
166170
&ast.TaskList{
167171
Symbol: tokenizer.Hyphen,
168172
Complete: false,
@@ -186,6 +190,7 @@ func TestParser(t *testing.T) {
186190
},
187191
},
188192
},
193+
&ast.LineBreak{},
189194
&ast.TaskList{
190195
Symbol: tokenizer.Hyphen,
191196
Complete: true,

plugin/gomark/renderer/html/html.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,19 @@ func (r *HTMLRenderer) RenderNode(node ast.Node) {
7272

7373
// RenderNodes renders a slice of AST nodes to HTML.
7474
func (r *HTMLRenderer) RenderNodes(nodes []ast.Node) {
75+
var prevNode ast.Node
76+
var skipNextLineBreakFlag bool
7577
for _, node := range nodes {
78+
if node.Type() == ast.LineBreakNode && skipNextLineBreakFlag {
79+
if prevNode != nil && ast.IsBlockNode(prevNode) {
80+
skipNextLineBreakFlag = false
81+
continue
82+
}
83+
}
84+
7685
r.RenderNode(node)
86+
prevNode = node
87+
skipNextLineBreakFlag = true
7788
}
7889
}
7990

@@ -111,7 +122,7 @@ func (r *HTMLRenderer) renderHorizontalRule(_ *ast.HorizontalRule) {
111122
}
112123

113124
func (r *HTMLRenderer) renderBlockquote(node *ast.Blockquote) {
114-
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
125+
prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node)
115126
if prevSibling == nil || prevSibling.Type() != ast.BlockquoteNode {
116127
r.output.WriteString("<blockquote>")
117128
}
@@ -122,7 +133,7 @@ func (r *HTMLRenderer) renderBlockquote(node *ast.Blockquote) {
122133
}
123134

124135
func (r *HTMLRenderer) renderTaskList(node *ast.TaskList) {
125-
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
136+
prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node)
126137
if prevSibling == nil || prevSibling.Type() != ast.TaskListNode {
127138
r.output.WriteString("<ul>")
128139
}
@@ -140,7 +151,7 @@ func (r *HTMLRenderer) renderTaskList(node *ast.TaskList) {
140151
}
141152

142153
func (r *HTMLRenderer) renderUnorderedList(node *ast.UnorderedList) {
143-
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
154+
prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node)
144155
if prevSibling == nil || prevSibling.Type() != ast.UnorderedListNode {
145156
r.output.WriteString("<ul>")
146157
}
@@ -153,7 +164,7 @@ func (r *HTMLRenderer) renderUnorderedList(node *ast.UnorderedList) {
153164
}
154165

155166
func (r *HTMLRenderer) renderOrderedList(node *ast.OrderedList) {
156-
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
167+
prevSibling, nextSibling := ast.FindPrevSiblingExceptLineBreak(node), ast.FindNextSiblingExceptLineBreak(node)
157168
if prevSibling == nil || prevSibling.Type() != ast.OrderedListNode {
158169
r.output.WriteString("<ol>")
159170
}

plugin/gomark/renderer/string/string.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,19 @@ func (r *StringRenderer) RenderNode(node ast.Node) {
7272

7373
// RenderNodes renders a slice of AST nodes to raw string.
7474
func (r *StringRenderer) RenderNodes(nodes []ast.Node) {
75+
var prevNode ast.Node
76+
var skipNextLineBreakFlag bool
7577
for _, node := range nodes {
78+
if node.Type() == ast.LineBreakNode && skipNextLineBreakFlag {
79+
if prevNode != nil && ast.IsBlockNode(prevNode) {
80+
skipNextLineBreakFlag = false
81+
continue
82+
}
83+
}
84+
7685
r.RenderNode(node)
86+
prevNode = node
87+
skipNextLineBreakFlag = true
7788
}
7889
}
7990

web/src/components/MemoContent/Renderer.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ import Text from "./Text";
4444
import UnorderedList from "./UnorderedList";
4545

4646
interface Props {
47+
index: string;
4748
node: Node;
4849
}
4950

50-
const Renderer: React.FC<Props> = ({ node }: Props) => {
51+
const Renderer: React.FC<Props> = ({ index, node }: Props) => {
5152
switch (node.type) {
5253
case NodeType.LINE_BREAK:
5354
return <LineBreak />;
@@ -66,7 +67,7 @@ const Renderer: React.FC<Props> = ({ node }: Props) => {
6667
case NodeType.UNORDERED_LIST:
6768
return <UnorderedList {...(node.unorderedListNode as UnorderedListNode)} />;
6869
case NodeType.TASK_LIST:
69-
return <TaskList {...(node.taskListNode as TaskListNode)} />;
70+
return <TaskList index={index} {...(node.taskListNode as TaskListNode)} />;
7071
case NodeType.MATH_BLOCK:
7172
return <Math {...(node.mathBlockNode as MathNode)} block={true} />;
7273
case NodeType.TEXT:

web/src/components/MemoContent/TaskList.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,51 @@
11
import { Checkbox } from "@mui/joy";
2-
import { Node } from "@/types/proto/api/v2/markdown_service";
2+
import { useContext } from "react";
3+
import { useMemoStore } from "@/store/v1";
4+
import { Node, NodeType } from "@/types/proto/api/v2/markdown_service";
35
import Renderer from "./Renderer";
6+
import { RendererContext } from "./types";
47

58
interface Props {
9+
index: string;
610
symbol: string;
711
complete: boolean;
812
children: Node[];
913
}
1014

11-
const TaskList: React.FC<Props> = ({ complete, children }: Props) => {
15+
const TaskList: React.FC<Props> = ({ index, complete, children }: Props) => {
16+
const context = useContext(RendererContext);
17+
const memoStore = useMemoStore();
18+
19+
const handleCheckboxChange = async (on: boolean) => {
20+
const nodeIndex = Number(index);
21+
if (isNaN(nodeIndex)) {
22+
return;
23+
}
24+
25+
const node = context.nodes[nodeIndex];
26+
if (node.type !== NodeType.TASK_LIST || !node.taskListNode) {
27+
return;
28+
}
29+
30+
node.taskListNode!.complete = on;
31+
await memoStore.updateMemo(
32+
{
33+
id: context.memoId,
34+
nodes: context.nodes,
35+
},
36+
["nodes"]
37+
);
38+
};
39+
1240
return (
1341
<ul>
1442
<li className="grid grid-cols-[24px_1fr] gap-1">
1543
<div className="w-7 h-6 flex justify-center items-center">
16-
<Checkbox size="sm" checked={complete} readOnly />
44+
<Checkbox size="sm" checked={complete} disabled={context.readonly} onChange={(e) => handleCheckboxChange(e.target.checked)} />
1745
</div>
1846
<div>
19-
{children.map((child, index) => (
20-
<Renderer key={`${child.type}-${index}`} node={child} />
47+
{children.map((child, subIndex) => (
48+
<Renderer key={`${child.type}-${subIndex}`} index={`${index}-${subIndex}`} node={child} />
2149
))}
2250
</div>
2351
</li>

0 commit comments

Comments
 (0)