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>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core">
|
|
||||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
|
<CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
|
||||||
|
@ -47,8 +44,6 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="FormatterTester.cs" />
|
|
||||||
<Compile Include="PropertyDictionaryTester.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\mustache-sharp\mustache-sharp.csproj">
|
<ProjectReference Include="..\mustache-sharp\mustache-sharp.csproj">
|
||||||
|
|
|
@ -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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to A key or property was not found with the given name..
|
/// Looks up a localized string similar to An attempt was made to define a parameter with a null or an invalid identifier..
|
||||||
|
/// </summary>
|
||||||
|
internal static string BlankParameterName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("BlankParameterName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to An attempt was made to define a tag with a null or an invalid identifier..
|
||||||
|
/// </summary>
|
||||||
|
internal static string BlankTagName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("BlankTagName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to A parameter with the same name already exists within the tag..
|
||||||
|
/// </summary>
|
||||||
|
internal static string DuplicateParameter {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DuplicateParameter", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The key {0} could not be found..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string KeyNotFound {
|
internal static string KeyNotFound {
|
||||||
get {
|
get {
|
||||||
return ResourceManager.GetString("KeyNotFound", resourceCulture);
|
return ResourceManager.GetString("KeyNotFound", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to A matching closing tag was not found for the {0} tag..
|
|
||||||
/// </summary>
|
|
||||||
internal static string MissingClosingTag {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("MissingClosingTag", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,10 +117,16 @@
|
||||||
<resheader name="writer">
|
<resheader name="writer">
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
</resheader>
|
</resheader>
|
||||||
<data name="KeyNotFound" xml:space="preserve">
|
<data name="BlankParameterName" xml:space="preserve">
|
||||||
<value>A key or property was not found with the given name.</value>
|
<value>An attempt was made to define a parameter with a null or an invalid identifier.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MissingClosingTag" xml:space="preserve">
|
<data name="BlankTagName" xml:space="preserve">
|
||||||
<value>A matching closing tag was not found for the {0} tag.</value>
|
<value>An attempt was made to define a tag with a null or an invalid identifier.</value>
|
||||||
|
</data>
|
||||||
|
<data name="DuplicateParameter" xml:space="preserve">
|
||||||
|
<value>A parameter with the same name already exists within the tag.</value>
|
||||||
|
</data>
|
||||||
|
<data name="KeyNotFound" xml:space="preserve">
|
||||||
|
<value>The key {0} could not be found.</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
|
@ -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>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="Microsoft.CSharp" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="CompoundBuilder.cs" />
|
<Compile Include="CompoundGenerator.cs" />
|
||||||
<Compile Include="EachBuilder.cs" />
|
<Compile Include="FormatParser.cs" />
|
||||||
<Compile Include="Formatter.cs" />
|
<Compile Include="IGenerator.cs" />
|
||||||
<Compile Include="IBuilder.cs" />
|
|
||||||
<Compile Include="IfBuilder.cs" />
|
|
||||||
<Compile Include="KeyBuilder.cs" />
|
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Properties\Resources.Designer.cs">
|
<Compile Include="Properties\Resources.Designer.cs">
|
||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
|
@ -49,12 +44,12 @@
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="PropertyDictionary.cs" />
|
<Compile Include="PropertyDictionary.cs" />
|
||||||
<Compile Include="Scope.cs" />
|
<Compile Include="RegexHelper.cs" />
|
||||||
<Compile Include="StaticBuilder.cs" />
|
<Compile Include="StaticGenerator.cs" />
|
||||||
<Compile Include="TagAttributes.cs" />
|
<Compile Include="TagDefinition.cs" />
|
||||||
<Compile Include="TagType.cs" />
|
<Compile Include="TagParameter.cs" />
|
||||||
<Compile Include="Trimmer.cs" />
|
<Compile Include="KeyScope.cs" />
|
||||||
<Compile Include="WithBuilder.cs" />
|
<Compile Include="TagScope.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Properties\Resources.resx">
|
<EmbeddedResource Include="Properties\Resources.resx">
|
||||||
|
|
Loading…
Reference in New Issue