Direct support for HTML formatting

This commit is contained in:
jehugaleahsa 2016-03-21 13:41:46 -04:00
parent 929764e58a
commit bb1b6cc936
23 changed files with 272 additions and 34 deletions

Binary file not shown.

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.
## 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 = @"<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.

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

@ -50,6 +50,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="FormatCompilerTester.cs" />
<Compile Include="HtmlFormatCompilerTester.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UpcastDictionaryTester.cs" />
</ItemGroup>

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);
IEnumerable<NestedContext> 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));

View File

@ -60,6 +60,11 @@ namespace Mustache
/// </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>
/// Registers the given tag definition with the parser.
/// </summary>
@ -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 = "{{(?<match>" + combined + ")}}";
if (AreExtensionTagsAllowed)
{
string tripleMatch = "{{{(?<extension>" + 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 @"(?<unknown>(#.*?))";
}
@ -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);

View File

@ -54,6 +54,11 @@ namespace Mustache
remove { _valueRequestedHandlers.Remove(value); }
}
/// <summary>
/// Occurs when a tag is replaced by its text.
/// </summary>
public event EventHandler<TagFormattedEventArgs> TagFormatted;
/// <summary>
/// Gets the text that is generated for the given object.
/// </summary>
@ -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;
}
}
}

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

@ -11,10 +11,11 @@ namespace Mustache
/// <summary>
/// Generates the text when applying the format plan.
/// </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="keyScope">The current lexical scope of the keys.</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>
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;
}
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;
if (_definition.IsSetter)

View File

@ -11,7 +11,8 @@ namespace Mustache
/// Initializes a new instance of a KeyFoundEventArgs.
/// </summary>
/// <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;
Substitute = value;
@ -22,6 +23,11 @@ namespace Mustache
/// </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>

View File

@ -12,6 +12,7 @@ namespace Mustache
private readonly string _key;
private readonly string _format;
private readonly bool _isVariable;
private readonly bool _isExtension;
/// <summary>
/// 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="alignment">The alignment 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("@"))
{
@ -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<Substitution> 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);
}
}
}

View File

@ -12,10 +12,12 @@ namespace Mustache
/// </summary>
/// <param name="key">The fully-qualified key.</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;
MissingMember = missingMember;
IsExtension = isExtension;
}
/// <summary>
@ -28,6 +30,11 @@ namespace Mustache
/// </summary>
public string MissingMember { get; private set; }
/// <summary>
/// Gets whether the key appeared within triple curly braces.
/// </summary>
public bool IsExtension { get; private set; }
/// <summary>
/// Gets or sets whether to use the substitute.
/// </summary>

View File

@ -18,7 +18,7 @@ namespace Mustache
public object GetValue(Scope keyScope, Scope contextScope)
{
return keyScope.Find(name);
return keyScope.Find(name, false);
}
}
}

View File

@ -14,8 +14,9 @@ namespace Mustache
/// <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="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>
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
/// </summary>
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>
/// Gets the context where the placeholder was found.
/// </summary>

View File

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

View File

@ -76,17 +76,18 @@ namespace Mustache
/// Attempts to find the value associated with the key with given name.
/// </summary>
/// <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>
/// <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);
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)
{

View File

@ -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<Substitution> postProcessor)
{
writer.Write(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

@ -18,7 +18,7 @@ namespace Mustache
public object GetValue(Scope keyScope, Scope contextScope)
{
return contextScope.Find(name);
return contextScope.Find(name, false);
}
}
}

View File

@ -14,12 +14,14 @@ namespace Mustache
/// <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="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>
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
/// </summary>
public string Formatting { get; set; }
/// <summary>
/// Gets or sets whether variable was found within triple curly braces.
/// </summary>
public bool IsExtension { get; set; }
/// <summary>
/// Gets the context where the placeholder was found.
/// </summary>

View File

@ -46,6 +46,9 @@
<Compile Include="ContentTagDefinition.cs" />
<Compile Include="Context.cs" />
<Compile Include="ContextParameter.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" />