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.
This commit is contained in:
Travis Parks 2013-01-01 21:01:34 -05:00
parent 3f8bf7413b
commit 827faa5d6e
25 changed files with 2470 additions and 0 deletions

10
Local.testsettings Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<TestSettings name="Local" id="2bc42439-1bb6-4112-9c20-eca1ffcae064" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Description>These are default test settings for a local test run.</Description>
<Deployment enabled="false" />
<Execution>
<TestTypeSpecific />
<AgentRule name="Execution Agents">
</AgentRule>
</Execution>
</TestSettings>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<TestSettings name="Trace and Test Impact" id="535ebf31-4d23-42a7-a823-ecb179ff7886" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Description>These are test settings for Trace and Test Impact.</Description>
<Execution>
<TestTypeSpecific />
<AgentRule name="Execution Agents">
<DataCollectors>
<DataCollector uri="datacollector://microsoft/SystemInfo/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TestTools.DataCollection.SystemInfo.SystemInfoDataCollector, Microsoft.VisualStudio.TestTools.DataCollection.SystemInfo, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="System Information">
</DataCollector>
<DataCollector uri="datacollector://microsoft/ActionLog/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TestTools.ManualTest.ActionLog.ActionLogPlugin, Microsoft.VisualStudio.TestTools.ManualTest.ActionLog, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="Actions">
</DataCollector>
<DataCollector uri="datacollector://microsoft/HttpProxy/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TraceCollector.HttpProxyCollector, Microsoft.VisualStudio.TraceCollector, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="ASP.NET Client Proxy for IntelliTrace and Test Impact">
</DataCollector>
<DataCollector uri="datacollector://microsoft/TestImpact/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TraceCollector.TestImpactDataCollector, Microsoft.VisualStudio.TraceCollector, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="Test Impact">
</DataCollector>
<DataCollector uri="datacollector://microsoft/TraceDebugger/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TraceCollector.TraceDebuggerDataCollector, Microsoft.VisualStudio.TraceCollector, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="IntelliTrace">
</DataCollector>
</DataCollectors>
</AgentRule>
</Execution>
</TestSettings>

36
mustache-sharp.sln Normal file
View File

@ -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

View File

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

View File

@ -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")]

View File

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

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>
</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{7F607362-0680-4751-B1DC-621219294AE3}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>mustache.test</RootNamespace>
<AssemblyName>mustache-sharp.test</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
</ItemGroup>
<ItemGroup>
<CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
<Visible>False</Visible>
</CodeAnalysisDependentAssemblyPaths>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="FormatterTester.cs" />
<Compile Include="PropertyDictionaryTester.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\mustache-sharp\mustache-sharp.csproj">
<Project>{D71B378F-A4BA-4263-A4F0-07A49A0C528D}</Project>
<Name>mustache-sharp</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

6
mustache-sharp.vsmdi Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<TestLists xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<TestList name="Lists of Tests" id="8c43106b-9dc1-4907-a29f-aa66a61bf5b6">
<RunConfiguration id="2bc42439-1bb6-4112-9c20-eca1ffcae064" name="Local" storage="local.testsettings" type="Microsoft.VisualStudio.TestTools.Common.TestRunConfiguration, Microsoft.VisualStudio.QualityTools.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</TestList>
</TestLists>

View File

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

View File

@ -0,0 +1,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<string, object> lookup = item as IDictionary<string, object>;
if (lookup == null)
{
lookup = new PropertyDictionary(item);
}
Scope itemScope = scope.CreateChildScope(item);
builder.Build(itemScope, output, provider);
}
}
}
}

303
mustache-sharp/Formatter.cs Normal file
View File

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

View File

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

View File

@ -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<object>().Any();
}
if (value is Char)
{
return (Char)value != '\0';
}
try
{
decimal number = (decimal)Convert.ChangeType(value, typeof(decimal));
return number != 0.0m;
}
catch
{
}
return true;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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")]

View File

@ -0,0 +1,81 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace mustache.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to A key or property was not found with the given name..
/// </summary>
internal static string KeyNotFound {
get {
return ResourceManager.GetString("KeyNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A matching closing tag was not found for the {0} tag..
/// </summary>
internal static string MissingClosingTag {
get {
return ResourceManager.GetString("MissingClosingTag", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="KeyNotFound" xml:space="preserve">
<value>A key or property was not found with the given name.</value>
</data>
<data name="MissingClosingTag" xml:space="preserve">
<value>A matching closing tag was not found for the {0} tag.</value>
</data>
</root>

View File

@ -0,0 +1,237 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace mustache
{
/// <summary>
/// Provides methods for creating instances of PropertyDictionary.
/// </summary>
internal sealed class PropertyDictionary : IDictionary<string, object>
{
private static readonly Dictionary<Type, Dictionary<string, PropertyInfo>> _cache = new Dictionary<Type,Dictionary<string,PropertyInfo>>();
private readonly object _instance;
private readonly Dictionary<string, PropertyInfo> _typeCache;
/// <summary>
/// Initializes a new instance of a PropertyDictionary.
/// </summary>
/// <param name="instance">The instance to wrap in the PropertyDictionary.</param>
public PropertyDictionary(object instance)
{
_instance = instance;
if (instance == null)
{
_typeCache = new Dictionary<string, PropertyInfo>();
}
else
{
_typeCache = getCacheType(_instance);
}
}
private static Dictionary<string, PropertyInfo> getCacheType(object instance)
{
Type type = instance.GetType();
Dictionary<string, PropertyInfo> typeCache;
if (!_cache.TryGetValue(type, out typeCache))
{
typeCache = new Dictionary<string, PropertyInfo>();
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;
}
/// <summary>
/// Gets the underlying instance.
/// </summary>
public object Instance
{
get { return _instance; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
void IDictionary<string, object>.Add(string key, object value)
{
throw new NotSupportedException();
}
/// <summary>
/// Determines whether a property with the given name exists.
/// </summary>
/// <param name="key">The name of the property.</param>
/// <returns>True if the property exists; otherwise, false.</returns>
public bool ContainsKey(string key)
{
return _typeCache.ContainsKey(key);
}
/// <summary>
/// Gets the name of the properties in the type.
/// </summary>
public ICollection<string> Keys
{
get { return _typeCache.Keys; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
bool IDictionary<string, object>.Remove(string key)
{
throw new NotSupportedException();
}
/// <summary>
/// Tries to get the value for the given property name.
/// </summary>
/// <param name="key">The name of the property to get the value for.</param>
/// <param name="value">The variable to store the value of the property or the default value if the property is not found.</param>
/// <returns>True if a property with the given name is found; otherwise, false.</returns>
/// <exception cref="System.ArgumentNullException">The name of the property was null.</exception>
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;
}
/// <summary>
/// Gets the values of all of the properties in the object.
/// </summary>
public ICollection<object> Values
{
get
{
ICollection<PropertyInfo> propertyInfos = _typeCache.Values;
List<object> values = new List<object>();
foreach (PropertyInfo propertyInfo in propertyInfos)
{
object value = getValue(propertyInfo);
values.Add(value);
}
return values.AsReadOnly();
}
}
/// <summary>
/// Gets or sets the value of the property with the given name.
/// </summary>
/// <param name="key">The name of the property to get or set.</param>
/// <returns>The value of the property with the given name.</returns>
/// <exception cref="System.ArgumentNullException">The property name was null.</exception>
/// <exception cref="System.Collections.Generic.KeyNotFoundException">The type does not have a property with the given name.</exception>
/// <exception cref="System.ArgumentException">The property did not support getting or setting.</exception>
/// <exception cref="System.ArgumentException">
/// The object does not match the target type, or a property is a value type but the value is null.
/// </exception>
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<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
{
throw new NotSupportedException();
}
[EditorBrowsable(EditorBrowsableState.Never)]
void ICollection<KeyValuePair<string, object>>.Clear()
{
throw new NotSupportedException();
}
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
{
PropertyInfo propertyInfo;
if (!_typeCache.TryGetValue(item.Key, out propertyInfo))
{
return false;
}
object value = getValue(propertyInfo);
return Equals(item.Value, value);
}
void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
List<KeyValuePair<string, object>> pairs = new List<KeyValuePair<string, object>>();
ICollection<KeyValuePair<string, PropertyInfo>> collection = _typeCache;
foreach (KeyValuePair<string, PropertyInfo> pair in collection)
{
PropertyInfo propertyInfo = pair.Value;
object value = getValue(propertyInfo);
pairs.Add(new KeyValuePair<string, object>(pair.Key, value));
}
pairs.CopyTo(array, arrayIndex);
}
/// <summary>
/// Gets the number of properties in the type.
/// </summary>
public int Count
{
get { return _typeCache.Count; }
}
/// <summary>
/// Gets or sets whether updates will be ignored.
/// </summary>
bool ICollection<KeyValuePair<string, object>>.IsReadOnly
{
get { return true; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
{
throw new NotSupportedException();
}
/// <summary>
/// Gets the propety name/value pairs in the object.
/// </summary>
/// <returns></returns>
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
foreach (KeyValuePair<string, PropertyInfo> pair in _typeCache)
{
object value = getValue(pair.Value);
yield return new KeyValuePair<string, object>(pair.Key, value);
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private object getValue(PropertyInfo propertyInfo)
{
return propertyInfo.GetValue(_instance, null);
}
}
}

68
mustache-sharp/Scope.cs Normal file
View File

@ -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<string, object> context = toLookup(nextLevel);
member = names[index];
nextLevel = context[member];
}
return nextLevel;
}
private object find(string name)
{
IDictionary<string, object> lookup = toLookup(topLevel);
if (lookup.ContainsKey(name))
{
return lookup[name];
}
if (parent == null)
{
string message = String.Format(Resources.KeyNotFound, name);
throw new KeyNotFoundException(message);
}
return parent.find(name);
}
private static IDictionary<string, object> toLookup(object value)
{
IDictionary<string, object> lookup = value as IDictionary<string, object>;
if (lookup == null)
{
lookup = new PropertyDictionary(value);
}
return lookup;
}
}
}

View File

@ -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);
}
}
}

View File

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

12
mustache-sharp/TagType.cs Normal file
View File

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

57
mustache-sharp/Trimmer.cs Normal file
View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{D71B378F-A4BA-4263-A4F0-07A49A0C528D}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>mustache</RootNamespace>
<AssemblyName>mustache-sharp</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
<Compile Include="CompoundBuilder.cs" />
<Compile Include="EachBuilder.cs" />
<Compile Include="Formatter.cs" />
<Compile Include="IBuilder.cs" />
<Compile Include="IfBuilder.cs" />
<Compile Include="KeyBuilder.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="PropertyDictionary.cs" />
<Compile Include="Scope.cs" />
<Compile Include="StaticBuilder.cs" />
<Compile Include="TagAttributes.cs" />
<Compile Include="TagType.cs" />
<Compile Include="Trimmer.cs" />
<Compile Include="WithBuilder.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>