Commit c8d7f93d authored by Steven's avatar Steven

feat: implement auto link parser

parent 6fac116d
...@@ -76,6 +76,8 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node { ...@@ -76,6 +76,8 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node {
node.Node = &apiv2pb.Node_ImageNode{ImageNode: &apiv2pb.ImageNode{AltText: n.AltText, Url: n.URL}} node.Node = &apiv2pb.Node_ImageNode{ImageNode: &apiv2pb.ImageNode{AltText: n.AltText, Url: n.URL}}
case *ast.Link: case *ast.Link:
node.Node = &apiv2pb.Node_LinkNode{LinkNode: &apiv2pb.LinkNode{Text: n.Text, Url: n.URL}} node.Node = &apiv2pb.Node_LinkNode{LinkNode: &apiv2pb.LinkNode{Text: n.Text, Url: n.URL}}
case *ast.AutoLink:
node.Node = &apiv2pb.Node_AutoLinkNode{AutoLinkNode: &apiv2pb.AutoLinkNode{Url: n.URL}}
case *ast.Tag: case *ast.Tag:
node.Node = &apiv2pb.Node_TagNode{TagNode: &apiv2pb.TagNode{Content: n.Content}} node.Node = &apiv2pb.Node_TagNode{TagNode: &apiv2pb.TagNode{Content: n.Content}}
case *ast.Strikethrough: case *ast.Strikethrough:
......
...@@ -22,6 +22,7 @@ const ( ...@@ -22,6 +22,7 @@ const (
CodeNode CodeNode
ImageNode ImageNode
LinkNode LinkNode
AutoLinkNode
TagNode TagNode
StrikethroughNode StrikethroughNode
EscapingCharacterNode EscapingCharacterNode
...@@ -61,6 +62,8 @@ func (t NodeType) String() string { ...@@ -61,6 +62,8 @@ func (t NodeType) String() string {
return "ImageNode" return "ImageNode"
case LinkNode: case LinkNode:
return "LinkNode" return "LinkNode"
case AutoLinkNode:
return "AutoLinkNode"
case TagNode: case TagNode:
return "TagNode" return "TagNode"
case StrikethroughNode: case StrikethroughNode:
......
...@@ -82,6 +82,16 @@ func (*Link) Type() NodeType { ...@@ -82,6 +82,16 @@ func (*Link) Type() NodeType {
return LinkNode return LinkNode
} }
type AutoLink struct {
BaseInline
URL string
}
func (*AutoLink) Type() NodeType {
return AutoLinkNode
}
type Tag struct { type Tag struct {
BaseInline BaseInline
......
package parser
import (
"errors"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type AutoLinkParser struct{}
func NewAutoLinkParser() *AutoLinkParser {
return &AutoLinkParser{}
}
func (*AutoLinkParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 3 {
return 0, false
}
if tokens[0].Type != tokenizer.LessThan {
return 0, false
}
urlTokens := []*tokenizer.Token{}
for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space {
return 0, false
}
if token.Type == tokenizer.GreaterThan {
break
}
urlTokens = append(urlTokens, token)
}
if 2+len(urlTokens) > len(tokens) {
return 0, false
}
return 2 + len(urlTokens), true
}
func (p *AutoLinkParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
size, ok := p.Match(tokens)
if size == 0 || !ok {
return nil, errors.New("not matched")
}
urlTokens := tokens[1 : size-1]
return &ast.AutoLink{
URL: tokenizer.Stringify(urlTokens),
}, nil
}
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 TestAutoLinkParser(t *testing.T) {
tests := []struct {
text string
link ast.Node
}{
{
text: "<https://example.com)",
link: nil,
},
{
text: "<https://example.com>",
link: &ast.AutoLink{
URL: "https://example.com",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
node, _ := NewAutoLinkParser().Parse(tokens)
require.Equal(t, StringifyNodes([]ast.Node{test.link}), StringifyNodes([]ast.Node{node}))
}
}
...@@ -76,6 +76,7 @@ var defaultInlineParsers = []InlineParser{ ...@@ -76,6 +76,7 @@ var defaultInlineParsers = []InlineParser{
NewBoldItalicParser(), NewBoldItalicParser(),
NewImageParser(), NewImageParser(),
NewLinkParser(), NewLinkParser(),
NewAutoLinkParser(),
NewBoldParser(), NewBoldParser(),
NewItalicParser(), NewItalicParser(),
NewCodeParser(), NewCodeParser(),
......
...@@ -250,6 +250,8 @@ func StringifyNode(node ast.Node) string { ...@@ -250,6 +250,8 @@ func StringifyNode(node ast.Node) string {
return "Image(" + n.URL + ", " + n.AltText + ")" return "Image(" + n.URL + ", " + n.AltText + ")"
case *ast.Link: case *ast.Link:
return "Link(" + n.Text + ", " + n.URL + ")" return "Link(" + n.Text + ", " + n.URL + ")"
case *ast.AutoLink:
return "AutoLink(" + n.URL + ")"
case *ast.Tag: case *ast.Tag:
return "Tag(" + n.Content + ")" return "Tag(" + n.Content + ")"
case *ast.Strikethrough: case *ast.Strikethrough:
......
...@@ -16,6 +16,7 @@ const ( ...@@ -16,6 +16,7 @@ const (
Hyphen TokenType = "-" Hyphen TokenType = "-"
PlusSign TokenType = "+" PlusSign TokenType = "+"
Dot TokenType = "." Dot TokenType = "."
LessThan TokenType = "<"
GreaterThan TokenType = ">" GreaterThan TokenType = ">"
Backslash TokenType = "\\" Backslash TokenType = "\\"
Newline TokenType = "\n" Newline TokenType = "\n"
...@@ -65,6 +66,8 @@ func Tokenize(text string) []*Token { ...@@ -65,6 +66,8 @@ func Tokenize(text string) []*Token {
tokens = append(tokens, NewToken(Tilde, "~")) tokens = append(tokens, NewToken(Tilde, "~"))
case '-': case '-':
tokens = append(tokens, NewToken(Hyphen, "-")) tokens = append(tokens, NewToken(Hyphen, "-"))
case '<':
tokens = append(tokens, NewToken(LessThan, "<"))
case '>': case '>':
tokens = append(tokens, NewToken(GreaterThan, ">")) tokens = append(tokens, NewToken(GreaterThan, ">"))
case '+': case '+':
......
...@@ -41,9 +41,10 @@ enum NodeType { ...@@ -41,9 +41,10 @@ enum NodeType {
CODE = 14; CODE = 14;
IMAGE = 15; IMAGE = 15;
LINK = 16; LINK = 16;
TAG = 17; AUTO_LINK = 17;
STRIKETHROUGH = 18; TAG = 18;
ESCAPING_CHARACTER = 19; STRIKETHROUGH = 19;
ESCAPING_CHARACTER = 20;
} }
message Node { message Node {
...@@ -65,9 +66,10 @@ message Node { ...@@ -65,9 +66,10 @@ message Node {
CodeNode code_node = 15; CodeNode code_node = 15;
ImageNode image_node = 16; ImageNode image_node = 16;
LinkNode link_node = 17; LinkNode link_node = 17;
TagNode tag_node = 18; AutoLinkNode auto_link_node = 18;
StrikethroughNode strikethrough_node = 19; TagNode tag_node = 19;
EscapingCharacterNode escaping_character_node = 20; StrikethroughNode strikethrough_node = 20;
EscapingCharacterNode escaping_character_node = 21;
} }
} }
...@@ -144,6 +146,10 @@ message LinkNode { ...@@ -144,6 +146,10 @@ message LinkNode {
string url = 2; string url = 2;
} }
message AutoLinkNode {
string url = 1;
}
message TagNode { message TagNode {
string content = 1; string content = 1;
} }
......
...@@ -66,6 +66,7 @@ ...@@ -66,6 +66,7 @@
- [InboxService](#memos-api-v2-InboxService) - [InboxService](#memos-api-v2-InboxService)
- [api/v2/markdown_service.proto](#api_v2_markdown_service-proto) - [api/v2/markdown_service.proto](#api_v2_markdown_service-proto)
- [AutoLinkNode](#memos-api-v2-AutoLinkNode)
- [BlockquoteNode](#memos-api-v2-BlockquoteNode) - [BlockquoteNode](#memos-api-v2-BlockquoteNode)
- [BoldItalicNode](#memos-api-v2-BoldItalicNode) - [BoldItalicNode](#memos-api-v2-BoldItalicNode)
- [BoldNode](#memos-api-v2-BoldNode) - [BoldNode](#memos-api-v2-BoldNode)
...@@ -956,6 +957,21 @@ ...@@ -956,6 +957,21 @@
<a name="memos-api-v2-AutoLinkNode"></a>
### AutoLinkNode
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| url | [string](#string) | | |
<a name="memos-api-v2-BlockquoteNode"></a> <a name="memos-api-v2-BlockquoteNode"></a>
### BlockquoteNode ### BlockquoteNode
...@@ -1163,6 +1179,7 @@ ...@@ -1163,6 +1179,7 @@
| code_node | [CodeNode](#memos-api-v2-CodeNode) | | | | code_node | [CodeNode](#memos-api-v2-CodeNode) | | |
| image_node | [ImageNode](#memos-api-v2-ImageNode) | | | | image_node | [ImageNode](#memos-api-v2-ImageNode) | | |
| link_node | [LinkNode](#memos-api-v2-LinkNode) | | | | link_node | [LinkNode](#memos-api-v2-LinkNode) | | |
| auto_link_node | [AutoLinkNode](#memos-api-v2-AutoLinkNode) | | |
| tag_node | [TagNode](#memos-api-v2-TagNode) | | | | tag_node | [TagNode](#memos-api-v2-TagNode) | | |
| strikethrough_node | [StrikethroughNode](#memos-api-v2-StrikethroughNode) | | | | strikethrough_node | [StrikethroughNode](#memos-api-v2-StrikethroughNode) | | |
| escaping_character_node | [EscapingCharacterNode](#memos-api-v2-EscapingCharacterNode) | | | | escaping_character_node | [EscapingCharacterNode](#memos-api-v2-EscapingCharacterNode) | | |
...@@ -1337,9 +1354,10 @@ ...@@ -1337,9 +1354,10 @@
| CODE | 14 | | | CODE | 14 | |
| IMAGE | 15 | | | IMAGE | 15 | |
| LINK | 16 | | | LINK | 16 | |
| TAG | 17 | | | AUTO_LINK | 17 | |
| STRIKETHROUGH | 18 | | | TAG | 18 | |
| ESCAPING_CHARACTER | 19 | | | STRIKETHROUGH | 19 | |
| ESCAPING_CHARACTER | 20 | |
......
This diff is collapsed.
interface Props {
url: string;
}
const AutoLink: React.FC<Props> = ({ url }: Props) => {
return (
<a
className="text-blue-600 dark:text-blue-400 cursor-pointer underline break-all hover:opacity-80 decoration-1"
href={url}
target="_blank"
>
{url}
</a>
);
};
export default AutoLink;
...@@ -5,7 +5,11 @@ interface Props { ...@@ -5,7 +5,11 @@ interface Props {
const Link: React.FC<Props> = ({ text, url }: Props) => { const Link: React.FC<Props> = ({ text, url }: Props) => {
return ( return (
<a className="text-blue-600 dark:text-blue-400 cursor-pointer underline break-all hover:opacity-80 decoration-1" href={url}> <a
className="text-blue-600 dark:text-blue-400 cursor-pointer underline break-all hover:opacity-80 decoration-1"
href={url}
target="_blank"
>
{text} {text}
</a> </a>
); );
......
import { import {
AutoLinkNode,
BlockquoteNode, BlockquoteNode,
BoldItalicNode, BoldItalicNode,
BoldNode, BoldNode,
...@@ -20,6 +21,7 @@ import { ...@@ -20,6 +21,7 @@ import {
TextNode, TextNode,
UnorderedListNode, UnorderedListNode,
} from "@/types/proto/api/v2/markdown_service"; } from "@/types/proto/api/v2/markdown_service";
import AutoLink from "./AutoLink";
import Blockquote from "./Blockquote"; import Blockquote from "./Blockquote";
import Bold from "./Bold"; import Bold from "./Bold";
import BoldItalic from "./BoldItalic"; import BoldItalic from "./BoldItalic";
...@@ -78,6 +80,8 @@ const Renderer: React.FC<Props> = ({ node }: Props) => { ...@@ -78,6 +80,8 @@ const Renderer: React.FC<Props> = ({ node }: Props) => {
return <Image {...(node.imageNode as ImageNode)} />; return <Image {...(node.imageNode as ImageNode)} />;
case NodeType.LINK: case NodeType.LINK:
return <Link {...(node.linkNode as LinkNode)} />; return <Link {...(node.linkNode as LinkNode)} />;
case NodeType.AUTO_LINK:
return <AutoLink {...(node.autoLinkNode as AutoLinkNode)} />;
case NodeType.TAG: case NodeType.TAG:
return <Tag {...(node.tagNode as TagNode)} />; return <Tag {...(node.tagNode as TagNode)} />;
case NodeType.STRIKETHROUGH: case NodeType.STRIKETHROUGH:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment