diff --git a/Deployment/NuGet.exe b/Deployment/NuGet.exe index 324daa8..9f8781d 100644 Binary files a/Deployment/NuGet.exe and b/Deployment/NuGet.exe differ diff --git a/README.md b/README.md index b063c14..6294eb4 100644 --- a/README.md +++ b/README.md @@ -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. ## 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 Each format item takes the following form and consists of the following components: @@ -215,6 +215,21 @@ Here's an example of a tag that will join the items of a collection: 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 = @"{{escaped}} and {{{unescaped}}}"; + Generator generator = compiler.Compile(format); + string result = generator.Render(new + { + escaped = "Awesome", + unescaped = "sweet" + }); + // Generates <b>Awesome</b> and sweet ## License This is free and unencumbered software released into the public domain. diff --git a/mustache-sharp.test/HtmlFormatCompilerTester.cs b/mustache-sharp.test/HtmlFormatCompilerTester.cs new file mode 100644 index 0000000..60e5974 --- /dev/null +++ b/mustache-sharp.test/HtmlFormatCompilerTester.cs @@ -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("Hello, {{Name}}!!!"); + string html = generator.Render(new + { + Name = "John \"The Man\" Standford" + }); + Assert.AreEqual("Hello, John "The Man" Standford!!!", html); + } + + [TestMethod] + public void ShouldIgnoreHTMLCharactersInsideTripleCurlyBraces() + { + HtmlFormatCompiler compiler = new HtmlFormatCompiler(); + var generator = compiler.Compile("Hello, {{{Name}}}!!!"); + string html = generator.Render(new + { + Name = "John \"The Man\" Standford" + }); + Assert.AreEqual("Hello, John \"The Man\" Standford!!!", html); + } + } +} diff --git a/mustache-sharp.test/mustache-sharp.test.csproj b/mustache-sharp.test/mustache-sharp.test.csproj index f2df8c6..0d2e9e3 100644 --- a/mustache-sharp.test/mustache-sharp.test.csproj +++ b/mustache-sharp.test/mustache-sharp.test.csproj @@ -50,6 +50,7 @@ + diff --git a/mustache-sharp/CompoundGenerator.cs b/mustache-sharp/CompoundGenerator.cs index 2af4fcb..a2a9f9d 100644 --- a/mustache-sharp/CompoundGenerator.cs +++ b/mustache-sharp/CompoundGenerator.cs @@ -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 postProcessor) { Dictionary arguments = _arguments.GetArguments(keyScope, contextScope); IEnumerable contexts = _definition.GetChildContext(writer, keyScope, arguments, contextScope); @@ -80,7 +80,7 @@ namespace Mustache { 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) { writer.Write(_definition.ConsolidateWriter(context.Writer ?? writer, arguments)); diff --git a/mustache-sharp/FormatCompiler.cs b/mustache-sharp/FormatCompiler.cs index 530eb92..16f6027 100644 --- a/mustache-sharp/FormatCompiler.cs +++ b/mustache-sharp/FormatCompiler.cs @@ -60,6 +60,11 @@ namespace Mustache /// public bool RemoveNewLines { get; set; } + /// + /// Gets or sets whether the compiler searches for tags using triple curly braces. + /// + public bool AreExtensionTagsAllowed { get; set; } + /// /// Registers the given tag definition with the parser. /// @@ -129,7 +134,13 @@ namespace Mustache matches.Add(getTagRegex(childDefinition)); } matches.Add(getUnknownTagRegex()); - string match = "{{(" + String.Join("|", matches) + ")}}"; + string combined = String.Join("|", matches); + string match = "{{(?" + combined + ")}}"; + if (AreExtensionTagsAllowed) + { + string tripleMatch = "{{{(?" + combined + ")}}}"; + match = "(?:" + match + ")|(?:" + tripleMatch + ")"; + } regex = new Regex(match); _regexLookup.Add(definition.Name, regex); } @@ -176,7 +187,7 @@ namespace Mustache return regexBuilder.ToString(); } - private string getUnknownTagRegex() + private static string getUnknownTagRegex() { return @"(?(#.*?))"; } @@ -207,32 +218,35 @@ namespace Mustache { generator.AddGenerator(new StaticGenerator(leading, RemoveNewLines)); formatIndex = match.Index + match.Length; + bool isExtension = match.Groups["extension"].Success; string key = match.Groups["key"].Value; string alignment = match.Groups["alignment"].Value; string formatting = match.Groups["format"].Value; 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) { VariableFound(this, args); key = "@" + args.Name; alignment = args.Alignment; formatting = args.Formatting; + isExtension = args.IsExtension; } } else { - PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(key, alignment, formatting, context.ToArray()); + PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(key, alignment, formatting, isExtension, context.ToArray()); if (PlaceholderFound != null) { PlaceholderFound(this, args); key = args.Key; alignment = args.Alignment; formatting = args.Formatting; + isExtension = args.IsExtension; } } - KeyGenerator keyGenerator = new KeyGenerator(key, alignment, formatting); + KeyGenerator keyGenerator = new KeyGenerator(key, alignment, formatting, isExtension); generator.AddGenerator(keyGenerator); } else if (match.Groups["open"].Success) @@ -347,7 +361,7 @@ namespace Mustache if (placeholder.StartsWith("@")) { string variableName = placeholder.Substring(1); - VariableFoundEventArgs args = new VariableFoundEventArgs(placeholder.Substring(1), String.Empty, String.Empty, context.ToArray()); + VariableFoundEventArgs args = new VariableFoundEventArgs(placeholder.Substring(1), String.Empty, String.Empty, false, context.ToArray()); if (VariableFound != null) { VariableFound(this, args); @@ -371,7 +385,7 @@ namespace Mustache else { string placeholderName = placeholder; - PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(placeholder, String.Empty, String.Empty, context.ToArray()); + PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(placeholder, String.Empty, String.Empty, false, context.ToArray()); if (PlaceholderFound != null) { PlaceholderFound(this, args); diff --git a/mustache-sharp/Generator.cs b/mustache-sharp/Generator.cs index bc2f0c7..2cd47dd 100644 --- a/mustache-sharp/Generator.cs +++ b/mustache-sharp/Generator.cs @@ -54,6 +54,11 @@ namespace Mustache remove { _valueRequestedHandlers.Remove(value); } } + /// + /// Occurs when a tag is replaced by its text. + /// + public event EventHandler TagFormatted; + /// /// Gets the text that is generated for the given object. /// @@ -98,8 +103,19 @@ namespace Mustache contextScope.ValueRequested += handler; } StringWriter writer = new StringWriter(provider); - _generator.GetText(keyScope, writer, contextScope); + _generator.GetText(writer, keyScope, contextScope, postProcess); 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; + } } } diff --git a/mustache-sharp/HtmlFormatCompiler.cs b/mustache-sharp/HtmlFormatCompiler.cs new file mode 100644 index 0000000..3e4d24a --- /dev/null +++ b/mustache-sharp/HtmlFormatCompiler.cs @@ -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; + } + + /// + /// Occurs when a placeholder is found in the template. + /// + public event EventHandler PlaceholderFound + { + add { compiler.PlaceholderFound += value; } + remove { compiler.PlaceholderFound -= value; } + } + + /// + /// Occurs when a variable is found in the template. + /// + public event EventHandler VariableFound + { + add { compiler.VariableFound += value; } + remove { compiler.VariableFound -= value; } + } + + /// + /// Registers the given tag definition with the parser. + /// + /// The tag definition to register. + /// Specifies whether the tag is immediately in scope. + public void RegisterTag(TagDefinition definition, bool isTopLevel) + { + compiler.RegisterTag(definition, isTopLevel); + } + + /// + /// Builds a text generator based on the given format. + /// + /// The format to parse. + /// The text generator. + 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); + } + } +} diff --git a/mustache-sharp/IGenerator.cs b/mustache-sharp/IGenerator.cs index 6b2fd24..b961d55 100644 --- a/mustache-sharp/IGenerator.cs +++ b/mustache-sharp/IGenerator.cs @@ -11,10 +11,11 @@ namespace Mustache /// /// Generates the text when applying the format plan. /// - /// The current lexical scope of the keys. /// The text writer to send all text to. + /// The current lexical scope of the keys. /// The data associated to the context. + /// A function to apply after a substitution is made. /// The generated text. - void GetText(Scope keyScope, TextWriter writer, Scope contextScope); + void GetText(TextWriter writer, Scope keyScope, Scope contextScope, Action postProcessor); } } diff --git a/mustache-sharp/InlineGenerator.cs b/mustache-sharp/InlineGenerator.cs index 0093d15..f1ed40a 100644 --- a/mustache-sharp/InlineGenerator.cs +++ b/mustache-sharp/InlineGenerator.cs @@ -23,7 +23,7 @@ namespace Mustache _arguments = arguments; } - void IGenerator.GetText(Scope scope, TextWriter writer, Scope context) + void IGenerator.GetText(TextWriter writer, Scope scope, Scope context, Action postProcessor) { Dictionary arguments; if (_definition.IsSetter) diff --git a/mustache-sharp/KeyFoundEventArgs.cs b/mustache-sharp/KeyFoundEventArgs.cs index c4bf91d..1d686fb 100644 --- a/mustache-sharp/KeyFoundEventArgs.cs +++ b/mustache-sharp/KeyFoundEventArgs.cs @@ -11,7 +11,8 @@ namespace Mustache /// Initializes a new instance of a KeyFoundEventArgs. /// /// The fully-qualified key. - internal KeyFoundEventArgs(string key, object value) + /// Specifies whether the key was found within triple curly braces. + internal KeyFoundEventArgs(string key, object value, bool isExtension) { Key = key; Substitute = value; @@ -22,6 +23,11 @@ namespace Mustache /// public string Key { get; private set; } + /// + /// Gets or sets whether the key appeared within triple curly braces. + /// + public bool IsExtension { get; private set; } + /// /// Gets or sets the object to use as the substitute. /// diff --git a/mustache-sharp/KeyGenerator.cs b/mustache-sharp/KeyGenerator.cs index 2e568ac..8a0b282 100644 --- a/mustache-sharp/KeyGenerator.cs +++ b/mustache-sharp/KeyGenerator.cs @@ -12,6 +12,7 @@ namespace Mustache private readonly string _key; private readonly string _format; private readonly bool _isVariable; + private readonly bool _isExtension; /// /// Initializes a new instance of a KeyGenerator. @@ -19,7 +20,8 @@ namespace Mustache /// The key to substitute with its value. /// The alignment specifier. /// The format specifier. - public KeyGenerator(string key, string alignment, string formatting) + /// Specifies whether the key was found within triple curly braces. + public KeyGenerator(string key, string alignment, string formatting, bool isExtension) { if (key.StartsWith("@")) { @@ -32,6 +34,7 @@ namespace Mustache _isVariable = false; } _format = getFormat(alignment, formatting); + _isExtension = isExtension; } private static string getFormat(string alignment, string formatting) @@ -52,10 +55,18 @@ namespace Mustache return formatBuilder.ToString(); } - void IGenerator.GetText(Scope scope, TextWriter writer, Scope context) + void IGenerator.GetText(TextWriter writer, Scope scope, Scope context, Action postProcessor) { - object value = _isVariable ? context.Find(_key) : scope.Find(_key); - writer.Write(_format, value); + object value = _isVariable ? context.Find(_key, _isExtension) : scope.Find(_key, _isExtension); + string result = String.Format(writer.FormatProvider, _format, value); + Substitution substitution = new Substitution() + { + Key = _key, + Substitute = result, + IsExtension = _isExtension + }; + postProcessor(substitution); + writer.Write(substitution.Substitute); } } } diff --git a/mustache-sharp/KeyNotFoundEventArgs.cs b/mustache-sharp/KeyNotFoundEventArgs.cs index f29b69e..d0776bd 100644 --- a/mustache-sharp/KeyNotFoundEventArgs.cs +++ b/mustache-sharp/KeyNotFoundEventArgs.cs @@ -12,10 +12,12 @@ namespace Mustache /// /// The fully-qualified key. /// The part of the key that could not be found. - internal KeyNotFoundEventArgs(string key, string missingMember) + /// Specifies whether the key appears within triple curly braces. + internal KeyNotFoundEventArgs(string key, string missingMember, bool isExtension) { Key = key; MissingMember = missingMember; + IsExtension = isExtension; } /// @@ -28,6 +30,11 @@ namespace Mustache /// public string MissingMember { get; private set; } + /// + /// Gets whether the key appeared within triple curly braces. + /// + public bool IsExtension { get; private set; } + /// /// Gets or sets whether to use the substitute. /// diff --git a/mustache-sharp/PlaceholderArgument.cs b/mustache-sharp/PlaceholderArgument.cs index 39047f3..092f84c 100644 --- a/mustache-sharp/PlaceholderArgument.cs +++ b/mustache-sharp/PlaceholderArgument.cs @@ -18,7 +18,7 @@ namespace Mustache public object GetValue(Scope keyScope, Scope contextScope) { - return keyScope.Find(name); + return keyScope.Find(name, false); } } } diff --git a/mustache-sharp/PlaceholderFoundEventArgs.cs b/mustache-sharp/PlaceholderFoundEventArgs.cs index e415512..a194209 100644 --- a/mustache-sharp/PlaceholderFoundEventArgs.cs +++ b/mustache-sharp/PlaceholderFoundEventArgs.cs @@ -14,8 +14,9 @@ namespace Mustache /// The key that was found. /// The alignment that will be applied to the substitute value. /// The formatting that will be applied to the substitute value. + /// Indicates whether the placeholder was found within triple curly braces. /// The context where the placeholder was found. - internal PlaceholderFoundEventArgs(string key, string alignment, string formatting, Context[] context) + internal PlaceholderFoundEventArgs(string key, string alignment, string formatting, bool isExtension, Context[] context) { Key = key; Alignment = alignment; @@ -38,6 +39,11 @@ namespace Mustache /// public string Formatting { get; set; } + /// + /// Gets or sets whether the placeholder was found within triple curly braces. + /// + public bool IsExtension { get; set; } + /// /// Gets the context where the placeholder was found. /// diff --git a/mustache-sharp/Properties/AssemblyInfo.cs b/mustache-sharp/Properties/AssemblyInfo.cs index 68d853a..2d907ce 100644 --- a/mustache-sharp/Properties/AssemblyInfo.cs +++ b/mustache-sharp/Properties/AssemblyInfo.cs @@ -14,6 +14,6 @@ using System.Runtime.InteropServices; [assembly: CLSCompliant(true)] [assembly: ComVisible(false)] [assembly: Guid("e5a4263d-d450-4d85-a4d5-44c0a2822668")] -[assembly: AssemblyVersion("0.2.8.2")] -[assembly: AssemblyFileVersion("0.2.8.2")] +[assembly: AssemblyVersion("0.2.9.0")] +[assembly: AssemblyFileVersion("0.2.9.0")] [assembly: InternalsVisibleTo("mustache-sharp.test,PublicKey=0024000004800000940000000602000000240000525341310004000001000100755df5a2b24c568812aae0eb194d08a4e3cba960673bcc07a7d446acf52f3f56ae2155b37b8d547bc5d8c562823bd592d1312bef9ad4740a8bb503d0095c31419f9d190882a2fa46090412bf15b13ca0057ba533c85a853333132ec8b70cf19655ef961b06d1c3fc35b3f68680420562be741456cb7a18bd5ab0fa779f8d47b1")] \ No newline at end of file diff --git a/mustache-sharp/Scope.cs b/mustache-sharp/Scope.cs index f84c5b7..5310bd6 100644 --- a/mustache-sharp/Scope.cs +++ b/mustache-sharp/Scope.cs @@ -76,17 +76,18 @@ namespace Mustache /// Attempts to find the value associated with the key with given name. /// /// The name of the key. + /// Specifies whether the key appeared within triple curly braces. /// The value associated with the key with the given name. /// A key with the given name could not be found. - internal object Find(string name) + internal object Find(string name, bool isExtension) { SearchResults results = tryFind(name); if (results.Found) { - return onKeyFound(name, results.Value); + return onKeyFound(name, results.Value, isExtension); } object value; - if (onKeyNotFound(name, results.Member, out value)) + if (onKeyNotFound(name, results.Member, isExtension, out value)) { return value; } @@ -94,25 +95,25 @@ namespace Mustache throw new KeyNotFoundException(message); } - private object onKeyFound(string name, object value) + private object onKeyFound(string name, object value, bool isExtension) { if (KeyFound == null) { return value; } - KeyFoundEventArgs args = new KeyFoundEventArgs(name, value); + KeyFoundEventArgs args = new KeyFoundEventArgs(name, value, isExtension); KeyFound(this, args); 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) { value = null; return false; } - KeyNotFoundEventArgs args = new KeyNotFoundEventArgs(name, member); + KeyNotFoundEventArgs args = new KeyNotFoundEventArgs(name, member, isExtension); KeyNotFound(this, args); if (!args.Handled) { diff --git a/mustache-sharp/StaticGenerator.cs b/mustache-sharp/StaticGenerator.cs index af27a90..5afc28a 100644 --- a/mustache-sharp/StaticGenerator.cs +++ b/mustache-sharp/StaticGenerator.cs @@ -33,7 +33,7 @@ namespace Mustache get { return value; } } - void IGenerator.GetText(Scope scope, TextWriter writer, Scope context) + void IGenerator.GetText(TextWriter writer, Scope scope, Scope context, Action postProcessor) { writer.Write(Value); } diff --git a/mustache-sharp/Substitution.cs b/mustache-sharp/Substitution.cs new file mode 100644 index 0000000..ffcd9e2 --- /dev/null +++ b/mustache-sharp/Substitution.cs @@ -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; } + } +} diff --git a/mustache-sharp/TagFormattedEventArgs.cs b/mustache-sharp/TagFormattedEventArgs.cs new file mode 100644 index 0000000..c9263ea --- /dev/null +++ b/mustache-sharp/TagFormattedEventArgs.cs @@ -0,0 +1,38 @@ +using System; + +namespace Mustache +{ + /// + /// Holds the information about a tag that's been converted to text. + /// + public class TagFormattedEventArgs : EventArgs + { + /// + /// Initializes a new instance of a TagFormattedEventArgs. + /// + /// The fully-qualified key. + /// The formatted value being extended. + /// Specifies whether the key was found within triple curly braces. + internal TagFormattedEventArgs(string key, string value, bool isExtension) + { + Key = key; + Substitute = value; + IsExtension = isExtension; + } + + /// + /// Gets the fully-qualified key. + /// + public string Key { get; private set; } + + /// + /// Gets or sets whether the key appeared within triple curly braces. + /// + public bool IsExtension { get; private set; } + + /// + /// Gets or sets the object to use as the substitute. + /// + public string Substitute { get; set; } + } +} diff --git a/mustache-sharp/VariableArgument.cs b/mustache-sharp/VariableArgument.cs index c59e128..bac6125 100644 --- a/mustache-sharp/VariableArgument.cs +++ b/mustache-sharp/VariableArgument.cs @@ -18,7 +18,7 @@ namespace Mustache public object GetValue(Scope keyScope, Scope contextScope) { - return contextScope.Find(name); + return contextScope.Find(name, false); } } } diff --git a/mustache-sharp/VariableFoundEventArgs.cs b/mustache-sharp/VariableFoundEventArgs.cs index 2d15ccc..08efe20 100644 --- a/mustache-sharp/VariableFoundEventArgs.cs +++ b/mustache-sharp/VariableFoundEventArgs.cs @@ -14,12 +14,14 @@ namespace Mustache /// The key that was found. /// The alignment that will be applied to the substitute value. /// The formatting that will be applied to the substitute value. + /// Specifies whether the variable was found within triple curly braces. /// The context where the placeholder was found. - internal VariableFoundEventArgs(string name, string alignment, string formatting, Context[] context) + internal VariableFoundEventArgs(string name, string alignment, string formatting, bool isExtension, Context[] context) { Name = name; Alignment = alignment; Formatting = formatting; + IsExtension = isExtension; Context = context; } @@ -38,6 +40,11 @@ namespace Mustache /// public string Formatting { get; set; } + /// + /// Gets or sets whether variable was found within triple curly braces. + /// + public bool IsExtension { get; set; } + /// /// Gets the context where the placeholder was found. /// diff --git a/mustache-sharp/mustache-sharp.csproj b/mustache-sharp/mustache-sharp.csproj index 7a3695e..a4fad22 100644 --- a/mustache-sharp/mustache-sharp.csproj +++ b/mustache-sharp/mustache-sharp.csproj @@ -46,6 +46,9 @@ + + +