2013-04-24 12:58:19 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.RegularExpressions;
|
2013-05-03 12:44:51 +00:00
|
|
|
|
using Mustache.Properties;
|
2013-04-24 12:58:19 +00:00
|
|
|
|
|
2013-05-03 12:44:51 +00:00
|
|
|
|
namespace Mustache
|
2013-04-24 12:58:19 +00:00
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Parses a format string and returns a text generator.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class FormatCompiler
|
|
|
|
|
{
|
|
|
|
|
private readonly Dictionary<string, TagDefinition> _tagLookup;
|
|
|
|
|
private readonly Dictionary<string, Regex> _regexLookup;
|
|
|
|
|
private readonly MasterTagDefinition _masterDefinition;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initializes a new instance of a FormatCompiler.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public FormatCompiler()
|
|
|
|
|
{
|
|
|
|
|
_tagLookup = new Dictionary<string, TagDefinition>();
|
|
|
|
|
_regexLookup = new Dictionary<string, Regex>();
|
|
|
|
|
_masterDefinition = new MasterTagDefinition();
|
|
|
|
|
|
|
|
|
|
IfTagDefinition ifDefinition = new IfTagDefinition();
|
|
|
|
|
_tagLookup.Add(ifDefinition.Name, ifDefinition);
|
|
|
|
|
ElifTagDefinition elifDefinition = new ElifTagDefinition();
|
|
|
|
|
_tagLookup.Add(elifDefinition.Name, elifDefinition);
|
|
|
|
|
ElseTagDefinition elseDefinition = new ElseTagDefinition();
|
|
|
|
|
_tagLookup.Add(elseDefinition.Name, elseDefinition);
|
|
|
|
|
EachTagDefinition eachDefinition = new EachTagDefinition();
|
|
|
|
|
_tagLookup.Add(eachDefinition.Name, eachDefinition);
|
2013-07-20 16:06:38 +00:00
|
|
|
|
IndexTagDefinition indexDefinition = new IndexTagDefinition();
|
|
|
|
|
_tagLookup.Add(indexDefinition.Name, indexDefinition);
|
2013-04-24 12:58:19 +00:00
|
|
|
|
WithTagDefinition withDefinition = new WithTagDefinition();
|
|
|
|
|
_tagLookup.Add(withDefinition.Name, withDefinition);
|
2013-07-23 12:44:48 +00:00
|
|
|
|
NewlineTagDefinition newlineDefinition = new NewlineTagDefinition();
|
|
|
|
|
_tagLookup.Add(newlineDefinition.Name, newlineDefinition);
|
2013-08-17 03:35:46 +00:00
|
|
|
|
SetTagDefinition setDefinition = new SetTagDefinition();
|
|
|
|
|
_tagLookup.Add(setDefinition.Name, setDefinition);
|
2013-04-24 12:58:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Occurs when a placeholder is found in the template.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public event EventHandler<PlaceholderFoundEventArgs> PlaceholderFound;
|
|
|
|
|
|
2013-10-30 19:39:38 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Occurs when a variable is found in the template.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public event EventHandler<VariableFoundEventArgs> VariableFound;
|
|
|
|
|
|
2013-04-24 12:58:19 +00:00
|
|
|
|
/// <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)
|
|
|
|
|
{
|
|
|
|
|
if (definition == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException("definition");
|
|
|
|
|
}
|
|
|
|
|
if (_tagLookup.ContainsKey(definition.Name))
|
|
|
|
|
{
|
|
|
|
|
string message = String.Format(Resources.DuplicateTagDefinition, definition.Name);
|
|
|
|
|
throw new ArgumentException(message, "definition");
|
|
|
|
|
}
|
|
|
|
|
_tagLookup.Add(definition.Name, definition);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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)
|
|
|
|
|
{
|
|
|
|
|
if (format == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException("format");
|
|
|
|
|
}
|
|
|
|
|
CompoundGenerator generator = new CompoundGenerator(_masterDefinition, new ArgumentCollection());
|
2013-04-25 12:46:03 +00:00
|
|
|
|
List<Context> context = new List<Context>() { new Context(_masterDefinition.Name, new ContextParameter[0]) };
|
2013-07-23 12:44:48 +00:00
|
|
|
|
int formatIndex = buildCompoundGenerator(_masterDefinition, context, generator, format, 0);
|
2013-04-24 12:58:19 +00:00
|
|
|
|
string trailing = format.Substring(formatIndex);
|
2013-07-23 12:44:48 +00:00
|
|
|
|
generator.AddGenerator(new StaticGenerator(trailing));
|
2013-04-24 12:58:19 +00:00
|
|
|
|
return new Generator(generator);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Match findNextTag(TagDefinition definition, string format, int formatIndex)
|
|
|
|
|
{
|
|
|
|
|
Regex regex = prepareRegex(definition);
|
|
|
|
|
return regex.Match(format, formatIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Regex prepareRegex(TagDefinition definition)
|
|
|
|
|
{
|
|
|
|
|
Regex regex;
|
|
|
|
|
if (!_regexLookup.TryGetValue(definition.Name, out regex))
|
|
|
|
|
{
|
|
|
|
|
List<string> matches = new List<string>();
|
|
|
|
|
matches.Add(getKeyRegex());
|
|
|
|
|
matches.Add(getCommentTagRegex());
|
|
|
|
|
foreach (string closingTag in definition.ClosingTags)
|
|
|
|
|
{
|
|
|
|
|
matches.Add(getClosingTagRegex(closingTag));
|
|
|
|
|
}
|
|
|
|
|
foreach (TagDefinition globalDefinition in _tagLookup.Values)
|
|
|
|
|
{
|
|
|
|
|
if (!globalDefinition.IsContextSensitive)
|
|
|
|
|
{
|
|
|
|
|
matches.Add(getTagRegex(globalDefinition));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
foreach (string childTag in definition.ChildTags)
|
|
|
|
|
{
|
|
|
|
|
TagDefinition childDefinition = _tagLookup[childTag];
|
|
|
|
|
matches.Add(getTagRegex(childDefinition));
|
|
|
|
|
}
|
|
|
|
|
matches.Add(getUnknownTagRegex());
|
|
|
|
|
string match = "{{(" + String.Join("|", matches) + ")}}";
|
2013-05-01 14:48:25 +00:00
|
|
|
|
regex = new Regex(match);
|
2013-04-24 12:58:19 +00:00
|
|
|
|
_regexLookup.Add(definition.Name, regex);
|
|
|
|
|
}
|
|
|
|
|
return regex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string getClosingTagRegex(string tagName)
|
|
|
|
|
{
|
|
|
|
|
StringBuilder regexBuilder = new StringBuilder();
|
|
|
|
|
regexBuilder.Append(@"(?<close>(/(?<name>");
|
|
|
|
|
regexBuilder.Append(tagName);
|
|
|
|
|
regexBuilder.Append(@")\s*?))");
|
|
|
|
|
return regexBuilder.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string getCommentTagRegex()
|
|
|
|
|
{
|
|
|
|
|
return @"(?<comment>#!.*?)";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string getKeyRegex()
|
|
|
|
|
{
|
2013-10-30 19:39:38 +00:00
|
|
|
|
return @"((?<key>@?" + RegexHelper.CompoundKey + @")(,(?<alignment>(\+|-)?[\d]+))?(:(?<format>.*?))?)";
|
2013-04-24 12:58:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string getTagRegex(TagDefinition definition)
|
|
|
|
|
{
|
|
|
|
|
StringBuilder regexBuilder = new StringBuilder();
|
|
|
|
|
regexBuilder.Append(@"(?<open>(#(?<name>");
|
|
|
|
|
regexBuilder.Append(definition.Name);
|
|
|
|
|
regexBuilder.Append(@")");
|
|
|
|
|
foreach (TagParameter parameter in definition.Parameters)
|
|
|
|
|
{
|
|
|
|
|
regexBuilder.Append(@"(\s+?");
|
2013-10-28 19:58:50 +00:00
|
|
|
|
regexBuilder.Append(@"(?<argument>(@?");
|
2013-04-24 12:58:19 +00:00
|
|
|
|
regexBuilder.Append(RegexHelper.CompoundKey);
|
2013-08-17 03:35:46 +00:00
|
|
|
|
regexBuilder.Append(@")))");
|
2013-04-24 12:58:19 +00:00
|
|
|
|
if (!parameter.IsRequired)
|
|
|
|
|
{
|
|
|
|
|
regexBuilder.Append("?");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
regexBuilder.Append(@"\s*?))");
|
|
|
|
|
return regexBuilder.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string getUnknownTagRegex()
|
|
|
|
|
{
|
|
|
|
|
return @"(?<unknown>(#.*?))";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int buildCompoundGenerator(
|
2013-04-25 01:21:00 +00:00
|
|
|
|
TagDefinition tagDefinition,
|
|
|
|
|
List<Context> context,
|
2013-04-24 12:58:19 +00:00
|
|
|
|
CompoundGenerator generator,
|
|
|
|
|
string format, int formatIndex)
|
|
|
|
|
{
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
Match match = findNextTag(tagDefinition, format, formatIndex);
|
|
|
|
|
|
|
|
|
|
if (!match.Success)
|
|
|
|
|
{
|
|
|
|
|
if (tagDefinition.ClosingTags.Any())
|
|
|
|
|
{
|
|
|
|
|
string message = String.Format(Resources.MissingClosingTag, tagDefinition.Name);
|
|
|
|
|
throw new FormatException(message);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string leading = format.Substring(formatIndex, match.Index - formatIndex);
|
|
|
|
|
|
|
|
|
|
if (match.Groups["key"].Success)
|
|
|
|
|
{
|
2013-07-23 12:44:48 +00:00
|
|
|
|
generator.AddGenerator(new StaticGenerator(leading));
|
2013-04-24 12:58:19 +00:00
|
|
|
|
formatIndex = match.Index + match.Length;
|
|
|
|
|
string key = match.Groups["key"].Value;
|
|
|
|
|
string alignment = match.Groups["alignment"].Value;
|
|
|
|
|
string formatting = match.Groups["format"].Value;
|
2013-10-30 19:39:38 +00:00
|
|
|
|
if (key.StartsWith("@"))
|
2013-04-24 12:58:19 +00:00
|
|
|
|
{
|
2013-10-30 19:39:38 +00:00
|
|
|
|
VariableFoundEventArgs args = new VariableFoundEventArgs(key.Substring(1), alignment, formatting, context.ToArray());
|
|
|
|
|
if (VariableFound != null)
|
|
|
|
|
{
|
|
|
|
|
VariableFound(this, args);
|
|
|
|
|
key = "@" + args.Name;
|
|
|
|
|
alignment = args.Alignment;
|
|
|
|
|
formatting = args.Formatting;
|
|
|
|
|
}
|
2013-04-24 12:58:19 +00:00
|
|
|
|
}
|
2013-10-30 19:39:38 +00:00
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(key, alignment, formatting, context.ToArray());
|
|
|
|
|
if (PlaceholderFound != null)
|
|
|
|
|
{
|
|
|
|
|
PlaceholderFound(this, args);
|
|
|
|
|
key = args.Key;
|
|
|
|
|
alignment = args.Alignment;
|
|
|
|
|
formatting = args.Formatting;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
KeyGenerator keyGenerator = new KeyGenerator(key, alignment, formatting);
|
2013-04-24 12:58:19 +00:00
|
|
|
|
generator.AddGenerator(keyGenerator);
|
|
|
|
|
}
|
|
|
|
|
else if (match.Groups["open"].Success)
|
|
|
|
|
{
|
|
|
|
|
formatIndex = match.Index + match.Length;
|
|
|
|
|
string tagName = match.Groups["name"].Value;
|
|
|
|
|
TagDefinition nextDefinition = _tagLookup[tagName];
|
|
|
|
|
if (nextDefinition == null)
|
|
|
|
|
{
|
|
|
|
|
string message = String.Format(Resources.UnknownTag, tagName);
|
|
|
|
|
throw new FormatException(message);
|
|
|
|
|
}
|
2013-10-30 19:39:38 +00:00
|
|
|
|
|
|
|
|
|
generator.AddGenerator(new StaticGenerator(leading));
|
|
|
|
|
ArgumentCollection arguments = getArguments(nextDefinition, match, context);
|
|
|
|
|
|
2013-04-24 12:58:19 +00:00
|
|
|
|
if (nextDefinition.HasContent)
|
|
|
|
|
{
|
|
|
|
|
CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments);
|
2013-04-25 12:46:03 +00:00
|
|
|
|
IEnumerable<TagParameter> contextParameters = nextDefinition.GetChildContextParameters();
|
|
|
|
|
bool hasContext = contextParameters.Any();
|
|
|
|
|
if (hasContext)
|
2013-04-25 01:21:00 +00:00
|
|
|
|
{
|
2013-04-25 12:46:03 +00:00
|
|
|
|
ContextParameter[] parameters = contextParameters.Select(p => new ContextParameter(p.Name, arguments.GetKey(p))).ToArray();
|
|
|
|
|
context.Add(new Context(nextDefinition.Name, parameters));
|
2013-04-25 01:21:00 +00:00
|
|
|
|
}
|
2013-07-23 12:44:48 +00:00
|
|
|
|
formatIndex = buildCompoundGenerator(nextDefinition, context, compoundGenerator, format, formatIndex);
|
2013-04-24 12:58:19 +00:00
|
|
|
|
generator.AddGenerator(nextDefinition, compoundGenerator);
|
2013-04-25 12:46:03 +00:00
|
|
|
|
if (hasContext)
|
|
|
|
|
{
|
|
|
|
|
context.RemoveAt(context.Count - 1);
|
|
|
|
|
}
|
2013-04-24 12:58:19 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
InlineGenerator inlineGenerator = new InlineGenerator(nextDefinition, arguments);
|
|
|
|
|
generator.AddGenerator(inlineGenerator);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (match.Groups["close"].Success)
|
|
|
|
|
{
|
2013-07-23 12:44:48 +00:00
|
|
|
|
generator.AddGenerator(new StaticGenerator(leading));
|
2013-04-24 12:58:19 +00:00
|
|
|
|
string tagName = match.Groups["name"].Value;
|
|
|
|
|
TagDefinition nextDefinition = _tagLookup[tagName];
|
|
|
|
|
formatIndex = match.Index;
|
|
|
|
|
if (tagName == tagDefinition.Name)
|
|
|
|
|
{
|
|
|
|
|
formatIndex += match.Length;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
else if (match.Groups["comment"].Success)
|
|
|
|
|
{
|
2013-07-23 12:44:48 +00:00
|
|
|
|
generator.AddGenerator(new StaticGenerator(leading));
|
2013-04-24 12:58:19 +00:00
|
|
|
|
formatIndex = match.Index + match.Length;
|
|
|
|
|
}
|
|
|
|
|
else if (match.Groups["unknown"].Success)
|
|
|
|
|
{
|
2013-07-18 16:04:12 +00:00
|
|
|
|
string tagName = match.Value;
|
|
|
|
|
string message = String.Format(Resources.UnknownTag, tagName);
|
|
|
|
|
throw new FormatException(message);
|
2013-04-24 12:58:19 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return formatIndex;
|
|
|
|
|
}
|
|
|
|
|
|
2013-10-30 19:39:38 +00:00
|
|
|
|
private ArgumentCollection getArguments(TagDefinition definition, Match match, List<Context> context)
|
2013-04-24 12:58:19 +00:00
|
|
|
|
{
|
2013-10-30 19:39:38 +00:00
|
|
|
|
// make sure we don't have too many arguments
|
2013-04-24 12:58:19 +00:00
|
|
|
|
List<Capture> captures = match.Groups["argument"].Captures.Cast<Capture>().ToList();
|
|
|
|
|
List<TagParameter> parameters = definition.Parameters.ToList();
|
|
|
|
|
if (captures.Count > parameters.Count)
|
|
|
|
|
{
|
|
|
|
|
string message = String.Format(Resources.WrongNumberOfArguments, definition.Name);
|
|
|
|
|
throw new FormatException(message);
|
|
|
|
|
}
|
2013-10-30 19:39:38 +00:00
|
|
|
|
|
|
|
|
|
// provide default values for missing arguments
|
2013-04-24 12:58:19 +00:00
|
|
|
|
if (captures.Count < parameters.Count)
|
|
|
|
|
{
|
|
|
|
|
captures.AddRange(Enumerable.Repeat((Capture)null, parameters.Count - captures.Count));
|
|
|
|
|
}
|
2013-10-30 19:39:38 +00:00
|
|
|
|
|
|
|
|
|
// pair up the parameters to the given arguments
|
|
|
|
|
// provide default for parameters with missing arguments
|
|
|
|
|
// throw an error if a missing argument is for a required parameter
|
|
|
|
|
Dictionary<TagParameter, string> arguments = new Dictionary<TagParameter, string>();
|
2013-04-24 12:58:19 +00:00
|
|
|
|
foreach (var pair in parameters.Zip(captures, (p, c) => new { Capture = c, Parameter = p }))
|
|
|
|
|
{
|
2013-10-30 19:39:38 +00:00
|
|
|
|
string value = null;
|
|
|
|
|
if (pair.Capture != null)
|
|
|
|
|
{
|
|
|
|
|
value = pair.Capture.Value;
|
|
|
|
|
}
|
|
|
|
|
else if (pair.Parameter.IsRequired)
|
|
|
|
|
{
|
|
|
|
|
string message = String.Format(Resources.WrongNumberOfArguments, definition.Name);
|
|
|
|
|
throw new FormatException(message);
|
|
|
|
|
}
|
|
|
|
|
arguments.Add(pair.Parameter, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// indicate that a key/variable has been encountered
|
|
|
|
|
// update the key/variable name
|
|
|
|
|
ArgumentCollection collection = new ArgumentCollection();
|
|
|
|
|
foreach (var pair in arguments)
|
|
|
|
|
{
|
|
|
|
|
string placeholder = pair.Value;
|
|
|
|
|
if (placeholder != null)
|
2013-04-24 12:58:19 +00:00
|
|
|
|
{
|
2013-10-30 19:39:38 +00:00
|
|
|
|
if (placeholder.StartsWith("@"))
|
2013-04-24 12:58:19 +00:00
|
|
|
|
{
|
2013-10-30 19:39:38 +00:00
|
|
|
|
VariableFoundEventArgs args = new VariableFoundEventArgs(placeholder.Substring(1), String.Empty, String.Empty, context.ToArray());
|
|
|
|
|
if (VariableFound != null)
|
|
|
|
|
{
|
|
|
|
|
VariableFound(this, args);
|
|
|
|
|
placeholder = "@" + args.Name;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(placeholder, String.Empty, String.Empty, context.ToArray());
|
|
|
|
|
if (PlaceholderFound != null)
|
|
|
|
|
{
|
|
|
|
|
PlaceholderFound(this, args);
|
|
|
|
|
placeholder = args.Key;
|
|
|
|
|
}
|
2013-04-24 12:58:19 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2013-10-30 19:39:38 +00:00
|
|
|
|
collection.AddArgument(pair.Key, placeholder);
|
2013-04-24 12:58:19 +00:00
|
|
|
|
}
|
|
|
|
|
return collection;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|