From 20dcfc059d83d4f9971aae4d33fa385216bdf935 Mon Sep 17 00:00:00 2001 From: Travis Parks Date: Wed, 24 Apr 2013 08:58:19 -0400 Subject: [PATCH] Fire event whenever a placeholder is found. It could be useful to track which placeholders were found when parsing the template. This event will allow keys to be mapped to different keys, and support changing alignment and formatting. --- mustache-sharp.test/FormatCompilerTester.cs | 23 +- .../Properties/AssemblyInfo.cs | 4 +- mustache-sharp/FormatCompiler.cs | 568 +++++++++--------- mustache-sharp/PlaceholderFoundEventArgs.cs | 39 ++ mustache-sharp/Properties/AssemblyInfo.cs | 4 +- mustache-sharp/RegexHelper.cs | 54 +- mustache-sharp/mustache-sharp.csproj | 1 + 7 files changed, 383 insertions(+), 310 deletions(-) create mode 100644 mustache-sharp/PlaceholderFoundEventArgs.cs diff --git a/mustache-sharp.test/FormatCompilerTester.cs b/mustache-sharp.test/FormatCompilerTester.cs index 92d1085..71aaf22 100644 --- a/mustache-sharp.test/FormatCompilerTester.cs +++ b/mustache-sharp.test/FormatCompilerTester.cs @@ -1,8 +1,8 @@ using System; -using System.Globalization; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Generic; using System.IO; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace mustache.test { @@ -327,6 +327,25 @@ Content"; Assert.AreEqual(expected, result, "The wrong text was generated."); } + /// + /// We can track all of the keys that appear in a template by + /// registering with the PlaceholderFound event. + /// + [TestMethod] + public void TestCompile_FindsKeys_RecordsKeys() + { + FormatCompiler compiler = new FormatCompiler(); + HashSet keys = new HashSet(); + compiler.PlaceholderFound += (o, e) => + { + keys.Add(e.Key); + }; + compiler.Compile(@"{{FirstName}} {{LastName}}"); + string[] expected = new string[] { "FirstName", "LastName" }; + string[] actual = keys.OrderBy(s => s).ToArray(); + CollectionAssert.AreEqual(expected, actual, "Not all placeholders were found."); + } + #endregion #region Comment diff --git a/mustache-sharp.test/Properties/AssemblyInfo.cs b/mustache-sharp.test/Properties/AssemblyInfo.cs index a321787..b5286ae 100644 --- a/mustache-sharp.test/Properties/AssemblyInfo.cs +++ b/mustache-sharp.test/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("0.0.5.0")] -[assembly: AssemblyFileVersion("0.0.5.0")] +[assembly: AssemblyVersion("0.0.6.0")] +[assembly: AssemblyFileVersion("0.0.6.0")] diff --git a/mustache-sharp/FormatCompiler.cs b/mustache-sharp/FormatCompiler.cs index 9f22b44..2f483e1 100644 --- a/mustache-sharp/FormatCompiler.cs +++ b/mustache-sharp/FormatCompiler.cs @@ -1,279 +1,289 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using mustache.Properties; - -namespace mustache -{ - /// - /// Parses a format string and returns a text generator. - /// - public sealed class FormatCompiler - { - private readonly Dictionary _tagLookup; - private readonly Dictionary _regexLookup; - private readonly MasterTagDefinition _masterDefinition; - - /// - /// Initializes a new instance of a FormatCompiler. - /// - public FormatCompiler() - { - _tagLookup = new Dictionary(); - _regexLookup = new Dictionary(); - _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); - WithTagDefinition withDefinition = new WithTagDefinition(); - _tagLookup.Add(withDefinition.Name, withDefinition); - } - - /// - /// 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) - { - 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); - } - - /// - /// Builds a text generator based on the given format. - /// - /// The format to parse. - /// The text generator. - public Generator Compile(string format) - { - if (format == null) - { - throw new ArgumentNullException("format"); - } - CompoundGenerator generator = new CompoundGenerator(_masterDefinition, new ArgumentCollection()); - Trimmer trimmer = new Trimmer(); - int formatIndex = buildCompoundGenerator(_masterDefinition, generator, trimmer, format, 0); - string trailing = format.Substring(formatIndex); - generator.AddStaticGenerators(trimmer.RecordText(trailing, false, false)); - trimmer.Trim(); - 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 matches = new List(); - 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) + ")}}"; - regex = new Regex(match, RegexOptions.Compiled); - _regexLookup.Add(definition.Name, regex); - } - return regex; - } - - private static string getClosingTagRegex(string tagName) - { - StringBuilder regexBuilder = new StringBuilder(); - regexBuilder.Append(@"(?(/(?"); - regexBuilder.Append(tagName); - regexBuilder.Append(@")\s*?))"); - return regexBuilder.ToString(); - } - - private static string getCommentTagRegex() - { - return @"(?#!.*?)"; - } - - private static string getKeyRegex() - { - return @"((?" + RegexHelper.CompoundKey + @")(,(?(\+|-)?[\d]+))?(:(?.*?))?)"; - } - - private static string getTagRegex(TagDefinition definition) - { - StringBuilder regexBuilder = new StringBuilder(); - regexBuilder.Append(@"(?(#(?"); - regexBuilder.Append(definition.Name); - regexBuilder.Append(@")"); - foreach (TagParameter parameter in definition.Parameters) - { - regexBuilder.Append(@"(\s+?"); - regexBuilder.Append(@"(?"); - regexBuilder.Append(RegexHelper.CompoundKey); - regexBuilder.Append(@"))"); - if (!parameter.IsRequired) - { - regexBuilder.Append("?"); - } - } - regexBuilder.Append(@"\s*?))"); - return regexBuilder.ToString(); - } - - private string getUnknownTagRegex() - { - return @"(?(#.*?))"; - } - - private int buildCompoundGenerator( - TagDefinition tagDefinition, - CompoundGenerator generator, - Trimmer trimmer, - 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) - { - generator.AddStaticGenerators(trimmer.RecordText(leading, true, true)); - formatIndex = match.Index + match.Length; - string key = match.Groups["key"].Value; - string alignment = match.Groups["alignment"].Value; - string formatting = match.Groups["format"].Value; - KeyGenerator keyGenerator = new KeyGenerator(key, alignment, formatting); - 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); - } - if (nextDefinition.HasContent) - { - generator.AddStaticGenerators(trimmer.RecordText(leading, true, false)); - ArgumentCollection arguments = getArguments(nextDefinition, match); - CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments); - formatIndex = buildCompoundGenerator(nextDefinition, compoundGenerator, trimmer, format, formatIndex); - generator.AddGenerator(nextDefinition, compoundGenerator); - } - else - { - generator.AddStaticGenerators(trimmer.RecordText(leading, true, true)); - Match nextMatch = findNextTag(nextDefinition, format, formatIndex); - ArgumentCollection arguments = getArguments(nextDefinition, nextMatch); - InlineGenerator inlineGenerator = new InlineGenerator(nextDefinition, arguments); - generator.AddGenerator(inlineGenerator); - } - } - else if (match.Groups["close"].Success) - { - generator.AddStaticGenerators(trimmer.RecordText(leading, true, false)); - 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) - { - generator.AddStaticGenerators(trimmer.RecordText(leading, true, false)); - formatIndex = match.Index + match.Length; - } - else if (match.Groups["unknown"].Success) - { - throw new FormatException(Resources.UnknownTag); - } - } - return formatIndex; - } - - private static ArgumentCollection getArguments(TagDefinition definition, Match match) - { - ArgumentCollection collection = new ArgumentCollection(); - List captures = match.Groups["argument"].Captures.Cast().ToList(); - List parameters = definition.Parameters.ToList(); - if (captures.Count > parameters.Count) - { - string message = String.Format(Resources.WrongNumberOfArguments, definition.Name); - throw new FormatException(message); - } - if (captures.Count < parameters.Count) - { - captures.AddRange(Enumerable.Repeat((Capture)null, parameters.Count - captures.Count)); - } - foreach (var pair in parameters.Zip(captures, (p, c) => new { Capture = c, Parameter = p })) - { - if (pair.Capture == null) - { - if (pair.Parameter.IsRequired) - { - string message = String.Format(Resources.WrongNumberOfArguments, definition.Name); - throw new FormatException(message); - } - collection.AddArgument(pair.Parameter, null); - } - else - { - collection.AddArgument(pair.Parameter, pair.Capture.Value); - } - } - return collection; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using mustache.Properties; + +namespace mustache +{ + /// + /// Parses a format string and returns a text generator. + /// + public sealed class FormatCompiler + { + private readonly Dictionary _tagLookup; + private readonly Dictionary _regexLookup; + private readonly MasterTagDefinition _masterDefinition; + + /// + /// Initializes a new instance of a FormatCompiler. + /// + public FormatCompiler() + { + _tagLookup = new Dictionary(); + _regexLookup = new Dictionary(); + _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); + WithTagDefinition withDefinition = new WithTagDefinition(); + _tagLookup.Add(withDefinition.Name, withDefinition); + } + + /// + /// Occurs when a placeholder is found in the template. + /// + public event EventHandler PlaceholderFound; + + /// + /// 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) + { + 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); + } + + /// + /// Builds a text generator based on the given format. + /// + /// The format to parse. + /// The text generator. + public Generator Compile(string format) + { + if (format == null) + { + throw new ArgumentNullException("format"); + } + CompoundGenerator generator = new CompoundGenerator(_masterDefinition, new ArgumentCollection()); + Trimmer trimmer = new Trimmer(); + int formatIndex = buildCompoundGenerator(_masterDefinition, generator, trimmer, format, 0); + string trailing = format.Substring(formatIndex); + generator.AddStaticGenerators(trimmer.RecordText(trailing, false, false)); + trimmer.Trim(); + 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 matches = new List(); + 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) + ")}}"; + regex = new Regex(match, RegexOptions.Compiled); + _regexLookup.Add(definition.Name, regex); + } + return regex; + } + + private static string getClosingTagRegex(string tagName) + { + StringBuilder regexBuilder = new StringBuilder(); + regexBuilder.Append(@"(?(/(?"); + regexBuilder.Append(tagName); + regexBuilder.Append(@")\s*?))"); + return regexBuilder.ToString(); + } + + private static string getCommentTagRegex() + { + return @"(?#!.*?)"; + } + + private static string getKeyRegex() + { + return @"((?" + RegexHelper.CompoundKey + @")(,(?(\+|-)?[\d]+))?(:(?.*?))?)"; + } + + private static string getTagRegex(TagDefinition definition) + { + StringBuilder regexBuilder = new StringBuilder(); + regexBuilder.Append(@"(?(#(?"); + regexBuilder.Append(definition.Name); + regexBuilder.Append(@")"); + foreach (TagParameter parameter in definition.Parameters) + { + regexBuilder.Append(@"(\s+?"); + regexBuilder.Append(@"(?"); + regexBuilder.Append(RegexHelper.CompoundKey); + regexBuilder.Append(@"))"); + if (!parameter.IsRequired) + { + regexBuilder.Append("?"); + } + } + regexBuilder.Append(@"\s*?))"); + return regexBuilder.ToString(); + } + + private string getUnknownTagRegex() + { + return @"(?(#.*?))"; + } + + private int buildCompoundGenerator( + TagDefinition tagDefinition, + CompoundGenerator generator, + Trimmer trimmer, + 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) + { + generator.AddStaticGenerators(trimmer.RecordText(leading, true, true)); + formatIndex = match.Index + match.Length; + string key = match.Groups["key"].Value; + string alignment = match.Groups["alignment"].Value; + string formatting = match.Groups["format"].Value; + PlaceholderFoundEventArgs args = new PlaceholderFoundEventArgs(key, alignment, formatting); + if (PlaceholderFound != null) + { + PlaceholderFound(this, args); + } + KeyGenerator keyGenerator = new KeyGenerator(args.Key, args.Alignment, args.Formatting); + 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); + } + if (nextDefinition.HasContent) + { + generator.AddStaticGenerators(trimmer.RecordText(leading, true, false)); + ArgumentCollection arguments = getArguments(nextDefinition, match); + CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments); + formatIndex = buildCompoundGenerator(nextDefinition, compoundGenerator, trimmer, format, formatIndex); + generator.AddGenerator(nextDefinition, compoundGenerator); + } + else + { + generator.AddStaticGenerators(trimmer.RecordText(leading, true, true)); + Match nextMatch = findNextTag(nextDefinition, format, formatIndex); + ArgumentCollection arguments = getArguments(nextDefinition, nextMatch); + InlineGenerator inlineGenerator = new InlineGenerator(nextDefinition, arguments); + generator.AddGenerator(inlineGenerator); + } + } + else if (match.Groups["close"].Success) + { + generator.AddStaticGenerators(trimmer.RecordText(leading, true, false)); + 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) + { + generator.AddStaticGenerators(trimmer.RecordText(leading, true, false)); + formatIndex = match.Index + match.Length; + } + else if (match.Groups["unknown"].Success) + { + throw new FormatException(Resources.UnknownTag); + } + } + return formatIndex; + } + + private static ArgumentCollection getArguments(TagDefinition definition, Match match) + { + ArgumentCollection collection = new ArgumentCollection(); + List captures = match.Groups["argument"].Captures.Cast().ToList(); + List parameters = definition.Parameters.ToList(); + if (captures.Count > parameters.Count) + { + string message = String.Format(Resources.WrongNumberOfArguments, definition.Name); + throw new FormatException(message); + } + if (captures.Count < parameters.Count) + { + captures.AddRange(Enumerable.Repeat((Capture)null, parameters.Count - captures.Count)); + } + foreach (var pair in parameters.Zip(captures, (p, c) => new { Capture = c, Parameter = p })) + { + if (pair.Capture == null) + { + if (pair.Parameter.IsRequired) + { + string message = String.Format(Resources.WrongNumberOfArguments, definition.Name); + throw new FormatException(message); + } + collection.AddArgument(pair.Parameter, null); + } + else + { + collection.AddArgument(pair.Parameter, pair.Capture.Value); + } + } + return collection; + } + } +} diff --git a/mustache-sharp/PlaceholderFoundEventArgs.cs b/mustache-sharp/PlaceholderFoundEventArgs.cs new file mode 100644 index 0000000..8849994 --- /dev/null +++ b/mustache-sharp/PlaceholderFoundEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using mustache.Properties; + +namespace mustache +{ + /// + /// Holds the information descibing a key that is found in a template. + /// + public class PlaceholderFoundEventArgs : EventArgs + { + /// + /// Initializes a new instance of a PlaceholderFoundEventArgs. + /// + /// 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. + internal PlaceholderFoundEventArgs(string key, string alignment, string formatting) + { + Key = key; + Alignment = alignment; + Formatting = formatting; + } + + /// + /// Gets or sets the key that was found. + /// + public string Key { get; set; } + + /// + /// Gets or sets the alignment that will be applied to the substitute value. + /// + public string Alignment { get; set; } + + /// + /// Gets or sets the formatting that will be applied to the substitute value. + /// + public string Formatting { get; set; } + } +} diff --git a/mustache-sharp/Properties/AssemblyInfo.cs b/mustache-sharp/Properties/AssemblyInfo.cs index f1d3dae..d72b0f3 100644 --- a/mustache-sharp/Properties/AssemblyInfo.cs +++ b/mustache-sharp/Properties/AssemblyInfo.cs @@ -34,6 +34,6 @@ using System.Runtime.CompilerServices; // 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.0.5.0")] -[assembly: AssemblyFileVersion("0.0.4.0")] +[assembly: AssemblyVersion("0.0.6.0")] +[assembly: AssemblyFileVersion("0.0.6.0")] [assembly: InternalsVisibleTo("mustache-sharp.test")] \ No newline at end of file diff --git a/mustache-sharp/RegexHelper.cs b/mustache-sharp/RegexHelper.cs index 38548c6..ad2b715 100644 --- a/mustache-sharp/RegexHelper.cs +++ b/mustache-sharp/RegexHelper.cs @@ -1,25 +1,29 @@ -using System; -using System.Text.RegularExpressions; - -namespace mustache -{ - /// - /// Provides utility methods that require regular expressions. - /// - public static class RegexHelper - { - private const string Key = @"[_\w][_\w\d]*"; - internal const string CompoundKey = Key + @"(\." + Key + ")*"; - - /// - /// Determines whether the given name is a legal identifier. - /// - /// The name to check. - /// True if the name is a legal identifier; otherwise, false. - public static bool IsValidIdentifier(string name) - { - Regex regex = new Regex("^" + Key + "$"); - return regex.IsMatch(name); - } - } -} +using System; +using System.Text.RegularExpressions; + +namespace mustache +{ + /// + /// Provides utility methods that require regular expressions. + /// + public static class RegexHelper + { + private const string Key = @"[_\w][_\w\d]*"; + internal const string CompoundKey = Key + @"(\." + Key + ")*"; + + /// + /// Determines whether the given name is a legal identifier. + /// + /// The name to check. + /// True if the name is a legal identifier; otherwise, false. + public static bool IsValidIdentifier(string name) + { + if (name == null) + { + return false; + } + Regex regex = new Regex("^" + Key + "$"); + return regex.IsMatch(name); + } + } +} diff --git a/mustache-sharp/mustache-sharp.csproj b/mustache-sharp/mustache-sharp.csproj index d50cf59..97b2235 100644 --- a/mustache-sharp/mustache-sharp.csproj +++ b/mustache-sharp/mustache-sharp.csproj @@ -47,6 +47,7 @@ +