diff --git a/plugin/gomark/ast/block.go b/plugin/gomark/ast/block.go index 236db0e9..42bf6f4b 100644 --- a/plugin/gomark/ast/block.go +++ b/plugin/gomark/ast/block.go @@ -9,10 +9,6 @@ type LineBreak struct { var NodeTypeLineBreak = NewNodeType("LineBreak") -func NewLineBreak() *LineBreak { - return &LineBreak{} -} - func (*LineBreak) Type() NodeType { return NodeTypeLineBreak } @@ -25,12 +21,6 @@ type Paragraph struct { var NodeTypeParagraph = NewNodeType("Paragraph") -func NewParagraph(children []Node) *Paragraph { - return &Paragraph{ - Children: children, - } -} - func (*Paragraph) Type() NodeType { return NodeTypeParagraph } @@ -44,13 +34,19 @@ type CodeBlock struct { var NodeTypeCodeBlock = NewNodeType("CodeBlock") -func NewCodeBlock(language, content string) *CodeBlock { - return &CodeBlock{ - Language: language, - Content: content, - } -} - func (*CodeBlock) Type() NodeType { return NodeTypeCodeBlock } + +type Heading struct { + BaseBlock + + Level int + Children []Node +} + +var NodeTypeHeading = NewNodeType("Heading") + +func (*Heading) Type() NodeType { + return NodeTypeHeading +} diff --git a/plugin/gomark/ast/inline.go b/plugin/gomark/ast/inline.go index 6bc230d7..3e4c57e5 100644 --- a/plugin/gomark/ast/inline.go +++ b/plugin/gomark/ast/inline.go @@ -10,12 +10,6 @@ type Text struct { var NodeTypeText = NewNodeType("Text") -func NewText(content string) *Text { - return &Text{ - Content: content, - } -} - func (*Text) Type() NodeType { return NodeTypeText } @@ -30,13 +24,70 @@ type Bold struct { var NodeTypeBold = NewNodeType("Bold") -func NewBold(symbol, content string) *Bold { - return &Bold{ - Symbol: symbol, - Content: content, - } -} - func (*Bold) Type() NodeType { 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 +} diff --git a/plugin/gomark/parser/bold_test.go b/plugin/gomark/parser/bold_test.go index de758d5f..43864e4f 100644 --- a/plugin/gomark/parser/bold_test.go +++ b/plugin/gomark/parser/bold_test.go @@ -44,7 +44,6 @@ func TestBoldParser(t *testing.T) { for _, test := range tests { tokens := tokenizer.Tokenize(test.text) - parser := NewBoldParser() - require.Equal(t, test.bold, parser.Parse(tokens)) + require.Equal(t, test.bold, NewBoldParser().Parse(tokens)) } } diff --git a/plugin/gomark/parser/code.go b/plugin/gomark/parser/code.go index 6eae650c..903b1cf7 100644 --- a/plugin/gomark/parser/code.go +++ b/plugin/gomark/parser/code.go @@ -1,38 +1,51 @@ 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 { - Content string -} +type CodeParser struct{} + +var defaultCodeParser = &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 { - return nil + return 0, false } if tokens[0].Type != tokenizer.Backtick { - return nil + return 0, false } - content, matched := "", false + contentTokens, matched := []*tokenizer.Token{}, false for _, token := range tokens[1:] { if token.Type == tokenizer.Newline { - return nil + return 0, false } if token.Type == tokenizer.Backtick { matched = true 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 &CodeParser{ - Content: content, + + contentTokens := tokens[1 : size-1] + return &ast.Code{ + Content: tokenizer.Stringify(contentTokens), } } diff --git a/plugin/gomark/parser/code_block_test.go b/plugin/gomark/parser/code_block_test.go index c4bcaaae..dca3cc24 100644 --- a/plugin/gomark/parser/code_block_test.go +++ b/plugin/gomark/parser/code_block_test.go @@ -58,7 +58,6 @@ func TestCodeBlockParser(t *testing.T) { for _, test := range tests { tokens := tokenizer.Tokenize(test.text) - parser := NewCodeBlockParser() - require.Equal(t, test.codeBlock, parser.Parse(tokens)) + require.Equal(t, test.codeBlock, NewCodeBlockParser().Parse(tokens)) } } diff --git a/plugin/gomark/parser/code_test.go b/plugin/gomark/parser/code_test.go index 8e4fed41..c1ee6ffd 100644 --- a/plugin/gomark/parser/code_test.go +++ b/plugin/gomark/parser/code_test.go @@ -5,13 +5,14 @@ import ( "github.com/stretchr/testify/require" + "github.com/usememos/memos/plugin/gomark/ast" "github.com/usememos/memos/plugin/gomark/parser/tokenizer" ) func TestCodeParser(t *testing.T) { tests := []struct { text string - code *CodeParser + code ast.Node }{ { text: "`Hello world!", @@ -19,7 +20,7 @@ func TestCodeParser(t *testing.T) { }, { text: "`Hello world!`", - code: &CodeParser{ + code: &ast.Code{ Content: "Hello world!", }, }, @@ -31,7 +32,6 @@ func TestCodeParser(t *testing.T) { for _, test := range tests { tokens := tokenizer.Tokenize(test.text) - code := NewCodeParser() - require.Equal(t, test.code, code.Match(tokens)) + require.Equal(t, test.code, NewCodeParser().Parse(tokens)) } } diff --git a/plugin/gomark/parser/heading.go b/plugin/gomark/parser/heading.go index c9c26323..8becfc5f 100644 --- a/plugin/gomark/parser/heading.go +++ b/plugin/gomark/parser/heading.go @@ -1,19 +1,17 @@ package parser import ( + "github.com/usememos/memos/plugin/gomark/ast" "github.com/usememos/memos/plugin/gomark/parser/tokenizer" ) -type HeadingParser struct { - Level int - ContentTokens []*tokenizer.Token -} +type HeadingParser struct{} func NewHeadingParser() *HeadingParser { return &HeadingParser{} } -func (*HeadingParser) Match(tokens []*tokenizer.Token) *HeadingParser { +func (*HeadingParser) Match(tokens []*tokenizer.Token) (int, bool) { cursor := 0 for _, token := range tokens { if token.Type == tokenizer.Hash { @@ -23,14 +21,14 @@ func (*HeadingParser) Match(tokens []*tokenizer.Token) *HeadingParser { } } if len(tokens) <= cursor+1 { - return nil + return 0, false } if tokens[cursor].Type != tokenizer.Space { - return nil + return 0, false } level := cursor if level == 0 || level > 6 { - return nil + return 0, false } cursor++ @@ -43,11 +41,34 @@ func (*HeadingParser) Match(tokens []*tokenizer.Token) *HeadingParser { cursor++ } 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 &HeadingParser{ - Level: level, - ContentTokens: contentTokens, + level := 0 + for _, token := range tokens { + 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, } } diff --git a/plugin/gomark/parser/heading_test.go b/plugin/gomark/parser/heading_test.go index b9ef4a5e..638d0466 100644 --- a/plugin/gomark/parser/heading_test.go +++ b/plugin/gomark/parser/heading_test.go @@ -5,13 +5,14 @@ import ( "github.com/stretchr/testify/require" + "github.com/usememos/memos/plugin/gomark/ast" "github.com/usememos/memos/plugin/gomark/parser/tokenizer" ) func TestHeadingParser(t *testing.T) { tests := []struct { text string - heading *HeadingParser + heading ast.Node }{ { text: "*Hello world", @@ -19,48 +20,22 @@ func TestHeadingParser(t *testing.T) { }, { text: "## Hello World", - heading: &HeadingParser{ + heading: &ast.Heading{ Level: 2, - ContentTokens: []*tokenizer.Token{ - { - Type: tokenizer.Text, - Value: "Hello", - }, - { - Type: tokenizer.Space, - Value: " ", - }, - { - Type: tokenizer.Text, - Value: "World", + Children: []ast.Node{ + &ast.Text{ + Content: "Hello World", }, }, }, }, { text: "# # Hello World", - heading: &HeadingParser{ + heading: &ast.Heading{ Level: 1, - ContentTokens: []*tokenizer.Token{ - { - Type: tokenizer.Hash, - Value: "#", - }, - { - Type: tokenizer.Space, - Value: " ", - }, - { - Type: tokenizer.Text, - Value: "Hello", - }, - { - Type: tokenizer.Space, - Value: " ", - }, - { - Type: tokenizer.Text, - Value: "World", + Children: []ast.Node{ + &ast.Text{ + Content: "# Hello World", }, }, }, @@ -72,16 +47,26 @@ func TestHeadingParser(t *testing.T) { { text: `# 123 Hello World`, - heading: &HeadingParser{ + heading: &ast.Heading{ Level: 1, - ContentTokens: []*tokenizer.Token{ - { - Type: tokenizer.Text, - Value: "123", + Children: []ast.Node{ + &ast.Text{ + Content: "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 { tokens := tokenizer.Tokenize(test.text) - heading := NewHeadingParser() - require.Equal(t, test.heading, heading.Match(tokens)) + require.Equal(t, test.heading, NewHeadingParser().Parse(tokens)) } } diff --git a/plugin/gomark/parser/image.go b/plugin/gomark/parser/image.go index def7d2af..71398120 100644 --- a/plugin/gomark/parser/image.go +++ b/plugin/gomark/parser/image.go @@ -1,30 +1,32 @@ 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 { - AltText string - URL string -} +type ImageParser struct{} + +var defaultImageParser = &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 { - return nil + return 0, false } if tokens[0].Type != tokenizer.ExclamationMark { - return nil + return 0, false } if tokens[1].Type != tokenizer.LeftSquareBracket { - return nil + return 0, false } cursor, altText := 2, "" for ; cursor < len(tokens)-2; cursor++ { if tokens[cursor].Type == tokenizer.Newline { - return nil + return 0, false } if tokens[cursor].Type == tokenizer.RightSquareBracket { break @@ -32,24 +34,42 @@ func (*ImageParser) Match(tokens []*tokenizer.Token) *ImageParser { altText += tokens[cursor].Value } if tokens[cursor+1].Type != tokenizer.LeftParenthesis { - return nil + return 0, false } - matched, url := false, "" - for _, token := range tokens[cursor+2:] { + cursor += 2 + contentTokens, matched := []*tokenizer.Token{}, false + for _, token := range tokens[cursor:] { if token.Type == tokenizer.Newline || token.Type == tokenizer.Space { - return nil + return 0, false } if token.Type == tokenizer.RightParenthesis { matched = true 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 &ImageParser{ - AltText: altText, - URL: url, + + altTextTokens := []*tokenizer.Token{} + 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), } } diff --git a/plugin/gomark/parser/image_test.go b/plugin/gomark/parser/image_test.go index cdcbfa2a..176030fc 100644 --- a/plugin/gomark/parser/image_test.go +++ b/plugin/gomark/parser/image_test.go @@ -5,17 +5,18 @@ import ( "github.com/stretchr/testify/require" + "github.com/usememos/memos/plugin/gomark/ast" "github.com/usememos/memos/plugin/gomark/parser/tokenizer" ) func TestImageParser(t *testing.T) { tests := []struct { text string - image *ImageParser + image ast.Node }{ { text: "![](https://example.com)", - image: &ImageParser{ + image: &ast.Image{ AltText: "", URL: "https://example.com", }, @@ -30,7 +31,7 @@ func TestImageParser(t *testing.T) { }, { text: "![al te](https://example.com)", - image: &ImageParser{ + image: &ast.Image{ AltText: "al te", URL: "https://example.com", }, @@ -38,6 +39,6 @@ func TestImageParser(t *testing.T) { } for _, test := range tests { tokens := tokenizer.Tokenize(test.text) - require.Equal(t, test.image, NewImageParser().Match(tokens)) + require.Equal(t, test.image, NewImageParser().Parse(tokens)) } } diff --git a/plugin/gomark/parser/italic_test.go b/plugin/gomark/parser/italic_test.go index 116616bf..76893123 100644 --- a/plugin/gomark/parser/italic_test.go +++ b/plugin/gomark/parser/italic_test.go @@ -89,7 +89,6 @@ func TestItalicParser(t *testing.T) { for _, test := range tests { tokens := tokenizer.Tokenize(test.text) - italic := NewItalicParser() - require.Equal(t, test.italic, italic.Match(tokens)) + require.Equal(t, test.italic, NewItalicParser().Match(tokens)) } } diff --git a/plugin/gomark/parser/line_break.go b/plugin/gomark/parser/line_break.go index 46a833e5..bdc0957d 100644 --- a/plugin/gomark/parser/line_break.go +++ b/plugin/gomark/parser/line_break.go @@ -29,5 +29,5 @@ func (p *LineBreakParser) Parse(tokens []*tokenizer.Token) ast.Node { return nil } - return ast.NewLineBreak() + return &ast.LineBreak{} } diff --git a/plugin/gomark/parser/paragraph.go b/plugin/gomark/parser/paragraph.go index dc413e29..c2403ad0 100644 --- a/plugin/gomark/parser/paragraph.go +++ b/plugin/gomark/parser/paragraph.go @@ -42,5 +42,7 @@ func (p *ParagraphParser) Parse(tokens []*tokenizer.Token) ast.Node { NewBoldParser(), NewTextParser(), }) - return ast.NewParagraph(children) + return &ast.Paragraph{ + Children: children, + } } diff --git a/plugin/gomark/parser/paragraph_test.go b/plugin/gomark/parser/paragraph_test.go index 3bc7c1af..8c3bfe3c 100644 --- a/plugin/gomark/parser/paragraph_test.go +++ b/plugin/gomark/parser/paragraph_test.go @@ -32,7 +32,6 @@ func TestParagraphParser(t *testing.T) { for _, test := range tests { tokens := tokenizer.Tokenize(test.text) - parser := NewParagraphParser() - require.Equal(t, test.paragraph, parser.Parse(tokens)) + require.Equal(t, test.paragraph, NewParagraphParser().Parse(tokens)) } } diff --git a/plugin/gomark/parser/parser_test.go b/plugin/gomark/parser/parser_test.go index 30460e3b..dc2d6e68 100644 --- a/plugin/gomark/parser/parser_test.go +++ b/plugin/gomark/parser/parser_test.go @@ -89,7 +89,6 @@ func TestParser(t *testing.T) { for _, test := range tests { tokens := tokenizer.Tokenize(test.text) - nodes := Parse(tokens) - require.Equal(t, test.nodes, nodes) + require.Equal(t, test.nodes, Parse(tokens)) } } diff --git a/plugin/gomark/parser/text.go b/plugin/gomark/parser/text.go index 0db94bf4..f930ad3d 100644 --- a/plugin/gomark/parser/text.go +++ b/plugin/gomark/parser/text.go @@ -24,7 +24,9 @@ func (*TextParser) Match(tokens []*tokenizer.Token) (int, bool) { func (*TextParser) Parse(tokens []*tokenizer.Token) ast.Node { if len(tokens) == 0 { - return ast.NewText("") + return &ast.Text{} + } + return &ast.Text{ + Content: tokens[0].String(), } - return ast.NewText(tokens[0].String()) }