/** * Breaks up the given `template` string into a tree of tokens. If the `tags` * argument is given here it must be an array with two string values: the * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of * course, the default is to use mustaches (i.e. mustache.tags). * * A token is an array with at least 4 elements. The first element is the * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag * did not contain a symbol (i.e. {{myValue}}) this element is "Name". For * all text that appears outside a symbol this element is "text". * * The second element of a token is its "Value". For mustache tags this is * whatever else was inside the tag besides the opening symbol. For text tokens * this is the text itself. * * The third and fourth elements of the token are the Start and End indices, * respectively, of the token in the original template. * * Tokens that are the root node of a subtree contain two more elements: 1) an * array of tokens in the subtree and 2) the index in the original template at * which the closing tag for that section begins. */ private static List<Token> ParseTemplate(string template, Tags tags = null) { if (!template.Any()) return new List<Token>(); var sections = new Stack<Token>(); // Stack to hold section tokens var tokens = new List<Token>(); // Buffer to hold the tokens var spaces = new Stack<int>(); // Indices of whitespace tokens on the current line var hasTag = false; // Is there a {{tag}} on the current line? var nonSpace = false; // Is there a non-space char on the current line? // Strips all whitespace tokens array for the current line // if there was a {{#tag}} on it and otherwise only space. Action stripSpace = () => { if (hasTag && !nonSpace) { while (spaces.Any()) tokens.RemoveAt(spaces.Pop()); } else { spaces.Clear(); } hasTag = false; nonSpace = false; }; // TODO: this `= null` is to avoid "Use of unassigned local variable" C# compiler error. Regex openingTagRe = null; Regex closingTagRe = null; Regex closingCurlyRe = null; Action<Tags> compileTags = delegate(Tags tagsToCompile) { openingTagRe = new Regex(Regex.Escape(tagsToCompile.Opener) + "\\s*"); closingTagRe = new Regex("\\s*" + Regex.Escape(tagsToCompile.Closer)); closingCurlyRe = new Regex("\\s*" + Regex.Escape('}' + tagsToCompile.Closer)); }; if (tags == null) compileTags(MustacheTags); else compileTags(tags); //var Start, Type, Value, chr, token, openSection; var scanner = new Scanner(template); Token openSection = null; while (!scanner.Eos()) { var start = scanner._pos; var value = scanner.ScanUntil(openingTagRe); var valueLength = value.Length; if (valueLength > 0) { for (var i = 0; i < valueLength; ++i) { string chr = "" + value[i]; if (IsWhitespace(chr)) { spaces.Push(tokens.Count); } else { nonSpace = true; } tokens.Add(new Token {Type = "text", Value = chr, Start = start, End = start + 1}); start += 1; // Check for whitespace on the current line. if (chr == "\n") stripSpace(); } } // Match the opening tag. if (!scanner.Scan(openingTagRe).Any()) break; hasTag = true; // Get the tag Type. var scanTag = scanner.Scan(_tagRe); string type; if (!scanTag.Any()) type = "Name"; else type = scanTag; scanner.Scan(_whiteRe); // Get the tag Value. switch (type) { case "=": value = scanner.ScanUntil(_equalsRe); scanner.Scan(_equalsRe); scanner.ScanUntil(closingTagRe); break; case "{": value = scanner.ScanUntil(closingCurlyRe); scanner.Scan(_curlyRe); scanner.ScanUntil(closingTagRe); type = "&"; break; default: value = scanner.ScanUntil(closingTagRe); break; } // Match the closing tag. if (!scanner.Scan(closingTagRe).Any()) throw new Exception("Unclosed tag at " + scanner._pos); var arr = value.Split('|'); string format = null; if (arr.Length == 2) { value = arr[0]; format = arr[1]; } var token = new Token {Type = type, Value = value, Format = format, Start = start, End = scanner._pos}; tokens.Add(token); switch (type) { case "#": case "^": sections.Push(token); break; case "/": // Check section nesting. openSection = sections.Pop(); if (openSection == null) throw new Exception("Unopened section \"" + value + "\" at " + start); if (openSection.Value != value) throw new Exception("Unclosed section \"" + openSection.Value + "\" at " + start); break; case "Name": case "{": case "&": nonSpace = true; break; case "=": // Set the tags for the next time around. var newTags = _spaceRe.Split(value, 2); compileTags(new Tags {Opener = newTags[0], Closer = newTags[1]}); break; } } // Make sure there are no open sections when we're done. if (sections.Any()) { openSection = sections.Pop(); throw new Exception("Unclosed section \"" + openSection.Value + "\" at " + scanner._pos); } return NestTokens(SquashTokens(tokens)); }