From ead9530e661a48050988450b6a70d2240f2ea8b1 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Thu, 15 Oct 2026 09:01:55 +0900 Subject: feat(markdown): open every chat link in a new tab (target=_blank, rel=noopener noreferrer) --- src/features/markdown/logic/markdown.test.ts | 40 ++++++++++++++++++++++++++++ src/features/markdown/logic/markdown.ts | 27 ++++++++++++++++--- 2 files changed, 64 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/features/markdown/logic/markdown.test.ts b/src/features/markdown/logic/markdown.test.ts index 54b0086..90f1938 100644 --- a/src/features/markdown/logic/markdown.test.ts +++ b/src/features/markdown/logic/markdown.test.ts @@ -25,6 +25,46 @@ describe("renderMarkdown", () => { expect(html).toContain("<b>"); }); + describe("links open in a new tab", () => { + function anchors(html: string): HTMLAnchorElement[] { + const doc = new DOMParser().parseFromString(html, "text/html"); + return Array.from(doc.querySelectorAll("a")); + } + + it.each([ + ["a markdown link", "See [the docs](https://example.com/docs)."], + ["a GFM autolink", "Visit https://example.com today"], + ["an angle-bracket autolink", ""], + ["a raw-HTML anchor", 'Raw link'], + ["a relative link", "[home](/settings)"], + ])("%s gets target=_blank + rel=noopener noreferrer", (_label, src) => { + const links = anchors(renderMarkdown(src)); + expect(links).toHaveLength(1); + expect(links[0]?.getAttribute("target")).toBe("_blank"); + expect(links[0]?.getAttribute("rel")).toBe("noopener noreferrer"); + }); + + it("overrides an author-supplied target/rel", () => { + const [link] = anchors( + renderMarkdown('x'), + ); + expect(link?.getAttribute("target")).toBe("_blank"); + expect(link?.getAttribute("rel")).toBe("noopener noreferrer"); + }); + + it("applies while streaming too", () => { + const [link] = anchors( + renderMarkdown("partial [link](https://example.com) **bo", { streaming: true }), + ); + expect(link?.getAttribute("target")).toBe("_blank"); + }); + + it("still strips javascript: hrefs", () => { + const html = renderMarkdown('x'); + expect(html).not.toContain("javascript:"); + }); + }); + it("sanitizes dangerous HTML", () => { const html = renderMarkdown("Hi there"); expect(html).not.toContain("