Merge remote-tracking branch 'original-master/master'

# Conflicts:
#	Local.testsettings
#	TraceAndTestImpact.testsettings
#	mustache-sharp.test/FormatCompilerTester.cs
#	mustache-sharp.vsmdi
#	mustache-sharp/ArgumentCollection.cs
#	mustache-sharp/FormatCompiler.cs
This commit is contained in:
Paul Grimshaw 2016-09-19 16:26:03 +01:00
commit 0195c39f42
40 changed files with 1354 additions and 210 deletions

Binary file not shown.

View File

@ -1,4 +1,4 @@
msbuild ../mustache-sharp.sln /p:Configuration=Release "C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe" ../mustache-sharp.sln /p:Configuration=Release
nuget pack ../mustache-sharp/mustache-sharp.csproj -Properties Configuration=Release nuget pack ../mustache-sharp/mustache-sharp.csproj -Properties Configuration=Release
nuget push *.nupkg nuget push *.nupkg
del *.nupkg del *.nupkg

View File

@ -37,7 +37,7 @@ Introducing [handlebars.js](http://handlebarsjs.com/)... If you've needed to gen
Most of the lines in the previous example will never appear in the final output. This allows you to use **mustache#** to write templates for normal text, not just HTML/XML. Most of the lines in the previous example will never appear in the final output. This allows you to use **mustache#** to write templates for normal text, not just HTML/XML.
## Placeholders ## Placeholders
The placeholders can be any valid identifier. These map to the property names in your classes. The placeholders can be any valid identifier. These map to the property names in your classes (or `Dictionary` keys).
### Formatting Placeholders ### Formatting Placeholders
Each format item takes the following form and consists of the following components: Each format item takes the following form and consists of the following components:
@ -247,3 +247,44 @@ Here's an example of a tag that will join the items of a collection:
writer.Write(joined); writer.Write(joined);
} }
} }
## HTML Support
**mustache#** was not originally designed to exclusively generate HTML. However, it is by far the most common use of **mustache#**. For that reason, there is a separate `HtmlFormatCompiler` class that will automatically configure the code to work with HTML documents. Particularly, this class will eliminate most newlines and escape any special HTML characters that might appear within the substituted values.
If you really need to embed HTML values, you can wrap placeholders in triple quotes rather than double quotes.
HtmlFormatCompiler compiler = new HtmlFormatCompiler();
const string format = @"<html><body>{{escaped}} and {{{unescaped}}}</body></html>";
Generator generator = compiler.Compile(format);
string result = generator.Render(new
{
escaped = "<b>Awesome</b>",
unescaped = "<i>sweet</i>"
});
// Generates <html><body>&lt;b&gt;Awesome&lt;/b&gt; and <i>sweet</i></body></html>
## License
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>

View File

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

24
UNLICENSE.txt Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@ -452,6 +452,82 @@ Content";
Assert.AreEqual(String.Empty, context[0].TagName, "The top-most context had the wrong tag type."); Assert.AreEqual(String.Empty, context[0].TagName, "The top-most context had the wrong tag type.");
} }
/// <summary>
/// If a key refers to a public field, its value should be substituted in the output.
/// </summary>
[TestMethod]
public void TestGenerate_KeyRefersToPublicField_SubstitutesValue()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"Hello, {{Field}}!!!";
Generator generator = compiler.Compile(format);
ClassWithPublicField instance = new ClassWithPublicField() { Field = "Bob" };
string result = generator.Render(instance);
Assert.AreEqual("Hello, Bob!!!", result, "The wrong text was generated.");
}
public class ClassWithPublicField
{
public string Field;
}
/// <summary>
/// If a derived class replaces a property/field in the base class (via new)
/// it should be used, instead of causing an exception or using the base's
/// property/field.
/// </summary>
[TestMethod]
public void TestGenerate_NewPropertyInDerivedClass_UsesDerivedProperty()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"Hello, {{Value}}!!!";
Generator generator = compiler.Compile(format);
DerivedClass instance = new DerivedClass() { Value = "Derived" };
string result = generator.Render(instance);
Assert.AreEqual("Hello, Derived!!!", result, "The wrong text was generated.");
}
public class BaseClass
{
public int Value { get; set; }
}
public class DerivedClass : BaseClass
{
public DerivedClass()
{
base.Value = 1;
}
public new string Value { get; set; }
}
/// <summary>
/// If a derived class replaces a property/field in the base class (via new)
/// it should be used, instead of causing an exception or using the base's
/// property/field.
/// </summary>
[TestMethod]
public void TestGenerate_NewPropertyInGenericDerivedClass_UsesDerivedProperty()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"Hello, {{Value}}!!!";
Generator generator = compiler.Compile(format);
DerivedClass<string> instance = new DerivedClass<string>() { Value = "Derived" };
string result = generator.Render(instance);
Assert.AreEqual("Hello, Derived!!!", result, "The wrong text was generated.");
}
public class DerivedClass<T> : BaseClass
{
public DerivedClass()
{
base.Value = 1;
}
public new T Value { get; set; }
}
#endregion #endregion
#region Comment #region Comment
@ -716,6 +792,110 @@ Middle";
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated."); Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
} }
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_If_null_SkipsContent()
{
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#if this}}Content{{/if}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(null);
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_If_DBNull_SkipsContent()
{
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#if this}}Content{{/if}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(DBNull.Value);
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_If_EmptyIEnumerable_SkipsContent()
{
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#if this}}Content{{/if}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(Enumerable.Empty<int>());
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_If_NullChar_SkipsContent()
{
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#if this}}Content{{/if}}After";
Generator generator = parser.Compile(format);
string result = generator.Render('\0');
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_If_ZeroInt_SkipsContent()
{
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#if this}}Content{{/if}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(0);
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_If_ZeroFloat_SkipsContent()
{
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#if this}}Content{{/if}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(0f);
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_If_ZeroDouble_SkipsContent()
{
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#if this}}Content{{/if}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(0.0);
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary>
[TestMethod]
public void TestCompile_If_ZeroDecimal_SkipsContent()
{
FormatCompiler parser = new FormatCompiler();
const string format = "Before{{#if this}}Content{{/if}}After";
Generator generator = parser.Compile(format);
string result = generator.Render(0m);
Assert.AreEqual("BeforeAfter", result, "The wrong text was generated.");
}
/// <summary> /// <summary>
/// If the condition evaluates to false, the content of an if statement should not be printed. /// If the condition evaluates to false, the content of an if statement should not be printed.
/// </summary> /// </summary>
@ -844,7 +1024,7 @@ Content{{/if}}";
/// If the a header follows a footer, it shouldn't generate a new line. /// If the a header follows a footer, it shouldn't generate a new line.
/// </summary> /// </summary>
[TestMethod] [TestMethod]
public void TestCompile_IfNewLineContentNewLineEndIfIfNewLineContenNewLineEndIf_PrintsContent() public void TestCompile_IfNewLineContentNewLineEndIfIfNewLineContentNewLineEndIf_PrintsContent()
{ {
FormatCompiler parser = new FormatCompiler(); FormatCompiler parser = new FormatCompiler();
const string format = @"{{#if this}} const string format = @"{{#if this}}
@ -1423,11 +1603,96 @@ Odd
#endregion #endregion
#region New Line Management
/// <summary>
/// If the compiler is configured to ignore new lines,
/// they should not be removed from the output.
/// </summary>
[TestMethod]
public void TestCompile_PreserveNewLines()
{
FormatCompiler compiler = new FormatCompiler();
compiler.RemoveNewLines = false;
const string format = @"Hello
";
const string expected = @"Hello
";
Generator generator = compiler.Compile(format);
string result = generator.Render(null);
Assert.AreEqual(expected, result, "The wrong text was generated.");
}
#endregion
#region Strings
/// <summary>
/// We will use a string variable to determine whether or not to print out a line.
/// </summary>
[TestMethod]
public void TestCompile_StringArgument_PassedToTag()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#if 'hello'}}Hello{{/if}}";
Generator generator = compiler.Compile(format);
string actual = generator.Render(null);
string expected = "Hello";
Assert.AreEqual(expected, actual, "The string was not passed to the formatter.");
}
/// <summary>
/// We will use a string variable to determine whether or not to print out a line.
/// </summary>
[TestMethod]
public void TestCompile_EmptyStringArgument_PassedToTag()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#if ''}}Hello{{/if}}";
Generator generator = compiler.Compile(format);
string actual = generator.Render(null);
string expected = "";
Assert.AreEqual(expected, actual, "The string was not passed to the formatter.");
}
#endregion
#region Numbers
/// <summary>
/// We will use a number variable to determine whether or not to print out a line.
/// </summary>
[TestMethod]
public void TestCompile_NumberArgument_PassedToTag()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#if 4}}Hello{{/if}}";
Generator generator = compiler.Compile(format);
string actual = generator.Render(null);
string expected = "Hello";
Assert.AreEqual(expected, actual, "The number was not passed to the formatter.");
}
/// <summary>
/// We will use a string variable to determine whether or not to print out a line.
/// </summary>
[TestMethod]
public void TestCompile_ZeroNumberArgument_PassedToTag()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#if 00.0000}}Hello{{/if}}";
Generator generator = compiler.Compile(format);
string actual = generator.Render(null);
string expected = "";
Assert.AreEqual(expected, actual, "The number was not passed to the formatter.");
}
#endregion
#region ValueIntemplateTests #region ValueIntemplateTests
[TestMethod] [TestMethod]
public void TestCompile_CanUseStringValueInEquals() public void TestCompile_CanUseStringValueInEquals() {
{
FormatCompiler compiler = new FormatCompiler(); FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#eq Value _Yesterday}}Yes!{{/eq}}"; const string format = @"{{#eq Value _Yesterday}}Yes!{{/eq}}";
Generator generator = compiler.Compile(format); Generator generator = compiler.Compile(format);
@ -1468,6 +1733,7 @@ Odd
[TestMethod] [TestMethod]
public void TestCompile_UrlEncode() { public void TestCompile_UrlEncode() {
FormatCompiler compiler = new FormatCompiler(); FormatCompiler compiler = new FormatCompiler();
compiler.RegisterTag(new UrlEncodeTagDefinition(), true);
const string format = @"{{#urlencode}}https://google.com{{/urlencode}}"; const string format = @"{{#urlencode}}https://google.com{{/urlencode}}";
Generator generator = compiler.Compile(format); Generator generator = compiler.Compile(format);
@ -1477,8 +1743,10 @@ Odd
} }
[TestMethod] [TestMethod]
public void TestCompile_UrlEncodeParam() { public void TestCompile_UrlEncodeVariableText() {
FormatCompiler compiler = new FormatCompiler(); FormatCompiler compiler = new FormatCompiler();
compiler.RegisterTag(new UrlEncodeTagDefinition(), true);
const string format = @"{{#urlencode}}{{url}}{{/urlencode}}"; const string format = @"{{#urlencode}}{{url}}{{/urlencode}}";
Generator generator = compiler.Compile(format); Generator generator = compiler.Compile(format);
@ -1490,6 +1758,7 @@ Odd
[TestMethod] [TestMethod]
public void TestCompile_UrlDecode() { public void TestCompile_UrlDecode() {
FormatCompiler compiler = new FormatCompiler(); FormatCompiler compiler = new FormatCompiler();
const string format = @"{{#urldecode}}https%3a%2f%2fgoogle.com{{/urldecode}}"; const string format = @"{{#urldecode}}https%3a%2f%2fgoogle.com{{/urldecode}}";
Generator generator = compiler.Compile(format); Generator generator = compiler.Compile(format);
@ -1501,6 +1770,5 @@ Odd
#endregion #endregion
} }
} }

View File

@ -0,0 +1,32 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Mustache.Test
{
[TestClass]
public class HtmlFormatCompilerTester
{
[TestMethod]
public void ShouldEscapeValueContainingHTMLCharacters()
{
HtmlFormatCompiler compiler = new HtmlFormatCompiler();
var generator = compiler.Compile("<html><body>Hello, {{Name}}!!!</body></html>");
string html = generator.Render(new
{
Name = "John \"The Man\" Standford"
});
Assert.AreEqual("<html><body>Hello, John &quot;The Man&quot; Standford!!!</body></html>", html);
}
[TestMethod]
public void ShouldIgnoreHTMLCharactersInsideTripleCurlyBraces()
{
HtmlFormatCompiler compiler = new HtmlFormatCompiler();
var generator = compiler.Compile("<html><body>Hello, {{{Name}}}!!!</body></html>");
string html = generator.Render(new
{
Name = "John \"The Man\" Standford"
});
Assert.AreEqual("<html><body>Hello, John \"The Man\" Standford!!!</body></html>", html);
}
}
}

View File

@ -1,35 +1,15 @@
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("mustache-sharp.test")] [assembly: AssemblyTitle("mustache-sharp.test")]
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")] [assembly: AssemblyCompany("Truncon")]
[assembly: AssemblyProduct("mustache-sharp.test")] [assembly: AssemblyProduct("mustache-sharp.test")]
[assembly: AssemblyCopyright("Copyright © 2013")] [assembly: AssemblyCopyright("Copyright © 2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("9975f293-f972-4751-9c8c-e25b17c0c8bc")] [assembly: Guid("9975f293-f972-4751-9c8c-e25b17c0c8bc")]
[assembly: AssemblyVersion("0.0.0.0")]
// Version information for an assembly consists of the following four values: [assembly: AssemblyFileVersion("0.0.0.0")]
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
[assembly: AssemblyVersion("0.2.2.0")]
[assembly: AssemblyFileVersion("0.2.2.0")]

View File

@ -0,0 +1,223 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Mustache.Test
{
[TestClass]
public class UpcastDictionaryTester
{
[TestMethod]
public void ShouldReturnNullForNull()
{
IDictionary<string, object> result = UpcastDictionary.Create(null);
Assert.IsNull(result, "Null should be returned for null.");
}
[TestMethod]
public void ShouldReturnArgumentIfIDictionary_string_object()
{
object source = new Dictionary<string, object>();
IDictionary<string, object> result = UpcastDictionary.Create(source);
Assert.AreSame(source, result, "The up-cast wrapper should not be applied if already a IDictionary<string, object>.");
}
[TestMethod]
public void ShouldReturnNullIfNotGenericType()
{
object source = String.Empty;
IDictionary<string, object> result = UpcastDictionary.Create(source);
Assert.IsNull(result, "Null should be returned for non-generic types.");
}
[TestMethod]
public void ShouldReturnNullIfWrongNumberOfGenericArguments()
{
object source = new List<string>();
IDictionary<string, object> result = UpcastDictionary.Create(source);
Assert.IsNull(result, "Null should be returned for generic types with the wrong number of type arguments.");
}
[TestMethod]
public void ShouldReturnNullIfFirstGenericTypeArgumentIsNotAString()
{
object source = new Dictionary<object, object>();
IDictionary<string, object> result = UpcastDictionary.Create(source);
Assert.IsNull(result, "Null should be returned if the first generic type argument is not a string.");
}
[TestMethod]
public void ShouldReturnNullIfNotDictionaryType()
{
object source = (Converter<string, object>)(s => (object)s);
IDictionary<string, object> result = UpcastDictionary.Create(source);
Assert.IsNull(result, "Null should be returned for non-dictionary types.");
}
[TestMethod]
public void ShouldReturnUpcastWrapperForDictionary_string_TValue()
{
object source = new Dictionary<string, string>();
IDictionary<string, object> result = UpcastDictionary.Create(source);
Assert.IsInstanceOfType(result, typeof(UpcastDictionary<string>), "The source was not wrapped.");
}
[TestMethod]
public void ShouldFindKeyIfInWrappedDictionary()
{
object source = new Dictionary<string, string>() { { "Name", "Bob" } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
bool containsKey = result.ContainsKey("Name");
Assert.IsTrue(containsKey, "The key Name should have been found.");
}
[TestMethod]
public void ShouldNotFindKeyIfNotInWrappedDictionary()
{
object source = new Dictionary<string, string>() { { "Name", "Bob" } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
bool containsKey = result.ContainsKey("Age");
Assert.IsFalse(containsKey, "The key Age should not have been found.");
}
[TestMethod]
public void ShouldFindKeysInWrappedDictionary()
{
var source = new Dictionary<string, string>() { { "Name", "Bob" }, { "Age", "100" } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
ICollection sourceKeys = source.Keys;
ICollection wrappedKeys = result.Keys.ToArray();
CollectionAssert.AreEquivalent(sourceKeys, wrappedKeys, "The same keys should have been found in both collections.");
}
[TestMethod]
public void ShouldFindKeyIfInWrappedDictionary_TryGetValue()
{
var source = new Dictionary<string, string>() { { "Name", "Bob" } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
object value;
bool found = result.TryGetValue("Name", out value);
Assert.IsTrue(found, "The key should have been found.");
Assert.AreSame(source["Name"], value, "The value in the underlying dictionary should have been returned.");
}
[TestMethod]
public void ShouldNotFindKeyIfNotInWrappedDictionary_TryGetValue()
{
var source = new Dictionary<string, int>() { { "Age", 100 } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
object value;
bool found = result.TryGetValue("Name", out value);
Assert.IsFalse(found, "The key should not have been found.");
Assert.IsNull(value, "The value should be null even if the actual type is a struct.");
}
[TestMethod]
public void ShouldReturnValuesAsObjects()
{
var source = new Dictionary<string, int>() { { "Age", 100 }, { "Weight", 500 } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
ICollection sourceValues = source.Values;
ICollection wrappedValues = result.Values.ToArray();
CollectionAssert.AreEquivalent(sourceValues, wrappedValues, "The underlying values were not returned.");
}
[TestMethod]
public void ShouldFindKeyIfInWrappedDictionary_Indexer()
{
var source = new Dictionary<string, string>() { { "Name", "Bob" } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
object value = result["Name"];
Assert.AreSame(source["Name"], value, "The value in the underlying dictionary should have been returned.");
}
[TestMethod]
[ExpectedException(typeof(KeyNotFoundException))]
public void ShouldNotFindKeyIfNotInWrappedDictionary_Indexer()
{
var source = new Dictionary<string, int>() { { "Age", 100 } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
object value = result["Name"];
}
[TestMethod]
public void ShouldNotFindPairIfValueWrongType()
{
var source = new Dictionary<string, int>() { { "Age", 100 } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
bool contains = result.Contains(new KeyValuePair<string, object>("Age", "Blah"));
Assert.IsFalse(contains, "The pair should not have been found.");
}
[TestMethod]
public void ShouldFindPairInWrappedDictionary()
{
var source = new Dictionary<string, int>() { { "Age", 100 } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
bool contains = result.Contains(new KeyValuePair<string, object>("Age", 100));
Assert.IsTrue(contains, "The pair should have been found.");
}
[TestMethod]
public void ShouldCopyPairsToArray()
{
var source = new Dictionary<string, int>() { { "Age", 100 }, { "Weight", 45 } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
var array = new KeyValuePair<string, object>[2];
result.CopyTo(array, 0);
var expected = new KeyValuePair<string, object>[]
{
new KeyValuePair<string, object>("Age", 100),
new KeyValuePair<string, object>("Weight", 45)
};
CollectionAssert.AreEqual(expected, array, "The pairs were not copied.");
}
[TestMethod]
public void ShouldGetCount()
{
var source = new Dictionary<string, int>() { { "Age", 100 }, { "Weight", 45 } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
Assert.AreEqual(source.Count, result.Count, "The source and Upcast dictionary should have the same count.");
}
[TestMethod]
public void ShouldGetEnumerator()
{
var source = new Dictionary<string, int>() { { "Age", 100 }, { "Weight", 45 } };
IDictionary<string, object> result = UpcastDictionary.Create(source);
IEnumerator<KeyValuePair<string, object>> enumerator = result.GetEnumerator();
var values = new List<KeyValuePair<string, object>>();
while (enumerator.MoveNext())
{
values.Add(enumerator.Current);
}
var expected = new KeyValuePair<string, object>[]
{
new KeyValuePair<string, object>("Age", 100),
new KeyValuePair<string, object>("Weight", 45)
};
CollectionAssert.AreEqual(expected, values, "The enumerator did not return the correct pairs.");
}
/// <summary>
/// Newtonsoft's JSON.NET has an object called JObject. This is a concrete class
/// that inherits from IDictionary&lt;string, JToken&gt;. The UpcastDictionary
/// should be able to handle this type.
/// </summary>
[TestMethod]
public void ShouldHandleConcreteClassInheritingFromDictionary()
{
var dictionary = new ConcreteDictionary() { { "Name", "Bob" } };
var result = UpcastDictionary.Create(dictionary);
Assert.AreEqual(dictionary["Name"], result["Name"]);
}
public class ConcreteDictionary : Dictionary<string, string>
{
}
}
}

View File

@ -33,6 +33,12 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<SignAssembly>true</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>mustache-sharp.test.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<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" />
@ -44,14 +50,19 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="FormatCompilerTester.cs" /> <Compile Include="FormatCompilerTester.cs" />
<Compile Include="HtmlFormatCompilerTester.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UpcastDictionaryTester.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\mustache-sharp\mustache-sharp.csproj"> <ProjectReference Include="..\mustache-sharp\mustache-sharp.csproj">
<Project>{D71B378F-A4BA-4263-A4F0-07A49A0C528D}</Project> <Project>{d71b378f-a4ba-4263-a4f0-07a49a0c528d}</Project>
<Name>mustache-sharp</Name> <Name>mustache-sharp</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="mustache-sharp.test.snk" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

Binary file not shown.

View File

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

View File

@ -9,25 +9,25 @@ namespace Mustache
/// </summary> /// </summary>
internal sealed class ArgumentCollection internal sealed class ArgumentCollection
{ {
private readonly Dictionary<TagParameter, string> _argumentLookup; private readonly Dictionary<TagParameter, IArgument> _argumentLookup;
/// <summary> /// <summary>
/// Initializes a new instance of an ArgumentCollection. /// Initializes a new instance of an ArgumentCollection.
/// </summary> /// </summary>
public ArgumentCollection() public ArgumentCollection()
{ {
_argumentLookup = new Dictionary<TagParameter, string>(); _argumentLookup = new Dictionary<TagParameter, IArgument>();
} }
/// <summary> /// <summary>
/// Associates the given parameter to the key placeholder. /// Associates the given parameter to the key placeholder.
/// </summary> /// </summary>
/// <param name="parameter">The parameter to associate the key with.</param> /// <param name="parameter">The parameter to associate the key with.</param>
/// <param name="key">The key placeholder used as the argument.</param> /// <param name="key">The argument.</param>
/// <remarks>If the key is null, the default value of the parameter will be used.</remarks> /// <remarks>If the key is null, the default value of the parameter will be used.</remarks>
public void AddArgument(TagParameter parameter, string key) public void AddArgument(TagParameter parameter, IArgument argument)
{ {
_argumentLookup.Add(parameter, key); _argumentLookup.Add(parameter, argument);
} }
/// <summary> /// <summary>
@ -36,10 +36,10 @@ namespace Mustache
/// <param name="parameterName">The name of the parameter.</param> /// <param name="parameterName">The name of the parameter.</param>
public string GetKey(TagParameter parameter) public string GetKey(TagParameter parameter)
{ {
string key; IArgument argument;
if (_argumentLookup.TryGetValue(parameter, out key)) if (_argumentLookup.TryGetValue(parameter, out argument) && argument != null)
{ {
return key; return argument.GetKey();
} }
else else
{ {
@ -56,23 +56,17 @@ namespace Mustache
public Dictionary<string, object> GetArguments(Scope keyScope, Scope contextScope) public Dictionary<string, object> GetArguments(Scope keyScope, Scope contextScope)
{ {
Dictionary<string, object> arguments = new Dictionary<string,object>(); Dictionary<string, object> arguments = new Dictionary<string,object>();
foreach (KeyValuePair<TagParameter, string> pair in _argumentLookup) foreach (KeyValuePair<TagParameter, IArgument> pair in _argumentLookup)
{ {
object value; object value;
if (pair.Value == null) if (pair.Value == null)
{ {
value = pair.Key.DefaultValue; value = pair.Key.DefaultValue;
} }
else if (pair.Value.StartsWith("@"))
{
value = contextScope.Find(pair.Value.Substring(1));
}
else if (pair.Value.StartsWith("_")) {
value = pair.Value.Remove(0, 1);
}
else else
{ {
value = keyScope.Find(pair.Value); value = pair.Value.GetValue(keyScope, contextScope);
} }
arguments.Add(pair.Key.Name, value); arguments.Add(pair.Key.Name, value);
} }
@ -81,7 +75,7 @@ namespace Mustache
public Dictionary<string, object> GetArgumentKeyNames() public Dictionary<string, object> GetArgumentKeyNames()
{ {
return _argumentLookup.ToDictionary(p => p.Key.Name, p => (object)p.Value); return _argumentLookup.ToDictionary(p => p.Key.Name, p => (object)GetKey(p.Key));
} }
} }
} }

View File

@ -59,7 +59,7 @@ namespace Mustache
} }
} }
void IGenerator.GetText(Scope keyScope, TextWriter writer, Scope contextScope) void IGenerator.GetText(TextWriter writer, Scope keyScope, Scope contextScope, Action<Substitution> postProcessor)
{ {
Dictionary<string, object> arguments = _arguments.GetArguments(keyScope, contextScope); Dictionary<string, object> arguments = _arguments.GetArguments(keyScope, contextScope);
IEnumerable<NestedContext> contexts = _definition.GetChildContext(writer, keyScope, arguments, contextScope); IEnumerable<NestedContext> contexts = _definition.GetChildContext(writer, keyScope, arguments, contextScope);
@ -80,7 +80,7 @@ namespace Mustache
{ {
foreach (IGenerator generator in generators) foreach (IGenerator generator in generators)
{ {
generator.GetText(context.KeyScope ?? keyScope, context.Writer ?? writer, context.ContextScope); generator.GetText(context.Writer ?? writer, context.KeyScope ?? keyScope, context.ContextScope, postProcessor);
if (context.WriterNeedsConsidated) if (context.WriterNeedsConsidated)
{ {
writer.Write(_definition.ConsolidateWriter(context.Writer ?? writer, arguments)); writer.Write(_definition.ConsolidateWriter(context.Writer ?? writer, arguments));

View File

@ -65,7 +65,7 @@ namespace Mustache
private bool isConditionSatisfied(object condition) private bool isConditionSatisfied(object condition)
{ {
if (condition == null) if (condition == null || condition == DBNull.Value)
{ {
return false; return false;
} }

View File

@ -52,8 +52,8 @@ namespace Mustache
_tagLookup.Add(gteTagDefinition.Name, gteTagDefinition); _tagLookup.Add(gteTagDefinition.Name, gteTagDefinition);
LteTagDefinition lteTagDefinition = new LteTagDefinition(); LteTagDefinition lteTagDefinition = new LteTagDefinition();
_tagLookup.Add(lteTagDefinition.Name, lteTagDefinition); _tagLookup.Add(lteTagDefinition.Name, lteTagDefinition);
UrlEncodeTagDefinition urlEncodeTagDefinition = new UrlEncodeTagDefinition(); // UrlEncodeTagDefinition urlEncodeTagDefinition = new UrlEncodeTagDefinition();
_tagLookup.Add(urlEncodeTagDefinition.Name, urlEncodeTagDefinition); // _tagLookup.Add(urlEncodeTagDefinition.Name, urlEncodeTagDefinition);
UrlDecodeTagDefinition urlDecodeTagDefinition = new UrlDecodeTagDefinition(); UrlDecodeTagDefinition urlDecodeTagDefinition = new UrlDecodeTagDefinition();
_tagLookup.Add(urlDecodeTagDefinition.Name,urlDecodeTagDefinition); _tagLookup.Add(urlDecodeTagDefinition.Name,urlDecodeTagDefinition);
@ -69,6 +69,16 @@ namespace Mustache
/// </summary> /// </summary>
public event EventHandler<VariableFoundEventArgs> VariableFound; public event EventHandler<VariableFoundEventArgs> VariableFound;
/// <summary>
/// Gets or sets whether newlines are removed from the template (default: true).
/// </summary>
public bool RemoveNewLines { get; set; }
/// <summary>
/// Gets or sets whether the compiler searches for tags using triple curly braces.
/// </summary>
public bool AreExtensionTagsAllowed { get; set; }
/// <summary> /// <summary>
/// Registers the given tag definition with the parser. /// Registers the given tag definition with the parser.
/// </summary> /// </summary>
@ -103,7 +113,7 @@ namespace Mustache
List<Context> context = new List<Context>() { new Context(_masterDefinition.Name, new ContextParameter[0]) }; List<Context> context = new List<Context>() { new Context(_masterDefinition.Name, new ContextParameter[0]) };
int formatIndex = buildCompoundGenerator(_masterDefinition, context, generator, format, 0); int formatIndex = buildCompoundGenerator(_masterDefinition, context, generator, format, 0);
string trailing = format.Substring(formatIndex); string trailing = format.Substring(formatIndex);
generator.AddGenerator(new StaticGenerator(trailing)); generator.AddGenerator(new StaticGenerator(trailing, RemoveNewLines));
return new Generator(generator); return new Generator(generator);
} }
@ -138,7 +148,13 @@ namespace Mustache
matches.Add(getTagRegex(childDefinition)); matches.Add(getTagRegex(childDefinition));
} }
matches.Add(getUnknownTagRegex()); matches.Add(getUnknownTagRegex());
string match = "{{(" + String.Join("|", matches) + ")}}"; string combined = String.Join("|", matches);
string match = "{{(?<match>" + combined + ")}}";
if (AreExtensionTagsAllowed)
{
string tripleMatch = "{{{(?<extension>" + combined + ")}}}";
match = "(?:" + match + ")|(?:" + tripleMatch + ")";
}
regex = new Regex(match); regex = new Regex(match);
_regexLookup.Add(definition.Name, regex); _regexLookup.Add(definition.Name, regex);
} }
@ -161,7 +177,7 @@ namespace Mustache
private static string getKeyRegex() private static string getKeyRegex()
{ {
return @"((?<key>@?" + RegexHelper.CompoundKey + @")(,(?<alignment>(\+|-)?[\d]+))?(:(?<format>.*?))?)"; return @"((?<key>" + RegexHelper.CompoundKey + @")(,(?<alignment>(\+|-)?[\d]+))?(:(?<format>.*?))?)";
} }
private static string getTagRegex(TagDefinition definition) private static string getTagRegex(TagDefinition definition)
@ -173,8 +189,8 @@ namespace Mustache
foreach (TagParameter parameter in definition.Parameters) foreach (TagParameter parameter in definition.Parameters)
{ {
regexBuilder.Append(@"(\s+?"); regexBuilder.Append(@"(\s+?");
regexBuilder.Append(@"(?<argument>(@?"); regexBuilder.Append(@"(?<argument>(");
regexBuilder.Append(RegexHelper.CompoundKey); regexBuilder.Append(RegexHelper.Argument);
regexBuilder.Append(@")))"); regexBuilder.Append(@")))");
if (!parameter.IsRequired) if (!parameter.IsRequired)
{ {
@ -185,7 +201,7 @@ namespace Mustache
return regexBuilder.ToString(); return regexBuilder.ToString();
} }
private string getUnknownTagRegex() private static string getUnknownTagRegex()
{ {
return @"(?<unknown>(#.*?))"; return @"(?<unknown>(#.*?))";
} }
@ -214,34 +230,37 @@ namespace Mustache
if (match.Groups["key"].Success) if (match.Groups["key"].Success)
{ {
generator.AddGenerator(new StaticGenerator(leading)); generator.AddGenerator(new StaticGenerator(leading, RemoveNewLines));
formatIndex = match.Index + match.Length; formatIndex = match.Index + match.Length;
bool isExtension = match.Groups["extension"].Success;
string key = match.Groups["key"].Value; string key = match.Groups["key"].Value;
string alignment = match.Groups["alignment"].Value; string alignment = match.Groups["alignment"].Value;
string formatting = match.Groups["format"].Value; string formatting = match.Groups["format"].Value;
if (key.StartsWith("@")) if (key.StartsWith("@"))
{ {
VariableFoundEventArgs args = new VariableFoundEventArgs(key.Substring(1), alignment, formatting, context.ToArray()); VariableFoundEventArgs args = new VariableFoundEventArgs(key.Substring(1), alignment, formatting, isExtension, context.ToArray());
if (VariableFound != null) if (VariableFound != null)
{ {
VariableFound(this, args); VariableFound(this, args);
key = "@" + args.Name; key = "@" + args.Name;
alignment = args.Alignment; alignment = args.Alignment;
formatting = args.Formatting; formatting = args.Formatting;
isExtension = args.IsExtension;
} }
} }
else else
{ {
PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(key, alignment, formatting, context.ToArray()); PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(key, alignment, formatting, isExtension, context.ToArray());
if (PlaceholderFound != null) if (PlaceholderFound != null)
{ {
PlaceholderFound(this, args); PlaceholderFound(this, args);
key = args.Key; key = args.Key;
alignment = args.Alignment; alignment = args.Alignment;
formatting = args.Formatting; formatting = args.Formatting;
isExtension = args.IsExtension;
} }
} }
KeyGenerator keyGenerator = new KeyGenerator(key, alignment, formatting); KeyGenerator keyGenerator = new KeyGenerator(key, alignment, formatting, isExtension);
generator.AddGenerator(keyGenerator); generator.AddGenerator(keyGenerator);
} }
else if (match.Groups["open"].Success) else if (match.Groups["open"].Success)
@ -255,7 +274,7 @@ namespace Mustache
throw new FormatException(message); throw new FormatException(message);
} }
generator.AddGenerator(new StaticGenerator(leading)); generator.AddGenerator(new StaticGenerator(leading, RemoveNewLines));
ArgumentCollection arguments = getArguments(nextDefinition, match, context); ArgumentCollection arguments = getArguments(nextDefinition, match, context);
if (nextDefinition.HasContent) if (nextDefinition.HasContent)
@ -283,7 +302,7 @@ namespace Mustache
} }
else if (match.Groups["close"].Success) else if (match.Groups["close"].Success)
{ {
generator.AddGenerator(new StaticGenerator(leading)); generator.AddGenerator(new StaticGenerator(leading, RemoveNewLines));
string tagName = match.Groups["name"].Value; string tagName = match.Groups["name"].Value;
TagDefinition nextDefinition = _tagLookup[tagName]; TagDefinition nextDefinition = _tagLookup[tagName];
formatIndex = match.Index; formatIndex = match.Index;
@ -295,7 +314,7 @@ namespace Mustache
} }
else if (match.Groups["comment"].Success) else if (match.Groups["comment"].Success)
{ {
generator.AddGenerator(new StaticGenerator(leading)); generator.AddGenerator(new StaticGenerator(leading, RemoveNewLines));
formatIndex = match.Index + match.Length; formatIndex = match.Index + match.Length;
} }
else if (match.Groups["unknown"].Success) else if (match.Groups["unknown"].Success)
@ -350,28 +369,46 @@ namespace Mustache
foreach (var pair in arguments) foreach (var pair in arguments)
{ {
string placeholder = pair.Value; string placeholder = pair.Value;
IArgument argument = null;
if (placeholder != null) if (placeholder != null)
{ {
if (placeholder.StartsWith("@")) if (placeholder.StartsWith("@"))
{ {
VariableFoundEventArgs args = new VariableFoundEventArgs(placeholder.Substring(1), String.Empty, String.Empty, context.ToArray()); string variableName = placeholder.Substring(1);
VariableFoundEventArgs args = new VariableFoundEventArgs(placeholder.Substring(1), String.Empty, String.Empty, false, context.ToArray());
if (VariableFound != null) if (VariableFound != null)
{ {
VariableFound(this, args); VariableFound(this, args);
placeholder = "@" + args.Name; variableName = args.Name;
}
argument = new VariableArgument(variableName);
}
else if (RegexHelper.IsString(placeholder))
{
string value = placeholder.Trim('\'');
argument = new StringArgument(value);
}
else if (RegexHelper.IsNumber(placeholder))
{
decimal number;
if (Decimal.TryParse(placeholder, out number))
{
argument = new NumberArgument(number);
} }
} }
else else
{ {
PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(placeholder, String.Empty, String.Empty, context.ToArray()); string placeholderName = placeholder;
PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(placeholder, String.Empty, String.Empty, false, context.ToArray());
if (PlaceholderFound != null) if (PlaceholderFound != null)
{ {
PlaceholderFound(this, args); PlaceholderFound(this, args);
placeholder = args.Key; placeholderName = args.Key;
}
argument = new PlaceholderArgument(placeholderName);
} }
} }
} collection.AddArgument(pair.Key, argument);
collection.AddArgument(pair.Key, placeholder);
} }
return collection; return collection;
} }

View File

@ -54,6 +54,11 @@ namespace Mustache
remove { _valueRequestedHandlers.Remove(value); } remove { _valueRequestedHandlers.Remove(value); }
} }
/// <summary>
/// Occurs when a tag is replaced by its text.
/// </summary>
public event EventHandler<TagFormattedEventArgs> TagFormatted;
/// <summary> /// <summary>
/// Gets the text that is generated for the given object. /// Gets the text that is generated for the given object.
/// </summary> /// </summary>
@ -98,8 +103,19 @@ namespace Mustache
contextScope.ValueRequested += handler; contextScope.ValueRequested += handler;
} }
StringWriter writer = new StringWriter(provider); StringWriter writer = new StringWriter(provider);
_generator.GetText(keyScope, writer, contextScope); _generator.GetText(writer, keyScope, contextScope, postProcess);
return writer.ToString(); return writer.ToString();
} }
private void postProcess(Substitution substitution)
{
if (TagFormatted == null)
{
return;
}
TagFormattedEventArgs args = new TagFormattedEventArgs(substitution.Key, substitution.Substitute, substitution.IsExtension);
TagFormatted(this, args);
substitution.Substitute = args.Substitute;
}
} }
} }

View File

@ -0,0 +1,67 @@
using System;
using System.Security;
namespace Mustache
{
public sealed class HtmlFormatCompiler
{
private readonly FormatCompiler compiler;
public HtmlFormatCompiler()
{
compiler = new FormatCompiler();
compiler.AreExtensionTagsAllowed = true;
compiler.RemoveNewLines = true;
}
/// <summary>
/// Occurs when a placeholder is found in the template.
/// </summary>
public event EventHandler<PlaceholderFoundEventArgs> PlaceholderFound
{
add { compiler.PlaceholderFound += value; }
remove { compiler.PlaceholderFound -= value; }
}
/// <summary>
/// Occurs when a variable is found in the template.
/// </summary>
public event EventHandler<VariableFoundEventArgs> VariableFound
{
add { compiler.VariableFound += value; }
remove { compiler.VariableFound -= value; }
}
/// <summary>
/// Registers the given tag definition with the parser.
/// </summary>
/// <param name="definition">The tag definition to register.</param>
/// <param name="isTopLevel">Specifies whether the tag is immediately in scope.</param>
public void RegisterTag(TagDefinition definition, bool isTopLevel)
{
compiler.RegisterTag(definition, isTopLevel);
}
/// <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 Generator Compile(string format)
{
Generator generator = compiler.Compile(format);
generator.TagFormatted += escapeInvalidHtml;
return generator;
}
private static void escapeInvalidHtml(object sender, TagFormattedEventArgs e)
{
if (e.IsExtension)
{
// Do not escape text within triple curly braces
return;
}
e.Substitute = SecurityElement.Escape(e.Substitute);
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Mustache
{
public interface IArgument
{
string GetKey();
object GetValue(Scope keyScope, Scope contextScope);
}
}

View File

@ -11,10 +11,11 @@ namespace Mustache
/// <summary> /// <summary>
/// Generates the text when applying the format plan. /// Generates the text when applying the format plan.
/// </summary> /// </summary>
/// <param name="keyScope">The current lexical scope of the keys.</param>
/// <param name="writer">The text writer to send all text to.</param> /// <param name="writer">The text writer to send all text to.</param>
/// <param name="keyScope">The current lexical scope of the keys.</param>
/// <param name="contextScope">The data associated to the context.</param> /// <param name="contextScope">The data associated to the context.</param>
/// <param name="postProcessor">A function to apply after a substitution is made.</param>
/// <returns>The generated text.</returns> /// <returns>The generated text.</returns>
void GetText(Scope keyScope, TextWriter writer, Scope contextScope); void GetText(TextWriter writer, Scope keyScope, Scope contextScope, Action<Substitution> postProcessor);
} }
} }

View File

@ -23,7 +23,7 @@ namespace Mustache
_arguments = arguments; _arguments = arguments;
} }
void IGenerator.GetText(Scope scope, TextWriter writer, Scope context) void IGenerator.GetText(TextWriter writer, Scope scope, Scope context, Action<Substitution> postProcessor)
{ {
Dictionary<string, object> arguments; Dictionary<string, object> arguments;
if (_definition.IsSetter) if (_definition.IsSetter)

View File

@ -11,9 +11,11 @@ namespace Mustache
/// Initializes a new instance of a KeyFoundEventArgs. /// Initializes a new instance of a KeyFoundEventArgs.
/// </summary> /// </summary>
/// <param name="key">The fully-qualified key.</param> /// <param name="key">The fully-qualified key.</param>
internal KeyFoundEventArgs(string key, object value) /// <param name="isExtension">Specifies whether the key was found within triple curly braces.</param>
internal KeyFoundEventArgs(string key, object value, bool isExtension)
{ {
Key = key; Key = key;
Substitute = value;
} }
/// <summary> /// <summary>
@ -21,6 +23,11 @@ namespace Mustache
/// </summary> /// </summary>
public string Key { get; private set; } public string Key { get; private set; }
/// <summary>
/// Gets or sets whether the key appeared within triple curly braces.
/// </summary>
public bool IsExtension { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the object to use as the substitute. /// Gets or sets the object to use as the substitute.
/// </summary> /// </summary>

View File

@ -12,6 +12,7 @@ namespace Mustache
private readonly string _key; private readonly string _key;
private readonly string _format; private readonly string _format;
private readonly bool _isVariable; private readonly bool _isVariable;
private readonly bool _isExtension;
/// <summary> /// <summary>
/// Initializes a new instance of a KeyGenerator. /// Initializes a new instance of a KeyGenerator.
@ -19,7 +20,8 @@ namespace Mustache
/// <param name="key">The key to substitute with its value.</param> /// <param name="key">The key to substitute with its value.</param>
/// <param name="alignment">The alignment specifier.</param> /// <param name="alignment">The alignment specifier.</param>
/// <param name="formatting">The format specifier.</param> /// <param name="formatting">The format specifier.</param>
public KeyGenerator(string key, string alignment, string formatting) /// <param name="isExtension">Specifies whether the key was found within triple curly braces.</param>
public KeyGenerator(string key, string alignment, string formatting, bool isExtension)
{ {
if (key.StartsWith("@")) if (key.StartsWith("@"))
{ {
@ -32,6 +34,7 @@ namespace Mustache
_isVariable = false; _isVariable = false;
} }
_format = getFormat(alignment, formatting); _format = getFormat(alignment, formatting);
_isExtension = isExtension;
} }
private static string getFormat(string alignment, string formatting) private static string getFormat(string alignment, string formatting)
@ -52,10 +55,18 @@ namespace Mustache
return formatBuilder.ToString(); return formatBuilder.ToString();
} }
void IGenerator.GetText(Scope scope, TextWriter writer, Scope context) void IGenerator.GetText(TextWriter writer, Scope scope, Scope context, Action<Substitution> postProcessor)
{ {
object value = _isVariable ? context.Find(_key) : scope.Find(_key); object value = _isVariable ? context.Find(_key, _isExtension) : scope.Find(_key, _isExtension);
writer.Write(_format, value); string result = String.Format(writer.FormatProvider, _format, value);
Substitution substitution = new Substitution()
{
Key = _key,
Substitute = result,
IsExtension = _isExtension
};
postProcessor(substitution);
writer.Write(substitution.Substitute);
} }
} }
} }

View File

@ -12,10 +12,12 @@ namespace Mustache
/// </summary> /// </summary>
/// <param name="key">The fully-qualified key.</param> /// <param name="key">The fully-qualified key.</param>
/// <param name="missingMember">The part of the key that could not be found.</param> /// <param name="missingMember">The part of the key that could not be found.</param>
internal KeyNotFoundEventArgs(string key, string missingMember) /// <param name="isExtension">Specifies whether the key appears within triple curly braces.</param>
internal KeyNotFoundEventArgs(string key, string missingMember, bool isExtension)
{ {
Key = key; Key = key;
MissingMember = missingMember; MissingMember = missingMember;
IsExtension = isExtension;
} }
/// <summary> /// <summary>
@ -28,6 +30,11 @@ namespace Mustache
/// </summary> /// </summary>
public string MissingMember { get; private set; } public string MissingMember { get; private set; }
/// <summary>
/// Gets whether the key appeared within triple curly braces.
/// </summary>
public bool IsExtension { get; private set; }
/// <summary> /// <summary>
/// Gets or sets whether to use the substitute. /// Gets or sets whether to use the substitute.
/// </summary> /// </summary>

View File

@ -0,0 +1,24 @@
using System;
namespace Mustache
{
public class NumberArgument : IArgument
{
private readonly decimal value;
public NumberArgument(decimal value)
{
this.value = value;
}
public string GetKey()
{
return null;
}
public object GetValue(Scope keyScope, Scope contextScope)
{
return value;
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace Mustache
{
public class PlaceholderArgument : IArgument
{
private readonly string name;
public PlaceholderArgument(string name)
{
this.name = name;
}
public string GetKey()
{
return name;
}
public object GetValue(Scope keyScope, Scope contextScope)
{
return keyScope.Find(name, false);
}
}
}

View File

@ -14,8 +14,9 @@ namespace Mustache
/// <param name="key">The key that was found.</param> /// <param name="key">The key that was found.</param>
/// <param name="alignment">The alignment that will be applied to the substitute value.</param> /// <param name="alignment">The alignment that will be applied to the substitute value.</param>
/// <param name="formatting">The formatting that will be applied to the substitute value.</param> /// <param name="formatting">The formatting that will be applied to the substitute value.</param>
/// <param name="isExtension">Indicates whether the placeholder was found within triple curly braces.</param>
/// <param name="context">The context where the placeholder was found.</param> /// <param name="context">The context where the placeholder was found.</param>
internal PlaceholderFoundEventArgs(string key, string alignment, string formatting, Context[] context) internal PlaceholderFoundEventArgs(string key, string alignment, string formatting, bool isExtension, Context[] context)
{ {
Key = key; Key = key;
Alignment = alignment; Alignment = alignment;
@ -38,6 +39,11 @@ namespace Mustache
/// </summary> /// </summary>
public string Formatting { get; set; } public string Formatting { get; set; }
/// <summary>
/// Gets or sets whether the placeholder was found within triple curly braces.
/// </summary>
public bool IsExtension { get; set; }
/// <summary> /// <summary>
/// Gets the context where the placeholder was found. /// Gets the context where the placeholder was found.
/// </summary> /// </summary>

View File

@ -1,11 +1,8 @@
using System; using System;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("mustache-sharp")] [assembly: AssemblyTitle("mustache-sharp")]
[assembly: AssemblyDescription("A extension of the mustache text template engine for .NET.")] [assembly: AssemblyDescription("A extension of the mustache text template engine for .NET.")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
@ -15,25 +12,8 @@ using System.Runtime.CompilerServices;
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: CLSCompliant(true)] [assembly: CLSCompliant(true)]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("e5a4263d-d450-4d85-a4d5-44c0a2822668")] [assembly: Guid("e5a4263d-d450-4d85-a4d5-44c0a2822668")]
[assembly: AssemblyVersion("0.2.9.0")]
// Version information for an assembly consists of the following four values: [assembly: AssemblyFileVersion("0.2.9.0")]
// [assembly: InternalsVisibleTo("mustache-sharp.test,PublicKey=0024000004800000940000000602000000240000525341310004000001000100755df5a2b24c568812aae0eb194d08a4e3cba960673bcc07a7d446acf52f3f56ae2155b37b8d547bc5d8c562823bd592d1312bef9ad4740a8bb503d0095c31419f9d190882a2fa46090412bf15b13ca0057ba533c85a853333132ec8b70cf19655ef961b06d1c3fc35b3f68680420562be741456cb7a18bd5ab0fa779f8d47b1")]
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.2.2.0")]
[assembly: AssemblyFileVersion("0.2.2.0")]
[assembly: InternalsVisibleTo("mustache-sharp.test")]

View File

@ -2,6 +2,7 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace Mustache namespace Mustache
@ -11,10 +12,10 @@ namespace Mustache
/// </summary> /// </summary>
internal sealed class PropertyDictionary : IDictionary<string, object> internal sealed class PropertyDictionary : IDictionary<string, object>
{ {
private static readonly Dictionary<Type, Dictionary<string, PropertyInfo>> _cache = new Dictionary<Type, Dictionary<string, PropertyInfo>>(); private static readonly Dictionary<Type, Dictionary<string, Func<object, object>>> _cache = new Dictionary<Type, Dictionary<string, Func<object, object>>>();
private readonly object _instance; private readonly object _instance;
private readonly Dictionary<string, PropertyInfo> _typeCache; private readonly Dictionary<string, Func<object, object>> _typeCache;
/// <summary> /// <summary>
/// Initializes a new instance of a PropertyDictionary. /// Initializes a new instance of a PropertyDictionary.
@ -25,34 +26,75 @@ namespace Mustache
_instance = instance; _instance = instance;
if (instance == null) if (instance == null)
{ {
_typeCache = new Dictionary<string, PropertyInfo>(); _typeCache = new Dictionary<string, Func<object, object>>();
} }
else else
{
lock (_cache)
{ {
_typeCache = getCacheType(_instance); _typeCache = getCacheType(_instance);
} }
} }
}
private static Dictionary<string, PropertyInfo> getCacheType(object instance) private static Dictionary<string, Func<object, object>> getCacheType(object instance)
{ {
Type type = instance.GetType(); Type type = instance.GetType();
Dictionary<string, PropertyInfo> typeCache; Dictionary<string, Func<object, object>> typeCache;
if (!_cache.TryGetValue(type, out typeCache)) if (!_cache.TryGetValue(type, out typeCache))
{ {
typeCache = new Dictionary<string, PropertyInfo>(); typeCache = new Dictionary<string, Func<object, object>>();
BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy; BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy;
foreach (PropertyInfo propertyInfo in type.GetProperties(flags))
var properties = getMembers(type, type.GetProperties(flags).Where(p => !p.IsSpecialName));
foreach (PropertyInfo propertyInfo in properties)
{ {
if (!propertyInfo.IsSpecialName) typeCache.Add(propertyInfo.Name, i => propertyInfo.GetValue(i, null));
}
var fields = getMembers(type, type.GetFields(flags).Where(f => !f.IsSpecialName));
foreach (FieldInfo fieldInfo in fields)
{ {
typeCache.Add(propertyInfo.Name, propertyInfo); typeCache.Add(fieldInfo.Name, i => fieldInfo.GetValue(i));
}
} }
_cache.Add(type, typeCache); _cache.Add(type, typeCache);
} }
return typeCache; return typeCache;
} }
private static IEnumerable<TMember> getMembers<TMember>(Type type, IEnumerable<TMember> members)
where TMember : MemberInfo
{
var singles = from member in members
group member by member.Name into nameGroup
where nameGroup.Count() == 1
from property in nameGroup
select property;
var multiples = from member in members
group member by member.Name into nameGroup
where nameGroup.Count() > 1
select
(
from member in nameGroup
orderby getDistance(type, member)
select member
).First();
var combined = singles.Concat(multiples);
return combined;
}
private static int getDistance(Type type, MemberInfo memberInfo)
{
int distance = 0;
for (; type != null && type != memberInfo.DeclaringType; type = type.BaseType)
{
++distance;
}
return distance;
}
/// <summary> /// <summary>
/// Gets the underlying instance. /// Gets the underlying instance.
/// </summary> /// </summary>
@ -100,13 +142,13 @@ namespace Mustache
/// <exception cref="System.ArgumentNullException">The name of the property was null.</exception> /// <exception cref="System.ArgumentNullException">The name of the property was null.</exception>
public bool TryGetValue(string key, out object value) public bool TryGetValue(string key, out object value)
{ {
PropertyInfo propertyInfo; Func<object, object> getter;
if (!_typeCache.TryGetValue(key, out propertyInfo)) if (!_typeCache.TryGetValue(key, out getter))
{ {
value = null; value = null;
return false; return false;
} }
value = getValue(propertyInfo); value = getter(_instance);
return true; return true;
} }
@ -117,11 +159,11 @@ namespace Mustache
{ {
get get
{ {
ICollection<PropertyInfo> propertyInfos = _typeCache.Values; ICollection<Func<object, object>> getters = _typeCache.Values;
List<object> values = new List<object>(); List<object> values = new List<object>();
foreach (PropertyInfo propertyInfo in propertyInfos) foreach (Func<object, object> getter in getters)
{ {
object value = getValue(propertyInfo); object value = getter(_instance);
values.Add(value); values.Add(value);
} }
return values.AsReadOnly(); return values.AsReadOnly();
@ -143,8 +185,8 @@ namespace Mustache
{ {
get get
{ {
PropertyInfo propertyInfo = _typeCache[key]; Func<object, object> getter = _typeCache[key];
return getValue(propertyInfo); return getter(_instance);
} }
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
set set
@ -167,23 +209,23 @@ namespace Mustache
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item) bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
{ {
PropertyInfo propertyInfo; Func<object, object> getter;
if (!_typeCache.TryGetValue(item.Key, out propertyInfo)) if (!_typeCache.TryGetValue(item.Key, out getter))
{ {
return false; return false;
} }
object value = getValue(propertyInfo); object value = getter(_instance);
return Equals(item.Value, value); return Equals(item.Value, value);
} }
void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{ {
List<KeyValuePair<string, object>> pairs = new List<KeyValuePair<string, object>>(); List<KeyValuePair<string, object>> pairs = new List<KeyValuePair<string, object>>();
ICollection<KeyValuePair<string, PropertyInfo>> collection = _typeCache; ICollection<KeyValuePair<string, Func<object, object>>> collection = _typeCache;
foreach (KeyValuePair<string, PropertyInfo> pair in collection) foreach (KeyValuePair<string, Func<object, object>> pair in collection)
{ {
PropertyInfo propertyInfo = pair.Value; Func<object, object> getter = pair.Value;
object value = getValue(propertyInfo); object value = getter(_instance);
pairs.Add(new KeyValuePair<string, object>(pair.Key, value)); pairs.Add(new KeyValuePair<string, object>(pair.Key, value));
} }
pairs.CopyTo(array, arrayIndex); pairs.CopyTo(array, arrayIndex);
@ -217,9 +259,10 @@ namespace Mustache
/// <returns></returns> /// <returns></returns>
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{ {
foreach (KeyValuePair<string, PropertyInfo> pair in _typeCache) foreach (KeyValuePair<string, Func<object, object>> pair in _typeCache)
{ {
object value = getValue(pair.Value); Func<object, object> getter = pair.Value;
object value = getter(_instance);
yield return new KeyValuePair<string, object>(pair.Key, value); yield return new KeyValuePair<string, object>(pair.Key, value);
} }
} }
@ -228,10 +271,5 @@ namespace Mustache
{ {
return GetEnumerator(); return GetEnumerator();
} }
private object getValue(PropertyInfo propertyInfo)
{
return propertyInfo.GetValue(_instance, null);
}
} }
} }

View File

@ -6,10 +6,13 @@ namespace Mustache
/// <summary> /// <summary>
/// Provides utility methods that require regular expressions. /// Provides utility methods that require regular expressions.
/// </summary> /// </summary>
public static class RegexHelper internal static class RegexHelper
{ {
internal const string Key = @"[_\w][_\w\d]*"; public const string Key = @"[_\w][_\w\d]*";
internal const string CompoundKey = Key + @"(\." + Key + ")*"; public const string String = @"'.*?'";
public const string Number = @"[-+]?\d*\.?\d+";
public const string CompoundKey = "@?" + Key + @"(?:\." + Key + ")*";
public const string Argument = @"(?:(?<arg_key>" + CompoundKey + @")|(?<arg_string>" + String + @")|(?<arg_number>" + Number + @"))";
/// <summary> /// <summary>
/// Determines whether the given name is a legal identifier. /// Determines whether the given name is a legal identifier.
@ -25,5 +28,25 @@ namespace Mustache
Regex regex = new Regex("^" + Key + "$"); Regex regex = new Regex("^" + Key + "$");
return regex.IsMatch(name); return regex.IsMatch(name);
} }
public static bool IsString(string value)
{
if (value == null)
{
return false;
}
Regex regex = new Regex("^" + String + "$");
return regex.IsMatch(value);
}
public static bool IsNumber(string value)
{
if (value == null)
{
return false;
}
Regex regex = new Regex("^" + Number + "$");
return regex.IsMatch(value);
}
} }
} }

View File

@ -76,17 +76,18 @@ namespace Mustache
/// Attempts to find the value associated with the key with given name. /// Attempts to find the value associated with the key with given name.
/// </summary> /// </summary>
/// <param name="name">The name of the key.</param> /// <param name="name">The name of the key.</param>
/// <param name="isExtension">Specifies whether the key appeared within triple curly braces.</param>
/// <returns>The value associated with the key with the given name.</returns> /// <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> /// <exception cref="System.Collections.Generic.KeyNotFoundException">A key with the given name could not be found.</exception>
internal object Find(string name) internal object Find(string name, bool isExtension)
{ {
SearchResults results = tryFind(name); SearchResults results = tryFind(name);
if (results.Found) if (results.Found)
{ {
return onKeyFound(name, results.Value); return onKeyFound(name, results.Value, isExtension);
} }
object value; object value;
if (onKeyNotFound(name, results.Member, out value)) if (onKeyNotFound(name, results.Member, isExtension, out value))
{ {
return value; return value;
} }
@ -94,25 +95,25 @@ namespace Mustache
throw new KeyNotFoundException(message); throw new KeyNotFoundException(message);
} }
private object onKeyFound(string name, object value) private object onKeyFound(string name, object value, bool isExtension)
{ {
if (KeyFound == null) if (KeyFound == null)
{ {
return value; return value;
} }
KeyFoundEventArgs args = new KeyFoundEventArgs(name, value); KeyFoundEventArgs args = new KeyFoundEventArgs(name, value, isExtension);
KeyFound(this, args); KeyFound(this, args);
return args.Substitute; return args.Substitute;
} }
private bool onKeyNotFound(string name, string member, out object value) private bool onKeyNotFound(string name, string member, bool isExtension, out object value)
{ {
if (KeyNotFound == null) if (KeyNotFound == null)
{ {
value = null; value = null;
return false; return false;
} }
KeyNotFoundEventArgs args = new KeyNotFoundEventArgs(name, member); KeyNotFoundEventArgs args = new KeyNotFoundEventArgs(name, member, isExtension);
KeyNotFound(this, args); KeyNotFound(this, args);
if (!args.Handled) if (!args.Handled)
{ {
@ -125,7 +126,7 @@ namespace Mustache
private static IDictionary<string, object> toLookup(object value) private static IDictionary<string, object> toLookup(object value)
{ {
IDictionary<string, object> lookup = value as IDictionary<string, object>; IDictionary<string, object> lookup = UpcastDictionary.Create(value);
if (lookup == null) if (lookup == null)
{ {
lookup = new PropertyDictionary(value); lookup = new PropertyDictionary(value);

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
namespace Mustache namespace Mustache
@ -14,10 +13,17 @@ namespace Mustache
/// <summary> /// <summary>
/// Initializes a new instance of a StaticGenerator. /// Initializes a new instance of a StaticGenerator.
/// </summary> /// </summary>
public StaticGenerator(string value) public StaticGenerator(string value, bool removeNewLines)
{
if (removeNewLines)
{ {
this.value = value.Replace(Environment.NewLine, String.Empty); this.value = value.Replace(Environment.NewLine, String.Empty);
} }
else
{
this.value = value;
}
}
/// <summary> /// <summary>
/// Gets or sets the static text. /// Gets or sets the static text.
@ -27,7 +33,7 @@ namespace Mustache
get { return value; } get { return value; }
} }
void IGenerator.GetText(Scope scope, TextWriter writer, Scope context) void IGenerator.GetText(TextWriter writer, Scope scope, Scope context, Action<Substitution> postProcessor)
{ {
writer.Write(Value); writer.Write(Value);
} }

View File

@ -0,0 +1,24 @@
using System;
namespace Mustache
{
public class StringArgument : IArgument
{
private readonly string value;
public StringArgument(string value)
{
this.value = value;
}
public string GetKey()
{
return null;
}
public object GetValue(Scope keyScope, Scope contextScope)
{
return value;
}
}
}

View File

@ -0,0 +1,13 @@
using System;
namespace Mustache
{
internal class Substitution
{
public string Key { get; set; }
public string Substitute { get; set; }
public bool IsExtension { get; set; }
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace Mustache
{
/// <summary>
/// Holds the information about a tag that's been converted to text.
/// </summary>
public class TagFormattedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of a TagFormattedEventArgs.
/// </summary>
/// <param name="key">The fully-qualified key.</param>
/// <param name="value">The formatted value being extended.</param>
/// <param name="isExtension">Specifies whether the key was found within triple curly braces.</param>
internal TagFormattedEventArgs(string key, string value, bool isExtension)
{
Key = key;
Substitute = value;
IsExtension = isExtension;
}
/// <summary>
/// Gets the fully-qualified key.
/// </summary>
public string Key { get; private set; }
/// <summary>
/// Gets or sets whether the key appeared within triple curly braces.
/// </summary>
public bool IsExtension { get; private set; }
/// <summary>
/// Gets or sets the object to use as the substitute.
/// </summary>
public string Substitute { get; set; }
}
}

View File

@ -0,0 +1,212 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace Mustache
{
internal static class UpcastDictionary
{
public static IDictionary<string, object> Create(object source)
{
if (source == null)
{
return null;
}
IDictionary<string, object> sourceDictionary = source as IDictionary<string, object>;
if (sourceDictionary != null)
{
return sourceDictionary;
}
Type sourceType = source.GetType();
var types = getTypes(sourceType);
return getDictionary(types, source);
}
private static IEnumerable<Type> getTypes(Type sourceType)
{
Queue<Type> pending = new Queue<Type>();
HashSet<Type> visited = new HashSet<Type>();
pending.Enqueue(sourceType);
while (pending.Count != 0)
{
Type type = pending.Dequeue();
visited.Add(type);
yield return type;
if (type.BaseType != null)
{
if (!visited.Contains(type.BaseType))
{
pending.Enqueue(type.BaseType);
}
}
foreach (Type interfaceType in type.GetInterfaces())
{
if (!visited.Contains(interfaceType))
{
pending.Enqueue(interfaceType);
}
}
}
}
private static IDictionary<string, object> getDictionary(IEnumerable<Type> types, object source)
{
var dictionaries = from type in types
let valueType = getValueType(type)
where valueType != null
let upcastType = typeof(UpcastDictionary<>).MakeGenericType(valueType)
select (IDictionary<string, object>)Activator.CreateInstance(upcastType, source);
return dictionaries.FirstOrDefault();
}
private static Type getValueType(Type type)
{
if (!type.IsGenericType)
{
return null;
}
Type[] argumentTypes = type.GetGenericArguments();
if (argumentTypes.Length != 2)
{
return null;
}
Type keyType = argumentTypes[0];
if (keyType != typeof(string))
{
return null;
}
Type valueType = argumentTypes[1];
Type genericType = typeof(IDictionary<,>).MakeGenericType(typeof(string), valueType);
if (!genericType.IsAssignableFrom(type))
{
return null;
}
return valueType;
}
}
internal class UpcastDictionary<TValue> : IDictionary<string, object>
{
private readonly IDictionary<string, TValue> dictionary;
public UpcastDictionary(IDictionary<string, TValue> dictionary)
{
this.dictionary = dictionary;
}
[EditorBrowsable(EditorBrowsableState.Never)]
void IDictionary<string, object>.Add(string key, object value)
{
throw new NotSupportedException();
}
public bool ContainsKey(string key)
{
return dictionary.ContainsKey(key);
}
public ICollection<string> Keys
{
get { return dictionary.Keys; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
bool IDictionary<string, object>.Remove(string key)
{
throw new NotSupportedException();
}
public bool TryGetValue(string key, out object value)
{
TValue result;
if (dictionary.TryGetValue(key, out result))
{
value = result;
return true;
}
else
{
value = null;
return false;
}
}
public ICollection<object> Values
{
get { return dictionary.Values.Cast<object>().ToArray(); }
}
public object this[string key]
{
get
{
return dictionary[key];
}
[EditorBrowsable(EditorBrowsableState.Never)]
set
{
throw new NotSupportedException();
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
{
throw new NotSupportedException();
}
[EditorBrowsable(EditorBrowsableState.Never)]
void ICollection<KeyValuePair<string, object>>.Clear()
{
throw new NotSupportedException();
}
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
{
if (!(item.Value is TValue))
{
return false;
}
KeyValuePair<string, TValue> pair = new KeyValuePair<string,TValue>(item.Key, (TValue)item.Value);
ICollection<KeyValuePair<string, TValue>> collection = dictionary;
return dictionary.Contains(pair);
}
void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
var pairs = dictionary.Select(p => new KeyValuePair<string, object>(p.Key, p.Value)).ToArray();
pairs.CopyTo(array, arrayIndex);
}
public int Count
{
get { return dictionary.Count; }
}
bool ICollection<KeyValuePair<string, object>>.IsReadOnly
{
get { return true; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
{
throw new NotSupportedException();
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return dictionary.Select(p => new KeyValuePair<string, object>(p.Key, p.Value)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace Mustache
{
public class VariableArgument : IArgument
{
private readonly string name;
public VariableArgument(string name)
{
this.name = name;
}
public string GetKey()
{
return null;
}
public object GetValue(Scope keyScope, Scope contextScope)
{
return contextScope.Find(name, false);
}
}
}

View File

@ -14,12 +14,14 @@ namespace Mustache
/// <param name="key">The key that was found.</param> /// <param name="key">The key that was found.</param>
/// <param name="alignment">The alignment that will be applied to the substitute value.</param> /// <param name="alignment">The alignment that will be applied to the substitute value.</param>
/// <param name="formatting">The formatting that will be applied to the substitute value.</param> /// <param name="formatting">The formatting that will be applied to the substitute value.</param>
/// <param name="isExtension">Specifies whether the variable was found within triple curly braces.</param>
/// <param name="context">The context where the placeholder was found.</param> /// <param name="context">The context where the placeholder was found.</param>
internal VariableFoundEventArgs(string name, string alignment, string formatting, Context[] context) internal VariableFoundEventArgs(string name, string alignment, string formatting, bool isExtension, Context[] context)
{ {
Name = name; Name = name;
Alignment = alignment; Alignment = alignment;
Formatting = formatting; Formatting = formatting;
IsExtension = isExtension;
Context = context; Context = context;
} }
@ -38,6 +40,11 @@ namespace Mustache
/// </summary> /// </summary>
public string Formatting { get; set; } public string Formatting { get; set; }
/// <summary>
/// Gets or sets whether variable was found within triple curly braces.
/// </summary>
public bool IsExtension { get; set; }
/// <summary> /// <summary>
/// Gets the context where the placeholder was found. /// Gets the context where the placeholder was found.
/// </summary> /// </summary>

View File

@ -30,6 +30,12 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<SignAssembly>true</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>mustache-sharp.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Web" /> <Reference Include="System.Web" />
@ -48,6 +54,15 @@
<Compile Include="GtTagDefinition.cs" /> <Compile Include="GtTagDefinition.cs" />
<Compile Include="UrlDecodeTagDefinition.cs" /> <Compile Include="UrlDecodeTagDefinition.cs" />
<Compile Include="UrlEncodeTagDefinition.cs" /> <Compile Include="UrlEncodeTagDefinition.cs" />
<Compile Include="Substitution.cs" />
<Compile Include="TagFormattedEventArgs.cs" />
<Compile Include="HtmlFormatCompiler.cs" />
<Compile Include="IArgument.cs" />
<Compile Include="NumberArgument.cs" />
<Compile Include="PlaceholderArgument.cs" />
<Compile Include="StringArgument.cs" />
<Compile Include="UpcastDictionary.cs" />
<Compile Include="VariableArgument.cs" />
<Compile Include="VariableFoundEventArgs.cs" /> <Compile Include="VariableFoundEventArgs.cs" />
<Compile Include="SetTagDefinition.cs" /> <Compile Include="SetTagDefinition.cs" />
<Compile Include="NewlineTagDefinition.cs" /> <Compile Include="NewlineTagDefinition.cs" />
@ -88,6 +103,9 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput> <LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="mustache-sharp.snk" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

Binary file not shown.