components
-export function convertUrlsToExternalLinks(text: string): string {
- const urlRegex = /(https?:\/\/[^\s]+)/g;
- return text.replace(
- urlRegex,
- '$1',
- );
-}
diff --git a/src/views/repos/Cob/CobCommitTeaser.svelte b/src/views/repos/Cob/CobCommitTeaser.svelte
index 09dc9b00e..ddc8bf702 100644
--- a/src/views/repos/Cob/CobCommitTeaser.svelte
+++ b/src/views/repos/Cob/CobCommitTeaser.svelte
@@ -2,7 +2,8 @@
import type { BaseUrl, CommitHeader } from "@http-client";
import type { Snippet } from "svelte";
- import { twemoji, convertUrlsToExternalLinks } from "@app/lib/utils";
+ import { twemoji } from "@app/lib/utils";
+ import { renderCommitDescription } from "@app/lib/commit";
import CompactCommitAuthorship from "@app/components/CompactCommitAuthorship.svelte";
import ExpandButton from "@app/components/ExpandButton.svelte";
@@ -12,9 +13,6 @@
import InlineTitle from "@app/views/repos/components/InlineTitle.svelte";
import Link from "@app/components/Link.svelte";
- import dompurify from "dompurify";
- import escape from "lodash/escape";
-
export let baseUrl: BaseUrl;
export let commit: CommitHeader;
export let repoId: string;
@@ -93,9 +91,7 @@
{#if commitMessageVisible}
-
{@html dompurify.sanitize(
- convertUrlsToExternalLinks(escape(commit.description.trim())),
- )}
+
{@html renderCommitDescription(commit.description)}
{/if}
diff --git a/src/views/repos/Commit.svelte b/src/views/repos/Commit.svelte
index 1f41ad46d..4f1adbe87 100644
--- a/src/views/repos/Commit.svelte
+++ b/src/views/repos/Commit.svelte
@@ -1,13 +1,8 @@
")).toEqual(
+ "<script>alert(1)</script>",
+ );
+ });
+
+ test("trims leading and trailing whitespace", () => {
+ expect(commit.renderCommitDescription(" hello ")).toEqual("hello");
+ });
+
+ test("converts a bare URL into a radicle-external-link", () => {
+ expect(commit.renderCommitDescription("see https://example.com")).toEqual(
+ 'see https://example.com',
+ );
+ });
+
+ test("strips surrounding angle brackets (CommonMark autolink)", () => {
+ const out = commit.renderCommitDescription(
+ "see for code",
+ );
+ expect(out).toContain('href="https://github.com/radicle-dev/heartwood"');
+ expect(out).not.toMatch(/href="[^"]*>/);
+ // Autolink syntax should not produce visible angle brackets.
+ expect(out).not.toContain("<");
+ expect(out).not.toContain(">");
+ });
+
+ test("does not include trailing period in the link href", () => {
+ const out = commit.renderCommitDescription("see https://example.com.");
+ expect(out).toContain('href="https://example.com"');
+ expect(out).not.toContain('href="https://example.com."');
+ expect(out.endsWith(".")).toBe(true);
+ });
+
+ test("autolink followed by period: brackets stripped, period preserved", () => {
+ const out = commit.renderCommitDescription(
+ "See .",
+ );
+ expect(out).toContain(
+ 'href="https://doc.rust-lang.org/edition-guide/rust-2024/"',
+ );
+ expect(out).not.toContain("<");
+ expect(out).not.toContain(">");
+ expect(out.endsWith(".")).toBe(true);
+ });
+
+ test("does not include enclosing parentheses in the link href", () => {
+ const out = commit.renderCommitDescription("see (https://example.com)");
+ expect(out).toContain('href="https://example.com"');
+ expect(out).not.toContain('href="https://example.com)"');
+ });
+
+ test("preserves ampersands inside query strings", () => {
+ const out = commit.renderCommitDescription("https://example.com/?a=1&b=2");
+ expect(out).toContain('href="https://example.com/?a=1&b=2"');
+ });
+
+ test("links multiple URLs in one description", () => {
+ const out = commit.renderCommitDescription(
+ "see https://a.example and https://b.example",
+ );
+ expect(out).toContain('href="https://a.example"');
+ expect(out).toContain('href="https://b.example"');
+ });
+
+ test("leaves text without URLs untouched", () => {
+ expect(commit.renderCommitDescription("just some plain text")).toEqual(
+ "just some plain text",
+ );
+ });
+
+ test("does not linkify text without a scheme (fuzzyLink off)", () => {
+ const out = commit.renderCommitDescription("contact foo@example.com");
+ expect(out).not.toContain("