/** * @factory/shared/mentions — comment @-mention tokens (TASK-0094 R3/R4). * * The comment composer inserts one token per mention; the backend re-parses * them on write (the body is untrusted text — the stored `mentionIds` / * `pageMentionIds` arrays are the source of truth, the tokens are the * display + parse layer). Tokens: * * @user: — a person (a `@user:` prefix avoids clashing with * a name that merely contains "team") * @team: — a team (fan-out to its members, per-member * deduped in the notifications service) * @page: — a page (stored in `pageMentionIds`, no fan-out) * @database: — a database (stored in `pageMentionIds`, no fan-out) * * `parseMentions` is the single parser (API + web import it — no duplicated * regexes). It returns deduped arrays per kind. A plain `@Name` (no * recognized token) is NOT a mention (it renders as literal text). */ export interface ParsedMentions { /** User ids — stored in `Comment.mentionIds` / `CommentReply.mentionIds`. */ users: string[]; /** Team ids — fanned out to members by the notifications service. */ teams: string[]; /** Page ids — stored in `Comment.pageMentionIds`. */ pages: string[]; /** Database ids — stored in `Comment.pageMentionIds`. */ databases: string[]; } const MENTION_RE = /@(user|team|page|database):([A-Za-z0-9_-]{8,})/g; /** Parse all mention tokens out of a body (deduped per kind, in order). */ export function parseMentions(body: string): ParsedMentions { const out: ParsedMentions = { users: [], teams: [], pages: [], databases: [] }; const seen = new Set(); let m: RegExpExecArray | null; MENTION_RE.lastIndex = 0; while ((m = MENTION_RE.exec(body)) !== null) { const kind = m[1]!; const id = m[2]!; const key = `${kind}:${id}`; if (seen.has(key)) continue; seen.add(key); if (kind === "user") out.users.push(id); else if (kind === "team") out.teams.push(id); else if (kind === "page") out.pages.push(id); else out.databases.push(id); } return out; }