aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core/Runs/Scripts/StepLexer.cs
blob: 4c54544cf6d455c72effd1c1b5aa71a8230bbbcc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// 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);

/// <summary>Line lexing for <c>.steps</c>: <c>#</c> comments outside quotes; tokens split on spaces, <c>"…"</c> (escapes
/// <c>\"</c>, <c>\\</c>) and <c>(…)</c> groups stay one token.</summary>
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<Token> Tokens(string text)
    {
        var tokens = new List<Token>();
        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;
    }
}

/// <summary>Timeouts and waits in frames (1 frame = 1/60 s): <c>120</c> = 120 frames, <c>2s</c> = 120, <c>0.5s</c> = 30;
/// negative, non-whole frame counts and other units → null.</summary>
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;
    }
}