Files
wehub-resource-sync 4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:02:24 +08:00

88 lines
1.8 KiB
Go

package parser
import (
"unicode"
"unicode/utf8"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
mast "github.com/usememos/memos/internal/markdown/ast"
)
const (
// MaxMentionLength matches the username token length accepted by the API.
MaxMentionLength = 32
)
type mentionParser struct{}
// NewMentionParser creates a new inline parser for @mention syntax.
func NewMentionParser() parser.InlineParser {
return &mentionParser{}
}
// Trigger returns the characters that trigger this parser.
func (*mentionParser) Trigger() []byte {
return []byte{'@'}
}
func isValidMentionRune(r rune) bool {
return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '-'
}
func isMentionBoundary(r rune) bool {
return unicode.IsSpace(r) || unicode.IsPunct(r) || unicode.IsSymbol(r)
}
// Parse parses @mention syntax while avoiding email-address matches.
func (*mentionParser) Parse(_ gast.Node, block text.Reader, _ parser.Context) gast.Node {
line, _ := block.PeekLine()
if len(line) == 0 || line[0] != '@' {
return nil
}
prev := block.PrecendingCharacter()
if prev != '\n' && !isMentionBoundary(prev) {
return nil
}
start := 1
pos := start
runeCount := 0
hasLetterOrNumber := false
for pos < len(line) {
r, size := utf8.DecodeRune(line[pos:])
if r == utf8.RuneError && size == 1 {
break
}
if !isValidMentionRune(r) {
break
}
if unicode.IsLetter(r) || unicode.IsNumber(r) {
hasLetterOrNumber = true
}
runeCount++
if runeCount > MaxMentionLength {
break
}
pos += size
}
if pos <= start || !hasLetterOrNumber {
return nil
}
username := line[start:pos]
usernameCopy := make([]byte, len(username))
copy(usernameCopy, username)
block.Advance(pos)
return &mast.MentionNode{
Username: usernameCopy,
}
}