/// <summary>Parse a template description into tokens</summary> private static IEnumerable <Token> Parse(Src src, bool root = false) { // Recursively build the token tree for (; Extract.AdvanceToNonDelim(src, " \t\r\n");) { switch (src) { case TemplateMark: { src.Next(); // Skip the '*' var flags = ETemplateFlags.Recursive; if (!root) { flags |= ETemplateFlags.Hidden; flags &= ~ETemplateFlags.Recursive; } if (src == HiddenMark) { src.Next(); flags |= ETemplateFlags.Hidden; flags &= ~ETemplateFlags.Recursive; } if (src == RootLevelOnlyMark) { src.Next(); flags |= ETemplateFlags.RootLevelOnly; flags &= ~ETemplateFlags.Recursive; } // Read the template keyword var keyword = Extract.Identifier(out var kw, src) ? kw : throw new ScriptException(Script.EResult.InvalidValue, src.Location, $"Invalid template keyword"); // Buffer the remaining template description var len = BufferTemplateDeclaration(src); // Create the template instance var remaining = src.Limit - len; using (var limit = Scope.Create(() => src.Limit = len, () => src.Limit = remaining)) yield return(new Template(keyword, flags, Parse(src))); break; } case ReferenceMark: case ExpandMark: { // Skip the '@' or '$' var expand = src == ExpandMark; src.Next(); // Read the keyword of the referenced template var keyword = Extract.Identifier(out var kw, src) ? kw : throw new ScriptException(Script.EResult.InvalidValue, src.Location, $"Invalid template keyword"); yield return(new TemplateRef(keyword, expand)); break; } case '{': case '}': { if (src == '{') { src.Next(); } else { throw new ScriptException(Script.EResult.TokenNotFound, src.Location, "Unmatched '}' token"); } // Buffer the content within the section int len = 0; for (var nest = 1; src[len] != 0 && nest != 0; ++len) { nest += src[len] == '{' ? 1 : 0; nest -= src[len] == '}' ? 1 : 0; } if (src[len - 1] != '}') { throw new ScriptException(Script.EResult.TokenNotFound, src.Location, "Unmatched '{' token"); } var remaining = src.Limit - (len - 1); using (var limit = Scope.Create(() => src.Limit = len - 1, () => src.Limit = remaining)) yield return(new Section(Parse(src))); src.Next(); break; } case '[': case ']': { if (src == '[') { src.Next(); } else { throw new ScriptException(Script.EResult.TokenNotFound, src.Location, "Unmatched ']' token"); } // Buffer the content within the optional int len = 0; for (var nest = 1; src[len] != 0 && nest != 0; ++len) { nest += src[len] == '[' ? 1 : 0; nest -= src[len] == ']' ? 1 : 0; } if (src[len - 1] != ']') { throw new ScriptException(Script.EResult.TokenNotFound, src.Location, "Unmatched '[' token"); } var remaining = src.Limit - (len - 1); using (var limit = Scope.Create(() => src.Limit = len - 1, () => src.Limit = remaining)) yield return(new Optional(Parse(src))); src.Next(); break; } case '(': case ')': { if (src == '(') { src.Next(); } else { throw new ScriptException(Script.EResult.TokenNotFound, src.Location, "Unmatched ')' token"); } // Buffer the content within the repeat int len = 0; for (var nest = 1; src[len] != 0 && nest != 0; ++len) { nest += src[len] == '(' ? 1 : 0; nest -= src[len] == ')' ? 1 : 0; } if (src[len - 1] != ')') { throw new ScriptException(Script.EResult.TokenNotFound, src.Location, $"Unmatched '(' token"); } var remaining = src.Limit - (len - 1); using (var limit = Scope.Create(() => src.Limit = len - 1, () => src.Limit = remaining)) yield return(new Repeat(Parse(src))); src.Next(); break; } case '<': case '>': { if (src == '<') { src.Next(); } else { throw new ScriptException(Script.EResult.TokenNotFound, src.Location, $"Unmatched '>' token"); } var name = Extract.Identifier(out var kw, src) ? kw : throw new ScriptException(Script.EResult.InvalidValue, src.Location, $"Invalid field identifier"); if (src != '>') { throw new ScriptException(Script.EResult.TokenNotFound, src.Location, $"Unmatched '<' token"); } yield return(new Field(name)); src.Next(); break; } case '|': { yield return(new Select()); src.Next(); break; } case ';': { yield return(new LineBreak()); src.Next(); break; } default: { // Literal text Extract.BufferWhile(src, (s, i) => !" \n*@{}[]()<>|".Contains(s[i]) ? 1 : 0, 0, out var len); yield return(new Literal(src.Buffer.ToString(0, len))); src.Next(len); break; } } } }