mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: implement task list parser
This commit is contained in:
@ -87,3 +87,16 @@ type UnorderedList struct {
|
||||
func (*UnorderedList) Type() NodeType {
|
||||
return UnorderedListNode
|
||||
}
|
||||
|
||||
type TaskList struct {
|
||||
BaseBlock
|
||||
|
||||
// Symbol is "*" or "-" or "+".
|
||||
Symbol string
|
||||
Complete bool
|
||||
Children []Node
|
||||
}
|
||||
|
||||
func (*TaskList) Type() NodeType {
|
||||
return TaskListNode
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ const (
|
||||
BlockquoteNode
|
||||
OrderedListNode
|
||||
UnorderedListNode
|
||||
TaskListNode
|
||||
// Inline nodes.
|
||||
TextNode
|
||||
BoldNode
|
||||
|
@ -25,21 +25,22 @@ type BlockParser interface {
|
||||
BaseParser
|
||||
}
|
||||
|
||||
func Parse(tokens []*tokenizer.Token) ([]ast.Node, error) {
|
||||
return ParseBlock(tokens)
|
||||
}
|
||||
|
||||
var defaultBlockParsers = []BlockParser{
|
||||
NewCodeBlockParser(),
|
||||
NewHorizontalRuleParser(),
|
||||
NewHeadingParser(),
|
||||
NewBlockquoteParser(),
|
||||
NewTaskListParser(),
|
||||
NewUnorderedListParser(),
|
||||
NewOrderedListParser(),
|
||||
NewParagraphParser(),
|
||||
NewLineBreakParser(),
|
||||
}
|
||||
|
||||
func Parse(tokens []*tokenizer.Token) ([]ast.Node, error) {
|
||||
return ParseBlock(tokens)
|
||||
}
|
||||
|
||||
func ParseBlock(tokens []*tokenizer.Token) ([]ast.Node, error) {
|
||||
return ParseBlockWithParsers(tokens, defaultBlockParsers)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -151,6 +152,51 @@ func TestParser(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "1. hello\n- [ ] world",
|
||||
nodes: []ast.Node{
|
||||
&ast.OrderedList{
|
||||
Number: "1",
|
||||
Children: []ast.Node{
|
||||
&ast.Text{
|
||||
Content: "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
&ast.TaskList{
|
||||
Symbol: tokenizer.Hyphen,
|
||||
Complete: false,
|
||||
Children: []ast.Node{
|
||||
&ast.Text{
|
||||
Content: "world",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "- [ ] hello\n- [x] world",
|
||||
nodes: []ast.Node{
|
||||
&ast.TaskList{
|
||||
Symbol: tokenizer.Hyphen,
|
||||
Complete: false,
|
||||
Children: []ast.Node{
|
||||
&ast.Text{
|
||||
Content: "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
&ast.TaskList{
|
||||
Symbol: tokenizer.Hyphen,
|
||||
Complete: true,
|
||||
Children: []ast.Node{
|
||||
&ast.Text{
|
||||
Content: "world",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@ -184,6 +230,12 @@ func StringifyNode(node ast.Node) string {
|
||||
return "HorizontalRule(" + n.Symbol + ")"
|
||||
case *ast.Blockquote:
|
||||
return "Blockquote(" + StringifyNodes(n.Children) + ")"
|
||||
case *ast.OrderedList:
|
||||
return "OrderedList(" + n.Number + ", " + StringifyNodes(n.Children) + ")"
|
||||
case *ast.UnorderedList:
|
||||
return "UnorderedList(" + n.Symbol + ", " + StringifyNodes(n.Children) + ")"
|
||||
case *ast.TaskList:
|
||||
return "TaskList(" + n.Symbol + ", " + strconv.FormatBool(n.Complete) + ", " + StringifyNodes(n.Children) + ")"
|
||||
case *ast.Text:
|
||||
return "Text(" + n.Content + ")"
|
||||
case *ast.Bold:
|
||||
@ -202,6 +254,8 @@ func StringifyNode(node ast.Node) string {
|
||||
return "Tag(" + n.Content + ")"
|
||||
case *ast.Strikethrough:
|
||||
return "Strikethrough(" + n.Content + ")"
|
||||
case *ast.EscapingCharacter:
|
||||
return "EscapingCharacter(" + n.Symbol + ")"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
69
plugin/gomark/parser/task_list.go
Normal file
69
plugin/gomark/parser/task_list.go
Normal file
@ -0,0 +1,69 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/usememos/memos/plugin/gomark/ast"
|
||||
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||
)
|
||||
|
||||
type TaskListParser struct{}
|
||||
|
||||
func NewTaskListParser() *TaskListParser {
|
||||
return &TaskListParser{}
|
||||
}
|
||||
|
||||
func (*TaskListParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||
if len(tokens) < 7 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
symbolToken := tokens[0]
|
||||
if symbolToken.Type != tokenizer.Hyphen && symbolToken.Type != tokenizer.Asterisk && symbolToken.Type != tokenizer.PlusSign {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[1].Type != tokenizer.Space {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[2].Type != tokenizer.LeftSquareBracket || (tokens[3].Type != tokenizer.Space && tokens[3].Value != "x") || tokens[4].Type != tokenizer.RightSquareBracket {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[5].Type != tokenizer.Space {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
contentTokens := []*tokenizer.Token{}
|
||||
for _, token := range tokens[6:] {
|
||||
contentTokens = append(contentTokens, token)
|
||||
if token.Type == tokenizer.Newline {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(contentTokens) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return len(contentTokens) + 6, true
|
||||
}
|
||||
|
||||
func (p *TaskListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
|
||||
size, ok := p.Match(tokens)
|
||||
if size == 0 || !ok {
|
||||
return nil, errors.New("not matched")
|
||||
}
|
||||
|
||||
symbolToken := tokens[0]
|
||||
contentTokens := tokens[6:size]
|
||||
if contentTokens[len(contentTokens)-1].Type == tokenizer.Newline {
|
||||
contentTokens = contentTokens[:len(contentTokens)-1]
|
||||
}
|
||||
children, err := ParseInline(contentTokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ast.TaskList{
|
||||
Symbol: symbolToken.Type,
|
||||
Complete: tokens[3].Value == "x",
|
||||
Children: children,
|
||||
}, nil
|
||||
}
|
57
plugin/gomark/parser/task_list_test.go
Normal file
57
plugin/gomark/parser/task_list_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/usememos/memos/plugin/gomark/ast"
|
||||
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||
)
|
||||
|
||||
func TestTaskListParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
text string
|
||||
node ast.Node
|
||||
}{
|
||||
{
|
||||
text: "*asd",
|
||||
node: nil,
|
||||
},
|
||||
{
|
||||
text: "+ [ ] Hello World",
|
||||
node: &ast.TaskList{
|
||||
Symbol: tokenizer.PlusSign,
|
||||
Complete: false,
|
||||
Children: []ast.Node{
|
||||
&ast.Text{
|
||||
Content: "Hello World",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "* [x] **Hello**",
|
||||
node: &ast.TaskList{
|
||||
Symbol: tokenizer.Asterisk,
|
||||
Complete: true,
|
||||
Children: []ast.Node{
|
||||
&ast.Bold{
|
||||
Symbol: "*",
|
||||
Children: []ast.Node{
|
||||
&ast.Text{
|
||||
Content: "Hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tokens := tokenizer.Tokenize(test.text)
|
||||
node, _ := NewTaskListParser().Parse(tokens)
|
||||
require.Equal(t, StringifyNodes([]ast.Node{test.node}), StringifyNodes([]ast.Node{node}))
|
||||
}
|
||||
}
|
@ -43,6 +43,8 @@ func (r *HTMLRenderer) RenderNode(node ast.Node) {
|
||||
r.renderUnorderedList(n)
|
||||
case *ast.OrderedList:
|
||||
r.renderOrderedList(n)
|
||||
case *ast.TaskList:
|
||||
r.renderTaskList(n)
|
||||
case *ast.Bold:
|
||||
r.renderBold(n)
|
||||
case *ast.Italic:
|
||||
@ -119,6 +121,24 @@ func (r *HTMLRenderer) renderBlockquote(node *ast.Blockquote) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderTaskList(node *ast.TaskList) {
|
||||
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
|
||||
if prevSibling == nil || prevSibling.Type() != ast.TaskListNode {
|
||||
r.output.WriteString("<ul>")
|
||||
}
|
||||
r.output.WriteString("<li>")
|
||||
r.output.WriteString("<input type=\"checkbox\"")
|
||||
if node.Complete {
|
||||
r.output.WriteString(" checked")
|
||||
}
|
||||
r.output.WriteString(" disabled>")
|
||||
r.RenderNodes(node.Children)
|
||||
r.output.WriteString("</li>")
|
||||
if nextSibling == nil || nextSibling.Type() != ast.TaskListNode {
|
||||
r.output.WriteString("</ul>")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderUnorderedList(node *ast.UnorderedList) {
|
||||
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
|
||||
if prevSibling == nil || prevSibling.Type() != ast.UnorderedListNode {
|
||||
|
@ -54,6 +54,14 @@ func TestHTMLRenderer(t *testing.T) {
|
||||
text: "1. Hello\n2. world\n* !",
|
||||
expected: `<ol><li>Hello</li><li>world</li></ol><ul><li>!</li></ul>`,
|
||||
},
|
||||
{
|
||||
text: "- [ ] hello\n- [x] world",
|
||||
expected: `<ul><li><input type="checkbox" disabled>hello</li><li><input type="checkbox" checked disabled>world</li></ul>`,
|
||||
},
|
||||
{
|
||||
text: "1. ordered\n* unorder\n- [ ] checkbox\n- [x] checked",
|
||||
expected: `<ol><li>ordered</li></ol><ul><li>unorder</li></ul><ul><li><input type="checkbox" disabled>checkbox</li><li><input type="checkbox" checked disabled>checked</li></ul>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
@ -39,6 +39,8 @@ func (r *StringRenderer) RenderNode(node ast.Node) {
|
||||
r.renderHorizontalRule(n)
|
||||
case *ast.Blockquote:
|
||||
r.renderBlockquote(n)
|
||||
case *ast.TaskList:
|
||||
r.renderTaskList(n)
|
||||
case *ast.UnorderedList:
|
||||
r.renderUnorderedList(n)
|
||||
case *ast.OrderedList:
|
||||
@ -109,6 +111,12 @@ func (r *StringRenderer) renderBlockquote(node *ast.Blockquote) {
|
||||
r.output.WriteString("\n")
|
||||
}
|
||||
|
||||
func (r *StringRenderer) renderTaskList(node *ast.TaskList) {
|
||||
r.output.WriteString(node.Symbol)
|
||||
r.RenderNodes(node.Children)
|
||||
r.output.WriteString("\n")
|
||||
}
|
||||
|
||||
func (r *StringRenderer) renderUnorderedList(node *ast.UnorderedList) {
|
||||
r.output.WriteString(node.Symbol)
|
||||
r.RenderNodes(node.Children)
|
||||
|
Reference in New Issue
Block a user