diff --git a/__test__/components/Event/utils/detectURLs.test.tsx b/__test__/components/Event/utils/detectURLs.test.tsx new file mode 100644 index 0000000..d228246 --- /dev/null +++ b/__test__/components/Event/utils/detectURLs.test.tsx @@ -0,0 +1,74 @@ +import { detectUrls } from '@/components/Event/utils/detectUrls' +import React from 'react' + +describe('detectUrls', () => { + it('returns plain text when no URL present', () => { + const result = detectUrls('Hello world') + expect(result).toHaveLength(1) + const hasLink = result.some((el: any) => Boolean(el?.props?.href)) + expect(hasLink).toBe(false) + }) + + it('wraps https URLs in a Link', () => { + const result = detectUrls('Visit https://example.com today') + const link = result.find( + el => (el as any).type?.displayName === 'Link' || (el as any).props?.href + ) + expect(link).toBeDefined() + expect((link as any).props.href).toBe('https://example.com') + }) + + it('prefixes www URLs with https://', () => { + const result = detectUrls('See www.example.com for more') + const link = result.find((el: any) => el.props?.href) + expect((link as any).props.href).toBe('https://www.example.com') + }) + + it('preserves text between two URLs', () => { + const result = detectUrls('Go to https://a.com and https://b.com') + const texts = result.filter((el: any) => el.type === React.Fragment) + expect(texts.some((el: any) => el.props.children.includes(' and '))).toBe( + true + ) + }) + + it('preserves ponctuation after URL', () => { + const result = detectUrls('Go to https://a.com.') + const link = result.find((el: any) => el.props?.href) + + const texts = result.filter((el: any) => el.type === React.Fragment) + expect(texts.some((el: any) => el.props.children.includes('.'))).toBe(true) + expect((link as any).props.href).toBe('https://a.com') + }) + + it('handles URLs wrapped in angle brackets', () => { + const result = detectUrls('Meeting: ') + const link = result.find((el: any) => el.props?.href) + expect((link as any).props.href).toContain('teams.microsoft.com') + }) + + it('handles URLs wrapped in parentheses', () => { + const result = detectUrls('Meeting: (https://teams.microsoft.com/join/abc)') + const link = result.find((el: any) => el.props?.href) + expect((link as any).props.href).toContain('teams.microsoft.com') + }) + + it('handles URLs with uppercase HTTPS://', () => { + const result = detectUrls('Meeting: (HTTPS://teams.microsoft.com/join/abc)') + const link = result.find((el: any) => el.props?.href) + expect((link as any).props.href).toContain('teams.microsoft.com') + }) + + it('does not include trailing punctuation in URL', () => { + const result = detectUrls('Visit https://example.com.') + const link = result.find((el: any) => el.props?.href) + expect((link as any).props.href).toBe('https://example.com') // no trailing dot + }) + + it('handles URLs with parentheses (Wikipedia-style)', () => { + const url = 'https://en.wikipedia.org/wiki/Rust_(programming_language)' + const result = detectUrls(url) + const link = result.find((el: any) => el.props?.href) + expect((link as any).props.href).toBe(url) + }) +}) diff --git a/src/components/Event/InfoRow.tsx b/src/components/Event/InfoRow.tsx index db44f35..b3043b0 100644 --- a/src/components/Event/InfoRow.tsx +++ b/src/components/Event/InfoRow.tsx @@ -6,6 +6,7 @@ import { useMediaQuery } from '@linagora/twake-mui' import React from 'react' +import { detectUrls } from './utils/detectUrls' type InfoRowProps = { icon: React.ReactNode @@ -18,51 +19,6 @@ type InfoRowProps = { flexWrap?: React.CSSProperties['flexWrap'] } -function detectUrls(text: string): JSX.Element[] { - // Simple regex that captures whole URLs without splitting them apart - const urlRegex = /(https?:\/\/[^\s]+|www\.[^\s]+)/gi - - const parts = [] - let lastIndex = 0 - - text.replace(urlRegex, (match, _, offset: number) => { - // Push the text before the match - if (lastIndex < offset) { - parts.push( - - {text.slice(lastIndex, offset)} - - ) - } - - // Normalize href - const href = match.startsWith('http') ? match : `https://${match}` - parts.push( - - {match} - - ) - - lastIndex = offset + match.length - return match - }) - - // Push remaining text after last URL - if (lastIndex < text.length) { - parts.push( - {text.slice(lastIndex)} - ) - } - - return parts -} - export function InfoRow({ icon, text, diff --git a/src/components/Event/utils/detectUrls.tsx b/src/components/Event/utils/detectUrls.tsx new file mode 100644 index 0000000..67898ff --- /dev/null +++ b/src/components/Event/utils/detectUrls.tsx @@ -0,0 +1,72 @@ +import { Link } from '@linagora/twake-mui' +import React from 'react' + +export function detectUrls(text: string): JSX.Element[] { + // Simple regex that captures whole URLs without splitting them apart + const urlRegex = /(https?:\/\/[^\s<>]+|www\.[^\s<>]+)/gi + + const parts = [] + let lastIndex = 0 + + text.replace(urlRegex, (match, _, offset: number) => { + // Strip trailing punctuation, but preserve balanced parentheses (e.g. Wikipedia) + let url = match + + while (/[.,;:!?)]$/.test(url)) { + if (url.endsWith(')')) { + const openCount = countChar(url, '(') + const closeCount = countChar(url, ')') + if (closeCount <= openCount) break + } + url = url.slice(0, -1) + } + + const trailing = match.slice(url.length) + + // Push the text before the match + if (lastIndex < offset) { + parts.push( + + {text.slice(lastIndex, offset)} + + ) + } + + // Normalize href + const href = /^https?:\/\//i.test(url) ? url : `https://${url}` + + parts.push( + + {url} + + ) + + if (trailing) { + parts.push( + {trailing} + ) + } + + lastIndex = offset + match.length + return match + }) + + // Push remaining text after last URL + if (lastIndex < text.length) { + parts.push( + {text.slice(lastIndex)} + ) + } + + return parts +} + +function countChar(value: string, char: '(' | ')'): number { + return (value.match(new RegExp(`\\${char}`, 'g')) || []).length +}