mirror of https://github.com/usememos/memos
chore: implement gomark skeleton
parent
7f1f6f77a0
commit
aa3632e2ac
@ -0,0 +1,42 @@
|
|||||||
|
package ast
|
||||||
|
|
||||||
|
type BaseBlock struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type Paragraph struct {
|
||||||
|
BaseBlock
|
||||||
|
|
||||||
|
Children []Node
|
||||||
|
}
|
||||||
|
|
||||||
|
var NodeTypeParagraph = NewNodeType("Paragraph")
|
||||||
|
|
||||||
|
func NewParagraph(children []Node) *Paragraph {
|
||||||
|
return &Paragraph{
|
||||||
|
Children: children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Paragraph) Type() NodeType {
|
||||||
|
return NodeTypeParagraph
|
||||||
|
}
|
||||||
|
|
||||||
|
type CodeBlock struct {
|
||||||
|
BaseBlock
|
||||||
|
|
||||||
|
Language string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
var NodeTypeCodeBlock = NewNodeType("CodeBlock")
|
||||||
|
|
||||||
|
func NewCodeBlock(language, content string) *CodeBlock {
|
||||||
|
return &CodeBlock{
|
||||||
|
Language: language,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CodeBlock) Type() NodeType {
|
||||||
|
return NodeTypeCodeBlock
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package ast
|
||||||
|
|
||||||
|
type BaseInline struct{}
|
||||||
|
|
||||||
|
type Text struct {
|
||||||
|
BaseInline
|
||||||
|
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
var NodeTypeText = NewNodeType("Text")
|
||||||
|
|
||||||
|
func NewText(content string) *Text {
|
||||||
|
return &Text{
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Text) Type() NodeType {
|
||||||
|
return NodeTypeText
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bold struct {
|
||||||
|
BaseInline
|
||||||
|
|
||||||
|
// Symbol is "*" or "_"
|
||||||
|
Symbol string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
var NodeTypeBold = NewNodeType("Bold")
|
||||||
|
|
||||||
|
func NewBold(symbol, content string) *Bold {
|
||||||
|
return &Bold{
|
||||||
|
Symbol: symbol,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Bold) Type() NodeType {
|
||||||
|
return NodeTypeBold
|
||||||
|
}
|
@ -1,12 +1,20 @@
|
|||||||
package ast
|
package ast
|
||||||
|
|
||||||
func NewNode(tp, text string) *Node {
|
type Node interface {
|
||||||
return &Node{
|
Type() NodeType
|
||||||
Type: tp,
|
|
||||||
Text: text,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) AddChild(child *Node) {
|
type NodeType int
|
||||||
n.Children = append(n.Children, child)
|
|
||||||
|
func (t NodeType) String() string {
|
||||||
|
return nodeTypeNames[t]
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodeTypeIndex NodeType
|
||||||
|
var nodeTypeNames = []string{""}
|
||||||
|
|
||||||
|
func NewNodeType(name string) NodeType {
|
||||||
|
nodeTypeNames = append(nodeTypeNames, name)
|
||||||
|
nodeTypeIndex++
|
||||||
|
return nodeTypeIndex
|
||||||
}
|
}
|
||||||
|
@ -1,49 +1,60 @@
|
|||||||
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 BoldParser struct {
|
type BoldParser struct{}
|
||||||
ContentTokens []*tokenizer.Token
|
|
||||||
}
|
var defaultBoldParser = &BoldParser{}
|
||||||
|
|
||||||
func NewBoldParser() *BoldParser {
|
func NewBoldParser() InlineParser {
|
||||||
return &BoldParser{}
|
return defaultBoldParser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*BoldParser) Match(tokens []*tokenizer.Token) *BoldParser {
|
func (*BoldParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||||
if len(tokens) < 5 {
|
if len(tokens) < 5 {
|
||||||
return nil
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
prefixTokens := tokens[:2]
|
prefixTokens := tokens[:2]
|
||||||
if prefixTokens[0].Type != prefixTokens[1].Type {
|
if prefixTokens[0].Type != prefixTokens[1].Type {
|
||||||
return nil
|
return 0, false
|
||||||
}
|
}
|
||||||
prefixTokenType := prefixTokens[0].Type
|
prefixTokenType := prefixTokens[0].Type
|
||||||
if prefixTokenType != tokenizer.Star && prefixTokenType != tokenizer.Underline {
|
if prefixTokenType != tokenizer.Star && prefixTokenType != tokenizer.Underline {
|
||||||
return nil
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
contentTokens := []*tokenizer.Token{}
|
|
||||||
cursor, matched := 2, false
|
cursor, matched := 2, false
|
||||||
for ; cursor < len(tokens)-1; cursor++ {
|
for ; cursor < len(tokens)-1; cursor++ {
|
||||||
token, nextToken := tokens[cursor], tokens[cursor+1]
|
token, nextToken := tokens[cursor], tokens[cursor+1]
|
||||||
if token.Type == tokenizer.Newline || nextToken.Type == tokenizer.Newline {
|
if token.Type == tokenizer.Newline || nextToken.Type == tokenizer.Newline {
|
||||||
return nil
|
return 0, false
|
||||||
}
|
}
|
||||||
if token.Type == prefixTokenType && nextToken.Type == prefixTokenType {
|
if token.Type == prefixTokenType && nextToken.Type == prefixTokenType {
|
||||||
matched = true
|
matched = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
contentTokens = append(contentTokens, token)
|
|
||||||
}
|
}
|
||||||
if !matched {
|
if !matched {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor + 2, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoldParser) Parse(tokens []*tokenizer.Token) ast.Node {
|
||||||
|
size, ok := p.Match(tokens)
|
||||||
|
if size == 0 || !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &BoldParser{
|
prefixTokenType := tokens[0].Type
|
||||||
ContentTokens: contentTokens,
|
contentTokens := tokens[2 : size-2]
|
||||||
|
return &ast.Bold{
|
||||||
|
Symbol: prefixTokenType,
|
||||||
|
Content: tokenizer.Stringify(contentTokens),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,52 +1,79 @@
|
|||||||
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 CodeBlockParser struct {
|
type CodeBlockParser struct {
|
||||||
Language string
|
Language string
|
||||||
Content string
|
Content string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaultCodeBlockParser = &CodeBlockParser{}
|
||||||
|
|
||||||
func NewCodeBlockParser() *CodeBlockParser {
|
func NewCodeBlockParser() *CodeBlockParser {
|
||||||
return &CodeBlockParser{}
|
return defaultCodeBlockParser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*CodeBlockParser) Match(tokens []*tokenizer.Token) *CodeBlockParser {
|
func (*CodeBlockParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||||
if len(tokens) < 9 {
|
if len(tokens) < 9 {
|
||||||
return nil
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokens[0].Type != tokenizer.Backtick || tokens[1].Type != tokenizer.Backtick || tokens[2].Type != tokenizer.Backtick {
|
if tokens[0].Type != tokenizer.Backtick || tokens[1].Type != tokenizer.Backtick || tokens[2].Type != tokenizer.Backtick {
|
||||||
return nil
|
return 0, false
|
||||||
}
|
}
|
||||||
if tokens[3].Type != tokenizer.Newline && tokens[4].Type != tokenizer.Newline {
|
if tokens[3].Type != tokenizer.Newline && tokens[4].Type != tokenizer.Newline {
|
||||||
return nil
|
return 0, false
|
||||||
}
|
}
|
||||||
cursor, language := 4, ""
|
cursor := 4
|
||||||
if tokens[3].Type != tokenizer.Newline {
|
if tokens[3].Type != tokenizer.Newline {
|
||||||
language = tokens[3].Value
|
|
||||||
cursor = 5
|
cursor = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
content, matched := "", false
|
matched := false
|
||||||
for ; cursor < len(tokens)-3; cursor++ {
|
for ; cursor < len(tokens)-3; cursor++ {
|
||||||
if tokens[cursor].Type == tokenizer.Newline && tokens[cursor+1].Type == tokenizer.Backtick && tokens[cursor+2].Type == tokenizer.Backtick && tokens[cursor+3].Type == tokenizer.Backtick {
|
if tokens[cursor].Type == tokenizer.Newline && tokens[cursor+1].Type == tokenizer.Backtick && tokens[cursor+2].Type == tokenizer.Backtick && tokens[cursor+3].Type == tokenizer.Backtick {
|
||||||
if cursor+3 == len(tokens)-1 {
|
if cursor+3 == len(tokens)-1 {
|
||||||
|
cursor += 4
|
||||||
matched = true
|
matched = true
|
||||||
break
|
break
|
||||||
} else if tokens[cursor+4].Type == tokenizer.Newline {
|
} else if tokens[cursor+4].Type == tokenizer.Newline {
|
||||||
|
cursor += 5
|
||||||
matched = true
|
matched = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content += tokens[cursor].Value
|
|
||||||
}
|
}
|
||||||
if !matched {
|
if !matched {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CodeBlockParser) Parse(tokens []*tokenizer.Token) ast.Node {
|
||||||
|
size, ok := p.Match(tokens)
|
||||||
|
if size == 0 || !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CodeBlockParser{
|
languageToken := tokens[3]
|
||||||
Language: language,
|
contentStart, contentEnd := 5, size-4
|
||||||
Content: content,
|
if languageToken.Type == tokenizer.Newline {
|
||||||
|
languageToken = nil
|
||||||
|
contentStart = 4
|
||||||
|
}
|
||||||
|
if tokens[size-1].Type == tokenizer.Newline {
|
||||||
|
contentEnd = size - 5
|
||||||
|
}
|
||||||
|
|
||||||
|
codeBlock := &ast.CodeBlock{
|
||||||
|
Content: tokenizer.Stringify(tokens[contentStart:contentEnd]),
|
||||||
|
}
|
||||||
|
if languageToken != nil {
|
||||||
|
codeBlock.Language = languageToken.String()
|
||||||
}
|
}
|
||||||
|
return codeBlock
|
||||||
}
|
}
|
||||||
|
@ -1 +1,65 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/usememos/memos/plugin/gomark/ast"
|
||||||
|
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
BlockParsers []BlockParser
|
||||||
|
InlineParsers []InlineParser
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseParser interface {
|
||||||
|
Match(tokens []*tokenizer.Token) (int, bool)
|
||||||
|
Parse(tokens []*tokenizer.Token) ast.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
type InlineParser interface {
|
||||||
|
BaseParser
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockParser interface {
|
||||||
|
BaseParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(tokens []*tokenizer.Token) []ast.Node {
|
||||||
|
nodes := []ast.Node{}
|
||||||
|
blockParsers := []BlockParser{
|
||||||
|
NewParagraphParser(),
|
||||||
|
}
|
||||||
|
for len(tokens) > 0 {
|
||||||
|
for _, blockParser := range blockParsers {
|
||||||
|
cursor, matched := blockParser.Match(tokens)
|
||||||
|
if matched {
|
||||||
|
node := blockParser.Parse(tokens)
|
||||||
|
nodes = append(nodes, node)
|
||||||
|
tokens = tokens[cursor:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseInline(tokens []*tokenizer.Token, inlineParsers []InlineParser) []ast.Node {
|
||||||
|
nodes := []ast.Node{}
|
||||||
|
var lastNode ast.Node
|
||||||
|
for len(tokens) > 0 {
|
||||||
|
for _, inlineParser := range inlineParsers {
|
||||||
|
cursor, matched := inlineParser.Match(tokens)
|
||||||
|
if matched {
|
||||||
|
node := inlineParser.Parse(tokens)
|
||||||
|
if node.Type() == ast.NodeTypeText && lastNode != nil && lastNode.Type() == ast.NodeTypeText {
|
||||||
|
lastNode.(*ast.Text).Content += node.(*ast.Text).Content
|
||||||
|
} else {
|
||||||
|
nodes = append(nodes, node)
|
||||||
|
lastNode = node
|
||||||
|
}
|
||||||
|
tokens = tokens[cursor:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
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 TestParser(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
text string
|
||||||
|
nodes []ast.Node
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
text: "Hello world!",
|
||||||
|
nodes: []ast.Node{
|
||||||
|
&ast.Paragraph{
|
||||||
|
Children: []ast.Node{
|
||||||
|
&ast.Text{
|
||||||
|
Content: "Hello world!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "**Hello** world!",
|
||||||
|
nodes: []ast.Node{
|
||||||
|
&ast.Paragraph{
|
||||||
|
Children: []ast.Node{
|
||||||
|
&ast.Bold{
|
||||||
|
Symbol: "*",
|
||||||
|
Content: "Hello",
|
||||||
|
},
|
||||||
|
&ast.Text{
|
||||||
|
Content: " world!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Hello **world**!",
|
||||||
|
nodes: []ast.Node{
|
||||||
|
&ast.Paragraph{
|
||||||
|
Children: []ast.Node{
|
||||||
|
&ast.Text{
|
||||||
|
Content: "Hello ",
|
||||||
|
},
|
||||||
|
&ast.Bold{
|
||||||
|
Symbol: "*",
|
||||||
|
Content: "world",
|
||||||
|
},
|
||||||
|
&ast.Text{
|
||||||
|
Content: "!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tokens := tokenizer.Tokenize(test.text)
|
||||||
|
nodes := Parse(tokens)
|
||||||
|
require.Equal(t, test.nodes, nodes)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/usememos/memos/plugin/gomark/ast"
|
||||||
|
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TextParser struct {
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultTextParser = &TextParser{}
|
||||||
|
|
||||||
|
func NewTextParser() *TextParser {
|
||||||
|
return defaultTextParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*TextParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return 1, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*TextParser) Parse(tokens []*tokenizer.Token) ast.Node {
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return ast.NewText("")
|
||||||
|
}
|
||||||
|
return ast.NewText(tokens[0].String())
|
||||||
|
}
|
Loading…
Reference in New Issue