mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 14:37:44 +02:00
* 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
48 lines
1.4 KiB
Go
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
|
|
}
|