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 {
|
func (*UnorderedList) Type() NodeType {
|
||||||
return UnorderedListNode
|
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
|
BlockquoteNode
|
||||||
OrderedListNode
|
OrderedListNode
|
||||||
UnorderedListNode
|
UnorderedListNode
|
||||||
|
TaskListNode
|
||||||
// Inline nodes.
|
// Inline nodes.
|
||||||
TextNode
|
TextNode
|
||||||
BoldNode
|
BoldNode
|
||||||
|
@ -25,21 +25,22 @@ type BlockParser interface {
|
|||||||
BaseParser
|
BaseParser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Parse(tokens []*tokenizer.Token) ([]ast.Node, error) {
|
||||||
|
return ParseBlock(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
var defaultBlockParsers = []BlockParser{
|
var defaultBlockParsers = []BlockParser{
|
||||||
NewCodeBlockParser(),
|
NewCodeBlockParser(),
|
||||||
NewHorizontalRuleParser(),
|
NewHorizontalRuleParser(),
|
||||||
NewHeadingParser(),
|
NewHeadingParser(),
|
||||||
NewBlockquoteParser(),
|
NewBlockquoteParser(),
|
||||||
|
NewTaskListParser(),
|
||||||
NewUnorderedListParser(),
|
NewUnorderedListParser(),
|
||||||
NewOrderedListParser(),
|
NewOrderedListParser(),
|
||||||
NewParagraphParser(),
|
NewParagraphParser(),
|
||||||
NewLineBreakParser(),
|
NewLineBreakParser(),
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(tokens []*tokenizer.Token) ([]ast.Node, error) {
|
|
||||||
return ParseBlock(tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseBlock(tokens []*tokenizer.Token) ([]ast.Node, error) {
|
func ParseBlock(tokens []*tokenizer.Token) ([]ast.Node, error) {
|
||||||
return ParseBlockWithParsers(tokens, defaultBlockParsers)
|
return ParseBlockWithParsers(tokens, defaultBlockParsers)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"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 {
|
for _, test := range tests {
|
||||||
@ -184,6 +230,12 @@ func StringifyNode(node ast.Node) string {
|
|||||||
return "HorizontalRule(" + n.Symbol + ")"
|
return "HorizontalRule(" + n.Symbol + ")"
|
||||||
case *ast.Blockquote:
|
case *ast.Blockquote:
|
||||||
return "Blockquote(" + StringifyNodes(n.Children) + ")"
|
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:
|
case *ast.Text:
|
||||||
return "Text(" + n.Content + ")"
|
return "Text(" + n.Content + ")"
|
||||||
case *ast.Bold:
|
case *ast.Bold:
|
||||||
@ -202,6 +254,8 @@ func StringifyNode(node ast.Node) string {
|
|||||||
return "Tag(" + n.Content + ")"
|
return "Tag(" + n.Content + ")"
|
||||||
case *ast.Strikethrough:
|
case *ast.Strikethrough:
|
||||||
return "Strikethrough(" + n.Content + ")"
|
return "Strikethrough(" + n.Content + ")"
|
||||||
|
case *ast.EscapingCharacter:
|
||||||
|
return "EscapingCharacter(" + n.Symbol + ")"
|
||||||
}
|
}
|
||||||
return ""
|
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)
|
r.renderUnorderedList(n)
|
||||||
case *ast.OrderedList:
|
case *ast.OrderedList:
|
||||||
r.renderOrderedList(n)
|
r.renderOrderedList(n)
|
||||||
|
case *ast.TaskList:
|
||||||
|
r.renderTaskList(n)
|
||||||
case *ast.Bold:
|
case *ast.Bold:
|
||||||
r.renderBold(n)
|
r.renderBold(n)
|
||||||
case *ast.Italic:
|
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) {
|
func (r *HTMLRenderer) renderUnorderedList(node *ast.UnorderedList) {
|
||||||
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
|
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
|
||||||
if prevSibling == nil || prevSibling.Type() != ast.UnorderedListNode {
|
if prevSibling == nil || prevSibling.Type() != ast.UnorderedListNode {
|
||||||
|
@ -54,6 +54,14 @@ func TestHTMLRenderer(t *testing.T) {
|
|||||||
text: "1. Hello\n2. world\n* !",
|
text: "1. Hello\n2. world\n* !",
|
||||||
expected: `<ol><li>Hello</li><li>world</li></ol><ul><li>!</li></ul>`,
|
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 {
|
for _, test := range tests {
|
||||||
|
@ -39,6 +39,8 @@ func (r *StringRenderer) RenderNode(node ast.Node) {
|
|||||||
r.renderHorizontalRule(n)
|
r.renderHorizontalRule(n)
|
||||||
case *ast.Blockquote:
|
case *ast.Blockquote:
|
||||||
r.renderBlockquote(n)
|
r.renderBlockquote(n)
|
||||||
|
case *ast.TaskList:
|
||||||
|
r.renderTaskList(n)
|
||||||
case *ast.UnorderedList:
|
case *ast.UnorderedList:
|
||||||
r.renderUnorderedList(n)
|
r.renderUnorderedList(n)
|
||||||
case *ast.OrderedList:
|
case *ast.OrderedList:
|
||||||
@ -109,6 +111,12 @@ func (r *StringRenderer) renderBlockquote(node *ast.Blockquote) {
|
|||||||
r.output.WriteString("\n")
|
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) {
|
func (r *StringRenderer) renderUnorderedList(node *ast.UnorderedList) {
|
||||||
r.output.WriteString(node.Symbol)
|
r.output.WriteString(node.Symbol)
|
||||||
r.RenderNodes(node.Children)
|
r.RenderNodes(node.Children)
|
||||||
|
Reference in New Issue
Block a user