Files
multica/server/internal/util/mention.go
Kagura cc94fbd305 fix: handle square brackets in agent names for mention parsing (#1992)
* fix: handle square brackets in agent names for mention parsing (#1991)

The mention regex used [^\]]* to match labels, which broke when agent
names contained square brackets (e.g. David[TF]). The ] inside the name
caused the regex to stop matching prematurely, silently dropping the
mention.

Changes:
- Backend (mention.go): Switch to .+? (non-greedy) anchored on
  ](mention:// to correctly match labels with brackets
- Frontend (mention-extension.ts): Same regex fix in tokenizer, plus
  escape [ and ] in renderMarkdown to prevent creating ambiguous
  markdown syntax
- Add comprehensive tests for ParseMentions covering bracket names

Fixes #1991

* fix: add optional chaining for match group access

Fixes TS2532: Object is possibly 'undefined' on match[1] when calling
.replace() in the mention tokenizer.

* fix: tighten mention tokenizer to reject ordinary Markdown links

- Replace .+? with (?:\\.|[^\]])+  in start() and tokenize() regexes
  so the label cannot cross a ]( Markdown link boundary
- Escaped brackets (\[ \]) from renderMarkdown() are still accepted
- Add frontend tokenizer/serializer round-trip tests:
  - Plain mention
  - Escaped brackets (David[TF]) round-trip
  - Normal Markdown link + mention on same line (regression)
  - Multiple links before mention
  - Nested brackets (Bot[v2][beta])
  - Issue mentions without @ prefix

Addresses review feedback on #1992.

* fix: add type assertions for tiptap MarkdownTokenizer interface in tests

The tiptap MarkdownTokenizer type allows start to be string | function
and tokenize to accept 3 arguments. Our extension always provides
single-arg functions, so cast them for TypeScript satisfaction.

Fixes CI typecheck failure in @multica/views package.

* fix: cast renderMarkdown to single-arg shape and reset file modes to 0644
2026-05-03 19:39:26 +08:00

48 lines
1.4 KiB
Go

package util
import "regexp"
// Mention represents a parsed @mention from markdown content.
type Mention struct {
Type string // "member", "agent", "issue", or "all"
ID string // user_id, agent_id, issue_id, or "all"
}
// MentionRe matches [@Label](mention://type/id) or [Label](mention://issue/id) in markdown.
// The @ prefix is optional to support issue mentions which use [MUL-123](mention://issue/...).
// Uses .+? (non-greedy) instead of [^\]]* so labels containing square brackets
// (e.g. "David[TF]") are matched correctly — the ](mention:// anchor is specific
// enough to prevent over-matching.
var MentionRe = regexp.MustCompile(`\[@?(.+?)\]\(mention://(member|agent|issue|all)/([0-9a-fA-F-]+|all)\)`)
// IsMentionAll returns true if the mention is an @all mention.
func (m Mention) IsMentionAll() bool {
return m.Type == "all"
}
// ParseMentions extracts deduplicated mentions from markdown content.
func ParseMentions(content string) []Mention {
matches := MentionRe.FindAllStringSubmatch(content, -1)
seen := make(map[string]bool)
var result []Mention
for _, m := range matches {
key := m[2] + ":" + m[3]
if seen[key] {
continue
}
seen[key] = true
result = append(result, Mention{Type: m[2], ID: m[3]})
}
return result
}
// HasMentionAll returns true if any mention in the slice is an @all mention.
func HasMentionAll(mentions []Mention) bool {
for _, m := range mentions {
if m.IsMentionAll() {
return true
}
}
return false
}