Implement custom tags.

This is the first step towards supporting custom tags. There are
wrinkles I need to work out, since I'm not 100% sure what the finished
code will look like.
This commit is contained in:
Travis Parks 2013-01-08 21:33:53 -05:00
parent 827faa5d6e
commit f8628aaf86
28 changed files with 603 additions and 1774 deletions

View File

@ -1,524 +0,0 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Globalization;
namespace mustache.test
{
/// <summary>
/// Tests the Formatter class.
/// </summary>
[TestClass]
public class FormatterTester
{
#region Real World Example
/// <summary>
/// The Formatter class is especially useful when performing simple mail merge operations.
/// Like String.Format, Formatter will substitute placeholders for actual values. In the case
/// of Formatter, placeholders are indicated by name, rather than index and are wrapped with
/// double curly braces: {{name}}. The name within the curly brace can include any characters,
/// including whitespace, except for two or more adjacent right curly braces (}}).
/// </summary>
[TestMethod]
public void TestFormatter_ReplaceNamedPlaceholdersWithFormats()
{
const string format = "Hello {{name}}! It is {{date:MM-dd-yyyy}}. You make {{income:C}} an hour.";
Formatter formatter = new Formatter(format);
string result1 = formatter.Format(new Dictionary<string, object>()
{
{ "name", "Bob" },
{ "date", new DateTime(2012, 03, 11) },
{ "income", 32.8 }
});
Assert.AreEqual("Hello Bob! It is 03-11-2012. You make $32.80 an hour.", result1);
}
/// <summary>
/// If we want to work with objects, rather than raw dictionaries, we can wrap the objects with
/// property dictionaries.
/// </summary>
[TestMethod]
public void TestFormatter_UseObject()
{
var person = new
{
Name = "Bob",
Date = new DateTime(2012, 03, 11),
Income = 32.8
};
const string format = "Hello {{Name}}! It is {{Date:MM-dd-yyyy}}. You make {{Income:C}} an hour.";
Formatter formatter = new Formatter(format);
string result1 = formatter.Format(person);
Assert.AreEqual("Hello Bob! It is 03-11-2012. You make $32.80 an hour.", result1);
}
/// <summary>
/// We can the Formatter to print out a list of items following a format.
/// </summary>
[TestMethod]
public void TestFormatter_PrintList()
{
List<int> values = new List<int>() { 0, 1, 2, 3, 4 };
const string format = "{{#each this}}{{this}} {{/each}}";
Formatter formatter = new Formatter(format);
string result = formatter.Format(values);
Assert.AreEqual("0 1 2 3 4 ", result);
}
/// <summary>
/// We can include some text conditionally.
/// </summary>
[TestMethod]
public void TestFormatter_ConditionallyIncludeText()
{
Random random = new Random();
int value = random.Next();
bool isEven = value % 2 == 0;
var data = new
{
Value = value,
IsEven = isEven,
};
const string format = "{{Value}} {{#if IsEven}}is even{{#else}}is odd{{/if}}.";
Formatter formatter = new Formatter(format);
string result = formatter.Format(data);
string expected = String.Format("{0}", value) + (isEven ? " is even." : " is odd.");
Assert.AreEqual(expected, result);
}
/// <summary>
/// Multiple cases can be handled using if/elif/else.
/// </summary>
[TestMethod]
public void TestFormatter_HandleCases()
{
const string format = @"{{#if No}}No{{#elif Yes}}Yes{{#else}}Maybe{{/if}}";
Formatter formatter = new Formatter(format);
var data = new
{
Yes = true,
No = false,
};
string result = formatter.Format(data);
Assert.AreEqual("Yes", result);
}
/// <summary>
/// We should be able to combine tags anyway we want.
/// </summary>
[TestMethod]
public void TestFormatter_Compound()
{
const string format = @"{{#with Customer}}
Hello{{#if FirstName}} {{FirstName}}{{/if}}:
{{/with}}
{{#! We only want to print out purchases if they have some. }}
{{#if Purchases}}
You recently purchased:
{{#each Purchases}}
{{Name}}: {{Quantity}} x {{Price:C}}
{{/each}}
Your total was: {{Total:C}}
{{/if}}
We thought you might be interested in buying: {{PromotionProduct}}.
Thank you,
{{#with Agent}}
{{Name}}
{{/with}}";
Formatter formatter = new Formatter(format);
var data = new
{
Customer = new
{
FirstName = "Bob",
},
Purchases = new object[]
{
new
{
Name = "Donkey",
Quantity = 8,
Price = 1.23m,
},
new
{
Name = "Hammer",
Quantity = 1,
Price = 8.32m,
},
},
Total = 18.16m,
PromotionProduct = "Sneakers",
Agent = new
{
Name = "Tom",
},
};
string result = formatter.Format(data);
Assert.AreEqual(@"Hello Bob:
You recently purchased:
Donkey: 8 x $1.23
Hammer: 1 x $8.32
Your total was: $18.16
We thought you might be interested in buying: Sneakers.
Thank you,
Tom
", result);
}
#endregion
#region Argument Checking
/// <summary>
/// An exception should be thrown if the format string is null.
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void TestCtor_NullFormat_ThrowsException()
{
string format = null;
new Formatter(format);
}
/// <summary>
/// If we try to replace a placeholder that we do not have a lookup key for,
/// an exception should be thrown.
/// </summary>
[TestMethod]
[ExpectedException(typeof(KeyNotFoundException))]
public void TestFormat_MissingKey_ThrowsException()
{
Formatter formatter = new Formatter("{{unknown}}");
IDictionary<string, object> lookup = new Dictionary<string, object>();
formatter.Format(lookup);
}
/// <summary>
/// A format exception should be thrown if there is not a matching closing if tag.
/// </summary>
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void TestFormat_MissingClosingIfTag_ThrowsException()
{
new Formatter("{{#if Bob}}Hello");
}
/// <summary>
/// A format exception should be thrown if the matching closing tag is wrong.
/// </summary>
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void TestFormat_WrongClosingIfTag_ThrowsException()
{
new Formatter("{{#with this}}{{#if Bob}}Hello{{/with}}{{/if}}");
}
#endregion
/// <summary>
/// If we specify a right alignment, the output should be aligned to the right.
/// </summary>
[TestMethod]
public void TestFormatter_WithRightAlignment_AlignsToRight()
{
string format = "{{Name,10}}";
var instance = new
{
Name = "Bob"
};
PropertyDictionary dictionary = new PropertyDictionary(instance);
string result = Formatter.Format(format, dictionary);
Assert.AreEqual(" Bob", result, "The text was not aligned.");
}
/// <summary>
/// If we specify a left alignment, the output should be aligned to the left.
/// </summary>
[TestMethod]
public void TestFormatter_WithLeftAlignment_AlignsToLeft()
{
string format = "{{Name,-10}}";
var instance = new
{
Name = "Bob"
};
PropertyDictionary dictionary = new PropertyDictionary(instance);
string result = Formatter.Format(null, format, dictionary);
Assert.AreEqual("Bob ", result, "The text was not aligned.");
}
/// <summary>
/// If we try to format an empty string, an empty string should be returned.
/// </summary>
[TestMethod]
public void TestFormatter_EmptyFormat_ReturnsEmpty()
{
Formatter formatter = new Formatter(String.Empty);
Dictionary<string, object> lookup = new Dictionary<string, object>();
string result = formatter.Format(lookup);
Assert.AreEqual(String.Empty, result, "The result should have been empty.");
}
/// <summary>
/// If our format string is just a placeholder, than just the replacement value should be returned.
/// </summary>
[TestMethod]
public void TestFormatter_FormatIsSinglePlaceholder_ReturnsReplaced()
{
Formatter formatter = new Formatter("{{name}}");
Dictionary<string, object> lookup = new Dictionary<string, object>()
{
{ "name", "test" }
};
string result = formatter.Format(lookup);
Assert.AreEqual("test", result, "The result was wrong.");
}
/// <summary>
/// We should be able to put just about anything inside of a placeholder, but it will
/// not be treated like a placeholder.
/// </summary>
[TestMethod]
public void TestFormatter_PlaceholderContainsSpecialCharacters_ReturnsUnreplaced()
{
Formatter formatter = new Formatter("{{ \\_@#$%^ }1233 abc}}");
Dictionary<string, object> lookup = new Dictionary<string, object>()
{
{ " \\_@#$%^ }1233 abc", "test" }
};
string result = formatter.Format(lookup);
Assert.AreEqual("{{ \\_@#$%^ }1233 abc}}", result, "The result was wrong.");
}
/// <summary>
/// If a lookup value is null, it should be replaced with an empty string.
/// </summary>
[TestMethod]
public void TestFormatter_NullValue_ReplacesWithBlank()
{
Formatter formatter = new Formatter("These quotes should be empty '{{name}}'.");
Dictionary<string, object> lookup = new Dictionary<string, object>()
{
{ "name", null }
};
string result = formatter.Format(lookup);
Assert.AreEqual("These quotes should be empty ''.", result, "The result was wrong.");
}
/// <summary>
/// If a replacement value contains a placeholder, it should NOT be evaluated.
/// </summary>
[TestMethod]
public void TestFormatter_ReplacementContainsPlaceholder_IgnoresPlaceholder()
{
Formatter formatter = new Formatter("The length of {{name}} is {{length}}.");
Dictionary<string, object> lookup = new Dictionary<string, object>()
{
{ "name", "Bob" },
{ "length", "{{name}}" }
};
string result = formatter.Format(lookup);
Assert.AreEqual("The length of Bob is {{name}}.", result, "The result was wrong.");
}
/// <summary>
/// If we pass null to as the format provider to the Format function,
/// the current culture is used.
/// </summary>
[TestMethod]
public void TestFormatter_NullFormatter_UsesCurrentCulture()
{
string format = "{0:C}";
Formatter formatter = new Formatter("{" + format + "}");
string result = formatter.Format((IFormatProvider)null, new Dictionary<string, object>() { { "0", 28.30m } });
string expected = String.Format(CultureInfo.CurrentCulture, format, 28.30m);
Assert.AreEqual(expected, result, "The wrong format provider was used.");
}
/// <summary>
/// If we put a tag on a line by itself, it shouldn't result in any whitespace.
/// </summary>
[TestMethod]
public void TestFormatter_TagOnLineByItself_NoNewlineGenerated()
{
const string format = @"Hello
{{#if Name}}
{{Name}}
{{/if}}
Goodbye
";
var data = new { Name = "George" };
Formatter formatter = new Formatter(format);
string result = formatter.Format(data);
const string expected = @"Hello
George
Goodbye
";
Assert.AreEqual(expected, result);
}
/// <summary>
/// If a key is not found at the current level, it is looked for at the parent level.
/// </summary>
[TestMethod]
public void TestFormatter_NameAtHigherScope_Finds()
{
const string format = "{{#with Child}}{{TopLevel}} and {{ChildLevel}}{{/with}}";
Formatter formatter = new Formatter(format);
var data = new
{
TopLevel = "Parent",
Child = new { ChildLevel = "Child" },
};
string result = formatter.Format(data);
Assert.AreEqual("Parent and Child", result);
}
/// <summary>
/// Null values are considered false by if statements.
/// </summary>
[TestMethod]
public void TestFormatter_ConditionOnNull_ConsideredFalse()
{
const string format = "{{#if this}}Bad{{#else}}Good{{/if}}";
Formatter formatter = new Formatter(format);
string result = formatter.Format(null);
Assert.AreEqual("Good", result);
}
/// <summary>
/// Empty collections are considered false by if statements.
/// </summary>
[TestMethod]
public void TestFormatter_ConditionOnEmptyCollection_ConsideredFalse()
{
const string format = "{{#if this}}Bad{{#else}}Good{{/if}}";
Formatter formatter = new Formatter(format);
string result = formatter.Format(new object[0]);
Assert.AreEqual("Good", result);
}
/// <summary>
/// Non-empty collections are considered true by if statements.
/// </summary>
[TestMethod]
public void TestFormatter_ConditionOnNonEmptyCollection_ConsideredTrue()
{
const string format = "{{#if this}}Good{{#else}}Bad{{/if}}";
Formatter formatter = new Formatter(format);
string result = formatter.Format(new object[1]);
Assert.AreEqual("Good", result);
}
/// <summary>
/// Null-char is considered false by if statements.
/// </summary>
[TestMethod]
public void TestFormatter_ConditionOnNullChar_ConsideredFalse()
{
const string format = "{{#if this}}Bad{{#else}}Good{{/if}}";
Formatter formatter = new Formatter(format);
string result = formatter.Format('\0');
Assert.AreEqual("Good", result);
}
/// <summary>
/// Zero is considered false by if statements.
/// </summary>
[TestMethod]
public void TestFormatter_ConditionOnZero_ConsideredFalse()
{
const string format = "{{#if this}}Bad{{#else}}Good{{/if}}";
Formatter formatter = new Formatter(format);
int? value = 0;
string result = formatter.Format(value);
Assert.AreEqual("Good", result);
}
/// <summary>
/// Everything else is considered true by if statements.
/// </summary>
[TestMethod]
public void TestFormatter_ConditionOnDateTime_ConsideredTrue()
{
const string format = "{{#if this}}Good{{#else}}Bad{{/if}}";
Formatter formatter = new Formatter(format);
string result = formatter.Format(DateTime.Now);
Assert.AreEqual("Good", result);
}
/// <summary>
/// Instead of requiring deeply nested "with" statements, members
/// can be separated by dots.
/// </summary>
[TestMethod]
public void TestFormatter_NestedMembers_SearchesMembers()
{
const string format = "{{Customer.Name}}";
Formatter formatter = new Formatter(format);
var data = new { Customer = new { Name = "Bob" } };
string result = formatter.Format(data);
Assert.AreEqual("Bob", result);
}
/// <summary>
/// Keys should cause newlines to be respected, since they are considered content.
/// </summary>
[TestMethod]
public void TestFormatter_KeyBetweenTags_RespectsTrailingNewline()
{
string format = "{{#each this}}{{this}} {{/each}}" + Environment.NewLine;
Formatter formatter = new Formatter(format);
string result = formatter.Format("Hello");
Assert.AreEqual("H e l l o " + Environment.NewLine, result);
}
/// <summary>
/// If someone tries to loop on a non-enumerable, it should do nothing.
/// </summary>
[TestMethod]
public void TestFormatter_EachOnNonEnumerable_PrintsNothing()
{
const string format = "{{#each this}}Bad{{/each}}";
Formatter formatter = new Formatter(format);
string result = formatter.Format(123);
Assert.AreEqual(String.Empty, result);
}
/// <summary>
/// If a tag header is on the same line as it's footer, the new-line should not be removed.
/// </summary>
[TestMethod]
public void TestFormatter_InlineTags_RespectNewLine()
{
const string format = @"{{#if this}}{{/if}}
";
Formatter formatter = new Formatter(format);
string result = formatter.Format(true);
Assert.AreEqual(Environment.NewLine, result);
}
/// <summary>
/// If a tag header is on the same line as it's footer, the new-line should not be removed.
/// </summary>
[TestMethod]
public void TestFormatter_TagFooterFollowedByTagHeader_RemovesNewLine()
{
const string format = @"{{#if this}}
{{/if}}{{#if this}}
Hello{{/if}}";
Formatter formatter = new Formatter(format);
string result = formatter.Format(true);
Assert.AreEqual("Hello", result);
}
}
}

View File

@ -1,484 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace mustache.test
{
/// <summary>
/// Tests the PropertyDictionary class.
/// </summary>
[TestClass]
public class PropertyDictionaryTester
{
#region Real World Example
/// <summary>
/// The purpose of the PropertyDictionary class is to allow an object to be inspected,
/// as if it were a dictionary. This means we can get and set properties by their names.
/// </summary>
[TestMethod]
public void TestPropertyDictionary_AccessPropertiesViaIndexer()
{
var person = new
{
Name = "Bob",
Age = 23,
Birthday = new DateTime(2012, 03, 12)
};
PropertyDictionary wrapper = new PropertyDictionary(person);
Assert.AreEqual(3, wrapper.Count, "The wrong number of properties were created.");
Assert.IsTrue(wrapper.ContainsKey("Name"));
Assert.IsTrue(wrapper.ContainsKey("Age"));
Assert.IsTrue(wrapper.ContainsKey("Birthday"));
Assert.AreEqual(person.Name, wrapper["Name"], "The name was not wrapped.");
Assert.AreEqual(person.Age, wrapper["Age"], "The age was not wrapped.");
Assert.AreEqual(person.Birthday, wrapper["Birthday"], "The birthday was not wrapped.");
}
#endregion
#region Ctor & Instance & IsReadOnly
/// <summary>
/// If we try to wrap null, an exception should be thrown.
/// </summary>
[TestMethod]
public void TestCtor_NullInstance_ThrowsException()
{
PropertyDictionary dictionary = new PropertyDictionary(null);
Assert.AreEqual(0, dictionary.Count);
}
/// <summary>
/// We should be able to access the underlying object.
/// </summary>
[TestMethod]
public void TestCtor_SetsInstance()
{
object instance = new object();
PropertyDictionary dictionary = new PropertyDictionary(instance);
Assert.AreSame(instance, dictionary.Instance, "The instance was not set.");
ICollection<KeyValuePair<string, object>> collection = dictionary;
Assert.IsTrue(collection.IsReadOnly, "The collection should not have been read-only.");
}
#endregion
#region Add
/// <summary>
/// Since the dictionary is a simple wrapper around an object, we cannot add new properties.
/// </summary>
[TestMethod]
[ExpectedException(typeof(NotSupportedException))]
public void TestAdd_IDictionary_ThrowsException()
{
IDictionary<string, object> dictionary = new PropertyDictionary(new object());
dictionary.Add("Name", "Bob");
}
/// <summary>
/// Since the dictionary is a simple wrapper around an object, we cannot add new properties.
/// </summary>
[TestMethod]
[ExpectedException(typeof(NotSupportedException))]
public void TestAdd_ICollection_ThrowsException()
{
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(new object());
collection.Add(new KeyValuePair<string, object>("Name", "Bob"));
}
#endregion
#region ContainsKey
/// <summary>
/// If the wrapped object has a property, the key should be found.
/// </summary>
[TestMethod]
public void TestContainsKey_PropertyExists_ReturnsTrue()
{
var person = new
{
Name = "Bob",
};
PropertyDictionary dictionary = new PropertyDictionary(person);
bool result = dictionary.ContainsKey("Name");
Assert.IsTrue(result, "The property name was not found.");
}
/// <summary>
/// If the wrapped object does not have a property, the key should not be found.
/// </summary>
[TestMethod]
public void TestContainsKey_PropertyMissing_ReturnsFalse()
{
var person = new { };
PropertyDictionary dictionary = new PropertyDictionary(person);
bool result = dictionary.ContainsKey("Name");
Assert.IsFalse(result, "The property name was found.");
}
private class BaseType
{
public string Inherited { get; set; }
}
private class DerivedType : BaseType
{
public string Local { get; set; }
}
/// <summary>
/// We should be able to see properties defined in the base type.
/// </summary>
[TestMethod]
public void TestContainsKey_PropertyInherited_ReturnsTrue()
{
BaseType b = new DerivedType();
PropertyDictionary dictionary = new PropertyDictionary(b);
bool result = dictionary.ContainsKey("Inherited");
Assert.IsTrue(result, "The property name was not found.");
}
private class PrivateType
{
private string Hidden { get; set; }
}
/// <summary>
/// We should not be able to see private properties.
/// </summary>
[TestMethod]
public void TestContainsKey_PropertyPrivate_ReturnsFalse()
{
PrivateType t = new PrivateType();
PropertyDictionary dictionary = new PropertyDictionary(t);
bool result = dictionary.ContainsKey("Hidden");
Assert.IsFalse(result, "The property name was found.");
}
private class StaticType
{
public static string Static { get; set; }
}
/// <summary>
/// We should not be able to see static properties.
/// </summary>
[TestMethod]
public void TestContainsKey_PropertyStatic_ReturnsFalse()
{
StaticType t = new StaticType();
PropertyDictionary dictionary = new PropertyDictionary(t);
bool result = dictionary.ContainsKey("Static");
Assert.IsFalse(result, "The property name was found.");
}
#endregion
#region Keys
/// <summary>
/// Keys should return the name of all of the property names in the object.
/// </summary>
[TestMethod]
public void TestKeys_GetsAllPropertyNames()
{
var person = new
{
Name = "Bob",
Age = 23
};
PropertyDictionary dictionary = new PropertyDictionary(person);
ICollection<string> keys = dictionary.Keys;
Assert.AreEqual(2, keys.Count, "The wrong number of keys were returned.");
Assert.IsTrue(keys.Contains("Name"), "The Name property was not found.");
Assert.IsTrue(keys.Contains("Age"), "The Age property was not found.");
}
#endregion
#region Remove
/// <summary>
/// Since a property dictionary is just a wrapper around an object, we cannot remove properties from it.
/// </summary>
[TestMethod]
[ExpectedException(typeof(NotSupportedException))]
public void TestRemove_IDictionary_ThrowsException()
{
object instance = new object();
IDictionary<string, object> dictionary = new PropertyDictionary(instance);
dictionary.Remove("Name");
}
/// <summary>
/// Since a property dictionary is just a wrapper around an object, we cannot remove properties from it.
/// </summary>
[TestMethod]
[ExpectedException(typeof(NotSupportedException))]
public void TestRemove_ICollection_ThrowsException()
{
object instance = new object();
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(instance);
collection.Remove(new KeyValuePair<string, object>("Name", "Whatever"));
}
#endregion
#region TryGetValue
/// <summary>
/// If we try to get the value for a property that doesn't exist, false should returned and object set to null.
/// </summary>
[TestMethod]
public void TestTryGetValue_NoSuchProperty_ReturnsFalse()
{
var instance = new { };
PropertyDictionary dictionary = new PropertyDictionary(instance);
object value;
bool result = dictionary.TryGetValue("Name", out value);
Assert.IsFalse(result, "The property should not have been found.");
Assert.IsNull(value, "The value should have been null.");
}
/// <summary>
/// If we try to get the value for a property that doesn't exist, false should returned and object set to null.
/// </summary>
[TestMethod]
public void TestTryGetValue_PropertyExists_ReturnsTrue()
{
var instance = new
{
Name = "Test"
};
PropertyDictionary dictionary = new PropertyDictionary(instance);
object value;
bool result = dictionary.TryGetValue("Name", out value);
Assert.IsTrue(result, "The property should have been found.");
Assert.AreEqual(instance.Name, value, "The value should have equaled the wrapped property value.");
}
#endregion
#region Values
/// <summary>
/// We should be able to get the value of all of the properties.
/// </summary>
[TestMethod]
public void TestValues_GetsValues()
{
var instance = new
{
Name = "Bob",
Age = 23
};
PropertyDictionary dictionary = new PropertyDictionary(instance);
ICollection<object> values = dictionary.Values;
Assert.AreEqual(2, values.Count, "The wrong number of values were returned.");
Assert.IsTrue(values.Contains("Bob"), "The value for Name was not found.");
Assert.IsTrue(values.Contains(23), "The value for Age was not found.");
}
#endregion
#region Indexer
/// <summary>
/// If we try to retrieve the value for a property that does not exist, an exception
/// should be thrown.
/// </summary>
[TestMethod]
[ExpectedException(typeof(KeyNotFoundException))]
public void TestIndexer_Getter_NoSuchProperty_ThrowsException()
{
object instance = new object();
PropertyDictionary dictionary = new PropertyDictionary(instance);
object value = dictionary["Name"];
}
/// <summary>
/// If we try to get a value for a property that exists, the value should
/// be returned.
/// </summary>
[TestMethod]
public void TestIndexer_Getter_PropertyExists_ReturnsValue()
{
var instance = new
{
Name = "Bob"
};
PropertyDictionary dictionary = new PropertyDictionary(instance);
object value = dictionary["Name"];
Assert.AreSame(instance.Name, value, "The wrong value was returned.");
}
/// <summary>
/// If we try to set the value for a property, an exception should be thrown.
/// </summary>
[TestMethod]
[ExpectedException(typeof(NotSupportedException))]
public void TestIndexer_Setter_ThrowsException()
{
PropertyDictionary dictionary = new PropertyDictionary(new { Name = 123 });
dictionary["Name"] = 123;
}
#endregion
#region Clear
/// <summary>
/// Since the dictionary is just a wrapper, Clear will simply throw an exception.
/// </summary>
[TestMethod]
[ExpectedException(typeof(NotSupportedException))]
public void TestClear_ThrowsException()
{
object instance = new object();
ICollection<KeyValuePair<string, object>> dictionary = new PropertyDictionary(instance);
dictionary.Clear();
}
#endregion
#region Contains
/// <summary>
/// Contains should find the key/value pair if both the key and value are equal.
/// </summary>
[TestMethod]
public void TestContains_Explicit_PairExists_ReturnsTrue()
{
var person = new
{
Name = "Bob"
};
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(person);
bool contains = collection.Contains(new KeyValuePair<string, object>("Name", "Bob"));
Assert.IsTrue(contains, "Did not find the pair.");
}
/// <summary>
/// Contains should not find the key/value pair if the keys are not equal.
/// </summary>
[TestMethod]
public void TestContains_Explicit_KeyDoesNotMatch_ReturnsFalse()
{
var person = new
{
Name = "Bob"
};
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(person);
bool contains = collection.Contains(new KeyValuePair<string, object>("Age", "Bob"));
Assert.IsFalse(contains, "The pair should not have been found.");
}
/// <summary>
/// Contains should not find the key/value pair if the values are not equal.
/// </summary>
[TestMethod]
public void TestContains_Explicit_ValueDoesNotMatch_ReturnsFalse()
{
var person = new
{
Name = "Bob"
};
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(person);
bool contains = collection.Contains(new KeyValuePair<string, object>("Name", "Sally"));
Assert.IsFalse(contains, "The pair should not have been found.");
}
#endregion
#region CopyTo
/// <summary>
/// CopyTo should copy the key/value pairs to an array, assuming there is enough room.
/// </summary>
[TestMethod]
public void TestCopyTo_Explicit()
{
var instance = new
{
Name = "Bob",
Age = 23
};
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(instance);
KeyValuePair<string, object>[] array = new KeyValuePair<string, object>[collection.Count];
int arrayIndex = 0;
collection.CopyTo(array, arrayIndex);
Assert.IsTrue(array.Contains(new KeyValuePair<string, object>("Name", "Bob")), "The name property was not found.");
Assert.IsTrue(array.Contains(new KeyValuePair<string, object>("Age", 23)), "The age property was not found.");
}
#endregion
#region GetEnumerator
/// <summary>
/// All the items should be enumerated in the dictionary.
/// </summary>
[TestMethod]
public void TestGetEnumerator_EnumeratesAllItems()
{
var instance = new
{
Name = "Bob",
Age = 23
};
IEnumerable<KeyValuePair<string, object>> dictionary = new PropertyDictionary(instance);
Assert.IsTrue(enumerate(dictionary).Contains(new KeyValuePair<string, object>("Name", "Bob")), "The first pair was not present.");
Assert.IsTrue(enumerate(dictionary).Contains(new KeyValuePair<string, object>("Age", 23)), "The second pair was not present.");
}
private static IEnumerable<T> enumerate<T>(IEnumerable<T> enumerable)
{
List<T> items = new List<T>();
foreach (T item in enumerable)
{
items.Add(item);
}
return items;
}
/// <summary>
/// All the items should be enumerated in the dictionary.
/// </summary>
[TestMethod]
public void TestGetEnumerator_Explicit_EnumeratesAllItems()
{
var instance = new
{
Name = "Bob",
Age = 23
};
IEnumerable dictionary = new PropertyDictionary(instance);
Assert.IsTrue(enumerate(dictionary).Cast<KeyValuePair<string, object>>().Contains(new KeyValuePair<string, object>("Name", "Bob")), "The first pair was not present.");
Assert.IsTrue(enumerate(dictionary).Cast<KeyValuePair<string, object>>().Contains(new KeyValuePair<string, object>("Age", 23)), "The second pair was not present.");
}
private static IEnumerable enumerate(IEnumerable enumerable)
{
ArrayList items = new ArrayList();
foreach (object item in enumerable)
{
items.Add(item);
}
return items;
}
#endregion
}
}

View File

@ -36,9 +36,6 @@
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" /> <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies"> <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
@ -47,8 +44,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="FormatterTester.cs" />
<Compile Include="PropertyDictionaryTester.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\mustache-sharp\mustache-sharp.csproj"> <ProjectReference Include="..\mustache-sharp\mustache-sharp.csproj">

View File

@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace mustache
{
internal sealed class CompoundBuilder : IBuilder
{
private readonly List<IBuilder> builders;
public CompoundBuilder()
{
builders = new List<IBuilder>();
}
public void AddBuilder(IBuilder builder)
{
builders.Add(builder);
}
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
{
foreach (IBuilder builder in builders)
{
builder.Build(scope, output, provider);
}
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace mustache
{
internal sealed class CompoundGenerator : IGenerator
{
private readonly List<IGenerator> _generators;
public CompoundGenerator()
{
_generators = new List<IGenerator>();
}
public void AddGenerator(StaticGenerator generator)
{
_generators.Add(generator);
}
string IGenerator.GetText(object source)
{
StringBuilder builder = new StringBuilder();
foreach (IGenerator generator in _generators)
{
builder.Append(generator.GetText(source));
}
string innerText = builder.ToString();
// TODO - process with tag's custom handler
return innerText;
}
}
}

View File

@ -1,48 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace mustache
{
internal sealed class EachBuilder : IBuilder
{
private readonly CompoundBuilder builder;
public EachBuilder()
{
builder = new CompoundBuilder();
}
public string Key
{
get;
set;
}
public CompoundBuilder Builder
{
get { return builder; }
}
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
{
object value = scope.Find(Key);
IEnumerable enumerable = value as IEnumerable;
if (enumerable == null)
{
return;
}
foreach (object item in enumerable)
{
IDictionary<string, object> lookup = item as IDictionary<string, object>;
if (lookup == null)
{
lookup = new PropertyDictionary(item);
}
Scope itemScope = scope.CreateChildScope(item);
builder.Build(itemScope, output, provider);
}
}
}
}

View File

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
namespace mustache
{
/// <summary>
/// Parses a format string and returns the text generator.
/// </summary>
internal sealed class FormatParser
{
private const string key = @"[_\w][_\w\d]*";
private const string compoundKey = key + @"(\." + key + ")*";
/// <summary>
/// Initializes a new instance of a FormatParser.
/// </summary>
public FormatParser()
{
}
/// <summary>
/// Builds a text generator based on the given format.
/// </summary>
/// <param name="format">The format to parse.</param>
/// <returns>The text generator.</returns>
public IGenerator Build(string format)
{
TagDefinition definition = new TagDefinition("builtins");
definition.HasBody = true;
CompoundGenerator generator = new CompoundGenerator();
TagScope tagScope = new TagScope();
registerTags(definition, tagScope);
Match match = findNextTag(definition, format, 0);
buildCompoundGenerator(definition, tagScope, generator, format, 0, match);
return generator;
}
private static void registerTags(TagDefinition definition, TagScope scope)
{
foreach (TagDefinition childTag in definition.ChildTags)
{
scope.AddTag(childTag);
}
}
private static Match findNextTag(TagDefinition definition, string format, int formatIndex)
{
List<string> matches = new List<string>();
matches.Add(getKeyRegex());
matches.Add(getClosingTagRegex(definition));
matches.Add(getCommentTagRegex());
foreach (TagDefinition childTag in definition.ChildTags)
{
matches.Add(getTagRegex(childTag));
}
string match = "{{(" + String.Join("|", matches) + ")}}";
Regex regex = new Regex(match);
return regex.Match(format, formatIndex);
}
private static string getClosingTagRegex(TagDefinition definition)
{
StringBuilder regexBuilder = new StringBuilder();
regexBuilder.Append(@"(?<close>(/");
regexBuilder.Append(definition.Name);
regexBuilder.Append(@"\s*?))");
return regexBuilder.ToString();
}
private static string getCommentTagRegex()
{
return @"(?<comment>#!.*?)";
}
private static string getKeyRegex()
{
return @"((?<key>" + compoundKey + @")(,(?<alignment>(-)?[\d]+))?(:(?<format>.*?))?)";
}
private static string getTagRegex(TagDefinition definition)
{
StringBuilder regexBuilder = new StringBuilder();
regexBuilder.Append(@"(?<open>(#(?<name>");
regexBuilder.Append(definition.Name);
regexBuilder.Append(@")");
foreach (TagParameter parameter in definition.Parameters)
{
regexBuilder.Append(@"\s+?");
regexBuilder.Append(@"(?<argument>");
regexBuilder.Append(compoundKey);
regexBuilder.Append(@")");
}
regexBuilder.Append(@"\s*?))");
return regexBuilder.ToString();
}
private static int buildCompoundGenerator(TagDefinition tagDefinition, TagScope scope, CompoundGenerator generator, string format, int formatIndex, Match match)
{
bool done = false;
while (!done)
{
string leading = format.Substring(formatIndex, match.Index - formatIndex);
formatIndex = match.Index + match.Length;
if (match.Groups["comment"].Success)
{
// TODO - process comment
}
else if (match.Groups["close"].Success)
{
// TODO - process closing tag
done = true;
}
else if (match.Groups["open"].Success)
{
string tagName = match.Groups["name"].Value;
TagDefinition nextDefinition = scope.Find(tagName);
if (nextDefinition == null)
{
// TODO - handle missing tag definition
}
if (nextDefinition.HasBody)
{
CompoundGenerator nextGenerator = new CompoundGenerator();
TagScope nextScope = new TagScope(scope);
registerTags(nextDefinition, nextScope);
Match nextMatch = findNextTag(nextDefinition, format, formatIndex);
formatIndex = buildCompoundGenerator(nextDefinition, nextScope, nextGenerator, format, formatIndex, nextMatch);
// TODO - grab the generated text and parameters and pass it to the tag's processor
// TODO - a parameter can be a key or a default value
}
else
{
// TODO - grab all of the parameters and pass them to the tag's generator
// TODO - a parameter can be a key or a default value
}
}
else if (match.Groups["key"].Success)
{
string alignment = match.Groups["alignment"].Value;
string formatting = match.Groups["format"].Value;
// TODO - create a key generator
}
}
return formatIndex;
}
}
}

View File

@ -1,303 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using mustache.Properties;
namespace mustache
{
/// <summary>
/// Allows for the generation of a string based on formatted template.
/// </summary>
public sealed class Formatter
{
private readonly CompoundBuilder builder;
/// <summary>
/// Initializes a new instance of a Formatter using the given format string.
/// </summary>
/// <param name="format">The string containing the placeholders to use as a template.</param>
/// <exception cref="System.ArgumentNullException">The format string is null.</exception>
/// <exception cref="System.FormatException">The format string is invald.</exception>
public Formatter(string format)
{
if (format == null)
{
throw new ArgumentNullException("format");
}
builder = new CompoundBuilder();
List<string> names = new List<string>();
const string key = @"[_\w][_\w\d]*";
const string compoundKey = key + @"(\." + key + ")*";
const string openIfMatch = @"(?<open_if>(#if\s+?" + compoundKey + @"\s*?))";
const string elifMatch = @"(?<elif>(#elif\s+?" + compoundKey + @"\s*?))";
const string elseMatch = @"(?<else>(#else\s*?))";
const string closeIfMatch = @"(?<close_if>(/if\s*?))";
const string openEachMatch = @"(?<open_each>(#each\s+?" + compoundKey + @"\s*?))";
const string closeEachMatch = @"(?<close_each>(/each\s*?))";
const string openWithMatch = @"(?<open_with>(#with\s+?" + compoundKey + @"\s*?))";
const string closeWithMatch = @"(?<close_with>(/with\s*?))";
const string commentMatch = @"(?<comment>#!.*?)";
const string keyMatch = @"((?<key>" + compoundKey + @")(,(?<alignment>(-)?[\d]+))?(:(?<format>.*?))?)";
const string match = "{{(" + openIfMatch + "|"
+ elifMatch + "|"
+ elseMatch + "|"
+ closeIfMatch + "|"
+ openEachMatch + "|"
+ closeEachMatch + "|"
+ openWithMatch + "|"
+ closeWithMatch + "|"
+ commentMatch + "|"
+ keyMatch + ")}}";
Regex formatFinder = new Regex(match, RegexOptions.Compiled);
List<Match> matches = formatFinder.Matches(format).Cast<Match>().ToList();
using (IEnumerator<Match> matchEnumerator = matches.GetEnumerator())
{
Trimmer trimmer = new Trimmer();
int formatIndex = buildCompoundBuilder(builder, trimmer, format, 0, matchEnumerator);
StaticBuilder trailingBuilder = new StaticBuilder();
string value = format.Substring(formatIndex);
TagAttributes attributes = new TagAttributes() { Type = TagType.None, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
}
}
/// <summary>
/// Substitutes the placeholders in the format string with the values found in the object.
/// </summary>
/// <param name="format">The string containing the placeholders to use as a template.</param>
/// <param name="value">The object to use to replace the placeholders.</param>
/// <returns>The format string with the placeholders substituted for by the object values.</returns>
/// <exception cref="System.ArgumentNullException">The format string is null.</exception>
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A property was not found in the value.</exception>
public static string Format(string format, object value)
{
Formatter formatter = new Formatter(format);
return formatter.Format(value);
}
/// <summary>
/// Substitutes the placeholders in the format string with the values found in the object.
/// </summary>
/// <param name="provider">The format provider to use -or- null to use the current culture.</param>
/// <param name="format">The string containing the placeholders to use as a template.</param>
/// <param name="value">The object to use to replace the placeholders.</param>
/// <returns>The format string with the placeholders substituted for by the object values.</returns>
/// <exception cref="System.ArgumentNullException">The format string is null.</exception>
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A property was not found in the value.</exception>
public static string Format(IFormatProvider provider, string format, object value)
{
Formatter formatter = new Formatter(format);
return formatter.Format(provider, value);
}
/// <summary>
/// Substitutes the placeholders in the format string with the values found in the given object.
/// </summary>
/// <param name="value">The object to use to replace the placeholders.</param>
/// <returns>The format string with the placeholders substituted for by the lookup values.</returns>
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A property was not found in the object.</exception>
/// <remarks>A null value will be replaced with an empty string.</remarks>
public string Format(object value)
{
return format(CultureInfo.CurrentCulture, value);
}
/// <summary>
/// Substitutes the placeholders in the format string with the values found in the given object.
/// </summary>
/// <param name="provider">The format provider to use -or- null to use the current culture.</param>
/// <param name="value">The object to use to replace the placeholders.</param>
/// <returns>The format string with the placeholders substituted for by the lookup values.</returns>
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A property was not found in the object.</exception>
/// <remarks>A null value will be replaced with an empty string.</remarks>
public string Format(IFormatProvider provider, object value)
{
if (provider == null)
{
provider = CultureInfo.CurrentCulture;
}
return format(provider, value);
}
private static int buildCompoundBuilder(CompoundBuilder builder, Trimmer trimmer, string format, int formatIndex, IEnumerator<Match> matches)
{
while (matches.MoveNext())
{
Match match = matches.Current;
string value = format.Substring(formatIndex, match.Index - formatIndex);
formatIndex = match.Index + match.Length;
Group keyGroup = match.Groups["key"];
if (keyGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Singleton, IsOutput = true };
trimmer.AddStaticBuilder(builder, attributes, value);
Group alignmentGroup = match.Groups["alignment"];
Group formatGroup = match.Groups["format"];
KeyBuilder keyBuilder = new KeyBuilder()
{
Key = keyGroup.Value,
Alignment = alignmentGroup.Value,
Format = formatGroup.Value,
};
builder.AddBuilder(keyBuilder);
continue;
}
Group openIfGroup = match.Groups["open_if"];
if (openIfGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Header, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
IfBuilder ifBuilder = new IfBuilder();
ifBuilder.Key = openIfGroup.Value.Substring(4).Trim();
formatIndex = buildIfBuilder(ifBuilder, true, trimmer, format, formatIndex, matches);
builder.AddBuilder(ifBuilder);
continue;
}
Group openEachGroup = match.Groups["open_each"];
if (openEachGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Header, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
EachBuilder eachBuilder = new EachBuilder();
eachBuilder.Key = openEachGroup.Value.Substring(6).Trim();
formatIndex = buildEachBuilder(eachBuilder, trimmer, format, formatIndex, matches);
builder.AddBuilder(eachBuilder);
continue;
}
Group openWithGroup = match.Groups["open_with"];
if (openWithGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Header, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
WithBuilder withBuilder = new WithBuilder();
withBuilder.Key = openWithGroup.Value.Substring(6).Trim();
formatIndex = buildWithBuilder(withBuilder, trimmer, format, formatIndex, matches);
builder.AddBuilder(withBuilder);
continue;
}
Group commentGroup = match.Groups["comment"];
if (commentGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Singleton, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
continue;
}
Group elifGroup = match.Groups["elif"];
if (elifGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Singleton, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
break;
}
Group elseGroup = match.Groups["else"];
if (elseGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Singleton, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
break;
}
Group closeIfGroup = match.Groups["close_if"];
if (closeIfGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Footer, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
break;
}
Group closeEachGroup = match.Groups["close_each"];
if (closeEachGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Footer, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
break;
}
Group closeWithGroup = match.Groups["close_with"];
if (closeWithGroup.Success)
{
TagAttributes attributes = new TagAttributes() { Type = TagType.Footer, IsOutput = false };
trimmer.AddStaticBuilder(builder, attributes, value);
break;
}
}
return formatIndex;
}
private static int buildIfBuilder(IfBuilder builder, bool expectClosingTag, Trimmer trimmer, string format, int formatIndex, IEnumerator<Match> matches)
{
formatIndex = buildCompoundBuilder(builder.TrueBuilder, trimmer, format, formatIndex, matches);
Match match = matches.Current;
if (match != null)
{
Group elifGroup = match.Groups["elif"];
if (elifGroup.Success)
{
IfBuilder elifBuilder = new IfBuilder();
elifBuilder.Key = elifGroup.Value.Substring(6).Trim();
formatIndex = buildIfBuilder(elifBuilder, false, trimmer, format, formatIndex, matches);
builder.FalseBuilder.AddBuilder(elifBuilder);
}
else
{
Group elseGroup = match.Groups["else"];
if (elseGroup.Success)
{
formatIndex = buildCompoundBuilder(builder.FalseBuilder, trimmer, format, formatIndex, matches);
}
}
}
if (expectClosingTag)
{
Match closingMatch = matches.Current;
checkClosingTag(closingMatch, "close_if", "if");
}
return formatIndex;
}
private static int buildEachBuilder(EachBuilder builder, Trimmer trimmer, string format, int formatIndex, IEnumerator<Match> matches)
{
formatIndex = buildCompoundBuilder(builder.Builder, trimmer, format, formatIndex, matches);
Match closingMatch = matches.Current;
checkClosingTag(closingMatch, "close_each", "each");
return formatIndex;
}
private static int buildWithBuilder(WithBuilder builder, Trimmer trimmer, string format, int formatIndex, IEnumerator<Match> matches)
{
formatIndex = buildCompoundBuilder(builder.Builder, trimmer, format, formatIndex, matches);
Match closingMatch = matches.Current;
checkClosingTag(closingMatch, "close_with", "with");
return formatIndex;
}
private static void checkClosingTag(Match match, string expectedTag, string openingTag)
{
if (match == null || !match.Groups[expectedTag].Success)
{
string errorMessage = String.Format(CultureInfo.CurrentCulture, Resources.MissingClosingTag, openingTag);
throw new FormatException(errorMessage);
}
}
private string format(IFormatProvider provider, object topLevel)
{
Scope scope = new Scope(topLevel);
StringBuilder output = new StringBuilder();
builder.Build(scope, output, provider);
return output.ToString();
}
}
}

View File

@ -1,10 +0,0 @@
using System;
using System.Text;
namespace mustache
{
internal interface IBuilder
{
void Build(Scope scope, StringBuilder output, IFormatProvider provider);
}
}

View File

@ -0,0 +1,17 @@
using System;
namespace mustache
{
/// <summary>
/// Applies the values of an object to the format plan, generating a string.
/// </summary>
internal interface IGenerator
{
/// <summary>
/// Generates the text when the values of the given object are applied to the format plan.
/// </summary>
/// <param name="source">The object whose values should be used to generate the text.</param>
/// <returns>The generated text.</returns>
string GetText(object source);
}
}

View File

@ -1,75 +0,0 @@
using System;
using System.Collections;
using System.Linq;
using System.Text;
namespace mustache
{
internal sealed class IfBuilder : IBuilder
{
private readonly CompoundBuilder trueBuilder;
private readonly CompoundBuilder falseBuilder;
public IfBuilder()
{
trueBuilder = new CompoundBuilder();
falseBuilder = new CompoundBuilder();
}
public string Key
{
get;
set;
}
public CompoundBuilder TrueBuilder
{
get { return trueBuilder; }
}
public CompoundBuilder FalseBuilder
{
get { return falseBuilder; }
}
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
{
object value = scope.Find(Key);
bool truthyness = getTruthyness(value);
if (truthyness)
{
trueBuilder.Build(scope, output, provider);
}
else
{
falseBuilder.Build(scope, output, provider);
}
}
private bool getTruthyness(object value)
{
if (value == null)
{
return false;
}
IEnumerable enumerable = value as IEnumerable;
if (enumerable != null)
{
return enumerable.Cast<object>().Any();
}
if (value is Char)
{
return (Char)value != '\0';
}
try
{
decimal number = (decimal)Convert.ChangeType(value, typeof(decimal));
return number != 0.0m;
}
catch
{
}
return true;
}
}
}

View File

@ -1,50 +0,0 @@
using System;
using System.Text;
namespace mustache
{
internal sealed class KeyBuilder : IBuilder
{
public KeyBuilder()
{
}
public string Key
{
get;
set;
}
public string Alignment
{
get;
set;
}
public string Format
{
get;
set;
}
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
{
object value = scope.Find(Key);
StringBuilder format = new StringBuilder();
format.Append("{");
format.Append("0");
if (!String.IsNullOrWhiteSpace(Alignment))
{
format.Append(",");
format.Append(Alignment);
}
if (!String.IsNullOrWhiteSpace(Format))
{
format.Append(":");
format.Append(Format);
}
format.Append("}");
output.AppendFormat(provider, format.ToString(), value);
}
}
}

View File

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using mustache.Properties;
namespace mustache
{
/// <summary>
/// Represents a scope of keys.
/// </summary>
public sealed class KeyScope
{
private readonly object _source;
private readonly KeyScope _parent;
/// <summary>
/// Initializes a new instance of a KeyScope.
/// </summary>
/// <param name="source">The object to search for keys in.</param>
internal KeyScope(object source)
: this(source, null)
{
}
/// <summary>
/// Initializes a new instance of a KeyScope.
/// </summary>
/// <param name="source">The object to search for keys in.</param>
/// <param name="parent">The parent scope to search in if the value is not found.</param>
internal KeyScope(object source, KeyScope parent)
{
_parent = parent;
_source = source;
}
/// <summary>
/// Creates a child scope that searches for keys in the given object.
/// </summary>
/// <param name="source">The object to search for keys in.</param>
/// <returns>The new child scope.</returns>
internal KeyScope CreateChildScope(object source)
{
KeyScope scope = new KeyScope(source, this);
return scope;
}
/// <summary>
/// Attempts to find the value associated with the key with given name.
/// </summary>
/// <param name="name">The name of the key.</param>
/// <returns>The value associated with the key with the given name.</returns>
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A key with the given name could not be found.</exception>
public object Find(string name)
{
string[] names = name.Split('.');
string member = names[0];
object nextLevel = _source;
if (member != "this")
{
nextLevel = find(member);
}
for (int index = 1; index < names.Length; ++index)
{
IDictionary<string, object> context = toLookup(nextLevel);
member = names[index];
nextLevel = context[member];
}
return nextLevel;
}
private object find(string name)
{
IDictionary<string, object> lookup = toLookup(_source);
if (lookup.ContainsKey(name))
{
return lookup[name];
}
if (_parent == null)
{
string message = String.Format(CultureInfo.CurrentCulture, Resources.KeyNotFound, name);
throw new KeyNotFoundException(message);
}
return _parent.find(name);
}
private static IDictionary<string, object> toLookup(object value)
{
IDictionary<string, object> lookup = value as IDictionary<string, object>;
if (lookup == null)
{
lookup = new PropertyDictionary(value);
}
return lookup;
}
}
}

View File

@ -61,21 +61,39 @@ namespace mustache.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to A key or property was not found with the given name.. /// Looks up a localized string similar to An attempt was made to define a parameter with a null or an invalid identifier..
/// </summary>
internal static string BlankParameterName {
get {
return ResourceManager.GetString("BlankParameterName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An attempt was made to define a tag with a null or an invalid identifier..
/// </summary>
internal static string BlankTagName {
get {
return ResourceManager.GetString("BlankTagName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A parameter with the same name already exists within the tag..
/// </summary>
internal static string DuplicateParameter {
get {
return ResourceManager.GetString("DuplicateParameter", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The key {0} could not be found..
/// </summary> /// </summary>
internal static string KeyNotFound { internal static string KeyNotFound {
get { get {
return ResourceManager.GetString("KeyNotFound", resourceCulture); return ResourceManager.GetString("KeyNotFound", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to A matching closing tag was not found for the {0} tag..
/// </summary>
internal static string MissingClosingTag {
get {
return ResourceManager.GetString("MissingClosingTag", resourceCulture);
}
}
} }
} }

View File

@ -117,10 +117,16 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="KeyNotFound" xml:space="preserve"> <data name="BlankParameterName" xml:space="preserve">
<value>A key or property was not found with the given name.</value> <value>An attempt was made to define a parameter with a null or an invalid identifier.</value>
</data> </data>
<data name="MissingClosingTag" xml:space="preserve"> <data name="BlankTagName" xml:space="preserve">
<value>A matching closing tag was not found for the {0} tag.</value> <value>An attempt was made to define a tag with a null or an invalid identifier.</value>
</data>
<data name="DuplicateParameter" xml:space="preserve">
<value>A parameter with the same name already exists within the tag.</value>
</data>
<data name="KeyNotFound" xml:space="preserve">
<value>The key {0} could not be found.</value>
</data> </data>
</root> </root>

View File

@ -0,0 +1,22 @@
using System;
using System.Text.RegularExpressions;
namespace mustache
{
/// <summary>
/// Provides utility methods that require regular expressions.
/// </summary>
public static class RegexHelper
{
/// <summary>
/// Determines whether the given name is a legal identifier.
/// </summary>
/// <param name="name">The name to check.</param>
/// <returns>True if the name is a legal identifier; otherwise, false.</returns>
public static bool IsValidIdentifier(string name)
{
Regex regex = new Regex(@"^[_\w][_\w\d]*$");
return regex.IsMatch(name);
}
}
}

View File

@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using mustache.Properties;
namespace mustache
{
internal sealed class Scope
{
private readonly object topLevel;
private Scope parent;
public Scope(object topLevel)
{
parent = null;
this.topLevel = topLevel;
}
public Scope CreateChildScope(object topLevel)
{
Scope scope = new Scope(topLevel);
scope.parent = this;
return scope;
}
public object Find(string name)
{
string[] names = name.Split('.');
string member = names[0];
object nextLevel = topLevel;
if (member != "this")
{
nextLevel = find(member);
}
for (int index = 1; index < names.Length; ++index)
{
IDictionary<string, object> context = toLookup(nextLevel);
member = names[index];
nextLevel = context[member];
}
return nextLevel;
}
private object find(string name)
{
IDictionary<string, object> lookup = toLookup(topLevel);
if (lookup.ContainsKey(name))
{
return lookup[name];
}
if (parent == null)
{
string message = String.Format(Resources.KeyNotFound, name);
throw new KeyNotFoundException(message);
}
return parent.find(name);
}
private static IDictionary<string, object> toLookup(object value)
{
IDictionary<string, object> lookup = value as IDictionary<string, object>;
if (lookup == null)
{
lookup = new PropertyDictionary(value);
}
return lookup;
}
}
}

View File

@ -1,23 +0,0 @@
using System;
using System.Text;
namespace mustache
{
internal sealed class StaticBuilder : IBuilder
{
public StaticBuilder()
{
}
public string Value
{
get;
set;
}
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
{
output.Append(Value);
}
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace mustache
{
internal sealed class StaticGenerator : IGenerator
{
private readonly string _value;
public StaticGenerator(string value)
{
_value = value;
}
string IGenerator.GetText(object source)
{
return _value;
}
}
}

View File

@ -1,23 +0,0 @@
using System;
namespace mustache
{
internal sealed class TagAttributes
{
public TagAttributes()
{
}
public TagType Type
{
get;
set;
}
public bool IsOutput
{
get;
set;
}
}
}

View File

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using mustache.Properties;
namespace mustache
{
/// <summary>
/// Defines the attributes of a custom tag.
/// </summary>
public sealed class TagDefinition
{
private readonly string _tagName;
private readonly List<TagParameter> _parameters;
private readonly List<TagDefinition> _childTagDefinitions;
private TagParameter _scopeParameter;
/// <summary>
/// Initializes a new instance of a TagDefinition.
/// </summary>
/// <param name="tagName">The name of the tag.</param>
/// <exception cref="System.ArgumentException">The name of the tag is null or blank.</exception>
public TagDefinition(string tagName)
{
if (!RegexHelper.IsValidIdentifier(tagName))
{
throw new ArgumentException(Resources.BlankTagName, "tagName");
}
_tagName = tagName;
_parameters = new List<TagParameter>();
_childTagDefinitions = new List<TagDefinition>();
}
/// <summary>
/// Gets the name of the tag.
/// </summary>
public string Name
{
get { return _tagName; }
}
/// <summary>
/// Specifies that the tag expects the given parameter information.
/// </summary>
/// <param name="parameter">The parameter to add.</param>
/// <exception cref="System.ArgumentNullException">The parameter is null.</exception>
/// <exception cref="System.ArgumentException">A parameter with the same name already exists.</exception>
public void AddParameter(TagParameter parameter)
{
if (parameter == null)
{
throw new ArgumentNullException("parameter");
}
if (_parameters.Any(p => p.Name == parameter.Name))
{
throw new ArgumentException(Resources.DuplicateParameter, "parameter");
}
_parameters.Add(parameter);
if (parameter.IsScopeContext)
{
_scopeParameter = parameter;
}
}
/// <summary>
/// Gets the parameters that are defined for the tag.
/// </summary>
public IEnumerable<TagParameter> Parameters
{
get { return new ReadOnlyCollection<TagParameter>(_parameters); }
}
/// <summary>
/// Gets or sets whether the tag contains content.
/// </summary>
public bool HasBody
{
get;
set;
}
/// <summary>
/// Gets or sets whether the tag defines a new scope based on an argument.
/// </summary>
public bool IsScoped
{
get;
set;
}
/// <summary>
/// Specifies that the given tag is in scope within the current tag.
/// </summary>
/// <param name="childTag">The tag that is in scope within the current tag.</param>
public void AddChildTag(TagDefinition childTag)
{
if (childTag == null)
{
throw new ArgumentNullException("childTag");
}
_childTagDefinitions.Add(childTag);
}
/// <summary>
/// Gets the tags that are in scope within the current tag.
/// </summary>
public IEnumerable<TagDefinition> ChildTags
{
get { return new ReadOnlyCollection<TagDefinition>(_childTagDefinitions); }
}
}
}

View File

@ -0,0 +1,63 @@
using System;
using mustache.Properties;
namespace mustache
{
/// <summary>
/// Defines a parameter belonging to a custom tag.
/// </summary>
public sealed class TagParameter
{
private readonly string _name;
/// <summary>
/// Initializes a new instance of a TagParameter.
/// </summary>
/// <param name="parameterName">The name of the parameter.</param>
/// <exception cref="System.ArgumentException">The parameter name is null or an invalid identifier.</exception>
public TagParameter(string parameterName)
{
if (!RegexHelper.IsValidIdentifier(parameterName))
{
throw new ArgumentException(Resources.BlankParameterName, "parameterName");
}
_name = parameterName;
}
/// <summary>
/// Gets the name of the parameter.
/// </summary>
public string Name
{
get { return _name; }
}
/// <summary>
/// Gets or sets whether the parameter should be used to define the parameter.
/// </summary>
public bool IsScopeContext
{
get;
set;
}
/// <summary>
/// Gets or sets whether the field is required.
/// </summary>
public bool IsRequired
{
get;
set;
}
/// <summary>
/// Gets or sets the default value to use when an argument is not provided
/// for the parameter.
/// </summary>
public object DefaultValue
{
get;
set;
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace mustache
{
internal sealed class TagScope
{
private readonly TagScope _parent;
private readonly Dictionary<string, TagDefinition> _tagLookup;
public TagScope()
: this(null)
{
}
public TagScope(TagScope parent)
{
_parent = parent;
_tagLookup = new Dictionary<string, TagDefinition>();
}
public void AddTag(TagDefinition tagDefinition)
{
_tagLookup.Add(tagDefinition.Name, tagDefinition);
}
public TagDefinition Find(string tagName)
{
TagDefinition definition;
if (_tagLookup.TryGetValue(tagName, out definition))
{
return definition;
}
if (_parent == null)
{
return null;
}
return _parent.Find(tagName);
}
}
}

View File

@ -1,12 +0,0 @@
using System;
namespace mustache
{
internal enum TagType
{
None,
Singleton,
Header,
Footer,
}
}

View File

@ -1,57 +0,0 @@
using System;
namespace mustache
{
internal sealed class Trimmer
{
private bool hasHeader;
private bool hasFooter;
private bool hasTag;
private bool canTrim;
public Trimmer()
{
hasTag = false;
canTrim = true;
}
public void AddStaticBuilder(CompoundBuilder builder, TagAttributes attributes, string value)
{
string trimmed = value;
int newline = value.IndexOf(Environment.NewLine);
if (newline == -1)
{
canTrim &= String.IsNullOrWhiteSpace(value);
}
else
{
// finish processing the previous line
if (canTrim && hasTag && (!hasHeader || !hasFooter))
{
string lineEnd = trimmed.Substring(0, newline);
if (String.IsNullOrWhiteSpace(lineEnd))
{
trimmed = trimmed.Substring(newline + Environment.NewLine.Length);
}
}
// start processing the next line
hasTag = false;
hasHeader = false;
hasFooter = false;
int lastNewline = value.LastIndexOf(Environment.NewLine);
string lineStart = value.Substring(lastNewline + Environment.NewLine.Length);
canTrim = String.IsNullOrWhiteSpace(lineStart);
}
hasTag |= attributes.Type != TagType.None;
hasHeader |= attributes.Type == TagType.Header;
hasFooter |= hasHeader && attributes.Type == TagType.Footer;
canTrim &= !attributes.IsOutput;
if (trimmed.Length > 0)
{
StaticBuilder leading = new StaticBuilder();
leading.Value = trimmed;
builder.AddBuilder(leading);
}
}
}
}

View File

@ -1,33 +0,0 @@
using System;
using System.Text;
namespace mustache
{
internal sealed class WithBuilder : IBuilder
{
private readonly CompoundBuilder builder;
public WithBuilder()
{
builder = new CompoundBuilder();
}
public string Key
{
get;
set;
}
public CompoundBuilder Builder
{
get { return builder; }
}
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
{
object value = scope.Find(Key);
Scope valueScope = scope.CreateChildScope(value);
builder.Build(valueScope, output, provider);
}
}
}

View File

@ -32,16 +32,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="CompoundBuilder.cs" /> <Compile Include="CompoundGenerator.cs" />
<Compile Include="EachBuilder.cs" /> <Compile Include="FormatParser.cs" />
<Compile Include="Formatter.cs" /> <Compile Include="IGenerator.cs" />
<Compile Include="IBuilder.cs" />
<Compile Include="IfBuilder.cs" />
<Compile Include="KeyBuilder.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs"> <Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
@ -49,12 +44,12 @@
<DependentUpon>Resources.resx</DependentUpon> <DependentUpon>Resources.resx</DependentUpon>
</Compile> </Compile>
<Compile Include="PropertyDictionary.cs" /> <Compile Include="PropertyDictionary.cs" />
<Compile Include="Scope.cs" /> <Compile Include="RegexHelper.cs" />
<Compile Include="StaticBuilder.cs" /> <Compile Include="StaticGenerator.cs" />
<Compile Include="TagAttributes.cs" /> <Compile Include="TagDefinition.cs" />
<Compile Include="TagType.cs" /> <Compile Include="TagParameter.cs" />
<Compile Include="Trimmer.cs" /> <Compile Include="KeyScope.cs" />
<Compile Include="WithBuilder.cs" /> <Compile Include="TagScope.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx"> <EmbeddedResource Include="Properties\Resources.resx">