From d8e286d6240abdf91976bbc17b74c3f406e11a4c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 24 Jun 2025 12:46:35 +0200 Subject: [PATCH 1/7] add version number has to hx-get to break cache on version updates --- .../Myst/Renderers/HtmxLinkInlineRenderer.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index 0d9360abe..aa86030b4 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -2,6 +2,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Reflection; +using Elastic.Documentation.Extensions; using Elastic.Documentation.Site; using Elastic.Documentation.Site.Navigation; using Elastic.Markdown.IO; @@ -14,6 +16,11 @@ namespace Elastic.Markdown.Myst.Renderers; public class HtmxLinkInlineRenderer : LinkInlineRenderer { + private static readonly string Version = + Assembly.GetExecutingAssembly().GetCustomAttributes() + .FirstOrDefault()?.InformationalVersion ?? "0.0.0"; + private static readonly string VersionHash = ShortId.Create(Version); + protected override void Write(HtmlRenderer renderer, LinkInline link) { if (renderer.EnableHtmlForInline && !link.IsImage) @@ -25,7 +32,8 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) return; } - var url = link.GetDynamicUrl != null ? link.GetDynamicUrl() : link.Url; + var url = link.GetDynamicUrl?.Invoke() ?? link.Url; + url = $"{url}?v={VersionHash}"; var isCrossLink = (link.GetData("isCrossLink") as bool?) == true; var isHttpLink = url?.StartsWith("http") ?? false; From 7fa4cb8e57272a771e65773e840dfd682962a878 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 24 Jun 2025 12:51:53 +0200 Subject: [PATCH 2/7] update more places with hx-get and make append of querystring try to check for existing querystring --- src/Elastic.Documentation.Site/Htmx.cs | 8 ++++++++ .../Myst/Renderers/HtmxLinkInlineRenderer.cs | 8 ++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Elastic.Documentation.Site/Htmx.cs b/src/Elastic.Documentation.Site/Htmx.cs index a934a17f6..42ca04f88 100644 --- a/src/Elastic.Documentation.Site/Htmx.cs +++ b/src/Elastic.Documentation.Site/Htmx.cs @@ -2,12 +2,19 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Reflection; using System.Text; +using Elastic.Documentation.Extensions; namespace Elastic.Documentation.Site; public static class Htmx { + private static readonly string Version = + Assembly.GetExecutingAssembly().GetCustomAttributes() + .FirstOrDefault()?.InformationalVersion ?? "0.0.0"; + public static readonly string VersionHash = ShortId.Create(Version); + public static string GetHxSelectOob(bool hasSameTopLevelGroup) => hasSameTopLevelGroup ? "#content-container,#toc-nav" : "#main-container"; public const string Preload = "mousedown"; public const string HxSwap = "none"; @@ -24,6 +31,7 @@ public static string GetHxAttributes( string? hxIndicator = HxIndicator ) { + targetUrl = targetUrl.Contains('?') ? $"{targetUrl}&v={VersionHash}" : $"{targetUrl}?v={VersionHash}"; var attributes = new StringBuilder(); _ = attributes.Append($" hx-get={targetUrl}"); _ = attributes.Append($" hx-select-oob={hxSwapOob ?? GetHxSelectOob(hasSameTopLevelGroup)}"); diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index aa86030b4..5463adc60 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -16,11 +16,6 @@ namespace Elastic.Markdown.Myst.Renderers; public class HtmxLinkInlineRenderer : LinkInlineRenderer { - private static readonly string Version = - Assembly.GetExecutingAssembly().GetCustomAttributes() - .FirstOrDefault()?.InformationalVersion ?? "0.0.0"; - private static readonly string VersionHash = ShortId.Create(Version); - protected override void Write(HtmlRenderer renderer, LinkInline link) { if (renderer.EnableHtmlForInline && !link.IsImage) @@ -33,7 +28,8 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) } var url = link.GetDynamicUrl?.Invoke() ?? link.Url; - url = $"{url}?v={VersionHash}"; + if (url is not null) + url = url.Contains('?') ? $"{url}&v={Htmx.VersionHash}" : $"{url}?v={Htmx.VersionHash}"; var isCrossLink = (link.GetData("isCrossLink") as bool?) == true; var isHttpLink = url?.StartsWith("http") ?? false; From fe39596430d2f43d70c3219abaa11e9f2e51d234 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 24 Jun 2025 12:53:49 +0200 Subject: [PATCH 3/7] only set it on hx-get --- src/Elastic.Documentation.Site/Htmx.cs | 4 ++-- .../Myst/Renderers/HtmxLinkInlineRenderer.cs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Elastic.Documentation.Site/Htmx.cs b/src/Elastic.Documentation.Site/Htmx.cs index 42ca04f88..587ff52bb 100644 --- a/src/Elastic.Documentation.Site/Htmx.cs +++ b/src/Elastic.Documentation.Site/Htmx.cs @@ -31,9 +31,9 @@ public static string GetHxAttributes( string? hxIndicator = HxIndicator ) { - targetUrl = targetUrl.Contains('?') ? $"{targetUrl}&v={VersionHash}" : $"{targetUrl}?v={VersionHash}"; + var hxGetUrl = targetUrl.Contains('?') ? $"{targetUrl}&v={VersionHash}" : $"{targetUrl}?v={VersionHash}"; var attributes = new StringBuilder(); - _ = attributes.Append($" hx-get={targetUrl}"); + _ = attributes.Append($" hx-get={hxGetUrl}"); _ = attributes.Append($" hx-select-oob={hxSwapOob ?? GetHxSelectOob(hasSameTopLevelGroup)}"); _ = attributes.Append($" hx-swap={hxSwap}"); _ = attributes.Append($" hx-push-url={hxPushUrl}"); diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index 5463adc60..3e007a174 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -28,8 +28,9 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) } var url = link.GetDynamicUrl?.Invoke() ?? link.Url; - if (url is not null) - url = url.Contains('?') ? $"{url}&v={Htmx.VersionHash}" : $"{url}?v={Htmx.VersionHash}"; + var hxGetUrl = url; + if (hxGetUrl is not null) + hxGetUrl = hxGetUrl.Contains('?') ? $"{hxGetUrl}&v={Htmx.VersionHash}" : $"{hxGetUrl}?v={Htmx.VersionHash}"; var isCrossLink = (link.GetData("isCrossLink") as bool?) == true; var isHttpLink = url?.StartsWith("http") ?? false; @@ -45,7 +46,7 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) var targetRootNavigation = link.GetData($"Target{nameof(MarkdownFile.NavigationRoot)}") as INodeNavigationItem; var hasSameTopLevelGroup = !isCrossLink && (currentRootNavigation?.Id == targetRootNavigation?.Id); _ = renderer.Write(" hx-get=\""); - _ = renderer.WriteEscapeUrl(url); + _ = renderer.WriteEscapeUrl(hxGetUrl); _ = renderer.Write('"'); _ = renderer.Write($" hx-select-oob=\"{Htmx.GetHxSelectOob(hasSameTopLevelGroup)}\""); _ = renderer.Write($" hx-swap=\"{Htmx.HxSwap}\""); From 0c55c784af17bacfa24fb48ffa22ad32aec97386 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 24 Jun 2025 14:06:27 +0200 Subject: [PATCH 4/7] update test cases --- .../Elastic.Markdown.Tests.csproj | 2 ++ .../Inline/AnchorLinkTests.cs | 16 ++++------ .../Inline/DirectiveBlockLinkTests.cs | 5 ++- .../Inline/InlineAnchorTests.cs | 3 +- .../Inline/InlineLinkTests.cs | 32 ++++++++----------- 5 files changed, 25 insertions(+), 33 deletions(-) diff --git a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj index 1f21189a5..5530dffce 100644 --- a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj +++ b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj @@ -14,10 +14,12 @@ + + diff --git a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs index 284effd8a..c281a7e38 100644 --- a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs @@ -74,7 +74,7 @@ [Sub Requirements](testing/req.md#sub-requirements) [Fact] public void GeneratesHtml() => // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """

Sub Requirements

""" ); @@ -92,7 +92,7 @@ [Sub Requirements](testing/req.md#new-reqs) [Fact] public void GeneratesHtml() => // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """

Sub Requirements

""" ); @@ -108,8 +108,7 @@ public class ExternalPageAnchorAutoTitleTests(ITestOutputHelper output) : Anchor { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """

Special Requirements > Sub Requirements

""" ); @@ -126,8 +125,7 @@ public class InPageBadAnchorTests(ITestOutputHelper output) : AnchorLinkTestBase { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """

Hello

""" ); @@ -144,8 +142,7 @@ [Sub Requirements](testing/req.md#sub-requirements2) { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """

Sub Requirements

""" ); @@ -163,8 +160,7 @@ [Heading inside dropdown](testing/req.md#heading-inside-dropdown) { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """Heading inside dropdown""" ); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs index f36d32280..f87c0df2a 100644 --- a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs @@ -64,9 +64,8 @@ [Sub Requirements](testing/req.md#hint_ref) { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Contain( - """

Sub Requirements

""" + Html.ShouldContainHtml( + """

Sub Requirements

""" ); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs index 74dd4b583..41e92e74d 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs @@ -198,8 +198,7 @@ [Sub Requirements](testing/req.md#custom-anchor) { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """

Sub Requirements

""" ); diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs index a4af4da49..f05df1411 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs @@ -47,8 +47,7 @@ public class InlineLinkTests(ITestOutputHelper output) : LinkTestBase(output, { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Be( + Html.ShouldContainHtml( """

Elasticsearch

""" ); @@ -64,8 +63,7 @@ public class LinkToPageTests(ITestOutputHelper output) : LinkTestBase(output, { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """

Requirements

""" ); @@ -84,8 +82,7 @@ public class InsertPageTitleTests(ITestOutputHelper output) : LinkTestBase(outpu { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """

Special Requirements

""" ); @@ -106,8 +103,7 @@ public class RepositoryLinksTest(ITestOutputHelper output) : LinkTestBase(output { [Fact] public void GeneratesHtml() => - // language=html - Html.Should().Contain( + Html.ShouldContainHtml( """

test

""" ); @@ -266,16 +262,16 @@ public class CommentedNonExistingLinks2(ITestOutputHelper output) : LinkTestBase { [Fact] public void GeneratesHtml() => - // language=html - Html.ReplaceLineEndings().TrimEnd().Should().Be(""" -

Links:

- - - """.ReplaceLineEndings()); + Html.ShouldMatchHtml( + """ +

Links:

+ + + """); [Fact] public void HasErrors() => Collector.Diagnostics.Should().HaveCount(0); From cf4cd4ec3008510876d85185232dd0e2142b69ae Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 24 Jun 2025 14:12:50 +0200 Subject: [PATCH 5/7] Add extension method class --- .../IO/Navigation/DocumentationGroup.cs | 2 +- .../PrettyHtmlExtensions.cs | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs diff --git a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs index afbac92bc..c82d7eba7 100644 --- a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs +++ b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs @@ -37,7 +37,7 @@ public bool TryGetTableOfContentsTree(Uri source, [NotNullWhen(true)] out TableO } -[DebuggerDisplay("Toc >{Depth} {FolderName} ({NavigationItems.Count} items)")] +[DebuggerDisplay("Toc >{Depth} {FolderName} {Source} ({NavigationItems.Count} items)")] public class TableOfContentsTree : DocumentationGroup, IRootNavigationItem { public Uri Source { get; } diff --git a/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs b/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs new file mode 100644 index 000000000..08a7279d5 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs @@ -0,0 +1,143 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using AngleSharp.Diffing; +using AngleSharp.Html; +using AngleSharp.Html.Parser; +using DiffPlex.DiffBuilder; +using DiffPlex.DiffBuilder.Model; +using FluentAssertions; +using JetBrains.Annotations; +using Xunit.Internal; +using Xunit.Sdk; + +namespace Elastic.Markdown.Tests; + +public static class PrettyHtmlExtensions +{ + public static string PrettyHtml([LanguageInjection("html")] this string html, bool sanitize = true) + { + var parser = new HtmlParser(); + + var document = parser.ParseDocument(html); + var element = document.Body; + if (element is null) + return string.Empty; + + if (sanitize) + { + var links = element.QuerySelectorAll("a"); + links + .ForEach(l => + { + l.RemoveAttribute("hx-get"); + l.RemoveAttribute("hx-select-oob"); + l.RemoveAttribute("hx-swap"); + l.RemoveAttribute("hx-indicator"); + l.RemoveAttribute("hx-push-url"); + l.RemoveAttribute("preload"); + }); + } + + using var sw = new StringWriter(); + var formatter = new PrettyMarkupFormatter(); + element.Children + .ForEach(c => + { + c.ToHtml(sw, formatter); + }); + return sw.ToString().TrimStart('\n'); + } + + public static void ShouldMatchHtml( + [LanguageInjection("html")] this string actual, + [LanguageInjection("html")] string expected, + bool sanitize = true + ) + { + expected = expected.Trim('\n').PrettyHtml(sanitize); + actual = actual.Trim('\n').PrettyHtml(sanitize); + + var diff = DiffBuilder + .Compare(actual) + .WithTest(expected) + .Build() + .ToArray(); + + if (diff.Length == 0) + return; + + throw new XunitException(CreateDiff(actual, expected, sanitize)); + } + public static void ShouldContainHtml( + [LanguageInjection("html")] this string actual, + [LanguageInjection("html")] string expected, + bool sanitize = true + ) + { + expected = expected.Trim('\n').PrettyHtml(sanitize); + actual = actual.Trim('\n').PrettyHtml(sanitize); + + actual.Should().Contain(expected); + } + + public static string CreateDiff(this string actual, string expected, bool sanitize = true) + { + expected = expected.Trim('\n').PrettyHtml(sanitize); + actual = actual.Trim('\n').PrettyHtml(sanitize); + var diffLines = InlineDiffBuilder.Diff(expected, actual).Lines; + + var mutatedCount = + diffLines + .Count(l => l.Type switch + { + ChangeType.Unchanged => false, + ChangeType.Deleted => true, + ChangeType.Inserted => true, + ChangeType.Imaginary => false, + ChangeType.Modified => true, + _ => false + }); + if (mutatedCount == 0) + return string.Empty; + + var actualLineLength = actual.Split("\n").Length; + if (mutatedCount >= actualLineLength) + { + return $$""" + Mutations {{mutatedCount}} on all {{actualLineLength}} showing + EXPECTED: + {{expected}} + ACTUAL: + {{actual}} + """; + } + + using var sw = new StringWriter(); + diffLines + .ForEach(l => + { + switch (l.Type) + { + case ChangeType.Unchanged: + sw.WriteLine(l.Text); + break; + case ChangeType.Deleted: + sw.WriteLine("- " + l.Text); + break; + case ChangeType.Inserted: + sw.WriteLine("+ " + l.Text); + break; + case ChangeType.Imaginary: + sw.WriteLine("? " + l.Text); + break; + case ChangeType.Modified: + sw.WriteLine("+ " + l.Text); + break; + } + }); + + return sw.ToString(); + } +} From 076f45063cef651aa6f6f82dab16f65bc24bebe2 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 24 Jun 2025 14:26:25 +0200 Subject: [PATCH 6/7] Account for anchors when appending version hash --- src/Elastic.Documentation.Site/Htmx.cs | 58 ++++++++++++++++++- .../Myst/Renderers/HtmxLinkInlineRenderer.cs | 2 +- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/Elastic.Documentation.Site/Htmx.cs b/src/Elastic.Documentation.Site/Htmx.cs index 587ff52bb..c10140132 100644 --- a/src/Elastic.Documentation.Site/Htmx.cs +++ b/src/Elastic.Documentation.Site/Htmx.cs @@ -4,15 +4,70 @@ using System.Reflection; using System.Text; +using System.Text.Encodings.Web; using Elastic.Documentation.Extensions; namespace Elastic.Documentation.Site; +public static class UrlHelper +{ + private static readonly KeyValuePair[] VersionParameters = [new("v", Htmx.VersionHash)]; + + public static string AddVersionParameters(string uri) => AddQueryString(uri, VersionParameters); + + /// + /// Append the given query keys and values to the URI. + /// + /// The base URI. + /// A collection of name value query pairs to append. + /// The combined result. + /// is null. + /// is null. + public static string AddQueryString( + string uri, + IEnumerable> queryString) + { + ArgumentNullException.ThrowIfNull(uri); + ArgumentNullException.ThrowIfNull(queryString); + + var anchorIndex = uri.IndexOf('#'); + var uriToBeAppended = uri.AsSpan(); + var anchorText = ReadOnlySpan.Empty; + // If there is an anchor, then the query string must be inserted before its first occurrence. + if (anchorIndex != -1) + { + anchorText = uriToBeAppended.Slice(anchorIndex); + uriToBeAppended = uriToBeAppended.Slice(0, anchorIndex); + } + + var queryIndex = uriToBeAppended.IndexOf('?'); + var hasQuery = queryIndex != -1; + + var sb = new StringBuilder(); + _ = sb.Append(uriToBeAppended); + foreach (var parameter in queryString) + { + if (parameter.Value == null) + continue; + + _ = sb.Append(hasQuery ? '&' : '?') + .Append(UrlEncoder.Default.Encode(parameter.Key)) + .Append('=') + .Append(UrlEncoder.Default.Encode(parameter.Value)); + hasQuery = true; + } + + _ = sb.Append(anchorText); + return sb.ToString(); + } +} + public static class Htmx { private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttributes() .FirstOrDefault()?.InformationalVersion ?? "0.0.0"; + public static readonly string VersionHash = ShortId.Create(Version); public static string GetHxSelectOob(bool hasSameTopLevelGroup) => hasSameTopLevelGroup ? "#content-container,#toc-nav" : "#main-container"; @@ -31,7 +86,8 @@ public static string GetHxAttributes( string? hxIndicator = HxIndicator ) { - var hxGetUrl = targetUrl.Contains('?') ? $"{targetUrl}&v={VersionHash}" : $"{targetUrl}?v={VersionHash}"; + var hxGetUrl = UrlHelper.AddVersionParameters(targetUrl); + var attributes = new StringBuilder(); _ = attributes.Append($" hx-get={hxGetUrl}"); _ = attributes.Append($" hx-select-oob={hxSwapOob ?? GetHxSelectOob(hasSameTopLevelGroup)}"); diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index 3e007a174..11dc5887c 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -30,7 +30,7 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) var url = link.GetDynamicUrl?.Invoke() ?? link.Url; var hxGetUrl = url; if (hxGetUrl is not null) - hxGetUrl = hxGetUrl.Contains('?') ? $"{hxGetUrl}&v={Htmx.VersionHash}" : $"{hxGetUrl}?v={Htmx.VersionHash}"; + hxGetUrl = UrlHelper.AddVersionParameters(hxGetUrl); var isCrossLink = (link.GetData("isCrossLink") as bool?) == true; var isHttpLink = url?.StartsWith("http") ?? false; From 21426709c1585508df52b8f404ebdd6c5588de3f Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 24 Jun 2025 14:31:43 +0200 Subject: [PATCH 7/7] include anchor and querystring test on hx-get --- tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs index c281a7e38..a939884df 100644 --- a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs @@ -78,6 +78,11 @@ public void GeneratesHtml() => """

Sub Requirements

""" ); + [Fact] + public void HxGetContainsVersionAnchor() => + // language=html + Html.Should().MatchRegex("""hx-get="/docs/testing/req\?v=(.+?)#sub-requirements"""); + [Fact] public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); }