diff --git a/src/Controls/tests/Core.UnitTests/WebViewHelperTests.cs b/src/Controls/tests/Core.UnitTests/WebViewHelperTests.cs index a3b895536b69..185a16784b29 100644 --- a/src/Controls/tests/Core.UnitTests/WebViewHelperTests.cs +++ b/src/Controls/tests/Core.UnitTests/WebViewHelperTests.cs @@ -104,4 +104,50 @@ public void EscapeJsString_RepeatedEscapedQuotes() var result = WebViewHelper.EscapeJsString(input); Assert.Equal(expected, result); } + + [Fact] + public void EscapeJsString_SimpleJavaScriptWithNewlines() + { + const string input = "var x = 5;\r\n" + + "var y = 10;\r" + + "var z = x + y;\n"; + + const string expected = "var x = 5;\\nvar y = 10;\\nvar z = x + y;\\n"; + var result = WebViewHelper.EscapeJsString(input); + Assert.Equal(expected, result); + } + + [Fact] + public void EscapeJsString_TemplateLiterals() + { + const string input = @"let poll = `Is .NET MAUI cool? +- Yes! +- Yes! +- Yes! +Wow, so it is! +` +console.log(poll);"; + + const string expected = "let poll = \\`Is .NET MAUI cool?\\n- Yes!\\n- Yes!\\n- Yes!\\n" + + "Wow, so it is!\\n\\`\\nconsole.log(poll);"; + var result = WebViewHelper.EscapeJsString(input); + Assert.Equal(expected, result); + } + + [Fact] + public void EscapeJsString_BackslashContinuations() + { + const string input = @"let poll = 'Is .NET MAUI cool? \n\ +- Yes! \n\ +- Yes! \n\ +- Yes! \n\ +Wow, so it is! \n\ +' +console.log(poll);"; + + const string expected = "let poll = \\'Is .NET MAUI cool? \\\\n- Yes! \\\\n- Yes! \\\\n" + + "- Yes! \\\\nWow, so it is! \\\\n\\'\\nconsole.log(poll);"; + var result = WebViewHelper.EscapeJsString(input); + Assert.Equal(expected, result); + } } diff --git a/src/Core/src/Handlers/WebView/WebViewHelper.cs b/src/Core/src/Handlers/WebView/WebViewHelper.cs index febd2f14a373..36b23fe02ed6 100644 --- a/src/Core/src/Handlers/WebView/WebViewHelper.cs +++ b/src/Core/src/Handlers/WebView/WebViewHelper.cs @@ -5,10 +5,54 @@ internal static partial class WebViewHelper { + const string NewlineMarker = "##NL##"; + internal static string? EscapeJsString(string js) { - if (js == null) - return null; + if (string.IsNullOrEmpty(js)) + return js; + + // Normalize line endings + js = Regex.Replace(js, @"\r\n|\r", "\n"); + +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + bool hasBacktick = js.Contains('`', StringComparison.Ordinal); +#else + bool hasBacktick = js.IndexOf('`') != -1; +#endif + + // Escape sequence marker +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + js = js.Replace("\\n", NewlineMarker, StringComparison.Ordinal); +#else + js = js.Replace("\\n", NewlineMarker); +#endif + // Escape backticks if present + if (hasBacktick) + { +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + js = js.Replace("`", "\\`", StringComparison.Ordinal); +#else + js = js.Replace("`", "\\`"); +#endif + } + + // Remove backslash-newline continuation + js = Regex.Replace(js, @"\\[ \t]*\n", string.Empty); + + // Replace literal newlines with \n +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + js = js.Replace("\n", "\\n", StringComparison.Ordinal); +#else + js = js.Replace("\n", "\\n"); +#endif + + // Restore original escape sequences +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + js = js.Replace(NewlineMarker, "\\\\n", StringComparison.Ordinal); +#else + js = js.Replace(NewlineMarker, "\\\\n"); +#endif #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER if (!js.Contains('\'', StringComparison.Ordinal))