@@ -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: <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 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
useMediaQuery
|
useMediaQuery
|
||||||
} from '@linagora/twake-mui'
|
} from '@linagora/twake-mui'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { detectUrls } from './utils/detectUrls'
|
||||||
|
|
||||||
type InfoRowProps = {
|
type InfoRowProps = {
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
@@ -18,51 +19,6 @@ type InfoRowProps = {
|
|||||||
flexWrap?: React.CSSProperties['flexWrap']
|
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(
|
|
||||||
<React.Fragment key={lastIndex}>
|
|
||||||
{text.slice(lastIndex, offset)}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize href
|
|
||||||
const href = match.startsWith('http') ? match : `https://${match}`
|
|
||||||
parts.push(
|
|
||||||
<Link
|
|
||||||
key={offset}
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
underline="always"
|
|
||||||
>
|
|
||||||
{match}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
|
|
||||||
lastIndex = offset + match.length
|
|
||||||
return match
|
|
||||||
})
|
|
||||||
|
|
||||||
// Push remaining text after last URL
|
|
||||||
if (lastIndex < text.length) {
|
|
||||||
parts.push(
|
|
||||||
<React.Fragment key={lastIndex}>{text.slice(lastIndex)}</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InfoRow({
|
export function InfoRow({
|
||||||
icon,
|
icon,
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
<React.Fragment key={lastIndex}>
|
||||||
|
{text.slice(lastIndex, offset)}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize href
|
||||||
|
const href = /^https?:\/\//i.test(url) ? url : `https://${url}`
|
||||||
|
|
||||||
|
parts.push(
|
||||||
|
<Link
|
||||||
|
key={offset}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
underline="always"
|
||||||
|
>
|
||||||
|
{url}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (trailing) {
|
||||||
|
parts.push(
|
||||||
|
<React.Fragment key={`${offset}-trailing`}>{trailing}</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = offset + match.length
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
|
||||||
|
// Push remaining text after last URL
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
parts.push(
|
||||||
|
<React.Fragment key={lastIndex}>{text.slice(lastIndex)}</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
function countChar(value: string, char: '(' | ')'): number {
|
||||||
|
return (value.match(new RegExp(`\\${char}`, 'g')) || []).length
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user