using System; using System.Collections.Generic; namespace PluralKit.Bot { public class Parameters { // Dictionary of (left, right) quote pairs // Each char in the string is an individual quote, multi-char strings imply "one of the following chars" private static readonly Dictionary _quotePairs = new Dictionary { // Basic {"'", "'"}, // ASCII single quotes {"\"", "\""}, // ASCII double quotes // "Smart quotes" // Specifically ignore the left/right status of the quotes and match any combination of them // Left string also includes "low" quotes to allow for the low-high style used in some locales {"\u201C\u201D\u201F\u201E", "\u201C\u201D\u201F"}, // double quotes {"\u2018\u2019\u201B\u201A", "\u2018\u2019\u201B"}, // single quotes // Chevrons (normal and "fullwidth" variants) {"\u00AB\u300A", "\u00BB\u300B"}, // double chevrons, pointing away (<>) {"\u00BB\u300B", "\u00AA\u300A"}, // double chevrons, pointing together (>>text<<) {"\u2039\u3008", "\u203A\u3009"}, // single chevrons, pointing away () {"\u203A\u3009", "\u2039\u3008"}, // single chevrons, pointing together (>text<) // Other {"\u300C\u300E", "\u300D\u300F"}, // corner brackets (Japanese/Chinese) }; private readonly string _cmd; private int _ptr; private ISet _flags = null; // Only parsed when requested first time private struct WordPosition { // Start of the word internal int startPos; // End of the word internal int endPos; // How much to advance word pointer afterwards to point at the start of the *next* word internal int advanceAfterWord; internal bool wasQuoted; public WordPosition(int startPos, int endPos, int advanceAfterWord, bool wasQuoted) { this.startPos = startPos; this.endPos = endPos; this.advanceAfterWord = advanceAfterWord; this.wasQuoted = wasQuoted; } } public Parameters(string cmd) { // This is a SUPER dirty hack to avoid having to match both spaces and newlines in the word detection below // Instead, we just add a space before every newline (which then gets stripped out later). _cmd = cmd.Replace("\n", " \n"); _ptr = 0; } private void ParseFlags() { _flags = new HashSet(); var ptr = 0; while (NextWordPosition(ptr) is { } wp) { ptr = wp.endPos + wp.advanceAfterWord; // Is this word a *flag* (as in, starts with a - AND is not quoted) if (_cmd[wp.startPos] != '-' || wp.wasQuoted) continue; // (if not, carry on w/ next word) // Find the *end* of the flag start (technically allowing arbitrary amounts of dashes) var flagNameStart = wp.startPos; while (flagNameStart < _cmd.Length && _cmd[flagNameStart] == '-') flagNameStart++; // Then add the word to the flag set var word = _cmd.Substring(flagNameStart, wp.endPos - flagNameStart).Trim(); if (word.Length > 0) _flags.Add(word.ToLowerInvariant()); } } public string Pop() { // Loop to ignore and skip past flags while (NextWordPosition(_ptr) is { } pos) { _ptr = pos.endPos + pos.advanceAfterWord; if (_cmd[pos.startPos] == '-' && !pos.wasQuoted) continue; return _cmd.Substring(pos.startPos, pos.endPos - pos.startPos).Trim(); } return ""; } public string Peek() { // Loop to ignore and skip past flags, temp ptr so we don't move the real ptr var ptr = _ptr; while (NextWordPosition(ptr) is { } pos) { ptr = pos.endPos + pos.advanceAfterWord; if (_cmd[pos.startPos] == '-' && !pos.wasQuoted) continue; return _cmd.Substring(pos.startPos, pos.endPos - pos.startPos).Trim(); } return ""; } public ISet Flags() { if (_flags == null) ParseFlags(); return _flags; } public string Remainder(bool skipFlags = true) { if (skipFlags) { // Skip all *leading* flags when taking the remainder while (NextWordPosition(_ptr) is {} wp) { if (_cmd[wp.startPos] != '-' || wp.wasQuoted) break; _ptr = wp.endPos + wp.advanceAfterWord; } } // *Then* get the remainder return _cmd.Substring(Math.Min(_ptr, _cmd.Length)).Trim(); } public string FullCommand => _cmd; private WordPosition? NextWordPosition(int position) { // Skip leading spaces before actual content while (position < _cmd.Length && _cmd[position] == ' ') position++; // Is this the end of the string? if (_cmd.Length <= position) return null; // Is this a quoted word? if (TryCheckQuote(_cmd[position], out var endQuotes)) { // We found a quoted word - find an instance of one of the corresponding end quotes var endQuotePosition = -1; for (var i = position + 1; i < _cmd.Length; i++) if (endQuotePosition == -1 && endQuotes.Contains(_cmd[i])) endQuotePosition = i; // need a break; don't feel like brackets tho lol // Position after the end quote should be EOL or a space // Otherwise we fallthrough to the unquoted word handler below if (_cmd.Length == endQuotePosition + 1 || _cmd[endQuotePosition + 1] == ' ') return new WordPosition(position + 1, endQuotePosition, 2, true); } // Not a quoted word, just find the next space and return if it's the end of the command var wordEnd = _cmd.IndexOf(' ', position + 1); return wordEnd == -1 ? new WordPosition(position, _cmd.Length, 0, false) : new WordPosition(position, wordEnd, 1, false); } private bool TryCheckQuote(char potentialLeftQuote, out string correspondingRightQuotes) { foreach (var (left, right) in _quotePairs) { if (left.Contains(potentialLeftQuote)) { correspondingRightQuotes = right; return true; } } correspondingRightQuotes = null; return false; } } }