1/** 2 * @factory/shared/mentions — comment @-mention tokens (TASK-0094 R3/R4). 3 * 4 * The comment composer inserts one token per mention; the backend re-parses 5 * them on write (the body is untrusted text — the stored `mentionIds` / 6 * `pageMentionIds` arrays are the source of truth, the tokens are the 7 * display + parse layer). Tokens: 8 * 9 * @user:<userId> — a person (a `@user:` prefix avoids clashing with 10 * a name that merely contains "team") 11 * @team:<teamId> — a team (fan-out to its members, per-member 12 * deduped in the notifications service) 13 * @page:<pageId> — a page (stored in `pageMentionIds`, no fan-out) 14 * @database:<dbId> — a database (stored in `pageMentionIds`, no fan-out) 15 * 16 * `parseMentions` is the single parser (API + web import it — no duplicated 17 * regexes). It returns deduped arrays per kind. A plain `@Name` (no 18 * recognized token) is NOT a mention (it renders as literal text). 19 */ 20 21export interface ParsedMentions { 22 /** User ids — stored in `Comment.mentionIds` / `CommentReply.mentionIds`. */ 23 users: string[]; 24 /** Team ids — fanned out to members by the notifications service. */ 25 teams: string[]; 26 /** Page ids — stored in `Comment.pageMentionIds`. */ 27 pages: string[]; 28 /** Database ids — stored in `Comment.pageMentionIds`. */ 29 databases: string[]; 30} 31 32const MENTION_RE = /@(user|team|page|database):([A-Za-z0-9_-]{8,})/g; 33 34/** Parse all mention tokens out of a body (deduped per kind, in order). */ 35export function parseMentions(body: string): ParsedMentions { 36 const out: ParsedMentions = { users: [], teams: [], pages: [], databases: [] }; 37 const seen = new Set<string>(); 38 let m: RegExpExecArray | null; 39 MENTION_RE.lastIndex = 0; 40 while ((m = MENTION_RE.exec(body)) !== null) { 41 const kind = m[1]!; 42 const id = m[2]!; 43 const key = `${kind}:${id}`; 44 if (seen.has(key)) continue; 45 seen.add(key); 46 if (kind === "user") out.users.push(id); 47 else if (kind === "team") out.teams.push(id); 48 else if (kind === "page") out.pages.push(id); 49 else out.databases.push(id); 50 } 51 return out; 52} 53