// // CompositeFormatStringParser.cs // // Authors: // Simon Lindgren // // Copyright (c) 2012 Simon Lindgren // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using System.Collections.Generic; namespace ICSharpCode.NRefactory.Utils { /// /// Composite format string parser. /// /// /// Implements a complete parser for valid strings as well as /// error reporting and best-effort parsing for invalid strings. /// public class CompositeFormatStringParser { public CompositeFormatStringParser () { errors = new List (); } /// /// Parse the specified format string. /// /// /// The format string. /// public IEnumerable Parse (string format) { if (format == null) throw new ArgumentNullException ("format"); // Format string syntax: http://msdn.microsoft.com/en-us/library/txafckwd.aspx int start = 0; var length = format.Length; for (int i = 0; i < length; i++) { if (format [i] == '{') { if (i + 1 == length) { // This is the end of the string. var textSegment = new TextSegment (format.Substring (start, i - start + 1), start) { Errors = { new DefaultFormatStringError { StartLocation = i, EndLocation = i + 1, Message = "Curly braces need to be escaped", OriginalText = "{", SuggestedReplacementText = "{{" } } }; yield return textSegment; yield break; } else if (format [i + 1] == '{') { // Escape sequence; we're still in a text segment // Skip ahead to the char after the escape sequence ++i; continue; } else { // This is the end of the text segment and the start of a FormatItem if (i - start > 0) { yield return TextSegment.FromUnescapedText (format.Substring (start, i - start)); start = i; } } int index; int? alignment = null; string argumentFormat = null; // Index ++i; index = ParseIndex(format, ref i); CheckForMissingEndBrace (format, i, length); // Alignment alignment = ParseAlignment(format, ref i, length); CheckForMissingEndBrace (format, i, length); // Format string argumentFormat = ParseSubFormatString(format, ref i, length); CheckForMissingEndBrace (format, i, length); // i may actually point outside of format; if that happens, we want the last position var endLocation = Math.Min (length, i + 1); var errors = GetErrors (); yield return new FormatItem (index, alignment, argumentFormat) { StartLocation = start, EndLocation = endLocation, Errors = errors }; ClearErrors (); // The next potential text segment starts after this format item start = i + 1; } } // Handle remaining text if (start < length) { yield return new TextSegment (TextSegment.UnEscape (format.Substring (start)), start); } } int ParseIndex (string format, ref int i) { int? maybeIndex = GetNumber (format, ref i); if (maybeIndex.HasValue) { return maybeIndex.Value; } AddError (new DefaultFormatStringError { StartLocation = i, EndLocation = i, Message = "Missing index", OriginalText = "", SuggestedReplacementText = "0" }); return 0; } int? ParseAlignment(string format, ref int i, int length) { if (i < length && format [i] == ',') { int alignmentBegin = i; ++i; while (i < length && char.IsWhiteSpace(format [i])) ++i; if (i == length) { var originalText = format.Substring (alignmentBegin); var message = string.Format ("Unexpected end of string: '{0}'", originalText); AddMissingEndBraceError(alignmentBegin, i, message, originalText); } else if (format [i] == '-') { ++i; return -int.Parse (GetUntil (format, ":}", ref i)); } else { return int.Parse (GetUntil (format, ":}", ref i)); } } return null; } string ParseSubFormatString(string format, ref int i, int length) { if (i < length && format [i] == ':') { ++i; int begin = i; while (i < length) { char c = format [i]; if (c != '}') { ++i; continue; } if (i + 1 < length && format [i + 1] == '}') { // Step past escape sequence i += 2; continue; } else { // This is the end of the FormatItem break; } } var escaped = format.Substring (begin, i - begin); return TextSegment.UnEscape (escaped); } return null; } string GetFormatItemText (string src, int index, out int endIndex) { int length = src.Length; int begin = index; while (index < length) { var c = src [index]; ++index; if (c != '}') { continue; } else if (index + 1 < length && src [index + 1] == '}') { // Step past escape sequence ++index; continue; } else { // This is the end of the FormatItem break; } } endIndex = index < length ? index : length; return src.Substring (begin, endIndex - begin); } void CheckForMissingEndBrace (string format, int i, int length) { if (i == length && format [length - 1] != '}') { AddMissingEndBraceError(i, i, "Missing '}'", ""); return; } return; } string GetUntil (string format, string delimiters, ref int index) { int start = index; while (index < format.Length && !delimiters.Contains(format[index].ToString())) ++index; return format.Substring (start, index - start); } int? GetNumber (string format, ref int index) { int sum = 0; int i = index; bool positive = format [i] != '-'; if (!positive) ++i; int numberStartIndex = i; while (i < format.Length && format[i] >= '0' && format[i] <= '9') { sum = 10 * sum + format [i] - '0'; ++i; } if (i == numberStartIndex) return null; index = i; return positive ? sum : -sum; } IList errors; bool hasMissingEndBrace = false; void AddError (IFormatStringError error) { errors.Add (error); } void AddMissingEndBraceError(int start, int end, string message, string originalText) { if (hasMissingEndBrace) return; AddError (new DefaultFormatStringError { StartLocation = start, EndLocation = end, Message = message, OriginalText = originalText, SuggestedReplacementText = "}" }); hasMissingEndBrace = true; } IList GetErrors () { return errors; } void ClearErrors () { hasMissingEndBrace = false; errors = new List (); } } }