private ScalarToken ParseScalar( LiteralToken token, String[] allowedContext) { // Not a string if (token.Type != TokenType.String) { return(token); } // Check if the value is definitely a literal var raw = token.ToString(); Int32 startExpression; if (String.IsNullOrEmpty(raw) || (startExpression = raw.IndexOf(TemplateConstants.OpenExpression)) < 0) // Doesn't contain ${{ { return(token); } // Break the value into segments of LiteralToken and ExpressionToken var segments = new List <ScalarToken>(); var i = 0; while (i < raw.Length) { // An expression starts here: if (i == startExpression) { // Find the end of the expression - i.e. }} startExpression = i; var endExpression = -1; var inString = false; for (i += TemplateConstants.OpenExpression.Length; i < raw.Length; i++) { if (raw[i] == '\'') { inString = !inString; // Note, this handles escaped single quotes gracefully. Ex. 'foo''bar' } else if (!inString && raw[i] == '}' && raw[i - 1] == '}') { endExpression = i; i++; break; } } // Check if not closed if (endExpression < startExpression) { m_context.Error(token, TemplateStrings.ExpressionNotClosed()); return(token); } // Parse the expression var rawExpression = raw.Substring( startExpression + TemplateConstants.OpenExpression.Length, endExpression - startExpression + 1 - TemplateConstants.OpenExpression.Length - TemplateConstants.CloseExpression.Length); var expression = ParseExpression(token.Line, token.Column, rawExpression, allowedContext, out Exception ex); // Check for error if (ex != null) { m_context.Error(token, ex); return(token); } // Check if a directive was used when not allowed if (!String.IsNullOrEmpty(expression.Directive) && ((startExpression != 0) || (i < raw.Length))) { m_context.Error(token, TemplateStrings.DirectiveNotAllowedInline(expression.Directive)); return(token); } // Add the segment segments.Add(expression); // Look for the next expression startExpression = raw.IndexOf(TemplateConstants.OpenExpression, i); } // The next expression is further ahead: else if (i < startExpression) { // Append the segment AddString(segments, token.Line, token.Column, raw.Substring(i, startExpression - i)); // Adjust the position i = startExpression; } // No remaining expressions: else { AddString(segments, token.Line, token.Column, raw.Substring(i)); break; } } // Check if can convert to a literal // For example, the escaped expression: ${{ '{{ this is a literal }}' }} if (segments.Count == 1 && segments[0] is BasicExpressionToken basicExpression && IsExpressionString(basicExpression.Expression, out String str)) { return(new StringToken(m_fileId, token.Line, token.Column, str)); } // Check if only ony segment if (segments.Count == 1) { return(segments[0]); } // Build the new expression, using the format function var format = new StringBuilder(); var args = new StringBuilder(); var argIndex = 0; foreach (var segment in segments) { if (segment is StringToken literal) { var text = ExpressionUtility.StringEscape(literal.Value) // Escape quotes .Replace("{", "{{") // Escape braces .Replace("}", "}}"); format.Append(text); } else { format.Append("{" + argIndex.ToString(CultureInfo.InvariantCulture) + "}"); // Append formatter argIndex++; var expression = segment as BasicExpressionToken; args.Append(", "); args.Append(expression.Expression); } } return(new BasicExpressionToken(m_fileId, token.Line, token.Column, $"format('{format}'{args})")); }