From d5a27f498115128ea2f880da941468d9e52b37e5 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Tue, 16 Jun 2026 10:51:59 +0100 Subject: [PATCH 1/2] feat: linkExternalUrls to use GFM-style detection Signed-off-by: Pedro Lamas --- src/util/__tests__/link-external-urls.spec.ts | 47 +++++++++++++++++++ src/util/link-external-urls.ts | 26 +++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/util/__tests__/link-external-urls.spec.ts diff --git a/src/util/__tests__/link-external-urls.spec.ts b/src/util/__tests__/link-external-urls.spec.ts new file mode 100644 index 0000000000..33bc065cf1 --- /dev/null +++ b/src/util/__tests__/link-external-urls.spec.ts @@ -0,0 +1,47 @@ +import linkExternalUrls from '@/util/link-external-urls' + +const anchor = (url: string) => `${url}` + +describe('linkExternalUrls', () => { + it('wraps a plain URL in an anchor', () => { + expect(linkExternalUrls('https://x.com')) + .toBe(anchor('https://x.com')) + }) + + it('keeps wrapping quotes outside the link', () => { + expect(linkExternalUrls('"https://x.com"')) + .toBe(`"${anchor('https://x.com')}"`) + }) + + it('keeps a sentence-ending period as plain text', () => { + expect(linkExternalUrls('see https://x.com.')) + .toBe(`see ${anchor('https://x.com')}.`) + }) + + it('keeps a trailing comma in mid-sentence as plain text', () => { + expect(linkExternalUrls('go to https://x.com, then stop')) + .toBe(`go to ${anchor('https://x.com')}, then stop`) + }) + + it('keeps wrapping parentheses outside the link', () => { + expect(linkExternalUrls('(https://x.com)')) + .toBe(`(${anchor('https://x.com')})`) + }) + + it('preserves balanced parentheses inside the URL', () => { + const url = 'https://en.wikipedia.org/wiki/Foo_(bar)' + + expect(linkExternalUrls(url)) + .toBe(anchor(url)) + }) + + it('links multiple URLs in one string', () => { + expect(linkExternalUrls('a https://x.com b https://y.com')) + .toBe(`a ${anchor('https://x.com')} b ${anchor('https://y.com')}`) + }) + + it('returns text without a URL unchanged', () => { + expect(linkExternalUrls('no link here')) + .toBe('no link here') + }) +}) diff --git a/src/util/link-external-urls.ts b/src/util/link-external-urls.ts index 03b0a757df..fbdc6375e4 100644 --- a/src/util/link-external-urls.ts +++ b/src/util/link-external-urls.ts @@ -1,5 +1,27 @@ -const externalUrlRegExp = /(https?:\/\/\S+)/gi +const externalUrlRegExp = /https?:\/\/[^\s<>"'`]+/gi -const linkExternalUrls = (text: string) => text.replace(externalUrlRegExp, '$1') +const trailingPunctuationRegExp = /[?!.,:*_~]+$/ + +// Trim trailing punctuation and unmatched closing parens (GFM autolink rules). +const trimUrl = (url: string): string => { + let result = url.replace(trailingPunctuationRegExp, '') + + while ( + result.endsWith(')') && + (result.match(/\)/g)?.length ?? 0) > (result.match(/\(/g)?.length ?? 0) + ) { + result = result.slice(0, -1).replace(trailingPunctuationRegExp, '') + } + + return result +} + +const linkExternalUrls = (text: string) => text.replace(externalUrlRegExp, (match) => { + const url = trimUrl(match) + + const trailing = match.slice(url.length) + + return `${url}${trailing}` +}) export default linkExternalUrls From ab85a78c452834e18e988fbc706fb12d05832eb6 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Tue, 16 Jun 2026 11:10:37 +0100 Subject: [PATCH 2/2] test: use relative import for consistency Signed-off-by: Pedro Lamas --- src/util/__tests__/link-external-urls.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/__tests__/link-external-urls.spec.ts b/src/util/__tests__/link-external-urls.spec.ts index 33bc065cf1..d84c9a33ac 100644 --- a/src/util/__tests__/link-external-urls.spec.ts +++ b/src/util/__tests__/link-external-urls.spec.ts @@ -1,4 +1,4 @@ -import linkExternalUrls from '@/util/link-external-urls' +import linkExternalUrls from '../link-external-urls' const anchor = (url: string) => `${url}`