// Package parser provides a parser for the VHS Tape language. package parser import ( "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/charmbracelet/vhs/lexer" "github.com/charmbracelet/vhs/token" ) // NewError returns a new parser.Error with the given token and message. func NewError(token token.Token, msg string) Error { return Error{ Token: token, Msg: msg, } } // CommandType is a type that represents a command. type CommandType token.Type // CommandTypes is a list of the available commands that can be executed. var CommandTypes = []CommandType{ token.BACKSPACE, token.DELETE, token.INSERT, token.CTRL, token.ALT, token.DOWN, token.ENTER, token.ESCAPE, token.ILLEGAL, token.LEFT, token.PAGE_UP, token.PAGE_DOWN, token.SCROLL_UP, token.SCROLL_DOWN, token.RIGHT, token.SET, token.OUTPUT, token.SLEEP, token.SPACE, token.HIDE, token.REQUIRE, token.SHOW, token.TAB, token.TYPE, token.UP, token.WAIT, token.SOURCE, token.SCREENSHOT, token.COPY, token.PASTE, token.ENV, } // String returns the string representation of the command. func (c CommandType) String() string { return token.ToCamel(string(c)) } // Command represents a command with options and arguments. type Command struct { Type CommandType Options string Args string Source string } // String returns the string representation of the command. // This includes the options and arguments of the command. func (c Command) String() string { if c.Options != "" { return fmt.Sprintf("%s %s %s", c.Type, c.Options, c.Args) } return fmt.Sprintf("%s %s", c.Type, c.Options) } // Error represents an error with parsing a tape file. // It tracks the token causing the error and a human readable error message. type Error struct { Token token.Token Msg string } // String returns a human readable error message printing the token line number // and message. func (e Error) String() string { return fmt.Sprintf("%2d:%-2d │ %s", e.Token.Line, e.Token.Column, e.Msg) } // Error implements the error interface. func (e Error) Error() string { return e.String() } // Parser is the structure that manages the parsing of tokens. type Parser struct { l *lexer.Lexer errors []Error cur token.Token peek token.Token } // New returns a new Parser. func New(l *lexer.Lexer) *Parser { p := &Parser{l: l, errors: []Error{}} // 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 != token.EOF { if p.cur.Type == token.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 token.SPACE, token.BACKSPACE, token.DELETE, token.INSERT, token.ENTER, token.ESCAPE, token.TAB, token.DOWN, token.LEFT, token.RIGHT, token.UP, token.PAGE_UP, token.PAGE_DOWN, token.SCROLL_UP, token.SCROLL_DOWN: return []Command{p.parseKeypress(p.cur.Type)} case token.SET: return []Command{p.parseSet()} case token.OUTPUT: return []Command{p.parseOutput()} case token.SLEEP: return []Command{p.parseSleep()} case token.TYPE: return []Command{p.parseType()} case token.CTRL: return []Command{p.parseCtrl()} case token.ALT: return []Command{p.parseAlt()} case token.SHIFT: return []Command{p.parseShift()} case token.HIDE: return []Command{p.parseHide()} case token.REQUIRE: return []Command{p.parseRequire()} case token.SHOW: return []Command{p.parseShow()} case token.WAIT: return []Command{p.parseWait()} case token.SOURCE: return p.parseSource() case token.SCREENSHOT: return []Command{p.parseScreenshot()} case token.COPY: return []Command{p.parseCopy()} case token.PASTE: return []Command{p.parsePaste()} case token.ENV: return []Command{p.parseEnv()} default: p.errors = append(p.errors, NewError(p.cur, "Invalid command: "+p.cur.Literal)) return []Command{{Type: token.ILLEGAL}} } } func (p *Parser) parseWait() Command { cmd := Command{Type: token.WAIT} if p.peek.Type == token.PLUS { p.nextToken() if p.peek.Type != token.STRING || (p.peek.Literal != "Line" && p.peek.Literal != "Screen") { p.errors = append(p.errors, NewError(p.peek, "Wait+ expects Line or Screen")) return cmd } cmd.Args = p.peek.Literal p.nextToken() } else { cmd.Args = "Line" } cmd.Options = p.parseSpeed() if cmd.Options != "" { dur, _ := time.ParseDuration(cmd.Options) if dur <= 0 { p.errors = append(p.errors, NewError(p.peek, "Wait expects positive duration")) return cmd } } if p.peek.Type != token.REGEX { // fallback to default return cmd } p.nextToken() if _, err := regexp.Compile(p.cur.Literal); err != nil { p.errors = append(p.errors, NewError(p.cur, fmt.Sprintf("Invalid regular expression '%s': %v", p.cur.Literal, err))) return cmd } cmd.Args += " " + p.cur.Literal return cmd } // parseSpeed parses a typing speed indication. // // i.e. @