|
1 | 1 | 'use client' |
2 | 2 |
|
3 | 3 | import * as React from 'react' |
4 | | -import { normalizeEmail } from '@sim/utils/string' |
| 4 | +import { isValidEmailSyntax, normalizeEmail } from '@sim/utils/string' |
5 | 5 | import { TagInput, type TagItem } from '../tag-input/tag-input' |
6 | 6 |
|
7 | | -/** |
8 | | - * Generic RFC 5322 email syntax gate. This is deliberately format-only — |
9 | | - * app-specific policy (disposable domains, MX/DNS, membership rules) is the |
10 | | - * consumer's concern and flows through the `validate` prop, keeping that logic |
11 | | - * in the app rather than the design system. |
12 | | - */ |
13 | | -const EMAIL_SYNTAX_REGEX = |
14 | | - /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ |
15 | | - |
16 | | -/** |
17 | | - * Bare `@domain.tld` pattern, accepted only when the consumer opts in via |
18 | | - * `allowDomains` — for allowlists that grant access to a whole domain. |
19 | | - */ |
20 | | -const EMAIL_DOMAIN_SYNTAX_REGEX = |
21 | | - /^@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/ |
22 | | - |
23 | | -function isValidEmailSyntax(email: string, allowDomains: boolean): boolean { |
24 | | - if (email.length > 254) return false |
25 | | - return EMAIL_SYNTAX_REGEX.test(email) || (allowDomains && EMAIL_DOMAIN_SYNTAX_REGEX.test(email)) |
26 | | -} |
| 7 | +const ENTER_PREFIX = 'enter' |
27 | 8 |
|
28 | 9 | /** |
29 | 10 | * Derives the post-first-chip placeholder from the initial placeholder so |
30 | 11 | * consumers don't have to spell both. Tries an `'Enter <noun>s'` → |
31 | 12 | * `'Add <noun>'` singularize; falls back to a generic `'Add another'`. |
| 13 | + * |
| 14 | + * Deliberately string ops rather than a regex — `/^Enter\s+(.+?)s?$/` has |
| 15 | + * polynomial backtracking on whitespace-heavy input (CodeQL js/redos). |
32 | 16 | */ |
33 | 17 | function derivePlaceholderWithTags(placeholder: string): string { |
34 | | - const match = placeholder.match(/^Enter\s+(.+?)s?$/i) |
35 | | - if (match) return `Add ${match[1]}` |
36 | | - return 'Add another' |
| 18 | + const rest = placeholder.slice(ENTER_PREFIX.length) |
| 19 | + const noun = rest.trim() |
| 20 | + const startsWithEnter = placeholder.slice(0, ENTER_PREFIX.length).toLowerCase() === ENTER_PREFIX |
| 21 | + if (!startsWithEnter || rest === noun || !noun) return 'Add another' |
| 22 | + const singular = noun.toLowerCase().endsWith('s') ? noun.slice(0, -1) : noun |
| 23 | + return `Add ${singular}` |
37 | 24 | } |
38 | 25 |
|
39 | 26 | export interface ChipEmailsInputProps { |
|
0 commit comments