Skip to content

Commit 8a14f68

Browse files
Force literal block for Unicode characters to preserve them without escaping (#751)
1 parent 9a80910 commit 8a14f68

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

src/Persistence.Tests/PaYaml/Serialization/PFxExpressionYamlConverterTests.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,100 @@ public void WriteYamlWithEscapedStringInCodeView()
151151
"\"=Set(gblFoo, true);\\n// The next line has one or more spaces:\\n \\nSet(gblBar, 42);\"");
152152
}
153153

154+
[TestMethod]
155+
public void WriteYamlWithUnicodeCharacters()
156+
{
157+
using var _ = new AssertionScope();
158+
159+
// Test Unicode characters in PowerFx expressions are preserved - most now use plain scalar format with high threshold
160+
VerifySerialize("""Set(用户名, "John")""", """=Set(用户名, "John")""");
161+
VerifySerialize("""Text("こんにちは世界")""", """=Text("こんにちは世界")""");
162+
VerifySerialize("""Format(价格, "$#,##0.00")""", """=Format(价格, "$#,##0.00")""");
163+
164+
// Test mixed Unicode and ASCII
165+
VerifySerialize("""Concatenate("Hello ", 世界, "!")""", """=Concatenate("Hello ", 世界, "!")""");
166+
167+
// Test emoji and special Unicode characters
168+
VerifySerialize("""Set(Status, "✅ Complete")""", """=Set(Status, "✅ Complete")""");
169+
VerifySerialize("""Text("温度: " & Temp & "℃")""", "|-\n =Text(\"温度: \" & Temp & \"\")");
170+
171+
// Test Unicode in variable names and strings - high Unicode characters above 0x20CD0 use literal block to preserve them
172+
VerifySerialize("""Set(变量_测试, "龭唉𫓧G㐁A𫟦D𠳐ⅷ𫇭C丂")""", "|-\n =Set(变量_测试, \"龭唉𫓧G㐁A𫟦D𠳐ⅷ𫇭C丂\")");
173+
}
174+
175+
[TestMethod]
176+
public void WriteYamlWithLatin1SupplementCharacters()
177+
{
178+
using var _ = new AssertionScope();
179+
180+
// Currency symbols that should be preserved without escaping
181+
VerifySerialize("""Set(BritishPrice, "£50.00")""", """=Set(BritishPrice, "£50.00")""");
182+
VerifySerialize("""Set(JapanesePrice, "¥1000")""", """=Set(JapanesePrice, "¥1000")""");
183+
VerifySerialize("""Set(EuroPrice, "€75.50")""", """=Set(EuroPrice, "€75.50")""");
184+
185+
// Accented characters commonly found in European languages
186+
VerifySerialize("""Set(Français, true)""", """=Set(Français, true)""");
187+
VerifySerialize("""Set(Español, "Niño")""", """=Set(Español, "Niño")""");
188+
VerifySerialize("""Set(Deutsch, "Mädchen")""", """=Set(Deutsch, "Mädchen")""");
189+
VerifySerialize("""Set(Português, "São")""", """=Set(Português, "São")""");
190+
191+
// Mathematical and typographical symbols
192+
VerifySerialize("""Set(Temperature, "±23°C")""", """=Set(Temperature, "±23°C")""");
193+
VerifySerialize("""Set(Fraction, "½ cup")""", """=Set(Fraction, "½ cup")""");
194+
VerifySerialize("""Set(Multiplication, "5×3")""", """=Set(Multiplication, "5×3")""");
195+
196+
// Legal and business symbols
197+
VerifySerialize("""Set(Copyright, "© 2024")""", """=Set(Copyright, "© 2024")""");
198+
VerifySerialize("""Set(Registered, "®")""", """=Set(Registered, "®")""");
199+
}
200+
201+
[TestMethod]
202+
public void ReadYamlWithUnicodeCharacters()
203+
{
204+
using var _ = new AssertionScope();
205+
206+
// Test Unicode characters can be deserialized from literal blocks
207+
VerifyDeserialize("Expression: |-\n =Set(用户名, \"John\")", """Set(用户名, "John")""");
208+
VerifyDeserialize("Expression: |-\n =Text(\"こんにちは世界\")", """Text("こんにちは世界")""");
209+
VerifyDeserialize("Expression: |-\n =Format(价格, \"$#,##0.00\")", """Format(价格, "$#,##0.00")""");
210+
211+
// Test mixed Unicode and ASCII
212+
VerifyDeserialize("Expression: |-\n =Concatenate(\"Hello \", 世界, \"!\")", """Concatenate("Hello ", 世界, "!")""");
213+
214+
// Test emoji and special Unicode characters
215+
VerifyDeserialize("Expression: |-\n =Set(Status, \"✅ Complete\")", """Set(Status, "✅ Complete")""");
216+
VerifyDeserialize("Expression: |-\n =Text(\"温度: \" & Temp & \"\")", """Text("温度: " & Temp & "℃")""");
217+
218+
// Test complex Unicode strings
219+
VerifyDeserialize("Expression: |-\n =Set(变量_测试, \"龭唉𫓧G㐁A𫟦D𠳐ⅷ𫇭C丂\")", """Set(变量_测试, "龭唉𫓧G㐁A𫟦D𠳐ⅷ𫇭C丂")""");
220+
}
221+
222+
[TestMethod]
223+
public void ReadYamlWithLatin1SupplementCharacters()
224+
{
225+
using var _ = new AssertionScope();
226+
227+
// Currency symbols that should be preserved when reading from plain scalar format
228+
VerifyDeserialize("""Expression: =Set(BritishPrice, "£50.00")""", """Set(BritishPrice, "£50.00")""");
229+
VerifyDeserialize("""Expression: =Set(JapanesePrice, "¥1000")""", """Set(JapanesePrice, "¥1000")""");
230+
VerifyDeserialize("""Expression: =Set(EuroPrice, "€75.50")""", """Set(EuroPrice, "€75.50")""");
231+
232+
// Accented characters from European languages
233+
VerifyDeserialize("""Expression: =Set(Français, true)""", """Set(Français, true)""");
234+
VerifyDeserialize("""Expression: =Set(Español, "Niño")""", """Set(Español, "Niño")""");
235+
VerifyDeserialize("""Expression: =Set(Deutsch, "Mädchen")""", """Set(Deutsch, "Mädchen")""");
236+
VerifyDeserialize("""Expression: =Set(Português, "São")""", """Set(Português, "São")""");
237+
238+
// Mathematical and typographical symbols
239+
VerifyDeserialize("""Expression: =Set(Temperature, "±23°C")""", """Set(Temperature, "±23°C")""");
240+
VerifyDeserialize("""Expression: =Set(Fraction, "½ cup")""", """Set(Fraction, "½ cup")""");
241+
VerifyDeserialize("""Expression: =Set(Multiplication, "5×3")""", """Set(Multiplication, "5×3")""");
242+
243+
// Legal and business symbols
244+
VerifyDeserialize("""Expression: =Set(Copyright, "© 2024")""", """Set(Copyright, "© 2024")""");
245+
VerifyDeserialize("""Expression: =Set(Registered, "®")""", """Set(Registered, "®")""");
246+
}
247+
154248
private void VerifySerialize(string? pfxScript, string expectedExpressionYaml)
155249
{
156250
var expression = pfxScript is null ? null : new PFxExpressionYaml(pfxScript);

src/Persistence/PaYaml/Serialization/PFxExpressionYamlConverter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public void WriteYaml(IEmitter emitter, object? value, Type type)
5858
// Force multi-line scripts to be literal blocks
5959
forceLiteralBlock |= expression.InvariantScript.Contains('\n')
6060
|| expression.InvariantScript.Contains('\r');
61+
// Force literal block for Unicode characters to preserve them without escaping
62+
forceLiteralBlock |= expression.InvariantScript.Any(char.IsSurrogate);
6163
if (!forceLiteralBlock && !_formattingOptions.ForceLiteralBlockIfContainsAny.IsDefaultOrEmpty)
6264
{
6365
// e.g. our original code was forcing literal block if the script contained any double quotes (`"`).

0 commit comments

Comments
 (0)