/** * Separator characters that turn a single paste into multiple tag tokens: * commas, newlines, carriage returns, and tabs (so columns copied from a * spreadsheet split correctly). */ const BULK_TAG_SEPARATOR = /[,\n\r\t]+/; /** Result of merging a raw paste/draft string into an existing tag list. */ export interface IBulkAddResult { /** Items appended to the existing list (after trim, dedupe, and cap). */ added: string[]; /** Total non-empty tokens parsed from the input. */ total: number; /** Final list = `existing` + `added`. */ next: string[]; } /** * True when the text contains at least one bulk separator. Use this to * decide whether a paste should be intercepted (multi-value) or allowed * to land in the input as plain text (single-value, user is mid-typing). */ export const containsBulkSeparator = (text: string): boolean => BULK_TAG_SEPARATOR.test(text); /** * Parses a raw string into a list of trimmed, non-empty tag tokens. */ export const parseBulkTags = (raw: string): string[] => raw .split(BULK_TAG_SEPARATOR) .map(token => token.trim()) .filter(Boolean); /** * Merges parsed tokens into an existing list with case-insensitive * dedupe and a hard cap. Overflow and duplicates are silently dropped; * the caller can compare ``added.length`` to ``total`` to surface a * "X of Y added" hint when something was skipped. */ export const addBulkTags = ( raw: string, existing: string[], cap: number ): IBulkAddResult => { const tokens = parseBulkTags(raw); const seen = new Set(existing.map(tag => tag.toLowerCase())); const next = [...existing]; const added: string[] = []; for (const token of tokens) { if (next.length >= cap) break; const key = token.toLowerCase(); if (seen.has(key)) continue; seen.add(key); next.push(token); added.push(token); } return { added, total: tokens.length, next }; };