diff --git a/src/Seq.Syntax/Expressions/Runtime/RuntimeOperators.cs b/src/Seq.Syntax/Expressions/Runtime/RuntimeOperators.cs
index 0d3a7d9..d5900e2 100644
--- a/src/Seq.Syntax/Expressions/Runtime/RuntimeOperators.cs
+++ b/src/Seq.Syntax/Expressions/Runtime/RuntimeOperators.cs
@@ -14,6 +14,7 @@
using System.Globalization;
using System.Runtime.CompilerServices;
+using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Seq.Syntax.Expressions.Compilation.Linq;
@@ -25,6 +26,13 @@ namespace Seq.Syntax.Expressions.Runtime;
static class RuntimeOperators
{
+ static readonly JsonSerializerOptions ToJsonSerializerOptions = new()
+ {
+ // Avoids defense-in-depth encoding of HTML content chars/non-ASCII data, which renders the results unreadable
+ // for many non-Latin languages.
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
internal static EvaluationResult ScalarBoolean(bool value)
{
return JsonValue.Create(value);
@@ -552,12 +560,10 @@ public static EvaluationResult UriEncode(string value)
}
public static EvaluationResult ToJson(JsonNode? value)
- {
- // Serializes over the *inserted* form of a typed scalar: `Values.Clone` degrades a level
- // to its string moniker and rejects pre-encoded `unsafe()` output. Nodes nested within
- // containers were already degraded when they were inserted.
+ {
+ // `Values.Clone` handles the level wrapper type and rejects pre-encoded `unsafe()` output.
var node = value is JsonValue ? Values.Clone(value) : value;
- return JsonValue.Create(node?.ToJsonString() ?? "null");
+ return JsonValue.Create(node?.ToJsonString(ToJsonSerializerOptions) ?? "null");
}
public static EvaluationResult FromJson(string json)
diff --git a/src/Seq.Syntax/Templates/Rendering/JsonWriter.cs b/src/Seq.Syntax/Templates/Rendering/JsonWriter.cs
index 02236f2..704ff93 100644
--- a/src/Seq.Syntax/Templates/Rendering/JsonWriter.cs
+++ b/src/Seq.Syntax/Templates/Rendering/JsonWriter.cs
@@ -45,13 +45,7 @@ public JsonWriter(TemplateOutputEncoder encoder)
_null = encoder.GetRun(TemplateThemeStyle.Null);
_scalar = encoder.GetRun(TemplateThemeStyle.Scalar);
}
-
- public void Format(JsonNode? value, TextWriter output)
- {
- var invisibleCharacterCount = 0;
- Format(value, output, ref invisibleCharacterCount);
- }
-
+
public void Format(JsonNode? value, TextWriter output, ref int invisibleCharacterCount)
{
try
diff --git a/test/Seq.Syntax.Tests/Cases/expression-evaluation-cases.asv b/test/Seq.Syntax.Tests/Cases/expression-evaluation-cases.asv
index 59eb40c..cdc0157 100644
--- a/test/Seq.Syntax.Tests/Cases/expression-evaluation-cases.asv
+++ b/test/Seq.Syntax.Tests/Cases/expression-evaluation-cases.asv
@@ -350,6 +350,12 @@ tojson(undefined()) ⇶ undefined()
tojson([1, 'b', null]) ⇶ '[1,"b",null]'
tojson({a: 1}) ⇶ '{"a":1}'
tojson(@Level) ⇶ '"Information"'
+// HTML-sensitive and non-ASCII characters are emitted literally rather than as `\uXXXX` escapes
+tojson('&') ⇶ '"&"'
+tojson('it''s 1+1') ⇶ '"it''s 1+1"'
+tojson('日本語 Ñandú') ⇶ '"日本語 Ñandú"'
+tojson({'名前': '値'}) ⇶ '{"名前":"値"}'
+tojson(['<', '>', '&', '''', '+']) ⇶ '["<",">","&","''","+"]'
// JSON deserialization
fromjson('{"a": [1, null, "x"]}') ⇶ {a: [1, null, 'x']}