Skip to content

Commit dd0858a

Browse files
authored
Expand image url as well as link url, use robust Markdig (#662)
* Expand image url as well as link url, use robust Markdig Rather than the brittle regex-based matching we were doing, switch to proper markdown processing so we can better support arbitrary content within the readme without potentially breaking due to the flaky nature of the regex. We were also not expanding the image url if it contained a link, so now we're more consistent. * Attempt to fix the broken test from obsolete MAUI Mac * Split restore from build for better logging * Avoid incompatible assembly from being copied to test output This assembly seems to be a new transitive dep from some prior updates. By excluding it explicitly, we prevent hard-to-diagnose test failures in local builds
1 parent 60bccf8 commit dd0858a

File tree

11 files changed

+224
-30
lines changed

11 files changed

+224
-30
lines changed

.github/workflows/build.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,17 @@ jobs:
4141
- name: ⚙ msbuild
4242
uses: microsoft/[email protected]
4343

44-
- name: 🙏 build
44+
- name: 🙏 restore
4545
shell: pwsh
4646
working-directory: src/NuGetizer.Tests
4747
run: |
4848
dotnet restore Scenarios/given_a_packaging_project/a.nuproj
49+
dotnet workload restore Scenarios/given_multitargeting_libraries/uilibrary.csproj
50+
51+
- name: 🙏 build
52+
shell: pwsh
53+
working-directory: src/NuGetizer.Tests
54+
run: |
4955
# THIS IS IMPORTANT: WE NEED TO BUILD WITH DESKTOP MSBUILD
5056
msbuild -r
5157

.github/workflows/publish.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,18 @@ jobs:
2424
- name: ⚙ msbuild
2525
uses: microsoft/[email protected]
2626

27-
- name: 🙏 build
27+
- name: 🙏 restore
2828
shell: pwsh
2929
working-directory: src/NuGetizer.Tests
3030
run: |
3131
dotnet restore Scenarios/given_a_packaging_project/a.nuproj
32+
dotnet workload restore Scenarios/given_multitargeting_libraries/uilibrary.csproj
33+
34+
- name: 🙏 buildwhen_getting_content_then_multitargets
35+
shell: pwsh
36+
working-directory: src/NuGetizer.Tests
37+
run: |
38+
# THIS IS IMPORTANT: WE NEED TO BUILD WITH DESKTOP MSBUILD
3239
msbuild -r
3340
3441
- name: 🐛 logs

src/ILRepack.targets

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<Project>
2+
<!--
3+
ILRepack.targets provides MSBuild integration for ILRepack, allowing merging of assemblies into a single output.
4+
5+
Extension Points:
6+
- Property 'ILRepack': Set to 'true' to enable ILRepack (default: true in Release, false otherwise).
7+
- Item 'ILRepackInclude': list of assembly filenames to explicitly include in merging.
8+
- Item 'ILRepackExclude': list of assembly filenames to exclude from merging.
9+
- Item 'ILRepackPreserve': Assemblies that should not be deleted after merging, even if merged.
10+
-->
11+
12+
<PropertyGroup>
13+
<ILRepack Condition="'$(ILRepack)' == '' and '$(Configuration)' == 'Release'">true</ILRepack>
14+
<ILRepack Condition="'$(ILRepack)' == ''">false</ILRepack>
15+
<!-- We need to turn on copy-local for ILRepack to find deps -->
16+
<CopyLocalLockFileAssemblies Condition="'$(CopyLocalLockFileAssemblies)' == '' and '$(ILRepack)' == 'true'">true</CopyLocalLockFileAssemblies>
17+
</PropertyGroup>
18+
19+
<Target Name="EnsureILRepack" BeforeTargets="ILRepack" Condition="'$(ILRepack)' == 'true'">
20+
<Exec Command="ilrepack --version" StandardErrorImportance="high" StandardOutputImportance="low" ConsoleToMSBuild="true" IgnoreExitCode="true" ContinueOnError="true">
21+
<Output TaskParameter="ConsoleOutput" PropertyName="ILRepackOutput" />
22+
<Output TaskParameter="ExitCode" PropertyName="ExitCode" />
23+
</Exec>
24+
<Message Importance="high" Text="Using installed dotnet-ilrepack v$(ILRepackOutput)" Condition="$(ExitCode) == '0'" />
25+
<Exec Command="dotnet tool install -g dotnet-ilrepack"
26+
Condition="$(ExitCode) != '0'" />
27+
<Exec Command="ilrepack --version" Condition="$(ExitCode) != '0'" ConsoleToMSBuild="true">
28+
<Output TaskParameter="ConsoleOutput" PropertyName="ILRepackInstalledOutput" />
29+
</Exec>
30+
<Message Importance="high" Text="Installed dotnet-ilrepack v$(ILRepackInstalledOutput)" Condition="$(ExitCode) != '0'" />
31+
</Target>
32+
33+
<Target Name="ILRepackPrepare">
34+
<ItemGroup>
35+
<ILRepackIncludeWildcard Include="@(ILRepackInclude -> WithMetadataValue('StartsWith', 'true'))"/>
36+
<ILRepackIncludeExact Include="@(ILRepackInclude)" Exclude="@(ILRepackIncludeWildcard)" />
37+
<ILRepackExcludeWildcard Include="@(ILRepackExclude -> WithMetadataValue('StartsWith', 'true'))" />
38+
<ILRepackExcludeExact Include="@(ILRepackExclude)" Exclude="@(ILRepackExcludeWildcard)" />
39+
</ItemGroup>
40+
</Target>
41+
42+
<Target Name="ILRepackFilterIncludeStartsWith" DependsOnTargets="ILRepackPrepare;CoreCompile"
43+
Inputs="@(ILRepackIncludeWildcard)" Outputs="|%(ILRepackIncludeWildcard.Identity)|">
44+
<PropertyGroup>
45+
<LocalFilter>%(ILRepackIncludeWildcard.Identity)</LocalFilter>
46+
</PropertyGroup>
47+
<ItemGroup>
48+
<ILRepackCandidate Include="@(ReferenceCopyLocalPaths)" Condition="
49+
'%(Extension)' == '.dll' And
50+
!$([MSBuild]::ValueOrDefault('%(FileName)', '').EndsWith('.resources', StringComparison.OrdinalIgnoreCase)) And
51+
$([MSBuild]::ValueOrDefault('%(FileName)', '').StartsWith('$(LocalFilter)'))" />
52+
</ItemGroup>
53+
</Target>
54+
55+
<Target Name="ILRepackFilterExcludeStartsWith" DependsOnTargets="ILRepackFilterIncludeStartsWith"
56+
Inputs="@(ILRepackExcludeWildcard)" Outputs="|%(ILRepackExcludeWildcard.Identity)|">
57+
<PropertyGroup>
58+
<LocalFilter>%(ILRepackExcludeWildcard.Identity)</LocalFilter>
59+
</PropertyGroup>
60+
<ItemGroup>
61+
<ILRepackCandidate Remove="@(ILRepackCandidate)" Condition="
62+
$([MSBuild]::ValueOrDefault('%(FileName)', '').StartsWith('$(LocalFilter)'))" />
63+
</ItemGroup>
64+
</Target>
65+
66+
<Target Name="ILRepackFilterInclude" BeforeTargets="ILRepack" DependsOnTargets="ILRepackFilterExcludeStartsWith">
67+
<PropertyGroup>
68+
<ILRepackInclude>;@(ILRepackIncludeExact, ';');</ILRepackInclude>
69+
</PropertyGroup>
70+
<ItemGroup>
71+
<ILRepackCandidate Include="@(ReferenceCopyLocalPaths)" Condition="
72+
'%(Extension)' == '.dll' And
73+
!$([MSBuild]::ValueOrDefault('%(FileName)', '').EndsWith('.resources', StringComparison.OrdinalIgnoreCase)) And
74+
$(ILRepackInclude.Contains(';%(FileName);'))" />
75+
</ItemGroup>
76+
</Target>
77+
78+
<Target Name="ILRepackFilterExclude" BeforeTargets="ILRepack" DependsOnTargets="CoreCompile">
79+
<PropertyGroup>
80+
<ILRepackExclude>;@(ILRepackExcludeExact, ';');</ILRepackExclude>
81+
</PropertyGroup>
82+
<ItemGroup>
83+
<ILRepackCandidate Remove="@(ILRepackCandidate)" Condition="$(ILRepackExclude.Contains(';%(FileName);'))" />
84+
</ItemGroup>
85+
</Target>
86+
87+
<Target Name="ILRepack" AfterTargets="CoreCompile" BeforeTargets="CopyFilesToOutputDirectory"
88+
Inputs="@(IntermediateAssembly -&gt; '%(FullPath)')"
89+
Outputs="$(IntermediateOutputPath)ilrepack.txt"
90+
Returns="@(MergedAssemblies)"
91+
Condition="Exists(@(IntermediateAssembly -&gt; '%(FullPath)')) And '$(ILRepack)' == 'true'">
92+
93+
<ItemGroup>
94+
<MergedAssemblies Include="@(ILRepackCandidate)" />
95+
</ItemGroup>
96+
<ItemGroup>
97+
<ReferenceCopyLocalDirs Include="@(ReferenceCopyLocalPaths -&gt; '%(RootDir)%(Directory)')" />
98+
<ReferenceCopyLocalPaths Remove="@(MergedAssemblies)" />
99+
<LibDir Include="@(ReferenceCopyLocalDirs -&gt; Distinct())" />
100+
</ItemGroup>
101+
<PropertyGroup>
102+
<AbsoluteAssemblyOriginatorKeyFile Condition="'$(SignAssembly)' == 'true' and '$(AssemblyOriginatorKeyFile)' != ''">$([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','$(AssemblyOriginatorKeyFile)'))))</AbsoluteAssemblyOriginatorKeyFile>
103+
<ILRepackArgs Condition="'$(AbsoluteAssemblyOriginatorKeyFile)' != ''">/keyfile:"$(AbsoluteAssemblyOriginatorKeyFile)" /delaysign</ILRepackArgs>
104+
<ILRepackArgs>$(ILRepackArgs) /internalize</ILRepackArgs>
105+
<ILRepackArgs>$(ILRepackArgs) /union</ILRepackArgs>
106+
<!-- This is needed to merge types with identical names into one, wich happens with IFluentInterface in Merq and Merq.Core (Xamarin.Messaging dependencies) -->
107+
<ILRepackArgs>$(ILRepackArgs) @(LibDir -&gt; '/lib:"%(Identity)."', ' ')</ILRepackArgs>
108+
<ILRepackArgs>$(ILRepackArgs) /out:"@(IntermediateAssembly -&gt; '%(FullPath)')"</ILRepackArgs>
109+
<ILRepackArgs>$(ILRepackArgs) "@(IntermediateAssembly -&gt; '%(FullPath)')"</ILRepackArgs>
110+
<ILRepackArgs>$(ILRepackArgs) @(MergedAssemblies -&gt; '"%(FullPath)"', ' ')</ILRepackArgs>
111+
<!--<ILRepackArgs>$(ILRepackArgs) "/lib:$(NetstandardDirectory)"</ILRepackArgs> -->
112+
<!-- This is needed for ilrepack to find netstandard.dll, which is referenced by the System.Text.Json assembly -->
113+
</PropertyGroup>
114+
<Exec Command='ilrepack $(ILRepackArgs)' WorkingDirectory="$(MSBuildProjectDirectory)\$(OutputPath)" StandardErrorImportance="high" IgnoreStandardErrorWarningFormat="true" StandardOutputImportance="low" ConsoleToMSBuild="true" ContinueOnError="true">
115+
<Output TaskParameter="ConsoleOutput" PropertyName="ILRepackOutput" />
116+
<Output TaskParameter="ExitCode" PropertyName="ExitCode" />
117+
</Exec>
118+
<Message Importance="high" Text="$(ILRepackOutput)" Condition="'$(ExitCode)' != '0'" />
119+
<Delete Files="$(IntermediateOutputPath)ilrepack.txt" Condition="'$(ExitCode)' != '0'" />
120+
<Touch AlwaysCreate="true" Files="$(IntermediateOutputPath)ilrepack.txt" Condition="'$(ExitCode)' == '0'" />
121+
<Error Text="$(ILRepackOutput)" Condition="'$(ExitCode)' != '0' And '$(ContinueOnError)' != 'true'" />
122+
<ItemGroup>
123+
<MergedAssembliesToRemove Include="@(MergedAssemblies)" />
124+
<MergedAssembliesToRemove Remove="@(ILRepackPreserve)" />
125+
</ItemGroup>
126+
<Delete Files="@(MergedAssembliesToRemove -&gt; '$(MSBuildProjectDirectory)\$(OutputPath)%(Filename)%(Extension)')" Condition="Exists('$(MSBuildProjectDirectory)\$(OutputPath)%(Filename)%(Extension)')" />
127+
</Target>
128+
129+
</Project>

src/NuGetizer.Tasks/CreatePackage.cs

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
using System.IO;
55
using System.Linq;
66
using System.Security.Cryptography;
7+
using System.Security.Policy;
78
using System.Text.RegularExpressions;
9+
using Markdig;
10+
using Markdig.Renderers.Normalize;
11+
using Markdig.Syntax;
12+
using Markdig.Syntax.Inlines;
813
using Microsoft.Build.Framework;
914
using Microsoft.Build.Utilities;
1015
using NuGet.Frameworks;
@@ -44,7 +49,6 @@ public class CreatePackage : Task
4449
Manifest manifest;
4550
Dictionary<string, string> tokens;
4651
Regex tokensExpr;
47-
Regex linkExpr;
4852

4953
public override bool Execute()
5054
{
@@ -237,33 +241,33 @@ void GeneratePackage(Stream output = null)
237241
Uri.TryCreate(manifest.Metadata.Repository.Url, UriKind.Absolute, out var uri) &&
238242
uri.Host.EndsWith("github.com"))
239243
{
240-
// expr to match markdown links with optional title. use named groups to capture the link text, url and optional title.
241-
// Handle image links inside clickable badges: [![alt](img)](url) by explicitly matching the image pattern
242-
linkExpr ??= new Regex(@"\[(?<text>!\[[^\]]*\]\([^\)]*\)|[^\]]+)\]\((?<url>[^\s)]+)(?:\s+""(?<title>[^""]*)"")?\)", RegexOptions.None);
243-
var repoUrl = manifest.Metadata.Repository.Url.TrimEnd('/');
244-
245244
// Extract owner and repo from URL for raw.githubusercontent.com format
246245
var repoPath = uri.AbsolutePath.TrimStart('/');
247246
var rawBaseUrl = $"https://raw.githubusercontent.com/{repoPath}";
248247

249-
replaced = linkExpr.Replace(replaced, match =>
250-
{
251-
var url = match.Groups["url"].Value;
252-
var title = match.Groups["title"].Value;
248+
var document = Markdown.Parse(replaced);
249+
var links = document.Descendants<LinkInline>().ToList();
253250

254-
// Check if the URL is already absolute
255-
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
256-
return match.Value;
251+
foreach (var link in links)
252+
{
253+
if (string.IsNullOrEmpty(link.Url) || Uri.IsWellFormedUriString(link.Url, UriKind.Absolute))
254+
continue;
257255

258-
// Use raw.githubusercontent.com format for proper image display on nuget.org
259-
var newUrl = $"{rawBaseUrl}/{manifest.Metadata.Repository.Commit}/{url.TrimStart('/')}";
256+
link.Url = $"{rawBaseUrl}/{manifest.Metadata.Repository.Commit}/{link.Url.TrimStart('/')}";
260257

261-
// Preserve the title if present
262-
if (!string.IsNullOrEmpty(title))
263-
return $"[{match.Groups["text"].Value}]({newUrl} \"{title}\")";
258+
if (link.FirstChild is LinkInline img &&
259+
!string.IsNullOrEmpty(img.Url) &&
260+
!Uri.IsWellFormedUriString(img.Url, UriKind.Absolute))
261+
{
262+
img.Url = $"{rawBaseUrl}/{manifest.Metadata.Repository.Commit}/{img.Url.TrimStart('/')}";
263+
}
264+
}
264265

265-
return $"[{match.Groups["text"].Value}]({newUrl})";
266-
});
266+
// render the document to console
267+
using var writer = new StringWriter();
268+
var renderer = new NormalizeRenderer(writer);
269+
renderer.Render(document);
270+
replaced = writer.ToString();
267271
}
268272

269273
if (!replaced.Equals(File.ReadAllText(readmeFile.Source), StringComparison.Ordinal))

src/NuGetizer.Tasks/NuGetizer.Tasks.csproj

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<DevelopmentDependency>true</DevelopmentDependency>
1919
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
2020
<PackageLicenseFile>OSMFEULA.txt</PackageLicenseFile>
21+
22+
<ILRepack>true</ILRepack>
2123
</PropertyGroup>
2224

2325
<ItemGroup>
@@ -27,10 +29,18 @@
2729
<PackageReference Include="ThisAssembly.Project" Version="2.1.2" PrivateAssets="all" />
2830
<PackageReference Include="ThisAssembly.Strings" Version="2.1.2" PrivateAssets="all" />
2931
<PackageReference Include="Minimatch" Version="2.0.0" PrivateAssets="all" />
32+
<PackageReference Include="Markdig" Version="0.42.0" PrivateAssets="all" />
33+
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" PrivateAssets="all" />
3034
</ItemGroup>
3135

3236
<ItemGroup>
3337
<ProjectCapability Include="Pack" />
38+
<ILRepackInclude Include="Markdig" />
39+
<ILRepackInclude Include="System.Buffers" />
40+
<ILRepackInclude Include="System.Memory" />
41+
<ILRepackInclude Include="System.Numerics.Vectors" />
42+
<ILRepackInclude Include="System.Runtime.CompilerServices.Unsafe" />
43+
<ILRepackInclude Include="Microsoft.NET.StringTools" />
3444
</ItemGroup>
3545

3646
<ItemGroup>
@@ -52,5 +62,5 @@
5262

5363
<Import Project="NuGetizer.Tasks.Pack.targets" Condition="'$(GeneratePackageOnBuild)' == 'false' AND '$(NuGetize)' != 'true'" />
5464
<Import Project="NuGetizer.Tasks.targets" />
55-
65+
<Import Project="..\ILRepack.targets" />
5666
</Project>

src/NuGetizer.Tests/CreatePackageTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,44 @@ public void when_readme_has_image_link_then_uses_raw_url()
443443
Assert.DoesNotContain("/blob/", readme);
444444
}
445445

446+
[Fact]
447+
public void when_readme_has_relative_image_url_and_link_then_expands_both()
448+
{
449+
var content = Path.GetTempFileName();
450+
File.WriteAllText(content, "[![Image](img/logo.png)](osmf.txt)");
451+
task.Contents = new[]
452+
{
453+
new TaskItem(content, new Metadata
454+
{
455+
{ MetadataName.PackageId, task.Manifest.GetMetadata("Id") },
456+
{ MetadataName.PackFolder, PackFolderKind.None },
457+
{ MetadataName.PackagePath, "readme.md" }
458+
}),
459+
};
460+
461+
task.Manifest.SetMetadata("Readme", "readme.md");
462+
task.Manifest.SetMetadata("RepositoryType", "git");
463+
task.Manifest.SetMetadata("RepositoryUrl", "https://github.com/devlooped/nugetizer");
464+
task.Manifest.SetMetadata("RepositorySha", "abc123def");
465+
466+
createPackage = true;
467+
ExecuteTask(out var manifest);
468+
469+
Assert.NotNull(manifest);
470+
471+
var file = manifest.Files.FirstOrDefault(f => Path.GetFileName(f.Target) == manifest.Metadata.Readme);
472+
Assert.NotNull(file);
473+
Assert.True(File.Exists(file.Source));
474+
475+
var readme = File.ReadAllText(file.Source);
476+
477+
// Should use raw.githubusercontent.com format for proper image display
478+
Assert.Contains("https://raw.githubusercontent.com/devlooped/nugetizer/abc123def/img/logo.png", readme);
479+
Assert.Contains("https://raw.githubusercontent.com/devlooped/nugetizer/abc123def/osmf.txt", readme);
480+
Assert.DoesNotContain("/blob/", readme);
481+
}
482+
483+
446484
[Fact]
447485
public void when_readme_has_clickable_image_badge_with_relative_url_then_replaces_url()
448486
{

src/NuGetizer.Tests/NuGetizer.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
</ItemGroup>
1212

1313
<ItemGroup>
14+
<PackageReference Include="Microsoft.NET.StringTools" Version="17.14.28" ExcludeAssets="all" />
1415
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
1516
<PackageReference Include="xunit" Version="2.9.3" />
1617
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" PrivateAssets="all" />

src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/common.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Import Project="$([MSBuild]::GetPathOfFileAbove(Scenario.props, $(MSBuildThisFileDirectory)))" />
33

44
<PropertyGroup>
5-
<TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
5+
<TargetFrameworks>net8.0;netstandard2.1</TargetFrameworks>
66
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
77
<IsPackable>true</IsPackable>
88
</PropertyGroup>

src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uilibrary.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
<Import Project="$([MSBuild]::GetPathOfFileAbove(Scenario.props, $(MSBuildThisFileDirectory)))" />
33

44
<PropertyGroup>
5-
<TargetFrameworks>net8.0;net8.0-windows;net8.0-maccatalyst</TargetFrameworks>
5+
<TargetFrameworks>net9.0;net9.0-windows;net9.0-maccatalyst</TargetFrameworks>
66
<PackOnBuild>true</PackOnBuild>
77
<IsPackable>true</IsPackable>
8-
<SupportedOSPlatformVersion Condition="'$(TargetFramework)' == 'net8.0-maccatalyst'">14.0</SupportedOSPlatformVersion>
98
</PropertyGroup>
109

1110
<ItemGroup>

src/NuGetizer.Tests/Scenarios/given_multitargeting_libraries/uishared.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Import Project="$([MSBuild]::GetPathOfFileAbove(Scenario.props, $(MSBuildThisFileDirectory)))" />
33

44
<PropertyGroup>
5-
<TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
5+
<TargetFrameworks>net8.0;netstandard2.1</TargetFrameworks>
66
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
77
<IsPackable>true</IsPackable>
88
</PropertyGroup>

0 commit comments

Comments
 (0)