Skip to content
Open
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
46 changes: 46 additions & 0 deletions src/Controls/tests/Core.UnitTests/WebViewHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
48 changes: 46 additions & 2 deletions src/Core/src/Handlers/WebView/WebViewHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regex operation is called on every JavaScript string. Consider caching the compiled regex or using RegexOptions.Compiled for better performance in repeated calls.

Copilot uses AI. Check for mistakes.

// 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))
Expand Down
Loading