Skip to content

Commit a63b3b1

Browse files
impworksRomanx
authored andcommitted
CultureInfo support for rendering values. (#78)
* CultureInfo support for rendering values. * Make compilation renderer work with culture setting * Rename culture-info function and make non-virtual
1 parent 2ab39e2 commit a63b3b1

File tree

8 files changed

+97
-32
lines changed

8 files changed

+97
-32
lines changed

src/Stubble.Compilation/Renderers/TokenRenderers/InterpolationTokenRenderer.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
using System;
77
using System.Linq.Expressions;
8-
using System.Reflection;
98
using System.Text;
109
using System.Threading.Tasks;
1110
using Stubble.Compilation.Contexts;
@@ -20,6 +19,8 @@ namespace Stubble.Compilation.Renderers.TokenRenderers
2019
/// </summary>
2120
public class InterpolationTokenRenderer : ExpressionObjectRenderer<InterpolationToken>
2221
{
22+
private static Type[] formatProviderTypeArgs = new[] { typeof(IFormatProvider) };
23+
2324
/// <inheritdoc/>
2425
protected override void Write(CompilationRenderer renderer, InterpolationToken obj, CompilerContext context)
2526
{
@@ -29,13 +30,24 @@ protected override void Write(CompilationRenderer renderer, InterpolationToken o
2930

3031
if (!context.CompilationSettings.SkipHtmlEncoding && obj.EscapeResult && expression != null)
3132
{
32-
var isValueType = expression.Type.GetIsValueType();
33+
Expression stringExpression;
34+
if (expression.Type == typeof(string))
35+
{
36+
stringExpression = expression;
37+
}
38+
else
39+
{
40+
var formattedToString = expression.Type
41+
.GetMethod(nameof(object.ToString), formatProviderTypeArgs);
42+
43+
var item = expression.Type.GetIsValueType()
44+
? expression
45+
: Expression.Coalesce(expression, Expression.Constant(string.Empty));
3346

34-
var stringExpression = expression.Type == typeof(string)
35-
? expression
36-
: Expression.Call(
37-
isValueType ? expression : Expression.Coalesce(expression, Expression.Constant(string.Empty)),
38-
expression.Type.GetMethod("ToString", Type.EmptyTypes));
47+
stringExpression = formattedToString is object
48+
? Expression.Call(item, formattedToString, Expression.Constant(context.CompilationSettings.CultureInfo))
49+
: Expression.Call(item, expression.Type.GetMethod(nameof(object.ToString), Type.EmptyTypes));
50+
}
3951

4052
expression = Expression.Invoke(context.CompilerSettings.EncodingFuction, stringExpression);
4153
}

src/Stubble.Compilation/Settings/CompilationSettings.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
// </copyright>
55

6+
using System.Globalization;
7+
68
namespace Stubble.Compilation.Settings
79
{
810
/// <summary>
@@ -27,6 +29,11 @@ public class CompilationSettings
2729
/// </summary>
2830
public bool SkipHtmlEncoding { get; set; }
2931

32+
/// <summary>
33+
/// Gets or sets the CultureInfo to use for rendering format-dependent values (doubles, etc.).
34+
/// </summary>
35+
public CultureInfo CultureInfo { get; set; } = CultureInfo.InvariantCulture;
36+
3037
/// <summary>
3138
/// Gets the default render settings
3239
/// </summary>
@@ -37,7 +44,8 @@ public static CompilationSettings GetDefaultRenderSettings()
3744
{
3845
SkipRecursiveLookup = false,
3946
ThrowOnDataMiss = false,
40-
SkipHtmlEncoding = false
47+
SkipHtmlEncoding = false,
48+
CultureInfo = CultureInfo.InvariantCulture
4149
};
4250
}
4351
}

src/Stubble.Core/Renderers/StringRenderer/TokenRenderers/InterpolationTokenRenderer.cs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// </copyright>
55

66
using System;
7-
using System.Net;
7+
using System.Globalization;
88
using System.Threading.Tasks;
99
using Stubble.Core.Contexts;
1010
using Stubble.Core.Tokens;
@@ -16,6 +16,22 @@ namespace Stubble.Core.Renderers.StringRenderer.TokenRenderers
1616
/// </summary>
1717
public class InterpolationTokenRenderer : StringObjectRenderer<InterpolationToken>
1818
{
19+
/// <summary>
20+
/// Renders the value to string using a locale.
21+
/// </summary>
22+
/// <param name="obj">The object to convert</param>
23+
/// <param name="culture">The culture to use</param>
24+
/// <returns>The object stringified into the locale</returns>
25+
protected static string ConvertToStringInCulture(object obj, CultureInfo culture)
26+
{
27+
if (obj is null || obj is string)
28+
{
29+
return obj as string;
30+
}
31+
32+
return Convert.ToString(obj, culture);
33+
}
34+
1935
/// <inheritdoc/>
2036
protected override void Write(StringRender renderer, InterpolationToken obj, Context context)
2137
{
@@ -27,7 +43,7 @@ protected override void Write(StringRender renderer, InterpolationToken obj, Con
2743
if (functionValueDynamic != null || functionValue != null)
2844
{
2945
object functionResult = functionValueDynamic != null ? functionValueDynamic.Invoke(context.View) : functionValue.Invoke();
30-
var resultString = functionResult.ToString();
46+
var resultString = ConvertToStringInCulture(functionResult, context.RenderSettings.CultureInfo);
3147
if (resultString.Contains("{{"))
3248
{
3349
renderer.Render(context.RendererSettings.Parser.Parse(resultString), context);
@@ -39,20 +55,20 @@ protected override void Write(StringRender renderer, InterpolationToken obj, Con
3955

4056
if (!context.RenderSettings.SkipHtmlEncoding && obj.EscapeResult && value != null)
4157
{
42-
value = context.RendererSettings.EncodingFuction(value.ToString());
58+
value = context.RendererSettings.EncodingFuction(ConvertToStringInCulture(value, context.RenderSettings.CultureInfo));
4359
}
4460

4561
if (obj.Indent > 0)
4662
{
4763
renderer.Write(' ', obj.Indent);
4864
}
4965

50-
renderer.Write(value?.ToString());
66+
renderer.Write(ConvertToStringInCulture(value, context.RenderSettings.CultureInfo));
5167
}
5268

53-
/// <inheritdoc/>
54-
protected override async Task WriteAsync(StringRender renderer, InterpolationToken obj, Context context)
55-
{
69+
/// <inheritdoc/>
70+
protected override async Task WriteAsync(StringRender renderer, InterpolationToken obj, Context context)
71+
{
5672
var value = context.Lookup(obj.Content.ToString());
5773

5874
var functionValueDynamic = value as Func<dynamic, object>;
@@ -61,7 +77,7 @@ protected override async Task WriteAsync(StringRender renderer, InterpolationTok
6177
if (functionValueDynamic != null || functionValue != null)
6278
{
6379
object functionResult = functionValueDynamic != null ? functionValueDynamic.Invoke(context.View) : functionValue.Invoke();
64-
var resultString = functionResult.ToString();
80+
var resultString = ConvertToStringInCulture(functionResult, context.RenderSettings.CultureInfo);
6581
if (resultString.Contains("{{"))
6682
{
6783
await renderer.RenderAsync(context.RendererSettings.Parser.Parse(resultString), context);
@@ -73,15 +89,15 @@ protected override async Task WriteAsync(StringRender renderer, InterpolationTok
7389

7490
if (!context.RenderSettings.SkipHtmlEncoding && obj.EscapeResult && value != null)
7591
{
76-
value = context.RendererSettings.EncodingFuction(value.ToString());
92+
value = context.RendererSettings.EncodingFuction(ConvertToStringInCulture(value, context.RenderSettings.CultureInfo));
7793
}
7894

7995
if (obj.Indent > 0)
8096
{
8197
renderer.Write(' ', obj.Indent);
8298
}
8399

84-
renderer.Write(value?.ToString());
100+
renderer.Write(ConvertToStringInCulture(value, context.RenderSettings.CultureInfo));
85101
}
86102
}
87103
}

src/Stubble.Core/Settings/RenderSettings.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
// </copyright>
55

6+
using System.Globalization;
7+
68
namespace Stubble.Core.Settings
79
{
810
/// <summary>
@@ -27,6 +29,11 @@ public class RenderSettings
2729
/// </summary>
2830
public bool SkipHtmlEncoding { get; set; }
2931

32+
/// <summary>
33+
/// Gets or sets the CultureInfo to use for rendering format-dependent values (doubles, etc.).
34+
/// </summary>
35+
public CultureInfo CultureInfo { get; set; } = CultureInfo.InvariantCulture;
36+
3037
/// <summary>
3138
/// Gets the default render settings
3239
/// </summary>
@@ -38,6 +45,7 @@ public static RenderSettings GetDefaultRenderSettings()
3845
SkipRecursiveLookup = false,
3946
ThrowOnDataMiss = false,
4047
SkipHtmlEncoding = false,
48+
CultureInfo = CultureInfo.InvariantCulture
4149
};
4250
}
4351
}

test/Stubble.Compilation.Tests/SpecTests.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,22 @@ namespace Stubble.Compilation.Tests
99
public class SpecTests
1010
{
1111
internal readonly ITestOutputHelper OutputStream;
12-
internal readonly CompilerSettings Settings;
1312

1413
public SpecTests(ITestOutputHelper output)
1514
{
1615
OutputStream = output;
17-
Settings = new CompilerSettingsBuilder().BuildSettings();
1816
}
1917

2018
[Theory]
2119
[MemberData(nameof(Specs.SpecTests), MemberType = typeof(Specs))]
2220
public void CompilationRendererSpecTest(SpecTest data)
2321
{
24-
OutputStream.WriteLine(data.Name);
22+
OutputStream.WriteLine(data.Name);
23+
var settings = CompilationSettings.GetDefaultRenderSettings();
24+
settings.CultureInfo = data.CultureInfo ?? settings.CultureInfo;
2525

26-
var stubble = new StubbleCompilationRenderer(Settings);
27-
var output = data.Partials != null ? stubble.Compile(data.Template, data.Data, data.Partials) : stubble.Compile(data.Template, data.Data);
26+
var stubble = new StubbleCompilationRenderer();
27+
var output = data.Partials != null ? stubble.Compile(data.Template, data.Data, data.Partials, settings) : stubble.Compile(data.Template, data.Data, settings);
2828

2929
var outputResult = output(data.Data);
3030

@@ -36,10 +36,12 @@ public void CompilationRendererSpecTest(SpecTest data)
3636
[MemberData(nameof(Specs.SpecTests), MemberType = typeof(Specs))]
3737
public async Task CompilationRendererSpecTest_Async(SpecTest data)
3838
{
39-
OutputStream.WriteLine(data.Name);
39+
OutputStream.WriteLine(data.Name);
40+
var settings = CompilationSettings.GetDefaultRenderSettings();
41+
settings.CultureInfo = data.CultureInfo ?? settings.CultureInfo;
4042

41-
var stubble = new StubbleCompilationRenderer(Settings);
42-
var output = await (data.Partials != null ? stubble.CompileAsync(data.Template, data.Data, data.Partials) : stubble.CompileAsync(data.Template, data.Data));
43+
var stubble = new StubbleCompilationRenderer();
44+
var output = await (data.Partials != null ? stubble.CompileAsync(data.Template, data.Data, data.Partials, settings) : stubble.CompileAsync(data.Template, data.Data, settings));
4345

4446
var outputResult = output(data.Data);
4547

test/Stubble.Core.Tests/SpecTests.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Stubble.Test.Shared.Spec;
22
using System.Threading.Tasks;
3+
using Stubble.Core.Settings;
34
using Xunit;
45
using Xunit.Abstractions;
56

@@ -18,9 +19,12 @@ public SpecTests(ITestOutputHelper output)
1819
[MemberData(nameof(Specs.SpecTestsWithLambda), MemberType = typeof(Specs))]
1920
public void StringRendererSpecTest(SpecTest data)
2021
{
22+
var settings = RenderSettings.GetDefaultRenderSettings();
23+
settings.CultureInfo = data.CultureInfo ?? settings.CultureInfo;
24+
2125
OutputStream.WriteLine(data.Name);
22-
var stubble = new StubbleVisitorRenderer();
23-
var output = data.Partials != null ? stubble.Render(data.Template, data.Data, data.Partials) : stubble.Render(data.Template, data.Data);
26+
var stubble = new StubbleVisitorRenderer();
27+
var output = data.Partials != null ? stubble.Render(data.Template, data.Data, data.Partials, settings) : stubble.Render(data.Template, data.Data, settings);
2428

2529
OutputStream.WriteLine("Expected \"{0}\", Actual \"{1}\"", data.Expected, output);
2630
Assert.Equal(data.Expected, output);
@@ -30,9 +34,12 @@ public void StringRendererSpecTest(SpecTest data)
3034
[MemberData(nameof(Specs.SpecTestsWithLambda), MemberType = typeof(Specs))]
3135
public async Task StringRendererSpecTest_Async(SpecTest data)
3236
{
37+
var settings = RenderSettings.GetDefaultRenderSettings();
38+
settings.CultureInfo = data.CultureInfo ?? settings.CultureInfo;
39+
3340
OutputStream.WriteLine(data.Name);
3441
var stubble = new StubbleVisitorRenderer();
35-
var output = await (data.Partials != null ? stubble.RenderAsync(data.Template, data.Data, data.Partials) : stubble.RenderAsync(data.Template, data.Data));
42+
var output = await (data.Partials != null ? stubble.RenderAsync(data.Template, data.Data, data.Partials, settings) : stubble.RenderAsync(data.Template, data.Data, settings));
3643

3744
OutputStream.WriteLine("Expected \"{0}\", Actual \"{1}\"", data.Expected, output);
3845
Assert.Equal(data.Expected, output);

test/Stubble.Test.Shared/Spec/Spec.Interpolation.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Globalization;
23
using System.Linq;
34

45
namespace Stubble.Test.Shared.Spec
@@ -94,6 +95,14 @@ public static partial class Specs
9495
Template = @"""{{&power}} jiggawatts!""",
9596
Expected = @"""1.21 jiggawatts!"""
9697
},
98+
new SpecTest {
99+
Name = @"Culture-specific Decimal Interpolation",
100+
Desc = @"Decimals should interpolate seamlessly with proper significance.",
101+
Data = new { power = 1.21, },
102+
CultureInfo = CultureInfo.GetCultureInfo("ru-RU"),
103+
Template = @"""{{power}} jiggawatts!""",
104+
Expected = @"""1,21 jiggawatts!"""
105+
},
97106
new SpecTest {
98107
Name = @"Basic Context Miss Interpolation",
99108
Desc = @"Failed context lookups should default to empty strings.",

test/Stubble.Test.Shared/Spec/SpecTest.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
// </copyright>
55

6-
using Newtonsoft.Json;
7-
using System;
6+
using Newtonsoft.Json;
7+
using System;
88
using System.Collections.Generic;
9+
using System.Globalization;
910
using Xunit.Abstractions;
1011

1112
namespace Stubble.Test.Shared.Spec
@@ -26,6 +27,8 @@ public class SpecTest
2627

2728
public IDictionary<string, string> Partials { get; set; }
2829

29-
public Exception ExpectedException { get; set; }
30+
public Exception ExpectedException { get; set; }
31+
32+
public CultureInfo CultureInfo { get; set; }
3033
}
3134
}

0 commit comments

Comments
 (0)