Implement custom tags.
This is the first step towards supporting custom tags. There are wrinkles I need to work out, since I'm not 100% sure what the finished code will look like.
This commit is contained in:
parent
827faa5d6e
commit
f8628aaf86
|
@ -1,524 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace mustache.test
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests the Formatter class.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class FormatterTester
|
||||
{
|
||||
#region Real World Example
|
||||
|
||||
/// <summary>
|
||||
/// The Formatter class is especially useful when performing simple mail merge operations.
|
||||
/// Like String.Format, Formatter will substitute placeholders for actual values. In the case
|
||||
/// of Formatter, placeholders are indicated by name, rather than index and are wrapped with
|
||||
/// double curly braces: {{name}}. The name within the curly brace can include any characters,
|
||||
/// including whitespace, except for two or more adjacent right curly braces (}}).
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_ReplaceNamedPlaceholdersWithFormats()
|
||||
{
|
||||
const string format = "Hello {{name}}! It is {{date:MM-dd-yyyy}}. You make {{income:C}} an hour.";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result1 = formatter.Format(new Dictionary<string, object>()
|
||||
{
|
||||
{ "name", "Bob" },
|
||||
{ "date", new DateTime(2012, 03, 11) },
|
||||
{ "income", 32.8 }
|
||||
});
|
||||
Assert.AreEqual("Hello Bob! It is 03-11-2012. You make $32.80 an hour.", result1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we want to work with objects, rather than raw dictionaries, we can wrap the objects with
|
||||
/// property dictionaries.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_UseObject()
|
||||
{
|
||||
var person = new
|
||||
{
|
||||
Name = "Bob",
|
||||
Date = new DateTime(2012, 03, 11),
|
||||
Income = 32.8
|
||||
};
|
||||
const string format = "Hello {{Name}}! It is {{Date:MM-dd-yyyy}}. You make {{Income:C}} an hour.";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result1 = formatter.Format(person);
|
||||
Assert.AreEqual("Hello Bob! It is 03-11-2012. You make $32.80 an hour.", result1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We can the Formatter to print out a list of items following a format.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_PrintList()
|
||||
{
|
||||
List<int> values = new List<int>() { 0, 1, 2, 3, 4 };
|
||||
const string format = "{{#each this}}{{this}} {{/each}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(values);
|
||||
Assert.AreEqual("0 1 2 3 4 ", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We can include some text conditionally.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_ConditionallyIncludeText()
|
||||
{
|
||||
Random random = new Random();
|
||||
int value = random.Next();
|
||||
bool isEven = value % 2 == 0;
|
||||
var data = new
|
||||
{
|
||||
Value = value,
|
||||
IsEven = isEven,
|
||||
};
|
||||
const string format = "{{Value}} {{#if IsEven}}is even{{#else}}is odd{{/if}}.";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(data);
|
||||
string expected = String.Format("{0}", value) + (isEven ? " is even." : " is odd.");
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiple cases can be handled using if/elif/else.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_HandleCases()
|
||||
{
|
||||
const string format = @"{{#if No}}No{{#elif Yes}}Yes{{#else}}Maybe{{/if}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
var data = new
|
||||
{
|
||||
Yes = true,
|
||||
No = false,
|
||||
};
|
||||
string result = formatter.Format(data);
|
||||
Assert.AreEqual("Yes", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We should be able to combine tags anyway we want.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_Compound()
|
||||
{
|
||||
const string format = @"{{#with Customer}}
|
||||
Hello{{#if FirstName}} {{FirstName}}{{/if}}:
|
||||
{{/with}}
|
||||
{{#! We only want to print out purchases if they have some. }}
|
||||
{{#if Purchases}}
|
||||
|
||||
You recently purchased:
|
||||
{{#each Purchases}}
|
||||
{{Name}}: {{Quantity}} x {{Price:C}}
|
||||
{{/each}}
|
||||
Your total was: {{Total:C}}
|
||||
{{/if}}
|
||||
|
||||
We thought you might be interested in buying: {{PromotionProduct}}.
|
||||
|
||||
Thank you,
|
||||
{{#with Agent}}
|
||||
{{Name}}
|
||||
{{/with}}";
|
||||
|
||||
Formatter formatter = new Formatter(format);
|
||||
var data = new
|
||||
{
|
||||
Customer = new
|
||||
{
|
||||
FirstName = "Bob",
|
||||
},
|
||||
Purchases = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Name = "Donkey",
|
||||
Quantity = 8,
|
||||
Price = 1.23m,
|
||||
},
|
||||
new
|
||||
{
|
||||
Name = "Hammer",
|
||||
Quantity = 1,
|
||||
Price = 8.32m,
|
||||
},
|
||||
},
|
||||
Total = 18.16m,
|
||||
PromotionProduct = "Sneakers",
|
||||
Agent = new
|
||||
{
|
||||
Name = "Tom",
|
||||
},
|
||||
};
|
||||
string result = formatter.Format(data);
|
||||
Assert.AreEqual(@"Hello Bob:
|
||||
|
||||
You recently purchased:
|
||||
Donkey: 8 x $1.23
|
||||
Hammer: 1 x $8.32
|
||||
Your total was: $18.16
|
||||
|
||||
We thought you might be interested in buying: Sneakers.
|
||||
|
||||
Thank you,
|
||||
Tom
|
||||
", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Argument Checking
|
||||
|
||||
/// <summary>
|
||||
/// An exception should be thrown if the format string is null.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void TestCtor_NullFormat_ThrowsException()
|
||||
{
|
||||
string format = null;
|
||||
new Formatter(format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we try to replace a placeholder that we do not have a lookup key for,
|
||||
/// an exception should be thrown.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(KeyNotFoundException))]
|
||||
public void TestFormat_MissingKey_ThrowsException()
|
||||
{
|
||||
Formatter formatter = new Formatter("{{unknown}}");
|
||||
IDictionary<string, object> lookup = new Dictionary<string, object>();
|
||||
formatter.Format(lookup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A format exception should be thrown if there is not a matching closing if tag.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(FormatException))]
|
||||
public void TestFormat_MissingClosingIfTag_ThrowsException()
|
||||
{
|
||||
new Formatter("{{#if Bob}}Hello");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A format exception should be thrown if the matching closing tag is wrong.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(FormatException))]
|
||||
public void TestFormat_WrongClosingIfTag_ThrowsException()
|
||||
{
|
||||
new Formatter("{{#with this}}{{#if Bob}}Hello{{/with}}{{/if}}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// If we specify a right alignment, the output should be aligned to the right.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_WithRightAlignment_AlignsToRight()
|
||||
{
|
||||
string format = "{{Name,10}}";
|
||||
var instance = new
|
||||
{
|
||||
Name = "Bob"
|
||||
};
|
||||
PropertyDictionary dictionary = new PropertyDictionary(instance);
|
||||
string result = Formatter.Format(format, dictionary);
|
||||
Assert.AreEqual(" Bob", result, "The text was not aligned.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we specify a left alignment, the output should be aligned to the left.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_WithLeftAlignment_AlignsToLeft()
|
||||
{
|
||||
string format = "{{Name,-10}}";
|
||||
var instance = new
|
||||
{
|
||||
Name = "Bob"
|
||||
};
|
||||
PropertyDictionary dictionary = new PropertyDictionary(instance);
|
||||
string result = Formatter.Format(null, format, dictionary);
|
||||
Assert.AreEqual("Bob ", result, "The text was not aligned.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we try to format an empty string, an empty string should be returned.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_EmptyFormat_ReturnsEmpty()
|
||||
{
|
||||
Formatter formatter = new Formatter(String.Empty);
|
||||
Dictionary<string, object> lookup = new Dictionary<string, object>();
|
||||
string result = formatter.Format(lookup);
|
||||
Assert.AreEqual(String.Empty, result, "The result should have been empty.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If our format string is just a placeholder, than just the replacement value should be returned.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_FormatIsSinglePlaceholder_ReturnsReplaced()
|
||||
{
|
||||
Formatter formatter = new Formatter("{{name}}");
|
||||
Dictionary<string, object> lookup = new Dictionary<string, object>()
|
||||
{
|
||||
{ "name", "test" }
|
||||
};
|
||||
string result = formatter.Format(lookup);
|
||||
Assert.AreEqual("test", result, "The result was wrong.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We should be able to put just about anything inside of a placeholder, but it will
|
||||
/// not be treated like a placeholder.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_PlaceholderContainsSpecialCharacters_ReturnsUnreplaced()
|
||||
{
|
||||
Formatter formatter = new Formatter("{{ \\_@#$%^ }1233 abc}}");
|
||||
Dictionary<string, object> lookup = new Dictionary<string, object>()
|
||||
{
|
||||
{ " \\_@#$%^ }1233 abc", "test" }
|
||||
};
|
||||
string result = formatter.Format(lookup);
|
||||
Assert.AreEqual("{{ \\_@#$%^ }1233 abc}}", result, "The result was wrong.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If a lookup value is null, it should be replaced with an empty string.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_NullValue_ReplacesWithBlank()
|
||||
{
|
||||
Formatter formatter = new Formatter("These quotes should be empty '{{name}}'.");
|
||||
Dictionary<string, object> lookup = new Dictionary<string, object>()
|
||||
{
|
||||
{ "name", null }
|
||||
};
|
||||
string result = formatter.Format(lookup);
|
||||
Assert.AreEqual("These quotes should be empty ''.", result, "The result was wrong.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If a replacement value contains a placeholder, it should NOT be evaluated.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_ReplacementContainsPlaceholder_IgnoresPlaceholder()
|
||||
{
|
||||
Formatter formatter = new Formatter("The length of {{name}} is {{length}}.");
|
||||
Dictionary<string, object> lookup = new Dictionary<string, object>()
|
||||
{
|
||||
{ "name", "Bob" },
|
||||
{ "length", "{{name}}" }
|
||||
};
|
||||
string result = formatter.Format(lookup);
|
||||
Assert.AreEqual("The length of Bob is {{name}}.", result, "The result was wrong.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we pass null to as the format provider to the Format function,
|
||||
/// the current culture is used.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_NullFormatter_UsesCurrentCulture()
|
||||
{
|
||||
string format = "{0:C}";
|
||||
Formatter formatter = new Formatter("{" + format + "}");
|
||||
string result = formatter.Format((IFormatProvider)null, new Dictionary<string, object>() { { "0", 28.30m } });
|
||||
string expected = String.Format(CultureInfo.CurrentCulture, format, 28.30m);
|
||||
Assert.AreEqual(expected, result, "The wrong format provider was used.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we put a tag on a line by itself, it shouldn't result in any whitespace.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_TagOnLineByItself_NoNewlineGenerated()
|
||||
{
|
||||
const string format = @"Hello
|
||||
{{#if Name}}
|
||||
{{Name}}
|
||||
{{/if}}
|
||||
Goodbye
|
||||
";
|
||||
var data = new { Name = "George" };
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(data);
|
||||
const string expected = @"Hello
|
||||
George
|
||||
Goodbye
|
||||
";
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If a key is not found at the current level, it is looked for at the parent level.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_NameAtHigherScope_Finds()
|
||||
{
|
||||
const string format = "{{#with Child}}{{TopLevel}} and {{ChildLevel}}{{/with}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
var data = new
|
||||
{
|
||||
TopLevel = "Parent",
|
||||
Child = new { ChildLevel = "Child" },
|
||||
};
|
||||
string result = formatter.Format(data);
|
||||
Assert.AreEqual("Parent and Child", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null values are considered false by if statements.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_ConditionOnNull_ConsideredFalse()
|
||||
{
|
||||
const string format = "{{#if this}}Bad{{#else}}Good{{/if}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(null);
|
||||
Assert.AreEqual("Good", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Empty collections are considered false by if statements.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_ConditionOnEmptyCollection_ConsideredFalse()
|
||||
{
|
||||
const string format = "{{#if this}}Bad{{#else}}Good{{/if}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(new object[0]);
|
||||
Assert.AreEqual("Good", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Non-empty collections are considered true by if statements.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_ConditionOnNonEmptyCollection_ConsideredTrue()
|
||||
{
|
||||
const string format = "{{#if this}}Good{{#else}}Bad{{/if}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(new object[1]);
|
||||
Assert.AreEqual("Good", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null-char is considered false by if statements.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_ConditionOnNullChar_ConsideredFalse()
|
||||
{
|
||||
const string format = "{{#if this}}Bad{{#else}}Good{{/if}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format('\0');
|
||||
Assert.AreEqual("Good", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zero is considered false by if statements.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_ConditionOnZero_ConsideredFalse()
|
||||
{
|
||||
const string format = "{{#if this}}Bad{{#else}}Good{{/if}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
int? value = 0;
|
||||
string result = formatter.Format(value);
|
||||
Assert.AreEqual("Good", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Everything else is considered true by if statements.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_ConditionOnDateTime_ConsideredTrue()
|
||||
{
|
||||
const string format = "{{#if this}}Good{{#else}}Bad{{/if}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(DateTime.Now);
|
||||
Assert.AreEqual("Good", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instead of requiring deeply nested "with" statements, members
|
||||
/// can be separated by dots.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_NestedMembers_SearchesMembers()
|
||||
{
|
||||
const string format = "{{Customer.Name}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
var data = new { Customer = new { Name = "Bob" } };
|
||||
string result = formatter.Format(data);
|
||||
Assert.AreEqual("Bob", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keys should cause newlines to be respected, since they are considered content.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_KeyBetweenTags_RespectsTrailingNewline()
|
||||
{
|
||||
string format = "{{#each this}}{{this}} {{/each}}" + Environment.NewLine;
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format("Hello");
|
||||
Assert.AreEqual("H e l l o " + Environment.NewLine, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If someone tries to loop on a non-enumerable, it should do nothing.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_EachOnNonEnumerable_PrintsNothing()
|
||||
{
|
||||
const string format = "{{#each this}}Bad{{/each}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(123);
|
||||
Assert.AreEqual(String.Empty, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If a tag header is on the same line as it's footer, the new-line should not be removed.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_InlineTags_RespectNewLine()
|
||||
{
|
||||
const string format = @"{{#if this}}{{/if}}
|
||||
";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(true);
|
||||
Assert.AreEqual(Environment.NewLine, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If a tag header is on the same line as it's footer, the new-line should not be removed.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestFormatter_TagFooterFollowedByTagHeader_RemovesNewLine()
|
||||
{
|
||||
const string format = @"{{#if this}}
|
||||
{{/if}}{{#if this}}
|
||||
Hello{{/if}}";
|
||||
Formatter formatter = new Formatter(format);
|
||||
string result = formatter.Format(true);
|
||||
Assert.AreEqual("Hello", result);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,484 +0,0 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace mustache.test
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests the PropertyDictionary class.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class PropertyDictionaryTester
|
||||
{
|
||||
#region Real World Example
|
||||
|
||||
/// <summary>
|
||||
/// The purpose of the PropertyDictionary class is to allow an object to be inspected,
|
||||
/// as if it were a dictionary. This means we can get and set properties by their names.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestPropertyDictionary_AccessPropertiesViaIndexer()
|
||||
{
|
||||
var person = new
|
||||
{
|
||||
Name = "Bob",
|
||||
Age = 23,
|
||||
Birthday = new DateTime(2012, 03, 12)
|
||||
};
|
||||
PropertyDictionary wrapper = new PropertyDictionary(person);
|
||||
|
||||
Assert.AreEqual(3, wrapper.Count, "The wrong number of properties were created.");
|
||||
|
||||
Assert.IsTrue(wrapper.ContainsKey("Name"));
|
||||
Assert.IsTrue(wrapper.ContainsKey("Age"));
|
||||
Assert.IsTrue(wrapper.ContainsKey("Birthday"));
|
||||
|
||||
Assert.AreEqual(person.Name, wrapper["Name"], "The name was not wrapped.");
|
||||
Assert.AreEqual(person.Age, wrapper["Age"], "The age was not wrapped.");
|
||||
Assert.AreEqual(person.Birthday, wrapper["Birthday"], "The birthday was not wrapped.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ctor & Instance & IsReadOnly
|
||||
|
||||
/// <summary>
|
||||
/// If we try to wrap null, an exception should be thrown.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestCtor_NullInstance_ThrowsException()
|
||||
{
|
||||
PropertyDictionary dictionary = new PropertyDictionary(null);
|
||||
Assert.AreEqual(0, dictionary.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We should be able to access the underlying object.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestCtor_SetsInstance()
|
||||
{
|
||||
object instance = new object();
|
||||
PropertyDictionary dictionary = new PropertyDictionary(instance);
|
||||
Assert.AreSame(instance, dictionary.Instance, "The instance was not set.");
|
||||
ICollection<KeyValuePair<string, object>> collection = dictionary;
|
||||
Assert.IsTrue(collection.IsReadOnly, "The collection should not have been read-only.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Add
|
||||
|
||||
/// <summary>
|
||||
/// Since the dictionary is a simple wrapper around an object, we cannot add new properties.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NotSupportedException))]
|
||||
public void TestAdd_IDictionary_ThrowsException()
|
||||
{
|
||||
IDictionary<string, object> dictionary = new PropertyDictionary(new object());
|
||||
dictionary.Add("Name", "Bob");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Since the dictionary is a simple wrapper around an object, we cannot add new properties.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NotSupportedException))]
|
||||
public void TestAdd_ICollection_ThrowsException()
|
||||
{
|
||||
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(new object());
|
||||
collection.Add(new KeyValuePair<string, object>("Name", "Bob"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ContainsKey
|
||||
|
||||
/// <summary>
|
||||
/// If the wrapped object has a property, the key should be found.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestContainsKey_PropertyExists_ReturnsTrue()
|
||||
{
|
||||
var person = new
|
||||
{
|
||||
Name = "Bob",
|
||||
};
|
||||
PropertyDictionary dictionary = new PropertyDictionary(person);
|
||||
bool result = dictionary.ContainsKey("Name");
|
||||
Assert.IsTrue(result, "The property name was not found.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the wrapped object does not have a property, the key should not be found.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestContainsKey_PropertyMissing_ReturnsFalse()
|
||||
{
|
||||
var person = new { };
|
||||
PropertyDictionary dictionary = new PropertyDictionary(person);
|
||||
bool result = dictionary.ContainsKey("Name");
|
||||
Assert.IsFalse(result, "The property name was found.");
|
||||
}
|
||||
|
||||
private class BaseType
|
||||
{
|
||||
public string Inherited { get; set; }
|
||||
}
|
||||
|
||||
private class DerivedType : BaseType
|
||||
{
|
||||
public string Local { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We should be able to see properties defined in the base type.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestContainsKey_PropertyInherited_ReturnsTrue()
|
||||
{
|
||||
BaseType b = new DerivedType();
|
||||
PropertyDictionary dictionary = new PropertyDictionary(b);
|
||||
bool result = dictionary.ContainsKey("Inherited");
|
||||
Assert.IsTrue(result, "The property name was not found.");
|
||||
}
|
||||
|
||||
private class PrivateType
|
||||
{
|
||||
private string Hidden { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We should not be able to see private properties.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestContainsKey_PropertyPrivate_ReturnsFalse()
|
||||
{
|
||||
PrivateType t = new PrivateType();
|
||||
PropertyDictionary dictionary = new PropertyDictionary(t);
|
||||
bool result = dictionary.ContainsKey("Hidden");
|
||||
Assert.IsFalse(result, "The property name was found.");
|
||||
}
|
||||
|
||||
private class StaticType
|
||||
{
|
||||
public static string Static { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We should not be able to see static properties.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestContainsKey_PropertyStatic_ReturnsFalse()
|
||||
{
|
||||
StaticType t = new StaticType();
|
||||
PropertyDictionary dictionary = new PropertyDictionary(t);
|
||||
bool result = dictionary.ContainsKey("Static");
|
||||
Assert.IsFalse(result, "The property name was found.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Keys
|
||||
|
||||
/// <summary>
|
||||
/// Keys should return the name of all of the property names in the object.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestKeys_GetsAllPropertyNames()
|
||||
{
|
||||
var person = new
|
||||
{
|
||||
Name = "Bob",
|
||||
Age = 23
|
||||
};
|
||||
PropertyDictionary dictionary = new PropertyDictionary(person);
|
||||
ICollection<string> keys = dictionary.Keys;
|
||||
Assert.AreEqual(2, keys.Count, "The wrong number of keys were returned.");
|
||||
Assert.IsTrue(keys.Contains("Name"), "The Name property was not found.");
|
||||
Assert.IsTrue(keys.Contains("Age"), "The Age property was not found.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Remove
|
||||
|
||||
/// <summary>
|
||||
/// Since a property dictionary is just a wrapper around an object, we cannot remove properties from it.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NotSupportedException))]
|
||||
public void TestRemove_IDictionary_ThrowsException()
|
||||
{
|
||||
object instance = new object();
|
||||
IDictionary<string, object> dictionary = new PropertyDictionary(instance);
|
||||
dictionary.Remove("Name");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Since a property dictionary is just a wrapper around an object, we cannot remove properties from it.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NotSupportedException))]
|
||||
public void TestRemove_ICollection_ThrowsException()
|
||||
{
|
||||
object instance = new object();
|
||||
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(instance);
|
||||
collection.Remove(new KeyValuePair<string, object>("Name", "Whatever"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryGetValue
|
||||
|
||||
/// <summary>
|
||||
/// If we try to get the value for a property that doesn't exist, false should returned and object set to null.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestTryGetValue_NoSuchProperty_ReturnsFalse()
|
||||
{
|
||||
var instance = new { };
|
||||
PropertyDictionary dictionary = new PropertyDictionary(instance);
|
||||
object value;
|
||||
bool result = dictionary.TryGetValue("Name", out value);
|
||||
Assert.IsFalse(result, "The property should not have been found.");
|
||||
Assert.IsNull(value, "The value should have been null.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we try to get the value for a property that doesn't exist, false should returned and object set to null.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestTryGetValue_PropertyExists_ReturnsTrue()
|
||||
{
|
||||
var instance = new
|
||||
{
|
||||
Name = "Test"
|
||||
};
|
||||
PropertyDictionary dictionary = new PropertyDictionary(instance);
|
||||
object value;
|
||||
bool result = dictionary.TryGetValue("Name", out value);
|
||||
Assert.IsTrue(result, "The property should have been found.");
|
||||
Assert.AreEqual(instance.Name, value, "The value should have equaled the wrapped property value.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Values
|
||||
|
||||
/// <summary>
|
||||
/// We should be able to get the value of all of the properties.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestValues_GetsValues()
|
||||
{
|
||||
var instance = new
|
||||
{
|
||||
Name = "Bob",
|
||||
Age = 23
|
||||
};
|
||||
PropertyDictionary dictionary = new PropertyDictionary(instance);
|
||||
ICollection<object> values = dictionary.Values;
|
||||
Assert.AreEqual(2, values.Count, "The wrong number of values were returned.");
|
||||
Assert.IsTrue(values.Contains("Bob"), "The value for Name was not found.");
|
||||
Assert.IsTrue(values.Contains(23), "The value for Age was not found.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Indexer
|
||||
|
||||
/// <summary>
|
||||
/// If we try to retrieve the value for a property that does not exist, an exception
|
||||
/// should be thrown.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(KeyNotFoundException))]
|
||||
public void TestIndexer_Getter_NoSuchProperty_ThrowsException()
|
||||
{
|
||||
object instance = new object();
|
||||
PropertyDictionary dictionary = new PropertyDictionary(instance);
|
||||
object value = dictionary["Name"];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we try to get a value for a property that exists, the value should
|
||||
/// be returned.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestIndexer_Getter_PropertyExists_ReturnsValue()
|
||||
{
|
||||
var instance = new
|
||||
{
|
||||
Name = "Bob"
|
||||
};
|
||||
PropertyDictionary dictionary = new PropertyDictionary(instance);
|
||||
object value = dictionary["Name"];
|
||||
Assert.AreSame(instance.Name, value, "The wrong value was returned.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we try to set the value for a property, an exception should be thrown.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NotSupportedException))]
|
||||
public void TestIndexer_Setter_ThrowsException()
|
||||
{
|
||||
PropertyDictionary dictionary = new PropertyDictionary(new { Name = 123 });
|
||||
dictionary["Name"] = 123;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Clear
|
||||
|
||||
/// <summary>
|
||||
/// Since the dictionary is just a wrapper, Clear will simply throw an exception.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NotSupportedException))]
|
||||
public void TestClear_ThrowsException()
|
||||
{
|
||||
object instance = new object();
|
||||
ICollection<KeyValuePair<string, object>> dictionary = new PropertyDictionary(instance);
|
||||
dictionary.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Contains
|
||||
|
||||
/// <summary>
|
||||
/// Contains should find the key/value pair if both the key and value are equal.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestContains_Explicit_PairExists_ReturnsTrue()
|
||||
{
|
||||
var person = new
|
||||
{
|
||||
Name = "Bob"
|
||||
};
|
||||
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(person);
|
||||
bool contains = collection.Contains(new KeyValuePair<string, object>("Name", "Bob"));
|
||||
Assert.IsTrue(contains, "Did not find the pair.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains should not find the key/value pair if the keys are not equal.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestContains_Explicit_KeyDoesNotMatch_ReturnsFalse()
|
||||
{
|
||||
var person = new
|
||||
{
|
||||
Name = "Bob"
|
||||
};
|
||||
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(person);
|
||||
bool contains = collection.Contains(new KeyValuePair<string, object>("Age", "Bob"));
|
||||
Assert.IsFalse(contains, "The pair should not have been found.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains should not find the key/value pair if the values are not equal.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestContains_Explicit_ValueDoesNotMatch_ReturnsFalse()
|
||||
{
|
||||
var person = new
|
||||
{
|
||||
Name = "Bob"
|
||||
};
|
||||
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(person);
|
||||
bool contains = collection.Contains(new KeyValuePair<string, object>("Name", "Sally"));
|
||||
Assert.IsFalse(contains, "The pair should not have been found.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CopyTo
|
||||
|
||||
/// <summary>
|
||||
/// CopyTo should copy the key/value pairs to an array, assuming there is enough room.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestCopyTo_Explicit()
|
||||
{
|
||||
var instance = new
|
||||
{
|
||||
Name = "Bob",
|
||||
Age = 23
|
||||
};
|
||||
ICollection<KeyValuePair<string, object>> collection = new PropertyDictionary(instance);
|
||||
|
||||
KeyValuePair<string, object>[] array = new KeyValuePair<string, object>[collection.Count];
|
||||
int arrayIndex = 0;
|
||||
collection.CopyTo(array, arrayIndex);
|
||||
|
||||
Assert.IsTrue(array.Contains(new KeyValuePair<string, object>("Name", "Bob")), "The name property was not found.");
|
||||
Assert.IsTrue(array.Contains(new KeyValuePair<string, object>("Age", 23)), "The age property was not found.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetEnumerator
|
||||
|
||||
/// <summary>
|
||||
/// All the items should be enumerated in the dictionary.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestGetEnumerator_EnumeratesAllItems()
|
||||
{
|
||||
var instance = new
|
||||
{
|
||||
Name = "Bob",
|
||||
Age = 23
|
||||
};
|
||||
IEnumerable<KeyValuePair<string, object>> dictionary = new PropertyDictionary(instance);
|
||||
|
||||
Assert.IsTrue(enumerate(dictionary).Contains(new KeyValuePair<string, object>("Name", "Bob")), "The first pair was not present.");
|
||||
Assert.IsTrue(enumerate(dictionary).Contains(new KeyValuePair<string, object>("Age", 23)), "The second pair was not present.");
|
||||
}
|
||||
|
||||
private static IEnumerable<T> enumerate<T>(IEnumerable<T> enumerable)
|
||||
{
|
||||
List<T> items = new List<T>();
|
||||
foreach (T item in enumerable)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All the items should be enumerated in the dictionary.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestGetEnumerator_Explicit_EnumeratesAllItems()
|
||||
{
|
||||
var instance = new
|
||||
{
|
||||
Name = "Bob",
|
||||
Age = 23
|
||||
};
|
||||
IEnumerable dictionary = new PropertyDictionary(instance);
|
||||
|
||||
Assert.IsTrue(enumerate(dictionary).Cast<KeyValuePair<string, object>>().Contains(new KeyValuePair<string, object>("Name", "Bob")), "The first pair was not present.");
|
||||
Assert.IsTrue(enumerate(dictionary).Cast<KeyValuePair<string, object>>().Contains(new KeyValuePair<string, object>("Age", 23)), "The second pair was not present.");
|
||||
}
|
||||
|
||||
private static IEnumerable enumerate(IEnumerable enumerable)
|
||||
{
|
||||
ArrayList items = new ArrayList();
|
||||
foreach (object item in enumerable)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -36,9 +36,6 @@
|
|||
<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">
|
||||
|
@ -47,8 +44,6 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="FormatterTester.cs" />
|
||||
<Compile Include="PropertyDictionaryTester.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\mustache-sharp\mustache-sharp.csproj">
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class CompoundBuilder : IBuilder
|
||||
{
|
||||
private readonly List<IBuilder> builders;
|
||||
|
||||
public CompoundBuilder()
|
||||
{
|
||||
builders = new List<IBuilder>();
|
||||
}
|
||||
|
||||
public void AddBuilder(IBuilder builder)
|
||||
{
|
||||
builders.Add(builder);
|
||||
}
|
||||
|
||||
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
|
||||
{
|
||||
foreach (IBuilder builder in builders)
|
||||
{
|
||||
builder.Build(scope, output, provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class CompoundGenerator : IGenerator
|
||||
{
|
||||
private readonly List<IGenerator> _generators;
|
||||
|
||||
public CompoundGenerator()
|
||||
{
|
||||
_generators = new List<IGenerator>();
|
||||
}
|
||||
|
||||
public void AddGenerator(StaticGenerator generator)
|
||||
{
|
||||
_generators.Add(generator);
|
||||
}
|
||||
|
||||
string IGenerator.GetText(object source)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (IGenerator generator in _generators)
|
||||
{
|
||||
builder.Append(generator.GetText(source));
|
||||
}
|
||||
string innerText = builder.ToString();
|
||||
// TODO - process with tag's custom handler
|
||||
return innerText;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class EachBuilder : IBuilder
|
||||
{
|
||||
private readonly CompoundBuilder builder;
|
||||
|
||||
public EachBuilder()
|
||||
{
|
||||
builder = new CompoundBuilder();
|
||||
}
|
||||
|
||||
public string Key
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public CompoundBuilder Builder
|
||||
{
|
||||
get { return builder; }
|
||||
}
|
||||
|
||||
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
|
||||
{
|
||||
object value = scope.Find(Key);
|
||||
IEnumerable enumerable = value as IEnumerable;
|
||||
if (enumerable == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (object item in enumerable)
|
||||
{
|
||||
IDictionary<string, object> lookup = item as IDictionary<string, object>;
|
||||
if (lookup == null)
|
||||
{
|
||||
lookup = new PropertyDictionary(item);
|
||||
}
|
||||
Scope itemScope = scope.CreateChildScope(item);
|
||||
builder.Build(itemScope, output, provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a format string and returns the text generator.
|
||||
/// </summary>
|
||||
internal sealed class FormatParser
|
||||
{
|
||||
private const string key = @"[_\w][_\w\d]*";
|
||||
private const string compoundKey = key + @"(\." + key + ")*";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a FormatParser.
|
||||
/// </summary>
|
||||
public FormatParser()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a text generator based on the given format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format to parse.</param>
|
||||
/// <returns>The text generator.</returns>
|
||||
public IGenerator Build(string format)
|
||||
{
|
||||
TagDefinition definition = new TagDefinition("builtins");
|
||||
definition.HasBody = true;
|
||||
CompoundGenerator generator = new CompoundGenerator();
|
||||
TagScope tagScope = new TagScope();
|
||||
registerTags(definition, tagScope);
|
||||
Match match = findNextTag(definition, format, 0);
|
||||
buildCompoundGenerator(definition, tagScope, generator, format, 0, match);
|
||||
return generator;
|
||||
}
|
||||
|
||||
private static void registerTags(TagDefinition definition, TagScope scope)
|
||||
{
|
||||
foreach (TagDefinition childTag in definition.ChildTags)
|
||||
{
|
||||
scope.AddTag(childTag);
|
||||
}
|
||||
}
|
||||
|
||||
private static Match findNextTag(TagDefinition definition, string format, int formatIndex)
|
||||
{
|
||||
List<string> matches = new List<string>();
|
||||
matches.Add(getKeyRegex());
|
||||
matches.Add(getClosingTagRegex(definition));
|
||||
matches.Add(getCommentTagRegex());
|
||||
foreach (TagDefinition childTag in definition.ChildTags)
|
||||
{
|
||||
matches.Add(getTagRegex(childTag));
|
||||
}
|
||||
string match = "{{(" + String.Join("|", matches) + ")}}";
|
||||
Regex regex = new Regex(match);
|
||||
return regex.Match(format, formatIndex);
|
||||
}
|
||||
|
||||
private static string getClosingTagRegex(TagDefinition definition)
|
||||
{
|
||||
StringBuilder regexBuilder = new StringBuilder();
|
||||
regexBuilder.Append(@"(?<close>(/");
|
||||
regexBuilder.Append(definition.Name);
|
||||
regexBuilder.Append(@"\s*?))");
|
||||
return regexBuilder.ToString();
|
||||
}
|
||||
|
||||
private static string getCommentTagRegex()
|
||||
{
|
||||
return @"(?<comment>#!.*?)";
|
||||
}
|
||||
|
||||
private static string getKeyRegex()
|
||||
{
|
||||
return @"((?<key>" + compoundKey + @")(,(?<alignment>(-)?[\d]+))?(:(?<format>.*?))?)";
|
||||
}
|
||||
|
||||
private static string getTagRegex(TagDefinition definition)
|
||||
{
|
||||
StringBuilder regexBuilder = new StringBuilder();
|
||||
regexBuilder.Append(@"(?<open>(#(?<name>");
|
||||
regexBuilder.Append(definition.Name);
|
||||
regexBuilder.Append(@")");
|
||||
foreach (TagParameter parameter in definition.Parameters)
|
||||
{
|
||||
regexBuilder.Append(@"\s+?");
|
||||
regexBuilder.Append(@"(?<argument>");
|
||||
regexBuilder.Append(compoundKey);
|
||||
regexBuilder.Append(@")");
|
||||
}
|
||||
regexBuilder.Append(@"\s*?))");
|
||||
return regexBuilder.ToString();
|
||||
}
|
||||
|
||||
private static int buildCompoundGenerator(TagDefinition tagDefinition, TagScope scope, CompoundGenerator generator, string format, int formatIndex, Match match)
|
||||
{
|
||||
bool done = false;
|
||||
while (!done)
|
||||
{
|
||||
string leading = format.Substring(formatIndex, match.Index - formatIndex);
|
||||
formatIndex = match.Index + match.Length;
|
||||
|
||||
if (match.Groups["comment"].Success)
|
||||
{
|
||||
// TODO - process comment
|
||||
}
|
||||
else if (match.Groups["close"].Success)
|
||||
{
|
||||
// TODO - process closing tag
|
||||
done = true;
|
||||
}
|
||||
else if (match.Groups["open"].Success)
|
||||
{
|
||||
string tagName = match.Groups["name"].Value;
|
||||
TagDefinition nextDefinition = scope.Find(tagName);
|
||||
if (nextDefinition == null)
|
||||
{
|
||||
// TODO - handle missing tag definition
|
||||
}
|
||||
if (nextDefinition.HasBody)
|
||||
{
|
||||
CompoundGenerator nextGenerator = new CompoundGenerator();
|
||||
TagScope nextScope = new TagScope(scope);
|
||||
registerTags(nextDefinition, nextScope);
|
||||
Match nextMatch = findNextTag(nextDefinition, format, formatIndex);
|
||||
formatIndex = buildCompoundGenerator(nextDefinition, nextScope, nextGenerator, format, formatIndex, nextMatch);
|
||||
// TODO - grab the generated text and parameters and pass it to the tag's processor
|
||||
// TODO - a parameter can be a key or a default value
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO - grab all of the parameters and pass them to the tag's generator
|
||||
// TODO - a parameter can be a key or a default value
|
||||
}
|
||||
}
|
||||
else if (match.Groups["key"].Success)
|
||||
{
|
||||
string alignment = match.Groups["alignment"].Value;
|
||||
string formatting = match.Groups["format"].Value;
|
||||
// TODO - create a key generator
|
||||
}
|
||||
}
|
||||
return formatIndex;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,303 +0,0 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using mustache.Properties;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows for the generation of a string based on formatted template.
|
||||
/// </summary>
|
||||
public sealed class Formatter
|
||||
{
|
||||
private readonly CompoundBuilder builder;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a Formatter using the given format string.
|
||||
/// </summary>
|
||||
/// <param name="format">The string containing the placeholders to use as a template.</param>
|
||||
/// <exception cref="System.ArgumentNullException">The format string is null.</exception>
|
||||
/// <exception cref="System.FormatException">The format string is invald.</exception>
|
||||
public Formatter(string format)
|
||||
{
|
||||
if (format == null)
|
||||
{
|
||||
throw new ArgumentNullException("format");
|
||||
}
|
||||
builder = new CompoundBuilder();
|
||||
|
||||
List<string> names = new List<string>();
|
||||
const string key = @"[_\w][_\w\d]*";
|
||||
const string compoundKey = key + @"(\." + key + ")*";
|
||||
const string openIfMatch = @"(?<open_if>(#if\s+?" + compoundKey + @"\s*?))";
|
||||
const string elifMatch = @"(?<elif>(#elif\s+?" + compoundKey + @"\s*?))";
|
||||
const string elseMatch = @"(?<else>(#else\s*?))";
|
||||
const string closeIfMatch = @"(?<close_if>(/if\s*?))";
|
||||
const string openEachMatch = @"(?<open_each>(#each\s+?" + compoundKey + @"\s*?))";
|
||||
const string closeEachMatch = @"(?<close_each>(/each\s*?))";
|
||||
const string openWithMatch = @"(?<open_with>(#with\s+?" + compoundKey + @"\s*?))";
|
||||
const string closeWithMatch = @"(?<close_with>(/with\s*?))";
|
||||
const string commentMatch = @"(?<comment>#!.*?)";
|
||||
const string keyMatch = @"((?<key>" + compoundKey + @")(,(?<alignment>(-)?[\d]+))?(:(?<format>.*?))?)";
|
||||
const string match = "{{(" + openIfMatch + "|"
|
||||
+ elifMatch + "|"
|
||||
+ elseMatch + "|"
|
||||
+ closeIfMatch + "|"
|
||||
+ openEachMatch + "|"
|
||||
+ closeEachMatch + "|"
|
||||
+ openWithMatch + "|"
|
||||
+ closeWithMatch + "|"
|
||||
+ commentMatch + "|"
|
||||
+ keyMatch + ")}}";
|
||||
Regex formatFinder = new Regex(match, RegexOptions.Compiled);
|
||||
List<Match> matches = formatFinder.Matches(format).Cast<Match>().ToList();
|
||||
using (IEnumerator<Match> matchEnumerator = matches.GetEnumerator())
|
||||
{
|
||||
Trimmer trimmer = new Trimmer();
|
||||
int formatIndex = buildCompoundBuilder(builder, trimmer, format, 0, matchEnumerator);
|
||||
StaticBuilder trailingBuilder = new StaticBuilder();
|
||||
string value = format.Substring(formatIndex);
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.None, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substitutes the placeholders in the format string with the values found in the object.
|
||||
/// </summary>
|
||||
/// <param name="format">The string containing the placeholders to use as a template.</param>
|
||||
/// <param name="value">The object to use to replace the placeholders.</param>
|
||||
/// <returns>The format string with the placeholders substituted for by the object values.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">The format string is null.</exception>
|
||||
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A property was not found in the value.</exception>
|
||||
public static string Format(string format, object value)
|
||||
{
|
||||
Formatter formatter = new Formatter(format);
|
||||
return formatter.Format(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substitutes the placeholders in the format string with the values found in the object.
|
||||
/// </summary>
|
||||
/// <param name="provider">The format provider to use -or- null to use the current culture.</param>
|
||||
/// <param name="format">The string containing the placeholders to use as a template.</param>
|
||||
/// <param name="value">The object to use to replace the placeholders.</param>
|
||||
/// <returns>The format string with the placeholders substituted for by the object values.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">The format string is null.</exception>
|
||||
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A property was not found in the value.</exception>
|
||||
public static string Format(IFormatProvider provider, string format, object value)
|
||||
{
|
||||
Formatter formatter = new Formatter(format);
|
||||
return formatter.Format(provider, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substitutes the placeholders in the format string with the values found in the given object.
|
||||
/// </summary>
|
||||
/// <param name="value">The object to use to replace the placeholders.</param>
|
||||
/// <returns>The format string with the placeholders substituted for by the lookup values.</returns>
|
||||
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A property was not found in the object.</exception>
|
||||
/// <remarks>A null value will be replaced with an empty string.</remarks>
|
||||
public string Format(object value)
|
||||
{
|
||||
return format(CultureInfo.CurrentCulture, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substitutes the placeholders in the format string with the values found in the given object.
|
||||
/// </summary>
|
||||
/// <param name="provider">The format provider to use -or- null to use the current culture.</param>
|
||||
/// <param name="value">The object to use to replace the placeholders.</param>
|
||||
/// <returns>The format string with the placeholders substituted for by the lookup values.</returns>
|
||||
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A property was not found in the object.</exception>
|
||||
/// <remarks>A null value will be replaced with an empty string.</remarks>
|
||||
public string Format(IFormatProvider provider, object value)
|
||||
{
|
||||
if (provider == null)
|
||||
{
|
||||
provider = CultureInfo.CurrentCulture;
|
||||
}
|
||||
return format(provider, value);
|
||||
}
|
||||
|
||||
private static int buildCompoundBuilder(CompoundBuilder builder, Trimmer trimmer, string format, int formatIndex, IEnumerator<Match> matches)
|
||||
{
|
||||
while (matches.MoveNext())
|
||||
{
|
||||
Match match = matches.Current;
|
||||
string value = format.Substring(formatIndex, match.Index - formatIndex);
|
||||
formatIndex = match.Index + match.Length;
|
||||
|
||||
Group keyGroup = match.Groups["key"];
|
||||
if (keyGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Singleton, IsOutput = true };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
Group alignmentGroup = match.Groups["alignment"];
|
||||
Group formatGroup = match.Groups["format"];
|
||||
KeyBuilder keyBuilder = new KeyBuilder()
|
||||
{
|
||||
Key = keyGroup.Value,
|
||||
Alignment = alignmentGroup.Value,
|
||||
Format = formatGroup.Value,
|
||||
};
|
||||
builder.AddBuilder(keyBuilder);
|
||||
continue;
|
||||
}
|
||||
|
||||
Group openIfGroup = match.Groups["open_if"];
|
||||
if (openIfGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Header, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
IfBuilder ifBuilder = new IfBuilder();
|
||||
ifBuilder.Key = openIfGroup.Value.Substring(4).Trim();
|
||||
formatIndex = buildIfBuilder(ifBuilder, true, trimmer, format, formatIndex, matches);
|
||||
builder.AddBuilder(ifBuilder);
|
||||
continue;
|
||||
}
|
||||
|
||||
Group openEachGroup = match.Groups["open_each"];
|
||||
if (openEachGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Header, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
EachBuilder eachBuilder = new EachBuilder();
|
||||
eachBuilder.Key = openEachGroup.Value.Substring(6).Trim();
|
||||
formatIndex = buildEachBuilder(eachBuilder, trimmer, format, formatIndex, matches);
|
||||
builder.AddBuilder(eachBuilder);
|
||||
continue;
|
||||
}
|
||||
|
||||
Group openWithGroup = match.Groups["open_with"];
|
||||
if (openWithGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Header, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
WithBuilder withBuilder = new WithBuilder();
|
||||
withBuilder.Key = openWithGroup.Value.Substring(6).Trim();
|
||||
formatIndex = buildWithBuilder(withBuilder, trimmer, format, formatIndex, matches);
|
||||
builder.AddBuilder(withBuilder);
|
||||
continue;
|
||||
}
|
||||
|
||||
Group commentGroup = match.Groups["comment"];
|
||||
if (commentGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Singleton, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
Group elifGroup = match.Groups["elif"];
|
||||
if (elifGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Singleton, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
break;
|
||||
}
|
||||
|
||||
Group elseGroup = match.Groups["else"];
|
||||
if (elseGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Singleton, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
break;
|
||||
}
|
||||
|
||||
Group closeIfGroup = match.Groups["close_if"];
|
||||
if (closeIfGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Footer, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
break;
|
||||
}
|
||||
|
||||
Group closeEachGroup = match.Groups["close_each"];
|
||||
if (closeEachGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Footer, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
break;
|
||||
}
|
||||
|
||||
Group closeWithGroup = match.Groups["close_with"];
|
||||
if (closeWithGroup.Success)
|
||||
{
|
||||
TagAttributes attributes = new TagAttributes() { Type = TagType.Footer, IsOutput = false };
|
||||
trimmer.AddStaticBuilder(builder, attributes, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return formatIndex;
|
||||
}
|
||||
|
||||
private static int buildIfBuilder(IfBuilder builder, bool expectClosingTag, Trimmer trimmer, string format, int formatIndex, IEnumerator<Match> matches)
|
||||
{
|
||||
formatIndex = buildCompoundBuilder(builder.TrueBuilder, trimmer, format, formatIndex, matches);
|
||||
Match match = matches.Current;
|
||||
if (match != null)
|
||||
{
|
||||
Group elifGroup = match.Groups["elif"];
|
||||
if (elifGroup.Success)
|
||||
{
|
||||
IfBuilder elifBuilder = new IfBuilder();
|
||||
elifBuilder.Key = elifGroup.Value.Substring(6).Trim();
|
||||
formatIndex = buildIfBuilder(elifBuilder, false, trimmer, format, formatIndex, matches);
|
||||
builder.FalseBuilder.AddBuilder(elifBuilder);
|
||||
}
|
||||
else
|
||||
{
|
||||
Group elseGroup = match.Groups["else"];
|
||||
if (elseGroup.Success)
|
||||
{
|
||||
formatIndex = buildCompoundBuilder(builder.FalseBuilder, trimmer, format, formatIndex, matches);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expectClosingTag)
|
||||
{
|
||||
Match closingMatch = matches.Current;
|
||||
checkClosingTag(closingMatch, "close_if", "if");
|
||||
}
|
||||
return formatIndex;
|
||||
}
|
||||
|
||||
private static int buildEachBuilder(EachBuilder builder, Trimmer trimmer, string format, int formatIndex, IEnumerator<Match> matches)
|
||||
{
|
||||
formatIndex = buildCompoundBuilder(builder.Builder, trimmer, format, formatIndex, matches);
|
||||
Match closingMatch = matches.Current;
|
||||
checkClosingTag(closingMatch, "close_each", "each");
|
||||
return formatIndex;
|
||||
}
|
||||
|
||||
private static int buildWithBuilder(WithBuilder builder, Trimmer trimmer, string format, int formatIndex, IEnumerator<Match> matches)
|
||||
{
|
||||
formatIndex = buildCompoundBuilder(builder.Builder, trimmer, format, formatIndex, matches);
|
||||
Match closingMatch = matches.Current;
|
||||
checkClosingTag(closingMatch, "close_with", "with");
|
||||
return formatIndex;
|
||||
}
|
||||
|
||||
private static void checkClosingTag(Match match, string expectedTag, string openingTag)
|
||||
{
|
||||
if (match == null || !match.Groups[expectedTag].Success)
|
||||
{
|
||||
string errorMessage = String.Format(CultureInfo.CurrentCulture, Resources.MissingClosingTag, openingTag);
|
||||
throw new FormatException(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private string format(IFormatProvider provider, object topLevel)
|
||||
{
|
||||
Scope scope = new Scope(topLevel);
|
||||
StringBuilder output = new StringBuilder();
|
||||
builder.Build(scope, output, provider);
|
||||
return output.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal interface IBuilder
|
||||
{
|
||||
void Build(Scope scope, StringBuilder output, IFormatProvider provider);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the values of an object to the format plan, generating a string.
|
||||
/// </summary>
|
||||
internal interface IGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the text when the values of the given object are applied to the format plan.
|
||||
/// </summary>
|
||||
/// <param name="source">The object whose values should be used to generate the text.</param>
|
||||
/// <returns>The generated text.</returns>
|
||||
string GetText(object source);
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class IfBuilder : IBuilder
|
||||
{
|
||||
private readonly CompoundBuilder trueBuilder;
|
||||
private readonly CompoundBuilder falseBuilder;
|
||||
|
||||
public IfBuilder()
|
||||
{
|
||||
trueBuilder = new CompoundBuilder();
|
||||
falseBuilder = new CompoundBuilder();
|
||||
}
|
||||
|
||||
public string Key
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public CompoundBuilder TrueBuilder
|
||||
{
|
||||
get { return trueBuilder; }
|
||||
}
|
||||
|
||||
public CompoundBuilder FalseBuilder
|
||||
{
|
||||
get { return falseBuilder; }
|
||||
}
|
||||
|
||||
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
|
||||
{
|
||||
object value = scope.Find(Key);
|
||||
bool truthyness = getTruthyness(value);
|
||||
if (truthyness)
|
||||
{
|
||||
trueBuilder.Build(scope, output, provider);
|
||||
}
|
||||
else
|
||||
{
|
||||
falseBuilder.Build(scope, output, provider);
|
||||
}
|
||||
}
|
||||
|
||||
private bool getTruthyness(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
IEnumerable enumerable = value as IEnumerable;
|
||||
if (enumerable != null)
|
||||
{
|
||||
return enumerable.Cast<object>().Any();
|
||||
}
|
||||
if (value is Char)
|
||||
{
|
||||
return (Char)value != '\0';
|
||||
}
|
||||
try
|
||||
{
|
||||
decimal number = (decimal)Convert.ChangeType(value, typeof(decimal));
|
||||
return number != 0.0m;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class KeyBuilder : IBuilder
|
||||
{
|
||||
public KeyBuilder()
|
||||
{
|
||||
}
|
||||
|
||||
public string Key
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string Alignment
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string Format
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
|
||||
{
|
||||
object value = scope.Find(Key);
|
||||
StringBuilder format = new StringBuilder();
|
||||
format.Append("{");
|
||||
format.Append("0");
|
||||
if (!String.IsNullOrWhiteSpace(Alignment))
|
||||
{
|
||||
format.Append(",");
|
||||
format.Append(Alignment);
|
||||
}
|
||||
if (!String.IsNullOrWhiteSpace(Format))
|
||||
{
|
||||
format.Append(":");
|
||||
format.Append(Format);
|
||||
}
|
||||
format.Append("}");
|
||||
output.AppendFormat(provider, format.ToString(), value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using mustache.Properties;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a scope of keys.
|
||||
/// </summary>
|
||||
public sealed class KeyScope
|
||||
{
|
||||
private readonly object _source;
|
||||
private readonly KeyScope _parent;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a KeyScope.
|
||||
/// </summary>
|
||||
/// <param name="source">The object to search for keys in.</param>
|
||||
internal KeyScope(object source)
|
||||
: this(source, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a KeyScope.
|
||||
/// </summary>
|
||||
/// <param name="source">The object to search for keys in.</param>
|
||||
/// <param name="parent">The parent scope to search in if the value is not found.</param>
|
||||
internal KeyScope(object source, KeyScope parent)
|
||||
{
|
||||
_parent = parent;
|
||||
_source = source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a child scope that searches for keys in the given object.
|
||||
/// </summary>
|
||||
/// <param name="source">The object to search for keys in.</param>
|
||||
/// <returns>The new child scope.</returns>
|
||||
internal KeyScope CreateChildScope(object source)
|
||||
{
|
||||
KeyScope scope = new KeyScope(source, this);
|
||||
return scope;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find the value associated with the key with given name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the key.</param>
|
||||
/// <returns>The value associated with the key with the given name.</returns>
|
||||
/// <exception cref="System.Collections.Generic.KeyNotFoundException">A key with the given name could not be found.</exception>
|
||||
public object Find(string name)
|
||||
{
|
||||
string[] names = name.Split('.');
|
||||
string member = names[0];
|
||||
object nextLevel = _source;
|
||||
if (member != "this")
|
||||
{
|
||||
nextLevel = find(member);
|
||||
}
|
||||
for (int index = 1; index < names.Length; ++index)
|
||||
{
|
||||
IDictionary<string, object> context = toLookup(nextLevel);
|
||||
member = names[index];
|
||||
nextLevel = context[member];
|
||||
}
|
||||
return nextLevel;
|
||||
}
|
||||
|
||||
private object find(string name)
|
||||
{
|
||||
IDictionary<string, object> lookup = toLookup(_source);
|
||||
if (lookup.ContainsKey(name))
|
||||
{
|
||||
return lookup[name];
|
||||
}
|
||||
if (_parent == null)
|
||||
{
|
||||
string message = String.Format(CultureInfo.CurrentCulture, Resources.KeyNotFound, name);
|
||||
throw new KeyNotFoundException(message);
|
||||
}
|
||||
return _parent.find(name);
|
||||
}
|
||||
|
||||
private static IDictionary<string, object> toLookup(object value)
|
||||
{
|
||||
IDictionary<string, object> lookup = value as IDictionary<string, object>;
|
||||
if (lookup == null)
|
||||
{
|
||||
lookup = new PropertyDictionary(value);
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -61,21 +61,39 @@ namespace mustache.Properties {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to A key or property was not found with the given name..
|
||||
/// Looks up a localized string similar to An attempt was made to define a parameter with a null or an invalid identifier..
|
||||
/// </summary>
|
||||
internal static string BlankParameterName {
|
||||
get {
|
||||
return ResourceManager.GetString("BlankParameterName", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to An attempt was made to define a tag with a null or an invalid identifier..
|
||||
/// </summary>
|
||||
internal static string BlankTagName {
|
||||
get {
|
||||
return ResourceManager.GetString("BlankTagName", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to A parameter with the same name already exists within the tag..
|
||||
/// </summary>
|
||||
internal static string DuplicateParameter {
|
||||
get {
|
||||
return ResourceManager.GetString("DuplicateParameter", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The key {0} could not be found..
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,10 +117,16 @@
|
|||
<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 name="BlankParameterName" xml:space="preserve">
|
||||
<value>An attempt was made to define a parameter with a null or an invalid identifier.</value>
|
||||
</data>
|
||||
<data name="MissingClosingTag" xml:space="preserve">
|
||||
<value>A matching closing tag was not found for the {0} tag.</value>
|
||||
<data name="BlankTagName" xml:space="preserve">
|
||||
<value>An attempt was made to define a tag with a null or an invalid identifier.</value>
|
||||
</data>
|
||||
<data name="DuplicateParameter" xml:space="preserve">
|
||||
<value>A parameter with the same name already exists within the tag.</value>
|
||||
</data>
|
||||
<data name="KeyNotFound" xml:space="preserve">
|
||||
<value>The key {0} could not be found.</value>
|
||||
</data>
|
||||
</root>
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides utility methods that require regular expressions.
|
||||
/// </summary>
|
||||
public static class RegexHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the given name is a legal identifier.
|
||||
/// </summary>
|
||||
/// <param name="name">The name to check.</param>
|
||||
/// <returns>True if the name is a legal identifier; otherwise, false.</returns>
|
||||
public static bool IsValidIdentifier(string name)
|
||||
{
|
||||
Regex regex = new Regex(@"^[_\w][_\w\d]*$");
|
||||
return regex.IsMatch(name);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using mustache.Properties;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class Scope
|
||||
{
|
||||
private readonly object topLevel;
|
||||
private Scope parent;
|
||||
|
||||
public Scope(object topLevel)
|
||||
{
|
||||
parent = null;
|
||||
this.topLevel = topLevel;
|
||||
}
|
||||
|
||||
public Scope CreateChildScope(object topLevel)
|
||||
{
|
||||
Scope scope = new Scope(topLevel);
|
||||
scope.parent = this;
|
||||
return scope;
|
||||
}
|
||||
|
||||
public object Find(string name)
|
||||
{
|
||||
string[] names = name.Split('.');
|
||||
string member = names[0];
|
||||
object nextLevel = topLevel;
|
||||
if (member != "this")
|
||||
{
|
||||
nextLevel = find(member);
|
||||
}
|
||||
for (int index = 1; index < names.Length; ++index)
|
||||
{
|
||||
IDictionary<string, object> context = toLookup(nextLevel);
|
||||
member = names[index];
|
||||
nextLevel = context[member];
|
||||
}
|
||||
return nextLevel;
|
||||
}
|
||||
|
||||
private object find(string name)
|
||||
{
|
||||
IDictionary<string, object> lookup = toLookup(topLevel);
|
||||
if (lookup.ContainsKey(name))
|
||||
{
|
||||
return lookup[name];
|
||||
}
|
||||
if (parent == null)
|
||||
{
|
||||
string message = String.Format(Resources.KeyNotFound, name);
|
||||
throw new KeyNotFoundException(message);
|
||||
}
|
||||
return parent.find(name);
|
||||
}
|
||||
|
||||
private static IDictionary<string, object> toLookup(object value)
|
||||
{
|
||||
IDictionary<string, object> lookup = value as IDictionary<string, object>;
|
||||
if (lookup == null)
|
||||
{
|
||||
lookup = new PropertyDictionary(value);
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class StaticBuilder : IBuilder
|
||||
{
|
||||
public StaticBuilder()
|
||||
{
|
||||
}
|
||||
|
||||
public string Value
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
|
||||
{
|
||||
output.Append(Value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class StaticGenerator : IGenerator
|
||||
{
|
||||
private readonly string _value;
|
||||
|
||||
public StaticGenerator(string value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
string IGenerator.GetText(object source)
|
||||
{
|
||||
return _value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class TagAttributes
|
||||
{
|
||||
public TagAttributes()
|
||||
{
|
||||
}
|
||||
|
||||
public TagType Type
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool IsOutput
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using mustache.Properties;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the attributes of a custom tag.
|
||||
/// </summary>
|
||||
public sealed class TagDefinition
|
||||
{
|
||||
private readonly string _tagName;
|
||||
private readonly List<TagParameter> _parameters;
|
||||
private readonly List<TagDefinition> _childTagDefinitions;
|
||||
private TagParameter _scopeParameter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a TagDefinition.
|
||||
/// </summary>
|
||||
/// <param name="tagName">The name of the tag.</param>
|
||||
/// <exception cref="System.ArgumentException">The name of the tag is null or blank.</exception>
|
||||
public TagDefinition(string tagName)
|
||||
{
|
||||
if (!RegexHelper.IsValidIdentifier(tagName))
|
||||
{
|
||||
throw new ArgumentException(Resources.BlankTagName, "tagName");
|
||||
}
|
||||
_tagName = tagName;
|
||||
_parameters = new List<TagParameter>();
|
||||
_childTagDefinitions = new List<TagDefinition>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the tag.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get { return _tagName; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the tag expects the given parameter information.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to add.</param>
|
||||
/// <exception cref="System.ArgumentNullException">The parameter is null.</exception>
|
||||
/// <exception cref="System.ArgumentException">A parameter with the same name already exists.</exception>
|
||||
public void AddParameter(TagParameter parameter)
|
||||
{
|
||||
if (parameter == null)
|
||||
{
|
||||
throw new ArgumentNullException("parameter");
|
||||
}
|
||||
if (_parameters.Any(p => p.Name == parameter.Name))
|
||||
{
|
||||
throw new ArgumentException(Resources.DuplicateParameter, "parameter");
|
||||
}
|
||||
_parameters.Add(parameter);
|
||||
if (parameter.IsScopeContext)
|
||||
{
|
||||
_scopeParameter = parameter;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters that are defined for the tag.
|
||||
/// </summary>
|
||||
public IEnumerable<TagParameter> Parameters
|
||||
{
|
||||
get { return new ReadOnlyCollection<TagParameter>(_parameters); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the tag contains content.
|
||||
/// </summary>
|
||||
public bool HasBody
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the tag defines a new scope based on an argument.
|
||||
/// </summary>
|
||||
public bool IsScoped
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the given tag is in scope within the current tag.
|
||||
/// </summary>
|
||||
/// <param name="childTag">The tag that is in scope within the current tag.</param>
|
||||
public void AddChildTag(TagDefinition childTag)
|
||||
{
|
||||
if (childTag == null)
|
||||
{
|
||||
throw new ArgumentNullException("childTag");
|
||||
}
|
||||
_childTagDefinitions.Add(childTag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags that are in scope within the current tag.
|
||||
/// </summary>
|
||||
public IEnumerable<TagDefinition> ChildTags
|
||||
{
|
||||
get { return new ReadOnlyCollection<TagDefinition>(_childTagDefinitions); }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using mustache.Properties;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a parameter belonging to a custom tag.
|
||||
/// </summary>
|
||||
public sealed class TagParameter
|
||||
{
|
||||
private readonly string _name;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of a TagParameter.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter.</param>
|
||||
/// <exception cref="System.ArgumentException">The parameter name is null or an invalid identifier.</exception>
|
||||
public TagParameter(string parameterName)
|
||||
{
|
||||
if (!RegexHelper.IsValidIdentifier(parameterName))
|
||||
{
|
||||
throw new ArgumentException(Resources.BlankParameterName, "parameterName");
|
||||
}
|
||||
_name = parameterName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the parameter.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get { return _name; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the parameter should be used to define the parameter.
|
||||
/// </summary>
|
||||
public bool IsScopeContext
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the field is required.
|
||||
/// </summary>
|
||||
public bool IsRequired
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default value to use when an argument is not provided
|
||||
/// for the parameter.
|
||||
/// </summary>
|
||||
public object DefaultValue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class TagScope
|
||||
{
|
||||
private readonly TagScope _parent;
|
||||
private readonly Dictionary<string, TagDefinition> _tagLookup;
|
||||
|
||||
public TagScope()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public TagScope(TagScope parent)
|
||||
{
|
||||
_parent = parent;
|
||||
_tagLookup = new Dictionary<string, TagDefinition>();
|
||||
}
|
||||
|
||||
public void AddTag(TagDefinition tagDefinition)
|
||||
{
|
||||
_tagLookup.Add(tagDefinition.Name, tagDefinition);
|
||||
}
|
||||
|
||||
public TagDefinition Find(string tagName)
|
||||
{
|
||||
TagDefinition definition;
|
||||
if (_tagLookup.TryGetValue(tagName, out definition))
|
||||
{
|
||||
return definition;
|
||||
}
|
||||
if (_parent == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _parent.Find(tagName);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal enum TagType
|
||||
{
|
||||
None,
|
||||
Singleton,
|
||||
Header,
|
||||
Footer,
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class Trimmer
|
||||
{
|
||||
private bool hasHeader;
|
||||
private bool hasFooter;
|
||||
private bool hasTag;
|
||||
private bool canTrim;
|
||||
|
||||
public Trimmer()
|
||||
{
|
||||
hasTag = false;
|
||||
canTrim = true;
|
||||
}
|
||||
|
||||
public void AddStaticBuilder(CompoundBuilder builder, TagAttributes attributes, string value)
|
||||
{
|
||||
string trimmed = value;
|
||||
int newline = value.IndexOf(Environment.NewLine);
|
||||
if (newline == -1)
|
||||
{
|
||||
canTrim &= String.IsNullOrWhiteSpace(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// finish processing the previous line
|
||||
if (canTrim && hasTag && (!hasHeader || !hasFooter))
|
||||
{
|
||||
string lineEnd = trimmed.Substring(0, newline);
|
||||
if (String.IsNullOrWhiteSpace(lineEnd))
|
||||
{
|
||||
trimmed = trimmed.Substring(newline + Environment.NewLine.Length);
|
||||
}
|
||||
}
|
||||
// start processing the next line
|
||||
hasTag = false;
|
||||
hasHeader = false;
|
||||
hasFooter = false;
|
||||
int lastNewline = value.LastIndexOf(Environment.NewLine);
|
||||
string lineStart = value.Substring(lastNewline + Environment.NewLine.Length);
|
||||
canTrim = String.IsNullOrWhiteSpace(lineStart);
|
||||
}
|
||||
hasTag |= attributes.Type != TagType.None;
|
||||
hasHeader |= attributes.Type == TagType.Header;
|
||||
hasFooter |= hasHeader && attributes.Type == TagType.Footer;
|
||||
canTrim &= !attributes.IsOutput;
|
||||
if (trimmed.Length > 0)
|
||||
{
|
||||
StaticBuilder leading = new StaticBuilder();
|
||||
leading.Value = trimmed;
|
||||
builder.AddBuilder(leading);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace mustache
|
||||
{
|
||||
internal sealed class WithBuilder : IBuilder
|
||||
{
|
||||
private readonly CompoundBuilder builder;
|
||||
|
||||
public WithBuilder()
|
||||
{
|
||||
builder = new CompoundBuilder();
|
||||
}
|
||||
|
||||
public string Key
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public CompoundBuilder Builder
|
||||
{
|
||||
get { return builder; }
|
||||
}
|
||||
|
||||
public void Build(Scope scope, StringBuilder output, IFormatProvider provider)
|
||||
{
|
||||
object value = scope.Find(Key);
|
||||
Scope valueScope = scope.CreateChildScope(value);
|
||||
builder.Build(valueScope, output, provider);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,16 +32,11 @@
|
|||
</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="CompoundGenerator.cs" />
|
||||
<Compile Include="FormatParser.cs" />
|
||||
<Compile Include="IGenerator.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
|
@ -49,12 +44,12 @@
|
|||
<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" />
|
||||
<Compile Include="RegexHelper.cs" />
|
||||
<Compile Include="StaticGenerator.cs" />
|
||||
<Compile Include="TagDefinition.cs" />
|
||||
<Compile Include="TagParameter.cs" />
|
||||
<Compile Include="KeyScope.cs" />
|
||||
<Compile Include="TagScope.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
|
|
Loading…
Reference in New Issue