From af0fd2ce496c9f5b4aaf595e35c47492bd485cdf Mon Sep 17 00:00:00 2001 From: eric-jy-park <2019147551@yonsei.ac.kr> Date: Sat, 11 Apr 2026 19:02:06 +0900 Subject: [PATCH 1/2] fix: handle URLs with balanced parentheses in URL detection URLs containing parentheses (e.g., Wikipedia links like https://en.wikipedia.org/wiki/Rust_(programming_language)) were truncated at the first `(` because the URL regex character class excluded parentheses, and TRAILING_PUNCTUATION unconditionally stripped `)`. - Add `()` to URL_REGEX character class - Remove `)` from TRAILING_PUNCTUATION to avoid stripping balanced parens - Add balanced-paren stripping: only strip trailing `)` when unbalanced - Add tests for Wikipedia URLs, nested parens, and wrapped URLs --- lib/providers/url-regex-provider.ts | 16 ++++++++++-- lib/url-detection.test.ts | 40 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/providers/url-regex-provider.ts b/lib/providers/url-regex-provider.ts index 82dad11..801b8f0 100644 --- a/lib/providers/url-regex-provider.ts +++ b/lib/providers/url-regex-provider.ts @@ -30,13 +30,13 @@ export class UrlRegexProvider implements ILinkProvider { * Excludes file paths (no ./ or ../ or bare /) */ private static readonly URL_REGEX = - /(?:https?:\/\/|mailto:|ftp:\/\/|ssh:\/\/|git:\/\/|tel:|magnet:|gemini:\/\/|gopher:\/\/|news:)[\w\-.~:\/?#@!$&*+,;=%]+/gi; + /(?:https?:\/\/|mailto:|ftp:\/\/|ssh:\/\/|git:\/\/|tel:|magnet:|gemini:\/\/|gopher:\/\/|news:)[\w\-.~:\/?#@!$&*+,;=%()]+/gi; /** * Characters to strip from end of URLs * Common punctuation that's unlikely to be part of the URL */ - private static readonly TRAILING_PUNCTUATION = /[.,;!?)\]]+$/; + private static readonly TRAILING_PUNCTUATION = /[.,;!?\]]+$/; constructor(private terminal: ITerminalForUrlProvider) {} @@ -72,6 +72,18 @@ export class UrlRegexProvider implements ILinkProvider { endX = startX + url.length - 1; } + // Strip unbalanced trailing parentheses + while (url.endsWith(')')) { + const open = url.split('(').length - 1; + const close = url.split(')').length - 1; + if (close > open) { + url = url.slice(0, -1); + endX--; + } else { + break; + } + } + // Skip if URL is too short (e.g., just "http://") if (url.length > 8) { links.push({ diff --git a/lib/url-detection.test.ts b/lib/url-detection.test.ts index f31dfc2..c0e4ecd 100644 --- a/lib/url-detection.test.ts +++ b/lib/url-detection.test.ts @@ -178,6 +178,46 @@ describe('URL Detection', () => { expect(links?.[0].text).toBe('tel:+1234567890'); }); + test('detects URLs with balanced parentheses (Wikipedia)', async () => { + const links = await getLinks( + 'https://en.wikipedia.org/wiki/Rust_(programming_language)' + ); + expect(links).toBeDefined(); + expect(links?.length).toBe(1); + expect(links?.[0].text).toBe( + 'https://en.wikipedia.org/wiki/Rust_(programming_language)' + ); + }); + + test('strips unbalanced trailing paren from wrapped URL', async () => { + const links = await getLinks( + '(see https://en.wikipedia.org/wiki/Rust_(programming_language))' + ); + expect(links).toBeDefined(); + expect(links?.length).toBe(1); + expect(links?.[0].text).toBe( + 'https://en.wikipedia.org/wiki/Rust_(programming_language)' + ); + }); + + test('handles URL with multiple parenthesized path segments', async () => { + const links = await getLinks( + 'https://example.com/a_(b)/c_(d)' + ); + expect(links).toBeDefined(); + expect(links?.length).toBe(1); + expect(links?.[0].text).toBe('https://example.com/a_(b)/c_(d)'); + }); + + test('handles URL with nested parentheses', async () => { + const links = await getLinks( + 'https://example.com/foo_(bar_(baz))' + ); + expect(links).toBeDefined(); + expect(links?.length).toBe(1); + expect(links?.[0].text).toBe('https://example.com/foo_(bar_(baz))'); + }); + test('detects magnet: URLs', async () => { const links = await getLinks('Download magnet:?xt=urn:btih:abc123'); expect(links).toBeDefined(); From d39b2294649aaa90f0c756f3b6b02968a51d5a44 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:30:02 +0000 Subject: [PATCH 2/2] chore: fix prettier formatting in url-detection tests --- lib/url-detection.test.ts | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/lib/url-detection.test.ts b/lib/url-detection.test.ts index c0e4ecd..8a620f6 100644 --- a/lib/url-detection.test.ts +++ b/lib/url-detection.test.ts @@ -179,40 +179,28 @@ describe('URL Detection', () => { }); test('detects URLs with balanced parentheses (Wikipedia)', async () => { - const links = await getLinks( - 'https://en.wikipedia.org/wiki/Rust_(programming_language)' - ); + const links = await getLinks('https://en.wikipedia.org/wiki/Rust_(programming_language)'); expect(links).toBeDefined(); expect(links?.length).toBe(1); - expect(links?.[0].text).toBe( - 'https://en.wikipedia.org/wiki/Rust_(programming_language)' - ); + expect(links?.[0].text).toBe('https://en.wikipedia.org/wiki/Rust_(programming_language)'); }); test('strips unbalanced trailing paren from wrapped URL', async () => { - const links = await getLinks( - '(see https://en.wikipedia.org/wiki/Rust_(programming_language))' - ); + const links = await getLinks('(see https://en.wikipedia.org/wiki/Rust_(programming_language))'); expect(links).toBeDefined(); expect(links?.length).toBe(1); - expect(links?.[0].text).toBe( - 'https://en.wikipedia.org/wiki/Rust_(programming_language)' - ); + expect(links?.[0].text).toBe('https://en.wikipedia.org/wiki/Rust_(programming_language)'); }); test('handles URL with multiple parenthesized path segments', async () => { - const links = await getLinks( - 'https://example.com/a_(b)/c_(d)' - ); + const links = await getLinks('https://example.com/a_(b)/c_(d)'); expect(links).toBeDefined(); expect(links?.length).toBe(1); expect(links?.[0].text).toBe('https://example.com/a_(b)/c_(d)'); }); test('handles URL with nested parentheses', async () => { - const links = await getLinks( - 'https://example.com/foo_(bar_(baz))' - ); + const links = await getLinks('https://example.com/foo_(bar_(baz))'); expect(links).toBeDefined(); expect(links?.length).toBe(1); expect(links?.[0].text).toBe('https://example.com/foo_(bar_(baz))');