import type { MentionDTO } from '$lib/types'; /** * Given the current textarea value and cursor position, returns the * @-mention query being typed (the text after the last triggering @), * or null if no mention is active. * * Rules: * - @ must be preceded by whitespace or be at the start of the string * - The text between @ and the cursor must not contain a space (a * completed mention word already has a space) */ export function detectMention(text: string, cursorPos: number): string | null { const before = text.slice(0, cursorPos); const atIndex = before.lastIndexOf('@'); if (atIndex === -1) return null; // @ must be at start or preceded by whitespace if (atIndex > 0 && !/\s/.test(before[atIndex - 1])) return null; const query = before.slice(atIndex + 1); // If the query contains a space the user has moved past the trigger word if (query.includes(' ')) return null; return query; } /** * Given the raw textarea value and a list of candidate users (from the * mention popup selections), returns the plain content string and the * de-duplicated list of mentioned user IDs. */ export function extractContent( text: string, candidates: MentionDTO[] ): { content: string; mentionedUserIds: string[] } { const seen = new Set(); for (const user of candidates) { const displayName = `${user.firstName} ${user.lastName}`.trim(); if (text.includes(`@${displayName}`)) { seen.add(user.id); } } return { content: text, mentionedUserIds: [...seen] }; } /** * Escapes the five HTML-special characters that can break out of text content * or attribute values. & must be escaped first to avoid double-encoding. * * Includes the apostrophe so the helper is safe in single-quoted attribute * values too — the renderTranscriptionBody anchor template in PR-B2 uses * double quotes today, but a future template change shouldn't open a * stored-XSS hole (Sina #5505 action item). */ export function escapeHtml(str: string): string { return str .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } /** * Renders a comment body as safe HTML: * 1. Escapes all HTML-special characters in the raw content * 2. Replaces every @FirstName LastName occurrence with an anchor link * 3. Converts newlines to
*/ export function renderBody(content: string, mentions: MentionDTO[]): string { let escaped = escapeHtml(content); for (const mention of mentions) { const displayName = `${mention.firstName} ${mention.lastName}`.trim(); const escapedDisplayName = escapeHtml(displayName); const span = `@${escapedDisplayName}`; escaped = escaped.replaceAll(`@${escapedDisplayName}`, span); } return escaped.replaceAll('\n', '
'); }