chore: implement task list parser

pull/2629/head
Steven 2 years ago
parent 1c7fb77e05
commit bb42042db4

@ -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 ""
}

@ -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
}

@ -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)

Loading…
Cancel
Save