1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819// Copyright 2015 Light Code Labs, LLC20//21// Licensed under the Apache License, Version 2.0 (the "License");22// you may not use this file except in compliance with the License.23// You may obtain a copy of the License at24//25// http://www.apache.org/licenses/LICENSE-2.026//27// Unless required by applicable law or agreed to in writing, software28// distributed under the License is distributed on an "AS IS" BASIS,29// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.30// See the License for the specific language governing permissions and31// limitations under the License.3233package lexer3435import (36 "errors"37 "fmt"38 "io"39 "strings"40)4142// Dispenser is a type that dispenses tokens, similarly to a lexer,43// except that it can do so with some notion of structure and has44// some really convenient methods.45type Dispenser struct {46 filename string47 tokens []Token48 cursor int49 nesting int50}5152// NewDispenser returns a Dispenser, ready to use for parsing the given input.53func NewDispenser(filename string, input io.Reader) Dispenser {54 tokens, _ := allTokens(input) // ignoring error because nothing to do with it55 return Dispenser{56 filename: filename,57 tokens: tokens,58 cursor: -1,59 }60}6162// NewDispenserTokens returns a Dispenser filled with the given tokens.63func NewDispenserTokens(filename string, tokens []Token) Dispenser {64 return Dispenser{65 filename: filename,66 tokens: tokens,67 cursor: -1,68 }69}7071// Next loads the next token. Returns true if a token72// was loaded; false otherwise. If false, all tokens73// have been consumed.74func (d *Dispenser) Next() bool {75 if d.cursor < len(d.tokens)-1 {76 d.cursor++77 return true78 }79 return false80}8182// NextArg loads the next token if it is on the same83// line. Returns true if a token was loaded; false84// otherwise. If false, all tokens on the line have85// been consumed. It handles imported tokens correctly.86func (d *Dispenser) NextArg() bool {87 if d.cursor < 0 {88 d.cursor++89 return true90 }91 if d.cursor >= len(d.tokens) {92 return false93 }94 if d.cursor < len(d.tokens)-1 &&95 d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&96 d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {97 d.cursor++98 return true99 }100 return false101}102103// NextLine loads the next token only if it is not on the same104// line as the current token, and returns true if a token was105// loaded; false otherwise. If false, there is not another token106// or it is on the same line. It handles imported tokens correctly.107func (d *Dispenser) NextLine() bool {108 if d.cursor < 0 {109 d.cursor++110 return true111 }112 if d.cursor >= len(d.tokens) {113 return false114 }115 if d.cursor < len(d.tokens)-1 &&116 (d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||117 d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {118 d.cursor++119 return true120 }121 return false122}123124// NextBlock can be used as the condition of a for loop125// to load the next token as long as it opens a block or126// is already in a block. It returns true if a token was127// loaded, or false when the block's closing curly brace128// was loaded and thus the block ended. Nested blocks are129// not supported.130func (d *Dispenser) NextBlock() bool {131 if d.nesting > 0 {132 d.Next()133 if d.Val() == "}" {134 d.nesting--135 return false136 }137 return true138 }139 if !d.NextArg() { // block must open on same line140 return false141 }142 if d.Val() != "{" {143 d.cursor-- // roll back if not opening brace144 return false145 }146 d.Next()147 if d.Val() == "}" {148 // Open and then closed right away149 return false150 }151 d.nesting++152 return true153}154155// Val gets the text of the current token. If there is no token156// loaded, it returns empty string.157func (d *Dispenser) Val() string {158 if d.cursor < 0 || d.cursor >= len(d.tokens) {159 return ""160 }161 return d.tokens[d.cursor].Text162}163164// Line gets the line number of the current token. If there is no token165// loaded, it returns 0.166func (d *Dispenser) Line() int {167 if d.cursor < 0 || d.cursor >= len(d.tokens) {168 return 0169 }170 return d.tokens[d.cursor].Line171}172173// File gets the filename of the current token. If there is no token loaded,174// it returns the filename originally given when parsing started.175func (d *Dispenser) File() string {176 if d.cursor < 0 || d.cursor >= len(d.tokens) {177 return d.filename178 }179 if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {180 return tokenFilename181 }182 return d.filename183}184185// Args is a convenience function that loads the next arguments186// (tokens on the same line) into an arbitrary number of strings187// pointed to in targets. If there are fewer tokens available188// than string pointers, the remaining strings will not be changed189// and false will be returned. If there were enough tokens available190// to fill the arguments, then true will be returned.191func (d *Dispenser) Args(targets ...*string) bool {192 enough := true193 for i := 0; i < len(targets); i++ {194 if !d.NextArg() {195 enough = false196 break197 }198 *targets[i] = d.Val()199 }200 return enough201}202203// RemainingArgs loads any more arguments (tokens on the same line)204// into a slice and returns them. Open curly brace tokens also indicate205// the end of arguments, and the curly brace is not included in206// the return value nor is it loaded.207func (d *Dispenser) RemainingArgs() []string {208 var args []string209210 for d.NextArg() {211 if d.Val() == "{" {212 d.cursor--213 break214 }215 args = append(args, d.Val())216 }217218 return args219}220221// ArgErr returns an argument error, meaning that another222// argument was expected but not found. In other words,223// a line break or open curly brace was encountered instead of224// an argument.225func (d *Dispenser) ArgErr() error {226 if d.Val() == "{" {227 return d.Err("Unexpected token '{', expecting argument")228 }229 return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())230}231232// SyntaxErr creates a generic syntax error which explains what was233// found and what was expected.234func (d *Dispenser) SyntaxErr(expected string) error {235 msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)236 return errors.New(msg)237}238239// EOFErr returns an error indicating that the dispenser reached240// the end of the input when searching for the next token.241func (d *Dispenser) EOFErr() error {242 return d.Errf("Unexpected EOF")243}244245// Err generates a custom parse-time error with a message of msg.246func (d *Dispenser) Err(msg string) error {247 msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)248 return errors.New(msg)249}250251// Errf is like Err, but for formatted error messages252func (d *Dispenser) Errf(format string, args ...interface{}) error {253 return d.Err(fmt.Sprintf(format, args...))254}255256// numLineBreaks counts how many line breaks are in the token257// value given by the token index tknIdx. It returns 0 if the258// token does not exist or there are no line breaks.259func (d *Dispenser) numLineBreaks(tknIdx int) int {260 if tknIdx < 0 || tknIdx >= len(d.tokens) {261 return 0262 }263 return strings.Count(d.tokens[tknIdx].Text, "\n")264}