-
Notifications
You must be signed in to change notification settings - Fork 271
/
Copy pathparser.go
312 lines (269 loc) · 6.63 KB
/
parser.go
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
package main
import (
"path/filepath"
"strings"
)
// Parser is the structure that manages the parsing of tokens.
type Parser struct {
l *Lexer
errors []ParserError
cur Token
peek Token
}
// NewParser returns a new Parser.
func NewParser(l *Lexer) *Parser {
p := &Parser{l: l, errors: []ParserError{}}
// Read two tokens, so cur and peek are both set.
p.nextToken()
p.nextToken()
return p
}
// Parse takes an input string provided by the lexer and parses it into a
// list of commands.
func (p *Parser) Parse() []Command {
cmds := []Command{}
for p.cur.Type != EOF {
if p.cur.Type == COMMENT {
p.nextToken()
continue
}
cmds = append(cmds, p.parseCommand())
p.nextToken()
}
return cmds
}
// parseCommand parses a command.
func (p *Parser) parseCommand() Command {
switch p.cur.Type {
case SPACE, BACKSPACE, ENTER, ESCAPE, TAB, DOWN, LEFT, RIGHT, UP:
return p.parseKeypress(p.cur.Type)
case SET:
return p.parseSet()
case OUTPUT:
return p.parseOutput()
case SLEEP:
return p.parseSleep()
case TYPE:
return p.parseType()
case CTRL:
return p.parseCtrl()
case HIDE:
return p.parseHide()
case REQUIRE:
return p.parseRequire()
case SHOW:
return p.parseShow()
default:
p.errors = append(p.errors, NewError(p.cur, "Invalid command: "+p.cur.Literal))
return Command{Type: ILLEGAL}
}
}
// parseSpeed parses a typing speed indication.
//
// i.e. @<time>
//
// This is optional (defaults to 100ms), thus skips (rather than error-ing)
// if the typing speed is not specified.
func (p *Parser) parseSpeed() string {
if p.peek.Type == AT {
p.nextToken()
return p.parseTime()
}
return ""
}
// parseRepeat parses an optional repeat count for a command.
//
// i.e. Backspace [count]
//
// This is optional (defaults to 1), thus skips (rather than error-ing)
// if the repeat count is not specified.
func (p *Parser) parseRepeat() string {
if p.peek.Type == NUMBER {
count := p.peek.Literal
p.nextToken()
return count
}
return "1"
}
// parseTime parses a time argument.
//
// <number>[ms]
func (p *Parser) parseTime() string {
var t string
if p.peek.Type == NUMBER {
t = p.peek.Literal
p.nextToken()
} else {
p.errors = append(p.errors, NewError(p.cur, "Expected time after "+p.cur.Literal))
}
// Allow TypingSpeed to have bare units (e.g. 50ms, 100ms)
if p.peek.Type == MILLISECONDS || p.peek.Type == SECONDS {
t += p.peek.Literal
p.nextToken()
} else {
t += "s"
}
return t
}
// parseCtrl parses a control command.
// A control command takes a character to type while the modifier is held down.
//
// Ctrl+<character>
func (p *Parser) parseCtrl() Command {
if p.peek.Type == PLUS {
p.nextToken()
if p.peek.Type == STRING {
c := p.peek.Literal
p.nextToken()
return Command{Type: CTRL, Args: c}
}
}
p.errors = append(p.errors, NewError(p.cur, "Expected control character, got "+p.cur.Literal))
return Command{Type: CTRL}
}
// parseKeypress parses a repeatable and time adjustable keypress command.
// A keypress command takes an optional typing speed and optional count.
//
// Key[@<time>] [count]
func (p *Parser) parseKeypress(ct TokenType) Command {
cmd := Command{Type: CommandType(ct)}
cmd.Options = p.parseSpeed()
cmd.Args = p.parseRepeat()
return cmd
}
// parseOutput parses an output command.
// An output command takes a file path to which to output.
//
// Output <path>
func (p *Parser) parseOutput() Command {
cmd := Command{Type: OUTPUT}
if p.peek.Type != STRING {
p.errors = append(p.errors, NewError(p.cur, "Expected file path after output"))
return cmd
}
ext := filepath.Ext(p.peek.Literal)
if ext != "" {
cmd.Options = ext
} else {
cmd.Options = ".png"
if !strings.HasSuffix(p.peek.Literal, "/") {
p.errors = append(p.errors, NewError(p.peek, "Expected folder with trailing slash"))
}
}
cmd.Args = p.peek.Literal
p.nextToken()
return cmd
}
// parseSet parses a set command.
// A set command takes a setting name and a value.
//
// Set <setting> <value>
func (p *Parser) parseSet() Command {
cmd := Command{Type: SET}
if IsSetting(p.peek.Type) {
cmd.Options = p.peek.Literal
} else {
p.errors = append(p.errors, NewError(p.peek, "Unknown setting: "+p.peek.Literal))
}
p.nextToken()
switch p.cur.Type {
case LOOP_OFFSET:
cmd.Args = p.peek.Literal
p.nextToken()
// Allow LoopOffset without '%'
// Set LoopOffset 20
cmd.Args += "%"
if p.peek.Type == PERCENT {
p.nextToken()
}
case TYPING_SPEED:
cmd.Args = p.peek.Literal
p.nextToken()
// Allow TypingSpeed to have bare units (e.g. 10ms)
// Set TypingSpeed 10ms
if p.peek.Type == MILLISECONDS || p.peek.Type == SECONDS {
cmd.Args += p.peek.Literal
p.nextToken()
} else if cmd.Options == "TypingSpeed" {
cmd.Args += "s"
}
default:
cmd.Args = p.peek.Literal
p.nextToken()
}
return cmd
}
// parseSleep parses a sleep command.
// A sleep command takes a time for how long to sleep.
//
// Sleep <time>
func (p *Parser) parseSleep() Command {
cmd := Command{Type: SLEEP}
cmd.Args = p.parseTime()
return cmd
}
// parseHide parses a Hide command.
//
// Hide
// ...
func (p *Parser) parseHide() Command {
cmd := Command{Type: HIDE}
return cmd
}
// parseRequire parses a Require command.
//
// ...
// Require
func (p *Parser) parseRequire() Command {
cmd := Command{Type: REQUIRE}
if p.peek.Type != STRING {
p.errors = append(p.errors, NewError(p.peek, p.cur.Literal+" expects one string"))
}
cmd.Args = p.peek.Literal
p.nextToken()
return cmd
}
// parseShow parses a Show command.
//
// ...
// Show
func (p *Parser) parseShow() Command {
cmd := Command{Type: SHOW}
return cmd
}
// parseType parses a type command.
// A type command takes a string to type.
//
// Type "string"
func (p *Parser) parseType() Command {
cmd := Command{Type: TYPE}
cmd.Options = p.parseSpeed()
if p.peek.Type != STRING {
p.errors = append(p.errors, NewError(p.peek, p.cur.Literal+" expects string"))
}
for p.peek.Type == STRING {
p.nextToken()
cmd.Args += p.cur.Literal
// If the next token is a string, add a space between them.
// Since tokens must be separated by a whitespace, this is most likely
// what the user intended.
//
// Although it is possible that there may be multiple spaces / tabs between
// the tokens, however if the user was intending to type multiple spaces
// they would need to use a string literal.
if p.peek.Type == STRING {
cmd.Args += " "
}
}
return cmd
}
// Errors returns any errors that occurred during parsing.
func (p *Parser) Errors() []ParserError {
return p.errors
}
// nextToken gets the next token from the lexer
// and updates the parser tokens accordingly.
func (p *Parser) nextToken() {
p.cur = p.peek
p.peek = p.l.NextToken()
}