From 827faa5d6e43b02932bd13203ecfd74804513ce6 Mon Sep 17 00:00:00 2001 From: Travis Parks Date: Tue, 1 Jan 2013 21:01:34 -0500 Subject: [PATCH] Initial Commit This is the code almost verbatim from the NList project. This project is due for a major overhaul, but I don't know the order I will be breaking out NList in the upcoming weeks. --- Local.testsettings | 10 + TraceAndTestImpact.testsettings | 21 + mustache-sharp.sln | 36 ++ mustache-sharp.test/FormatterTester.cs | 524 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 35 ++ .../PropertyDictionaryTester.cs | 484 ++++++++++++++++ .../mustache-sharp.test.csproj | 67 +++ mustache-sharp.vsmdi | 6 + mustache-sharp/CompoundBuilder.cs | 29 + mustache-sharp/EachBuilder.cs | 48 ++ mustache-sharp/Formatter.cs | 303 ++++++++++ mustache-sharp/IBuilder.cs | 10 + mustache-sharp/IfBuilder.cs | 75 +++ mustache-sharp/KeyBuilder.cs | 50 ++ mustache-sharp/Properties/AssemblyInfo.cs | 39 ++ .../Properties/Resources.Designer.cs | 81 +++ mustache-sharp/Properties/Resources.resx | 126 +++++ mustache-sharp/PropertyDictionary.cs | 237 ++++++++ mustache-sharp/Scope.cs | 68 +++ mustache-sharp/StaticBuilder.cs | 23 + mustache-sharp/TagAttributes.cs | 23 + mustache-sharp/TagType.cs | 12 + mustache-sharp/Trimmer.cs | 57 ++ mustache-sharp/WithBuilder.cs | 33 ++ mustache-sharp/mustache-sharp.csproj | 73 +++ 25 files changed, 2470 insertions(+) create mode 100644 Local.testsettings create mode 100644 TraceAndTestImpact.testsettings create mode 100644 mustache-sharp.sln create mode 100644 mustache-sharp.test/FormatterTester.cs create mode 100644 mustache-sharp.test/Properties/AssemblyInfo.cs create mode 100644 mustache-sharp.test/PropertyDictionaryTester.cs create mode 100644 mustache-sharp.test/mustache-sharp.test.csproj create mode 100644 mustache-sharp.vsmdi create mode 100644 mustache-sharp/CompoundBuilder.cs create mode 100644 mustache-sharp/EachBuilder.cs create mode 100644 mustache-sharp/Formatter.cs create mode 100644 mustache-sharp/IBuilder.cs create mode 100644 mustache-sharp/IfBuilder.cs create mode 100644 mustache-sharp/KeyBuilder.cs create mode 100644 mustache-sharp/Properties/AssemblyInfo.cs create mode 100644 mustache-sharp/Properties/Resources.Designer.cs create mode 100644 mustache-sharp/Properties/Resources.resx create mode 100644 mustache-sharp/PropertyDictionary.cs create mode 100644 mustache-sharp/Scope.cs create mode 100644 mustache-sharp/StaticBuilder.cs create mode 100644 mustache-sharp/TagAttributes.cs create mode 100644 mustache-sharp/TagType.cs create mode 100644 mustache-sharp/Trimmer.cs create mode 100644 mustache-sharp/WithBuilder.cs create mode 100644 mustache-sharp/mustache-sharp.csproj diff --git a/Local.testsettings b/Local.testsettings new file mode 100644 index 0000000..9654e6a --- /dev/null +++ b/Local.testsettings @@ -0,0 +1,10 @@ + + + These are default test settings for a local test run. + + + + + + + \ No newline at end of file diff --git a/TraceAndTestImpact.testsettings b/TraceAndTestImpact.testsettings new file mode 100644 index 0000000..88f9753 --- /dev/null +++ b/TraceAndTestImpact.testsettings @@ -0,0 +1,21 @@ + + + These are test settings for Trace and Test Impact. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mustache-sharp.sln b/mustache-sharp.sln new file mode 100644 index 0000000..98d40ad --- /dev/null +++ b/mustache-sharp.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mustache-sharp", "mustache-sharp\mustache-sharp.csproj", "{D71B378F-A4BA-4263-A4F0-07A49A0C528D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mustache-sharp.test", "mustache-sharp.test\mustache-sharp.test.csproj", "{7F607362-0680-4751-B1DC-621219294AE3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{25414E49-67E6-4B8D-8AD8-78C70F8992A7}" + ProjectSection(SolutionItems) = preProject + Local.testsettings = Local.testsettings + mustache-sharp.vsmdi = mustache-sharp.vsmdi + TraceAndTestImpact.testsettings = TraceAndTestImpact.testsettings + EndProjectSection +EndProject +Global + GlobalSection(TestCaseManagementSettings) = postSolution + CategoryFile = mustache-sharp.vsmdi + EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D71B378F-A4BA-4263-A4F0-07A49A0C528D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D71B378F-A4BA-4263-A4F0-07A49A0C528D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D71B378F-A4BA-4263-A4F0-07A49A0C528D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D71B378F-A4BA-4263-A4F0-07A49A0C528D}.Release|Any CPU.Build.0 = Release|Any CPU + {7F607362-0680-4751-B1DC-621219294AE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F607362-0680-4751-B1DC-621219294AE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F607362-0680-4751-B1DC-621219294AE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F607362-0680-4751-B1DC-621219294AE3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/mustache-sharp.test/FormatterTester.cs b/mustache-sharp.test/FormatterTester.cs new file mode 100644 index 0000000..52d236e --- /dev/null +++ b/mustache-sharp.test/FormatterTester.cs @@ -0,0 +1,524 @@ +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/Properties/AssemblyInfo.cs b/mustache-sharp.test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0c78dd7 --- /dev/null +++ b/mustache-sharp.test/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("mustache-sharp.test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("mustache-sharp.test")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9975f293-f972-4751-9c8c-e25b17c0c8bc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/mustache-sharp.test/PropertyDictionaryTester.cs b/mustache-sharp.test/PropertyDictionaryTester.cs new file mode 100644 index 0000000..811505a --- /dev/null +++ b/mustache-sharp.test/PropertyDictionaryTester.cs @@ -0,0 +1,484 @@ +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 new file mode 100644 index 0000000..1bb5f88 --- /dev/null +++ b/mustache-sharp.test/mustache-sharp.test.csproj @@ -0,0 +1,67 @@ + + + + Debug + AnyCPU + + + 2.0 + {7F607362-0680-4751-B1DC-621219294AE3} + Library + Properties + mustache.test + mustache-sharp.test + v4.0 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + 3.5 + + + + + False + + + + + + + + + + {D71B378F-A4BA-4263-A4F0-07A49A0C528D} + mustache-sharp + + + + + \ No newline at end of file diff --git a/mustache-sharp.vsmdi b/mustache-sharp.vsmdi new file mode 100644 index 0000000..881fd48 --- /dev/null +++ b/mustache-sharp.vsmdi @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/mustache-sharp/CompoundBuilder.cs b/mustache-sharp/CompoundBuilder.cs new file mode 100644 index 0000000..7277946 --- /dev/null +++ b/mustache-sharp/CompoundBuilder.cs @@ -0,0 +1,29 @@ +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/EachBuilder.cs b/mustache-sharp/EachBuilder.cs new file mode 100644 index 0000000..d2dcf2e --- /dev/null +++ b/mustache-sharp/EachBuilder.cs @@ -0,0 +1,48 @@ +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/Formatter.cs b/mustache-sharp/Formatter.cs new file mode 100644 index 0000000..9a49c94 --- /dev/null +++ b/mustache-sharp/Formatter.cs @@ -0,0 +1,303 @@ +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 new file mode 100644 index 0000000..519bcc1 --- /dev/null +++ b/mustache-sharp/IBuilder.cs @@ -0,0 +1,10 @@ +using System; +using System.Text; + +namespace mustache +{ + internal interface IBuilder + { + void Build(Scope scope, StringBuilder output, IFormatProvider provider); + } +} diff --git a/mustache-sharp/IfBuilder.cs b/mustache-sharp/IfBuilder.cs new file mode 100644 index 0000000..a861530 --- /dev/null +++ b/mustache-sharp/IfBuilder.cs @@ -0,0 +1,75 @@ +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 new file mode 100644 index 0000000..f5c8d9d --- /dev/null +++ b/mustache-sharp/KeyBuilder.cs @@ -0,0 +1,50 @@ +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/Properties/AssemblyInfo.cs b/mustache-sharp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..da48055 --- /dev/null +++ b/mustache-sharp/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("mustache-sharp")] +[assembly: AssemblyDescription("A extension of the mustache text template engine for .NET.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("truncon")] +[assembly: AssemblyProduct("mustache-sharp")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: CLSCompliant(true)] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e5a4263d-d450-4d85-a4d5-44c0a2822668")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.0.0.0")] +[assembly: AssemblyFileVersion("0.0.0.0")] +[assembly: InternalsVisibleTo("mustache-sharp.test")] \ No newline at end of file diff --git a/mustache-sharp/Properties/Resources.Designer.cs b/mustache-sharp/Properties/Resources.Designer.cs new file mode 100644 index 0000000..7ca007a --- /dev/null +++ b/mustache-sharp/Properties/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.296 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace mustache.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("mustache.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to A key or property was not found with the given name.. + /// + 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 new file mode 100644 index 0000000..4e0171f --- /dev/null +++ b/mustache-sharp/Properties/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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. + + + A matching closing tag was not found for the {0} tag. + + \ No newline at end of file diff --git a/mustache-sharp/PropertyDictionary.cs b/mustache-sharp/PropertyDictionary.cs new file mode 100644 index 0000000..7de64ee --- /dev/null +++ b/mustache-sharp/PropertyDictionary.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; + +namespace mustache +{ + /// + /// Provides methods for creating instances of PropertyDictionary. + /// + internal sealed class PropertyDictionary : IDictionary + { + private static readonly Dictionary> _cache = new Dictionary>(); + + private readonly object _instance; + private readonly Dictionary _typeCache; + + /// + /// Initializes a new instance of a PropertyDictionary. + /// + /// The instance to wrap in the PropertyDictionary. + public PropertyDictionary(object instance) + { + _instance = instance; + if (instance == null) + { + _typeCache = new Dictionary(); + } + else + { + _typeCache = getCacheType(_instance); + } + } + + private static Dictionary getCacheType(object instance) + { + Type type = instance.GetType(); + Dictionary typeCache; + if (!_cache.TryGetValue(type, out typeCache)) + { + typeCache = new Dictionary(); + BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy; + foreach (PropertyInfo propertyInfo in type.GetProperties(flags)) + { + if (!propertyInfo.IsSpecialName) + { + typeCache.Add(propertyInfo.Name, propertyInfo); + } + } + _cache.Add(type, typeCache); + } + return typeCache; + } + + /// + /// Gets the underlying instance. + /// + public object Instance + { + get { return _instance; } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + void IDictionary.Add(string key, object value) + { + throw new NotSupportedException(); + } + + /// + /// Determines whether a property with the given name exists. + /// + /// The name of the property. + /// True if the property exists; otherwise, false. + public bool ContainsKey(string key) + { + return _typeCache.ContainsKey(key); + } + + /// + /// Gets the name of the properties in the type. + /// + public ICollection Keys + { + get { return _typeCache.Keys; } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + bool IDictionary.Remove(string key) + { + throw new NotSupportedException(); + } + + /// + /// Tries to get the value for the given property name. + /// + /// The name of the property to get the value for. + /// The variable to store the value of the property or the default value if the property is not found. + /// True if a property with the given name is found; otherwise, false. + /// The name of the property was null. + public bool TryGetValue(string key, out object value) + { + PropertyInfo propertyInfo; + if (!_typeCache.TryGetValue(key, out propertyInfo)) + { + value = null; + return false; + } + value = getValue(propertyInfo); + return true; + } + + /// + /// Gets the values of all of the properties in the object. + /// + public ICollection Values + { + get + { + ICollection propertyInfos = _typeCache.Values; + List values = new List(); + foreach (PropertyInfo propertyInfo in propertyInfos) + { + object value = getValue(propertyInfo); + values.Add(value); + } + return values.AsReadOnly(); + } + } + + /// + /// Gets or sets the value of the property with the given name. + /// + /// The name of the property to get or set. + /// The value of the property with the given name. + /// The property name was null. + /// The type does not have a property with the given name. + /// The property did not support getting or setting. + /// + /// The object does not match the target type, or a property is a value type but the value is null. + /// + public object this[string key] + { + get + { + PropertyInfo propertyInfo = _typeCache[key]; + return getValue(propertyInfo); + } + [EditorBrowsable(EditorBrowsableState.Never)] + set + { + throw new NotSupportedException(); + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + void ICollection>.Add(KeyValuePair item) + { + throw new NotSupportedException(); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + void ICollection>.Clear() + { + throw new NotSupportedException(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + PropertyInfo propertyInfo; + if (!_typeCache.TryGetValue(item.Key, out propertyInfo)) + { + return false; + } + object value = getValue(propertyInfo); + return Equals(item.Value, value); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + List> pairs = new List>(); + ICollection> collection = _typeCache; + foreach (KeyValuePair pair in collection) + { + PropertyInfo propertyInfo = pair.Value; + object value = getValue(propertyInfo); + pairs.Add(new KeyValuePair(pair.Key, value)); + } + pairs.CopyTo(array, arrayIndex); + } + + /// + /// Gets the number of properties in the type. + /// + public int Count + { + get { return _typeCache.Count; } + } + + /// + /// Gets or sets whether updates will be ignored. + /// + bool ICollection>.IsReadOnly + { + get { return true; } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotSupportedException(); + } + + /// + /// Gets the propety name/value pairs in the object. + /// + /// + public IEnumerator> GetEnumerator() + { + foreach (KeyValuePair pair in _typeCache) + { + object value = getValue(pair.Value); + yield return new KeyValuePair(pair.Key, value); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private object getValue(PropertyInfo propertyInfo) + { + return propertyInfo.GetValue(_instance, null); + } + } +} diff --git a/mustache-sharp/Scope.cs b/mustache-sharp/Scope.cs new file mode 100644 index 0000000..f74777f --- /dev/null +++ b/mustache-sharp/Scope.cs @@ -0,0 +1,68 @@ +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 new file mode 100644 index 0000000..6f98f83 --- /dev/null +++ b/mustache-sharp/StaticBuilder.cs @@ -0,0 +1,23 @@ +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/TagAttributes.cs b/mustache-sharp/TagAttributes.cs new file mode 100644 index 0000000..0cc4f49 --- /dev/null +++ b/mustache-sharp/TagAttributes.cs @@ -0,0 +1,23 @@ +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/TagType.cs b/mustache-sharp/TagType.cs new file mode 100644 index 0000000..8bc697a --- /dev/null +++ b/mustache-sharp/TagType.cs @@ -0,0 +1,12 @@ +using System; + +namespace mustache +{ + internal enum TagType + { + None, + Singleton, + Header, + Footer, + } +} diff --git a/mustache-sharp/Trimmer.cs b/mustache-sharp/Trimmer.cs new file mode 100644 index 0000000..67028eb --- /dev/null +++ b/mustache-sharp/Trimmer.cs @@ -0,0 +1,57 @@ +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 new file mode 100644 index 0000000..bd657ce --- /dev/null +++ b/mustache-sharp/WithBuilder.cs @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..740e4ce --- /dev/null +++ b/mustache-sharp/mustache-sharp.csproj @@ -0,0 +1,73 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {D71B378F-A4BA-4263-A4F0-07A49A0C528D} + Library + Properties + mustache + mustache-sharp + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + \ No newline at end of file