From 4120b8d6c3a60d18b890f402a2ad4d4219f19709 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 05:24:07 +0000 Subject: [PATCH 1/4] Initial plan From fb55d20a1f1b068ab70a4bd032bbb64a981aa556 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 05:37:11 +0000 Subject: [PATCH 2/4] Fix markdown link replacement: use raw URLs, preserve tooltips, and handle absolute URLs correctly Co-authored-by: kzu <169707+kzu@users.noreply.github.com> --- src/NuGetizer.Tasks/CreatePackage.cs | 15 ++- src/NuGetizer.Tests/CreatePackageTests.cs | 109 +++++++++++++++++++++- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/NuGetizer.Tasks/CreatePackage.cs b/src/NuGetizer.Tasks/CreatePackage.cs index 8fd17957..ba7edcfd 100644 --- a/src/NuGetizer.Tasks/CreatePackage.cs +++ b/src/NuGetizer.Tasks/CreatePackage.cs @@ -237,16 +237,25 @@ void GeneratePackage(Stream output = null) Uri.TryCreate(manifest.Metadata.Repository.Url, UriKind.Absolute, out var uri) && uri.Host.EndsWith("github.com")) { - // expr to match markdown links. use named groups to capture the link text and url. - linkExpr ??= new Regex(@"\[(?[^\]]+)\]\((?[^)]+)\)", RegexOptions.None); + // expr to match markdown links with optional title. use named groups to capture the link text, url and optional title. + linkExpr ??= new Regex(@"\[(?[^\]]+)\]\((?[^\s)]+)(?:\s+""(?[^""]*)"")?\)", RegexOptions.None); var repoUrl = manifest.Metadata.Repository.Url.TrimEnd('/'); replaced = linkExpr.Replace(replaced, match => { var url = match.Groups["url"].Value; + var title = match.Groups["title"].Value; + + // Check if the URL is already absolute if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) return match.Value; - var newUrl = $"{repoUrl}/blob/{manifest.Metadata.Repository.Commit}/{url.TrimStart('/')}"; + // Use 'raw' instead of 'blob' for proper image display on nuget.org + var newUrl = $"{repoUrl}/raw/{manifest.Metadata.Repository.Commit}/{url.TrimStart('/')}"; + + // Preserve the title if present + if (!string.IsNullOrEmpty(title)) + return $"[{match.Groups["text"].Value}]({newUrl} \"{title}\")"; + return $"[{match.Groups["text"].Value}]({newUrl})"; }); } diff --git a/src/NuGetizer.Tests/CreatePackageTests.cs b/src/NuGetizer.Tests/CreatePackageTests.cs index c9813088..fd776e00 100644 --- a/src/NuGetizer.Tests/CreatePackageTests.cs +++ b/src/NuGetizer.Tests/CreatePackageTests.cs @@ -333,7 +333,114 @@ public void when_readme_has_relativeurl_then_expands_github_url() var readme = File.ReadAllText(file.Source); - Assert.Contains("[license](https://github.com/devlooped/nugetizer/blob/9dc2cb5de/license.txt)", readme); + Assert.Contains("[license](https://github.com/devlooped/nugetizer/raw/9dc2cb5de/license.txt)", readme); + } + + [Fact] + public void when_readme_has_link_with_tooltip_then_preserves_tooltip() + { + var content = Path.GetTempFileName(); + File.WriteAllText(content, "See ![avatar](avatars/user.png \"User Avatar\")."); + task.Contents = new[] + { + new TaskItem(content, new Metadata + { + { MetadataName.PackageId, task.Manifest.GetMetadata("Id") }, + { MetadataName.PackFolder, PackFolderKind.None }, + { MetadataName.PackagePath, "readme.md" } + }), + }; + + task.Manifest.SetMetadata("Readme", "readme.md"); + task.Manifest.SetMetadata("RepositoryType", "git"); + task.Manifest.SetMetadata("RepositoryUrl", "https://github.com/devlooped/nugetizer"); + task.Manifest.SetMetadata("RepositorySha", "abc123def"); + + createPackage = true; + ExecuteTask(out var manifest); + + Assert.NotNull(manifest); + + var file = manifest.Files.FirstOrDefault(f => Path.GetFileName(f.Target) == manifest.Metadata.Readme); + Assert.NotNull(file); + Assert.True(File.Exists(file.Source)); + + var readme = File.ReadAllText(file.Source); + + Assert.Contains("[avatar](https://github.com/devlooped/nugetizer/raw/abc123def/avatars/user.png \"User Avatar\")", readme); + } + + [Fact] + public void when_readme_has_absolute_url_then_does_not_replace() + { + var content = Path.GetTempFileName(); + File.WriteAllText(content, "[![badge](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/user.png \"User\")](https://github.com/user)"); + task.Contents = new[] + { + new TaskItem(content, new Metadata + { + { MetadataName.PackageId, task.Manifest.GetMetadata("Id") }, + { MetadataName.PackFolder, PackFolderKind.None }, + { MetadataName.PackagePath, "readme.md" } + }), + }; + + task.Manifest.SetMetadata("Readme", "readme.md"); + task.Manifest.SetMetadata("RepositoryType", "git"); + task.Manifest.SetMetadata("RepositoryUrl", "https://github.com/devlooped/nugetizer"); + task.Manifest.SetMetadata("RepositorySha", "abc123def"); + + createPackage = true; + ExecuteTask(out var manifest); + + Assert.NotNull(manifest); + + var file = manifest.Files.FirstOrDefault(f => Path.GetFileName(f.Target) == manifest.Metadata.Readme); + Assert.NotNull(file); + Assert.True(File.Exists(file.Source)); + + var readme = File.ReadAllText(file.Source); + + // Should NOT prepend repository URL to absolute URLs + Assert.DoesNotContain("https://github.com/devlooped/nugetizer/raw/abc123def/https://raw.githubusercontent.com", readme); + // Should preserve the original absolute URL + Assert.Contains("https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/user.png", readme); + } + + [Fact] + public void when_readme_has_image_link_then_uses_raw_url() + { + var content = Path.GetTempFileName(); + File.WriteAllText(content, "![Image](img/logo.png)"); + task.Contents = new[] + { + new TaskItem(content, new Metadata + { + { MetadataName.PackageId, task.Manifest.GetMetadata("Id") }, + { MetadataName.PackFolder, PackFolderKind.None }, + { MetadataName.PackagePath, "readme.md" } + }), + }; + + task.Manifest.SetMetadata("Readme", "readme.md"); + task.Manifest.SetMetadata("RepositoryType", "git"); + task.Manifest.SetMetadata("RepositoryUrl", "https://github.com/devlooped/nugetizer"); + task.Manifest.SetMetadata("RepositorySha", "abc123def"); + + createPackage = true; + ExecuteTask(out var manifest); + + Assert.NotNull(manifest); + + var file = manifest.Files.FirstOrDefault(f => Path.GetFileName(f.Target) == manifest.Metadata.Readme); + Assert.NotNull(file); + Assert.True(File.Exists(file.Source)); + + var readme = File.ReadAllText(file.Source); + + // Should use 'raw' instead of 'blob' for proper image display + Assert.Contains("https://github.com/devlooped/nugetizer/raw/abc123def/img/logo.png", readme); + Assert.DoesNotContain("/blob/", readme); } [Fact] From 723f09b53378c72f33a37723d023b8c26b37c9ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 05:55:22 +0000 Subject: [PATCH 3/4] Use raw.githubusercontent.com format for GitHub raw content URLs Co-authored-by: kzu <169707+kzu@users.noreply.github.com> --- src/NuGetizer.Tasks/CreatePackage.cs | 9 +++++++-- src/NuGetizer.Tests/CreatePackageTests.cs | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/NuGetizer.Tasks/CreatePackage.cs b/src/NuGetizer.Tasks/CreatePackage.cs index ba7edcfd..7bacfd64 100644 --- a/src/NuGetizer.Tasks/CreatePackage.cs +++ b/src/NuGetizer.Tasks/CreatePackage.cs @@ -240,6 +240,11 @@ void GeneratePackage(Stream output = null) // expr to match markdown links with optional title. use named groups to capture the link text, url and optional title. linkExpr ??= new Regex(@"\[(?<text>[^\]]+)\]\((?<url>[^\s)]+)(?:\s+""(?<title>[^""]*)"")?\)", RegexOptions.None); var repoUrl = manifest.Metadata.Repository.Url.TrimEnd('/'); + + // Extract owner and repo from URL for raw.githubusercontent.com format + var repoPath = uri.AbsolutePath.TrimStart('/'); + var rawBaseUrl = $"https://raw.githubusercontent.com/{repoPath}"; + replaced = linkExpr.Replace(replaced, match => { var url = match.Groups["url"].Value; @@ -249,8 +254,8 @@ void GeneratePackage(Stream output = null) if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) return match.Value; - // Use 'raw' instead of 'blob' for proper image display on nuget.org - var newUrl = $"{repoUrl}/raw/{manifest.Metadata.Repository.Commit}/{url.TrimStart('/')}"; + // Use raw.githubusercontent.com format for proper image display on nuget.org + var newUrl = $"{rawBaseUrl}/{manifest.Metadata.Repository.Commit}/{url.TrimStart('/')}"; // Preserve the title if present if (!string.IsNullOrEmpty(title)) diff --git a/src/NuGetizer.Tests/CreatePackageTests.cs b/src/NuGetizer.Tests/CreatePackageTests.cs index fd776e00..9442ed7b 100644 --- a/src/NuGetizer.Tests/CreatePackageTests.cs +++ b/src/NuGetizer.Tests/CreatePackageTests.cs @@ -333,7 +333,7 @@ public void when_readme_has_relativeurl_then_expands_github_url() var readme = File.ReadAllText(file.Source); - Assert.Contains("[license](https://github.com/devlooped/nugetizer/raw/9dc2cb5de/license.txt)", readme); + Assert.Contains("[license](https://raw.githubusercontent.com/devlooped/nugetizer/9dc2cb5de/license.txt)", readme); } [Fact] @@ -367,7 +367,7 @@ public void when_readme_has_link_with_tooltip_then_preserves_tooltip() var readme = File.ReadAllText(file.Source); - Assert.Contains("[avatar](https://github.com/devlooped/nugetizer/raw/abc123def/avatars/user.png \"User Avatar\")", readme); + Assert.Contains("[avatar](https://raw.githubusercontent.com/devlooped/nugetizer/abc123def/avatars/user.png \"User Avatar\")", readme); } [Fact] @@ -402,7 +402,7 @@ public void when_readme_has_absolute_url_then_does_not_replace() var readme = File.ReadAllText(file.Source); // Should NOT prepend repository URL to absolute URLs - Assert.DoesNotContain("https://github.com/devlooped/nugetizer/raw/abc123def/https://raw.githubusercontent.com", readme); + Assert.DoesNotContain("https://raw.githubusercontent.com/devlooped/nugetizer/abc123def/https://raw.githubusercontent.com", readme); // Should preserve the original absolute URL Assert.Contains("https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/user.png", readme); } @@ -438,8 +438,8 @@ public void when_readme_has_image_link_then_uses_raw_url() var readme = File.ReadAllText(file.Source); - // Should use 'raw' instead of 'blob' for proper image display - Assert.Contains("https://github.com/devlooped/nugetizer/raw/abc123def/img/logo.png", readme); + // Should use raw.githubusercontent.com format for proper image display + Assert.Contains("https://raw.githubusercontent.com/devlooped/nugetizer/abc123def/img/logo.png", readme); Assert.DoesNotContain("/blob/", readme); } From 7c81aaa7939690bd663e3ee416dbfec492eb9f61 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino <daniel@cazzulino.com> Date: Thu, 16 Oct 2025 03:06:32 -0300 Subject: [PATCH 4/4] Apply dotnet-format --- src/NuGetizer.Tasks/CreatePackage.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/NuGetizer.Tasks/CreatePackage.cs b/src/NuGetizer.Tasks/CreatePackage.cs index 7bacfd64..bf77a577 100644 --- a/src/NuGetizer.Tasks/CreatePackage.cs +++ b/src/NuGetizer.Tasks/CreatePackage.cs @@ -240,27 +240,27 @@ void GeneratePackage(Stream output = null) // expr to match markdown links with optional title. use named groups to capture the link text, url and optional title. linkExpr ??= new Regex(@"\[(?<text>[^\]]+)\]\((?<url>[^\s)]+)(?:\s+""(?<title>[^""]*)"")?\)", RegexOptions.None); var repoUrl = manifest.Metadata.Repository.Url.TrimEnd('/'); - + // Extract owner and repo from URL for raw.githubusercontent.com format var repoPath = uri.AbsolutePath.TrimStart('/'); var rawBaseUrl = $"https://raw.githubusercontent.com/{repoPath}"; - + replaced = linkExpr.Replace(replaced, match => { var url = match.Groups["url"].Value; var title = match.Groups["title"].Value; - + // Check if the URL is already absolute if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) return match.Value; // Use raw.githubusercontent.com format for proper image display on nuget.org var newUrl = $"{rawBaseUrl}/{manifest.Metadata.Repository.Commit}/{url.TrimStart('/')}"; - + // Preserve the title if present if (!string.IsNullOrEmpty(title)) return $"[{match.Groups["text"].Value}]({newUrl} \"{title}\")"; - + return $"[{match.Groups["text"].Value}]({newUrl})"; }); }