using Superpower; using Superpower.Display; using Superpower.Model; using Superpower.Parsers; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LANCommander.SDK.Parsers.Ini { enum IniToken { [Token(Example = "[")] SectionHeaderStart, [Token(Example = "]")] SectionHeaderEnd, [Token(Example = ";")] CommentStart, [Token(Example = "=")] KeyValueDelimiter } static class IniTokenizer { static TextParser IniStringToken { get; } = from open in Character.EqualTo('"') from content in Character.EqualTo('\\').IgnoreThen(Character.AnyChar).Value(Unit.Value).Try() .Or(Character.Except('"').Value(Unit.Value)) .IgnoreMany() from close in Character.EqualTo('"') select Unit.Value; } // ── Data model ─────────────────────────────────────────────────────────────── /// Represents a single key-value pair inside an INI section. public class IniKey { public string Name { get; } public string? Value { get; set; } public IniKey(string name, string? value = null) { Name = name; Value = value; } } /// An ordered, LINQ-queryable collection of entries. public class IniKeyCollection : IEnumerable { private readonly List _keys = new(); /// Total number of keys, including duplicates. public int Count => _keys.Count; /// Returns true if any key has the given name (case-insensitive). public bool Contains(string name) => _keys.Any(k => string.Equals(k.Name, name, StringComparison.OrdinalIgnoreCase)); /// Returns the first key with the given name, or null. public IniKey? this[string name] => _keys.FirstOrDefault(k => string.Equals(k.Name, name, StringComparison.OrdinalIgnoreCase)); public void Add(IniKey key) => _keys.Add(key); public void Add(string name, string? value) => _keys.Add(new IniKey(name, value)); public void Insert(int index, IniKey key) => _keys.Insert(index, key); public void Insert(int index, string name, string? value) => _keys.Insert(index, new IniKey(name, value)); public bool Remove(IniKey key) => _keys.Remove(key); public IEnumerator GetEnumerator() => _keys.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } /// Represents an INI section with a name and a collection of keys. public class IniSection { public string Name { get; } public IniKeyCollection Keys { get; } = new(); public IniSection(string name) => Name = name; } /// An ordered, LINQ-queryable collection of entries. public class IniSectionCollection : IEnumerable { private readonly List _sections = new(); /// Total number of sections. public int Count => _sections.Count; /// Returns true if any section has the given name (case-insensitive). public bool Contains(string name) => _sections.Any(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase)); /// Returns the first section with the given name, or null. public IniSection? this[string name] => _sections.FirstOrDefault(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase)); public void Add(IniSection section) => _sections.Add(section); public bool Remove(IniSection section) => _sections.Remove(section); public IEnumerator GetEnumerator() => _sections.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } /// The parsed representation of an INI document. public class IniDocument { public IniSectionCollection Sections { get; } = new(); /// Serializes the document back to INI format. public string Serialize() { var sb = new StringBuilder(); foreach (var section in Sections) { sb.AppendLine($"[{section.Name}]"); foreach (var key in section.Keys) { sb.AppendLine($"{key.Name}={key.Value}"); } sb.AppendLine(); } return sb.ToString(); } } /// Options controlling INI parse/write behavior. public class IniParseOptions { /// If false, duplicate keys within a section are merged (first value wins). public bool AllowDuplicateKeys { get; set; } = true; /// If false, duplicate sections are merged (keys combined). public bool AllowDuplicateSections { get; set; } = true; /// Encoding for file I/O operations. public Encoding Encoding { get; set; } = Encoding.Default; } // ── Parser ─────────────────────────────────────────────────────────────────── /// /// A Superpower-based parser for INI-formatted text. /// /// Supports: /// /// Sections: [SectionName] (names may contain .) /// Key-value pairs: key = value or key=value /// Keys with brackets: Aliases[0], bare digits: 0 /// Values containing =, ", (), | — split on first = /// Duplicate keys within the same section (multi-value) /// Comment lines beginning with ; or # /// Blank lines (ignored) /// /// /// public class IniParser { // Consume any characters that are not a line-ending character. private static readonly TextParser RestOfLine = Character.ExceptIn('\r', '\n').Many(); // Section header: optional leading whitespace, '[', section name (trimmed), ']', // then any trailing content on the same line is consumed and discarded. private static readonly TextParser SectionHeaderParser = from _ws in Character.In(' ', '\t').Many() from _ob in Character.EqualTo('[') from name in Character.ExceptIn(']', '\r', '\n').AtLeastOnce() .Select(chars => new string(chars).Trim()) from _cb in Character.EqualTo(']') from _rest in RestOfLine select name; // Key-value line. // Key = everything before the first '=' on the line, trimmed. // Value = everything after the first '=' on the line, trimmed. // The Where guard rejects lines whose key part is entirely whitespace // (e.g. a stray '=' with no key), letting them fall through to the // comment/blank skip path. private static readonly TextParser<(string Key, string Value)> KeyValueLineParser = from key in Character.ExceptIn('=', '\r', '\n').AtLeastOnce() .Select(chars => new string(chars).Trim()) .Where(s => !string.IsNullOrWhiteSpace(s)) from _eq in Character.EqualTo('=') from value in RestOfLine.Select(chars => new string(chars).Trim()) select (key, value); /// /// Parses an INI-formatted string and returns an . /// /// The INI file content to parse. /// A parsed . public static IniDocument Parse(string text) => Parse(text, new IniParseOptions()); /// /// Parses an INI-formatted string with options and returns an . /// public static IniDocument Parse(string text, IniParseOptions options) { var doc = new IniDocument(); IniSection? currentSection = null; var lines = text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); foreach (var rawLine in lines) { var span = new TextSpan(rawLine); // 1. Section header? var sectionResult = SectionHeaderParser(span); if (sectionResult.HasValue) { var sectionName = sectionResult.Value; if (!options.AllowDuplicateSections) { var existing = doc.Sections[sectionName]; if (existing != null) { currentSection = existing; continue; } } currentSection = new IniSection(sectionName); doc.Sections.Add(currentSection); continue; } // 2. Key-value pair (only inside a section)? var kvResult = KeyValueLineParser(span); if (kvResult.HasValue && currentSection is not null) { if (!options.AllowDuplicateKeys) { var existingKey = currentSection.Keys[kvResult.Value.Key]; if (existingKey != null) { // Merge: keep first value (ignore duplicates) continue; } } currentSection.Keys.Add(new IniKey(kvResult.Value.Key, kvResult.Value.Value)); continue; } // 3. Comment or blank line — skip. } return doc; } /// /// Loads and parses an INI file from disk. /// public static IniDocument Load(string filePath, IniParseOptions? options = null) { var opts = options ?? new IniParseOptions(); var text = File.ReadAllText(filePath, opts.Encoding); return Parse(text, opts); } /// /// Saves an to disk. /// public static void Save(IniDocument document, string filePath, Encoding? encoding = null) { File.WriteAllText(filePath, document.Serialize(), encoding ?? Encoding.Default); } } }