From 0f4cf4d8bf559ca8aea5aaeb093106a3f2c692bb Mon Sep 17 00:00:00 2001 From: Sumanth K B Date: Wed, 18 Oct 2023 15:59:31 +0530 Subject: [PATCH] Tommy Reference added --- CA.nuspec | 1 + src/LCT.Common/LCT.Common.csproj | 1 + src/LCT.Common/Tommy.cs | 2129 ------------------------------ 3 files changed, 2 insertions(+), 2129 deletions(-) delete mode 100644 src/LCT.Common/Tommy.cs diff --git a/CA.nuspec b/CA.nuspec index dfa351f5..a8a92c66 100644 --- a/CA.nuspec +++ b/CA.nuspec @@ -122,6 +122,7 @@ + diff --git a/src/LCT.Common/LCT.Common.csproj b/src/LCT.Common/LCT.Common.csproj index 7e250545..d5fe12af 100644 --- a/src/LCT.Common/LCT.Common.csproj +++ b/src/LCT.Common/LCT.Common.csproj @@ -27,6 +27,7 @@ + diff --git a/src/LCT.Common/Tommy.cs b/src/LCT.Common/Tommy.cs deleted file mode 100644 index ef51073a..00000000 --- a/src/LCT.Common/Tommy.cs +++ /dev/null @@ -1,2129 +0,0 @@ -#region LICENSE - -/* - * MIT License - * - * Copyright (c) 2020 Denis Zhidkikh - * - * 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. - */ - -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace Tommy -{ - #region TOML Nodes - - [ExcludeFromCodeCoverage] - public abstract class TomlNode : IEnumerable - { - public virtual bool HasValue { get; } = false; - public virtual bool IsArray { get; } = false; - public virtual bool IsTable { get; } = false; - public virtual bool IsString { get; } = false; - public virtual bool IsInteger { get; } = false; - public virtual bool IsFloat { get; } = false; - public bool IsDateTime => IsDateTimeLocal || IsDateTimeOffset; - public virtual bool IsDateTimeLocal { get; } = false; - public virtual bool IsDateTimeOffset { get; } = false; - public virtual bool IsBoolean { get; } = false; - public virtual string Comment { get; set; } - public virtual int CollapseLevel { get; set; } - - public virtual TomlTable AsTable => this as TomlTable; - public virtual TomlString AsString => this as TomlString; - public virtual TomlInteger AsInteger => this as TomlInteger; - public virtual TomlFloat AsFloat => this as TomlFloat; - public virtual TomlBoolean AsBoolean => this as TomlBoolean; - public virtual TomlDateTimeLocal AsDateTimeLocal => this as TomlDateTimeLocal; - public virtual TomlDateTimeOffset AsDateTimeOffset => this as TomlDateTimeOffset; - public virtual TomlDateTime AsDateTime => this as TomlDateTime; - public virtual TomlArray AsArray => this as TomlArray; - - public virtual int ChildrenCount => 0; - - public virtual TomlNode this[string key] - { - get => null; - set { } - } - - public virtual TomlNode this[int index] - { - get => null; - set { } - } - - public virtual IEnumerable Children - { - get { yield break; } - } - - public virtual IEnumerable Keys - { - get { yield break; } - } - - public IEnumerator GetEnumerator() => Children.GetEnumerator(); - - public virtual bool TryGetNode(string key, out TomlNode node) - { - node = null; - return false; - } - - public virtual bool HasKey(string key) => false; - - public virtual bool HasItemAt(int index) => false; - - public virtual void Add(string key, TomlNode node) { } - - public virtual void Add(TomlNode node) { } - - public virtual void Delete(TomlNode node) { } - - public virtual void Delete(string key) { } - - public virtual void Delete(int index) { } - - public virtual void AddRange(IEnumerable nodes) - { - foreach (var tomlNode in nodes) Add(tomlNode); - } - - public virtual void WriteTo(TextWriter tw, string name = null) => tw.WriteLine(ToInlineToml()); - - public virtual string ToInlineToml() => ToString(); - - #region Native type to TOML cast - - public static implicit operator TomlNode(string value) => new TomlString { Value = value }; - - public static implicit operator TomlNode(bool value) => new TomlBoolean { Value = value }; - - public static implicit operator TomlNode(long value) => new TomlInteger { Value = value }; - - public static implicit operator TomlNode(float value) => new TomlFloat { Value = value }; - - public static implicit operator TomlNode(double value) => new TomlFloat { Value = value }; - - public static implicit operator TomlNode(DateTime value) => new TomlDateTimeLocal { Value = value }; - - public static implicit operator TomlNode(DateTimeOffset value) => new TomlDateTimeOffset { Value = value }; - - public static implicit operator TomlNode(TomlNode[] nodes) - { - var result = new TomlArray(); - result.AddRange(nodes); - return result; - } - - #endregion - - #region TOML to native type cast - - public static implicit operator string(TomlNode value) => value.ToString(); - - public static implicit operator int(TomlNode value) => (int)value.AsInteger.Value; - - public static implicit operator long(TomlNode value) => value.AsInteger.Value; - - public static implicit operator float(TomlNode value) => (float)value.AsFloat.Value; - - public static implicit operator double(TomlNode value) => value.AsFloat.Value; - - public static implicit operator bool(TomlNode value) => value.AsBoolean.Value; - - public static implicit operator DateTime(TomlNode value) => value.AsDateTimeLocal.Value; - - public static implicit operator DateTimeOffset(TomlNode value) => value.AsDateTimeOffset.Value; - - #endregion - } - - public class TomlString : TomlNode - { - public override bool HasValue { get; } = true; - public override bool IsString { get; } = true; - public bool IsMultiline { get; set; } - public bool MultilineTrimFirstLine { get; set; } - public bool PreferLiteral { get; set; } - - public string Value { get; set; } - - public override string ToString() => Value; - - public override string ToInlineToml() - { - // Automatically convert literal to non-literal if there are too many literal string symbols - if (Value.IndexOf(new string(TomlSyntax.LITERAL_STRING_SYMBOL, IsMultiline ? 3 : 1), StringComparison.Ordinal) != -1 && PreferLiteral) PreferLiteral = false; - var quotes = new string(PreferLiteral ? TomlSyntax.LITERAL_STRING_SYMBOL : TomlSyntax.BASIC_STRING_SYMBOL, - IsMultiline ? 3 : 1); - var result = PreferLiteral ? Value : Value.Escape(!IsMultiline); - if (IsMultiline) - result = result.Replace("\r\n", "\n").Replace("\n", Environment.NewLine); - if (IsMultiline && (MultilineTrimFirstLine || !MultilineTrimFirstLine && result.StartsWith(Environment.NewLine))) - result = $"{Environment.NewLine}{result}"; - return $"{quotes}{result}{quotes}"; - } - } - - public class TomlInteger : TomlNode - { - public enum Base - { - Binary = 2, - Octal = 8, - Decimal = 10, - Hexadecimal = 16 - } - - public override bool IsInteger { get; } = true; - public override bool HasValue { get; } = true; - public Base IntegerBase { get; set; } = Base.Decimal; - - public long Value { get; set; } - - public override string ToString() => Value.ToString(); - - public override string ToInlineToml() => - IntegerBase != Base.Decimal - ? $"0{TomlSyntax.BaseIdentifiers[(int)IntegerBase]}{Convert.ToString(Value, (int)IntegerBase)}" - : Value.ToString(CultureInfo.InvariantCulture); - } - - public class TomlFloat : TomlNode, IFormattable - { - public override bool IsFloat { get; } = true; - public override bool HasValue { get; } = true; - - public double Value { get; set; } - - public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); - - public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider); - - public string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); - - public override string ToInlineToml() => - Value switch - { - var v when double.IsNaN(v) => TomlSyntax.NAN_VALUE, - var v when double.IsPositiveInfinity(v) => TomlSyntax.INF_VALUE, - var v when double.IsNegativeInfinity(v) => TomlSyntax.NEG_INF_VALUE, - var v => v.ToString("G", CultureInfo.InvariantCulture).ToLowerInvariant() - }; - } - - public class TomlBoolean : TomlNode - { - public override bool IsBoolean { get; } = true; - public override bool HasValue { get; } = true; - - public bool Value { get; set; } - - public override string ToString() => Value.ToString(); - - public override string ToInlineToml() => Value ? TomlSyntax.TRUE_VALUE : TomlSyntax.FALSE_VALUE; - } - - public class TomlDateTime : TomlNode, IFormattable - { - public int SecondsPrecision { get; set; } - public override bool HasValue { get; } = true; - public virtual string ToString(string format, IFormatProvider formatProvider) => string.Empty; - public virtual string ToString(IFormatProvider formatProvider) => string.Empty; - protected virtual string ToInlineTomlInternal() => string.Empty; - - public override string ToInlineToml() => ToInlineTomlInternal() - .Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator) - .Replace(TomlSyntax.ISO861ZeroZone, TomlSyntax.RFC3339ZeroZone); - } - - public class TomlDateTimeOffset : TomlDateTime - { - public override bool IsDateTimeOffset { get; } = true; - public DateTimeOffset Value { get; set; } - - public override string ToString() => Value.ToString(CultureInfo.CurrentCulture); - public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); - - public override string ToString(string format, IFormatProvider formatProvider) => - Value.ToString(format, formatProvider); - - protected override string ToInlineTomlInternal() => Value.ToString(TomlSyntax.RFC3339Formats[SecondsPrecision]); - } - - public class TomlDateTimeLocal : TomlDateTime - { - public enum DateTimeStyle - { - Date, - Time, - DateTime - } - - public override bool IsDateTimeLocal { get; } = true; - public DateTimeStyle Style { get; set; } = DateTimeStyle.DateTime; - public DateTime Value { get; set; } - - public override string ToString() => Value.ToString(CultureInfo.CurrentCulture); - - public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); - - public override string ToString(string format, IFormatProvider formatProvider) => - Value.ToString(format, formatProvider); - - public override string ToInlineToml() => - Style switch - { - DateTimeStyle.Date => Value.ToString(TomlSyntax.LocalDateFormat), - DateTimeStyle.Time => Value.ToString(TomlSyntax.RFC3339LocalTimeFormats[SecondsPrecision]), - var _ => Value.ToString(TomlSyntax.RFC3339LocalDateTimeFormats[SecondsPrecision]) - }; - } - - public class TomlArray : TomlNode - { - private List values; - - public override bool HasValue { get; } = true; - public override bool IsArray { get; } = true; - public bool IsMultiline { get; set; } - public bool IsTableArray { get; set; } - public List RawArray => values ??= new List(); - - public override TomlNode this[int index] - { - get - { - if (index < RawArray.Count) return RawArray[index]; - var lazy = new TomlLazy(this); - this[index] = lazy; - return lazy; - } - set - { - if (index == RawArray.Count) - RawArray.Add(value); - else - RawArray[index] = value; - } - } - - public override int ChildrenCount => RawArray.Count; - - public override IEnumerable Children => RawArray.AsEnumerable(); - - public override void Add(TomlNode node) => RawArray.Add(node); - - public override void AddRange(IEnumerable nodes) => RawArray.AddRange(nodes); - - public override void Delete(TomlNode node) => RawArray.Remove(node); - - public override void Delete(int index) => RawArray.RemoveAt(index); - - public override string ToString() => ToString(false); - - public string ToString(bool multiline) - { - var sb = new StringBuilder(); - sb.Append(TomlSyntax.ARRAY_START_SYMBOL); - if (ChildrenCount != 0) - { - var arrayStart = multiline ? $"{Environment.NewLine} " : " "; - var arraySeparator = multiline ? $"{TomlSyntax.ITEM_SEPARATOR}{Environment.NewLine} " : $"{TomlSyntax.ITEM_SEPARATOR} "; - var arrayEnd = multiline ? Environment.NewLine : " "; - sb.Append(arrayStart) - .Append(arraySeparator.Join(RawArray.Select(n => n.ToInlineToml()))) - .Append(arrayEnd); - } - sb.Append(TomlSyntax.ARRAY_END_SYMBOL); - return sb.ToString(); - } - - public override void WriteTo(TextWriter tw, string name = null) - { - // If it's a normal array, write it as usual - if (!IsTableArray) - { - tw.WriteLine(ToString(IsMultiline)); - return; - } - - if (Comment is not null) - { - tw.WriteLine(); - Comment.AsComment(tw); - } - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(name); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.WriteLine(); - - var first = true; - - foreach (var tomlNode in RawArray) - { - if (tomlNode is not TomlTable tbl) - throw new TomlFormatException("The array is marked as array table but contains non-table nodes!"); - - // Ensure it's parsed as a section - tbl.IsInline = false; - - if (!first) - { - tw.WriteLine(); - - Comment?.AsComment(tw); - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(name); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.WriteLine(); - } - - first = false; - - // Don't write section since it's already written here - tbl.WriteTo(tw, name, false); - } - } - } - - public class TomlTable : TomlNode - { - private Dictionary children; - internal bool isImplicit; - - public override bool HasValue { get; } = false; - public override bool IsTable { get; } = true; - public bool IsInline { get; set; } - public Dictionary RawTable => children ??= new Dictionary(); - - public override TomlNode this[string key] - { - get - { - if (RawTable.TryGetValue(key, out var result)) return result; - var lazy = new TomlLazy(this); - RawTable[key] = lazy; - return lazy; - } - set => RawTable[key] = value; - } - - public override int ChildrenCount => RawTable.Count; - public override IEnumerable Children => RawTable.Select(kv => kv.Value); - public override IEnumerable Keys => RawTable.Select(kv => kv.Key); - public override bool HasKey(string key) => RawTable.ContainsKey(key); - public override void Add(string key, TomlNode node) => RawTable.Add(key, node); - public override bool TryGetNode(string key, out TomlNode node) => RawTable.TryGetValue(key, out node); - public override void Delete(TomlNode node) => RawTable.Remove(RawTable.First(kv => kv.Value == node).Key); - public override void Delete(string key) => RawTable.Remove(key); - - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append(TomlSyntax.INLINE_TABLE_START_SYMBOL); - - if (ChildrenCount != 0) - { - var collapsed = CollectCollapsedItems(normalizeOrder: false); - - if (collapsed.Count != 0) - sb.Append(' ') - .Append($"{TomlSyntax.ITEM_SEPARATOR} ".Join(collapsed.Select(n => - $"{n.Key} {TomlSyntax.KEY_VALUE_SEPARATOR} {n.Value.ToInlineToml()}"))); - sb.Append(' '); - } - - sb.Append(TomlSyntax.INLINE_TABLE_END_SYMBOL); - return sb.ToString(); - } - - private LinkedList> CollectCollapsedItems(string prefix = "", int level = 0, bool normalizeOrder = true) - { - var nodes = new LinkedList>(); - var postNodes = normalizeOrder ? new LinkedList>() : nodes; - - foreach (var keyValuePair in RawTable) - { - var node = keyValuePair.Value; - var key = keyValuePair.Key.AsKey(); - - if (node is TomlTable tbl) - { - var subnodes = tbl.CollectCollapsedItems($"{prefix}{key}.", level + 1, normalizeOrder); - // Write main table first before writing collapsed items - if (subnodes.Count == 0 && node.CollapseLevel == level) - { - postNodes.AddLast(new KeyValuePair($"{prefix}{key}", node)); - } - foreach (var kv in subnodes) - postNodes.AddLast(kv); - } - else if (node.CollapseLevel == level) - nodes.AddLast(new KeyValuePair($"{prefix}{key}", node)); - } - - if (normalizeOrder) - foreach (var kv in postNodes) - nodes.AddLast(kv); - - return nodes; - } - - public override void WriteTo(TextWriter tw, string name = null) => WriteTo(tw, name, true); - - internal void WriteTo(TextWriter tw, string name, bool writeSectionName) - { - // The table is inline table - if (IsInline && name != null) - { - tw.WriteLine(ToInlineToml()); - return; - } - - var collapsedItems = CollectCollapsedItems(); - - if (collapsedItems.Count == 0) - return; - - var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable { IsInline: false } or TomlArray { IsTableArray: true }); - - Comment?.AsComment(tw); - - if (name != null && (hasRealValues || Comment != null) && writeSectionName) - { - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(name); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.WriteLine(); - } - else if (Comment != null) // Add some spacing between the first node and the comment - { - tw.WriteLine(); - } - - var namePrefix = name == null ? "" : $"{name}."; - var first = true; - - foreach (var collapsedItem in collapsedItems) - { - var key = collapsedItem.Key; - if (collapsedItem.Value is TomlArray { IsTableArray: true } or TomlTable { IsInline: false }) - { - if (!first) tw.WriteLine(); - first = false; - collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}"); - continue; - } - first = false; - - collapsedItem.Value.Comment?.AsComment(tw); - tw.Write(key); - tw.Write(' '); - tw.Write(TomlSyntax.KEY_VALUE_SEPARATOR); - tw.Write(' '); - - collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}"); - } - } - } - - internal class TomlLazy : TomlNode - { - private readonly TomlNode parent; - private TomlNode replacement; - - public TomlLazy(TomlNode parent) => this.parent = parent; - - public override TomlNode this[int index] - { - get => Set()[index]; - set => Set()[index] = value; - } - - public override TomlNode this[string key] - { - get => Set()[key]; - set => Set()[key] = value; - } - - public override void Add(TomlNode node) => Set().Add(node); - - public override void Add(string key, TomlNode node) => Set().Add(key, node); - - public override void AddRange(IEnumerable nodes) => Set().AddRange(nodes); - - private TomlNode Set() where T : TomlNode, new() - { - if (replacement != null) return replacement; - - var newNode = new T - { - Comment = Comment - }; - - if (parent.IsTable) - { - var key = parent.Keys.FirstOrDefault(s => parent.TryGetNode(s, out var node) && node.Equals(this)); - if (key == null) return default(T); - - parent[key] = newNode; - } - else if (parent.IsArray) - { - var index = parent.Children.TakeWhile(child => child != this).Count(); - if (index == parent.ChildrenCount) return default(T); - parent[index] = newNode; - } - else - { - return default(T); - } - - replacement = newNode; - return newNode; - } - } - - #endregion - - #region Parser - - public class TOMLParser : IDisposable - { - public enum ParseState - { - None, - KeyValuePair, - SkipToNextLine, - Table - } - - private readonly TextReader reader; - private ParseState currentState; - private int line, col; - private List syntaxErrors; - - public TOMLParser(TextReader reader) - { - this.reader = reader; - line = col = 0; - } - - public bool ForceASCII { get; set; } - - public void Dispose() => reader?.Dispose(); - - public TomlTable Parse() - { - syntaxErrors = new List(); - line = col = 1; - var rootNode = new TomlTable(); - var currentNode = rootNode; - currentState = ParseState.None; - var keyParts = new List(); - var arrayTable = false; - StringBuilder latestComment = null; - var firstComment = true; - - int currentChar; - while ((currentChar = reader.Peek()) >= 0) - { - var c = (char)currentChar; - - if (currentState == ParseState.None) - { - // Skip white space - if (TomlSyntax.IsWhiteSpace(c)) goto consume_character; - - if (TomlSyntax.IsNewLine(c)) - { - // Check if there are any comments and so far no items being declared - if (latestComment != null && firstComment) - { - rootNode.Comment = latestComment.ToString().TrimEnd(); - latestComment = null; - firstComment = false; - } - - if (TomlSyntax.IsLineBreak(c)) - AdvanceLine(); - - goto consume_character; - } - - // Start of a comment; ignore until newline - if (c == TomlSyntax.COMMENT_SYMBOL) - { - latestComment ??= new StringBuilder(); - latestComment.AppendLine(ParseComment()); - AdvanceLine(1); - continue; - } - - // Encountered a non-comment value. The comment must belong to it (ignore possible newlines)! - firstComment = false; - - if (c == TomlSyntax.TABLE_START_SYMBOL) - { - currentState = ParseState.Table; - goto consume_character; - } - - if (TomlSyntax.IsBareKey(c) || TomlSyntax.IsQuoted(c)) - { - currentState = ParseState.KeyValuePair; - } - else - { - AddError($"Unexpected character \"{c}\""); - continue; - } - } - - if (currentState == ParseState.KeyValuePair) - { - var keyValuePair = ReadKeyValuePair(keyParts); - - if (keyValuePair == null) - { - latestComment = null; - keyParts.Clear(); - - if (currentState != ParseState.None) - AddError("Failed to parse key-value pair!"); - continue; - } - - keyValuePair.Comment = latestComment?.ToString()?.TrimEnd(); - var inserted = InsertNode(keyValuePair, currentNode, keyParts); - latestComment = null; - keyParts.Clear(); - if (inserted) - currentState = ParseState.SkipToNextLine; - continue; - } - - if (currentState == ParseState.Table) - { - if (keyParts.Count == 0) - { - // We have array table - if (c == TomlSyntax.TABLE_START_SYMBOL) - { - // Consume the character - ConsumeChar(); - arrayTable = true; - } - - if (!ReadKeyName(ref keyParts, TomlSyntax.TABLE_END_SYMBOL)) - { - keyParts.Clear(); - continue; - } - - if (keyParts.Count == 0) - { - AddError("Table name is emtpy."); - arrayTable = false; - latestComment = null; - keyParts.Clear(); - } - - continue; - } - - if (c == TomlSyntax.TABLE_END_SYMBOL) - { - if (arrayTable) - { - // Consume the ending bracket so we can peek the next character - ConsumeChar(); - var nextChar = reader.Peek(); - if (nextChar < 0 || (char)nextChar != TomlSyntax.TABLE_END_SYMBOL) - { - AddError($"Array table {".".Join(keyParts)} has only one closing bracket."); - keyParts.Clear(); - arrayTable = false; - latestComment = null; - continue; - } - } - - currentNode = CreateTable(rootNode, keyParts, arrayTable); - if (currentNode != null) - { - currentNode.IsInline = false; - currentNode.Comment = latestComment?.ToString()?.TrimEnd(); - } - - keyParts.Clear(); - arrayTable = false; - latestComment = null; - - if (currentNode == null) - { - if (currentState != ParseState.None) - AddError("Error creating table array!"); - // Reset a node to root in order to try and continue parsing - currentNode = rootNode; - continue; - } - - currentState = ParseState.SkipToNextLine; - goto consume_character; - } - - if (keyParts.Count != 0) - { - AddError($"Unexpected character \"{c}\""); - keyParts.Clear(); - arrayTable = false; - latestComment = null; - } - } - - if (currentState == ParseState.SkipToNextLine) - { - if (TomlSyntax.IsWhiteSpace(c) || c == TomlSyntax.NEWLINE_CARRIAGE_RETURN_CHARACTER) - goto consume_character; - - if (c is TomlSyntax.COMMENT_SYMBOL or TomlSyntax.NEWLINE_CHARACTER) - { - currentState = ParseState.None; - AdvanceLine(); - - if (c == TomlSyntax.COMMENT_SYMBOL) - { - col++; - ParseComment(); - continue; - } - - goto consume_character; - } - - AddError($"Unexpected character \"{c}\" at the end of the line."); - } - - consume_character: - reader.Read(); - col++; - } - - if (currentState != ParseState.None && currentState != ParseState.SkipToNextLine) - AddError("Unexpected end of file!"); - - if (syntaxErrors.Count > 0) - throw new TomlParseException(rootNode, syntaxErrors); - - return rootNode; - } - - private bool AddError(string message, bool skipLine = true) - { - syntaxErrors.Add(new TomlSyntaxException(message, currentState, line, col)); - // Skip the whole line in hope that it was only a single faulty value (and non-multiline one at that) - if (skipLine) - { - reader.ReadLine(); - AdvanceLine(1); - } - currentState = ParseState.None; - return false; - } - - private void AdvanceLine(int startCol = 0) - { - line++; - col = startCol; - } - - private int ConsumeChar() - { - col++; - return reader.Read(); - } - - #region Key-Value pair parsing - - /** - * Reads a single key-value pair. - * Assumes the cursor is at the first character that belong to the pair (including possible whitespace). - * Consumes all characters that belong to the key and the value (ignoring possible trailing whitespace at the end). - * - * Example: - * foo = "bar" ==> foo = "bar" - * ^ ^ - */ - private TomlNode ReadKeyValuePair(List keyParts) - { - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - if (TomlSyntax.IsQuoted(c) || TomlSyntax.IsBareKey(c)) - { - if (keyParts.Count != 0) - { - AddError("Encountered extra characters in key definition!"); - return null; - } - - if (!ReadKeyName(ref keyParts, TomlSyntax.KEY_VALUE_SEPARATOR)) - return null; - - continue; - } - - if (TomlSyntax.IsWhiteSpace(c)) - { - ConsumeChar(); - continue; - } - - if (c == TomlSyntax.KEY_VALUE_SEPARATOR) - { - ConsumeChar(); - return ReadValue(); - } - - AddError($"Unexpected character \"{c}\" in key name."); - return null; - } - - return null; - } - - /** - * Reads a single value. - * Assumes the cursor is at the first character that belongs to the value (including possible starting whitespace). - * Consumes all characters belonging to the value (ignoring possible trailing whitespace at the end). - * - * Example: - * "test" ==> "test" - * ^ ^ - */ - private TomlNode ReadValue(bool skipNewlines = false) - { - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - if (TomlSyntax.IsWhiteSpace(c)) - { - ConsumeChar(); - continue; - } - - if (c == TomlSyntax.COMMENT_SYMBOL) - { - AddError("No value found!"); - return null; - } - - if (TomlSyntax.IsNewLine(c)) - { - if (skipNewlines) - { - reader.Read(); - AdvanceLine(1); - continue; - } - - AddError("Encountered a newline when expecting a value!"); - return null; - } - - if (TomlSyntax.IsQuoted(c)) - { - var isMultiline = IsTripleQuote(c, out var excess); - - // Error occurred in triple quote parsing - if (currentState == ParseState.None) - return null; - - var value = isMultiline - ? ReadQuotedValueMultiLine(c) - : ReadQuotedValueSingleLine(c, excess); - - if (value is null) - return null; - - return new TomlString - { - Value = value, - IsMultiline = isMultiline, - PreferLiteral = c == TomlSyntax.LITERAL_STRING_SYMBOL - }; - } - - return c switch - { - TomlSyntax.INLINE_TABLE_START_SYMBOL => ReadInlineTable(), - TomlSyntax.ARRAY_START_SYMBOL => ReadArray(), - var _ => ReadTomlValue() - }; - } - - return null; - } - - /** - * Reads a single key name. - * Assumes the cursor is at the first character belonging to the key (with possible trailing whitespace if `skipWhitespace = true`). - * Consumes all the characters until the `until` character is met (but does not consume the character itself). - * - * Example 1: - * foo.bar ==> foo.bar (`skipWhitespace = false`, `until = ' '`) - * ^ ^ - * - * Example 2: - * [ foo . bar ] ==> [ foo . bar ] (`skipWhitespace = true`, `until = ']'`) - * ^ ^ - */ - private bool ReadKeyName(ref List parts, char until) - { - var buffer = new StringBuilder(); - var quoted = false; - var prevWasSpace = false; - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - // Reached the final character - if (c == until) break; - - if (TomlSyntax.IsWhiteSpace(c)) - { - prevWasSpace = true; - goto consume_character; - } - - if (buffer.Length == 0) prevWasSpace = false; - - if (c == TomlSyntax.SUBKEY_SEPARATOR) - { - if (buffer.Length == 0 && !quoted) - return AddError($"Found an extra subkey separator in {".".Join(parts)}..."); - - parts.Add(buffer.ToString()); - buffer.Length = 0; - quoted = false; - prevWasSpace = false; - goto consume_character; - } - - if (prevWasSpace) - return AddError("Invalid spacing in key name"); - - if (TomlSyntax.IsQuoted(c)) - { - if (quoted) - - return AddError("Expected a subkey separator but got extra data instead!"); - - if (buffer.Length != 0) - return AddError("Encountered a quote in the middle of subkey name!"); - - // Consume the quote character and read the key name - col++; - buffer.Append(ReadQuotedValueSingleLine((char)reader.Read())); - quoted = true; - continue; - } - - if (TomlSyntax.IsBareKey(c)) - { - buffer.Append(c); - goto consume_character; - } - - // If we see an invalid symbol, let the next parser handle it - break; - - consume_character: - reader.Read(); - col++; - } - - if (buffer.Length == 0 && !quoted) - return AddError($"Found an extra subkey separator in {".".Join(parts)}..."); - - parts.Add(buffer.ToString()); - - return true; - } - - #endregion - - #region Non-string value parsing - - /** - * Reads the whole raw value until the first non-value character is encountered. - * Assumes the cursor start position at the first value character and consumes all characters that may be related to the value. - * Example: - * - * 1_0_0_0 ==> 1_0_0_0 - * ^ ^ - */ - private string ReadRawValue() - { - var result = new StringBuilder(); - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - if (c == TomlSyntax.COMMENT_SYMBOL || TomlSyntax.IsNewLine(c) || TomlSyntax.IsValueSeparator(c)) break; - result.Append(c); - ConsumeChar(); - } - - // Replace trim with manual space counting? - return result.ToString().Trim(); - } - - /** - * Reads and parses a non-string, non-composite TOML value. - * Assumes the cursor at the first character that is related to the value (with possible spaces). - * Consumes all the characters that are related to the value. - * - * Example - * 1_0_0_0 # This is a comment - * - * ==> 1_0_0_0 # This is a comment - * ^ ^ - */ - private TomlNode ReadTomlValue() - { - var value = ReadRawValue(); - TomlNode node = value switch - { - var v when TomlSyntax.IsBoolean(v) => bool.Parse(v), - var v when TomlSyntax.IsNaN(v) => double.NaN, - var v when TomlSyntax.IsPosInf(v) => double.PositiveInfinity, - var v when TomlSyntax.IsNegInf(v) => double.NegativeInfinity, - var v when TomlSyntax.IsInteger(v) => long.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), - CultureInfo.InvariantCulture), - var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), - CultureInfo.InvariantCulture), - var v when TomlSyntax.IsIntegerWithBase(v, out var numberBase) => new TomlInteger - { - Value = Convert.ToInt64(value.Substring(2).RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), numberBase), - IntegerBase = (TomlInteger.Base)numberBase - }, - var _ => null - }; - if (node != null) return node; - - // Normalize by removing space separator - value = value.Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator); - if (StringUtils.TryParseDateTime(value, - TomlSyntax.RFC3339LocalDateTimeFormats, - DateTimeStyles.AssumeLocal, - DateTime.TryParseExact, - out var dateTimeResult, - out var precision)) - return new TomlDateTimeLocal - { - Value = dateTimeResult, - SecondsPrecision = precision - }; - - if (DateTime.TryParseExact(value, - TomlSyntax.LocalDateFormat, - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal, - out dateTimeResult)) - return new TomlDateTimeLocal - { - Value = dateTimeResult, - Style = TomlDateTimeLocal.DateTimeStyle.Date - }; - - if (StringUtils.TryParseDateTime(value, - TomlSyntax.RFC3339LocalTimeFormats, - DateTimeStyles.AssumeLocal, - DateTime.TryParseExact, - out dateTimeResult, - out precision)) - return new TomlDateTimeLocal - { - Value = dateTimeResult, - Style = TomlDateTimeLocal.DateTimeStyle.Time, - SecondsPrecision = precision - }; - - if (StringUtils.TryParseDateTime(value, - TomlSyntax.RFC3339Formats, - DateTimeStyles.None, - DateTimeOffset.TryParseExact, - out var dateTimeOffsetResult, - out precision)) - return new TomlDateTimeOffset - { - Value = dateTimeOffsetResult, - SecondsPrecision = precision - }; - - AddError($"Value \"{value}\" is not a valid TOML value!"); - return null; - } - - /** - * Reads an array value. - * Assumes the cursor is at the start of the array definition. Reads all character until the array closing bracket. - * - * Example: - * [1, 2, 3] ==> [1, 2, 3] - * ^ ^ - */ - private TomlArray ReadArray() - { - // Consume the start of array character - ConsumeChar(); - var result = new TomlArray(); - TomlNode currentValue = null; - var expectValue = true; - - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - if (c == TomlSyntax.ARRAY_END_SYMBOL) - { - ConsumeChar(); - break; - } - - if (c == TomlSyntax.COMMENT_SYMBOL) - { - reader.ReadLine(); - AdvanceLine(1); - continue; - } - - if (TomlSyntax.IsWhiteSpace(c) || TomlSyntax.IsNewLine(c)) - { - if (TomlSyntax.IsLineBreak(c)) - AdvanceLine(); - goto consume_character; - } - - if (c == TomlSyntax.ITEM_SEPARATOR) - { - if (currentValue == null) - { - AddError("Encountered multiple value separators"); - return null; - } - - result.Add(currentValue); - currentValue = null; - expectValue = true; - goto consume_character; - } - - if (!expectValue) - { - AddError("Missing separator between values"); - return null; - } - currentValue = ReadValue(true); - if (currentValue == null) - { - if (currentState != ParseState.None) - AddError("Failed to determine and parse a value!"); - return null; - } - expectValue = false; - - continue; - consume_character: - ConsumeChar(); - } - - if (currentValue != null) result.Add(currentValue); - return result; - } - - /** - * Reads an inline table. - * Assumes the cursor is at the start of the table definition. Reads all character until the table closing bracket. - * - * Example: - * { test = "foo", value = 1 } ==> { test = "foo", value = 1 } - * ^ ^ - */ - private TomlNode ReadInlineTable() - { - ConsumeChar(); - var result = new TomlTable { IsInline = true }; - TomlNode currentValue = null; - var separator = false; - var keyParts = new List(); - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - if (c == TomlSyntax.INLINE_TABLE_END_SYMBOL) - { - ConsumeChar(); - break; - } - - if (c == TomlSyntax.COMMENT_SYMBOL) - { - AddError("Incomplete inline table definition!"); - return null; - } - - if (TomlSyntax.IsNewLine(c)) - { - AddError("Inline tables are only allowed to be on single line"); - return null; - } - - if (TomlSyntax.IsWhiteSpace(c)) - goto consume_character; - - if (c == TomlSyntax.ITEM_SEPARATOR) - { - if (currentValue == null) - { - AddError("Encountered multiple value separators in inline table!"); - return null; - } - - if (!InsertNode(currentValue, result, keyParts)) - return null; - keyParts.Clear(); - currentValue = null; - separator = true; - goto consume_character; - } - - separator = false; - currentValue = ReadKeyValuePair(keyParts); - continue; - - consume_character: - ConsumeChar(); - } - - if (separator) - { - AddError("Trailing commas are not allowed in inline tables."); - return null; - } - - if (currentValue != null && !InsertNode(currentValue, result, keyParts)) - return null; - - return result; - } - - #endregion - - #region String parsing - - /** - * Checks if the string value a multiline string (i.e. a triple quoted string). - * Assumes the cursor is at the first quote character. Consumes the least amount of characters needed to determine if the string is multiline. - * - * If the result is false, returns the consumed character through the `excess` variable. - * - * Example 1: - * """test""" ==> """test""" - * ^ ^ - * - * Example 2: - * "test" ==> "test" (doesn't return the first quote) - * ^ ^ - * - * Example 3: - * "" ==> "" (returns the extra `"` through the `excess` variable) - * ^ ^ - */ - private bool IsTripleQuote(char quote, out char excess) - { - // Copypasta, but it's faster... - - int cur; - // Consume the first quote - ConsumeChar(); - if ((cur = reader.Peek()) < 0) - { - excess = '\0'; - return AddError("Unexpected end of file!"); - } - - if ((char)cur != quote) - { - excess = '\0'; - return false; - } - - // Consume the second quote - excess = (char)ConsumeChar(); - if ((cur = reader.Peek()) < 0 || (char)cur != quote) return false; - - // Consume the final quote - ConsumeChar(); - excess = '\0'; - return true; - } - - /** - * A convenience method to process a single character within a quote. - */ - private bool ProcessQuotedValueCharacter(char quote, - bool isNonLiteral, - char c, - StringBuilder sb, - ref bool escaped) - { - if (TomlSyntax.MustBeEscaped(c)) - return AddError($"The character U+{(int)c:X8} must be escaped in a string!"); - - if (escaped) - { - sb.Append(c); - escaped = false; - return false; - } - - if (c == quote) return true; - if (isNonLiteral && c == TomlSyntax.ESCAPE_SYMBOL) - escaped = true; - if (c == TomlSyntax.NEWLINE_CHARACTER) - return AddError("Encountered newline in single line string!"); - - sb.Append(c); - return false; - } - - /** - * Reads a single-line string. - * Assumes the cursor is at the first character that belongs to the string. - * Consumes all characters that belong to the string (including the closing quote). - * - * Example: - * "test" ==> "test" - * ^ ^ - */ - private string ReadQuotedValueSingleLine(char quote, char initialData = '\0') - { - var isNonLiteral = quote == TomlSyntax.BASIC_STRING_SYMBOL; - var sb = new StringBuilder(); - var escaped = false; - - if (initialData != '\0') - { - var shouldReturn = - ProcessQuotedValueCharacter(quote, isNonLiteral, initialData, sb, ref escaped); - if (currentState == ParseState.None) return null; - if (shouldReturn) - if (isNonLiteral) - { - if (sb.ToString().TryUnescape(out var res, out var ex)) return res; - AddError(ex.Message); - return null; - } - else - return sb.ToString(); - } - - int cur; - var readDone = false; - while ((cur = reader.Read()) >= 0) - { - // Consume the character - col++; - var c = (char)cur; - readDone = ProcessQuotedValueCharacter(quote, isNonLiteral, c, sb, ref escaped); - if (readDone) - { - if (currentState == ParseState.None) return null; - break; - } - } - - if (!readDone) - { - AddError("Unclosed string."); - return null; - } - - if (!isNonLiteral) return sb.ToString(); - if (sb.ToString().TryUnescape(out var unescaped, out var unescapedEx)) return unescaped; - AddError(unescapedEx.Message); - return null; - } - - /** - * Reads a multiline string. - * Assumes the cursor is at the first character that belongs to the string. - * Consumes all characters that belong to the string and the three closing quotes. - * - * Example: - * """test""" ==> """test""" - * ^ ^ - */ - private string ReadQuotedValueMultiLine(char quote) - { - var isBasic = quote == TomlSyntax.BASIC_STRING_SYMBOL; - var sb = new StringBuilder(); - var escaped = false; - var skipWhitespace = false; - var skipWhitespaceLineSkipped = false; - var quotesEncountered = 0; - var first = true; - int cur; - while ((cur = ConsumeChar()) >= 0) - { - var c = (char)cur; - if (TomlSyntax.MustBeEscaped(c, true)) - { - AddError($"The character U+{(int)c:X8} must be escaped!"); - return null; - } - // Trim the first newline - if (first && TomlSyntax.IsNewLine(c)) - { - if (TomlSyntax.IsLineBreak(c)) - first = false; - else - AdvanceLine(); - continue; - } - - first = false; - //TODO: Reuse ProcessQuotedValueCharacter - // Skip the current character if it is going to be escaped later - if (escaped) - { - sb.Append(c); - escaped = false; - continue; - } - - // If we are currently skipping empty spaces, skip - if (skipWhitespace) - { - if (TomlSyntax.IsEmptySpace(c)) - { - if (TomlSyntax.IsLineBreak(c)) - { - skipWhitespaceLineSkipped = true; - AdvanceLine(); - } - continue; - } - - if (!skipWhitespaceLineSkipped) - { - AddError("Non-whitespace character after trim marker."); - return null; - } - - skipWhitespaceLineSkipped = false; - skipWhitespace = false; - } - - // If we encounter an escape sequence... - if (isBasic && c == TomlSyntax.ESCAPE_SYMBOL) - { - var next = reader.Peek(); - var nc = (char)next; - if (next >= 0) - { - // ...and the next char is empty space, we must skip all whitespaces - if (TomlSyntax.IsEmptySpace(nc)) - { - skipWhitespace = true; - continue; - } - - // ...and we have \" or \, skip the character - if (nc == quote || nc == TomlSyntax.ESCAPE_SYMBOL) escaped = true; - } - } - - // Count the consecutive quotes - if (c == quote) - quotesEncountered++; - else - quotesEncountered = 0; - - // If the are three quotes, count them as closing quotes - if (quotesEncountered == 3) break; - - sb.Append(c); - } - - // TOML actually allows to have five ending quotes like - // """"" => "" belong to the string + """ is the actual ending - quotesEncountered = 0; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - if (c == quote && ++quotesEncountered < 3) - { - sb.Append(c); - ConsumeChar(); - } - else break; - } - - // Remove last two quotes (third one wasn't included by default) - sb.Length -= 2; - if (!isBasic) return sb.ToString(); - if (sb.ToString().TryUnescape(out var res, out var ex)) return res; - AddError(ex.Message); - return null; - } - - #endregion - - #region Node creation - - private bool InsertNode(TomlNode node, TomlNode root, IList path) - { - var latestNode = root; - if (path.Count > 1) - for (var index = 0; index < path.Count - 1; index++) - { - var subkey = path[index]; - if (latestNode.TryGetNode(subkey, out var currentNode)) - { - if (currentNode.HasValue) - return AddError($"The key {".".Join(path)} already has a value assigned to it!"); - } - else - { - currentNode = new TomlTable(); - latestNode[subkey] = currentNode; - } - - latestNode = currentNode; - if (latestNode is TomlTable { IsInline: true }) - return AddError($"Cannot assign {".".Join(path)} because it will edit an immutable table."); - } - - if (latestNode.HasKey(path[path.Count - 1])) - return AddError($"The key {".".Join(path)} is already defined!"); - latestNode[path[path.Count - 1]] = node; - node.CollapseLevel = path.Count - 1; - return true; - } - - private TomlTable CreateTable(TomlNode root, IList path, bool arrayTable) - { - if (path.Count == 0) return null; - var latestNode = root; - for (var index = 0; index < path.Count; index++) - { - var subkey = path[index]; - - if (latestNode.TryGetNode(subkey, out var node)) - { - if (node.IsArray && arrayTable) - { - var arr = (TomlArray)node; - - if (!arr.IsTableArray) - { - AddError($"The array {".".Join(path)} cannot be redefined as an array table!"); - return null; - } - - if (index == path.Count - 1) - { - latestNode = new TomlTable(); - arr.Add(latestNode); - break; - } - - latestNode = arr[arr.ChildrenCount - 1]; - continue; - } - - if (node is TomlTable { IsInline: true }) - { - AddError($"Cannot create table {".".Join(path)} because it will edit an immutable table."); - return null; - } - - if (node.HasValue) - { - if (node is not TomlArray { IsTableArray: true } array) - { - AddError($"The key {".".Join(path)} has a value assigned to it!"); - return null; - } - - latestNode = array[array.ChildrenCount - 1]; - continue; - } - - if (index == path.Count - 1) - { - if (arrayTable && !node.IsArray) - { - AddError($"The table {".".Join(path)} cannot be redefined as an array table!"); - return null; - } - - if (node is TomlTable { isImplicit: false }) - { - AddError($"The table {".".Join(path)} is defined multiple times!"); - return null; - } - } - } - else - { - if (index == path.Count - 1 && arrayTable) - { - var table = new TomlTable(); - var arr = new TomlArray - { - IsTableArray = true - }; - arr.Add(table); - latestNode[subkey] = arr; - latestNode = table; - break; - } - - node = new TomlTable { isImplicit = true }; - latestNode[subkey] = node; - } - - latestNode = node; - } - - var result = (TomlTable)latestNode; - result.isImplicit = false; - return result; - } - - #endregion - - #region Misc parsing - - private string ParseComment() - { - ConsumeChar(); - var commentLine = reader.ReadLine()?.Trim() ?? ""; - if (commentLine.Any(ch => TomlSyntax.MustBeEscaped(ch))) - AddError("Comment must not contain control characters other than tab.", false); - return commentLine; - } - #endregion - } - - #endregion - - public static class TOML - { - public static bool ForceASCII { get; set; } = false; - - public static TomlTable Parse(TextReader reader) - { - using var parser = new TOMLParser(reader) { ForceASCII = ForceASCII }; - return parser.Parse(); - } - } - - #region Exception Types - - public class TomlFormatException : Exception - { - public TomlFormatException(string message) : base(message) { } - } - - public class TomlParseException : Exception - { - public TomlParseException(TomlTable parsed, IEnumerable exceptions) : - base("TOML file contains format errors") - { - ParsedTable = parsed; - SyntaxErrors = exceptions; - } - - public TomlTable ParsedTable { get; } - - public IEnumerable SyntaxErrors { get; } - } - - public class TomlSyntaxException : Exception - { - public TomlSyntaxException(string message, TOMLParser.ParseState state, int line, int col) : base(message) - { - ParseState = state; - Line = line; - Column = col; - } - - public TOMLParser.ParseState ParseState { get; } - - public int Line { get; } - - public int Column { get; } - } - - #endregion - - #region Parse utilities - - internal static class TomlSyntax - { - #region Type Patterns - - public const string TRUE_VALUE = "true"; - public const string FALSE_VALUE = "false"; - public const string NAN_VALUE = "nan"; - public const string POS_NAN_VALUE = "+nan"; - public const string NEG_NAN_VALUE = "-nan"; - public const string INF_VALUE = "inf"; - public const string POS_INF_VALUE = "+inf"; - public const string NEG_INF_VALUE = "-inf"; - - public static bool IsBoolean(string s) => s is TRUE_VALUE or FALSE_VALUE; - - public static bool IsPosInf(string s) => s is INF_VALUE or POS_INF_VALUE; - - public static bool IsNegInf(string s) => s == NEG_INF_VALUE; - - public static bool IsNaN(string s) => s is NAN_VALUE or POS_NAN_VALUE or NEG_NAN_VALUE; - - public static bool IsInteger(string s) => IntegerPattern.IsMatch(s); - - public static bool IsFloat(string s) => FloatPattern.IsMatch(s); - - public static bool IsIntegerWithBase(string s, out int numberBase) - { - numberBase = 10; - var match = BasedIntegerPattern.Match(s); - if (!match.Success) return false; - IntegerBases.TryGetValue(match.Groups["base"].Value, out numberBase); - return true; - } - - /** - * A pattern to verify the integer value according to the TOML specification. - */ - public static readonly Regex IntegerPattern = - new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)*)$", RegexOptions.Compiled); - - /** - * A pattern to verify a special 0x, 0o and 0b forms of an integer according to the TOML specification. - */ - public static readonly Regex BasedIntegerPattern = - new(@"^0(?x|b|o)(?!_)(_?[0-9A-F])*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - /** - * A pattern to verify the float value according to the TOML specification. - */ - public static readonly Regex FloatPattern = - new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)+)(((e(\+|-)?(?!_)(_?\d)+)?)|(\.(?!_)(_?\d)+(e(\+|-)?(?!_)(_?\d)+)?))$", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - /** - * A helper dictionary to map TOML base codes into the radii. - */ - public static readonly Dictionary IntegerBases = new() - { - ["x"] = 16, - ["o"] = 8, - ["b"] = 2 - }; - - /** - * A helper dictionary to map non-decimal bases to their TOML identifiers - */ - public static readonly Dictionary BaseIdentifiers = new() - { - [2] = "b", - [8] = "o", - [16] = "x" - }; - - public const string RFC3339EmptySeparator = " "; - public const string ISO861Separator = "T"; - public const string ISO861ZeroZone = "+00:00"; - public const string RFC3339ZeroZone = "Z"; - - /** - * Valid date formats with timezone as per RFC3339. - */ - public static readonly string[] RFC3339Formats = - { - "yyyy'-'MM-ddTHH':'mm':'ssK", "yyyy'-'MM-ddTHH':'mm':'ss'.'fK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffK", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffK", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffffK", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffffK" - }; - - /** - * Valid date formats without timezone (assumes local) as per RFC3339. - */ - public static readonly string[] RFC3339LocalDateTimeFormats = - { - "yyyy'-'MM-ddTHH':'mm':'ss", "yyyy'-'MM-ddTHH':'mm':'ss'.'f", "yyyy'-'MM-ddTHH':'mm':'ss'.'ff", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffff", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffff", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffff" - }; - - /** - * Valid full date format as per TOML spec. - */ - public static readonly string LocalDateFormat = "yyyy'-'MM'-'dd"; - - /** - * Valid time formats as per TOML spec. - */ - public static readonly string[] RFC3339LocalTimeFormats = - { - "HH':'mm':'ss", "HH':'mm':'ss'.'f", "HH':'mm':'ss'.'ff", "HH':'mm':'ss'.'fff", "HH':'mm':'ss'.'ffff", - "HH':'mm':'ss'.'fffff", "HH':'mm':'ss'.'ffffff", "HH':'mm':'ss'.'fffffff" - }; - - #endregion - - #region Character definitions - - public const char ARRAY_END_SYMBOL = ']'; - public const char ITEM_SEPARATOR = ','; - public const char ARRAY_START_SYMBOL = '['; - public const char BASIC_STRING_SYMBOL = '\"'; - public const char COMMENT_SYMBOL = '#'; - public const char ESCAPE_SYMBOL = '\\'; - public const char KEY_VALUE_SEPARATOR = '='; - public const char NEWLINE_CARRIAGE_RETURN_CHARACTER = '\r'; - public const char NEWLINE_CHARACTER = '\n'; - public const char SUBKEY_SEPARATOR = '.'; - public const char TABLE_END_SYMBOL = ']'; - public const char TABLE_START_SYMBOL = '['; - public const char INLINE_TABLE_START_SYMBOL = '{'; - public const char INLINE_TABLE_END_SYMBOL = '}'; - public const char LITERAL_STRING_SYMBOL = '\''; - public const char INT_NUMBER_SEPARATOR = '_'; - - public static readonly char[] NewLineCharacters = { NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER }; - - public static bool IsQuoted(char c) => c is BASIC_STRING_SYMBOL or LITERAL_STRING_SYMBOL; - - public static bool IsWhiteSpace(char c) => c is ' ' or '\t'; - - public static bool IsNewLine(char c) => c is NEWLINE_CHARACTER or NEWLINE_CARRIAGE_RETURN_CHARACTER; - - public static bool IsLineBreak(char c) => c == NEWLINE_CHARACTER; - - public static bool IsEmptySpace(char c) => IsWhiteSpace(c) || IsNewLine(c); - - public static bool IsBareKey(char c) => - c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_' or '-'; - - public static bool MustBeEscaped(char c, bool allowNewLines = false) - { - var result = c is (>= '\u0000' and <= '\u0008') or '\u000b' or '\u000c' or (>= '\u000e' and <= '\u001f') or '\u007f'; - if (!allowNewLines) - result |= c is >= '\u000a' and <= '\u000e'; - return result; - } - - public static bool IsValueSeparator(char c) => - c is ITEM_SEPARATOR or ARRAY_END_SYMBOL or INLINE_TABLE_END_SYMBOL; - - #endregion - } - - internal static class StringUtils - { - public static string AsKey(this string key) - { - var quote = key == string.Empty || key.Any(c => !TomlSyntax.IsBareKey(c)); - return !quote ? key : $"{TomlSyntax.BASIC_STRING_SYMBOL}{key.Escape()}{TomlSyntax.BASIC_STRING_SYMBOL}"; - } - - public static string Join(this string self, IEnumerable subItems) - { - var sb = new StringBuilder(); - var first = true; - - foreach (var subItem in subItems) - { - if (!first) sb.Append(self); - first = false; - sb.Append(subItem); - } - - return sb.ToString(); - } - - public delegate bool TryDateParseDelegate(string s, string format, IFormatProvider ci, DateTimeStyles dts, out T dt); - - public static bool TryParseDateTime(string s, - string[] formats, - DateTimeStyles styles, - TryDateParseDelegate parser, - out T dateTime, - out int parsedFormat) - { - parsedFormat = 0; - dateTime = default; - for (var i = 0; i < formats.Length; i++) - { - var format = formats[i]; - if (!parser(s, format, CultureInfo.InvariantCulture, styles, out dateTime)) continue; - parsedFormat = i; - return true; - } - - return false; - } - - public static void AsComment(this string self, TextWriter tw) - { - foreach (var line in self.Split(TomlSyntax.NEWLINE_CHARACTER)) - tw.WriteLine($"{TomlSyntax.COMMENT_SYMBOL} {line.Trim()}"); - } - - public static string RemoveAll(this string txt, char toRemove) - { - var sb = new StringBuilder(txt.Length); - foreach (var c in txt.Where(c => c != toRemove)) - sb.Append(c); - return sb.ToString(); - } - - public static string Escape(this string txt, bool escapeNewlines = true) - { - var stringBuilder = new StringBuilder(txt.Length + 2); - for (var i = 0; i < txt.Length; i++) - { - var c = txt[i]; - - static string CodePoint(string txt, ref int i, char c) => char.IsSurrogatePair(txt, i) - ? $"\\U{char.ConvertToUtf32(txt, i++):X8}" - : $"\\u{(ushort)c:X4}"; - - stringBuilder.Append(c switch - { - '\b' => @"\b", - '\t' => @"\t", - '\n' when escapeNewlines => @"\n", - '\f' => @"\f", - '\r' when escapeNewlines => @"\r", - '\\' => @"\\", - '\"' => @"\""", - var _ when TomlSyntax.MustBeEscaped(c, !escapeNewlines) || TOML.ForceASCII && c > sbyte.MaxValue => - CodePoint(txt, ref i, c), - var _ => c - }); - } - - return stringBuilder.ToString(); - } - - public static bool TryUnescape(this string txt, out string unescaped, out Exception exception) - { - try - { - exception = null; - unescaped = txt.Unescape(); - return true; - } - catch (Exception e) - { - exception = e; - unescaped = null; - return false; - } - } - - public static string Unescape(this string txt) - { - if (string.IsNullOrEmpty(txt)) return txt; - var stringBuilder = new StringBuilder(txt.Length); - for (var i = 0; i < txt.Length;) - { - var num = txt.IndexOf('\\', i); - var next = num + 1; - if (num < 0 || num == txt.Length - 1) num = txt.Length; - stringBuilder.Append(txt, i, num - i); - if (num >= txt.Length) break; - var c = txt[next]; - - static string CodePoint(int next, string txt, ref int num, int size) - { - if (next + size >= txt.Length) throw new Exception("Undefined escape sequence!"); - num += size; - return char.ConvertFromUtf32(Convert.ToInt32(txt.Substring(next + 1, size), 16)); - } - - stringBuilder.Append(c switch - { - 'b' => "\b", - 't' => "\t", - 'n' => "\n", - 'f' => "\f", - 'r' => "\r", - '\'' => "\'", - '\"' => "\"", - '\\' => "\\", - 'u' => CodePoint(next, txt, ref num, 4), - 'U' => CodePoint(next, txt, ref num, 8), - var _ => throw new Exception("Undefined escape sequence!") - }); - i = num + 2; - } - - return stringBuilder.ToString(); - } - } - - #endregion -} \ No newline at end of file