From 8c06d8ca268a55d3f60802aa4b44035ad4689c9a Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Sun, 21 Dec 2025 07:17:05 +0000 Subject: [PATCH] appview/pages: use robust handle regex from indigo/syntax Change-Id: vtyvsnwqkrwrksmnloppspuxtuvvvoym should cut down the false positives a bit. tests are included. Signed-off-by: oppiliappan --- appview/pages/markup/extension/atlink.go | 3 +- appview/pages/markup/markdown_test.go | 106 +++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 appview/pages/markup/markdown_test.go diff --git a/appview/pages/markup/extension/atlink.go b/appview/pages/markup/extension/atlink.go index 4da88b46..09243d21 100644 --- a/appview/pages/markup/extension/atlink.go +++ b/appview/pages/markup/extension/atlink.go @@ -35,7 +35,7 @@ func (n *AtNode) Kind() ast.NodeKind { return KindAt } -var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)`) +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)`) type atParser struct{} @@ -55,6 +55,7 @@ func (s *atParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) if m == nil { return nil } + atSegment := text.NewSegment(segment.Start, segment.Start+m[1]) block.Advance(m[1]) node := &AtNode{} diff --git a/appview/pages/markup/markdown_test.go b/appview/pages/markup/markdown_test.go new file mode 100644 index 00000000..b08773a3 --- /dev/null +++ b/appview/pages/markup/markdown_test.go @@ -0,0 +1,106 @@ +package markup + +import ( + "bytes" + "testing" +) + +func TestAtExtension_Rendering(t *testing.T) { + tests := []struct { + name string + markdown string + expected string + }{ + { + name: "renders simple at mention", + markdown: "Hello @user.tngl.sh!", + expected: `

Hello @user.tngl.sh!

`, + }, + { + name: "renders multiple at mentions", + markdown: "Hi @alice.tngl.sh and @bob.example.com", + expected: `

Hi @alice.tngl.sh and @bob.example.com

`, + }, + { + name: "renders at mention in parentheses", + markdown: "Check this out (@user.tngl.sh)", + expected: `

Check this out (@user.tngl.sh)

`, + }, + { + name: "does not render email", + markdown: "Contact me at test@example.com", + expected: `

Contact me at test@example.com

`, + }, + { + name: "renders at mention with hyphen", + markdown: "Follow @user-name.tngl.sh", + expected: `

Follow @user-name.tngl.sh

`, + }, + { + name: "renders at mention with numbers", + markdown: "@user123.test456.social", + expected: `

@user123.test456.social

`, + }, + { + name: "at mention at start of line", + markdown: "@user.tngl.sh is cool", + expected: `

@user.tngl.sh is cool

`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + md := NewMarkdown() + + var buf bytes.Buffer + if err := md.Convert([]byte(tt.markdown), &buf); err != nil { + t.Fatalf("failed to convert markdown: %v", err) + } + + result := buf.String() + if result != tt.expected+"\n" { + t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, result) + } + }) + } +} + +func TestAtExtension_WithOtherMarkdown(t *testing.T) { + tests := []struct { + name string + markdown string + contains string + }{ + { + name: "at mention with bold", + markdown: "**Hello @user.tngl.sh**", + contains: `Hello @user.tngl.sh`, + }, + { + name: "at mention with italic", + markdown: "*Check @user.tngl.sh*", + contains: `Check @user.tngl.sh`, + }, + { + name: "at mention in list", + markdown: "- Item 1\n- @user.tngl.sh\n- Item 3", + contains: `@user.tngl.sh`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + md := NewMarkdown() + + var buf bytes.Buffer + if err := md.Convert([]byte(tt.markdown), &buf); err != nil { + t.Fatalf("failed to convert markdown: %v", err) + } + + result := buf.String() + if !bytes.Contains([]byte(result), []byte(tt.contains)) { + t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result) + } + }) + } +} -- 2.43.0