Created
February 24, 2011 12:37
-
-
Save grumpydev/842117 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
<html xmlns="http://www.w3.org/1999/xhtml"> | |
<head> | |
<title>@Model.Title</title> | |
</head> | |
<body> | |
<h1>@Model.Title</h1> | |
<p>Hello there @Model.Name!</p> | |
<h2>Users:</h2> | |
@IfNot.HasUsers | |
<p>No users found</p> | |
@EndIf | |
@If.HasUsers | |
<ul id="users"> | |
@Each.Users | |
<li>@Current</li> | |
@EndEach | |
</ul> | |
@EndIf | |
<h2>Admins:</h2> | |
@IfNot.HasAdmins | |
<p>No admin users found</p> | |
@EndIf | |
@If.HasAdmins | |
<ul id="admins"> | |
@Each.Admins | |
<li>@Current</li> | |
@EndEach | |
</ul> | |
@EndIf | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace TinyTemplates.ViewEngine | |
{ | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Dynamic; | |
using System.Linq; | |
using System.Reflection; | |
using System.Text.RegularExpressions; | |
/// <summary> | |
/// A super-simple view engine | |
/// </summary> | |
public class SuperSimpleViewEngine | |
{ | |
/// <summary> | |
/// Compiled Regex for single substitutions | |
/// </summary> | |
private Regex singleSubstitutionsRegEx = new Regex(@"@Model\.(?<ParameterName>[a-zA-Z0-9-_]*)", RegexOptions.Compiled); | |
/// <summary> | |
/// Compiled Regex for each blocks | |
/// </summary> | |
private Regex eachSubstitutionRegEx = new Regex(@"@Each\.(?<ParameterName>[a-zA-Z0-9-_]*)(?<Contents>.*?)@EndEach", RegexOptions.Compiled | RegexOptions.Singleline); | |
/// <summary> | |
/// Compiled Regex for each block current substitutions | |
/// </summary> | |
private Regex eachItemSubstitutionRegEx = new Regex("@Current", RegexOptions.Compiled); | |
/// <summary> | |
/// Compiled Regex for if blocks | |
/// </summary> | |
private Regex ifSubstitutionRegEx = new Regex(@"@If\.(?<ParameterName>[a-zA-Z0-9-_]*)(?<Contents>.*?)@EndIf", RegexOptions.Compiled | RegexOptions.Singleline); | |
/// <summary> | |
/// Compiled Regex for ifnot blocks | |
/// </summary> | |
private Regex ifNotSubstitutionRegEx = new Regex(@"@IfNot\.(?<ParameterName>[a-zA-Z0-9-_]*)(?<Contents>.*?)@EndIf", RegexOptions.Compiled | RegexOptions.Singleline); | |
/// <summary> | |
/// View engine transform processors | |
/// </summary> | |
private List<Func<string, object, Func<object, string, object>, string>> processors; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="SuperSimpleViewEngine"/> class. | |
/// </summary> | |
public SuperSimpleViewEngine() | |
{ | |
this.processors = new List<Func<string, object, Func<object, string, object>, string>>() | |
{ | |
this.PerformSingleSubstitutions, | |
this.PerformEachSubstitutions, | |
this.PerformConditionalSubstitutions, | |
}; | |
} | |
/// <summary> | |
/// Renders a template | |
/// </summary> | |
/// <param name="template"> | |
/// The template to render. | |
/// </param> | |
/// <param name="model"> | |
/// The model to user for rendering. | |
/// </param> | |
/// <returns> | |
/// A string containing the expanded template. | |
/// </returns> | |
public string Render(string template, dynamic model) | |
{ | |
var propertyExtractor = this.GetPropertyExtractor(model); | |
return this.processors.Aggregate(template, (current, processor) => processor(current, model, propertyExtractor)); | |
} | |
/// <summary> | |
/// <para> | |
/// Gets the correct property extractor for the given model. | |
/// </para> | |
/// <para> | |
/// Anonymous types, standard types and ExpandoObject are supported. | |
/// Arbitrary dynamics (implementing IDynamicMetaObjectProvicer) are not, unless | |
/// they also implmennt IDictionary string, object for accessing properties. | |
/// </para> | |
/// </summary> | |
/// <param name="model"> | |
/// The model. | |
/// </param> | |
/// <returns> | |
/// Delegate for getting properties - delegate returns a value or null if not there. | |
/// </returns> | |
/// <exception cref="ArgumentException"> | |
/// Model type is not supported. | |
/// </exception> | |
private Func<object, string, object> GetPropertyExtractor(object model) | |
{ | |
if (!typeof(IDynamicMetaObjectProvider).IsAssignableFrom(model.GetType())) | |
{ | |
return this.GetStandardTypePropertyExtractor(model); | |
} | |
if (typeof(IDictionary<string, object>).IsAssignableFrom(model.GetType())) | |
{ | |
return this.DynamicDictionaryPropertyExtractor; | |
} | |
throw new ArgumentException("model must be a standard type or implement IDictionary<string, object>", "model"); | |
} | |
/// <summary> | |
/// <para> | |
/// Returns the standard property extractor. | |
/// </para> | |
/// <para> | |
/// Model properties are enumerated once and a closure is returned that captures them. | |
/// </para> | |
/// </summary> | |
/// <param name="model"> | |
/// The model. | |
/// </param> | |
/// <returns>Delegate for getting properties - delegate returns a value or null if not there.</returns> | |
private Func<object, string, object> GetStandardTypePropertyExtractor(object model) | |
{ | |
var properties = model.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); | |
return (mdl, propName) => | |
{ | |
var property = | |
properties.Where(p => String.Equals(p.Name, propName, StringComparison.InvariantCulture)). | |
FirstOrDefault(); | |
return property == null ? null : property.GetValue(mdl, null); | |
}; | |
} | |
/// <summary> | |
/// A property extractor designed for ExpandoObject, but also for any | |
/// type that implements IDictionary string object for accessing its | |
/// properties. | |
/// </summary> | |
/// <param name="model"> | |
/// The model. | |
/// </param> | |
/// <param name="propertyName"> | |
/// The property name. | |
/// </param> | |
/// <returns> | |
/// Object if property is found, null if not. | |
/// </returns> | |
private object DynamicDictionaryPropertyExtractor(object model, string propertyName) | |
{ | |
var dictionaryModel = (IDictionary<string, object>)model; | |
object output; | |
dictionaryModel.TryGetValue(propertyName, out output); | |
return output; | |
} | |
/// <summary> | |
/// Performs single @Model.PropertyName substitutions. | |
/// </summary> | |
/// <param name="template"> | |
/// The template. | |
/// </param> | |
/// <param name="model"> | |
/// The model. | |
/// </param> | |
/// <param name="propertyExtractor"> | |
/// The property extractor. | |
/// </param> | |
/// <returns> | |
/// Template with @Model.PropertyName blocks expanded | |
/// </returns> | |
private string PerformSingleSubstitutions(string template, object model, Func<object, string, object> propertyExtractor) | |
{ | |
return this.singleSubstitutionsRegEx.Replace( | |
template, | |
(m) => | |
{ | |
var substitution = propertyExtractor(model, m.Groups["ParameterName"].Value); | |
return substitution == null ? "[ERR!]" : substitution.ToString(); | |
}); | |
} | |
/// <summary> | |
/// Performs @Each.PropertyName substitutions | |
/// </summary> | |
/// <param name="template"> | |
/// The template. | |
/// </param> | |
/// <param name="model"> | |
/// The model. | |
/// </param> | |
/// <param name="propertyExtractor"> | |
/// The property extractor. | |
/// </param> | |
/// <returns> | |
/// Template with @Each.PropertyName blocks expanded | |
/// </returns> | |
private string PerformEachSubstitutions(string template, object model, Func<object, string, object> propertyExtractor) | |
{ | |
return this.eachSubstitutionRegEx.Replace( | |
template, | |
(m) => | |
{ | |
var substitutionObject = propertyExtractor(model, m.Groups["ParameterName"].Value); | |
if (substitutionObject == null) | |
{ | |
return "[ERR!]"; | |
} | |
var substitutionEnumerable = substitutionObject as IEnumerable; | |
if (substitutionEnumerable == null) | |
{ | |
return "[ERR!]"; | |
} | |
var result = string.Empty; | |
foreach (var item in substitutionEnumerable) | |
{ | |
result += eachItemSubstitutionRegEx.Replace(m.Groups["Contents"].Value, item.ToString()); | |
} | |
return result; | |
}); | |
} | |
/// <summary> | |
/// Performs @If.PropertyName and @IfNot.PropertyName substitutions | |
/// </summary> | |
/// <param name="template"> | |
/// The template. | |
/// </param> | |
/// <param name="model"> | |
/// The model. | |
/// </param> | |
/// <param name="propertyExtractor"> | |
/// The property extractor. | |
/// </param> | |
/// <returns> | |
/// Template with @If.PropertyName @IfNot.PropertyName blocks removed/expanded | |
/// </returns> | |
private string PerformConditionalSubstitutions(string template, object model, Func<object, string, object> propertyExtractor) | |
{ | |
var result = template; | |
result = this.ifSubstitutionRegEx.Replace( | |
result, | |
(m) => | |
{ | |
var predicateResult = false; | |
var substitutionObject = propertyExtractor(model, m.Groups["ParameterName"].Value); | |
if (substitutionObject != null) | |
{ | |
var substitutionBool = substitutionObject as bool?; | |
if (substitutionBool != null) | |
{ | |
predicateResult = substitutionBool.Value; | |
} | |
} | |
return !predicateResult ? String.Empty : m.Groups["Contents"].Value; | |
}); | |
result = this.ifNotSubstitutionRegEx.Replace( | |
result, | |
(m) => | |
{ | |
var predicateResult = true; | |
var substitutionObject = propertyExtractor(model, m.Groups["ParameterName"].Value); | |
if (substitutionObject != null) | |
{ | |
var substitutionBool = substitutionObject as bool?; | |
if (substitutionBool != null) | |
{ | |
predicateResult = !substitutionBool.Value; | |
} | |
} | |
return !predicateResult ? String.Empty : m.Groups["Contents"].Value; | |
}); | |
return result; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace TinyTemplates.Tests.ViewEngine | |
{ | |
using System.Dynamic; | |
using System.Linq; | |
using TinyTemplates.ViewEngine; | |
using Xunit; | |
using System.Collections.Generic; | |
public class SuperSimpleViewEngineTests | |
{ | |
public class FakeModel | |
{ | |
public FakeModel(string name, List<string> users) | |
{ | |
this.Name = name; | |
this.Users = users; | |
} | |
public List<string> Users { get; private set; } | |
public string Name { get; private set; } | |
public bool HasUsers | |
{ | |
get | |
{ | |
return this.Users.Any(); | |
} | |
} | |
} | |
private SuperSimpleViewEngine viewEngine; | |
public SuperSimpleViewEngineTests() | |
{ | |
this.viewEngine = new SuperSimpleViewEngine(); | |
} | |
[Fact] | |
public void Replaces_valid_property_when_followed_by_closing_tag() | |
{ | |
var input = @"<html><head></head><body>Hello there @Model.Name</body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Name = "Bob"; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body>Hello there Bob</body></html>", output); | |
} | |
[Fact] | |
public void Replaces_multiple_properties_with_the_same_name() | |
{ | |
var input = @"<html><head></head><body>Hello there @Model.Name, nice to see you @Model.Name</body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Name = "Bob"; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body>Hello there Bob, nice to see you Bob</body></html>", output); | |
} | |
[Fact] | |
public void Replaces_invalid_properties_with_error_string() | |
{ | |
var input = @"<html><head></head><body>Hello there @Model.Wrong</body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Name = "Bob"; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body>Hello there [ERR!]</body></html>", output); | |
} | |
[Fact] | |
public void Does_not_replace_properties_if_case_is_incorrect() | |
{ | |
var input = @"<html><head></head><body>Hello there @Model.name</body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Name = "Bob"; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body>Hello there [ERR!]</body></html>", output); | |
} | |
[Fact] | |
public void Replaces_multiple_properties_from_dictionary() | |
{ | |
var input = @"<html><head></head><body>Hello there @Model.Name - welcome to @Model.SiteName</body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Name = "Bob"; | |
model.SiteName = "Cool Site!"; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body>Hello there Bob - welcome to Cool Site!</body></html>", output); | |
} | |
[Fact] | |
public void Creates_multiple_entries_for_each_statements() | |
{ | |
var input = @"<html><head></head><body><ul>@Each.Users<li>@Current</li>@EndEach</ul></body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Users = new List<string>() { "Bob", "Jim", "Bill" }; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><ul><li>Bob</li><li>Jim</li><li>Bill</li></ul></body></html>", output); | |
} | |
[Fact] | |
public void Can_use_multiple_current_statements_inside_each() | |
{ | |
var input = @"<html><head></head><body><ul>@Each.Users<li id=""@Current"">@Current</li>@EndEach</ul></body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Users = new List<string>() { "Bob", "Jim", "Bill" }; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><ul><li id=""Bob"">Bob</li><li id=""Jim"">Jim</li><li id=""Bill"">Bill</li></ul></body></html>", output); | |
} | |
[Fact] | |
public void Trying_to_use_non_enumerable_in_each_shows_error() | |
{ | |
var input = @"<html><head></head><body><ul>@Each.Users<li id=""@Current"">@Current</li>@EndEach</ul></body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Users = new object(); | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><ul>[ERR!]</ul></body></html>", output); | |
} | |
[Fact] | |
public void Can_combine_single_substitutions_and_each_substitutions() | |
{ | |
var input = @"<html><head></head><body><ul>@Each.Users<li>Hello @Current, @Model.Name says hello!</li>@EndEach</ul></body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Name = "Nancy"; | |
model.Users = new List<string>() { "Bob", "Jim", "Bill" }; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><ul><li>Hello Bob, Nancy says hello!</li><li>Hello Jim, Nancy says hello!</li><li>Hello Bill, Nancy says hello!</li></ul></body></html>", output); | |
} | |
[Fact] | |
public void Model_statement_can_be_followed_by_a_newline() | |
{ | |
var input = "<html><head></head><body>Hello there @Model.Name\n</body></html>"; | |
dynamic model = new ExpandoObject(); | |
model.Name = "Bob"; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal("<html><head></head><body>Hello there Bob\n</body></html>", output); | |
} | |
[Fact] | |
public void Each_statements_should_work_over_multiple_lines() | |
{ | |
var input = "<html>\n\t<head>\n\t</head>\n\t<body>\n\t\t<ul>@Each.Users\n\t\t\t<li>@Current</li>@EndEach\n\t\t</ul>\n\t</body>\n</html>"; | |
dynamic model = new ExpandoObject(); | |
model.Users = new List<string>() { "Bob", "Jim", "Bill" }; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal("<html>\n\t<head>\n\t</head>\n\t<body>\n\t\t<ul>\n\t\t\t<li>Bob</li>\n\t\t\t<li>Jim</li>\n\t\t\t<li>Bill</li>\n\t\t</ul>\n\t</body>\n</html>", output); | |
} | |
[Fact] | |
public void Single_substitutions_work_with_standard_anonymous_type_objects() | |
{ | |
var input = @"<html><head></head><body>Hello there @Model.Name - welcome to @Model.SiteName</body></html>"; | |
var model = new { Name = "Bob", SiteName = "Cool Site!" }; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body>Hello there Bob - welcome to Cool Site!</body></html>", output); | |
} | |
[Fact] | |
public void Each_substitutions_work_with_standard_anonymous_type_objects() | |
{ | |
var input = @"<html><head></head><body><ul>@Each.Users<li id=""@Current"">@Current</li>@EndEach</ul></body></html>"; | |
var model = new { Users = new List<string>() { "Bob", "Jim", "Bill" } }; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><ul><li id=""Bob"">Bob</li><li id=""Jim"">Jim</li><li id=""Bill"">Bill</li></ul></body></html>", output); | |
} | |
[Fact] | |
public void Substitutions_work_with_standard_objects() | |
{ | |
var input = @"<html><head></head><body><ul>@Each.Users<li>Hello @Current, @Model.Name says hello!</li>@EndEach</ul></body></html>"; | |
var model = new FakeModel("Nancy", new List<string>() { "Bob", "Jim", "Bill" }); | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><ul><li>Hello Bob, Nancy says hello!</li><li>Hello Jim, Nancy says hello!</li><li>Hello Bill, Nancy says hello!</li></ul></body></html>", output); | |
} | |
[Fact] | |
public void If_statement_with_true_returned_renders_block() | |
{ | |
var input = @"<html><head></head><body>@If.HasUsers<ul>@Each.Users<li>Hello @Current, @Model.Name says hello!</li>@EndEach</ul>@EndIf</body></html>"; | |
var model = new FakeModel("Nancy", new List<string>() { "Bob", "Jim", "Bill" }); | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><ul><li>Hello Bob, Nancy says hello!</li><li>Hello Jim, Nancy says hello!</li><li>Hello Bill, Nancy says hello!</li></ul></body></html>", output); | |
} | |
[Fact] | |
public void If_statement_with_false_returned_does_not_render_block() | |
{ | |
var input = @"<html><head></head><body>@If.HasUsers<ul>@Each.Users<li>Hello @Current, @Model.Name says hello!</li>@EndEach</ul>@EndIf</body></html>"; | |
var model = new FakeModel("Nancy", new List<string>()); | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body></body></html>", output); | |
} | |
[Fact] | |
public void IfNot_statement_with_true_returned_does_not_renders_block() | |
{ | |
var input = @"<html><head></head><body>@IfNot.HasUsers<p>No users found!</p>@EndIf<ul>@Each.Users<li>Hello @Current, @Model.Name says hello!</li>@EndEach</ul></body></html>"; | |
var model = new FakeModel("Nancy", new List<string>() { "Bob", "Jim", "Bill" }); | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><ul><li>Hello Bob, Nancy says hello!</li><li>Hello Jim, Nancy says hello!</li><li>Hello Bill, Nancy says hello!</li></ul></body></html>", output); | |
} | |
[Fact] | |
public void IfNot_statement_with_false_returned_renders_block() | |
{ | |
var input = @"<html><head></head><body>@IfNot.HasUsers<p>No users found!</p>@EndIf<ul>@Each.Users<li>Hello @Current, @Model.Name says hello!</li>@EndEach</ul></body></html>"; | |
var model = new FakeModel("Nancy", new List<string>()); | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><p>No users found!</p><ul></ul></body></html>", output); | |
} | |
[Fact] | |
public void If_and_IfNot_statements_combined_but_not_nested_do_not_conflict() | |
{ | |
var input = @"<html><head></head><body>@IfNot.HasUsers<p>No users found!</p>@EndIf@If.HasUsers<ul>@Each.Users<li>Hello @Current, @Model.Name says hello!</li>@EndEach</ul>@EndIf</body></html>"; | |
var model = new FakeModel("Nancy", new List<string>()); | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<html><head></head><body><p>No users found!</p></body></html>", output); | |
} | |
[Fact] | |
public void Multiple_if_statements_match_correctly() | |
{ | |
var input = "@If.One<p>One</p>@EndIf @If.Two<p>Two</p>@EndIf"; | |
var model = new { One = true, Two = true }; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<p>One</p> <p>Two</p>", output); | |
} | |
[Fact] | |
public void Multiple_each_statements_match_correctly() | |
{ | |
var input = "@Each.Users<li>@Current</li>@EndEach @Each.Admins<li>@Current</li>@EndEach"; | |
var model = new { Users = new List<string> { "1", "2" }, Admins = new List<string> { "3", "4" } }; | |
var output = viewEngine.Render(input, model); | |
Assert.Equal(@"<li>1</li><li>2</li> <li>3</li><li>4</li>", output); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment