Skip to content

Commit

Permalink
CultureInfo support for rendering values. (#78)
Browse files Browse the repository at this point in the history
* CultureInfo support for rendering values.

* Make compilation renderer work with culture setting

* Rename culture-info function and make non-virtual
  • Loading branch information
impworks authored and Romanx committed Nov 22, 2019
1 parent 2ab39e2 commit a63b3b1
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Stubble.Compilation.Contexts;
Expand All @@ -20,6 +19,8 @@ namespace Stubble.Compilation.Renderers.TokenRenderers
/// </summary>
public class InterpolationTokenRenderer : ExpressionObjectRenderer<InterpolationToken>
{
private static Type[] formatProviderTypeArgs = new[] { typeof(IFormatProvider) };

/// <inheritdoc/>
protected override void Write(CompilationRenderer renderer, InterpolationToken obj, CompilerContext context)
{
Expand All @@ -29,13 +30,24 @@ protected override void Write(CompilationRenderer renderer, InterpolationToken o

if (!context.CompilationSettings.SkipHtmlEncoding && obj.EscapeResult && expression != null)
{
var isValueType = expression.Type.GetIsValueType();
Expression stringExpression;
if (expression.Type == typeof(string))
{
stringExpression = expression;
}
else
{
var formattedToString = expression.Type
.GetMethod(nameof(object.ToString), formatProviderTypeArgs);

var item = expression.Type.GetIsValueType()
? expression
: Expression.Coalesce(expression, Expression.Constant(string.Empty));

var stringExpression = expression.Type == typeof(string)
? expression
: Expression.Call(
isValueType ? expression : Expression.Coalesce(expression, Expression.Constant(string.Empty)),
expression.Type.GetMethod("ToString", Type.EmptyTypes));
stringExpression = formattedToString is object
? Expression.Call(item, formattedToString, Expression.Constant(context.CompilationSettings.CultureInfo))
: Expression.Call(item, expression.Type.GetMethod(nameof(object.ToString), Type.EmptyTypes));
}

expression = Expression.Invoke(context.CompilerSettings.EncodingFuction, stringExpression);
}
Expand Down
10 changes: 9 additions & 1 deletion src/Stubble.Compilation/Settings/CompilationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// </copyright>

using System.Globalization;

namespace Stubble.Compilation.Settings
{
/// <summary>
Expand All @@ -27,6 +29,11 @@ public class CompilationSettings
/// </summary>
public bool SkipHtmlEncoding { get; set; }

/// <summary>
/// Gets or sets the CultureInfo to use for rendering format-dependent values (doubles, etc.).
/// </summary>
public CultureInfo CultureInfo { get; set; } = CultureInfo.InvariantCulture;

/// <summary>
/// Gets the default render settings
/// </summary>
Expand All @@ -37,7 +44,8 @@ public static CompilationSettings GetDefaultRenderSettings()
{
SkipRecursiveLookup = false,
ThrowOnDataMiss = false,
SkipHtmlEncoding = false
SkipHtmlEncoding = false,
CultureInfo = CultureInfo.InvariantCulture
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// </copyright>

using System;
using System.Net;
using System.Globalization;
using System.Threading.Tasks;
using Stubble.Core.Contexts;
using Stubble.Core.Tokens;
Expand All @@ -16,6 +16,22 @@ namespace Stubble.Core.Renderers.StringRenderer.TokenRenderers
/// </summary>
public class InterpolationTokenRenderer : StringObjectRenderer<InterpolationToken>
{
/// <summary>
/// Renders the value to string using a locale.
/// </summary>
/// <param name="obj">The object to convert</param>
/// <param name="culture">The culture to use</param>
/// <returns>The object stringified into the locale</returns>
protected static string ConvertToStringInCulture(object obj, CultureInfo culture)
{
if (obj is null || obj is string)
{
return obj as string;
}

return Convert.ToString(obj, culture);
}

/// <inheritdoc/>
protected override void Write(StringRender renderer, InterpolationToken obj, Context context)
{
Expand All @@ -27,7 +43,7 @@ protected override void Write(StringRender renderer, InterpolationToken obj, Con
if (functionValueDynamic != null || functionValue != null)
{
object functionResult = functionValueDynamic != null ? functionValueDynamic.Invoke(context.View) : functionValue.Invoke();
var resultString = functionResult.ToString();
var resultString = ConvertToStringInCulture(functionResult, context.RenderSettings.CultureInfo);
if (resultString.Contains("{{"))
{
renderer.Render(context.RendererSettings.Parser.Parse(resultString), context);
Expand All @@ -39,20 +55,20 @@ protected override void Write(StringRender renderer, InterpolationToken obj, Con

if (!context.RenderSettings.SkipHtmlEncoding && obj.EscapeResult && value != null)
{
value = context.RendererSettings.EncodingFuction(value.ToString());
value = context.RendererSettings.EncodingFuction(ConvertToStringInCulture(value, context.RenderSettings.CultureInfo));
}

if (obj.Indent > 0)
{
renderer.Write(' ', obj.Indent);
}

renderer.Write(value?.ToString());
renderer.Write(ConvertToStringInCulture(value, context.RenderSettings.CultureInfo));
}

/// <inheritdoc/>
protected override async Task WriteAsync(StringRender renderer, InterpolationToken obj, Context context)
{
/// <inheritdoc/>
protected override async Task WriteAsync(StringRender renderer, InterpolationToken obj, Context context)
{
var value = context.Lookup(obj.Content.ToString());

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

if (!context.RenderSettings.SkipHtmlEncoding && obj.EscapeResult && value != null)
{
value = context.RendererSettings.EncodingFuction(value.ToString());
value = context.RendererSettings.EncodingFuction(ConvertToStringInCulture(value, context.RenderSettings.CultureInfo));
}

if (obj.Indent > 0)
{
renderer.Write(' ', obj.Indent);
}

renderer.Write(value?.ToString());
renderer.Write(ConvertToStringInCulture(value, context.RenderSettings.CultureInfo));
}
}
}
8 changes: 8 additions & 0 deletions src/Stubble.Core/Settings/RenderSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// </copyright>

using System.Globalization;

namespace Stubble.Core.Settings
{
/// <summary>
Expand All @@ -27,6 +29,11 @@ public class RenderSettings
/// </summary>
public bool SkipHtmlEncoding { get; set; }

/// <summary>
/// Gets or sets the CultureInfo to use for rendering format-dependent values (doubles, etc.).
/// </summary>
public CultureInfo CultureInfo { get; set; } = CultureInfo.InvariantCulture;

/// <summary>
/// Gets the default render settings
/// </summary>
Expand All @@ -38,6 +45,7 @@ public static RenderSettings GetDefaultRenderSettings()
SkipRecursiveLookup = false,
ThrowOnDataMiss = false,
SkipHtmlEncoding = false,
CultureInfo = CultureInfo.InvariantCulture
};
}
}
Expand Down
18 changes: 10 additions & 8 deletions test/Stubble.Compilation.Tests/SpecTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ namespace Stubble.Compilation.Tests
public class SpecTests
{
internal readonly ITestOutputHelper OutputStream;
internal readonly CompilerSettings Settings;

public SpecTests(ITestOutputHelper output)
{
OutputStream = output;
Settings = new CompilerSettingsBuilder().BuildSettings();
}

[Theory]
[MemberData(nameof(Specs.SpecTests), MemberType = typeof(Specs))]
public void CompilationRendererSpecTest(SpecTest data)
{
OutputStream.WriteLine(data.Name);
OutputStream.WriteLine(data.Name);
var settings = CompilationSettings.GetDefaultRenderSettings();
settings.CultureInfo = data.CultureInfo ?? settings.CultureInfo;

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

var outputResult = output(data.Data);

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

var stubble = new StubbleCompilationRenderer(Settings);
var output = await (data.Partials != null ? stubble.CompileAsync(data.Template, data.Data, data.Partials) : stubble.CompileAsync(data.Template, data.Data));
var stubble = new StubbleCompilationRenderer();
var output = await (data.Partials != null ? stubble.CompileAsync(data.Template, data.Data, data.Partials, settings) : stubble.CompileAsync(data.Template, data.Data, settings));

var outputResult = output(data.Data);

Expand Down
13 changes: 10 additions & 3 deletions test/Stubble.Core.Tests/SpecTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Stubble.Test.Shared.Spec;
using System.Threading.Tasks;
using Stubble.Core.Settings;
using Xunit;
using Xunit.Abstractions;

Expand All @@ -18,9 +19,12 @@ public SpecTests(ITestOutputHelper output)
[MemberData(nameof(Specs.SpecTestsWithLambda), MemberType = typeof(Specs))]
public void StringRendererSpecTest(SpecTest data)
{
var settings = RenderSettings.GetDefaultRenderSettings();
settings.CultureInfo = data.CultureInfo ?? settings.CultureInfo;

OutputStream.WriteLine(data.Name);
var stubble = new StubbleVisitorRenderer();
var output = data.Partials != null ? stubble.Render(data.Template, data.Data, data.Partials) : stubble.Render(data.Template, data.Data);
var stubble = new StubbleVisitorRenderer();
var output = data.Partials != null ? stubble.Render(data.Template, data.Data, data.Partials, settings) : stubble.Render(data.Template, data.Data, settings);

OutputStream.WriteLine("Expected \"{0}\", Actual \"{1}\"", data.Expected, output);
Assert.Equal(data.Expected, output);
Expand All @@ -30,9 +34,12 @@ public void StringRendererSpecTest(SpecTest data)
[MemberData(nameof(Specs.SpecTestsWithLambda), MemberType = typeof(Specs))]
public async Task StringRendererSpecTest_Async(SpecTest data)
{
var settings = RenderSettings.GetDefaultRenderSettings();
settings.CultureInfo = data.CultureInfo ?? settings.CultureInfo;

OutputStream.WriteLine(data.Name);
var stubble = new StubbleVisitorRenderer();
var output = await (data.Partials != null ? stubble.RenderAsync(data.Template, data.Data, data.Partials) : stubble.RenderAsync(data.Template, data.Data));
var output = await (data.Partials != null ? stubble.RenderAsync(data.Template, data.Data, data.Partials, settings) : stubble.RenderAsync(data.Template, data.Data, settings));

OutputStream.WriteLine("Expected \"{0}\", Actual \"{1}\"", data.Expected, output);
Assert.Equal(data.Expected, output);
Expand Down
9 changes: 9 additions & 0 deletions test/Stubble.Test.Shared/Spec/Spec.Interpolation.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace Stubble.Test.Shared.Spec
Expand Down Expand Up @@ -94,6 +95,14 @@ public static partial class Specs
Template = @"""{{&power}} jiggawatts!""",
Expected = @"""1.21 jiggawatts!"""
},
new SpecTest {
Name = @"Culture-specific Decimal Interpolation",
Desc = @"Decimals should interpolate seamlessly with proper significance.",
Data = new { power = 1.21, },
CultureInfo = CultureInfo.GetCultureInfo("ru-RU"),
Template = @"""{{power}} jiggawatts!""",
Expected = @"""1,21 jiggawatts!"""
},
new SpecTest {
Name = @"Basic Context Miss Interpolation",
Desc = @"Failed context lookups should default to empty strings.",
Expand Down
9 changes: 6 additions & 3 deletions test/Stubble.Test.Shared/Spec/SpecTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// </copyright>

using Newtonsoft.Json;
using System;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using Xunit.Abstractions;

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

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

public Exception ExpectedException { get; set; }
public Exception ExpectedException { get; set; }

public CultureInfo CultureInfo { get; set; }
}
}

0 comments on commit a63b3b1

Please sign in to comment.