Compare commits

...

24 Commits

Author SHA1 Message Date
Michael Vesely 0b34a4ccee Fix errors and warnings 2021-12-17 11:21:14 +01:00
Michael Vesely 5bcd101beb TargetFrameworks 2021-08-28 08:16:37 +02:00
Michael Vesely 28f4ae3178 Revise README.md
- rename: mustache# to MustacheSharp for consistency
- add: color coding to code samples
- change: branch name for clarity
2020-02-26 05:58:13 +01:00
Michael Vesely 0c3a385b98
Replace (wrong) download info with branch info 2020-02-25 12:35:16 +01:00
Michael Vesely 81dd092e7b Fix: move added TagDefinitions to project folder
After the master branch did a rename of the project folder
when migrating to dotnet core we need to also move
our added TagDefinitions to the new location of the project.
2020-02-25 12:11:18 +01:00
Michael Vesely 3f383cc46a Rename Assembly to MustacheSharp for consistency
-change: TargetFrameworks of test project
    to be consistent with mein project
2020-02-25 12:00:05 +01:00
Michael Vesely ea17d8fadc Merge branch 'master' into feature-compare-tags 2020-02-25 10:12:17 +01:00
Michael Vesely 75551c7ce0
Fix error in the description of the 'lt' tag
Its 'less than' instead of 'equal'.
2020-02-12 07:58:54 +01:00
Michael Vesely 173cfe839f Add unit tests for empty string, null and single curly brace. 2018-12-12 05:31:43 +01:00
Travis Parks eada46d86b
Merge pull request #85 from lhaussknecht/feature/Culture_Specific_Fix
Specified culture to make test run green on non-us systems.
2018-11-05 15:25:48 -05:00
lhaussknecht c4615ef125 Specified culture to make test run green on non-us systems. 2018-11-05 21:17:07 +01:00
Travis Parks 11d4396543 Add CHANGELOG 2018-07-15 19:02:22 -04:00
Paul Grimshaw d4f6004ec4 Added some further tests 2016-09-23 16:05:15 +01:00
Paul Grimshaw 98b0c501e2 Merge remote-tracking branch 'original-master/master'
# Conflicts:
#	mustache-sharp.test/FormatCompilerTester.cs
2016-09-23 14:37:16 +01:00
Paul Grimshaw 0195c39f42 Merge remote-tracking branch 'original-master/master'
# Conflicts:
#	Local.testsettings
#	TraceAndTestImpact.testsettings
#	mustache-sharp.test/FormatCompilerTester.cs
#	mustache-sharp.vsmdi
#	mustache-sharp/ArgumentCollection.cs
#	mustache-sharp/FormatCompiler.cs
2016-09-19 16:26:03 +01:00
Paul Grimshaw b5e7aceaa6 Url from variable test added 2016-09-19 12:32:46 +01:00
Paul Grimshaw bed6302ea8 Added Url encode / Url decode options 2016-09-19 11:40:43 +01:00
Paul Grimshaw 1ea4e00904 Unit test mistakes corrected 2014-02-13 23:17:53 +00:00
Paul Grimshaw b31f393f65 Added support for explicit values in equality comparisons in the template, using the _ prefix 2014-02-05 00:20:33 +00:00
Paul Grimshaw a16262d43e Tags altered to allow string number values 2014-02-04 13:52:20 +00:00
Paul Grimshaw ded9ac56aa Readme correction 2014-02-04 13:21:17 +00:00
Paul Grimshaw 466ec8a009 Readme updated with new tag types 2014-02-04 13:19:55 +00:00
Paul Grimshaw 6c902dff3b Lt, Lte, Gt, Gte (less than etc.) tags added 2014-02-04 13:07:28 +00:00
Paul Grimshaw 7c3df01029 Eq Tag added, to allow values to be compared 2014-02-04 12:10:11 +00:00
17 changed files with 931 additions and 166 deletions

6
CHANGELOG.md Normal file
View File

@ -0,0 +1,6 @@
## Version 1.0.0 (2018-07-15)
**Summary** - Migrate to .NET Standard 2.0
### Enhancements
* Support for .NET 4.0 and .NET Standard 2.0
* Initial attempts to cleanup and modernize the code

26
Local.testsettings Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<TestSettings name="Local" id="2bc42439-1bb6-4112-9c20-eca1ffcae064" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Description>These are default test settings for a local test run.</Description>
<Execution>
<TestTypeSpecific>
<UnitTestRunConfig testTypeId="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b">
<AssemblyResolution>
<TestDirectory useLoadContext="true" />
</AssemblyResolution>
</UnitTestRunConfig>
</TestTypeSpecific>
<AgentRule name="LocalMachineDefaultRole">
<DataCollectors>
<DataCollector uri="datacollector://microsoft/CodeCoverage/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TestTools.CodeCoverage.CoveragePlugIn, Microsoft.VisualStudio.QualityTools.Plugins.CodeCoverage, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="Code Coverage">
<Configuration>
<CodeCoverage xmlns="">
<Regular>
<CodeCoverageItem binaryFile="mustache-sharp\bin\Debug\mustache-sharp.dll" pdbFile="mustache-sharp\bin\Debug\mustache-sharp.pdb" instrumentInPlace="true" />
</Regular>
</CodeCoverage>
</Configuration>
</DataCollector>
</DataCollectors>
</AgentRule>
</Execution>
</TestSettings>

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Web;
@ -1341,6 +1342,165 @@ Item Number: foo<br />
#endregion
#region eq
/// <summary>
/// If the two values don't match, the content of an eq statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_Eq_EvaluatesToFalse_SkipsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#eq OneValue OtherValue}}Content{{/eq}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = "Foo", OtherValue = "Bar" });
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the two values match, the content of an eq statement should be printed.
/// </summary>
[TestMethod]
public void TestCompile_Eq_EvaluatesToTrue_PrintsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#eq OneValue OtherValue}}Content{{/eq}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = "Foo", OtherValue = "Foo" });
Assert.AreEqual("BeforeContentAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the two values match, the content of an eq statement should be printed.
/// </summary>
[TestMethod]
public void TestCompile_Eq_EvaluatesToTrueInLoop_PrintsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#each Items}}{{#eq Name OtherValue}}Content{{/eq}}{{/each}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { Items = new List<object> { new {Name="Test"}, new { Name = "Foo" } }, OneValue = "Foo", OtherValue = "Foo" });
Assert.AreEqual("BeforeContentAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the two values match, the content of an eq statement should be printed.
/// </summary>
[TestMethod]
public void TestCompile_Eq_EvaluatesToTrueInLoopWithNumber_PrintsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#each Items}}{{#eq Name OtherValue}}Content{{/eq}}{{/each}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { Items = new List<object> { new { Name = 1 }, new { Name = 2 } }, OneValue = "1", OtherValue = "2" });
Assert.AreEqual("BeforeContentAfter", result, "The wrong text was generated.");
}
#endregion
#region gt
/// <summary>
/// If first value is not greater than the second, the content of the gt statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_Gt_EvaluatesToFalse_SkipsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#gt OneValue OtherValue}}Content{{/gt}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = 10, OtherValue = 11 });
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If first value is greater than the second, the content of the gt statement should be printed.
/// </summary>
[TestMethod]
public void TestCompile_Gt_EvaluatesToTrue_PrintsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#gt OneValue OtherValue}}Content{{/gt}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = 11.11, OtherValue = 11.1 });
Assert.AreEqual("BeforeContentAfter", result, "The wrong text was generated.");
}
#endregion
#region lt
/// <summary>
/// If first value is not greater than the second, the content of the gt statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_Lt_EvaluatesToFalse_SkipsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#lt OneValue OtherValue}}Content{{/lt}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = 11, OtherValue = 10.5 });
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If first value is greater than the second, the content of the gt statement should be printed.
/// </summary>
[TestMethod]
public void TestCompile_Lt_EvaluatesToTrue_PrintsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#lt OneValue OtherValue}}Content{{/lt}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = 11.1, OtherValue = 11.11 });
Assert.AreEqual("BeforeContentAfter", result, "The wrong text was generated.");
}
#endregion
#region gte
/// <summary>
/// If first value is not greater than or equal to the second, the content of the gt statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_Gte_EvaluatesToFalse_SkipsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#gte OneValue OtherValue}}Content{{/gte}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = 9, OtherValue = 10 });
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If first value is greater than or equal to the second, the content of the gt statement should be printed.
/// </summary>
[TestMethod]
public void TestCompile_Gte_EvaluatesToTrue_PrintsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#gte OneValue OtherValue}}Content{{/gte}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = 11.11, OtherValue = 11.11 });
Assert.AreEqual("BeforeContentAfter", result, "The wrong text was generated.");
}
#endregion
#region lte
/// <summary>
/// If first value is not greater than the second, the content of the gt statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_Lte_EvaluatesToFalse_SkipsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#lte OneValue OtherValue}}Content{{/lte}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = 11, OtherValue = 10.5 });
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If first value is greater than the second, the content of the gt statement should be printed.
/// </summary>
[TestMethod]
public void TestCompile_Lte_EvaluatesToTrue_PrintsContent() {
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#lte OneValue OtherValue}}Content{{/lte}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(new { OneValue = 11.11, OtherValue = 11.11 });
Assert.AreEqual("BeforeContentAfter", result, "The wrong text was generated.");
}
#endregion
#region Compound Tags
/// <summary>
@ -1366,7 +1526,8 @@ Your order total was: {{Total:C}}
{{/if}}
{{/with}}";
Generator generator = compiler.Compile(format);
string result = generator.Render(new
string result = generator.Render(CultureInfo.GetCultureInfo("en-US"), new
{
Customer = new { FirstName = "Bob" },
Order = new
@ -1525,6 +1686,51 @@ Odd
Assert.AreEqual(expected, actual, "The string was not passed to the formatter.");
}
[TestMethod]
public void TestCompile_EmptyStringProperty()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{Greeting}} {{Name}}";
var data = new
{
Greeting = "Hello",
Name = ""
};
Generator generator = compiler.Compile(format);
string actual = generator.Render(data);
string expected = "Hello ";
Assert.AreEqual(expected, actual, "An empty string property should be rendered as an empty string.");
}
[TestMethod]
public void TestCompile_NullValueProperty() {
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{Greeting}} {{Name}}";
var data = new
{
Greeting = "Hello",
Name = (String)null
};
Generator generator = compiler.Compile(format);
string actual = generator.Render(data);
string expected = "Hello ";
Assert.AreEqual(expected, actual, "An null valued property should be rendered as an empty string.");
}
[TestMethod]
public void TestCompile_AllowSingleCurlyBracesInData() {
FormatCompiler compiler = new FormatCompiler();
const string format = @"See this code: {{Code}}!";
var data = new
{
Code = "function() { retrurn 'this is evil'; }"
};
Generator generator = compiler.Compile(format);
string actual = generator.Render(data);
string expected = "See this code: function() { retrurn 'this is evil'; }!";
Assert.AreEqual(expected, actual, "Should not touch single curly braces in data values.");
}
#endregion
#region Numbers
@ -1575,35 +1781,7 @@ Odd
Assert.AreEqual(expected, actual, "Value field didn't work");
}
public class UrlEncodeTagDefinition : ContentTagDefinition
{
public UrlEncodeTagDefinition()
: base("urlencode")
{
}
public override IEnumerable<NestedContext> GetChildContext(TextWriter writer, Scope keyScope, Dictionary<string, object> arguments, Scope contextScope)
{
NestedContext context = new NestedContext()
{
KeyScope = keyScope,
Writer = new StringWriter(),
WriterNeedsConsidated = true,
};
yield return context;
}
public override IEnumerable<TagParameter> GetChildContextParameters()
{
return new TagParameter[] { new TagParameter("collection") };
}
public override string ConsolidateWriter(TextWriter writer, Dictionary<string, object> arguments)
{
return HttpUtility.UrlEncode(writer.ToString());
}
}
#endregion
}
}

View File

@ -28,5 +28,28 @@ namespace Mustache.Test
});
Assert.AreEqual("<html><body>Hello, John \"The Man\" Standford!!!</body></html>", html);
}
[TestMethod]
public void ShouldNotTouchValueContainingSingleCurlyBraces() {
HtmlFormatCompiler compiler = new HtmlFormatCompiler();
var generator = compiler.Compile("<html><head><style>{{Style}}</style></head><body><b>Bold</b> statement!</body></html>");
string html = generator.Render(new
{
Style = "b { color: red; }"
});
Assert.AreEqual("<html><head><style>b { color: red; }</style></head><body><b>Bold</b> statement!</body></html>", html);
}
[TestMethod]
public void ShouldNotTouchValueContainingSingleCurlyBracesInsideTripleCurlyBraces() {
HtmlFormatCompiler compiler = new HtmlFormatCompiler();
var generator = compiler.Compile("<html><head><style>{{{Style}}}</style></head><body><b>Bold</b> statement!</body></html>");
string html = generator.Render(new
{
Style = "b { color: red; }"
});
Assert.AreEqual("<html><head><style>b { color: red; }</style></head><body><b>Bold</b> statement!</body></html>", html);
}
}
}

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<!-- <TargetFramework>netcoreapp2.1</TargetFramework> -->
<TargetFrameworks>net48;netstandard2.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<SignAssembly>true</SignAssembly>

View File

@ -50,6 +50,7 @@ namespace Mustache
{
value = pair.Key.DefaultValue;
}
else
{
value = pair.Value.GetValue(keyScope, contextScope);

View File

@ -28,7 +28,7 @@ namespace Mustache
/// </summary>
protected override IEnumerable<string> GetClosingTags()
{
return new string[] { "if" };
return new string[] { "if","eq","gt","gte","lt","lte" };
}
/// <summary>

View File

@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Mustache {
/// <summary>
/// Defines a tag that conditionally prints its content, based on whether the passed in values are equal
/// </summary>
internal sealed class EqTagDefinition : ConditionTagDefinition {
private const string ConditionParameter = "condition";
private const string TargetValueParameter = "targetValue";
/// <summary>
/// Initializes a new instance of a IfTagDefinition.
/// </summary>
public EqTagDefinition()
: base("eq")
{}
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive()
{
return false;
}
/// <summary>
/// Gets the parameters that can be passed to the tag.
/// </summary>
/// <returns>The parameters.</returns>
protected override IEnumerable<TagParameter> GetParameters() {
return new[] { new TagParameter(ConditionParameter) { IsRequired = true },
new TagParameter(TargetValueParameter){IsRequired = true} };
}
/// <summary>
/// Gets whether the primary generator group should be used to render the tag.
/// </summary>
/// <param name="arguments">The arguments passed to the tag.</param>
/// <returns>
/// True if the primary generator group should be used to render the tag;
/// otherwise, false to use the secondary group.
/// </returns>
public override bool ShouldGeneratePrimaryGroup(Dictionary<string, object> arguments) {
object condition = arguments[ConditionParameter];
object targetValue = arguments[TargetValueParameter];
return isConditionSatisfied(condition,targetValue);
}
private bool isConditionSatisfied(object condition,object targetValue) {
if (condition == null || targetValue == null) {
if (condition == null && targetValue == null) {
return true;
}
return false;
}
if ((condition is double || condition is int) || (targetValue is double || targetValue is int) ) {
return Convert.ToDouble(condition) == Convert.ToDouble(targetValue);
}
if (condition is string && targetValue is string) {
return condition.ToString().Equals(targetValue.ToString(), StringComparison.OrdinalIgnoreCase);
}
if (condition is bool && targetValue is bool) {
return (bool) condition == (bool) targetValue;
}
if (condition is Char && targetValue is Char) {
return (Char)condition == (Char)targetValue;
}
return false;
}
/// <summary>
/// Gets the parameters that are used to create a new child context.
/// </summary>
/// <returns>The parameters that are used to create a new child context.</returns>
public override IEnumerable<TagParameter> GetChildContextParameters() {
return new TagParameter[0];
}
}
}

View File

@ -38,7 +38,21 @@ namespace Mustache
SetTagDefinition setDefinition = new SetTagDefinition();
_tagLookup.Add(setDefinition.Name, setDefinition);
RemoveNewLines = true;
EqTagDefinition eqTagDefinition = new EqTagDefinition();
_tagLookup.Add(eqTagDefinition.Name,eqTagDefinition);
GtTagDefinition gtTagDefinition = new GtTagDefinition();
_tagLookup.Add(gtTagDefinition.Name,gtTagDefinition);
LtTagDefinition ltTagDefinition = new LtTagDefinition();
_tagLookup.Add(ltTagDefinition.Name, ltTagDefinition);
GteTagDefinition gteTagDefinition = new GteTagDefinition();
_tagLookup.Add(gteTagDefinition.Name, gteTagDefinition);
LteTagDefinition lteTagDefinition = new LteTagDefinition();
_tagLookup.Add(lteTagDefinition.Name, lteTagDefinition);
UrlEncodeTagDefinition urlEncodeTagDefinition = new UrlEncodeTagDefinition();
_tagLookup.Add(urlEncodeTagDefinition.Name, urlEncodeTagDefinition);
UrlDecodeTagDefinition urlDecodeTagDefinition = new UrlDecodeTagDefinition();
_tagLookup.Add(urlDecodeTagDefinition.Name,urlDecodeTagDefinition);
}
/// <summary>

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Mustache {
/// <summary>
/// Defines a tag that conditionally prints its content, based on whether the passed in values are equal
/// </summary>
internal sealed class GtTagDefinition : ConditionTagDefinition {
private const string ConditionParameter = "condition";
private const string TargetValueParameter = "targetValue";
/// <summary>
/// Initializes a new instance of a IfTagDefinition.
/// </summary>
public GtTagDefinition()
: base("gt") {}
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive() {
return false;
}
/// <summary>
/// Gets the parameters that can be passed to the tag.
/// </summary>
/// <returns>The parameters.</returns>
protected override IEnumerable<TagParameter> GetParameters() {
return new[] {
new TagParameter(ConditionParameter) {IsRequired = true},
new TagParameter(TargetValueParameter) {IsRequired = true}
};
}
/// <summary>
/// Gets whether the primary generator group should be used to render the tag.
/// </summary>
/// <param name="arguments">The arguments passed to the tag.</param>
/// <returns>
/// True if the primary generator group should be used to render the tag;
/// otherwise, false to use the secondary group.
/// </returns>
public override bool ShouldGeneratePrimaryGroup(Dictionary<string, object> arguments) {
object condition = arguments[ConditionParameter];
object targetValue = arguments[TargetValueParameter];
return isConditionSatisfied(condition, targetValue);
}
private bool isConditionSatisfied(object condition, object targetValue) {
if (condition == null || targetValue == null) {
return false;
}
try {
return Convert.ToDouble(condition) > Convert.ToDouble(targetValue);
} catch (Exception /* ex */) {
return false;
}
}
/// <summary>
/// Gets the parameters that are used to create a new child context.
/// </summary>
/// <returns>The parameters that are used to create a new child context.</returns>
public override IEnumerable<TagParameter> GetChildContextParameters() {
return new TagParameter[0];
}
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Mustache {
/// <summary>
/// Defines a tag that conditionally prints its content, based on whether the passed in values are equal
/// </summary>
internal sealed class GteTagDefinition : ConditionTagDefinition {
private const string ConditionParameter = "condition";
private const string TargetValueParameter = "targetValue";
/// <summary>
/// Initializes a new instance of a IfTagDefinition.
/// </summary>
public GteTagDefinition()
: base("gte") {}
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive() {
return false;
}
/// <summary>
/// Gets the parameters that can be passed to the tag.
/// </summary>
/// <returns>The parameters.</returns>
protected override IEnumerable<TagParameter> GetParameters() {
return new[] {
new TagParameter(ConditionParameter) {IsRequired = true},
new TagParameter(TargetValueParameter) {IsRequired = true}
};
}
/// <summary>
/// Gets whether the primary generator group should be used to render the tag.
/// </summary>
/// <param name="arguments">The arguments passed to the tag.</param>
/// <returns>
/// True if the primary generator group should be used to render the tag;
/// otherwise, false to use the secondary group.
/// </returns>
public override bool ShouldGeneratePrimaryGroup(Dictionary<string, object> arguments) {
object condition = arguments[ConditionParameter];
object targetValue = arguments[TargetValueParameter];
return isConditionSatisfied(condition, targetValue);
}
private bool isConditionSatisfied(object condition, object targetValue) {
if (condition == null || targetValue == null) {
return false;
}
try {
return Convert.ToDouble(condition) >= Convert.ToDouble(targetValue);
} catch (Exception /* ex */) {
return false;
}
}
/// <summary>
/// Gets the parameters that are used to create a new child context.
/// </summary>
/// <returns>The parameters that are used to create a new child context.</returns>
public override IEnumerable<TagParameter> GetChildContextParameters() {
return new TagParameter[0];
}
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
namespace Mustache {
/// <summary>
/// Defines a tag that conditionally prints its content, based on whether the passed in values are equal
/// </summary>
internal sealed class LtTagDefinition : ConditionTagDefinition {
private const string ConditionParameter = "condition";
private const string TargetValueParameter = "targetValue";
/// <summary>
/// Initializes a new instance of a IfTagDefinition.
/// </summary>
public LtTagDefinition()
: base("lt") {}
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive() {
return false;
}
/// <summary>
/// Gets the parameters that can be passed to the tag.
/// </summary>
/// <returns>The parameters.</returns>
protected override IEnumerable<TagParameter> GetParameters() {
return new[] {
new TagParameter(ConditionParameter) {IsRequired = true},
new TagParameter(TargetValueParameter) {IsRequired = true}
};
}
/// <summary>
/// Gets whether the primary generator group should be used to render the tag.
/// </summary>
/// <param name="arguments">The arguments passed to the tag.</param>
/// <returns>
/// True if the primary generator group should be used to render the tag;
/// otherwise, false to use the secondary group.
/// </returns>
public override bool ShouldGeneratePrimaryGroup(Dictionary<string, object> arguments) {
object condition = arguments[ConditionParameter];
object targetValue = arguments[TargetValueParameter];
return isConditionSatisfied(condition, targetValue);
}
private bool isConditionSatisfied(object condition, object targetValue) {
if (condition == null || targetValue == null) {
return false;
}
try {
return Convert.ToDouble(condition) < Convert.ToDouble(targetValue);
}
catch (Exception /* ex */) {
return false;
}
}
/// <summary>
/// Gets the parameters that are used to create a new child context.
/// </summary>
/// <returns>The parameters that are used to create a new child context.</returns>
public override IEnumerable<TagParameter> GetChildContextParameters() {
return new TagParameter[0];
}
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Mustache {
/// <summary>
/// Defines a tag that conditionally prints its content, based on whether the passed in values are equal
/// </summary>
internal sealed class LteTagDefinition : ConditionTagDefinition {
private const string ConditionParameter = "condition";
private const string TargetValueParameter = "targetValue";
/// <summary>
/// Initializes a new instance of a IfTagDefinition.
/// </summary>
public LteTagDefinition()
: base("lte") {}
/// <summary>
/// Gets whether the tag only exists within the scope of its parent.
/// </summary>
protected override bool GetIsContextSensitive() {
return false;
}
/// <summary>
/// Gets the parameters that can be passed to the tag.
/// </summary>
/// <returns>The parameters.</returns>
protected override IEnumerable<TagParameter> GetParameters() {
return new[] {
new TagParameter(ConditionParameter) {IsRequired = true},
new TagParameter(TargetValueParameter) {IsRequired = true}
};
}
/// <summary>
/// Gets whether the primary generator group should be used to render the tag.
/// </summary>
/// <param name="arguments">The arguments passed to the tag.</param>
/// <returns>
/// True if the primary generator group should be used to render the tag;
/// otherwise, false to use the secondary group.
/// </returns>
public override bool ShouldGeneratePrimaryGroup(Dictionary<string, object> arguments) {
object condition = arguments[ConditionParameter];
object targetValue = arguments[TargetValueParameter];
return isConditionSatisfied(condition, targetValue);
}
private bool isConditionSatisfied(object condition, object targetValue) {
if (condition == null || targetValue == null) {
return false;
}
try {
return Convert.ToDouble(condition) <= Convert.ToDouble(targetValue);
} catch (Exception /* ex */) {
return false;
}
}
/// <summary>
/// Gets the parameters that are used to create a new child context.
/// </summary>
/// <returns>The parameters that are used to create a new child context.</returns>
public override IEnumerable<TagParameter> GetChildContextParameters() {
return new TagParameter[0];
}
}
}

View File

@ -6,7 +6,7 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>MustacheSharp.snk</AssemblyOriginatorKeyFile>
<DelaySign>false</DelaySign>
<AssemblyName>mustache-sharp</AssemblyName>
<AssemblyName>MustacheSharp</AssemblyName>
<Authors>Truncon</Authors>
<Description>An extension of the mustache text template engine for .NET.</Description>
<Copyright>Copyright © 2013</Copyright>

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.IO;
namespace Mustache
{
class UrlDecodeTagDefinition: ContentTagDefinition {
public UrlDecodeTagDefinition()
: base("urldecode") {
}
public override IEnumerable<NestedContext> GetChildContext(TextWriter writer, Scope keyScope, Dictionary<string, object> arguments, Scope contextScope) {
NestedContext context = new NestedContext() {
KeyScope = keyScope,
Writer = new StringWriter(),
WriterNeedsConsidated = true,
};
yield return context;
}
public override IEnumerable<TagParameter> GetChildContextParameters() {
return new TagParameter[] { new TagParameter("collection") };
}
public override string ConsolidateWriter(TextWriter writer, Dictionary<string, object> arguments) {
return System.Net.WebUtility.UrlDecode(writer.ToString());
}
}
}

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.IO;
namespace Mustache
{
public class UrlEncodeTagDefinition : ContentTagDefinition {
public UrlEncodeTagDefinition()
: base("urlencode") {
}
public override IEnumerable<NestedContext> GetChildContext(TextWriter writer,Scope keyScope,Dictionary<string, object> arguments,Scope contextScope) {
NestedContext context = new NestedContext() {
KeyScope = keyScope,
Writer = new StringWriter(),
WriterNeedsConsidated = true,
};
yield return context;
}
public override IEnumerable<TagParameter> GetChildContextParameters() {
return new TagParameter[] { new TagParameter("collection") };
}
public override string ConsolidateWriter(TextWriter writer, Dictionary<string, object> arguments) {
return System.Net.WebUtility.UrlEncode(writer.ToString());
}
}
}

335
README.md
View File

@ -1,8 +1,10 @@
# mustache#
# MustacheSharp <sup>Plus</sup>
An extension of the mustache text template engine for .NET.
Download using NuGet: [mustache#](http://nuget.org/packages/mustache-sharp)
## Branches
- **master-plus**: adds additional tags like `eq`, `lt`, `gt`, ... as described below. (These tags are marked as <sup>Plus</sup>)
- **master**: contains the original version forked from [jehugaleahsa/mustache-sharp](/jehugaleahsa/mustache-sharp).
## Overview
Generating text has always been a chore. Either you're concatenating strings like a mad man or you're getting fancy with `StringBuilder`. Either way, the logic for conditionally including values or looping over a collection really obscures the intention of the code. A more declarative approach would improve your code big time. Hey, that's why server-side scripting got popular in the first place, right?
@ -13,28 +15,33 @@ Generating text has always been a chore. Either you're concatenating strings lik
Introducing [handlebars.js](http://handlebarsjs.com/)... If you've needed to generate any HTML templates, **handlebars.js** is a really awesome tool. Not only does it support an `if` and `each` tag, it lets you define your own tags! It also makes it easy to reference nested values `{{Customer.Address.ZipCode}}`.
**mustache#** brings the power of **handlebars.js** to .NET and then takes it a little bit further. It is geared towards building ordinary text documents, rather than just HTML. It differs from **handlebars.js** in the way it handles newlines. With **mustache#**, you explicitly indicate when you want newlines - actual newlines are ignored.
**MustacheSharp** (aka. **mustache#**, **mustache-sharp**) brings the power of **handlebars.js** to .NET and then takes it a little bit further. It is geared towards building ordinary text documents, rather than just HTML. It differs from **handlebars.js** in the way it handles newlines. With **MustacheSharp**, you explicitly indicate when you want newlines - actual newlines are ignored.
Hello, {{Customer.Name}}
```handlebars
Hello, {{Customer.Name}}
{{#newline}}
{{#newline}}
{{#with Order}}
{{#if LineItems}}
Here is a summary of your previous order:
{{#newline}}
{{#newline}}
{{#each LineItems}}
{{ProductName}}: {{UnitPrice:C}} x {{Quantity}}
{{#newline}}
{{#newline}}
{{#with Order}}
{{#if LineItems}}
Here is a summary of your previous order:
{{#newline}}
{{#newline}}
{{#each LineItems}}
{{ProductName}}: {{UnitPrice:C}} x {{Quantity}}
{{#newline}}
{{/each}}
{{#newline}}
Your total was {{Total:C}}.
{{#else}}
You do not have any recent purchases.
{{/if}}
{{/with}}
Most of the lines in the previous example will never appear in the final output. This allows you to use **mustache#** to write templates for normal text, not just HTML/XML.
{{/each}}
{{#newline}}
Your total was {{Total:C}}.
{{#else}}
You do not have any recent purchases.
{{/if}}
{{/with}}
```
Most of the lines in the previous example will never appear in the final output. This allows you to use **MustacheSharp** to write templates for normal text, not just HTML/XML.
**MustacheSharp <sup>Plus</sup>** is a fork of the MustacheSharp engine that adds some basic logic (eq, lt, gt, lte, gte) and some other tags.
## Placeholders
The placeholders can be any valid identifier. These map to the property names in your classes (or `Dictionary` keys).
@ -42,33 +49,41 @@ The placeholders can be any valid identifier. These map to the property names in
### Formatting Placeholders
Each format item takes the following form and consists of the following components:
{{identifier[,alignment][:formatString]}}
```handlebars
{{identifier[,alignment][:formatString]}}
```
The matching braces are required. Notice that they are double curly braces! The alignment and the format strings are optional and match the syntax accepted by `String.Format`. Refer to [String.Format](http://msdn.microsoft.com/en-us/library/system.string.format.aspx)'s documentation to learn more about the standard and custom format strings.
### Placeholder Scope
The identifier is used to find a property with a matching name. If you want to print out the object itself, you can use the special identifier `this`.
FormatCompiler compiler = new FormatCompiler();
Generator generator = compiler.Compile("Hello, {{this}}!!!");
string result = generator.Render("Bob");
Console.Out.WriteLine(result); // Hello, Bob!!!
```cs
FormatCompiler compiler = new FormatCompiler();
Generator generator = compiler.Compile("Hello, {{this}}!!!");
string result = generator.Render("Bob");
Console.Out.WriteLine(result); // Hello, Bob!!!
```
Some tags, such as `each` and `with`, change which object the values will be retrieved from.
If a property with the placeholder name can't be found at the current scope, the name will be searched for at the next highest level.
**mustache#** will automatically detect when an object is a dictionary and search for a matching key. In this case, it still needs to be a valid identifier name.
**MustacheSharp** will automatically detect when an object is a dictionary and search for a matching key. In this case, it still needs to be a valid identifier name.
### Nested Placeholders
If you want to grab a nested property, you can separate identifiers using `.`.
{{Customer.Address.ZipCode}}
```handlebars
{{Customer.Address.ZipCode}}
```
## The 'if' tag
The **if** tag allows you to conditionally include a block of text.
Hello{{#if Name}}, {{Name}}{{/if}}!!!
```handlebars
Hello{{#if Name}}, {{Name}}{{/if}}!!!
```
The block will be printed if:
* The value is a non-empty string.
@ -79,87 +94,139 @@ The block will be printed if:
The **if** tag has complimentary **elif** and **else** tags. There can be as many **elif** tags as desired but the **else** tag must appear only once and after all other tags.
{{#if Male}}Mr.{{#elif Married}}Mrs.{{#else}}Ms.{{/if}}
```handlebars
{{#if Male}}Mr.{{#elif Married}}Mrs.{{#else}}Ms.{{/if}}
```
## The 'eq' tag <sup>Plus</sup>
The **eq** tag allows you to conditionally include a block of text, by comparing if two values are equal.
```handlebars
{{#eq Name UserName}}Hello {{Name}} !!!{{/eq}}
```
You can also use specific values as the target value for comparison, rather than values from the model by prefixing the value with the "_" character:
```handlebars
Hello {{#eq User.Role _admin}}Maestro!{{#else}}{{Name}}{{/eq}}
```
The block will be printed if:
* Both values are null
* Both values are strings, and are equal (case insensitive)
* Both values are integers or doubles, and are equal
* Both values are boolean, and are equal
## The 'lt' tag <sup>Plus</sup>
The **lt** tag allows you to conditionally include a block of text, if the first value is less than the second value.
```handlebars
<span {{#lt Budget BudgetLimit}} class="underBudget" {{/lt}}>{{Budget}}</span>
```
Again, you can use specific values as the target for the comparison parameter, by prefixing the value with the "_" character:
```handlebars
<span {{#lt Budget _500}} class="underBudget" {{/lt}}>{{Budget}}</span>
```
The block will be printed if:
* Both values are integers or doubles, and the first value is less than the second
## The 'lte', 'gt', 'gte' tags <sup>Plus</sup>
These tags work in the same way as the 'lt' tag, ('lte' = less than or equal to).
## The 'each' tag
If you need to print out a block of text for each item in a collection, use the **each** tag.
{{#each Customers}}
Hello, {{Name}}!!
{{/each}}
```handlebars
{{#each Customers}}
Hello, {{Name}}!!
{{/each}}
```
Within the context of the **each** block, the scope changes to the current item. So, in the example above, `Name` would refer to a property in the `Customer` class.
Additionally, you can access the current index into the collection being enumerated using the **index** tag.
<ul>
{{#each Items}}
<li class="list-item{{#index}}" value="{{Value}}">{{Description}}</li>
{{/each}}
</ul>
```handlebars
<ul>
{{#each Items}}
<li class="list-item{{#index}}" value="{{Value}}">{{Description}}</li>
{{/each}}
</ul>
```
This will build an HTML list, building a list of items with `Description` and `Value` properties. Additionally, the `index` tag is used to create a CSS class with increasing numbers.
## The 'with' tag
Within a block of text, you may refer to a same top-level placeholder over and over. You can cut down the amount of text by using the **with** tag.
{{#with Customer.Address}}
{{FirstName}} {{LastName}}
{{Line1}}
{{#if Line2}}
{{Line2}}
{{/if}}
{{#if Line3}}
{{Line3}}
{{/if}}
{{City}} {{State}}, {{ZipCode}}
{{/with}}
```handlebars
{{#with Customer.Address}}
{{FirstName}} {{LastName}}
{{Line1}}
{{#if Line2}}
{{Line2}}
{{/if}}
{{#if Line3}}
{{Line3}}
{{/if}}
{{City}} {{State}}, {{ZipCode}}
{{/with}}
```
Here, the `Customer.Address` property will be searched first for the placeholders. If a property cannot be found in the `Address` object, it will be searched for in the `Customer` object and on up.
## The 'set' tag
**mustache#** provides limited support for variables through use of the `set` tag. Once a variable is declared, it is visible to all child scopes. Multiple definitions of a variable with the same name cannot be created within the same scope. In fact, I highly recommend making variable names unique to the entire template just to prevent unexpected behavior!
**MustacheSharp** provides limited support for variables through use of the `set` tag. Once a variable is declared, it is visible to all child scopes. Multiple definitions of a variable with the same name cannot be created within the same scope. In fact, I highly recommend making variable names unique to the entire template just to prevent unexpected behavior!
The following example will print out "EvenOddEvenOdd" by toggling a variable called `even`:
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#set even}}
{{#each this}}
{{#if @even}}
Even
{{#else}}
Odd
{{/if}}
{{#set even}}
{{/each}}";
Generator generator = compiler.Compile(format);
generator.ValueRequested += (sender, e) =>
{
e.Value = !(bool)(e.Value ?? false);
};
string result = generator.Render(new int[] { 0, 1, 2, 3 });
```cs
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#set even}}
{{#each this}}
{{#if @even}}
Even
{{#else}}
Odd
{{/if}}
{{#set even}}
{{/each}}";
Generator generator = compiler.Compile(format);
generator.ValueRequested += (sender, e) =>
{
e.Value = !(bool)(e.Value ?? false);
};
string result = generator.Render(new int[] { 0, 1, 2, 3 });
```
This code works by specifying a function to call whenever a value is needed for the `even` variable. The first time the function is called, `e.Value` will be null. All additional calls will hold the last known value of the variable.
Notice that when you set the variable, you don't qualify it with an `@`. You only need the `@` when you request its value, like in the `if` statement above.
You should attempt to limit your use of variables within templates. Instead, perform as many up-front calculations as possible and make sure your view model closely represents its final appearance. In this case, it would make more sense to first convert the array into strings of "Even" and "Odd".
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#each this}}{{this}}{{/each}}";
Generator generator = compiler.Compile(format);
string result = generator.Render(new string[] { "Even", "Odd", "Even", "Odd" });
```cs
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#each this}}{{this}}{{/each}}";
Generator generator = compiler.Compile(format);
string result = generator.Render(new string[] { "Even", "Odd", "Even", "Odd" });
```
This code is much easier to read and understand. It is also going to run significantly faster. In cases where you also need the original value, you can create an array containing objects with properties for the original value *and* `Even`/`Odd`.
## Defining Your Own Tags
If you need to define your own tags, **mustache#** has everything you need.
If you need to define your own tags, **MustacheSharp** has everything you need.
Once you define your own tags, you can register them with the compiler using the `RegisterTag` method.
FormatCompiler compiler = new FormatCompiler();
compiler.RegisterTag(myTag);
Your tag can be referenced within the template by leading its name with a `#`.
Custom tags can take any number of parameters. Parameters can have default values if you don't want to pass them all the time. Arguments are passed by specifying a placeholder.
@ -167,69 +234,75 @@ Custom tags can take any number of parameters. Parameters can have default value
### Multi-line Tags
Here's an example of a tag that will make all of its content upper case:
public class UpperTagDefinition : ContentTagDefinition
```cs
public class UpperTagDefinition : ContentTagDefinition
{
public UpperTagDefinition()
: base("upper")
{
public UpperTagDefinition()
: base("upper")
{
}
public override IEnumerable<NestedContext> GetChildContext(TextWriter writer, KeyScope scope, Dictionary<string, object> arguments)
{
NestedContext context = new NestedContext()
{
KeyScope = scope,
Writer = new StringWriter(),
WriterNeedsConsolidated = true,
};
yield return context;
}
public override string ConsolidateWriter(TextWriter writer, Dictionary<string, object> arguments)
{
return writer.ToString().ToUpperInvariant();
}
}
public override IEnumerable<NestedContext> GetChildContext(TextWriter writer, KeyScope scope, Dictionary<string, object> arguments)
{
NestedContext context = new NestedContext()
{
KeyScope = scope,
Writer = new StringWriter(),
WriterNeedsConsolidated = true,
};
yield return context;
}
public override string ConsolidateWriter(TextWriter writer, Dictionary<string, object> arguments)
{
return writer.ToString().ToUpperInvariant();
}
}
```
Another solution is to wrap the given TextWriter with another TextWriter that will change the case of the strings passed to it. This approach requires more work, but would be more efficient. You should attempt to wrap or reuse the text writer passed to the tag.
### In-line Tags
Here's an example of a tag that will join the items of a collection:
public class JoinTagDefinition : InlineTagDefinition
```cs
public class JoinTagDefinition : InlineTagDefinition
{
public JoinTagDefinition()
: base("join")
{
public JoinTagDefinition()
: base("join")
{
}
protected override IEnumerable<TagParameter> GetParameters()
{
return new TagParameter[] { new TagParameter("collection") };
}
protected override void GetText(TextWriter writer, Dictionary<string, object> arguments)
{
IEnumerable collection = (IEnumerable)arguments["collection"];
string joined = String.Join(", ", collection.Cast<object>().Select(o => o.ToString()));
writer.Write(joined);
}
}
protected override IEnumerable<TagParameter> GetParameters()
{
return new TagParameter[] { new TagParameter("collection") };
}
protected override void GetText(TextWriter writer, Dictionary<string, object> arguments)
{
IEnumerable collection = (IEnumerable)arguments["collection"];
string joined = String.Join(", ", collection.Cast<object>().Select(o => o.ToString()));
writer.Write(joined);
}
}
```
## HTML Support
**mustache#** was not originally designed to exclusively generate HTML. However, it is by far the most common use of **mustache#**. For that reason, there is a separate `HtmlFormatCompiler` class that will automatically configure the code to work with HTML documents. Particularly, this class will eliminate most newlines and escape any special HTML characters that might appear within the substituted values.
**MustacheSharp** was not originally designed to exclusively generate HTML. However, it is by far the most common use of **MustacheSharp**. For that reason, there is a separate `HtmlFormatCompiler` class that will automatically configure the code to work with HTML documents. Particularly, this class will eliminate most newlines and escape any special HTML characters that might appear within the substituted values.
If you really need to embed HTML values, you can wrap placeholders in triple quotes rather than double quotes.
HtmlFormatCompiler compiler = new HtmlFormatCompiler();
const string format = @"<html><body>{{escaped}} and {{{unescaped}}}</body></html>";
Generator generator = compiler.Compile(format);
string result = generator.Render(new
{
escaped = "<b>Awesome</b>",
unescaped = "<i>sweet</i>"
});
// Generates <html><body>&lt;b&gt;Awesome&lt;/b&gt; and <i>sweet</i></body></html>
```cs
HtmlFormatCompiler compiler = new HtmlFormatCompiler();
const string format = @"<html><body>{{escaped}} and {{{unescaped}}}</body></html>";
Generator generator = compiler.Compile(format);
string result = generator.Render(new
{
escaped = "<b>Awesome</b>",
unescaped = "<i>sweet</i>"
});
// Generates <html><body>&lt;b&gt;Awesome&lt;/b&gt; and <i>sweet</i></body></html>
```
## License
This is free and unencumbered software released into the public domain.