fix(terminal): end a url at CJK punctuation instead of swallowing it

Terminal URL detection treated every non-ASCII character as part of the
url, so Japanese or Chinese text written straight after one was absorbed
into the link. A line like

  PR: https://github.com/org/repo/pull/12345(作成済み・マージ待ち)

underlined the whole run and opened the annotation percent-encoded onto
the end of the url, which 404s.

The body terminator listed only ASCII codes, so nothing at or above 0x80
could end a url.

Terminating on all non-ASCII is the obvious repair and is wrong: a url
path may legitimately carry unencoded CJK, and the wrapped-url tests
cover exactly that - a real multi-line url whose continuation row is a
Chinese path segment. That repair was written first and broke four of
them.

The distinction is punctuation, not ASCII. Non-ASCII punctuation,
symbols and separators end a url; letters do not. Full-width brackets,
an ideographic space, a full-width comma and the katakana middle dot are
prose; 文档 in a path is not.

Adds the extraction test file the module never had, covering the
reporter's three cases, the CJK-path case that must keep working, and
ASCII behaviour as a regression guard.

Closes #10571
This commit is contained in:
Neil
2026-08-17 22:15:32 -07:00
parent d143922561
commit 6e8875df7f
2 changed files with 53 additions and 0 deletions
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest'
import { extractTerminalHttpLinks } from './terminal-http-url-extraction'
const urlsIn = (line: string): string[] => extractTerminalHttpLinks(line).map((link) => link.url)
describe('extractTerminalHttpLinks', () => {
it('stops at a full-width parenthesis instead of swallowing the annotation', () => {
expect(
urlsIn('- PR: https://github.com/stablyai/orca/pull/12345(作成済み・マージ待ち)')
).toEqual(['https://github.com/stablyai/orca/pull/12345'])
})
it('stops at a full-width closing quote', () => {
expect(urlsIn('詳細は「https://example.com/docs」を参照')).toEqual(['https://example.com/docs'])
})
it('stops at an ideographic space', () => {
expect(urlsIn('PR: https://example.com/a (全角スペース区切り)')).toEqual([
'https://example.com/a'
])
})
it('keeps unencoded CJK letters that are genuinely part of the path', () => {
// Why: wrapped URLs with Chinese path segments are supported; only punctuation ends a url.
expect(urlsIn('https://example.com/文档/路径')).toEqual([
new URL('https://example.com/文档/路径').toString()
])
})
it('stops at a full-width comma and full stop between a url and prose', () => {
expect(urlsIn('https://example.com/a、次へ')).toEqual(['https://example.com/a'])
expect(urlsIn('https://example.com/b。')).toEqual(['https://example.com/b'])
})
it('still extracts a plain ascii url and keeps its own punctuation', () => {
expect(urlsIn('see https://example.com/a_b-c?d=1&e=2#f end')).toEqual([
'https://example.com/a_b-c?d=1&e=2#f'
])
})
it('still trims ascii trailing punctuation', () => {
expect(urlsIn('see https://example.com/a.')).toEqual(['https://example.com/a'])
})
})
@@ -98,6 +98,7 @@ function trimHttpUrlTrailingPunctuation(
function isHttpUrlBodyTerminator(code: number): boolean {
return (
isNonAsciiUrlTerminator(code) ||
isAsciiWhitespace(code) ||
code === 0x22 ||
code === 0x27 ||
@@ -142,6 +143,14 @@ function isHttpUrlTrailingPunctuation(code: number): boolean {
)
}
// Why not all non-ascii: a url path may legitimately carry unencoded CJK (see the wrapped-url
// tests), so only punctuation and spaces end it - full-width brackets, an ideographic space, `\u30fb`.
const NON_ASCII_URL_TERMINATOR = /[\p{P}\p{S}\p{Z}]/u
function isNonAsciiUrlTerminator(code: number): boolean {
return code > 0x7e && NON_ASCII_URL_TERMINATOR.test(String.fromCharCode(code))
}
function isAsciiWhitespace(code: number): boolean {
return code === 9 || code === 10 || code === 11 || code === 12 || code === 13 || code === 32
}