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}}
|
||||
case *ast.Table:
|
||||
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:
|
||||
node.Node = &apiv2pb.Node_TextNode{TextNode: &apiv2pb.TextNode{Content: n.Content}}
|
||||
case *ast.Bold:
|
||||
@ -142,6 +144,8 @@ func convertToASTNode(node *apiv2pb.Node) ast.Node {
|
||||
return &ast.MathBlock{Content: n.MathBlockNode.Content}
|
||||
case *apiv2pb.Node_TableNode:
|
||||
return convertTableToASTNode(node)
|
||||
case *apiv2pb.Node_EmbeddedContentNode:
|
||||
return &ast.EmbeddedContent{ResourceName: n.EmbeddedContentNode.ResourceName}
|
||||
case *apiv2pb.Node_TextNode:
|
||||
return &ast.Text{Content: n.TextNode.Content}
|
||||
case *apiv2pb.Node_BoldNode:
|
||||
|
@ -16,6 +16,7 @@ const (
|
||||
TaskListNode
|
||||
MathBlockNode
|
||||
TableNode
|
||||
EmbeddedContentNode
|
||||
// Inline nodes.
|
||||
TextNode
|
||||
BoldNode
|
||||
|
@ -228,3 +228,17 @@ func (n *Table) Restore() string {
|
||||
}
|
||||
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(),
|
||||
NewOrderedListParser(),
|
||||
NewMathBlockParser(),
|
||||
NewEmbeddedContentParser(),
|
||||
NewParagraphParser(),
|
||||
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 {
|
||||
|
@ -39,9 +39,11 @@ func (*TableParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||
rowTokens := []*tokenizer.Token{}
|
||||
for index, token := range tokens[len(headerTokens)+len(delimiterTokens)+2:] {
|
||||
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 {
|
||||
if (temp == len(tokens)-1) || (temp+1 == len(tokens)-1 && tokens[temp+1].Type == tokenizer.Newline) {
|
||||
break
|
||||
}
|
||||
}
|
||||
rowTokens = append(rowTokens, token)
|
||||
}
|
||||
if len(rowTokens) < 5 {
|
||||
@ -65,7 +67,18 @@ func (*TableParser) Match(tokens []*tokenizer.Token) (int, bool) {
|
||||
if delimiterCells != headerCells || !ok {
|
||||
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]
|
||||
if len(delimiterTokens) < 3 {
|
||||
return 0, false
|
||||
@ -112,15 +125,16 @@ func (p *TableParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
|
||||
delimiter := 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]))
|
||||
}
|
||||
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]))
|
||||
}
|
||||
for _, row := range rowTokens {
|
||||
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]))
|
||||
}
|
||||
rows = append(rows, cells)
|
||||
@ -145,10 +159,13 @@ func matchTableCellTokens(tokens []*tokenizer.Token) (int, bool) {
|
||||
}
|
||||
}
|
||||
cells := tokenizer.Split(tokens, tokenizer.Pipe)
|
||||
if len(cells) != pipes-1 {
|
||||
if len(cells) != pipes+1 {
|
||||
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 {
|
||||
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 {
|
||||
if len(tokens) == 0 {
|
||||
return [][]*Token{}
|
||||
}
|
||||
|
||||
result := make([][]*Token, 0)
|
||||
current := make([]*Token, 0)
|
||||
for _, token := range tokens {
|
||||
if token.Type == delimiter {
|
||||
if len(current) > 0 {
|
||||
result = append(result, current)
|
||||
current = make([]*Token, 0)
|
||||
}
|
||||
} else {
|
||||
current = append(current, token)
|
||||
}
|
||||
}
|
||||
if len(current) > 0 {
|
||||
result = append(result, current)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -77,3 +77,144 @@ func TestTokenize(t *testing.T) {
|
||||
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;
|
||||
MATH_BLOCK = 10;
|
||||
TABLE = 11;
|
||||
TEXT = 12;
|
||||
BOLD = 13;
|
||||
ITALIC = 14;
|
||||
BOLD_ITALIC = 15;
|
||||
CODE = 16;
|
||||
IMAGE = 17;
|
||||
LINK = 18;
|
||||
AUTO_LINK = 19;
|
||||
TAG = 20;
|
||||
STRIKETHROUGH = 21;
|
||||
ESCAPING_CHARACTER = 22;
|
||||
MATH = 23;
|
||||
HIGHLIGHT = 24;
|
||||
SUBSCRIPT = 25;
|
||||
SUPERSCRIPT = 26;
|
||||
EMBEDDED_CONTENT = 12;
|
||||
TEXT = 13;
|
||||
BOLD = 14;
|
||||
ITALIC = 15;
|
||||
BOLD_ITALIC = 16;
|
||||
CODE = 17;
|
||||
IMAGE = 18;
|
||||
LINK = 19;
|
||||
AUTO_LINK = 20;
|
||||
TAG = 21;
|
||||
STRIKETHROUGH = 22;
|
||||
ESCAPING_CHARACTER = 23;
|
||||
MATH = 24;
|
||||
HIGHLIGHT = 25;
|
||||
SUBSCRIPT = 26;
|
||||
SUPERSCRIPT = 27;
|
||||
}
|
||||
|
||||
message Node {
|
||||
@ -67,21 +68,22 @@ message Node {
|
||||
TaskListNode task_list_node = 10;
|
||||
MathBlockNode math_block_node = 11;
|
||||
TableNode table_node = 12;
|
||||
TextNode text_node = 13;
|
||||
BoldNode bold_node = 14;
|
||||
ItalicNode italic_node = 15;
|
||||
BoldItalicNode bold_italic_node = 16;
|
||||
CodeNode code_node = 17;
|
||||
ImageNode image_node = 18;
|
||||
LinkNode link_node = 19;
|
||||
AutoLinkNode auto_link_node = 20;
|
||||
TagNode tag_node = 21;
|
||||
StrikethroughNode strikethrough_node = 22;
|
||||
EscapingCharacterNode escaping_character_node = 23;
|
||||
MathNode math_node = 24;
|
||||
HighlightNode highlight_node = 25;
|
||||
SubscriptNode subscript_node = 26;
|
||||
SuperscriptNode superscript_node = 27;
|
||||
EmbeddedContentNode embedded_content_node = 13;
|
||||
TextNode text_node = 14;
|
||||
BoldNode bold_node = 15;
|
||||
ItalicNode italic_node = 16;
|
||||
BoldItalicNode bold_italic_node = 17;
|
||||
CodeNode code_node = 18;
|
||||
ImageNode image_node = 19;
|
||||
LinkNode link_node = 20;
|
||||
AutoLinkNode auto_link_node = 21;
|
||||
TagNode tag_node = 22;
|
||||
StrikethroughNode strikethrough_node = 23;
|
||||
EscapingCharacterNode escaping_character_node = 24;
|
||||
MathNode math_node = 25;
|
||||
HighlightNode highlight_node = 26;
|
||||
SubscriptNode subscript_node = 27;
|
||||
SuperscriptNode superscript_node = 28;
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,6 +144,10 @@ message TableNode {
|
||||
repeated Row rows = 3;
|
||||
}
|
||||
|
||||
message EmbeddedContentNode {
|
||||
string resource_name = 1;
|
||||
}
|
||||
|
||||
message TextNode {
|
||||
string content = 1;
|
||||
}
|
||||
|
@ -72,6 +72,7 @@
|
||||
- [BoldNode](#memos-api-v2-BoldNode)
|
||||
- [CodeBlockNode](#memos-api-v2-CodeBlockNode)
|
||||
- [CodeNode](#memos-api-v2-CodeNode)
|
||||
- [EmbeddedContentNode](#memos-api-v2-EmbeddedContentNode)
|
||||
- [EscapingCharacterNode](#memos-api-v2-EscapingCharacterNode)
|
||||
- [HeadingNode](#memos-api-v2-HeadingNode)
|
||||
- [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>
|
||||
|
||||
### EscapingCharacterNode
|
||||
@ -1227,6 +1243,7 @@
|
||||
| task_list_node | [TaskListNode](#memos-api-v2-TaskListNode) | | |
|
||||
| math_block_node | [MathBlockNode](#memos-api-v2-MathBlockNode) | | |
|
||||
| table_node | [TableNode](#memos-api-v2-TableNode) | | |
|
||||
| embedded_content_node | [EmbeddedContentNode](#memos-api-v2-EmbeddedContentNode) | | |
|
||||
| text_node | [TextNode](#memos-api-v2-TextNode) | | |
|
||||
| bold_node | [BoldNode](#memos-api-v2-BoldNode) | | |
|
||||
| italic_node | [ItalicNode](#memos-api-v2-ItalicNode) | | |
|
||||
@ -1473,21 +1490,22 @@
|
||||
| TASK_LIST | 9 | |
|
||||
| MATH_BLOCK | 10 | |
|
||||
| TABLE | 11 | |
|
||||
| TEXT | 12 | |
|
||||
| BOLD | 13 | |
|
||||
| ITALIC | 14 | |
|
||||
| BOLD_ITALIC | 15 | |
|
||||
| CODE | 16 | |
|
||||
| IMAGE | 17 | |
|
||||
| LINK | 18 | |
|
||||
| AUTO_LINK | 19 | |
|
||||
| TAG | 20 | |
|
||||
| STRIKETHROUGH | 21 | |
|
||||
| ESCAPING_CHARACTER | 22 | |
|
||||
| MATH | 23 | |
|
||||
| HIGHLIGHT | 24 | |
|
||||
| SUBSCRIPT | 25 | |
|
||||
| SUPERSCRIPT | 26 | |
|
||||
| EMBEDDED_CONTENT | 12 | |
|
||||
| TEXT | 13 | |
|
||||
| BOLD | 14 | |
|
||||
| ITALIC | 15 | |
|
||||
| BOLD_ITALIC | 16 | |
|
||||
| CODE | 17 | |
|
||||
| IMAGE | 18 | |
|
||||
| LINK | 19 | |
|
||||
| AUTO_LINK | 20 | |
|
||||
| TAG | 21 | |
|
||||
| STRIKETHROUGH | 22 | |
|
||||
| ESCAPING_CHARACTER | 23 | |
|
||||
| MATH | 24 | |
|
||||
| HIGHLIGHT | 25 | |
|
||||
| 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,
|
||||
CodeBlockNode,
|
||||
CodeNode,
|
||||
EmbeddedContentNode,
|
||||
EscapingCharacterNode,
|
||||
HeadingNode,
|
||||
HighlightNode,
|
||||
@ -31,6 +32,7 @@ import Bold from "./Bold";
|
||||
import BoldItalic from "./BoldItalic";
|
||||
import Code from "./Code";
|
||||
import CodeBlock from "./CodeBlock";
|
||||
import EmbeddedContent from "./EmbeddedContent";
|
||||
import EscapingCharacter from "./EscapingCharacter";
|
||||
import Heading from "./Heading";
|
||||
import Highlight from "./Highlight";
|
||||
@ -80,6 +82,8 @@ const Renderer: React.FC<Props> = ({ index, node }: Props) => {
|
||||
return <Math {...(node.mathBlockNode as MathNode)} block={true} />;
|
||||
case NodeType.TABLE:
|
||||
return <Table {...(node.tableNode as TableNode)} />;
|
||||
case NodeType.EMBEDDED_CONTENT:
|
||||
return <EmbeddedContent {...(node.embeddedContentNode as EmbeddedContentNode)} />;
|
||||
case NodeType.TEXT:
|
||||
return <Text {...(node.textNode as TextNode)} />;
|
||||
case NodeType.BOLD:
|
||||
|
@ -10,12 +10,15 @@ interface Props {
|
||||
memoId?: number;
|
||||
readonly?: 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;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const MemoContent: React.FC<Props> = (props: Props) => {
|
||||
const { className, memoId, nodes, onClick } = props;
|
||||
const { className, memoId, nodes, embeddedMemos, onClick } = props;
|
||||
const currentUser = useCurrentUser();
|
||||
const memoStore = useMemoStore();
|
||||
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
||||
@ -37,6 +40,7 @@ const MemoContent: React.FC<Props> = (props: Props) => {
|
||||
memoId,
|
||||
readonly: !allowEdit,
|
||||
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 || ""}`}>
|
||||
|
@ -3,6 +3,9 @@ import { Node } from "@/types/proto/api/v2/markdown_service";
|
||||
|
||||
interface Context {
|
||||
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;
|
||||
readonly?: boolean;
|
||||
disableFilter?: boolean;
|
||||
@ -10,4 +13,5 @@ interface Context {
|
||||
|
||||
export const RendererContext = createContext<Context>({
|
||||
nodes: [],
|
||||
embeddedMemos: new Set(),
|
||||
});
|
||||
|
Reference in New Issue
Block a user