From cd4fdb318ed0ab92e2bbb138e501647b3e602a40 Mon Sep 17 00:00:00 2001 From: rameel Date: Sat, 10 Oct 2026 23:00:15 +0500 Subject: [PATCH] http: parse swap styles the way HTMX does ParseHtmxSwap used Enum.TryParse with ignoreCase, so typed getters accepted numeric strings, other casings and comma-separated names that HTMX treats as unknown styles, while rejecting expressions with leading whitespace or non-space separators that HTMX accepts. Skip leading whitespace, end the style at any whitespace and compare it case-sensitively with the values written by GetSwapValue. --- .../Internal/EnumHelper.cs | 39 +++++++++++----- .../HtmxResponseHeadersTests.cs | 8 ++-- .../Internal/EnumHelperTests.cs | 44 ++++++++++++++----- 3 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/Ramstack.HtmxToolkit/Internal/EnumHelper.cs b/src/Ramstack.HtmxToolkit/Internal/EnumHelper.cs index 6ffbb0d..79f872d 100644 --- a/src/Ramstack.HtmxToolkit/Internal/EnumHelper.cs +++ b/src/Ramstack.HtmxToolkit/Internal/EnumHelper.cs @@ -178,25 +178,44 @@ or HtmxFetchMode.Cors } /// - /// Parses a string into a value. + /// Parses the swap style at the start of a swap expression into a value. /// - /// The string to parse. + /// The swap expression to parse. /// /// The parsed value if successful; /// otherwise, . /// + /// + /// Like HTMX, ignores leading whitespace, treats any whitespace as the separator + /// before swap modifiers, and compares the style case-sensitively. + /// Only the values returned by are recognized. + /// public static HtmxSwap? ParseHtmxSwap(string? expression) { - expression ??= ""; + var s = expression.AsSpan().TrimStart(); - var index = expression.IndexOf(' '); - if (index < 0) - index = expression.Length; + var index = 0; + while ((uint)index < (uint)s.Length && !char.IsWhiteSpace(s[index])) + index++; - var s = expression.AsSpan(0, index); - if (Enum.TryParse(s, ignoreCase: true, out var value)) - return value; + if ((uint)index < (uint)s.Length) + s = s[..index]; - return null; + return s switch + { + "innerHTML" => HtmxSwap.InnerHtml, + "outerHTML" => HtmxSwap.OuterHtml, + "innerMorph" => HtmxSwap.InnerMorph, + "outerMorph" => HtmxSwap.OuterMorph, + "outerSync" => HtmxSwap.OuterSync, + "textContent" => HtmxSwap.TextContent, + "beforebegin" => HtmxSwap.BeforeBegin, + "afterbegin" => HtmxSwap.AfterBegin, + "beforeend" => HtmxSwap.BeforeEnd, + "afterend" => HtmxSwap.AfterEnd, + "delete" => HtmxSwap.Delete, + "none" => HtmxSwap.None, + _ => null + }; } } diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseHeadersTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseHeadersTests.cs index 9948c1d..95c4779 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseHeadersTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseHeadersTests.cs @@ -60,13 +60,15 @@ public void Reswap_IsNull_WhenHeaderAbsent() Assert.That(headers.Reswap, Is.Null); } - [Test] - public void Reswap_IsNull_WhenHeaderUnknown() + [TestCase("bogus")] + [TestCase("OuterHTML")] + [TestCase("1")] + public void Reswap_IsNull_WhenHeaderUnknown(string value) { var context = TestHelper.CreateHttpContext(); var headers = context.Response.GetHtmxHeaders(); - context.Response.Headers[HtmxResponseHeaderNames.Reswap] = "bogus"; + context.Response.Headers[HtmxResponseHeaderNames.Reswap] = value; Assert.That(headers.Reswap, Is.Null); } diff --git a/tests/Ramstack.HtmxToolkit.Tests/Internal/EnumHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/Internal/EnumHelperTests.cs index 3e1ee4a..e85d56f 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/Internal/EnumHelperTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/Internal/EnumHelperTests.cs @@ -42,21 +42,41 @@ public void GetSwapValue_ReturnsNull_ForNullValue() public void ParseHtmxSwap_ParsesValue(string expression, HtmxSwap expected) => Assert.That(EnumHelper.ParseHtmxSwap(expression), Is.EqualTo(expected)); - [Test] - public void ParseHtmxSwap_IgnoresModifiers() => - Assert.That(EnumHelper.ParseHtmxSwap("innerHTML show:#content"), Is.EqualTo(HtmxSwap.InnerHtml)); + [TestCase("innerHTML show:#content", HtmxSwap.InnerHtml)] + [TestCase("outerHTML\tsettle:200ms", HtmxSwap.OuterHtml)] + [TestCase("beforeend\n swap:1s", HtmxSwap.BeforeEnd)] + public void ParseHtmxSwap_IgnoresModifiers(string expression, HtmxSwap expected) => + Assert.That(EnumHelper.ParseHtmxSwap(expression), Is.EqualTo(expected)); - [Test] - public void ParseHtmxSwap_IsCaseInsensitive() => - Assert.That(EnumHelper.ParseHtmxSwap("OuterHTML"), Is.EqualTo(HtmxSwap.OuterHtml)); + [TestCase(" outerHTML", HtmxSwap.OuterHtml)] + [TestCase("\t outerHTML show:top", HtmxSwap.OuterHtml)] + public void ParseHtmxSwap_IgnoresLeadingWhitespace(string expression, HtmxSwap expected) => + Assert.That(EnumHelper.ParseHtmxSwap(expression), Is.EqualTo(expected)); - [Test] - public void ParseHtmxSwap_ReturnsNull_ForUnknownValue() => - Assert.That(EnumHelper.ParseHtmxSwap("bogus"), Is.Null); + [TestCase("OuterHTML")] + [TestCase("outerhtml")] + [TestCase("InnerHtml")] + [TestCase("BEFOREEND")] + public void ParseHtmxSwap_IsCaseSensitive(string expression) => + Assert.That(EnumHelper.ParseHtmxSwap(expression), Is.Null); - [Test] - public void ParseHtmxSwap_ReturnsNull_ForNullExpression() => - Assert.That(EnumHelper.ParseHtmxSwap(null), Is.Null); + [TestCase("bogus")] + [TestCase("1")] + [TestCase("42")] + [TestCase("-1")] + [TestCase("innerHTML,outerHTML")] + [TestCase("before")] + [TestCase("append")] + public void ParseHtmxSwap_ReturnsNull_ForUnknownValue(string expression) => + Assert.That(EnumHelper.ParseHtmxSwap(expression), Is.Null); + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("show:top")] + [TestCase("swap:1s settle:200ms")] + public void ParseHtmxSwap_ReturnsNull_WithoutStyle(string? expression) => + Assert.That(EnumHelper.ParseHtmxSwap(expression), Is.Null); [TestCase(HttpVerb.Get, "get")] [TestCase(HttpVerb.Head, "head")]