From f8628aaf86d9634b2f41f5a4973c4afdddd8cdea Mon Sep 17 00:00:00 2001 From: Travis Parks Date: Tue, 8 Jan 2013 21:33:53 -0500 Subject: [PATCH] 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. --- mustache-sharp.test/FormatterTester.cs | 524 ------------------ .../PropertyDictionaryTester.cs | 484 ---------------- .../mustache-sharp.test.csproj | 5 - mustache-sharp/CompoundBuilder.cs | 29 - mustache-sharp/CompoundGenerator.cs | 33 ++ mustache-sharp/EachBuilder.cs | 48 -- mustache-sharp/FormatParser.cs | 150 +++++ mustache-sharp/Formatter.cs | 303 ---------- mustache-sharp/IBuilder.cs | 10 - mustache-sharp/IGenerator.cs | 17 + mustache-sharp/IfBuilder.cs | 75 --- mustache-sharp/KeyBuilder.cs | 50 -- mustache-sharp/KeyScope.cs | 96 ++++ .../Properties/Resources.Designer.cs | 38 +- mustache-sharp/Properties/Resources.resx | 14 +- mustache-sharp/PropertyDictionary.cs | 4 +- mustache-sharp/RegexHelper.cs | 22 + mustache-sharp/Scope.cs | 68 --- mustache-sharp/StaticBuilder.cs | 23 - mustache-sharp/StaticGenerator.cs | 19 + mustache-sharp/TagAttributes.cs | 23 - mustache-sharp/TagDefinition.cs | 113 ++++ mustache-sharp/TagParameter.cs | 63 +++ mustache-sharp/TagScope.cs | 41 ++ mustache-sharp/TagType.cs | 12 - mustache-sharp/Trimmer.cs | 57 -- mustache-sharp/WithBuilder.cs | 33 -- mustache-sharp/mustache-sharp.csproj | 23 +- 28 files changed, 603 insertions(+), 1774 deletions(-) delete mode 100644 mustache-sharp.test/FormatterTester.cs delete mode 100644 mustache-sharp.test/PropertyDictionaryTester.cs delete mode 100644 mustache-sharp/CompoundBuilder.cs create mode 100644 mustache-sharp/CompoundGenerator.cs delete mode 100644 mustache-sharp/EachBuilder.cs create mode 100644 mustache-sharp/FormatParser.cs delete mode 100644 mustache-sharp/Formatter.cs delete mode 100644 mustache-sharp/IBuilder.cs create mode 100644 mustache-sharp/IGenerator.cs delete mode 100644 mustache-sharp/IfBuilder.cs delete mode 100644 mustache-sharp/KeyBuilder.cs create mode 100644 mustache-sharp/KeyScope.cs create mode 100644 mustache-sharp/RegexHelper.cs delete mode 100644 mustache-sharp/Scope.cs delete mode 100644 mustache-sharp/StaticBuilder.cs create mode 100644 mustache-sharp/StaticGenerator.cs delete mode 100644 mustache-sharp/TagAttributes.cs create mode 100644 mustache-sharp/TagDefinition.cs create mode 100644 mustache-sharp/TagParameter.cs create mode 100644 mustache-sharp/TagScope.cs delete mode 100644 mustache-sharp/TagType.cs delete mode 100644 mustache-sharp/Trimmer.cs delete mode 100644 mustache-sharp/WithBuilder.cs diff --git a/mustache-sharp.test/FormatterTester.cs b/mustache-sharp.test/FormatterTester.cs deleted file mode 100644 index 52d236e..0000000 --- a/mustache-sharp.test/FormatterTester.cs +++ /dev/null @@ -1,524 +0,0 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Collections.Generic; -using System.Globalization; - -namespace mustache.test -{ - /// - /// Tests the Formatter class. - /// - [TestClass] - public class FormatterTester - { - #region Real World Example - - /// - /// 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 (}}). - /// - [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() - { - { "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); - } - - /// - /// If we want to work with objects, rather than raw dictionaries, we can wrap the objects with - /// property dictionaries. - /// - [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); - } - - /// - /// We can the Formatter to print out a list of items following a format. - /// - [TestMethod] - public void TestFormatter_PrintList() - { - List values = new List() { 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); - } - - /// - /// We can include some text conditionally. - /// - [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); - } - - /// - /// Multiple cases can be handled using if/elif/else. - /// - [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); - } - - /// - /// We should be able to combine tags anyway we want. - /// - [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 - - /// - /// An exception should be thrown if the format string is null. - /// - [TestMethod] - [ExpectedException(typeof(ArgumentNullException))] - public void TestCtor_NullFormat_ThrowsException() - { - string format = null; - new Formatter(format); - } - - /// - /// If we try to replace a placeholder that we do not have a lookup key for, - /// an exception should be thrown. - /// - [TestMethod] - [ExpectedException(typeof(KeyNotFoundException))] - public void TestFormat_MissingKey_ThrowsException() - { - Formatter formatter = new Formatter("{{unknown}}"); - IDictionary lookup = new Dictionary(); - formatter.Format(lookup); - } - - /// - /// A format exception should be thrown if there is not a matching closing if tag. - /// - [TestMethod] - [ExpectedException(typeof(FormatException))] - public void TestFormat_MissingClosingIfTag_ThrowsException() - { - new Formatter("{{#if Bob}}Hello"); - } - - /// - /// A format exception should be thrown if the matching closing tag is wrong. - /// - [TestMethod] - [ExpectedException(typeof(FormatException))] - public void TestFormat_WrongClosingIfTag_ThrowsException() - { - new Formatter("{{#with this}}{{#if Bob}}Hello{{/with}}{{/if}}"); - } - - #endregion - - /// - /// If we specify a right alignment, the output should be aligned to the right. - /// - [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."); - } - - /// - /// If we specify a left alignment, the output should be aligned to the left. - /// - [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."); - } - - /// - /// If we try to format an empty string, an empty string should be returned. - /// - [TestMethod] - public void TestFormatter_EmptyFormat_ReturnsEmpty() - { - Formatter formatter = new Formatter(String.Empty); - Dictionary lookup = new Dictionary(); - string result = formatter.Format(lookup); - Assert.AreEqual(String.Empty, result, "The result should have been empty."); - } - - /// - /// If our format string is just a placeholder, than just the replacement value should be returned. - /// - [TestMethod] - public void TestFormatter_FormatIsSinglePlaceholder_ReturnsReplaced() - { - Formatter formatter = new Formatter("{{name}}"); - Dictionary lookup = new Dictionary() - { - { "name", "test" } - }; - string result = formatter.Format(lookup); - Assert.AreEqual("test", result, "The result was wrong."); - } - - /// - /// We should be able to put just about anything inside of a placeholder, but it will - /// not be treated like a placeholder. - /// - [TestMethod] - public void TestFormatter_PlaceholderContainsSpecialCharacters_ReturnsUnreplaced() - { - Formatter formatter = new Formatter("{{ \\_@#$%^ }1233 abc}}"); - Dictionary lookup = new Dictionary() - { - { " \\_@#$%^ }1233 abc", "test" } - }; - string result = formatter.Format(lookup); - Assert.AreEqual("{{ \\_@#$%^ }1233 abc}}", result, "The result was wrong."); - } - - /// - /// If a lookup value is null, it should be replaced with an empty string. - /// - [TestMethod] - public void TestFormatter_NullValue_ReplacesWithBlank() - { - Formatter formatter = new Formatter("These quotes should be empty '{{name}}'."); - Dictionary lookup = new Dictionary() - { - { "name", null } - }; - string result = formatter.Format(lookup); - Assert.AreEqual("These quotes should be empty ''.", result, "The result was wrong."); - } - - /// - /// If a replacement value contains a placeholder, it should NOT be evaluated. - /// - [TestMethod] - public void TestFormatter_ReplacementContainsPlaceholder_IgnoresPlaceholder() - { - Formatter formatter = new Formatter("The length of {{name}} is {{length}}."); - Dictionary lookup = new Dictionary() - { - { "name", "Bob" }, - { "length", "{{name}}" } - }; - string result = formatter.Format(lookup); - Assert.AreEqual("The length of Bob is {{name}}.", result, "The result was wrong."); - } - - /// - /// If we pass null to as the format provider to the Format function, - /// the current culture is used. - /// - [TestMethod] - public void TestFormatter_NullFormatter_UsesCurrentCulture() - { - string format = "{0:C}"; - Formatter formatter = new Formatter("{" + format + "}"); - string result = formatter.Format((IFormatProvider)null, new Dictionary() { { "0", 28.30m } }); - string expected = String.Format(CultureInfo.CurrentCulture, format, 28.30m); - Assert.AreEqual(expected, result, "The wrong format provider was used."); - } - - /// - /// If we put a tag on a line by itself, it shouldn't result in any whitespace. - /// - [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); - } - - /// - /// If a key is not found at the current level, it is looked for at the parent level. - /// - [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); - } - - /// - /// Null values are considered false by if statements. - /// - [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); - } - - /// - /// Empty collections are considered false by if statements. - /// - [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); - } - - /// - /// Non-empty collections are considered true by if statements. - /// - [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); - } - - /// - /// Null-char is considered false by if statements. - /// - [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); - } - - /// - /// Zero is considered false by if statements. - /// - [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); - } - - /// - /// Everything else is considered true by if statements. - /// - [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); - } - - /// - /// Instead of requiring deeply nested "with" statements, members - /// can be separated by dots. - /// - [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); - } - - /// - /// Keys should cause newlines to be respected, since they are considered content. - /// - [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); - } - - /// - /// If someone tries to loop on a non-enumerable, it should do nothing. - /// - [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); - } - - /// - /// If a tag header is on the same line as it's footer, the new-line should not be removed. - /// - [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); - } - - /// - /// If a tag header is on the same line as it's footer, the new-line should not be removed. - /// - [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); - } - } -} diff --git a/mustache-sharp.test/PropertyDictionaryTester.cs b/mustache-sharp.test/PropertyDictionaryTester.cs deleted file mode 100644 index 811505a..0000000 --- a/mustache-sharp.test/PropertyDictionaryTester.cs +++ /dev/null @@ -1,484 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace mustache.test -{ - /// - /// Tests the PropertyDictionary class. - /// - [TestClass] - public class PropertyDictionaryTester - { - #region Real World Example - - /// - /// 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. - /// - [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 - - /// - /// If we try to wrap null, an exception should be thrown. - /// - [TestMethod] - public void TestCtor_NullInstance_ThrowsException() - { - PropertyDictionary dictionary = new PropertyDictionary(null); - Assert.AreEqual(0, dictionary.Count); - } - - /// - /// We should be able to access the underlying object. - /// - [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> collection = dictionary; - Assert.IsTrue(collection.IsReadOnly, "The collection should not have been read-only."); - } - - #endregion - - #region Add - - /// - /// Since the dictionary is a simple wrapper around an object, we cannot add new properties. - /// - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void TestAdd_IDictionary_ThrowsException() - { - IDictionary dictionary = new PropertyDictionary(new object()); - dictionary.Add("Name", "Bob"); - } - - /// - /// Since the dictionary is a simple wrapper around an object, we cannot add new properties. - /// - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void TestAdd_ICollection_ThrowsException() - { - ICollection> collection = new PropertyDictionary(new object()); - collection.Add(new KeyValuePair("Name", "Bob")); - } - - #endregion - - #region ContainsKey - - /// - /// If the wrapped object has a property, the key should be found. - /// - [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."); - } - - /// - /// If the wrapped object does not have a property, the key should not be found. - /// - [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; } - } - - /// - /// We should be able to see properties defined in the base type. - /// - [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; } - } - - /// - /// We should not be able to see private properties. - /// - [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; } - } - - /// - /// We should not be able to see static properties. - /// - [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 - - /// - /// Keys should return the name of all of the property names in the object. - /// - [TestMethod] - public void TestKeys_GetsAllPropertyNames() - { - var person = new - { - Name = "Bob", - Age = 23 - }; - PropertyDictionary dictionary = new PropertyDictionary(person); - ICollection 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 - - /// - /// Since a property dictionary is just a wrapper around an object, we cannot remove properties from it. - /// - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void TestRemove_IDictionary_ThrowsException() - { - object instance = new object(); - IDictionary dictionary = new PropertyDictionary(instance); - dictionary.Remove("Name"); - } - - /// - /// Since a property dictionary is just a wrapper around an object, we cannot remove properties from it. - /// - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void TestRemove_ICollection_ThrowsException() - { - object instance = new object(); - ICollection> collection = new PropertyDictionary(instance); - collection.Remove(new KeyValuePair("Name", "Whatever")); - } - - #endregion - - #region TryGetValue - - /// - /// If we try to get the value for a property that doesn't exist, false should returned and object set to null. - /// - [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."); - } - - /// - /// If we try to get the value for a property that doesn't exist, false should returned and object set to null. - /// - [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 - - /// - /// We should be able to get the value of all of the properties. - /// - [TestMethod] - public void TestValues_GetsValues() - { - var instance = new - { - Name = "Bob", - Age = 23 - }; - PropertyDictionary dictionary = new PropertyDictionary(instance); - ICollection 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 - - /// - /// If we try to retrieve the value for a property that does not exist, an exception - /// should be thrown. - /// - [TestMethod] - [ExpectedException(typeof(KeyNotFoundException))] - public void TestIndexer_Getter_NoSuchProperty_ThrowsException() - { - object instance = new object(); - PropertyDictionary dictionary = new PropertyDictionary(instance); - object value = dictionary["Name"]; - } - - /// - /// If we try to get a value for a property that exists, the value should - /// be returned. - /// - [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."); - } - - /// - /// If we try to set the value for a property, an exception should be thrown. - /// - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void TestIndexer_Setter_ThrowsException() - { - PropertyDictionary dictionary = new PropertyDictionary(new { Name = 123 }); - dictionary["Name"] = 123; - } - - #endregion - - #region Clear - - /// - /// Since the dictionary is just a wrapper, Clear will simply throw an exception. - /// - [TestMethod] - [ExpectedException(typeof(NotSupportedException))] - public void TestClear_ThrowsException() - { - object instance = new object(); - ICollection> dictionary = new PropertyDictionary(instance); - dictionary.Clear(); - } - - #endregion - - #region Contains - - /// - /// Contains should find the key/value pair if both the key and value are equal. - /// - [TestMethod] - public void TestContains_Explicit_PairExists_ReturnsTrue() - { - var person = new - { - Name = "Bob" - }; - ICollection> collection = new PropertyDictionary(person); - bool contains = collection.Contains(new KeyValuePair("Name", "Bob")); - Assert.IsTrue(contains, "Did not find the pair."); - } - - /// - /// Contains should not find the key/value pair if the keys are not equal. - /// - [TestMethod] - public void TestContains_Explicit_KeyDoesNotMatch_ReturnsFalse() - { - var person = new - { - Name = "Bob" - }; - ICollection> collection = new PropertyDictionary(person); - bool contains = collection.Contains(new KeyValuePair("Age", "Bob")); - Assert.IsFalse(contains, "The pair should not have been found."); - } - - /// - /// Contains should not find the key/value pair if the values are not equal. - /// - [TestMethod] - public void TestContains_Explicit_ValueDoesNotMatch_ReturnsFalse() - { - var person = new - { - Name = "Bob" - }; - ICollection> collection = new PropertyDictionary(person); - bool contains = collection.Contains(new KeyValuePair("Name", "Sally")); - Assert.IsFalse(contains, "The pair should not have been found."); - } - - #endregion - - #region CopyTo - - /// - /// CopyTo should copy the key/value pairs to an array, assuming there is enough room. - /// - [TestMethod] - public void TestCopyTo_Explicit() - { - var instance = new - { - Name = "Bob", - Age = 23 - }; - ICollection> collection = new PropertyDictionary(instance); - - KeyValuePair[] array = new KeyValuePair[collection.Count]; - int arrayIndex = 0; - collection.CopyTo(array, arrayIndex); - - Assert.IsTrue(array.Contains(new KeyValuePair("Name", "Bob")), "The name property was not found."); - Assert.IsTrue(array.Contains(new KeyValuePair("Age", 23)), "The age property was not found."); - } - - #endregion - - #region GetEnumerator - - /// - /// All the items should be enumerated in the dictionary. - /// - [TestMethod] - public void TestGetEnumerator_EnumeratesAllItems() - { - var instance = new - { - Name = "Bob", - Age = 23 - }; - IEnumerable> dictionary = new PropertyDictionary(instance); - - Assert.IsTrue(enumerate(dictionary).Contains(new KeyValuePair("Name", "Bob")), "The first pair was not present."); - Assert.IsTrue(enumerate(dictionary).Contains(new KeyValuePair("Age", 23)), "The second pair was not present."); - } - - private static IEnumerable enumerate(IEnumerable enumerable) - { - List items = new List(); - foreach (T item in enumerable) - { - items.Add(item); - } - return items; - } - - /// - /// All the items should be enumerated in the dictionary. - /// - [TestMethod] - public void TestGetEnumerator_Explicit_EnumeratesAllItems() - { - var instance = new - { - Name = "Bob", - Age = 23 - }; - IEnumerable dictionary = new PropertyDictionary(instance); - - Assert.IsTrue(enumerate(dictionary).Cast>().Contains(new KeyValuePair("Name", "Bob")), "The first pair was not present."); - Assert.IsTrue(enumerate(dictionary).Cast>().Contains(new KeyValuePair("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 - } -} diff --git a/mustache-sharp.test/mustache-sharp.test.csproj b/mustache-sharp.test/mustache-sharp.test.csproj index 1bb5f88..afb4a59 100644 --- a/mustache-sharp.test/mustache-sharp.test.csproj +++ b/mustache-sharp.test/mustache-sharp.test.csproj @@ -36,9 +36,6 @@ - - 3.5 - @@ -47,8 +44,6 @@ - - diff --git a/mustache-sharp/CompoundBuilder.cs b/mustache-sharp/CompoundBuilder.cs deleted file mode 100644 index 7277946..0000000 --- a/mustache-sharp/CompoundBuilder.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace mustache -{ - internal sealed class CompoundBuilder : IBuilder - { - private readonly List builders; - - public CompoundBuilder() - { - builders = new List(); - } - - 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); - } - } - } -} diff --git a/mustache-sharp/CompoundGenerator.cs b/mustache-sharp/CompoundGenerator.cs new file mode 100644 index 0000000..8a3556c --- /dev/null +++ b/mustache-sharp/CompoundGenerator.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace mustache +{ + internal sealed class CompoundGenerator : IGenerator + { + private readonly List _generators; + + public CompoundGenerator() + { + _generators = new List(); + } + + 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; + } + } +} \ No newline at end of file diff --git a/mustache-sharp/EachBuilder.cs b/mustache-sharp/EachBuilder.cs deleted file mode 100644 index d2dcf2e..0000000 --- a/mustache-sharp/EachBuilder.cs +++ /dev/null @@ -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 lookup = item as IDictionary; - if (lookup == null) - { - lookup = new PropertyDictionary(item); - } - Scope itemScope = scope.CreateChildScope(item); - builder.Build(itemScope, output, provider); - } - } - } -} diff --git a/mustache-sharp/FormatParser.cs b/mustache-sharp/FormatParser.cs new file mode 100644 index 0000000..1966508 --- /dev/null +++ b/mustache-sharp/FormatParser.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace mustache +{ + /// + /// Parses a format string and returns the text generator. + /// + internal sealed class FormatParser + { + private const string key = @"[_\w][_\w\d]*"; + private const string compoundKey = key + @"(\." + key + ")*"; + + /// + /// Initializes a new instance of a FormatParser. + /// + public FormatParser() + { + } + + /// + /// Builds a text generator based on the given format. + /// + /// The format to parse. + /// The text generator. + 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 matches = new List(); + 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(@"(?(/"); + regexBuilder.Append(definition.Name); + regexBuilder.Append(@"\s*?))"); + return regexBuilder.ToString(); + } + + private static string getCommentTagRegex() + { + return @"(?#!.*?)"; + } + + private static string getKeyRegex() + { + return @"((?" + compoundKey + @")(,(?(-)?[\d]+))?(:(?.*?))?)"; + } + + private static string getTagRegex(TagDefinition definition) + { + StringBuilder regexBuilder = new StringBuilder(); + regexBuilder.Append(@"(?(#(?"); + regexBuilder.Append(definition.Name); + regexBuilder.Append(@")"); + foreach (TagParameter parameter in definition.Parameters) + { + regexBuilder.Append(@"\s+?"); + regexBuilder.Append(@"(?"); + 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; + } + } +} diff --git a/mustache-sharp/Formatter.cs b/mustache-sharp/Formatter.cs deleted file mode 100644 index 9a49c94..0000000 --- a/mustache-sharp/Formatter.cs +++ /dev/null @@ -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 -{ - /// - /// Allows for the generation of a string based on formatted template. - /// - public sealed class Formatter - { - private readonly CompoundBuilder builder; - - /// - /// Initializes a new instance of a Formatter using the given format string. - /// - /// The string containing the placeholders to use as a template. - /// The format string is null. - /// The format string is invald. - public Formatter(string format) - { - if (format == null) - { - throw new ArgumentNullException("format"); - } - builder = new CompoundBuilder(); - - List names = new List(); - const string key = @"[_\w][_\w\d]*"; - const string compoundKey = key + @"(\." + key + ")*"; - const string openIfMatch = @"(?(#if\s+?" + compoundKey + @"\s*?))"; - const string elifMatch = @"(?(#elif\s+?" + compoundKey + @"\s*?))"; - const string elseMatch = @"(?(#else\s*?))"; - const string closeIfMatch = @"(?(/if\s*?))"; - const string openEachMatch = @"(?(#each\s+?" + compoundKey + @"\s*?))"; - const string closeEachMatch = @"(?(/each\s*?))"; - const string openWithMatch = @"(?(#with\s+?" + compoundKey + @"\s*?))"; - const string closeWithMatch = @"(?(/with\s*?))"; - const string commentMatch = @"(?#!.*?)"; - const string keyMatch = @"((?" + compoundKey + @")(,(?(-)?[\d]+))?(:(?.*?))?)"; - const string match = "{{(" + openIfMatch + "|" - + elifMatch + "|" - + elseMatch + "|" - + closeIfMatch + "|" - + openEachMatch + "|" - + closeEachMatch + "|" - + openWithMatch + "|" - + closeWithMatch + "|" - + commentMatch + "|" - + keyMatch + ")}}"; - Regex formatFinder = new Regex(match, RegexOptions.Compiled); - List matches = formatFinder.Matches(format).Cast().ToList(); - using (IEnumerator 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); - } - } - - /// - /// Substitutes the placeholders in the format string with the values found in the object. - /// - /// The string containing the placeholders to use as a template. - /// The object to use to replace the placeholders. - /// The format string with the placeholders substituted for by the object values. - /// The format string is null. - /// A property was not found in the value. - public static string Format(string format, object value) - { - Formatter formatter = new Formatter(format); - return formatter.Format(value); - } - - /// - /// Substitutes the placeholders in the format string with the values found in the object. - /// - /// The format provider to use -or- null to use the current culture. - /// The string containing the placeholders to use as a template. - /// The object to use to replace the placeholders. - /// The format string with the placeholders substituted for by the object values. - /// The format string is null. - /// A property was not found in the value. - public static string Format(IFormatProvider provider, string format, object value) - { - Formatter formatter = new Formatter(format); - return formatter.Format(provider, value); - } - - /// - /// Substitutes the placeholders in the format string with the values found in the given object. - /// - /// The object to use to replace the placeholders. - /// The format string with the placeholders substituted for by the lookup values. - /// A property was not found in the object. - /// A null value will be replaced with an empty string. - public string Format(object value) - { - return format(CultureInfo.CurrentCulture, value); - } - - /// - /// Substitutes the placeholders in the format string with the values found in the given object. - /// - /// The format provider to use -or- null to use the current culture. - /// The object to use to replace the placeholders. - /// The format string with the placeholders substituted for by the lookup values. - /// A property was not found in the object. - /// A null value will be replaced with an empty string. - 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 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 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 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 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(); - } - } -} diff --git a/mustache-sharp/IBuilder.cs b/mustache-sharp/IBuilder.cs deleted file mode 100644 index 519bcc1..0000000 --- a/mustache-sharp/IBuilder.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Text; - -namespace mustache -{ - internal interface IBuilder - { - void Build(Scope scope, StringBuilder output, IFormatProvider provider); - } -} diff --git a/mustache-sharp/IGenerator.cs b/mustache-sharp/IGenerator.cs new file mode 100644 index 0000000..f155055 --- /dev/null +++ b/mustache-sharp/IGenerator.cs @@ -0,0 +1,17 @@ +using System; + +namespace mustache +{ + /// + /// Applies the values of an object to the format plan, generating a string. + /// + internal interface IGenerator + { + /// + /// Generates the text when the values of the given object are applied to the format plan. + /// + /// The object whose values should be used to generate the text. + /// The generated text. + string GetText(object source); + } +} diff --git a/mustache-sharp/IfBuilder.cs b/mustache-sharp/IfBuilder.cs deleted file mode 100644 index a861530..0000000 --- a/mustache-sharp/IfBuilder.cs +++ /dev/null @@ -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().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; - } - } -} diff --git a/mustache-sharp/KeyBuilder.cs b/mustache-sharp/KeyBuilder.cs deleted file mode 100644 index f5c8d9d..0000000 --- a/mustache-sharp/KeyBuilder.cs +++ /dev/null @@ -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); - } - } -} diff --git a/mustache-sharp/KeyScope.cs b/mustache-sharp/KeyScope.cs new file mode 100644 index 0000000..22ab489 --- /dev/null +++ b/mustache-sharp/KeyScope.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using mustache.Properties; + +namespace mustache +{ + /// + /// Represents a scope of keys. + /// + public sealed class KeyScope + { + private readonly object _source; + private readonly KeyScope _parent; + + /// + /// Initializes a new instance of a KeyScope. + /// + /// The object to search for keys in. + internal KeyScope(object source) + : this(source, null) + { + } + + /// + /// Initializes a new instance of a KeyScope. + /// + /// The object to search for keys in. + /// The parent scope to search in if the value is not found. + internal KeyScope(object source, KeyScope parent) + { + _parent = parent; + _source = source; + } + + /// + /// Creates a child scope that searches for keys in the given object. + /// + /// The object to search for keys in. + /// The new child scope. + internal KeyScope CreateChildScope(object source) + { + KeyScope scope = new KeyScope(source, this); + return scope; + } + + /// + /// Attempts to find the value associated with the key with given name. + /// + /// The name of the key. + /// The value associated with the key with the given name. + /// A key with the given name could not be found. + 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 context = toLookup(nextLevel); + member = names[index]; + nextLevel = context[member]; + } + return nextLevel; + } + + private object find(string name) + { + IDictionary 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 toLookup(object value) + { + IDictionary lookup = value as IDictionary; + if (lookup == null) + { + lookup = new PropertyDictionary(value); + } + return lookup; + } + } +} diff --git a/mustache-sharp/Properties/Resources.Designer.cs b/mustache-sharp/Properties/Resources.Designer.cs index 7ca007a..62c3781 100644 --- a/mustache-sharp/Properties/Resources.Designer.cs +++ b/mustache-sharp/Properties/Resources.Designer.cs @@ -61,21 +61,39 @@ namespace mustache.Properties { } /// - /// 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.. + /// + internal static string BlankParameterName { + get { + return ResourceManager.GetString("BlankParameterName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An attempt was made to define a tag with a null or an invalid identifier.. + /// + internal static string BlankTagName { + get { + return ResourceManager.GetString("BlankTagName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A parameter with the same name already exists within the tag.. + /// + internal static string DuplicateParameter { + get { + return ResourceManager.GetString("DuplicateParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The key {0} could not be found.. /// internal static string KeyNotFound { get { return ResourceManager.GetString("KeyNotFound", resourceCulture); } } - - /// - /// Looks up a localized string similar to A matching closing tag was not found for the {0} tag.. - /// - internal static string MissingClosingTag { - get { - return ResourceManager.GetString("MissingClosingTag", resourceCulture); - } - } } } diff --git a/mustache-sharp/Properties/Resources.resx b/mustache-sharp/Properties/Resources.resx index 4e0171f..6f8484a 100644 --- a/mustache-sharp/Properties/Resources.resx +++ b/mustache-sharp/Properties/Resources.resx @@ -117,10 +117,16 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - A key or property was not found with the given name. + + An attempt was made to define a parameter with a null or an invalid identifier. - - A matching closing tag was not found for the {0} tag. + + An attempt was made to define a tag with a null or an invalid identifier. + + + A parameter with the same name already exists within the tag. + + + The key {0} could not be found. \ No newline at end of file diff --git a/mustache-sharp/PropertyDictionary.cs b/mustache-sharp/PropertyDictionary.cs index 7de64ee..dc5264d 100644 --- a/mustache-sharp/PropertyDictionary.cs +++ b/mustache-sharp/PropertyDictionary.cs @@ -11,7 +11,7 @@ namespace mustache /// internal sealed class PropertyDictionary : IDictionary { - private static readonly Dictionary> _cache = new Dictionary>(); + private static readonly Dictionary> _cache = new Dictionary>(); private readonly object _instance; private readonly Dictionary _typeCache; @@ -234,4 +234,4 @@ namespace mustache return propertyInfo.GetValue(_instance, null); } } -} +} \ No newline at end of file diff --git a/mustache-sharp/RegexHelper.cs b/mustache-sharp/RegexHelper.cs new file mode 100644 index 0000000..a267910 --- /dev/null +++ b/mustache-sharp/RegexHelper.cs @@ -0,0 +1,22 @@ +using System; +using System.Text.RegularExpressions; + +namespace mustache +{ + /// + /// Provides utility methods that require regular expressions. + /// + public static class RegexHelper + { + /// + /// Determines whether the given name is a legal identifier. + /// + /// The name to check. + /// True if the name is a legal identifier; otherwise, false. + public static bool IsValidIdentifier(string name) + { + Regex regex = new Regex(@"^[_\w][_\w\d]*$"); + return regex.IsMatch(name); + } + } +} diff --git a/mustache-sharp/Scope.cs b/mustache-sharp/Scope.cs deleted file mode 100644 index f74777f..0000000 --- a/mustache-sharp/Scope.cs +++ /dev/null @@ -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 context = toLookup(nextLevel); - member = names[index]; - nextLevel = context[member]; - } - return nextLevel; - } - - private object find(string name) - { - IDictionary 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 toLookup(object value) - { - IDictionary lookup = value as IDictionary; - if (lookup == null) - { - lookup = new PropertyDictionary(value); - } - return lookup; - } - } -} diff --git a/mustache-sharp/StaticBuilder.cs b/mustache-sharp/StaticBuilder.cs deleted file mode 100644 index 6f98f83..0000000 --- a/mustache-sharp/StaticBuilder.cs +++ /dev/null @@ -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); - } - } -} diff --git a/mustache-sharp/StaticGenerator.cs b/mustache-sharp/StaticGenerator.cs new file mode 100644 index 0000000..e8aaa2e --- /dev/null +++ b/mustache-sharp/StaticGenerator.cs @@ -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; + } + } +} diff --git a/mustache-sharp/TagAttributes.cs b/mustache-sharp/TagAttributes.cs deleted file mode 100644 index 0cc4f49..0000000 --- a/mustache-sharp/TagAttributes.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace mustache -{ - internal sealed class TagAttributes - { - public TagAttributes() - { - } - - public TagType Type - { - get; - set; - } - - public bool IsOutput - { - get; - set; - } - } -} diff --git a/mustache-sharp/TagDefinition.cs b/mustache-sharp/TagDefinition.cs new file mode 100644 index 0000000..c0aaa6e --- /dev/null +++ b/mustache-sharp/TagDefinition.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using mustache.Properties; + +namespace mustache +{ + /// + /// Defines the attributes of a custom tag. + /// + public sealed class TagDefinition + { + private readonly string _tagName; + private readonly List _parameters; + private readonly List _childTagDefinitions; + private TagParameter _scopeParameter; + + /// + /// Initializes a new instance of a TagDefinition. + /// + /// The name of the tag. + /// The name of the tag is null or blank. + public TagDefinition(string tagName) + { + if (!RegexHelper.IsValidIdentifier(tagName)) + { + throw new ArgumentException(Resources.BlankTagName, "tagName"); + } + _tagName = tagName; + _parameters = new List(); + _childTagDefinitions = new List(); + } + + /// + /// Gets the name of the tag. + /// + public string Name + { + get { return _tagName; } + } + + /// + /// Specifies that the tag expects the given parameter information. + /// + /// The parameter to add. + /// The parameter is null. + /// A parameter with the same name already exists. + 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; + } + } + + /// + /// Gets the parameters that are defined for the tag. + /// + public IEnumerable Parameters + { + get { return new ReadOnlyCollection(_parameters); } + } + + /// + /// Gets or sets whether the tag contains content. + /// + public bool HasBody + { + get; + set; + } + + /// + /// Gets or sets whether the tag defines a new scope based on an argument. + /// + public bool IsScoped + { + get; + set; + } + + /// + /// Specifies that the given tag is in scope within the current tag. + /// + /// The tag that is in scope within the current tag. + public void AddChildTag(TagDefinition childTag) + { + if (childTag == null) + { + throw new ArgumentNullException("childTag"); + } + _childTagDefinitions.Add(childTag); + } + + /// + /// Gets the tags that are in scope within the current tag. + /// + public IEnumerable ChildTags + { + get { return new ReadOnlyCollection(_childTagDefinitions); } + } + } +} diff --git a/mustache-sharp/TagParameter.cs b/mustache-sharp/TagParameter.cs new file mode 100644 index 0000000..f838379 --- /dev/null +++ b/mustache-sharp/TagParameter.cs @@ -0,0 +1,63 @@ +using System; +using mustache.Properties; + +namespace mustache +{ + /// + /// Defines a parameter belonging to a custom tag. + /// + public sealed class TagParameter + { + private readonly string _name; + + /// + /// Initializes a new instance of a TagParameter. + /// + /// The name of the parameter. + /// The parameter name is null or an invalid identifier. + public TagParameter(string parameterName) + { + if (!RegexHelper.IsValidIdentifier(parameterName)) + { + throw new ArgumentException(Resources.BlankParameterName, "parameterName"); + } + _name = parameterName; + } + + /// + /// Gets the name of the parameter. + /// + public string Name + { + get { return _name; } + } + + /// + /// Gets or sets whether the parameter should be used to define the parameter. + /// + public bool IsScopeContext + { + get; + set; + } + + /// + /// Gets or sets whether the field is required. + /// + public bool IsRequired + { + get; + set; + } + + /// + /// Gets or sets the default value to use when an argument is not provided + /// for the parameter. + /// + public object DefaultValue + { + get; + set; + } + } +} diff --git a/mustache-sharp/TagScope.cs b/mustache-sharp/TagScope.cs new file mode 100644 index 0000000..8f20127 --- /dev/null +++ b/mustache-sharp/TagScope.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace mustache +{ + internal sealed class TagScope + { + private readonly TagScope _parent; + private readonly Dictionary _tagLookup; + + public TagScope() + : this(null) + { + } + + public TagScope(TagScope parent) + { + _parent = parent; + _tagLookup = new Dictionary(); + } + + 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); + } + } +} diff --git a/mustache-sharp/TagType.cs b/mustache-sharp/TagType.cs deleted file mode 100644 index 8bc697a..0000000 --- a/mustache-sharp/TagType.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace mustache -{ - internal enum TagType - { - None, - Singleton, - Header, - Footer, - } -} diff --git a/mustache-sharp/Trimmer.cs b/mustache-sharp/Trimmer.cs deleted file mode 100644 index 67028eb..0000000 --- a/mustache-sharp/Trimmer.cs +++ /dev/null @@ -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); - } - } - } -} diff --git a/mustache-sharp/WithBuilder.cs b/mustache-sharp/WithBuilder.cs deleted file mode 100644 index bd657ce..0000000 --- a/mustache-sharp/WithBuilder.cs +++ /dev/null @@ -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); - } - } -} diff --git a/mustache-sharp/mustache-sharp.csproj b/mustache-sharp/mustache-sharp.csproj index 740e4ce..72626ea 100644 --- a/mustache-sharp/mustache-sharp.csproj +++ b/mustache-sharp/mustache-sharp.csproj @@ -32,16 +32,11 @@ - - - - - - - - + + + True @@ -49,12 +44,12 @@ Resources.resx - - - - - - + + + + + +