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 const string key = @"[_\w][_\w\d]*"; private const string compoundKey = key + @"(\." + key + ")*"; private readonly MasterTagDefinition _master; private readonly TagScope _tagScope; /// /// Initializes a new instance of a FormatCompiler. /// public FormatCompiler() { _master = new MasterTagDefinition(); _tagScope = new TagScope(); registerTags(_master, _tagScope); } /// /// Registers the given tag definition with the parser. /// /// The tag definition to register. public void RegisterTag(TagDefinition definition) { if (definition == null) { throw new ArgumentNullException("definition"); } _tagScope.AddTag(definition); } /// /// Builds a text generator based on the given format. /// /// The format to parse. /// The text generator. public Generator Compile(string format) { CompoundGenerator generator = new CompoundGenerator(_master, new ArgumentCollection()); Trimmer trimmer = new Trimmer(); int formatIndex = buildCompoundGenerator(_master, _tagScope, generator, trimmer, format, 0); string trailing = format.Substring(formatIndex); StaticGenerator staticGenerator = new StaticGenerator(trailing); generator.AddGenerator(staticGenerator); return new Generator(generator); } private static void registerTags(TagDefinition definition, TagScope scope) { foreach (TagDefinition childTag in definition.ChildTags) { scope.TryAddTag(childTag); } } private static Match findNextTag(TagDefinition definition, string format, int formatIndex) { List matches = new List(); matches.Add(getKeyRegex()); matches.Add(getCommentTagRegex()); foreach (TagDefinition closingTag in definition.ClosingTags) { matches.Add(getClosingTagRegex(closingTag)); } foreach (TagDefinition childTag in definition.ChildTags) { matches.Add(getTagRegex(childTag)); } string match = "{{(" + String.Join("|", matches) + ")}}"; Regex regex = new Regex(match); return regex.Match(format, formatIndex); } private static string getClosingTagRegex(TagDefinition definition) { StringBuilder regexBuilder = new StringBuilder(); regexBuilder.Append(@"(?(/(?"); regexBuilder.Append(definition.Name); regexBuilder.Append(@")\s*?))"); return regexBuilder.ToString(); } private static string getCommentTagRegex() { return @"(?#!.*?)"; } private static string getKeyRegex() { return @"((?" + 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(compoundKey); regexBuilder.Append(@")"); } regexBuilder.Append(@"\s*?))"); return regexBuilder.ToString(); } private static int buildCompoundGenerator( TagDefinition tagDefinition, TagScope scope, 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) { trimmer.AddStaticGenerator(generator, true, leading); 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 = scope.Find(tagName); if (nextDefinition == null) { string message = String.Format(Resources.UnknownTag, tagName); throw new FormatException(message); } trimmer.AddStaticGeneratorBeforeTag(generator, true, leading); if (nextDefinition.HasBody) { ArgumentCollection arguments = getArguments(nextDefinition, match); CompoundGenerator compoundGenerator = new CompoundGenerator(nextDefinition, arguments); TagScope nextScope = new TagScope(scope); registerTags(nextDefinition, nextScope); formatIndex = buildCompoundGenerator(nextDefinition, nextScope, compoundGenerator, trimmer, format, formatIndex); generator.AddGenerator(nextDefinition, compoundGenerator); } else { 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) { string tagName = match.Groups["name"].Value; TagDefinition nextDefinition = scope.Find(tagName); trimmer.AddStaticGeneratorBeforeTag(generator, false, leading); formatIndex = match.Index; if (tagName == tagDefinition.Name) { formatIndex += match.Length; } break; } else if (match.Groups["comment"].Success) { trimmer.AddStaticGenerator(generator, false, leading); formatIndex = match.Index + match.Length; } } 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; } } }