From bb7fb73f3f7fca3c17302dd0b09523384495d452 Mon Sep 17 00:00:00 2001 From: MaxWang007 Date: Thu, 12 Nov 2026 05:10:18 +0000 Subject: [PATCH] Keep sentence punctuation out of citation link destinations PageText trimmed trailing punctuation once, then removed an unmatched closing bracket. When a source URL sat at the end of a bracketed phrase, the period or comma left behind stayed in the href. (See https://example.com/pricing.) linked to https://example.com/pricing. and [See https://example.org/plans,] linked to https://example.org/plans,. Trim again after peeling unmatched closers, and repeat until the suffix stops changing. Balanced parentheses in a URL, such as Project_(software), and a period that already sits outside the brackets stay as they were. --- openbot/app/src/components/duet/page-text.tsx | 27 ++++++++++++------- openbot/app/tests/pages.test.tsx | 15 +++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/openbot/app/src/components/duet/page-text.tsx b/openbot/app/src/components/duet/page-text.tsx index c73c05f9b..43d3f67ba 100644 --- a/openbot/app/src/components/duet/page-text.tsx +++ b/openbot/app/src/components/duet/page-text.tsx @@ -6,16 +6,23 @@ export function PageText({ text }: { text: string }) { let cursor = 0; for (const match of text.matchAll(/https?:\/\/[^\s<>"']+/g)) { const start = match.index; - let source = match[0].replace(/[.,;:!?]+$/, ""); - for (const [open, close] of [ - ["(", ")"], - ["[", "]"], - ]) { - while ( - source.endsWith(close) && - source.split(close).length > source.split(open).length - ) { - source = source.slice(0, -1); + // A period or comma can sit just inside the closing bracket. Trim, peel + // unmatched closers, and trim again until the suffix stops changing. + let source = match[0]; + let previous = ""; + while (source !== previous) { + previous = source; + source = source.replace(/[.,;:!?]+$/, ""); + for (const [open, close] of [ + ["(", ")"], + ["[", "]"], + ]) { + while ( + source.endsWith(close) && + source.split(close).length > source.split(open).length + ) { + source = source.slice(0, -1); + } } } let url: URL; diff --git a/openbot/app/tests/pages.test.tsx b/openbot/app/tests/pages.test.tsx index 6ae974ed1..bf83430ba 100644 --- a/openbot/app/tests/pages.test.tsx +++ b/openbot/app/tests/pages.test.tsx @@ -943,6 +943,21 @@ test("source links keep balanced URL parentheses and support HTTP sources", () = expect(view.container.textContent).toBe(text); }); +test("a bracketed citation drops sentence punctuation from the link", () => { + const text = + "(See https://example.com/pricing.) [See https://example.org/plans,] (https://example.org/plans). https://example.com/wiki/Project_(software)"; + const view = render(); + expect( + view.getAllByRole("link").map((link) => link.getAttribute("href")), + ).toEqual([ + "https://example.com/pricing", + "https://example.org/plans", + "https://example.org/plans", + "https://example.com/wiki/Project_(software)", + ]); + expect(view.container.textContent).toBe(text); +}); + test("source text never turns markup, unsafe schemes or URL credentials into links", () => { const text = " javascript:alert(1) data:text/html,unsafe https://user:secret@example.com/private https://";