···3535 return KindAt
3636}
37373838-var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)`)
3838+var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\b)`)
3939var markdownLinkRegexp = regexp.MustCompile(`(?ms)\[.*\]\(.*\)`)
40404141type atParser struct{}
···5757 return nil
5858 }
59596060- if !util.IsSpaceRune(block.PrecendingCharacter()) {
6161- return nil
6262- }
6363-6460 // Check for all links in the markdown to see if the handle found is inside one
6561 linksIndexes := markdownLinkRegexp.FindAllIndex(block.Source(), -1)
6662 for _, linkMatch := range linksIndexes {
+121
appview/pages/markup/markdown_test.go
···11+package markup
22+33+import (
44+ "bytes"
55+ "testing"
66+)
77+88+func TestAtExtension_Rendering(t *testing.T) {
99+ tests := []struct {
1010+ name string
1111+ markdown string
1212+ expected string
1313+ }{
1414+ {
1515+ name: "renders simple at mention",
1616+ markdown: "Hello @user.tngl.sh!",
1717+ expected: `<p>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>!</p>`,
1818+ },
1919+ {
2020+ name: "renders multiple at mentions",
2121+ markdown: "Hi @alice.tngl.sh and @bob.example.com",
2222+ expected: `<p>Hi <a href="/alice.tngl.sh" class="mention">@alice.tngl.sh</a> and <a href="/bob.example.com" class="mention">@bob.example.com</a></p>`,
2323+ },
2424+ {
2525+ name: "renders at mention in parentheses",
2626+ markdown: "Check this out (@user.tngl.sh)",
2727+ expected: `<p>Check this out (<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>)</p>`,
2828+ },
2929+ {
3030+ name: "does not render email",
3131+ markdown: "Contact me at test@example.com",
3232+ expected: `<p>Contact me at <a href="mailto:test@example.com">test@example.com</a></p>`,
3333+ },
3434+ {
3535+ name: "renders at mention with hyphen",
3636+ markdown: "Follow @user-name.tngl.sh",
3737+ expected: `<p>Follow <a href="/user-name.tngl.sh" class="mention">@user-name.tngl.sh</a></p>`,
3838+ },
3939+ {
4040+ name: "renders at mention with numbers",
4141+ markdown: "@user123.test456.social",
4242+ expected: `<p><a href="/user123.test456.social" class="mention">@user123.test456.social</a></p>`,
4343+ },
4444+ {
4545+ name: "at mention at start of line",
4646+ markdown: "@user.tngl.sh is cool",
4747+ expected: `<p><a href="/user.tngl.sh" class="mention">@user.tngl.sh</a> is cool</p>`,
4848+ },
4949+ }
5050+5151+ for _, tt := range tests {
5252+ t.Run(tt.name, func(t *testing.T) {
5353+ md := NewMarkdown()
5454+5555+ var buf bytes.Buffer
5656+ if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
5757+ t.Fatalf("failed to convert markdown: %v", err)
5858+ }
5959+6060+ result := buf.String()
6161+ if result != tt.expected+"\n" {
6262+ t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, result)
6363+ }
6464+ })
6565+ }
6666+}
6767+6868+func TestAtExtension_WithOtherMarkdown(t *testing.T) {
6969+ tests := []struct {
7070+ name string
7171+ markdown string
7272+ contains string
7373+ }{
7474+ {
7575+ name: "at mention with bold",
7676+ markdown: "**Hello @user.tngl.sh**",
7777+ contains: `<strong>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></strong>`,
7878+ },
7979+ {
8080+ name: "at mention with italic",
8181+ markdown: "*Check @user.tngl.sh*",
8282+ contains: `<em>Check <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></em>`,
8383+ },
8484+ {
8585+ name: "at mention in list",
8686+ markdown: "- Item 1\n- @user.tngl.sh\n- Item 3",
8787+ contains: `<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>`,
8888+ },
8989+ {
9090+ name: "at mention in link",
9191+ markdown: "[@regnault.dev](https://regnault.dev)",
9292+ contains: `<a href="https://regnault.dev">@regnault.dev</a>`,
9393+ },
9494+ {
9595+ name: "at mention in link again",
9696+ markdown: "[check out @regnault.dev](https://regnault.dev)",
9797+ contains: `<a href="https://regnault.dev">check out @regnault.dev</a>`,
9898+ },
9999+ {
100100+ name: "at mention in link again, multiline",
101101+ markdown: "[\ncheck out @regnault.dev](https://regnault.dev)",
102102+ contains: "<a href=\"https://regnault.dev\">\ncheck out @regnault.dev</a>",
103103+ },
104104+ }
105105+106106+ for _, tt := range tests {
107107+ t.Run(tt.name, func(t *testing.T) {
108108+ md := NewMarkdown()
109109+110110+ var buf bytes.Buffer
111111+ if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
112112+ t.Fatalf("failed to convert markdown: %v", err)
113113+ }
114114+115115+ result := buf.String()
116116+ if !bytes.Contains([]byte(result), []byte(tt.contains)) {
117117+ t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
118118+ }
119119+ })
120120+ }
121121+}