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
|
||||
|
||||
func NewNode(tp, text string) *Node {
|
||||
return &Node{
|
||||
Type: tp,
|
||||
Text: text,
|
||||
}
|
||||
type Node interface {
|
||||
Type() NodeType
|
||||
}
|
||||
|
||||
func (n *Node) AddChild(child *Node) {
|
||||
n.Children = append(n.Children, child)
|
||||
type NodeType int
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/usememos/memos/plugin/gomark/ast"
|
||||
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||
)
|
||||
|
||||
type BoldParser struct {
|
||||
ContentTokens []*tokenizer.Token
|
||||
}
|
||||
type BoldParser struct{}
|
||||
|
||||
var defaultBoldParser = &BoldParser{}
|
||||
|
||||
func NewBoldParser() *BoldParser {
|
||||
return &BoldParser{}
|
||||
func NewBoldParser() InlineParser {
|
||||
return defaultBoldParser
|
||||
}
|
||||
|
||||
func (*BoldParser) Match(tokens []*tokenizer.Token) *BoldParser {
|
||||
func (*BoldParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||
if len(tokens) < 5 {
|
||||
return nil
|
||||
return 0, false
|
||||
}
|
||||
|
||||
prefixTokens := tokens[:2]
|
||||
if prefixTokens[0].Type != prefixTokens[1].Type {
|
||||
return nil
|
||||
return 0, false
|
||||
}
|
||||
prefixTokenType := prefixTokens[0].Type
|
||||
if prefixTokenType != tokenizer.Star && prefixTokenType != tokenizer.Underline {
|
||||
return nil
|
||||
return 0, false
|
||||
}
|
||||
|
||||
contentTokens := []*tokenizer.Token{}
|
||||
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 nil
|
||||
return 0, false
|
||||
}
|
||||
if token.Type == prefixTokenType && nextToken.Type == prefixTokenType {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
contentTokens = append(contentTokens, token)
|
||||
}
|
||||
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 &BoldParser{
|
||||
ContentTokens: contentTokens,
|
||||
prefixTokenType := tokens[0].Type
|
||||
contentTokens := tokens[2 : size-2]
|
||||
return &ast.Bold{
|
||||
Symbol: prefixTokenType,
|
||||
Content: tokenizer.Stringify(contentTokens),
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +1,79 @@
|
||||
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 {
|
||||
Language string
|
||||
Content string
|
||||
}
|
||||
|
||||
var defaultCodeBlockParser = &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 {
|
||||
return nil
|
||||
return 0, false
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil
|
||||
return 0, false
|
||||
}
|
||||
cursor, language := 4, ""
|
||||
cursor := 4
|
||||
if tokens[3].Type != tokenizer.Newline {
|
||||
language = tokens[3].Value
|
||||
cursor = 5
|
||||
}
|
||||
|
||||
content, matched := "", false
|
||||
matched := false
|
||||
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 cursor+3 == len(tokens)-1 {
|
||||
cursor += 4
|
||||
matched = true
|
||||
break
|
||||
} else if tokens[cursor+4].Type == tokenizer.Newline {
|
||||
cursor += 5
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
content += tokens[cursor].Value
|
||||
}
|
||||
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 &CodeBlockParser{
|
||||
Language: language,
|
||||
Content: content,
|
||||
languageToken := tokens[3]
|
||||
contentStart, contentEnd := 5, size-4
|
||||
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
|
||||
|
||||
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