mirror of https://github.com/usememos/memos
feat: implement gomark parsers
parent
2d9c5d16e1
commit
453707d18c
@ -0,0 +1,47 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/usememos/memos/plugin/gomark/ast"
|
||||
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||
)
|
||||
|
||||
type BlockquoteParser struct{}
|
||||
|
||||
func NewBlockquoteParser() *BlockquoteParser {
|
||||
return &BlockquoteParser{}
|
||||
}
|
||||
|
||||
func (*BlockquoteParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||
if len(tokens) < 4 {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[0].Type != tokenizer.GreaterThan || 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 *BlockquoteParser) Parse(tokens []*tokenizer.Token) ast.Node {
|
||||
size, ok := p.Match(tokens)
|
||||
if size == 0 || !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
contentTokens := tokens[2:size]
|
||||
children := ParseInline(contentTokens)
|
||||
return &ast.Blockquote{
|
||||
Children: children,
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
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 TestBlockquoteParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
text string
|
||||
blockquote ast.Node
|
||||
}{
|
||||
{
|
||||
text: "> Hello world",
|
||||
blockquote: &ast.Blockquote{
|
||||
Children: []ast.Node{
|
||||
&ast.Text{
|
||||
Content: "Hello world",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "> Hello\nworld",
|
||||
blockquote: &ast.Blockquote{
|
||||
Children: []ast.Node{
|
||||
&ast.Text{
|
||||
Content: "Hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
text: ">Hello\nworld",
|
||||
blockquote: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tokens := tokenizer.Tokenize(test.text)
|
||||
require.Equal(t, test.blockquote, NewBlockquoteParser().Parse(tokens))
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/usememos/memos/plugin/gomark/ast"
|
||||
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||
)
|
||||
|
||||
type BoldItalicParser struct{}
|
||||
|
||||
func NewBoldItalicParser() InlineParser {
|
||||
return &BoldItalicParser{}
|
||||
}
|
||||
|
||||
func (*BoldItalicParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||
if len(tokens) < 7 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
prefixTokens := tokens[:3]
|
||||
if prefixTokens[0].Type != prefixTokens[1].Type || prefixTokens[0].Type != prefixTokens[2].Type || prefixTokens[1].Type != prefixTokens[2].Type {
|
||||
return 0, false
|
||||
}
|
||||
prefixTokenType := prefixTokens[0].Type
|
||||
if prefixTokenType != tokenizer.Asterisk && prefixTokenType != tokenizer.Underline {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
cursor, matched := 3, false
|
||||
for ; cursor < len(tokens)-2; cursor++ {
|
||||
token, nextToken, endToken := tokens[cursor], tokens[cursor+1], tokens[cursor+2]
|
||||
if token.Type == tokenizer.Newline || nextToken.Type == tokenizer.Newline || endToken.Type == tokenizer.Newline {
|
||||
return 0, false
|
||||
}
|
||||
if token.Type == prefixTokenType && nextToken.Type == prefixTokenType && endToken.Type == prefixTokenType {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return cursor + 3, true
|
||||
}
|
||||
|
||||
func (p *BoldItalicParser) Parse(tokens []*tokenizer.Token) ast.Node {
|
||||
size, ok := p.Match(tokens)
|
||||
if size == 0 || !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
prefixTokenType := tokens[0].Type
|
||||
contentTokens := tokens[3 : size-3]
|
||||
return &ast.BoldItalic{
|
||||
Symbol: prefixTokenType,
|
||||
Content: tokenizer.Stringify(contentTokens),
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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 TestBoldItalicParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
text string
|
||||
boldItalic ast.Node
|
||||
}{
|
||||
{
|
||||
text: "*Hello world!",
|
||||
boldItalic: nil,
|
||||
},
|
||||
{
|
||||
text: "***Hello***",
|
||||
boldItalic: &ast.BoldItalic{
|
||||
Symbol: "*",
|
||||
Content: "Hello",
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "*** Hello ***",
|
||||
boldItalic: &ast.BoldItalic{
|
||||
Symbol: "*",
|
||||
Content: " Hello ",
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "*** Hello * *",
|
||||
boldItalic: nil,
|
||||
},
|
||||
{
|
||||
text: "*** Hello **",
|
||||
boldItalic: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tokens := tokenizer.Tokenize(test.text)
|
||||
require.Equal(t, test.boldItalic, NewBoldItalicParser().Parse(tokens))
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/usememos/memos/plugin/gomark/ast"
|
||||
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||
)
|
||||
|
||||
type HorizontalRuleParser struct{}
|
||||
|
||||
func NewHorizontalRuleParser() *HorizontalRuleParser {
|
||||
return &HorizontalRuleParser{}
|
||||
}
|
||||
|
||||
func (*HorizontalRuleParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||
if len(tokens) < 3 {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[0].Type != tokens[1].Type || tokens[0].Type != tokens[2].Type || tokens[1].Type != tokens[2].Type {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[0].Type != tokenizer.Dash && tokens[0].Type != tokenizer.Underline && tokens[0].Type != tokenizer.Asterisk {
|
||||
return 0, false
|
||||
}
|
||||
if len(tokens) > 3 && tokens[3].Type != tokenizer.Newline {
|
||||
return 0, false
|
||||
}
|
||||
return 3, true
|
||||
}
|
||||
|
||||
func (p *HorizontalRuleParser) Parse(tokens []*tokenizer.Token) ast.Node {
|
||||
size, ok := p.Match(tokens)
|
||||
if size == 0 || !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ast.HorizontalRule{
|
||||
Symbol: tokens[0].Type,
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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 TestHorizontalRuleParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
text string
|
||||
horizontalRule ast.Node
|
||||
}{
|
||||
{
|
||||
text: "---",
|
||||
horizontalRule: &ast.HorizontalRule{
|
||||
Symbol: "-",
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "****",
|
||||
horizontalRule: nil,
|
||||
},
|
||||
{
|
||||
text: "***",
|
||||
horizontalRule: &ast.HorizontalRule{
|
||||
Symbol: "*",
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "-*-",
|
||||
horizontalRule: nil,
|
||||
},
|
||||
{
|
||||
text: "___",
|
||||
horizontalRule: &ast.HorizontalRule{
|
||||
Symbol: "_",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tokens := tokenizer.Tokenize(test.text)
|
||||
require.Equal(t, test.horizontalRule, NewHorizontalRuleParser().Parse(tokens))
|
||||
}
|
||||
}
|
@ -1,58 +1,72 @@
|
||||
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 LinkParser struct {
|
||||
ContentTokens []*tokenizer.Token
|
||||
URL string
|
||||
}
|
||||
type LinkParser struct{}
|
||||
|
||||
func NewLinkParser() *LinkParser {
|
||||
return &LinkParser{}
|
||||
}
|
||||
|
||||
func (*LinkParser) Match(tokens []*tokenizer.Token) *LinkParser {
|
||||
if len(tokens) < 4 {
|
||||
return nil
|
||||
func (*LinkParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||
if len(tokens) < 5 {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[0].Type != tokenizer.LeftSquareBracket {
|
||||
return nil
|
||||
return 0, false
|
||||
}
|
||||
cursor, contentTokens := 1, []*tokenizer.Token{}
|
||||
for ; cursor < len(tokens)-2; cursor++ {
|
||||
if tokens[cursor].Type == tokenizer.Newline {
|
||||
return nil
|
||||
textTokens := []*tokenizer.Token{}
|
||||
for _, token := range tokens[1:] {
|
||||
if token.Type == tokenizer.Newline {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[cursor].Type == tokenizer.RightSquareBracket {
|
||||
if token.Type == tokenizer.RightSquareBracket {
|
||||
break
|
||||
}
|
||||
contentTokens = append(contentTokens, tokens[cursor])
|
||||
textTokens = append(textTokens, token)
|
||||
}
|
||||
if tokens[cursor+1].Type != tokenizer.LeftParenthesis {
|
||||
return nil
|
||||
if len(textTokens)+4 >= len(tokens) {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[2+len(textTokens)].Type != tokenizer.LeftParenthesis {
|
||||
return 0, false
|
||||
}
|
||||
matched, url := false, ""
|
||||
for _, token := range tokens[cursor+2:] {
|
||||
urlTokens := []*tokenizer.Token{}
|
||||
for _, token := range tokens[3+len(textTokens):] {
|
||||
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
|
||||
urlTokens = append(urlTokens, token)
|
||||
}
|
||||
if 4+len(urlTokens)+len(textTokens) > len(tokens) {
|
||||
return 0, false
|
||||
}
|
||||
if !matched || url == "" {
|
||||
|
||||
return 4 + len(urlTokens) + len(textTokens), true
|
||||
}
|
||||
|
||||
func (p *LinkParser) Parse(tokens []*tokenizer.Token) ast.Node {
|
||||
size, ok := p.Match(tokens)
|
||||
if size == 0 || !ok {
|
||||
return nil
|
||||
}
|
||||
if len(contentTokens) == 0 {
|
||||
contentTokens = append(contentTokens, &tokenizer.Token{
|
||||
Type: tokenizer.Text,
|
||||
Value: url,
|
||||
})
|
||||
|
||||
textTokens := []*tokenizer.Token{}
|
||||
for _, token := range tokens[1:] {
|
||||
if token.Type == tokenizer.RightSquareBracket {
|
||||
break
|
||||
}
|
||||
textTokens = append(textTokens, token)
|
||||
}
|
||||
return &LinkParser{
|
||||
ContentTokens: contentTokens,
|
||||
URL: url,
|
||||
urlTokens := tokens[2+len(textTokens)+1 : size-1]
|
||||
return &ast.Link{
|
||||
Text: tokenizer.Stringify(textTokens),
|
||||
URL: tokenizer.Stringify(urlTokens),
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/usememos/memos/plugin/gomark/ast"
|
||||
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||
)
|
||||
|
||||
type StrikethroughParser struct{}
|
||||
|
||||
func NewStrikethroughParser() *StrikethroughParser {
|
||||
return &StrikethroughParser{}
|
||||
}
|
||||
|
||||
func (*StrikethroughParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||
if len(tokens) < 5 {
|
||||
return 0, false
|
||||
}
|
||||
if tokens[0].Type != tokenizer.Tilde || tokens[1].Type != tokenizer.Tilde {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
cursor, matched := 2, false
|
||||
for ; cursor < len(tokens)-1; cursor++ {
|
||||
token, nextToken := tokens[cursor], tokens[cursor+1]
|
||||
if token.Type == tokenizer.Newline || nextToken.Type == tokenizer.Newline {
|
||||
return 0, false
|
||||
}
|
||||
if token.Type == tokenizer.Tilde && nextToken.Type == tokenizer.Tilde {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return 0, false
|
||||
}
|
||||
return cursor + 2, true
|
||||
}
|
||||
|
||||
func (p *StrikethroughParser) Parse(tokens []*tokenizer.Token) ast.Node {
|
||||
size, ok := p.Match(tokens)
|
||||
if size == 0 || !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
contentTokens := tokens[2 : size-2]
|
||||
return &ast.Strikethrough{
|
||||
Content: tokenizer.Stringify(contentTokens),
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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 TestStrikethroughParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
text string
|
||||
strikethrough ast.Node
|
||||
}{
|
||||
{
|
||||
text: "~~Hello world",
|
||||
strikethrough: nil,
|
||||
},
|
||||
{
|
||||
text: "~~Hello~~",
|
||||
strikethrough: &ast.Strikethrough{
|
||||
Content: "Hello",
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "~~ Hello ~~",
|
||||
strikethrough: &ast.Strikethrough{
|
||||
Content: " Hello ",
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "~~1~~ Hello ~~~",
|
||||
strikethrough: &ast.Strikethrough{
|
||||
Content: "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tokens := tokenizer.Tokenize(test.text)
|
||||
require.Equal(t, test.strikethrough, NewStrikethroughParser().Parse(tokens))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue