chore: implement part of nodes

This commit is contained in:
Steven 2023-12-13 09:06:47 +08:00
parent dd83782522
commit b20e0097cf
16 changed files with 227 additions and 142 deletions

View File

@ -9,10 +9,6 @@ type LineBreak struct {
var NodeTypeLineBreak = NewNodeType("LineBreak") var NodeTypeLineBreak = NewNodeType("LineBreak")
func NewLineBreak() *LineBreak {
return &LineBreak{}
}
func (*LineBreak) Type() NodeType { func (*LineBreak) Type() NodeType {
return NodeTypeLineBreak return NodeTypeLineBreak
} }
@ -25,12 +21,6 @@ type Paragraph struct {
var NodeTypeParagraph = NewNodeType("Paragraph") var NodeTypeParagraph = NewNodeType("Paragraph")
func NewParagraph(children []Node) *Paragraph {
return &Paragraph{
Children: children,
}
}
func (*Paragraph) Type() NodeType { func (*Paragraph) Type() NodeType {
return NodeTypeParagraph return NodeTypeParagraph
} }
@ -44,13 +34,19 @@ type CodeBlock struct {
var NodeTypeCodeBlock = NewNodeType("CodeBlock") var NodeTypeCodeBlock = NewNodeType("CodeBlock")
func NewCodeBlock(language, content string) *CodeBlock {
return &CodeBlock{
Language: language,
Content: content,
}
}
func (*CodeBlock) Type() NodeType { func (*CodeBlock) Type() NodeType {
return NodeTypeCodeBlock return NodeTypeCodeBlock
} }
type Heading struct {
BaseBlock
Level int
Children []Node
}
var NodeTypeHeading = NewNodeType("Heading")
func (*Heading) Type() NodeType {
return NodeTypeHeading
}

View File

@ -10,12 +10,6 @@ type Text struct {
var NodeTypeText = NewNodeType("Text") var NodeTypeText = NewNodeType("Text")
func NewText(content string) *Text {
return &Text{
Content: content,
}
}
func (*Text) Type() NodeType { func (*Text) Type() NodeType {
return NodeTypeText return NodeTypeText
} }
@ -30,13 +24,70 @@ type Bold struct {
var NodeTypeBold = NewNodeType("Bold") var NodeTypeBold = NewNodeType("Bold")
func NewBold(symbol, content string) *Bold {
return &Bold{
Symbol: symbol,
Content: content,
}
}
func (*Bold) Type() NodeType { func (*Bold) Type() NodeType {
return NodeTypeBold return NodeTypeBold
} }
type Code struct {
BaseInline
Content string
}
var NodeTypeCode = NewNodeType("Code")
func (*Code) Type() NodeType {
return NodeTypeCode
}
type Image struct {
BaseInline
AltText string
URL string
}
var NodeTypeImage = NewNodeType("Image")
func (*Image) Type() NodeType {
return NodeTypeImage
}
type Link struct {
BaseInline
Text string
URL string
}
var NodeTypeLink = NewNodeType("Link")
func (*Link) Type() NodeType {
return NodeTypeLink
}
type Italic struct {
BaseInline
// Symbol is "*" or "_"
Symbol string
Content string
}
var NodeTypeItalic = NewNodeType("Italic")
func (*Italic) Type() NodeType {
return NodeTypeItalic
}
type Tag struct {
BaseInline
Content string
}
var NodeTypeTag = NewNodeType("Tag")
func (*Tag) Type() NodeType {
return NodeTypeTag
}

View File

@ -44,7 +44,6 @@ func TestBoldParser(t *testing.T) {
for _, test := range tests { for _, test := range tests {
tokens := tokenizer.Tokenize(test.text) tokens := tokenizer.Tokenize(test.text)
parser := NewBoldParser() require.Equal(t, test.bold, NewBoldParser().Parse(tokens))
require.Equal(t, test.bold, parser.Parse(tokens))
} }
} }

View File

@ -1,38 +1,51 @@
package parser package parser
import "github.com/usememos/memos/plugin/gomark/parser/tokenizer" import (
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type CodeParser struct { type CodeParser struct{}
Content string
} var defaultCodeParser = &CodeParser{}
func NewCodeParser() *CodeParser { func NewCodeParser() *CodeParser {
return &CodeParser{} return defaultCodeParser
} }
func (*CodeParser) Match(tokens []*tokenizer.Token) *CodeParser { func (*CodeParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 3 { if len(tokens) < 3 {
return nil return 0, false
} }
if tokens[0].Type != tokenizer.Backtick { if tokens[0].Type != tokenizer.Backtick {
return nil return 0, false
} }
content, matched := "", false contentTokens, matched := []*tokenizer.Token{}, false
for _, token := range tokens[1:] { for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline { if token.Type == tokenizer.Newline {
return nil return 0, false
} }
if token.Type == tokenizer.Backtick { if token.Type == tokenizer.Backtick {
matched = true matched = true
break break
} }
content += token.Value contentTokens = append(contentTokens, token)
} }
if !matched || len(content) == 0 { if !matched || len(contentTokens) == 0 {
return 0, false
}
return len(contentTokens) + 2, true
}
func (p *CodeParser) Parse(tokens []*tokenizer.Token) ast.Node {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil return nil
} }
return &CodeParser{
Content: content, contentTokens := tokens[1 : size-1]
return &ast.Code{
Content: tokenizer.Stringify(contentTokens),
} }
} }

View File

@ -58,7 +58,6 @@ func TestCodeBlockParser(t *testing.T) {
for _, test := range tests { for _, test := range tests {
tokens := tokenizer.Tokenize(test.text) tokens := tokenizer.Tokenize(test.text)
parser := NewCodeBlockParser() require.Equal(t, test.codeBlock, NewCodeBlockParser().Parse(tokens))
require.Equal(t, test.codeBlock, parser.Parse(tokens))
} }
} }

View File

@ -5,13 +5,14 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer" "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
) )
func TestCodeParser(t *testing.T) { func TestCodeParser(t *testing.T) {
tests := []struct { tests := []struct {
text string text string
code *CodeParser code ast.Node
}{ }{
{ {
text: "`Hello world!", text: "`Hello world!",
@ -19,7 +20,7 @@ func TestCodeParser(t *testing.T) {
}, },
{ {
text: "`Hello world!`", text: "`Hello world!`",
code: &CodeParser{ code: &ast.Code{
Content: "Hello world!", Content: "Hello world!",
}, },
}, },
@ -31,7 +32,6 @@ func TestCodeParser(t *testing.T) {
for _, test := range tests { for _, test := range tests {
tokens := tokenizer.Tokenize(test.text) tokens := tokenizer.Tokenize(test.text)
code := NewCodeParser() require.Equal(t, test.code, NewCodeParser().Parse(tokens))
require.Equal(t, test.code, code.Match(tokens))
} }
} }

View File

@ -1,19 +1,17 @@
package parser package parser
import ( import (
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer" "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
) )
type HeadingParser struct { type HeadingParser struct{}
Level int
ContentTokens []*tokenizer.Token
}
func NewHeadingParser() *HeadingParser { func NewHeadingParser() *HeadingParser {
return &HeadingParser{} return &HeadingParser{}
} }
func (*HeadingParser) Match(tokens []*tokenizer.Token) *HeadingParser { 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.Hash {
@ -23,14 +21,14 @@ func (*HeadingParser) Match(tokens []*tokenizer.Token) *HeadingParser {
} }
} }
if len(tokens) <= cursor+1 { if len(tokens) <= cursor+1 {
return nil return 0, false
} }
if tokens[cursor].Type != tokenizer.Space { if tokens[cursor].Type != tokenizer.Space {
return nil return 0, false
} }
level := cursor level := cursor
if level == 0 || level > 6 { if level == 0 || level > 6 {
return nil return 0, false
} }
cursor++ cursor++
@ -43,11 +41,34 @@ func (*HeadingParser) Match(tokens []*tokenizer.Token) *HeadingParser {
cursor++ cursor++
} }
if len(contentTokens) == 0 { if len(contentTokens) == 0 {
return 0, false
}
return cursor, true
}
func (p *HeadingParser) Parse(tokens []*tokenizer.Token) ast.Node {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil return nil
} }
return &HeadingParser{ level := 0
Level: level, for _, token := range tokens {
ContentTokens: contentTokens, if token.Type == tokenizer.Hash {
level++
} else {
break
}
}
contentTokens := tokens[level+1 : size]
children := ParseInline(contentTokens, []InlineParser{
NewBoldParser(),
NewCodeParser(),
NewTextParser(),
})
return &ast.Heading{
Level: level,
Children: children,
} }
} }

View File

@ -5,13 +5,14 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer" "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
) )
func TestHeadingParser(t *testing.T) { func TestHeadingParser(t *testing.T) {
tests := []struct { tests := []struct {
text string text string
heading *HeadingParser heading ast.Node
}{ }{
{ {
text: "*Hello world", text: "*Hello world",
@ -19,48 +20,22 @@ func TestHeadingParser(t *testing.T) {
}, },
{ {
text: "## Hello World", text: "## Hello World",
heading: &HeadingParser{ heading: &ast.Heading{
Level: 2, Level: 2,
ContentTokens: []*tokenizer.Token{ Children: []ast.Node{
{ &ast.Text{
Type: tokenizer.Text, Content: "Hello World",
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "World",
}, },
}, },
}, },
}, },
{ {
text: "# # Hello World", text: "# # Hello World",
heading: &HeadingParser{ heading: &ast.Heading{
Level: 1, Level: 1,
ContentTokens: []*tokenizer.Token{ Children: []ast.Node{
{ &ast.Text{
Type: tokenizer.Hash, Content: "# Hello World",
Value: "#",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "World",
}, },
}, },
}, },
@ -72,16 +47,26 @@ func TestHeadingParser(t *testing.T) {
{ {
text: `# 123 text: `# 123
Hello World`, Hello World`,
heading: &HeadingParser{ heading: &ast.Heading{
Level: 1, Level: 1,
ContentTokens: []*tokenizer.Token{ Children: []ast.Node{
{ &ast.Text{
Type: tokenizer.Text, Content: "123 ",
Value: "123",
}, },
{ },
Type: tokenizer.Space, },
Value: " ", },
{
text: "### **Hello** World",
heading: &ast.Heading{
Level: 3,
Children: []ast.Node{
&ast.Bold{
Symbol: "*",
Content: "Hello",
},
&ast.Text{
Content: " World",
}, },
}, },
}, },
@ -90,7 +75,6 @@ Hello World`,
for _, test := range tests { for _, test := range tests {
tokens := tokenizer.Tokenize(test.text) tokens := tokenizer.Tokenize(test.text)
heading := NewHeadingParser() require.Equal(t, test.heading, NewHeadingParser().Parse(tokens))
require.Equal(t, test.heading, heading.Match(tokens))
} }
} }

View File

@ -1,30 +1,32 @@
package parser package parser
import "github.com/usememos/memos/plugin/gomark/parser/tokenizer" import (
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type ImageParser struct { type ImageParser struct{}
AltText string
URL string var defaultImageParser = &ImageParser{}
}
func NewImageParser() *ImageParser { func NewImageParser() *ImageParser {
return &ImageParser{} return defaultImageParser
} }
func (*ImageParser) Match(tokens []*tokenizer.Token) *ImageParser { func (*ImageParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 5 { if len(tokens) < 5 {
return nil return 0, false
} }
if tokens[0].Type != tokenizer.ExclamationMark { if tokens[0].Type != tokenizer.ExclamationMark {
return nil return 0, false
} }
if tokens[1].Type != tokenizer.LeftSquareBracket { if tokens[1].Type != tokenizer.LeftSquareBracket {
return nil return 0, false
} }
cursor, altText := 2, "" cursor, altText := 2, ""
for ; cursor < len(tokens)-2; cursor++ { for ; cursor < len(tokens)-2; cursor++ {
if tokens[cursor].Type == tokenizer.Newline { if tokens[cursor].Type == tokenizer.Newline {
return nil return 0, false
} }
if tokens[cursor].Type == tokenizer.RightSquareBracket { if tokens[cursor].Type == tokenizer.RightSquareBracket {
break break
@ -32,24 +34,42 @@ func (*ImageParser) Match(tokens []*tokenizer.Token) *ImageParser {
altText += tokens[cursor].Value altText += tokens[cursor].Value
} }
if tokens[cursor+1].Type != tokenizer.LeftParenthesis { if tokens[cursor+1].Type != tokenizer.LeftParenthesis {
return nil return 0, false
} }
matched, url := false, "" cursor += 2
for _, token := range tokens[cursor+2:] { contentTokens, matched := []*tokenizer.Token{}, false
for _, token := range tokens[cursor:] {
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space { if token.Type == tokenizer.Newline || token.Type == tokenizer.Space {
return nil return 0, false
} }
if token.Type == tokenizer.RightParenthesis { if token.Type == tokenizer.RightParenthesis {
matched = true matched = true
break break
} }
url += token.Value contentTokens = append(contentTokens, token)
} }
if !matched || url == "" { if !matched || len(contentTokens) == 0 {
return 0, false
}
return cursor + len(contentTokens) + 1, true
}
func (p *ImageParser) Parse(tokens []*tokenizer.Token) ast.Node {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil return nil
} }
return &ImageParser{
AltText: altText, altTextTokens := []*tokenizer.Token{}
URL: url, for _, token := range tokens[2:] {
if token.Type == tokenizer.RightSquareBracket {
break
}
altTextTokens = append(altTextTokens, token)
}
contentTokens := tokens[2+len(altTextTokens)+2 : size-1]
return &ast.Image{
AltText: tokenizer.Stringify(altTextTokens),
URL: tokenizer.Stringify(contentTokens),
} }
} }

View File

@ -5,17 +5,18 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer" "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
) )
func TestImageParser(t *testing.T) { func TestImageParser(t *testing.T) {
tests := []struct { tests := []struct {
text string text string
image *ImageParser image ast.Node
}{ }{
{ {
text: "![](https://example.com)", text: "![](https://example.com)",
image: &ImageParser{ image: &ast.Image{
AltText: "", AltText: "",
URL: "https://example.com", URL: "https://example.com",
}, },
@ -30,7 +31,7 @@ func TestImageParser(t *testing.T) {
}, },
{ {
text: "![al te](https://example.com)", text: "![al te](https://example.com)",
image: &ImageParser{ image: &ast.Image{
AltText: "al te", AltText: "al te",
URL: "https://example.com", URL: "https://example.com",
}, },
@ -38,6 +39,6 @@ func TestImageParser(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
tokens := tokenizer.Tokenize(test.text) tokens := tokenizer.Tokenize(test.text)
require.Equal(t, test.image, NewImageParser().Match(tokens)) require.Equal(t, test.image, NewImageParser().Parse(tokens))
} }
} }

View File

@ -89,7 +89,6 @@ func TestItalicParser(t *testing.T) {
for _, test := range tests { for _, test := range tests {
tokens := tokenizer.Tokenize(test.text) tokens := tokenizer.Tokenize(test.text)
italic := NewItalicParser() require.Equal(t, test.italic, NewItalicParser().Match(tokens))
require.Equal(t, test.italic, italic.Match(tokens))
} }
} }

View File

@ -29,5 +29,5 @@ func (p *LineBreakParser) Parse(tokens []*tokenizer.Token) ast.Node {
return nil return nil
} }
return ast.NewLineBreak() return &ast.LineBreak{}
} }

View File

@ -42,5 +42,7 @@ func (p *ParagraphParser) Parse(tokens []*tokenizer.Token) ast.Node {
NewBoldParser(), NewBoldParser(),
NewTextParser(), NewTextParser(),
}) })
return ast.NewParagraph(children) return &ast.Paragraph{
Children: children,
}
} }

View File

@ -32,7 +32,6 @@ func TestParagraphParser(t *testing.T) {
for _, test := range tests { for _, test := range tests {
tokens := tokenizer.Tokenize(test.text) tokens := tokenizer.Tokenize(test.text)
parser := NewParagraphParser() require.Equal(t, test.paragraph, NewParagraphParser().Parse(tokens))
require.Equal(t, test.paragraph, parser.Parse(tokens))
} }
} }

View File

@ -89,7 +89,6 @@ func TestParser(t *testing.T) {
for _, test := range tests { for _, test := range tests {
tokens := tokenizer.Tokenize(test.text) tokens := tokenizer.Tokenize(test.text)
nodes := Parse(tokens) require.Equal(t, test.nodes, Parse(tokens))
require.Equal(t, test.nodes, nodes)
} }
} }

View File

@ -24,7 +24,9 @@ func (*TextParser) Match(tokens []*tokenizer.Token) (int, bool) {
func (*TextParser) Parse(tokens []*tokenizer.Token) ast.Node { func (*TextParser) Parse(tokens []*tokenizer.Token) ast.Node {
if len(tokens) == 0 { if len(tokens) == 0 {
return ast.NewText("") return &ast.Text{}
}
return &ast.Text{
Content: tokens[0].String(),
} }
return ast.NewText(tokens[0].String())
} }