Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions PowerForge.Tests/WebReleaseHubRenderingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,24 @@ public void Build_ReleaseChangelog_NamespacesRepeatedHeadingIdsPerRelease()
Assert.Contains("href=\"#v1-3-0-whats-changed\"", html, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void Build_ReleaseChangelog_PreservesNonHeadingFragmentLinks()
{
var html = BuildSinglePageSite(
"""
{{< release-changelog product="intelligencex.chat" limit="5" includePreview="true" >}}
""",
setup: WriteReleaseHubDataWithCustomAnchor,
useScribanTheme: false,
scribanLayoutBody: null);

Assert.Contains("id=\"v1-2-0-whats-changed\"", html, StringComparison.OrdinalIgnoreCase);
Assert.Contains("href=\"#v1-2-0-whats-changed\"", html, StringComparison.OrdinalIgnoreCase);
Assert.Contains("id=\"custom-anchor\"", html, StringComparison.OrdinalIgnoreCase);
Assert.Contains("href=\"#custom-anchor\"", html, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("href=\"#v1-2-0-custom-anchor\"", html, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void Build_RendersReleaseButtons_ForAllProducts_WithWildcardFilter()
{
Expand Down Expand Up @@ -477,4 +495,42 @@ private static void WriteDuplicateHeadingReleaseHubData(string root)
}
""");
}

private static void WriteReleaseHubDataWithCustomAnchor(string root)
{
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(dataDir);
File.WriteAllText(Path.Combine(dataDir, "release-hub.json"),
"""
{
"title": "IntelligenceX Releases",
"products": [
{ "id": "intelligencex.chat", "name": "IX Chat", "order": 10 }
],
"releases": [
{
"tag": "v1.2.0",
"title": "IntelligenceX 1.2.0",
"url": "https://github.com/EvotecIT/IntelligenceX/releases/tag/v1.2.0",
"publishedAt": "2026-02-25T10:00:00Z",
"isPrerelease": false,
"isLatestStable": true,
"body_html": "<h2 id=\"whats-changed\">What's Changed</h2><ul><li>Stable improvements</li></ul><p><a href=\"#whats-changed\">Jump heading</a> <a id=\"custom-anchor\"></a><a href=\"#custom-anchor\">Jump anchor</a></p>",
"assets": [
{
"name": "IntelligenceX.Chat-win-x64-v1.2.0.zip",
"downloadUrl": "https://example.test/downloads/ix-chat-win-x64-v1.2.0.zip",
"product": "intelligencex.chat",
"channel": "stable",
"platform": "windows",
"arch": "x64",
"kind": "zip",
"size": 5242880
}
]
}
]
}
""");
}
}
9 changes: 8 additions & 1 deletion PowerForge.Web/Services/ReleaseHubRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ private static string NamespaceReleaseBodyHtml(string html, string? releaseTag)
if (string.IsNullOrWhiteSpace(prefix))
return html;

var remappedHeadingIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var namespaced = HeadingWithIdRegex.Replace(html, match =>
{
var level = match.Groups["level"].Value;
Expand All @@ -551,19 +552,25 @@ private static string NamespaceReleaseBodyHtml(string html, string? releaseTag)
return match.Value;

var namespacedId = NamespaceFragment(prefix, baseId);
remappedHeadingIds[baseId] = namespacedId;
var attrsWithId = idMatch.Success
? IdAttributeRegex.Replace(attrs, $" id=\"{namespacedId}\"", 1)
: (string.IsNullOrWhiteSpace(attrs) ? $" id=\"{namespacedId}\"" : $"{attrs} id=\"{namespacedId}\"");
return $"<h{level}{attrsWithId}>{text}</h{level}>";
});

if (remappedHeadingIds.Count == 0)
return namespaced;

return FragmentLinkRegex.Replace(namespaced, match =>
{
var target = match.Groups["target"].Value;
if (string.IsNullOrWhiteSpace(target))
return match.Value;

var namespacedTarget = NamespaceFragment(prefix, target);
if (!remappedHeadingIds.TryGetValue(target, out var namespacedTarget))
return match.Value;

return $"{match.Groups["prefix"].Value}{match.Groups["quote"].Value}#{namespacedTarget}{match.Groups["quote"].Value}{match.Groups["suffix"].Value}";
});
}
Expand Down
Loading