mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: implement list nodes
This commit is contained in:
@ -64,3 +64,26 @@ type Blockquote struct {
|
|||||||
func (*Blockquote) Type() NodeType {
|
func (*Blockquote) Type() NodeType {
|
||||||
return BlockquoteNode
|
return BlockquoteNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OrderedList struct {
|
||||||
|
BaseBlock
|
||||||
|
|
||||||
|
Number string
|
||||||
|
Children []Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*OrderedList) Type() NodeType {
|
||||||
|
return OrderedListNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnorderedList struct {
|
||||||
|
BaseBlock
|
||||||
|
|
||||||
|
// Symbol is "*" or "-" or "+".
|
||||||
|
Symbol string
|
||||||
|
Children []Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UnorderedList) Type() NodeType {
|
||||||
|
return UnorderedListNode
|
||||||
|
}
|
||||||
|
@ -11,6 +11,8 @@ const (
|
|||||||
HeadingNode
|
HeadingNode
|
||||||
HorizontalRuleNode
|
HorizontalRuleNode
|
||||||
BlockquoteNode
|
BlockquoteNode
|
||||||
|
OrderedListNode
|
||||||
|
UnorderedListNode
|
||||||
// Inline nodes.
|
// Inline nodes.
|
||||||
TextNode
|
TextNode
|
||||||
BoldNode
|
BoldNode
|
||||||
|
@ -23,7 +23,7 @@ func (*BoldParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
|||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
prefixTokenType := prefixTokens[0].Type
|
prefixTokenType := prefixTokens[0].Type
|
||||||
if prefixTokenType != tokenizer.Asterisk && prefixTokenType != tokenizer.Underline {
|
if prefixTokenType != tokenizer.Asterisk && prefixTokenType != tokenizer.Underscore {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ func (*BoldItalicParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
|||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
prefixTokenType := prefixTokens[0].Type
|
prefixTokenType := prefixTokens[0].Type
|
||||||
if prefixTokenType != tokenizer.Asterisk && prefixTokenType != tokenizer.Underline {
|
if prefixTokenType != tokenizer.Asterisk && prefixTokenType != tokenizer.Underscore {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ func NewHeadingParser() *HeadingParser {
|
|||||||
func (*HeadingParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
func (*HeadingParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||||
cursor := 0
|
cursor := 0
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
if token.Type == tokenizer.Hash {
|
if token.Type == tokenizer.PoundSign {
|
||||||
cursor++
|
cursor++
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
@ -57,7 +57,7 @@ func (p *HeadingParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
|
|||||||
|
|
||||||
level := 0
|
level := 0
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
if token.Type == tokenizer.Hash {
|
if token.Type == tokenizer.PoundSign {
|
||||||
level++
|
level++
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
|
@ -20,7 +20,7 @@ func (*HorizontalRuleParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
|||||||
if tokens[0].Type != tokens[1].Type || tokens[0].Type != tokens[2].Type || tokens[1].Type != tokens[2].Type {
|
if tokens[0].Type != tokens[1].Type || tokens[0].Type != tokens[2].Type || tokens[1].Type != tokens[2].Type {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
if tokens[0].Type != tokenizer.Dash && tokens[0].Type != tokenizer.Underline && tokens[0].Type != tokenizer.Asterisk {
|
if tokens[0].Type != tokenizer.Hyphen && tokens[0].Type != tokenizer.Underscore && tokens[0].Type != tokenizer.Asterisk {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
if len(tokens) > 3 && tokens[3].Type != tokenizer.Newline {
|
if len(tokens) > 3 && tokens[3].Type != tokenizer.Newline {
|
||||||
|
@ -21,7 +21,7 @@ func (*ItalicParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prefixTokens := tokens[:1]
|
prefixTokens := tokens[:1]
|
||||||
if prefixTokens[0].Type != tokenizer.Asterisk && prefixTokens[0].Type != tokenizer.Underline {
|
if prefixTokens[0].Type != tokenizer.Asterisk && prefixTokens[0].Type != tokenizer.Underscore {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
prefixTokenType := prefixTokens[0].Type
|
prefixTokenType := prefixTokens[0].Type
|
||||||
|
54
plugin/gomark/parser/ordered_list.go
Normal file
54
plugin/gomark/parser/ordered_list.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/usememos/memos/plugin/gomark/ast"
|
||||||
|
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderedListParser struct{}
|
||||||
|
|
||||||
|
func NewOrderedListParser() *OrderedListParser {
|
||||||
|
return &OrderedListParser{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*OrderedListParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||||
|
if len(tokens) < 4 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
if tokens[0].Type != tokenizer.Number || tokens[1].Type != tokenizer.Dot || tokens[2].Type != tokenizer.Space {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
contentTokens := []*tokenizer.Token{}
|
||||||
|
for _, token := range tokens[3:] {
|
||||||
|
contentTokens = append(contentTokens, token)
|
||||||
|
if token.Type == tokenizer.Newline {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(contentTokens) == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(contentTokens) + 3, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OrderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
|
||||||
|
size, ok := p.Match(tokens)
|
||||||
|
if size == 0 || !ok {
|
||||||
|
return nil, errors.New("not matched")
|
||||||
|
}
|
||||||
|
|
||||||
|
contentTokens := tokens[3:size]
|
||||||
|
children, err := ParseInline(contentTokens)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ast.OrderedList{
|
||||||
|
Number: tokens[0].Value,
|
||||||
|
Children: children,
|
||||||
|
}, nil
|
||||||
|
}
|
58
plugin/gomark/parser/ordered_list_test.go
Normal file
58
plugin/gomark/parser/ordered_list_test.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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 TestOrderedListParser(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
text string
|
||||||
|
node ast.Node
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
text: "1.asd",
|
||||||
|
node: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "1. Hello World",
|
||||||
|
node: &ast.OrderedList{
|
||||||
|
Number: "1",
|
||||||
|
Children: []ast.Node{
|
||||||
|
&ast.Text{
|
||||||
|
Content: "Hello World",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "1aa. Hello World",
|
||||||
|
node: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "22. Hello *World*",
|
||||||
|
node: &ast.OrderedList{
|
||||||
|
Number: "22",
|
||||||
|
Children: []ast.Node{
|
||||||
|
&ast.Text{
|
||||||
|
Content: "Hello ",
|
||||||
|
},
|
||||||
|
&ast.Italic{
|
||||||
|
Symbol: "*",
|
||||||
|
Content: "World",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tokens := tokenizer.Tokenize(test.text)
|
||||||
|
node, _ := NewOrderedListParser().Parse(tokens)
|
||||||
|
require.Equal(t, StringifyNodes([]ast.Node{test.node}), StringifyNodes([]ast.Node{node}))
|
||||||
|
}
|
||||||
|
}
|
@ -17,12 +17,12 @@ func (*TagParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
|||||||
if len(tokens) < 2 {
|
if len(tokens) < 2 {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
if tokens[0].Type != tokenizer.Hash {
|
if tokens[0].Type != tokenizer.PoundSign {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
contentTokens := []*tokenizer.Token{}
|
contentTokens := []*tokenizer.Token{}
|
||||||
for _, token := range tokens[1:] {
|
for _, token := range tokens[1:] {
|
||||||
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space || token.Type == tokenizer.Hash {
|
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space || token.Type == tokenizer.PoundSign {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
contentTokens = append(contentTokens, token)
|
contentTokens = append(contentTokens, token)
|
||||||
|
@ -3,9 +3,9 @@ package tokenizer
|
|||||||
type TokenType = string
|
type TokenType = string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Underline TokenType = "_"
|
Underscore TokenType = "_"
|
||||||
Asterisk TokenType = "*"
|
Asterisk TokenType = "*"
|
||||||
Hash TokenType = "#"
|
PoundSign TokenType = "#"
|
||||||
Backtick TokenType = "`"
|
Backtick TokenType = "`"
|
||||||
LeftSquareBracket TokenType = "["
|
LeftSquareBracket TokenType = "["
|
||||||
RightSquareBracket TokenType = "]"
|
RightSquareBracket TokenType = "]"
|
||||||
@ -13,14 +13,17 @@ const (
|
|||||||
RightParenthesis TokenType = ")"
|
RightParenthesis TokenType = ")"
|
||||||
ExclamationMark TokenType = "!"
|
ExclamationMark TokenType = "!"
|
||||||
Tilde TokenType = "~"
|
Tilde TokenType = "~"
|
||||||
Dash TokenType = "-"
|
Hyphen TokenType = "-"
|
||||||
|
PlusSign TokenType = "+"
|
||||||
|
Dot TokenType = "."
|
||||||
GreaterThan TokenType = ">"
|
GreaterThan TokenType = ">"
|
||||||
Newline TokenType = "\n"
|
Newline TokenType = "\n"
|
||||||
Space TokenType = " "
|
Space TokenType = " "
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Text TokenType = ""
|
Number TokenType = "number"
|
||||||
|
Text TokenType = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
type Token struct {
|
type Token struct {
|
||||||
@ -40,11 +43,11 @@ func Tokenize(text string) []*Token {
|
|||||||
for _, c := range text {
|
for _, c := range text {
|
||||||
switch c {
|
switch c {
|
||||||
case '_':
|
case '_':
|
||||||
tokens = append(tokens, NewToken(Underline, "_"))
|
tokens = append(tokens, NewToken(Underscore, "_"))
|
||||||
case '*':
|
case '*':
|
||||||
tokens = append(tokens, NewToken(Asterisk, "*"))
|
tokens = append(tokens, NewToken(Asterisk, "*"))
|
||||||
case '#':
|
case '#':
|
||||||
tokens = append(tokens, NewToken(Hash, "#"))
|
tokens = append(tokens, NewToken(PoundSign, "#"))
|
||||||
case '`':
|
case '`':
|
||||||
tokens = append(tokens, NewToken(Backtick, "`"))
|
tokens = append(tokens, NewToken(Backtick, "`"))
|
||||||
case '[':
|
case '[':
|
||||||
@ -60,9 +63,13 @@ func Tokenize(text string) []*Token {
|
|||||||
case '~':
|
case '~':
|
||||||
tokens = append(tokens, NewToken(Tilde, "~"))
|
tokens = append(tokens, NewToken(Tilde, "~"))
|
||||||
case '-':
|
case '-':
|
||||||
tokens = append(tokens, NewToken(Dash, "-"))
|
tokens = append(tokens, NewToken(Hyphen, "-"))
|
||||||
case '>':
|
case '>':
|
||||||
tokens = append(tokens, NewToken(GreaterThan, ">"))
|
tokens = append(tokens, NewToken(GreaterThan, ">"))
|
||||||
|
case '+':
|
||||||
|
tokens = append(tokens, NewToken(PlusSign, "+"))
|
||||||
|
case '.':
|
||||||
|
tokens = append(tokens, NewToken(Dot, "."))
|
||||||
case '\n':
|
case '\n':
|
||||||
tokens = append(tokens, NewToken(Newline, "\n"))
|
tokens = append(tokens, NewToken(Newline, "\n"))
|
||||||
case ' ':
|
case ' ':
|
||||||
@ -72,10 +79,19 @@ func Tokenize(text string) []*Token {
|
|||||||
if len(tokens) > 0 {
|
if len(tokens) > 0 {
|
||||||
prevToken = tokens[len(tokens)-1]
|
prevToken = tokens[len(tokens)-1]
|
||||||
}
|
}
|
||||||
if prevToken == nil || prevToken.Type != Text {
|
|
||||||
tokens = append(tokens, NewToken(Text, string(c)))
|
isNumber := c >= '0' && c <= '9'
|
||||||
|
if prevToken != nil {
|
||||||
|
if (prevToken.Type == Text && !isNumber) || (prevToken.Type == Number && isNumber) {
|
||||||
|
prevToken.Value += string(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isNumber {
|
||||||
|
tokens = append(tokens, NewToken(Number, string(c)))
|
||||||
} else {
|
} else {
|
||||||
prevToken.Value += string(c)
|
tokens = append(tokens, NewToken(Text, string(c)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ func TestTokenize(t *testing.T) {
|
|||||||
world`,
|
world`,
|
||||||
tokens: []*Token{
|
tokens: []*Token{
|
||||||
{
|
{
|
||||||
Type: Hash,
|
Type: PoundSign,
|
||||||
Value: "#",
|
Value: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
55
plugin/gomark/parser/unordered_list.go
Normal file
55
plugin/gomark/parser/unordered_list.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/usememos/memos/plugin/gomark/ast"
|
||||||
|
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnorderedListParser struct{}
|
||||||
|
|
||||||
|
func NewUnorderedListParser() *UnorderedListParser {
|
||||||
|
return &UnorderedListParser{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UnorderedListParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||||
|
if len(tokens) < 3 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
symbolToken := tokens[0]
|
||||||
|
if (symbolToken.Type != tokenizer.Hyphen && symbolToken.Type != tokenizer.Asterisk && symbolToken.Type != tokenizer.PlusSign) || tokens[1].Type != tokenizer.Space {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
contentTokens := []*tokenizer.Token{}
|
||||||
|
for _, token := range tokens[2:] {
|
||||||
|
if token.Type == tokenizer.Newline {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
contentTokens = append(contentTokens, token)
|
||||||
|
}
|
||||||
|
if len(contentTokens) == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(contentTokens) + 2, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnorderedListParser) 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[2:size]
|
||||||
|
children, err := ParseInline(contentTokens)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ast.UnorderedList{
|
||||||
|
Symbol: symbolToken.Type,
|
||||||
|
Children: children,
|
||||||
|
}, nil
|
||||||
|
}
|
51
plugin/gomark/parser/unordered_list_test.go
Normal file
51
plugin/gomark/parser/unordered_list_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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 TestUnorderedListParser(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
text string
|
||||||
|
node ast.Node
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
text: "*asd",
|
||||||
|
node: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "+ Hello World",
|
||||||
|
node: &ast.UnorderedList{
|
||||||
|
Symbol: tokenizer.PlusSign,
|
||||||
|
Children: []ast.Node{
|
||||||
|
&ast.Text{
|
||||||
|
Content: "Hello World",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "* **Hello**",
|
||||||
|
node: &ast.UnorderedList{
|
||||||
|
Symbol: tokenizer.Asterisk,
|
||||||
|
Children: []ast.Node{
|
||||||
|
&ast.Bold{
|
||||||
|
Symbol: "*",
|
||||||
|
Content: "Hello",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tokens := tokenizer.Tokenize(test.text)
|
||||||
|
node, _ := NewUnorderedListParser().Parse(tokens)
|
||||||
|
require.Equal(t, StringifyNodes([]ast.Node{test.node}), StringifyNodes([]ast.Node{node}))
|
||||||
|
}
|
||||||
|
}
|
@ -159,7 +159,7 @@ func (r *HTMLRender) renderLink(node *ast.Link) {
|
|||||||
|
|
||||||
func (r *HTMLRender) renderTag(node *ast.Tag) {
|
func (r *HTMLRender) renderTag(node *ast.Tag) {
|
||||||
r.output.WriteString(`<span>`)
|
r.output.WriteString(`<span>`)
|
||||||
r.output.WriteString(`# `)
|
r.output.WriteString(`#`)
|
||||||
r.output.WriteString(node.Content)
|
r.output.WriteString(node.Content)
|
||||||
r.output.WriteString(`</span>`)
|
r.output.WriteString(`</span>`)
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,10 @@ func TestHTMLRender(t *testing.T) {
|
|||||||
text: "**Hello** world!",
|
text: "**Hello** world!",
|
||||||
expected: `<p><strong>Hello</strong> world!</p>`,
|
expected: `<p><strong>Hello</strong> world!</p>`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "#article #memo",
|
||||||
|
expected: `<p><span>#article</span> <span>#memo</span></p>`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
Reference in New Issue
Block a user