Skip to content

Instantly share code, notes, and snippets.

@grumpydev
Created February 24, 2011 12:37
Show Gist options
  • Save grumpydev/842117 to your computer and use it in GitHub Desktop.
Save grumpydev/842117 to your computer and use it in GitHub Desktop.
<!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>
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;
}
}
}
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