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 @@ +