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 {
node.Node = &apiv2pb.Node_ImageNode{ImageNode: &apiv2pb.ImageNode{AltText: n.AltText, Url: n.URL}}
case *ast.Link:
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:
node.Node = &apiv2pb.Node_TagNode{TagNode: &apiv2pb.TagNode{Content: n.Content}}
case *ast.Strikethrough:
......
......@@ -22,6 +22,7 @@ const (
CodeNode
ImageNode
LinkNode
AutoLinkNode
TagNode
StrikethroughNode
EscapingCharacterNode
......@@ -61,6 +62,8 @@ func (t NodeType) String() string {
return "ImageNode"
case LinkNode:
return "LinkNode"
case AutoLinkNode:
return "AutoLinkNode"
case TagNode:
return "TagNode"
case StrikethroughNode:
......
......@@ -82,6 +82,16 @@ func (*Link) Type() NodeType {
return LinkNode
}
type AutoLink struct {
BaseInline
URL string
}
func (*AutoLink) Type() NodeType {
return AutoLinkNode
}
type Tag struct {
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{
NewBoldItalicParser(),
NewImageParser(),
NewLinkParser(),
NewAutoLinkParser(),
NewBoldParser(),
NewItalicParser(),
NewCodeParser(),
......
......@@ -250,6 +250,8 @@ func StringifyNode(node ast.Node) string {
return "Image(" + n.URL + ", " + n.AltText + ")"
case *ast.Link:
return "Link(" + n.Text + ", " + n.URL + ")"
case *ast.AutoLink:
return "AutoLink(" + n.URL + ")"
case *ast.Tag:
return "Tag(" + n.Content + ")"
case *ast.Strikethrough:
......
......@@ -16,6 +16,7 @@ const (
Hyphen TokenType = "-"
PlusSign TokenType = "+"
Dot TokenType = "."
LessThan TokenType = "<"
GreaterThan TokenType = ">"
Backslash TokenType = "\\"
Newline TokenType = "\n"
......@@ -65,6 +66,8 @@ func Tokenize(text string) []*Token {
tokens = append(tokens, NewToken(Tilde, "~"))
case '-':
tokens = append(tokens, NewToken(Hyphen, "-"))
case '<':
tokens = append(tokens, NewToken(LessThan, "<"))
case '>':
tokens = append(tokens, NewToken(GreaterThan, ">"))
case '+':
......
......@@ -41,9 +41,10 @@ enum NodeType {
CODE = 14;
IMAGE = 15;
LINK = 16;
TAG = 17;
STRIKETHROUGH = 18;
ESCAPING_CHARACTER = 19;
AUTO_LINK = 17;
TAG = 18;
STRIKETHROUGH = 19;
ESCAPING_CHARACTER = 20;
}
message Node {
......@@ -65,9 +66,10 @@ message Node {
CodeNode code_node = 15;
ImageNode image_node = 16;
LinkNode link_node = 17;
TagNode tag_node = 18;
StrikethroughNode strikethrough_node = 19;
EscapingCharacterNode escaping_character_node = 20;
AutoLinkNode auto_link_node = 18;
TagNode tag_node = 19;
StrikethroughNode strikethrough_node = 20;
EscapingCharacterNode escaping_character_node = 21;
}
}
......@@ -144,6 +146,10 @@ message LinkNode {
string url = 2;
}
message AutoLinkNode {
string url = 1;
}
message TagNode {
string content = 1;
}
......
......@@ -66,6 +66,7 @@
- [InboxService](#memos-api-v2-InboxService)
- [api/v2/markdown_service.proto](#api_v2_markdown_service-proto)
- [AutoLinkNode](#memos-api-v2-AutoLinkNode)
- [BlockquoteNode](#memos-api-v2-BlockquoteNode)
- [BoldItalicNode](#memos-api-v2-BoldItalicNode)
- [BoldNode](#memos-api-v2-BoldNode)
......@@ -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>
### BlockquoteNode
......@@ -1163,6 +1179,7 @@
| code_node | [CodeNode](#memos-api-v2-CodeNode) | | |
| image_node | [ImageNode](#memos-api-v2-ImageNode) | | |
| link_node | [LinkNode](#memos-api-v2-LinkNode) | | |
| auto_link_node | [AutoLinkNode](#memos-api-v2-AutoLinkNode) | | |
| tag_node | [TagNode](#memos-api-v2-TagNode) | | |
| strikethrough_node | [StrikethroughNode](#memos-api-v2-StrikethroughNode) | | |
| escaping_character_node | [EscapingCharacterNode](#memos-api-v2-EscapingCharacterNode) | | |
......@@ -1337,9 +1354,10 @@
| CODE | 14 | |
| IMAGE | 15 | |
| LINK | 16 | |
| TAG | 17 | |
| STRIKETHROUGH | 18 | |
| ESCAPING_CHARACTER | 19 | |
| AUTO_LINK | 17 | |
| TAG | 18 | |
| 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 {
const Link: React.FC<Props> = ({ text, url }: Props) => {
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}
</a>
);
......
import {
AutoLinkNode,
BlockquoteNode,
BoldItalicNode,
BoldNode,
......@@ -20,6 +21,7 @@ import {
TextNode,
UnorderedListNode,
} from "@/types/proto/api/v2/markdown_service";
import AutoLink from "./AutoLink";
import Blockquote from "./Blockquote";
import Bold from "./Bold";
import BoldItalic from "./BoldItalic";
......@@ -78,6 +80,8 @@ const Renderer: React.FC<Props> = ({ node }: Props) => {
return <Image {...(node.imageNode as ImageNode)} />;
case NodeType.LINK:
return <Link {...(node.linkNode as LinkNode)} />;
case NodeType.AUTO_LINK:
return <AutoLink {...(node.autoLinkNode as AutoLinkNode)} />;
case NodeType.TAG:
return <Tag {...(node.tagNode as TagNode)} />;
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