Skip to content

Commit

Permalink
Merge pull request #987 from riganti/feature/string-interpolation
Browse files Browse the repository at this point in the history
Added support for string interpolation in bindings
  • Loading branch information
quigamdev authored Apr 21, 2021
2 parents 21aa3d7 + c0c252a commit c0a9321
Show file tree
Hide file tree
Showing 11 changed files with 551 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,88 @@ public void BindingCompiler_Valid_StringLiteralInSingleQuotes()
Assert.AreEqual(ExecuteBinding("StringProp + 'def'", viewModel), "abcdef");
}

[TestMethod]
[DataRow(@"$'Non-Interpolated'", "Non-Interpolated")]
[DataRow(@"$'Non-Interpolated {{'", "Non-Interpolated {")]
[DataRow(@"$'Non-Interpolated {{ no-expression }}'", "Non-Interpolated { no-expression }")]
public void BindingCompiler_Valid_InterpolatedString_NoExpressions(string expression, string evaluated)
{
var binding = ExecuteBinding(expression);
Assert.AreEqual(evaluated, binding);
}

[TestMethod]
[DataRow(@"$'} Malformed'", "Unexpected token '$' ---->}<----")]
[DataRow(@"$'{ Malformed'", "Could not find matching closing character '}' for an interpolated expression")]
[DataRow(@"$'Malformed {expr'", "Could not find matching closing character '}' for an interpolated expression")]
[DataRow(@"$'Malformed expr}'", "Unexpected token '$'Malformed expr ---->}<---- ")]
[DataRow(@"$'Malformed {'", "Could not find matching closing character '}' for an interpolated expression")]
[DataRow(@"$'Malformed }'", "Unexpected token '$'Malformed ---->}<----")]
[DataRow(@"$'Malformed {}'", "Expected expression, but instead found empty")]
[DataRow(@"$'Malformed {StringProp; IntProp}'", "Expected end of interpolated expression, but instead found Semicolon")]
[DataRow(@"$'Malformed {(string arg) => arg.Length}'", "Expected end of interpolated expression, but instead found Identifier")]
[DataRow(@"$'Malformed {(StringProp == null) ? 'StringPropWasNull' : 'StringPropWasNotNull'}'", "Conditional expression needs to be enclosed in parentheses")]
public void BindingCompiler_Invalid_InterpolatedString_MalformedExpression(string expression, string errorMessage)
{
try
{
ExecuteBinding(expression);
}
catch (Exception e)
{
// Get inner-most exception
var current = e;
while (current.InnerException != null)
current = current.InnerException;

Assert.AreEqual(typeof(BindingCompilationException), current.GetType());
StringAssert.Contains(current.Message, errorMessage);
}
}

[TestMethod]
[DataRow(@"$""Interpolated {StringProp} {StringProp}""", "Interpolated abc abc")]
[DataRow(@"$'Interpolated {StringProp} {StringProp}'", "Interpolated abc abc")]
[DataRow(@"$'Interpolated {StringProp.Length}'", "Interpolated 3")]
public void BindingCompiler_Valid_InterpolatedString_WithSimpleExpressions(string expression, string evaluated)
{
var viewModel = new TestViewModel() { StringProp = "abc" };
var binding = ExecuteBinding(expression, viewModel);
Assert.AreEqual(evaluated, binding);
}

[TestMethod]
[DataRow(@"$'{string.Join(', ', IntArray)}'", "1, 2, 3")]
[DataRow(@"$'{string.Join(', ', 'abc', 'def', $'{string.Join(', ', IntArray)}')}'", "abc, def, 1, 2, 3")]
public void BindingCompiler_Valid_InterpolatedString_NestedExpressions(string expression, string evaluated)
{
var viewModel = new TestViewModel { IntArray = new[] { 1, 2, 3 } };
var binding = ExecuteBinding(expression, viewModel);
Assert.AreEqual(evaluated, binding);
}

[TestMethod]
[DataRow(@"$'Interpolated {IntProp < LongProperty}'", "Interpolated True")]
[DataRow(@"$'Interpolated {StringProp ?? 'StringPropWasNull'}'", "Interpolated StringPropWasNull")]
[DataRow(@"$'Interpolated {((StringProp == null) ? 'StringPropWasNull' : 'StringPropWasNotNull')}'", "Interpolated StringPropWasNull")]
public void BindingCompiler_Valid_InterpolatedString_WithComplexExpressions(string expression, string evaluated)
{
var viewModel = new TestViewModel() { IntProp = 1, LongProperty = 2 };
var binding = ExecuteBinding(expression, viewModel);
Assert.AreEqual(evaluated, binding);
}

[TestMethod]
[DataRow(@"$'Interpolated {DateFrom:R}'", "Interpolated Fri, 11 Nov 2011 12:11:11 GMT")]
[DataRow(@"$'Interpolated {$'{DateFrom:R}'}'", "Interpolated Fri, 11 Nov 2011 12:11:11 GMT")]
[DataRow(@"$'Interpolated {$'{IntProp:0000}'}'", "Interpolated 0006")]
public void BindingCompiler_Valid_InterpolatedString_WithFormattingComponent(string expression, string evaluated)
{
var viewModel = new TestViewModel() { DateFrom = DateTime.Parse("2011-11-11T11:11:11+00:00"), IntProp = 6 };
var binding = ExecuteBinding(expression, viewModel);
Assert.AreEqual(evaluated, binding);
}

[TestMethod]
public void BindingCompiler_Valid_PropertyProperty()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,22 @@ public void JavascriptCompilation_ToString_Invalid()
});
}

[TestMethod]
[DataRow(@"$""Interpolated {StringProp} {StringProp}""")]
[DataRow(@"$'Interpolated {StringProp} {StringProp}'")]
public void JavascriptCompilation_InterpolatedString(string expression)
{
var js = CompileBinding(expression, new[] { typeof(TestViewModel) }, typeof(string));
Assert.AreEqual("dotvvm.globalize.format(\"Interpolated {0} {1}\",[StringProp(),StringProp()])", js);
}

[TestMethod]
public void JavascriptCompilation_InterpolatedString_NoExpressions()
{
var js = CompileBinding("$'Non-Interpolated {{ no-expr }}'", new[] { typeof(TestViewModel) });
Assert.AreEqual("\"Non-Interpolated { no-expr }\"", js);
}

[TestMethod]
public void JavascriptCompilation_UnwrappedObservables()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,29 @@ public void BindingParser_StringLiteral_Valid()
Assert.AreEqual("help\"help", ((LiteralExpressionBindingParserNode)result).Value);
}

[TestMethod]
public void BindingParser_InterpolatedString_Valid()
{
var result = bindingParserNodeFactory.Parse("$\"Hello {Argument1} with {Argument2}!\"") as InterpolatedStringBindingParserNode;
Assert.AreEqual("Hello {0} with {1}!", result.Format);
Assert.IsFalse(result.HasNodeErrors);
Assert.AreEqual(2, result.Arguments.Count);
Assert.AreEqual("Argument1", ((SimpleNameBindingParserNode)result.Arguments[0]).Name);
Assert.AreEqual("Argument2", ((SimpleNameBindingParserNode)result.Arguments[1]).Name);
}

[TestMethod]
[DataRow("$'{DateProperty:dd/MM/yyyy}'", "{0:dd/MM/yyyy}")]
[DataRow("$'{IntProperty:####}'", "{0:####}")]
public void BindingParser_InterpolatedString_WithFormattingComponenet_Valid(string expression, string formatOptions)
{
var result = bindingParserNodeFactory.Parse(expression) as InterpolatedStringBindingParserNode;
Assert.IsFalse(result.HasNodeErrors);
Assert.AreEqual(1, result.Arguments.Count);
Assert.AreEqual(typeof(FormattedBindingParserNode), result.Arguments.First().GetType());
Assert.AreEqual(formatOptions, ((FormattedBindingParserNode)result.Arguments.First()).Format);
}

[TestMethod]
public void BindingParser_StringLiteral_SingleQuotes_Valid()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,26 @@ public void BindingTokenizer_MultiblockExpression_BunchingOperators_Valid()
Assert.AreEqual(index, tokens.Count);
}

[TestMethod]
[DataRow("$\"String {Arg}\"")]
[DataRow("$'String {Arg}'")]
public void BindingTokenizer_InterpolatedString_Valid(string expression)
{
var tokens = Tokenize(expression);
Assert.AreEqual(1, tokens.Count);
Assert.AreEqual(BindingTokenType.InterpolatedStringToken, tokens[0].Type);
}

[TestMethod]
[DataRow("$'String {'InnerString'}'")]
[DataRow("$'String {$'Inner {'InnerInnerString'}'}'")]
public void BindingTokenizer_InterpolatedString_InnerString(string expression)
{
var tokens = Tokenize(expression);
Assert.AreEqual(1, tokens.Count);
Assert.AreEqual(BindingTokenType.InterpolatedStringToken, tokens[0].Type);
}

[TestMethod]
public void BindingTokenizer_UnaryOperator_BunchingOperators_Valid()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,26 @@ protected override Expression VisitLiteralExpression(LiteralExpressionBindingPar
return Expression.Constant(node.Value);
}

protected override Expression VisitInterpolatedStringExpression(InterpolatedStringBindingParserNode node)
{
var target = new MethodGroupExpression() {
MethodName = nameof(String.Format),
Target = new StaticClassIdentifierExpression(typeof(string))
};

if (node.Arguments.Any())
{
// Translate to a String.Format(...) call
var arguments = node.Arguments.Select((arg, index) => HandleErrors(node.Arguments[index], Visit)).ToArray();
return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format) }.Concat(arguments).ToArray());
}
else
{
// There are no interpolation expressions - we can just return string
return Expression.Constant(node.Format);
}
}

protected override Expression VisitParenthesizedExpression(ParenthesizedExpressionBindingParserNode node)
{
// just visit content
Expand Down Expand Up @@ -364,6 +384,17 @@ protected override Expression VisitBlock(BlockBindingParserNode node)
else return Expression.Block(variables, left, right);
}

protected override Expression VisitFormattedExpression(FormattedBindingParserNode node)
{
var target = new MethodGroupExpression() {
MethodName = nameof(String.Format),
Target = new StaticClassIdentifierExpression(typeof(string))
};

var nodeObj = HandleErrors(node.Node, Visit);
return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format), nodeObj });
}

protected override Expression VisitVoid(VoidBindingParserNode node) => Expression.Default(typeof(void));

private Expression? GetMemberOrTypeExpression(IdentifierNameBindingParserNode node, Type[]? typeParameters)
Expand Down
Loading

0 comments on commit c0a9321

Please sign in to comment.