[#769] updated regex to properly handle <> brackets (#792)

This commit is contained in:
Camille Moussu
2026-04-20 12:33:36 +02:00
committed by GitHub
parent 2df87b9b9c
commit b481eefed0
3 changed files with 147 additions and 45 deletions
@@ -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)
})
})
+1 -45
View File
@@ -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(
<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({
icon,
text,
+72
View File
@@ -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
}