// Provenance: own design — shared run scripts using System.Globalization; using System.Text; namespace Godosa.Core.Runs.Scripts; public sealed record Token(string Text, bool Quoted); /// Line lexing for .steps: # comments outside quotes; tokens split on spaces, "…" (escapes /// \", \\) and (…) groups stay one token. public static class StepLexer { public static string StripComment(string line) { var quoted = false; for (var i = 0; i < line.Length; i++) { var c = line[i]; if (quoted && c == '\\' && i + 1 < line.Length) { i++; continue; } if (c == '"') quoted = !quoted; else if (c == '#' && !quoted) return line[..i]; } return line; } public static List Tokens(string text) { var tokens = new List(); var i = 0; while (i < text.Length) { if (text[i] == ' ') { i++; continue; } if (text[i] == '"') { var sb = new StringBuilder(); i++; while (true) { if (i >= text.Length) throw new FormatException("unterminated quote"); var c = text[i]; if (c == '\\' && i + 1 < text.Length) { sb.Append(text[i + 1]); i += 2; continue; } if (c == '"') { i++; break; } sb.Append(c); i++; } tokens.Add(new Token(sb.ToString(), true)); continue; } var start = i; var depth = 0; while (i < text.Length && (depth > 0 || text[i] != ' ')) { if (text[i] == '(') depth++; else if (text[i] == ')' && depth > 0) depth--; i++; } tokens.Add(new Token(text[start..i], false)); } return tokens; } } /// Timeouts and waits in frames (1 frame = 1/60 s): 120 = 120 frames, 2s = 120, 0.5s = 30; /// negative, non-whole frame counts and other units → null. public static class Durations { public const int FramesPerSecond = 60; public static int? Frames(string text) { if (text.EndsWith('s')) { var number = text[..^1]; if (number.Length == 0 || !number.All(c => char.IsAsciiDigit(c) || c == '.') || !decimal.TryParse(number, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var seconds)) return null; var frames = seconds * FramesPerSecond; return frames == decimal.Truncate(frames) ? (int)frames : null; } return text.Length > 0 && text.All(char.IsAsciiDigit) && int.TryParse(text, CultureInfo.InvariantCulture, out var n) ? n : null; } }