mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: implement embedded memo renderer
This commit is contained in:
@ -65,6 +65,8 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node {
|
|||||||
node.Node = &apiv2pb.Node_MathBlockNode{MathBlockNode: &apiv2pb.MathBlockNode{Content: n.Content}}
|
node.Node = &apiv2pb.Node_MathBlockNode{MathBlockNode: &apiv2pb.MathBlockNode{Content: n.Content}}
|
||||||
case *ast.Table:
|
case *ast.Table:
|
||||||
node.Node = &apiv2pb.Node_TableNode{TableNode: convertTableFromASTNode(n)}
|
node.Node = &apiv2pb.Node_TableNode{TableNode: convertTableFromASTNode(n)}
|
||||||
|
case *ast.EmbeddedContent:
|
||||||
|
node.Node = &apiv2pb.Node_EmbeddedContentNode{EmbeddedContentNode: &apiv2pb.EmbeddedContentNode{ResourceName: n.ResourceName}}
|
||||||
case *ast.Text:
|
case *ast.Text:
|
||||||
node.Node = &apiv2pb.Node_TextNode{TextNode: &apiv2pb.TextNode{Content: n.Content}}
|
node.Node = &apiv2pb.Node_TextNode{TextNode: &apiv2pb.TextNode{Content: n.Content}}
|
||||||
case *ast.Bold:
|
case *ast.Bold:
|
||||||
@ -142,6 +144,8 @@ func convertToASTNode(node *apiv2pb.Node) ast.Node {
|
|||||||
return &ast.MathBlock{Content: n.MathBlockNode.Content}
|
return &ast.MathBlock{Content: n.MathBlockNode.Content}
|
||||||
case *apiv2pb.Node_TableNode:
|
case *apiv2pb.Node_TableNode:
|
||||||
return convertTableToASTNode(node)
|
return convertTableToASTNode(node)
|
||||||
|
case *apiv2pb.Node_EmbeddedContentNode:
|
||||||
|
return &ast.EmbeddedContent{ResourceName: n.EmbeddedContentNode.ResourceName}
|
||||||
case *apiv2pb.Node_TextNode:
|
case *apiv2pb.Node_TextNode:
|
||||||
return &ast.Text{Content: n.TextNode.Content}
|
return &ast.Text{Content: n.TextNode.Content}
|
||||||
case *apiv2pb.Node_BoldNode:
|
case *apiv2pb.Node_BoldNode:
|
||||||
|
@ -16,6 +16,7 @@ const (
|
|||||||
TaskListNode
|
TaskListNode
|
||||||
MathBlockNode
|
MathBlockNode
|
||||||
TableNode
|
TableNode
|
||||||
|
EmbeddedContentNode
|
||||||
// Inline nodes.
|
// Inline nodes.
|
||||||
TextNode
|
TextNode
|
||||||
BoldNode
|
BoldNode
|
||||||
|
@ -228,3 +228,17 @@ func (n *Table) Restore() string {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EmbeddedContent struct {
|
||||||
|
BaseBlock
|
||||||
|
|
||||||
|
ResourceName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*EmbeddedContent) Type() NodeType {
|
||||||
|
return EmbeddedContentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *EmbeddedContent) Restore() string {
|
||||||
|
return fmt.Sprintf("![[%s]]", n.ResourceName)
|
||||||
|
}
|
||||||
|
51
plugin/gomark/parser/embedded_content.go
Normal file
51
plugin/gomark/parser/embedded_content.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/usememos/memos/plugin/gomark/ast"
|
||||||
|
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmbeddedContentParser struct{}
|
||||||
|
|
||||||
|
func NewEmbeddedContentParser() *EmbeddedContentParser {
|
||||||
|
return &EmbeddedContentParser{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*EmbeddedContentParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||||
|
lines := tokenizer.Split(tokens, tokenizer.Newline)
|
||||||
|
if len(lines) < 1 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
firstLine := lines[0]
|
||||||
|
if len(firstLine) < 5 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
if firstLine[0].Type != tokenizer.ExclamationMark || firstLine[1].Type != tokenizer.LeftSquareBracket || firstLine[2].Type != tokenizer.LeftSquareBracket {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
matched := false
|
||||||
|
for index, token := range firstLine[:len(firstLine)-1] {
|
||||||
|
if token.Type == tokenizer.RightSquareBracket && firstLine[index+1].Type == tokenizer.RightSquareBracket && index+1 == len(firstLine)-1 {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(firstLine), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *EmbeddedContentParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
|
||||||
|
size, ok := p.Match(tokens)
|
||||||
|
if size == 0 || !ok {
|
||||||
|
return nil, errors.New("not matched")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ast.EmbeddedContent{
|
||||||
|
ResourceName: tokenizer.Stringify(tokens[3 : size-2]),
|
||||||
|
}, nil
|
||||||
|
}
|
51
plugin/gomark/parser/embedded_content_test.go
Normal file
51
plugin/gomark/parser/embedded_content_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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 TestEmbeddedContentParser(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
text string
|
||||||
|
embeddedContent ast.Node
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
text: "![[Hello world]",
|
||||||
|
embeddedContent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "![[Hello world]]",
|
||||||
|
embeddedContent: &ast.EmbeddedContent{
|
||||||
|
ResourceName: "Hello world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "![[memos/1]]",
|
||||||
|
embeddedContent: &ast.EmbeddedContent{
|
||||||
|
ResourceName: "memos/1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "![[resources/101]] \n123",
|
||||||
|
embeddedContent: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "![[resources/101]]\n123",
|
||||||
|
embeddedContent: &ast.EmbeddedContent{
|
||||||
|
ResourceName: "resources/101",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tokens := tokenizer.Tokenize(test.text)
|
||||||
|
node, _ := NewEmbeddedContentParser().Parse(tokens)
|
||||||
|
require.Equal(t, restore.Restore([]ast.Node{test.embeddedContent}), restore.Restore([]ast.Node{node}))
|
||||||
|
}
|
||||||
|
}
|
@ -39,6 +39,7 @@ var defaultBlockParsers = []BlockParser{
|
|||||||
NewUnorderedListParser(),
|
NewUnorderedListParser(),
|
||||||
NewOrderedListParser(),
|
NewOrderedListParser(),
|
||||||
NewMathBlockParser(),
|
NewMathBlockParser(),
|
||||||
|
NewEmbeddedContentParser(),
|
||||||
NewParagraphParser(),
|
NewParagraphParser(),
|
||||||
NewLineBreakParser(),
|
NewLineBreakParser(),
|
||||||
}
|
}
|
||||||
|
@ -218,6 +218,22 @@ func TestParser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "Hello\n![[memos/101]]",
|
||||||
|
nodes: []ast.Node{
|
||||||
|
&ast.Paragraph{
|
||||||
|
Children: []ast.Node{
|
||||||
|
&ast.Text{
|
||||||
|
Content: "Hello",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&ast.LineBreak{},
|
||||||
|
&ast.EmbeddedContent{
|
||||||
|
ResourceName: "memos/101",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
@ -39,8 +39,10 @@ func (*TableParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
|||||||
rowTokens := []*tokenizer.Token{}
|
rowTokens := []*tokenizer.Token{}
|
||||||
for index, token := range tokens[len(headerTokens)+len(delimiterTokens)+2:] {
|
for index, token := range tokens[len(headerTokens)+len(delimiterTokens)+2:] {
|
||||||
temp := len(headerTokens) + len(delimiterTokens) + 2 + index
|
temp := len(headerTokens) + len(delimiterTokens) + 2 + index
|
||||||
if token.Type == tokenizer.Newline && temp != len(tokens)-1 && tokens[temp+1].Type != tokenizer.Pipe {
|
if token.Type == tokenizer.Newline {
|
||||||
break
|
if (temp == len(tokens)-1) || (temp+1 == len(tokens)-1 && tokens[temp+1].Type == tokenizer.Newline) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rowTokens = append(rowTokens, token)
|
rowTokens = append(rowTokens, token)
|
||||||
}
|
}
|
||||||
@ -65,7 +67,18 @@ func (*TableParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
|||||||
if delimiterCells != headerCells || !ok {
|
if delimiterCells != headerCells || !ok {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
for _, t := range tokenizer.Split(delimiterTokens, tokenizer.Pipe) {
|
|
||||||
|
for index, t := range tokenizer.Split(delimiterTokens, tokenizer.Pipe) {
|
||||||
|
if index == 0 || index == headerCells {
|
||||||
|
if len(t) != 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(t) < 5 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
delimiterTokens := t[1 : len(t)-1]
|
delimiterTokens := t[1 : len(t)-1]
|
||||||
if len(delimiterTokens) < 3 {
|
if len(delimiterTokens) < 3 {
|
||||||
return 0, false
|
return 0, false
|
||||||
@ -112,15 +125,16 @@ func (p *TableParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
|
|||||||
delimiter := make([]string, 0)
|
delimiter := make([]string, 0)
|
||||||
rows := make([][]string, 0)
|
rows := make([][]string, 0)
|
||||||
|
|
||||||
for _, t := range tokenizer.Split(headerTokens, tokenizer.Pipe) {
|
cols := len(tokenizer.Split(headerTokens, tokenizer.Pipe)) - 2
|
||||||
|
for _, t := range tokenizer.Split(headerTokens, tokenizer.Pipe)[1 : cols+1] {
|
||||||
header = append(header, tokenizer.Stringify(t[1:len(t)-1]))
|
header = append(header, tokenizer.Stringify(t[1:len(t)-1]))
|
||||||
}
|
}
|
||||||
for _, t := range tokenizer.Split(dilimiterTokens, tokenizer.Pipe) {
|
for _, t := range tokenizer.Split(dilimiterTokens, tokenizer.Pipe)[1 : cols+1] {
|
||||||
delimiter = append(delimiter, tokenizer.Stringify(t[1:len(t)-1]))
|
delimiter = append(delimiter, tokenizer.Stringify(t[1:len(t)-1]))
|
||||||
}
|
}
|
||||||
for _, row := range rowTokens {
|
for _, row := range rowTokens {
|
||||||
cells := make([]string, 0)
|
cells := make([]string, 0)
|
||||||
for _, t := range tokenizer.Split(row, tokenizer.Pipe) {
|
for _, t := range tokenizer.Split(row, tokenizer.Pipe)[1 : cols+1] {
|
||||||
cells = append(cells, tokenizer.Stringify(t[1:len(t)-1]))
|
cells = append(cells, tokenizer.Stringify(t[1:len(t)-1]))
|
||||||
}
|
}
|
||||||
rows = append(rows, cells)
|
rows = append(rows, cells)
|
||||||
@ -145,10 +159,13 @@ func matchTableCellTokens(tokens []*tokenizer.Token) (int, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cells := tokenizer.Split(tokens, tokenizer.Pipe)
|
cells := tokenizer.Split(tokens, tokenizer.Pipe)
|
||||||
if len(cells) != pipes-1 {
|
if len(cells) != pipes+1 {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
for _, cellTokens := range cells {
|
if len(cells[0]) != 0 || len(cells[len(cells)-1]) != 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
for _, cellTokens := range cells[1 : len(cells)-1] {
|
||||||
if len(cellTokens) == 0 {
|
if len(cellTokens) == 0 {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
@ -160,5 +177,5 @@ func matchTableCellTokens(tokens []*tokenizer.Token) (int, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return len(cells), true
|
return len(cells) - 1, true
|
||||||
}
|
}
|
||||||
|
@ -132,20 +132,20 @@ func Stringify(tokens []*Token) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Split(tokens []*Token, delimiter TokenType) [][]*Token {
|
func Split(tokens []*Token, delimiter TokenType) [][]*Token {
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return [][]*Token{}
|
||||||
|
}
|
||||||
|
|
||||||
result := make([][]*Token, 0)
|
result := make([][]*Token, 0)
|
||||||
current := make([]*Token, 0)
|
current := make([]*Token, 0)
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
if token.Type == delimiter {
|
if token.Type == delimiter {
|
||||||
if len(current) > 0 {
|
result = append(result, current)
|
||||||
result = append(result, current)
|
current = make([]*Token, 0)
|
||||||
current = make([]*Token, 0)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
current = append(current, token)
|
current = append(current, token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(current) > 0 {
|
result = append(result, current)
|
||||||
result = append(result, current)
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -77,3 +77,144 @@ func TestTokenize(t *testing.T) {
|
|||||||
require.Equal(t, test.tokens, result)
|
require.Equal(t, test.tokens, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSplit(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tokens []*Token
|
||||||
|
sep TokenType
|
||||||
|
result [][]*Token
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
tokens: []*Token{
|
||||||
|
{
|
||||||
|
Type: Asterisk,
|
||||||
|
Value: "*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Space,
|
||||||
|
Value: " ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: ExclamationMark,
|
||||||
|
Value: "!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sep: Asterisk,
|
||||||
|
result: [][]*Token{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Space,
|
||||||
|
Value: " ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: ExclamationMark,
|
||||||
|
Value: "!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tokens: []*Token{
|
||||||
|
{
|
||||||
|
Type: Asterisk,
|
||||||
|
Value: "*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Space,
|
||||||
|
Value: " ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: ExclamationMark,
|
||||||
|
Value: "!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sep: Text,
|
||||||
|
result: [][]*Token{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Type: Asterisk,
|
||||||
|
Value: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Type: Space,
|
||||||
|
Value: " ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Type: ExclamationMark,
|
||||||
|
Value: "!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tokens: []*Token{
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Space,
|
||||||
|
Value: " ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Newline,
|
||||||
|
Value: "\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sep: Newline,
|
||||||
|
result: [][]*Token{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Space,
|
||||||
|
Value: " ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: Text,
|
||||||
|
Value: "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := Split(test.tokens, test.sep)
|
||||||
|
require.Equal(t, test.result, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -36,21 +36,22 @@ enum NodeType {
|
|||||||
TASK_LIST = 9;
|
TASK_LIST = 9;
|
||||||
MATH_BLOCK = 10;
|
MATH_BLOCK = 10;
|
||||||
TABLE = 11;
|
TABLE = 11;
|
||||||
TEXT = 12;
|
EMBEDDED_CONTENT = 12;
|
||||||
BOLD = 13;
|
TEXT = 13;
|
||||||
ITALIC = 14;
|
BOLD = 14;
|
||||||
BOLD_ITALIC = 15;
|
ITALIC = 15;
|
||||||
CODE = 16;
|
BOLD_ITALIC = 16;
|
||||||
IMAGE = 17;
|
CODE = 17;
|
||||||
LINK = 18;
|
IMAGE = 18;
|
||||||
AUTO_LINK = 19;
|
LINK = 19;
|
||||||
TAG = 20;
|
AUTO_LINK = 20;
|
||||||
STRIKETHROUGH = 21;
|
TAG = 21;
|
||||||
ESCAPING_CHARACTER = 22;
|
STRIKETHROUGH = 22;
|
||||||
MATH = 23;
|
ESCAPING_CHARACTER = 23;
|
||||||
HIGHLIGHT = 24;
|
MATH = 24;
|
||||||
SUBSCRIPT = 25;
|
HIGHLIGHT = 25;
|
||||||
SUPERSCRIPT = 26;
|
SUBSCRIPT = 26;
|
||||||
|
SUPERSCRIPT = 27;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Node {
|
message Node {
|
||||||
@ -67,21 +68,22 @@ message Node {
|
|||||||
TaskListNode task_list_node = 10;
|
TaskListNode task_list_node = 10;
|
||||||
MathBlockNode math_block_node = 11;
|
MathBlockNode math_block_node = 11;
|
||||||
TableNode table_node = 12;
|
TableNode table_node = 12;
|
||||||
TextNode text_node = 13;
|
EmbeddedContentNode embedded_content_node = 13;
|
||||||
BoldNode bold_node = 14;
|
TextNode text_node = 14;
|
||||||
ItalicNode italic_node = 15;
|
BoldNode bold_node = 15;
|
||||||
BoldItalicNode bold_italic_node = 16;
|
ItalicNode italic_node = 16;
|
||||||
CodeNode code_node = 17;
|
BoldItalicNode bold_italic_node = 17;
|
||||||
ImageNode image_node = 18;
|
CodeNode code_node = 18;
|
||||||
LinkNode link_node = 19;
|
ImageNode image_node = 19;
|
||||||
AutoLinkNode auto_link_node = 20;
|
LinkNode link_node = 20;
|
||||||
TagNode tag_node = 21;
|
AutoLinkNode auto_link_node = 21;
|
||||||
StrikethroughNode strikethrough_node = 22;
|
TagNode tag_node = 22;
|
||||||
EscapingCharacterNode escaping_character_node = 23;
|
StrikethroughNode strikethrough_node = 23;
|
||||||
MathNode math_node = 24;
|
EscapingCharacterNode escaping_character_node = 24;
|
||||||
HighlightNode highlight_node = 25;
|
MathNode math_node = 25;
|
||||||
SubscriptNode subscript_node = 26;
|
HighlightNode highlight_node = 26;
|
||||||
SuperscriptNode superscript_node = 27;
|
SubscriptNode subscript_node = 27;
|
||||||
|
SuperscriptNode superscript_node = 28;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +144,10 @@ message TableNode {
|
|||||||
repeated Row rows = 3;
|
repeated Row rows = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message EmbeddedContentNode {
|
||||||
|
string resource_name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message TextNode {
|
message TextNode {
|
||||||
string content = 1;
|
string content = 1;
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,7 @@
|
|||||||
- [BoldNode](#memos-api-v2-BoldNode)
|
- [BoldNode](#memos-api-v2-BoldNode)
|
||||||
- [CodeBlockNode](#memos-api-v2-CodeBlockNode)
|
- [CodeBlockNode](#memos-api-v2-CodeBlockNode)
|
||||||
- [CodeNode](#memos-api-v2-CodeNode)
|
- [CodeNode](#memos-api-v2-CodeNode)
|
||||||
|
- [EmbeddedContentNode](#memos-api-v2-EmbeddedContentNode)
|
||||||
- [EscapingCharacterNode](#memos-api-v2-EscapingCharacterNode)
|
- [EscapingCharacterNode](#memos-api-v2-EscapingCharacterNode)
|
||||||
- [HeadingNode](#memos-api-v2-HeadingNode)
|
- [HeadingNode](#memos-api-v2-HeadingNode)
|
||||||
- [HighlightNode](#memos-api-v2-HighlightNode)
|
- [HighlightNode](#memos-api-v2-HighlightNode)
|
||||||
@ -1058,6 +1059,21 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="memos-api-v2-EmbeddedContentNode"></a>
|
||||||
|
|
||||||
|
### EmbeddedContentNode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Type | Label | Description |
|
||||||
|
| ----- | ---- | ----- | ----------- |
|
||||||
|
| resource_name | [string](#string) | | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="memos-api-v2-EscapingCharacterNode"></a>
|
<a name="memos-api-v2-EscapingCharacterNode"></a>
|
||||||
|
|
||||||
### EscapingCharacterNode
|
### EscapingCharacterNode
|
||||||
@ -1227,6 +1243,7 @@
|
|||||||
| task_list_node | [TaskListNode](#memos-api-v2-TaskListNode) | | |
|
| task_list_node | [TaskListNode](#memos-api-v2-TaskListNode) | | |
|
||||||
| math_block_node | [MathBlockNode](#memos-api-v2-MathBlockNode) | | |
|
| math_block_node | [MathBlockNode](#memos-api-v2-MathBlockNode) | | |
|
||||||
| table_node | [TableNode](#memos-api-v2-TableNode) | | |
|
| table_node | [TableNode](#memos-api-v2-TableNode) | | |
|
||||||
|
| embedded_content_node | [EmbeddedContentNode](#memos-api-v2-EmbeddedContentNode) | | |
|
||||||
| text_node | [TextNode](#memos-api-v2-TextNode) | | |
|
| text_node | [TextNode](#memos-api-v2-TextNode) | | |
|
||||||
| bold_node | [BoldNode](#memos-api-v2-BoldNode) | | |
|
| bold_node | [BoldNode](#memos-api-v2-BoldNode) | | |
|
||||||
| italic_node | [ItalicNode](#memos-api-v2-ItalicNode) | | |
|
| italic_node | [ItalicNode](#memos-api-v2-ItalicNode) | | |
|
||||||
@ -1473,21 +1490,22 @@
|
|||||||
| TASK_LIST | 9 | |
|
| TASK_LIST | 9 | |
|
||||||
| MATH_BLOCK | 10 | |
|
| MATH_BLOCK | 10 | |
|
||||||
| TABLE | 11 | |
|
| TABLE | 11 | |
|
||||||
| TEXT | 12 | |
|
| EMBEDDED_CONTENT | 12 | |
|
||||||
| BOLD | 13 | |
|
| TEXT | 13 | |
|
||||||
| ITALIC | 14 | |
|
| BOLD | 14 | |
|
||||||
| BOLD_ITALIC | 15 | |
|
| ITALIC | 15 | |
|
||||||
| CODE | 16 | |
|
| BOLD_ITALIC | 16 | |
|
||||||
| IMAGE | 17 | |
|
| CODE | 17 | |
|
||||||
| LINK | 18 | |
|
| IMAGE | 18 | |
|
||||||
| AUTO_LINK | 19 | |
|
| LINK | 19 | |
|
||||||
| TAG | 20 | |
|
| AUTO_LINK | 20 | |
|
||||||
| STRIKETHROUGH | 21 | |
|
| TAG | 21 | |
|
||||||
| ESCAPING_CHARACTER | 22 | |
|
| STRIKETHROUGH | 22 | |
|
||||||
| MATH | 23 | |
|
| ESCAPING_CHARACTER | 23 | |
|
||||||
| HIGHLIGHT | 24 | |
|
| MATH | 24 | |
|
||||||
| SUBSCRIPT | 25 | |
|
| HIGHLIGHT | 25 | |
|
||||||
| SUPERSCRIPT | 26 | |
|
| SUBSCRIPT | 26 | |
|
||||||
|
| SUPERSCRIPT | 27 | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,32 @@
|
|||||||
|
import { useContext, useEffect } from "react";
|
||||||
|
import { useMemoStore } from "@/store/v1";
|
||||||
|
import MemoContent from "..";
|
||||||
|
import { RendererContext } from "../types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
memoId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmbeddedMemo = ({ memoId }: Props) => {
|
||||||
|
const context = useContext(RendererContext);
|
||||||
|
const memoStore = useMemoStore();
|
||||||
|
const memo = memoStore.getMemoById(memoId);
|
||||||
|
const resourceName = `memos/${memoId}`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
memoStore.getOrFetchMemoById(memoId);
|
||||||
|
}, [memoId]);
|
||||||
|
|
||||||
|
if (memoId === context.memoId || context.embeddedMemos.has(resourceName)) {
|
||||||
|
return <p>Nested Rendering Error: {`![[${resourceName}]]`}</p>;
|
||||||
|
}
|
||||||
|
context.embeddedMemos.add(resourceName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="embedded-memo">
|
||||||
|
<MemoContent nodes={memo.nodes} memoId={memoId} embeddedMemos={context.embeddedMemos} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmbeddedMemo;
|
20
web/src/components/MemoContent/EmbeddedContent/index.tsx
Normal file
20
web/src/components/MemoContent/EmbeddedContent/index.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import EmbeddedMemo from "./EmbeddedMemo";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
resourceName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractResourceTypeAndId = (resourceName: string) => {
|
||||||
|
const [resourceType, resourceId] = resourceName.split("/");
|
||||||
|
return { resourceType, resourceId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmbeddedContent = ({ resourceName }: Props) => {
|
||||||
|
const { resourceType, resourceId } = extractResourceTypeAndId(resourceName);
|
||||||
|
if (resourceType === "memos") {
|
||||||
|
return <EmbeddedMemo memoId={Number(resourceId)} />;
|
||||||
|
}
|
||||||
|
return <p>Unknown resource: {resourceName}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmbeddedContent;
|
@ -5,6 +5,7 @@ import {
|
|||||||
BoldNode,
|
BoldNode,
|
||||||
CodeBlockNode,
|
CodeBlockNode,
|
||||||
CodeNode,
|
CodeNode,
|
||||||
|
EmbeddedContentNode,
|
||||||
EscapingCharacterNode,
|
EscapingCharacterNode,
|
||||||
HeadingNode,
|
HeadingNode,
|
||||||
HighlightNode,
|
HighlightNode,
|
||||||
@ -31,6 +32,7 @@ import Bold from "./Bold";
|
|||||||
import BoldItalic from "./BoldItalic";
|
import BoldItalic from "./BoldItalic";
|
||||||
import Code from "./Code";
|
import Code from "./Code";
|
||||||
import CodeBlock from "./CodeBlock";
|
import CodeBlock from "./CodeBlock";
|
||||||
|
import EmbeddedContent from "./EmbeddedContent";
|
||||||
import EscapingCharacter from "./EscapingCharacter";
|
import EscapingCharacter from "./EscapingCharacter";
|
||||||
import Heading from "./Heading";
|
import Heading from "./Heading";
|
||||||
import Highlight from "./Highlight";
|
import Highlight from "./Highlight";
|
||||||
@ -80,6 +82,8 @@ const Renderer: React.FC<Props> = ({ index, node }: Props) => {
|
|||||||
return <Math {...(node.mathBlockNode as MathNode)} block={true} />;
|
return <Math {...(node.mathBlockNode as MathNode)} block={true} />;
|
||||||
case NodeType.TABLE:
|
case NodeType.TABLE:
|
||||||
return <Table {...(node.tableNode as TableNode)} />;
|
return <Table {...(node.tableNode as TableNode)} />;
|
||||||
|
case NodeType.EMBEDDED_CONTENT:
|
||||||
|
return <EmbeddedContent {...(node.embeddedContentNode as EmbeddedContentNode)} />;
|
||||||
case NodeType.TEXT:
|
case NodeType.TEXT:
|
||||||
return <Text {...(node.textNode as TextNode)} />;
|
return <Text {...(node.textNode as TextNode)} />;
|
||||||
case NodeType.BOLD:
|
case NodeType.BOLD:
|
||||||
|
@ -10,12 +10,15 @@ interface Props {
|
|||||||
memoId?: number;
|
memoId?: number;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
disableFilter?: boolean;
|
disableFilter?: boolean;
|
||||||
|
// embeddedMemos is a set of memo resource names that are embedded in the current memo.
|
||||||
|
// This is used to prevent infinite loops when a memo embeds itself.
|
||||||
|
embeddedMemos?: Set<string>;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (e: React.MouseEvent) => void;
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoContent: React.FC<Props> = (props: Props) => {
|
const MemoContent: React.FC<Props> = (props: Props) => {
|
||||||
const { className, memoId, nodes, onClick } = props;
|
const { className, memoId, nodes, embeddedMemos, onClick } = props;
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -37,6 +40,7 @@ const MemoContent: React.FC<Props> = (props: Props) => {
|
|||||||
memoId,
|
memoId,
|
||||||
readonly: !allowEdit,
|
readonly: !allowEdit,
|
||||||
disableFilter: props.disableFilter,
|
disableFilter: props.disableFilter,
|
||||||
|
embeddedMemos: embeddedMemos || new Set(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={`w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300 ${className || ""}`}>
|
<div className={`w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300 ${className || ""}`}>
|
||||||
|
@ -3,6 +3,9 @@ import { Node } from "@/types/proto/api/v2/markdown_service";
|
|||||||
|
|
||||||
interface Context {
|
interface Context {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
|
// embeddedMemos is a set of memo resource names that are embedded in the current memo.
|
||||||
|
// This is used to prevent infinite loops when a memo embeds itself.
|
||||||
|
embeddedMemos: Set<string>;
|
||||||
memoId?: number;
|
memoId?: number;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
disableFilter?: boolean;
|
disableFilter?: boolean;
|
||||||
@ -10,4 +13,5 @@ interface Context {
|
|||||||
|
|
||||||
export const RendererContext = createContext<Context>({
|
export const RendererContext = createContext<Context>({
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
embeddedMemos: new Set(),
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user