From e11e1277da6760a12124f73bf6c3f350b023bd57 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 2 Sep 2026 11:37:37 +1000 Subject: [PATCH] Have `ToJson()` match the default formatter's escaping behavior (it's unusable for non-Latin languages, otherwise). Assisted-by: Claude:claude-fable-5-1 --- .../Expressions/Runtime/RuntimeOperators.cs | 16 +++++++++++----- src/Seq.Syntax/Templates/Rendering/JsonWriter.cs | 8 +------- .../Cases/expression-evaluation-cases.asv | 6 ++++++ 3 files changed, 18 insertions(+), 12 deletions(-) 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']}