diff --git a/plugin/gomark/ast/ast.go b/plugin/gomark/ast/ast.go index 5da1bc61..b78f42c1 100644 --- a/plugin/gomark/ast/ast.go +++ b/plugin/gomark/ast/ast.go @@ -30,6 +30,8 @@ const ( EscapingCharacterNode MathNode HighlightNode + SubscriptNode + SuperscriptNode ) type Node interface { diff --git a/plugin/gomark/ast/inline.go b/plugin/gomark/ast/inline.go index 0631c747..cbc5b716 100644 --- a/plugin/gomark/ast/inline.go +++ b/plugin/gomark/ast/inline.go @@ -205,3 +205,31 @@ func (*Highlight) Type() NodeType { func (n *Highlight) Restore() string { return fmt.Sprintf("==%s==", n.Content) } + +type Subscript struct { + BaseInline + + Content string +} + +func (*Subscript) Type() NodeType { + return SubscriptNode +} + +func (n *Subscript) Restore() string { + return fmt.Sprintf("~%s~", n.Content) +} + +type Superscript struct { + BaseInline + + Content string +} + +func (*Superscript) Type() NodeType { + return SuperscriptNode +} + +func (n *Superscript) Restore() string { + return fmt.Sprintf("^%s^", n.Content) +} diff --git a/plugin/gomark/parser/parser.go b/plugin/gomark/parser/parser.go index b819471a..37f1be45 100644 --- a/plugin/gomark/parser/parser.go +++ b/plugin/gomark/parser/parser.go @@ -83,6 +83,8 @@ var defaultInlineParsers = []InlineParser{ NewItalicParser(), NewHighlightParser(), NewCodeParser(), + NewSubscriptParser(), + NewSuperscriptParser(), NewMathParser(), NewTagParser(), NewStrikethroughParser(), diff --git a/plugin/gomark/parser/subscript.go b/plugin/gomark/parser/subscript.go new file mode 100644 index 00000000..19825048 --- /dev/null +++ b/plugin/gomark/parser/subscript.go @@ -0,0 +1,53 @@ +package parser + +import ( + "errors" + + "github.com/usememos/memos/plugin/gomark/ast" + "github.com/usememos/memos/plugin/gomark/parser/tokenizer" +) + +type SubscriptParser struct{} + +func NewSubscriptParser() *SubscriptParser { + return &SubscriptParser{} +} + +func (*SubscriptParser) Match(tokens []*tokenizer.Token) (int, bool) { + if len(tokens) < 3 { + return 0, false + } + if tokens[0].Type != tokenizer.Tilde { + return 0, false + } + + contentTokens := []*tokenizer.Token{} + matched := false + for _, token := range tokens[1:] { + if token.Type == tokenizer.Newline { + return 0, false + } + if token.Type == tokenizer.Tilde { + matched = true + break + } + contentTokens = append(contentTokens, token) + } + if !matched || len(contentTokens) == 0 { + return 0, false + } + + return len(contentTokens) + 2, true +} + +func (p *SubscriptParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) { + size, ok := p.Match(tokens) + if size == 0 || !ok { + return nil, errors.New("not matched") + } + + contentTokens := tokens[1 : size-1] + return &ast.Subscript{ + Content: tokenizer.Stringify(contentTokens), + }, nil +} diff --git a/plugin/gomark/parser/subscript_test.go b/plugin/gomark/parser/subscript_test.go new file mode 100644 index 00000000..df3b2e85 --- /dev/null +++ b/plugin/gomark/parser/subscript_test.go @@ -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" + "github.com/usememos/memos/plugin/gomark/restore" +) + +func TestSubscriptParser(t *testing.T) { + tests := []struct { + text string + subscript ast.Node + }{ + { + text: "~Hello world!", + subscript: nil, + }, + { + text: "~Hello~", + subscript: &ast.Subscript{ + Content: "Hello", + }, + }, + { + text: "~ Hello ~", + subscript: &ast.Subscript{ + Content: " Hello ", + }, + }, + { + text: "~1~ Hello ~ ~", + subscript: &ast.Subscript{ + Content: "1", + }, + }, + } + + for _, test := range tests { + tokens := tokenizer.Tokenize(test.text) + node, _ := NewSubscriptParser().Parse(tokens) + require.Equal(t, restore.Restore([]ast.Node{test.subscript}), restore.Restore([]ast.Node{node})) + } +} diff --git a/plugin/gomark/parser/superscript.go b/plugin/gomark/parser/superscript.go new file mode 100644 index 00000000..f3c1f91e --- /dev/null +++ b/plugin/gomark/parser/superscript.go @@ -0,0 +1,53 @@ +package parser + +import ( + "errors" + + "github.com/usememos/memos/plugin/gomark/ast" + "github.com/usememos/memos/plugin/gomark/parser/tokenizer" +) + +type SuperscriptParser struct{} + +func NewSuperscriptParser() *SuperscriptParser { + return &SuperscriptParser{} +} + +func (*SuperscriptParser) Match(tokens []*tokenizer.Token) (int, bool) { + if len(tokens) < 3 { + return 0, false + } + if tokens[0].Type != tokenizer.Caret { + return 0, false + } + + contentTokens := []*tokenizer.Token{} + matched := false + for _, token := range tokens[1:] { + if token.Type == tokenizer.Newline { + return 0, false + } + if token.Type == tokenizer.Caret { + matched = true + break + } + contentTokens = append(contentTokens, token) + } + if !matched || len(contentTokens) == 0 { + return 0, false + } + + return len(contentTokens) + 2, true +} + +func (p *SuperscriptParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) { + size, ok := p.Match(tokens) + if size == 0 || !ok { + return nil, errors.New("not matched") + } + + contentTokens := tokens[1 : size-1] + return &ast.Superscript{ + Content: tokenizer.Stringify(contentTokens), + }, nil +} diff --git a/plugin/gomark/parser/superscript_test.go b/plugin/gomark/parser/superscript_test.go new file mode 100644 index 00000000..ef9e1cda --- /dev/null +++ b/plugin/gomark/parser/superscript_test.go @@ -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" + "github.com/usememos/memos/plugin/gomark/restore" +) + +func TestSuperscriptParser(t *testing.T) { + tests := []struct { + text string + superscript ast.Node + }{ + { + text: "^Hello world!", + superscript: nil, + }, + { + text: "^Hello^", + superscript: &ast.Superscript{ + Content: "Hello", + }, + }, + { + text: "^ Hello ^", + superscript: &ast.Superscript{ + Content: " Hello ", + }, + }, + { + text: "^1^ Hello ^ ^", + superscript: &ast.Superscript{ + Content: "1", + }, + }, + } + + for _, test := range tests { + tokens := tokenizer.Tokenize(test.text) + node, _ := NewSuperscriptParser().Parse(tokens) + require.Equal(t, restore.Restore([]ast.Node{test.superscript}), restore.Restore([]ast.Node{node})) + } +} diff --git a/plugin/gomark/parser/tokenizer/tokenizer.go b/plugin/gomark/parser/tokenizer/tokenizer.go index f719f6c7..e4651820 100644 --- a/plugin/gomark/parser/tokenizer/tokenizer.go +++ b/plugin/gomark/parser/tokenizer/tokenizer.go @@ -22,6 +22,7 @@ const ( EqualSign TokenType = "=" Pipe TokenType = "|" Colon TokenType = ":" + Caret TokenType = "^" Backslash TokenType = "\\" Newline TokenType = "\n" Space TokenType = " " @@ -86,6 +87,8 @@ func Tokenize(text string) []*Token { tokens = append(tokens, NewToken(Pipe, "|")) case ':': tokens = append(tokens, NewToken(Colon, ":")) + case '^': + tokens = append(tokens, NewToken(Caret, "^")) case '\\': tokens = append(tokens, NewToken(Backslash, `\`)) case '\n':